first commit
This commit is contained in:
306
plugins/otel/converter.go
Normal file
306
plugins/otel/converter.go
Normal file
@@ -0,0 +1,306 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user