oklog

pub package

A simple yet capable logging utility for Dart and Flutter. Just log. ok.

Features

  • Six log levels: trace, debug, info, notice, warn, error
  • Colored, emoji-decorated console output with level labels (e.g. [INFO]) via ConsoleSink and ConsoleFormatter
  • Filter logs by level via LevelFilterProcessor and by class name via NameFilterProcessor
  • Extensible pipeline: add LogProcessor instances to transform/filter, and LogSink instances to route output
  • Global log instance (OkLogger) ready to use out of the box
  • Observability support: structured events and metrics via log.obs
  • Error alerting with context: ContextBufferProcessor + ErrorAlertSink + ErrorExporter; composable HTTP transport via HttpErrorExporter + ErrorFormatter
  • Slack integration: SlackPayloadFormatter and SlackErrorExporter available via package:oklog/oklog_slack.dart
  • PII (Personally Identifiable Information) support: mark sensitive values with pii() at the call site; each sink handles masking independently

Getting started

Add the dependency to your pubspec.yaml:

dependencies:
  oklog: ^1.1.0

Then import the library:

import 'package:oklog/oklog.dart';

For Slack integration, add the additional import:

import 'package:oklog/oklog_slack.dart';

Usage

Basic logging

import 'package:oklog/oklog.dart';

void main() {
  log.level = LogLevel.trace; // show all levels

  log.trace('main', 'Trace message');
  log.debug('main', 'Debug message');
  log.info('main', 'Info message');
  log.notice('main', 'Notice message');
  log.warn('main', 'Warning message');

  try {
    throw Exception('Something went wrong!');
  } catch (e, st) {
    log.error('main', 'An error occurred.', error: e, stackTrace: st);
  }
}

Using with a class instance

Pass this as the first argument and the class name is resolved automatically:

class MyClass {
  void myMethod() {
    log.info(this, 'Hello from MyClass');
  }
}

Attaching structured attributes

All log methods accept an optional attrs map for structured key-value metadata:

log.debug(this, 'User action', attrs: {'userId': 123, 'action': 'login'});

PII (Personally Identifiable Information)

Wrap sensitive values with pii() to mark them as personally identifiable information. Each sink decides independently how to handle PiiValue entries:

  • ConsoleSink — reveals the raw value (safe for local development).
  • ErrorAlertSink — replaces every PiiValue with [REDACTED] before forwarding to the ErrorExporter, so sensitive data never leaves the device in error reports.
log.info(
  this,
  'login',
  attrs: {
    'email': pii(email),       // PII — masked in error exports
    'session_id': sessionId,   // non-PII — forwarded as-is
  },
);

Logger itself is PII-agnostic. Masking is a per-sink concern, so you can add your own custom sinks that call maskPiiAttrs(entry.attrs) (or ignore PiiValue entirely) depending on the destination.

Filtering by class name

Use log.nameFilter (a NameFilterProcessor) to restrict which classes are logged:

// Only log messages from classes whose name contains 'main'
log.nameFilter.allowList = ['main'];

// Suppress log messages from classes whose name contains 'MyClass'
log.nameFilter.denyList = ['MyClass'];

// Clear filters
log.nameFilter.allowList = [];
log.nameFilter.denyList = [];

Silencing output

log.sinks.clear(); // remove all sinks to suppress output

Changing the log level

log.level = LogLevel.warn; // only warn and error are printed

Custom sinks

Implement LogSink and override emit to route entries anywhere:

class FileSink extends LogSink {
  @override
  void emit(LogEntry entry) {
    if (entry is LogRecord) {
      // write to a file, remote service, etc.
    }
  }
}

log.sinks.add(FileSink());

Multiple sinks can be active at the same time. The built-in ConsoleSink is added automatically by OkLogger.

Custom formatters

ConsoleSink accepts a LogFormatter<String> that converts a LogEntry to a string. Replace the default ConsoleFormatter to change how entries are rendered without touching sink behaviour:

class JsonFormatter extends LogFormatter<String> {
  @override
  String format(LogEntry entry) {
    if (entry is LogRecord) {
      return jsonEncode({
        'level': entry.level.name,
        'class': entry.className,
        'message': entry.message,
      });
    }
    return entry.toString();
  }
}

log.sinks
  ..clear()
  ..add(ConsoleSink(formatter: JsonFormatter()));

Custom processors

Implement LogProcessor and return false to drop an entry from the pipeline:

class SamplingProcessor implements LogProcessor {
  @override
  bool process(LogEntry entry) => Random().nextDouble() > 0.9; // keep 10%
}

log.processors.add(SamplingProcessor());

Error alerting with context

ContextBufferProcessor keeps a ring buffer of recent LogRecord entries. ErrorAlertSink detects error-level records and forwards them — together with that buffer — to an ErrorExporter, giving the recipient rich context about what happened before the error.

SlackErrorExporter

SlackErrorExporter is available via package:oklog/oklog_slack.dart. It sends a formatted Block Kit message to a Slack channel via an Incoming Webhook. The notification includes the error message, error object, stack trace, and the recent context logs captured by ContextBufferProcessor.

