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