From 44c7e65fac7738bf214c8709b9c597f5872aca99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Bedi?= Date: Thu, 5 Aug 2021 10:55:15 +0200 Subject: [PATCH] Zipkin: add ability to upload trace json (#37483) * Zipkin: upload json * Change back mock json --- docs/sources/datasources/zipkin.md | 32 ++++++- .../datasource/zipkin/QueryField.test.tsx | 11 +-- .../plugins/datasource/zipkin/QueryField.tsx | 90 +++++++++++++------ .../datasource/zipkin/datasource.test.ts | 17 +++- .../plugins/datasource/zipkin/datasource.ts | 27 +++--- .../datasource/zipkin/mockJsonResponse.json | 45 ++++++++++ public/app/plugins/datasource/zipkin/types.ts | 8 ++ 7 files changed, 186 insertions(+), 44 deletions(-) create mode 100644 public/app/plugins/datasource/zipkin/mockJsonResponse.json diff --git a/docs/sources/datasources/zipkin.md b/docs/sources/datasources/zipkin.md index 6edb7208000..70e8578f849 100644 --- a/docs/sources/datasources/zipkin.md +++ b/docs/sources/datasources/zipkin.md @@ -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. diff --git a/public/app/plugins/datasource/zipkin/QueryField.test.tsx b/public/app/plugins/datasource/zipkin/QueryField.test.tsx index 3d504ed480b..1e167d253c0 100644 --- a/public/app/plugins/datasource/zipkin/QueryField.test.tsx +++ b/public/app/plugins/datasource/zipkin/QueryField.test.tsx @@ -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', () => { diff --git a/public/app/plugins/datasource/zipkin/QueryField.tsx b/public/app/plugins/datasource/zipkin/QueryField.tsx index c4eaf371b72..9acf4d4ffcf 100644 --- a/public/app/plugins/datasource/zipkin/QueryField.tsx +++ b/public/app/plugins/datasource/zipkin/QueryField.tsx @@ -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; 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 ( <> -
-
- - Traces - + + + + options={[ + { value: 'traceID', label: 'TraceID' }, + { value: 'upload', label: 'JSON file' }, + ]} + value={query.queryType || 'traceID'} + onChange={(v) => + onChange({ + ...query, + queryType: v, + }) + } + size="md" + /> + + + {query.queryType === 'upload' ? ( +
+ { + datasource.uploadedJson = result; + onRunQuery(); + }} + />
-
-
-
- - onChange({ - ...query, - query: e.currentTarget.value, - }) - } - /> + ) : ( +
+
+ + Traces + +
+
+
+
+ + onChange({ + ...query, + query: e.currentTarget.value, + }) + } + /> +
-
+ )} ); }; diff --git a/public/app/plugins/datasource/zipkin/datasource.test.ts b/public/app/plugins/datasource/zipkin/datasource.test.ts index 593817e7cd1..c1cd6149df2 100644 --- a/public/app/plugins/datasource/zipkin/datasource.test.ts +++ b/public/app/plugins/datasource/zipkin/datasource.test.ts @@ -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', () => { diff --git a/public/app/plugins/datasource/zipkin/datasource.ts b/public/app/plugins/datasource/zipkin/datasource.ts index bd01b102f9a..1de80cf39a7 100644 --- a/public/app/plugins/datasource/zipkin/datasource.ts +++ b/public/app/plugins/datasource/zipkin/datasource.ts @@ -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 { + uploadedJson: string | ArrayBuffer | null = null; constructor(private instanceSettings: DataSourceInstanceSettings) { super(instanceSettings); } query(options: DataQueryRequest): Observable { - const traceId = options.targets[0]?.query; - if (traceId) { - return this.request(`${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(`${apiPrefix}/trace/${encodeURIComponent(target.query)}`).pipe( map(responseToDataQueryResponse) ); - } else { - return of(emptyDataQueryResponse); } + return of(emptyDataQueryResponse); } async metadataRequest(url: string, params?: Record): Promise { diff --git a/public/app/plugins/datasource/zipkin/mockJsonResponse.json b/public/app/plugins/datasource/zipkin/mockJsonResponse.json new file mode 100644 index 00000000000..e02dc101315 --- /dev/null +++ b/public/app/plugins/datasource/zipkin/mockJsonResponse.json @@ -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" + } + } +] diff --git a/public/app/plugins/datasource/zipkin/types.ts b/public/app/plugins/datasource/zipkin/types.ts index 98d8c1e5483..4bf6be40d51 100644 --- a/public/app/plugins/datasource/zipkin/types.ts +++ b/public/app/plugins/datasource/zipkin/types.ts @@ -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; +}