app_factory_logging
Privacy-safe logging for Flutter apps, with legacy AppLogger calls and the
structured app.runtime-log.v1 event contract.
The package provides centralized redaction and bounds before data reaches the console, test fake, or sink. It stays separate from analytics and crash reporting and ships no local persistence or remote log transport.
Features
- Five levels:
trace/debug/info/warn/error - Existing
logger.info('repository', 'load completed')calls remain valid app.runtime-log.v1events with stable machine-readableeventNamevalues- Central sanitization for messages, errors, stack traces, attributes, and URLs
- Bounded JSON-safe attributes and whole-event size limits
- Single-line
APP_RUNTIME_LOG {json}console framing for structured events ConsoleAppLogger, sanitizedFakeAppLogger, and Riverpod providersAppLogEventSinkfor structured capture and legacyAppLogSinkbreadcrumbsSanitizingAppLoggerfor existing custom or third-party logger backends- Provider Observer that records lifecycle metadata without provider values
- Full-message sanitization before bounded
debugChunkedsplitting
Installation
dependencies:
app_factory_logging: ^0.2.0
flutter pub get
Bootstrap
Create one logger and expose it through Riverpod. Console output defaults to debug builds; release builds stay silent unless a sink is explicitly supplied.
import 'package:app_factory_logging/app_factory_logging.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
void main() {
final logger = ConsoleAppLogger(
// sink: MyBreadcrumbSink(), // warn/error only
// eventSink: MyBoundedEventSink(), // every allowed level
);
final container = ProviderContainer(
overrides: [appLoggerProvider.overrideWithValue(logger)],
observers: [
if (kDebugMode)
AppLoggerProviderObserver(
logger,
include: {
'authControllerProvider',
'bootstrapControllerProvider',
},
),
],
);
runApp(
UncontrolledProviderScope(
container: container,
child: const MyApp(),
),
);
}
appLogSinkProvider and appLogEventSinkProvider both default to null and
may instead be overridden during bootstrap.
Migrating from 0.1.x
Existing AppLogger calls and the AppLogSink.write method signature remain
source-compatible. appLogEventSinkProvider and logger.event(...) are new
optional APIs; custom AppLogger implementations continue to receive
logger.event(...) as the equivalent legacy-level call unless they implement
StructuredAppLogger.
0.2.0 changes output behavior: built-in fakes and AppLogSink receive
sanitized error and stack text rather than the original object identities, and
debugChunked sanitizes then caps output at 16 chunks by default. The existing
ConsoleAppLogger.maxMessageLength parameter is now validated against the
published Schema range of 0–4096 characters. No Dart/Flutter/Riverpod
constraint change is required from 0.1.1.
Legacy calls
Existing business calls keep their source shape:
final logger = ref.read(appLoggerProvider);
logger.trace('rtc', 'socket state=connecting');
logger.info('repository', 'load completed');
logger.error(
'repository',
'load failed',
error: error,
stackTrace: stackTrace,
);
Bind a class-derived diagnostic tag once when stable cross-release querying is not required:
final class AsrClient {
AsrClient(AppLogger logger) {
_log = logger.forOwner(this);
}
late final AppLogScope _log;
void start() => _log.info('transcription started');
}
Runtime type names may change with generics, minification, or obfuscation. Use
explicit tags and eventName values for stable machine queries.
Structured events
Use a stable eventName; keep summary short and human-readable:
logger.event(
level: AppLogLevel.error,
eventName: 'repository.load.failed',
tag: 'repository',
summary: 'load failed',
stage: 'repository',
action: 'load',
status: 'failed',
durationMs: 1200,
errorCode: 'timeout',
correlationId: journeyCorrelationId,
attributes: {
'page': 2,
'cached': false,
'retryCount': 1,
},
error: error,
stackTrace: stackTrace,
);
Structured console output is one prefixed JSON line without ANSI styling:
APP_RUNTIME_LOG {"schemaVersion":"app.runtime-log.v1",...}
The published contract is available at
schema/app.runtime-log.v1.schema.json.
Required fields are filled by the logger:
schemaVersion,timestamp,level,eventName,tag,summaryredactionApplied,truncationApplied
Optional fields are stage, action, status, durationMs, errorCode,
correlationId, attributes, error, and stackTrace.
eventName must be lowercase and dot/dash/underscore separated, for example
router.navigation.failed. Invalid names become logging.invalid_event
without throwing into business code.
Privacy and bounds
The default redactor covers common Authorization/Bearer/Cookie values, token or
password assignments (including quoted JSON-style keys and quoted values),
JWTs, URL query/fragment data, email addresses, and phone numbers. Credential
and header keys are matched case-insensitively with ordinary whitespace around
: or =; quoted values may contain escaped characters. Escaped or encoded
credential key names and arbitrary prose are outside this conservative safety
net. Sensitive attribute names have their values replaced entirely.
Attributes accept only JSON-safe null/bool/finite-number/string/list/map data.
The processor limits string lengths, collection sizes, nesting depth, node
count, and final serialized event size. Unsupported values are replaced rather
than converted with arbitrary toString() calls.
Configured field and collection limits are checked against the published
Schema and package safety minimums. Truncation markers are included in those
field limits, and maxEventJsonLength must be at least 512 JSON characters.
When an event exceeds its total budget, the processor removes attributes, then
bounds stack trace, error, and summary text, rechecking the encoded JSON after
each step. If the remaining optional metadata still cannot fit, it emits a
fixed schema-valid logging.event_truncated fallback.
Sanitization is fail-closed for data: if a redactor or serializer fails, the
original payload is dropped and a generic logging.payload_dropped event is
emitted. Sink failures remain fail-open for the application.
redactionApplied: false means no configured rule changed the input. It is not
proof that arbitrary input is free of sensitive information. Prefer bounded
metadata and summaries; do not log complete requests, responses, user content,
tokens, passwords, or full URLs.
Provider Observer
AppLoggerProviderObserver is opt-in and should normally be mounted only in
debug builds. Its include whitelist applies to every callback:
| Callback | Level | Provider value recorded? |
|---|---|---|
didAddProvider |
debug | No |
didDisposeProvider |
debug | No |
didUpdateProvider |
debug | No |
providerDidFail |
error | Error and stack are sanitized |
Failures are breadcrumbs only. Business boundaries still own explicit error
reporting, and a sink must never call recordError.
Bounded long diagnostics
debugChunked remains available for temporary, bounded diagnostic text. It
sanitizes the complete input before splitting and defaults to at most 16 chunks
of 800 characters:
logger.debugChunked('parser', boundedDiagnosticSummary, maxChunks: 4);
It is not a supported mechanism for complete network responses or user data.
Existing custom loggers
An arbitrary implementation of AppLogger cannot be made safe by this package
unless calls pass through the package processor. Wrap an existing backend:
final logger = SanitizingAppLogger(
existingLogger,
eventSink: boundedEventSink,
);
The delegate receives sanitized legacy text. The optional event sink receives the complete sanitized structured event.
Sink compatibility
AppLogSink.write remains source-compatible and still receives warn/error
only. Starting with 0.2.0, its error may be a sanitized String and its
stackTrace is reconstructed from sanitized text, so sinks must not rely on
the caller's original object identity.
AppLogEventSink receives immutable sanitized events for every allowed level.
The package ships no Firebase, local database, or remote shipping implementation.
Testing
final fake = FakeAppLogger();
fake.event(
level: AppLogLevel.info,
eventName: 'app.bootstrap.completed',
tag: 'bootstrap',
summary: 'ready',
);
expect(fake.events.single.eventName, 'app.bootstrap.completed');
expect(fake.entries.single.message, 'ready');
FakeAppLogger records sanitized values so privacy behavior can be verified in
tests. Error and stack object identity is intentionally not preserved.
Out of scope
- Analytics and crash reporting
- Log UI or local persistence
- Remote log shipping
- Factory Event Log, Gate Fact, Evidence, or Correction Hint semantics
- Source-token scanning as proof that logging is correctly integrated
License
Licensed under the Apache License, Version 2.0. See LICENSE and NOTICE.
Libraries
- app_factory_logging
- Privacy-safe legacy and structured logging for Flutter applications.