The Overwatch
A comprehensive observability package for Flutter applications that enables running with different observability stacks. Currently supports Grafana stack but can be swapped with different stacks as well.
Features
- Multi-Backend Support: Easily integrate with multiple observability backends
- Grafana Stack Integration: Built-in support for Loki (logging) and Faro (RUM/metrics/traces)
- Privacy Controls: Automatic PII scrubbing with configurable patterns
- Offline Buffering: Store events offline and replay when connectivity is restored
- Type-Safe: Strongly typed data models for events, errors, logs, and metrics
- Extensible: Easy to add custom backend adapters
- Performance Optimized: Batching, compression, and background processing
Architecture
Core Components
- Observability: Main facade providing a simple API
- ObservabilityDispatcher: Manages multiple backend adapters and data flow
- BackendAdapter: Abstract interface for observability backends
- Data Models: Strongly typed models for AppEvent, AppError, AppLog, AppMetric
- Privacy Utils: PII scrubbing and data anonymization
- Offline Buffer: Persistent storage for offline event replay
Backend Adapters
LokiAdapter
- Sends structured logs to Grafana Loki
- Batches logs by labels for efficient streaming
- Converts errors to structured log entries
- Supports custom labels and global context
FaroAdapter
- Integrates with Grafana Faro for Real User Monitoring
- Tracks custom events and user interactions
- Captures errors with breadcrumbs and context
- Records custom metrics and performance data
- Automatically instruments console logs and web vitals
Installation
Add this to your package's pubspec.yaml file:
dependencies:
the_overwatch: ^1.0.0
Then run:
flutter pub get
Quick Start
1. Basic Setup
import 'package:the_overwatch/the_overwatch.dart';
void main() async {
// Configure observability
final config = ObservabilityConfig(
backendConfigs: [],
privacy: PrivacyConfig(
scrubPii: true,
enableAnalytics: true,
enableErrorReporting: true,
),
enableOfflineBuffer: true,
globalContext: {'app_type': 'mobile'},
);
// Setup with Grafana stack
await Observability.setup(config, adapters: [
(LokiAdapter(), LokiConfig(
enabled: true,
host: 'http://localhost:3100',
globalLabels: {'app': 'my_flutter_app'},
)),
(FaroAdapter(), FaroConfig(
enabled: true,
appName: 'My Flutter App',
appVersion: '1.0.0',
collectorUrl: 'https://faro-collector.example.com',
)),
]);
// Your app code here
runApp(MyApp());
}
2. Track Events
// Track user interactions
await Observability.instance.trackEvent('button_clicked', properties: {
'button_id': 'submit_form',
'form_type': 'contact',
});
// Track feature usage
await Observability.instance.trackEvent('feature_used', properties: {
'feature_name': 'advanced_search',
'usage_count': 5,
});
3. Logging
// Simple logging
await Observability.instance.info('User logged in successfully');
await Observability.instance.error('Failed to sync data');
// Structured logging with context
await Observability.instance.log(
LogLevel.warn,
'API rate limit approaching',
labels: {'component': 'api_client'},
context: {'remaining_requests': 10},
);
4. Error Handling
try {
await riskyOperation();
} catch (e, stack) {
await Observability.instance.captureError(
e,
stackTrace: stack,
severity: ErrorSeverity.high,
context: {'operation': 'data_sync'},
breadcrumbs: [
Breadcrumb(
message: 'User initiated sync',
timestamp: DateTime.now().subtract(Duration(seconds: 5)),
category: 'user_action',
),
],
);
}
5. Metrics
// Performance metrics
await Observability.instance.recordMetric('page_load_time', 1.234, unit: 'seconds');
// Business metrics
await Observability.instance.recordMetric('api_response_time', 0.845,
unit: 'seconds',
tags: {'endpoint': '/api/users', 'method': 'GET'},
);
6. User Context
// Set user information
await Observability.instance.setUserId('user-123');
await Observability.instance.setUserProperties({
'plan': 'premium',
'signup_date': '2024-01-15',
'email': 'user@example.com', // Will be scrubbed if PII protection is enabled
});
// Add global context
Observability.instance.addGlobalContext('screen', 'home');
Configuration
Privacy Configuration
PrivacyConfig(
scrubPii: true, // Enable PII scrubbing
enableAnalytics: true, // Enable event tracking
enableErrorReporting: true, // Enable error capture
enablePerformanceMonitoring: true, // Enable metrics
enableLogging: true, // Enable logging
piiPatterns: [ // Custom PII patterns
r'\\bcustom-secret-\\d+\\b',
],
)
Loki Configuration
LokiConfig(
enabled: true,
host: 'http://localhost:3100',
globalLabels: {
'app': 'my_app',
'environment': 'production',
},
batchSize: 100,
flushInterval: Duration(seconds: 30),
enableCompression: true,
)
Faro Configuration
FaroConfig(
enabled: true,
appName: 'My Flutter App',
appVersion: '1.0.0',
collectorUrl: 'https://faro-collector.example.com',
environment: 'production',
enableConsoleInstrumentation: true,
enableWebVitalsInstrumentation: true,
enableErrorInstrumentation: true,
enableUserInteractionInstrumentation: true,
sessionSampleRate: 1.0,
traceSampleRate: 0.1,
globalAttributes: {
'team': 'mobile',
'feature_flag_group': 'beta',
},
)
Custom Adapters
Create custom backend adapters by extending BackendAdapter:
class CustomAdapter extends BackendAdapter {
@override
String get name => 'CustomBackend';
@override
Future<void> initialize(BackendConfig config) async {
// Initialize your backend
}
@override
Future<void> trackEvent(AppEvent event) async {
// Handle events
}
@override
Future<void> captureError(AppError error) async {
// Handle errors
}
@override
Future<void> log(AppLog log) async {
// Handle logs
}
@override
Future<void> recordMetric(AppMetric metric) async {
// Handle metrics
}
// ... other required methods
}
Data Models
AppEvent
name: Event nameproperties: Event propertiestimestamp: When the event occurreduserId: Associated user IDsessionId: Session identifierdeviceInfo: Device informationcontext: Additional context
AppError
exception: The exception objectstackTrace: Stack tracemessage: Error messageseverity: Error severity levelbreadcrumbs: User actions leading to errorcontext: Additional context
AppLog
level: Log level (trace, debug, info, warn, error, fatal)message: Log messagelabels: Structured labelscontext: Additional context
AppMetric
name: Metric namevalue: Numeric valueunit: Unit of measurementtags: Metric tagstraceId: Associated trace IDspanId: Associated span ID
Offline Support
The package includes robust offline support:
- Automatic Buffering: Events are automatically buffered when offline
- Intelligent Retry: Exponential backoff with max retry limits
- Persistent Storage: Uses SQLite for durable offline storage
- Manual Control: Flush buffer manually or check buffer size
// Check buffer status
final bufferSize = await Observability.instance.getBufferSize();
// Manual flush
await Observability.instance.flushBuffer();
Performance Considerations
- Batching: Events are batched for efficient transmission
- Background Processing: All operations are non-blocking
- Memory Management: Configurable buffer limits
- Compression: Optional gzip compression for network efficiency
- Sampling: Configurable sampling rates for high-volume applications
Flutter Integration
Error Handling
// Capture Flutter framework errors
FlutterError.onError = (FlutterErrorDetails details) {
Observability.instance.captureError(
details.exception,
stackTrace: details.stack,
context: {'library': details.library},
);
};
// Capture platform errors
PlatformDispatcher.instance.onError = (error, stack) {
Observability.instance.captureError(error, stackTrace: stack);
return true;
};
Navigation Tracking
class ObservabilityNavigatorObserver extends NavigatorObserver {
@override
void didPush(Route route, Route? previousRoute) {
if (route.settings.name != null) {
Observability.instance.trackEvent('screen_view', properties: {
'screen_name': route.settings.name!,
'previous_screen': previousRoute?.settings.name,
});
}
}
}
Security & Privacy
- PII Protection: Automatic detection and scrubbing of personally identifiable information
- Configurable Data Collection: Granular control over what data is collected
- Secure Transmission: HTTPS-only communication with backends
- Data Minimization: Only collect what's necessary for observability
Testing
The package includes comprehensive test utilities:
// Mock observability for testing
await Observability.instance.initialize(ObservabilityConfig(
backendConfigs: [],
enableDebugLogging: true,
));
// Register test adapter
await Observability.instance.registerAdapter(
TestAdapter(),
TestConfig(enabled: true),
);
Troubleshooting
Common Issues
- Events not appearing in Loki: Check Loki URL and label configuration
- High memory usage: Reduce buffer size or increase flush frequency
- PII still visible: Review and update PII patterns
- Offline events not replaying: Check network connectivity and retry configuration
Debug Mode
Enable debug logging to troubleshoot issues:
ObservabilityConfig(
enableDebugLogging: true,
// ... other config
)
Contributing
- Fork the repository
- Create a feature branch
- Add tests for new functionality
- Ensure all tests pass
- Submit a pull request
License
This project is licensed under the MIT License - see the LICENSE file for details.
Changelog
1.0.0
- Initial release
- Loki adapter for structured logging
- Faro adapter for RUM and metrics
- Privacy controls and PII scrubbing
- Offline buffering and retry logic
- Comprehensive documentation and examples