Middleware Flutter OTel is an OpenTelemetry SDK for Flutter applications built on the Middleware OpenTelemetry SDK, providing comprehensive observability for Flutter applications across all platforms.
The Wondrous OpenTelemetry project instruments the Wondrous App for observability with Middleware Flutter OTel.
The main.dart and router.dart show how to set up your app with Middleware Flutter OpenTelemetry.
Flutterrific OpenTelemetry implements the OpenTelemetry specification for Flutter, enabling developers to collect traces, metrics, and logs from Flutter applications. OpenTelemetry is a vendor-neutral standard for observability and is the second most active Cloud Native Computing Foundation (CNCF) project, after Kubernetes.
- Report Errors: Get notified of errors in real time with structured context
- App Lifecycle: Track when users launch, pause, resume, and quit
- Watch Your Users: Understand navigation flows and improve conversion rates
- Get Metrics: Measure route load times, frame rates, and APDEX scores in production
- Structured Logs: Emit OTel Events for lifecycle, navigation, errors, and custom telemetry
- Future-Proof: OpenTelemetry is an industry standard with broad ecosystem support
- Vendor Neutral: Works with any OpenTelemetry-compatible backend (Grafana, Elastic, Datadog, etc.)
- Cross-Platform: Supports all Flutter platforms (Android, iOS, Web, Desktop)
- Performance: Designed for minimal overhead in production applications
- 🚀 Simple Integration: Get started with just a few lines of code
- 👣 Automatic Instrumentation: Navigation, app lifecycle, and user interaction tracking
- 📹 Session Replay: Lightweight Session replay of application.
- 📊 Performance Metrics: Web vitals, APDEX scores, and custom performance metrics
- 🧩 Widget Extensions: Easy-to-use extensions for widget-level observability
- 🐞 Error Tracking: Comprehensive error handling and reporting
- 📐 Standards Compliant: Full adherence to OpenTelemetry specification
- 🌐 Multi-Platform: Supports Android, iOS, Web, and Desktop platforms
- 💪 Context Propagation: Seamless trace correlation across async boundaries
- 🔧 Configurable Sampling: Multiple sampling strategies for cost optimization
- 🧷 Type-Safe Semantics: Strongly-typed semantic conventions
dependencies:
middleware_flutter_opentelemetry: ^1.0.0import 'package:flutter/material.dart';
import 'package:middleware_flutter_opentelemetry/middleware_flutter_opentelemetry.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await FlutterOTel.initialize(
serviceName: 'my-flutter-app',
middlewareAccountKey: '*****', // from the RUM Flutter installation page
endpoint: 'https://<account>.middleware.io',
);
runApp(MyApp());
}Everything is on by default, with no other code needed:
- Flutter and platform errors (
autoCaptureErrors, chaining any handlers you already installed; an error reported by both is recorded once) - taps, scrolls and swipes (
enableAutomaticUserInteractions) - app lifecycle and logs
- session replay on every platform: native on Android/iOS, the Dart recorder on web and desktop, which starts by itself after the first frame and captures the whole app
- on web, the full browser instrumentation (see Flutter Web Instrumentation)
Pass false to any of these options to turn it off. Flutter metrics (frame
timings, APDEX, ...) are the one opt-in: enableMetrics: true.
For GoRouter:
final router = GoRouter(
observers: [FlutterOTel.routeObserver],
routes: [ /* ... */ ],
);For standard Navigator:
MaterialApp(
navigatorObservers: [FlutterOTel.routeObserver],
// ...
);That's it. With these three steps, your app automatically emits traces, metrics, and log events for all lifecycle changes, navigation, and errors.
Every auto-instrumented event creates a short, independent span with its own trace ID. This avoids the problem of a single long-running trace spanning the entire app session.
// Access the tracer for custom spans
final tracer = FlutterOTel.tracer;
// Create a custom span
final span = tracer.startSpan('fetch_user_data',
kind: SpanKind.client,
attributes: {'user.id': userId}.toAttributes(),
);
try {
final result = await apiClient.getUser(userId);
span.setStatus(SpanStatusCode.Ok);
return result;
} catch (e, stackTrace) {
span.recordException(e, stackTrace: stackTrace);
span.setStatus(SpanStatusCode.Error, e.toString());
rethrow;
} finally {
span.end();
}The UITracer provides convenience methods:
| Method | Description |
|---|---|
startSpan() |
Start a span (must be ended manually) |
recordNavChange() |
Record a navigation change (starts and ends span) |
recordUserInteraction() |
Record a user interaction (starts and ends span) |
recordError() |
Record an error with stack trace |
recordPerformanceMetric() |
Record a performance measurement |
startNavigationChangeSpan() |
Start a navigation span (caller ends it) |
startAppLifecycleSpan() |
Start a lifecycle span (caller ends it) |
Metrics are automatically collected via OTelMetricsBridge:
| Metric | Type | Description |
|---|---|---|
flutter.frame.duration |
Histogram | Frame rendering times (ms) |
flutter.page.load_time |
Histogram | Page load times (ms) |
flutter.navigation.duration |
Histogram | Navigation transition times (ms) |
flutter.errors.count |
Counter | Error count |
flutter.interaction.response_time |
Histogram | User interaction response times (ms) |
flutter.paint.duration |
Histogram | Paint operation durations (ms) |
flutter.layout.shift_score |
Histogram | Layout shift scores |
flutter.apdex.score |
Gauge | APDEX performance index |
You can also record custom metrics:
// Using FlutterOTelMetrics
FlutterOTelMetrics.recordMetric(
name: 'my_custom_metric',
value: 42,
unit: 'ms',
metricType: 'histogram',
);
// Or directly with a Meter
final meter = FlutterOTel.meter(name: 'my-feature');
meter.createCounter(name: 'feature.usage', unit: '{count}')
.add(1, {'feature.name': 'dark_mode'}.toAttributes());The Log Signal emits structured OTel Events — log records with an EventName field that identifies the event type. This follows the emerging OTel client instrumentation standard.
UILogger wraps the SDK Logger with Flutter-specific convenience methods:
final logger = FlutterOTel.logger('my-feature');
// Standard log levels
logger.info('Feature loaded');
logger.warn('Cache miss');
logger.error('Failed to fetch data');
// Structured OTel Events
logger.emitEvent('user.action',
body: 'Button tapped',
attributes: {'button.id': 'submit'}.toAttributes(),
);
// Flutter error logging
FlutterError.onError = (details) {
logger.emitFlutterError(details);
};
// Lifecycle events (auto-emitted when enableAutoLogEvents is true)
logger.emitLifecycleEvent('resumed', previousState: 'paused');
// Navigation events (auto-emitted when enableAutoLogEvents is true)
logger.emitNavigationEvent('/details', fromRoute: '/home', action: 'push');When enableAutoLogEvents is true (the default), Flutterrific automatically emits structured log events for:
| Event Name | Trigger | Key Attributes |
|---|---|---|
device.app.lifecycle |
App lifecycle state change | app_lifecycle.state, app_lifecycle.previous_state, app_lifecycle.duration |
browser.navigation |
Route change | navigation.route.name, navigation.previous_route_name, navigation.action |
device.app.error |
FlutterOTel.reportError() |
error.type, error.message, exception.stacktrace |
Set enableAutoLogEvents: false in initialize() to disable automatic log events while keeping manual logger access.
Flutterrific uses strongly-typed semantic enums for all attribute keys, following OTel conventions.
These enums come from the standard OTel semantic conventions:
NavigationSemantics—navigation.route.name,navigation.action, etc.AppLifecycleSemantics—app_lifecycle.state,app_lifecycle.duration, etc.ErrorSemantics—error.type,error.message, etc.InteractionSemantics—interaction.type,interaction.target, etc.PerformanceSemantics—render.duration,frame.rate, etc.SessionViewSemantics—view.name,session.id, etc.UserSemantics—user.id,user.role, etc.
These enums are defined in FlutterSemantics and are proposed additions to the OTel client instrumentation spec. They cover areas not yet standardized:
FlutterErrorSemantics—error.context,error.widget,error.widget_context,error.locationFlutterPerformanceSemantics—perf.metric.name,perf.duration_msFlutterUISemantics—ui.typeFlutterScrollSemantics—scroll.positionFlutterRedirectSemantics—redirect.toFlutterLifecycleMetricSemantics—lifecycle.state(metric context)FlutterRouteMetricSemantics—route.name,route.action,navigation.from_route,navigation.to_routeFlutterEventNames—device.app.lifecycle,device.app.error,browser.navigation
await FlutterOTel.initialize(
// Required
serviceName: 'my-app',
// Optional — all have sensible defaults
appName: 'My App', // defaults to serviceName
serviceVersion: '1.0.0',
endpoint: 'https://collector:4317', // defaults to localhost:4317
// Traces
spanProcessor: null, // auto-creates BatchSpanProcessor
sampler: AlwaysOnSampler(),
flushTracesInterval: Duration(seconds: 30),
// Metrics
enableMetrics: false, // Flutter metrics are opt-in
metricExporter: null, // auto-creates platform-specific exporter
metricReader: null, // auto-creates PeriodicExportingMetricReader
// Logs
enableLogs: true,
logRecordExporter: null, // auto-creates platform-specific exporter
logRecordProcessor: null,
logPrint: false, // bridge Dart print() to OTel logs
enableAutoLogEvents: true, // auto-emit lifecycle/nav/error events
// Resources
resourceAttributes: null,
detectPlatformResources: true,
commonAttributesFunction: null, // called on every span creation
// Security
secure: true,
dartasticApiKey: null,
tenantId: null,
// Flutter web browser instrumentation (ignored on other platforms)
webInstrumentation: const WebInstrumentationOptions(),
);On Flutter web the SDK also instruments the browser, the same way the
Middleware browser SDK (@middleware.io/browser) does, with the same span
names and attributes, so Flutter web sessions show up in RUM like any other
web app. Everything is on by default; nothing changes on Android, iOS or
desktop.
| Option | Default | What it records |
|---|---|---|
documentLoad |
on | documentLoad trace from Navigation Timing (documentFetch + one span per resource), plus resources loaded later (CanvasKit, fonts, images) with resource.post_load=true |
network |
on | fetch and XMLHttpRequest spans (GET host/path, event.type=fetch|xhr, resource.* timings). Patched in the browser, so package:http, dio and Flutter's asset loading are all covered without wrapping clients |
captureNetworkHeaders |
off (or the initialize value) |
request/response headers on network spans; see Headers and response bodies |
captureResponseBodies |
off (or the initialize value) |
JSON / plain-text response bodies on network spans |
ignoreHeaders |
none | header names never captured (added to the initialize list) |
advanceNetworkCapture |
off | deprecated: turns on both captureNetworkHeaders and captureResponseBodies when they are unset |
tracePropagationTargets |
same origin | cross-origin URLs that get traceparent / b3 headers (their CORS policy must allow them) |
webVitals |
on | LCP, FCP, CLS, INP and TTFB with attribution |
longTask |
on | main-thread tasks over 50ms |
pageTracking |
on | pageview on URL changes (hash routes included), pageleave, pagehide; sets root.url / page.href / page.title on all spans |
errors |
on | uncaught JS errors, unhandled rejections, failed resource loads and console.error, with parsed JS stacks |
console |
on | console.log/info/warn/debug calls from JavaScript as logs (100/s limit). Dart print / debugPrint output is not captured here (so the SDK never records its own diagnostics); use logPrint for that |
websocket |
on | WebSocket connect / send / onmessage |
rageClick |
on | frustration.type=rage_click on taps (web; always on elsewhere) |
blockBotTraffic |
on | no telemetry for crawlers and headless browsers |
Taps (on every platform) are recorded the way the RUM heatmap reads them, as
the native Android/iOS SDKs do: event.type=tap, screen.name,
logical-pixel x/y/pageX/pageY with viewport.*, and a target_xpath
that is unique per control on a screen (the heatmap draws one point per
xpath). On web, network requests started within a second of a tap get
interaction.trace_id / interaction.span_id / interaction.name.
await FlutterOTel.initialize(
serviceName: 'my-web-app',
middlewareAccountKey: '<key>',
endpoint: 'https://<account>.middleware.io',
webInstrumentation: WebInstrumentationOptions(
tracePropagationTargets: [RegExp(r'api\.example\.com')],
),
);Outside the browser there is nothing to patch, so wrap the client your app
uses. Each request gets a GET host/path client span (event.type=xhr), the
same name the web SDK uses, and carries trace headers so your backend's spans
join the app's trace:
final client = http.Client().instrument(); // package:http
dio.addOTelInstrumentation(); // dioBy default both W3C (traceparent) and B3 (b3, x-b3-*) headers are sent.
To send only one:
final client = http.Client().instrument(
config: const HttpInstrumentationConfig(
tracePropagationFormat: TracePropagationFormat.b3, // or .w3c
),
);Use shouldInstrument: (url) => ... to skip URLs. Skipped requests get no
span and no headers. On web the browser instrumentation already covers every
client, so wrap only when !kIsWeb to avoid a second span per request.
Network spans carry method, URL, status, body sizes and timings. Headers and response bodies can contain credentials and personal data, so they are recorded only when you opt in, the same way as in the Middleware browser SDK:
await FlutterOTel.initialize(
// ...
captureNetworkHeaders: true, // request + response headers
captureResponseBodies: true, // JSON and text/plain response bodies
ignoreHeaders: {'Authorization', 'Cookie', 'Set-Cookie'},
);| Option | Default | What it records |
|---|---|---|
captureNetworkHeaders |
off | every request and response header as http.request.header.<name> / http.response.header.<name> (lowercase, - becomes _; repeated headers joined with , ). x-access-token is recorded as ******** |
captureResponseBodies |
off | http.response.body when the response Content-Type is application/json, *+json or text/plain. Capped at 64 KiB on Android/iOS/desktop (the app still reads the full body) and 8 KB on the web |
ignoreHeaders |
none | header names (case-insensitive) never recorded |
Request bodies are never recorded.
These apply to every platform: the automatic dart:io instrumentation on
Android, iOS and desktop, and the browser fetch/XHR instrumentation on the
web. On the web, WebInstrumentationOptions.captureNetworkHeaders /
captureResponseBodies override them when set. On the web, header capture also
reads Server-Timing trace links from responses.
On Android/iOS/desktop, HttpInstrumentationConfig.captureRequestHeaders /
captureResponseHeaders still record the capturedHeaders allowlist
(content-type, content-length, user-agent by default) when
captureNetworkHeaders is off. With captureNetworkHeaders on, every header
is recorded unless you set your own capturedHeaders, which then limits it.
Requests traced by a manual OTelHttpClient / OTelDioInterceptor keep that
wrapper's own HttpInstrumentationConfig behaviour (no body capture).
Flutterrific automatically selects the right protocol:
| Platform | Traces | Metrics | Logs |
|---|---|---|---|
| Android, iOS, Desktop | OTLP/gRPC | OTLP/gRPC | OTLP/gRPC |
| Web | OTLP/HTTP | OTLP/HTTP | OTLP/HTTP |
Web browsers cannot use gRPC due to browser limitations, so Flutterrific automatically switches to HTTP for web builds.
Use commonAttributesFunction to add attributes to every span (e.g., user ID, session info):
await FlutterOTel.initialize(
serviceName: 'my-app',
commonAttributesFunction: () => {
UserSemantics.userId.key: currentUser.id,
UserSemantics.userRole.key: currentUser.role,
SessionViewSemantics.sessionId.key: sessionManager.currentSessionId,
}.toAttributes(),
);Standard OpenTelemetry environment variables are supported via --dart-define:
flutter run \
--dart-define=OTEL_SERVICE_NAME=my-flutter-app \
--dart-define=OTEL_SERVICE_VERSION=1.0.0 \
--dart-define=OTEL_EXPORTER_OTLP_ENDPOINT=https://collector:4317 \
--dart-define=OTEL_EXPORTER_OTLP_PROTOCOL=grpc \
--dart-define=OTEL_EXPORTER_OTLP_HEADERS=api-key=your-api-key| Variable | Description | Default |
|---|---|---|
OTEL_EXPORTER_OTLP_ENDPOINT |
Collector endpoint | http://localhost:4317 |
OTEL_EXPORTER_OTLP_PROTOCOL |
Protocol (grpc or http/protobuf) |
grpc |
OTEL_EXPORTER_OTLP_HEADERS |
Additional headers | None |
OTEL_SERVICE_NAME |
Service name | None |
OTEL_SERVICE_VERSION |
Service version | None |
OTEL_LOG_LEVEL |
SDK internal log level | INFO |
OTEL_CONSOLE_EXPORTER |
Add console exporter for debugging | false |
Signal-specific variables (OTEL_EXPORTER_OTLP_TRACES_*, OTEL_EXPORTER_OTLP_METRICS_*) are also supported and take precedence.
// Track button interactions
ElevatedButton(
onPressed: handleSubmit,
child: Text('Submit'),
).withOTelButtonTracking('submit_form');
// Error boundaries
RiskyWidget().withOTelErrorBoundary('risky_operation');
// Track interactions via the interaction tracker
FlutterOTel.interactionTracker.trackButtonClick(context, 'submit_btn');
FlutterOTel.interactionTracker.trackScroll(context, 'feed_list', scrollPosition);
FlutterOTel.interactionTracker.trackSwipeGesture(context, 'card', 'left');// Start a screen span (creates a new trace)
final span = FlutterOTel().startScreenSpan('checkout');
// ... user interacts with screen ...
// End when leaving
FlutterOTel().endScreenSpan('checkout');Uncaught Flutter and platform errors are reported automatically
(autoCaptureErrors, on by default). Report errors you catch yourself with
reportError:
FlutterOTel.reportError('network_error', error, stackTrace,
attributes: {'endpoint': '/api/users'},
);final logger = FlutterOTel.logger('checkout');
// Emit a structured event
logger.emitEvent('checkout.completed',
body: 'Order placed successfully',
attributes: {
'order.id': orderId,
'order.total': total,
'payment.method': 'credit_card',
}.toAttributes(),
);For widget tests, use SimpleSpanProcessor with ConsoleExporter to avoid gRPC timer conflicts with FakeAsync:
import 'package:flutterrific_opentelemetry/flutterrific_opentelemetry.dart';
Future<void> initializeForTest() async {
await FlutterOTel.initialize(
endpoint: 'http://localhost:4317',
serviceName: 'test-service',
spanProcessor: SimpleSpanProcessor(ConsoleExporter()),
enableMetrics: false,
enableLogs: false,
flushTracesInterval: null, // No periodic timer in tests
detectPlatformResources: false,
);
}
// In tests:
setUp(() async {
await FlutterOTel.reset();
await initializeForTest();
});
tearDown(() async {
await FlutterOTel.reset();
});Run an OpenTelemetry collector locally:
# Grafana LGTM stack (Loki, Grafana, Tempo, Mimir) — all-in-one
docker run -p 3000:3000 -p 4317:4317 -p 4318:4318 --rm -ti grafana/otel-lgtmThen open Grafana at http://localhost:3000 to see traces, metrics, and logs.
// Force flush all pending data (traces, metrics, logs)
FlutterOTel.forceFlush();By default, traces are flushed every 30 seconds. Set flushTracesInterval to change this, or null to disable periodic flushing.
// Clean up on app shutdown
FlutterOTel().dispose();Session recording is enabled by default. On Android and iOS it is captured by the native SDKs (rrweb replay); on web the Dart screenshot recorder is used instead.
You can control it at runtime on either path:
await FlutterOTel.startSessionRecording();
await FlutterOTel.stopSessionRecording();
final recording = await FlutterOTel.isSessionRecording();Both calls are sticky: they survive session rotation and override the
session sampler, so recording stays in the state you asked for until you change
it again. startSessionRecording() also overrides
enableSessionRecording: false, which lets you keep recording off by default
and turn it on only for the flows you care about:
await FlutterOTel.initialize(
serviceName: 'my-app',
middlewareAccountKey: '<account-key>',
enableSessionRecording: false,
);
// ...later, e.g. when the user enters checkout
await FlutterOTel.startSessionRecording();
// ...
await FlutterOTel.stopSessionRecording();The Dart recorder (web and desktop) starts by itself after the first frame
and captures the whole app window; no widget wrapping is needed. To record
only part of the UI, wrap it in a RepaintBoundary with the SDK key, and the
recorder captures that instead. The native recorder (Android/iOS) always
captures the real window, including platform views.
RepaintBoundary(
key: FlutterOTel.repaintBoundaryKey,
child: /* the part of the app to record */,
)Control the session replay frame quality with RecordingOptions, passed to
FlutterOTel.initialize. The defaults match the native SDKs' standard
setting: JPEG quality 50, a 640 px short edge (captured at up to the screen's
pixel ratio), one frame a second, and no frame at all while nothing on screen
changes. Lower qualityValue / minShortSidePx to save bandwidth.
import 'package:middleware_flutter_opentelemetry/middleware_flutter_opentelemetry.dart';
FlutterOTel.initialize(
serviceName: 'my-flutter-app',
tracerName: 'main',
middlewareAccountKey: '*****',
endpoint: 'https://<account>.middleware.io',
// Medium quality preset (good balance of clarity vs. bandwidth):
recordingOptions: const RecordingOptions(
qualityValue: 35, // JPEG quality 1–100 (default 10). ~35 is medium.
minShortSidePx: 480, // Frame short-side resolution (default 320).
),
);RecordingOptions knobs:
| Option | Default | Description |
|---|---|---|
qualityValue |
10 |
JPEG quality (1–100). Low ≈ 10, medium ≈ 35, high ≈ 60. |
minShortSidePx |
320 |
Frames are scaled so the short side is this many pixels. |
screenshotInterval |
500 ms |
How often a frame is captured. |
archiveChunkSize |
10 |
Frames bundled per upload batch. |
Tip: bandwidth grows quickly with quality. Avoid
qualityValueabove ~60 unless you also lowerminShortSidePx.
| Platform | Support Level | Protocol | Notes |
|---|---|---|---|
| Android | Full | OTLP/gRPC | Complete feature support |
| iOS | Full | OTLP/gRPC | Complete feature support |
| Web | Full | OTLP/HTTP | Auto-switches due to browser limitations |
| Windows | Beta | OTLP/gRPC | Desktop support |
| macOS | Beta | OTLP/gRPC | Desktop support |
| Linux | Beta | OTLP/gRPC | Desktop support |
Middleware OpenTelemetry supports standard OpenTelemetry environment variables as defined in the OpenTelemetry Specification.
These environment variables can be set to configure the SDK behavior without changing code. Signal-specific variables take precedence over general ones.
| Variable | Description | Example |
|---|---|---|
OTEL_SERVICE_NAME |
Sets the service name | my-dart-app |
OTEL_SERVICE_VERSION |
Sets the service version | 1.0.0 |
OTEL_RESOURCE_ATTRIBUTES |
Additional resource attributes as comma-separated key=value pairs | environment=production,region=us-west |
| Variable | Description | Default | Example |
|---|---|---|---|
OTEL_EXPORTER_OTLP_ENDPOINT |
The OTLP endpoint URL | http://localhost:4317 for gRPC, http://localhost:4318 for HTTP |
https://otel-collector:4317 |
OTEL_EXPORTER_OTLP_PROTOCOL |
The protocol to use | http/protobuf |
grpc, http/protobuf, http/json |
OTEL_EXPORTER_OTLP_HEADERS |
Additional headers as comma-separated key=value pairs | None | api-key=secret,tenant=acme |
OTEL_EXPORTER_OTLP_INSECURE |
Whether to use insecure connection | false |
true |
OTEL_EXPORTER_OTLP_TIMEOUT |
Export timeout in milliseconds | 10000 |
5000 |
OTEL_EXPORTER_OTLP_COMPRESSION |
Compression to use | None | gzip |
| Variable | Description | Default | Example |
|---|---|---|---|
OTEL_TRACES_EXPORTER |
Trace exporter to use | otlp |
console, none |
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT |
Traces-specific endpoint | Uses general endpoint | https://traces.example.com |
OTEL_EXPORTER_OTLP_TRACES_PROTOCOL |
Traces-specific protocol | Uses general protocol | grpc |
OTEL_EXPORTER_OTLP_TRACES_HEADERS |
Traces-specific headers | Uses general headers | trace-key=value |
OTEL_EXPORTER_OTLP_TRACES_INSECURE |
Traces-specific insecure setting | Uses general setting | true |
OTEL_EXPORTER_OTLP_TRACES_TIMEOUT |
Traces-specific timeout | Uses general timeout | 30000 |
OTEL_EXPORTER_OTLP_TRACES_COMPRESSION |
Traces-specific compression | Uses general compression | gzip |
| Variable | Description | Default | Example |
|---|---|---|---|
OTEL_METRICS_EXPORTER |
Metrics exporter to use | otlp |
console, none |
OTEL_EXPORTER_OTLP_METRICS_ENDPOINT |
Metrics-specific endpoint | Uses general endpoint | https://metrics.example.com |
OTEL_EXPORTER_OTLP_METRICS_PROTOCOL |
Metrics-specific protocol | Uses general protocol | http/protobuf |
OTEL_EXPORTER_OTLP_METRICS_HEADERS |
Metrics-specific headers | Uses general headers | metric-key=value |
OTEL_EXPORTER_OTLP_METRICS_INSECURE |
Metrics-specific insecure setting | Uses general setting | false |
OTEL_EXPORTER_OTLP_METRICS_TIMEOUT |
Metrics-specific timeout | Uses general timeout | 60000 |
OTEL_EXPORTER_OTLP_METRICS_COMPRESSION |
Metrics-specific compression | Uses general compression | gzip |
| Variable | Description | Example |
|---|---|---|
OTEL_LOG_LEVEL |
SDK internal log level | TRACE, DEBUG, INFO, WARN, ERROR, FATAL |
OTEL_LOG_METRICS |
Enable metrics logging | true, 1, yes, on |
OTEL_LOG_SPANS |
Enable spans logging | true, 1, yes, on |
OTEL_LOG_EXPORT |
Enable export logging | true, 1, yes, on |
OTEL_CONSOLE_EXPORTER |
Add console exporter for debugging | true, 1, yes, on |
When running a Flutter app:
flutter run \
--dart-define=OTEL_SERVICE_NAME=my-flutter-app \
--dart-define=OTEL_SERVICE_VERSION=1.0.0 \
--dart-define=OTEL_EXPORTER_OTLP_ENDPOINT=https://otel-collector:4317 \
--dart-define=OTEL_EXPORTER_OTLP_PROTOCOL=grpc \
--dart-define=OTEL_EXPORTER_OTLP_HEADERS=Authorization=your-api-keyvoid fetchUserData() async {
final tracer = FlutterOTel.tracer;
final span = tracer.startSpan('fetch_user_data', attributes: {
'user.id': userId,
'api.endpoint': '/users',
});
try {
final result = await apiClient.getUser(userId);
span.setStatus(SpanStatusCode.Ok);
return result;
} catch (e, stackTrace) {
span.recordException(e, stackTrace: stackTrace);
span.setStatus(SpanStatusCode.Error, e.toString());
rethrow;
} finally {
span.end();
}
}// Track button interactions
ElevatedButton(
onPressed: handleSubmit,
child: Text('Submit'),
).withOTelButtonTracking('submit_form');
// Track text field
TextField(
decoration: const InputDecoration(
labelText: 'Enter something',
border: OutlineInputBorder(),
),
).withOTelTextFieldTracking('demo_text_field')
// Error boundaries
RiskyWidget().withOTelErrorBoundary('risky_operation');## Configuration
### Environment Variables
Standard OpenTelemetry environment variables are supported:
```bash
# Exporter endpoint
--dart-define=OTEL_EXPORTER_OTLP_ENDPOINT=https://your-collector:4317
# Protocol selection (default is http/protobuf as per OTel spec)
--dart-define=OTEL_EXPORTER_OTLP_PROTOCOL=grpc
# Service information
--dart-define=OTEL_SERVICE_NAME=my-flutter-app
--dart-define=OTEL_SERVICE_VERSION=1.0.0
```
### Local Development
For local development, run an OpenTelemetry collector on localhost:4317, the default.
```bash
docker run -p 3000:3000 -p 4317:4317 -p 4318:4318 --rm -ti grafana/otel-lgtm
```
- Basic Integration Example
- Flutter_Wonderous OpenTelemetry - Complete app example based on Wonderous
We are looking for contributors and maintainers! Please see our Contributing Guide for details.
# Clone the repository
git clone https://github.com/middleware-labs/flutter-sdk.git
cd flutter-sdk
# Install dependencies
make install
# Run tests
make test
# Run all checks
make allThis project follows the CNCF Code of Conduct and maintains open governance. We welcome community participation and contributions.
Security vulnerabilities should be reported privately to the maintainers. See our Security Policy for details.
See our Roadmap for planned features and improvements.
This project is licensed under the Apache License 2.0 - see the LICENSE file for details.
- GitHub Issues - Bug reports and feature requests
- OpenTelemetry Community - Broader OpenTelemetry community
Built on the foundation of: