LCOV - code coverage report
Current view: top level - lib/src/logs/bridge - dart_log_bridge.dart (source / functions) Coverage Total Hit
Test: lcov.info Lines: 100.0 % 56 56
Test Date: 2026-07-11 13:35:19 Functions: - 0 0

            Line data    Source code
       1              : // Copyright The OpenTelemetry Authors
       2              : // SPDX-License-Identifier: Apache-2.0
       3              : 
       4              : import 'dart:async';
       5              : import 'dart:developer' as developer;
       6              : 
       7              : import 'package:dartastic_opentelemetry_api/dartastic_opentelemetry_api.dart';
       8              : 
       9              : import '../logger.dart';
      10              : import '../logger_provider.dart';
      11              : 
      12              : /// A bridge that intercepts Dart's built-in logging and sends it to OpenTelemetry.
      13              : ///
      14              : /// This bridge provides integration with Dart's `dart:developer` log function
      15              : /// by allowing you to configure a callback that converts Dart log calls to
      16              : /// OpenTelemetry log records.
      17              : ///
      18              : /// Note: Dart's built-in `print` function cannot be intercepted directly.
      19              : /// For `print` interception, you must replace `print` with a custom function
      20              : /// in your zone or use a logging framework.
      21              : ///
      22              : /// Example usage:
      23              : /// ```dart
      24              : /// // Initialize OTel and set up the bridge
      25              : /// await OTel.initialize(...);
      26              : /// final logger = OTel.logger('my-app');
      27              : ///
      28              : /// // Install the bridge
      29              : /// DartLogBridge.install(logger);
      30              : ///
      31              : /// // Now developer.log calls will be captured
      32              : /// developer.log('Hello from dart:developer', name: 'my-app');
      33              : /// ```
      34              : class DartLogBridge {
      35              :   /// The logger provider to use for creating loggers.
      36              :   final LoggerProvider _loggerProvider;
      37              : 
      38              :   /// Default logger name for logs without a name.
      39              :   final String _defaultLoggerName;
      40              : 
      41              :   /// Minimum severity level to capture.
      42              :   final Severity _minimumSeverity;
      43              : 
      44              :   /// Whether the bridge is active.
      45              :   bool _isActive = false;
      46              : 
      47              :   /// Re-entrancy guard to prevent infinite recursion when OTelLog.logFunction
      48              :   /// is set to print (e.g. via OTEL_LOG_LEVEL env var) and print is
      49              :   /// intercepted by the zone — OTelLog.debug → print → zone → log → OTelLog.debug → …
      50              :   bool _isLogging = false;
      51              : 
      52              :   /// Singleton instance for the installed bridge.
      53              :   static DartLogBridge? _instance;
      54              : 
      55              :   /// Creates a new DartLogBridge.
      56              :   ///
      57              :   /// @param loggerProvider The LoggerProvider to use for creating loggers
      58              :   /// @param defaultLoggerName Default name for logs without a name
      59              :   /// @param minimumSeverity Minimum severity level to capture
      60            3 :   DartLogBridge({
      61              :     required LoggerProvider loggerProvider,
      62              :     String defaultLoggerName = 'dart.developer',
      63              :     Severity minimumSeverity = Severity.DEBUG,
      64              :   })  : _loggerProvider = loggerProvider,
      65              :         _defaultLoggerName = defaultLoggerName,
      66              :         _minimumSeverity = minimumSeverity;
      67              : 
      68              :   /// Installs the bridge using a specific OTelLogger.
      69              :   ///
      70              :   /// This is a convenience method for simple use cases where you want
      71              :   /// all dart:developer logs to go to a single logger.
      72              :   ///
      73              :   /// @param logger The logger to send all logs to
      74              :   /// @param minimumSeverity Minimum severity level to capture
      75              :   /// @return The installed DartLogBridge instance
      76            3 :   static DartLogBridge install(
      77              :     OTelLogger logger, {
      78              :     Severity minimumSeverity = Severity.DEBUG,
      79              :   }) {
      80            3 :     _instance = DartLogBridge(
      81            3 :       loggerProvider: logger.provider,
      82            3 :       defaultLoggerName: logger.name,
      83              :       minimumSeverity: minimumSeverity,
      84              :     );
      85            3 :     _instance!.activate();
      86              :     return _instance!;
      87              :   }
      88              : 
      89              :   /// Installs the bridge using a LoggerProvider.
      90              :   ///
      91              :   /// This allows logs to be routed to different loggers based on the
      92              :   /// log name in dart:developer.log().
      93              :   ///
      94              :   /// @param loggerProvider The LoggerProvider to use
      95              :   /// @param defaultLoggerName Default name for logs without a name
      96              :   /// @param minimumSeverity Minimum severity level to capture
      97              :   /// @return The installed DartLogBridge instance
      98            1 :   static DartLogBridge installWithProvider(
      99              :     LoggerProvider loggerProvider, {
     100              :     String defaultLoggerName = 'dart.developer',
     101              :     Severity minimumSeverity = Severity.DEBUG,
     102              :   }) {
     103            1 :     _instance = DartLogBridge(
     104              :       loggerProvider: loggerProvider,
     105              :       defaultLoggerName: defaultLoggerName,
     106              :       minimumSeverity: minimumSeverity,
     107              :     );
     108            1 :     _instance!.activate();
     109              :     return _instance!;
     110              :   }
     111              : 
     112              :   /// Uninstalls the current bridge.
     113            3 :   static void uninstall() {
     114            3 :     _instance?.deactivate();
     115              :     _instance = null;
     116              :   }
     117              : 
     118              :   /// Gets the currently installed bridge, if any.
     119            1 :   static DartLogBridge? get current => _instance;
     120              : 
     121              :   /// Activates the bridge to start capturing logs.
     122            3 :   void activate() {
     123            3 :     _isActive = true;
     124            3 :     if (OTelLog.isDebug()) {
     125            3 :       OTelLog.debug('DartLogBridge: Activated');
     126              :     }
     127              :   }
     128              : 
     129              :   /// Deactivates the bridge to stop capturing logs.
     130            3 :   void deactivate() {
     131            3 :     _isActive = false;
     132            3 :     if (OTelLog.isDebug()) {
     133            3 :       OTelLog.debug('DartLogBridge: Deactivated');
     134              :     }
     135              :   }
     136              : 
     137              :   /// Whether the bridge is currently active.
     138            4 :   bool get isActive => _isActive;
     139              : 
     140              :   /// Logs a message using the bridge.
     141              :   ///
     142              :   /// This method is called by the zone handler when dart:developer.log is used.
     143              :   ///
     144              :   /// @param message The log message
     145              :   /// @param time The timestamp
     146              :   /// @param sequenceNumber The sequence number
     147              :   /// @param level The log level (0-2000)
     148              :   /// @param name The log name (logger name)
     149              :   /// @param zone The zone where the log was made
     150              :   /// @param error The error object, if any
     151              :   /// @param stackTrace The stack trace, if any
     152            3 :   void log(
     153              :     String message, {
     154              :     DateTime? time,
     155              :     int? sequenceNumber,
     156              :     int level = 0,
     157              :     String? name,
     158              :     Object? error,
     159              :     StackTrace? stackTrace,
     160              :   }) {
     161            3 :     if (!_isActive) return;
     162              : 
     163              :     // Convert Dart log level to OTel Severity
     164              :     // Dart levels: 0 = finest/trace, 500 = fine/debug, 800 = config/info,
     165              :     // 900 = info, 1000 = warning, 2000 = severe
     166            3 :     final severity = _levelToSeverity(level);
     167              : 
     168              :     // Filter by minimum severity
     169           12 :     if (severity.severityNumber < _minimumSeverity.severityNumber) {
     170              :       return;
     171              :     }
     172              : 
     173              :     // Get the appropriate logger
     174            7 :     final loggerName = name?.isNotEmpty == true ? name! : _defaultLoggerName;
     175            6 :     final logger = _loggerProvider.getLogger(loggerName);
     176              : 
     177              :     // Build attributes
     178            3 :     final attributes = <Attribute>[];
     179              :     if (sequenceNumber != null) {
     180            2 :       attributes.add(OTelAPI.attributeInt('sequence_number', sequenceNumber));
     181              :     }
     182              :     if (error != null) {
     183            2 :       attributes.add(OTelAPI.attributeString(
     184            2 :           'exception.type', error.runtimeType.toString()));
     185              :       attributes
     186            3 :           .add(OTelAPI.attributeString('exception.message', error.toString()));
     187              :     }
     188              :     if (stackTrace != null) {
     189            2 :       attributes.add(OTelAPI.attributeString(
     190            1 :           'exception.stacktrace', stackTrace.toString()));
     191              :     }
     192              : 
     193              :     // Emit the log
     194            3 :     logger.emit(
     195              :       timeStamp: time,
     196              :       severityNumber: severity,
     197            3 :       severityText: severity.name,
     198              :       body: message,
     199              :       attributes:
     200            4 :           attributes.isNotEmpty ? OTelAPI.attributesFromList(attributes) : null,
     201              :     );
     202              :   }
     203              : 
     204              :   /// Converts a Dart log level to an OpenTelemetry Severity.
     205              :   ///
     206              :   /// Dart log levels (from logging package):
     207              :   /// - 0: ALL/finest (trace)
     208              :   /// - 300: FINEST
     209              :   /// - 400: FINER
     210              :   /// - 500: FINE (debug)
     211              :   /// - 700: CONFIG
     212              :   /// - 800: INFO
     213              :   /// - 900: WARNING
     214              :   /// - 1000: SEVERE (error)
     215              :   /// - 1200: SHOUT
     216              :   /// - 2000: OFF
     217            3 :   Severity _levelToSeverity(int level) {
     218            3 :     if (level < 300) {
     219              :       return Severity.TRACE;
     220            3 :     } else if (level < 500) {
     221              :       return Severity.TRACE2;
     222            3 :     } else if (level < 700) {
     223              :       return Severity.DEBUG;
     224            3 :     } else if (level < 800) {
     225              :       return Severity.DEBUG2;
     226            3 :     } else if (level < 900) {
     227              :       return Severity.INFO;
     228            1 :     } else if (level < 1000) {
     229              :       return Severity.WARN;
     230            1 :     } else if (level < 1200) {
     231              :       return Severity.ERROR;
     232              :     } else {
     233              :       return Severity.FATAL;
     234              :     }
     235              :   }
     236              : 
     237              :   /// Creates a zone specification that captures dart:developer logs.
     238              :   ///
     239              :   /// Use this to create a zone that automatically routes all
     240              :   /// dart:developer.log calls through the bridge.
     241              :   ///
     242              :   /// Example:
     243              :   /// ```dart
     244              :   /// final bridge = DartLogBridge.install(logger);
     245              :   /// runZoned(
     246              :   ///   () {
     247              :   ///     // Your app code here
     248              :   ///     developer.log('This will be captured', name: 'my-app');
     249              :   ///   },
     250              :   ///   zoneSpecification: bridge.createZoneSpecification(),
     251              :   /// );
     252              :   /// ```
     253            3 :   ZoneSpecification createZoneSpecification() {
     254            3 :     return ZoneSpecification(
     255            3 :       print: (Zone self, ZoneDelegate parent, Zone zone, String line) {
     256              :         // Capture print statements as INFO logs.
     257              :         // Guard against re-entrancy: if OTelLog.logFunction == print, the
     258              :         // log() call below can trigger OTelLog.debug → print → this handler
     259              :         // again, causing a stack overflow.
     260            6 :         if (_isActive && !_isLogging) {
     261            3 :           _isLogging = true;
     262              :           try {
     263            3 :             log(line, level: 800); // INFO level
     264              :           } finally {
     265            3 :             _isLogging = false;
     266              :           }
     267              :         }
     268              :         // Still call the original print
     269            3 :         parent.print(zone, line);
     270              :       },
     271              :     );
     272              :   }
     273              : }
     274              : 
     275              : /// Extension to easily use the logging bridge with dart:developer.
     276              : extension DartDeveloperLogBridge on developer.ServiceExtensionHandler {
     277              :   /// Helper to emit a log that will be captured by the bridge if installed.
     278            1 :   static void emitLog(
     279              :     String message, {
     280              :     DateTime? time,
     281              :     int? sequenceNumber,
     282              :     int level = 0,
     283              :     String name = '',
     284              :     Object? error,
     285              :     StackTrace? stackTrace,
     286              :   }) {
     287              :     // First, call the original dart:developer log
     288            1 :     developer.log(
     289              :       message,
     290              :       time: time,
     291              :       sequenceNumber: sequenceNumber,
     292              :       level: level,
     293              :       name: name,
     294              :       error: error,
     295              :       stackTrace: stackTrace,
     296              :     );
     297              : 
     298              :     // If bridge is installed, also log via OTel
     299            2 :     DartLogBridge.current?.log(
     300              :       message,
     301              :       time: time,
     302              :       sequenceNumber: sequenceNumber,
     303              :       level: level,
     304              :       name: name,
     305              :       error: error,
     306              :       stackTrace: stackTrace,
     307              :     );
     308              :   }
     309              : }
        

Generated by: LCOV version 2.0-1