Explore: Download traces as JSON in Explore Inspector (#38614)

* Transform dataframe to jaeger format

* Transform dataframe to Zipkin format

* Add endpoint type and shared to Zipkin

* Transform dataframe to OTLP format

* Add data tab tests and note in inspector docs

* Remove comments and logs

* Resolve typescript strict errors

* Update docs/sources/explore/explore-inspector.md

Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com>

* Update docs

* Improve OTLP conversion to include service info and additional tags

Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com>
This commit is contained in:
Connor Lindsey 2021-09-08 07:04:27 -06:00 committed by GitHub
parent 3bb2ee9de6
commit 6a39ac7407
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 702 additions and 11 deletions

View File

@ -57,6 +57,14 @@ Grafana generates a TXT file in your default browser download location. You can
1. Inspect the log query results as described above.
1. Click **Download logs**.
### Download trace results
Based on the data source type, Grafana generates a JSON file for the trace results in one of the supported formats: Jaeger, Zipkin, or OTLP formats.
1. Open the inspector.
1. Inspect the trace query results [as described above](#inspect-raw-query-results).
1. Click **Download traces**.
### Inspect query performance
The Stats tab displays statistics that tell you how long your query takes, how many queries you send, and the number of rows returned. This information can help you troubleshoot your queries, especially if any of the numbers are unexpectedly high or low.

View File

@ -83,5 +83,63 @@ describe('InspectDataTab', () => {
render(<InspectDataTab {...createProps()} />);
expect(screen.queryByText(/Download logs/i)).not.toBeInTheDocument();
});
it('should show download traces button if traces data', () => {
const dataWithtraces = ([
{
name: 'Data frame with traces',
fields: [
{ name: 'traceID', values: ['3fa414edcef6ad90', '3fa414edcef6ad90'] },
{ name: 'spanID', values: ['3fa414edcef6ad90', '0f5c1808567e4403'] },
{ name: 'parentSpanID', values: [undefined, '3fa414edcef6ad90'] },
{ name: 'operationName', values: ['HTTP GET - api_traces_traceid', '/tempopb.Querier/FindTraceByID'] },
{ name: 'serviceName', values: ['tempo-querier', 'tempo-querier'] },
{
name: 'serviceTags',
values: [
[
{ key: 'cluster', type: 'string', value: 'ops-tools1' },
{ key: 'container', type: 'string', value: 'tempo-query' },
],
[
{ key: 'cluster', type: 'string', value: 'ops-tools1' },
{ key: 'container', type: 'string', value: 'tempo-query' },
],
],
},
{ name: 'startTime', values: [1605873894680.409, 1605873894680.587] },
{ name: 'duration', values: [1049.141, 1.847] },
{ name: 'logs', values: [[], []] },
{
name: 'tags',
values: [
[
{ key: 'sampler.type', type: 'string', value: 'probabilistic' },
{ key: 'sampler.param', type: 'float64', value: 1 },
],
[
{ key: 'component', type: 'string', value: 'gRPC' },
{ key: 'span.kind', type: 'string', value: 'client' },
],
],
},
{ name: 'warnings', values: [undefined, undefined] },
{ name: 'stackTraces', values: [undefined, undefined] },
],
length: 2,
meta: {
preferredVisualisationType: 'trace',
custom: {
traceFormat: 'jaeger',
},
},
},
] as unknown) as DataFrame[];
render(<InspectDataTab {...createProps({ data: dataWithtraces })} />);
expect(screen.getByText(/Download traces/i)).toBeInTheDocument();
});
it('should not show download traces button if no traces data', () => {
render(<InspectDataTab {...createProps()} />);
expect(screen.queryByText(/Download traces/i)).not.toBeInTheDocument();
});
});
});

View File

