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 : }
|