diff --git a/docs/sources/datasources/tempo.md b/docs/sources/datasources/tempo.md index fcbad90fefe..fcb3bb45729 100644 --- a/docs/sources/datasources/tempo.md +++ b/docs/sources/datasources/tempo.md @@ -31,7 +31,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 -** A shift in the start time for the Loki query based on the start time for the span. To extend the time to the past, use a negative value. You can use time units, for example, 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') @@ -47,6 +47,56 @@ To query a particular trace, select the **TraceID** query type, and then put the {{< figure src="/static/img/docs/tempo/query-editor-traceid.png" class="docs-image--no-shadow" caption="Screenshot of the Tempo TraceID query type" >}} +## Upload JSON trace file + +You can upload a JSON file that contains a single trace to visualize it. If the file has multiple traces then the first trace is used for visualization. + +{{< figure src="/static/img/docs/explore/tempo-upload-json.png" class="docs-image--no-shadow" caption="Screenshot of the Tempo data source in explore with upload selected" >}} + +Here is an example JSON: + +```json +{ + "batches": [ + { + "resource": { + "attributes": [ + { "key": "service.name", "value": { "stringValue": "db" } }, + { "key": "job", "value": { "stringValue": "tns/db" } }, + { "key": "opencensus.exporterversion", "value": { "stringValue": "Jaeger-Go-2.22.1" } }, + { "key": "host.name", "value": { "stringValue": "63d16772b4a2" } }, + { "key": "ip", "value": { "stringValue": "0.0.0.0" } }, + { "key": "client-uuid", "value": { "stringValue": "39fb01637a579639" } } + ] + }, + "instrumentationLibrarySpans": [ + { + "instrumentationLibrary": {}, + "spans": [ + { + "traceId": "AAAAAAAAAABguiq7RPE+rg==", + "spanId": "cmteMBAvwNA=", + "parentSpanId": "OY8PIaPbma4=", + "name": "HTTP GET - root", + "kind": "SPAN_KIND_SERVER", + "startTimeUnixNano": "1627471657255809000", + "endTimeUnixNano": "1627471657256268000", + "attributes": [ + { "key": "http.status_code", "value": { "intValue": "200" } }, + { "key": "http.method", "value": { "stringValue": "GET" } }, + { "key": "http.url", "value": { "stringValue": "/" } }, + { "key": "component", "value": { "stringValue": "net/http" } } + ], + "status": {} + } + ] + } + ] + } + ] +} +``` + ## Linking Trace ID from logs You can link to Tempo trace from logs in Loki or Elastic by configuring an internal link. See the [Derived fields]({{< relref "loki.md#derived-fields" >}}) section in the [Loki data source]({{< relref "loki.md" >}}) or [Data links]({{< relref "elasticsearch.md#data-links" >}}) section in the [Elastic data source]({{< relref "elasticsearch.md" >}}) for configuration instructions. diff --git a/package.json b/package.json index 832829612d9..f32cba356db 100644 --- a/package.json +++ b/package.json @@ -265,6 +265,9 @@ "mousetrap": "1.6.5", "mousetrap-global-bind": "1.1.0", "ol": "^6.5.0", + "@opentelemetry/api": "1.0.2", + "@opentelemetry/exporter-collector": "0.23.0", + "@opentelemetry/semantic-conventions": "0.23.0", "papaparse": "5.3.0", "pluralize": "^8.0.0", "prismjs": "1.23.0", diff --git a/pkg/tsdb/tempo/trace_transform.go b/pkg/tsdb/tempo/trace_transform.go index ee4ba15b6f7..6d6e30fb8b1 100644 --- a/pkg/tsdb/tempo/trace_transform.go +++ b/pkg/tsdb/tempo/trace_transform.go @@ -102,6 +102,7 @@ func resourceSpansToRows(rs pdata.ResourceSpans) ([][]interface{}, error) { } func spanToSpanRow(span pdata.Span, libraryTags pdata.InstrumentationLibrary, resource pdata.Resource) ([]interface{}, error) { + // If the id representation changed from hexstring to something else we need to change the transformBase64IDToHexString in the frontend code traceID := span.TraceID().HexString() traceID = strings.TrimLeft(traceID, "0") diff --git a/public/app/plugins/datasource/tempo/QueryField.tsx b/public/app/plugins/datasource/tempo/QueryField.tsx index aff4db7eae6..55f5c54ac05 100644 --- a/public/app/plugins/datasource/tempo/QueryField.tsx +++ b/public/app/plugins/datasource/tempo/QueryField.tsx @@ -1,18 +1,29 @@ +import { css } from '@emotion/css'; import { DataQuery, DataSourceApi, ExploreQueryFieldProps } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { getDataSourceSrv } from '@grafana/runtime'; -import { InlineField, InlineFieldRow, InlineLabel, LegacyForms, RadioButtonGroup } from '@grafana/ui'; +import { + FileDropzone, + InlineField, + InlineFieldRow, + InlineLabel, + LegacyForms, + RadioButtonGroup, + Themeable2, + withTheme2, +} from '@grafana/ui'; import { TraceToLogsOptions } from 'app/core/components/TraceToLogsSettings'; import React from 'react'; import { LokiQueryField } from '../loki/components/LokiQueryField'; import { TempoDatasource, TempoQuery, TempoQueryType } from './datasource'; -type Props = ExploreQueryFieldProps; +interface Props extends ExploreQueryFieldProps, Themeable2 {} + const DEFAULT_QUERY_TYPE: TempoQueryType = 'traceId'; interface State { linkedDatasource?: DataSourceApi; } -export class TempoQueryField extends React.PureComponent { +class TempoQueryFieldComponent extends React.PureComponent { state = { linkedDatasource: undefined, }; @@ -59,6 +70,7 @@ export class TempoQueryField extends React.PureComponent { options={[ { value: 'search', label: 'Search' }, { value: 'traceId', label: 'TraceID' }, + { value: 'upload', label: 'JSON file' }, ]} value={query.queryType || DEFAULT_QUERY_TYPE} onChange={(v) => @@ -89,7 +101,18 @@ export class TempoQueryField extends React.PureComponent { {query.queryType === 'search' && !linkedDatasource && (
Please set up a Traces-to-logs datasource in the datasource settings.
)} - {query.queryType !== 'search' && ( + {query.queryType === 'upload' && ( +
+ { + this.props.datasource.uploadedJson = result; + this.props.onRunQuery(); + }} + /> +
+ )} + {(!query.queryType || query.queryType === 'traceId') && ( { ); } } + +export const TempoQueryField = withTheme2(TempoQueryFieldComponent); diff --git a/public/app/plugins/datasource/tempo/datasource.test.ts b/public/app/plugins/datasource/tempo/datasource.test.ts index 66b189924f1..8ceb5eb99c4 100644 --- a/public/app/plugins/datasource/tempo/datasource.test.ts +++ b/public/app/plugins/datasource/tempo/datasource.test.ts @@ -1,8 +1,16 @@ -import { DataFrame, dataFrameToJSON, DataSourceInstanceSettings, MutableDataFrame, PluginType } from '@grafana/data'; +import { + DataFrame, + dataFrameToJSON, + DataSourceInstanceSettings, + FieldType, + MutableDataFrame, + PluginType, +} from '@grafana/data'; +import { BackendDataSourceResponse, FetchResponse, setBackendSrv } from '@grafana/runtime'; import { Observable, of } from 'rxjs'; import { createFetchResponse } from 'test/helpers/createFetchResponse'; import { TempoDatasource } from './datasource'; -import { FetchResponse, setBackendSrv, BackendDataSourceResponse } from '@grafana/runtime'; +import mockJson from './mockJsonResponse.json'; describe('Tempo data source', () => { it('parses json fields from backend', async () => { @@ -68,6 +76,21 @@ describe('Tempo data source', () => { { name: 'source', values: [] }, ]); }); + + it('should handle json file upload', async () => { + const ds = new TempoDatasource(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.get(0)).toBe('60ba2abb44f13eae'); + expect(field.values.length).toBe(6); + }); }); function setupBackendSrv(frame: DataFrame) { diff --git a/public/app/plugins/datasource/tempo/datasource.ts b/public/app/plugins/datasource/tempo/datasource.ts index 4f38d7d616f..c03c6b3e1df 100644 --- a/public/app/plugins/datasource/tempo/datasource.ts +++ b/public/app/plugins/datasource/tempo/datasource.ts @@ -4,16 +4,17 @@ import { DataQueryResponse, DataSourceApi, DataSourceInstanceSettings, + LoadingState, } from '@grafana/data'; import { DataSourceWithBackend } from '@grafana/runtime'; import { TraceToLogsData, TraceToLogsOptions } from 'app/core/components/TraceToLogsSettings'; import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; -import { from, merge, Observable, throwError } from 'rxjs'; +import { from, merge, Observable, of, throwError } from 'rxjs'; import { map, mergeMap } from 'rxjs/operators'; import { LokiOptions } from '../loki/types'; -import { transformTrace, transformTraceList } from './resultTransformer'; +import { transformFromOTLP as transformFromOTEL, transformTrace, transformTraceList } from './resultTransformer'; -export type TempoQueryType = 'search' | 'traceId'; +export type TempoQueryType = 'search' | 'traceId' | 'upload'; export type TempoQuery = { query: string; @@ -24,6 +25,7 @@ export type TempoQuery = { export class TempoDatasource extends DataSourceWithBackend { tracesToLogs?: TraceToLogsOptions; + uploadedJson?: string | ArrayBuffer | null = null; constructor(instanceSettings: DataSourceInstanceSettings) { super(instanceSettings); @@ -34,6 +36,7 @@ export class TempoDatasource extends DataSourceWithBackend> = []; const filteredTargets = options.targets.filter((target) => !target.hide); const searchTargets = filteredTargets.filter((target) => target.queryType === 'search'); + const uploadTargets = filteredTargets.filter((target) => target.queryType === 'upload'); const traceTargets = filteredTargets.filter( (target) => target.queryType === 'traceId' || target.queryType === undefined ); @@ -68,6 +71,19 @@ export class TempoDatasource extends DataSourceWithBackend 0) { const traceRequest: DataQueryRequest = { ...options, targets: traceTargets }; subQueries.push( diff --git a/public/app/plugins/datasource/tempo/mockJsonResponse.json b/public/app/plugins/datasource/tempo/mockJsonResponse.json new file mode 100644 index 00000000000..0697ad16fee --- /dev/null +++ b/public/app/plugins/datasource/tempo/mockJsonResponse.json @@ -0,0 +1,319 @@ +{ + "batches": [ + { + "resource": { + "attributes": [ + { "key": "service.name", "value": { "stringValue": "db" } }, + { "key": "job", "value": { "stringValue": "tns/db" } }, + { "key": "opencensus.exporterversion", "value": { "stringValue": "Jaeger-Go-2.22.1" } }, + { "key": "host.name", "value": { "stringValue": "63d16772b4a2" } }, + { "key": "ip", "value": { "stringValue": "172.24.0.2" } }, + { "key": "client-uuid", "value": { "stringValue": "39fb01637a579639" } } + ] + }, + "instrumentationLibrarySpans": [ + { + "instrumentationLibrary": {}, + "spans": [ + { + "traceId": "AAAAAAAAAABguiq7RPE+rg==", + "spanId": "cmteMBAvwNA=", + "parentSpanId": "OY8PIaPbma4=", + "name": "HTTP GET - root", + "kind": "SPAN_KIND_SERVER", + "startTimeUnixNano": "1627471657255809000", + "endTimeUnixNano": "1627471657256268000", + "attributes": [ + { "key": "http.status_code", "value": { "intValue": "200" } }, + { "key": "http.method", "value": { "stringValue": "GET" } }, + { "key": "http.url", "value": { "stringValue": "/" } }, + { "key": "component", "value": { "stringValue": "net/http" } } + ], + "status": {} + } + ] + } + ] + }, + { + "resource": { + "attributes": [ + { "key": "service.name", "value": { "stringValue": "app" } }, + { "key": "job", "value": { "stringValue": "tns/app" } }, + { "key": "opencensus.exporterversion", "value": { "stringValue": "Jaeger-Go-2.22.1" } }, + { "key": "host.name", "value": { "stringValue": "f68212e86151" } }, + { "key": "ip", "value": { "stringValue": "172.24.0.7" } }, + { "key": "client-uuid", "value": { "stringValue": "8b6e8600b53a24" } } + ] + }, + "instrumentationLibrarySpans": [ + { + "instrumentationLibrary": {}, + "spans": [ + { + "traceId": "AAAAAAAAAABguiq7RPE+rg==", + "spanId": "GVdnHErYWoo=", + "parentSpanId": "WgdmnUQaoyY=", + "name": "HTTP Client", + "startTimeUnixNano": "1627471657251425000", + "endTimeUnixNano": "1627471657258789000", + "status": {} + } + ] + } + ] + }, + { + "resource": { + "attributes": [ + { "key": "service.name", "value": { "stringValue": "lb" } }, + { "key": "job", "value": { "stringValue": "tns/loadgen" } }, + { "key": "opencensus.exporterversion", "value": { "stringValue": "Jaeger-Go-2.22.1" } }, + { "key": "host.name", "value": { "stringValue": "6475ba6d3c8b" } }, + { "key": "ip", "value": { "stringValue": "172.24.0.8" } }, + { "key": "client-uuid", "value": { "stringValue": "535eb863d7ade742" } } + ] + }, + "instrumentationLibrarySpans": [ + { + "instrumentationLibrary": {}, + "spans": [ + { + "traceId": "AAAAAAAAAABguiq7RPE+rg==", + "spanId": "YLoqu0TxPq4=", + "name": "HTTP Client", + "startTimeUnixNano": "1627471657247632000", + "endTimeUnixNano": "1627471657260233000", + "attributes": [ + { "key": "sampler.type", "value": { "stringValue": "const" } }, + { "key": "sampler.param", "value": { "boolValue": true } } + ], + "status": {} + } + ] + } + ] + }, + { + "resource": { + "attributes": [ + { "key": "service.name", "value": { "stringValue": "lb" } }, + { "key": "job", "value": { "stringValue": "tns/loadgen" } }, + { "key": "opencensus.exporterversion", "value": { "stringValue": "Jaeger-Go-2.22.1" } }, + { "key": "host.name", "value": { "stringValue": "6475ba6d3c8b" } }, + { "key": "ip", "value": { "stringValue": "172.24.0.8" } }, + { "key": "client-uuid", "value": { "stringValue": "535eb863d7ade742" } } + ] + }, + "instrumentationLibrarySpans": [ + { + "instrumentationLibrary": {}, + "spans": [ + { + "traceId": "AAAAAAAAAABguiq7RPE+rg==", + "spanId": "VzhhbsaOedI=", + "parentSpanId": "YLoqu0TxPq4=", + "name": "HTTP GET", + "kind": "SPAN_KIND_CLIENT", + "startTimeUnixNano": "1627471657247674000", + "endTimeUnixNano": "1627471657261178000", + "attributes": [ + { "key": "http.status_code", "value": { "intValue": "200" } }, + { "key": "component", "value": { "stringValue": "net/http" } }, + { "key": "http.method", "value": { "stringValue": "GET" } }, + { "key": "http.url", "value": { "stringValue": "app:80" } }, + { "key": "net/http.reused", "value": { "boolValue": false } }, + { "key": "net/http.was_idle", "value": { "boolValue": false } } + ], + "events": [ + { + "timeUnixNano": "1627471657247711000", + "attributes": [{ "key": "event", "value": { "stringValue": "GetConn" } }] + }, + { + "timeUnixNano": "1627471657247822000", + "attributes": [ + { "key": "event", "value": { "stringValue": "DNSStart" } }, + { "key": "host", "value": { "stringValue": "app" } } + ] + }, + { + "timeUnixNano": "1627471657249309000", + "attributes": [ + { "key": "event", "value": { "stringValue": "DNSDone" } }, + { "key": "addr", "value": { "stringValue": "172.24.0.7" } } + ] + }, + { + "timeUnixNano": "1627471657249395000", + "attributes": [ + { "key": "event", "value": { "stringValue": "ConnectStart" } }, + { "key": "network", "value": { "stringValue": "tcp" } }, + { "key": "addr", "value": { "stringValue": "172.24.0.7:80" } } + ] + }, + { + "timeUnixNano": "1627471657250250000", + "attributes": [ + { "key": "event", "value": { "stringValue": "ConnectDone" } }, + { "key": "network", "value": { "stringValue": "tcp" } }, + { "key": "addr", "value": { "stringValue": "172.24.0.7:80" } } + ] + }, + { + "timeUnixNano": "1627471657250313000", + "attributes": [{ "key": "event", "value": { "stringValue": "GotConn" } }] + }, + { + "timeUnixNano": "1627471657250446000", + "attributes": [{ "key": "event", "value": { "stringValue": "WroteHeaders" } }] + }, + { + "timeUnixNano": "1627471657250465000", + "attributes": [{ "key": "event", "value": { "stringValue": "WroteRequest" } }] + }, + { + "timeUnixNano": "1627471657260087000", + "attributes": [{ "key": "event", "value": { "stringValue": "GotFirstResponseByte" } }] + }, + { + "timeUnixNano": "1627471657260804000", + "attributes": [{ "key": "event", "value": { "stringValue": "ClosedBody" } }] + } + ], + "status": {} + } + ] + } + ] + }, + { + "resource": { + "attributes": [ + { "key": "service.name", "value": { "stringValue": "app" } }, + { "key": "job", "value": { "stringValue": "tns/app" } }, + { "key": "opencensus.exporterversion", "value": { "stringValue": "Jaeger-Go-2.22.1" } }, + { "key": "host.name", "value": { "stringValue": "f68212e86151" } }, + { "key": "ip", "value": { "stringValue": "172.24.0.7" } }, + { "key": "client-uuid", "value": { "stringValue": "8b6e8600b53a24" } } + ] + }, + "instrumentationLibrarySpans": [ + { + "instrumentationLibrary": {}, + "spans": [ + { + "traceId": "AAAAAAAAAABguiq7RPE+rg==", + "spanId": "OY8PIaPbma4=", + "parentSpanId": "GVdnHErYWoo=", + "name": "HTTP GET", + "kind": "SPAN_KIND_CLIENT", + "startTimeUnixNano": "1627471657251445000", + "endTimeUnixNano": "1627471657260828000", + "attributes": [ + { "key": "http.status_code", "value": { "intValue": "200" } }, + { "key": "component", "value": { "stringValue": "net/http" } }, + { "key": "http.method", "value": { "stringValue": "GET" } }, + { "key": "http.url", "value": { "stringValue": "db:80" } }, + { "key": "net/http.reused", "value": { "boolValue": false } }, + { "key": "net/http.was_idle", "value": { "boolValue": false } } + ], + "events": [ + { + "timeUnixNano": "1627471657251575000", + "attributes": [{ "key": "event", "value": { "stringValue": "GetConn" } }] + }, + { + "timeUnixNano": "1627471657251700000", + "attributes": [ + { "key": "event", "value": { "stringValue": "DNSStart" } }, + { "key": "host", "value": { "stringValue": "db" } } + ] + }, + { + "timeUnixNano": "1627471657254144000", + "attributes": [ + { "key": "event", "value": { "stringValue": "DNSDone" } }, + { "key": "addr", "value": { "stringValue": "172.24.0.2" } } + ] + }, + { + "timeUnixNano": "1627471657254295000", + "attributes": [ + { "key": "event", "value": { "stringValue": "ConnectStart" } }, + { "key": "network", "value": { "stringValue": "tcp" } }, + { "key": "addr", "value": { "stringValue": "172.24.0.2:80" } } + ] + }, + { + "timeUnixNano": "1627471657255054000", + "attributes": [ + { "key": "event", "value": { "stringValue": "ConnectDone" } }, + { "key": "network", "value": { "stringValue": "tcp" } }, + { "key": "addr", "value": { "stringValue": "172.24.0.2:80" } } + ] + }, + { + "timeUnixNano": "1627471657255199000", + "attributes": [{ "key": "event", "value": { "stringValue": "GotConn" } }] + }, + { + "timeUnixNano": "1627471657255257000", + "attributes": [{ "key": "event", "value": { "stringValue": "WroteHeaders" } }] + }, + { + "timeUnixNano": "1627471657255274000", + "attributes": [{ "key": "event", "value": { "stringValue": "WroteRequest" } }] + }, + { + "timeUnixNano": "1627471657258308000", + "attributes": [{ "key": "event", "value": { "stringValue": "GotFirstResponseByte" } }] + }, + { + "timeUnixNano": "1627471657260811000", + "attributes": [{ "key": "event", "value": { "stringValue": "ClosedBody" } }] + } + ], + "status": {} + } + ] + } + ] + }, + { + "resource": { + "attributes": [ + { "key": "service.name", "value": { "stringValue": "app" } }, + { "key": "job", "value": { "stringValue": "tns/app" } }, + { "key": "opencensus.exporterversion", "value": { "stringValue": "Jaeger-Go-2.22.1" } }, + { "key": "host.name", "value": { "stringValue": "f68212e86151" } }, + { "key": "ip", "value": { "stringValue": "172.24.0.7" } }, + { "key": "client-uuid", "value": { "stringValue": "8b6e8600b53a24" } } + ] + }, + "instrumentationLibrarySpans": [ + { + "instrumentationLibrary": {}, + "spans": [ + { + "traceId": "AAAAAAAAAABguiq7RPE+rg==", + "spanId": "WgdmnUQaoyY=", + "parentSpanId": "VzhhbsaOedI=", + "name": "HTTP GET - root", + "kind": "SPAN_KIND_SERVER", + "startTimeUnixNano": "1627471657251228000", + "endTimeUnixNano": "1627471657260930000", + "attributes": [ + { "key": "http.status_code", "value": { "intValue": "200" } }, + { "key": "http.method", "value": { "stringValue": "GET" } }, + { "key": "http.url", "value": { "stringValue": "/" } }, + { "key": "component", "value": { "stringValue": "net/http" } } + ], + "status": {} + } + ] + } + ] + } + ] +} diff --git a/public/app/plugins/datasource/tempo/resultTransformer.ts b/public/app/plugins/datasource/tempo/resultTransformer.ts index ea3d88ab608..274cbd61876 100644 --- a/public/app/plugins/datasource/tempo/resultTransformer.ts +++ b/public/app/plugins/datasource/tempo/resultTransformer.ts @@ -1,4 +1,17 @@ -import { DataQueryResponse, ArrayVector, DataFrame, Field, FieldType, MutableDataFrame } from '@grafana/data'; +import { + ArrayVector, + DataFrame, + DataQueryResponse, + Field, + FieldType, + MutableDataFrame, + TraceKeyValuePair, + TraceLog, + TraceSpanRow, +} from '@grafana/data'; +import { SpanKind, SpanStatusCode } from '@opentelemetry/api'; +import { collectorTypes } from '@opentelemetry/exporter-collector'; +import { ResourceAttributes } from '@opentelemetry/semantic-conventions'; import { createGraphFrames } from './graphTransform'; export function createTableFrame( @@ -98,6 +111,168 @@ export function transformTraceList( return response; } +// Don't forget to change the backend code when the id representation changed +function transformBase64IDToHexString(base64: string) { + const buffer = Buffer.from(base64, 'base64'); + const id = buffer.toString('hex'); + return id.length > 16 ? id.slice(16) : id; +} + +function getAttributeValue(value: collectorTypes.opentelemetryProto.common.v1.AnyValue): any { + if (value.stringValue) { + return value.stringValue; + } + + if (value.boolValue !== undefined) { + return Boolean(value.boolValue); + } + + if (value.intValue !== undefined) { + return Number.parseInt(value.intValue as any, 10); + } + + if (value.doubleValue) { + return Number.parseFloat(value.doubleValue as any); + } + + if (value.arrayValue) { + const arrayValue = []; + for (const arValue of value.arrayValue.values) { + arrayValue.push(getAttributeValue(arValue)); + } + return arrayValue; + } + + return ''; +} + +function resourceToProcess(resource: collectorTypes.opentelemetryProto.resource.v1.Resource | undefined) { + const serviceTags: TraceKeyValuePair[] = []; + let serviceName = 'OTLPResourceNoServiceName'; + if (!resource) { + return { serviceName, serviceTags }; + } + + for (const attribute of resource.attributes) { + if (attribute.key === ResourceAttributes.SERVICE_NAME) { + serviceName = attribute.value.stringValue || serviceName; + } + serviceTags.push({ key: attribute.key, value: getAttributeValue(attribute.value) }); + } + + return { serviceName, serviceTags }; +} + +function getSpanTags( + span: collectorTypes.opentelemetryProto.trace.v1.Span, + instrumentationLibrary?: collectorTypes.opentelemetryProto.common.v1.InstrumentationLibrary +): TraceKeyValuePair[] { + const spanTags: TraceKeyValuePair[] = []; + + if (instrumentationLibrary) { + if (instrumentationLibrary.name) { + spanTags.push({ key: 'otel.library.name', value: instrumentationLibrary.name }); + } + if (instrumentationLibrary.version) { + spanTags.push({ key: 'otel.library.version', value: instrumentationLibrary.version }); + } + } + + if (span.attributes) { + for (const attribute of span.attributes) { + spanTags.push({ key: attribute.key, value: getAttributeValue(attribute.value) }); + } + } + + if (span.status) { + if (span.status.code && (span.status.code as any) !== SpanStatusCode.UNSET) { + spanTags.push({ + key: 'otel.status_code', + value: SpanStatusCode[span.status.code], + }); + if (span.status.message) { + spanTags.push({ key: 'otel.status_description', value: span.status.message }); + } + } + if (span.status.code === SpanStatusCode.ERROR) { + spanTags.push({ key: 'error', value: true }); + } + } + + if ( + span.kind !== undefined && + span.kind !== collectorTypes.opentelemetryProto.trace.v1.Span.SpanKind.SPAN_KIND_INTERNAL + ) { + spanTags.push({ + key: 'span.kind', + value: SpanKind[collectorTypes.opentelemetryProto.trace.v1.Span.SpanKind[span.kind] as any].toLowerCase(), + }); + } + + return spanTags; +} + +function getLogs(span: collectorTypes.opentelemetryProto.trace.v1.Span) { + const logs: TraceLog[] = []; + if (span.events) { + for (const event of span.events) { + const fields: TraceKeyValuePair[] = []; + if (event.attributes) { + for (const attribute of event.attributes) { + fields.push({ key: attribute.key, value: getAttributeValue(attribute.value) }); + } + } + logs.push({ fields, timestamp: event.timeUnixNano / 1000000 }); + } + } + + return logs; +} + +export function transformFromOTLP( + traceData: collectorTypes.opentelemetryProto.trace.v1.ResourceSpans[] +): DataQueryResponse { + const frame = new MutableDataFrame({ + fields: [ + { name: 'traceID', type: FieldType.string }, + { name: 'spanID', type: FieldType.string }, + { name: 'parentSpanID', type: FieldType.string }, + { name: 'operationName', type: FieldType.string }, + { name: 'serviceName', type: FieldType.string }, + { name: 'serviceTags', type: FieldType.other }, + { name: 'startTime', type: FieldType.number }, + { name: 'duration', type: FieldType.number }, + { name: 'logs', type: FieldType.other }, + { name: 'tags', type: FieldType.other }, + ], + meta: { + preferredVisualisationType: 'trace', + }, + }); + + for (const data of traceData) { + const { serviceName, serviceTags } = resourceToProcess(data.resource); + for (const librarySpan of data.instrumentationLibrarySpans) { + for (const span of librarySpan.spans) { + frame.add({ + traceID: transformBase64IDToHexString(span.traceId), + spanID: transformBase64IDToHexString(span.spanId), + parentSpanID: transformBase64IDToHexString(span.parentSpanId || ''), + operationName: span.name || '', + serviceName, + serviceTags, + startTime: span.startTimeUnixNano! / 1000000, + duration: (span.endTimeUnixNano! - span.startTimeUnixNano!) / 1000000, + tags: getSpanTags(span, librarySpan.instrumentationLibrary), + logs: getLogs(span), + } as TraceSpanRow); + } + } + } + + return { data: [frame, ...createGraphFrames(frame)] }; +} + export function transformTrace(response: DataQueryResponse): DataQueryResponse { // We need to parse some of the fields which contain stringified json. // Seems like we can't just map the values as the frame we got from backend has some default processing diff --git a/yarn.lock b/yarn.lock index 903be8bbfa5..d8324e76992 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3600,6 +3600,68 @@ "@octokit/openapi-types" "^2.3.1" "@types/node" ">= 8" +"@opentelemetry/api-metrics@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/api-metrics/-/api-metrics-0.23.0.tgz#5b0bae3cee1bab2993aebd44275788577761d1d4" + integrity sha512-MGfH9aMnVktRTagYHvhksrk42vPDjTIz5N6Cxu31t6dgJa6iUYR6MemnOdphyLk73DUaqmR5s2Fn6jg0Xd9gqA== + +"@opentelemetry/api@1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@opentelemetry/api/-/api-1.0.2.tgz#921e1f2b2484b762d77225a8a25074482d93fccf" + integrity sha512-DCF9oC89ao8/EJUqrp/beBlDR8Bp2R43jqtzayqCoomIvkwTuPfLcHdVhIGRR69GFlkykFjcDW+V92t0AS7Tww== + +"@opentelemetry/core@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/core/-/core-0.23.0.tgz#611a39255ac8296a79fbc6548a6d3b1bc87ee17e" + integrity sha512-7COVsnGEW96ITjc0waWYo/R27sFqjPUg4SCoP8XL48zAGr9zjzeuJoQe/xVchs7op//qOeeEEeBxiBvXy2QS0Q== + dependencies: + "@opentelemetry/semantic-conventions" "0.23.0" + semver "^7.1.3" + +"@opentelemetry/exporter-collector@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/exporter-collector/-/exporter-collector-0.23.0.tgz#7ee7b96c61f1b148eba22fa173bf74ec44bd6164" + integrity sha512-rDy0sFSy8uUQH5i3JntVjjsUJfRaHoeMXrByl5ejuHtNRleGidx9UIZK0oSZMRvK/5lFvvJJrQFMhZQyppDfsw== + dependencies: + "@opentelemetry/api-metrics" "0.23.0" + "@opentelemetry/core" "0.23.0" + "@opentelemetry/metrics" "0.23.0" + "@opentelemetry/resources" "0.23.0" + "@opentelemetry/tracing" "0.23.0" + +"@opentelemetry/metrics@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/metrics/-/metrics-0.23.0.tgz#3c64d8b86ff4952c91aa9010fdf1c76783b33fe1" + integrity sha512-IRl/AfnNFmmNZrM58R2T/gVqatPve+T2EpaBbWv4zVfY9Q2S8q7oT8HZAPUc/GQTb2pvwLXpcKO8QmeEt4gfHQ== + dependencies: + "@opentelemetry/api-metrics" "0.23.0" + "@opentelemetry/core" "0.23.0" + "@opentelemetry/resources" "0.23.0" + lodash.merge "^4.6.2" + +"@opentelemetry/resources@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/resources/-/resources-0.23.0.tgz#221c123306708ceac707599e3a201896b953f53b" + integrity sha512-sAiaoQ0pOwjaaKySuwCUlvej/W9M5d+SxpcuBFUBUojqRlEAYDbx1FHClPnKtOysIb9rXJDQvM3xlH++7NQQzg== + dependencies: + "@opentelemetry/core" "0.23.0" + "@opentelemetry/semantic-conventions" "0.23.0" + +"@opentelemetry/semantic-conventions@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-0.23.0.tgz#ec1467fd71f6551628b60cd2107acc923b9b77cc" + integrity sha512-Tzo+VGR1zlzLbjVI+7mlDJ2xuaUsue4scWvFlK+fzcUfn9siF4NWbxoC2X6Br2B/g4dsq1OAwAYsPVYIEoY2rQ== + +"@opentelemetry/tracing@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/tracing/-/tracing-0.23.0.tgz#bf80a987f57508f2202170f4f2bc4385988ecb02" + integrity sha512-3vNLS55bE0CG1RBDz7+wAAKpLjbl8fhQKqM4MvTy/LYHSolgyM5BNutSb/TcA9LtWvkdI0djgFXxeRig1OFqoQ== + dependencies: + "@opentelemetry/core" "0.23.0" + "@opentelemetry/resources" "0.23.0" + "@opentelemetry/semantic-conventions" "0.23.0" + lodash.merge "^4.6.2" + "@pmmmwh/react-refresh-webpack-plugin@^0.4.3": version "0.4.3" resolved "https://registry.yarnpkg.com/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.4.3.tgz#1eec460596d200c0236bf195b078a5d1df89b766" @@ -15652,6 +15714,11 @@ lodash.memoize@4.x, lodash.memoize@^4.1.2: resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4= +lodash.merge@^4.6.2: + version "4.6.2" + resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" + integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== + lodash.once@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac"