55 "encoding/json"
66 "log/slog"
77 "os"
8- "time "
8+ "strconv "
99
1010 "github.com/google/uuid"
1111 "github.com/malamtime/cli/model"
@@ -54,14 +54,15 @@ func (p *CCOtelProcessor) ProcessMetrics(ctx context.Context, req *collmetricsv1
5454 continue
5555 }
5656
57- session := extractSessionFromResource (resource )
57+ // Extract resource attributes once for all metrics in this resource
58+ resourceAttrs := extractResourceAttributes (resource )
5859 project := p .detectProject (resource )
5960
6061 var metrics []model.CCOtelMetric
6162
6263 for _ , sm := range rm .GetScopeMetrics () {
6364 for _ , m := range sm .GetMetrics () {
64- parsedMetrics := p .parseMetric (m )
65+ parsedMetrics := p .parseMetric (m , resourceAttrs )
6566 metrics = append (metrics , parsedMetrics ... )
6667 }
6768 }
@@ -70,11 +71,10 @@ func (p *CCOtelProcessor) ProcessMetrics(ctx context.Context, req *collmetricsv1
7071 continue
7172 }
7273
73- // Build and send request immediately
74+ // Build and send request immediately - flat structure without session
7475 ccReq := & model.CCOtelRequest {
7576 Host : p .hostname ,
7677 Project : project ,
77- Session : session ,
7878 Metrics : metrics ,
7979 }
8080
@@ -103,14 +103,15 @@ func (p *CCOtelProcessor) ProcessLogs(ctx context.Context, req *collogsv1.Export
103103 continue
104104 }
105105
106- session := extractSessionFromResource (resource )
106+ // Extract resource attributes once for all events in this resource
107+ resourceAttrs := extractResourceAttributes (resource )
107108 project := p .detectProject (resource )
108109
109110 var events []model.CCOtelEvent
110111
111112 for _ , sl := range rl .GetScopeLogs () {
112113 for _ , lr := range sl .GetLogRecords () {
113- event := p .parseLogRecord (lr )
114+ event := p .parseLogRecord (lr , resourceAttrs )
114115 if event != nil {
115116 events = append (events , * event )
116117 }
@@ -121,11 +122,10 @@ func (p *CCOtelProcessor) ProcessLogs(ctx context.Context, req *collogsv1.Export
121122 continue
122123 }
123124
124- // Build and send request immediately
125+ // Build and send request immediately - flat structure without session
125126 ccReq := & model.CCOtelRequest {
126127 Host : p .hostname ,
127128 Project : project ,
128- Session : session ,
129129 Events : events ,
130130 }
131131
@@ -155,50 +155,105 @@ func isClaudeCodeResource(resource *resourcev1.Resource) bool {
155155 return false
156156}
157157
158- // extractSessionFromResource extracts session info from resource attributes
159- func extractSessionFromResource (resource * resourcev1.Resource ) * model.CCOtelSession {
160- session := & model.CCOtelSession {
161- StartedAt : time .Now ().Unix (),
162- }
158+ // extractResourceAttributes extracts resource-level attributes from OTEL resource
159+ // Returns a struct that can be used to populate metrics and events
160+ func extractResourceAttributes (resource * resourcev1.Resource ) * model.CCOtelResourceAttributes {
161+ attrs := & model.CCOtelResourceAttributes {}
163162
164163 if resource == nil {
165- return session
164+ return attrs
166165 }
167166
168167 for _ , attr := range resource .GetAttributes () {
169168 key := attr .GetKey ()
170169 value := attr .GetValue ()
171170
172171 switch key {
172+ // Standard resource attributes
173173 case "session.id" :
174- session .SessionID = value .GetStringValue ()
174+ attrs .SessionID = value .GetStringValue ()
175175 case "app.version" :
176- session .AppVersion = value .GetStringValue ()
176+ attrs .AppVersion = value .GetStringValue ()
177177 case "organization.id" :
178- session .OrganizationID = value .GetStringValue ()
178+ attrs .OrganizationID = value .GetStringValue ()
179179 case "user.account_uuid" :
180- session .UserAccountUUID = value .GetStringValue ()
180+ attrs .UserAccountUUID = value .GetStringValue ()
181181 case "terminal.type" :
182- session .TerminalType = value .GetStringValue ()
182+ attrs .TerminalType = value .GetStringValue ()
183183 case "service.version" :
184- session .ServiceVersion = value .GetStringValue ()
184+ attrs .ServiceVersion = value .GetStringValue ()
185185 case "os.type" :
186- session .OSType = value .GetStringValue ()
186+ attrs .OSType = value .GetStringValue ()
187187 case "os.version" :
188- session .OSVersion = value .GetStringValue ()
188+ attrs .OSVersion = value .GetStringValue ()
189189 case "host.arch" :
190- session .HostArch = value .GetStringValue ()
190+ attrs .HostArch = value .GetStringValue ()
191191 case "wsl.version" :
192- session .WSLVersion = value .GetStringValue ()
192+ attrs .WSLVersion = value .GetStringValue ()
193+ // Additional identifiers
194+ case "user.id" :
195+ attrs .UserID = value .GetStringValue ()
196+ case "user.email" :
197+ attrs .UserEmail = value .GetStringValue ()
198+ // Custom resource attributes (from OTEL_RESOURCE_ATTRIBUTES)
199+ case "user.name" :
200+ attrs .UserName = value .GetStringValue ()
201+ case "machine.name" :
202+ attrs .MachineName = value .GetStringValue ()
203+ case "team.id" :
204+ attrs .TeamID = value .GetStringValue ()
205+ case "pwd" :
206+ attrs .Pwd = value .GetStringValue ()
193207 }
194208 }
195209
196- // Generate session ID if not present
197- if session .SessionID == "" {
198- session .SessionID = uuid .New ().String ()
199- }
210+ return attrs
211+ }
200212
201- return session
213+ // applyResourceAttributesToMetric copies resource attributes into a metric
214+ func applyResourceAttributesToMetric (metric * model.CCOtelMetric , attrs * model.CCOtelResourceAttributes ) {
215+ // Standard resource attributes
216+ metric .SessionID = attrs .SessionID
217+ metric .UserAccountUUID = attrs .UserAccountUUID
218+ metric .OrganizationID = attrs .OrganizationID
219+ metric .TerminalType = attrs .TerminalType
220+ metric .AppVersion = attrs .AppVersion
221+ metric .OSType = attrs .OSType
222+ metric .OSVersion = attrs .OSVersion
223+ metric .HostArch = attrs .HostArch
224+
225+ // Additional identifiers
226+ metric .UserID = attrs .UserID
227+ metric .UserEmail = attrs .UserEmail
228+
229+ // Custom resource attributes
230+ metric .UserName = attrs .UserName
231+ metric .MachineName = attrs .MachineName
232+ metric .TeamID = attrs .TeamID
233+ metric .Pwd = attrs .Pwd
234+ }
235+
236+ // applyResourceAttributesToEvent copies resource attributes into an event
237+ func applyResourceAttributesToEvent (event * model.CCOtelEvent , attrs * model.CCOtelResourceAttributes ) {
238+ // Standard resource attributes
239+ event .SessionID = attrs .SessionID
240+ event .UserAccountUUID = attrs .UserAccountUUID
241+ event .OrganizationID = attrs .OrganizationID
242+ event .TerminalType = attrs .TerminalType
243+ event .AppVersion = attrs .AppVersion
244+ event .OSType = attrs .OSType
245+ event .OSVersion = attrs .OSVersion
246+ event .HostArch = attrs .HostArch
247+
248+ // Additional identifiers
249+ event .UserID = attrs .UserID
250+ event .UserEmail = attrs .UserEmail
251+
252+ // Custom resource attributes
253+ event .UserName = attrs .UserName
254+ event .MachineName = attrs .MachineName
255+ event .TeamID = attrs .TeamID
256+ event .Pwd = attrs .Pwd
202257}
203258
204259// detectProject extracts project from resource attributes or environment
@@ -224,7 +279,7 @@ func (p *CCOtelProcessor) detectProject(resource *resourcev1.Resource) string {
224279}
225280
226281// parseMetric parses an OTEL metric into CCOtelMetric(s)
227- func (p * CCOtelProcessor ) parseMetric (m * metricsv1.Metric ) []model.CCOtelMetric {
282+ func (p * CCOtelProcessor ) parseMetric (m * metricsv1.Metric , resourceAttrs * model. CCOtelResourceAttributes ) []model.CCOtelMetric {
228283 var metrics []model.CCOtelMetric
229284
230285 name := m .GetName ()
@@ -243,7 +298,9 @@ func (p *CCOtelProcessor) parseMetric(m *metricsv1.Metric) []model.CCOtelMetric
243298 Timestamp : int64 (dp .GetTimeUnixNano () / 1e9 ), // Convert to seconds
244299 Value : getDataPointValue (dp ),
245300 }
246- // Extract attributes
301+ // Apply resource attributes first
302+ applyResourceAttributesToMetric (& metric , resourceAttrs )
303+ // Then extract data point attributes (can override resource attrs)
247304 for _ , attr := range dp .GetAttributes () {
248305 applyMetricAttribute (& metric , attr , metricType )
249306 }
@@ -257,6 +314,9 @@ func (p *CCOtelProcessor) parseMetric(m *metricsv1.Metric) []model.CCOtelMetric
257314 Timestamp : int64 (dp .GetTimeUnixNano () / 1e9 ),
258315 Value : getDataPointValue (dp ),
259316 }
317+ // Apply resource attributes first
318+ applyResourceAttributesToMetric (& metric , resourceAttrs )
319+ // Then extract data point attributes (can override resource attrs)
260320 for _ , attr := range dp .GetAttributes () {
261321 applyMetricAttribute (& metric , attr , metricType )
262322 }
@@ -268,34 +328,39 @@ func (p *CCOtelProcessor) parseMetric(m *metricsv1.Metric) []model.CCOtelMetric
268328}
269329
270330// parseLogRecord parses an OTEL log record into a CCOtelEvent
271- func (p * CCOtelProcessor ) parseLogRecord (lr * logsv1.LogRecord ) * model.CCOtelEvent {
331+ func (p * CCOtelProcessor ) parseLogRecord (lr * logsv1.LogRecord , resourceAttrs * model. CCOtelResourceAttributes ) * model.CCOtelEvent {
272332 event := & model.CCOtelEvent {
273333 EventID : uuid .New ().String (),
274334 Timestamp : int64 (lr .GetTimeUnixNano () / 1e9 ), // Convert to seconds
275335 }
276336
277- // Extract event type and other attributes
337+ // Apply resource attributes first
338+ applyResourceAttributesToEvent (event , resourceAttrs )
339+
340+ // Extract event type and other attributes from log record
278341 for _ , attr := range lr .GetAttributes () {
279342 key := attr .GetKey ()
280343 value := attr .GetValue ()
281344
282345 switch key {
283346 case "event.name" :
284347 event .EventType = mapEventName (value .GetStringValue ())
348+ case "event.timestamp" :
349+ event .EventTimestamp = value .GetStringValue ()
285350 case "model" :
286351 event .Model = value .GetStringValue ()
287352 case "cost_usd" :
288- event .CostUSD = value . GetDoubleValue ( )
353+ event .CostUSD = getFloatFromValue ( value )
289354 case "duration_ms" :
290- event .DurationMs = int (value . GetIntValue () )
355+ event .DurationMs = getIntFromValue (value )
291356 case "input_tokens" :
292- event .InputTokens = int (value . GetIntValue () )
357+ event .InputTokens = getIntFromValue (value )
293358 case "output_tokens" :
294- event .OutputTokens = int (value . GetIntValue () )
359+ event .OutputTokens = getIntFromValue (value )
295360 case "cache_read_tokens" :
296- event .CacheReadTokens = int (value . GetIntValue () )
361+ event .CacheReadTokens = getIntFromValue (value )
297362 case "cache_creation_tokens" :
298- event .CacheCreationTokens = int (value . GetIntValue () )
363+ event .CacheCreationTokens = getIntFromValue (value )
299364 case "tool_name" :
300365 event .ToolName = value .GetStringValue ()
301366 case "success" :
@@ -307,7 +372,7 @@ func (p *CCOtelProcessor) parseLogRecord(lr *logsv1.LogRecord) *model.CCOtelEven
307372 case "error" :
308373 event .Error = value .GetStringValue ()
309374 case "prompt_length" :
310- event .PromptLength = int (value . GetIntValue () )
375+ event .PromptLength = getIntFromValue (value )
311376 case "prompt" :
312377 event .Prompt = value .GetStringValue ()
313378 case "tool_parameters" :
@@ -321,11 +386,26 @@ func (p *CCOtelProcessor) parseLogRecord(lr *logsv1.LogRecord) *model.CCOtelEven
321386 }
322387 }
323388 case "status_code" :
324- event .StatusCode = int (value . GetIntValue () )
389+ event .StatusCode = getIntFromValue (value )
325390 case "attempt" :
326- event .Attempt = int (value . GetIntValue () )
391+ event .Attempt = getIntFromValue (value )
327392 case "language" :
328393 event .Language = value .GetStringValue ()
394+ // Log record level attributes that override resource attrs
395+ case "user.id" :
396+ event .UserID = value .GetStringValue ()
397+ case "user.email" :
398+ event .UserEmail = value .GetStringValue ()
399+ case "session.id" :
400+ event .SessionID = value .GetStringValue ()
401+ case "app.version" :
402+ event .AppVersion = value .GetStringValue ()
403+ case "organization.id" :
404+ event .OrganizationID = value .GetStringValue ()
405+ case "user.account_uuid" :
406+ event .UserAccountUUID = value .GetStringValue ()
407+ case "terminal.type" :
408+ event .TerminalType = value .GetStringValue ()
329409 }
330410 }
331411
@@ -391,6 +471,36 @@ func getDataPointValue(dp *metricsv1.NumberDataPoint) float64 {
391471 }
392472}
393473
474+ // getIntFromValue extracts an int from an OTEL value, handling both int and string formats
475+ func getIntFromValue (value * commonv1.AnyValue ) int {
476+ // First try to get as int
477+ if intVal := value .GetIntValue (); intVal != 0 {
478+ return int (intVal )
479+ }
480+ // Try to parse from string (Claude Code sends some values as strings)
481+ if strVal := value .GetStringValue (); strVal != "" {
482+ if parsed , err := strconv .Atoi (strVal ); err == nil {
483+ return parsed
484+ }
485+ }
486+ return 0
487+ }
488+
489+ // getFloatFromValue extracts a float64 from an OTEL value, handling both double and string formats
490+ func getFloatFromValue (value * commonv1.AnyValue ) float64 {
491+ // First try to get as double
492+ if doubleVal := value .GetDoubleValue (); doubleVal != 0 {
493+ return doubleVal
494+ }
495+ // Try to parse from string (Claude Code sends some values as strings)
496+ if strVal := value .GetStringValue (); strVal != "" {
497+ if parsed , err := strconv .ParseFloat (strVal , 64 ); err == nil {
498+ return parsed
499+ }
500+ }
501+ return 0
502+ }
503+
394504// applyMetricAttribute applies an attribute to a metric
395505func applyMetricAttribute (metric * model.CCOtelMetric , attr * commonv1.KeyValue , metricType string ) {
396506 key := attr .GetKey ()
@@ -411,5 +521,27 @@ func applyMetricAttribute(metric *model.CCOtelMetric, attr *commonv1.KeyValue, m
411521 metric .Decision = value .GetStringValue ()
412522 case "language" :
413523 metric .Language = value .GetStringValue ()
524+ // Resource attributes at data point level - apply them (override if already set from resource)
525+ case "session.id" :
526+ metric .SessionID = value .GetStringValue ()
527+ case "user.account_uuid" :
528+ metric .UserAccountUUID = value .GetStringValue ()
529+ case "organization.id" :
530+ metric .OrganizationID = value .GetStringValue ()
531+ case "terminal.type" :
532+ metric .TerminalType = value .GetStringValue ()
533+ case "app.version" :
534+ metric .AppVersion = value .GetStringValue ()
535+ case "os.type" :
536+ metric .OSType = value .GetStringValue ()
537+ case "os.version" :
538+ metric .OSVersion = value .GetStringValue ()
539+ case "host.arch" :
540+ metric .HostArch = value .GetStringValue ()
541+ // Additional identifiers at data point level
542+ case "user.id" :
543+ metric .UserID = value .GetStringValue ()
544+ case "user.email" :
545+ metric .UserEmail = value .GetStringValue ()
414546 }
415547}
0 commit comments