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 : show OTelLog;
6 : import 'package:fixnum/fixnum.dart';
7 :
8 : import '../../../../proto/collector/metrics/v1/metrics_service.pb.dart';
9 : import '../../../../proto/common/v1/common.pb.dart' as common_proto;
10 : import '../../../../proto/metrics/v1/metrics.pb.dart' as proto;
11 : import '../../../../proto/resource/v1/resource.pb.dart' as resource_proto;
12 : import '../../../resource/resource.dart';
13 : import '../../data/exemplar.dart';
14 : import '../../data/metric.dart';
15 : import '../../data/metric_data.dart';
16 : import '../../data/metric_point.dart';
17 : import '../metrics_sdk_config.dart';
18 :
19 : /// Utility class for transforming metric data to OTLP protobuf format.
20 : class MetricTransformer {
21 : /// Convert a whole [MetricData] batch to an OTLP
22 : /// `OtlpLogRecordTransformer.transformLogRecords`.
23 : ///
24 : /// This is exactly the request the OTLP metric exporters build before
25 : /// they send, factored out so alternative exporters and sinks can reuse
26 : /// the transform instead of re-implementing the per-metric mapping.
27 : /// Callers get wire bytes via `transformMetrics(data).writeToBuffer()`.
28 : ///
29 : /// When [MetricData.resource] is null the caller-supplied
30 : /// [fallbackResource] is used (the bundled exporters pass
31 : /// `OTel.resource(null)`); if that is also null an empty resource proto
32 : /// is emitted. The fallback is resolved by the caller so this
33 : /// transformer stays a pure leaf (no dependency on `OTel`).
34 10 : static ExportMetricsServiceRequest transformMetrics(
35 : MetricData data, {
36 : Resource? fallbackResource,
37 : MetricsExemplarFilter exemplarFilter = MetricsExemplarFilter.traceBased,
38 : }) {
39 10 : final request = ExportMetricsServiceRequest();
40 10 : final resourceMetrics = proto.ResourceMetrics();
41 :
42 10 : final effectiveResource = data.resource ?? fallbackResource;
43 10 : resourceMetrics.resource = effectiveResource != null
44 10 : ? transformResource(effectiveResource)
45 1 : : resource_proto.Resource();
46 :
47 10 : final scopeMetrics = proto.ScopeMetrics();
48 20 : scopeMetrics.scope = common_proto.InstrumentationScope(
49 : name: '@dart/dartastic_opentelemetry',
50 : version: '1.0.0',
51 : );
52 20 : for (final metric in data.metrics) {
53 10 : scopeMetrics.metrics
54 20 : .add(transformMetric(metric, exemplarFilter: exemplarFilter));
55 : }
56 :
57 20 : resourceMetrics.scopeMetrics.add(scopeMetrics);
58 20 : request.resourceMetrics.add(resourceMetrics);
59 : return request;
60 : }
61 :
62 : /// Transforms a Resource to an OTLP Resource proto.
63 12 : static resource_proto.Resource transformResource(Resource resource) {
64 12 : final resourceProto = resource_proto.Resource();
65 12 : final attributes = resource.attributes;
66 :
67 24 : resourceProto.attributes.addAll(
68 36 : attributes.toMap().entries.map(
69 30 : (entry) => _createKeyValue(entry.key, entry.value.value),
70 : ),
71 : );
72 :
73 : return resourceProto;
74 : }
75 :
76 : /// Transforms a Metric to an OTLP Metric proto.
77 11 : static proto.Metric transformMetric(Metric metric,
78 : {MetricsExemplarFilter exemplarFilter =
79 : MetricsExemplarFilter.traceBased}) {
80 11 : final metricProto = proto.Metric();
81 22 : metricProto.name = metric.name;
82 :
83 11 : if (metric.description != null) {
84 10 : metricProto.description = metric.description!;
85 : }
86 :
87 11 : if (metric.unit != null) {
88 10 : metricProto.unit = metric.unit!;
89 : }
90 :
91 11 : if (OTelLog.isLogMetrics()) {
92 10 : OTelLog.logMetric(
93 30 : 'MetricTransformer: Transforming metric ${metric.name} of type ${metric.type}',
94 : );
95 : }
96 :
97 : // Set data based on metric type
98 11 : switch (metric.type) {
99 11 : case MetricType.histogram:
100 : // Histogram metric
101 3 : final histogramDataPoints = <proto.HistogramDataPoint>[];
102 6 : for (final point in metric.points) {
103 6 : if (point.value is HistogramValue) {
104 3 : final dataPoint = _createHistogramDataPoint(point, exemplarFilter);
105 3 : histogramDataPoints.add(dataPoint);
106 : }
107 : }
108 :
109 : // Create a new histogram with the correct temporality and data points
110 3 : final histogram = proto.Histogram(
111 6 : aggregationTemporality: metric.temporality ==
112 : AggregationTemporality.delta
113 : ? proto.AggregationTemporality.AGGREGATION_TEMPORALITY_DELTA
114 : : proto.AggregationTemporality.AGGREGATION_TEMPORALITY_CUMULATIVE,
115 : dataPoints: histogramDataPoints,
116 : );
117 :
118 3 : metricProto.histogram = histogram;
119 : break;
120 :
121 11 : case MetricType.sum:
122 : // Sum metric
123 11 : final numberDataPoints = <proto.NumberDataPoint>[];
124 22 : for (final point in metric.points) {
125 11 : final dataPoint = _createNumberDataPoint(point, exemplarFilter);
126 11 : numberDataPoints.add(dataPoint);
127 : }
128 :
129 : // Create a new sum with the correct temporality and data points
130 11 : final sum = proto.Sum(
131 11 : isMonotonic: metric.isMonotonic ??
132 : true, // Assuming sum metrics are monotonic by default
133 22 : aggregationTemporality: metric.temporality ==
134 : AggregationTemporality.delta
135 : ? proto.AggregationTemporality.AGGREGATION_TEMPORALITY_DELTA
136 : : proto.AggregationTemporality.AGGREGATION_TEMPORALITY_CUMULATIVE,
137 : dataPoints: numberDataPoints,
138 : );
139 :
140 11 : metricProto.sum = sum;
141 : break;
142 :
143 4 : case MetricType.gauge:
144 : // Gauge metric
145 4 : final numberDataPoints = <proto.NumberDataPoint>[];
146 8 : for (final point in metric.points) {
147 4 : final dataPoint = _createNumberDataPoint(point, exemplarFilter);
148 4 : numberDataPoints.add(dataPoint);
149 : }
150 :
151 : // Create a new gauge with the data points
152 4 : final gauge = proto.Gauge(dataPoints: numberDataPoints);
153 4 : metricProto.gauge = gauge;
154 : break;
155 : }
156 :
157 : return metricProto;
158 : }
159 :
160 : /// Creates a histogram data point for the given MetricPoint.
161 3 : static proto.HistogramDataPoint _createHistogramDataPoint(
162 : MetricPoint<dynamic> point,
163 : MetricsExemplarFilter exemplarFilter,
164 : ) {
165 3 : final histogramValue = point.value as HistogramValue;
166 :
167 : // Prepare attributes
168 6 : final attributes = point.attributes.toMap();
169 3 : final attributeKeyValues = attributes.entries
170 13 : .map((entry) => _createKeyValue(entry.key, entry.value.value))
171 3 : .toList();
172 :
173 : // Prepare exemplars if available
174 : final exemplars =
175 8 : _transformExemplars(point.exemplars?.cast<Exemplar>(), exemplarFilter);
176 :
177 : // Create bucket counts as Int64 list
178 : final bucketCountsInt64 =
179 9 : histogramValue.bucketCounts.map(Int64.new).toList();
180 :
181 : // Create the HistogramDataPoint with all fields set
182 3 : return proto.HistogramDataPoint(
183 : attributes: attributeKeyValues,
184 12 : startTimeUnixNano: Int64(point.startTime.microsecondsSinceEpoch * 1000),
185 12 : timeUnixNano: Int64(point.endTime.microsecondsSinceEpoch * 1000),
186 6 : count: Int64(histogramValue.count),
187 6 : sum: histogramValue.sum.toDouble(),
188 : bucketCounts: bucketCountsInt64,
189 6 : explicitBounds: List<double>.from(histogramValue.boundaries),
190 : exemplars: exemplars,
191 6 : min: histogramValue.min?.toDouble(),
192 6 : max: histogramValue.max?.toDouble(),
193 : );
194 : }
195 :
196 : /// Creates a number data point for the given MetricPoint.
197 11 : static proto.NumberDataPoint _createNumberDataPoint(
198 : MetricPoint<dynamic> point,
199 : MetricsExemplarFilter exemplarFilter,
200 : ) {
201 : // Prepare attributes
202 22 : final attributes = point.attributes.toMap();
203 11 : final attributeKeyValues = attributes.entries
204 51 : .map((entry) => _createKeyValue(entry.key, entry.value.value))
205 11 : .toList();
206 :
207 : // Prepare exemplars if available
208 : final exemplars =
209 26 : _transformExemplars(point.exemplars?.cast<Exemplar>(), exemplarFilter);
210 :
211 : // Create the NumberDataPoint with all fields set
212 11 : return proto.NumberDataPoint(
213 : attributes: attributeKeyValues,
214 44 : startTimeUnixNano: Int64(point.startTime.microsecondsSinceEpoch * 1000),
215 44 : timeUnixNano: Int64(point.endTime.microsecondsSinceEpoch * 1000),
216 22 : asDouble: (point.value is num)
217 22 : ? (point.value as num).toDouble()
218 3 : : double.tryParse(point.value.toString()) ?? 0.0,
219 : exemplars: exemplars,
220 : );
221 : }
222 :
223 : /// Creates a KeyValue proto from a key and value.
224 12 : static common_proto.KeyValue _createKeyValue(String key, dynamic value) {
225 12 : final keyValue = common_proto.KeyValue();
226 12 : keyValue.key = key;
227 :
228 12 : if (value is String) {
229 24 : keyValue.value = common_proto.AnyValue(stringValue: value);
230 2 : } else if (value is bool) {
231 4 : keyValue.value = common_proto.AnyValue(boolValue: value);
232 2 : } else if (value is int) {
233 6 : keyValue.value = common_proto.AnyValue(intValue: Int64(value));
234 2 : } else if (value is double) {
235 4 : keyValue.value = common_proto.AnyValue(doubleValue: value);
236 1 : } else if (value is List) {
237 1 : final arrayValue = common_proto.ArrayValue();
238 2 : for (final item in value) {
239 1 : final anyValue = common_proto.AnyValue();
240 1 : if (item is String) {
241 1 : anyValue.stringValue = item;
242 1 : } else if (item is bool) {
243 1 : anyValue.boolValue = item;
244 1 : } else if (item is int) {
245 2 : anyValue.intValue = Int64(item);
246 1 : } else if (item is double) {
247 1 : anyValue.doubleValue = item;
248 : }
249 2 : arrayValue.values.add(anyValue);
250 : }
251 2 : keyValue.value = common_proto.AnyValue(arrayValue: arrayValue);
252 : } else {
253 : // Default to string representation for unsupported types
254 0 : keyValue.value = common_proto.AnyValue(stringValue: value.toString());
255 : }
256 :
257 : return keyValue;
258 : }
259 :
260 11 : static List<proto.Exemplar> _transformExemplars(
261 : List<Exemplar>? exemplars, MetricsExemplarFilter exemplarFilter) {
262 4 : if (exemplars == null || exemplars.isEmpty) {
263 : return const [];
264 : }
265 :
266 4 : return exemplars.where((exemplar) {
267 : switch (exemplarFilter) {
268 2 : case MetricsExemplarFilter.alwaysOn:
269 : return true;
270 1 : case MetricsExemplarFilter.alwaysOff:
271 : return false;
272 1 : case MetricsExemplarFilter.traceBased:
273 1 : return exemplar.traceId != null &&
274 2 : exemplar.traceId!.isValid &&
275 1 : exemplar.spanId != null &&
276 2 : exemplar.spanId!.isValid;
277 : }
278 4 : }).map((exemplar) {
279 2 : final protoExemplar = proto.Exemplar(
280 8 : timeUnixNano: Int64(exemplar.timestamp.microsecondsSinceEpoch * 1000),
281 : );
282 :
283 2 : final val = exemplar.value;
284 2 : if (val is int) {
285 0 : protoExemplar.asInt = Int64(val);
286 : } else {
287 4 : protoExemplar.asDouble = val.toDouble();
288 : }
289 :
290 4 : if (exemplar.traceId != null && exemplar.traceId!.isValid) {
291 3 : protoExemplar.traceId = exemplar.traceId!.bytes;
292 : }
293 4 : if (exemplar.spanId != null && exemplar.spanId!.isValid) {
294 3 : protoExemplar.spanId = exemplar.spanId!.bytes;
295 : }
296 :
297 4 : if (!exemplar.filteredAttributes.isEmpty) {
298 0 : protoExemplar.filteredAttributes.addAll(
299 0 : exemplar.filteredAttributes.toMap().entries.map(
300 0 : (entry) => _createKeyValue(entry.key, entry.value.value),
301 : ),
302 : );
303 : }
304 :
305 : return protoExemplar;
306 2 : }).toList(growable: false);
307 : }
308 : }
|