@ -8,6 +8,7 @@ import {
DataTransformerID,
dateTimeFormat,
dateTimeFormatISO,
MutableDataFrame,
SelectableValue,
toCSV,
transformDataFrame,
@ -22,6 +23,9 @@ import { css } from '@emotion/css';
import { GetDataOptions } from 'app/features/query/state/PanelQueryRunner';
import { PanelModel } from 'app/features/dashboard/state';
import { dataFrameToLogsModel } from 'app/core/logs_model';
import { transformToJaeger } from 'app/plugins/datasource/jaeger/responseTransform';
import { transformToZipkin } from 'app/plugins/datasource/zipkin/utils/transforms';
import { transformToOTLP } from 'app/plugins/datasource/tempo/resultTransformer';
interface Props {
isLoading: boolean;
@ -122,6 +126,48 @@ export class InspectDataTab extends PureComponent<Props, State> {
saveAs(blob, fileName);
};
exportTracesAsJson = () => {
const { data, panel } = this.props;
if (!data) {
return;
}
for (const df of data) {
// Only export traces
if (df.meta?.preferredVisualisationType !== 'trace') {
continue;
}
switch (df.meta?.custom?.traceFormat) {
case 'jaeger': {
let res = transformToJaeger(new MutableDataFrame(df));
this.saveTraceJson(res, panel);
break;
}
case 'zipkin': {
let res = transformToZipkin(new MutableDataFrame(df));
this.saveTraceJson(res, panel);
break;
}
case 'otlp':
default: {
let res = transformToOTLP(new MutableDataFrame(df));
this.saveTraceJson(res, panel);
break;
}
}
}
};
saveTraceJson = (json: any, panel?: PanelModel) => {
const blob = new Blob([JSON.stringify(json)], {
type: 'application/json',
});
const displayTitle = panel ? panel.getDisplayTitle() : 'Explore';
const fileName = `${displayTitle}-traces-${dateTimeFormat(new Date())}.json`;
saveAs(blob, fileName);
};
onDataFrameChange = (item: SelectableValue<DataTransformerID | number>) => {
this.setState({
transformId:
@ -180,6 +226,7 @@ export class InspectDataTab extends PureComponent<Props, State> {
const index = !dataFrames[dataFrameIndex] ? 0 : dataFrameIndex;
const dataFrame = dataFrames[index];
const hasLogs = dataFrames.some((df) => df?.meta?.preferredVisualisationType === 'logs');
const hasTraces = dataFrames.some((df) => df?.meta?.preferredVisualisationType === 'trace');
return (
<div className={styles.dataTabContent} aria-label={selectors.components.PanelInspector.Data.content}>
@ -218,6 +265,18 @@ export class InspectDataTab extends PureComponent<Props, State> {
Download logs
</Button>
)}
{hasTraces && (
<Button
variant="primary"
onClick={this.exportTracesAsJson}
className={css`
margin-bottom: 10px;
margin-left: 10px;
`}
>
Download traces
</Button>
)}
</div>
<Container grow={1}>
<AutoSizer>

View File

@ -171,5 +171,8 @@ const emptyTraceDataFrame = new MutableDataFrame({
],
meta: {
preferredVisualisationType: 'trace',
custom: {
traceFormat: 'jaeger',
},
},
});

View File

@ -1,9 +1,16 @@
import { createTraceFrame } from './responseTransform';
import { createTraceFrame, transformToJaeger } from './responseTransform';
import { testResponse, testResponseDataFrameFields } from './testResponse';
import { MutableDataFrame } from '@grafana/data';
describe('createTraceFrame', () => {
it('creates data frame from jaeger response', () => {
const dataFrame = createTraceFrame(testResponse);
expect(dataFrame.fields).toMatchObject(testResponseDataFrameFields);
});
it('transforms to jaeger format from data frame', () => {
const dataFrame = createTraceFrame(testResponse);
const response = transformToJaeger(new MutableDataFrame(dataFrame));
expect(response).toMatchObject({ data: [testResponse] });
});
});

View File

@ -1,6 +1,13 @@
import { DataFrame, DataSourceInstanceSettings, FieldType, MutableDataFrame, TraceSpanRow } from '@grafana/data';
import {
DataFrame,
DataSourceInstanceSettings,
FieldType,
MutableDataFrame,
TraceLog,
TraceSpanRow,
} from '@grafana/data';
import { transformTraceData } from '@jaegertracing/jaeger-ui-components';
import { Span, TraceProcess, TraceResponse } from './types';
import { JaegerResponse, Span, TraceProcess, TraceResponse } from './types';
export function createTraceFrame(data: TraceResponse): DataFrame {
const spans = data.spans.map((s) => toSpanRow(s, data.processes));
@ -22,6 +29,9 @@ export function createTraceFrame(data: TraceResponse): DataFrame {
],
meta: {
preferredVisualisationType: 'trace',
custom: {
traceFormat: 'jaeger',
},
},
});
@ -107,3 +117,62 @@ function transformToTraceData(data: TraceResponse) {
traceName: traceData.traceName,
};
}
export function transformToJaeger(data: MutableDataFrame): JaegerResponse {
let traceResponse: TraceResponse = {
traceID: '',
spans: [],
processes: {},
warnings: null,
};
let processes: string[] = [];
for (let i = 0; i < data.length; i++) {
const span = data.get(i);
// Set traceID
if (!traceResponse.traceID) {
traceResponse.traceID = span.traceID;
}
// Create process if doesn't exist
if (!processes.find((p) => p === span.serviceName)) {
processes.push(span.serviceName);
traceResponse.processes[`p${processes.length}`] = {
serviceName: span.serviceName,
tags: span.serviceTags,
};
}
// Create span
traceResponse.spans.push({
traceID: span.traceID,
spanID: span.spanID,
duration: span.duration * 1000,
references: span.parentSpanID
? [
{
refType: 'CHILD_OF',
spanID: span.parentSpanID,
traceID: span.traceID,
},
]
: [],
flags: 0,
logs: span.logs.map((l: TraceLog) => ({
...l,
timestamp: l.timestamp * 1000,
})),
operationName: span.operationName,
processID:
Object.keys(traceResponse.processes).find(
(key) => traceResponse.processes[key].serviceName === span.serviceName
) || '',
startTime: span.startTime * 1000,
tags: span.tags,
warnings: span.warnings ? span.warnings : null,
});
}
return { data: [traceResponse], total: 0, limit: 0, offset: 0, errors: null };
}

View File

@ -64,3 +64,11 @@ export type JaegerQuery = {
} & DataQuery;
export type JaegerQueryType = 'search' | 'upload';
export type JaegerResponse = {
data: TraceResponse[];
total: number;
limit: number;
offset: number;
errors?: string[] | null;
};

View File

@ -84,7 +84,10 @@ export class TempoDatasource extends DataSourceWithBackend<TempoQuery, TempoJson
.map((field) => field.matcherRegex) || [];
if (!traceLinkMatcher || traceLinkMatcher.length === 0) {
return throwError(
'No Loki datasource configured for search. Set up Derived Fields for traces in a Loki datasource settings and link it to this Tempo datasource.'
() =>
new Error(
'No Loki datasource configured for search. Set up Derived Fields for traces in a Loki datasource settings and link it to this Tempo datasource.'
)
);
} else {
return (linkedDatasource.query(linkedRequest) as Observable<DataQueryResponse>).pipe(

View File

@ -1,5 +1,6 @@
import { FieldType, MutableDataFrame } from '@grafana/data';
import { createTableFrame } from './resultTransformer';
import { createTableFrame, transformToOTLP } from './resultTransformer';
import { otlpDataFrame, otlpResponse } from './testResponse';
describe('transformTraceList()', () => {
const lokiDataFrame = new MutableDataFrame({
@ -35,3 +36,10 @@ describe('transformTraceList()', () => {
expect(frame.fields[1].values.get(1)).toBe('as');
});
});
describe('transformToOTLP()', () => {
test('transforms dataframe to OTLP format', () => {
const otlp = transformToOTLP(otlpDataFrame);
expect(otlp).toMatchObject(otlpResponse);
});
});

View File

@ -10,7 +10,7 @@ import {
TraceLog,
TraceSpanRow,
} from '@grafana/data';
import { SpanKind, SpanStatusCode } from '@opentelemetry/api';
import { SpanKind, SpanStatus, SpanStatusCode } from '@opentelemetry/api';
import { collectorTypes } from '@opentelemetry/exporter-collector';
import { ResourceAttributes } from '@opentelemetry/semantic-conventions';
import { createGraphFrames } from './graphTransform';
@ -119,6 +119,12 @@ function transformBase64IDToHexString(base64: string) {
return id.length > 16 ? id.slice(16) : id;
}
function transformHexStringToBase64ID(hex: string) {
const buffer = Buffer.from(hex, 'hex');
const id = buffer.toString('base64');
return id;
}
function getAttributeValue(value: collectorTypes.opentelemetryProto.common.v1.AnyValue): any {
if (value.stringValue) {
return value.stringValue;
@ -248,6 +254,9 @@ export function transformFromOTLP(
],
meta: {
preferredVisualisationType: 'trace',
custom: {
traceFormat: 'otlp',
},
},
});
try {
@ -277,6 +286,183 @@ export function transformFromOTLP(
return { data: [frame, ...createGraphFrames(frame)] };
}
/**
* Transforms trace dataframes to the OpenTelemetry format
*/
export function transformToOTLP(
data: MutableDataFrame
): { batches: collectorTypes.opentelemetryProto.trace.v1.ResourceSpans[] } {
let result: { batches: collectorTypes.opentelemetryProto.trace.v1.ResourceSpans[] } = {
batches: [],
};
// Lookup object to see which batch contains spans for which services
let services: { [key: string]: number } = {};
for (let i = 0; i < data.length; i++) {
const span = data.get(i);
// Group spans based on service
if (!services[span.serviceName]) {
services[span.serviceName] = result.batches.length;
result.batches.push({
resource: {
attributes: [],
droppedAttributesCount: 0,
},
instrumentationLibrarySpans: [
{
spans: [],
},
],
});
}
let batchIndex = services[span.serviceName];
// Populate resource attributes from service tags
if (result.batches[batchIndex].resource!.attributes.length === 0) {
result.batches[batchIndex].resource!.attributes = tagsToAttributes(span.serviceTags);
}
// 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) {
result.batches[batchIndex].instrumentationLibrarySpans[0].instrumentationLibrary = {
name: libraryName,
version: span.tags.find((t: TraceKeyValuePair) => t.key === 'otel.library.version')?.value,
};
}
}
result.batches[batchIndex].instrumentationLibrarySpans[0].spans.push({
traceId: transformHexStringToBase64ID(span.traceID.padStart(32, '0')),
spanId: transformHexStringToBase64ID(span.spanID),
traceState: '',
parentSpanId: transformHexStringToBase64ID(span.parentSpanID || ''),
name: span.operationName,
kind: getOTLPSpanKind(span.tags) as any,
startTimeUnixNano: span.startTime * 1000000,
endTimeUnixNano: (span.startTime + span.duration) * 1000000,
attributes: tagsToAttributes(span.tags),
droppedAttributesCount: 0,
droppedEventsCount: 0,
droppedLinksCount: 0,
status: getOTLPStatus(span.tags),
events: getOTLPEvents(span.logs),
});
}
return result;
}
function getOTLPSpanKind(tags: TraceKeyValuePair[]): 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;
}
return spanKind;
}
/**
* 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) }],
[]
);
}
/**
* Returns the correct OTLP AnyValue based on the value of the tag value
*/
function toAttributeValue(tag: TraceKeyValuePair): collectorTypes.opentelemetryProto.common.v1.AnyValue {
if (typeof tag.value === 'string') {
return { stringValue: tag.value };
} else if (typeof tag.value === 'boolean') {
return { boolValue: tag.value };
} else if (typeof tag.value === 'number') {
if (tag.value % 1 === 0) {
return { intValue: tag.value };
} else {
return { doubleValue: tag.value };
}
} else if (typeof tag.value === 'object') {
if (Array.isArray(tag.value)) {
const values: collectorTypes.opentelemetryProto.common.v1.AnyValue[] = [];
for (const val of tag.value) {
values.push(toAttributeValue(val));
}
return { arrayValue: { values } };
}
}
return { stringValue: tag.value };
}
function getOTLPStatus(tags: TraceKeyValuePair[]): SpanStatus | undefined {
let status = undefined;
const statusCodeTag = tags.find((t) => t.key === 'otel.status_code');
if (statusCodeTag) {
status = {
code: statusCodeTag.value,
message: tags.find((t) => t.key === 'otel_status_description')?.value,
};
}
return status;
}
function getOTLPEvents(logs: TraceLog[]): collectorTypes.opentelemetryProto.trace.v1.Span.Event[] | undefined {
if (!logs.length) {
return undefined;
}
let events: collectorTypes.opentelemetryProto.trace.v1.Span.Event[] = [];
for (const log of logs) {
let event: collectorTypes.opentelemetryProto.trace.v1.Span.Event = {
timeUnixNano: log.timestamp * 1000000,
attributes: [],
droppedAttributesCount: 0,
name: '',
};
for (const field of log.fields) {
event.attributes!.push({
key: field.key,
value: toAttributeValue(field),
});
}
events.push(event);
}
return events;
}
export function transformTrace(response: DataQueryResponse): DataQueryResponse {
// We need to parse some of the fields which contain stringified json.
// Seems like we can't just map the values as the frame we got from backend has some default processing
@ -400,6 +586,9 @@ const emptyDataQueryResponse = {
],
meta: {
preferredVisualisationType: 'trace',
custom: {
traceFormat: 'otlp',
},
},
}),
],

View File

@ -1848,3 +1848,194 @@ export const bigResponse = new MutableDataFrame({
},
],
});
export const otlpDataFrame = new MutableDataFrame({
meta: {
preferredVisualisationType: 'trace',
custom: {
traceFormat: 'otlp',
},
},
fields: [
{
name: 'traceID',
type: 'string',
config: {},
values: ['60ba2abb44f13eae'],
state: {
displayName: 'traceID',
},
},
{
name: 'spanID',
type: 'string',
config: {},
values: ['726b5e30102fc0d0'],
state: {
displayName: 'spanID',
},
},
{
name: 'parentSpanID',
type: 'string',
config: {},
values: ['398f0f21a3db99ae'],
state: {
displayName: 'parentSpanID',
},
},
{
name: 'operationName',
type: 'string',
config: {},
values: ['HTTP GET - root'],
state: {
displayName: 'operationName',
},
},
{
name: 'serviceName',
type: 'string',
config: {},
values: ['db'],
state: {
displayName: 'serviceName',
},
},
{
name: 'serviceTags',
type: 'other',
config: {},
values: [
[
{
key: 'service.name',
value: 'db',
},
{
key: 'job',
value: 'tns/db',
},
{
key: 'opencensus.exporterversion',
value: 'Jaeger-Go-2.22.1',
},
{
key: 'host.name',
value: '63d16772b4a2',
},
{
key: 'ip',
value: '0.0.0.0',
},
{
key: 'client-uuid',
value: '39fb01637a579639',
},
],
],
state: {
displayName: 'serviceTags',
},
},
{
name: 'startTime',
type: 'number',
config: {},
values: [1627471657255.809],
state: {
displayName: 'startTime',
},
},
{
name: 'duration',
type: 'number',
config: {},
values: [0.459008],
state: {
displayName: 'duration',
},
},
{
name: 'logs',
type: 'other',
config: {},
values: [[]],
state: {
displayName: 'logs',
},
},
{
name: 'tags',
type: 'other',
config: {},
values: [
[
{
key: 'http.status_code',
value: 200,
},
{
key: 'http.method',
value: 'GET',
},
{
key: 'http.url',
value: '/',
},
{
key: 'component',
value: 'net/http',
},
{
key: 'span.kind',
value: 'client',
},
],
],
state: {
displayName: 'tags',
},
},
],
first: ['60ba2abb44f13eae'],
length: 1,
} as any);
export const otlpResponse = {
batches: [
{
resource: {
attributes: [
{ key: 'service.name', value: { stringValue: 'db' } },
{ key: 'job', value: { stringValue: 'tns/db' } },
{ key: 'opencensus.exporterversion', value: { stringValue: 'Jaeger-Go-2.22.1' } },
{ key: 'host.name', value: { stringValue: '63d16772b4a2' } },
{ key: 'ip', value: { stringValue: '0.0.0.0' } },
{ key: 'client-uuid', value: { stringValue: '39fb01637a579639' } },
],
},
instrumentationLibrarySpans: [
{
spans: [
{
traceId: 'AAAAAAAAAABguiq7RPE+rg==',
spanId: 'cmteMBAvwNA=',
parentSpanId: 'OY8PIaPbma4=',
name: 'HTTP GET - root',
kind: 'SPAN_KIND_CLIENT',
startTimeUnixNano: 1627471657255809000,
endTimeUnixNano: 1627471657256268000,
attributes: [
{ key: 'http.status_code', value: { intValue: 200 } },
{ key: 'http.method', value: { stringValue: 'GET' } },
{ key: 'http.url', value: { stringValue: '/' } },
{ key: 'component', value: { stringValue: 'net/http' } },
],
},
],
},
],
},
],
};

