Skip to main content

Logger

walkerOS provides a centralized logging system that enables consistent logging across the collector, sources, and destinations. The logger supports four log levels and provides scoped logging for better debugging.

Configuration

Configure logging when initializing the collector:

import { startFlow } from '@walkeros/collector';

const { collector, elb } = await startFlow({
  logger: {
    level: 'DEBUG', // 'ERROR' | 'WARN' | 'INFO' | 'DEBUG'
  },
});

Log levels

LevelValueDescription
ERROR0Only errors (default)
WARN1Errors and warnings
INFO2Errors, warnings, and informational
DEBUG3All messages including debug information

Choosing a level on public endpoints

A collector endpoint is exposed to the open internet, so log level decides whether ambient noise becomes an alert.

SituationLevel
Ambient internet noise: unparseable bodies, unknown paths, oversized payloadsdebug, plus a status counter
An event the pipeline rejects as invalidwarn with the reason, counted as rejected
A settled ok: false arriving after an HTTP ack was already sentwarn
Handler faults and rejected push promiseserror

The collector already logs thrown pipeline failures at error, so sources do not duplicate that. Nothing a scanner can trigger should reach error.

Using the logger

The logger is automatically available in destination and source contexts:

In destinations

const myDestination = {
  type: 'my-destination',
  config: {},

  push(event, { logger }) {
    logger.debug('Processing event', { name: event.name });

    try {
      // Send event to service
      sendToService(event);
      logger.info('Event sent successfully');
    } catch (error) {
      if (error.retryable) {
        logger.warn('Event send failed, will retry', error);
      } else {
        logger.error('Failed to send event', error);
      }
    }
  },
};

In sources

const mySource = async (config, env) => {
  const { logger } = env;

  logger.info('Source initialized');

  return {
    type: 'my-source',
    config,
    push: async (event) => {
      logger.debug('Received event', event);
      await env.push(event);
    },
  };
};

Logger API

Methods

MethodDescription
error(message, context?)Log an error message
warn(message, context?)Log a warning (degraded state, config issues)
info(message, context?)Log an informational message
debug(message, context?)Log a debug message
throw(message, context?)Log an error and throw an exception (returns never)
json(data)Output structured JSON data
scope(name)Create a scoped logger with a prefixed scope

Error handling

The logger accepts Error objects directly and automatically extracts relevant information:

try {
  await riskyOperation();
} catch (error) {
  // Error properties (message, name, stack, cause) are extracted automatically
  logger.error(error);

  // Or with additional context
  logger.error('Operation failed', { error, operation: 'sync' });
}

Boundary error logs

When an exception escapes the inner pipeline of collector.push or collector.command, the collector emits a structured error log and increments collector.status.failed. The messages are 'push failed' and 'command failed' respectively, with the full event/ingest or command/data context as structured fields. See Status — What failed counts for details and PII implications.

Internal pipeline failures from mapping, source startup, transformer init, and destination init are scoped under 'source', 'transformer:<type>', or the destination type. User-callback throws from on subscriptions are scoped under 'on'. The full list of error verbs and scopes is documented in Status — What failed counts.

Throwing errors

Use logger.throw() to log an error and throw in a single call:

function validateConfig(config) {
  if (!config.apiKey) {
    // Logs the error AND throws - returns 'never' type
    logger.throw('API key is required');
  }

  // TypeScript knows apiKey exists here due to 'never' return type
  return config.apiKey;
}

Scoped loggers

Scoped loggers prefix log messages with context information, making it easier to trace logs to their origin:

// In a destination, the logger is already scoped: [google-gtag:myGtag]
push(event, { logger }) {
  logger.info('Processing'); // Output: [google-gtag:myGtag] Processing

  // Create nested scope
  const ga4Logger = logger.scope('ga4');
  ga4Logger.debug('Sending to GA4'); // Output: [google-gtag:myGtag:ga4] Sending to GA4
}

Custom handler

For advanced use cases like sending logs to external services, configure a custom handler:

import { startFlow } from '@walkeros/collector';

const { collector, elb } = await startFlow({
  logger: {
    level: 'INFO',
    handler: (level, message, context, scope, originalHandler) => {
      // Send to external logging service
      loggingService.log({
        level,
        message,
        context,
        scope: scope.join(':'),
        timestamp: Date.now(),
      });

      // Optionally call the original console handler
      originalHandler(level, message, context, scope);
    },
  },
});

The handler receives:

ParameterTypeDescription
levelLevelLog level enum (ERROR=0, WARN=1, INFO=2, DEBUG=3)
messagestringThe log message
contextLogContextAdditional context object
scopestring[]Array of scope names
originalHandlerDefaultHandlerThe default console handler

Testing

Use the mock logger utility for testing destinations and sources:

import { createMockLogger } from '@walkeros/core';

describe('MyDestination', () => {
  it('logs events correctly', async () => {
    const mockLogger = createMockLogger();

    await myDestination.push(testEvent, {
      config: {},
      logger: mockLogger,
    });

    expect(mockLogger.info).toHaveBeenCalledWith(
      'Event sent successfully'
    );
  });
});

The mock logger provides jest mock functions for all logger methods plus:

  • scopedLoggers: Array of scoped loggers created via scope()
💡 Need implementation support?
elbwalker offers hands-on support: setup review, measurement planning, destination mapping, and live troubleshooting. Book a 2-hour session (€399)