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