View File

@ -93,6 +93,9 @@ const emptyDataQueryResponse = {
],
meta: {
preferredVisualisationType: 'trace',
custom: {
traceFormat: 'zipkin',
},
},
}),
],

View File

@ -12,6 +12,7 @@ export type ZipkinSpan = {
annotations?: ZipkinAnnotation[];
tags?: { [key: string]: string };
kind?: 'CLIENT' | 'SERVER' | 'PRODUCER' | 'CONSUMER';
shared?: boolean;
};
export type ZipkinEndpoint = {

View File

@ -29,7 +29,6 @@ export const zipkinResponse: ZipkinSpan[] = [
},
kind: 'CLIENT',
},
{
traceId: 'trace_id',
parentId: 'span 1 id',
@ -71,9 +70,16 @@ export const traceFrameFields = [
[
{ key: 'ipv4', value: '1.0.0.1' },
{ key: 'port', value: 42 },
{ key: 'endpointType', value: 'local' },
],
[
{ key: 'ipv4', value: '1.0.0.1' },
{ key: 'endpointType', value: 'local' },
],
[
{ key: 'ipv6', value: '127.0.0.1' },
{ key: 'endpointType', value: 'remote' },
],
[{ key: 'ipv4', value: '1.0.0.1' }],
[{ key: 'ipv6', value: '127.0.0.1' }],
],
},
{ name: 'startTime', values: [0.001, 0.004, 0.006] },

