LCOV - code coverage report
Current view: top level - src/environment - otel_env.dart (source / functions) Coverage Total Hit
Test: lcov.info Lines: 30.2 % 182 55
Test Date: 2025-11-15 13:23:01 Functions: - 0 0

            Line data    Source code
       1              : // Licensed under the Apache License, Version 2.0
       2              : // Copyright 2025, Michael Bushe, All rights reserved.
       3              : 
       4              : import 'package:dartastic_opentelemetry_api/dartastic_opentelemetry_api.dart';
       5              : import 'env_constants.dart';
       6              : import 'environment_service.dart';
       7              : 
       8              : /// Utility class for handling OpenTelemetry environment variables.
       9              : ///
      10              : /// This class provides methods for reading standard OpenTelemetry environment
      11              : /// variables and applying their configuration to the SDK.
      12              : ///
      13              : /// OpenTelemetry standard environment variables:
      14              : /// https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/
      15              : class OTelEnv {
      16              :   /// Initialize logging based on environment variables.
      17              :   ///
      18              :   /// This method reads the logging-related environment variables
      19              :   /// and configures the OTelLog accordingly.
      20              :   ///
      21              :   /// If a custom log function has already been set (e.g., by tests),
      22              :   /// this method will preserve it and only update the log level.
      23              :   /// This allows tests to capture logs while still respecting
      24              :   /// environment variable configuration.
      25           73 :   static void initializeLogging() {
      26              :     // Save the current log function to check if it's custom
      27           73 :     final existingLogFunction = OTelLog.logFunction;
      28              : 
      29              :     // A custom function is one that's not null and not the default print function
      30              :     final hasCustomLogFunction =
      31           73 :         existingLogFunction != null && existingLogFunction != print;
      32              : 
      33              :     // Set log level based on environment variable
      34          146 :     final logLevel = _getEnv(otelLogLevel)?.toLowerCase();
      35              :     if (logLevel != null) {
      36              :       switch (logLevel) {
      37           73 :         case 'trace':
      38           73 :           OTelLog.enableTraceLogging();
      39              :           break;
      40            0 :         case 'debug':
      41            0 :           OTelLog.enableDebugLogging();
      42              :           break;
      43            0 :         case 'info':
      44            0 :           OTelLog.enableInfoLogging();
      45              :           break;
      46            0 :         case 'warn':
      47            0 :           OTelLog.enableWarnLogging();
      48              :           break;
      49            0 :         case 'error':
      50            0 :           OTelLog.enableErrorLogging();
      51              :           break;
      52            0 :         case 'fatal':
      53            0 :           OTelLog.enableFatalLogging();
      54              :           break;
      55              :         default:
      56              :           // No change to logging if level not recognized
      57              :           break;
      58              :       }
      59              : 
      60              :       // Only set to print if no custom function is already configured
      61              :       if (!hasCustomLogFunction) {
      62              :         OTelLog.logFunction = print;
      63              :       }
      64              :     }
      65              : 
      66              :     // Enable metrics logging based on environment variable
      67           73 :     if (_getEnvBool(otelLogMetrics) && OTelLog.metricLogFunction == null) {
      68              :       OTelLog.metricLogFunction = print;
      69              :     }
      70              : 
      71              :     // Enable spans logging based on environment variable
      72           73 :     if (_getEnvBool(otelLogSpans) && OTelLog.spanLogFunction == null) {
      73              :       OTelLog.spanLogFunction = print;
      74              :     }
      75              : 
      76              :     // Enable export logging based on environment variable
      77           73 :     if (_getEnvBool(otelLogExport) && OTelLog.exportLogFunction == null) {
      78              :       OTelLog.exportLogFunction = print;
      79              :     }
      80              :   }
      81              : 
      82              :   /// Get OTLP configuration from environment variables.
      83              :   ///
      84              :   /// Returns a map containing the OTLP configuration read from environment variables.
      85              :   /// Signal-specific variables take precedence over general ones.
      86           73 :   static Map<String, dynamic> getOtlpConfig({String signal = 'traces'}) {
      87           73 :     final config = <String, dynamic>{};
      88              : 
      89              :     // Get endpoint (signal-specific takes precedence)
      90              :     String? endpoint;
      91              :     switch (signal) {
      92           73 :       case 'traces':
      93           73 :         endpoint = _getEnv(otelExporterOtlpTracesEndpoint) ??
      94           73 :             _getEnv(otelExporterOtlpEndpoint);
      95              :         break;
      96            0 :       case 'metrics':
      97            0 :         endpoint = _getEnv(otelExporterOtlpMetricsEndpoint) ??
      98            0 :             _getEnv(otelExporterOtlpEndpoint);
      99              :         break;
     100            0 :       case 'logs':
     101            0 :         endpoint = _getEnv(otelExporterOtlpLogsEndpoint) ??
     102            0 :             _getEnv(otelExporterOtlpEndpoint);
     103              :         break;
     104              :     }
     105              :     if (endpoint != null) {
     106            0 :       config['endpoint'] = endpoint;
     107              :     }
     108              : 
     109              :     // Get protocol (signal-specific takes precedence)
     110              :     String? protocol;
     111              :     switch (signal) {
     112           73 :       case 'traces':
     113           73 :         protocol = _getEnv(otelExporterOtlpTracesProtocol) ??
     114           73 :             _getEnv(otelExporterOtlpProtocol);
     115              :         break;
     116            0 :       case 'metrics':
     117            0 :         protocol = _getEnv(otelExporterOtlpMetricsProtocol) ??
     118            0 :             _getEnv(otelExporterOtlpProtocol);
     119              :         break;
     120            0 :       case 'logs':
     121            0 :         protocol = _getEnv(otelExporterOtlpLogsProtocol) ??
     122            0 :             _getEnv(otelExporterOtlpProtocol);
     123              :         break;
     124              :     }
     125              :     if (protocol != null) {
     126            0 :       config['protocol'] = protocol;
     127              :     }
     128              : 
     129              :     // Get headers (signal-specific takes precedence)
     130              :     String? headers;
     131              :     switch (signal) {
     132           73 :       case 'traces':
     133           73 :         headers = _getEnv(otelExporterOtlpTracesHeaders) ??
     134           73 :             _getEnv(otelExporterOtlpHeaders);
     135              :         break;
     136            0 :       case 'metrics':
     137            0 :         headers = _getEnv(otelExporterOtlpMetricsHeaders) ??
     138            0 :             _getEnv(otelExporterOtlpHeaders);
     139              :         break;
     140            0 :       case 'logs':
     141            0 :         headers = _getEnv(otelExporterOtlpLogsHeaders) ??
     142            0 :             _getEnv(otelExporterOtlpHeaders);
     143              :         break;
     144              :     }
     145              :     if (headers != null) {
     146            0 :       if (OTelLog.isDebug()) {
     147            0 :         OTelLog.debug('OTelEnv: Parsing $signal headers from env: $headers');
     148              :       }
     149            0 :       final parsedHeaders = _parseHeaders(headers);
     150            0 :       if (OTelLog.isDebug()) {
     151            0 :         OTelLog.debug('OTelEnv: Parsed ${parsedHeaders.length} header(s)');
     152            0 :         parsedHeaders.forEach((key, value) {
     153            0 :           if (key.toLowerCase() == 'authorization') {
     154            0 :             OTelLog.debug('  $key: [REDACTED - length: ${value.length}]');
     155              :           } else {
     156            0 :             OTelLog.debug('  $key: $value');
     157              :           }
     158              :         });
     159              :       }
     160            0 :       config['headers'] = parsedHeaders;
     161              :     }
     162              : 
     163              :     // Get insecure setting (signal-specific takes precedence)
     164              :     bool? insecure;
     165              :     switch (signal) {
     166           73 :       case 'traces':
     167           73 :         insecure = _getEnvBoolNullable(otelExporterOtlpTracesInsecure) ??
     168           73 :             _getEnvBoolNullable(otelExporterOtlpInsecure);
     169              :         break;
     170            0 :       case 'metrics':
     171            0 :         insecure = _getEnvBoolNullable(otelExporterOtlpMetricsInsecure) ??
     172            0 :             _getEnvBoolNullable(otelExporterOtlpInsecure);
     173              :         break;
     174            0 :       case 'logs':
     175            0 :         insecure = _getEnvBoolNullable(otelExporterOtlpLogsInsecure) ??
     176            0 :             _getEnvBoolNullable(otelExporterOtlpInsecure);
     177              :         break;
     178              :     }
     179              :     if (insecure != null) {
     180            0 :       config['insecure'] = insecure;
     181              :     }
     182              : 
     183              :     // Get timeout (signal-specific takes precedence)
     184              :     String? timeout;
     185              :     switch (signal) {
     186           73 :       case 'traces':
     187           73 :         timeout = _getEnv(otelExporterOtlpTracesTimeout) ??
     188           73 :             _getEnv(otelExporterOtlpTimeout);
     189              :         break;
     190            0 :       case 'metrics':
     191            0 :         timeout = _getEnv(otelExporterOtlpMetricsTimeout) ??
     192            0 :             _getEnv(otelExporterOtlpTimeout);
     193              :         break;
     194            0 :       case 'logs':
     195            0 :         timeout = _getEnv(otelExporterOtlpLogsTimeout) ??
     196            0 :             _getEnv(otelExporterOtlpTimeout);
     197              :         break;
     198              :     }
     199              :     if (timeout != null) {
     200            0 :       final timeoutMs = int.tryParse(timeout);
     201              :       if (timeoutMs != null) {
     202            0 :         config['timeout'] = Duration(milliseconds: timeoutMs);
     203              :       }
     204              :     }
     205              : 
     206              :     // Get compression (signal-specific takes precedence)
     207              :     String? compression;
     208              :     switch (signal) {
     209           73 :       case 'traces':
     210           73 :         compression = _getEnv(otelExporterOtlpTracesCompression) ??
     211           73 :             _getEnv(otelExporterOtlpCompression);
     212              :         break;
     213            0 :       case 'metrics':
     214            0 :         compression = _getEnv(otelExporterOtlpMetricsCompression) ??
     215            0 :             _getEnv(otelExporterOtlpCompression);
     216              :         break;
     217            0 :       case 'logs':
     218            0 :         compression = _getEnv(otelExporterOtlpLogsCompression) ??
     219            0 :             _getEnv(otelExporterOtlpCompression);
     220              :         break;
     221              :     }
     222              :     if (compression != null) {
     223            0 :       config['compression'] = compression;
     224              :     }
     225              : 
     226              :     // Get certificate (signal-specific takes precedence)
     227              :     String? certificate;
     228              :     switch (signal) {
     229           73 :       case 'traces':
     230           73 :         certificate = _getEnv(otelExporterOtlpTracesCertificate) ??
     231           73 :             _getEnv(otelExporterOtlpCertificate);
     232              :         break;
     233            0 :       case 'metrics':
     234            0 :         certificate = _getEnv(otelExporterOtlpMetricsCertificate) ??
     235            0 :             _getEnv(otelExporterOtlpCertificate);
     236              :         break;
     237            0 :       case 'logs':
     238            0 :         certificate = _getEnv(otelExporterOtlpLogsCertificate) ??
     239            0 :             _getEnv(otelExporterOtlpCertificate);
     240              :         break;
     241              :     }
     242              :     if (certificate != null) {
     243            0 :       config['certificate'] = certificate;
     244              :     }
     245              : 
     246              :     // Get client key (signal-specific takes precedence)
     247              :     String? clientKey;
     248              :     switch (signal) {
     249           73 :       case 'traces':
     250           73 :         clientKey = _getEnv(otelExporterOtlpTracesClientKey) ??
     251           73 :             _getEnv(otelExporterOtlpClientKey);
     252              :         break;
     253            0 :       case 'metrics':
     254            0 :         clientKey = _getEnv(otelExporterOtlpMetricsClientKey) ??
     255            0 :             _getEnv(otelExporterOtlpClientKey);
     256              :         break;
     257            0 :       case 'logs':
     258            0 :         clientKey = _getEnv(otelExporterOtlpLogsClientKey) ??
     259            0 :             _getEnv(otelExporterOtlpClientKey);
     260              :         break;
     261              :     }
     262              :     if (clientKey != null) {
     263            0 :       config['clientKey'] = clientKey;
     264              :     }
     265              : 
     266              :     // Get client certificate (signal-specific takes precedence)
     267              :     String? clientCertificate;
     268              :     switch (signal) {
     269           73 :       case 'traces':
     270           73 :         clientCertificate = _getEnv(otelExporterOtlpTracesClientCertificate) ??
     271           73 :             _getEnv(otelExporterOtlpClientCertificate);
     272              :         break;
     273            0 :       case 'metrics':
     274            0 :         clientCertificate = _getEnv(otelExporterOtlpMetricsClientCertificate) ??
     275            0 :             _getEnv(otelExporterOtlpClientCertificate);
     276              :         break;
     277            0 :       case 'logs':
     278            0 :         clientCertificate = _getEnv(otelExporterOtlpLogsClientCertificate) ??
     279            0 :             _getEnv(otelExporterOtlpClientCertificate);
     280              :         break;
     281              :     }
     282              :     if (clientCertificate != null) {
     283            0 :       config['clientCertificate'] = clientCertificate;
     284              :     }
     285              : 
     286              :     return config;
     287              :   }
     288              : 
     289              :   /// Get service configuration from environment variables.
     290              :   ///
     291              :   /// Returns a map containing the service configuration read from environment variables.
     292              :   ///
     293              :   /// Handles the spec precedence rules:
     294              :   /// - If `service.name` is in OTEL_RESOURCE_ATTRIBUTES, it's used as the base value
     295              :   /// - OTEL_SERVICE_NAME takes precedence over `service.name` in OTEL_RESOURCE_ATTRIBUTES
     296              :   /// - `service.version` comes from OTEL_RESOURCE_ATTRIBUTES only
     297           57 :   static Map<String, dynamic> getServiceConfig() {
     298           57 :     final config = <String, dynamic>{};
     299              : 
     300              :     // First, parse service.name and service.version from OTEL_RESOURCE_ATTRIBUTES
     301           57 :     final resourceStr = _getEnv(otelResourceAttributes);
     302              :     if (resourceStr != null) {
     303            0 :       final pairs = resourceStr.split(',');
     304            0 :       for (final pair in pairs) {
     305            0 :         final equalIndex = pair.indexOf('=');
     306            0 :         if (equalIndex > 0 && equalIndex < pair.length - 1) {
     307            0 :           final key = pair.substring(0, equalIndex).trim();
     308            0 :           final value = pair.substring(equalIndex + 1).trim();
     309              : 
     310            0 :           if (key == 'service.name') {
     311            0 :             config['serviceName'] = value;
     312            0 :           } else if (key == 'service.version') {
     313            0 :             config['serviceVersion'] = value;
     314              :           }
     315              :         }
     316              :       }
     317              :     }
     318              : 
     319              :     // OTEL_SERVICE_NAME takes precedence over service.name from resource attributes
     320           57 :     final serviceName = _getEnv(otelServiceName);
     321              :     if (serviceName != null) {
     322            0 :       config['serviceName'] = serviceName;
     323              :     }
     324              : 
     325              :     return config;
     326              :   }
     327              : 
     328              :   /// Get resource attributes from environment variables.
     329              :   ///
     330              :   /// Parses the OTEL_RESOURCE_ATTRIBUTES environment variable which should be
     331              :   /// a comma-separated list of key=value pairs.
     332           73 :   static Map<String, Object> getResourceAttributes() {
     333           73 :     final resourceAttrs = <String, Object>{};
     334              : 
     335           73 :     final resourceStr = _getEnv(otelResourceAttributes);
     336              :     if (resourceStr != null) {
     337            0 :       final pairs = resourceStr.split(',');
     338            0 :       for (final pair in pairs) {
     339            0 :         final parts = pair.split('=');
     340            0 :         if (parts.length == 2) {
     341            0 :           final key = parts[0].trim();
     342            0 :           final value = parts[1].trim();
     343              :           // Try to parse as number if possible
     344            0 :           final intValue = int.tryParse(value);
     345              :           if (intValue != null) {
     346            0 :             resourceAttrs[key] = intValue;
     347              :           } else {
     348            0 :             final doubleValue = double.tryParse(value);
     349              :             if (doubleValue != null) {
     350            0 :               resourceAttrs[key] = doubleValue;
     351              :             } else {
     352              :               // Handle boolean values
     353            0 :               if (value.toLowerCase() == 'true') {
     354            0 :                 resourceAttrs[key] = true;
     355            0 :               } else if (value.toLowerCase() == 'false') {
     356            0 :                 resourceAttrs[key] = false;
     357              :               } else {
     358            0 :                 resourceAttrs[key] = value;
     359              :               }
     360              :             }
     361              :           }
     362              :         }
     363              :       }
     364              :     }
     365              : 
     366              :     return resourceAttrs;
     367              :   }
     368              : 
     369              :   /// Get the selected exporter for a signal.
     370              :   ///
     371              :   /// Returns the exporter type configured via environment variables.
     372           72 :   static String? getExporter({String signal = 'traces'}) {
     373              :     switch (signal) {
     374           72 :       case 'traces':
     375           72 :         return _getEnv(otelTracesExporter);
     376            0 :       case 'metrics':
     377            0 :         return _getEnv(otelMetricsExporter);
     378            0 :       case 'logs':
     379            0 :         return _getEnv(otelLogsExporter);
     380              :       default:
     381              :         return null;
     382              :     }
     383              :   }
     384              : 
     385              :   /// Parse headers from the environment variable format.
     386              :   ///
     387              :   /// Headers are expected in the format: key1=value1,key2=value2
     388              :   /// Note: Header values can contain '=' characters (e.g., base64), so we only
     389              :   /// split on the first '=' for each pair.
     390            0 :   static Map<String, String> _parseHeaders(String headerStr) {
     391            0 :     final headers = <String, String>{};
     392              : 
     393            0 :     final pairs = headerStr.split(',');
     394            0 :     for (final pair in pairs) {
     395            0 :       final equalIndex = pair.indexOf('=');
     396            0 :       if (equalIndex > 0 && equalIndex < pair.length - 1) {
     397            0 :         final key = pair.substring(0, equalIndex).trim();
     398            0 :         final value = pair.substring(equalIndex + 1).trim();
     399            0 :         headers[key] = value;
     400              :       }
     401              :     }
     402              : 
     403              :     return headers;
     404              :   }
     405              : 
     406              :   /// Get environment variable value.
     407              :   ///
     408              :   /// This method safely retrieves an environment variable value,
     409              :   /// handling exceptions that might occur in environments where
     410              :   /// Platform is not available (e.g., browsers).
     411              :   ///
     412              :   /// @param name The name of the environment variable
     413              :   /// @return The value of the environment variable, or null if not found
     414           73 :   static String? _getEnv(String name) {
     415          146 :     return EnvironmentService.instance.getValue(name);
     416              :   }
     417              : 
     418              :   /// Get boolean environment variable value.
     419              :   ///
     420              :   /// This method converts an environment variable value to a boolean.
     421              :   /// Values of '1', 'true', 'yes', and 'on' (case-insensitive) are considered true.
     422              :   ///
     423              :   /// @param name The name of the environment variable
     424              :   /// @return true if the environment variable has a truthy value, false otherwise
     425           73 :   static bool _getEnvBool(String name) {
     426          146 :     final value = _getEnv(name)?.toLowerCase();
     427          146 :     return value == '1' || value == 'true' || value == 'yes' || value == 'on';
     428              :   }
     429              : 
     430              :   /// Get boolean environment variable value that can be null.
     431              :   ///
     432              :   /// This method converts an environment variable value to a boolean.
     433              :   /// Values of '1', 'true', 'yes', and 'on' (case-insensitive) are considered true.
     434              :   /// Values of '0', 'false', 'no', and 'off' (case-insensitive) are considered false.
     435              :   ///
     436              :   /// @param name The name of the environment variable
     437              :   /// @return true/false if the environment variable has a valid boolean value, null otherwise
     438           73 :   static bool? _getEnvBoolNullable(String name) {
     439           73 :     final value = _getEnv(name)?.toLowerCase();
     440              :     if (value == null) return null;
     441              : 
     442            0 :     if (value == '1' || value == 'true' || value == 'yes' || value == 'on') {
     443              :       return true;
     444            0 :     } else if (value == '0' ||
     445            0 :         value == 'false' ||
     446            0 :         value == 'no' ||
     447            0 :         value == 'off') {
     448              :       return false;
     449              :     }
     450              : 
     451              :     return null;
     452              :   }
     453              : }
        

Generated by: LCOV version 2.0-1