import 'package:oklog/oklog.dart';
import 'package:oklog/oklog_slack.dart';

final buffer = ContextBufferProcessor();
final exporter = SlackErrorExporter(
  'https://hooks.slack.com/services/YOUR/WEBHOOK/URL',
);

log.processors.add(buffer);
log.sinks.add(
  ErrorAlertSink(
    buffer,
    exporter,
    metadata: {
      'app': 'MyApp',
      'version': '1.0.0',
      'env': 'production',
    },
  ),
);

log.info('main', 'Application started.');
log.warn('main', 'Cache miss — fetching from origin.');
try {
  throw Exception('Database connection failed');
} catch (e, st) {
  // Sends a Slack message that includes the error plus the info/warn above.
  log.error('main', 'Unhandled error.', error: e, stackTrace: st);
}

Use payloadBuilder to merge dynamic top-level fields into the payload on every send, or headersBuilder to inject dynamic HTTP headers (e.g. auth tokens, routing keys) when going through a proxy:

final exporter = SlackErrorExporter(
  'https://proxy.example.com/slack',
  payloadBuilder: () => {
    'channel': '#alerts',
    'username': 'ErrorBot',
  },
  headersBuilder: () => {
    'Authorization': 'Bearer ${tokenProvider.current}',
    'X-Routing-Key': 'my-service',
  },
);

HttpErrorExporter + custom ErrorFormatter

HttpErrorExporter is a generic HTTP transport that accepts any ErrorFormatter implementation. Use it to send structured payloads to any webhook-based service without duplicating transport logic.

An optional payloadBuilder callback lets you merge extra fields or reshape the payload before delivery without subclassing. An optional headersBuilder callback lets you inject dynamic HTTP headers (e.g. auth tokens) on every send:

final exporter = HttpErrorExporter(
  'https://discord.com/api/webhooks/YOUR/WEBHOOK',
  DiscordFormatter(),
  payloadBuilder: (payload) => {...payload, 'username': 'ErrorBot'},
  headersBuilder: () => {'Authorization': 'Bearer $token'},
);

Implement ErrorFormatter to control the request body:

class DiscordFormatter implements ErrorFormatter {
  @override
  Map<String, dynamic> format(
    LogRecord error,
    List<LogRecord> contextLogs,
    Map<String, String> metadata,
  ) {
    return {
      'content': '**${error.className}**: ${error.message}',
    };
  }
}

final exporter = HttpErrorExporter(
  'https://discord.com/api/webhooks/YOUR/WEBHOOK',
  DiscordFormatter(),
);

final buffer = ContextBufferProcessor();
log.processors.add(buffer);
log.sinks.add(
  ErrorAlertSink(
    buffer,
    exporter,
    metadata: {'app': 'MyApp', 'version': '1.0.0'},
  ),
);

Custom ErrorExporter

For full control over the transport (non-HTTP, batching, etc.), implement ErrorExporter directly:

class MyExporter implements ErrorExporter {
  @override
  Future<void> send(
    LogRecord error,
    List<LogRecord> contextLogs,
    Map<String, String> metadata,
  ) async {
    // `error`       — the error-level LogRecord that triggered the alert
    // `contextLogs` — recent records from ContextBufferProcessor
    // `metadata`    — key-value pairs set on ErrorAlertSink (e.g. app name, version)
    await myService.report(
      message: error.message,
      context: contextLogs.map((r) => r.message).toList(),
      metadata: metadata,
    );
  }
}

final buffer = ContextBufferProcessor();
log.processors.add(buffer);
log.sinks.add(
  ErrorAlertSink(
    buffer,
    MyExporter(),
    metadata: {'app': 'MyApp', 'version': '1.0.0'},
  ),
);

Observability

Access structured observability methods through log.obs. These are separate from severity-level logs and are designed for structured data that can later be forwarded to an external observability backend without changing call-site code.

log.obs.event

Logs a named event with an optional payload and metadata attributes.

log.obs.event(
  this,              // source: pass `this`, a Type, or a String
  'user_signed_in',  // event name / message
  data: {'userId': '42', 'plan': 'pro'},
  attrs: {'env': 'prod'},
);

Console output:

[2026-03-13 10:00:00.000] 📡 [EVENT] MyClass: user_signed_in : {userId: 42, plan: pro} : {env: prod}

log.obs.metric

Logs a numeric measurement with an optional unit and metadata attributes.

log.obs.metric(
  this,               // source
  'request_duration', // metric name
  142,                // value
  unit: 'ms',
  attrs: {'endpoint': '/api/login'},
);

Console output:

[2026-03-13 10:00:00.000] 📊 [METRIC] MyClass: request_duration : 142 [ms] : {endpoint: /api/login}

Parameter reference

Parameter Type Required Description
source Object Yes Origin class. Pass this to resolve the runtime type automatically, or a Type or String.
message String Yes (event only) Human-readable event description.
name String Yes (metric only) Metric name (e.g. 'request_duration').
value num Yes (metric only) Numeric measurement.
unit String? No Unit label, e.g. 'ms', 'count' (metric only).
data Map<String, dynamic>? No Arbitrary payload (event only).
attrs Map<String, Object>? No Structured metadata, e.g. environment or version.

