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 : }
|