Files
bifrost/plugins/otel/converter.go
Beyhan Oğur 880f412e2c first commit
2026-04-26 21:52:23 +03:00

307 lines
8.4 KiB
Go

package otel
import (
"encoding/hex"
"fmt"
"strings"
"github.com/maximhq/bifrost/core/schemas"
commonpb "go.opentelemetry.io/proto/otlp/common/v1"
resourcepb "go.opentelemetry.io/proto/otlp/resource/v1"
tracepb "go.opentelemetry.io/proto/otlp/trace/v1"
)
// kvStr creates a key-value pair with a string value
func kvStr(k, v string) *KeyValue {
return &KeyValue{Key: k, Value: &AnyValue{Value: &StringValue{StringValue: v}}}
}
// kvInt creates a key-value pair with an integer value
func kvInt(k string, v int64) *KeyValue {
return &KeyValue{Key: k, Value: &AnyValue{Value: &IntValue{IntValue: v}}}
}
// kvDbl creates a key-value pair with a double value
func kvDbl(k string, v float64) *KeyValue {
return &KeyValue{Key: k, Value: &AnyValue{Value: &DoubleValue{DoubleValue: v}}}
}
// kvBool creates a key-value pair with a boolean value
func kvBool(k string, v bool) *KeyValue {
return &KeyValue{Key: k, Value: &AnyValue{Value: &BoolValue{BoolValue: v}}}
}
// kvAny creates a key-value pair with an any value
func kvAny(k string, v *AnyValue) *KeyValue {
return &KeyValue{Key: k, Value: v}
}
// arrValue converts a list of any values to an OpenTelemetry array value
func arrValue(vals ...*AnyValue) *AnyValue {
return &AnyValue{Value: &ArrayValue{ArrayValue: &ArrayValueValue{Values: vals}}}
}
// listValue converts a list of key-value pairs to an OpenTelemetry list value
func listValue(kvs ...*KeyValue) *AnyValue {
return &AnyValue{Value: &ListValue{KvlistValue: &KeyValueList{Values: kvs}}}
}
// hexToBytes converts a hex string to bytes, padding/truncating as needed
func hexToBytes(hexStr string, length int) []byte {
// Remove any non-hex characters
cleaned := strings.Map(func(r rune) rune {
if (r >= '0' && r <= '9') || (r >= 'a' && r <= 'f') || (r >= 'A' && r <= 'F') {
return r
}
return -1
}, hexStr)
// Ensure even length
if len(cleaned)%2 != 0 {
cleaned = "0" + cleaned
}
// Truncate or pad to desired length
if len(cleaned) > length*2 {
cleaned = cleaned[:length*2]
} else if len(cleaned) < length*2 {
cleaned = strings.Repeat("0", length*2-len(cleaned)) + cleaned
}
bytes, _ := hex.DecodeString(cleaned)
return bytes
}
// convertTraceToResourceSpan converts a Bifrost trace to OTEL ResourceSpan
func (p *OtelPlugin) convertTraceToResourceSpan(trace *schemas.Trace) *ResourceSpan {
otelSpans := make([]*Span, 0, len(trace.Spans))
for _, span := range trace.Spans {
otelSpans = append(otelSpans, p.convertSpanToOTELSpan(trace.TraceID, span))
}
return &ResourceSpan{
Resource: &resourcepb.Resource{
Attributes: p.getResourceAttributes(),
},
ScopeSpans: []*ScopeSpan{{
Scope: p.getInstrumentationScope(),
Spans: otelSpans,
}},
}
}
// convertSpanToOTELSpan converts a single Bifrost span to OTEL format
func (p *OtelPlugin) convertSpanToOTELSpan(traceID string, span *schemas.Span) *Span {
otelSpan := &Span{
TraceId: hexToBytes(traceID, 16),
SpanId: hexToBytes(span.SpanID, 8),
Name: span.Name,
Kind: convertSpanKind(span.Kind),
StartTimeUnixNano: uint64(span.StartTime.UnixNano()),
EndTimeUnixNano: uint64(span.EndTime.UnixNano()),
Attributes: convertAttributesToKeyValues(span.Attributes),
Status: convertSpanStatus(span.Status, span.StatusMsg),
Events: convertSpanEvents(span.Events),
}
// Set parent span ID if present
if span.ParentID != "" {
otelSpan.ParentSpanId = hexToBytes(span.ParentID, 8)
}
return otelSpan
}
// getResourceAttributes returns the resource attributes for the OTEL span
func (p *OtelPlugin) getResourceAttributes() []*KeyValue {
attrs := []*KeyValue{
kvStr("service.name", p.serviceName),
kvStr("service.version", p.bifrostVersion),
kvStr("telemetry.sdk.name", "bifrost"),
kvStr("telemetry.sdk.language", "go"),
}
// Add environment attributes
attrs = append(attrs, p.attributesFromEnvironment...)
return attrs
}
// getInstrumentationScope returns the instrumentation scope for OTEL
func (p *OtelPlugin) getInstrumentationScope() *commonpb.InstrumentationScope {
return &commonpb.InstrumentationScope{
Name: p.serviceName,
Version: p.bifrostVersion,
}
}
// convertAttributesToKeyValues converts map[string]any to OTEL KeyValue slice
func convertAttributesToKeyValues(attrs map[string]any) []*KeyValue {
if attrs == nil {
return nil
}
kvs := make([]*KeyValue, 0, len(attrs))
for k, v := range attrs {
kv := anyToKeyValue(k, v)
if kv != nil {
kvs = append(kvs, kv)
}
}
return kvs
}
// anyToKeyValue converts any Go value to OTEL KeyValue
func anyToKeyValue(key string, value any) *KeyValue {
if value == nil {
return nil
}
switch v := value.(type) {
case string:
if v == "" {
return nil
}
return kvStr(key, v)
case int:
return kvInt(key, int64(v))
case int32:
return kvInt(key, int64(v))
case int64:
return kvInt(key, v)
case uint:
return kvInt(key, int64(v))
case uint32:
return kvInt(key, int64(v))
case uint64:
return kvInt(key, int64(v))
case float32:
return kvDbl(key, float64(v))
case float64:
return kvDbl(key, v)
case bool:
return kvBool(key, v)
case []string:
if len(v) == 0 {
return nil
}
vals := make([]*AnyValue, len(v))
for i, s := range v {
vals[i] = &AnyValue{Value: &StringValue{StringValue: s}}
}
return kvAny(key, arrValue(vals...))
case []int:
if len(v) == 0 {
return nil
}
vals := make([]*AnyValue, len(v))
for i, n := range v {
vals[i] = &AnyValue{Value: &IntValue{IntValue: int64(n)}}
}
return kvAny(key, arrValue(vals...))
case []int64:
if len(v) == 0 {
return nil
}
vals := make([]*AnyValue, len(v))
for i, n := range v {
vals[i] = &AnyValue{Value: &IntValue{IntValue: n}}
}
return kvAny(key, arrValue(vals...))
case []float64:
if len(v) == 0 {
return nil
}
vals := make([]*AnyValue, len(v))
for i, n := range v {
vals[i] = &AnyValue{Value: &DoubleValue{DoubleValue: n}}
}
return kvAny(key, arrValue(vals...))
case []any:
if len(v) == 0 {
return nil
}
vals := make([]*AnyValue, 0, len(v))
for _, item := range v {
if kv := anyToKeyValue("_", item); kv != nil {
vals = append(vals, kv.Value)
}
}
if len(vals) == 0 {
return nil
}
return kvAny(key, arrValue(vals...))
case map[string]any:
if len(v) == 0 {
return nil
}
kvList := make([]*KeyValue, 0, len(v))
for k, val := range v {
kv := anyToKeyValue(k, val)
if kv != nil {
kvList = append(kvList, kv)
}
}
return kvAny(key, listValue(kvList...))
default:
data, err := schemas.MarshalSorted(v)
if err != nil {
return kvStr(key, fmt.Sprintf("%v", v))
}
var generic any
if err := schemas.Unmarshal(data, &generic); err != nil {
return kvStr(key, string(data))
}
return anyToKeyValue(key, generic)
}
}
// convertSpanKind maps Bifrost SpanKind to OTEL SpanKind
func convertSpanKind(kind schemas.SpanKind) tracepb.Span_SpanKind {
switch kind {
case schemas.SpanKindLLMCall:
return tracepb.Span_SPAN_KIND_CLIENT
case schemas.SpanKindHTTPRequest:
return tracepb.Span_SPAN_KIND_SERVER
case schemas.SpanKindPlugin:
return tracepb.Span_SPAN_KIND_INTERNAL
case schemas.SpanKindInternal:
return tracepb.Span_SPAN_KIND_INTERNAL
case schemas.SpanKindRetry:
return tracepb.Span_SPAN_KIND_INTERNAL
case schemas.SpanKindFallback:
return tracepb.Span_SPAN_KIND_INTERNAL
case schemas.SpanKindMCPTool:
return tracepb.Span_SPAN_KIND_CLIENT
case schemas.SpanKindEmbedding:
return tracepb.Span_SPAN_KIND_CLIENT
case schemas.SpanKindSpeech:
return tracepb.Span_SPAN_KIND_CLIENT
case schemas.SpanKindTranscription:
return tracepb.Span_SPAN_KIND_CLIENT
default:
return tracepb.Span_SPAN_KIND_UNSPECIFIED
}
}
// convertSpanStatus maps Bifrost SpanStatus to OTEL Status
func convertSpanStatus(status schemas.SpanStatus, msg string) *tracepb.Status {
switch status {
case schemas.SpanStatusOk:
return &tracepb.Status{Code: tracepb.Status_STATUS_CODE_OK}
case schemas.SpanStatusError:
return &tracepb.Status{Code: tracepb.Status_STATUS_CODE_ERROR, Message: msg}
default:
return &tracepb.Status{Code: tracepb.Status_STATUS_CODE_UNSET}
}
}
// convertSpanEvents converts Bifrost span events to OTEL events
func convertSpanEvents(events []schemas.SpanEvent) []*Event {
if len(events) == 0 {
return nil
}
otelEvents := make([]*Event, len(events))
for i, event := range events {
otelEvents[i] = &Event{
TimeUnixNano: uint64(event.Timestamp.UnixNano()),
Name: event.Name,
Attributes: convertAttributesToKeyValues(event.Attributes),
}
}
return otelEvents
}