Log levels

Level Description
trace Fine-grained diagnostic messages
debug General debugging information
info Informational messages
notice Notable events worth highlighting
warn Warnings with optional error/stack
error Errors with error object and stack

Class diagram

classDiagram
    %% Core
    class LogEntry {
        +String className
        +DateTime timestamp
        +LogEntry(Object source)
        +resolveClassName(Object source) String
    }
    class LogRecord {
        +LogLevel level
        +String message
        +Object? error
        +StackTrace? stackTrace
        +Map~String,Object~? attrs
        +copyWithAttrs(Map~String,Object~?) LogRecord
    }
    class EventEntry {
        +String message
        +Map~String,dynamic~? data
        +Map~String,Object~? attrs
    }
    class MetricEntry {
        +String name
        +num value
        +String? unit
        +Map~String,Object~? attrs
    }
    class LogLevel {
        <<enumeration>>
        trace
        debug
        info
        notice
        warn
        error
    }

    LogEntry <|-- LogRecord
    LogEntry <|-- EventEntry
    LogEntry <|-- MetricEntry
    LogRecord --> LogLevel

    %% PII
    class PiiValue~T~ {
        +T value
        +toString() String
    }
    class pii~T~ {
        <<function>>
        pii(T value) PiiValue~T~
    }
    class maskPiiAttrs {
        <<function>>
        maskPiiAttrs(Map?, String) Map?
    }
    LogRecord ..> PiiValue : attrs may contain

    %% Pipeline
    class Logger {
        +List~LogProcessor~ processors
        +List~LogSink~ sinks
        +emit(LogEntry) Future~void~
        +trace(Object, String, attrs?)
        +debug(Object, String, attrs?)
        +info(Object, String, attrs?)
        +notice(Object, String, attrs?)
        +warn(Object, String, error?, stackTrace?, attrs?)
        +error(Object, String, error?, stackTrace?, attrs?)
        +obs ObservabilityLogger
    }
    class OkLogger {
        +LogLevel level
        +NameFilterProcessor nameFilter
    }
    class ObservabilityLogger {
        +event(Object, String, data?, attrs?)
        +metric(Object, String, num, unit?, attrs?)
    }

    Logger <|-- OkLogger
    Logger --> ObservabilityLogger : creates

    %% Processors
    class LogProcessor {
        <<abstract>>
        +process(LogEntry) bool
    }
    class LevelFilterProcessor {
        +LogLevel minLevel
        +process(LogEntry) bool
    }
    class NameFilterProcessor {
        +List~String~ denyList
        +List~String~ allowList
        +process(LogEntry) bool
    }
    class ContextBufferProcessor {
        +int capacity
        +process(LogEntry) bool
        +getRecent() List~LogRecord~
    }

    LogProcessor <|.. LevelFilterProcessor
    LogProcessor <|.. NameFilterProcessor
    LogProcessor <|.. ContextBufferProcessor
    Logger o-- LogProcessor

    %% Sinks
    class LogSink {
        <<abstract>>
        +emit(LogEntry)
    }
    class ConsoleSink {
        +LogFormatter~String~ formatter
        +emit(LogEntry)
    }
    class ConsoleFormatter {
        +format(LogEntry) String
    }
    class LogFormatter~T~ {
        <<abstract>>
        +format(LogEntry) T
    }
    class ErrorAlertSink {
        +ContextBufferProcessor buffer
        +ErrorExporter exporter
        +Map~String,String~ metadata
        +emit(LogEntry)
    }
    class ErrorExporter {
        <<abstract>>
        +send(LogRecord, List~LogRecord~, Map~String,String~) Future~void~
    }
    class ErrorFormatter {
        <<abstract>>
        +format(LogRecord, List~LogRecord~, Map~String,String~) Map~String,dynamic~
    }
    class HttpErrorExporter {
        +String url
        +ErrorFormatter formatter
        +send(LogRecord, List~LogRecord~, Map~String,String~) Future~void~
    }

    LogSink <|.. ConsoleSink
    LogSink <|.. ErrorAlertSink
    LogFormatter <|.. ConsoleFormatter
    ConsoleSink --> LogFormatter
    ErrorAlertSink --> ContextBufferProcessor
    ErrorAlertSink --> ErrorExporter
    ErrorAlertSink ..> maskPiiAttrs : uses
    ErrorExporter <|.. HttpErrorExporter
    HttpErrorExporter --> ErrorFormatter
    Logger o-- LogSink

    %% Slack integration
    class SlackPayloadFormatter {
        +format(LogRecord, List~LogRecord~, Map~String,String~) Map~String,dynamic~
    }
    class SlackErrorExporter {
        +String webhookUrl
    }

    ErrorFormatter <|.. SlackPayloadFormatter
    ErrorExporter <|.. SlackErrorExporter
    SlackErrorExporter --> SlackPayloadFormatter

Libraries

oklog
oklog — a lightweight, extensible logger for Dart.
oklog_slack
oklog Slack integration.