From 79ef10e659702b596f577485083271b6ad875981 Mon Sep 17 00:00:00 2001 From: Harshit Kushwaha Date: Sat, 11 Jul 2026 06:25:07 -0400 Subject: [PATCH 1/3] feat: implement BatchLogRecordProcessor environment variables and validation Signed-off-by: Harshit Kushwaha --- lib/src/environment/otel_env.dart | 114 ++++++++++++++----- lib/src/logs/export/logs_config.dart | 45 +++++--- test/unit/environment/env_coverage_test.dart | 70 ++++++++++++ test/unit/logs/logs_config_test.dart | 14 +++ 4 files changed, 201 insertions(+), 42 deletions(-) diff --git a/lib/src/environment/otel_env.dart b/lib/src/environment/otel_env.dart index 06efd98..42acb5b 100644 --- a/lib/src/environment/otel_env.dart +++ b/lib/src/environment/otel_env.dart @@ -395,42 +395,75 @@ class OTelEnv { /// - 'exportTimeout': Duration for the export timeout /// - 'maxQueueSize': int for maximum queue size /// - 'maxExportBatchSize': int for maximum export batch size + /// + /// Validation rules: + /// - Numeric values must be positive integers + /// - `maxExportBatchSize` is clamped to `maxQueueSize` per OTel spec + /// - Defaults used for validation: queue=2048, batch=512 static Map getBlrpConfig() { final config = {}; + const defaultMaxQueueSize = 2048; + const defaultMaxExportBatchSize = 512; // Get schedule delay - final scheduleDelay = _getEnv(otelBlrpScheduleDelay); - if (scheduleDelay != null) { - final delayMs = int.tryParse(scheduleDelay); - if (delayMs != null) { - config['scheduleDelay'] = Duration(milliseconds: delayMs); - } + final scheduleDelayMs = + _getPositiveIntEnv(otelBlrpScheduleDelay, minInclusive: 1); + if (scheduleDelayMs != null) { + config['scheduleDelay'] = Duration(milliseconds: scheduleDelayMs); } // Get export timeout - final exportTimeout = _getEnv(otelBlrpExportTimeout); - if (exportTimeout != null) { - final timeoutMs = int.tryParse(exportTimeout); - if (timeoutMs != null) { - config['exportTimeout'] = Duration(milliseconds: timeoutMs); + final exportTimeoutMs = + _getPositiveIntEnv(otelBlrpExportTimeout, minInclusive: 1); + if (exportTimeoutMs != null) { + config['exportTimeout'] = Duration(milliseconds: exportTimeoutMs); + } + + // Get queue and batch sizes. + final hasMaxQueueSizeEnv = _getEnv(otelBlrpMaxQueueSize) != null; + final hasMaxExportBatchSizeEnv = _getEnv(otelBlrpMaxExportBatchSize) != null; + + final parsedMaxQueueSize = + _getPositiveIntEnv(otelBlrpMaxQueueSize, minInclusive: 1); + var effectiveMaxQueueSize = parsedMaxQueueSize ?? defaultMaxQueueSize; + + if (parsedMaxQueueSize != null) { + config['maxQueueSize'] = parsedMaxQueueSize; + } else if (hasMaxQueueSizeEnv && OTelLog.isWarn()) { + OTelLog.warn( + 'OTelEnv: Ignoring invalid $otelBlrpMaxQueueSize. Value must be a positive integer.', + ); + } + + final parsedMaxExportBatchSize = + _getPositiveIntEnv(otelBlrpMaxExportBatchSize, minInclusive: 1); + var effectiveMaxExportBatchSize = + parsedMaxExportBatchSize ?? defaultMaxExportBatchSize; + + if (parsedMaxExportBatchSize != null) { + config['maxExportBatchSize'] = parsedMaxExportBatchSize; + } else if (hasMaxExportBatchSizeEnv && OTelLog.isWarn()) { + OTelLog.warn( + 'OTelEnv: Ignoring invalid $otelBlrpMaxExportBatchSize. Value must be a positive integer.', + ); + } + + // Per OTel spec, max export batch size must not exceed max queue size. + if (effectiveMaxExportBatchSize > effectiveMaxQueueSize) { + if (OTelLog.isWarn()) { + OTelLog.warn( + 'OTelEnv: $otelBlrpMaxExportBatchSize ($effectiveMaxExportBatchSize) exceeds ' + '$otelBlrpMaxQueueSize ($effectiveMaxQueueSize). Clamping batch size to queue size.', + ); } - } - // Get max queue size - final maxQueueSize = _getEnv(otelBlrpMaxQueueSize); - if (maxQueueSize != null) { - final size = int.tryParse(maxQueueSize); - if (size != null) { - config['maxQueueSize'] = size; - } - } + effectiveMaxExportBatchSize = effectiveMaxQueueSize; + config['maxExportBatchSize'] = effectiveMaxExportBatchSize; - // Get max export batch size - final maxExportBatchSize = _getEnv(otelBlrpMaxExportBatchSize); - if (maxExportBatchSize != null) { - final size = int.tryParse(maxExportBatchSize); - if (size != null) { - config['maxExportBatchSize'] = size; + // Keep returned queue value internally consistent if batch forced us to clamp + // based on an explicitly provided queue value. + if (parsedMaxQueueSize != null) { + effectiveMaxQueueSize = parsedMaxQueueSize; } } @@ -535,4 +568,33 @@ class OTelEnv { return null; } + + /// Get a positive integer environment variable value. + /// + /// Returns null when not set, non-numeric, or outside the accepted range. + static int? _getPositiveIntEnv( + String name, { + required int minInclusive, + int? maxInclusive, + }) { + final rawValue = _getEnv(name); + if (rawValue == null) { + return null; + } + + final value = int.tryParse(rawValue); + if (value == null) { + return null; + } + + if (value < minInclusive) { + return null; + } + + if (maxInclusive != null && value > maxInclusive) { + return null; + } + + return value; + } } diff --git a/lib/src/logs/export/logs_config.dart b/lib/src/logs/export/logs_config.dart index f1d8369..10f6730 100644 --- a/lib/src/logs/export/logs_config.dart +++ b/lib/src/logs/export/logs_config.dart @@ -167,29 +167,42 @@ class LogsConfiguration { /// Creates a log record processor with BLRP configuration from environment. static LogRecordProcessor _createProcessor(LogRecordExporter exporter) { final blrpConfig = OTelEnv.getBlrpConfig(); + final processorConfig = buildBatchLogRecordProcessorConfig(blrpConfig); + return BatchLogRecordProcessor(exporter, processorConfig); + } + + /// Builds [BatchLogRecordProcessorConfig] from BLRP environment config. + /// + /// Exposed for testing to validate normalization and spec-rule handling. + static BatchLogRecordProcessorConfig buildBatchLogRecordProcessorConfig( + Map blrpConfig, + ) { if (blrpConfig.isEmpty) { - // Use defaults - return BatchLogRecordProcessor( - exporter, - const BatchLogRecordProcessorConfig(), - ); + return const BatchLogRecordProcessorConfig(); } // Build config from environment final scheduleDelay = blrpConfig['scheduleDelay'] as Duration?; final exportTimeout = blrpConfig['exportTimeout'] as Duration?; - final maxQueueSize = blrpConfig['maxQueueSize'] as int?; - final maxExportBatchSize = blrpConfig['maxExportBatchSize'] as int?; - - return BatchLogRecordProcessor( - exporter, - BatchLogRecordProcessorConfig( - scheduleDelay: scheduleDelay ?? const Duration(milliseconds: 1000), - exportTimeout: exportTimeout ?? const Duration(seconds: 30), - maxQueueSize: maxQueueSize ?? 2048, - maxExportBatchSize: maxExportBatchSize ?? 512, - ), + final maxQueueSize = blrpConfig['maxQueueSize'] as int? ?? 2048; + var maxExportBatchSize = blrpConfig['maxExportBatchSize'] as int? ?? 512; + + if (maxExportBatchSize > maxQueueSize) { + if (OTelLog.isWarn()) { + OTelLog.warn( + 'LogsConfiguration: maxExportBatchSize ($maxExportBatchSize) exceeds ' + 'maxQueueSize ($maxQueueSize). Clamping batch size to queue size.', + ); + } + maxExportBatchSize = maxQueueSize; + } + + return BatchLogRecordProcessorConfig( + scheduleDelay: scheduleDelay ?? const Duration(milliseconds: 1000), + exportTimeout: exportTimeout ?? const Duration(seconds: 30), + maxQueueSize: maxQueueSize, + maxExportBatchSize: maxExportBatchSize, ); } diff --git a/test/unit/environment/env_coverage_test.dart b/test/unit/environment/env_coverage_test.dart index 853bf72..ca307cc 100644 --- a/test/unit/environment/env_coverage_test.dart +++ b/test/unit/environment/env_coverage_test.dart @@ -165,6 +165,76 @@ void main() { expect(result.containsKey('maxExportBatchSize'), isFalse); }); + test('ignores non-positive scheduleDelay', () async { + final output = await runWithEnv( + 'test/unit/environment/helpers/check_blrp_config.dart', + {'OTEL_BLRP_SCHEDULE_DELAY': '0'}, + ); + final result = jsonDecode(output.trim()) as Map; + expect(result.containsKey('scheduleDelay_ms'), isFalse); + }); + + test('ignores non-positive exportTimeout', () async { + final output = await runWithEnv( + 'test/unit/environment/helpers/check_blrp_config.dart', + {'OTEL_BLRP_EXPORT_TIMEOUT': '-1'}, + ); + final result = jsonDecode(output.trim()) as Map; + expect(result.containsKey('exportTimeout_ms'), isFalse); + }); + + test('ignores non-positive maxQueueSize', () async { + final output = await runWithEnv( + 'test/unit/environment/helpers/check_blrp_config.dart', + {'OTEL_BLRP_MAX_QUEUE_SIZE': '0'}, + ); + final result = jsonDecode(output.trim()) as Map; + expect(result.containsKey('maxQueueSize'), isFalse); + }); + + test('ignores non-positive maxExportBatchSize', () async { + final output = await runWithEnv( + 'test/unit/environment/helpers/check_blrp_config.dart', + {'OTEL_BLRP_MAX_EXPORT_BATCH_SIZE': '-10'}, + ); + final result = jsonDecode(output.trim()) as Map; + expect(result.containsKey('maxExportBatchSize'), isFalse); + }); + + test('clamps maxExportBatchSize to maxQueueSize when both are set', () async { + final output = await runWithEnv( + 'test/unit/environment/helpers/check_blrp_config.dart', + { + 'OTEL_BLRP_MAX_QUEUE_SIZE': '100', + 'OTEL_BLRP_MAX_EXPORT_BATCH_SIZE': '200', + }, + ); + final result = jsonDecode(output.trim()) as Map; + expect(result['maxQueueSize'], equals(100)); + expect(result['maxExportBatchSize'], equals(100)); + }); + + test('clamps maxExportBatchSize to default maxQueueSize when queue is unset', + () async { + final output = await runWithEnv( + 'test/unit/environment/helpers/check_blrp_config.dart', + {'OTEL_BLRP_MAX_EXPORT_BATCH_SIZE': '5000'}, + ); + final result = jsonDecode(output.trim()) as Map; + expect(result['maxExportBatchSize'], equals(2048)); + }); + + test('sets maxExportBatchSize when maxQueueSize is smaller than default batch', + () async { + final output = await runWithEnv( + 'test/unit/environment/helpers/check_blrp_config.dart', + {'OTEL_BLRP_MAX_QUEUE_SIZE': '100'}, + ); + final result = jsonDecode(output.trim()) as Map; + expect(result['maxQueueSize'], equals(100)); + expect(result['maxExportBatchSize'], equals(100)); + }); + test('returns empty map when no BLRP env vars set', () async { final output = await runWithEnv( 'test/unit/environment/helpers/check_blrp_config.dart', diff --git a/test/unit/logs/logs_config_test.dart b/test/unit/logs/logs_config_test.dart index 828e377..b1699f2 100644 --- a/test/unit/logs/logs_config_test.dart +++ b/test/unit/logs/logs_config_test.dart @@ -217,5 +217,19 @@ void main() { expect(config.maxExportBatchSize, equals(1024)); expect(config.exportTimeout, equals(const Duration(seconds: 60))); }); + + test('LogsConfiguration clamps batch size to queue size', () { + final config = LogsConfiguration.buildBatchLogRecordProcessorConfig({ + 'maxQueueSize': 100, + 'maxExportBatchSize': 200, + 'scheduleDelay': const Duration(milliseconds: 1234), + 'exportTimeout': const Duration(milliseconds: 5678), + }); + + expect(config.maxQueueSize, equals(100)); + expect(config.maxExportBatchSize, equals(100)); + expect(config.scheduleDelay, equals(const Duration(milliseconds: 1234))); + expect(config.exportTimeout, equals(const Duration(milliseconds: 5678))); + }); }); } From 015d6bfa296d119fb5ec461e3620768cedb105fe Mon Sep 17 00:00:00 2001 From: Harshit Kushwaha Date: Sat, 11 Jul 2026 06:34:38 -0400 Subject: [PATCH 2/3] format code Signed-off-by: Harshit Kushwaha --- lib/src/environment/otel_env.dart | 3 ++- test/unit/environment/env_coverage_test.dart | 9 ++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/src/environment/otel_env.dart b/lib/src/environment/otel_env.dart index 42acb5b..46ca0ee 100644 --- a/lib/src/environment/otel_env.dart +++ b/lib/src/environment/otel_env.dart @@ -421,7 +421,8 @@ class OTelEnv { // Get queue and batch sizes. final hasMaxQueueSizeEnv = _getEnv(otelBlrpMaxQueueSize) != null; - final hasMaxExportBatchSizeEnv = _getEnv(otelBlrpMaxExportBatchSize) != null; + final hasMaxExportBatchSizeEnv = + _getEnv(otelBlrpMaxExportBatchSize) != null; final parsedMaxQueueSize = _getPositiveIntEnv(otelBlrpMaxQueueSize, minInclusive: 1); diff --git a/test/unit/environment/env_coverage_test.dart b/test/unit/environment/env_coverage_test.dart index ca307cc..3109c9a 100644 --- a/test/unit/environment/env_coverage_test.dart +++ b/test/unit/environment/env_coverage_test.dart @@ -201,7 +201,8 @@ void main() { expect(result.containsKey('maxExportBatchSize'), isFalse); }); - test('clamps maxExportBatchSize to maxQueueSize when both are set', () async { + test('clamps maxExportBatchSize to maxQueueSize when both are set', + () async { final output = await runWithEnv( 'test/unit/environment/helpers/check_blrp_config.dart', { @@ -214,7 +215,8 @@ void main() { expect(result['maxExportBatchSize'], equals(100)); }); - test('clamps maxExportBatchSize to default maxQueueSize when queue is unset', + test( + 'clamps maxExportBatchSize to default maxQueueSize when queue is unset', () async { final output = await runWithEnv( 'test/unit/environment/helpers/check_blrp_config.dart', @@ -224,7 +226,8 @@ void main() { expect(result['maxExportBatchSize'], equals(2048)); }); - test('sets maxExportBatchSize when maxQueueSize is smaller than default batch', + test( + 'sets maxExportBatchSize when maxQueueSize is smaller than default batch', () async { final output = await runWithEnv( 'test/unit/environment/helpers/check_blrp_config.dart', From d97aa7882d43e841f86827086c0fedd7d50bade2 Mon Sep 17 00:00:00 2001 From: harshitt13 Date: Mon, 20 Jul 2026 21:07:56 +0530 Subject: [PATCH 3/3] applied suggested changes Signed-off-by: harshitt13 --- CHANGELOG.md | 19 +++ lib/src/environment/otel_env.dart | 76 ++++------- .../export/batch_log_record_processor.dart | 98 ++++++++++++++ lib/src/logs/export/logs_config.dart | 7 +- test/unit/environment/env_coverage_test.dart | 122 +++++++++++++++--- .../helpers/check_blrp_from_env.dart | 28 ++++ 6 files changed, 275 insertions(+), 75 deletions(-) create mode 100644 test/unit/environment/helpers/check_blrp_from_env.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index b8b74f9..0a5c9a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 two hand-rolled copies of the same assembly; wire output is unchanged (same instrumentation-scope constant, same `OTel.resource(null)` fallback, resolved by the caller so the transformer stays a pure leaf). +- **`BatchLogRecordProcessorConfig.fromEnvironment()`** — new factory + constructor symmetric with `BatchSpanProcessorConfig.fromEnvironment()`. + Spec defaults and validation now live on the config class (Single + Responsibility), eliminating duplication across `OTelEnv`, + `LogsConfiguration`, and the constructor. + +### Changed +- **OTEL_BLRP_* env var validation now warns on invalid values** — previously, + invalid `OTEL_BLRP_SCHEDULE_DELAY` and `OTEL_BLRP_EXPORT_TIMEOUT` values + were silently ignored; they now emit `OTelLog.warn` diagnostics consistent + with BSP behavior. `OTEL_BLRP_SCHEDULE_DELAY=0` is now accepted as valid + (meaning "export as fast as possible"), and `OTEL_BLRP_EXPORT_TIMEOUT=0` + means "no limit", mirroring BSP semantics. +- **`OTelEnv._getPositiveIntEnv` now warns on unusable values** — non-numeric, + below-minimum, and above-maximum values all emit `OTelLog.warn`, giving + consistent diagnostics to every caller without per-site bookkeeping. +- **`OTelEnv.getBlrpConfig()` simplified to raw env reading** — domain-level + defaults, validation, and batch-to-queue clamping moved to + `BatchLogRecordProcessorConfig.fromEnvironment()`. ## [1.1.0-beta.9] - 2026-07-18 diff --git a/lib/src/environment/otel_env.dart b/lib/src/environment/otel_env.dart index 2ab4d13..a909418 100644 --- a/lib/src/environment/otel_env.dart +++ b/lib/src/environment/otel_env.dart @@ -492,83 +492,43 @@ class OTelEnv { /// Get Batch LogRecord Processor (BLRP) configuration from environment variables. /// - /// Returns a map containing the BLRP configuration read from environment variables. - /// Keys returned: + /// Returns a map containing the raw parsed BLRP values from environment + /// variables. Domain-level defaults, validation, and clamping belong in + /// [BatchLogRecordProcessorConfig.fromEnvironment]. + /// + /// Keys returned (only present when the env var is set and valid): /// - 'scheduleDelay': Duration for the schedule delay /// - 'exportTimeout': Duration for the export timeout /// - 'maxQueueSize': int for maximum queue size /// - 'maxExportBatchSize': int for maximum export batch size - /// - /// Validation rules: - /// - Numeric values must be positive integers - /// - `maxExportBatchSize` is clamped to `maxQueueSize` per OTel spec - /// - Defaults used for validation: queue=2048, batch=512 static Map getBlrpConfig() { final config = {}; - const defaultMaxQueueSize = 2048; - const defaultMaxExportBatchSize = 512; - // Get schedule delay + // Get schedule delay — 0 is valid ("export as fast as possible") final scheduleDelayMs = - _getPositiveIntEnv(otelBlrpScheduleDelay, minInclusive: 1); + _getPositiveIntEnv(otelBlrpScheduleDelay, minInclusive: 0); if (scheduleDelayMs != null) { config['scheduleDelay'] = Duration(milliseconds: scheduleDelayMs); } - // Get export timeout + // Get export timeout — 0 is valid ("no limit") final exportTimeoutMs = - _getPositiveIntEnv(otelBlrpExportTimeout, minInclusive: 1); + _getPositiveIntEnv(otelBlrpExportTimeout, minInclusive: 0); if (exportTimeoutMs != null) { config['exportTimeout'] = Duration(milliseconds: exportTimeoutMs); } - // Get queue and batch sizes. - final hasMaxQueueSizeEnv = _getEnv(otelBlrpMaxQueueSize) != null; - final hasMaxExportBatchSizeEnv = - _getEnv(otelBlrpMaxExportBatchSize) != null; - + // Get queue and batch sizes — must be positive (>0) final parsedMaxQueueSize = _getPositiveIntEnv(otelBlrpMaxQueueSize, minInclusive: 1); - var effectiveMaxQueueSize = parsedMaxQueueSize ?? defaultMaxQueueSize; - if (parsedMaxQueueSize != null) { config['maxQueueSize'] = parsedMaxQueueSize; - } else if (hasMaxQueueSizeEnv && OTelLog.isWarn()) { - OTelLog.warn( - 'OTelEnv: Ignoring invalid $otelBlrpMaxQueueSize. Value must be a positive integer.', - ); } final parsedMaxExportBatchSize = _getPositiveIntEnv(otelBlrpMaxExportBatchSize, minInclusive: 1); - var effectiveMaxExportBatchSize = - parsedMaxExportBatchSize ?? defaultMaxExportBatchSize; - if (parsedMaxExportBatchSize != null) { config['maxExportBatchSize'] = parsedMaxExportBatchSize; - } else if (hasMaxExportBatchSizeEnv && OTelLog.isWarn()) { - OTelLog.warn( - 'OTelEnv: Ignoring invalid $otelBlrpMaxExportBatchSize. Value must be a positive integer.', - ); - } - - // Per OTel spec, max export batch size must not exceed max queue size. - if (effectiveMaxExportBatchSize > effectiveMaxQueueSize) { - if (OTelLog.isWarn()) { - OTelLog.warn( - 'OTelEnv: $otelBlrpMaxExportBatchSize ($effectiveMaxExportBatchSize) exceeds ' - '$otelBlrpMaxQueueSize ($effectiveMaxQueueSize). Clamping batch size to queue size.', - ); - } - - effectiveMaxExportBatchSize = effectiveMaxQueueSize; - config['maxExportBatchSize'] = effectiveMaxExportBatchSize; - - // Keep returned queue value internally consistent if batch forced us to clamp - // based on an explicitly provided queue value. - if (parsedMaxQueueSize != null) { - effectiveMaxQueueSize = parsedMaxQueueSize; - } } return config; @@ -673,9 +633,11 @@ class OTelEnv { return null; } - /// Get a positive integer environment variable value. + /// Get a non-negative integer environment variable value. /// /// Returns null when not set, non-numeric, or outside the accepted range. + /// Warns via [OTelLog.warn] when the raw value is present but unusable + /// (non-numeric, below [minInclusive], or above [maxInclusive]). static int? _getPositiveIntEnv( String name, { required int minInclusive, @@ -688,14 +650,26 @@ class OTelEnv { final value = int.tryParse(rawValue); if (value == null) { + if (OTelLog.isWarn()) { + OTelLog.warn('OTelEnv: Illegal non-numeric value for $name: ' + '"$rawValue", ignoring.'); + } return null; } if (value < minInclusive) { + if (OTelLog.isWarn()) { + OTelLog.warn('OTelEnv: Illegal value for $name: $value is below ' + 'minimum $minInclusive, ignoring.'); + } return null; } if (maxInclusive != null && value > maxInclusive) { + if (OTelLog.isWarn()) { + OTelLog.warn('OTelEnv: Illegal value for $name: $value exceeds ' + 'maximum $maxInclusive, ignoring.'); + } return null; } diff --git a/lib/src/logs/export/batch_log_record_processor.dart b/lib/src/logs/export/batch_log_record_processor.dart index 44e5dc4..5fae4db 100644 --- a/lib/src/logs/export/batch_log_record_processor.dart +++ b/lib/src/logs/export/batch_log_record_processor.dart @@ -7,6 +7,7 @@ import 'dart:collection'; import 'package:dartastic_opentelemetry_api/dartastic_opentelemetry_api.dart'; import 'package:synchronized/synchronized.dart'; +import '../../environment/otel_env.dart'; import '../log_record_processor.dart'; import '../readable_log_record.dart'; import 'log_record_exporter.dart'; @@ -16,6 +17,12 @@ import 'log_record_exporter.dart'; /// This class configures how the batch log record processor behaves, including /// queue size limits, export scheduling, and batch size parameters. class BatchLogRecordProcessorConfig { + /// Stand-in for "no limit": `OTEL_BLRP_EXPORT_TIMEOUT=0` means no timeout + /// per spec, represented as the max milliseconds in a 32-bit integer + /// (~24.8 days). Web-safe and within Dart timer limits for + /// `Future.timeout()`. + static const Duration noLimit = Duration(milliseconds: 0x7FFFFFFF); + /// The maximum queue size for log records. After this is reached, /// log records will be dropped. final int maxQueueSize; @@ -41,6 +48,97 @@ class BatchLogRecordProcessorConfig { this.maxExportBatchSize = 512, this.exportTimeout = const Duration(seconds: 30), }); + + /// Creates a configuration by reading `OTEL_BLRP_*` environment variables + /// via [OTelEnv]. Falls back to standard OTel defaults if variables are + /// missing or invalid. + /// + /// | Environment Variable | Type | Default | Notes | + /// |-----------------------------------|----------|----------|----------------------------------------------------------| + /// | `OTEL_BLRP_SCHEDULE_DELAY` | Duration | `1000` | Delay between exports (ms). 0 is valid (export ASAP). | + /// | `OTEL_BLRP_EXPORT_TIMEOUT` | Timeout | `30000` | Export timeout (ms). 0 means no limit. | + /// | `OTEL_BLRP_MAX_QUEUE_SIZE` | Integer | `2048` | Maximum log record queue size. | + /// | `OTEL_BLRP_MAX_EXPORT_BATCH_SIZE` | Integer | `512` | Maximum batch size. Must be ≤ `MAX_QUEUE_SIZE`. | + /// + /// Invalid or out-of-range values emit an [OTelLog.warn] diagnostic and + /// fall back to the spec default. + factory BatchLogRecordProcessorConfig.fromEnvironment() { + final env = OTelEnv.getBlrpConfig(); + + var queueSize = (env['maxQueueSize'] as int?) ?? 2048; + var batchSize = (env['maxExportBatchSize'] as int?) ?? 512; + + // --- scheduleDelay --- + // Spec type: Duration. Zero is valid ("export as fast as possible"). + // Negative values MUST warn and fall back to default. + Duration scheduleDelay; + if (env['scheduleDelay'] is Duration) { + final delay = env['scheduleDelay'] as Duration; + if (delay.inMilliseconds >= 0) { + scheduleDelay = delay; + } else { + if (OTelLog.isWarn()) { + OTelLog.warn('BatchLogRecordProcessorConfig: Negative ' + 'OTEL_BLRP_SCHEDULE_DELAY (${delay.inMilliseconds} ms) is ' + 'invalid per spec, using default 1000 ms.'); + } + scheduleDelay = const Duration(milliseconds: 1000); + } + } else { + scheduleDelay = const Duration(milliseconds: 1000); + } + + // --- exportTimeout --- + // Spec type: Timeout. Zero means "no limit" — substitute a very large + // duration. Negative values MUST warn and fall back to default. + Duration exportTimeout; + if (env['exportTimeout'] is Duration) { + final timeout = env['exportTimeout'] as Duration; + if (timeout.inMilliseconds == 0) { + exportTimeout = noLimit; + } else if (timeout.inMilliseconds > 0) { + exportTimeout = timeout; + } else { + if (OTelLog.isWarn()) { + OTelLog.warn('BatchLogRecordProcessorConfig: Negative ' + 'OTEL_BLRP_EXPORT_TIMEOUT (${timeout.inMilliseconds} ms) is ' + 'invalid per spec, using default 30000 ms.'); + } + exportTimeout = const Duration(milliseconds: 30000); + } + } else { + exportTimeout = const Duration(milliseconds: 30000); + } + + // --- Validation Logic --- + if (queueSize <= 0) { + if (OTelLog.isWarn()) { + OTelLog.warn('BatchLogRecordProcessorConfig: Non-positive ' + 'OTEL_BLRP_MAX_QUEUE_SIZE ($queueSize) is invalid per spec, ' + 'using default 2048.'); + } + queueSize = 2048; + } + if (batchSize <= 0) { + if (OTelLog.isWarn()) { + OTelLog.warn('BatchLogRecordProcessorConfig: Non-positive ' + 'OTEL_BLRP_MAX_EXPORT_BATCH_SIZE ($batchSize) is invalid per ' + 'spec, using default 512.'); + } + batchSize = 512; + } + // Spec rule: maxExportBatchSize must be less than or equal to maxQueueSize + if (batchSize > queueSize) { + batchSize = queueSize; + } + + return BatchLogRecordProcessorConfig( + maxQueueSize: queueSize, + maxExportBatchSize: batchSize, + scheduleDelay: scheduleDelay, + exportTimeout: exportTimeout, + ); + } } /// A LogRecordProcessor that batches log records before export. diff --git a/lib/src/logs/export/logs_config.dart b/lib/src/logs/export/logs_config.dart index f2897d0..9acda5c 100644 --- a/lib/src/logs/export/logs_config.dart +++ b/lib/src/logs/export/logs_config.dart @@ -189,15 +189,14 @@ class LogsConfiguration { /// Creates a log record processor with BLRP configuration from environment. static LogRecordProcessor _createProcessor(LogRecordExporter exporter) { - final blrpConfig = OTelEnv.getBlrpConfig(); - final processorConfig = buildBatchLogRecordProcessorConfig(blrpConfig); - + final processorConfig = BatchLogRecordProcessorConfig.fromEnvironment(); return BatchLogRecordProcessor(exporter, processorConfig); } - /// Builds [BatchLogRecordProcessorConfig] from BLRP environment config. + /// Builds [BatchLogRecordProcessorConfig] from a BLRP environment config map. /// /// Exposed for testing to validate normalization and spec-rule handling. + /// Prefer [BatchLogRecordProcessorConfig.fromEnvironment] for production use. static BatchLogRecordProcessorConfig buildBatchLogRecordProcessorConfig( Map blrpConfig, ) { diff --git a/test/unit/environment/env_coverage_test.dart b/test/unit/environment/env_coverage_test.dart index 44a0d6b..f47fda0 100644 --- a/test/unit/environment/env_coverage_test.dart +++ b/test/unit/environment/env_coverage_test.dart @@ -340,16 +340,25 @@ void main() { expect(result.containsKey('maxExportBatchSize'), isFalse); }); - test('ignores non-positive scheduleDelay', () async { + test('accepts scheduleDelay=0 (export ASAP)', () async { final output = await runWithEnv( 'test/unit/environment/helpers/check_blrp_config.dart', {'OTEL_BLRP_SCHEDULE_DELAY': '0'}, ); final result = jsonDecode(output.trim()) as Map; - expect(result.containsKey('scheduleDelay_ms'), isFalse); + expect(result['scheduleDelay_ms'], equals(0)); + }); + + test('accepts exportTimeout=0 (no limit)', () async { + final output = await runWithEnv( + 'test/unit/environment/helpers/check_blrp_config.dart', + {'OTEL_BLRP_EXPORT_TIMEOUT': '0'}, + ); + final result = jsonDecode(output.trim()) as Map; + expect(result['exportTimeout_ms'], equals(0)); }); - test('ignores non-positive exportTimeout', () async { + test('ignores negative exportTimeout', () async { final output = await runWithEnv( 'test/unit/environment/helpers/check_blrp_config.dart', {'OTEL_BLRP_EXPORT_TIMEOUT': '-1'}, @@ -376,8 +385,8 @@ void main() { expect(result.containsKey('maxExportBatchSize'), isFalse); }); - test('clamps maxExportBatchSize to maxQueueSize when both are set', - () async { + test('returns both queue and batch without clamping', () async { + // getBlrpConfig no longer clamps; that's fromEnvironment()'s job final output = await runWithEnv( 'test/unit/environment/helpers/check_blrp_config.dart', { @@ -387,39 +396,112 @@ void main() { ); final result = jsonDecode(output.trim()) as Map; expect(result['maxQueueSize'], equals(100)); - expect(result['maxExportBatchSize'], equals(100)); + expect(result['maxExportBatchSize'], equals(200)); }); - test( - 'clamps maxExportBatchSize to default maxQueueSize when queue is unset', - () async { + test('returns empty map when no BLRP env vars set', () async { final output = await runWithEnv( 'test/unit/environment/helpers/check_blrp_config.dart', - {'OTEL_BLRP_MAX_EXPORT_BATCH_SIZE': '5000'}, + {}, ); final result = jsonDecode(output.trim()) as Map; - expect(result['maxExportBatchSize'], equals(2048)); + expect(result, isEmpty); }); + }); - test( - 'sets maxExportBatchSize when maxQueueSize is smaller than default batch', - () async { + // ========================================================================= + // BatchLogRecordProcessorConfig - subprocess tests for fromEnvironment + // ========================================================================= + group('BatchLogRecordProcessorConfig.fromEnvironment() (subprocess)', () { + test('uses defaults when no vars set', () async { final output = await runWithEnv( - 'test/unit/environment/helpers/check_blrp_config.dart', - {'OTEL_BLRP_MAX_QUEUE_SIZE': '100'}, + 'test/unit/environment/helpers/check_blrp_from_env.dart', + {}, + ); + final result = jsonDecode(output.trim()) as Map; + expect(result['scheduleDelay_ms'], equals(1000)); + expect(result['exportTimeout_ms'], equals(30000)); + expect(result['maxQueueSize'], equals(2048)); + expect(result['maxExportBatchSize'], equals(512)); + expect(result['logs'], isEmpty); + }); + + test('honors scheduleDelay=0 (export ASAP)', () async { + final output = await runWithEnv( + 'test/unit/environment/helpers/check_blrp_from_env.dart', + {'OTEL_BLRP_SCHEDULE_DELAY': '0'}, + ); + final result = jsonDecode(output.trim()) as Map; + expect(result['scheduleDelay_ms'], equals(0)); + expect(result['logs'], isEmpty); + }); + + test('warns and defaults for scheduleDelay=-1', () async { + final output = await runWithEnv( + 'test/unit/environment/helpers/check_blrp_from_env.dart', + {'OTEL_BLRP_SCHEDULE_DELAY': '-1'}, + ); + final result = jsonDecode(output.trim()) as Map; + expect(result['scheduleDelay_ms'], equals(1000)); + final logs = result['logs'] as List; + expect(logs, isNotEmpty); + }); + + test('honors exportTimeout=0 (no limit)', () async { + final output = await runWithEnv( + 'test/unit/environment/helpers/check_blrp_from_env.dart', + {'OTEL_BLRP_EXPORT_TIMEOUT': '0'}, + ); + final result = jsonDecode(output.trim()) as Map; + expect(result['exportTimeout_ms'], equals(0x7FFFFFFF)); + expect(result['logs'], isEmpty); + }); + + test('warns and defaults for exportTimeout=-1', () async { + final output = await runWithEnv( + 'test/unit/environment/helpers/check_blrp_from_env.dart', + {'OTEL_BLRP_EXPORT_TIMEOUT': '-1'}, + ); + final result = jsonDecode(output.trim()) as Map; + expect(result['exportTimeout_ms'], equals(30000)); + final logs = result['logs'] as List; + expect(logs, isNotEmpty); + }); + + test('clamps maxExportBatchSize to maxQueueSize', () async { + final output = await runWithEnv( + 'test/unit/environment/helpers/check_blrp_from_env.dart', + { + 'OTEL_BLRP_MAX_QUEUE_SIZE': '100', + 'OTEL_BLRP_MAX_EXPORT_BATCH_SIZE': '200', + }, ); final result = jsonDecode(output.trim()) as Map; expect(result['maxQueueSize'], equals(100)); expect(result['maxExportBatchSize'], equals(100)); + expect(result['logs'], isEmpty); }); - test('returns empty map when no BLRP env vars set', () async { + test('clamps maxExportBatchSize to default maxQueueSize when queue unset', + () async { final output = await runWithEnv( - 'test/unit/environment/helpers/check_blrp_config.dart', - {}, + 'test/unit/environment/helpers/check_blrp_from_env.dart', + {'OTEL_BLRP_MAX_EXPORT_BATCH_SIZE': '5000'}, ); final result = jsonDecode(output.trim()) as Map; - expect(result, isEmpty); + expect(result['maxQueueSize'], equals(2048)); + expect(result['maxExportBatchSize'], equals(2048)); + }); + + test('warns and defaults for non-positive queue size', () async { + final output = await runWithEnv( + 'test/unit/environment/helpers/check_blrp_from_env.dart', + {'OTEL_BLRP_MAX_QUEUE_SIZE': '-5'}, + ); + final result = jsonDecode(output.trim()) as Map; + expect(result['maxQueueSize'], equals(2048)); + final logs = result['logs'] as List; + expect(logs, isNotEmpty); }); }); diff --git a/test/unit/environment/helpers/check_blrp_from_env.dart b/test/unit/environment/helpers/check_blrp_from_env.dart new file mode 100644 index 0000000..426afed --- /dev/null +++ b/test/unit/environment/helpers/check_blrp_from_env.dart @@ -0,0 +1,28 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +// Helper script: constructs BatchLogRecordProcessorConfig.fromEnvironment() in a +// subprocess and prints the four resolved fields as JSON. +// Run via subprocess with OTEL_BLRP_* env vars set. + +import 'dart:convert'; + +import 'package:dartastic_opentelemetry/dartastic_opentelemetry.dart'; + +void main() { + final logs = []; + OTelLog.logFunction = logs.add; + OTelLog.enableWarnLogging(); + + final config = BatchLogRecordProcessorConfig.fromEnvironment(); + + final jsonConfig = { + 'scheduleDelay_ms': config.scheduleDelay.inMilliseconds, + 'exportTimeout_ms': config.exportTimeout.inMilliseconds, + 'maxQueueSize': config.maxQueueSize, + 'maxExportBatchSize': config.maxExportBatchSize, + 'logs': logs, + }; + + print(jsonEncode(jsonConfig)); +}