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-08-27 23:42:02 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           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              : }
        

Generated by: LCOV version 2.0-1