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 : 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 along with the current log level.
23 : /// This allows tests to fully control logging configuration without
24 : /// environment variables overriding their settings.
25 129 : static void initializeLogging() {
26 : // Save the current log function to check if it's custom
27 129 : 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 129 : existingLogFunction != null && existingLogFunction != print;
32 :
33 : // Set log level and function based on environment variable,
34 : // but only if no custom log function is already configured.
35 258 : final logLevel = _getEnv(otelLogLevel)?.toLowerCase();
36 : if (logLevel != null && !hasCustomLogFunction) {
37 : switch (logLevel) {
38 116 : case 'trace':
39 116 : OTelLog.enableTraceLogging();
40 : break;
41 0 : case 'debug':
42 0 : OTelLog.enableDebugLogging();
43 : break;
44 0 : case 'info':
45 0 : OTelLog.enableInfoLogging();
46 : break;
47 0 : case 'warn':
48 0 : OTelLog.enableWarnLogging();
49 : break;
50 0 : case 'error':
51 0 : OTelLog.enableErrorLogging();
52 : break;
53 0 : case 'fatal':
54 0 : OTelLog.enableFatalLogging();
55 : break;
56 : default:
57 : // No change to logging if level not recognized
58 : break;
59 : }
60 :
61 : OTelLog.logFunction = print;
62 : }
63 :
64 : // Enable metrics logging based on environment variable
65 129 : if (_getEnvBool(otelLogMetrics) && OTelLog.metricLogFunction == null) {
66 : OTelLog.metricLogFunction = print;
67 : }
68 :
69 : // Enable spans logging based on environment variable
70 129 : if (_getEnvBool(otelLogSpans) && OTelLog.spanLogFunction == null) {
71 : OTelLog.spanLogFunction = print;
72 : }
73 :
74 : // Enable export logging based on environment variable
75 129 : if (_getEnvBool(otelLogExport) && OTelLog.exportLogFunction == null) {
76 : OTelLog.exportLogFunction = print;
77 : }
78 : }
79 :
80 : /// Get OTLP configuration from environment variables.
81 : ///
82 : /// Returns a map containing the OTLP configuration read from environment variables.
83 : /// Signal-specific variables take precedence over general ones.
84 129 : static Map<String, dynamic> getOtlpConfig({String signal = 'traces'}) {
85 129 : final config = <String, dynamic>{};
86 :
87 : // Get endpoint (signal-specific takes precedence)
88 : String? endpoint;
89 : switch (signal) {
90 129 : case 'traces':
91 129 : endpoint = _getEnv(otelExporterOtlpTracesEndpoint) ??
92 129 : _getEnv(otelExporterOtlpEndpoint);
93 : break;
94 129 : case 'metrics':
95 121 : endpoint = _getEnv(otelExporterOtlpMetricsEndpoint) ??
96 121 : _getEnv(otelExporterOtlpEndpoint);
97 : break;
98 117 : case 'logs':
99 117 : endpoint = _getEnv(otelExporterOtlpLogsEndpoint) ??
100 117 : _getEnv(otelExporterOtlpEndpoint);
101 : break;
102 : }
103 : if (endpoint != null) {
104 0 : config['endpoint'] = endpoint;
105 : }
106 :
107 : // Get protocol (signal-specific takes precedence)
108 : String? protocol;
109 : switch (signal) {
110 129 : case 'traces':
111 129 : protocol = _getEnv(otelExporterOtlpTracesProtocol) ??
112 129 : _getEnv(otelExporterOtlpProtocol);
113 : break;
114 129 : case 'metrics':
115 121 : protocol = _getEnv(otelExporterOtlpMetricsProtocol) ??
116 121 : _getEnv(otelExporterOtlpProtocol);
117 : break;
118 117 : case 'logs':
119 117 : protocol = _getEnv(otelExporterOtlpLogsProtocol) ??
120 117 : _getEnv(otelExporterOtlpProtocol);
121 : break;
122 : }
123 : if (protocol != null) {
124 0 : config['protocol'] = protocol;
125 : }
126 :
127 : // Get headers (signal-specific takes precedence)
128 : String? headers;
129 : switch (signal) {
130 129 : case 'traces':
131 129 : headers = _getEnv(otelExporterOtlpTracesHeaders) ??
132 129 : _getEnv(otelExporterOtlpHeaders);
133 : break;
134 129 : case 'metrics':
135 121 : headers = _getEnv(otelExporterOtlpMetricsHeaders) ??
136 121 : _getEnv(otelExporterOtlpHeaders);
137 : break;
138 117 : case 'logs':
139 117 : headers = _getEnv(otelExporterOtlpLogsHeaders) ??
140 117 : _getEnv(otelExporterOtlpHeaders);
141 : break;
142 : }
143 : if (headers != null) {
144 0 : if (OTelLog.isDebug()) {
145 0 : OTelLog.debug('OTelEnv: Parsing $signal headers from env: $headers');
146 : }
147 0 : final parsedHeaders = _parseHeaders(headers);
148 0 : if (OTelLog.isDebug()) {
149 0 : OTelLog.debug('OTelEnv: Parsed ${parsedHeaders.length} header(s)');
150 0 : parsedHeaders.forEach((key, value) {
151 0 : if (key.toLowerCase() == 'authorization') {
152 0 : OTelLog.debug(' $key: [REDACTED - length: ${value.length}]');
153 : } else {
154 0 : OTelLog.debug(' $key: $value');
155 : }
156 : });
157 : }
158 0 : config['headers'] = parsedHeaders;
159 : }
160 :
161 : // Get insecure setting (signal-specific takes precedence)
162 : bool? insecure;
163 : switch (signal) {
164 129 : case 'traces':
165 129 : insecure = _getEnvBoolNullable(otelExporterOtlpTracesInsecure) ??
166 129 : _getEnvBoolNullable(otelExporterOtlpInsecure);
167 : break;
168 129 : case 'metrics':
169 121 : insecure = _getEnvBoolNullable(otelExporterOtlpMetricsInsecure) ??
170 121 : _getEnvBoolNullable(otelExporterOtlpInsecure);
171 : break;
172 117 : case 'logs':
173 117 : insecure = _getEnvBoolNullable(otelExporterOtlpLogsInsecure) ??
174 117 : _getEnvBoolNullable(otelExporterOtlpInsecure);
175 : break;
176 : }
177 : if (insecure != null) {
178 0 : config['insecure'] = insecure;
179 : }
180 :
181 : // Get timeout (signal-specific takes precedence)
182 : String? timeout;
183 : switch (signal) {
184 129 : case 'traces':
185 129 : timeout = _getEnv(otelExporterOtlpTracesTimeout) ??
186 129 : _getEnv(otelExporterOtlpTimeout);
187 : break;
188 129 : case 'metrics':
189 121 : timeout = _getEnv(otelExporterOtlpMetricsTimeout) ??
190 121 : _getEnv(otelExporterOtlpTimeout);
191 : break;
192 117 : case 'logs':
193 117 : timeout = _getEnv(otelExporterOtlpLogsTimeout) ??
194 117 : _getEnv(otelExporterOtlpTimeout);
195 : break;
196 : }
197 : if (timeout != null) {
198 0 : final timeoutMs = int.tryParse(timeout);
199 : if (timeoutMs != null) {
200 0 : config['timeout'] = Duration(milliseconds: timeoutMs);
201 : }
202 : }
203 :
204 : // Get compression (signal-specific takes precedence)
205 : String? compression;
206 : switch (signal) {
207 129 : case 'traces':
208 129 : compression = _getEnv(otelExporterOtlpTracesCompression) ??
209 129 : _getEnv(otelExporterOtlpCompression);
210 : break;
211 129 : case 'metrics':
212 121 : compression = _getEnv(otelExporterOtlpMetricsCompression) ??
213 121 : _getEnv(otelExporterOtlpCompression);
214 : break;
215 117 : case 'logs':
216 117 : compression = _getEnv(otelExporterOtlpLogsCompression) ??
217 117 : _getEnv(otelExporterOtlpCompression);
218 : break;
219 : }
220 : if (compression != null) {
221 0 : config['compression'] = compression;
222 : }
223 :
224 : // Get certificate (signal-specific takes precedence)
225 : String? certificate;
226 : switch (signal) {
227 129 : case 'traces':
228 129 : certificate = _getEnv(otelExporterOtlpTracesCertificate) ??
229 129 : _getEnv(otelExporterOtlpCertificate);
230 : break;
231 129 : case 'metrics':
232 121 : certificate = _getEnv(otelExporterOtlpMetricsCertificate) ??
233 121 : _getEnv(otelExporterOtlpCertificate);
234 : break;
235 117 : case 'logs':
236 117 : certificate = _getEnv(otelExporterOtlpLogsCertificate) ??
237 117 : _getEnv(otelExporterOtlpCertificate);
238 : break;
239 : }
240 : if (certificate != null) {
241 0 : config['certificate'] = certificate;
242 : }
243 :
244 : // Get client key (signal-specific takes precedence)
245 : String? clientKey;
246 : switch (signal) {
247 129 : case 'traces':
248 129 : clientKey = _getEnv(otelExporterOtlpTracesClientKey) ??
249 129 : _getEnv(otelExporterOtlpClientKey);
250 : break;
251 129 : case 'metrics':
252 121 : clientKey = _getEnv(otelExporterOtlpMetricsClientKey) ??
253 121 : _getEnv(otelExporterOtlpClientKey);
254 : break;
255 117 : case 'logs':
256 117 : clientKey = _getEnv(otelExporterOtlpLogsClientKey) ??
257 117 : _getEnv(otelExporterOtlpClientKey);
258 : break;
259 : }
260 : if (clientKey != null) {
261 0 : config['clientKey'] = clientKey;
262 : }
263 :
264 : // Get client certificate (signal-specific takes precedence)
265 : String? clientCertificate;
266 : switch (signal) {
267 129 : case 'traces':
268 129 : clientCertificate = _getEnv(otelExporterOtlpTracesClientCertificate) ??
269 129 : _getEnv(otelExporterOtlpClientCertificate);
270 : break;
271 129 : case 'metrics':
272 121 : clientCertificate = _getEnv(otelExporterOtlpMetricsClientCertificate) ??
273 121 : _getEnv(otelExporterOtlpClientCertificate);
274 : break;
275 117 : case 'logs':
276 117 : clientCertificate = _getEnv(otelExporterOtlpLogsClientCertificate) ??
277 117 : _getEnv(otelExporterOtlpClientCertificate);
278 : break;
279 : }
280 : if (clientCertificate != null) {
281 0 : config['clientCertificate'] = clientCertificate;
282 : }
283 :
284 : return config;
285 : }
286 :
287 : /// Get service configuration from environment variables.
288 : ///
289 : /// Returns a map containing the service configuration read from environment variables.
290 : ///
291 : /// Handles the spec precedence rules:
292 : /// - If `service.name` is in OTEL_RESOURCE_ATTRIBUTES, it's used as the base value
293 : /// - OTEL_SERVICE_NAME takes precedence over `service.name` in OTEL_RESOURCE_ATTRIBUTES
294 : /// - `service.version` comes from OTEL_RESOURCE_ATTRIBUTES only
295 108 : static Map<String, dynamic> getServiceConfig() {
296 108 : final config = <String, dynamic>{};
297 :
298 : // First, parse service.name and service.version from OTEL_RESOURCE_ATTRIBUTES
299 108 : final resourceStr = _getEnv(otelResourceAttributes);
300 : if (resourceStr != null) {
301 0 : final pairs = resourceStr.split(',');
302 0 : for (final pair in pairs) {
303 0 : final equalIndex = pair.indexOf('=');
304 0 : if (equalIndex > 0 && equalIndex < pair.length - 1) {
305 0 : final key = pair.substring(0, equalIndex).trim();
306 0 : final value = pair.substring(equalIndex + 1).trim();
307 :
308 0 : if (key == 'service.name') {
309 0 : config['serviceName'] = value;
310 0 : } else if (key == 'service.version') {
311 0 : config['serviceVersion'] = value;
312 : }
313 : }
314 : }
315 : }
316 :
317 : // OTEL_SERVICE_NAME takes precedence over service.name from resource attributes
318 108 : final serviceName = _getEnv(otelServiceName);
319 : if (serviceName != null) {
320 0 : config['serviceName'] = serviceName;
321 : }
322 :
323 : return config;
324 : }
325 :
326 : /// Get resource attributes from environment variables.
327 : ///
328 : /// Parses the OTEL_RESOURCE_ATTRIBUTES environment variable which should be
329 : /// a comma-separated list of key=value pairs.
330 129 : static Map<String, Object> getResourceAttributes() {
331 129 : final resourceAttrs = <String, Object>{};
332 :
333 129 : final resourceStr = _getEnv(otelResourceAttributes);
334 : if (resourceStr != null) {
335 0 : final pairs = resourceStr.split(',');
336 0 : for (final pair in pairs) {
337 0 : final parts = pair.split('=');
338 0 : if (parts.length == 2) {
339 0 : final key = parts[0].trim();
340 0 : final value = parts[1].trim();
341 : // Try to parse as number if possible
342 0 : final intValue = int.tryParse(value);
343 : if (intValue != null) {
344 0 : resourceAttrs[key] = intValue;
345 : } else {
346 0 : final doubleValue = double.tryParse(value);
347 : if (doubleValue != null) {
348 0 : resourceAttrs[key] = doubleValue;
349 : } else {
350 : // Handle boolean values
351 0 : if (value.toLowerCase() == 'true') {
352 0 : resourceAttrs[key] = true;
353 0 : } else if (value.toLowerCase() == 'false') {
354 0 : resourceAttrs[key] = false;
355 : } else {
356 0 : resourceAttrs[key] = value;
357 : }
358 : }
359 : }
360 : }
361 : }
362 : }
363 :
364 : return resourceAttrs;
365 : }
366 :
367 : /// Whether `OTEL_SDK_DISABLED` is set to a truthy value.
368 : ///
369 : /// Per the OTel spec, when this is true the SDK acts as a no-op for all
370 : /// signals — no span processors, metric readers, or log record processors
371 : /// should be installed.
372 256 : static bool isSdkDisabled() => _getEnvBool(otelSdkDisabled);
373 :
374 : /// Get the selected exporter for a signal.
375 : ///
376 : /// Returns the exporter type configured via environment variables.
377 128 : static String? getExporter({String signal = 'traces'}) {
378 : switch (signal) {
379 128 : case 'traces':
380 122 : return _getEnv(otelTracesExporter);
381 128 : case 'metrics':
382 107 : return _getEnv(otelMetricsExporter);
383 119 : case 'logs':
384 119 : return _getEnv(otelLogsExporter);
385 : default:
386 : return null;
387 : }
388 : }
389 :
390 : /// Get Batch LogRecord Processor (BLRP) configuration from environment variables.
391 : ///
392 : /// Returns a map containing the BLRP configuration read from environment variables.
393 : /// Keys returned:
394 : /// - 'scheduleDelay': Duration for the schedule delay
395 : /// - 'exportTimeout': Duration for the export timeout
396 : /// - 'maxQueueSize': int for maximum queue size
397 : /// - 'maxExportBatchSize': int for maximum export batch size
398 118 : static Map<String, dynamic> getBlrpConfig() {
399 118 : final config = <String, dynamic>{};
400 :
401 : // Get schedule delay
402 118 : final scheduleDelay = _getEnv(otelBlrpScheduleDelay);
403 : if (scheduleDelay != null) {
404 0 : final delayMs = int.tryParse(scheduleDelay);
405 : if (delayMs != null) {
406 0 : config['scheduleDelay'] = Duration(milliseconds: delayMs);
407 : }
408 : }
409 :
410 : // Get export timeout
411 118 : final exportTimeout = _getEnv(otelBlrpExportTimeout);
412 : if (exportTimeout != null) {
413 0 : final timeoutMs = int.tryParse(exportTimeout);
414 : if (timeoutMs != null) {
415 0 : config['exportTimeout'] = Duration(milliseconds: timeoutMs);
416 : }
417 : }
418 :
419 : // Get max queue size
420 118 : final maxQueueSize = _getEnv(otelBlrpMaxQueueSize);
421 : if (maxQueueSize != null) {
422 0 : final size = int.tryParse(maxQueueSize);
423 : if (size != null) {
424 0 : config['maxQueueSize'] = size;
425 : }
426 : }
427 :
428 : // Get max export batch size
429 118 : final maxExportBatchSize = _getEnv(otelBlrpMaxExportBatchSize);
430 : if (maxExportBatchSize != null) {
431 0 : final size = int.tryParse(maxExportBatchSize);
432 : if (size != null) {
433 0 : config['maxExportBatchSize'] = size;
434 : }
435 : }
436 :
437 : return config;
438 : }
439 :
440 : /// Get LogRecord attribute limits from environment variables.
441 : ///
442 : /// Returns a map containing the log record attribute limits.
443 : /// Keys returned:
444 : /// - 'attributeValueLengthLimit': int for max attribute value length
445 : /// - 'attributeCountLimit': int for max number of attributes
446 2 : static Map<String, dynamic> getLogRecordLimits() {
447 2 : final config = <String, dynamic>{};
448 :
449 : // Get attribute value length limit
450 2 : final valueLengthLimit = _getEnv(otelLogrecordAttributeValueLengthLimit);
451 : if (valueLengthLimit != null) {
452 0 : final limit = int.tryParse(valueLengthLimit);
453 : if (limit != null) {
454 0 : config['attributeValueLengthLimit'] = limit;
455 : }
456 : }
457 :
458 : // Get attribute count limit
459 2 : final countLimit = _getEnv(otelLogrecordAttributeCountLimit);
460 : if (countLimit != null) {
461 0 : final limit = int.tryParse(countLimit);
462 : if (limit != null) {
463 0 : config['attributeCountLimit'] = limit;
464 : }
465 : }
466 :
467 : return config;
468 : }
469 :
470 : /// Parse headers from the environment variable format.
471 : ///
472 : /// Headers are expected in the format: key1=value1,key2=value2
473 : /// Note: Header values can contain '=' characters (e.g., base64), so we only
474 : /// split on the first '=' for each pair.
475 0 : static Map<String, String> _parseHeaders(String headerStr) {
476 0 : final headers = <String, String>{};
477 :
478 0 : final pairs = headerStr.split(',');
479 0 : for (final pair in pairs) {
480 0 : final equalIndex = pair.indexOf('=');
481 0 : if (equalIndex > 0 && equalIndex < pair.length - 1) {
482 0 : final key = pair.substring(0, equalIndex).trim();
483 0 : final value = pair.substring(equalIndex + 1).trim();
484 0 : headers[key] = value;
485 : }
486 : }
487 :
488 : return headers;
489 : }
490 :
491 : /// Get environment variable value.
492 : ///
493 : /// This method safely retrieves an environment variable value,
494 : /// handling exceptions that might occur in environments where
495 : /// Platform is not available (e.g., browsers).
496 : ///
497 : /// @param name The name of the environment variable
498 : /// @return The value of the environment variable, or null if not found
499 129 : static String? _getEnv(String name) {
500 258 : return EnvironmentService.instance.getValue(name);
501 : }
502 :
503 : /// Get boolean environment variable value.
504 : ///
505 : /// This method converts an environment variable value to a boolean.
506 : /// Values of '1', 'true', 'yes', and 'on' (case-insensitive) are considered true.
507 : ///
508 : /// @param name The name of the environment variable
509 : /// @return true if the environment variable has a truthy value, false otherwise
510 129 : static bool _getEnvBool(String name) {
511 258 : final value = _getEnv(name)?.toLowerCase();
512 514 : return value == '1' || value == 'true' || value == 'yes' || value == 'on';
513 : }
514 :
515 : /// Get boolean environment variable value that can be null.
516 : ///
517 : /// This method converts an environment variable value to a boolean.
518 : /// Values of '1', 'true', 'yes', and 'on' (case-insensitive) are considered true.
519 : /// Values of '0', 'false', 'no', and 'off' (case-insensitive) are considered false.
520 : ///
521 : /// @param name The name of the environment variable
522 : /// @return true/false if the environment variable has a valid boolean value, null otherwise
523 129 : static bool? _getEnvBoolNullable(String name) {
524 129 : final value = _getEnv(name)?.toLowerCase();
525 : if (value == null) return null;
526 :
527 0 : if (value == '1' || value == 'true' || value == 'yes' || value == 'on') {
528 : return true;
529 0 : } else if (value == '0' ||
530 0 : value == 'false' ||
531 0 : value == 'no' ||
532 0 : value == 'off') {
533 : return false;
534 : }
535 :
536 : return null;
537 : }
538 : }
|