mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Tracing: Zipkin datasource (#23829)
This commit is contained in:
parent
800228c100
commit
58b566a252
6
devenv/docker/blocks/zipkin/docker-compose.yaml
Normal file
6
devenv/docker/blocks/zipkin/docker-compose.yaml
Normal file
@ -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"
|
@ -52,3 +52,5 @@ export const ButtonCascader: React.FC<ButtonCascaderProps> = props => {
|
||||
</RCCascader>
|
||||
);
|
||||
};
|
||||
|
||||
ButtonCascader.displayName = 'ButtonCascader';
|
||||
|
@ -15,6 +15,8 @@ const influxdbPlugin = async () =>
|
||||
const lokiPlugin = async () => await import(/* webpackChunkName: "lokiPlugin" */ 'app/plugins/datasource/loki/module');
|
||||
const jaegerPlugin = async () =>
|
||||
await import(/* webpackChunkName: "jaegerPlugin" */ 'app/plugins/datasource/jaeger/module');
|
||||
const zipkinPlugin = async () =>
|
||||
await import(/* webpackChunkName: "zipkinPlugin" */ 'app/plugins/datasource/zipkin/module');
|
||||
const mixedPlugin = async () =>
|
||||
await import(/* webpackChunkName: "mixedPlugin" */ 'app/plugins/datasource/mixed/module');
|
||||
const mysqlPlugin = async () =>
|
||||
@ -65,6 +67,7 @@ const builtInPlugins: any = {
|
||||
'app/plugins/datasource/influxdb/module': influxdbPlugin,
|
||||
'app/plugins/datasource/loki/module': lokiPlugin,
|
||||
'app/plugins/datasource/jaeger/module': jaegerPlugin,
|
||||
'app/plugins/datasource/zipkin/module': zipkinPlugin,
|
||||
'app/plugins/datasource/mixed/module': mixedPlugin,
|
||||
'app/plugins/datasource/mysql/module': mysqlPlugin,
|
||||
'app/plugins/datasource/postgres/module': postgresPlugin,
|
||||
|
@ -14,6 +14,7 @@ interface State {
|
||||
}
|
||||
|
||||
function getLabelFromTrace(trace: any): string {
|
||||
// TODO: seems like the spans are not ordered so this may not be actually a root span
|
||||
const firstSpan = trace.spans && trace.spans[0];
|
||||
if (firstSpan) {
|
||||
return `${firstSpan.operationName} [${firstSpan.duration} ms]`;
|
||||
@ -33,6 +34,7 @@ export class JaegerQueryField extends React.PureComponent<Props, State> {
|
||||
|
||||
componentDidMount() {
|
||||
this._isMounted = true;
|
||||
// We should probably call this periodically to get new services after mount.
|
||||
this.getServices();
|
||||
}
|
||||
|
||||
|
@ -7,7 +7,7 @@ export type Props = DataSourcePluginOptionsEditorProps;
|
||||
export const ConfigEditor: React.FC<Props> = ({ options, onOptionsChange }) => {
|
||||
return (
|
||||
<DataSourceHttpSettings
|
||||
defaultUrl={'http://localhost:3100'}
|
||||
defaultUrl={'http://localhost:9411'}
|
||||
dataSourceConfig={options}
|
||||
showAccessOptions={true}
|
||||
onChange={onOptionsChange}
|
||||
|
82
public/app/plugins/datasource/zipkin/QueryField.test.tsx
Normal file
82
public/app/plugins/datasource/zipkin/QueryField.test.tsx
Normal file
@ -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 { 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>;
|
||||
|
||||
export const QueryField = (props: Props) => (
|
||||
<div className={'slate-query-field__wrapper'}>
|
||||
<div className="slate-query-field">
|
||||
<input
|
||||
style={{ width: '100%' }}
|
||||
value={props.query.query || ''}
|
||||
onChange={e =>
|
||||
props.onChange({
|
||||
...props.query,
|
||||
query: e.currentTarget.value,
|
||||
})
|
||||
export const QueryField = ({ query, onChange, onRunQuery, datasource }: Props) => {
|
||||
const serviceOptions = useServices(datasource);
|
||||
const { onLoadOptions, allOptions } = useLoadOptions(datasource);
|
||||
|
||||
const onSelectTrace = useCallback(
|
||||
(values: string[], selectedOptions: CascaderOption[]) => {
|
||||
if (selectedOptions.length === 3) {
|
||||
const traceID = selectedOptions[2].value;
|
||||
onChange({ ...query, query: traceID });
|
||||
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,
|
||||
};
|
||||
});
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} catch (error) {
|
||||
appEvents.emit(AppEvents.alertError, ['Failed to load spans from Zipkin', error]);
|
||||
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,
|
||||
};
|
||||
|
1
public/app/plugins/datasource/zipkin/constants.ts
Normal file
1
public/app/plugins/datasource/zipkin/constants.ts
Normal file
@ -0,0 +1 @@
|
||||
export const apiPrefix = '/api/v2';
|
44
public/app/plugins/datasource/zipkin/datasource.test.ts
Normal file
44
public/app/plugins/datasource/zipkin/datasource.test.ts
Normal file
@ -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: {},
|
||||
};
|
@ -5,34 +5,88 @@ import {
|
||||
DataQueryRequest,
|
||||
DataQueryResponse,
|
||||
DataQuery,
|
||||
FieldType,
|
||||
} from '@grafana/data';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { from, Observable, of } from 'rxjs';
|
||||
import { DatasourceRequestOptions } from '../../../core/services/backend_srv';
|
||||
import { serializeParams } from '../../../core/utils/fetch';
|
||||
import { getBackendSrv } from '@grafana/runtime';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { apiPrefix } from './constants';
|
||||
import { ZipkinSpan } from './types';
|
||||
import { transformResponse } from './utils/transforms';
|
||||
|
||||
export type ZipkinQuery = {
|
||||
// At the moment this should be simply the trace ID to get
|
||||
query: string;
|
||||
} & DataQuery;
|
||||
|
||||
export class ZipkinDatasource extends DataSourceApi<ZipkinQuery> {
|
||||
constructor(instanceSettings: DataSourceInstanceSettings) {
|
||||
constructor(private instanceSettings: DataSourceInstanceSettings) {
|
||||
super(instanceSettings);
|
||||
}
|
||||
|
||||
query(options: DataQueryRequest<ZipkinQuery>): Observable<DataQueryResponse> {
|
||||
return of({
|
||||
data: [
|
||||
new MutableDataFrame({
|
||||
fields: [
|
||||
{
|
||||
name: 'url',
|
||||
values: [],
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
||||
const traceId = options.targets[0]?.query;
|
||||
if (traceId) {
|
||||
return this.request<ZipkinSpan[]>(`${apiPrefix}/trace/${traceId}`).pipe(map(responseToDataQueryResponse));
|
||||
} else {
|
||||
return of(emptyDataQueryResponse);
|
||||
}
|
||||
}
|
||||
|
||||
async metadataRequest(url: string, params?: Record<string, any>): Promise<any> {
|
||||
const res = await this.request(url, params, { silent: true }).toPromise();
|
||||
return res.data;
|
||||
}
|
||||
|
||||
async testDatasource(): Promise<any> {
|
||||
await this.metadataRequest(`${apiPrefix}/services`);
|
||||
return true;
|
||||
}
|
||||
|
||||
private request<T = any>(apiUrl: string, data?: any, options?: DatasourceRequestOptions): Observable<{ data: T }> {
|
||||
// Hack for proxying metadata requests
|
||||
const baseUrl = `/api/datasources/proxy/${this.instanceSettings.id}`;
|
||||
const params = data ? serializeParams(data) : '';
|
||||
const url = `${baseUrl}${apiUrl}${params.length ? `?${params}` : ''}`;
|
||||
const req = {
|
||||
...options,
|
||||
url,
|
||||
};
|
||||
|
||||
return from(getBackendSrv().datasourceRequest(req));
|
||||
}
|
||||
}
|
||||
|
||||
function responseToDataQueryResponse(response: { data: ZipkinSpan[] }): DataQueryResponse {
|
||||
return {
|
||||
data: [
|
||||
new MutableDataFrame({
|
||||
fields: [
|
||||
{
|
||||
name: 'trace',
|
||||
type: FieldType.trace,
|
||||
// There is probably better mapping than just putting everything in as a single value but that's how
|
||||
// we do it with jaeger and is the simplest right now.
|
||||
values: response?.data ? [transformResponse(response?.data)] : [],
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
const emptyDataQueryResponse = {
|
||||
data: [
|
||||
new MutableDataFrame({
|
||||
fields: [
|
||||
{
|
||||
name: 'trace',
|
||||
type: FieldType.trace,
|
||||
values: [],
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
21
public/app/plugins/datasource/zipkin/types.ts
Normal file
21
public/app/plugins/datasource/zipkin/types.ts
Normal file
@ -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;
|
||||
};
|
145
public/app/plugins/datasource/zipkin/utils/testData.ts
Normal file
145
public/app/plugins/datasource/zipkin/utils/testData.ts
Normal file
@ -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);
|
||||
});
|
||||
});
|
98
public/app/plugins/datasource/zipkin/utils/transforms.ts
Normal file
98
public/app/plugins/datasource/zipkin/utils/transforms.ts
Normal file
@ -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');
|
||||
}
|
@ -4,7 +4,7 @@ echo -e "Collecting code stats (typescript errors & more)"
|
||||
|
||||
|
||||
|
||||
ERROR_COUNT_LIMIT=791
|
||||
ERROR_COUNT_LIMIT=788
|
||||
DIRECTIVES_LIMIT=172
|
||||
CONTROLLERS_LIMIT=139
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user