app_factory_logging
Riverpod-native debug print baseline for Flutter apps.
This package gives you a small AppLogger abstraction, a console implementation
backed by Flutter's debugPrint, generated Riverpod providers, and a test fake.
Logging stays separate from telemetry (Analytics / Crashlytics): this package
never depends on Firebase.
Features
- Five public levels:
trace/debug/info/warn/error - Default
ConsoleAppLoggerprints[LEVEL][tag] messageviadebugPrint - Debug builds print by default; release builds stay silent by default
- Optional ANSI styles by level (dim / gray / cyan / yellow / red), off by default
- Per-instance
minLevelandenableConsoleOutput forOwner(this)binds the runtime class name once as the tag- Message truncation (default 2000 chars) and
debugChunkedfor large payloads - Generated keepAlive providers:
appLoggerProvider/appLogSinkProvider FakeAppLoggerfor unit tests- Optional debug-only
AppLoggerProviderObserver AppLogSinkinterface for breadcrumb bridges (e.g. Crashlytics in another package)
What This Package Does Not Do
- Production analytics or crash reporting
- Log UI, local persistence, or remote log shipping
- Third-party logging ecosystems (Talker, etc.)
- Structured DevTools
developer.log(may be added later if needed)
Release builds do not print by default (enableConsoleOutput defaults to
kDebugMode). In release, prefer an optional AppLogSink for breadcrumbs.
Disable console output:
ConsoleAppLogger(enableConsoleOutput: false);
Enable ANSI colors (some IDEs / CI show escape codes as plain text):
ConsoleAppLogger(useAnsiColors: true);
Installation
dependencies:
app_factory_logging: ^0.1.0
flutter pub get
Quick Start
Create one logger instance, override the provider with it, and optionally attach the provider observer in debug:
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 appLogger = ConsoleAppLogger(
// useAnsiColors: true,
// sink: MyCrashlyticsBreadcrumbSink(...),
);
final container = ProviderContainer(
overrides: [
appLoggerProvider.overrideWithValue(appLogger),
],
observers: [
if (kDebugMode)
AppLoggerProviderObserver(
appLogger,
include: {
'authControllerProvider',
'bootstrapControllerProvider',
},
),
],
);
runApp(
UncontrolledProviderScope(
container: container,
child: const MyApp(),
),
);
}
Custom minimum level:
appLoggerProvider.overrideWithValue(
ConsoleAppLogger(minLevel: AppLogLevel.info),
);
Using the logger in features
Bind the logger once in a class to automatically use its runtime class name as the tag. Later calls only need a message:
final class AsrClient {
AsrClient(AppLogger logger) {
_log = logger.forOwner(this);
}
late final AppLogScope _log;
Future<void> transcribe() async {
_log.trace('request start');
try {
// ...
_log.info('transcription completed');
} catch (e, st) {
_log.error('transcription failed', error: e, stackTrace: st);
rethrow;
}
}
}
The example prints tags such as [AsrClient]. Create the scope once per class
instance rather than once per log statement. Generic owners may include type
arguments in the tag (for example Repo<User>), so different type arguments
can produce different tags. Runtime class names may also change under
minification or obfuscation. Use explicit stable tags when logs must be queried
consistently across release builds:
final log = ref.read(appLoggerProvider);
log.trace('rtc', 'socket state=connecting');
log.info('asr', 'transcription started');
log.error(
'rtc',
'join channel failed',
error: e,
stackTrace: st,
);
// Large payloads (network bodies, etc.): default chunk size is 800
log.debugChunked('net', responseBody);
Do not put tokens, passwords, emails, or full URLs in log messages.
Provider Observer
AppLoggerProviderObserver is off by default. Mount it only in debug:
| Callback | Level | Filtered by include? |
|---|---|---|
didAddProvider |
debug | No |
didDisposeProvider |
debug | No |
didUpdateProvider |
debug | Yes (null = all updates; noisy) |
providerDidFail |
error | No |
include matches context.provider.name. Codegen providers already have names
such as authControllerProvider. Hand-written providers without name: never
match a whitelist.
Failures observed here are breadcrumbs only (see sink contract below). Your app still owns explicit error reporting.
AppLogSink contract
AppLogSink is a breadcrumb channel. Implementations may do lightweight
logging (for example FirebaseCrashlytics.instance.log) and must never call
error-reporting APIs such as recordError. Error reporting stays at the
business boundary so each failure is recorded once.
Override the sink from bootstrap:
final container = ProviderContainer(
overrides: [
appLogSinkProvider.overrideWithValue(MyCrashlyticsBreadcrumbSink()),
// Or pass sink into ConsoleAppLogger(...) and override appLoggerProvider.
],
);
This package ships no Firebase (or other) sink implementation.
Testing
final fake = FakeAppLogger();
final container = ProviderContainer(
overrides: [
appLoggerProvider.overrideWithValue(fake),
],
);
container.read(appLoggerProvider).info('rtc', 'hello');
expect(fake.entries, hasLength(1));
Suggested lint gates
Keep feature code on AppLogger instead of ad-hoc prints:
# Ban print/debugPrint in lib (allow your bootstrap path if needed)
rg -n "\bdebugPrint\(|\bprint\(" lib --glob "*.dart"
Enable avoid_print in analysis_options.yaml (flutter_lints includes it).
License
Licensed under the Apache License, Version 2.0. See LICENSE and NOTICE.
Libraries
- app_factory_logging
- App Factory debug logging baseline.