diff --git a/.betterer.results b/.betterer.results index b1a0ad4f169..cbc0ba39a4e 100644 --- a/.betterer.results +++ b/.betterer.results @@ -4642,11 +4642,9 @@ exports[`better eslint`] = { [0, 0, 0, "Do not use any type assertions.", "4"], [0, 0, 0, "Unexpected any. Specify a different type.", "5"], [0, 0, 0, "Do not use any type assertions.", "6"], - [0, 0, 0, "Unexpected any. Specify a different type.", "7"], + [0, 0, 0, "Do not use any type assertions.", "7"], [0, 0, 0, "Do not use any type assertions.", "8"], - [0, 0, 0, "Do not use any type assertions.", "9"], - [0, 0, 0, "Do not use any type assertions.", "10"], - [0, 0, 0, "Unexpected any. Specify a different type.", "11"] + [0, 0, 0, "Unexpected any. Specify a different type.", "9"] ], "public/app/plugins/datasource/tempo/testResponse.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"], diff --git a/docs/sources/datasources/jaeger/_index.md b/docs/sources/datasources/jaeger/_index.md index 10154656ce3..4e77ce3e2fc 100644 --- a/docs/sources/datasources/jaeger/_index.md +++ b/docs/sources/datasources/jaeger/_index.md @@ -152,7 +152,7 @@ You can choose one of three options: | ------------ | -------------------------------------------------------------------------------------------------------------------------------- | | **None** | Adds nothing to the span bar row. | | **Duration** | _(Default)_ Displays the span duration on the span bar row. | -| **Tag** | Displays the span tag on the span bar row. You must also specify which tag key to use to get the tag value, such as `span.kind`. | +| **Tag** | Displays the span tag on the span bar row. You must also specify which tag key to use to get the tag value, such as `component`. | ### Provision the data source diff --git a/docs/sources/datasources/tempo/_index.md b/docs/sources/datasources/tempo/_index.md index 5948054e6cb..f3d9f442985 100644 --- a/docs/sources/datasources/tempo/_index.md +++ b/docs/sources/datasources/tempo/_index.md @@ -185,7 +185,7 @@ You can choose one of three options: | ------------ | -------------------------------------------------------------------------------------------------------------------------------- | | **None** | Adds nothing to the span bar row. | | **Duration** | _(Default)_ Displays the span duration on the span bar row. | -| **Tag** | Displays the span tag on the span bar row. You must also specify which tag key to use to get the tag value, such as `span.kind`. | +| **Tag** | Displays the span tag on the span bar row. You must also specify which tag key to use to get the tag value, such as `component`. | ### Provision the data source diff --git a/docs/sources/datasources/zipkin/_index.md b/docs/sources/datasources/zipkin/_index.md index fa309fe0fe8..7487cc693f0 100644 --- a/docs/sources/datasources/zipkin/_index.md +++ b/docs/sources/datasources/zipkin/_index.md @@ -150,7 +150,7 @@ You can choose one of three options: | ------------ | -------------------------------------------------------------------------------------------------------------------------------- | | **None** | Adds nothing to the span bar row. | | **Duration** | _(Default)_ Displays the span duration on the span bar row. | -| **Tag** | Displays the span tag on the span bar row. You must also specify which tag key to use to get the tag value, such as `span.kind`. | +| **Tag** | Displays the span tag on the span bar row. You must also specify which tag key to use to get the tag value, such as `component`. | ### Provision the data source diff --git a/packages/grafana-data/src/types/trace.ts b/packages/grafana-data/src/types/trace.ts index db73ff9eebe..27b16a9f73c 100644 --- a/packages/grafana-data/src/types/trace.ts +++ b/packages/grafana-data/src/types/trace.ts @@ -40,6 +40,12 @@ export interface TraceSpanRow { references?: TraceSpanReference[]; // Note: To mark spen as having error add tag error: true tags?: TraceKeyValuePair[]; + kind?: string; + statusCode?: number; + statusMessage?: string; + instrumentationLibraryName?: string; + instrumentationLibraryVersion?: string; + traceState?: string; warnings?: string[]; stackTraces?: string[]; diff --git a/pkg/tsdb/tempo/trace_transform.go b/pkg/tsdb/tempo/trace_transform.go index 46ba1b2c077..86407d1d307 100644 --- a/pkg/tsdb/tempo/trace_transform.go +++ b/pkg/tsdb/tempo/trace_transform.go @@ -46,6 +46,12 @@ func TraceToFrame(td pdata.Traces) (*data.Frame, error) { data.NewField("parentSpanID", nil, []string{}), data.NewField("operationName", nil, []string{}), data.NewField("serviceName", nil, []string{}), + data.NewField("kind", nil, []string{}), + data.NewField("statusCode", nil, []int64{}), + data.NewField("statusMessage", nil, []string{}), + data.NewField("instrumentationLibraryName", nil, []string{}), + data.NewField("instrumentationLibraryVersion", nil, []string{}), + data.NewField("traceState", nil, []string{}), data.NewField("serviceTags", nil, []json.RawMessage{}), data.NewField("startTime", nil, []float64{}), data.NewField("duration", nil, []float64{}), @@ -119,12 +125,20 @@ func spanToSpanRow(span pdata.Span, libraryTags pdata.InstrumentationLibrary, re startTime := float64(span.StartTimestamp()) / 1_000_000 serviceName, serviceTags := resourceToProcess(resource) + status := span.Status() + statusCode := int64(status.Code()) + statusMessage := status.Message() + + libraryName := libraryTags.Name() + libraryVersion := libraryTags.Version() + traceState := getTraceState(span.TraceState()) + serviceTagsJson, err := json.Marshal(serviceTags) if err != nil { return nil, fmt.Errorf("failed to marshal service tags: %w", err) } - spanTags, err := json.Marshal(getSpanTags(span, libraryTags)) + spanTags, err := json.Marshal(getSpanTags(span)) if err != nil { return nil, fmt.Errorf("failed to marshal span tags: %w", err) } @@ -147,6 +161,12 @@ func spanToSpanRow(span pdata.Span, libraryTags pdata.InstrumentationLibrary, re parentSpanID, span.Name(), serviceName, + getSpanKind(span.Kind()), + statusCode, + statusMessage, + libraryName, + libraryVersion, + traceState, json.RawMessage(serviceTagsJson), startTime, float64(span.EndTimestamp()-span.StartTimestamp()) / 1_000_000, @@ -192,56 +212,16 @@ func getAttributeVal(attr pdata.AttributeValue) interface{} { } } -func getSpanTags(span pdata.Span, instrumentationLibrary pdata.InstrumentationLibrary) []*KeyValue { +func getSpanTags(span pdata.Span) []*KeyValue { var tags []*KeyValue - - libraryTags := getTagsFromInstrumentationLibrary(instrumentationLibrary) - if libraryTags != nil { - tags = append(tags, libraryTags...) - } span.Attributes().Range(func(key string, attr pdata.AttributeValue) bool { tags = append(tags, &KeyValue{Key: key, Value: getAttributeVal(attr)}) return true }) - - status := span.Status() - possibleNilTags := []*KeyValue{ - getTagFromSpanKind(span.Kind()), - getTagFromStatusCode(status.Code()), - getErrorTagFromStatusCode(status.Code()), - getTagFromStatusMsg(status.Message()), - getTagFromTraceState(span.TraceState()), - } - - for _, tag := range possibleNilTags { - if tag != nil { - tags = append(tags, tag) - } - } return tags } -func getTagsFromInstrumentationLibrary(il pdata.InstrumentationLibrary) []*KeyValue { - var keyValues []*KeyValue - if ilName := il.Name(); ilName != "" { - kv := &KeyValue{ - Key: conventions.InstrumentationLibraryName, - Value: ilName, - } - keyValues = append(keyValues, kv) - } - if ilVersion := il.Version(); ilVersion != "" { - kv := &KeyValue{ - Key: conventions.InstrumentationLibraryVersion, - Value: ilVersion, - } - keyValues = append(keyValues, kv) - } - - return keyValues -} - -func getTagFromSpanKind(spanKind pdata.SpanKind) *KeyValue { +func getSpanKind(spanKind pdata.SpanKind) string { var tagStr string switch spanKind { case pdata.SpanKindClient: @@ -255,50 +235,17 @@ func getTagFromSpanKind(spanKind pdata.SpanKind) *KeyValue { case pdata.SpanKindInternal: tagStr = string(tracetranslator.OpenTracingSpanKindInternal) default: - return nil - } - - return &KeyValue{ - Key: tracetranslator.TagSpanKind, - Value: tagStr, - } -} - -func getTagFromStatusCode(statusCode pdata.StatusCode) *KeyValue { - return &KeyValue{ - Key: tracetranslator.TagStatusCode, - Value: int64(statusCode), - } -} - -func getErrorTagFromStatusCode(statusCode pdata.StatusCode) *KeyValue { - if statusCode == pdata.StatusCodeError { - return &KeyValue{ - Key: tracetranslator.TagError, - Value: true, - } + return "" } - return nil -} -func getTagFromStatusMsg(statusMsg string) *KeyValue { - if statusMsg == "" { - return nil - } - return &KeyValue{ - Key: tracetranslator.TagStatusMsg, - Value: statusMsg, - } + return tagStr } -func getTagFromTraceState(traceState pdata.TraceState) *KeyValue { +func getTraceState(traceState pdata.TraceState) string { if traceState != pdata.TraceStateEmpty { - return &KeyValue{ - Key: tracetranslator.TagW3CTraceState, - Value: string(traceState), - } + return string(traceState) } - return nil + return "" } func spanEventsToLogs(events pdata.SpanEventSlice) []*TraceLog { diff --git a/pkg/tsdb/tempo/trace_transform_test.go b/pkg/tsdb/tempo/trace_transform_test.go index 5f378c6bce6..7eff7dc36de 100644 --- a/pkg/tsdb/tempo/trace_transform_test.go +++ b/pkg/tsdb/tempo/trace_transform_test.go @@ -40,7 +40,7 @@ func TestTraceToFrame(t *testing.T) { require.Equal(t, 1616072924070.497, root["startTime"]) require.Equal(t, 8.421, root["duration"]) require.Equal(t, json.RawMessage("null"), root["logs"]) - require.Equal(t, json.RawMessage("[{\"value\":\"const\",\"key\":\"sampler.type\"},{\"value\":true,\"key\":\"sampler.param\"},{\"value\":200,\"key\":\"http.status_code\"},{\"value\":\"GET\",\"key\":\"http.method\"},{\"value\":\"/loki/api/v1/query_range?direction=BACKWARD\\u0026limit=1000\\u0026query=%7Bcompose_project%3D%22devenv%22%7D%20%7C%3D%22traceID%22\\u0026start=1616070921000000000\\u0026end=1616072722000000000\\u0026step=2\",\"key\":\"http.url\"},{\"value\":\"net/http\",\"key\":\"component\"},{\"value\":\"server\",\"key\":\"span.kind\"},{\"value\":0,\"key\":\"status.code\"}]"), root["tags"]) + require.Equal(t, json.RawMessage("[{\"value\":\"const\",\"key\":\"sampler.type\"},{\"value\":true,\"key\":\"sampler.param\"},{\"value\":200,\"key\":\"http.status_code\"},{\"value\":\"GET\",\"key\":\"http.method\"},{\"value\":\"/loki/api/v1/query_range?direction=BACKWARD\\u0026limit=1000\\u0026query=%7Bcompose_project%3D%22devenv%22%7D%20%7C%3D%22traceID%22\\u0026start=1616070921000000000\\u0026end=1616072722000000000\\u0026step=2\",\"key\":\"http.url\"},{\"value\":\"net/http\",\"key\":\"component\"}]"), root["tags"]) span := bFrame.FindRowWithValue("spanID", "7198307df9748606") @@ -50,7 +50,6 @@ func TestTraceToFrame(t *testing.T) { require.Equal(t, 1616072924072.852, span["startTime"]) require.Equal(t, 0.094, span["duration"]) require.Equal(t, json.RawMessage("[{\"timestamp\":1616072924072.856,\"fields\":[{\"value\":1,\"key\":\"chunks requested\"}]},{\"timestamp\":1616072924072.9448,\"fields\":[{\"value\":1,\"key\":\"chunks fetched\"}]}]"), span["logs"]) - require.Equal(t, json.RawMessage("[{\"value\":0,\"key\":\"status.code\"}]"), span["tags"]) }) t.Run("should transform correct traceID", func(t *testing.T) { @@ -141,6 +140,12 @@ var fields = []string{ "parentSpanID", "operationName", "serviceName", + "kind", + "statusCode", + "statusMessage", + "instrumentationLibraryName", + "instrumentationLibraryVersion", + "traceState", "serviceTags", "startTime", "duration", diff --git a/public/app/features/explore/TraceView/components/TracePageHeader/SpanFilters/SpanFilters.test.tsx b/public/app/features/explore/TraceView/components/TracePageHeader/SpanFilters/SpanFilters.test.tsx index 3ae9182526a..7f92562459d 100644 --- a/public/app/features/explore/TraceView/components/TracePageHeader/SpanFilters/SpanFilters.test.tsx +++ b/public/app/features/explore/TraceView/components/TracePageHeader/SpanFilters/SpanFilters.test.tsx @@ -15,6 +15,12 @@ const trace: Trace = { spanID: '1ed38015486087ca', operationName: 'Span0', tags: [{ key: 'TagKey0', type: 'string', value: 'TagValue0' }], + kind: 'server', + statusCode: 2, + statusMessage: 'message', + instrumentationLibraryName: 'name', + instrumentationLibraryVersion: 'version', + traceState: 'state', process: { serviceName: 'Service0', tags: [{ key: 'ProcessKey0', type: 'string', value: 'ProcessValue0' }], @@ -123,6 +129,7 @@ describe('SpanFilters', () => { await waitFor(() => { expect(screen.getByText('TagKey0')).toBeInTheDocument(); expect(screen.getByText('TagKey1')).toBeInTheDocument(); + expect(screen.getByText('kind')).toBeInTheDocument(); expect(screen.getByText('ProcessKey0')).toBeInTheDocument(); expect(screen.getByText('ProcessKey1')).toBeInTheDocument(); expect(screen.getByText('LogKey0')).toBeInTheDocument(); @@ -164,8 +171,14 @@ describe('SpanFilters', () => { expect(container?.childNodes[1].textContent).toBe('ProcessKey1'); expect(container?.childNodes[2].textContent).toBe('TagKey0'); expect(container?.childNodes[3].textContent).toBe('TagKey1'); - expect(container?.childNodes[4].textContent).toBe('LogKey0'); - expect(container?.childNodes[5].textContent).toBe('LogKey1'); + expect(container?.childNodes[4].textContent).toBe('kind'); + expect(container?.childNodes[5].textContent).toBe('library.name'); + expect(container?.childNodes[6].textContent).toBe('library.version'); + expect(container?.childNodes[7].textContent).toBe('status'); + expect(container?.childNodes[8].textContent).toBe('status.message'); + expect(container?.childNodes[9].textContent).toBe('trace.state'); + expect(container?.childNodes[10].textContent).toBe('LogKey0'); + expect(container?.childNodes[11].textContent).toBe('LogKey1'); }); }); diff --git a/public/app/features/explore/TraceView/components/TracePageHeader/SpanFilters/SpanFilters.tsx b/public/app/features/explore/TraceView/components/TracePageHeader/SpanFilters/SpanFilters.tsx index 8ef71486527..64494eca20f 100644 --- a/public/app/features/explore/TraceView/components/TracePageHeader/SpanFilters/SpanFilters.tsx +++ b/public/app/features/explore/TraceView/components/TracePageHeader/SpanFilters/SpanFilters.tsx @@ -13,6 +13,7 @@ // limitations under the License. import { css } from '@emotion/css'; +import { SpanStatusCode } from '@opentelemetry/api'; import { uniq } from 'lodash'; import React, { useState, useEffect, memo, useCallback } from 'react'; @@ -31,6 +32,7 @@ import { } from '@grafana/ui'; import { defaultFilters, randomId, SearchProps, Tag } from '../../../useSearch'; +import { KIND, LIBRARY_NAME, LIBRARY_VERSION, STATUS, STATUS_MESSAGE, TRACE_STATE } from '../../constants/span'; import { Trace } from '../../types'; import NewTracePageSearchBar from '../NewTracePageSearchBar'; @@ -119,6 +121,25 @@ export const SpanFilters = memo((props: SpanFilterProps) => { }); }); } + + if (span.kind) { + keys.push(KIND); + } + if (span.statusCode !== undefined) { + keys.push(STATUS); + } + if (span.statusMessage) { + keys.push(STATUS_MESSAGE); + } + if (span.instrumentationLibraryName) { + keys.push(LIBRARY_NAME); + } + if (span.instrumentationLibraryVersion) { + keys.push(LIBRARY_VERSION); + } + if (span.traceState) { + keys.push(TRACE_STATE); + } }); keys = uniq(keys).sort(); logKeys = uniq(logKeys).sort(); @@ -147,6 +168,41 @@ export const SpanFilters = memo((props: SpanFilterProps) => { } }); } + + switch (key) { + case KIND: + if (span.kind) { + values.push(span.kind); + } + break; + case STATUS: + if (span.statusCode !== undefined) { + values.push(SpanStatusCode[span.statusCode].toLowerCase()); + } + break; + case STATUS_MESSAGE: + if (span.statusMessage) { + values.push(span.statusMessage); + } + break; + case LIBRARY_NAME: + if (span.instrumentationLibraryName) { + values.push(span.instrumentationLibraryName); + } + break; + case LIBRARY_VERSION: + if (span.instrumentationLibraryVersion) { + values.push(span.instrumentationLibraryVersion); + } + break; + case TRACE_STATE: + if (span.traceState) { + values.push(span.traceState); + } + break; + default: + break; + } }); return uniq(values).sort().map(toOption); diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanBarRow.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanBarRow.tsx index ce163c74168..3035e3ce64d 100644 --- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanBarRow.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanBarRow.tsx @@ -558,13 +558,13 @@ export class UnthemedSpanBarRow extends React.PureComponent { const tag = span.tags?.find((tag: TraceKeyValuePair) => { return tag.key === tagKey; }); - const process = span.process?.tags?.find((process: TraceKeyValuePair) => { - return process.key === tagKey; - }); - if (tag) { return `(${tag.value})`; } + + const process = span.process?.tags?.find((process: TraceKeyValuePair) => { + return process.key === tagKey; + }); if (process) { return `(${process.value})`; } diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/index.test.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/index.test.tsx index ece47433797..a048f3a4f9b 100644 --- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/index.test.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/index.test.tsx @@ -47,6 +47,14 @@ describe('', () => { createFocusSpanLink: jest.fn().mockReturnValue({}), topOfViewRefType: 'Explore', }; + + span.kind = 'test-kind'; + span.statusCode = 2; + span.statusMessage = 'test-message'; + span.instrumentationLibraryName = 'test-name'; + span.instrumentationLibraryVersion = 'test-version'; + span.traceState = 'test-state'; + span.logs = [ { timestamp: 10, @@ -125,11 +133,22 @@ describe('', () => { expect(screen.getByRole('heading', { name: span.operationName })).toBeInTheDocument(); }); - it('lists the service name, duration and start time', () => { + it('lists the service name, duration, start time and kind', () => { render(); expect(screen.getByText('Duration:')).toBeInTheDocument(); expect(screen.getByText('Service:')).toBeInTheDocument(); expect(screen.getByText('Start Time:')).toBeInTheDocument(); + expect(screen.getByText('Kind:')).toBeInTheDocument(); + expect(screen.getByText('test-kind')).toBeInTheDocument(); + expect(screen.getByText('Status:')).toBeInTheDocument(); + expect(screen.getByText('Status Message:')).toBeInTheDocument(); + expect(screen.getByText('test-message')).toBeInTheDocument(); + expect(screen.getByText('Library Name:')).toBeInTheDocument(); + expect(screen.getByText('test-name')).toBeInTheDocument(); + expect(screen.getByText('Library Version:')).toBeInTheDocument(); + expect(screen.getByText('test-version')).toBeInTheDocument(); + expect(screen.getByText('Trace State:')).toBeInTheDocument(); + expect(screen.getByText('test-state')).toBeInTheDocument(); }); it('start time shows the absolute time', () => { diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/index.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/index.tsx index 4586dd883d3..20a6aea1bd2 100644 --- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/index.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/index.tsx @@ -13,6 +13,7 @@ // limitations under the License. import { css } from '@emotion/css'; +import { SpanStatusCode } from '@opentelemetry/api'; import cx from 'classnames'; import React from 'react'; @@ -23,6 +24,7 @@ import { Button, DataLinkButton, Icon, TextArea, useStyles2 } from '@grafana/ui' import { autoColor } from '../../Theme'; import { Divider } from '../../common/Divider'; import LabeledList from '../../common/LabeledList'; +import { KIND, LIBRARY_NAME, LIBRARY_VERSION, STATUS, STATUS_MESSAGE, TRACE_STATE } from '../../constants/span'; import { SpanLinkFunc, TNil } from '../../types'; import { SpanLinkType } from '../../types/links'; import { TraceKeyValuePair, TraceLink, TraceLog, TraceSpan, TraceSpanReference } from '../../types/trace'; @@ -167,7 +169,7 @@ export default function SpanDetail(props: SpanDetailProps) { stackTraces, } = span; const { timeZone } = props; - const overviewItems = [ + let overviewItems = [ { key: 'svc', label: 'Service:', @@ -193,6 +195,50 @@ export default function SpanDetail(props: SpanDetailProps) { ] : []), ]; + + if (span.kind) { + overviewItems.push({ + key: KIND, + label: 'Kind:', + value: span.kind, + }); + } + if (span.statusCode !== undefined) { + overviewItems.push({ + key: STATUS, + label: 'Status:', + value: SpanStatusCode[span.statusCode].toLowerCase(), + }); + } + if (span.statusMessage) { + overviewItems.push({ + key: STATUS_MESSAGE, + label: 'Status Message:', + value: span.statusMessage, + }); + } + if (span.instrumentationLibraryName) { + overviewItems.push({ + key: LIBRARY_NAME, + label: 'Library Name:', + value: span.instrumentationLibraryName, + }); + } + if (span.instrumentationLibraryVersion) { + overviewItems.push({ + key: LIBRARY_VERSION, + label: 'Library Version:', + value: span.instrumentationLibraryVersion, + }); + } + if (span.traceState) { + overviewItems.push({ + key: TRACE_STATE, + label: 'Trace State:', + value: span.traceState, + }); + } + const styles = useStyles2(getStyles); let logLinkButton: JSX.Element | undefined = undefined; diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/utils.test.ts b/public/app/features/explore/TraceView/components/TraceTimelineViewer/utils.test.ts index 50d87b73bc8..9306391774f 100644 --- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/utils.test.ts +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/utils.test.ts @@ -57,11 +57,24 @@ describe('TraceTimelineViewer/utils', () => { }); describe('spanHasTag() and variants', () => { - it('returns true iff the key/value pair is found', () => { + it('returns true if client span', () => { + const span = traceGenerator.span; + span.kind = 'client'; + expect(isServerSpan(span)).toBe(false); + expect(isClientSpan(span)).toBe(true); + span.kind = 'server'; + expect(isServerSpan(span)).toBe(true); + expect(isClientSpan(span)).toBe(false); + span.statusCode = 0; + expect(isErrorSpan(span)).toBe(false); + span.statusCode = 2; + expect(isErrorSpan(span)).toBe(true); + }); + + it('returns true if the key/value pair is found', () => { const span = traceGenerator.span; span.tags = [{ key: 'span.kind', value: 'server' }]; expect(spanHasTag('span.kind', 'client', span)).toBe(false); - expect(spanHasTag('span.kind', 'client', span)).toBe(false); expect(spanHasTag('span.kind', 'server', span)).toBe(true); }); @@ -77,41 +90,56 @@ describe('TraceTimelineViewer/utils', () => { it(msg, () => { const span = { tags: traceGenerator.tags() } as TraceSpan; expect(testCase.fn(span)).toBe(false); - span.tags.push(testCase); + span.tags!.push(testCase); expect(testCase.fn(span)).toBe(true); }); }); }); describe('spanContainsErredSpan()', () => { + // Using a string to generate the test spans. Each line results in a span. The + // left number indicates whether or not the generated span has a descendant + // with an error tag (the expectation). The length of the line indicates the + // depth of the span (i.e. further right is higher depth). The right number + // indicates whether or not the span has an error tag. + const config = ` + 1 0 + 1 0 + 0 1 + 0 0 + 1 0 + 1 1 + 0 1 + 0 0 + 1 0 + 0 1 + 0 0 + ` + .trim() + .split('\n') + .map((s) => s.trim()); + // Get the expectation, str -> number -> bool + const expectations = config.map((s) => Boolean(Number(s[0]))); + + it('returns true only when a descendant has an error value', () => { + const spans = config.map((line) => ({ + depth: line.length, + statusCode: +line.slice(-1) ? 2 : 0, + })) as TraceSpan[]; + + expectations.forEach((target, i) => { + // include the index in the expect condition to know which span failed + // (if there is a failure, that is) + const result = [i, spanContainsErredSpan(spans, i)]; + expect(result).toEqual([i, target]); + }); + }); + it('returns true only when a descendant has an error tag', () => { const errorTag = { key: 'error', type: 'bool', value: true }; const getTags = (withError: number) => withError ? traceGenerator.tags().concat(errorTag) : traceGenerator.tags(); - // Using a string to generate the test spans. Each line results in a span. The - // left number indicates whether or not the generated span has a descendant - // with an error tag (the expectation). The length of the line indicates the - // depth of the span (i.e. further right is higher depth). The right number - // indicates whether or not the span has an error tag. - const config = ` - 1 0 - 1 0 - 0 1 - 0 0 - 1 0 - 1 1 - 0 1 - 0 0 - 1 0 - 0 1 - 0 0 - ` - .trim() - .split('\n') - .map((s) => s.trim()); - // Get the expectation, str -> number -> bool - const expectations = config.map((s) => Boolean(Number(s[0]))); const spans = config.map((line) => ({ depth: line.length, tags: getTags(+line.slice(-1)), @@ -126,6 +154,36 @@ describe('TraceTimelineViewer/utils', () => { }); }); + describe('findServerChildSpan() for OTEL', () => { + let spans: TraceSpan[]; + + beforeEach(() => { + spans = [ + { depth: 0, kind: 'client' }, + { depth: 1 }, + { depth: 1, kind: 'server' }, + { depth: 1, kind: 'third-kind' }, + { depth: 1, kind: 'server' }, + ] as TraceSpan[]; + }); + + it('returns falsy if the frist span is not a client', () => { + expect(findServerChildSpan(spans.slice(1))).toBeFalsy(); + }); + + it('returns the first server span', () => { + const span = findServerChildSpan(spans); + expect(span).toBe(spans[2]); + }); + + it('bails when a non-child-depth span is encountered', () => { + spans[1].depth++; + expect(findServerChildSpan(spans)).toBeFalsy(); + spans[1].depth = spans[0].depth; + expect(findServerChildSpan(spans)).toBeFalsy(); + }); + }); + describe('findServerChildSpan()', () => { let spans: TraceSpan[]; diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/utils.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/utils.tsx index ee195495589..1fa55b1d957 100644 --- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/utils.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/utils.tsx @@ -51,11 +51,10 @@ export function createViewedBoundsFunc(viewRange: { min: number; max: number; vi /** * Returns `true` if the `span` has a tag matching `key` = `value`. * - * @param {string} key The tag key to match on. - * @param {any} value The tag value to match. - * @param {{tags}} span An object with a `tags` property of { key, value } - * items. - * @returns {boolean} True if a match was found. + * @param {string} key The tag key to match on. + * @param {any} value The tag value to match. + * @param {{tag}} span An object with a `tag` property of { key, value } items. + * @returns {boolean} True if a match was found. */ export function spanHasTag(key: string, value: unknown, span: TraceSpan) { if (!Array.isArray(span.tags) || !span.tags.length) { @@ -64,12 +63,17 @@ export function spanHasTag(key: string, value: unknown, span: TraceSpan) { return span.tags.some((tag) => tag.key === key && tag.value === value); } -export const isClientSpan = spanHasTag.bind(null, 'span.kind', 'client'); -export const isServerSpan = spanHasTag.bind(null, 'span.kind', 'server'); +const isClientOtel = (span: TraceSpan) => span.kind === 'client'; +const isClient = spanHasTag.bind(null, 'span.kind', 'client'); +export const isClientSpan = (span: TraceSpan) => isClientOtel(span) || isClient(span); +const isServerOtel = (span: TraceSpan) => span.kind === 'server'; +const isServer = spanHasTag.bind(null, 'span.kind', 'server'); +export const isServerSpan = (span: TraceSpan) => isServerOtel(span) || isServer(span); +const isErrorOtel = (span: TraceSpan) => span.statusCode === 2; const isErrorBool = spanHasTag.bind(null, 'error', true); const isErrorStr = spanHasTag.bind(null, 'error', 'true'); -export const isErrorSpan = (span: TraceSpan) => isErrorBool(span) || isErrorStr(span); +export const isErrorSpan = (span: TraceSpan) => isErrorOtel(span) || isErrorBool(span) || isErrorStr(span); /** * Returns `true` if at least one of the descendants of the `parentSpanIndex` @@ -112,7 +116,11 @@ export function findServerChildSpan(spans: TraceSpan[]) { return null; } -export const isKindClient = (span: TraceSpan): Boolean => - span.tags.some(({ key, value }) => key === 'span.kind' && value === 'client'); +export const isKindClient = (span: TraceSpan): Boolean => { + if (span.kind) { + return span.kind === 'client'; + } + return span.tags.some(({ key, value }) => key === 'span.kind' && value === 'client'); +}; export { formatDuration } from '../utils/date'; diff --git a/public/app/features/explore/TraceView/components/common/LabeledList.tsx b/public/app/features/explore/TraceView/components/common/LabeledList.tsx index 58df134c695..55e08dbf014 100644 --- a/public/app/features/explore/TraceView/components/common/LabeledList.tsx +++ b/public/app/features/explore/TraceView/components/common/LabeledList.tsx @@ -52,7 +52,7 @@ const getStyles = (divider: boolean) => (theme: GrafanaTheme2) => { `, LabeledListValue: css` label: LabeledListValue; - margin-right: 0.55rem; + ${!divider && `margin-right: 0.55rem;`} `, }; }; diff --git a/public/app/features/explore/TraceView/components/constants/span.ts b/public/app/features/explore/TraceView/components/constants/span.ts new file mode 100644 index 00000000000..95a04adba06 --- /dev/null +++ b/public/app/features/explore/TraceView/components/constants/span.ts @@ -0,0 +1,6 @@ +export const KIND = 'kind'; +export const STATUS = 'status'; +export const STATUS_MESSAGE = 'status.message'; +export const LIBRARY_NAME = 'library.name'; +export const LIBRARY_VERSION = 'library.version'; +export const TRACE_STATE = 'trace.state'; diff --git a/public/app/features/explore/TraceView/components/model/transform-trace-data.test.ts b/public/app/features/explore/TraceView/components/model/transform-trace-data.test.ts index 8c3270ca88c..53d3f3b696d 100644 --- a/public/app/features/explore/TraceView/components/model/transform-trace-data.test.ts +++ b/public/app/features/explore/TraceView/components/model/transform-trace-data.test.ts @@ -47,7 +47,7 @@ describe('deduplicateTags()', () => { { key: 'a.ip', value: '8.8.8.8' }, ]); - expect(tagsInfo.tags).toEqual([ + expect(tagsInfo.dedupedTags).toEqual([ { key: 'b.ip', value: '8.8.4.4' }, { key: 'b.ip', value: '8.8.8.8' }, { key: 'a.ip', value: '8.8.8.8' }, diff --git a/public/app/features/explore/TraceView/components/model/transform-trace-data.tsx b/public/app/features/explore/TraceView/components/model/transform-trace-data.tsx index d116874f1cb..ccd1905bd77 100644 --- a/public/app/features/explore/TraceView/components/model/transform-trace-data.tsx +++ b/public/app/features/explore/TraceView/components/model/transform-trace-data.tsx @@ -24,9 +24,9 @@ import { getConfigValue } from '../utils/config/get-config'; import { getTraceName } from './trace-viewer'; // exported for tests -export function deduplicateTags(spanTags: TraceKeyValuePair[]) { +export function deduplicateTags(tags: TraceKeyValuePair[]) { const warningsHash: Map = new Map(); - const tags: TraceKeyValuePair[] = spanTags.reduce((uniqueTags, tag) => { + const dedupedTags: TraceKeyValuePair[] = tags.reduce((uniqueTags, tag) => { if (!uniqueTags.some((t) => t.key === tag.key && t.value === tag.value)) { uniqueTags.push(tag); } else { @@ -35,12 +35,12 @@ export function deduplicateTags(spanTags: TraceKeyValuePair[]) { return uniqueTags; }, []); const warnings = Array.from(warningsHash.values()); - return { tags, warnings }; + return { dedupedTags, warnings }; } // exported for tests -export function orderTags(spanTags: TraceKeyValuePair[], topPrefixes?: string[]) { - const orderedTags: TraceKeyValuePair[] = spanTags?.slice() ?? []; +export function orderTags(tags: TraceKeyValuePair[], topPrefixes?: string[]) { + const orderedTags: TraceKeyValuePair[] = tags?.slice() ?? []; const tp = (topPrefixes || []).map((p: string) => p.toLowerCase()); orderedTags.sort((a, b) => { @@ -156,7 +156,7 @@ export default function transformTraceData(data: TraceResponse | undefined): Tra span.tags = span.tags || []; span.references = span.references || []; const tagsInfo = deduplicateTags(span.tags); - span.tags = orderTags(tagsInfo.tags, getConfigValue('topTagPrefixes')); + span.tags = orderTags(tagsInfo.dedupedTags, getConfigValue('topTagPrefixes')); span.warnings = span.warnings.concat(tagsInfo.warnings); span.references.forEach((ref, index) => { const refSpan = spanMap.get(ref.spanID); diff --git a/public/app/features/explore/TraceView/components/types/trace.ts b/public/app/features/explore/TraceView/components/types/trace.ts index 4e180189a4d..178c8d864e6 100644 --- a/public/app/features/explore/TraceView/components/types/trace.ts +++ b/public/app/features/explore/TraceView/components/types/trace.ts @@ -57,6 +57,12 @@ export type TraceSpanData = { duration: number; logs: TraceLog[]; tags?: TraceKeyValuePair[]; + kind?: string; + statusCode?: number; + statusMessage?: string; + instrumentationLibraryName?: string; + instrumentationLibraryVersion?: string; + traceState?: string; references?: TraceSpanReference[]; warnings?: string[] | null; stackTraces?: string[]; diff --git a/public/app/features/explore/TraceView/components/utils/filter-spans.test.ts b/public/app/features/explore/TraceView/components/utils/filter-spans.test.ts index ed733b88a4c..e4f95c7c8db 100644 --- a/public/app/features/explore/TraceView/components/utils/filter-spans.test.ts +++ b/public/app/features/explore/TraceView/components/utils/filter-spans.test.ts @@ -24,6 +24,12 @@ describe('filterSpans', () => { spanID: spanID0, operationName: 'operationName0', duration: 3050, + kind: 'kind0', + statusCode: 0, + statusMessage: 'statusMessage0', + instrumentationLibraryName: 'libraryName', + instrumentationLibraryVersion: 'libraryVersion0', + traceState: 'traceState0', process: { serviceName: 'serviceName0', tags: [ @@ -69,6 +75,12 @@ describe('filterSpans', () => { spanID: spanID2, operationName: 'operationName2', duration: 5000, + kind: 'kind2', + statusCode: 2, + statusMessage: 'statusMessage2', + instrumentationLibraryName: 'libraryName', + instrumentationLibraryVersion: 'libraryVersion2', + traceState: 'traceState2', process: { serviceName: 'serviceName2', tags: [ @@ -178,6 +190,117 @@ describe('filterSpans', () => { ).toEqual(new Set([spanID0])); }); + it('should return spans whose kind, statusCode, statusMessage, libraryName, libraryVersion or traceState match a filter', () => { + expect( + filterSpansNewTraceViewHeader({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'kind' }] }, spans) + ).toEqual(new Set([spanID0, spanID2])); + expect( + filterSpansNewTraceViewHeader( + { ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'kind', value: 'kind0' }] }, + spans + ) + ).toEqual(new Set([spanID0])); + expect( + filterSpansNewTraceViewHeader( + { ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'kind', operator: '!=', value: 'kind0' }] }, + spans + ) + ).toEqual(new Set([spanID2])); + expect( + filterSpansNewTraceViewHeader({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'status' }] }, spans) + ).toEqual(new Set([spanID0, spanID2])); + expect( + filterSpansNewTraceViewHeader( + { ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'status', value: 'unset' }] }, + spans + ) + ).toEqual(new Set([spanID0])); + expect( + filterSpansNewTraceViewHeader( + { ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'status', operator: '!=', value: 'unset' }] }, + spans + ) + ).toEqual(new Set([spanID2])); + expect( + filterSpansNewTraceViewHeader( + { ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'status.message' }] }, + spans + ) + ).toEqual(new Set([spanID0, spanID2])); + expect( + filterSpansNewTraceViewHeader( + { ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'status.message', value: 'statusMessage0' }] }, + spans + ) + ).toEqual(new Set([spanID0])); + expect( + filterSpansNewTraceViewHeader( + { + ...defaultFilters, + tags: [{ ...defaultTagFilter, key: 'status.message', operator: '!=', value: 'statusMessage0' }], + }, + spans + ) + ).toEqual(new Set([spanID2])); + expect( + filterSpansNewTraceViewHeader({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'library.name' }] }, spans) + ).toEqual(new Set([spanID0, spanID2])); + expect( + filterSpansNewTraceViewHeader( + { ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'library.name', value: 'libraryName' }] }, + spans + ) + ).toEqual(new Set([spanID0, spanID2])); + expect( + filterSpansNewTraceViewHeader( + { + ...defaultFilters, + tags: [{ ...defaultTagFilter, key: 'library.name', operator: '!=', value: 'libraryName' }], + }, + spans + ) + ).toEqual(new Set([])); + expect( + filterSpansNewTraceViewHeader( + { ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'library.version' }] }, + spans + ) + ).toEqual(new Set([spanID0, spanID2])); + expect( + filterSpansNewTraceViewHeader( + { ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'library.version', value: 'libraryVersion0' }] }, + spans + ) + ).toEqual(new Set([spanID0])); + expect( + filterSpansNewTraceViewHeader( + { + ...defaultFilters, + tags: [{ ...defaultTagFilter, key: 'library.version', operator: '!=', value: 'libraryVersion0' }], + }, + spans + ) + ).toEqual(new Set([spanID2])); + expect( + filterSpansNewTraceViewHeader({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'trace.state' }] }, spans) + ).toEqual(new Set([spanID0, spanID2])); + expect( + filterSpansNewTraceViewHeader( + { ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'trace.state', value: 'traceState0' }] }, + spans + ) + ).toEqual(new Set([spanID0])); + expect( + filterSpansNewTraceViewHeader( + { + ...defaultFilters, + tags: [{ ...defaultTagFilter, key: 'trace.state', operator: '!=', value: 'traceState0' }], + }, + spans + ) + ).toEqual(new Set([spanID2])); + }); + it('should return spans whose process.tags kv.key match a filter', () => { expect( filterSpansNewTraceViewHeader( @@ -533,6 +656,15 @@ describe('filterSpans', () => { expect(filterSpans('tagValue1 -tagKey1', spans)).toEqual(new Set([spanID2])); }); + it('should return spans whose kind, statusCode, statusMessage, libraryName, libraryVersion or traceState value match a filter', () => { + expect(filterSpans('kind0', spans)).toEqual(new Set([spanID0])); + expect(filterSpans('error', spans)).toEqual(new Set([spanID2])); + expect(filterSpans('statusMessage0', spans)).toEqual(new Set([spanID0])); + expect(filterSpans('libraryName', spans)).toEqual(new Set([spanID0, spanID2])); + expect(filterSpans('libraryVersion2', spans)).toEqual(new Set([spanID2])); + expect(filterSpans('traceState0', spans)).toEqual(new Set([spanID0])); + }); + it('should return spans whose logs have a field whose kv.key match a filter', () => { expect(filterSpans('logFieldKey1', spans)).toEqual(new Set([spanID0, spanID2])); expect(filterSpans('logFieldKey0', spans)).toEqual(new Set([spanID0])); diff --git a/public/app/features/explore/TraceView/components/utils/filter-spans.tsx b/public/app/features/explore/TraceView/components/utils/filter-spans.tsx index e3d4735c769..7fe267a58e0 100644 --- a/public/app/features/explore/TraceView/components/utils/filter-spans.tsx +++ b/public/app/features/explore/TraceView/components/utils/filter-spans.tsx @@ -12,7 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. +import { SpanStatusCode } from '@opentelemetry/api'; + import { SearchProps, Tag } from '../../useSearch'; +import { KIND, LIBRARY_NAME, LIBRARY_VERSION, STATUS, STATUS_MESSAGE, TRACE_STATE } from '../constants/span'; import { TNil, TraceKeyValuePair, TraceSpan } from '../types'; // filter spans where all filters added need to be true for each individual span that is returned @@ -56,21 +59,36 @@ const getTagMatches = (spans: TraceSpan[], tags: Tag[]) => { // match against every tag filter return tags.every((tag: Tag) => { if (tag.key && tag.value) { - if (span.tags.some((kv) => checkKeyAndValueForMatch(tag, kv))) { - return getReturnValue(tag.operator, true); - } else if (span.process.tags.some((kv) => checkKeyAndValueForMatch(tag, kv))) { - return getReturnValue(tag.operator, true); - } else if (span.logs.some((log) => log.fields.some((kv) => checkKeyAndValueForMatch(tag, kv)))) { + if ( + span.tags.some((kv) => checkKeyAndValueForMatch(tag, kv)) || + span.process.tags.some((kv) => checkKeyAndValueForMatch(tag, kv)) || + (span.logs && span.logs.some((log) => log.fields.some((kv) => checkKeyAndValueForMatch(tag, kv)))) || + (span.kind && tag.key === KIND && tag.value === span.kind) || + (span.statusCode !== undefined && + tag.key === STATUS && + tag.value === SpanStatusCode[span.statusCode].toLowerCase()) || + (span.statusMessage && tag.key === STATUS_MESSAGE && tag.value === span.statusMessage) || + (span.instrumentationLibraryName && + tag.key === LIBRARY_NAME && + tag.value === span.instrumentationLibraryName) || + (span.instrumentationLibraryVersion && + tag.key === LIBRARY_VERSION && + tag.value === span.instrumentationLibraryVersion) || + (span.traceState && tag.key === TRACE_STATE && tag.value === span.traceState) + ) { return getReturnValue(tag.operator, true); } } else if (tag.key) { - if (span.tags.some((kv) => checkKeyForMatch(tag.key!, kv.key))) { - return getReturnValue(tag.operator, true); - } else if (span.process.tags.some((kv) => checkKeyForMatch(tag.key!, kv.key))) { - return getReturnValue(tag.operator, true); - } else if ( - span.logs && - span.logs.some((log) => log.fields.some((kv) => checkKeyForMatch(tag.key!, kv.key))) + if ( + span.tags.some((kv) => checkKeyForMatch(tag.key!, kv.key)) || + span.process.tags.some((kv) => checkKeyForMatch(tag.key!, kv.key)) || + (span.logs && span.logs.some((log) => log.fields.some((kv) => checkKeyForMatch(tag.key!, kv.key)))) || + (span.kind && tag.key === KIND) || + (span.statusCode !== undefined && tag.key === STATUS) || + (span.statusMessage && tag.key === STATUS_MESSAGE) || + (span.instrumentationLibraryName && tag.key === LIBRARY_NAME) || + (span.instrumentationLibraryVersion && tag.key === LIBRARY_VERSION) || + (span.traceState && tag.key === TRACE_STATE) ) { return getReturnValue(tag.operator, true); } @@ -194,6 +212,12 @@ export function filterSpans(textFilter: string, spans: TraceSpan[] | TNil) { isTextInFilters(includeFilters, span.operationName) || isTextInFilters(includeFilters, span.process.serviceName) || isTextInKeyValues(span.tags) || + (span.kind && isTextInFilters(includeFilters, span.kind)) || + (span.statusCode !== undefined && isTextInFilters(includeFilters, SpanStatusCode[span.statusCode])) || + (span.statusMessage && isTextInFilters(includeFilters, span.statusMessage)) || + (span.instrumentationLibraryName && isTextInFilters(includeFilters, span.instrumentationLibraryName)) || + (span.instrumentationLibraryVersion && isTextInFilters(includeFilters, span.instrumentationLibraryVersion)) || + (span.traceState && isTextInFilters(includeFilters, span.traceState)) || (span.logs !== null && span.logs.some((log) => isTextInKeyValues(log.fields))) || isTextInKeyValues(span.process.tags) || includeFilters.some((filter) => filter === span.spanID); diff --git a/public/app/plugins/datasource/tempo/resultTransformer.ts b/public/app/plugins/datasource/tempo/resultTransformer.ts index 17be5d64cb9..cbf5a0c5b87 100644 --- a/public/app/plugins/datasource/tempo/resultTransformer.ts +++ b/public/app/plugins/datasource/tempo/resultTransformer.ts @@ -1,4 +1,4 @@ -import { SpanStatus, SpanStatusCode } from '@opentelemetry/api'; +import { SpanStatus } from '@opentelemetry/api'; import { collectorTypes } from '@opentelemetry/exporter-collector'; import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions'; @@ -161,51 +161,25 @@ function resourceToProcess(resource: collectorTypes.opentelemetryProto.resource. return { serviceName, serviceTags }; } -function getSpanTags( - span: collectorTypes.opentelemetryProto.trace.v1.Span, - instrumentationLibrary?: collectorTypes.opentelemetryProto.common.v1.InstrumentationLibrary -): TraceKeyValuePair[] { +function getSpanTags(span: collectorTypes.opentelemetryProto.trace.v1.Span): 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 }); - } - } + return spanTags; +} - if (span.kind !== undefined) { +function getSpanKind(span: collectorTypes.opentelemetryProto.trace.v1.Span) { + let kind = undefined; + if (span.kind) { const split = span.kind.toString().toLowerCase().split('_'); - spanTags.push({ - key: 'span.kind', - value: split.length ? split[split.length - 1] : span.kind.toString(), - }); + kind = split.length ? split[split.length - 1] : span.kind.toString(); } - - return spanTags; + return kind; } function getReferences(span: collectorTypes.opentelemetryProto.trace.v1.Span) { @@ -254,6 +228,12 @@ export function transformFromOTLP( { name: 'parentSpanID', type: FieldType.string }, { name: 'operationName', type: FieldType.string }, { name: 'serviceName', type: FieldType.string }, + { name: 'kind', type: FieldType.string }, + { name: 'statusCode', type: FieldType.number }, + { name: 'statusMessage', type: FieldType.string }, + { name: 'instrumentationLibraryName', type: FieldType.string }, + { name: 'instrumentationLibraryVersion', type: FieldType.string }, + { name: 'traceState', type: FieldType.string }, { name: 'serviceTags', type: FieldType.other }, { name: 'startTime', type: FieldType.number }, { name: 'duration', type: FieldType.number }, @@ -279,10 +259,16 @@ export function transformFromOTLP( parentSpanID: span.parentSpanId || '', operationName: span.name || '', serviceName, + kind: getSpanKind(span), + statusCode: span.status?.code, + statusMessage: span.status?.message, + instrumentationLibraryName: librarySpan.instrumentationLibrary?.name, + instrumentationLibraryVersion: librarySpan.instrumentationLibrary?.version, + traceState: span.traceState, serviceTags, startTime: span.startTimeUnixNano! / 1000000, duration: (span.endTimeUnixNano! - span.startTimeUnixNano!) / 1000000, - tags: getSpanTags(span, librarySpan.instrumentationLibrary), + tags: getSpanTags(span), logs: getLogs(span), references: getReferences(span), } as TraceSpanRow); @@ -343,11 +329,10 @@ export function transformToOTLP(data: MutableDataFrame): { // Populate instrumentation library if it exists if (!result.batches[batchIndex].instrumentationLibrarySpans[0].instrumentationLibrary) { - let libraryName = span.tags.find((t: TraceKeyValuePair) => t.key === 'otel.library.name')?.value; - if (libraryName) { + if (span.instrumentationLibraryName) { result.batches[batchIndex].instrumentationLibrarySpans[0].instrumentationLibrary = { - name: libraryName, - version: span.tags.find((t: TraceKeyValuePair) => t.key === 'otel.library.version')?.value, + name: span.instrumentationLibraryName, + version: span.instrumentationLibraryVersion ? span.instrumentationLibraryVersion : '', }; } } @@ -356,16 +341,16 @@ export function transformToOTLP(data: MutableDataFrame): { traceId: span.traceID.padStart(32, '0'), spanId: span.spanID, parentSpanId: span.parentSpanID || '', - traceState: '', + traceState: span.traceState || '', name: span.operationName, - kind: getOTLPSpanKind(span.tags) as any, + kind: getOTLPSpanKind(span.kind) as any, startTimeUnixNano: span.startTime * 1000000, endTimeUnixNano: (span.startTime + span.duration) * 1000000, - attributes: tagsToAttributes(span.tags), + attributes: span.tags ? tagsToAttributes(span.tags) : [], droppedAttributesCount: 0, droppedEventsCount: 0, droppedLinksCount: 0, - status: getOTLPStatus(span.tags), + status: getOTLPStatus(span), events: getOTLPEvents(span.logs), links: getOTLPReferences(span.references), }); @@ -374,24 +359,27 @@ export function transformToOTLP(data: MutableDataFrame): { return result; } -function getOTLPSpanKind(tags: TraceKeyValuePair[]): string | undefined { +function getOTLPSpanKind(kind: string): string | undefined { let spanKind = undefined; - const spanKindTagValue = tags.find((t) => t.key === 'span.kind')?.value; - switch (spanKindTagValue) { - case 'server': - spanKind = 'SPAN_KIND_SERVER'; - break; - case 'client': - spanKind = 'SPAN_KIND_CLIENT'; - break; - case 'producer': - spanKind = 'SPAN_KIND_PRODUCER'; - break; - case 'consumer': - spanKind = 'SPAN_KIND_CONSUMER'; - break; + if (kind) { + switch (kind) { + case 'server': + spanKind = 'SPAN_KIND_SERVER'; + break; + case 'client': + spanKind = 'SPAN_KIND_CLIENT'; + break; + case 'producer': + spanKind = 'SPAN_KIND_PRODUCER'; + break; + case 'consumer': + spanKind = 'SPAN_KIND_CONSUMER'; + break; + case 'internal': + spanKind = 'SPAN_KIND_INTERNAL'; + break; + } } - return spanKind; } @@ -399,21 +387,10 @@ function getOTLPSpanKind(tags: TraceKeyValuePair[]): string | undefined { * Converts key-value tags to OTLP attributes and removes tags added by Grafana */ function tagsToAttributes(tags: TraceKeyValuePair[]): collectorTypes.opentelemetryProto.common.v1.KeyValue[] { - return tags - .filter( - (t) => - ![ - 'span.kind', - 'otel.library.name', - 'otel.libary.version', - 'otel.status_description', - 'otel.status_code', - ].includes(t.key) - ) - .reduce( - (attributes, tag) => [...attributes, { key: tag.key, value: toAttributeValue(tag) }], - [] - ); + return tags.reduce( + (attributes, tag) => [...attributes, { key: tag.key, value: toAttributeValue(tag) }], + [] + ); } /** @@ -443,16 +420,14 @@ function toAttributeValue(tag: TraceKeyValuePair): collectorTypes.opentelemetryP return { stringValue: tag.value }; } -function getOTLPStatus(tags: TraceKeyValuePair[]): SpanStatus | undefined { +function getOTLPStatus(span: TraceSpanRow): SpanStatus | undefined { let status = undefined; - const statusCodeTag = tags.find((t) => t.key === 'otel.status_code'); - if (statusCodeTag) { + if (span.statusCode !== undefined) { status = { - code: statusCodeTag.value, - message: tags.find((t) => t.key === 'otel_status_description')?.value, + code: span.statusCode, + message: span.statusMessage ? span.statusMessage : '', }; } - return status; } diff --git a/public/app/plugins/datasource/tempo/testResponse.ts b/public/app/plugins/datasource/tempo/testResponse.ts index 5d91c5ceca7..ecee2d745c4 100644 --- a/public/app/plugins/datasource/tempo/testResponse.ts +++ b/public/app/plugins/datasource/tempo/testResponse.ts @@ -1835,6 +1835,42 @@ export const otlpDataFrameFromResponse = new MutableDataFrame({ config: {}, values: ['db'], }, + { + name: 'kind', + type: 'string', + config: {}, + values: ['client'], + }, + { + name: 'statusCode', + type: 'number', + config: {}, + values: [2], + }, + { + name: 'statusMessage', + type: 'string', + config: {}, + values: ['message'], + }, + { + name: 'instrumentationLibraryName', + type: 'string', + config: {}, + values: ['libraryName'], + }, + { + name: 'instrumentationLibraryVersion', + type: 'string', + config: {}, + values: ['libraryVersion'], + }, + { + name: 'traceState', + type: 'string', + config: {}, + values: ['traceState'], + }, { name: 'serviceTags', type: 'other', @@ -1930,10 +1966,6 @@ export const otlpDataFrameFromResponse = new MutableDataFrame({ key: 'component', value: 'net/http', }, - { - key: 'span.kind', - value: 'client', - }, ], ], }, @@ -1994,6 +2026,60 @@ export const otlpDataFrameToResponse = new MutableDataFrame({ displayName: 'serviceName', }, }, + { + name: 'kind', + type: 'string', + config: {}, + values: ['client'], + state: { + displayName: 'kind', + }, + }, + { + name: 'statusCode', + type: 'number', + config: {}, + values: [2], + state: { + displayName: 'statusCode', + }, + }, + { + name: 'statusMessage', + type: 'string', + config: {}, + values: ['message'], + state: { + displayName: 'statusMessage', + }, + }, + { + name: 'instrumentationLibraryName', + type: 'string', + config: {}, + values: ['libraryName'], + state: { + displayName: 'instrumentationLibraryName', + }, + }, + { + name: 'instrumentationLibraryVersion', + type: 'string', + config: {}, + values: ['libraryVersion'], + state: { + displayName: 'instrumentationLibraryVersion', + }, + }, + { + name: 'traceState', + type: 'string', + config: {}, + values: ['traceState'], + state: { + displayName: 'traceState', + }, + }, { name: 'serviceTags', type: 'other', @@ -2079,10 +2165,6 @@ export const otlpDataFrameToResponse = new MutableDataFrame({ key: 'component', value: 'net/http', }, - { - key: 'span.kind', - value: 'client', - }, ], ], state: { @@ -2135,6 +2217,10 @@ export const otlpResponse = { }, instrumentationLibrarySpans: [ { + instrumentationLibrary: { + name: 'libraryName', + version: 'libraryVersion', + }, spans: [ { traceId: '000000000000000060ba2abb44f13eae', @@ -2142,6 +2228,11 @@ export const otlpResponse = { parentSpanId: '398f0f21a3db99ae', name: 'HTTP GET - root', kind: 'SPAN_KIND_CLIENT', + status: { + code: 2, + message: 'message', + }, + traceState: 'traceState', startTimeUnixNano: 1627471657255809000, endTimeUnixNano: 1627471657256268000, attributes: [