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 12 : static proto.Metric transformMetric(Metric metric,
78 : {MetricsExemplarFilter exemplarFilter =
79 : MetricsExemplarFilter.traceBased}) {
80 12 : final metricProto = proto.Metric();
81 24 : metricProto.name = metric.name;
82 :
83 12 : if (metric.description != null) {
84 10 : metricProto.description = metric.description!;
85 : }
86 :
87 12 : if (metric.unit != null) {
88 10 : metricProto.unit = metric.unit!;
89 : }
90 :
91 12 : if (OTelLog.isLogMetrics()) {
92 11 : OTelLog.logMetric(
93 33 : 'MetricTransformer: Transforming metric ${metric.name} of type ${metric.type}',
94 : );
95 : }
96 :
97 : // Set data based on metric type
98 12 : switch (metric.type) {
99 12 : 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 12 : case MetricType.sum:
122 : // Sum metric
123 12 : final numberDataPoints = <proto.NumberDataPoint>[];
124 24 : for (final point in metric.points) {
125 12 : final dataPoint = _createNumberDataPoint(point, exemplarFilter);
126 12 : numberDataPoints.add(dataPoint);
127 : }
128 :
129 : // Create a new sum with the correct temporality and data points
130 12 : final sum = proto.Sum(
131 : // Unknown monotonicity is exported as non-monotonic: a backend
132 : // reading a non-monotonic sum as monotonic treats every decrease
133 : // as a reset, which corrupts the data.
134 12 : isMonotonic: metric.isMonotonic ?? false,
135 24 : aggregationTemporality: metric.temporality ==
136 : AggregationTemporality.delta
137 : ? proto.AggregationTemporality.AGGREGATION_TEMPORALITY_DELTA
138 : : proto.AggregationTemporality.AGGREGATION_TEMPORALITY_CUMULATIVE,
139 : dataPoints: numberDataPoints,
140 : );
141 :
142 12 : metricProto.sum = sum;
143 : break;
144 :
145 4 : case MetricType.gauge:
146 : // Gauge metric
147 4 : final numberDataPoints = <proto.NumberDataPoint>[];
148 8 : for (final point in metric.points) {
149 4 : final dataPoint = _createNumberDataPoint(point, exemplarFilter);
150 4 : numberDataPoints.add(dataPoint);
151 : }
152 :
153 : // Create a new gauge with the data points
154 4 : final gauge = proto.Gauge(dataPoints: numberDataPoints);
155 4 : metricProto.gauge = gauge;
156 : break;
157 : }
158 :
159 : return metricProto;
160 : }
161 :
162 : /// Creates a histogram data point for the given MetricPoint.
163 3 : static proto.HistogramDataPoint _createHistogramDataPoint(
164 : MetricPoint<dynamic> point,
165 : MetricsExemplarFilter exemplarFilter,
166 : ) {
167 3 : final histogramValue = point.value as HistogramValue;
168 :
169 : // Prepare attributes
170 6 : final attributes = point.attributes.toMap();
171 3 : final attributeKeyValues = attributes.entries
172 13 : .map((entry) => _createKeyValue(entry.key, entry.value.value))
173 3 : .toList();
174 :
175 : // Prepare exemplars if available
176 : final exemplars =
177 8 : _transformExemplars(point.exemplars?.cast<Exemplar>(), exemplarFilter);
178 :
179 : // Create bucket counts as Int64 list
180 : final bucketCountsInt64 =
181 9 : histogramValue.bucketCounts.map(Int64.new).toList();
182 :
183 : // Create the HistogramDataPoint with all fields set
184 3 : return proto.HistogramDataPoint(
185 : attributes: attributeKeyValues,
186 12 : startTimeUnixNano: Int64(point.startTime.microsecondsSinceEpoch * 1000),
187 12 : timeUnixNano: Int64(point.endTime.microsecondsSinceEpoch * 1000),
188 6 : count: Int64(histogramValue.count),
189 6 : sum: histogramValue.sum.toDouble(),
190 : bucketCounts: bucketCountsInt64,
191 6 : explicitBounds: List<double>.from(histogramValue.boundaries),
192 : exemplars: exemplars,
193 6 : min: histogramValue.min?.toDouble(),
194 6 : max: histogramValue.max?.toDouble(),
195 : );
196 : }
197 :
198 : /// Creates a number data point for the given MetricPoint.
199 12 : static proto.NumberDataPoint _createNumberDataPoint(
200 : MetricPoint<dynamic> point,
201 : MetricsExemplarFilter exemplarFilter,
202 : ) {
203 : // Prepare attributes
204 24 : final attributes = point.attributes.toMap();
205 12 : final attributeKeyValues = attributes.entries
206 52 : .map((entry) => _createKeyValue(entry.key, entry.value.value))
207 12 : .toList();
208 :
209 : // Prepare exemplars if available
210 : final exemplars =
211 29 : _transformExemplars(point.exemplars?.cast<Exemplar>(), exemplarFilter);
212 :
213 : // Create the NumberDataPoint with all fields set
214 12 : return proto.NumberDataPoint(
215 : attributes: attributeKeyValues,
216 48 : startTimeUnixNano: Int64(point.startTime.microsecondsSinceEpoch * 1000),
217 48 : timeUnixNano: Int64(point.endTime.microsecondsSinceEpoch * 1000),
218 24 : asDouble: (point.value is num)
219 24 : ? (point.value as num).toDouble()
220 3 : : double.tryParse(point.value.toString()) ?? 0.0,
221 : exemplars: exemplars,
222 : );
223 : }
224 :
225 : /// Creates a KeyValue proto from a key and value.
226 12 : static common_proto.KeyValue _createKeyValue(String key, dynamic value) {
227 12 : final keyValue = common_proto.KeyValue();
228 12 : keyValue.key = key;
229 :
230 12 : if (value is String) {
231 24 : keyValue.value = common_proto.AnyValue(stringValue: value);
232 2 : } else if (value is bool) {
233 4 : keyValue.value = common_proto.AnyValue(boolValue: value);
234 2 : } else if (value is int) {
235 6 : keyValue.value = common_proto.AnyValue(intValue: Int64(value));
236 2 : } else if (value is double) {
237 4 : keyValue.value = common_proto.AnyValue(doubleValue: value);
238 1 : } else if (value is List) {
239 1 : final arrayValue = common_proto.ArrayValue();
240 2 : for (final item in value) {
241 1 : final anyValue = common_proto.AnyValue();
242 1 : if (item is String) {
243 1 : anyValue.stringValue = item;
244 1 : } else if (item is bool) {
245 1 : anyValue.boolValue = item;
246 1 : } else if (item is int) {
247 2 : anyValue.intValue = Int64(item);
248 1 : } else if (item is double) {
249 1 : anyValue.doubleValue = item;
250 : }
251 2 : arrayValue.values.add(anyValue);
252 : }
253 2 : keyValue.value = common_proto.AnyValue(arrayValue: arrayValue);
254 : } else {
255 : // Default to string representation for unsupported types
256 0 : keyValue.value = common_proto.AnyValue(stringValue: value.toString());
257 : }
258 :
259 : return keyValue;
260 : }
261 :
262 12 : static List<proto.Exemplar> _transformExemplars(
263 : List<Exemplar>? exemplars, MetricsExemplarFilter exemplarFilter) {
264 5 : if (exemplars == null || exemplars.isEmpty) {
265 : return const [];
266 : }
267 :
268 4 : return exemplars.where((exemplar) {
269 : switch (exemplarFilter) {
270 2 : case MetricsExemplarFilter.alwaysOn:
271 : return true;
272 1 : case MetricsExemplarFilter.alwaysOff:
273 : return false;
274 1 : case MetricsExemplarFilter.traceBased:
275 1 : return exemplar.traceId != null &&
276 2 : exemplar.traceId!.isValid &&
277 1 : exemplar.spanId != null &&
278 2 : exemplar.spanId!.isValid;
279 : }
280 4 : }).map((exemplar) {
281 2 : final protoExemplar = proto.Exemplar(
282 8 : timeUnixNano: Int64(exemplar.timestamp.microsecondsSinceEpoch * 1000),
283 : );
284 :
285 2 : final val = exemplar.value;
286 2 : if (val is int) {
287 0 : protoExemplar.asInt = Int64(val);
288 : } else {
289 4 : protoExemplar.asDouble = val.toDouble();
290 : }
291 :
292 4 : if (exemplar.traceId != null && exemplar.traceId!.isValid) {
293 3 : protoExemplar.traceId = exemplar.traceId!.bytes;
294 : }
295 4 : if (exemplar.spanId != null && exemplar.spanId!.isValid) {
296 3 : protoExemplar.spanId = exemplar.spanId!.bytes;
297 : }
298 :
299 4 : if (!exemplar.filteredAttributes.isEmpty) {
300 0 : protoExemplar.filteredAttributes.addAll(
301 0 : exemplar.filteredAttributes.toMap().entries.map(
302 0 : (entry) => _createKeyValue(entry.key, entry.value.value),
303 : ),
304 : );
305 : }
306 :
307 : return protoExemplar;
308 2 : }).toList(growable: false);
309 : }
310 : }
|