mirror of https://github.com/grafana/grafana
Tracing: Zipkin datasource (#23829)
parent
800228c100
commit
58b566a252
@ -0,0 +1,6 @@ |
|||||||
|
# There is no data generator for this so easiest way to get some data here is run this example app |
||||||
|
# https://github.com/openzipkin/zipkin-js-example/tree/master/web |
||||||
|
zipkin: |
||||||
|
image: openzipkin/zipkin:latest |
||||||
|
ports: |
||||||
|
- "9411:9411" |
||||||
@ -0,0 +1,82 @@ |
|||||||
|
import React from 'react'; |
||||||
|
import { QueryField, useLoadOptions, useServices } from './QueryField'; |
||||||
|
import { ZipkinDatasource, ZipkinQuery } from './datasource'; |
||||||
|
import { shallow } from 'enzyme'; |
||||||
|
import { ButtonCascader, CascaderOption } from '@grafana/ui'; |
||||||
|
import { renderHook, act } from '@testing-library/react-hooks'; |
||||||
|
|
||||||
|
describe('QueryField', () => { |
||||||
|
it('renders properly', () => { |
||||||
|
const ds = {} as ZipkinDatasource; |
||||||
|
const wrapper = shallow( |
||||||
|
<QueryField |
||||||
|
history={[]} |
||||||
|
datasource={ds} |
||||||
|
query={{ query: '1234' } as ZipkinQuery} |
||||||
|
onRunQuery={() => {}} |
||||||
|
onChange={() => {}} |
||||||
|
/> |
||||||
|
); |
||||||
|
|
||||||
|
expect(wrapper.find(ButtonCascader).length).toBe(1); |
||||||
|
expect(wrapper.find('input').length).toBe(1); |
||||||
|
expect(wrapper.find('input').props().value).toBe('1234'); |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
describe('useServices', () => { |
||||||
|
it('returns services from datasource', async () => { |
||||||
|
const ds = { |
||||||
|
async metadataRequest(url: string, params?: Record<string, any>): Promise<any> { |
||||||
|
if (url === '/api/v2/services') { |
||||||
|
return Promise.resolve(['service1', 'service2']); |
||||||
|
} |
||||||
|
}, |
||||||
|
} as ZipkinDatasource; |
||||||
|
|
||||||
|
const { result, waitForNextUpdate } = renderHook(() => useServices(ds)); |
||||||
|
await waitForNextUpdate(); |
||||||
|
expect(result.current.value).toEqual([ |
||||||
|
{ label: 'service1', value: 'service1', isLeaf: false }, |
||||||
|
{ label: 'service2', value: 'service2', isLeaf: false }, |
||||||
|
]); |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
describe('useLoadOptions', () => { |
||||||
|
it('loads spans and traces', async () => { |
||||||
|
const ds = { |
||||||
|
async metadataRequest(url: string, params?: Record<string, any>): Promise<any> { |
||||||
|
if (url === '/api/v2/spans' && params?.serviceName === 'service1') { |
||||||
|
return Promise.resolve(['span1', 'span2']); |
||||||
|
} |
||||||
|
|
||||||
|
console.log({ url }); |
||||||
|
if (url === '/api/v2/traces' && params?.serviceName === 'service1' && params?.spanName === 'span1') { |
||||||
|
return Promise.resolve([[{ name: 'trace1', duration: 10_000, traceId: 'traceId1' }]]); |
||||||
|
} |
||||||
|
}, |
||||||
|
} as ZipkinDatasource; |
||||||
|
|
||||||
|
const { result, waitForNextUpdate } = renderHook(() => useLoadOptions(ds)); |
||||||
|
expect(result.current.allOptions).toEqual({}); |
||||||
|
|
||||||
|
act(() => { |
||||||
|
result.current.onLoadOptions([{ value: 'service1' } as CascaderOption]); |
||||||
|
}); |
||||||
|
|
||||||
|
await waitForNextUpdate(); |
||||||
|
|
||||||
|
expect(result.current.allOptions).toEqual({ service1: { span1: undefined, span2: undefined } }); |
||||||
|
|
||||||
|
act(() => { |
||||||
|
result.current.onLoadOptions([{ value: 'service1' } as CascaderOption, { value: 'span1' } as CascaderOption]); |
||||||
|
}); |
||||||
|
|
||||||
|
await waitForNextUpdate(); |
||||||
|
|
||||||
|
expect(result.current.allOptions).toEqual({ |
||||||
|
service1: { span1: { 'trace1 [10 ms]': 'traceId1' }, span2: undefined }, |
||||||
|
}); |
||||||
|
}); |
||||||
|
}); |
||||||
@ -1,22 +1,235 @@ |
|||||||
import React from 'react'; |
import React, { useCallback, useMemo, useState } from 'react'; |
||||||
import { ZipkinDatasource, ZipkinQuery } from './datasource'; |
import { ZipkinDatasource, ZipkinQuery } from './datasource'; |
||||||
import { ExploreQueryFieldProps } from '@grafana/data'; |
import { AppEvents, ExploreQueryFieldProps } from '@grafana/data'; |
||||||
|
import { ButtonCascader, CascaderOption } from '@grafana/ui'; |
||||||
|
import { useAsyncFn, useMount, useMountedState } from 'react-use'; |
||||||
|
import { appEvents } from '../../../core/core'; |
||||||
|
import { apiPrefix } from './constants'; |
||||||
|
import { ZipkinSpan } from './types'; |
||||||
|
import { fromPairs } from 'lodash'; |
||||||
|
import { AsyncState } from 'react-use/lib/useAsyncFn'; |
||||||
|
|
||||||
type Props = ExploreQueryFieldProps<ZipkinDatasource, ZipkinQuery>; |
type Props = ExploreQueryFieldProps<ZipkinDatasource, ZipkinQuery>; |
||||||
|
|
||||||
export const QueryField = (props: Props) => ( |
export const QueryField = ({ query, onChange, onRunQuery, datasource }: Props) => { |
||||||
<div className={'slate-query-field__wrapper'}> |
const serviceOptions = useServices(datasource); |
||||||
<div className="slate-query-field"> |
const { onLoadOptions, allOptions } = useLoadOptions(datasource); |
||||||
<input |
|
||||||
style={{ width: '100%' }} |
const onSelectTrace = useCallback( |
||||||
value={props.query.query || ''} |
(values: string[], selectedOptions: CascaderOption[]) => { |
||||||
onChange={e => |
if (selectedOptions.length === 3) { |
||||||
props.onChange({ |
const traceID = selectedOptions[2].value; |
||||||
...props.query, |
onChange({ ...query, query: traceID }); |
||||||
query: e.currentTarget.value, |
onRunQuery(); |
||||||
}) |
} |
||||||
|
}, |
||||||
|
[onChange, onRunQuery, query] |
||||||
|
); |
||||||
|
|
||||||
|
let cascaderOptions = useMapToCascaderOptions(serviceOptions, allOptions); |
||||||
|
|
||||||
|
return ( |
||||||
|
<> |
||||||
|
<div className="gf-form-inline gf-form-inline--nowrap"> |
||||||
|
<div className="gf-form flex-shrink-0"> |
||||||
|
<ButtonCascader options={cascaderOptions} onChange={onSelectTrace} loadData={onLoadOptions}> |
||||||
|
Traces |
||||||
|
</ButtonCascader> |
||||||
|
</div> |
||||||
|
<div className="gf-form gf-form--grow flex-shrink-1"> |
||||||
|
<div className={'slate-query-field__wrapper'}> |
||||||
|
<div className="slate-query-field"> |
||||||
|
<input |
||||||
|
style={{ width: '100%' }} |
||||||
|
value={query.query || ''} |
||||||
|
onChange={e => |
||||||
|
onChange({ |
||||||
|
...query, |
||||||
|
query: e.currentTarget.value, |
||||||
|
}) |
||||||
|
} |
||||||
|
/> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
// Exported for tests
|
||||||
|
export function useServices(datasource: ZipkinDatasource): AsyncState<CascaderOption[]> { |
||||||
|
const url = `${apiPrefix}/services`; |
||||||
|
|
||||||
|
const [servicesOptions, fetch] = useAsyncFn(async (): Promise<CascaderOption[]> => { |
||||||
|
try { |
||||||
|
const services: string[] | null = await datasource.metadataRequest(url); |
||||||
|
if (services) { |
||||||
|
return services.sort().map(service => ({ |
||||||
|
label: service, |
||||||
|
value: service, |
||||||
|
isLeaf: false, |
||||||
|
})); |
||||||
|
} |
||||||
|
return []; |
||||||
|
} catch (error) { |
||||||
|
appEvents.emit(AppEvents.alertError, ['Failed to load services from Zipkin', error]); |
||||||
|
throw error; |
||||||
|
} |
||||||
|
}, [datasource]); |
||||||
|
|
||||||
|
useMount(() => { |
||||||
|
// We should probably call this periodically to get new services after mount.
|
||||||
|
fetch(); |
||||||
|
}); |
||||||
|
|
||||||
|
return servicesOptions; |
||||||
|
} |
||||||
|
|
||||||
|
type OptionsState = { |
||||||
|
[serviceName: string]: { |
||||||
|
[spanName: string]: { |
||||||
|
[traceId: string]: string; |
||||||
|
}; |
||||||
|
}; |
||||||
|
}; |
||||||
|
|
||||||
|
// Exported for tests
|
||||||
|
export function useLoadOptions(datasource: ZipkinDatasource) { |
||||||
|
const isMounted = useMountedState(); |
||||||
|
const [allOptions, setAllOptions] = useState({} as OptionsState); |
||||||
|
|
||||||
|
const [, fetchSpans] = useAsyncFn( |
||||||
|
async function findSpans(service: string): Promise<void> { |
||||||
|
const url = `${apiPrefix}/spans`; |
||||||
|
try { |
||||||
|
// The response of this should have been full ZipkinSpan objects based on API docs but is just list
|
||||||
|
// of span names.
|
||||||
|
// TODO: check if this is some issue of version used or something else
|
||||||
|
const response: string[] = await datasource.metadataRequest(url, { serviceName: service }); |
||||||
|
if (isMounted()) { |
||||||
|
setAllOptions(state => { |
||||||
|
const spanOptions = fromPairs(response.map((span: string) => [span, undefined])); |
||||||
|
return { |
||||||
|
...state, |
||||||
|
[service]: spanOptions as any, |
||||||
|
}; |
||||||
|
}); |
||||||
} |
} |
||||||
/> |
} catch (error) { |
||||||
</div> |
appEvents.emit(AppEvents.alertError, ['Failed to load spans from Zipkin', error]); |
||||||
</div> |
throw error; |
||||||
); |
} |
||||||
|
}, |
||||||
|
[datasource, allOptions] |
||||||
|
); |
||||||
|
|
||||||
|
const [, fetchTraces] = useAsyncFn( |
||||||
|
async function findTraces(serviceName: string, spanName: string): Promise<void> { |
||||||
|
const url = `${apiPrefix}/traces`; |
||||||
|
const search = { |
||||||
|
serviceName, |
||||||
|
spanName, |
||||||
|
// See other params and default here https://zipkin.io/zipkin-api/#/default/get_traces
|
||||||
|
}; |
||||||
|
try { |
||||||
|
// This should return just root traces as there isn't any nesting
|
||||||
|
const traces: ZipkinSpan[][] = await datasource.metadataRequest(url, search); |
||||||
|
if (isMounted()) { |
||||||
|
const newTraces = traces.length |
||||||
|
? fromPairs( |
||||||
|
traces.map(trace => { |
||||||
|
const rootSpan = trace.find(span => !span.parentId); |
||||||
|
|
||||||
|
return [`${rootSpan.name} [${Math.floor(rootSpan.duration / 1000)} ms]`, rootSpan.traceId]; |
||||||
|
}) |
||||||
|
) |
||||||
|
: noTracesOptions; |
||||||
|
|
||||||
|
setAllOptions(state => { |
||||||
|
const spans = state[serviceName]; |
||||||
|
return { |
||||||
|
...state, |
||||||
|
[serviceName]: { |
||||||
|
...spans, |
||||||
|
[spanName]: newTraces, |
||||||
|
}, |
||||||
|
}; |
||||||
|
}); |
||||||
|
} |
||||||
|
} catch (error) { |
||||||
|
appEvents.emit(AppEvents.alertError, ['Failed to load spans from Zipkin', error]); |
||||||
|
throw error; |
||||||
|
} |
||||||
|
}, |
||||||
|
[datasource] |
||||||
|
); |
||||||
|
|
||||||
|
const onLoadOptions = useCallback( |
||||||
|
(selectedOptions: CascaderOption[]) => { |
||||||
|
const service = selectedOptions[0].value; |
||||||
|
if (selectedOptions.length === 1) { |
||||||
|
fetchSpans(service); |
||||||
|
} else if (selectedOptions.length === 2) { |
||||||
|
const spanName = selectedOptions[1].value; |
||||||
|
fetchTraces(service, spanName); |
||||||
|
} |
||||||
|
}, |
||||||
|
[fetchSpans, fetchTraces] |
||||||
|
); |
||||||
|
|
||||||
|
return { |
||||||
|
onLoadOptions, |
||||||
|
allOptions, |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
function useMapToCascaderOptions(services: AsyncState<CascaderOption[]>, allOptions: OptionsState) { |
||||||
|
return useMemo(() => { |
||||||
|
let cascaderOptions: CascaderOption[]; |
||||||
|
if (services.value && services.value.length) { |
||||||
|
cascaderOptions = services.value.map(services => { |
||||||
|
return { |
||||||
|
...services, |
||||||
|
children: |
||||||
|
allOptions[services.value] && |
||||||
|
Object.keys(allOptions[services.value]).map(spanName => { |
||||||
|
return { |
||||||
|
label: spanName, |
||||||
|
value: spanName, |
||||||
|
isLeaf: false, |
||||||
|
children: |
||||||
|
allOptions[services.value][spanName] && |
||||||
|
Object.keys(allOptions[services.value][spanName]).map(traceName => { |
||||||
|
return { |
||||||
|
label: traceName, |
||||||
|
value: allOptions[services.value][spanName][traceName], |
||||||
|
}; |
||||||
|
}), |
||||||
|
}; |
||||||
|
}), |
||||||
|
}; |
||||||
|
}); |
||||||
|
} else if (services.value && !services.value.length) { |
||||||
|
cascaderOptions = noTracesFoundOptions; |
||||||
|
} |
||||||
|
|
||||||
|
return cascaderOptions; |
||||||
|
}, [services, allOptions]); |
||||||
|
} |
||||||
|
|
||||||
|
const NO_TRACES_KEY = '__NO_TRACES__'; |
||||||
|
const noTracesFoundOptions = [ |
||||||
|
{ |
||||||
|
label: 'No traces found', |
||||||
|
value: 'no_traces', |
||||||
|
isLeaf: true, |
||||||
|
|
||||||
|
// Cannot be disabled because then cascader shows 'loading' for some reason.
|
||||||
|
// disabled: true,
|
||||||
|
}, |
||||||
|
]; |
||||||
|
|
||||||
|
const noTracesOptions = { |
||||||
|
'[No traces in time range]': NO_TRACES_KEY, |
||||||
|
}; |
||||||
|
|||||||
@ -0,0 +1 @@ |
|||||||
|
export const apiPrefix = '/api/v2'; |
||||||
@ -0,0 +1,44 @@ |
|||||||
|
import { ZipkinDatasource, ZipkinQuery } from './datasource'; |
||||||
|
import { DataQueryRequest, DataSourceInstanceSettings } from '@grafana/data'; |
||||||
|
import { BackendSrv, BackendSrvRequest, setBackendSrv } from '@grafana/runtime'; |
||||||
|
import { jaegerTrace, zipkinResponse } from './utils/testData'; |
||||||
|
|
||||||
|
describe('ZipkinDatasource', () => { |
||||||
|
describe('query', () => { |
||||||
|
it('runs query', async () => { |
||||||
|
setupBackendSrv({ url: '/api/datasources/proxy/1/api/v2/trace/12345', response: zipkinResponse }); |
||||||
|
const ds = new ZipkinDatasource(defaultSettings); |
||||||
|
const response = await ds.query({ targets: [{ query: '12345' }] } as DataQueryRequest<ZipkinQuery>).toPromise(); |
||||||
|
expect(response.data[0].fields[0].values.get(0)).toEqual(jaegerTrace); |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
describe('metadataRequest', () => { |
||||||
|
it('runs query', async () => { |
||||||
|
setupBackendSrv({ url: '/api/datasources/proxy/1/api/v2/services', response: ['service 1', 'service 2'] }); |
||||||
|
const ds = new ZipkinDatasource(defaultSettings); |
||||||
|
const response = await ds.metadataRequest('/api/v2/services'); |
||||||
|
expect(response).toEqual(['service 1', 'service 2']); |
||||||
|
}); |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
function setupBackendSrv<T>({ url, response }: { url: string; response: T }): void { |
||||||
|
setBackendSrv({ |
||||||
|
datasourceRequest(options: BackendSrvRequest): Promise<any> { |
||||||
|
if (options.url === url) { |
||||||
|
return Promise.resolve({ data: response }); |
||||||
|
} |
||||||
|
throw new Error(`Unexpected url ${options.url}`); |
||||||
|
}, |
||||||
|
} as BackendSrv); |
||||||
|
} |
||||||
|
|
||||||
|
const defaultSettings: DataSourceInstanceSettings = { |
||||||
|
id: 1, |
||||||
|
uid: '1', |
||||||
|
type: 'tracing', |
||||||
|
name: 'zipkin', |
||||||
|
meta: {} as any, |
||||||
|
jsonData: {}, |
||||||
|
}; |
||||||
@ -0,0 +1,21 @@ |
|||||||
|
export type ZipkinSpan = { |
||||||
|
traceId: string; |
||||||
|
parentId?: string; |
||||||
|
name: string; |
||||||
|
id: string; |
||||||
|
timestamp: number; |
||||||
|
duration: number; |
||||||
|
localEndpoint: { |
||||||
|
serviceName: string; |
||||||
|
ipv4: string; |
||||||
|
port?: number; |
||||||
|
}; |
||||||
|
annotations?: ZipkinAnnotation[]; |
||||||
|
tags?: { [key: string]: string }; |
||||||
|
kind?: 'CLIENT' | 'SERVER' | 'PRODUCER' | 'CONSUMER'; |
||||||
|
}; |
||||||
|
|
||||||
|
export type ZipkinAnnotation = { |
||||||
|
timestamp: number; |
||||||
|
value: string; |
||||||
|
}; |
||||||
@ -0,0 +1,145 @@ |
|||||||
|
import { SpanData, TraceData } from '@jaegertracing/jaeger-ui-components'; |
||||||
|
import { ZipkinSpan } from '../types'; |
||||||
|
|
||||||
|
export const zipkinResponse: ZipkinSpan[] = [ |
||||||
|
{ |
||||||
|
traceId: 'trace_id', |
||||||
|
name: 'span 1', |
||||||
|
id: 'span 1 id', |
||||||
|
timestamp: 1, |
||||||
|
duration: 10, |
||||||
|
localEndpoint: { |
||||||
|
serviceName: 'service 1', |
||||||
|
ipv4: '1.0.0.1', |
||||||
|
port: 42, |
||||||
|
}, |
||||||
|
annotations: [ |
||||||
|
{ |
||||||
|
timestamp: 2, |
||||||
|
value: 'annotation text', |
||||||
|
}, |
||||||
|
{ |
||||||
|
timestamp: 6, |
||||||
|
value: 'annotation text 3', |
||||||
|
}, |
||||||
|
], |
||||||
|
tags: { |
||||||
|
tag1: 'val1', |
||||||
|
tag2: 'val2', |
||||||
|
}, |
||||||
|
kind: 'CLIENT', |
||||||
|
}, |
||||||
|
|
||||||
|
{ |
||||||
|
traceId: 'trace_id', |
||||||
|
parentId: 'span 1 id', |
||||||
|
name: 'span 2', |
||||||
|
id: 'span 2 id', |
||||||
|
timestamp: 4, |
||||||
|
duration: 5, |
||||||
|
localEndpoint: { |
||||||
|
serviceName: 'service 2', |
||||||
|
ipv4: '1.0.0.1', |
||||||
|
}, |
||||||
|
tags: { |
||||||
|
error: '404', |
||||||
|
}, |
||||||
|
}, |
||||||
|
]; |
||||||
|
|
||||||
|
export const jaegerTrace: TraceData & { spans: SpanData[] } = { |
||||||
|
processes: { |
||||||
|
'service 1': { |
||||||
|
serviceName: 'service 1', |
||||||
|
tags: [ |
||||||
|
{ |
||||||
|
key: 'ipv4', |
||||||
|
type: 'string', |
||||||
|
value: '1.0.0.1', |
||||||
|
}, |
||||||
|
{ |
||||||
|
key: 'port', |
||||||
|
type: 'number', |
||||||
|
value: 42, |
||||||
|
}, |
||||||
|
], |
||||||
|
}, |
||||||
|
'service 2': { |
||||||
|
serviceName: 'service 2', |
||||||
|
tags: [ |
||||||
|
{ |
||||||
|
key: 'ipv4', |
||||||
|
type: 'string', |
||||||
|
value: '1.0.0.1', |
||||||
|
}, |
||||||
|
], |
||||||
|
}, |
||||||
|
}, |
||||||
|
traceID: 'trace_id', |
||||||
|
warnings: null, |
||||||
|
spans: [ |
||||||
|
{ |
||||||
|
duration: 10, |
||||||
|
flags: 1, |
||||||
|
logs: [ |
||||||
|
{ |
||||||
|
timestamp: 2, |
||||||
|
fields: [{ key: 'annotation', type: 'string', value: 'annotation text' }], |
||||||
|
}, |
||||||
|
{ |
||||||
|
timestamp: 6, |
||||||
|
fields: [{ key: 'annotation', type: 'string', value: 'annotation text 3' }], |
||||||
|
}, |
||||||
|
], |
||||||
|
operationName: 'span 1', |
||||||
|
processID: 'service 1', |
||||||
|
startTime: 1, |
||||||
|
spanID: 'span 1 id', |
||||||
|
traceID: 'trace_id', |
||||||
|
warnings: null as any, |
||||||
|
tags: [ |
||||||
|
{ |
||||||
|
key: 'kind', |
||||||
|
type: 'string', |
||||||
|
value: 'CLIENT', |
||||||
|
}, |
||||||
|
{ |
||||||
|
key: 'tag1', |
||||||
|
type: 'string', |
||||||
|
value: 'val1', |
||||||
|
}, |
||||||
|
{ |
||||||
|
key: 'tag2', |
||||||
|
type: 'string', |
||||||
|
value: 'val2', |
||||||
|
}, |
||||||
|
], |
||||||
|
references: [], |
||||||
|
}, |
||||||
|
{ |
||||||
|
duration: 5, |
||||||
|
flags: 1, |
||||||
|
logs: [], |
||||||
|
operationName: 'span 2', |
||||||
|
processID: 'service 2', |
||||||
|
startTime: 4, |
||||||
|
spanID: 'span 2 id', |
||||||
|
traceID: 'trace_id', |
||||||
|
warnings: null as any, |
||||||
|
tags: [ |
||||||
|
{ |
||||||
|
key: 'error', |
||||||
|
type: 'bool', |
||||||
|
value: true, |
||||||
|
}, |
||||||
|
], |
||||||
|
references: [ |
||||||
|
{ |
||||||
|
refType: 'CHILD_OF', |
||||||
|
spanID: 'span 1 id', |
||||||
|
traceID: 'trace_id', |
||||||
|
}, |
||||||
|
], |
||||||
|
}, |
||||||
|
], |
||||||
|
}; |
||||||
@ -0,0 +1,8 @@ |
|||||||
|
import { transformResponse } from './transforms'; |
||||||
|
import { jaegerTrace, zipkinResponse } from './testData'; |
||||||
|
|
||||||
|
describe('transformResponse', () => { |
||||||
|
it('transforms response', () => { |
||||||
|
expect(transformResponse(zipkinResponse)).toEqual(jaegerTrace); |
||||||
|
}); |
||||||
|
}); |
||||||
@ -0,0 +1,98 @@ |
|||||||
|
import { identity } from 'lodash'; |
||||||
|
import { keyBy } from 'lodash'; |
||||||
|
import { ZipkinAnnotation, ZipkinSpan } from '../types'; |
||||||
|
import { KeyValuePair, Log, Process, SpanData, TraceData } from '@jaegertracing/jaeger-ui-components'; |
||||||
|
|
||||||
|
/** |
||||||
|
* Transforms response to format similar to Jaegers as we use Jaeger ui on the frontend. |
||||||
|
*/ |
||||||
|
export function transformResponse(zSpans: ZipkinSpan[]): TraceData & { spans: SpanData[] } { |
||||||
|
return { |
||||||
|
processes: gatherProcesses(zSpans), |
||||||
|
traceID: zSpans[0].traceId, |
||||||
|
spans: zSpans.map(transformSpan), |
||||||
|
warnings: null, |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
function transformSpan(span: ZipkinSpan): SpanData { |
||||||
|
const jaegerSpan: SpanData = { |
||||||
|
duration: span.duration, |
||||||
|
// TODO: not sure what this is
|
||||||
|
flags: 1, |
||||||
|
logs: span.annotations?.map(transformAnnotation) ?? [], |
||||||
|
operationName: span.name, |
||||||
|
processID: span.localEndpoint.serviceName, |
||||||
|
startTime: span.timestamp, |
||||||
|
spanID: span.id, |
||||||
|
traceID: span.traceId, |
||||||
|
warnings: null as any, |
||||||
|
tags: Object.keys(span.tags || {}).map(key => { |
||||||
|
// If tag is error we remap it to simple boolean so that the Jaeger ui will show an error icon.
|
||||||
|
return { |
||||||
|
key, |
||||||
|
type: key === 'error' ? 'bool' : 'string', |
||||||
|
value: key === 'error' ? true : span.tags![key], |
||||||
|
}; |
||||||
|
}), |
||||||
|
references: span.parentId |
||||||
|
? [ |
||||||
|
{ |
||||||
|
refType: 'CHILD_OF', |
||||||
|
spanID: span.parentId, |
||||||
|
traceID: span.traceId, |
||||||
|
}, |
||||||
|
] |
||||||
|
: [], |
||||||
|
}; |
||||||
|
if (span.kind) { |
||||||
|
jaegerSpan.tags = [ |
||||||
|
{ |
||||||
|
key: 'kind', |
||||||
|
type: 'string', |
||||||
|
value: span.kind, |
||||||
|
}, |
||||||
|
...jaegerSpan.tags, |
||||||
|
]; |
||||||
|
} |
||||||
|
|
||||||
|
return jaegerSpan; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Maps annotations as a Jaeger log as that seems to be the closest thing. |
||||||
|
* See https://zipkin.io/zipkin-api/#/default/get_trace__traceId_
|
||||||
|
*/ |
||||||
|
function transformAnnotation(annotation: ZipkinAnnotation): Log { |
||||||
|
return { |
||||||
|
timestamp: annotation.timestamp, |
||||||
|
fields: [ |
||||||
|
{ |
||||||
|
key: 'annotation', |
||||||
|
type: 'string', |
||||||
|
value: annotation.value, |
||||||
|
}, |
||||||
|
], |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
function gatherProcesses(zSpans: ZipkinSpan[]): Record<string, Process> { |
||||||
|
const processes = zSpans.map(span => ({ |
||||||
|
serviceName: span.localEndpoint.serviceName, |
||||||
|
tags: [ |
||||||
|
{ |
||||||
|
key: 'ipv4', |
||||||
|
type: 'string', |
||||||
|
value: span.localEndpoint.ipv4, |
||||||
|
}, |
||||||
|
span.localEndpoint.port |
||||||
|
? { |
||||||
|
key: 'port', |
||||||
|
type: 'number', |
||||||
|
value: span.localEndpoint.port, |
||||||
|
} |
||||||
|
: undefined, |
||||||
|
].filter(identity) as KeyValuePair[], |
||||||
|
})); |
||||||
|
return keyBy(processes, 'serviceName'); |
||||||
|
} |
||||||
Loading…
Reference in new issue