LCOV - code coverage report
Current view: top level - src/trace/export/otlp - span_transformer.dart (source / functions) Coverage Total Hit
Test: lcov.info Lines: 99.4 % 154 153
Test Date: 2025-11-15 13:23:01 Functions: - 0 0

            Line data    Source code
       1              : // Licensed under the Apache License, Version 2.0
       2              : // Copyright 2025, Michael Bushe, All rights reserved.
       3              : 
       4              : // ignore_for_file: invalid_use_of_visible_for_testing_member
       5              : 
       6              : import 'package:dartastic_opentelemetry_api/dartastic_opentelemetry_api.dart';
       7              : import 'package:fixnum/fixnum.dart';
       8              : 
       9              : import '../../../../proto/opentelemetry_proto_dart.dart' as proto;
      10              : import '../../../otel.dart';
      11              : import '../../span.dart';
      12              : 
      13              : /// Transforms internal span representation to OTLP format
      14              : class OtlpSpanTransformer {
      15              :   /// Convert a list of spans to OTLP ExportTraceServiceRequest
      16           20 :   static proto.ExportTraceServiceRequest transformSpans(List<Span> spans) {
      17           20 :     final exportTraceServiceRequest = proto.ExportTraceServiceRequest();
      18           20 :     if (spans.isEmpty) return exportTraceServiceRequest;
      19              : 
      20              :     // Group spans by their resource first
      21           20 :     final resourceGroups = <String, List<Span>>{};
      22              : 
      23           40 :     for (final span in spans) {
      24           20 :       final resource = span.resource;
      25              :       final key = resource != null
      26           40 :           ? _getResourceServiceName(resource.attributes)
      27              :           : 'default-service';
      28           80 :       resourceGroups.putIfAbsent(key, () => []).add(span);
      29              :     }
      30              : 
      31              :     // Process each resource group
      32           40 :     for (final resourceEntry in resourceGroups.entries) {
      33           20 :       final spanList = resourceEntry.value;
      34           20 :       if (spanList.isEmpty) continue;
      35              : 
      36              :       // Extract resource attributes from the span's resource
      37           40 :       final resource = spanList.first.resource;
      38           20 :       final resourceAttrs = resource?.attributes ?? OTel.createAttributes();
      39              : 
      40           20 :       if (OTelLog.isDebug()) {
      41           20 :         OTelLog.debug('Extracting resource attributes for export:');
      42           60 :         resourceAttrs.toList().forEach((attr) {
      43           80 :           if (attr.key == 'tenant_id' || attr.key == 'service.name') {
      44           80 :             OTelLog.debug('  ${attr.key}: ${attr.value}');
      45              :           }
      46              :         });
      47              :       }
      48              : 
      49           20 :       if (OTelLog.isDebug()) {
      50           20 :         OTelLog.debug('Extracting resource attributes for export:');
      51           60 :         resourceAttrs.toList().forEach((attr) {
      52           80 :           if (attr.key == 'tenant_id' || attr.key == 'service.name') {
      53           80 :             OTelLog.debug('  ${attr.key}: ${attr.value}');
      54              :           }
      55              :         });
      56              :       }
      57              : 
      58              :       // Create resource
      59           20 :       final protoResource = proto.Resource()
      60           60 :         ..attributes.addAll(transformAttributeMap(resourceAttrs));
      61              : 
      62              :       // Group spans by instrumentation scope
      63           20 :       final scopeGroups = <String, List<Span>>{};
      64           40 :       for (final span in spanList) {
      65           20 :         final scopeKey = _instrumentationKey(span);
      66           80 :         scopeGroups.putIfAbsent(scopeKey, () => []).add(span);
      67              :       }
      68              : 
      69              :       // Create ResourceSpans
      70           40 :       final resourceSpan = proto.ResourceSpans()..resource = protoResource;
      71              : 
      72              :       // Process each instrumentation scope group
      73           40 :       for (final scopeEntry in scopeGroups.entries) {
      74           20 :         final scopeSpanList = scopeEntry.value;
      75           20 :         if (scopeSpanList.isEmpty) continue;
      76              : 
      77              :         // Get instrumentation scope information from the first span
      78           40 :         final scope = scopeSpanList.first.instrumentationScope;
      79           60 :         final otlpScope = proto.InstrumentationScope()..name = scope.name;
      80           20 :         if (scope.version != null) {
      81           40 :           otlpScope.version = scope.version!;
      82              :         }
      83              : 
      84              :         // Transform all spans in this scope to OTLP format
      85           20 :         final otlpSpans = <proto.Span>[];
      86           40 :         for (final span in scopeSpanList) {
      87           40 :           otlpSpans.add(transformSpan(span));
      88              :         }
      89              : 
      90              :         // Create ScopeSpans
      91           20 :         final otlpScopeSpans = proto.ScopeSpans()
      92           20 :           ..scope = otlpScope
      93           40 :           ..spans.addAll(otlpSpans);
      94              : 
      95           40 :         resourceSpan.scopeSpans.add(otlpScopeSpans);
      96              :       }
      97              : 
      98           40 :       exportTraceServiceRequest.resourceSpans.add(resourceSpan);
      99              :     }
     100              : 
     101              :     return exportTraceServiceRequest;
     102              :   }
     103              : 
     104              :   /// Get service name from resource attributes
     105           20 :   static String _getResourceServiceName(Attributes attributes) {
     106           40 :     for (final attr in attributes.toList()) {
     107           40 :       if (attr.key == 'service.name') {
     108           40 :         return attr.value.toString();
     109              :       }
     110              :     }
     111              :     return 'default-service';
     112              :   }
     113              : 
     114              :   /// Creates a key for grouping spans by instrumentation scope
     115           20 :   static String _instrumentationKey(Span span) {
     116           20 :     final scope = span.instrumentationScope;
     117           60 :     return '${scope.name}:${scope.version ?? ''}';
     118              :   }
     119              : 
     120              :   /// Convert a single span to OTLP Span
     121           20 :   static proto.Span transformSpan(Span span) {
     122           20 :     if (OTelLog.isDebug()) {
     123           60 :       OTelLog.debug('Transforming span: ${span.name}');
     124              :     }
     125           20 :     final context = span.spanContext;
     126              : 
     127           20 :     final otlpSpan = proto.Span()
     128           60 :       ..traceId = context.traceId.bytes
     129           60 :       ..spanId = context.spanId.bytes
     130           40 :       ..name = span.name
     131           60 :       ..kind = transformSpanKind(span.kind)
     132          100 :       ..startTimeUnixNano = Int64(span.startTime.microsecondsSinceEpoch * 1000);
     133              : 
     134           20 :     if (span.endTime != null) {
     135           17 :       otlpSpan.endTimeUnixNano =
     136           68 :           Int64(span.endTime!.microsecondsSinceEpoch * 1000);
     137              :     }
     138              : 
     139              :     // First check if we have a parent span
     140           20 :     final parentSpan = span.parentSpan;
     141           28 :     if (parentSpan != null && parentSpan.spanContext.isValid) {
     142           14 :       if (OTelLog.isDebug()) {
     143           42 :         OTelLog.debug('Setting parentSpanId from parentSpan for ${span.name}');
     144              :       }
     145           56 :       otlpSpan.parentSpanId = parentSpan.spanContext.spanId.bytes;
     146              :     }
     147              :     // If we don't have a parent span but have a parent span ID in our context
     148           60 :     else if (context.parentSpanId != null && context.parentSpanId!.isValid) {
     149            1 :       if (OTelLog.isDebug()) {
     150            3 :         OTelLog.debug('Setting parentSpanId from context for ${span.name}');
     151              :       }
     152            3 :       otlpSpan.parentSpanId = context.parentSpanId!.bytes;
     153              :     }
     154              : 
     155              :     // Add attributes
     156           20 :     final attrs = span.attributes;
     157           60 :     otlpSpan.attributes.addAll(transformAttributeMap(attrs));
     158              : 
     159              :     // Add events
     160           20 :     final events = span.spanEvents;
     161            4 :     if (events != null && events.isNotEmpty) {
     162           12 :       otlpSpan.events.addAll(transformEvents(events));
     163              :     }
     164              : 
     165              :     // Add links
     166           20 :     final links = span.spanLinks;
     167            3 :     if (links != null && links.isNotEmpty) {
     168            9 :       otlpSpan.links.addAll(transformLinks(links));
     169              :     }
     170              : 
     171              :     // Add status
     172           20 :     final status = span.status;
     173           60 :     otlpSpan.status = transformStatus(status, span.statusDescription);
     174              : 
     175              :     return otlpSpan;
     176              :   }
     177              : 
     178              :   /// Convert span status to OTLP Status
     179           20 :   static proto.Status transformStatus(
     180              :       SpanStatusCode status, String? description) {
     181           20 :     final otlpStatus = proto.Status();
     182              : 
     183              :     switch (status) {
     184           20 :       case SpanStatusCode.Ok:
     185           17 :         otlpStatus.code = proto.Status_StatusCode.STATUS_CODE_OK;
     186              :         break;
     187            6 :       case SpanStatusCode.Error:
     188            3 :         otlpStatus.code = proto.Status_StatusCode.STATUS_CODE_ERROR;
     189              :         // TODO The OTel spec requires the description for error statuses
     190              :         if (description != null) {
     191            3 :           otlpStatus.message = description;
     192              :         }
     193              :         break;
     194            4 :       case SpanStatusCode.Unset:
     195            4 :         otlpStatus.code = proto.Status_StatusCode.STATUS_CODE_UNSET;
     196              :         break;
     197              :     }
     198              : 
     199              :     return otlpStatus;
     200              :   }
     201              : 
     202              :   /// Convert span kind to OTLP SpanKind
     203           20 :   static proto.Span_SpanKind transformSpanKind(SpanKind kind) {
     204              :     switch (kind) {
     205           20 :       case SpanKind.internal:
     206              :         return proto.Span_SpanKind.SPAN_KIND_INTERNAL;
     207            2 :       case SpanKind.server:
     208              :         return proto.Span_SpanKind.SPAN_KIND_SERVER;
     209            2 :       case SpanKind.client:
     210              :         return proto.Span_SpanKind.SPAN_KIND_CLIENT;
     211            1 :       case SpanKind.producer:
     212              :         return proto.Span_SpanKind.SPAN_KIND_PRODUCER;
     213            1 :       case SpanKind.consumer:
     214              :         return proto.Span_SpanKind.SPAN_KIND_CONSUMER;
     215              :     }
     216              :   }
     217              : 
     218              :   /// Convert events to OTLP Event list
     219            4 :   static List<proto.Span_Event> transformEvents(List<SpanEvent> events) {
     220            8 :     return events.map((event) {
     221            4 :       final spanEvent = proto.Span_Event()
     222           20 :         ..timeUnixNano = Int64(event.timestamp.microsecondsSinceEpoch * 1000)
     223            8 :         ..name = event.name;
     224              : 
     225            4 :       if (event.attributes != null) {
     226           16 :         spanEvent.attributes.addAll(transformAttributeMap(event.attributes!));
     227              :       }
     228              : 
     229              :       return spanEvent;
     230            4 :     }).toList();
     231              :   }
     232              : 
     233              :   /// Convert links to OTLP Link list
     234            3 :   static List<proto.Span_Link> transformLinks(List<dynamic> links) {
     235            6 :     return links.map((link) {
     236            3 :       final spanLink = proto.Span_Link();
     237              : 
     238            3 :       if (link is SpanLink) {
     239            3 :         final spanContext = link.spanContext;
     240              :         spanLink
     241            9 :           ..traceId = spanContext.traceId.bytes
     242            9 :           ..spanId = spanContext.spanId.bytes;
     243              : 
     244           12 :         spanLink.attributes.addAll(transformAttributeMap(link.attributes));
     245              :       }
     246              : 
     247              :       return spanLink;
     248            3 :     }).toList();
     249              :   }
     250              : 
     251              :   /// Convert attribute map to OTLP KeyValue list
     252           20 :   static List<proto.KeyValue> transformAttributeMap(Attributes attributes) {
     253           20 :     final result = <proto.KeyValue>[];
     254              : 
     255           60 :     attributes.toList().forEach((attr) {
     256           20 :       final keyValue = proto.KeyValue()
     257           40 :         ..key = attr.key
     258           40 :         ..value = _transformAttributeValue(attr);
     259              : 
     260           20 :       result.add(keyValue);
     261              :     });
     262              : 
     263              :     return result;
     264              :   }
     265              : 
     266              :   /// Convert AttributeValue to OTLP AnyValue
     267           20 :   static proto.AnyValue _transformAttributeValue(Attribute attr) {
     268           20 :     final anyValue = proto.AnyValue();
     269              : 
     270           40 :     if (attr.value is String) {
     271           40 :       anyValue.stringValue = attr.value as String;
     272           38 :     } else if (attr.value is bool) {
     273            8 :       anyValue.boolValue = attr.value as bool;
     274           38 :     } else if (attr.value is int) {
     275           57 :       anyValue.intValue = Int64(attr.value as int);
     276            8 :     } else if (attr.value is double) {
     277            6 :       anyValue.doubleValue = attr.value as double;
     278            4 :     } else if (attr.value is List<String>) {
     279            2 :       final arrayValue = proto.ArrayValue();
     280            6 :       arrayValue.values.addAll((attr.value as List<String>)
     281            8 :           .map((v) => proto.AnyValue()..stringValue = v));
     282            2 :       anyValue.arrayValue = arrayValue;
     283            4 :     } else if (attr.value is List<bool>) {
     284            1 :       final arrayValue = proto.ArrayValue();
     285            3 :       arrayValue.values.addAll((attr.value as List<bool>)
     286            4 :           .map((v) => proto.AnyValue()..boolValue = v));
     287            1 :       anyValue.arrayValue = arrayValue;
     288            4 :     } else if (attr.value is List<int>) {
     289            2 :       final arrayValue = proto.ArrayValue();
     290            6 :       arrayValue.values.addAll((attr.value as List<int>)
     291           10 :           .map((v) => proto.AnyValue()..intValue = Int64(v)));
     292            2 :       anyValue.arrayValue = arrayValue;
     293            2 :     } else if (attr.value is List<double>) {
     294            1 :       final arrayValue = proto.ArrayValue();
     295            3 :       arrayValue.values.addAll((attr.value as List<double>)
     296            4 :           .map((v) => proto.AnyValue()..doubleValue = v));
     297            1 :       anyValue.arrayValue = arrayValue;
     298              :     } else {
     299              :       // For any other type, convert to string
     300            0 :       anyValue.stringValue = attr.value.toString();
     301              :     }
     302              : 
     303              :     return anyValue;
     304              :   }
     305              : }
        

Generated by: LCOV version 2.0-1