88 "os"
99 "path/filepath"
1010 "strconv"
11+ "strings"
1112 "time"
1213
1314 "github.com/google/uuid"
@@ -198,10 +199,10 @@ func detectOtelSource(resource *resourcev1.Resource) string {
198199 for _ , attr := range resource .GetAttributes () {
199200 if attr .GetKey () == "service.name" {
200201 serviceName := attr .GetValue ().GetStringValue ()
201- switch serviceName {
202- case "claude-code" :
202+ if strings .Contains (serviceName , "claude" ) {
203203 return model .AICodeOtelSourceClaudeCode
204- case "codex" , "codex-cli" , "openai-codex" :
204+ }
205+ if strings .Contains (serviceName , "codex" ) {
205206 return model .AICodeOtelSourceCodex
206207 }
207208 }
@@ -226,11 +227,13 @@ func extractResourceAttributes(resource *resourcev1.Resource) *model.AICodeOtelR
226227 // Standard resource attributes
227228 case "session.id" :
228229 attrs .SessionID = value .GetStringValue ()
230+ case "conversation.id" :
231+ attrs .ConversationID = value .GetStringValue ()
229232 case "app.version" :
230233 attrs .AppVersion = value .GetStringValue ()
231234 case "organization.id" :
232235 attrs .OrganizationID = value .GetStringValue ()
233- case "user.account_uuid" :
236+ case "user.account_uuid" , "user.account_id" :
234237 attrs .UserAccountUUID = value .GetStringValue ()
235238 case "terminal.type" :
236239 attrs .TerminalType = value .GetStringValue ()
@@ -268,6 +271,7 @@ func extractResourceAttributes(resource *resourcev1.Resource) *model.AICodeOtelR
268271func applyResourceAttributesToMetric (metric * model.AICodeOtelMetric , attrs * model.AICodeOtelResourceAttributes ) {
269272 // Standard resource attributes
270273 metric .SessionID = attrs .SessionID
274+ metric .ConversationID = attrs .ConversationID
271275 metric .UserAccountUUID = attrs .UserAccountUUID
272276 metric .OrganizationID = attrs .OrganizationID
273277 metric .TerminalType = attrs .TerminalType
@@ -291,6 +295,7 @@ func applyResourceAttributesToMetric(metric *model.AICodeOtelMetric, attrs *mode
291295func applyResourceAttributesToEvent (event * model.AICodeOtelEvent , attrs * model.AICodeOtelResourceAttributes ) {
292296 // Standard resource attributes
293297 event .SessionID = attrs .SessionID
298+ event .ConversationID = attrs .ConversationID
294299 event .UserAccountUUID = attrs .UserAccountUUID
295300 event .OrganizationID = attrs .OrganizationID
296301 event .TerminalType = attrs .TerminalType
@@ -358,6 +363,7 @@ func (p *AICodeOtelProcessor) parseMetric(m *metricsv1.Metric, resourceAttrs *mo
358363 MetricType : metricType ,
359364 Timestamp : int64 (dp .GetTimeUnixNano () / 1e9 ), // Convert to seconds
360365 Value : getDataPointValue (dp ),
366+ ClientType : source ,
361367 }
362368 // Apply resource attributes first
363369 applyResourceAttributesToMetric (& metric , resourceAttrs )
@@ -374,6 +380,7 @@ func (p *AICodeOtelProcessor) parseMetric(m *metricsv1.Metric, resourceAttrs *mo
374380 MetricType : metricType ,
375381 Timestamp : int64 (dp .GetTimeUnixNano () / 1e9 ),
376382 Value : getDataPointValue (dp ),
383+ ClientType : source ,
377384 }
378385 // Apply resource attributes first
379386 applyResourceAttributesToMetric (& metric , resourceAttrs )
@@ -393,6 +400,12 @@ func (p *AICodeOtelProcessor) parseLogRecord(lr *logsv1.LogRecord, resourceAttrs
393400 event := & model.AICodeOtelEvent {
394401 EventID : uuid .New ().String (),
395402 Timestamp : int64 (lr .GetTimeUnixNano () / 1e9 ), // Convert to seconds
403+
404+ ClientType : source ,
405+ }
406+
407+ if event .Timestamp == 0 {
408+ event .Timestamp = int64 (lr .GetObservedTimeUnixNano () / 1e9 ) // Convert to seconds
396409 }
397410
398411 // Apply resource attributes first
@@ -446,17 +459,61 @@ func (p *AICodeOtelProcessor) parseLogRecord(lr *logsv1.LogRecord, resourceAttrs
446459 slog .Debug ("AICodeOtel: Failed to parse tool_parameters" , "error" , err )
447460 }
448461 }
449- case "status_code" :
462+ case "status_code" , "http.response.status_code" :
450463 event .StatusCode = getIntFromValue (value )
451464 case "attempt" :
452465 event .Attempt = getIntFromValue (value )
466+ case "error.message" :
467+ event .Error = value .GetStringValue ()
453468 case "language" :
454469 event .Language = value .GetStringValue ()
455470 // Codex-specific fields
456471 case "reasoning_tokens" :
457472 event .ReasoningTokens = getIntFromValue (value )
458473 case "provider" :
459474 event .Provider = value .GetStringValue ()
475+ // Codex-specific fields for tool_decision
476+ case "call_id" , "callId" :
477+ event .CallID = value .GetStringValue ()
478+ // Codex-specific fields for sse_event
479+ case "event_kind" , "eventKind" :
480+ event .EventKind = value .GetStringValue ()
481+ case "tool_tokens" , "toolTokens" :
482+ event .ToolTokens = getIntFromValue (value )
483+ // Codex-specific fields for conversation_starts
484+ case "auth_mode" , "authMode" :
485+ event .AuthMode = value .GetStringValue ()
486+ case "slug" :
487+ event .Slug = value .GetStringValue ()
488+ case "context_window" , "contextWindow" :
489+ event .ContextWindow = getIntFromValue (value )
490+ case "approval_policy" , "approvalPolicy" :
491+ event .ApprovalPolicy = value .GetStringValue ()
492+ case "sandbox_policy" , "sandboxPolicy" :
493+ event .SandboxPolicy = value .GetStringValue ()
494+ case "mcp_servers" , "mcpServers" :
495+ event .MCPServers = getStringArrayFromValue (value )
496+ case "profile" :
497+ event .Profile = value .GetStringValue ()
498+ case "reasoning_enabled" , "reasoningEnabled" :
499+ event .ReasoningEnabled = getBoolFromValue (value )
500+ // Codex-specific fields for tool_result
501+ case "tool_arguments" , "toolArguments" :
502+ if jsonStr := value .GetStringValue (); jsonStr != "" {
503+ var args map [string ]interface {}
504+ if err := json .Unmarshal ([]byte (jsonStr ), & args ); err == nil {
505+ event .ToolArguments = args
506+ } else {
507+ slog .Debug ("AICodeOtel: Failed to parse tool_arguments" , "error" , err )
508+ }
509+ }
510+ case "tool_output" , "toolOutput" :
511+ event .ToolOutput = value .GetStringValue ()
512+ case "prompt_encrypted" , "promptEncrypted" :
513+ event .PromptEncrypted = getBoolFromValue (value )
514+ // Codex uses conversation.id instead of session.id
515+ case "conversation.id" , "conversationId" :
516+ event .ConversationID = value .GetStringValue ()
460517 // Log record level attributes that override resource attrs
461518 case "user.id" :
462519 event .UserID = value .GetStringValue ()
@@ -468,7 +525,7 @@ func (p *AICodeOtelProcessor) parseLogRecord(lr *logsv1.LogRecord, resourceAttrs
468525 event .AppVersion = value .GetStringValue ()
469526 case "organization.id" :
470527 event .OrganizationID = value .GetStringValue ()
471- case "user.account_uuid" :
528+ case "user.account_uuid" , "user.account_id" :
472529 event .UserAccountUUID = value .GetStringValue ()
473530 case "terminal.type" :
474531 event .TerminalType = value .GetStringValue ()
@@ -550,6 +607,10 @@ func mapEventName(name string, source string) string {
550607 return model .AICodeEventApiError
551608 case "codex.exec_command" :
552609 return model .AICodeEventExecCommand
610+ case "codex.conversation_starts" :
611+ return model .AICodeEventConversationStarts
612+ case "codex.sse_event" :
613+ return model .AICodeEventSSEEvent
553614 default :
554615 return name // Return as-is if not in our map
555616 }
@@ -611,6 +672,20 @@ func getFloatFromValue(value *commonv1.AnyValue) float64 {
611672 return 0
612673}
613674
675+ // getStringArrayFromValue extracts a string array from an OTEL value
676+ func getStringArrayFromValue (value * commonv1.AnyValue ) []string {
677+ if arr := value .GetArrayValue (); arr != nil {
678+ var result []string
679+ for _ , v := range arr .GetValues () {
680+ if s := v .GetStringValue (); s != "" {
681+ result = append (result , s )
682+ }
683+ }
684+ return result
685+ }
686+ return nil
687+ }
688+
614689// applyMetricAttribute applies an attribute to a metric
615690func applyMetricAttribute (metric * model.AICodeOtelMetric , attr * commonv1.KeyValue , metricType string ) {
616691 key := attr .GetKey ()
0 commit comments