From 77ecc8e5e9d72f0d14d4f1de71265b476721c66b Mon Sep 17 00:00:00 2001 From: Michael Bushe Date: Sat, 11 Jul 2026 08:30:52 +0200 Subject: [PATCH 1/2] test: pin provider safe-default contracts (6 currently failing) trace/api.md Get a Tracer: an invalid name MUST return a working fallback "rather than returning null or throwing an exception"; error-handling.md: API methods MUST NOT throw on misuse. Current OTelAPI.tracerProvider/meterProvider/loggerProvider throw ArgumentError on empty names, and getTracer/getMeter/getLogger throw StateError after provider shutdown. Co-Authored-By: Claude Fable 5 --- .../unit/api/otel_api_safe_defaults_test.dart | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 test/unit/api/otel_api_safe_defaults_test.dart 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..dd7d8e4 --- /dev/null +++ b/test/unit/api/otel_api_safe_defaults_test.dart @@ -0,0 +1,62 @@ +// Licensed under the Apache License, Version 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()); + }); + }); +} From cc6a377ff2be7d867bcded285c20ebd61e40e91e Mon Sep 17 00:00:00 2001 From: Michael Bushe Date: Sat, 11 Jul 2026 08:36:00 +0200 Subject: [PATCH 2/2] fix: provider accessors warn and fall back instead of throwing Turns the red spec tests green: - OTelAPI.tracerProvider('')/meterProvider('')/loggerProvider('') warn and return the global default instead of ArgumentError - getTracer/getMeter/getLogger after provider shutdown warn and return a no-op instance instead of StateError Co-Authored-By: Claude Fable 5 --- CHANGELOG.md | 8 ++++++++ lib/src/api/logs/logger_provider.dart | 3 ++- lib/src/api/metrics/meter_provider.dart | 2 +- lib/src/api/otel_api.dart | 15 ++++++--------- lib/src/api/trace/tracer_provider.dart | 3 ++- test/unit/api/logs/log_provider_test.dart | 9 +++------ test/unit/api/metrics/meter_provider_test.dart | 5 ++--- test/unit/api/otel_api_safe_defaults_test.dart | 3 ++- test/unit/api/otel_api_test.dart | 8 ++++---- test/unit/api/trace/tracer_provider_test.dart | 14 ++++---------- 10 files changed, 34 insertions(+), 36 deletions(-) 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 index dd7d8e4..08fa2f6 100644 --- a/test/unit/api/otel_api_safe_defaults_test.dart +++ b/test/unit/api/otel_api_safe_defaults_test.dart @@ -1,4 +1,5 @@ -// Licensed under the Apache License, Version 2.0 +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 import 'package:dartastic_opentelemetry_api/dartastic_opentelemetry_api.dart'; import 'package:test/test.dart'; 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); }); }); }