Line data Source code
1 : // Copyright The OpenTelemetry Authors
2 : // SPDX-License-Identifier: Apache-2.0
3 :
4 : import 'dart:async';
5 : import 'dart:typed_data';
6 :
7 : import 'package:meta/meta.dart';
8 :
9 : import '../dartastic_opentelemetry.dart';
10 :
11 : /// Main entry point for the OpenTelemetry SDK.
12 : ///
13 : /// The [OTel] class provides static methods for initializing the SDK and
14 : /// creating OpenTelemetry objects such as Tracers, Spans, Meters, and other
15 : /// components necessary for instrumentation.
16 : ///
17 : /// To use the SDK, you must first call [initialize] to set up the global
18 : /// configuration and install the SDK implementation. After initialization,
19 : /// you can use the various factory methods to create OpenTelemetry objects.
20 : ///
21 : /// Example usage:
22 : /// ```dart
23 : /// await OTel.initialize(
24 : /// serviceName: 'my-service',
25 : /// serviceVersion: '1.0.0',
26 : /// endpoint: 'https://otel-collector.example.com:4317',
27 : /// );
28 : ///
29 : /// final tracer = OTel.tracer();
30 : /// final span = tracer.startSpan('my-operation');
31 : /// // ... perform work ...
32 : /// span.end();
33 : /// ```
34 : ///
35 : /// The tenant_id and the resources from platform resource detection are merged
36 : /// with resource attributes with resource attributes taking priority.
37 : /// The values must be valid Attribute types (String, bool, int, double, or
38 : /// List\<String>, List\<bool>, List\<int> or List\<double>).
39 : class OTel {
40 : static OTelSDKFactory? _otelFactory;
41 : static Sampler? _defaultSampler;
42 : static TimeProvider? _defaultTimeProvider;
43 :
44 : /// The global default exception handling options applied by the withSpan
45 : /// family of methods. Configured via `OTel.initialize(...)` and propagated
46 : /// to TracerProviders. Per-call `exceptionOptions` override this. Null
47 : /// until set by initialize(); the Tracer falls back to
48 : /// [SpanExceptionOptions.defaults] when unset.
49 : static SpanExceptionOptions? _defaultSpanExceptionOptions;
50 :
51 : /// Whether print interception is enabled (set via initialize).
52 : static bool _logPrintEnabled = false;
53 :
54 : /// OTelLogger name for print interception (set via initialize).
55 : static String _logPrintLoggerName = 'dart.print';
56 :
57 : /// Lazily initialized DartLogBridge for print interception.
58 : static DartLogBridge? _logBridge;
59 :
60 : /// Lazily initialized zone specification for print interception.
61 : static ZoneSpecification? _printInterceptionZoneSpec;
62 :
63 : /// Default resource for the SDK.
64 : ///
65 : /// This is set during initialization and used by tracer and meter providers
66 : /// that don't have a specific resource set.
67 : static Resource? defaultResource;
68 :
69 : /// API key for Dartastic.io backend, if used.
70 : static String? dartasticApiKey;
71 :
72 : /// Default service name used if none is provided.
73 : static const defaultServiceName = '@dart/dartastic_opentelemetry';
74 :
75 : /// Default OTEL endpoint.
76 : ///
77 : /// Defaults to the OTLP/HTTP port (4318) since http/protobuf is the default
78 : /// protocol per the OpenTelemetry specification. When using gRPC, override
79 : /// this with port 4317.
80 : static const defaultEndpoint = 'http://localhost:4318';
81 :
82 : /// Default tracer name used if none is provided.
83 : static const String _defaultTracerName = 'dartastic';
84 :
85 : /// Default tracer name that can be customized.
86 27 : static String defaultTracerName = _defaultTracerName;
87 :
88 : /// Default tracer version.
89 : static String defaultTracerVersion = '1.0.0';
90 :
91 : /// Initializes the OpenTelemetry SDK with the specified configuration.
92 : ///
93 : /// This method must be called before any other OpenTelemetry operations.
94 : /// It sets up the global configuration and installs the SDK implementation.
95 : ///
96 : /// When OTEL_CONSOLE_EXPORTER is set to true (a compile-time
97 : /// --dart-define), a ConsoleExporter is added alongside the configured
98 : /// span exporter. To replace the default exporter with console output
99 : /// instead, set OTEL_TRACES_EXPORTER=console. Per the OTel spec the
100 : /// default exporter is otlp only — debug logging does not change the
101 : /// export pipeline (use OTEL_LOG_SPANS for span logging).
102 : ///
103 : /// @param endpoint The endpoint URL for the OpenTelemetry collector (default: http://localhost:4318)
104 : /// @param secure Whether to use TLS for the connection (default: true)
105 : /// @param serviceName Name that uniquely identifies the service (default: "@dart/dartastic_opentelemetry")
106 : /// @param serviceVersion Version of the service (defaults to the OTel spec version)
107 : /// @param tracerName Name of the default tracer (default: "dartastic")
108 : /// @param tracerVersion Version of the default tracer (default: null)
109 : /// @param resourceAttributes Additional attributes for the resource
110 : /// @param spanProcessor Custom span processor (default: BatchSpanProcessor with OtlpGrpcSpanExporter)
111 : /// @param sampler Sampling strategy to use (default: AlwaysOnSampler)
112 : /// @param spanKind Default span kind (default: SpanKind.server)
113 : /// @param metricExporter Custom metric exporter for metrics
114 : /// @param metricReader Custom metric reader for metrics
115 : /// @param enableMetrics Whether to enable metrics collection (default: true)
116 : /// @param enableLogs Whether to enable logs collection and auto-configure exporter (default: true).
117 : /// When enabled, the logs exporter is configured based on OTEL_LOGS_EXPORTER env var.
118 : /// @param logRecordExporter Custom log record exporter (overrides OTEL_LOGS_EXPORTER)
119 : /// @param logRecordProcessor Custom log record processor (overrides auto-configuration)
120 : /// @param dartasticApiKey API key for Dartastic.io backend
121 : /// @param tenantId Tenant ID for multi-tenant backends (required for Dartastic.io)
122 : /// @param detectPlatformResources Whether to detect platform resources (default: true)
123 : /// @param logPrint Whether to intercept print() calls and route them to OTel logs (default: false).
124 : /// When enabled, all print() calls within [runWithPrintInterception] will be captured
125 : /// as INFO level logs. Set to true to automatically bridge print statements to OpenTelemetry.
126 : /// @param logPrintLoggerName OTelLogger name for print-intercepted logs (default: 'dart.print')
127 : /// @param timeProvider Clock used for span start, end, and event timestamps.
128 : /// When omitted, defaults to the platform-aware `defaultTimeProvider`:
129 : /// `SystemTimeProvider` (`DateTime.now`, microsecond floor) on native;
130 : /// `WebTimeProvider` (`window.performance.now()` + `timeOrigin`, sub-
131 : /// millisecond) on Dart-on-JS web and Wasm — so web users pick up sub-
132 : /// ms span timing automatically with no opt-in. Pass a custom provider
133 : /// only to override the platform default, e.g. a fake clock in tests.
134 : /// @param oTelFactoryCreationFunction Factory function for creating OTelSDKFactory instances
135 : /// @return A Future that completes when initialization is done
136 : /// @throws StateError if called more than once
137 : /// @throws ArgumentError if required parameters are invalid
138 128 : static Future<void> initialize({
139 : String? endpoint,
140 : bool? secure,
141 : String? serviceName,
142 : String? serviceVersion,
143 : String? tracerName,
144 : String? tracerVersion,
145 : Attributes? resourceAttributes,
146 : SpanProcessor? spanProcessor,
147 : Sampler sampler = const AlwaysOnSampler(),
148 : SpanExceptionOptions spanExceptionOptions = const SpanExceptionOptions(),
149 : SpanKind spanKind = SpanKind.server,
150 : MetricExporter? metricExporter,
151 : MetricReader? metricReader,
152 : bool enableMetrics = true,
153 : bool enableLogs = true,
154 : LogRecordExporter? logRecordExporter,
155 : LogRecordProcessor? logRecordProcessor,
156 : String? dartasticApiKey,
157 : String? tenantId,
158 : bool detectPlatformResources = true,
159 : bool logPrint = false,
160 : String logPrintLoggerName = 'dart.print',
161 : TimeProvider? timeProvider,
162 : OTelFactoryCreationFunction? oTelFactoryCreationFunction =
163 : otelSDKFactoryFactoryFunction,
164 : }) async {
165 : // Apply environment variables only if parameters are not provided
166 : final envServiceName = serviceName == null
167 24 : ? OTelEnv.getServiceConfig()['serviceName'] as String?
168 : : null;
169 : final envServiceVersion = serviceVersion == null
170 214 : ? OTelEnv.getServiceConfig()['serviceVersion'] as String?
171 : : null;
172 :
173 : serviceName ??= envServiceName;
174 : serviceVersion ??= envServiceVersion;
175 :
176 : final otlpConfig = (endpoint == null || secure == null)
177 128 : ? OTelEnv.getOtlpConfig(signal: 'traces')
178 0 : : <String, dynamic>{};
179 : final envEndpoint =
180 104 : endpoint == null ? otlpConfig['endpoint'] as String? : null;
181 128 : final envInsecure = secure == null ? otlpConfig['insecure'] as bool? : null;
182 :
183 : endpoint ??= envEndpoint;
184 : if (secure == null) {
185 : if (envInsecure != null) {
186 : secure = !envInsecure;
187 : } else {
188 : secure = true;
189 : }
190 : }
191 :
192 : // Apply defaults if still null
193 : serviceName ??= defaultServiceName;
194 : serviceVersion ??= '1.0.0';
195 : endpoint ??= defaultEndpoint;
196 : // secure is guaranteed non-null from above
197 :
198 : // Log environment variable usage
199 128 : if (OTelLog.isDebug()) {
200 : if (envServiceName != null) {
201 0 : OTelLog.debug('Using service name from environment: $serviceName');
202 : }
203 : if (envServiceVersion != null) {
204 0 : OTelLog.debug(
205 0 : 'Using service version from environment: $serviceVersion',
206 : );
207 : }
208 : if (envEndpoint != null) {
209 0 : OTelLog.debug('Using endpoint from environment: $endpoint');
210 : }
211 : if (envInsecure != null) {
212 0 : OTelLog.debug('Using insecure setting from environment: $envInsecure');
213 : }
214 : }
215 :
216 : // Get otlpConfig for exporter creation later
217 128 : final otlpConfigForExporter = OTelEnv.getOtlpConfig(signal: 'traces');
218 :
219 : // Get resource attributes from environment and merge with provided ones
220 128 : final envResourceAttrs = OTelEnv.getResourceAttributes();
221 128 : if (envResourceAttrs.isNotEmpty) {
222 : if (resourceAttributes != null) {
223 : // Merge with provided attributes - provided ones take precedence
224 0 : final mergedAttrs = Map<String, Object>.from(envResourceAttrs);
225 0 : resourceAttributes.toList().forEach((attr) {
226 0 : mergedAttrs[attr.key] = attr.value;
227 : });
228 0 : resourceAttributes = OTel.attributesFromMap(mergedAttrs);
229 : } else {
230 0 : resourceAttributes = OTel.attributesFromMap(envResourceAttrs);
231 : }
232 : }
233 : // The API auto-installs its OTelAPIFactory if API-only code runs
234 : // before the SDK initializes. The SDK must upgrade it to the SDK (per spec).
235 : final existingFactory = OTelFactory.otelFactory;
236 5 : if (existingFactory != null && !existingFactory.isAPIFactory) {
237 4 : throw StateError(
238 : 'OTel.initialize() can only be called once. If you need multiple endpoints or service names or versions create a named TracerProvider',
239 : );
240 : }
241 4 : if (existingFactory != null && OTelLog.isDebug()) {
242 4 : OTelLog.debug(
243 : 'OTel.initialize: replacing the auto-installed no-op API factory '
244 : 'with the SDK factory. API objects obtained before initialize() '
245 : 'remain no-ops.',
246 : );
247 : }
248 :
249 128 : if (endpoint.isEmpty) {
250 2 : throw ArgumentError(
251 : 'endpoint must not be the empty string.',
252 : ); //TODO validate url
253 : }
254 128 : if (serviceName.isEmpty) {
255 2 : throw ArgumentError('serviceName must not be the empty string.');
256 : }
257 128 : if (serviceVersion.isEmpty) {
258 2 : throw ArgumentError('serviceVersion must not be the empty string.');
259 : }
260 : final factoryFactory =
261 : oTelFactoryCreationFunction ?? otelSDKFactoryFactoryFunction;
262 : // Initialize with default sampler
263 : _defaultSampler = sampler;
264 : _defaultSpanExceptionOptions = spanExceptionOptions;
265 : _defaultTimeProvider = timeProvider;
266 : OTel.defaultTracerName = tracerName ?? _defaultTracerName;
267 : OTel.defaultTracerVersion = tracerVersion ?? defaultTracerVersion;
268 : OTel.dartasticApiKey = dartasticApiKey;
269 : // Initialize logging from environment variables if needed
270 128 : initializeLogging();
271 :
272 128 : final createdFactory = factoryFactory(
273 : apiEndpoint: endpoint,
274 : apiServiceName: serviceName,
275 : apiServiceVersion: serviceVersion,
276 : );
277 : OTelFactory.otelFactory = createdFactory;
278 : // Populate the cache _getAndCacheOtelFactory maintains, so methods that
279 : // branch on it before the guard (attributes, attributesFromMap) route
280 : // through the SDK factory immediately after initialize.
281 128 : if (createdFactory is OTelSDKFactory) {
282 : _otelFactory = createdFactory;
283 : }
284 :
285 128 : if (OTelLog.isDebug()) {
286 124 : OTelLog.debug(
287 124 : 'OTel initialized with endpoint: $endpoint, service: $serviceName',
288 : );
289 : }
290 :
291 128 : final serviceResourceAttributes = {
292 : 'service.name': serviceName,
293 : 'service.version': serviceVersion,
294 : };
295 : // Create initial resource with service attributes
296 128 : var baseResource = OTel.resource(
297 128 : OTel.attributesFromMap(serviceResourceAttributes),
298 : );
299 :
300 : if (tenantId != null) {
301 : // Create a separate tenant_id resource to ensure it's preserved
302 3 : final tenantResource = OTel.resource(
303 6 : OTel.attributesFromMap({'tenant_id': tenantId}),
304 : );
305 3 : if (OTelLog.isDebug()) {
306 3 : OTelLog.debug(
307 3 : 'OTel.initialize: Creating tenant_id resource with: $tenantId',
308 : );
309 : }
310 : // Merge tenant into the base resource
311 3 : baseResource = baseResource.merge(tenantResource);
312 : }
313 :
314 : // Initialize with tenant-aware resource
315 : var mergedResource = baseResource;
316 : if (detectPlatformResources) {
317 50 : final resourceDetector = PlatformResourceDetector.create();
318 50 : final platformResource = await resourceDetector.detect();
319 : // Merge platform resource with our service resource - our service resource takes precedence
320 50 : mergedResource = platformResource.merge(mergedResource);
321 :
322 50 : if (OTelLog.isDebug()) {
323 50 : OTelLog.debug('Resource after platform merge:');
324 200 : mergedResource.attributes.toList().forEach((attr) {
325 200 : if (attr.key == 'tenant_id' || attr.key == 'service.name') {
326 200 : OTelLog.debug(' ${attr.key}: ${attr.value}');
327 : }
328 : });
329 : }
330 : }
331 : if (resourceAttributes != null) {
332 2 : final initResources = OTel.resource(resourceAttributes);
333 : // Merge user-provided attributes - they have highest priority
334 2 : mergedResource = mergedResource.merge(initResources);
335 :
336 2 : if (OTelLog.isDebug()) {
337 2 : OTelLog.debug('Resource after user attributes merge:');
338 8 : mergedResource.attributes.toList().forEach((attr) {
339 8 : if (attr.key == 'tenant_id' || attr.key == 'service.name') {
340 8 : OTelLog.debug(' ${attr.key}: ${attr.value}');
341 : }
342 : });
343 : }
344 : }
345 : // Set the final merged resource as default
346 : OTel.defaultResource = mergedResource;
347 :
348 128 : if (OTelLog.isDebug()) {
349 : // Final check to ensure tenant_id is preserved
350 : if (tenantId != null && OTel.defaultResource != null) {
351 : var hasTenantId = false;
352 12 : OTel.defaultResource!.attributes.toList().forEach((attr) {
353 6 : if (attr.key == 'tenant_id') {
354 : hasTenantId = true;
355 3 : if (OTelLog.isDebug()) {
356 3 : OTelLog.debug(
357 6 : 'Final resource check - tenant_id is present: ${attr.value}',
358 : );
359 : }
360 : }
361 : });
362 :
363 : if (!hasTenantId) {
364 : // As a last resort, add the tenant_id directly
365 0 : if (OTelLog.isDebug()) {
366 0 : OTelLog.debug('tenant_id was missing - adding it as fallback');
367 : }
368 0 : final tenantResource = OTel.resource(
369 0 : OTel.attributesFromMap({'tenant_id': tenantId}),
370 : );
371 0 : OTel.defaultResource = OTel.defaultResource!.merge(tenantResource);
372 : }
373 : }
374 : }
375 :
376 : // OTEL_SDK_DISABLED=true is the spec-defined global off-switch: all three
377 : // signals become no-ops. Honoring this here keeps signal-specific configs
378 : // (`OTEL_*_EXPORTER`) from being ever consulted.
379 128 : final sdkDisabled = OTelEnv.isSdkDisabled();
380 0 : if (sdkDisabled && OTelLog.isDebug()) {
381 0 : OTelLog.debug('OTel: OTEL_SDK_DISABLED=true, skipping all signal setup');
382 : }
383 :
384 : if (spanProcessor == null && !sdkDisabled) {
385 : // Determine which exporter to create based on environment or defaults
386 121 : final exporterType = OTelEnv.getExporter(signal: 'traces') ?? 'otlp';
387 :
388 121 : if (exporterType != 'none') {
389 : // Determine protocol - default to http/protobuf if not set
390 : final protocol =
391 121 : otlpConfigForExporter['protocol'] as String? ?? 'http/protobuf';
392 :
393 : SpanExporter exporter;
394 121 : if (exporterType == 'console') {
395 0 : exporter = ConsoleExporter();
396 121 : } else if (exporterType == 'otlp') {
397 : // Create appropriate exporter based on protocol
398 121 : if (protocol == 'grpc') {
399 0 : exporter = OtlpGrpcSpanExporter(
400 0 : OtlpGrpcExporterConfig(
401 : endpoint: endpoint,
402 : insecure: !secure,
403 : headers:
404 0 : otlpConfigForExporter['headers'] as Map<String, String>? ??
405 0 : {},
406 0 : timeout: otlpConfigForExporter['timeout'] as Duration? ??
407 : const Duration(seconds: 10),
408 0 : compression: otlpConfigForExporter['compression'] == 'gzip',
409 0 : certificate: otlpConfigForExporter['certificate'] as String?,
410 0 : clientKey: otlpConfigForExporter['clientKey'] as String?,
411 : clientCertificate:
412 0 : otlpConfigForExporter['clientCertificate'] as String?,
413 : ),
414 : );
415 : } else {
416 : // http/protobuf (default) or http/json (opt-in via env-var).
417 : // Anything else falls back to http/protobuf — the spec-
418 : // recommended default per `specification/protocol/exporter.md`.
419 121 : final httpProtocol = otlpHttpProtocolFromString(protocol) ??
420 : OtlpHttpProtocol.httpProtobuf;
421 121 : exporter = OtlpHttpSpanExporter(
422 121 : OtlpHttpExporterConfig(
423 : endpoint: endpoint,
424 : headers:
425 121 : otlpConfigForExporter['headers'] as Map<String, String>? ??
426 121 : {},
427 121 : timeout: otlpConfigForExporter['timeout'] as Duration? ??
428 : const Duration(seconds: 10),
429 242 : compression: otlpConfigForExporter['compression'] == 'gzip',
430 121 : certificate: otlpConfigForExporter['certificate'] as String?,
431 121 : clientKey: otlpConfigForExporter['clientKey'] as String?,
432 : clientCertificate:
433 121 : otlpConfigForExporter['clientCertificate'] as String?,
434 : protocol: httpProtocol,
435 : ),
436 : );
437 : }
438 : } else {
439 : // Fallback to gRPC for backward compatibility
440 0 : exporter = OtlpGrpcSpanExporter(
441 0 : OtlpGrpcExporterConfig(endpoint: endpoint, insecure: !secure),
442 : );
443 : }
444 :
445 : // Only add ConsoleExporter in debug mode or if explicitly requested
446 121 : final exporters = <SpanExporter>[exporter];
447 : // Spec: the default pipeline is otlp only — debug logging must not
448 : // alter it (see #49 for the identical metrics fix). Console output
449 : // stays available via OTEL_TRACES_EXPORTER=console (replaces) or the
450 : // OTEL_CONSOLE_EXPORTER dart-define (adds alongside).
451 : if (const bool.fromEnvironment(
452 : 'OTEL_CONSOLE_EXPORTER',
453 : defaultValue: false,
454 : )) {
455 0 : exporters.add(ConsoleExporter());
456 : }
457 :
458 121 : spanProcessor = BatchSpanProcessor(
459 242 : exporters.length == 1 ? exporter : CompositeExporter(exporters),
460 : const BatchSpanProcessorConfig(
461 : maxQueueSize: 2048,
462 : scheduleDelay: Duration(seconds: 1),
463 : maxExportBatchSize: 512,
464 : ),
465 : );
466 : }
467 : // If exporterType == 'none', spanProcessor remains null and no processor is added
468 : }
469 :
470 : // Create and configure TracerProvider — but when OTEL_SDK_DISABLED=true,
471 : // do not install any processor even if the caller passed one explicitly,
472 : // so the SDK is a true no-op for traces.
473 : if (spanProcessor != null && !sdkDisabled) {
474 256 : OTel.tracerProvider().addSpanProcessor(spanProcessor);
475 : }
476 :
477 : // Configure metrics if enabled
478 : if (enableMetrics && !sdkDisabled) {
479 : // If no explicit metric exporter is provided, create one with the same endpoint
480 : if (metricExporter == null && metricReader == null) {
481 106 : MetricsConfiguration.configureMeterProvider(
482 : endpoint: endpoint,
483 : secure: secure,
484 : resource: OTel.defaultResource,
485 : );
486 : } else {
487 : // Use the provided exporter and/or reader
488 16 : MetricsConfiguration.configureMeterProvider(
489 : endpoint: endpoint,
490 : secure: secure,
491 : metricExporter: metricExporter,
492 : metricReader: metricReader,
493 : resource: OTel.defaultResource,
494 : );
495 : }
496 : }
497 :
498 : // Configure logs if enabled
499 : if (enableLogs && !sdkDisabled) {
500 118 : LogsConfiguration.configureLoggerProvider(
501 : endpoint: endpoint,
502 : secure: secure,
503 : logRecordExporter: logRecordExporter,
504 : logRecordProcessor: logRecordProcessor,
505 : resource: OTel.defaultResource,
506 : );
507 : }
508 :
509 : // Store print interception configuration (lazily initialized when needed)
510 : _logPrintEnabled = logPrint;
511 : _logPrintLoggerName = logPrintLoggerName;
512 :
513 2 : if (logPrint && OTelLog.isDebug()) {
514 2 : OTelLog.debug(
515 2 : 'OTel: Print interception enabled with logger: $logPrintLoggerName');
516 : }
517 : }
518 :
519 : /// Ensures the print interception bridge is initialized.
520 : /// Called lazily when runWithPrintInterception is first used.
521 2 : static void _ensurePrintInterceptionInitialized() {
522 : if (_logBridge != null) return;
523 :
524 2 : final logger = OTel.logger(_logPrintLoggerName);
525 2 : _logBridge = DartLogBridge.install(
526 : logger,
527 : minimumSeverity: Severity.TRACE,
528 : );
529 2 : _printInterceptionZoneSpec = _logBridge!.createZoneSpecification();
530 :
531 2 : if (OTelLog.isDebug()) {
532 2 : OTelLog.debug(
533 2 : 'OTel: Print interception bridge initialized with logger: $_logPrintLoggerName');
534 : }
535 : }
536 :
537 : /// Creates a Resource with the specified attributes and schema URL.
538 : ///
539 : /// Resources represent the entity producing telemetry, such as a service,
540 : /// process, or device. They are a collection of attributes that provide
541 : /// identifying information about the entity.
542 : ///
543 : /// @param attributes Attributes describing the resource
544 : /// @param schemaUrl Optional URL of the schema defining the attributes
545 : /// @return A new Resource instance
546 128 : static Resource resource(Attributes? attributes, [String? schemaUrl]) {
547 128 : _getAndCacheOtelFactory();
548 128 : return (_otelFactory as OTelSDKFactory).resource(
549 5 : attributes ?? OTel.attributes(),
550 : schemaUrl,
551 : );
552 : }
553 :
554 : /// Creates a new ContextKey with the given name.
555 : ///
556 : /// Context keys are used to store and retrieve values in a Context.
557 : /// Each instance will be unique, even with the same name, per the OTel spec.
558 : /// The name is for debugging purposes only.
559 : ///
560 : /// @param name The name of the context key (for debugging only)
561 : /// @param isTransferable When `true`, values stored under this key transfer
562 : /// across isolate boundaries via `Context.runIsolate()`. Defaults to `false`
563 : /// (custom keys are local to their isolate). Built-in `Baggage` and
564 : /// `SpanContext` always transfer regardless of this flag.
565 : /// @return A new ContextKey instance
566 2 : static ContextKey<T> contextKey<T>(String name,
567 : {bool isTransferable = false}) {
568 2 : _getAndCacheOtelFactory();
569 1 : return _otelFactory!.contextKey<T>(
570 : name,
571 1 : ContextKey.generateContextKeyId(),
572 : isTransferable: isTransferable,
573 : );
574 : }
575 :
576 : /// Creates a new Context with optional Baggage and SpanContext.
577 : ///
578 : /// Contexts are used to propagate information across the execution path,
579 : /// such as trace context, baggage, and other cross-cutting concerns.
580 : ///
581 : /// @param baggage Optional baggage to include in the context
582 : /// @param spanContext Optional span context to include in the context
583 : /// @return A new Context instance
584 16 : static Context context({Baggage? baggage, SpanContext? spanContext}) {
585 16 : _getAndCacheOtelFactory();
586 16 : var context = OTelFactory.otelFactory!.context(baggage: baggage);
587 : if (spanContext != null) {
588 3 : context = context.copyWithSpanContext(spanContext);
589 : }
590 : return context;
591 : }
592 :
593 : /// Gets a TracerProvider for creating Tracers.
594 : ///
595 : /// If name is null, this returns the global default TracerProvider, which shares
596 : /// the endpoint, serviceName, serviceVersion, sampler and resource set in initialize().
597 : /// If the name is not null, it returns a TracerProvider for the name that was added
598 : /// with addTracerProvider.
599 : ///
600 : /// The endpoint, serviceName, serviceVersion, sampler and resource set flow down
601 : /// to the [Tracer]s created by the TracerProvider and the [Span]
602 : /// created by those tracers
603 : /// @param name Optional name of a specific TracerProvider
604 : /// @return The TracerProvider instance
605 128 : static TracerProvider tracerProvider({String? name}) {
606 128 : _getAndCacheOtelFactory();
607 128 : final tracerProvider = OTelAPI.tracerProvider(name) as TracerProvider;
608 : // Ensure the resource is properly set
609 128 : if (tracerProvider.resource == null && defaultResource != null) {
610 128 : tracerProvider.resource = defaultResource;
611 128 : if (OTelLog.isDebug()) {
612 124 : OTelLog.debug('OTel.tracerProvider: Setting resource from default');
613 : if (defaultResource != null) {
614 496 : defaultResource!.attributes.toList().forEach((attr) {
615 496 : if (attr.key == 'tenant_id' || attr.key == 'service.name') {
616 496 : OTelLog.debug(' ${attr.key}: ${attr.value}');
617 : }
618 : });
619 : }
620 : }
621 : }
622 :
623 128 : tracerProvider.sampler ??= _defaultSampler;
624 128 : tracerProvider.spanExceptionOptions ??= _defaultSpanExceptionOptions;
625 : if (_defaultTimeProvider != null) {
626 1 : tracerProvider.timeProvider = _defaultTimeProvider!;
627 : }
628 : return tracerProvider;
629 : }
630 :
631 : /// Gets a MeterProvider for creating Meters.
632 : ///
633 : /// If name is null, this returns the global default MeterProvider, which shares
634 : /// the endpoint, serviceName, serviceVersion and resource set in initialize().
635 : /// If the name is not null, it returns a MeterProvider for the name that was added
636 : /// with addMeterProvider.
637 : ///
638 : /// @param name Optional name of a specific MeterProvider
639 : /// @return The MeterProvider instance
640 122 : static MeterProvider meterProvider({String? name}) {
641 122 : _getAndCacheOtelFactory();
642 121 : final meterProvider = OTelAPI.meterProvider(name) as MeterProvider;
643 121 : meterProvider.resource ??= defaultResource;
644 : return meterProvider;
645 : }
646 :
647 : /// Adds or replaces a named TracerProvider.
648 : ///
649 : /// This allows for creating multiple TracerProviders with different configurations,
650 : /// which can be useful for sending telemetry to different backends or with different
651 : /// settings.
652 : ///
653 : /// @param name The name of the TracerProvider
654 : /// @param endpoint Optional custom endpoint URL
655 : /// @param serviceName Optional custom service name
656 : /// @param serviceVersion Optional custom service version
657 : /// @param resource Optional custom resource
658 : /// @param sampler Optional custom sampler
659 : /// @param spanExceptionOptions Optional default exception handling options;
660 : /// defaults to the options set in initialize()
661 : /// @return The newly created or replaced TracerProvider
662 10 : static TracerProvider addTracerProvider(
663 : String name, {
664 : String? endpoint,
665 : String? serviceName,
666 : String? serviceVersion,
667 : Resource? resource,
668 : Sampler? sampler,
669 : SpanExceptionOptions? spanExceptionOptions,
670 : }) {
671 10 : _getAndCacheOtelFactory();
672 9 : final sdkTracerProvider = OTelAPI.addTracerProvider(name) as TracerProvider;
673 9 : sdkTracerProvider.resource = resource ?? defaultResource;
674 9 : sdkTracerProvider.sampler = sampler ?? _defaultSampler;
675 9 : sdkTracerProvider.spanExceptionOptions =
676 : spanExceptionOptions ?? _defaultSpanExceptionOptions;
677 : if (_defaultTimeProvider != null) {
678 0 : sdkTracerProvider.timeProvider = _defaultTimeProvider!;
679 : }
680 : return sdkTracerProvider;
681 : }
682 :
683 : /// @return the [TracerProvider]s, the global default and named ones.
684 127 : static List<APITracerProvider> tracerProviders() {
685 127 : return OTelAPI.tracerProviders();
686 : }
687 :
688 : /// Gets the default Tracer from the default TracerProvider.
689 : ///
690 : /// This is a convenience method for getting a Tracer with the default configuration.
691 : /// The endpoint, serviceName, serviceVersion, sampler and resource all flow down
692 : /// from the OTel defaults set during initialization.
693 : ///
694 : /// @return The default Tracer instance
695 26 : static Tracer tracer() {
696 52 : return tracerProvider().getTracer(
697 26 : defaultTracerName,
698 : version: defaultTracerVersion,
699 : );
700 : }
701 :
702 : /// Activates [span] for the duration of [fn] (so `Context.current.span`
703 : /// returns it inside `fn`) and records any thrown exception with
704 : /// `SpanStatusCode.Error`. The caller is still responsible for
705 : /// `span.end()` — typically in a `finally` block.
706 : ///
707 : /// Convenience over `OTel.tracer().withSpan(span, fn)` for callers
708 : /// that don't already have a [Tracer] reference.
709 : ///
710 : /// [exceptionOptions] controls how a thrown exception is recorded and
711 : /// whether the span status is set; see [SpanExceptionOptions].
712 6 : static T withSpan<T>(
713 : APISpan span,
714 : T Function() fn, {
715 : SpanExceptionOptions? exceptionOptions,
716 : }) =>
717 12 : tracer().withSpan(span, fn, exceptionOptions: exceptionOptions);
718 :
719 : /// Async variant of [withSpan]. Propagates the active span across
720 : /// `await` boundaries via Zone-based context.
721 : ///
722 : /// [exceptionOptions] controls how a thrown exception is recorded and
723 : /// whether the span status is set; see [SpanExceptionOptions].
724 4 : static Future<T> withSpanAsync<T>(
725 : APISpan span,
726 : Future<T> Function() fn, {
727 : SpanExceptionOptions? exceptionOptions,
728 : }) =>
729 8 : tracer().withSpanAsync(span, fn, exceptionOptions: exceptionOptions);
730 :
731 : /// Adds or replaces a named MeterProvider.
732 : ///
733 : /// This allows for creating multiple MeterProviders with different configurations,
734 : /// which can be useful for sending metrics to different backends or with different
735 : /// settings.
736 : ///
737 : /// @param name The name of the MeterProvider
738 : /// @param endpoint Optional custom endpoint URL
739 : /// @param serviceName Optional custom service name
740 : /// @param serviceVersion Optional custom service version
741 : /// @param resource Optional custom resource
742 : /// @return The newly created or replaced MeterProvider
743 2 : static MeterProvider addMeterProvider(
744 : String name, {
745 : String? endpoint,
746 : String? serviceName,
747 : String? serviceVersion,
748 : Resource? resource,
749 : }) {
750 2 : _getAndCacheOtelFactory();
751 2 : final mp = _otelFactory!.addMeterProvider(
752 : name,
753 : endpoint: endpoint,
754 : serviceName: serviceName,
755 : serviceVersion: serviceVersion,
756 : ) as MeterProvider;
757 2 : mp.resource = resource ?? defaultResource;
758 : return mp;
759 : }
760 :
761 : /// @return the [MeterProvider]s, the global default and named ones.
762 127 : static List<APIMeterProvider> meterProviders() {
763 127 : return OTelAPI.meterProviders();
764 : }
765 :
766 : /// Gets the default Meter from the default MeterProvider.
767 : ///
768 : /// This is a convenience method for getting a Meter with the default configuration.
769 : /// The endpoint, serviceName, serviceVersion and resource all flow down from
770 : /// the OTel defaults set during initialization.
771 : ///
772 : /// @param name Optional custom name for the meter (defaults to defaultTracerName)
773 : /// @return The default Meter instance
774 15 : static Meter meter([String? name]) {
775 30 : return meterProvider().getMeter(
776 3 : name: name ?? defaultTracerName,
777 : version: defaultTracerVersion,
778 : ) as Meter;
779 : }
780 :
781 : /// Gets a LoggerProvider for creating Loggers.
782 : ///
783 : /// If name is null, this returns the global default LoggerProvider, which shares
784 : /// the endpoint, serviceName, serviceVersion and resource set in initialize().
785 : /// If the name is not null, it returns a LoggerProvider for the name that was added
786 : /// with addLoggerProvider.
787 : ///
788 : /// @param name Optional name of a specific LoggerProvider
789 : /// @return The LoggerProvider instance
790 120 : static LoggerProvider loggerProvider({String? name}) {
791 120 : _getAndCacheOtelFactory();
792 120 : final logProvider = OTelAPI.loggerProvider(name) as LoggerProvider;
793 120 : logProvider.resource ??= defaultResource;
794 : return logProvider;
795 : }
796 :
797 : /// Adds or replaces a named LoggerProvider.
798 : ///
799 : /// This allows for creating multiple LoggerProviders with different configurations,
800 : /// which can be useful for sending logs to different backends or with different
801 : /// settings.
802 : ///
803 : /// @param name The name of the LoggerProvider
804 : /// @param endpoint Optional custom endpoint URL
805 : /// @param serviceName Optional custom service name
806 : /// @param serviceVersion Optional custom service version
807 : /// @param resource Optional custom resource
808 : /// @return The newly created or replaced LoggerProvider
809 2 : static LoggerProvider addLoggerProvider(
810 : String name, {
811 : String? endpoint,
812 : String? serviceName,
813 : String? serviceVersion,
814 : Resource? resource,
815 : }) {
816 2 : _getAndCacheOtelFactory();
817 2 : final lp = _otelFactory!.addLogProvider(name,
818 : endpoint: endpoint,
819 : serviceName: serviceName,
820 : serviceVersion: serviceVersion) as LoggerProvider;
821 2 : lp.resource = resource ?? defaultResource;
822 : return lp;
823 : }
824 :
825 : /// Gets the default OTelLogger from the default LoggerProvider.
826 : ///
827 : /// This is a convenience method for getting a OTelLogger with the default configuration.
828 : /// The endpoint, serviceName, serviceVersion and resource all flow down from
829 : /// the OTel defaults set during initialization.
830 : ///
831 : /// @param name Optional custom name for the logger (defaults to defaultTracerName)
832 : /// @return The default OTelLogger instance
833 5 : static OTelLogger logger([String? name]) {
834 10 : return loggerProvider().getLogger(
835 1 : name ?? defaultTracerName,
836 : version: defaultTracerVersion,
837 : );
838 : }
839 :
840 : /// Whether print interception is enabled.
841 : ///
842 : /// Returns true if [initialize] was called with `logPrint: true`.
843 2 : static bool get isLogPrintEnabled => _logPrintEnabled;
844 :
845 : /// Gets the current DartLogBridge instance, if print interception is enabled.
846 : ///
847 : /// Returns null if print interception was not enabled during initialization.
848 2 : static DartLogBridge? get logBridge => _logBridge;
849 :
850 : /// Runs the given callback in a zone that intercepts print() calls.
851 : ///
852 : /// When [initialize] is called with `logPrint: true`, this method runs
853 : /// the callback in a zone where all `print()` calls are captured and
854 : /// routed to OpenTelemetry logs as INFO level messages.
855 : ///
856 : /// If print interception is not enabled, the callback is run directly
857 : /// without any interception.
858 : ///
859 : /// Example usage:
860 : /// ```dart
861 : /// await OTel.initialize(
862 : /// serviceName: 'my-service',
863 : /// logPrint: true,
864 : /// );
865 : ///
866 : /// OTel.runWithPrintInterception(() {
867 : /// print('This will be captured as an OTel log');
868 : /// });
869 : /// ```
870 : ///
871 : /// @param callback The code to run with print interception
872 : /// @return The result of the callback
873 3 : static R runWithPrintInterception<R>(R Function() callback) {
874 : if (!_logPrintEnabled) {
875 3 : return callback();
876 : }
877 2 : _ensurePrintInterceptionInitialized();
878 2 : return runZoned(callback, zoneSpecification: _printInterceptionZoneSpec);
879 : }
880 :
881 : /// Runs the given async callback in a zone that intercepts print() calls.
882 : ///
883 : /// This is the async version of [runWithPrintInterception].
884 : ///
885 : /// @param callback The async code to run with print interception
886 : /// @return A Future containing the result of the callback
887 3 : static Future<R> runWithPrintInterceptionAsync<R>(
888 : Future<R> Function() callback) {
889 : if (!_logPrintEnabled) {
890 2 : return callback();
891 : }
892 2 : _ensurePrintInterceptionInitialized();
893 2 : return runZoned(callback, zoneSpecification: _printInterceptionZoneSpec);
894 : }
895 :
896 : /// Creates a SpanContext with the specified parameters.
897 : ///
898 : /// A SpanContext represents the portion of a span that must be propagated
899 : /// to descendant spans and across process boundaries. It contains the
900 : /// traceId, spanId, traceFlags, and traceState.
901 : ///
902 : /// @param traceId The trace ID (defaults to a new random ID)
903 : /// @param spanId The span ID (defaults to a new random ID)
904 : /// @param parentSpanId The parent span ID (defaults to an invalid span ID)
905 : /// @param traceFlags Trace flags (defaults to NONE_FLAG)
906 : /// @param traceState Trace state
907 : /// @param isRemote Whether this context was received from a remote source
908 : /// @return A new SpanContext instance
909 60 : static SpanContext spanContext({
910 : TraceId? traceId,
911 : SpanId? spanId,
912 : SpanId? parentSpanId,
913 : TraceFlags? traceFlags,
914 : TraceState? traceState,
915 : bool? isRemote,
916 : }) {
917 60 : return OTelAPI.spanContext(
918 2 : traceId: traceId ?? OTel.traceId(),
919 2 : spanId: spanId ?? OTel.spanId(),
920 21 : parentSpanId: parentSpanId ?? spanIdInvalid(),
921 14 : traceFlags: traceFlags ?? OTelAPI.traceFlags(),
922 : traceState: traceState,
923 : isRemote: isRemote,
924 : );
925 : }
926 :
927 : /// Creates a child SpanContext from a parent context.
928 : ///
929 : /// This creates a new SpanContext that shares the same traceId as the parent,
930 : /// but has a new spanId and the parentSpanId set to the parent's spanId.
931 : ///
932 : /// @param parent The parent SpanContext
933 : /// @return A new child SpanContext
934 1 : static SpanContext spanContextFromParent(SpanContext parent) {
935 1 : _getAndCacheOtelFactory();
936 1 : return OTelFactory.otelFactory!.spanContextFromParent(parent);
937 : }
938 :
939 : /// Creates an invalid SpanContext (all zeros).
940 : ///
941 : /// An invalid SpanContext represents the absence of a trace context.
942 : ///
943 : /// @return An invalid SpanContext instance
944 2 : static SpanContext spanContextInvalid() {
945 2 : _getAndCacheOtelFactory();
946 2 : return OTelFactory.otelFactory!.spanContextInvalid();
947 : }
948 :
949 : /// Creates a SpanEvent with the current timestamp.
950 : ///
951 : /// Note: Per [OTEP 0265](https://opentelemetry.io/docs/specs/semconv/general/events/),
952 : /// span events are being deprecated and will be replaced by the Logging API in future versions.
953 : ///
954 : /// @param name The name of the event
955 : /// @param attributes Attributes to associate with the event
956 : /// @return A new SpanEvent instance with the current timestamp
957 1 : static SpanEvent spanEventNow(String name, Attributes attributes) {
958 1 : _getAndCacheOtelFactory();
959 2 : return spanEvent(name, attributes, DateTime.now());
960 : }
961 :
962 : /// Creates a SpanEvent with the specified parameters.
963 : ///
964 : /// Note: Per [OTEP 0265](https://opentelemetry.io/docs/specs/semconv/general/events/),
965 : /// span events are being deprecated and will be replaced by the Logging API in future versions.
966 : ///
967 : /// @param name The name of the event
968 : /// @param attributes Optional attributes to associate with the event
969 : /// @param timestamp Optional timestamp for the event (defaults to null)
970 : /// @return A new SpanEvent instance
971 4 : static SpanEvent spanEvent(
972 : String name, [
973 : Attributes? attributes,
974 : DateTime? timestamp,
975 : ]) {
976 4 : _getAndCacheOtelFactory();
977 4 : return _otelFactory!.spanEvent(name, attributes, timestamp);
978 : }
979 :
980 : /// Creates a Baggage with key-value pairs.
981 : ///
982 : /// Baggage is a set of key-value pairs that can be propagated across service boundaries
983 : /// along with the trace context. It can be used to add contextual information to traces.
984 : ///
985 : /// @param keyValuePairs A map of key-value pairs to include in the baggage
986 : /// @return A new Baggage instance
987 1 : static Baggage baggageForMap(Map<String, String> keyValuePairs) {
988 1 : _getAndCacheOtelFactory();
989 1 : return _otelFactory!.baggageForMap(keyValuePairs);
990 : }
991 :
992 : /// Creates a BaggageEntry with the specified value and optional metadata.
993 : ///
994 : /// @param value The value of the baggage entry
995 : /// @param metadata Optional metadata for the baggage entry
996 : /// @return A new BaggageEntry instance
997 3 : static BaggageEntry baggageEntry(String value, [String? metadata]) {
998 3 : _getAndCacheOtelFactory();
999 3 : return _otelFactory!.baggageEntry(value, metadata);
1000 : }
1001 :
1002 : /// Creates a Baggage with the specified entries.
1003 : ///
1004 : /// @param entries Optional map of baggage entries
1005 : /// @return A new Baggage instance
1006 4 : static Baggage baggage([Map<String, BaggageEntry>? entries]) {
1007 4 : _getAndCacheOtelFactory();
1008 4 : return _otelFactory!.baggage(entries);
1009 : }
1010 :
1011 : /// Creates a Baggage instance from a JSON representation.
1012 : ///
1013 : /// @param json JSON representation of a baggage
1014 : /// @return A new Baggage instance
1015 1 : static Baggage baggageFromJson(Map<String, dynamic> json) {
1016 1 : return OTelAPI.baggageFromJson(json);
1017 : }
1018 :
1019 : /// Creates a string attribute.
1020 : ///
1021 : /// @param name The name of the attribute
1022 : /// @param value The string value of the attribute
1023 : /// @return A new Attribute instance
1024 14 : static Attribute<String> attributeString(String name, String value) {
1025 14 : _getAndCacheOtelFactory();
1026 14 : return _otelFactory!.attributeString(name, value);
1027 : }
1028 :
1029 : /// Creates a boolean attribute.
1030 : ///
1031 : /// @param name The name of the attribute
1032 : /// @param value The boolean value of the attribute
1033 : /// @return A new Attribute instance
1034 2 : static Attribute<bool> attributeBool(String name, bool value) {
1035 2 : _getAndCacheOtelFactory();
1036 2 : return _otelFactory!.attributeBool(name, value);
1037 : }
1038 :
1039 : /// Creates an integer attribute.
1040 : ///
1041 : /// @param name The name of the attribute
1042 : /// @param value The integer value of the attribute
1043 : /// @return A new Attribute instance
1044 5 : static Attribute<int> attributeInt(String name, int value) {
1045 5 : _getAndCacheOtelFactory();
1046 5 : return _otelFactory!.attributeInt(name, value);
1047 : }
1048 :
1049 : /// Creates a double attribute.
1050 : ///
1051 : /// @param name The name of the attribute
1052 : /// @param value The double value of the attribute
1053 : /// @return A new Attribute instance
1054 3 : static Attribute<double> attributeDouble(String name, double value) {
1055 3 : _getAndCacheOtelFactory();
1056 3 : return _otelFactory!.attributeDouble(name, value);
1057 : }
1058 :
1059 : /// Creates a string list attribute.
1060 : ///
1061 : /// @param name The name of the attribute
1062 : /// @param value The list of string values
1063 : /// @return A new Attribute instance
1064 3 : static Attribute<List<String>> attributeStringList(
1065 : String name,
1066 : List<String> value,
1067 : ) {
1068 3 : _getAndCacheOtelFactory();
1069 3 : return _otelFactory!.attributeStringList(name, value);
1070 : }
1071 :
1072 : /// Creates a boolean list attribute.
1073 : ///
1074 : /// @param name The name of the attribute
1075 : /// @param value The list of boolean values
1076 : /// @return A new Attribute instance
1077 3 : static Attribute<List<bool>> attributeBoolList(
1078 : String name,
1079 : List<bool> value,
1080 : ) {
1081 3 : _getAndCacheOtelFactory();
1082 3 : return _otelFactory!.attributeBoolList(name, value);
1083 : }
1084 :
1085 : /// Creates an integer list attribute.
1086 : ///
1087 : /// @param name The name of the attribute
1088 : /// @param value The list of integer values
1089 : /// @return A new Attribute instance
1090 3 : static Attribute<List<int>> attributeIntList(String name, List<int> value) {
1091 3 : _getAndCacheOtelFactory();
1092 3 : return _otelFactory!.attributeIntList(name, value);
1093 : }
1094 :
1095 : /// Creates a double list attribute.
1096 : ///
1097 : /// @param name The name of the attribute
1098 : /// @param value The list of double values
1099 : /// @return A new Attribute instance
1100 3 : static Attribute<List<double>> attributeDoubleList(
1101 : String name,
1102 : List<double> value,
1103 : ) {
1104 3 : _getAndCacheOtelFactory();
1105 3 : return _otelFactory!.attributeDoubleList(name, value);
1106 : }
1107 :
1108 : /// Creates an empty Attributes collection.
1109 : ///
1110 : /// @return A new empty Attributes collection
1111 5 : static Attributes createAttributes() {
1112 5 : _getAndCacheOtelFactory();
1113 5 : return _otelFactory!.attributes();
1114 : }
1115 :
1116 : /// Creates an Attributes collection from a list of Attribute objects.
1117 : ///
1118 : /// Often called before initialize; pre-init this routes through the API,
1119 : /// which lazily installs the no-op factory per spec (API ≥ beta.9 — there
1120 : /// is no factory-bypassing cheat path anymore).
1121 : ///
1122 : /// @param entries Optional list of Attribute objects
1123 : /// @return A new Attributes collection
1124 26 : static Attributes attributes([List<Attribute>? entries]) {
1125 : return _otelFactory == null
1126 0 : ? OTelAPI.attributes(entries)
1127 26 : : _otelFactory!.attributes(entries);
1128 : }
1129 :
1130 : /// Creates an Attributes collection from a map of named values.
1131 : ///
1132 : /// String, bool, int, double, or Lists of those types get converted
1133 : /// to the matching typed attribute. DateTime gets converted to a
1134 : /// String attribute with the UTC time string.
1135 : ///
1136 : /// Unlike most methods, this does not create the OTelFactory if
1137 : /// one does not exist, instead it uses the OTelAPI's attributesFromMap.
1138 : ///
1139 : /// Alternatively, consider using the toAttributes()
1140 : /// extension on \<String, Map>{}.
1141 : /// @param namedMap Map of attribute names to values
1142 : /// @return A new Attributes collection
1143 128 : static Attributes attributesFromMap(Map<String, Object> namedMap) {
1144 : if (_otelFactory == null) {
1145 2 : return OTelAPI.attributesFromMap(namedMap);
1146 : } else {
1147 128 : return _otelFactory!.attributesFromMap(namedMap);
1148 : }
1149 : }
1150 :
1151 : /// Creates an [Attributes] from a map keyed by [OTelSemantic] enum values
1152 : /// (e.g. `Http.requestMethod`). Each enum's `.key` is used as the
1153 : /// attribute name. Lets you write
1154 : ///
1155 : /// ```dart
1156 : /// OTel.attributesFromSemanticMap({
1157 : /// Http.requestMethod: 'GET',
1158 : /// Http.responseStatusCode: 200,
1159 : /// })
1160 : /// ```
1161 : ///
1162 : /// instead of `attributesFromMap({Http.requestMethod.key: 'GET', …})`.
1163 : /// Mixing enum types in one map is fine — the param is `Map<OTelSemantic, Object>`,
1164 : /// and every semconv enum implements `OTelSemantic`.
1165 : ///
1166 : /// Passthrough to [OTelAPI.attributesFromSemanticMap] for symmetry with
1167 : /// the [attributesFromMap] convenience.
1168 0 : static Attributes attributesFromSemanticMap(
1169 : Map<OTelSemantic, Object> semanticMap,
1170 : ) {
1171 0 : return OTelAPI.attributesFromSemanticMap(semanticMap);
1172 : }
1173 :
1174 : /// Like [attributesFromSemanticMap], but parameterized on a single
1175 : /// concrete semconv enum [E]. The expected key type is concrete at
1176 : /// the call site, so Dart 3.10 static dot-shorthand can drop the
1177 : /// enum prefix on every entry:
1178 : ///
1179 : /// ```dart
1180 : /// // Today and forever:
1181 : /// OTel.attributesOf<Http>({
1182 : /// Http.requestMethod: 'GET',
1183 : /// Http.responseStatusCode: 200,
1184 : /// });
1185 : ///
1186 : /// // With Dart 3.10+ static dot-shorthand enabled:
1187 : /// OTel.attributesOf<Http>({
1188 : /// .requestMethod: 'GET',
1189 : /// .responseStatusCode: 200,
1190 : /// });
1191 : /// ```
1192 : ///
1193 : /// **Mix and match**: each typed-enum map can be spread into a wider
1194 : /// `Map<OTelSemantic, Object>` literal, which is exactly what
1195 : /// [attributesFromSemanticMap] takes — so combining HTTP + Database
1196 : /// keys with full dot-shorthand looks like:
1197 : ///
1198 : /// ```dart
1199 : /// OTel.attributesFromSemanticMap({
1200 : /// ...<Http, Object>{
1201 : /// Http.requestMethod: 'GET',
1202 : /// Http.responseStatusCode: 200,
1203 : /// },
1204 : /// ...<Database, Object>{
1205 : /// Database.dbSystemName: DbSystem.postgresql.value,
1206 : /// },
1207 : /// });
1208 : /// ```
1209 : ///
1210 : /// Passthrough to [OTelAPI.attributesOf].
1211 0 : static Attributes attributesOf<E extends OTelSemantic>(
1212 : Map<E, Object> typedMap,
1213 : ) =>
1214 0 : OTelAPI.attributesOf<E>(typedMap);
1215 :
1216 : /// Creates an Attributes collection from a list of Attribute objects.
1217 : ///
1218 : /// @param attributeList List of Attribute objects
1219 : /// @return A new Attributes collection
1220 6 : static Attributes attributesFromList(List<Attribute> attributeList) {
1221 6 : _getAndCacheOtelFactory();
1222 6 : return _otelFactory!.attributesFromList(attributeList);
1223 : }
1224 :
1225 : /// Creates a TraceState with the specified entries.
1226 : ///
1227 : /// TraceState carries vendor-specific trace identification data across systems.
1228 : ///
1229 : /// @param entries Optional map of key-value pairs for the trace state
1230 : /// @return A new TraceState instance
1231 8 : static TraceState traceState(Map<String, String>? entries) {
1232 8 : _getAndCacheOtelFactory();
1233 8 : return _otelFactory!.traceState(entries);
1234 : }
1235 :
1236 : /// Creates TraceFlags with the specified flags.
1237 : ///
1238 : /// TraceFlags are used to encode bit field flags in the trace context.
1239 : /// The most commonly used flag is SAMPLED_FLAG, which indicates
1240 : /// that the trace should be sampled.
1241 : ///
1242 : /// @param flags Optional flags value (default: NONE_FLAG)
1243 : /// @return A new TraceFlags instance
1244 52 : static TraceFlags traceFlags([int? flags]) {
1245 52 : _getAndCacheOtelFactory();
1246 52 : return _otelFactory!.traceFlags(flags ?? TraceFlags.NONE_FLAG);
1247 : }
1248 :
1249 : /// Generates a new random TraceId.
1250 : ///
1251 : /// @return A new random TraceId
1252 57 : static TraceId traceId() {
1253 114 : return traceIdOf(IdGenerator.generateTraceId());
1254 : }
1255 :
1256 : /// Creates a TraceId from the specified bytes.
1257 : ///
1258 : /// @param traceId The bytes for the trace ID (must be exactly 16 bytes)
1259 : /// @return A new TraceId instance
1260 : /// @throws ArgumentError if traceId is not exactly 16 bytes
1261 58 : static TraceId traceIdOf(Uint8List traceId) {
1262 58 : _getAndCacheOtelFactory();
1263 116 : if (traceId.length != TraceId.traceIdLength) {
1264 1 : throw ArgumentError(
1265 2 : 'Trace ID must be exactly ${TraceId.traceIdLength} bytes, got ${traceId.length} bytes',
1266 : );
1267 : }
1268 58 : return OTelFactory.otelFactory!.traceId(traceId);
1269 : }
1270 :
1271 : /// Creates a TraceId from a hex string.
1272 : ///
1273 : /// @param hexString Hexadecimal representation of the trace ID
1274 : /// @return A new TraceId instance
1275 13 : static TraceId traceIdFrom(String hexString) {
1276 13 : return OTelAPI.traceIdFrom(hexString);
1277 : }
1278 :
1279 : /// Creates an invalid TraceId (all zeros).
1280 : ///
1281 : /// @return An invalid TraceId instance
1282 2 : static TraceId traceIdInvalid() {
1283 4 : return traceIdOf(TraceId.invalidTraceIdBytes);
1284 : }
1285 :
1286 : /// Generates a new random SpanId.
1287 : ///
1288 : /// @return A new random SpanId
1289 55 : static SpanId spanId() {
1290 110 : return spanIdOf(IdGenerator.generateSpanId());
1291 : }
1292 :
1293 : /// Creates a SpanId from the specified bytes.
1294 : ///
1295 : /// @param spanId The bytes for the span ID (must be exactly 8 bytes)
1296 : /// @return A new SpanId instance
1297 : /// @throws ArgumentError if spanId is not exactly 8 bytes
1298 65 : static SpanId spanIdOf(Uint8List spanId) {
1299 65 : _getAndCacheOtelFactory();
1300 130 : if (spanId.length != 8) {
1301 1 : throw ArgumentError(
1302 2 : 'Span ID must be exactly 8 bytes, got ${spanId.length} bytes',
1303 : );
1304 : }
1305 65 : return _otelFactory!.spanId(spanId);
1306 : }
1307 :
1308 : /// Creates a SpanId from a hex string.
1309 : ///
1310 : /// @param hexString Hexadecimal representation of the span ID
1311 : /// @return A new SpanId instance
1312 13 : static SpanId spanIdFrom(String hexString) {
1313 13 : return OTelAPI.spanIdFrom(hexString);
1314 : }
1315 :
1316 : /// Creates an invalid SpanId (all zeros).
1317 : ///
1318 : /// @return An invalid SpanId instance
1319 61 : static SpanId spanIdInvalid() {
1320 122 : return spanIdOf(SpanId.invalidSpanIdBytes);
1321 : }
1322 :
1323 : /// Creates a SpanLink with the specified SpanContext and optional attributes.
1324 : ///
1325 : /// SpanLinks are used to associate spans that may be causally related
1326 : /// but not via a parent-child relationship.
1327 : ///
1328 : /// @param spanContext The SpanContext to link to
1329 : /// @param attributes Optional attributes to associate with the link
1330 : /// @return A new SpanLink instance
1331 5 : static SpanLink spanLink(SpanContext spanContext, {Attributes? attributes}) {
1332 5 : _getAndCacheOtelFactory();
1333 5 : return _otelFactory!.spanLink(spanContext, attributes: attributes);
1334 : }
1335 :
1336 : /// Retrieves and caches the OTelFactory instance.
1337 : ///
1338 : /// @return The OTelFactory instance
1339 : /// @throws StateError if initialize() has not been called
1340 128 : static OTelFactory _getAndCacheOtelFactory() {
1341 : if (_otelFactory != null) {
1342 : return _otelFactory!;
1343 : }
1344 : final installed = OTelFactory.otelFactory;
1345 : // An installed factory that is not SDK-capable is the API's auto-installed
1346 : // no-op (API-only code ran first) — the SDK still isn't initialized, and
1347 : // saying so beats the `as` TypeError the cast would produce (#50).
1348 2 : if (installed == null || installed is! OTelSDKFactory) {
1349 3 : throw StateError('OTel.initialize() must be called first.');
1350 : }
1351 : return _otelFactory = installed;
1352 : }
1353 :
1354 : /// Initializes logging based on environment variables.
1355 : ///
1356 : /// This can be called separately from initialize(), but initialize() will
1357 : /// call it automatically if not already done.
1358 128 : static void initializeLogging() {
1359 : // Initialize log settings from environment variables
1360 128 : OTelEnv.initializeLogging();
1361 :
1362 128 : if (OTelLog.isDebug()) {
1363 124 : OTelLog.debug('OTel logging initialized');
1364 : }
1365 : }
1366 :
1367 : /// Flushes and shuts down trace and metric providers,
1368 : /// processors and exporters. Typically called from [OTel.shutdown]
1369 127 : static Future<void> shutdown() async {
1370 : // Shutdown any tracer providers to clean up span processors
1371 : try {
1372 127 : final tracerProviders = OTel.tracerProviders();
1373 253 : for (final tracerProvider in tracerProviders) {
1374 126 : if (OTelLog.isDebug()) {
1375 125 : OTelLog.debug('OTel: Shutting down tracer providers');
1376 : }
1377 126 : if (tracerProvider is TracerProvider) {
1378 : try {
1379 126 : await tracerProvider.forceFlush();
1380 126 : if (OTelLog.isDebug()) {
1381 125 : OTelLog.debug('OTel: Tracer provider flush complete');
1382 : }
1383 : } catch (e) {
1384 0 : if (OTelLog.isDebug()) {
1385 0 : OTelLog.debug('OTel: Error during tracer provider flush: $e');
1386 : }
1387 : }
1388 : }
1389 : try {
1390 126 : await tracerProvider.shutdown();
1391 126 : if (OTelLog.isDebug()) {
1392 125 : OTelLog.debug('OTel: Tracer provider shutdown complete');
1393 : }
1394 : } catch (e) {
1395 0 : if (OTelLog.isDebug()) {
1396 0 : OTelLog.debug('OTel: Error during tracer provider shutdown: $e');
1397 : }
1398 : }
1399 : }
1400 : } catch (e) {
1401 0 : if (OTelLog.isDebug()) {
1402 0 : OTelLog.debug('OTel: Error accessing tracer provider: $e');
1403 : }
1404 : }
1405 :
1406 : // Shutdown meter providers to clean up metric readers and exporters
1407 127 : final meterProviders = OTel.meterProviders();
1408 246 : for (var meterProvider in meterProviders) {
1409 : try {
1410 119 : if (OTelLog.isDebug()) {
1411 118 : OTelLog.debug('OTel: Shutting down meter provider');
1412 : }
1413 119 : await meterProvider.shutdown();
1414 119 : if (OTelLog.isDebug()) {
1415 118 : OTelLog.debug('OTel: Meter provider shutdown complete');
1416 : }
1417 : } catch (e) {
1418 0 : if (OTelLog.isDebug()) {
1419 0 : OTelLog.debug('OTel: Error during meter provider shutdown: $e');
1420 : }
1421 : }
1422 : }
1423 :
1424 : // Shut down all LoggerProviders — default plus any named ones added
1425 : // via `OTel.addLoggerProvider(name)`. Without this, each provider's
1426 : // BatchLogRecordProcessor `Timer.periodic` keeps the Dart isolate
1427 : // alive after `main()` returns, so short-lived CLI binaries hang
1428 : // indefinitely after `await OTel.shutdown()` (issue #33).
1429 : //
1430 : // Note: enumeration relies on `OTelAPI.loggerProviders()`, added in
1431 : // API `1.0.0-beta.4`. Earlier versions only had access to the default
1432 : // provider, which is why beta.1 of this SDK left this as a documented
1433 : // gap — closed here.
1434 : try {
1435 127 : final loggerProviders = OTelAPI.loggerProviders();
1436 245 : for (final loggerProvider in loggerProviders) {
1437 : try {
1438 118 : if (OTelLog.isDebug()) {
1439 117 : OTelLog.debug('OTel: Shutting down logger provider');
1440 : }
1441 118 : await loggerProvider.shutdown();
1442 118 : if (OTelLog.isDebug()) {
1443 117 : OTelLog.debug('OTel: Logger provider shutdown complete');
1444 : }
1445 : } catch (e) {
1446 0 : if (OTelLog.isDebug()) {
1447 0 : OTelLog.debug('OTel: Error during logger provider shutdown: $e');
1448 : }
1449 : }
1450 : }
1451 : } catch (e) {
1452 0 : if (OTelLog.isDebug()) {
1453 0 : OTelLog.debug('OTel: Error accessing logger providers: $e');
1454 : }
1455 : }
1456 : }
1457 :
1458 : /// Resets the OTel state for testing purposes.
1459 : ///
1460 : /// This method should only be used in tests to reset the state between test runs.
1461 : /// It shuts down all tracer and meter providers and resets all static fields.
1462 : ///
1463 : /// @return A Future that completes when the reset is done
1464 127 : @visibleForTesting
1465 : static Future<void> reset() async {
1466 252 : if (OTelLog.isDebug()) OTelLog.debug('OTel: Resetting state');
1467 :
1468 127 : await shutdown();
1469 :
1470 : // Reset all static fields
1471 : _otelFactory = null;
1472 : _defaultSampler = null;
1473 : _defaultSpanExceptionOptions = null;
1474 : _defaultTimeProvider = null;
1475 : defaultResource = null;
1476 : dartasticApiKey = null;
1477 :
1478 : // Reset print interception state
1479 : if (_logBridge != null) {
1480 2 : DartLogBridge.uninstall();
1481 : }
1482 : _logBridge = null;
1483 : _printInterceptionZoneSpec = null;
1484 : _logPrintEnabled = false;
1485 : _logPrintLoggerName = 'dart.print';
1486 252 : if (OTelLog.isDebug()) OTelLog.debug('OTel: Reset static fields');
1487 :
1488 : // Reset API state
1489 : try {
1490 : // ignore: invalid_use_of_visible_for_testing_member
1491 127 : OTelAPI.reset();
1492 252 : if (OTelLog.isDebug()) OTelLog.debug('OTel: Reset OTelAPI');
1493 : } catch (e) {
1494 0 : if (OTelLog.isDebug()) OTelLog.debug('OTel: Error resetting OTelAPI: $e');
1495 : }
1496 :
1497 : // Reset OTelFactory
1498 : OTelFactory.otelFactory = null;
1499 252 : if (OTelLog.isDebug()) OTelLog.debug('OTel: Reset OTelFactory');
1500 :
1501 252 : if (OTelLog.isDebug()) OTelLog.debug('OTel: Cleared test environment');
1502 :
1503 : // Add a short delay to ensure resources are released
1504 127 : await Future<void>.delayed(const Duration(milliseconds: 250));
1505 252 : if (OTelLog.isDebug()) OTelLog.debug('OTel: Reset complete');
1506 : }
1507 :
1508 : /// Creates a new InstrumentationScope.
1509 : ///
1510 : /// [name] is required and represents the instrumentation scope name (e.g. 'io.opentelemetry.contrib.mongodb')
1511 : /// [version] is optional and specifies the version of the instrumentation scope, defaults to '1.0.0'
1512 : /// [schemaUrl] is optional and specifies the Schema URL
1513 : /// [attributes] is optional and specifies instrumentation scope attributes
1514 20 : static InstrumentationScope instrumentationScope({
1515 : required String name,
1516 : String version = '1.0.0',
1517 : String? schemaUrl,
1518 : Attributes? attributes,
1519 : }) {
1520 20 : return OTelAPI.instrumentationScope(
1521 : name: name,
1522 : version: version,
1523 : schemaUrl: schemaUrl,
1524 : attributes: attributes,
1525 : );
1526 : }
1527 : }
|