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