mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -171,5 +171,8 @@ const emptyTraceDataFrame = new MutableDataFrame({
|
||||
],
|
||||
meta: {
|
||||
preferredVisualisationType: 'trace',
|
||||
custom: {
|
||||
traceFormat: 'jaeger',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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] });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
|
||||
@@ -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' } },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -93,6 +93,9 @@ const emptyDataQueryResponse = {
|
||||
],
|
||||
meta: {
|
||||
preferredVisualisationType: 'trace',
|
||||
custom: {
|
||||
traceFormat: 'zipkin',
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
|
||||
@@ -12,6 +12,7 @@ export type ZipkinSpan = {
|
||||
annotations?: ZipkinAnnotation[];
|
||||
tags?: { [key: string]: string };
|
||||
kind?: 'CLIENT' | 'SERVER' | 'PRODUCER' | 'CONSUMER';
|
||||
shared?: boolean;
|
||||
};
|
||||
|
||||
export type ZipkinEndpoint = {
|
||||
|
||||
@@ -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] },
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user