Skip to content

Commit 4d5faa3

Browse files
authored
feat: Add basic flutter plugin. (#233)
## Summary Adds a basic observability plugin for flutter. This doesn't have any configuration and only support basic tracing. There are wrappers for spans, attributes, and the span kind because there isn't an official open telemetry implementation for dart. We don't want to encourage importing the API package as is supported in most other SDKs. We may want to consider vendoring the otel code and keeping it internal to limit the potential breakage if we change implementations. ## How did you test this change? Manual testing. Unit tests. ## Are there any deployment considerations? <!-- Backend - Do we need to consider migrations or backfilling data? -->
1 parent a2e192c commit 4d5faa3

14 files changed

+889
-172
lines changed
Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
/// A Calculator.
2-
class Calculator {
3-
/// Returns [value] plus 1.
4-
int addOne(int value) => value + 1;
5-
}
1+
export 'src/plugin/observability_plugin.dart' show ObservabilityPlugin;
2+
export 'src/observe.dart' show Observe;
3+
export 'src/api/attribute.dart' show Attribute;
4+
export 'src/api/span.dart' show Span;
5+
export 'src/api/span_kind.dart' show SpanKind;
6+
export 'src/api/span_status_code.dart' show SpanStatusCode;
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
/// Base class representing an open telemetry attribute.
2+
///
3+
/// Attributes may be a boolean, integer, double, string, list of booleans,
4+
/// list of integers, list of doubles, or list of strings.
5+
///
6+
/// There is a type-safe attribute constructor for each attribute type, or
7+
/// alternatively [Attribute.fromDynamic] can be used. If the dynamic attribute
8+
/// is not a compatible type, then the attribute will not be added.
9+
/// ```dart
10+
/// span.setAttribute('my-integer', IntAttribute(42));
11+
/// span.setAttribute('my-string', StringAttribute('test'));
12+
/// span.setAttribute('from-dynamic-value', Attribute.fromDynamic(value));
13+
/// ```
14+
sealed class Attribute {
15+
factory Attribute.fromDynamic(dynamic value) {
16+
if (value is bool) {
17+
return BooleanAttribute(value);
18+
}
19+
if (value is int) {
20+
return IntAttribute(value);
21+
}
22+
if (value is double) {
23+
return DoubleAttribute(value);
24+
}
25+
if (value is String) {
26+
return StringAttribute(value);
27+
}
28+
if (value is List<String>) {
29+
return StringListAttribute(value);
30+
}
31+
if (value is List<double>) {
32+
return DoubleListAttribute(value);
33+
}
34+
if (value is List<int>) {
35+
return IntListAttribute(value);
36+
}
37+
if (value is List<bool>) {
38+
return BooleanListAttribute(value);
39+
}
40+
return InvalidAttribute._internal();
41+
}
42+
const Attribute._internal();
43+
}
44+
45+
/// When using [fromDynamic] it is possible to get a value that cannot be
46+
/// represented as an attribute. When this happens an [InvalidAttribute] will
47+
/// be created. This attribute will be omitted from otel data.
48+
final class InvalidAttribute extends Attribute {
49+
const InvalidAttribute._internal() : super._internal();
50+
51+
@override
52+
String toString() => 'InvalidAttribute()';
53+
}
54+
55+
// Implementation note: Most of the constructors are non-const because the list
56+
// versions cannot be const. It is a bit safer to make them non-const, because
57+
// if we made them const, and later they needed to not be const, then removing
58+
// the const would be a breaking change.
59+
// The constructor for the InvalidAttribute and the base Attribute are internal
60+
// only, so they are safe to make const.
61+
62+
/// An integer attribute.
63+
final class IntAttribute extends Attribute {
64+
final int value;
65+
IntAttribute(this.value) : super._internal();
66+
67+
@override
68+
String toString() => 'IntAttribute{value: $value}';
69+
}
70+
71+
/// A double attribute.
72+
final class DoubleAttribute extends Attribute {
73+
final double value;
74+
75+
DoubleAttribute(this.value) : super._internal();
76+
77+
@override
78+
String toString() => 'DoubleAttribute{value: $value}';
79+
}
80+
81+
/// A boolean attribute.
82+
final class BooleanAttribute extends Attribute {
83+
final bool value;
84+
85+
BooleanAttribute(this.value) : super._internal();
86+
87+
@override
88+
String toString() => 'BooleanAttribute{value: $value}';
89+
}
90+
91+
/// A string attribute.
92+
final class StringAttribute extends Attribute {
93+
final String value;
94+
95+
StringAttribute(this.value) : super._internal();
96+
97+
@override
98+
String toString() => 'StringAttribute{value: $value}';
99+
}
100+
101+
/// An attribute containing a list of strings.
102+
final class StringListAttribute extends Attribute {
103+
late final List<String> value;
104+
105+
StringListAttribute(List<String> input)
106+
: value = List.unmodifiable(input),
107+
super._internal();
108+
109+
@override
110+
String toString() => 'StringListAttribute{value: $value}';
111+
}
112+
113+
/// An attribute containing a list of doubles.
114+
final class DoubleListAttribute extends Attribute {
115+
late final List<double> value;
116+
117+
DoubleListAttribute(List<double> input)
118+
: value = List.unmodifiable(input),
119+
super._internal();
120+
121+
@override
122+
String toString() => 'DoubleListAttribute{value: $value}';
123+
}
124+
125+
/// An attribute containing a list of integers.
126+
final class IntListAttribute extends Attribute {
127+
late final List<int> value;
128+
129+
IntListAttribute(List<int> input)
130+
: value = List.unmodifiable(input),
131+
super._internal();
132+
133+
@override
134+
String toString() => 'IntListAttribute{value: $value}';
135+
}
136+
137+
/// An attribute containing a list of booleans.
138+
final class BooleanListAttribute extends Attribute {
139+
late final List<bool> value;
140+
141+
BooleanListAttribute(List<bool> input)
142+
: value = List.unmodifiable(input),
143+
super._internal();
144+
145+
@override
146+
String toString() => 'BooleanListAttribute{value: $value}';
147+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import 'package:launchdarkly_flutter_observability/src/api/attribute.dart';
2+
import 'package:opentelemetry/api.dart' as otel;
3+
4+
import '../otel/conversions.dart';
5+
import 'span_status_code.dart';
6+
7+
/// Represents a single operation within a trace.
8+
final class Span {
9+
final otel.Span _innerSpan;
10+
11+
// The context token type is not exported from the opentelemetry package.
12+
final dynamic _contextToken;
13+
14+
Span._internal(this._innerSpan, this._contextToken);
15+
16+
void end() {
17+
otel.Context.detach(_contextToken);
18+
_innerSpan.end();
19+
}
20+
21+
/// Set an attribute on the span.
22+
///
23+
/// The attribute may be a boolean, integer, double, string, list of booleans,
24+
/// list of integers, list of doubles, or list of strings.
25+
///
26+
/// There is a type-safe attribute constructor for each attribute type, or
27+
/// alternatively [Attribute.fromDynamic] can be used. If the dynamic attribute
28+
/// is not a compatible type, then the attribute will not be added.
29+
///
30+
/// ```dart
31+
/// span.setAttribute('my-integer', IntAttribute(42));
32+
/// span.setAttribute('my-string', StringAttribute('test'));
33+
/// span.setAttribute('from-dynamic-value', Attribute.fromDynamic(value));
34+
/// ```
35+
void setAttribute(String name, Attribute attribute) {
36+
final otelAttribute = convertAttribute(name, attribute);
37+
if (otelAttribute != null) {
38+
_innerSpan.setAttribute(otelAttribute);
39+
}
40+
}
41+
42+
/// Set attributes on the span.
43+
/// For details about attributes refer to [setAttribute].
44+
void setAttributes(Map<String, Attribute> attributes) {
45+
_innerSpan.setAttributes(convertAttributes(attributes));
46+
}
47+
48+
/// Record information about an exception that happened during this span.
49+
void recordException(
50+
dynamic exception, {
51+
StackTrace stackTrace = StackTrace.empty,
52+
Map<String, Attribute>? attributes,
53+
}) {
54+
// The otel library supports an "escaped" attribute, but attribute is
55+
// deprecated and no longer recommended, so we aren't exporting it.
56+
_innerSpan.recordException(
57+
exception,
58+
stackTrace: stackTrace,
59+
attributes: convertAttributes(attributes),
60+
);
61+
}
62+
63+
/// Add an event to the span with the given attributes.
64+
void addEvent(String name, {Map<String, Attribute>? attributes}) {
65+
_innerSpan.addEvent(name, attributes: convertAttributes(attributes));
66+
}
67+
68+
void setStatus(SpanStatusCode status) {
69+
_innerSpan.setStatus(convertSpanStatus(status));
70+
}
71+
}
72+
73+
/// Wrap a span with a LaunchDarkly specific API type.
74+
///
75+
/// Not for export.
76+
Span wrapSpan(otel.Span span, dynamic token) {
77+
return Span._internal(span, token);
78+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/// The kind of the span.
2+
enum SpanKind {
3+
/// Default value. Indicates that the span represents an internal operation
4+
/// within an application, as opposed to an operations with remote parents or
5+
/// children.
6+
internal,
7+
8+
/// Indicates that the span describes a request to a remote service where the
9+
/// client awaits a response.
10+
client,
11+
12+
/// Indicates that the span covers server-side handling of a remote request
13+
/// while the client awaits a response.
14+
server,
15+
16+
/// Indicates that the span describes the initiation or scheduling of a local
17+
/// or remote operation.
18+
producer,
19+
20+
/// Indicates that the span represents the processing of an operation
21+
/// initiated by a producer, where the producer does not wait for the outcome.
22+
consumer,
23+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/// The status of the span.
2+
enum SpanStatusCode {
3+
/// The status has not been set. This is the default status.
4+
unset,
5+
6+
/// The operation the span represents encountered an error.
7+
error,
8+
9+
/// The operation the span represents completed successfully.
10+
ok,
11+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import 'package:launchdarkly_flutter_observability/src/api/attribute.dart';
2+
import 'package:opentelemetry/api.dart' as otel;
3+
import 'api/span.dart';
4+
import 'api/span_kind.dart';
5+
import 'otel/conversions.dart';
6+
7+
const _launchDarklyTracerName = 'launchdarkly-observability';
8+
9+
/// Singleton used to access observability features.
10+
final class Observe {
11+
/// Start a span with the given name and optional attributes.
12+
static Span startSpan(
13+
String name, {
14+
SpanKind kind = SpanKind.internal,
15+
Map<String, Attribute>? attributes,
16+
}) {
17+
final tracer = otel.globalTracerProvider.getTracer(_launchDarklyTracerName);
18+
final span = tracer.startSpan(
19+
name,
20+
kind: convertKind(kind),
21+
attributes: convertAttributes(attributes),
22+
);
23+
final token = otel.Context.attach(
24+
otel.contextWithSpan(otel.Context.current, span),
25+
);
26+
27+
return wrapSpan(span, token);
28+
}
29+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import 'package:launchdarkly_flutter_observability/src/api/span_kind.dart';
2+
import 'package:launchdarkly_flutter_observability/src/api/span_status_code.dart';
3+
import 'package:opentelemetry/api.dart' as otel;
4+
5+
import '../api/attribute.dart';
6+
7+
/// Not for export.
8+
otel.Attribute? convertAttribute(String name, Attribute attribute) {
9+
switch (attribute) {
10+
case IntAttribute():
11+
return otel.Attribute.fromInt(name, attribute.value);
12+
case DoubleAttribute():
13+
return otel.Attribute.fromDouble(name, attribute.value);
14+
case BooleanAttribute():
15+
return otel.Attribute.fromBoolean(name, attribute.value);
16+
case StringAttribute():
17+
return otel.Attribute.fromString(name, attribute.value);
18+
case StringListAttribute():
19+
return otel.Attribute.fromStringList(name, attribute.value);
20+
case DoubleListAttribute():
21+
return otel.Attribute.fromDoubleList(name, attribute.value);
22+
case IntListAttribute():
23+
return otel.Attribute.fromIntList(name, attribute.value);
24+
case BooleanListAttribute():
25+
return otel.Attribute.fromBooleanList(name, attribute.value);
26+
case InvalidAttribute():
27+
return null;
28+
}
29+
}
30+
31+
/// Not for export.
32+
List<otel.Attribute> convertAttributes(Map<String, Attribute>? attributes) {
33+
if (attributes == null) {
34+
return [];
35+
}
36+
final otelAttributes = <otel.Attribute>[];
37+
attributes.forEach((name, attribute) {
38+
final otelAttribute = convertAttribute(name, attribute);
39+
if (otelAttribute != null) {
40+
otelAttributes.add(otelAttribute);
41+
}
42+
});
43+
return otelAttributes;
44+
}
45+
46+
/// Not for export.
47+
otel.SpanKind convertKind(SpanKind kind) {
48+
switch (kind) {
49+
case SpanKind.server:
50+
return otel.SpanKind.server;
51+
case SpanKind.client:
52+
return otel.SpanKind.client;
53+
case SpanKind.producer:
54+
return otel.SpanKind.producer;
55+
case SpanKind.consumer:
56+
return otel.SpanKind.consumer;
57+
case SpanKind.internal:
58+
return otel.SpanKind.internal;
59+
}
60+
}
61+
62+
otel.StatusCode convertSpanStatus(SpanStatusCode status) {
63+
switch (status) {
64+
case SpanStatusCode.unset:
65+
return otel.StatusCode.unset;
66+
case SpanStatusCode.error:
67+
return otel.StatusCode.error;
68+
case SpanStatusCode.ok:
69+
return otel.StatusCode.ok;
70+
}
71+
}

0 commit comments

Comments
 (0)