View File

@ -1,5 +1,6 @@
import { transformResponse } from './transforms';
import { transformResponse, transformToZipkin } from './transforms';
import { traceFrameFields, zipkinResponse } from './testData';
import { MutableDataFrame } from '@grafana/data';
describe('transformResponse', () => {
it('transforms response', () => {
@ -7,4 +8,9 @@ describe('transformResponse', () => {
expect(dataFrame.fields).toMatchObject(traceFrameFields);
});
it('converts dataframe to ZipkinSpan[]', () => {
const dataFrame = transformResponse(zipkinResponse);
const response = transformToZipkin(new MutableDataFrame(dataFrame));
expect(response).toMatchObject(zipkinResponse);
});
});

View File

@ -1,5 +1,5 @@
import { identity } from 'lodash';
import { ZipkinAnnotation, ZipkinSpan } from '../types';
import { ZipkinAnnotation, ZipkinEndpoint, ZipkinSpan } from '../types';
import { DataFrame, FieldType, MutableDataFrame, TraceKeyValuePair, TraceLog, TraceSpanRow } from '@grafana/data';
/**
@ -22,6 +22,9 @@ export function transformResponse(zSpans: ZipkinSpan[]): DataFrame {
],
meta: {
preferredVisualisationType: 'trace',
custom: {
traceFormat: 'zipkin',
},
},
});
@ -72,6 +75,16 @@ function transformSpan(span: ZipkinSpan): TraceSpanRow {
];
}
if (span.shared) {
row.tags = [
{
key: 'shared',
value: span.shared,
},
...(row.tags ?? []),
];
}
return row;
}
@ -100,6 +113,7 @@ function serviceTags(span: ZipkinSpan): TraceKeyValuePair[] {
valueToTag('ipv4', endpoint.ipv4),
valueToTag('ipv6', endpoint.ipv6),
valueToTag('port', endpoint.port),
valueToTag('endpointType', span.localEndpoint ? 'local' : 'remote'),
].filter(identity) as TraceKeyValuePair[];
}
@ -112,3 +126,61 @@ function valueToTag<T>(key: string, value: T): TraceKeyValuePair<T> | undefined
value,
};
}
/**
* Transforms data frame to Zipkin response
*/
export const transformToZipkin = (data: MutableDataFrame): ZipkinSpan[] => {
let response: ZipkinSpan[] = [];
for (let i = 0; i < data.length; i++) {
const span = data.get(i);
response.push({
traceId: span.traceID,
parentId: span.parentSpanID,
name: span.operationName,
id: span.spanID,
timestamp: span.startTime * 1000,
duration: span.duration * 1000,
...getEndpoint(span),
annotations: span.logs.length
? span.logs.map((l: TraceLog) => ({ timestamp: l.timestamp, value: l.fields[0].value }))
: undefined,
tags: span.tags.length
? span.tags
.filter((t: TraceKeyValuePair) => t.key !== 'kind' && t.key !== 'endpointType' && t.key !== 'shared')
.reduce((tags: { [key: string]: string }, t: TraceKeyValuePair) => {
if (t.key === 'error') {
return {
...tags,
[t.key]: span.tags.find((t: TraceKeyValuePair) => t.key === 'errorValue').value || '',
};
}
return { ...tags, [t.key]: t.value };
}, {})
: undefined,
kind: span.tags.find((t: TraceKeyValuePair) => t.key === 'kind')?.value,
shared: span.tags.find((t: TraceKeyValuePair) => t.key === 'shared')?.value,
});
}
return response;
};
// Returns remote or local endpoint object
const getEndpoint = (span: any): { [key: string]: ZipkinEndpoint } | undefined => {
const key =
span.serviceTags.find((t: TraceKeyValuePair) => t.key === 'endpointType')?.value === 'local'
? 'localEndpoint'
: 'remoteEndpoint';
return span.serviceName !== 'unknown'
? {
[key]: {
serviceName: span.serviceName,
ipv4: span.serviceTags.find((t: TraceKeyValuePair) => t.key === 'ipv4')?.value,
ipv6: span.serviceTags.find((t: TraceKeyValuePair) => t.key === 'ipv6')?.value,
port: span.serviceTags.find((t: TraceKeyValuePair) => t.key === 'port')?.value,
},
}
: undefined;
};