Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
3 changes: 2 additions & 1 deletion lib/src/api/logs/logger_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion lib/src/api/metrics/meter_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
15 changes: 6 additions & 9 deletions lib/src/api/otel_api.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand Down
3 changes: 2 additions & 1 deletion lib/src/api/trace/tracer_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
9 changes: 3 additions & 6 deletions test/unit/api/logs/log_provider_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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<StateError>()),
);
// 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', () {
Expand Down
5 changes: 2 additions & 3 deletions test/unit/api/metrics/meter_provider_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<APIMeter>());
});

test('forceFlush returns true', () async {
Expand Down
63 changes: 63 additions & 0 deletions test/unit/api/otel_api_safe_defaults_test.dart
Original file line number Diff line number Diff line change
@@ -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<APITracer>());
});

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<APIMeter>());
});

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<APILogger>());
});
});
}
8 changes: 4 additions & 4 deletions test/unit/api/otel_api_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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', () {
Expand Down
14 changes: 4 additions & 10 deletions test/unit/api/trace/tracer_provider_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<StateError>()),
);
expect(provider.getTracer('test-lib'), isNotNull);
});

test('shutdown returns true and marks provider as shutdown', () async {
Expand Down Expand Up @@ -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<StateError>()),
);
// After shutdown, getting a tracer works and is a fresh no-op
expect(provider.getTracer('test-lib', version: '1.0.0'), isNotNull);
});
});
}
Loading