LCOV - code coverage report
Current view: top level - lib/src/metrics/export/otlp - metric_transformer.dart (source / functions) Coverage Total Hit
Test: lcov.info Lines: 96.3 % 136 131
Test Date: 2026-10-11 16:35:14 Functions: - 0 0

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

Generated by: LCOV version 2.0-1