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 ConsoleAppLogger prints [LEVEL][tag] message via debugPrint
  • 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 minLevel and enableConsoleOutput
  • forOwner(this) binds the runtime class name once as the tag
  • Message truncation (default 2000 chars) and debugChunked for large payloads
  • Generated keepAlive providers: appLoggerProvider / appLogSinkProvider
  • FakeAppLogger for unit tests
  • Optional debug-only AppLoggerProviderObserver
  • AppLogSink interface 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

See LICENSE.

Libraries

app_factory_logging
App Factory debug logging baseline.