diff --git a/lib/dartastic_opentelemetry.dart b/lib/dartastic_opentelemetry.dart index b2a9473..fc4ce67 100644 --- a/lib/dartastic_opentelemetry.dart +++ b/lib/dartastic_opentelemetry.dart @@ -74,6 +74,8 @@ export 'src/environment/env_constants.dart'; export 'src/environment/environment_service.dart'; export 'src/environment/otel_env.dart'; export 'src/factory/otel_sdk_factory.dart'; +export 'src/log/logger.dart'; +export 'src/log/logger_provider.dart'; export 'src/metrics/data/exemplar.dart'; export 'src/metrics/data/metric.dart'; export 'src/metrics/data/metric_data.dart'; diff --git a/lib/src/factory/otel_sdk_factory.dart b/lib/src/factory/otel_sdk_factory.dart index 482621e..37e5d6b 100644 --- a/lib/src/factory/otel_sdk_factory.dart +++ b/lib/src/factory/otel_sdk_factory.dart @@ -2,8 +2,9 @@ // Copyright 2025, Michael Bushe, All rights reserved. import 'package:dartastic_opentelemetry/src/trace/tracer_provider.dart'; import 'package:dartastic_opentelemetry_api/dartastic_opentelemetry_api.dart'; -import '../metrics/meter_provider.dart'; +import '../log/logger_provider.dart'; +import '../metrics/meter_provider.dart'; import '../resource/resource.dart'; /// Factory function that creates an OTelSDKFactory with the specified configuration. @@ -121,4 +122,21 @@ class OTelSDKFactory extends OTelAPIFactory { serviceName: serviceName), resource: resource); } + + @override + APILoggerProvider loggerProvider({ + required String endpoint, + String serviceName = "@dart/opentelemetry_api", + String? serviceVersion, + Resource? resource, + }) { + return SDKLoggerProviderCreate.create( + delegate: super.loggerProvider( + endpoint: endpoint, + serviceVersion: serviceVersion, + serviceName: serviceName, + ), + resource: resource, + ); + } } diff --git a/lib/src/log/logger.dart b/lib/src/log/logger.dart new file mode 100644 index 0000000..f302aa1 --- /dev/null +++ b/lib/src/log/logger.dart @@ -0,0 +1,68 @@ +import 'package:dartastic_opentelemetry_api/dartastic_opentelemetry_api.dart'; + +import '../resource/resource.dart'; +import 'logger_provider.dart'; + +part 'logger_create.dart'; + +/// SDK implementation of the APILogger interface. +/// +/// A Logger is responsible for creating and managing logs. +/// +/// This implementation delegates some functionality to the API Logger +/// implementation while adding SDK-specific. +/// +/// +/// More information: +/// https://opentelemetry.io/docs/specs/otel/logs/sdk/ +class Logger implements APILogger { + final LoggerProvider _provider; + final APILogger _delegate; + + bool _enabled = true; + + /// Private constructor for creating Logger instances. + Logger._({ + required LoggerProvider provider, + required APILogger delegate, + }) : _provider = provider, + _delegate = delegate; + + @override + String get name => _delegate.name; + + @override + String? get schemaUrl => _delegate.schemaUrl; + + @override + String? get version => _delegate.version; + + @override + Attributes? get attributes => _delegate.attributes; + + @override + bool get enabled => _enabled; + + set enabled(bool value) { + _enabled = value; + } + + /// Gets the provider that created this logger. + LoggerProvider get provider => _provider; + + /// Gets the resource associated with this logger's provider. + Resource? get resource => _provider.resource; + + @override + void emit({ + Attributes? attributes, + Context? context, + body, + DateTime? observedTimestamp, + Severity? severityNumber, + String? severityText, + DateTime? timeStamp, + }) { + // TODO: implement emit + } +} diff --git a/lib/src/log/logger_create.dart b/lib/src/log/logger_create.dart new file mode 100644 index 0000000..e416011 --- /dev/null +++ b/lib/src/log/logger_create.dart @@ -0,0 +1,16 @@ +part of 'logger.dart'; + +/// Factory for creating Logger instances. +/// +/// This factory class provides a static create method for constructing +/// properly configured Logger instances. It follows the factory pattern +/// to separate the construction logic from the Logger class itself. +class SDKLoggerCreate { + /// Creates a new Logger with the specified delegate and provider. + static Logger create({ + required APILogger delegate, + required LoggerProvider provider, + }) { + return Logger._(delegate: delegate, provider: provider); + } +} diff --git a/lib/src/log/logger_provider.dart b/lib/src/log/logger_provider.dart new file mode 100644 index 0000000..7ee899a --- /dev/null +++ b/lib/src/log/logger_provider.dart @@ -0,0 +1,188 @@ +import 'package:dartastic_opentelemetry_api/dartastic_opentelemetry_api.dart'; + +import '../otel.dart'; +import '../resource/resource.dart'; +import 'logger.dart'; + +part 'logger_provider_create.dart'; + +/// SDK implementation of the APILoggerProvider interface. +/// +/// The LoggerProvider is the entry point to the logger API. It is responsible +/// for creating and managing Loggers. +/// +/// This implementation delegates some functionality to the API LoggerProvider +/// implementation while adding SDK-specific behaviors. +/// +/// +/// More information: +/// https://opentelemetry.io/docs/specs/otel/logs/sdk/#loggerprovider +class LoggerProvider implements APILoggerProvider { + /// Registry of loggers managed by this provider. + final Map _loggers = {}; + + final APILoggerProvider _delegate; + + // TODO: LogsProcessor type + final List _logProcessors = []; + + /// The resource associated with this provider. + Resource? resource; + + LoggerProvider._({ + required APILoggerProvider delegate, + this.resource, + }) : _delegate = delegate { + if (OTelLog.isDebug()) { + OTelLog.debug('LoggerProvider: Created with resource: $resource'); + if (resource != null) { + OTelLog.debug('Resource attributes:'); + resource!.attributes.toList().forEach((attr) { + OTelLog.debug(' ${attr.key}: ${attr.value}'); + }); + } + } + } + + @override + bool get isShutdown => _delegate.isShutdown; + + @override + set isShutdown(bool value) { + _delegate.isShutdown = value; + } + + @override + String get endpoint => _delegate.endpoint; + + @override + set endpoint(String value) { + _delegate.endpoint = value; + } + + @override + String get serviceName => _delegate.serviceName; + + @override + set serviceName(String value) { + _delegate.serviceName = value; + } + + @override + String? get serviceVersion => _delegate.serviceVersion; + + @override + set serviceVersion(String? value) { + _delegate.serviceVersion = value; + } + + @override + bool get enabled => _delegate.enabled; + + @override + set enabled(bool value) { + _delegate.enabled = value; + } + + @override + APILogger getLogger(String name, + {String? version, String? schemaUrl, Attributes? attributes}) { + if (OTelLog.isDebug()) { + OTelLog.debug( + 'LoggerProvider: Getting logger with name: $name, version: $version, schemaUrl: $schemaUrl'); + } + if (isShutdown) { + throw StateError('LoggerProvider has been shut down'); + } + + // Ensure resource is set before creating logger + ensureResourceIsSet(); + + final key = '$name:${version ?? ''}'; + return _loggers.putIfAbsent( + key, + () => SDKLoggerCreate.create( + delegate: _delegate.getLogger( + name, + version: version, + schemaUrl: schemaUrl, + attributes: attributes, + ), + provider: this, + ), + ); + } + + @override + Future shutdown() async { + if (OTelLog.isDebug()) { + OTelLog.debug( + 'LoggerProvider: Shutting down with ${_logProcessors.length} processors'); + } + + if (!isShutdown) { + // Shutdown all log processors + for (final processor in _logProcessors) { + if (OTelLog.isDebug()) { + OTelLog.debug( + 'LoggerProvider: Shutting down processor ${processor.runtimeType}'); + } + try { + // TODO: processor shutdown here. + if (OTelLog.isDebug()) { + OTelLog.debug( + 'LoggerProvider: Successfully shut down processor ${processor.runtimeType}'); + } + } catch (e) { + if (OTelLog.isDebug()) { + OTelLog.debug( + 'LoggerProvider: Error shutting down processor ${processor.runtimeType}: $e'); + } + } + } + + // Clear cached loggers + _loggers.clear(); + if (OTelLog.isDebug()) { + OTelLog.debug('LoggerProvider: Cleared cached loggers'); + } + + try { + await _delegate.shutdown(); + if (OTelLog.isDebug()) { + OTelLog.debug('LoggerProvider: Delegate shutdown complete'); + } + } catch (e) { + if (OTelLog.isDebug()) { + OTelLog.debug('LoggerProvider: Error during delegate shutdown: $e'); + } + } + + isShutdown = true; + if (OTelLog.isDebug()) OTelLog.debug('LoggerProvider: Shutdown complete'); + } else { + if (OTelLog.isDebug()) OTelLog.debug('LoggerProvider: Already shut down'); + } + return isShutdown; + } + + /// Ensures the resource for this provider is properly set. + /// + /// If no resource has been set, the default resource will be used. + void ensureResourceIsSet() { + if (resource != null) return; + resource = OTel.defaultResource; + if (!OTelLog.isDebug()) return; + OTelLog.debug('LoggerProvider: Setting resource from default'); + + // By right, this should already set based on [OTel.defaultResource]. In case if default is null, + // ignore next operations. + if (resource != null) return; + OTelLog.debug('Resource attributes:'); + resource!.attributes.toList().forEach((attr) { + if (attr.key == 'tenant_id' || attr.key == 'service.name') { + OTelLog.debug(' ${attr.key}: ${attr.value}'); + } + }); + } +} diff --git a/lib/src/log/logger_provider_create.dart b/lib/src/log/logger_provider_create.dart new file mode 100644 index 0000000..d83d023 --- /dev/null +++ b/lib/src/log/logger_provider_create.dart @@ -0,0 +1,15 @@ +part of 'logger_provider.dart'; + +/// Factory for creating LoggerProvider instances. +/// +/// This factory class provides a static create method for constructing +/// properly configured LoggerProvider instances. It follows the factory +/// pattern to separate the construction logic from the LoggerProvider +/// class itself. +class SDKLoggerProviderCreate { + /// Creates a new LoggerProvider with the specified delegate and resource. + static LoggerProvider create( + {required APILoggerProvider delegate, Resource? resource}) { + return LoggerProvider._(delegate: delegate, resource: resource); + } +} diff --git a/lib/src/otel.dart b/lib/src/otel.dart index 448c363..dc8f989 100644 --- a/lib/src/otel.dart +++ b/lib/src/otel.dart @@ -498,6 +498,37 @@ class OTel { return meterProvider; } + /// Gets a LoggerProvider for creating Tracers. + /// + /// If name is null, this returns the global default LoggerProvider, which shares + /// the endpoint, serviceName, serviceVersion, sampler and resource set in initialize(). + /// If the name is not null, it returns a LoggerProvider for the name that was added + /// with addLoggerProvider. + /// + /// The endpoint, serviceName, serviceVersion, sampler and resource set flow down + /// to the [Logger]s created by the LoggerProvider + /// @param name Optional name of a specific LoggerProvider + /// @return The LoggerProvider instance + static LoggerProvider loggerProvider({String? name}) { + final loggerProvider = OTelAPI.loggerProvider(name) as LoggerProvider; + // Ensure the resource is properly set + if (loggerProvider.resource == null && defaultResource != null) { + loggerProvider.resource = defaultResource; + if (OTelLog.isDebug()) { + OTelLog.debug('OTel.loggerProvider: Setting resource from default'); + if (defaultResource != null) { + defaultResource!.attributes.toList().forEach((attr) { + if (attr.key == 'tenant_id' || attr.key == 'service.name') { + OTelLog.debug(' ${attr.key}: ${attr.value}'); + } + }); + } + } + } + + return loggerProvider; + } + /// Adds or replaces a named TracerProvider. /// /// This allows for creating multiple TracerProviders with different configurations, @@ -525,6 +556,24 @@ class OTel { return sdkTracerProvider; } + /// Adds or replaces a named LoggerProvider. + static LoggerProvider addLoggerProvider( + String name, { + String? endpoint, + String? serviceName, + String? serviceVersion, + Resource? resource, + }) { + final sdkLoggerProvider = OTelAPI.addLoggerProvider( + name, + endpoint: endpoint, + serviceName: serviceName, + serviceVersion: serviceVersion, + ) as LoggerProvider; + sdkLoggerProvider.resource = resource ?? defaultResource; + return sdkLoggerProvider; + } + /// @return the [TracerProvider]s, the global default and named ones. static List tracerProviders() { return OTelAPI.tracerProviders(); diff --git a/pubspec.yaml b/pubspec.yaml index 95d1453..1fcab22 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -8,7 +8,10 @@ environment: sdk: ^3.0.0 dependencies: - dartastic_opentelemetry_api: ^0.8.7 + dartastic_opentelemetry_api: + git: + url: git@github.com:yuzurihaaa/dartastic_opentelemetry_api.git + ref: d4a122033ad5de81a1e46bd50fbe31c73b4dabe9 fixnum: ^1.1.1 grpc: ^4.1.0 http: ^1.5.0 diff --git a/test/unit/log/logger_provider_test.dart b/test/unit/log/logger_provider_test.dart new file mode 100644 index 0000000..87f130a --- /dev/null +++ b/test/unit/log/logger_provider_test.dart @@ -0,0 +1,81 @@ +// Licensed under the Apache License, Version 2.0 +// Copyright 2025, Michael Bushe, All rights reserved. + +import 'package:dartastic_opentelemetry/dartastic_opentelemetry.dart'; +import 'package:test/test.dart'; + +void main() { + group('LoggerProvider Tests', () { + late LoggerProvider loggerProvider; + + setUp(() async { + await OTel.reset(); + + // Initialize OTel + await OTel.initialize( + serviceName: 'logger-provider-test-service', + detectPlatformResources: false, + ); + + loggerProvider = OTel.loggerProvider(); + }); + + tearDown(() async { + await OTel.shutdown(); + await OTel.reset(); + }); + + test('LoggerProvider properties reflect API delegate', () { + // Set properties + loggerProvider.endpoint = 'https://test-endpoint'; + loggerProvider.serviceName = 'updated-service-name'; + loggerProvider.serviceVersion = '1.2.3'; + loggerProvider.enabled = false; + + // Verify properties + expect(loggerProvider.endpoint, equals('https://test-endpoint')); + expect(loggerProvider.serviceName, equals('updated-service-name')); + expect(loggerProvider.serviceVersion, equals('1.2.3')); + expect(loggerProvider.enabled, isFalse); + + // Reset enabled back to true for other tests + loggerProvider.enabled = true; + }); + + test('LoggerProvider returns same logger for same configuration', () { + final logger1 = loggerProvider.getLogger('test-logger'); + final logger2 = loggerProvider.getLogger('test-logger'); + final logger3 = loggerProvider.getLogger('different-logger'); + + // Same name should return same logger + expect(identical(logger1, logger2), isTrue); + + // Different name should return different logger + expect(identical(logger1, logger3), isFalse); + }); + + test('ensureResourceIsSet sets resource if null', () { + // Initially resource is default from OTel.initialize + expect(loggerProvider.resource, isNotNull); + + // Set resource to null + loggerProvider.resource = null; + expect(loggerProvider.resource, isNull); + + // Call ensureResourceIsSet + loggerProvider.ensureResourceIsSet(); + + // Resource should now be set to default + expect(loggerProvider.resource, isNotNull); + expect(loggerProvider.resource, equals(OTel.defaultResource)); + }); + + test('resource can be set and retrieved', () { + final newResource = + OTel.resource({'custom.key': 'custom.value'}.toAttributes()); + + loggerProvider.resource = newResource; + expect(loggerProvider.resource, equals(newResource)); + }); + }); +}