Skip to content

Internal Logging Mechanism

d-markey edited this page Jan 22, 2026 · 2 revisions

Squadron provides a sophisticated logging infrastructure that transparently captures log output from worker threads and forwards it to the main thread. This mechanism enables comprehensive observability of both Squadron's internal operations and your custom application logging.


How It Works

Global Logger Output Capture

When a worker starts up, Squadron registers a global output listener on the Logger class from the logger package. This listener intercepts all log events generated within the worker thread, regardless of which Logger instance produced them.

This means Squadron captures:

  • Squadron's internal logging: Worker lifecycle events, channel operations, request/response communications, errors, etc.
  • Your custom logger output: Any Logger instances you create in your worker service implementations

Log Event Forwarding

Captured log events are automatically:

  1. Serialized in the worker thread
  2. Sent to the main thread via special response messages
  3. Deserialized on the main thread
  4. Forwarded to the channelLogger if one is configured

The forwarding process preserves all log event details:

  • Log level (trace, debug, info, warning, error, fatal)
  • Message content
  • Timestamp
  • Error objects
  • Stack traces

Filtering and Control

The channelLogger on the main thread controls which messages are actually logged based on its configured log level. Even if a worker logs at Level.trace, those messages will only appear if the channelLogger is set to Level.trace or Level.all.

This design allows you to:

  • Control verbosity from the main thread without modifying worker code
  • Dynamically adjust logging levels at runtime
  • Use different log levels for different workers or pools

Practical Implications

Custom Loggers in Worker Services

If your worker service creates its own Logger instance, all output from that logger will be automatically forwarded to the main thread:

class MyService implements WorkerService {
  final myLogger = Logger(
    printer: PrettyPrinter(),
    level: Level.debug,
  );
  
  void processRequest(String data) {
    myLogger.i('Processing request with data: $data');
    // ... processing logic ...
    myLogger.d('Request completed successfully');
  }
  
  @override
  late final operations = OperationsMap({
    1: (r) => processRequest(r.args[0] as String),
  });
}

When you configure a channelLogger:

final mainLogger = Logger(
  printer: SimplePrinter(),
  level: Level.info,
);

final worker = MyServiceWorker();
worker.channelLogger = mainLogger;
await worker.start();

All messages from myLogger in the worker will appear in mainLogger on the main thread. In this example, the info message will be logged, but the debug message will be filtered out because mainLogger is set to Level.info.

Unified Logging View

This mechanism provides a unified logging view where:

  • You don't need separate logging infrastructure for worker threads
  • All worker output appears in a single logger on the main thread
  • You can use familiar logging tools and patterns
  • Background thread debugging becomes significantly easier

Performance Considerations

Log forwarding is asynchronous and non-blocking. The worker thread continues execution immediately after sending a log message without waiting for it to be processed on the main thread.

However, keep in mind:

  • Each log message involves serialization and cross-thread communication
  • Excessive logging in hot code paths can impact performance
  • Consider using appropriate log levels in production environments

Internal Logger Implementation

Squadron uses a special InternalLogger class for its own internal operations. This logger:

  • Logs all events (no filtering at the source)
  • Uses a no-op output (doesn't write locally)
  • Relies entirely on the forwarding mechanism
  • Delegates filtering to the receiving channelLogger

This design ensures that Squadron's internal logging has minimal overhead in the worker thread while still providing complete observability when needed.

Best Practices

  1. Set appropriate log levels: Use Level.debug or Level.trace during development, but consider Level.info or higher in production.

  2. Be mindful of log volume: Logging every iteration of a tight loop can generate thousands of messages and impact performance.

  3. Use deferred messages: The logger package supports function-based messages that are only evaluated if the log level permits:

    logger.d(() => 'Expensive computation: ${expensiveOperation()}');
  4. Leverage log levels appropriately:

    • trace: Very detailed debugging information
    • debug: Diagnostic information useful during development
    • info: General informational messages
    • warning: Warning messages for potentially problematic situations
    • error: Error messages for failures
    • fatal: Critical errors that may cause termination
  5. Remember the delay: Log forwarding is asynchronous, so there may be a slight delay between when a log is generated in the worker and when it appears in the channelLogger.

Clone this wiki locally