From a189b3d2a18a2d8a15a8f86dd98ab20702fb7790 Mon Sep 17 00:00:00 2001 From: "jiangqi.rrt" Date: Wed, 28 Jan 2026 20:25:31 +0800 Subject: [PATCH 1/4] otel add status_code --- backend/modules/observability/lib/otel/consts.go | 1 + backend/modules/observability/lib/otel/otel_convert.go | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/backend/modules/observability/lib/otel/consts.go b/backend/modules/observability/lib/otel/consts.go index f73982d54..94441aad5 100644 --- a/backend/modules/observability/lib/otel/consts.go +++ b/backend/modules/observability/lib/otel/consts.go @@ -21,6 +21,7 @@ const ( otelAttributeInput = "cozeloop.input" otelAttributeOutput = "cozeloop.output" otelAttributeLogID = "cozeloop.logid" + otelAttributeStatusCode = "cozeloop.status_code" apmInput = "gen_ai.input" apmOutput = "gen_ai.output" diff --git a/backend/modules/observability/lib/otel/otel_convert.go b/backend/modules/observability/lib/otel/otel_convert.go index 6b33a3954..a20f5a350 100644 --- a/backend/modules/observability/lib/otel/otel_convert.go +++ b/backend/modules/observability/lib/otel/otel_convert.go @@ -68,6 +68,11 @@ var ( IsTag: true, DataType: dataTypeString, }, + "status_code": { + AttributeKey: []string{otelAttributeStatusCode}, + IsTag: false, + DataType: dataTypeInt64, + }, "psm": { AttributeKey: []string{"service.name"}, IsTag: false, @@ -343,6 +348,11 @@ func OtelSpanConvertToSendSpan(ctx context.Context, spaceID string, resourceScop } if conf.IsTag { tagsLong[fieldKey] = value + } else { + switch fieldKey { + case "status_code": + statusCode = int32(value) + } } case dataTypeBool: value, ok := srcValue.(bool) From d501597568b68ffeb6bc12ab54b2f22c43b75622 Mon Sep 17 00:00:00 2001 From: "jiangqi.rrt" Date: Fri, 30 Jan 2026 14:18:45 +0800 Subject: [PATCH 2/4] otel add gen_ai.input.messages and gen_ai.output.messages --- backend/modules/observability/lib/otel/consts.go | 2 +- backend/modules/observability/lib/otel/otel_convert.go | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/backend/modules/observability/lib/otel/consts.go b/backend/modules/observability/lib/otel/consts.go index 94441aad5..69f21b14c 100644 --- a/backend/modules/observability/lib/otel/consts.go +++ b/backend/modules/observability/lib/otel/consts.go @@ -97,7 +97,7 @@ const ( // otel attribute key const ( - otelAttributeModelInputTools = "gen_ai.tool.definitions" + otelAttributeModelInputTools = "gen_ai.tool.definitions" // model tools ) var otelMessageEventNameMap = []string{ diff --git a/backend/modules/observability/lib/otel/otel_convert.go b/backend/modules/observability/lib/otel/otel_convert.go index a20f5a350..11c2f712c 100644 --- a/backend/modules/observability/lib/otel/otel_convert.go +++ b/backend/modules/observability/lib/otel/otel_convert.go @@ -25,6 +25,8 @@ import ( // FieldConfMap Field configuration, supports configuring data sources and export methods for fields, currently supports attribute, event, is_tag, data_type // Among them, attributes and events support configuring multiple, while tags and datatypes only support configuring one. +// For AttributeKey and AttributeKeyPrefix, the field at the head has higher priority, and result will be returned once a match is found. +// FOr Events, the fields have same priority, and result will be fixed by all fields. // Other types of configurations need to be manually processed in the code. var ( FieldConfMap = map[string]FieldConf{ @@ -96,6 +98,7 @@ var ( openInferenceAttributeModelInputMessages, openInferenceAttributeToolInput, string(semconv1_27_0.GenAIPromptKey), + string(semconv.GenAIInputMessagesKey), }, EventName: []string{otelEventModelSystemMessage, otelEventModelUserMessage, otelEventModelToolMessage, otelEventModelAssistantMessage, otelSpringAIEventModelPrompt}, DataType: dataTypeString, @@ -122,6 +125,7 @@ var ( AttributeKeyPrefix: []string{ openInferenceAttributeModelOutputMessages, string(semconv1_27_0.GenAICompletionKey), + string(semconv.GenAIOutputMessagesKey), }, EventName: []string{otelEventModelChoice, otelSpringAIEventModelCompletion}, DataType: dataTypeString, @@ -677,7 +681,7 @@ func processAttributePrefix(ctx context.Context, fieldKey string, conf FieldConf } } } - case openInferenceAttributeModelInputMessages: // openInference(or litellm) input message + case openInferenceAttributeModelInputMessages, string(semconv.GenAIInputMessagesKey): // openInference(or litellm) or openTelemetry input message srcInput, err := open_inference.ConvertToModelInput(srcAttrAggrRes) if err != nil { continue @@ -696,7 +700,7 @@ func processAttributePrefix(ctx context.Context, fieldKey string, conf FieldConf continue } } - case openInferenceAttributeModelOutputMessages: // openInference output message + case openInferenceAttributeModelOutputMessages, string(semconv.GenAIOutputMessagesKey): // openInference(or litellm) or openTelemetry output message resObject, err := open_inference.ConvertToModelOutput(srcAttrAggrRes) if err == nil { toBeMarshalObject = resObject From 88c9ac3e38acfaca7c9ffb7799b3978234fc7ef9 Mon Sep 17 00:00:00 2001 From: "jiangqi.rrt" Date: Mon, 2 Feb 2026 20:21:07 +0800 Subject: [PATCH 3/4] runtime support get from attribute --- .../modules/observability/lib/otel/consts.go | 3 +++ .../lib/otel/open_inference/openinference.go | 12 +++++++----- .../observability/lib/otel/otel_convert.go | 19 +++++++++++++------ .../lib/otel/otel_convert_test.go | 6 +++--- 4 files changed, 26 insertions(+), 14 deletions(-) diff --git a/backend/modules/observability/lib/otel/consts.go b/backend/modules/observability/lib/otel/consts.go index 69f21b14c..7b2626c3c 100644 --- a/backend/modules/observability/lib/otel/consts.go +++ b/backend/modules/observability/lib/otel/consts.go @@ -35,6 +35,9 @@ const ( otelAttributePromptKey = "cozeloop.prompt_key" otelAttributePromptVersion = "cozeloop.prompt_version" otelAttributePromptProvider = "cozeloop.prompt_provider" + + // system + otelAttributeSystemRuntime = "cozeloop.system_tag_runtime" ) // openinference attribute key diff --git a/backend/modules/observability/lib/otel/open_inference/openinference.go b/backend/modules/observability/lib/otel/open_inference/openinference.go index 0afe465ba..d34eb5d2d 100644 --- a/backend/modules/observability/lib/otel/open_inference/openinference.go +++ b/backend/modules/observability/lib/otel/open_inference/openinference.go @@ -67,12 +67,14 @@ func convertModelMsg(msg map[string]interface{}) map[string]interface{} { modelMsg["reasoning_content"] = c } - // contents + // contents or parts var contents []interface{} - if c, ok := msg["contents"].([]interface{}); ok && len(c) > 0 { - contents = c - } else if c, ok := msg["content"].([]interface{}); ok && len(c) > 0 { - contents = c + partsKey := []string{"contents", "content", "parts"} + for _, key := range partsKey { + if parts, ok := msg[key].([]interface{}); ok && len(parts) > 0 { + contents = parts + break + } } if len(contents) > 0 { parts := make([]interface{}, 0, len(contents)) diff --git a/backend/modules/observability/lib/otel/otel_convert.go b/backend/modules/observability/lib/otel/otel_convert.go index 11c2f712c..9a0d568d3 100644 --- a/backend/modules/observability/lib/otel/otel_convert.go +++ b/backend/modules/observability/lib/otel/otel_convert.go @@ -398,7 +398,7 @@ func OtelSpanConvertToSendSpan(ctx context.Context, spaceID string, resourceScop // set attributes calOtherAttribute(ctx, span, tagsString, tagsLong, tagsDouble, tagsBool) // set runtime - calRuntime(systemTagsString, resourceScopeSpan) + calRuntime(systemTagsString, tagsString, resourceScopeSpan) result := &LoopSpan{ StartTime: startTimeUnixNanoInt64 / 1000, @@ -544,12 +544,19 @@ func calOtherAttribute(ctx context.Context, span *Span, tagsString map[string]st } } -func calRuntime(systemTagsString map[string]string, resourceScopeSpan *ResourceScopeSpan) { - systemTagsString[tracespec.Runtime_] = getRuntime(resourceScopeSpan) +func calRuntime(systemTagsString map[string]string, tagsString map[string]string, resourceScopeSpan *ResourceScopeSpan) { + systemTagsString[tracespec.Runtime_] = getRuntime(tagsString, resourceScopeSpan) } -func getRuntime(resourceScopeSpan *ResourceScopeSpan) string { - runtime := processRuntime(resourceScopeSpan) +func getRuntime(tagsString map[string]string, resourceScopeSpan *ResourceScopeSpan) string { + if len(tagsString) > 0 { + if runtime, ok := tagsString[otelAttributeSystemRuntime]; ok && len(runtime) > 0 { + delete(tagsString, otelAttributeSystemRuntime) + return runtime + } + } + + runtime := processRuntimeByScope(resourceScopeSpan) marshalString, err := sonic.MarshalString(runtime) if err != nil { return "" // unexpected @@ -558,7 +565,7 @@ func getRuntime(resourceScopeSpan *ResourceScopeSpan) string { return marshalString } -func processRuntime(resourceScopeSpan *ResourceScopeSpan) *tracespec.Runtime { +func processRuntimeByScope(resourceScopeSpan *ResourceScopeSpan) *tracespec.Runtime { res := &tracespec.Runtime{ Library: tracespec.VLibOpentelemetry, Scene: "", diff --git a/backend/modules/observability/lib/otel/otel_convert_test.go b/backend/modules/observability/lib/otel/otel_convert_test.go index 1ceafd7ea..c0c0c55bd 100644 --- a/backend/modules/observability/lib/otel/otel_convert_test.go +++ b/backend/modules/observability/lib/otel/otel_convert_test.go @@ -1036,7 +1036,7 @@ func TestProcessRuntime(t *testing.T) { panic(r) // Re-panic if unexpected } }() - result := processRuntime(tt.resourceScopeSpan) + result := processRuntimeByScope(tt.resourceScopeSpan) tt.validate(t, result) }) } @@ -1093,7 +1093,7 @@ func TestGetRuntime(t *testing.T) { panic(r) // Re-panic if unexpected } }() - result := getRuntime(tt.resourceScopeSpan) + result := getRuntime(nil, tt.resourceScopeSpan) tt.validate(t, result) }) } @@ -1149,7 +1149,7 @@ func TestCalRuntime(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - calRuntime(tt.systemTagsString, tt.resourceScopeSpan) + calRuntime(tt.systemTagsString, nil, tt.resourceScopeSpan) tt.validate(t, tt.systemTagsString) }) } From f9c43e0ebfb2cff3dcad0c0c77c122e2d9e80ea5 Mon Sep 17 00:00:00 2001 From: "jiangqi.rrt" Date: Wed, 4 Feb 2026 14:57:27 +0800 Subject: [PATCH 4/4] support otel tool input and outputy --- backend/modules/observability/lib/otel/consts.go | 3 +++ .../observability/lib/otel/open_inference/openinference.go | 3 +++ backend/modules/observability/lib/otel/otel_convert.go | 4 +++- 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/backend/modules/observability/lib/otel/consts.go b/backend/modules/observability/lib/otel/consts.go index 7b2626c3c..41ef27e78 100644 --- a/backend/modules/observability/lib/otel/consts.go +++ b/backend/modules/observability/lib/otel/consts.go @@ -101,6 +101,9 @@ const ( // otel attribute key const ( otelAttributeModelInputTools = "gen_ai.tool.definitions" // model tools + + otelAttributeToolInput = "gen_ai.tool.call.arguments" // tool input + otelAttributeToolOutput = "gen_ai.tool.call.result" ) var otelMessageEventNameMap = []string{ diff --git a/backend/modules/observability/lib/otel/open_inference/openinference.go b/backend/modules/observability/lib/otel/open_inference/openinference.go index d34eb5d2d..83a08bb9f 100644 --- a/backend/modules/observability/lib/otel/open_inference/openinference.go +++ b/backend/modules/observability/lib/otel/open_inference/openinference.go @@ -86,6 +86,9 @@ func convertModelMsg(msg map[string]interface{}) map[string]interface{} { } typ, _ := mcContent["type"] text, _ := mcContent["text"] + if text == nil { + text, _ = mcContent["content"] + } image, _ := mcContent["image_url"] part := map[string]interface{}{} switch typ { diff --git a/backend/modules/observability/lib/otel/otel_convert.go b/backend/modules/observability/lib/otel/otel_convert.go index 9a0d568d3..6bb08c646 100644 --- a/backend/modules/observability/lib/otel/otel_convert.go +++ b/backend/modules/observability/lib/otel/otel_convert.go @@ -26,7 +26,7 @@ import ( // FieldConfMap Field configuration, supports configuring data sources and export methods for fields, currently supports attribute, event, is_tag, data_type // Among them, attributes and events support configuring multiple, while tags and datatypes only support configuring one. // For AttributeKey and AttributeKeyPrefix, the field at the head has higher priority, and result will be returned once a match is found. -// FOr Events, the fields have same priority, and result will be fixed by all fields. +// For Events, the fields have same priority, and result will be fixed by all fields. // Other types of configurations need to be manually processed in the code. var ( FieldConfMap = map[string]FieldConf{ @@ -93,6 +93,7 @@ var ( springAIAttributeToolInput, otelAttributeInput, apmInput, + otelAttributeToolInput, }, AttributeKeyPrefix: []string{ openInferenceAttributeModelInputMessages, @@ -121,6 +122,7 @@ var ( springAIAttributeToolOutput, otelAttributeOutput, apmOutput, + otelAttributeToolOutput, }, AttributeKeyPrefix: []string{ openInferenceAttributeModelOutputMessages,