Zipkin: add ability to upload trace json (#37483)

* Zipkin: upload json

* Change back mock json
This commit is contained in:
Zoltán Bedi 2021-08-05 10:55:15 +02:00 committed by GitHub
parent 0563bc68fc
commit 44c7e65fac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 186 additions and 44 deletions

View File

@ -32,7 +32,7 @@ This is a configuration for the [trace to logs feature]({{< relref "../explore/t
- **Data source -** Target data source.
- **Tags -** The tags that will be used in the Loki query. Default is `'cluster', 'hostname', 'namespace', 'pod'`.
- **Span start time shift -** Shift in the start time for the Loki query based on the span start time. In order to extend to the past, you need to use a negative value. Time units can be used here, for example, 5s, 1m, 3h. The default is 0.
- **Span start time shift -** Shift in the start time for the Loki query based on the span start time. In order to extend to the past, you need to use a negative value. Use time interval units like 5s, 1m, 3h. The default is 0.
- **Span end time shift -** Shift in the end time for the Loki query based on the span end time. Time units can be used here, for example, 5s, 1m, 3h. The default is 0.
![Trace to logs settings](/static/img/docs/explore/trace-to-logs-settings-8.png 'Screenshot of the trace to logs settings')
@ -57,6 +57,36 @@ Use the trace selector to pick particular trace from all traces logged in the ti
Zipkin annotations are shown in the trace view as logs with annotation value shown under annotation key.
## Upload JSON trace file
You can upload a JSON file that contains a single trace to visualize it.
{{< figure src="/static/img/docs/explore/zipkin-upload-json.png" class="docs-image--no-shadow" caption="Screenshot of the Zipkin data source in explore with upload selected" >}}
Here is an example JSON:
```json
[
{
"traceId": "efe9cb8857f68c8f",
"parentId": "efe9cb8857f68c8f",
"id": "8608dc6ce5cafe8e",
"kind": "SERVER",
"name": "get /api",
"timestamp": 1627975249601797,
"duration": 23457,
"localEndpoint": { "serviceName": "backend", "ipv4": "127.0.0.1", "port": 9000 },
"tags": {
"http.method": "GET",
"http.path": "/api",
"jaxrs.resource.class": "Resource",
"jaxrs.resource.method": "printDate"
},
"shared": true
}
]
```
## Linking Trace ID from logs
You can link to Zipkin trace from logs in Loki by configuring a derived field with internal link. See [Loki documentation]({{< relref "loki#derived-fields" >}}) for details.

View File

@ -1,9 +1,10 @@
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';
import { act, renderHook } from '@testing-library/react-hooks';
import { shallow } from 'enzyme';
import React from 'react';
import { ZipkinDatasource } from './datasource';
import { QueryField, useLoadOptions, useServices } from './QueryField';
import { ZipkinQuery } from './types';
describe('QueryField', () => {
it('renders properly', () => {

View File

@ -1,21 +1,31 @@
import { css } from '@emotion/css';
import { ExploreQueryFieldProps } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { ButtonCascader, CascaderOption } from '@grafana/ui';
import {
ButtonCascader,
CascaderOption,
FileDropzone,
InlineField,
InlineFieldRow,
RadioButtonGroup,
useTheme2,
} from '@grafana/ui';
import { notifyApp } from 'app/core/actions';
import { createErrorNotification } from 'app/core/copy/appNotification';
import { dispatch } from 'app/store/store';
import { fromPairs } from 'lodash';
import React, { useCallback, useMemo, useState } from 'react';
import { useAsyncFn, useMount, useMountedState } from 'react-use';
import { AsyncState } from 'react-use/lib/useAsyncFn';
import { apiPrefix } from './constants';
import { ZipkinDatasource, ZipkinQuery } from './datasource';
import { ZipkinSpan } from './types';
import { dispatch } from 'app/store/store';
import { notifyApp } from 'app/core/actions';
import { createErrorNotification } from 'app/core/copy/appNotification';
import { ZipkinDatasource } from './datasource';
import { ZipkinQuery, ZipkinQueryType, ZipkinSpan } from './types';
type Props = ExploreQueryFieldProps<ZipkinDatasource, ZipkinQuery>;
export const QueryField = ({ query, onChange, onRunQuery, datasource }: Props) => {
const serviceOptions = useServices(datasource);
const theme = useTheme2();
const { onLoadOptions, allOptions } = useLoadOptions(datasource);
const onSelectTrace = useCallback(
@ -33,29 +43,59 @@ export const QueryField = ({ query, onChange, onRunQuery, datasource }: Props) =
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>
<InlineFieldRow>
<InlineField label="Query type">
<RadioButtonGroup<ZipkinQueryType>
options={[
{ value: 'traceID', label: 'TraceID' },
{ value: 'upload', label: 'JSON file' },
]}
value={query.queryType || 'traceID'}
onChange={(v) =>
onChange({
...query,
queryType: v,
})
}
size="md"
/>
</InlineField>
</InlineFieldRow>
{query.queryType === 'upload' ? (
<div className={css({ padding: theme.spacing(2) })}>
<FileDropzone
options={{ multiple: false }}
onLoad={(result) => {
datasource.uploadedJson = result;
onRunQuery();
}}
/>
</div>
<div className="gf-form gf-form--grow flex-shrink-1">
<div className="slate-query-field__wrapper">
<div className="slate-query-field" aria-label={selectors.components.QueryField.container}>
<input
style={{ width: '100%' }}
value={query.query || ''}
onChange={(e) =>
onChange({
...query,
query: e.currentTarget.value,
})
}
/>
) : (
<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" aria-label={selectors.components.QueryField.container}>
<input
style={{ width: '100%' }}
value={query.query || ''}
onChange={(e) =>
onChange({
...query,
query: e.currentTarget.value,
})
}
/>
</div>
</div>
</div>
</div>
</div>
)}
</>
);
};

View File

@ -1,8 +1,9 @@
import { DataSourceInstanceSettings } from '@grafana/data';
import { DataSourceInstanceSettings, FieldType } from '@grafana/data';
import { backendSrv } from 'app/core/services/backend_srv';
import { of } from 'rxjs';
import { createFetchResponse } from 'test/helpers/createFetchResponse';
import { ZipkinDatasource } from './datasource';
import mockJson from './mockJsonResponse.json';
import { traceFrameFields, zipkinResponse } from './utils/testData';
jest.mock('@grafana/runtime', () => ({
@ -26,6 +27,20 @@ describe('ZipkinDatasource', () => {
expect(val[0].data[0].fields).toMatchObject(traceFrameFields);
});
});
it('should handle json file upload', async () => {
const ds = new ZipkinDatasource(defaultSettings);
ds.uploadedJson = JSON.stringify(mockJson);
const response = await ds
.query({
targets: [{ queryType: 'upload', refId: 'A' }],
} as any)
.toPromise();
const field = response.data[0].fields[0];
expect(field.name).toBe('traceID');
expect(field.type).toBe(FieldType.string);
expect(field.values.length).toBe(3);
});
});
describe('metadataRequest', () => {

View File

@ -1,5 +1,4 @@
import {
DataQuery,
DataQueryRequest,
DataQueryResponse,
DataSourceApi,
@ -12,28 +11,32 @@ import { Observable, of } from 'rxjs';
import { map } from 'rxjs/operators';
import { serializeParams } from '../../../core/utils/fetch';
import { apiPrefix } from './constants';
import { ZipkinSpan } from './types';
import { transformResponse } from './utils/transforms';
import { ZipkinQuery, ZipkinSpan } from './types';
import { createGraphFrames } from './utils/graphTransform';
export interface ZipkinQuery extends DataQuery {
query: string;
}
import { transformResponse } from './utils/transforms';
export class ZipkinDatasource extends DataSourceApi<ZipkinQuery> {
uploadedJson: string | ArrayBuffer | null = null;
constructor(private instanceSettings: DataSourceInstanceSettings) {
super(instanceSettings);
}
query(options: DataQueryRequest<ZipkinQuery>): Observable<DataQueryResponse> {
const traceId = options.targets[0]?.query;
if (traceId) {
return this.request<ZipkinSpan[]>(`${apiPrefix}/trace/${encodeURIComponent(traceId)}`).pipe(
const target = options.targets[0];
if (target.queryType === 'upload') {
if (!this.uploadedJson) {
return of({ data: [] });
}
const traceData = JSON.parse(this.uploadedJson as string);
return of(responseToDataQueryResponse({ data: traceData }));
}
if (target.query) {
return this.request<ZipkinSpan[]>(`${apiPrefix}/trace/${encodeURIComponent(target.query)}`).pipe(
map(responseToDataQueryResponse)
);
} else {
return of(emptyDataQueryResponse);
}
return of(emptyDataQueryResponse);
}
async metadataRequest(url: string, params?: Record<string, any>): Promise<any> {

View File

@ -0,0 +1,45 @@
[
{
"traceId": "efe9cb8857f68c8f",
"parentId": "efe9cb8857f68c8f",
"id": "8608dc6ce5cafe8e",
"kind": "SERVER",
"name": "get /api",
"timestamp": 1627975249601797,
"duration": 23457,
"localEndpoint": { "serviceName": "backend", "ipv4": "127.0.0.1", "port": 9000 },
"tags": {
"http.method": "GET",
"http.path": "/api",
"jaxrs.resource.class": "Resource",
"jaxrs.resource.method": "printDate"
},
"shared": true
},
{
"traceId": "efe9cb8857f68c8f",
"parentId": "efe9cb8857f68c8f",
"id": "8608dc6ce5cafe8e",
"kind": "CLIENT",
"name": "get",
"timestamp": 1627975249491667,
"duration": 148265,
"localEndpoint": { "serviceName": "frontend", "ipv4": "127.0.0.1", "port": 8081 },
"tags": { "http.method": "GET", "http.path": "/api" }
},
{
"traceId": "efe9cb8857f68c8f",
"id": "efe9cb8857f68c8f",
"kind": "SERVER",
"name": "get",
"timestamp": 1627975249258448,
"duration": 413469,
"localEndpoint": { "serviceName": "frontend", "ipv4": "127.0.0.1", "port": 8081 },
"tags": {
"http.method": "GET",
"http.path": "/",
"jaxrs.resource.class": "Resource",
"jaxrs.resource.method": "callBackend"
}
}
]

View File

@ -1,3 +1,5 @@
import { DataQuery } from '@grafana/data';
export type ZipkinSpan = {
traceId: string;
parentId?: string;
@ -23,3 +25,9 @@ export type ZipkinAnnotation = {
timestamp: number;
value: string;
};
export type ZipkinQueryType = 'traceID' | 'upload';
export interface ZipkinQuery extends DataQuery {
query: string;
queryType?: ZipkinQueryType;
}