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
16 changed files with 702 additions and 11 deletions

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