Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ void main() {
AutoEnvAttributes.enabled,
plugins: [
ObservabilityPlugin(
instrumentation: InstrumentationConfig(
debugPrint: DebugPrintSetting.always(),
),
applicationName: 'test-application',
// This could be a semantic version or a git commit hash.
// This demonstrates how to use an environment variable to set the hash.
Expand Down Expand Up @@ -57,6 +60,9 @@ void main() {

// Any additional default error handling.
},
// Used to intercept print statements. Generally print statements in
// production are treated as a warning and this is not required.
zoneSpecification: Observe.zoneSpecification(),
);
}

Expand Down Expand Up @@ -197,6 +203,19 @@ class _MyHomePageState extends State<MyHomePage> {
},
child: const Text('Record error log with stack trace'),
),
ElevatedButton(
onPressed: () {
debugPrint("This is a message from debug print");
},
child: const Text('Call debugPrint'),
),
ElevatedButton(
onPressed: () {
// ignore: avoid_print
print('This is a message from print');
},
child: const Text('Call print'),
),
const Text('You have pushed the button this many times:'),
Text(
'$_counter',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export 'src/plugin/observability_plugin.dart' show ObservabilityPlugin;
export 'src/plugin/observability_config.dart'
show InstrumentationConfig, DebugPrintSetting;
export 'src/observe.dart' show Observe;
export 'src/api/attribute.dart'
show
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import 'package:flutter/foundation.dart';
import 'package:launchdarkly_flutter_observability/src/instrumentation/instrumentation.dart';

import '../plugin/observability_config.dart';
import '../observe.dart';

class DebugPrintInstrumentation implements Instrumentation {
DebugPrintCallback? _originalCallback;

DebugPrintInstrumentation(InstrumentationConfig config) {
switch (config.debugPrint) {
case DebugPrintReleaseOnly():
if (!kReleaseMode) {
return;
}
case DebugPrintAlways():
break;
case DebugPrintDisabled():
return;
}
_instrument();
}

void _instrument() {
_originalCallback = debugPrint;

debugPrint = (String? message, {int? wrapWidth}) {
if (message != null) {
Observe.recordLog(message, severity: 'debug');
}
};
}

@override
void dispose() {
if (_originalCallback != null) {
debugPrint = _originalCallback!;
_originalCallback = null;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
/// Interfaces which instrumentations should implement.
interface class Instrumentation {}
abstract interface class Instrumentation {
void dispose();
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import 'platform/stub_lifecycle_listener.dart'

final class LifecycleInstrumentation implements Instrumentation {
late final LDAppLifecycleListener _lifecycleListener;
bool disposed = false;

LifecycleInstrumentation() {
final initialState = SchedulerBinding.instance.lifecycleState;
Expand Down Expand Up @@ -52,7 +53,11 @@ final class LifecycleInstrumentation implements Instrumentation {
..end();
}

@override
void dispose() {
_lifecycleListener.close();
if (!disposed) {
_lifecycleListener.close();
disposed = true;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
import 'package:launchdarkly_flutter_observability/src/api/attribute.dart';
import 'package:launchdarkly_flutter_observability/src/otel/log_convention.dart';
import 'dart:async';

import 'package:opentelemetry/api.dart' as otel;

import 'api/attribute.dart';
import 'api/span.dart';
import 'api/span_kind.dart';
import 'otel/conversions.dart';
import 'otel/log_convention.dart';
import 'otel/setup.dart';
import 'plugin/observability_plugin.dart';
import 'plugin/observability_config.dart';

const _launchDarklyTracerName = 'launchdarkly-observability';
const _launchDarklyErrorSpanName = 'launchdarkly.error';
const _defaultLogLevel = 'info';

/// Singleton used to access observability features.
final class Observe {
static bool _shutdown = false;
static final List<ObservabilityPlugin> _pluginInstances = [];

/// Start a span with the given name and optional attributes.
static Span startSpan(
String name, {
Expand Down Expand Up @@ -88,4 +97,36 @@ final class Observe {
span.addEvent(LogConvention.eventName, attributes: combinedAttributes);
span.end();
}

/// Shutdown observability. Once shutdown observability cannot be restarted.
static void shutdown() {
if (!_shutdown) {
Otel.shutdown();
for (final plugin in _pluginInstances) {
plugin.dispose();
}
_shutdown = true;
}
}

/// Get a zone specification which intercepts print statements.
static ZoneSpecification zoneSpecification() {
return ZoneSpecification(
print: (Zone self, ZoneDelegate parent, Zone zone, String line) {
parent.print(zone, line);
Observe.recordLog(line);
},
);
}
}

/// Not for export.
/// Registers a plugin with the singleton and sets up otel.
registerPlugin(
ObservabilityPlugin plugin,
String credential,
ObservabilityConfig config,
) {
Otel.setup(credential, config);
Observe._pluginInstances.add(plugin);
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,26 +9,42 @@ import 'package:opentelemetry/sdk.dart'
const _highlightProjectIdAttr = 'highlight.project_id';
const _tracesSuffix = '/v1/traces';

void setup(String sdkKey, ObservabilityConfig config) {
final resourceAttributes = <Attribute>[
Attribute.fromString(_highlightProjectIdAttr, sdkKey),
];
resourceAttributes.addAll(
convertAttributes(
ServiceConvention.getAttributes(
serviceName: config.applicationName,
serviceVersion: config.applicationVersion,
),
),
);
final tracerProvider = TracerProviderBase(
processors: [
BatchSpanProcessor(
CollectorExporter(Uri.parse('${config.otlpEndpoint}$_tracesSuffix')),
class Otel {
static final List<TracerProviderBase> _tracerProviders = [];

static void setup(String sdkKey, ObservabilityConfig config) {
// TODO: Log when otel is setup multiple times. It will work, but the
// behavior may be confusing.

final resourceAttributes = <Attribute>[
Attribute.fromString(_highlightProjectIdAttr, sdkKey),
];
resourceAttributes.addAll(
convertAttributes(
ServiceConvention.getAttributes(
serviceName: config.applicationName,
serviceVersion: config.applicationVersion,
),
),
],
resource: Resource(resourceAttributes),
);
);
final tracerProvider = TracerProviderBase(
processors: [
BatchSpanProcessor(
CollectorExporter(Uri.parse('${config.otlpEndpoint}$_tracesSuffix')),
),
],
resource: Resource(resourceAttributes),
);

_tracerProviders.add(tracerProvider);

registerGlobalTracerProvider(tracerProvider);
}

registerGlobalTracerProvider(tracerProvider);
static void shutdown() {
for (final tracerProvider in _tracerProviders) {
tracerProvider.shutdown();
}
_tracerProviders.clear();
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'package:flutter/foundation.dart';
import 'package:launchdarkly_flutter_client_sdk/launchdarkly_flutter_client_sdk.dart';

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

// Implementation note: Use classes for instrumentation settings to allow them
// to be extended in the future. For example a logging setting may start as
// enabled/disabled, but could evolve to requiring filters or other advanced
// configuration. Using classes allows us to extend this functionality in
// a non-breaking way. If you want to enumerate settings use a final base
// class to prevent a user from doing exhaustive matching. If you can represent
// the state safely without a union, then just use factory constructors to
// represent the potential options.

/// Configuration for the debugPrint instrumentation.
final class DebugPrintSetting {
const DebugPrintSetting._internal();

/// Only record debugPrint statements in a release configuration.
///
/// By convention most debug prints should be guarded by [kDebugMode], so
/// very few should be present in release.
///
/// When this setting is enabled debugPrint statements will not be forwarded
/// to the default handler in a release configuration. They will not appear
/// in the flutter tools or console. They will still be in debug.
factory DebugPrintSetting.releaseOnly() {
return const DebugPrintReleaseOnly();
}

/// Record debugPrint statements in any configuration.
///
/// Depending on the application this could result in a high volume of
/// log messages.
///
/// When this setting is enabled debugPrint statements will not be forwarded
/// to the default handler in any configuration. They will not appear
/// in the flutter tools or console.
factory DebugPrintSetting.always() {
return const DebugPrintAlways();
}

/// Do not instrument debugPrint.
factory DebugPrintSetting.disabled() {
return const DebugPrintDisabled();
}
}

/// Not for export.
/// Should be created using the factories for DebugPrintSetting.
final class DebugPrintReleaseOnly extends DebugPrintSetting {
const DebugPrintReleaseOnly() : super._internal();
}

/// Not for export.
/// Should be created using the factories for DebugPrintSetting.
final class DebugPrintAlways extends DebugPrintSetting {
const DebugPrintAlways() : super._internal();
}

/// Not for export.
/// Should be created using the factories for DebugPrintSetting.
final class DebugPrintDisabled extends DebugPrintSetting {
const DebugPrintDisabled() : super._internal();
}

/// Configuration for instrumentations.
final class InstrumentationConfig {
/// Configuration for the debug print instrumentation.
///
/// Defaults to [DebugPrintSetting.releaseOnly].
final DebugPrintSetting debugPrint;

/// Construct an instrumentation configuration.
///
/// [InstrumentationConfig.debugPrint] Controls the the instrumentation
/// of `debugPrint`.
InstrumentationConfig({this.debugPrint = const DebugPrintReleaseOnly()});
}

final class ObservabilityConfig {
/// The configured OTLP endpoint.
final String otlpEndpoint;
Expand All @@ -28,11 +104,15 @@ final class ObservabilityConfig {
/// observability UI.
final String? Function(LDContext context)? contextFriendlyName;

/// Configuration of instrumentations.
final InstrumentationConfig instrumentationConfig;

ObservabilityConfig({
this.applicationName,
this.applicationVersion,
required this.otlpEndpoint,
required this.backendUrl,
required this.instrumentationConfig,
this.contextFriendlyName,
});
}
Expand All @@ -43,12 +123,14 @@ ObservabilityConfig configWithDefaults({
String? otlpEndpoint,
String? backendUrl,
String? Function(LDContext context)? contextFriendlyName,
InstrumentationConfig? instrumentationConfig,
}) {
return ObservabilityConfig(
applicationName: applicationName,
applicationVersion: applicationVersion,
otlpEndpoint: otlpEndpoint ?? _defaultOtlpEndpoint,
backendUrl: backendUrl ?? _defaultBackendUrl,
contextFriendlyName: contextFriendlyName,
instrumentationConfig: instrumentationConfig ?? InstrumentationConfig(),
);
}
Loading
Loading