diff --git a/CHANGELOG.md b/CHANGELOG.md index 40746eb..f178444 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -348,6 +348,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 invalid keys/values are now ignored with a warning (previously `ArgumentError`), and both operations work without an installed SDK (previously `StateError`). +- **Provider accessors use safe defaults instead of throwing.** Per the + trace API spec, an invalid name must return "a working Tracer + implementation... as a fallback rather than returning null or throwing an + exception": `OTelAPI.tracerProvider('')` / `meterProvider('')` / + `loggerProvider('')` now warn and return the global default (previously + `ArgumentError`), and `getTracer` / `getMeter` / `getLogger` after + provider shutdown warn and return a no-op instance (previously + `StateError`). ## [1.0.0-beta.9] - 2026-07-11 diff --git a/lib/src/api/logs/logger_provider.dart b/lib/src/api/logs/logger_provider.dart index ce43976..a4dcf3f 100644 --- a/lib/src/api/logs/logger_provider.dart +++ b/lib/src/api/logs/logger_provider.dart @@ -73,7 +73,8 @@ class APILoggerProvider { Attributes? attributes, }) { if (_isShutdown) { - throw StateError('LogProvider has been shut down'); + OTelLog.warn( + 'getLogger called after shutdown; returning a no-op logger.'); } // Validate the logger name; if invalid (empty), log a warning and use empty string. diff --git a/lib/src/api/metrics/meter_provider.dart b/lib/src/api/metrics/meter_provider.dart index cd9f406..8e52125 100644 --- a/lib/src/api/metrics/meter_provider.dart +++ b/lib/src/api/metrics/meter_provider.dart @@ -78,7 +78,7 @@ class APIMeterProvider { String? schemaUrl, Attributes? attributes}) { if (_isShutdown) { - throw StateError('MeterProvider has been shut down'); + OTelLog.warn('getMeter called after shutdown; returning a no-op meter.'); } // Validate the meter name; if invalid (empty), log a warning and use empty string. diff --git a/lib/src/api/otel_api.dart b/lib/src/api/otel_api.dart index b97286d..3521699 100644 --- a/lib/src/api/otel_api.dart +++ b/lib/src/api/otel_api.dart @@ -229,10 +229,9 @@ class OTelAPI { static APITracerProvider tracerProvider([String? name]) { _getAndCacheOtelFactory(); if (name != null && name.isEmpty) { - throw ArgumentError( - 'Name must not be empty. To retrieve the global default tracer provider, omit the name parameter.'); + OTelLog.warn('Empty tracer provider name; returning the global default.'); } - if (name == null) { + if (name == null || name.isEmpty) { return _otelFactory!.globalDefaultTracerProvider(); } else { var tp = _otelFactory!.getNamedTracerProvider(name); @@ -248,10 +247,9 @@ class OTelAPI { static APIMeterProvider meterProvider([String? name]) { _getAndCacheOtelFactory(); if (name != null && name.isEmpty) { - throw ArgumentError( - 'Name must not be empty. To retrieve the global default meter provider, omit the name parameter.'); + OTelLog.warn('Empty meter provider name; returning the global default.'); } - if (name == null) { + if (name == null || name.isEmpty) { return _otelFactory!.globalDefaultMeterProvider(); } else { var mp = _otelFactory!.getNamedMeterProvider(name); @@ -267,10 +265,9 @@ class OTelAPI { static APILoggerProvider loggerProvider([String? name]) { _getAndCacheOtelFactory(); if (name != null && name.isEmpty) { - throw ArgumentError( - 'Name must not be empty. To retrieve the global default tracer provider, omit the name parameter.'); + OTelLog.warn('Empty logger provider name; returning the global default.'); } - if (name == null) { + if (name == null || name.isEmpty) { return _otelFactory!.globalDefaultLogProvider(); } else { var lp = _otelFactory!.getNamedLogProvider(name); diff --git a/lib/src/api/trace/tracer_provider.dart b/lib/src/api/trace/tracer_provider.dart index 6f9f775..6bc0d3a 100644 --- a/lib/src/api/trace/tracer_provider.dart +++ b/lib/src/api/trace/tracer_provider.dart @@ -83,7 +83,8 @@ class APITracerProvider { APITracer getTracer(String name, {String? version, String? schemaUrl, Attributes? attributes}) { if (_isShutdown) { - throw StateError('TracerProvider has been shut down'); + OTelLog.warn( + 'getTracer called after shutdown; returning a no-op tracer.'); } // Validate the tracer name; if invalid (empty), log a warning and use empty string. diff --git a/test/unit/api/logs/log_provider_test.dart b/test/unit/api/logs/log_provider_test.dart index 1fd63fd..517b70c 100644 --- a/test/unit/api/logs/log_provider_test.dart +++ b/test/unit/api/logs/log_provider_test.dart @@ -203,7 +203,7 @@ void main() { expect(provider.isShutdown, isTrue); }); - test('getLogger throws StateError after shutdown', () async { + test('getLogger returns a working logger after shutdown', () async { final provider = OTelAPI.loggerProvider(); // Create a logger before shutdown (should work) @@ -213,11 +213,8 @@ void main() { // Shutdown the provider await provider.shutdown(); - // Attempting to create a logger after shutdown should throw - expect( - () => provider.getLogger('after-shutdown'), - throwsA(isA()), - ); + // Creating a logger after shutdown works and is a fresh no-op + expect(provider.getLogger('after-shutdown'), isNotNull); }); test('getLogger uses defaults when no optional parameters provided', () { diff --git a/test/unit/api/metrics/meter_provider_test.dart b/test/unit/api/metrics/meter_provider_test.dart index 197a7e8..e8552d3 100644 --- a/test/unit/api/metrics/meter_provider_test.dart +++ b/test/unit/api/metrics/meter_provider_test.dart @@ -101,9 +101,8 @@ void main() { expect(meterProvider.isShutdown, isTrue); expect(meterProvider.enabled, isFalse); - // Should throw when trying to get a meter after shutdown - expect( - () => meterProvider.getMeter(name: 'test-meter'), throwsStateError); + // Getting a meter after shutdown works and is a fresh no-op + expect(meterProvider.getMeter(name: 'test-meter'), isA()); }); test('forceFlush returns true', () async { diff --git a/test/unit/api/otel_api_safe_defaults_test.dart b/test/unit/api/otel_api_safe_defaults_test.dart new file mode 100644 index 0000000..08fa2f6 --- /dev/null +++ b/test/unit/api/otel_api_safe_defaults_test.dart @@ -0,0 +1,63 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +import 'package:dartastic_opentelemetry_api/dartastic_opentelemetry_api.dart'; +import 'package:test/test.dart'; + +// Spec-compliance tests for provider accessors: +// +// - trace/api.md, Get a Tracer: "In case an invalid name (null or empty +// string) is specified, a working Tracer implementation MUST be returned +// as a fallback rather than returning null or throwing an exception." +// The same fallback principle applies to the provider accessors and to +// getMeter/getLogger. +// - error-handling.md: "API methods MUST NOT throw unhandled exceptions +// when used incorrectly by end users. The API and SDK SHOULD provide +// safe defaults for missing or invalid arguments." +void main() { + setUp(() { + OTelAPI.reset(); + OTelAPI.initialize( + endpoint: 'http://localhost:4318', + serviceName: 'test-service', + serviceVersion: '1.0.0', + ); + }); + + group('empty provider names fall back to the global default', () { + test('tracerProvider("") returns the global default', () { + expect(OTelAPI.tracerProvider(''), same(OTelAPI.tracerProvider())); + }); + + test('meterProvider("") returns the global default', () { + expect(OTelAPI.meterProvider(''), same(OTelAPI.meterProvider())); + }); + + test('loggerProvider("") returns the global default', () { + expect(OTelAPI.loggerProvider(''), same(OTelAPI.loggerProvider())); + }); + }); + + group('provider getters work after shutdown', () { + test('getTracer returns a working tracer after shutdown', () async { + final provider = OTelAPI.tracerProvider(); + await provider.shutdown(); + final tracer = provider.getTracer('post-shutdown'); + expect(tracer, isA()); + }); + + test('getMeter returns a working meter after shutdown', () async { + final provider = OTelAPI.meterProvider(); + await provider.shutdown(); + final meter = provider.getMeter(name: 'post-shutdown'); + expect(meter, isA()); + }); + + test('getLogger returns a working logger after shutdown', () async { + final provider = OTelAPI.loggerProvider(); + await provider.shutdown(); + final logger = provider.getLogger('post-shutdown'); + expect(logger, isA()); + }); + }); +} diff --git a/test/unit/api/otel_api_test.dart b/test/unit/api/otel_api_test.dart index 0ab2cba..aa6df5d 100644 --- a/test/unit/api/otel_api_test.dart +++ b/test/unit/api/otel_api_test.dart @@ -77,12 +77,12 @@ void main() { expect(tracer.name, equals('test-tracer')); }); - test('tracerProvider throws ArgumentError for empty name', () { - expect(() => OTelAPI.tracerProvider(''), throwsArgumentError); + test('tracerProvider falls back to the global default for empty name', () { + expect(OTelAPI.tracerProvider(''), same(OTelAPI.tracerProvider())); }); - test('meterProvider throws ArgumentError for empty name', () { - expect(() => OTelAPI.meterProvider(''), throwsArgumentError); + test('meterProvider falls back to the global default for empty name', () { + expect(OTelAPI.meterProvider(''), same(OTelAPI.meterProvider())); }); test('tracerProviders returns empty list initially', () { diff --git a/test/unit/api/trace/tracer_provider_test.dart b/test/unit/api/trace/tracer_provider_test.dart index 891790c..8253314 100644 --- a/test/unit/api/trace/tracer_provider_test.dart +++ b/test/unit/api/trace/tracer_provider_test.dart @@ -165,14 +165,11 @@ void main() { expect(provider.isShutdown, isTrue); }); - test('getTracer throws StateError after shutdown', () { + test('getTracer returns a working tracer after shutdown', () { final provider = OTelAPI.tracerProvider(); provider.isShutdown = true; - expect( - () => provider.getTracer('test-lib'), - throwsA(isA()), - ); + expect(provider.getTracer('test-lib'), isNotNull); }); test('shutdown returns true and marks provider as shutdown', () async { @@ -205,11 +202,8 @@ void main() { // Shutdown the provider await provider.shutdown(); - // After shutdown, getting tracer should throw - expect( - () => provider.getTracer('test-lib', version: '1.0.0'), - throwsA(isA()), - ); + // After shutdown, getting a tracer works and is a fresh no-op + expect(provider.getTracer('test-lib', version: '1.0.0'), isNotNull); }); }); }