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

@@ -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' } },
],
},
],
},
],
},
],
};