LCOV - code coverage report
Current view: top level - lib/src/metrics/export/prometheus - prometheus_exporter.dart (source / functions) Coverage Total Hit
Test: lcov.info Lines: 96.3 % 81 78
Test Date: 2026-07-11 13:35:19 Functions: - 0 0

            Line data    Source code
       1              : // Copyright The OpenTelemetry Authors
       2              : // SPDX-License-Identifier: Apache-2.0
       3              : 
       4              : import 'dart:async';
       5              : 
       6              : import 'package:dartastic_opentelemetry_api/dartastic_opentelemetry_api.dart'
       7              :     show OTelLog;
       8              : import '../../data/metric.dart';
       9              : import '../../data/metric_data.dart';
      10              : import '../../data/metric_point.dart';
      11              : import '../../metric_exporter.dart';
      12              : 
      13              : /// PrometheusExporter exports metrics in Prometheus format.
      14              : /// This can be exposed via an HTTP endpoint or written to a file.
      15              : /// This could be used on Dart server but not Flutter clients since
      16              : /// Prometheus is a pull model and expects stable http servers and
      17              : /// a Flutter client typically can't provide that.
      18              : /// Dartastic.io offers Prometheus for OTel by forwarding the OTLP data
      19              : /// to use Prometheus.  To forward OTLP to your own Prometheus backend you would
      20              : /// configure an OTel collector similar to the following:
      21              : /// receivers:
      22              : //   otlp:
      23              : //     protocols:
      24              : //       grpc:
      25              : //         endpoint: 0.0.0.0:4317
      26              : //
      27              : // processors:
      28              : //   batch:
      29              : //     timeout: 10s
      30              : //
      31              : // exporters:
      32              : //   prometheus:
      33              : //     endpoint: "0.0.0.0:8889"  # Endpoint for Prometheus to scrape
      34              : //     namespace: "flutter_apps"
      35              : //     const_labels:
      36              : //       source: "mobile_clients"
      37              : //
      38              : // service:
      39              : //   pipelines:
      40              : //     metrics:
      41              : //       receivers: [otlp]
      42              : //       processors: [batch]
      43              : //       exporters: [prometheus]
      44              : class PrometheusExporter implements MetricExporter {
      45              :   bool _shutdown = false;
      46              : 
      47              :   /// The last generated Prometheus text exposition format data.
      48              :   String _lastExportData = '';
      49              : 
      50              :   /// Creates a new PrometheusExporter.
      51            2 :   PrometheusExporter();
      52              : 
      53              :   /// Gets the latest Prometheus exposition format data.
      54            4 :   String get prometheusData => _lastExportData;
      55              : 
      56            2 :   @override
      57              :   Future<bool> export(MetricData data) async {
      58            2 :     if (_shutdown) {
      59            2 :       if (OTelLog.isLogExport()) {
      60            1 :         OTelLog.logExport('PrometheusExporter: Cannot export after shutdown');
      61              :       }
      62              :       return false;
      63              :     }
      64              : 
      65            4 :     if (data.metrics.isEmpty) {
      66            2 :       if (OTelLog.isLogExport()) {
      67            1 :         OTelLog.logExport('PrometheusExporter: No metrics to export');
      68              :       }
      69              :       return true;
      70              :     }
      71              : 
      72              :     try {
      73            2 :       if (OTelLog.isLogExport()) {
      74            1 :         OTelLog.logExport(
      75            3 :           'PrometheusExporter: Exporting ${data.metrics.length} metrics',
      76              :         );
      77              :       }
      78              : 
      79              :       // Convert metrics to Prometheus format
      80            4 :       _lastExportData = _toPrometheusFormat(data);
      81              : 
      82            2 :       if (OTelLog.isLogExport()) {
      83            1 :         OTelLog.logExport('PrometheusExporter: Export successful');
      84              :       }
      85              :       return true;
      86              :     } catch (e) {
      87            0 :       if (OTelLog.isLogExport()) {
      88            0 :         OTelLog.logExport('PrometheusExporter: Export failed: $e');
      89              :       }
      90              :       return false;
      91              :     }
      92              :   }
      93              : 
      94              :   /// Converts metric data to Prometheus exposition format.
      95            2 :   String _toPrometheusFormat(MetricData data) {
      96            2 :     final buffer = StringBuffer();
      97              : 
      98            4 :     for (final metric in data.metrics) {
      99              :       // Add HELP comment
     100            2 :       if (metric.description != null) {
     101            2 :         buffer.writeln(
     102           10 :           '# HELP ${_sanitizeName(metric.name)} ${_sanitizeComment(metric.description!)}',
     103              :         );
     104              :       }
     105              : 
     106              :       // Add TYPE comment
     107            2 :       buffer.writeln(
     108            8 :         '# TYPE ${_sanitizeName(metric.name)} ${_getPrometheusType(metric)}',
     109              :       );
     110              : 
     111              :       // Add metric data points
     112            4 :       for (final point in metric.points) {
     113            2 :         _writeMetricPoint(buffer, metric, point);
     114              :       }
     115              : 
     116              :       // Empty line between metrics
     117            2 :       buffer.writeln();
     118              :     }
     119              : 
     120            2 :     return buffer.toString();
     121              :   }
     122              : 
     123              :   /// Writes a metric point in Prometheus format.
     124            2 :   void _writeMetricPoint(
     125              :     StringBuffer buffer,
     126              :     Metric metric,
     127              :     MetricPoint<dynamic> point,
     128              :   ) {
     129            4 :     final metricName = _sanitizeName(metric.name);
     130              : 
     131              :     // Add labels
     132            6 :     final labels = _formatLabels(point.attributes.toMap());
     133              : 
     134            4 :     if (point.value is HistogramValue) {
     135              :       // Histogram metrics require special handling
     136            1 :       final histogram = point.value as HistogramValue;
     137              : 
     138              :       // Write sum
     139            3 :       buffer.writeln('${metricName}_sum$labels ${histogram.sum}');
     140              : 
     141              :       // Write count
     142            3 :       buffer.writeln('${metricName}_count$labels ${histogram.count}');
     143              : 
     144              :       // Write buckets
     145            4 :       for (var i = 0; i < histogram.boundaries.length; i++) {
     146            2 :         final boundary = histogram.boundaries[i];
     147            2 :         final count = histogram.bucketCounts[i];
     148            1 :         buffer.writeln(
     149            4 :           '${metricName}_bucket{${_formatLabelsWithLe(point.attributes.toMap(), boundary)}} $count',
     150              :         );
     151              :       }
     152              : 
     153              :       // Add +Inf bucket
     154            1 :       buffer.writeln(
     155            5 :         '${metricName}_bucket{${_formatLabelsWithLe(point.attributes.toMap(), double.infinity)}} ${histogram.count}',
     156              :       );
     157              :     } else {
     158              :       // Simple metrics (counters, gauges)
     159            6 :       buffer.writeln('$metricName$labels ${point.value}');
     160              :     }
     161              :   }
     162              : 
     163              :   /// Gets the Prometheus metric type from an OTel metric.
     164            2 :   String _getPrometheusType(Metric metric) {
     165            4 :     if (metric.points.isNotEmpty &&
     166            8 :         metric.points.first.value is HistogramValue) {
     167              :       return 'histogram';
     168            4 :     } else if (metric.type == MetricType.sum) {
     169              :       return 'counter';
     170              :     } else {
     171              :       return 'gauge';
     172              :     }
     173              :   }
     174              : 
     175              :   /// Formats attributes as Prometheus labels.
     176            2 :   String _formatLabels(Map<String, dynamic> attributes) {
     177            2 :     if (attributes.isEmpty) {
     178              :       return '{}'; // Return empty braces for metrics without attributes
     179              :     }
     180              : 
     181            6 :     final labelPairs = attributes.entries.map((entry) {
     182           10 :       return '${_sanitizeName(entry.key)}="${_sanitizeValue(entry.value)}"';
     183            2 :     }).join(',');
     184              : 
     185            2 :     return '{$labelPairs}';
     186              :   }
     187              : 
     188              :   /// Formats attributes with an added 'le' label for histogram buckets.
     189            1 :   String _formatLabelsWithLe(Map<String, dynamic> attributes, double le) {
     190            1 :     final newAttributes = Map<String, dynamic>.from(attributes);
     191            1 :     if (le == double.infinity) {
     192            1 :       newAttributes['le'] = '+Inf';
     193            2 :     } else if (le == le.truncateToDouble()) {
     194              :       // If the number is an integer (no decimal component),
     195              :       // format it without the decimal point
     196            3 :       newAttributes['le'] = le.toInt().toString();
     197              :     } else {
     198            0 :       newAttributes['le'] = le.toString();
     199              :     }
     200              : 
     201            3 :     final labelPairs = newAttributes.entries.map((entry) {
     202            5 :       return '${_sanitizeName(entry.key)}="${_sanitizeValue(entry.value)}"';
     203            1 :     }).join(',');
     204              : 
     205              :     return labelPairs;
     206              :   }
     207              : 
     208              :   /// Sanitizes a metric or label name.
     209            2 :   String _sanitizeName(String name) {
     210              :     // Replace invalid characters with underscores
     211              :     // Valid characters in Prometheus are: [a-zA-Z0-9_]
     212            4 :     return name.replaceAll(RegExp(r'[^a-zA-Z0-9_]'), '_');
     213              :   }
     214              : 
     215              :   /// Sanitizes a comment for HELP.
     216            2 :   String _sanitizeComment(String comment) {
     217              :     // Escape backslashes and newlines
     218            4 :     return comment.replaceAll(r'\', r'\\').replaceAll('\n', '\\n');
     219              :   }
     220              : 
     221              :   /// Sanitizes a label value.
     222            2 :   String _sanitizeValue(dynamic value) {
     223              :     // Handle AttributeValue objects by extracting the raw value
     224            4 :     if (value.toString().startsWith('AttributeValue(') &&
     225            4 :         value.toString().endsWith(')')) {
     226              :       // Extract the value inside AttributeValue(...)
     227            4 :       final rawValue = value.toString().substring(
     228            2 :             'AttributeValue('.length,
     229            6 :             value.toString().length - 1,
     230              :           );
     231              :       value = rawValue;
     232              :     }
     233              : 
     234              :     // Escape quotes, backslashes, and newlines
     235              :     return value
     236            2 :         .toString()
     237            2 :         .replaceAll(r'\', r'\\')
     238            2 :         .replaceAll('"', r'\"')
     239            2 :         .replaceAll('\n', '\\n');
     240              :   }
     241              : 
     242            1 :   @override
     243              :   Future<bool> forceFlush() async {
     244              :     // No-op for this exporter
     245              :     return true;
     246              :   }
     247              : 
     248            2 :   @override
     249              :   Future<bool> shutdown() async {
     250            2 :     _shutdown = true;
     251              :     return true;
     252              :   }
     253              : }
        

Generated by: LCOV version 2.0-1