Tempo: Represent OTLP Span Intrinsics correctly (#69394)

* Span intrinsics

* Update intrinsics and add to span details

* Remove intrinsics section

* Update tests

* Update status code text

* Self review

* Move previously intrinsic values to span

* Remove few methods
pull/70503/head
Joey 2 years ago committed by GitHub
parent e9d42a6395
commit 00ec9fceb9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      .betterer.results
  2. 2
      docs/sources/datasources/jaeger/_index.md
  3. 2
      docs/sources/datasources/tempo/_index.md
  4. 2
      docs/sources/datasources/zipkin/_index.md
  5. 6
      packages/grafana-data/src/types/trace.ts
  6. 109
      pkg/tsdb/tempo/trace_transform.go
  7. 9
      pkg/tsdb/tempo/trace_transform_test.go
  8. 17
      public/app/features/explore/TraceView/components/TracePageHeader/SpanFilters/SpanFilters.test.tsx
  9. 56
      public/app/features/explore/TraceView/components/TracePageHeader/SpanFilters/SpanFilters.tsx
  10. 8
      public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanBarRow.tsx
  11. 21
      public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/index.test.tsx
  12. 48
      public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/index.tsx
  13. 110
      public/app/features/explore/TraceView/components/TraceTimelineViewer/utils.test.ts
  14. 28
      public/app/features/explore/TraceView/components/TraceTimelineViewer/utils.tsx
  15. 2
      public/app/features/explore/TraceView/components/common/LabeledList.tsx
  16. 6
      public/app/features/explore/TraceView/components/constants/span.ts
  17. 2
      public/app/features/explore/TraceView/components/model/transform-trace-data.test.ts
  18. 12
      public/app/features/explore/TraceView/components/model/transform-trace-data.tsx
  19. 6
      public/app/features/explore/TraceView/components/types/trace.ts
  20. 132
      public/app/features/explore/TraceView/components/utils/filter-spans.test.ts
  21. 48
      public/app/features/explore/TraceView/components/utils/filter-spans.tsx
  22. 137
      public/app/plugins/datasource/tempo/resultTransformer.ts
  23. 107
      public/app/plugins/datasource/tempo/testResponse.ts

@ -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"],

@ -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

@ -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

@ -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

@ -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[];

@ -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 {

@ -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",

@ -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');
});
});

@ -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);

@ -558,13 +558,13 @@ export class UnthemedSpanBarRow extends React.PureComponent<SpanBarRowProps> {
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})`;
}

@ -47,6 +47,14 @@ describe('<SpanDetail>', () => {
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('<SpanDetail>', () => {
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(<SpanDetail {...(props as unknown as SpanDetailProps)} />);
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', () => {

@ -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;

@ -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[];

@ -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';

@ -52,7 +52,7 @@ const getStyles = (divider: boolean) => (theme: GrafanaTheme2) => {
`,
LabeledListValue: css`
label: LabeledListValue;
margin-right: 0.55rem;
${!divider && `margin-right: 0.55rem;`}
`,
};
};

@ -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';

@ -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' },

@ -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<string, string> = new Map<string, string>();
const tags: TraceKeyValuePair[] = spanTags.reduce<TraceKeyValuePair[]>((uniqueTags, tag) => {
const dedupedTags: TraceKeyValuePair[] = tags.reduce<TraceKeyValuePair[]>((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);

@ -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[];

@ -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]));

@ -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);

@ -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<collectorTypes.opentelemetryProto.common.v1.KeyValue[]>(
(attributes, tag) => [...attributes, { key: tag.key, value: toAttributeValue(tag) }],
[]
);
return tags.reduce<collectorTypes.opentelemetryProto.common.v1.KeyValue[]>(
(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;
}

@ -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: [

Loading…
Cancel
Save