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:
parent
3bb2ee9de6
commit
6a39ac7407
@ -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.
|
||||
|
@ -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;
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user