Skip to content

Commit b0f3bb1

Browse files
authored
feat: Add debug print instrumentation. (#255)
## Summary Adds debugPrint instrumentation and zone configuration for normal print. Includes some additional refactoring to account for shutting down instrumentations. ## How did you test this change? Manual testing. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Adds configurable debugPrint/print instrumentation, introduces Observe/Otel shutdown and plugin registration, and updates example and exports accordingly. > > - **Instrumentation**: > - Add `DebugPrintInstrumentation` to capture `debugPrint` (configurable via new `InstrumentationConfig` and `DebugPrintSetting`). > - Extend `Instrumentation` interface with `dispose()`; make `LifecycleInstrumentation.dispose()` idempotent. > - Wire instrumentation into `ObservabilityPlugin` (new `instrumentation` param) and add `dispose()`. > - **Observe/Otel lifecycle**: > - Add `Observe.shutdown()` and plugin tracking; introduce `registerPlugin(...)`. > - Add `Observe.zoneSpecification()` to capture `print`. > - Refactor OTEL setup into `Otel` class with `setup()`/`shutdown()`. > - **Config/Exports**: > - Extend `ObservabilityConfig` to include `instrumentationConfig` and export `InstrumentationConfig`/`DebugPrintSetting`. > - **Example app**: > - Configure `ObservabilityPlugin` with `InstrumentationConfig(debugPrint: DebugPrintSetting.always())`. > - Use `zoneSpecification: Observe.zoneSpecification()` and add buttons to trigger `debugPrint`/`print`. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit cdaf620. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 6c0092a commit b0f3bb1

File tree

9 files changed

+247
-26
lines changed

9 files changed

+247
-26
lines changed

sdk/@launchdarkly/launchdarkly_flutter_observability/example/lib/main.dart

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ void main() {
2828
AutoEnvAttributes.enabled,
2929
plugins: [
3030
ObservabilityPlugin(
31+
instrumentation: InstrumentationConfig(
32+
debugPrint: DebugPrintSetting.always(),
33+
),
3134
applicationName: 'test-application',
3235
// This could be a semantic version or a git commit hash.
3336
// This demonstrates how to use an environment variable to set the hash.
@@ -57,6 +60,9 @@ void main() {
5760

5861
// Any additional default error handling.
5962
},
63+
// Used to intercept print statements. Generally print statements in
64+
// production are treated as a warning and this is not required.
65+
zoneSpecification: Observe.zoneSpecification(),
6066
);
6167
}
6268

@@ -197,6 +203,19 @@ class _MyHomePageState extends State<MyHomePage> {
197203
},
198204
child: const Text('Record error log with stack trace'),
199205
),
206+
ElevatedButton(
207+
onPressed: () {
208+
debugPrint("This is a message from debug print");
209+
},
210+
child: const Text('Call debugPrint'),
211+
),
212+
ElevatedButton(
213+
onPressed: () {
214+
// ignore: avoid_print
215+
print('This is a message from print');
216+
},
217+
child: const Text('Call print'),
218+
),
200219
const Text('You have pushed the button this many times:'),
201220
Text(
202221
'$_counter',

sdk/@launchdarkly/launchdarkly_flutter_observability/lib/launchdarkly_flutter_observability.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
export 'src/plugin/observability_plugin.dart' show ObservabilityPlugin;
2+
export 'src/plugin/observability_config.dart'
3+
show InstrumentationConfig, DebugPrintSetting;
24
export 'src/observe.dart' show Observe;
35
export 'src/api/attribute.dart'
46
show
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import 'package:flutter/foundation.dart';
2+
import 'package:launchdarkly_flutter_observability/src/instrumentation/instrumentation.dart';
3+
4+
import '../plugin/observability_config.dart';
5+
import '../observe.dart';
6+
7+
class DebugPrintInstrumentation implements Instrumentation {
8+
DebugPrintCallback? _originalCallback;
9+
10+
DebugPrintInstrumentation(InstrumentationConfig config) {
11+
switch (config.debugPrint) {
12+
case DebugPrintReleaseOnly():
13+
if (!kReleaseMode) {
14+
return;
15+
}
16+
case DebugPrintAlways():
17+
break;
18+
case DebugPrintDisabled():
19+
return;
20+
}
21+
_instrument();
22+
}
23+
24+
void _instrument() {
25+
_originalCallback = debugPrint;
26+
27+
debugPrint = (String? message, {int? wrapWidth}) {
28+
if (message != null) {
29+
Observe.recordLog(message, severity: 'debug');
30+
}
31+
};
32+
}
33+
34+
@override
35+
void dispose() {
36+
if (_originalCallback != null) {
37+
debugPrint = _originalCallback!;
38+
_originalCallback = null;
39+
}
40+
}
41+
}
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
/// Interfaces which instrumentations should implement.
2-
interface class Instrumentation {}
2+
abstract interface class Instrumentation {
3+
void dispose();
4+
}

sdk/@launchdarkly/launchdarkly_flutter_observability/lib/src/instrumentation/lifecycle/lifecycle_instrumentation.dart

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import 'platform/stub_lifecycle_listener.dart'
1111

1212
final class LifecycleInstrumentation implements Instrumentation {
1313
late final LDAppLifecycleListener _lifecycleListener;
14+
bool disposed = false;
1415

1516
LifecycleInstrumentation() {
1617
final initialState = SchedulerBinding.instance.lifecycleState;
@@ -52,7 +53,11 @@ final class LifecycleInstrumentation implements Instrumentation {
5253
..end();
5354
}
5455

56+
@override
5557
void dispose() {
56-
_lifecycleListener.close();
58+
if (!disposed) {
59+
_lifecycleListener.close();
60+
disposed = true;
61+
}
5762
}
5863
}

sdk/@launchdarkly/launchdarkly_flutter_observability/lib/src/observe.dart

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,25 @@
1-
import 'package:launchdarkly_flutter_observability/src/api/attribute.dart';
2-
import 'package:launchdarkly_flutter_observability/src/otel/log_convention.dart';
1+
import 'dart:async';
2+
33
import 'package:opentelemetry/api.dart' as otel;
4+
5+
import 'api/attribute.dart';
46
import 'api/span.dart';
57
import 'api/span_kind.dart';
68
import 'otel/conversions.dart';
9+
import 'otel/log_convention.dart';
10+
import 'otel/setup.dart';
11+
import 'plugin/observability_plugin.dart';
12+
import 'plugin/observability_config.dart';
713

814
const _launchDarklyTracerName = 'launchdarkly-observability';
915
const _launchDarklyErrorSpanName = 'launchdarkly.error';
1016
const _defaultLogLevel = 'info';
1117

1218
/// Singleton used to access observability features.
1319
final class Observe {
20+
static bool _shutdown = false;
21+
static final List<ObservabilityPlugin> _pluginInstances = [];
22+
1423
/// Start a span with the given name and optional attributes.
1524
static Span startSpan(
1625
String name, {
@@ -88,4 +97,36 @@ final class Observe {
8897
span.addEvent(LogConvention.eventName, attributes: combinedAttributes);
8998
span.end();
9099
}
100+
101+
/// Shutdown observability. Once shutdown observability cannot be restarted.
102+
static void shutdown() {
103+
if (!_shutdown) {
104+
Otel.shutdown();
105+
for (final plugin in _pluginInstances) {
106+
plugin.dispose();
107+
}
108+
_shutdown = true;
109+
}
110+
}
111+
112+
/// Get a zone specification which intercepts print statements.
113+
static ZoneSpecification zoneSpecification() {
114+
return ZoneSpecification(
115+
print: (Zone self, ZoneDelegate parent, Zone zone, String line) {
116+
parent.print(zone, line);
117+
Observe.recordLog(line);
118+
},
119+
);
120+
}
121+
}
122+
123+
/// Not for export.
124+
/// Registers a plugin with the singleton and sets up otel.
125+
registerPlugin(
126+
ObservabilityPlugin plugin,
127+
String credential,
128+
ObservabilityConfig config,
129+
) {
130+
Otel.setup(credential, config);
131+
Observe._pluginInstances.add(plugin);
91132
}

sdk/@launchdarkly/launchdarkly_flutter_observability/lib/src/otel/setup.dart

Lines changed: 36 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,26 +9,42 @@ import 'package:opentelemetry/sdk.dart'
99
const _highlightProjectIdAttr = 'highlight.project_id';
1010
const _tracesSuffix = '/v1/traces';
1111

12-
void setup(String sdkKey, ObservabilityConfig config) {
13-
final resourceAttributes = <Attribute>[
14-
Attribute.fromString(_highlightProjectIdAttr, sdkKey),
15-
];
16-
resourceAttributes.addAll(
17-
convertAttributes(
18-
ServiceConvention.getAttributes(
19-
serviceName: config.applicationName,
20-
serviceVersion: config.applicationVersion,
21-
),
22-
),
23-
);
24-
final tracerProvider = TracerProviderBase(
25-
processors: [
26-
BatchSpanProcessor(
27-
CollectorExporter(Uri.parse('${config.otlpEndpoint}$_tracesSuffix')),
12+
class Otel {
13+
static final List<TracerProviderBase> _tracerProviders = [];
14+
15+
static void setup(String sdkKey, ObservabilityConfig config) {
16+
// TODO: Log when otel is setup multiple times. It will work, but the
17+
// behavior may be confusing.
18+
19+
final resourceAttributes = <Attribute>[
20+
Attribute.fromString(_highlightProjectIdAttr, sdkKey),
21+
];
22+
resourceAttributes.addAll(
23+
convertAttributes(
24+
ServiceConvention.getAttributes(
25+
serviceName: config.applicationName,
26+
serviceVersion: config.applicationVersion,
27+
),
2828
),
29-
],
30-
resource: Resource(resourceAttributes),
31-
);
29+
);
30+
final tracerProvider = TracerProviderBase(
31+
processors: [
32+
BatchSpanProcessor(
33+
CollectorExporter(Uri.parse('${config.otlpEndpoint}$_tracesSuffix')),
34+
),
35+
],
36+
resource: Resource(resourceAttributes),
37+
);
38+
39+
_tracerProviders.add(tracerProvider);
40+
41+
registerGlobalTracerProvider(tracerProvider);
42+
}
3243

33-
registerGlobalTracerProvider(tracerProvider);
44+
static void shutdown() {
45+
for (final tracerProvider in _tracerProviders) {
46+
tracerProvider.shutdown();
47+
}
48+
_tracerProviders.clear();
49+
}
3450
}

sdk/@launchdarkly/launchdarkly_flutter_observability/lib/src/plugin/observability_config.dart

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import 'package:flutter/foundation.dart';
12
import 'package:launchdarkly_flutter_client_sdk/launchdarkly_flutter_client_sdk.dart';
23

34
const _defaultOtlpEndpoint =
@@ -9,6 +10,81 @@ const _defaultBackendUrl = 'https://pub.observability.app.launchdarkly.com';
910
// in the configuration. This centralizes the assignment of defaults versus
1011
// having them in each location that requires them.
1112

13+
// Implementation note: Use classes for instrumentation settings to allow them
14+
// to be extended in the future. For example a logging setting may start as
15+
// enabled/disabled, but could evolve to requiring filters or other advanced
16+
// configuration. Using classes allows us to extend this functionality in
17+
// a non-breaking way. If you want to enumerate settings use a final base
18+
// class to prevent a user from doing exhaustive matching. If you can represent
19+
// the state safely without a union, then just use factory constructors to
20+
// represent the potential options.
21+
22+
/// Configuration for the debugPrint instrumentation.
23+
final class DebugPrintSetting {
24+
const DebugPrintSetting._internal();
25+
26+
/// Only record debugPrint statements in a release configuration.
27+
///
28+
/// By convention most debug prints should be guarded by [kDebugMode], so
29+
/// very few should be present in release.
30+
///
31+
/// When this setting is enabled debugPrint statements will not be forwarded
32+
/// to the default handler in a release configuration. They will not appear
33+
/// in the flutter tools or console. They will still be in debug.
34+
factory DebugPrintSetting.releaseOnly() {
35+
return const DebugPrintReleaseOnly();
36+
}
37+
38+
/// Record debugPrint statements in any configuration.
39+
///
40+
/// Depending on the application this could result in a high volume of
41+
/// log messages.
42+
///
43+
/// When this setting is enabled debugPrint statements will not be forwarded
44+
/// to the default handler in any configuration. They will not appear
45+
/// in the flutter tools or console.
46+
factory DebugPrintSetting.always() {
47+
return const DebugPrintAlways();
48+
}
49+
50+
/// Do not instrument debugPrint.
51+
factory DebugPrintSetting.disabled() {
52+
return const DebugPrintDisabled();
53+
}
54+
}
55+
56+
/// Not for export.
57+
/// Should be created using the factories for DebugPrintSetting.
58+
final class DebugPrintReleaseOnly extends DebugPrintSetting {
59+
const DebugPrintReleaseOnly() : super._internal();
60+
}
61+
62+
/// Not for export.
63+
/// Should be created using the factories for DebugPrintSetting.
64+
final class DebugPrintAlways extends DebugPrintSetting {
65+
const DebugPrintAlways() : super._internal();
66+
}
67+
68+
/// Not for export.
69+
/// Should be created using the factories for DebugPrintSetting.
70+
final class DebugPrintDisabled extends DebugPrintSetting {
71+
const DebugPrintDisabled() : super._internal();
72+
}
73+
74+
/// Configuration for instrumentations.
75+
final class InstrumentationConfig {
76+
/// Configuration for the debug print instrumentation.
77+
///
78+
/// Defaults to [DebugPrintSetting.releaseOnly].
79+
final DebugPrintSetting debugPrint;
80+
81+
/// Construct an instrumentation configuration.
82+
///
83+
/// [InstrumentationConfig.debugPrint] Controls the the instrumentation
84+
/// of `debugPrint`.
85+
InstrumentationConfig({this.debugPrint = const DebugPrintReleaseOnly()});
86+
}
87+
1288
final class ObservabilityConfig {
1389
/// The configured OTLP endpoint.
1490
final String otlpEndpoint;
@@ -28,11 +104,15 @@ final class ObservabilityConfig {
28104
/// observability UI.
29105
final String? Function(LDContext context)? contextFriendlyName;
30106

107+
/// Configuration of instrumentations.
108+
final InstrumentationConfig instrumentationConfig;
109+
31110
ObservabilityConfig({
32111
this.applicationName,
33112
this.applicationVersion,
34113
required this.otlpEndpoint,
35114
required this.backendUrl,
115+
required this.instrumentationConfig,
36116
this.contextFriendlyName,
37117
});
38118
}
@@ -43,12 +123,14 @@ ObservabilityConfig configWithDefaults({
43123
String? otlpEndpoint,
44124
String? backendUrl,
45125
String? Function(LDContext context)? contextFriendlyName,
126+
InstrumentationConfig? instrumentationConfig,
46127
}) {
47128
return ObservabilityConfig(
48129
applicationName: applicationName,
49130
applicationVersion: applicationVersion,
50131
otlpEndpoint: otlpEndpoint ?? _defaultOtlpEndpoint,
51132
backendUrl: backendUrl ?? _defaultBackendUrl,
52133
contextFriendlyName: contextFriendlyName,
134+
instrumentationConfig: instrumentationConfig ?? InstrumentationConfig(),
53135
);
54136
}

0 commit comments

Comments
 (0)