grafana/public/app/plugins/datasource/tempo/resultTransformer.ts
2024-01-03 11:28:13 +01:00

1084 lines
30 KiB
TypeScript

import { SpanStatus } from '@opentelemetry/api';
import { collectorTypes } from '@opentelemetry/exporter-collector';
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';
import {
DataFrame,
DataQueryResponse,
DataSourceInstanceSettings,
FieldType,
MutableDataFrame,
TraceKeyValuePair,
TraceLog,
TraceSpanReference,
TraceSpanRow,
FieldDTO,
createDataFrame,
getDisplayProcessor,
createTheme,
DataFrameDTO,
toDataFrame,
DataLink,
DataSourceJsonData,
Field,
DataLinkConfigOrigin,
} from '@grafana/data';
import { config } from '@grafana/runtime';
import { TraceToProfilesData } from 'app/core/components/TraceToProfiles/TraceToProfilesSettings';
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
import { SearchTableType } from './dataquery.gen';
import { createGraphFrames } from './graphTransform';
import { Span, SpanAttributes, Spanset, TempoJsonData, TraceSearchMetadata } from './types';
export function createTableFrame(
logsFrame: DataFrame | DataFrameDTO,
datasourceUid: string,
datasourceName: string,
traceRegexs: string[]
): DataFrame {
const tableFrame = new MutableDataFrame({
fields: [
{
name: 'Time',
type: FieldType.time,
config: {
custom: {
width: 200,
},
},
values: [],
},
{
name: 'traceID',
type: FieldType.string,
config: {
displayNameFromDS: 'Trace ID',
custom: { width: 180 },
links: [
{
title: 'Click to open trace ${__value.raw}',
url: '',
internal: {
datasourceUid,
datasourceName,
query: {
query: '${__value.raw}',
},
},
},
],
},
values: [],
},
{
name: 'Message',
type: FieldType.string,
values: [],
},
],
meta: {
preferredVisualisationType: 'table',
},
});
if (!logsFrame || traceRegexs.length === 0) {
return tableFrame;
}
const timeField = logsFrame.fields.find((f) => f.type === FieldType.time);
// Going through all string fields to look for trace IDs
for (let field of logsFrame.fields) {
let hasMatch = false;
if (field.type === FieldType.string) {
const values = field.values!;
for (let i = 0; i < values.length; i++) {
const line = values[i];
if (line) {
for (let traceRegex of traceRegexs) {
const match = line.match(traceRegex);
if (match) {
const traceId = match[1];
const time = timeField ? timeField.values![i] : null;
tableFrame.fields[0].values.push(time);
tableFrame.fields[1].values.push(traceId);
tableFrame.fields[2].values.push(line);
hasMatch = true;
}
}
}
}
}
if (hasMatch) {
break;
}
}
return tableFrame;
}
export function transformTraceList(
response: DataQueryResponse,
datasourceId: string,
datasourceName: string,
traceRegexs: string[]
): DataQueryResponse {
response.data.forEach((data, index) => {
const frame = createTableFrame(data, datasourceId, datasourceName, traceRegexs);
response.data[index] = frame;
});
return response;
}
function getAttributeValue(value: collectorTypes.opentelemetryProto.common.v1.AnyValue): any {
if (value.stringValue) {
return value.stringValue;
}
if (value.boolValue !== undefined) {
return Boolean(value.boolValue);
}
if (value.intValue !== undefined) {
return Number.parseInt(value.intValue as any, 10);
}
if (value.doubleValue) {
return Number.parseFloat(value.doubleValue as any);
}
if (value.arrayValue) {
const arrayValue = [];
for (const arValue of value.arrayValue.values) {
arrayValue.push(getAttributeValue(arValue));
}
return arrayValue;
}
return '';
}
function resourceToProcess(resource: collectorTypes.opentelemetryProto.resource.v1.Resource | undefined) {
const serviceTags: TraceKeyValuePair[] = [];
let serviceName = 'OTLPResourceNoServiceName';
if (!resource) {
return { serviceName, serviceTags };
}
for (const attribute of resource.attributes) {
if (attribute.key === SemanticResourceAttributes.SERVICE_NAME) {
serviceName = attribute.value.stringValue || serviceName;
}
serviceTags.push({ key: attribute.key, value: getAttributeValue(attribute.value) });
}
return { serviceName, serviceTags };
}
function getSpanTags(span: collectorTypes.opentelemetryProto.trace.v1.Span): TraceKeyValuePair[] {
const spanTags: TraceKeyValuePair[] = [];
if (span.attributes) {
for (const attribute of span.attributes) {
spanTags.push({ key: attribute.key, value: getAttributeValue(attribute.value) });
}
}
return spanTags;
}
function getSpanKind(span: collectorTypes.opentelemetryProto.trace.v1.Span) {
let kind = undefined;
if (span.kind) {
const split = span.kind.toString().toLowerCase().split('_');
kind = split.length ? split[split.length - 1] : span.kind.toString();
}
return kind;
}
function getReferences(span: collectorTypes.opentelemetryProto.trace.v1.Span) {
const references: TraceSpanReference[] = [];
if (span.links) {
for (const link of span.links) {
const { traceId, spanId } = link;
const tags: TraceKeyValuePair[] = [];
if (link.attributes) {
for (const attribute of link.attributes) {
tags.push({ key: attribute.key, value: getAttributeValue(attribute.value) });
}
}
references.push({ traceID: traceId, spanID: spanId, tags });
}
}
return references;
}
function getLogs(span: collectorTypes.opentelemetryProto.trace.v1.Span) {
const logs: TraceLog[] = [];
if (span.events) {
for (const event of span.events) {
const fields: TraceKeyValuePair[] = [];
if (event.attributes) {
for (const attribute of event.attributes) {
fields.push({ key: attribute.key, value: getAttributeValue(attribute.value) });
}
}
logs.push({ fields, timestamp: event.timeUnixNano / 1000000 });
}
}
return logs;
}
export function transformFromOTLP(
traceData: collectorTypes.opentelemetryProto.trace.v1.ResourceSpans[],
nodeGraph = false
): DataQueryResponse {
const frame = new MutableDataFrame({
fields: [
{ name: 'traceID', type: FieldType.string, values: [] },
{ name: 'spanID', type: FieldType.string, values: [] },
{ name: 'parentSpanID', type: FieldType.string, values: [] },
{ name: 'operationName', type: FieldType.string, values: [] },
{ name: 'serviceName', type: FieldType.string, values: [] },
{ name: 'kind', type: FieldType.string, values: [] },
{ name: 'statusCode', type: FieldType.number, values: [] },
{ name: 'statusMessage', type: FieldType.string, values: [] },
{ name: 'instrumentationLibraryName', type: FieldType.string, values: [] },
{ name: 'instrumentationLibraryVersion', type: FieldType.string, values: [] },
{ name: 'traceState', type: FieldType.string, values: [] },
{ name: 'serviceTags', type: FieldType.other, values: [] },
{ name: 'startTime', type: FieldType.number, values: [] },
{ name: 'duration', type: FieldType.number, values: [] },
{ name: 'logs', type: FieldType.other, values: [] },
{ name: 'references', type: FieldType.other, values: [] },
{ name: 'tags', type: FieldType.other, values: [] },
],
meta: {
preferredVisualisationType: 'trace',
custom: {
traceFormat: 'otlp',
},
},
});
try {
for (const data of traceData) {
const { serviceName, serviceTags } = resourceToProcess(data.resource);
for (const librarySpan of data.instrumentationLibrarySpans) {
for (const span of librarySpan.spans) {
frame.add({
traceID: span.traceId.length > 16 ? span.traceId.slice(16) : span.traceId,
spanID: span.spanId,
parentSpanID: span.parentSpanId || '',
operationName: span.name || '',
serviceName,
kind: getSpanKind(span),
statusCode: span.status?.code,
statusMessage: span.status?.message,
instrumentationLibraryName: librarySpan.instrumentationLibrary?.name,
instrumentationLibraryVersion: librarySpan.instrumentationLibrary?.version,
traceState: span.traceState,
serviceTags,
startTime: span.startTimeUnixNano! / 1000000,
duration: (span.endTimeUnixNano! - span.startTimeUnixNano!) / 1000000,
tags: getSpanTags(span),
logs: getLogs(span),
references: getReferences(span),
});
}
}
}
} catch (error) {
console.error(error);
return { error: { message: 'JSON is not valid OpenTelemetry format: ' + error }, data: [] };
}
let data = [frame];
if (nodeGraph) {
data.push(...(createGraphFrames(frame) as MutableDataFrame[]));
}
return { data };
}
/**
* 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) {
if (span.instrumentationLibraryName) {
result.batches[batchIndex].instrumentationLibrarySpans[0].instrumentationLibrary = {
name: span.instrumentationLibraryName,
version: span.instrumentationLibraryVersion ? span.instrumentationLibraryVersion : '',
};
}
}
result.batches[batchIndex].instrumentationLibrarySpans[0].spans.push({
traceId: span.traceID.padStart(32, '0'),
spanId: span.spanID,
parentSpanId: span.parentSpanID || '',
traceState: span.traceState || '',
name: span.operationName,
kind: getOTLPSpanKind(span.kind) as any,
startTimeUnixNano: span.startTime * 1000000,
endTimeUnixNano: (span.startTime + span.duration) * 1000000,
attributes: span.tags ? tagsToAttributes(span.tags) : [],
droppedAttributesCount: 0,
droppedEventsCount: 0,
droppedLinksCount: 0,
status: getOTLPStatus(span),
events: getOTLPEvents(span.logs),
links: getOTLPReferences(span.references),
});
}
return result;
}
function getOTLPSpanKind(kind: string): string | undefined {
let spanKind = undefined;
if (kind) {
switch (kind) {
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;
case 'internal':
spanKind = 'SPAN_KIND_INTERNAL';
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.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(span: TraceSpanRow): SpanStatus | undefined {
let status = undefined;
if (span.statusCode !== undefined) {
status = {
code: span.statusCode,
message: span.statusMessage ? span.statusMessage : '',
};
}
return status;
}
function getOTLPEvents(logs: TraceLog[]): collectorTypes.opentelemetryProto.trace.v1.Span.Event[] | undefined {
if (!logs || !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;
}
function getOTLPReferences(
references: TraceSpanReference[]
): collectorTypes.opentelemetryProto.trace.v1.Span.Link[] | undefined {
if (!references || !references.length) {
return undefined;
}
let links: collectorTypes.opentelemetryProto.trace.v1.Span.Link[] = [];
for (const ref of references) {
let link: collectorTypes.opentelemetryProto.trace.v1.Span.Link = {
traceId: ref.traceID,
spanId: ref.spanID,
attributes: [],
droppedAttributesCount: 0,
};
if (ref.tags?.length) {
for (const tag of ref.tags) {
link.attributes?.push({
key: tag.key,
value: toAttributeValue(tag),
});
}
}
links.push(link);
}
return links;
}
export const RelatedProfilesTitle = 'Related profiles';
export function transformTrace(
response: DataQueryResponse,
instanceSettings: DataSourceInstanceSettings<TempoJsonData>,
nodeGraph = false
): DataQueryResponse {
const frame = response.data[0];
if (!frame) {
return emptyDataQueryResponse;
}
// Get profiles links
if (config.featureToggles.traceToProfiles) {
const traceToProfilesData: TraceToProfilesData | undefined = instanceSettings?.jsonData;
const traceToProfilesOptions = traceToProfilesData?.tracesToProfiles;
let profilesDataSourceSettings: DataSourceInstanceSettings<DataSourceJsonData> | undefined;
if (traceToProfilesOptions?.datasourceUid) {
profilesDataSourceSettings = getDatasourceSrv().getInstanceSettings(traceToProfilesOptions.datasourceUid);
}
if (traceToProfilesOptions && profilesDataSourceSettings) {
const customQuery = traceToProfilesOptions.customQuery ? traceToProfilesOptions.query : undefined;
const dataLink: DataLink = {
title: RelatedProfilesTitle,
url: '',
internal: {
datasourceUid: profilesDataSourceSettings.uid,
datasourceName: profilesDataSourceSettings.name,
query: {
labelSelector: customQuery ? customQuery : '{${__tags}}',
groupBy: [],
profileTypeId: traceToProfilesOptions.profileTypeId ?? '',
queryType: 'profile',
spanSelector: ['${__span.tags["pyroscope.profile.id"]}'],
refId: 'profile',
},
},
origin: DataLinkConfigOrigin.Datasource,
};
frame.fields.forEach((field: Field) => {
if (field.name === 'tags') {
field.config.links = [dataLink];
}
});
}
}
let data = [...response.data];
if (nodeGraph) {
data.push(...createGraphFrames(toDataFrame(frame)));
}
return {
...response,
data,
};
}
export function createTableFrameFromSearch(data: TraceSearchMetadata[], instanceSettings: DataSourceInstanceSettings) {
const frame = new MutableDataFrame({
name: 'Traces',
refId: 'traces',
fields: [
{
name: 'traceID',
type: FieldType.string,
values: [],
config: {
unit: 'string',
displayNameFromDS: 'Trace ID',
links: [
{
title: 'Trace: ${__value.raw}',
url: '',
internal: {
datasourceUid: instanceSettings.uid,
datasourceName: instanceSettings.name,
query: {
query: '${__value.raw}',
queryType: 'traceql',
},
},
},
],
},
},
{ name: 'traceService', type: FieldType.string, config: { displayNameFromDS: 'Trace service' }, values: [] },
{ name: 'traceName', type: FieldType.string, config: { displayNameFromDS: 'Trace name' }, values: [] },
{ name: 'startTime', type: FieldType.time, config: { displayNameFromDS: 'Start time' }, values: [] },
{
name: 'traceDuration',
type: FieldType.number,
config: { displayNameFromDS: 'Duration', unit: 'ms' },
values: [],
},
],
meta: {
preferredVisualisationType: 'table',
},
});
if (!data?.length) {
return frame;
}
// Show the most recent traces
const traceData = data
.sort((a, b) => parseInt(b?.startTimeUnixNano!, 10) / 1000000 - parseInt(a?.startTimeUnixNano!, 10) / 1000000)
.map(transformToTraceData);
for (const trace of traceData) {
frame.add(trace);
}
return frame;
}
function transformToTraceData(data: TraceSearchMetadata) {
return {
traceID: data.traceID,
startTime: parseInt(data.startTimeUnixNano!, 10) / 1000000,
traceDuration: data.durationMs,
traceService: data.rootServiceName || '',
traceName: data.rootTraceName || '',
};
}
export function formatTraceQLResponse(
data: TraceSearchMetadata[],
instanceSettings: DataSourceInstanceSettings,
tableType?: SearchTableType
) {
if (tableType === SearchTableType.Spans) {
return createTableFrameFromTraceQlQueryAsSpans(data, instanceSettings);
}
return createTableFrameFromTraceQlQuery(data, instanceSettings);
}
export function createTableFrameFromTraceQlQuery(
data: TraceSearchMetadata[],
instanceSettings: DataSourceInstanceSettings
): DataFrame[] {
const frame = createDataFrame({
name: 'Traces',
refId: 'traces',
fields: [
{
name: 'traceID',
type: FieldType.string,
config: {
unit: 'string',
displayNameFromDS: 'Trace ID',
custom: {
width: 200,
},
links: [
{
title: 'Trace: ${__value.raw}',
url: '',
internal: {
datasourceUid: instanceSettings.uid,
datasourceName: instanceSettings.name,
query: {
query: '${__value.raw}',
queryType: 'traceql',
},
},
},
],
},
},
{
name: 'startTime',
type: FieldType.time,
config: {
displayNameFromDS: 'Start time',
custom: {
width: 200,
},
},
},
{ name: 'traceService', type: FieldType.string, config: { displayNameFromDS: 'Service' } },
{ name: 'traceName', type: FieldType.string, config: { displayNameFromDS: 'Name' } },
{
name: 'traceDuration',
type: FieldType.number,
config: {
displayNameFromDS: 'Duration',
unit: 'ms',
custom: {
width: 120,
},
},
},
{
name: 'nested',
type: FieldType.nestedFrames,
},
],
meta: {
preferredVisualisationType: 'table',
},
});
if (!data?.length) {
return [frame];
}
frame.length = data.length;
data
// Show the most recent traces
.sort((a, b) => parseInt(b?.startTimeUnixNano!, 10) / 1000000 - parseInt(a?.startTimeUnixNano!, 10) / 1000000)
.forEach((trace) => {
const traceData: TraceTableData = transformToTraceData(trace);
frame.fields[0].values.push(traceData.traceID);
frame.fields[1].values.push(traceData.startTime);
frame.fields[2].values.push(traceData.traceService);
frame.fields[3].values.push(traceData.traceName);
frame.fields[4].values.push(traceData.traceDuration);
if (trace.spanSets) {
frame.fields[5].values.push(
trace.spanSets.map((spanSet: Spanset) => {
return traceSubFrame(trace, spanSet, instanceSettings);
})
);
} else if (trace.spanSet) {
frame.fields[5].values.push([traceSubFrame(trace, trace.spanSet, instanceSettings)]);
}
});
return [frame];
}
export function createTableFrameFromTraceQlQueryAsSpans(
data: TraceSearchMetadata[],
instanceSettings: DataSourceInstanceSettings
): DataFrame[] {
const spanDynamicAttrs: Record<string, FieldDTO> = {};
let hasNameAttribute = false;
const frame = new MutableDataFrame({
name: 'Spans',
refId: 'traces',
fields: [
{
name: 'traceIdHidden',
type: FieldType.string,
config: {
custom: { hidden: true },
},
},
{
name: 'traceService',
type: FieldType.string,
config: {
displayNameFromDS: 'Trace Service',
custom: {
width: 200,
},
},
},
{
name: 'traceName',
type: FieldType.string,
config: {
displayNameFromDS: 'Trace Name',
custom: {
width: 200,
},
},
},
{
name: 'spanID',
type: FieldType.string,
config: {
unit: 'string',
displayNameFromDS: 'Span ID',
custom: {
width: 200,
},
links: [
{
title: 'Span: ${__value.raw}',
url: '',
internal: {
datasourceUid: instanceSettings.uid,
datasourceName: instanceSettings.name,
query: {
query: '${__data.fields.traceIdHidden}',
queryType: 'traceql',
},
panelsState: {
trace: {
spanId: '${__value.raw}',
},
},
},
},
],
},
},
{
name: 'time',
type: FieldType.time,
config: {
displayNameFromDS: 'Start time',
},
},
{
name: 'name',
type: FieldType.string,
config: { displayNameFromDS: 'Name', custom: { hidden: !hasNameAttribute } },
},
...Object.values(spanDynamicAttrs).sort((a, b) => a.name.localeCompare(b.name)),
{
name: 'duration',
type: FieldType.number,
config: {
displayNameFromDS: 'Duration',
unit: 'ns',
custom: {
width: 120,
},
},
},
],
meta: {
preferredVisualisationType: 'table',
},
});
// According to the parameter type, `data` should never be undefined of null, but the old code had
// entries such as `!data` or `data?`, so we keep this check just for safety
if (data === undefined || data === null) {
console.error(`Unexpected ${data} value for \`data\``);
return [frame];
}
if (!data.length) {
return [frame];
}
data.forEach((trace) =>
getSpanSets(trace).forEach((ss) => {
ss.attributes?.forEach((attr) => {
spanDynamicAttrs[attr.key] = {
name: attr.key,
type: FieldType.string,
config: { displayNameFromDS: attr.key },
};
});
ss.spans.forEach((span) => {
if (span.name) {
hasNameAttribute = true;
}
span.attributes?.forEach((attr) => {
spanDynamicAttrs[attr.key] = {
name: attr.key,
type: FieldType.string,
config: { displayNameFromDS: attr.key },
};
});
});
})
);
data
// Show the most recent traces
.sort((a, b) => parseInt(b?.startTimeUnixNano!, 10) / 1000000 - parseInt(a?.startTimeUnixNano!, 10) / 1000000)
.forEach((trace) => {
getSpanSets(trace).forEach((spanSet) => {
spanSet.spans.forEach((span) => {
frame.add(transformSpanToTraceData(span, spanSet, trace));
});
});
});
return [frame];
}
/**
* Get the spansets of a trace.
*
* Field `spanSets` is preferred to `spanSet` since the latter is deprecated in Tempo, but we
* support both for backward compatibility.
*
* @param trace a trace
* @returns the spansets of the trace, if existing
*/
const getSpanSets = (trace: TraceSearchMetadata): Spanset[] => {
if (trace.spanSets && trace.spanSet) {
console.warn('Both `spanSets` and `spanSet` are set. `spanSet` will be ignored');
}
return trace.spanSets || (trace.spanSet ? [trace.spanSet] : []);
};
const traceSubFrame = (
trace: TraceSearchMetadata,
spanSet: Spanset,
instanceSettings: DataSourceInstanceSettings
): DataFrame => {
const spanDynamicAttrs: Record<string, FieldDTO> = {};
let hasNameAttribute = false;
spanSet.attributes?.map((attr) => {
spanDynamicAttrs[attr.key] = {
name: attr.key,
type: FieldType.string,
config: { displayNameFromDS: attr.key },
values: [],
};
});
spanSet.spans.forEach((span) => {
if (span.name) {
hasNameAttribute = true;
}
span.attributes?.forEach((attr) => {
spanDynamicAttrs[attr.key] = {
name: attr.key,
type: FieldType.string,
config: { displayNameFromDS: attr.key },
values: [],
};
});
});
const subFrame = new MutableDataFrame({
fields: [
{
name: 'traceIdHidden',
type: FieldType.string,
config: {
custom: { hidden: true },
},
values: [],
},
{
name: 'spanID',
type: FieldType.string,
values: [],
config: {
unit: 'string',
displayNameFromDS: 'Span ID',
custom: {
width: 200,
},
links: [
{
title: 'Span: ${__value.raw}',
url: '',
internal: {
datasourceUid: instanceSettings.uid,
datasourceName: instanceSettings.name,
query: {
query: '${__data.fields.traceIdHidden}',
queryType: 'traceql',
},
panelsState: {
trace: {
spanId: '${__value.raw}',
},
},
},
},
],
},
},
{
name: 'time',
type: FieldType.time,
config: {
displayNameFromDS: 'Start time',
custom: {
width: 200,
},
},
},
{
name: 'name',
type: FieldType.string,
values: [],
config: { displayNameFromDS: 'Name', custom: { hidden: !hasNameAttribute } },
},
...Object.values(spanDynamicAttrs).sort((a, b) => a.name.localeCompare(b.name)),
{
name: 'duration',
type: FieldType.number,
values: [],
config: {
displayNameFromDS: 'Duration',
unit: 'ns',
custom: {
width: 120,
},
},
},
],
meta: {
preferredVisualisationType: 'table',
},
});
const theme = createTheme();
for (const field of subFrame.fields) {
field.display = getDisplayProcessor({ field, theme });
}
spanSet.spans.forEach((span) => {
subFrame.add(transformSpanToTraceData(span, spanSet, trace));
});
return subFrame;
};
interface TraceTableData {
[key: string]: string | number | boolean | undefined; // dynamic attribute name
traceID?: string;
spanID?: string;
startTime?: number;
name?: string;
traceDuration?: number;
}
function transformSpanToTraceData(span: Span, spanSet: Spanset, trace: TraceSearchMetadata): TraceTableData {
const spanStartTimeUnixMs = parseInt(span.startTimeUnixNano, 10) / 1000000;
const data: TraceTableData = {
traceIdHidden: trace.traceID,
traceService: trace.rootServiceName || '',
traceName: trace.rootTraceName || '',
spanID: span.spanID,
time: spanStartTimeUnixMs,
duration: parseInt(span.durationNanos, 10),
name: span.name,
};
let attrs: SpanAttributes[] = [];
if (spanSet.attributes) {
attrs = attrs.concat(spanSet.attributes);
}
if (span.attributes) {
attrs = attrs.concat(span.attributes);
}
attrs.forEach((attr) => {
if (attr.value.boolValue || attr.value.Value?.bool_value) {
data[attr.key] = attr.value.boolValue || attr.value.Value?.bool_value;
}
if (attr.value.doubleValue || attr.value.Value?.double_value) {
data[attr.key] = attr.value.doubleValue || attr.value.Value?.double_value;
}
if (attr.value.intValue || attr.value.Value?.int_value) {
data[attr.key] = attr.value.intValue || attr.value.Value?.int_value;
}
if (attr.value.stringValue || attr.value.Value?.string_value) {
data[attr.key] = attr.value.stringValue || attr.value.Value?.string_value;
}
});
return data;
}
const emptyDataQueryResponse = {
data: [
new MutableDataFrame({
fields: [
{
name: 'trace',
type: FieldType.trace,
values: [],
},
],
meta: {
preferredVisualisationType: 'trace',
custom: {
traceFormat: 'otlp',
},
},
}),
],
};