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:
@@ -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' } },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user