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 '../metrics/export/metrics_sdk_config.dart';
6 : import '../util/header_redaction.dart';
7 : import 'env_constants.dart';
8 : import 'environment_service.dart';
9 :
10 : /// Raw BLRP environment values parsed by [OTelEnv.getBlrpConfig].
11 : ///
12 : /// All fields are nullable — `null` means the corresponding env var was
13 : /// unset or contained a non-numeric value (a warning is logged in that case).
14 : /// Domain-level validation (e.g. "queue size must be > 0") belongs in
15 : /// [BatchLogRecordProcessorConfig.fromEnvironment], not here.
16 : typedef BlrpEnvironmentValues = ({
17 : /// Parsed from `OTEL_BLRP_SCHEDULE_DELAY`
18 : Duration? scheduleDelay,
19 :
20 : /// Parsed from `OTEL_BLRP_EXPORT_TIMEOUT`
21 : Duration? exportTimeout,
22 :
23 : /// Parsed from `OTEL_BLRP_MAX_QUEUE_SIZE`
24 : int? maxQueueSize,
25 :
26 : /// Parsed from `OTEL_BLRP_MAX_EXPORT_BATCH_SIZE`
27 : int? maxExportBatchSize,
28 : });
29 :
30 : /// Raw OTLP environment values parsed by [OTelEnv.getOtlpConfig].
31 : ///
32 : /// All fields are nullable — `null` means the corresponding env var was
33 : /// unset or unparseable. Signal-specific variables take precedence over
34 : /// general ones per the OTel specification.
35 : typedef OtlpEnvironmentValues = ({
36 : /// Parsed from `OTEL_EXPORTER_OTLP_{SIGNAL}_ENDPOINT` or
37 : /// `OTEL_EXPORTER_OTLP_ENDPOINT`
38 : String? endpoint,
39 :
40 : /// Parsed from `OTEL_EXPORTER_OTLP_{SIGNAL}_PROTOCOL` or
41 : /// `OTEL_EXPORTER_OTLP_PROTOCOL`
42 : String? protocol,
43 :
44 : /// Parsed from `OTEL_EXPORTER_OTLP_{SIGNAL}_HEADERS` or
45 : /// `OTEL_EXPORTER_OTLP_HEADERS`
46 : Map<String, String>? headers,
47 :
48 : /// Parsed from `OTEL_EXPORTER_OTLP_{SIGNAL}_INSECURE` or
49 : /// `OTEL_EXPORTER_OTLP_INSECURE`
50 : bool? insecure,
51 :
52 : /// Parsed from `OTEL_EXPORTER_OTLP_{SIGNAL}_TIMEOUT` or
53 : /// `OTEL_EXPORTER_OTLP_TIMEOUT`
54 : Duration? timeout,
55 :
56 : /// Parsed from `OTEL_EXPORTER_OTLP_{SIGNAL}_COMPRESSION` or
57 : /// `OTEL_EXPORTER_OTLP_COMPRESSION`
58 : String? compression,
59 :
60 : /// Parsed from `OTEL_EXPORTER_OTLP_{SIGNAL}_CERTIFICATE` or
61 : /// `OTEL_EXPORTER_OTLP_CERTIFICATE`
62 : String? certificate,
63 :
64 : /// Parsed from `OTEL_EXPORTER_OTLP_{SIGNAL}_CLIENT_KEY` or
65 : /// `OTEL_EXPORTER_OTLP_CLIENT_KEY`
66 : String? clientKey,
67 :
68 : /// Parsed from `OTEL_EXPORTER_OTLP_{SIGNAL}_CLIENT_CERTIFICATE` or
69 : /// `OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE`
70 : String? clientCertificate,
71 : });
72 :
73 : /// Raw BSP environment values parsed by [OTelEnv.getBspConfig].
74 : ///
75 : /// All fields are nullable — `null` means the corresponding env var was
76 : /// unset or contained a non-numeric value (a warning is logged in that case).
77 : /// Domain-level validation belongs in
78 : /// [BatchSpanProcessorConfig.fromEnvironment], not here.
79 : typedef BspEnvironmentValues = ({
80 : /// Parsed from `OTEL_BSP_SCHEDULE_DELAY`
81 : Duration? scheduleDelay,
82 :
83 : /// Parsed from `OTEL_BSP_EXPORT_TIMEOUT`
84 : Duration? exportTimeout,
85 :
86 : /// Parsed from `OTEL_BSP_MAX_QUEUE_SIZE`
87 : int? maxQueueSize,
88 :
89 : /// Parsed from `OTEL_BSP_MAX_EXPORT_BATCH_SIZE`
90 : int? maxExportBatchSize,
91 : });
92 :
93 : /// Raw service environment values parsed by [OTelEnv.getServiceConfig].
94 : ///
95 : /// All fields are nullable — `null` means the corresponding env var was
96 : /// unset. `OTEL_SERVICE_NAME` takes precedence over `service.name` in
97 : /// `OTEL_RESOURCE_ATTRIBUTES` per the spec.
98 : typedef ServiceEnvironmentValues = ({
99 : /// Parsed from `OTEL_SERVICE_NAME` or `service.name` in
100 : /// `OTEL_RESOURCE_ATTRIBUTES`
101 : String? serviceName,
102 :
103 : /// Parsed from `service.version` in `OTEL_RESOURCE_ATTRIBUTES`
104 : String? serviceVersion,
105 : });
106 :
107 : /// Raw attribute limit values parsed by [OTelEnv.getAttributeLimits].
108 : ///
109 : /// All fields are nullable — `null` means the corresponding env var was
110 : /// unset or contained a non-numeric value.
111 : typedef AttributeLimitsEnvironmentValues = ({
112 : /// Parsed from `OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT`
113 : int? attributeValueLengthLimit,
114 :
115 : /// Parsed from `OTEL_ATTRIBUTE_COUNT_LIMIT`
116 : int? attributeCountLimit,
117 : });
118 :
119 : /// Raw metrics environment values parsed by [OTelEnv.getMetricsConfig].
120 : ///
121 : /// All fields are nullable — `null` means the corresponding env var was
122 : /// unset. Domain-level validation and defaults belong in
123 : /// [MetricsSdkConfig.fromEnvironment], not here.
124 : typedef MetricsEnvironmentValues = ({
125 : /// Parsed from `OTEL_METRICS_EXEMPLAR_FILTER`
126 : String? exemplarFilter,
127 :
128 : /// Parsed from `OTEL_METRIC_EXPORT_INTERVAL`
129 : Duration? exportInterval,
130 :
131 : /// Parsed from `OTEL_METRIC_EXPORT_TIMEOUT`
132 : Duration? exportTimeout,
133 : });
134 :
135 : /// Utility class for handling OpenTelemetry environment variables.
136 : ///
137 : /// This class provides methods for reading standard OpenTelemetry environment
138 : /// variables and applying their configuration to the SDK.
139 : ///
140 : /// OpenTelemetry standard environment variables:
141 : /// https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/
142 : class OTelEnv {
143 : /// Initialize logging based on environment variables.
144 : ///
145 : /// This method reads the logging-related environment variables
146 : /// and configures the OTelLog accordingly.
147 : ///
148 : /// If a custom log function has already been set (e.g., by tests),
149 : /// this method will preserve it along with the current log level.
150 : /// This allows tests to fully control logging configuration without
151 : /// environment variables overriding their settings.
152 155 : static void initializeLogging() {
153 : // Save the current log function to check if it's custom
154 155 : final existingLogFunction = OTelLog.logFunction;
155 :
156 : // A custom function is one that's not null and not the default print function
157 : final hasCustomLogFunction =
158 155 : existingLogFunction != null && existingLogFunction != print;
159 :
160 : // Set log level and function based on environment variable,
161 : // but only if no custom log function is already configured.
162 297 : final logLevel = _getEnv(otelLogLevel)?.toLowerCase();
163 : if (logLevel != null && !hasCustomLogFunction) {
164 : switch (logLevel) {
165 124 : case 'trace':
166 124 : OTelLog.enableTraceLogging();
167 : break;
168 1 : case 'debug':
169 1 : OTelLog.enableDebugLogging();
170 : break;
171 1 : case 'info':
172 1 : OTelLog.enableInfoLogging();
173 : break;
174 1 : case 'warn':
175 1 : OTelLog.enableWarnLogging();
176 : break;
177 1 : case 'error':
178 1 : OTelLog.enableErrorLogging();
179 : break;
180 1 : case 'fatal':
181 1 : OTelLog.enableFatalLogging();
182 : break;
183 : default:
184 : // No change to logging if level not recognized
185 : break;
186 : }
187 :
188 : OTelLog.logFunction = print;
189 : }
190 :
191 : // Dart-specific per-signal diagnostic sinks, named per the spec's
192 : // language-specific env var convention (OTEL_{LANGUAGE}_{FEATURE}).
193 : // The spec's self-diagnostics section leaves this to language
194 : // conventions; OTEL_LOG_LEVEL governs only the internal logger level.
195 155 : if (_getEnvBool(otelDartLogMetrics) && OTelLog.metricLogFunction == null) {
196 : OTelLog.metricLogFunction = print;
197 : }
198 155 : if (_getEnvBool(otelDartLogSpans) && OTelLog.spanLogFunction == null) {
199 : OTelLog.spanLogFunction = print;
200 : }
201 155 : if (_getEnvBool(otelDartLogExport) && OTelLog.exportLogFunction == null) {
202 : OTelLog.exportLogFunction = print;
203 : }
204 : }
205 :
206 : /// Applies the OTLP header log allowlist.
207 : ///
208 : /// [headerNames] comes from `OTel.initialize`. When null,
209 : /// `OTEL_DART_HEADER_LOG_ALLOWLIST` is read instead; code wins over
210 : /// configuration and the two are never combined. With neither set, no header
211 : /// value is logged.
212 : ///
213 : /// The allowlist is read at log time, so call this before anything logs
214 : /// headers.
215 153 : static void applyHeaderLogAllowlist([Iterable<String>? headerNames]) {
216 : if (headerNames != null) {
217 0 : configureHeaderLogAllowlist(headerNames);
218 : return;
219 : }
220 153 : configureHeaderLogAllowlist(
221 306 : parseHeaderLogAllowlist(_getEnv(otelDartHeaderLogAllowlist)),
222 : );
223 : }
224 :
225 : /// Get OTLP configuration from environment variables.
226 : ///
227 : /// Returns an [OtlpEnvironmentValues] record containing the OTLP
228 : /// configuration read from environment variables. Signal-specific
229 : /// variables take precedence over general ones.
230 155 : static OtlpEnvironmentValues getOtlpConfig({String signal = 'traces'}) {
231 : // Get endpoint (signal-specific takes precedence)
232 : String? endpoint;
233 : switch (signal) {
234 155 : case 'traces':
235 155 : endpoint = _getEnv(otelExporterOtlpTracesEndpoint) ??
236 155 : _getEnv(otelExporterOtlpEndpoint);
237 : break;
238 142 : case 'metrics':
239 133 : endpoint = _getEnv(otelExporterOtlpMetricsEndpoint) ??
240 133 : _getEnv(otelExporterOtlpEndpoint);
241 : break;
242 129 : case 'logs':
243 129 : endpoint = _getEnv(otelExporterOtlpLogsEndpoint) ??
244 129 : _getEnv(otelExporterOtlpEndpoint);
245 : break;
246 : }
247 :
248 : // Get protocol (signal-specific takes precedence)
249 : String? protocol;
250 : switch (signal) {
251 155 : case 'traces':
252 155 : protocol = _getEnv(otelExporterOtlpTracesProtocol) ??
253 155 : _getEnv(otelExporterOtlpProtocol);
254 : break;
255 142 : case 'metrics':
256 133 : protocol = _getEnv(otelExporterOtlpMetricsProtocol) ??
257 133 : _getEnv(otelExporterOtlpProtocol);
258 : break;
259 129 : case 'logs':
260 129 : protocol = _getEnv(otelExporterOtlpLogsProtocol) ??
261 129 : _getEnv(otelExporterOtlpProtocol);
262 : break;
263 : }
264 :
265 : // Get headers (signal-specific takes precedence)
266 : Map<String, String>? parsedHeaders;
267 : String? rawHeaders;
268 : switch (signal) {
269 155 : case 'traces':
270 155 : rawHeaders = _getEnv(otelExporterOtlpTracesHeaders) ??
271 155 : _getEnv(otelExporterOtlpHeaders);
272 : break;
273 142 : case 'metrics':
274 133 : rawHeaders = _getEnv(otelExporterOtlpMetricsHeaders) ??
275 133 : _getEnv(otelExporterOtlpHeaders);
276 : break;
277 129 : case 'logs':
278 129 : rawHeaders = _getEnv(otelExporterOtlpLogsHeaders) ??
279 129 : _getEnv(otelExporterOtlpHeaders);
280 : break;
281 : }
282 : if (rawHeaders != null) {
283 1 : if (OTelLog.isDebug()) {
284 : // The raw value holds the Authorization header the loop below redacts.
285 2 : OTelLog.debug('OTelEnv: Parsing $signal headers from env');
286 : }
287 1 : parsedHeaders = _parseHeaders(rawHeaders);
288 1 : if (OTelLog.isDebug()) {
289 3 : OTelLog.debug('OTelEnv: Parsed ${parsedHeaders.length} header(s)');
290 2 : parsedHeaders.forEach((key, value) {
291 3 : OTelLog.debug(' ${formatHeaderForLog(key, value)}');
292 : });
293 : }
294 : }
295 :
296 : // Get insecure setting (signal-specific takes precedence)
297 : bool? insecure;
298 : switch (signal) {
299 155 : case 'traces':
300 155 : insecure = _getEnvBoolNullable(otelExporterOtlpTracesInsecure) ??
301 155 : _getEnvBoolNullable(otelExporterOtlpInsecure);
302 : break;
303 142 : case 'metrics':
304 133 : insecure = _getEnvBoolNullable(otelExporterOtlpMetricsInsecure) ??
305 133 : _getEnvBoolNullable(otelExporterOtlpInsecure);
306 : break;
307 129 : case 'logs':
308 129 : insecure = _getEnvBoolNullable(otelExporterOtlpLogsInsecure) ??
309 129 : _getEnvBoolNullable(otelExporterOtlpInsecure);
310 : break;
311 : }
312 :
313 : // Get timeout (signal-specific takes precedence)
314 : Duration? parsedTimeout;
315 : String? rawTimeout;
316 : switch (signal) {
317 155 : case 'traces':
318 155 : rawTimeout = _getEnv(otelExporterOtlpTracesTimeout) ??
319 155 : _getEnv(otelExporterOtlpTimeout);
320 : break;
321 142 : case 'metrics':
322 133 : rawTimeout = _getEnv(otelExporterOtlpMetricsTimeout) ??
323 133 : _getEnv(otelExporterOtlpTimeout);
324 : break;
325 129 : case 'logs':
326 129 : rawTimeout = _getEnv(otelExporterOtlpLogsTimeout) ??
327 129 : _getEnv(otelExporterOtlpTimeout);
328 : break;
329 : }
330 : if (rawTimeout != null) {
331 1 : final timeoutMs = int.tryParse(rawTimeout);
332 : if (timeoutMs != null) {
333 1 : parsedTimeout = Duration(milliseconds: timeoutMs);
334 : }
335 : }
336 :
337 : // Get compression (signal-specific takes precedence)
338 : String? compression;
339 : switch (signal) {
340 155 : case 'traces':
341 155 : compression = _getEnv(otelExporterOtlpTracesCompression) ??
342 155 : _getEnv(otelExporterOtlpCompression);
343 : break;
344 142 : case 'metrics':
345 133 : compression = _getEnv(otelExporterOtlpMetricsCompression) ??
346 133 : _getEnv(otelExporterOtlpCompression);
347 : break;
348 129 : case 'logs':
349 129 : compression = _getEnv(otelExporterOtlpLogsCompression) ??
350 129 : _getEnv(otelExporterOtlpCompression);
351 : break;
352 : }
353 :
354 : // Get certificate (signal-specific takes precedence)
355 : String? certificate;
356 : switch (signal) {
357 155 : case 'traces':
358 155 : certificate = _getEnv(otelExporterOtlpTracesCertificate) ??
359 155 : _getEnv(otelExporterOtlpCertificate);
360 : break;
361 142 : case 'metrics':
362 133 : certificate = _getEnv(otelExporterOtlpMetricsCertificate) ??
363 133 : _getEnv(otelExporterOtlpCertificate);
364 : break;
365 129 : case 'logs':
366 129 : certificate = _getEnv(otelExporterOtlpLogsCertificate) ??
367 129 : _getEnv(otelExporterOtlpCertificate);
368 : break;
369 : }
370 :
371 : // Get client key (signal-specific takes precedence)
372 : String? clientKey;
373 : switch (signal) {
374 155 : case 'traces':
375 155 : clientKey = _getEnv(otelExporterOtlpTracesClientKey) ??
376 155 : _getEnv(otelExporterOtlpClientKey);
377 : break;
378 142 : case 'metrics':
379 133 : clientKey = _getEnv(otelExporterOtlpMetricsClientKey) ??
380 133 : _getEnv(otelExporterOtlpClientKey);
381 : break;
382 129 : case 'logs':
383 129 : clientKey = _getEnv(otelExporterOtlpLogsClientKey) ??
384 129 : _getEnv(otelExporterOtlpClientKey);
385 : break;
386 : }
387 :
388 : // Get client certificate (signal-specific takes precedence)
389 : String? clientCertificate;
390 : switch (signal) {
391 155 : case 'traces':
392 155 : clientCertificate = _getEnv(otelExporterOtlpTracesClientCertificate) ??
393 155 : _getEnv(otelExporterOtlpClientCertificate);
394 : break;
395 142 : case 'metrics':
396 133 : clientCertificate = _getEnv(otelExporterOtlpMetricsClientCertificate) ??
397 133 : _getEnv(otelExporterOtlpClientCertificate);
398 : break;
399 129 : case 'logs':
400 129 : clientCertificate = _getEnv(otelExporterOtlpLogsClientCertificate) ??
401 129 : _getEnv(otelExporterOtlpClientCertificate);
402 : break;
403 : }
404 :
405 : return (
406 : endpoint: endpoint,
407 : protocol: protocol,
408 : headers: parsedHeaders,
409 : insecure: insecure,
410 : timeout: parsedTimeout,
411 : compression: compression,
412 : certificate: certificate,
413 : clientKey: clientKey,
414 : clientCertificate: clientCertificate,
415 : );
416 : }
417 :
418 : /// Get service configuration from environment variables.
419 : ///
420 : /// Returns a [ServiceEnvironmentValues] record containing the service
421 : /// configuration read from environment variables.
422 : ///
423 : /// Handles the spec precedence rules:
424 : /// - If `service.name` is in OTEL_RESOURCE_ATTRIBUTES, it's used as the base value
425 : /// - OTEL_SERVICE_NAME takes precedence over `service.name` in OTEL_RESOURCE_ATTRIBUTES
426 : /// - `service.version` comes from OTEL_RESOURCE_ATTRIBUTES only
427 : /// Parses a resource attributes string into a map, handling escaping,
428 : /// percent-encoding, and dropping malformed entries.
429 5 : static Map<String, String> parseResourceAttributesString(String resourceStr) {
430 5 : final resourceAttrs = <String, String>{};
431 10 : final parts = resourceStr.split(RegExp(r'(?<!\\),'));
432 10 : for (var part in parts) {
433 5 : part = part.trim();
434 5 : final equalIndex = part.indexOf('=');
435 10 : if (equalIndex == -1) continue;
436 :
437 10 : final key = part.substring(0, equalIndex).trim();
438 15 : var value = part.substring(equalIndex + 1).trim();
439 :
440 : // Handle percent-encoded characters safely
441 : try {
442 5 : value = Uri.decodeComponent(value);
443 : } catch (e) {
444 1 : if (OTelLog.isWarn()) {
445 1 : OTelLog.warn(
446 1 : 'OTelEnv: Dropped malformed resource attribute "$key": $e');
447 : }
448 : continue; // Drop the malformed attribute per spec
449 : }
450 :
451 : // Remove escape characters
452 5 : value = value.replaceAll(r'\,', ',');
453 :
454 5 : resourceAttrs[key] = value;
455 : }
456 : return resourceAttrs;
457 : }
458 :
459 155 : static ServiceEnvironmentValues getServiceConfig() {
460 : String? parsedServiceName;
461 : String? parsedServiceVersion;
462 :
463 : // First, parse service.name and service.version from OTEL_RESOURCE_ATTRIBUTES
464 155 : final resourceStr = _getEnv(otelResourceAttributes);
465 : if (resourceStr != null) {
466 4 : final attrs = parseResourceAttributesString(resourceStr);
467 8 : parsedServiceName = attrs[Service.serviceName.key];
468 8 : parsedServiceVersion = attrs[Service.serviceVersion.key];
469 : }
470 :
471 : // OTEL_SERVICE_NAME takes precedence over service.name from resource attributes
472 155 : final serviceName = _getEnv(otelServiceName);
473 : if (serviceName != null) {
474 : parsedServiceName = serviceName;
475 : }
476 :
477 : return (
478 : serviceName: parsedServiceName,
479 : serviceVersion: parsedServiceVersion,
480 : );
481 : }
482 :
483 : /// Get resource attributes from environment variables.
484 : ///
485 : /// Parses the OTEL_RESOURCE_ATTRIBUTES environment variable which should be
486 : /// a comma-separated list of key=value pairs.
487 2 : static Map<String, Object> getResourceAttributes() {
488 2 : final resourceStr = _getEnv(otelResourceAttributes);
489 : if (resourceStr != null) {
490 1 : return parseResourceAttributesString(resourceStr);
491 : }
492 1 : return <String, Object>{};
493 : }
494 :
495 : /// Whether `OTEL_SDK_DISABLED` is set to a truthy value.
496 : ///
497 : /// Per the OTel spec, when this is true the SDK acts as a no-op for all
498 : /// signals — no span processors, metric readers, or log record processors
499 : /// should be installed.
500 308 : static bool isSdkDisabled() => _getEnvBool(otelSdkDisabled);
501 :
502 : /// Get the selected exporter for a signal.
503 : ///
504 : /// Returns the exporter type configured via environment variables.
505 155 : static String? getExporter({String signal = 'traces'}) {
506 : switch (signal) {
507 155 : case 'traces':
508 155 : return _getEnv(otelTracesExporter);
509 142 : case 'metrics':
510 133 : return _getEnv(otelMetricsExporter);
511 129 : case 'logs':
512 129 : return _getEnv(otelLogsExporter);
513 : default:
514 : return null;
515 : }
516 : }
517 :
518 : /// Reads `OTEL_<SIGNAL>_EXPORTER` as the spec's comma-separated list
519 : /// (sdk-environment-variables.md, "Exporter Selection": "The
520 : /// implementation MAY accept a comma-separated list to enable setting
521 : /// multiple exporters"). Returns normalized (trimmed, lowercased,
522 : /// deduplicated) names, or null when the variable is unset or empty.
523 154 : static List<String>? getExporters({String signal = 'traces'}) {
524 154 : final raw = getExporter(signal: signal);
525 : if (raw == null) return null;
526 12 : final names = <String>[];
527 24 : for (final part in raw.split(',')) {
528 24 : final name = part.trim().toLowerCase();
529 24 : if (name.isNotEmpty && !names.contains(name)) {
530 12 : names.add(name);
531 : }
532 : }
533 12 : return names.isEmpty ? null : names;
534 : }
535 :
536 : /// Get Batch Span Processor (BSP) configuration from environment variables.
537 : ///
538 : /// Returns a [BspEnvironmentValues] record containing the raw parsed BSP
539 : /// values from environment variables. Fields are `null` when the
540 : /// corresponding env var is unset or contains a non-numeric value.
541 : ///
542 : /// Domain-level defaults, validation, and clamping belong in
543 : /// [BatchSpanProcessorConfig.fromEnvironment], not here.
544 154 : static BspEnvironmentValues getBspConfig() {
545 : // Get schedule delay
546 : Duration? parsedScheduleDelay;
547 154 : final scheduleDelay = _getEnv(otelBspScheduleDelay);
548 : if (scheduleDelay != null) {
549 2 : final delayMs = int.tryParse(scheduleDelay);
550 : if (delayMs != null) {
551 2 : parsedScheduleDelay = Duration(milliseconds: delayMs);
552 : } else {
553 1 : if (OTelLog.isWarn()) {
554 2 : OTelLog.warn('OTelEnv: Invalid OTEL_BSP_SCHEDULE_DELAY value '
555 : '"$scheduleDelay", ignoring.');
556 : }
557 : }
558 : }
559 :
560 : // Get export timeout
561 : Duration? parsedExportTimeout;
562 154 : final exportTimeout = _getEnv(otelBspExportTimeout);
563 : if (exportTimeout != null) {
564 2 : final timeoutMs = int.tryParse(exportTimeout);
565 : if (timeoutMs != null) {
566 2 : parsedExportTimeout = Duration(milliseconds: timeoutMs);
567 : } else {
568 1 : if (OTelLog.isWarn()) {
569 2 : OTelLog.warn('OTelEnv: Invalid OTEL_BSP_EXPORT_TIMEOUT value '
570 : '"$exportTimeout", ignoring.');
571 : }
572 : }
573 : }
574 :
575 : // Get max queue size
576 : int? parsedMaxQueueSize;
577 154 : final maxQueueSize = _getEnv(otelBspMaxQueueSize);
578 : if (maxQueueSize != null) {
579 2 : final size = int.tryParse(maxQueueSize);
580 : if (size != null) {
581 : parsedMaxQueueSize = size;
582 : } else {
583 1 : if (OTelLog.isWarn()) {
584 2 : OTelLog.warn('OTelEnv: Invalid OTEL_BSP_MAX_QUEUE_SIZE value '
585 : '"$maxQueueSize", ignoring.');
586 : }
587 : }
588 : }
589 :
590 : // Get max export batch size
591 : int? parsedMaxExportBatchSize;
592 154 : final maxExportBatchSize = _getEnv(otelBspMaxExportBatchSize);
593 : if (maxExportBatchSize != null) {
594 2 : final size = int.tryParse(maxExportBatchSize);
595 : if (size != null) {
596 : parsedMaxExportBatchSize = size;
597 : } else {
598 1 : if (OTelLog.isWarn()) {
599 2 : OTelLog.warn('OTelEnv: Invalid OTEL_BSP_MAX_EXPORT_BATCH_SIZE '
600 : 'value "$maxExportBatchSize", ignoring.');
601 : }
602 : }
603 : }
604 :
605 : return (
606 : scheduleDelay: parsedScheduleDelay,
607 : exportTimeout: parsedExportTimeout,
608 : maxQueueSize: parsedMaxQueueSize,
609 : maxExportBatchSize: parsedMaxExportBatchSize,
610 : );
611 : }
612 :
613 : /// Reads `OTEL_PROPAGATORS` (sdk-environment-variables.md, "General SDK
614 : /// Configuration"): a comma-separated list of propagator names. Returns
615 : /// the normalized (trimmed, lowercased) names, defaulting to the spec
616 : /// default `[tracecontext, baggage]` when unset or empty.
617 154 : static List<String> getPropagators() {
618 154 : final raw = _getEnv(otelPropagators);
619 4 : if (raw == null || raw.trim().isEmpty) {
620 : return const ['tracecontext', 'baggage'];
621 : }
622 : return raw
623 2 : .split(',')
624 8 : .map((name) => name.trim().toLowerCase())
625 6 : .where((name) => name.isNotEmpty)
626 2 : .toList();
627 : }
628 :
629 : /// Get Batch LogRecord Processor (BLRP) configuration from environment variables.
630 : ///
631 : /// Returns a [BlrpEnvironmentValues] record containing the raw parsed BLRP
632 : /// values from environment variables. Fields are `null` when the
633 : /// corresponding env var is unset or contains a non-numeric value.
634 : ///
635 : /// Domain-level defaults, validation, and clamping belong in
636 : /// [BatchLogRecordProcessorConfig.fromEnvironment], not here.
637 128 : static BlrpEnvironmentValues getBlrpConfig() {
638 : // Get schedule delay — 0 is valid ("export as fast as possible")
639 : final scheduleDelayMs =
640 128 : getPositiveIntEnv(otelBlrpScheduleDelay, minInclusive: 0);
641 :
642 : // Get export timeout — 0 is valid ("no limit")
643 : final exportTimeoutMs =
644 128 : getPositiveIntEnv(otelBlrpExportTimeout, minInclusive: 0);
645 :
646 : // Get queue and batch sizes — just parse, no domain validation here
647 : final parsedMaxQueueSize =
648 128 : getPositiveIntEnv(otelBlrpMaxQueueSize, minInclusive: 0);
649 : final parsedMaxExportBatchSize =
650 128 : getPositiveIntEnv(otelBlrpMaxExportBatchSize, minInclusive: 0);
651 :
652 : return (
653 : scheduleDelay: scheduleDelayMs != null
654 3 : ? Duration(milliseconds: scheduleDelayMs)
655 : : null,
656 : exportTimeout: exportTimeoutMs != null
657 2 : ? Duration(milliseconds: exportTimeoutMs)
658 : : null,
659 : maxQueueSize: parsedMaxQueueSize,
660 : maxExportBatchSize: parsedMaxExportBatchSize,
661 : );
662 : }
663 :
664 3 : static AttributeLimitsEnvironmentValues _parseAttributeLimits({
665 : required String lengthVar,
666 : required String countVar,
667 : String? fallbackLengthVar,
668 : String? fallbackCountVar,
669 : }) {
670 3 : int? parseLimit(String primaryVar, String? fallbackVar) {
671 3 : var val = _getEnv(primaryVar);
672 : var varName = primaryVar;
673 :
674 : if (val == null && fallbackVar != null) {
675 3 : val = _getEnv(fallbackVar);
676 : varName = fallbackVar;
677 : }
678 :
679 : if (val != null) {
680 1 : final limit = int.tryParse(val);
681 1 : if (limit != null && limit >= 0) {
682 : return limit;
683 : }
684 1 : if (OTelLog.isWarn()) {
685 2 : OTelLog.warn('OTelEnv: Invalid value "$val" for $varName. '
686 : 'Limit must be a non-negative integer.');
687 : }
688 : }
689 : return null;
690 : }
691 :
692 : return (
693 3 : attributeValueLengthLimit: parseLimit(lengthVar, fallbackLengthVar),
694 3 : attributeCountLimit: parseLimit(countVar, fallbackCountVar),
695 : );
696 : }
697 :
698 : /// Get general attribute limits from environment variables.
699 : ///
700 : /// These limits apply globally to all telemetry signals (traces, metrics,
701 : /// logs) unless overridden by signal-specific limits (e.g., span or
702 : /// log record attribute limits).
703 : ///
704 : /// Returns an [AttributeLimitsEnvironmentValues] containing the general attribute
705 : /// limits. Fields that are not set via environment variables will be `null`.
706 : ///
707 : /// Per the OpenTelemetry specification:
708 : /// - Values exceeding the length limit should be truncated.
709 : /// - Attributes exceeding the count limit should be dropped.
710 : /// - Warnings should be logged when limits are exceeded.
711 : ///
712 : /// See: https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/#attribute-limits
713 2 : static AttributeLimitsEnvironmentValues getAttributeLimits() {
714 2 : return _parseAttributeLimits(
715 : lengthVar: otelAttributeValueLengthLimit,
716 : countVar: otelAttributeCountLimit,
717 : );
718 : }
719 :
720 : /// Get LogRecord attribute limits from environment variables.
721 : ///
722 : /// Returns a [AttributeLimitsEnvironmentValues] record containing the
723 : /// log record attribute limits. Fields are `null` when the corresponding
724 : /// env var is unset or contains a non-numeric value.
725 3 : static AttributeLimitsEnvironmentValues getLogRecordLimits() {
726 3 : return _parseAttributeLimits(
727 : lengthVar: otelLogrecordAttributeValueLengthLimit,
728 : countVar: otelLogrecordAttributeCountLimit,
729 : fallbackLengthVar: otelAttributeValueLengthLimit,
730 : fallbackCountVar: otelAttributeCountLimit,
731 : );
732 : }
733 :
734 : /// Get metrics SDK configuration from environment variables.
735 131 : static MetricsEnvironmentValues getMetricsConfig() {
736 : final exportIntervalMs =
737 131 : getPositiveIntEnv(otelMetricExportInterval, minInclusive: 0);
738 : final exportTimeoutMs =
739 131 : getPositiveIntEnv(otelMetricExportTimeout, minInclusive: 0);
740 :
741 : return (
742 131 : exemplarFilter: _getEnv(otelMetricsExemplarFilter),
743 : exportInterval: exportIntervalMs != null
744 0 : ? Duration(milliseconds: exportIntervalMs)
745 : : null,
746 : exportTimeout: exportTimeoutMs != null
747 0 : ? Duration(milliseconds: exportTimeoutMs)
748 : : null,
749 : );
750 : }
751 :
752 : /// Resolves whether an OTLP connection should use TLS, per the OTLP
753 : /// exporter spec's precedence:
754 : ///
755 : /// 1. if [endpoint] carries an `http://` or `https://` scheme, that scheme
756 : /// alone decides — the spec says a scheme "takes precedence over the
757 : /// `insecure` configuration setting", from either channel;
758 : /// 2. otherwise the endpoint is scheme-less (gRPC's native
759 : /// `my-collector:4317` form), which is the only case where the insecure
760 : /// setting applies at all. There, an explicit programmatic choice
761 : /// ([explicitSecure]) wins, being the code equivalent of the
762 : /// environment variable;
763 : /// 3. otherwise `OTEL_EXPORTER_OTLP_INSECURE` (or its per-signal
764 : /// variant, passed as [envInsecure]) applies;
765 : /// 4. otherwise [fallback] (secure by default).
766 : ///
767 : /// Note that the setting is meaningful only for OTLP/gRPC: OTLP/HTTP
768 : /// always takes its security from the endpoint scheme.
769 : ///
770 : /// Bare `host:port` endpoints parse with a bogus scheme (`host`), so
771 : /// only exact `http`/`https` schemes participate in step 2.
772 153 : static bool resolveOtlpSecure({
773 : bool? explicitSecure,
774 : bool? envInsecure,
775 : String? endpoint,
776 : bool fallback = true,
777 : }) {
778 : // The endpoint scheme outranks the insecure setting from either
779 : // channel: "A scheme of https indicates a secure connection and takes
780 : // precedence over the insecure configuration setting" (and likewise
781 : // for http) - protocol/exporter.md, Endpoint (OTLP/gRPC).
782 : final scheme =
783 456 : endpoint == null ? null : Uri.tryParse(endpoint)?.scheme.toLowerCase();
784 153 : if (scheme == 'http') {
785 : return false;
786 : }
787 129 : if (scheme == 'https') {
788 : return true;
789 : }
790 : // Scheme-less endpoint: the insecure setting decides. The programmatic
791 : // choice is the code equivalent of OTEL_EXPORTER_OTLP_INSECURE and wins
792 : // over it, per "The environment-based configuration MUST have a direct
793 : // code configuration equivalent" - configuration/sdk-environment-variables.md.
794 : if (explicitSecure != null) {
795 : return explicitSecure;
796 : }
797 : if (envInsecure != null) {
798 : return !envInsecure;
799 : }
800 : return fallback;
801 : }
802 :
803 : /// Parse headers from the environment variable format.
804 : ///
805 : /// Headers are expected in the format: key1=value1,key2=value2
806 : /// Note: Header values can contain '=' characters (e.g., base64), so we only
807 : /// split on the first '=' for each pair.
808 1 : static Map<String, String> _parseHeaders(String headerStr) {
809 1 : final headers = <String, String>{};
810 :
811 1 : final pairs = headerStr.split(',');
812 2 : for (final pair in pairs) {
813 1 : final equalIndex = pair.indexOf('=');
814 4 : if (equalIndex > 0 && equalIndex < pair.length - 1) {
815 2 : final key = pair.substring(0, equalIndex).trim();
816 3 : final value = pair.substring(equalIndex + 1).trim();
817 1 : headers[key] = value;
818 : }
819 : }
820 :
821 : return headers;
822 : }
823 :
824 : /// Get environment variable value.
825 : ///
826 : /// This method safely retrieves an environment variable value,
827 : /// handling exceptions that might occur in environments where
828 : /// Platform is not available (e.g., browsers).
829 : ///
830 : /// @param name The name of the environment variable
831 : /// @return The value of the environment variable, or null if not found
832 155 : static String? _getEnv(String name) {
833 310 : return EnvironmentService.instance.getValue(name);
834 : }
835 :
836 : /// Get boolean environment variable value.
837 : ///
838 : /// This method converts an environment variable value to a boolean.
839 : /// Values of '1', 'true', 'yes', and 'on' (case-insensitive) are considered true.
840 : ///
841 : /// @param name The name of the environment variable
842 : /// @return true if the environment variable has a truthy value, false otherwise
843 155 : static bool _getEnvBool(String name) {
844 155 : final rawValue = _getEnv(name);
845 143 : if (rawValue == null || rawValue.isEmpty) return false;
846 :
847 143 : final value = rawValue.toLowerCase();
848 143 : if (value == 'true') {
849 : return true;
850 1 : } else if (value == 'false') {
851 : return false;
852 : } else {
853 1 : if (OTelLog.isWarn()) {
854 2 : OTelLog.warn('OTelEnv: Invalid boolean value for $name: "$rawValue". '
855 : 'Expected "true" or "false". Treating as false.');
856 : }
857 : return false;
858 : }
859 : }
860 :
861 : /// Get boolean environment variable value that can be null.
862 : ///
863 : /// This method converts an environment variable value to a boolean.
864 : /// Only the case-insensitive string 'true' is considered true.
865 : /// All other values are considered false.
866 : /// Returns null only when the variable is unset or empty.
867 : ///
868 : /// @param name The name of the environment variable
869 : /// @return true/false if the environment variable is set, null otherwise
870 155 : static bool? _getEnvBoolNullable(String name) {
871 155 : final rawValue = _getEnv(name);
872 3 : if (rawValue == null || rawValue.isEmpty) return null;
873 :
874 3 : final value = rawValue.toLowerCase();
875 3 : if (value == 'true') {
876 : return true;
877 2 : } else if (value == 'false') {
878 : return false;
879 : } else {
880 1 : if (OTelLog.isWarn()) {
881 2 : OTelLog.warn('OTelEnv: Invalid boolean value for $name: "$rawValue". '
882 : 'Expected "true" or "false". Treating as false.');
883 : }
884 : return false;
885 : }
886 : }
887 :
888 : /// Get a non-negative integer environment variable value.
889 : ///
890 : /// Returns null when not set, non-numeric, or outside the accepted range.
891 : /// Warns via [OTelLog.warn] when the raw value is present but unusable
892 : /// (non-numeric, below [minInclusive], or above [maxInclusive]).
893 141 : static int? getPositiveIntEnv(
894 : String name, {
895 : required int minInclusive,
896 : int? maxInclusive,
897 : }) {
898 141 : final rawValue = _getEnv(name);
899 : if (rawValue == null) {
900 : return null;
901 : }
902 :
903 3 : final value = int.tryParse(rawValue);
904 : if (value == null) {
905 1 : if (OTelLog.isWarn()) {
906 2 : OTelLog.warn('OTelEnv: Illegal non-numeric value for $name: '
907 : '"$rawValue", ignoring.');
908 : }
909 : return null;
910 : }
911 :
912 3 : if (value < minInclusive) {
913 1 : if (OTelLog.isWarn()) {
914 2 : OTelLog.warn('OTelEnv: Illegal value for $name: $value is below '
915 : 'minimum $minInclusive, ignoring.');
916 : }
917 : return null;
918 : }
919 :
920 0 : if (maxInclusive != null && value > maxInclusive) {
921 0 : if (OTelLog.isWarn()) {
922 0 : OTelLog.warn('OTelEnv: Illegal value for $name: $value exceeds '
923 : 'maximum $maxInclusive, ignoring.');
924 : }
925 : return null;
926 : }
927 :
928 : return value;
929 : }
930 : }
|