@ -10,7 +10,7 @@ import {
TraceLog ,
TraceSpanRow ,
} from '@grafana/data' ;
import { SpanKind , SpanStatusCode } from '@opentelemetry/api' ;
import { SpanKind , SpanStatus , SpanStatus Code } 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' ,
} ,
} ,
} ) ,
] ,