LCOV - code coverage report
Current view: top level - lib/src/trace/export/otlp - span_transformer.dart (source / functions) Coverage Total Hit
Test: lcov.info Lines: 99.4 % 157 156
Test Date: 2026-07-11 13:35:19 Functions: - 0 0

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

Generated by: LCOV version 2.0-1