log_pilot 1.1.0
log_pilot: ^1.1.0 copied to clipboard
Flutter logging with MCP server for AI agents. DevTools extension, structured output, sinks, breadcrumbs, and LLM export.
LogPilot #
The first Flutter logging package built for AI agents.
LogPilot gives AI coding agents (Cursor, Claude Code, Windsurf, Copilot, Gemini CLI) live access to your Flutter app's logs through an MCP server and DevTools extension — no manual copy-paste, no stale terminal output. Agents can query, filter, watch, and change log levels at runtime while you code.
It also does everything you expect from a logger — pretty console output, structured metadata, sinks, file logging, breadcrumbs — but the differentiator is the agent-first architecture: your AI assistant sees what your app sees, in real time.

void main() {
LogPilot.init(child: const MyApp());
}
// Anywhere in your app:
LogPilot.info('User signed in', metadata: {'userId': '123'});
LogPilot.error('Payment failed', error: e, stackTrace: st);
For AI Agents #
If you are an AI coding agent reading this README, this section gives you everything you need. The rest of the README is for human developers.
Setup #
The app already depends on log_pilot. Use LogPilot for all logging — never
raw print() or debugPrint().
import 'package:log_pilot/log_pilot.dart';
Logging API #
LogPilot.verbose('Starting sync...');
LogPilot.debug('Cache key: user_42');
LogPilot.info('Order placed', metadata: {'orderId': 'ORD-456', 'total': 29.99});
LogPilot.warning('Retry attempt 3/5');
LogPilot.error('Checkout failed', error: e, stackTrace: st);
LogPilot.fatal('Database corrupted');
Every method accepts optional error, stackTrace, metadata (Map), and
tag (String). Always pass error + stackTrace together on error/fatal.
Scoped Loggers #
class AuthService {
static const _log = LogPilotLogger('AuthService');
Future<void> signIn(String email) async {
_log.info('Attempting sign in', metadata: {'email': email});
try {
await _auth.signIn(email);
_log.info('Sign in successful');
} catch (e, st) {
_log.error('Sign in failed', error: e, stackTrace: st);
}
}
}
Querying Logs (in-process) #
LogPilot.history; // all records
LogPilot.historyWhere(level: LogLevel.error); // filter by level
LogPilot.historyWhere(tag: 'http', hasError: true); // filter by tag + error
LogPilot.export(format: ExportFormat.json); // NDJSON export
LogPilot.exportForLLM(tokenBudget: 2000); // compressed for LLM context
LogPilot.snapshot(); // structured diagnostic summary
LogPilot.setLogLevel(LogLevel.verbose); // increase verbosity at runtime
Querying Logs (via MCP) #
If the LogPilot MCP server is configured, you can call these tools directly:
| MCP Tool | What it does |
|---|---|
get_snapshot |
Session ID, config, error counts, recent logs, active timers |
query_logs |
Filter by level, tag, message, trace ID, error presence, metadata key |
export_logs |
Full history as text or NDJSON |
export_for_llm |
Compressed summary optimized for your context window |
set_log_level |
Change verbosity at runtime without restart |
get_log_level |
Read current minimum level |
clear_logs |
Wipe in-memory history |
watch_logs |
Stream new entries as push notifications (filterable) |
stop_watch |
Stop the active watcher |
Setting Up the MCP Server #
You MUST complete ALL steps below. The MCP server will not appear in your tool list until the developer finishes step 4. Do not skip ahead — you cannot use LogPilot MCP tools until setup is fully done.
Prerequisites: The app must call LogPilot.init() or
LogPilot.configure() — both register the service extensions the MCP
server needs. If using configure() (e.g. for Firebase/async startup),
MCP works exactly the same way.
Steps 1–3 are actions YOU (the agent) perform. Step 4 requires the developer — you must ask them and wait.
-
Install the MCP server. Two options — pick one:
Option 1 — Global activation (recommended, works everywhere):
dart pub global activate log_pilot_mcpOption 2 — Dev dependency (scoped to your app):
dart pub add --dev log_pilot_mcp -
Ensure the Flutter app is running in debug mode. Auto-discovery requires the app to be running so it can write the VM service URI. If the app is not running, ask the developer to run
flutter runfirst. On Android/iOS, auto-discovery from the device is not possible — use--project-rootin the MCP config or ask the developer to runlog_pilot_mcp write-uri <ws://...>from the host machine.Agents: prefer global activation. It avoids working-directory problems —
dart run log_pilot_mcponly works from the directory that has the package inpubspec.yaml, but IDEs may start MCP servers from a different directory. -
Run the Flutter app in debug mode (if it's not already running):
flutter runThe app must be running so LogPilot can write the VM service URI. For Flutter Web, note the
ws://...URI printed in the debug console — you'll need it in step 3. -
Write the MCP config file.
Detect the IDE and write to the correct location:
IDE Config file Cursor .cursor/mcp.jsonin workspace rootVS Code (Copilot) .vscode/mcp.jsonin workspace rootWindsurf ~/.codeium/windsurf/mcp_config.jsonClaude Code ~/.claude/mcp.jsonGemini CLI ~/.gemini/settings.jsonBuild the JSON using this logic:
Command: On macOS/Linux use
"log_pilot_mcp". On Windows use the absolute path"C:\\Users\\<USER>\\AppData\\Local\\Pub\\Cache\\bin\\log_pilot_mcp.bat". If you used a dev dependency instead of global activation, use"dart"with["run", "log_pilot_mcp"]as args.Args — add only what's needed:
- Windows: add
"--project-root=<ABSOLUTE_PATH_TO_APP>" - Flutter Web: add
"--vm-service-uri=<URI_FROM_STEP_2>"(changes every restart) - Dev dependency not at workspace root: add
"cwd": "<PATH_TO_APP>"at the server level
Typical result (global activation, Windows):
{ "mcpServers": { "LogPilot": { "command": "C:\\Users\\you\\AppData\\Local\\Pub\\Cache\\bin\\log_pilot_mcp.bat", "args": ["--project-root=C:/Users/you/my-flutter-app"] } } }Typical result (global activation, macOS/Linux):
{ "mcpServers": { "LogPilot": { "command": "log_pilot_mcp", "args": [] } } }If the config file already exists, merge into the existing
mcpServers— do not overwrite other servers. - Windows: add
-
Ask the developer to reload the IDE window and enable LogPilot. The IDE won't discover the new MCP server until it reloads. Ask the developer to reload the window, toggle LogPilot ON in MCP settings if needed, and confirm a green/connected indicator. Then call
get_snapshotto verify the connection.
Rules for Agents #
- Always pass
error+stackTraceto error/fatal calls. - Use
tag:for every domain (Auth,Cart,API,DB,Nav, etc.). - Prefer
metadata:maps over string interpolation. - Use
const LogPilotLogger('Tag')in classes for automatic tagging. - Never silence errors with empty catch blocks — log them.
Copy this block into your project's agent instruction file (e.g.
AGENTS.md, GEMINI.md, CLAUDE.md, or your IDE's rules directory)
for correct agent behavior.
Table of Contents #
- For AI Agents
- MCP Server
- DevTools Extension
- In-App Log Viewer
- LLM Export
- Quick Start
- Console Output
- Log Messages
- JSON Pretty-Printing
- Scoped Instance Loggers
- Network Logging
- Log Sinks
- File Logging
- Config Presets
- Tagged Logging & Focus Mode
- Rate Limiting / Deduplication
- Log History / Ring Buffer
- Session & Trace IDs
- Navigation Logging
- BLoC Observer
- Performance Timing
- Error Breadcrumbs
- Error IDs
- Sensitive Field Masking
- Error Silencing
- Runtime Log-Level Override
- Lazy Message Evaluation
- Instrumentation Helpers
- Self-Diagnostics
- Crash Reporter Integration
- Diagnostic Snapshot
- Web Platform
- Testing
- Configuration Reference
- Package Imports
- Example App
- Features at a Glance
- Contributing
- License
- Migrating from plog
MCP Server #
The log_pilot_mcp
package is a standalone MCP server that gives AI coding agents live,
bidirectional access to your running Flutter app's logs. No terminal
scraping — the agent calls structured tools over the Model Context Protocol.
Install it globally (dart pub global activate log_pilot_mcp) or as a
dev dependency (dart pub add --dev log_pilot_mcp).
How It Works #
+----------------+ +------------------+
| Flutter App | -- VM Service ---> | log_pilot_mcp |
| (debug mode) | <-- ext.LogPilot.* | (MCP server) |
+-------+--------+ +---------+--------+
| |
| writes URI | MCP protocol
| on start |
v v
.dart_tool/ +------------------+
log_pilot_vm_service_uri | Cursor / Claude |
| | Windsurf / ... |
+--- watched by MCP server --->+------------------+
- When your Flutter app starts in debug mode,
LogPilot.init()orLogPilot.configure()registersext.LogPilot.*service extensions on the Dart VM and writes the VM service URI to.dart_tool/log_pilot_vm_service_uri. - The MCP server watches that file, auto-discovers the URI, and connects.
- AI agents call MCP tools (
get_snapshot,query_logs, etc.) which the server translates into service extension calls on the running app. - On hot restart, the VM extensions re-register and the server auto-reconnects. On full restart, the URI file updates and the server reconnects within seconds — no manual action needed.
Setup #
- Install:
dart pub global activate log_pilot_mcp(ordart pub add --dev log_pilot_mcp) - Run your app:
flutter run - Add LogPilot to your IDE's MCP config
- Reload the IDE window and toggle LogPilot ON
For detailed per-IDE instructions (Cursor, VS Code, Windsurf, Claude Code,
Antigravity, Gemini CLI), see the
log_pilot_mcp README.
Auto-Discovery #
LogPilot writes the Dart VM WebSocket URI to
.dart_tool/log_pilot_vm_service_uri every time your app starts in debug
mode. The MCP server:
- Reads the file on startup
- Watches for changes (file system watcher)
- If the file doesn't exist yet (server started before app), waits for it to appear
- On hot restart: isolate recycles, extensions re-register, server detects the isolate event and re-resolves on the next tool call
- On full restart: URI changes, file updates, server detects the change and reconnects within the watch interval
No manual URI copying is needed in the normal desktop workflow.
Android / iOS: The app runs on a device and cannot write to the host's
.dart_tool directory. Use one of these approaches on the host machine:
write-uricommand — copy thews://...URI from the debug console and run:log_pilot_mcp write-uri ws://127.0.0.1:PORT/TOKEN=/ws(add--project-root=<APP_PATH>if needed). The MCP server's file watcher detects the change and reconnects automatically.--project-root— add--project-root=<ABSOLUTE_PATH_TO_YOUR_APP>to the MCP server args so it knows where to watch for the URI file.
Flutter Web: Auto-discovery does not work on web (no dart:io). You
must pass the VM service URI manually — it changes on every app restart.
The simplest approach is to copy the ws://... URI from Flutter's debug
console and update the --vm-service-uri argument in your MCP config.
The log_pilot_mcp repo
provides optional helper scripts (bash + PowerShell) that automate this
capture by parsing flutter run output. Note: these scripts depend
on the exact format of Flutter's console output and may need adjustment
across Flutter SDK versions.
If auto-discovery fails on desktop (e.g., .dart_tool is not in the
expected location, or the app's working directory differs from the project
root on Windows), you have two fallback options:
- Pass the project root — add
--project-root=<ABSOLUTE_PATH_TO_YOUR_APP>to theargsarray so the server knows where to find.dart_tool/log_pilot_vm_service_uri. - Pass the URI manually — copy the
ws://...URI from the Flutter debug console and add--vm-service-uri=ws://127.0.0.1:PORT/TOKEN=/wsto theargsarray inmcp.json.
What Agents Can Do #
| Tool | What it does |
|---|---|
get_snapshot |
Structured summary: session ID, config, history counts, recent errors, active timers. Supports group_by_tag for per-tag breakdown. |
query_logs |
Filter by level, tag, message text, trace ID, error presence, metadata key. deduplicate: true collapses repeated entries while preserving different call sites. |
export_logs |
Full history as human-readable text or NDJSON. |
export_for_llm |
Compressed summary optimized for LLM context windows — prioritizes errors, deduplicates, truncates verbose entries. |
set_log_level / get_log_level |
Change or read verbosity at runtime. Crank to verbose for debugging, back to warning when done. |
clear_logs |
Wipe in-memory history. |
watch_logs |
Stream new entries as MCP push notifications. Filter by tag and level. |
stop_watch |
Stop the watcher and get a delivery summary. |
| Resource | Contents |
|---|---|
LogPilot://config |
Current LogPilotConfig as JSON |
LogPilot://session |
Session ID and active trace ID |
LogPilot://tail |
Latest batch from the active watcher (subscribable) |
Agent Debugging Workflow #
get_snapshot— see what's happening (errors, config, timers)set_log_level(level: "verbose")— increase detail- Reproduce the issue
query_logs(level: "error", deduplicate: true)— find the root causeexport_for_llm(token_budget: 2000)— get compressed context for analysisset_log_level(level: "warning")— restore quiet mode
Claude Code / Terminal Usage #
Flutter Web has no dart:io, so auto-discovery is unavailable — you
must pass --vm-service-uri manually. See the
log_pilot_mcp Flutter Web docs
for details and helper scripts.
If auto-discovery fails on native (e.g. Windows cwd mismatch), add
--project-root=<ABSOLUTE_PATH_TO_APP> to the MCP server args, or pass
--vm-service-uri directly.
| Problem | Solution |
|---|---|
| Server shows "Disabled" in IDE's MCP settings | Toggle the switch ON manually. Most IDEs default new servers to disabled. |
| Server not appearing in MCP settings | Reload your IDE window after creating/editing the MCP config file. |
Could not find package "log_pilot_mcp" |
Run dart pub add --dev log_pilot_mcp in your app's directory first. |
Failed to connect to VM service |
App isn't running in debug mode, or the URI is stale. Start the app first. |
| Auto-discovery file not created | On Windows, the app's working directory may not match the project root. On Android/iOS, the device can't write to the host. Pass --project-root=<APP_PATH>, use log_pilot_mcp write-uri <ws://...>, or --vm-service-uri manually. |
| Tools fail after hot restart | Auto-recovers on the next call. If it persists, the VM port changed (full restart) — the URI file watcher handles this. |
| Server connects but tools return errors | The app must import 'package:log_pilot/log_pilot.dart' so the library is loaded. |
See the log_pilot_mcp README
for the full MCP tool reference, parameter tables, debugging workflow,
and troubleshooting guide.
DevTools Extension #

Zero configuration — add log_pilot as a dependency and a LogPilot tab
appears in Dart DevTools automatically.
- Real-time log table with color-coded level badges, tags, timestamps, and caller locations
- Level filter dropdown + tag filter dropdown + free-text search
- Auto-scroll with manual override
- Toolbar: Refresh, Clear, Set log level, Export (text/JSON), Snapshot
- Detail view: full message, metadata JSON tree, error + stack trace, error ID, breadcrumb timeline with copy-to-clipboard
- Works on all platforms including Flutter Web (uses VM service extensions, not expression evaluation)
| Filtered by Level + Tag | Detail with Metadata & Breadcrumbs |
|---|---|
![]() |
![]() |
In-App Log Viewer #

MaterialApp(
builder: (context, child) => LogPilotOverlay(child: child!),
home: const MyHome(),
)
A draggable, resizable bottom sheet with full debug capabilities:
- Snap points at 25%, 50%, 75%, and full-screen
- Level filter chips (ALL, VERBOSE, DEBUG, INFO, WARNING, ERROR, FATAL)
- Tag filter chips — dynamically generated from logged records
- Text search across messages, tags, and levels
- Record detail view — tap any entry for full metadata, error, stack trace, breadcrumbs, caller, session/trace/error IDs, with copy-to-clipboard
- Auto-scroll toggle
- Copy full history as text or NDJSON
- Clear history button
Auto-hides in production. Override with LogPilotOverlay(enabled: true).
Control FAB position: LogPilotOverlay(entryButtonAlignment: Alignment.bottomLeft).
| Overlay List View | Record Detail View |
|---|---|
![]() |
![]() |
LLM Export #
Compress log history to fit within an LLM's context window:
final summary = LogPilot.exportForLLM(tokenBudget: 2000);
The algorithm prioritizes errors, deduplicates consecutive identical messages, truncates verbose entries, and fills remaining budget with the most recent records. Default budget is 4000 tokens (~16k chars).
Also available via MCP: the export_for_llm tool accepts a token_budget
parameter and returns the compressed summary directly to the agent.
Quick Start #
Install #
dependencies:
log_pilot: ^1.1.0-beta.1
Pick Your Setup Level #
LogPilot offers four setup options. Choose one:
| Setup | What it does | You call runApp()? |
MCP / DevTools? |
|---|---|---|---|
Option A: LogPilot.init() |
Full setup — error zones, error catching, logging. Replaces runApp(). |
No — init() calls it |
Yes |
Option B: LogPilot.configure() |
Config + service extensions. No error zones, no automatic error catching. | Yes | Yes |
| Option C: Firebase / async startup | configure() inside your own runZonedGuarded. Best for apps with async init before runApp(). |
Yes | Yes |
| Option D: Zero setup | No init — works with defaults in debug mode. | Yes | No |
init() vs configure() #
| Capability | init() |
configure() |
|---|---|---|
| Console logging | Yes | Yes |
Config presets (.debug(), .production(), etc.) |
Yes | Yes |
| Log history / ring buffer | Yes | Yes |
| Service extensions (MCP + DevTools) | Yes | Yes |
| VM URI file for auto-discovery | Yes | Yes |
| Log sinks | Yes | Yes |
| Breadcrumbs | Yes | Yes |
FlutterError.onError handler |
Yes | No — you manage it |
PlatformDispatcher.onError handler |
Yes | No — you manage it |
runZonedGuarded (uncaught async errors) |
Yes | No — you manage it |
| Error cascade suppression | Yes | No |
onError callback (for Crashlytics/Sentry) |
Yes | No — use sinks or your own zone |
Option D (zero setup) only gives you console logging with defaults — no service extensions, history, sinks, or breadcrumbs.
Option A: LogPilot.init() — Full Setup (recommended for simple apps)
init()callsrunApp()internally. Do NOT also callrunApp()— doing so will cause double-initialization bugs.
import 'package:flutter/material.dart';
import 'package:log_pilot/log_pilot.dart';
void main() {
LogPilot.init(child: const MyApp());
}
This auto-catches every Flutter error, platform error, and uncaught zone exception. It also registers service extensions for DevTools and MCP.
With crash reporter forwarding:
void main() {
LogPilot.init(
config: LogPilotConfig.debug(),
onError: (error, stack) {
FirebaseCrashlytics.instance.recordError(error, stack);
},
child: const MyApp(),
);
}
Limitation:
init()callsWidgetsFlutterBinding.ensureInitialized()andrunApp()inside its own zone. If you need to run async code beforerunApp()(e.g.Firebase.initializeApp()), use Option B or C instead.
Option B: LogPilot.configure() — Config + Extensions, You Handle Errors
Use this when you need
LogPilotlogging and MCP/DevTools but want to manage error handling yourself.configure()registers service extensions and the VM URI file, but does not set up error zones,FlutterError.onError, orPlatformDispatcher.onError.
void main() {
WidgetsFlutterBinding.ensureInitialized();
LogPilot.configure(config: LogPilotConfig(logLevel: LogLevel.info));
runApp(const MyApp());
}
Option C: Firebase / Crashlytics / Async Startup (recommended for production apps)
This is the most common production pattern — apps that call
Firebase.initializeApp(), set up Crashlytics, or do other async work beforerunApp(). Useconfigure()inside your ownrunZonedGuarded.
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:log_pilot/log_pilot.dart';
void main() {
runZonedGuarded<Future<void>>(() async {
WidgetsFlutterBinding.ensureInitialized();
// Configure LogPilot first — registers service extensions for
// MCP and DevTools, and writes the VM URI for auto-discovery.
LogPilot.configure(config: LogPilotConfig.debug());
await Firebase.initializeApp();
await FirebaseCrashlytics.instance
.setCrashlyticsCollectionEnabled(!kDebugMode);
FlutterError.onError = (details) {
LogPilot.error(
'Flutter error: ${details.summary}',
error: details.exception,
stackTrace: details.stack,
);
FirebaseCrashlytics.instance.recordFlutterError(details);
};
runApp(const MyApp());
}, (error, stack) {
LogPilot.error('Uncaught', error: error, stackTrace: stack);
FirebaseCrashlytics.instance.recordError(error, stack);
});
}
Why
configure()and notinit()?init()callsensureInitialized()andrunApp()inside its own zone — you can't runawait Firebase.initializeApp()in between.configure()gives you full control over binding initialization, async setup, error zones, andrunApp()timing, while still registering the service extensions needed for MCP and DevTools.
Option D: Zero Setup
LogPilot.info('Hello world'); // no init needed in debug mode
Note: Zero setup provides basic console logging only. Service extensions (required for MCP tools and DevTools), log history, sinks, and breadcrumbs all require
LogPilot.init()orLogPilot.configure().
Console Output #
LogPilot wraps every log in a box-bordered block with level, timestamp, clickable caller location, and your message:

Three output modes are available:
| Mode | Use case | Example |
|---|---|---|
OutputFormat.pretty |
Human in IDE (default) | Box-bordered, colorized |
OutputFormat.plain |
AI agents / CI | [INFO] [auth] User signed in | {"userId": "123"} |
OutputFormat.json |
Structured pipelines | {"level":"INFO","timestamp":"...","message":"..."} |
| Pretty | Plain | NDJSON |
|---|---|---|
![]() |
![]() |
![]() |
Log Messages #
Every method supports optional error, stackTrace, metadata, and tag:
LogPilot.verbose('Starting sync...');
LogPilot.debug('Cache key: user_42');
LogPilot.info('Order placed', metadata: {'orderId': 'ORD-456', 'total': 29.99});
LogPilot.warning('Retry attempt 3/5');
LogPilot.error('Checkout failed', error: e, stackTrace: st);
LogPilot.fatal('Database corrupted');
| Verbose | Info + Metadata |
|---|---|
![]() |
![]() |
| Error + Stack Trace + Breadcrumbs | Fatal |
|---|---|
![]() |
![]() |
JSON Pretty-Printing #
LogPilot.json('{"users": [{"id": 1, "name": "Alice"}]}');
Keys and values render in different colors. Customize with jsonKeyColor
and jsonValueColor in the config.

Scoped Instance Loggers #
Create a LogPilotLogger for class-level logging — every log is automatically
tagged:
class AuthService {
// LogPilotLogger has a const constructor for compile-time constant loggers:
static const _log = LogPilotLogger('AuthService');
// Or use the convenience factory: LogPilot.create('AuthService')
Future<void> signIn(String email) async {
_log.info('Attempting sign in', metadata: {'email': email});
try {
await _auth.signIn(email);
_log.info('Sign in successful');
} catch (e, st) {
_log.error('Sign in failed', error: e, stackTrace: st);
}
}
}
All instance methods accept an optional tag: override — when provided it
replaces the instance tag for that single call. This makes the static and
instance APIs fully compatible: _log.info('msg', tag: 'http') works on
both without code changes.
Scoped loggers also prefix timer labels: _log.time('query') produces AuthService/query.
Network Logging #
The http interceptor is built into the published package:
import 'package:log_pilot/log_pilot.dart';
final client = LogPilotHttpClient();
final response = await client.get(Uri.parse('https://api.example.com/users'));

Response log levels are set by HTTP status code: 5xx -> error, 4xx -> warning, 2xx/3xx -> info.
LogPilotHttpClient(
logRequestHeaders: true,
logRequestBody: true,
logResponseHeaders: false,
logResponseBody: true, // opt-in (default: false)
maxResponseBodySize: 4 * 1024, // truncate after 4 KB
injectSessionHeader: true, // adds X-LogPilot-Session / X-LogPilot-Trace
createRecords: true, // creates LogPilotRecord entries in history
)
Override level per status code:
LogPilotHttpClient(
logLevelForStatus: (status) =>
status == 429 ? LogLevel.error : LogPilotHttpClient.defaultLogLevelForStatus(status),
)
Query network errors from history:
final httpErrors = LogPilot.historyWhere(tag: 'http', hasError: true);
Dio, Chopper, GraphQL, and BLoC integrations will fail to import if you installed from pub.dev. These barrels are
.pubignored and only exist in the source repo.To use them now: copy the source file from the repo into your project (e.g.
lib/src/network/log_pilot_dio_interceptor.dart). Standalone packages (log_pilot_dio,log_pilot_bloc, etc.) are planned.
Log Sinks #
Route log records to any destination alongside console output:
LogPilot.init(
config: LogPilotConfig(
sinks: [
CallbackSink((record) {
FirebaseCrashlytics.instance.log(record.message ?? '');
}),
],
),
child: const MyApp(),
);
Sinks fire even when console output is off (enabled: false), making
them ideal for production. Implement LogSink for custom sinks:
class RemoteSink implements LogSink {
@override
void onLog(LogPilotRecord record) {
httpClient.post(apiUrl, body: record.toJsonString());
}
@override
void dispose() {}
}
Choosing the Right Sink #
Warning:
CallbackSinkfires synchronously inside the log dispatch pipeline. If your callback updates aValueNotifieror callssetState(), you will crash with "setState() during build." UseBufferedCallbackSinkfor UI state or wrap your callback inscheduleMicrotask().
| Sink | Delivery | Best for |
|---|---|---|
CallbackSink |
Synchronous, per-record | Fire-and-forget: crash reporters, analytics |
AsyncLogSink |
Microtask-batched | Expensive I/O: HTTP uploads, file writes |
BufferedCallbackSink |
Timer + size-based batches | UI state — avoids setState-during-build |
// Microtask-batched
AsyncLogSink(flush: (records) {
for (final r in records) { analyticsService.track(r.message ?? ''); }
})
// Timer + size-based
BufferedCallbackSink(
maxBatchSize: 50,
flushInterval: Duration(milliseconds: 500),
onFlush: (batch) { setState(() => logRecords.addAll(batch)); },
)
Each sink's onLog is wrapped in a try-catch — a broken sink cannot silence the pipeline.
File Logging #
FileSink writes to local files with automatic rotation. Mobile/desktop
only (requires dart:io):
import 'dart:io';
import 'package:log_pilot/log_pilot.dart';
import 'package:log_pilot/log_pilot_io.dart';
final fileSink = FileSink(
directory: Directory('/path/to/logs'),
maxFileSize: 2 * 1024 * 1024, // 2 MB per file
maxFileCount: 5,
format: FileLogFormat.text, // or .json for NDJSON
baseFileName: 'LogPilot',
);
LogPilot.init(
config: LogPilotConfig(sinks: [fileSink]),
child: const MyApp(),
);
// Export all logs for bug reports:
final allLogs = await fileSink.readAll();
Config Presets #
LogPilotConfig.debug() // verbose, all details, colors on
LogPilotConfig.staging() // info+, compact, 5s dedup window
LogPilotConfig.production( // console off, warning+, sinks only
sinks: [myCrashlyticsSink],
)
LogPilotConfig.web() // info+, plain output, no caller capture, 5s dedup
| Factory | Log Level | Caller | Details | Dedup | History / Breadcrumbs | Best for |
|---|---|---|---|---|---|---|
LogPilotConfig() |
verbose | Yes | Yes | off | 500 / 20 | Default |
.debug() |
verbose | Yes | Yes | off | 500 / 20 | IDE development |
.staging() |
info | Yes | No | 5s | 500 / 20 | QA builds |
.production() |
warning | No | No | 5s | 500 / 20 | Release (console off) |
.web() |
info | No | No | 5s | 200 / 10 | Flutter Web (also: stackTraceDepth: 4, maxPayloadSize: 4096) |
Tagged Logging & Focus Mode #
LogPilot.info('Starting payment', tag: 'checkout');
// Only show specific tags during development:
LogPilotConfig(onlyTags: {'checkout', 'auth'})
Rate Limiting / Deduplication #
Collapse identical messages within a time window:
LogPilotConfig(deduplicateWindow: Duration(seconds: 5))
When the same message + level repeats, only the first is printed. After the window, a summary appears:
│ RenderFlex overflowed by 42.0 pixels
│ ... repeated 47 times
Deduplication applies to both console output and sink dispatch. The in-memory history still receives every record.
Log History / Ring Buffer #
final records = LogPilot.history;
final errors = LogPilot.historyWhere(level: LogLevel.error);
final text = LogPilot.export();
final json = LogPilot.export(format: ExportFormat.json);
LogPilot.clearHistory();
historyWhere supports rich filtering — all parameters combine with AND
logic. The level parameter is a minimum severity filter, not an
exact match — LogLevel.warning returns warnings, errors, AND fatals:
LogPilot.historyWhere(
level: LogLevel.warning,
tag: 'http',
messageContains: 'timeout',
traceId: 'req-abc',
hasError: true,
after: DateTime.now().subtract(const Duration(minutes: 5)),
before: DateTime.now(),
metadataKey: 'statusCode',
);
Configure the buffer size (default 500, set to 0 to disable):
LogPilotConfig(maxHistorySize: 1000)
Session & Trace IDs #
Every app launch gets a unique session UUID:
print(LogPilot.sessionId); // "a1b2c3d4-e5f6-4a7b-..."
For per-request correlation, use the scoped helper:
await LogPilot.withTraceId('req-12345', () async {
await processPayment(); // all logs carry traceId 'req-12345'
await sendReceipt();
});
// traceId is null here — even if processPayment threw
A synchronous variant is also available:
final total = LogPilot.withTraceIdSync('calc-1', () => computeTotal(cart));
Network interceptors automatically inject X-LogPilot-Session and
X-LogPilot-Trace headers.
Navigation Logging #
Auto-log every route transition:
MaterialApp(
navigatorObservers: [LogPilotNavigatorObserver()],
)
Customize:
LogPilotNavigatorObserver(
logLevel: LogLevel.info,
tag: 'Nav',
logArguments: false, // hide sensitive route arguments
)
BLoC Observer #
Not yet published — this import will fail if you installed
log_pilotfrom pub.dev. The BLoC integration is in the GitHub repo source only.To use it now: copy
lib/src/state/log_pilot_bloc_observer.dartinto your project and adjust the import. A standalonelog_pilot_blocpackage is planned.
Log BLoC/Cubit lifecycle events:
// ⚠ REPO ONLY — this import does not work from the pub.dev package.
// See the note above for how to use this integration today.
import 'package:log_pilot/log_pilot_bloc.dart';
void main() {
Bloc.observer = LogPilotBlocObserver();
LogPilot.init(child: const MyApp());
}
Customize:
LogPilotBlocObserver(
tag: 'state',
logEvents: true,
logTransitions: true,
logCreations: false,
transitionLevel: LogLevel.debug,
)
Performance Timing #
LogPilot.time('fetchUsers');
final users = await api.fetchUsers();
LogPilot.timeEnd('fetchUsers'); // logs: "fetchUsers: 342ms"
Exception-safe scoped timing:
final users = await LogPilot.withTimer('fetchUsers', work: () => api.getUsers());
final config = LogPilot.withTimerSync('parseConfig', work: () => parse(raw));
Multiple timers run concurrently. Scoped loggers prefix automatically:
final log = LogPilot.create('DB');
log.time('query'); // label: "DB/query"
log.timeEnd('query'); // logs: "DB/query: 12ms" with tag "DB"
timeCancel removes a timer without logging the elapsed time. If no
matching timer exists, a verbose-level hint is logged to help detect
misspelled labels or double-cancels. Note: timeEnd logs at warning
level for a missing timer, while timeCancel logs at verbose — this
is intentional since timeEnd represents an expected measurement that's
missing.
Error Breadcrumbs #
Automatic trail of events before each error:
LogPilot.info('User tapped checkout', tag: 'UI');
LogPilot.info('Cart validated', tag: 'Cart');
LogPilot.error('Payment failed', error: e, stackTrace: st);
// ↑ Breadcrumbs for the 2 prior events are attached
Manual breadcrumbs:
LogPilot.addBreadcrumb('Button tapped', category: 'ui');
LogPilot.addBreadcrumb('Theme changed', category: 'state', metadata: {'theme': 'dark'});
Configure: LogPilotConfig(maxBreadcrumbs: 30) (default 20, 0 to disable).
Error IDs #
Each error/fatal log receives a deterministic hash-based ID:
LogPilot.error('Network timeout', error: TimeoutException('connect'));
// Record includes: errorId: "lk-a1b2c3"
The same error signature always produces the same ID across sessions. Numeric variations are normalized — "index 5 out of range 10" and "index 3 out of range 8" produce the same ID.
Sensitive Field Masking #
LogPilotConfig(
maskPatterns: [
'password', // substring — masks any key containing "password"
'=accessToken', // exact — masks only the key "accessToken"
'~^(refresh|auth)_.*', // regex — matches keys via RegExp
'Authorization',
'secret',
],
)
| Prefix | Match type | Example | Masks |
|---|---|---|---|
| (none) | Substring | 'token' |
accessToken, tokenExpiry, refresh_token |
= |
Exact key | '=accessToken' |
accessToken only |
~ |
Regex | '~^api_key$' |
api_key only |
Recursive masking applies to both headers and nested JSON bodies.
Error Silencing #
Suppress known, noisy errors from console — crash reporters still receive them:
LogPilotConfig(silencedErrors: {'RenderFlex overflowed', 'HTTP 404'})
Runtime Log-Level Override #
Change verbosity without code edits or restart:
LogPilot.setLogLevel(LogLevel.verbose); // crank up for debugging
// ... reproduce the issue ...
LogPilot.setLogLevel(LogLevel.warning); // quiet down
Lazy Message Evaluation #
LogPilot.debug(() => 'Cache: ${cache.entries.map((e) => e.key).join(", ")}');
The closure is only called if debug level is active.
Instrumentation Helpers #
Wrap any expression with automatic timing, result logging, and error capture:
final config = LogPilot.instrument('parseConfig', () => parseConfig(raw));
final users = await LogPilot.instrumentAsync('fetchUsers', () => api.getUsers());
On success: logs at debug level with return value and elapsed time.
On failure: logs at error level with exception and stack trace, then rethrows.
Self-Diagnostics #
Monitor LogPilot's own performance and automatically degrade verbosity when throughput spikes:
LogPilot.enableDiagnostics(
autoDegrade: true,
throughputThreshold: 50, // records per second before degrading
);
final snap = LogPilot.diagnostics?.snapshot;
// LogPilotDiagnosticsSnapshot(records: 142, avgSinkLatency: 34us, ...)
LogPilot.disableDiagnostics();
When throughput exceeds the threshold, the minimum log level is
automatically raised to warning, reducing verbosity by filtering out
verbose/debug/info messages. When throughput drops below half the
threshold, the original level is restored.
Crash Reporter Integration #
Use init()'s onError callback for simple apps, or configure() inside
your own runZonedGuarded for Firebase/async startup. See
Option A
and Option C
in Quick Start for complete code examples.
Diagnostic Snapshot #
One-call structured summary of recent LogPilot activity:
final snap = LogPilot.snapshot();
// Returns Map with: sessionId, traceId, config, history counts,
// recentErrors (last 5), recentLogs (last 10), activeTimers
final jsonStr = LogPilot.snapshotAsJson();
Group recent logs by tag:
final snap = LogPilot.snapshot(groupByTag: true, perTagLimit: 3);
// snap['recentByTag']['Auth'] -> {total: 15, recent: [...last 3...]}
Web Platform #
The core package:log_pilot/log_pilot.dart is fully web-compatible — zero dart:io
dependency. All features work on Flutter Web:
- Console output, log history, navigation observer, timing
- In-app log viewer overlay
- Network logging with
LogPilotHttpClient - DevTools extension
- BLoC observer (repo-only — see Package Imports)
File logging requires dart:io — import package:log_pilot/log_pilot_io.dart
for mobile/desktop only. Use LogPilotConfig.web() for optimized web defaults.
Testing #
tearDown(() {
LogPilot.reset(); // clears config, history, timers, and trace IDs
});
Configuration Reference #
| Option | Type | Default | Description |
|---|---|---|---|
enabled |
bool |
kDebugMode |
Master switch. Off in release. |
logLevel |
LogLevel |
verbose |
Minimum severity to print. |
outputFormat |
OutputFormat |
pretty |
pretty / plain / json |
showTimestamp |
bool |
true |
Show HH:mm:ss.SSS |
showCaller |
bool |
true |
Clickable source location |
showDetails |
bool |
true |
Error body, stack traces |
colorize |
bool |
true |
ANSI colors |
maxLineWidth |
int |
100 |
Box width in characters |
stackTraceDepth |
int |
8 |
Max stack frames shown |
maxPayloadSize |
int |
10240 |
Truncate payloads (bytes) |
maskPatterns |
List<String> |
['Authorization', 'password', 'token', 'secret'] |
Fields to mask (=exact, ~regex, or substring) |
jsonKeyColor |
AnsiColor |
cyan |
JSON key color |
jsonValueColor |
AnsiColor |
green |
JSON value color |
silencedErrors |
Set<String> |
{} |
Suppress matching errors |
onlyTags |
Set<String> |
{} |
Only print matching tags |
sinks |
List<LogSink> |
[] |
Additional output destinations |
deduplicateWindow |
Duration |
Duration.zero |
Collapse identical messages (console + sinks) |
maxHistorySize |
int |
500 |
Ring buffer size (0 = off) |
maxBreadcrumbs |
int |
20 |
Breadcrumb buffer (0 = off) |
Package Imports #
| Import | What you get | Web safe? | Published? |
|---|---|---|---|
package:log_pilot/log_pilot.dart |
Core: LogPilot, LogPilotLogger, LogPilotConfig, LogPilotRecord, LogLevel, LogSink, CallbackSink, AsyncLogSink, BufferedCallbackSink, LogHistory, ExportFormat, LogPilotNavigatorObserver, LogPilotOverlay, LogPilotHttpClient, ANSI helpers |
Yes | Yes |
package:log_pilot/log_pilot_io.dart |
FileSink, FileLogFormat (requires dart:io) |
No | Yes |
package:log_pilot/log_pilot_dio.dart |
LogPilotDioInterceptor (add dio to pubspec) |
Yes | No — repo only* |
package:log_pilot/log_pilot_chopper.dart |
LogPilotChopperInterceptor (add chopper) |
Yes | No — repo only* |
package:log_pilot/log_pilot_graphql.dart |
LogPilotGraphQLLink (add gql, gql_exec, gql_link) |
Yes | No — repo only* |
package:log_pilot/log_pilot_bloc.dart |
LogPilotBlocObserver (add bloc) |
Yes | No — repo only* |
* These imports WILL FAIL from the pub.dev package. They are in the source repo but
.pubignored from the published package. Copy the source files into your project, or wait for the standalone packages (e.g.log_pilot_dio) in a future release.
Example App #
A full runnable example with tappable buttons for every feature lives in
example/:
cd example && flutter run

Features at a Glance #
| Feature | What it does |
|---|---|
| MCP server | AI agents query, filter, watch, and control live logs via MCP protocol |
| DevTools extension | Real-time log viewer tab inside Dart DevTools — zero config |
| In-app log viewer | LogPilotOverlay debug sheet with filters, search, and live updates |
| LLM export | Compress log history for AI context windows |
| One-line setup | Replace runApp() with LogPilot.init() — every error is auto-formatted |
| Pretty Flutter errors | 15+ contextual hints, simplified stacks, clickable source locations |
| Level-based logging | verbose / debug / info / warning / error / fatal with structured metadata and tags |
| Scoped loggers | const LogPilotLogger('Tag') or LogPilot.create('Tag') for class-level auto-tagging |
| Log sinks | Route records to files, Crashlytics, Sentry, or any backend |
| Built-in file logging | FileSink with automatic rotation by size, text or JSON format |
| Lazy messages | LogPilot.debug(() => expensiveString()) — skips work when filtered |
| Network interceptors | http (published); Dio, Chopper, GraphQL (repo / future packages) |
| JSON highlighting | Auto-detect and colorize keys/values |
| Sensitive field masking | Recursive masking in headers and JSON bodies |
| Config presets | LogPilotConfig.debug(), .staging(), .production(), .web() |
| Rate limiting / dedup | Collapse identical messages within a time window |
| Log history | In-memory ring buffer — filter, export, attach to bug reports |
| Output formats | pretty, plain, json — human and machine modes |
| Diagnostic snapshot | LogPilot.snapshot() — one-call summary for bug reports |
| Error breadcrumbs | Automatic trail of events before each error |
| Error IDs | Deterministic lk-XXXXXX hash for cross-session tracking |
| Runtime log-level override | Change verbosity at runtime without restart |
| Instrumentation helpers | One-line timing + error capture for any expression |
| Session & trace IDs | Auto-generated session UUID + per-request trace IDs |
| Navigation logging | Auto-logs push/pop/replace with route names & arguments |
| BLoC observer | Logs create/close, events, state changes, and errors |
| Performance timing | LogPilot.time / LogPilot.timeEnd — like console.time |
| Web compatible | Core barrel is dart:io-free — works on Flutter Web |
| Lightweight core | Zero required dependencies beyond Flutter SDK; Dio, Chopper, GraphQL, BLoC integrations available in source repo (standalone packages planned) |
Contributing #
Contributions are welcome. See CONTRIBUTING.md for architecture details and development setup.
License #
MIT — see LICENSE.
Migrating from plog #
If you're upgrading from the plog package, here's a quick mapping:
| plog | log_pilot |
|---|---|
import 'package:plog/plog.dart' |
import 'package:log_pilot/log_pilot.dart' |
Plog.init(child: ...) |
LogPilot.init(child: ...) |
Plog.info(...) |
LogPilot.info(...) |
PlogLogger('Tag') |
LogPilotLogger('Tag') (still const-constructible) |
Plog.create('Tag') |
LogPilot.create('Tag') |
PlogConfig(...) |
LogPilotConfig(...) |
PlogRecord |
LogPilotRecord |
plog_dio.dart |
log_pilot_dio.dart (repo only) |
plog_bloc.dart |
log_pilot_bloc.dart (repo only) |
PlogNavigatorObserver |
LogPilotNavigatorObserver |
PlogOverlay |
LogPilotOverlay |
PlogHttpClient |
LogPilotHttpClient |
All APIs are functionally identical — only the names changed. A
project-wide find-and-replace of Plog → LogPilot and plog →
log_pilot covers the vast majority of cases.











