Logging

Log Adapter Interface

All log adapters implement the LogAdapterInterface. The interface requires a log() method for writing log entries, a reset() method for clearing state, and levelName / levelNumber properties that control the minimum log level threshold. Any object that satisfies this interface can be used as a log adapter in the runner.

Built-in Adapters

BitDiver ships with four log adapter implementations covering the most common use cases:

  • LogAdapterConsole — Colored stdout output with human-readable formatting. Best for local development and interactive debugging.
  • LogAdapterConsoleJson — JSON-formatted console output with one JSON object per log entry. Ideal for structured logging in CI pipelines and log aggregation systems.
  • LogAdapterFile — Filesystem storage organized by run, testcase, and step. Each execution creates a directory hierarchy that can be inspected after the run completes.
  • LogAdapterMemory — In-memory storage that collects all log entries in an array. Designed for testing scenarios where you need to assert on log output without filesystem or console side effects.
import { LogAdapterConsole, LogAdapterFile } from '@xhubio/bitdiver-runner'

const consoleAdapter = new LogAdapterConsole({ levelName: 'info' })
const fileAdapter = new LogAdapterFile({ levelName: 'debug', dir: './logs' })

Log Levels

BitDiver defines five log levels, each with a numeric value. Setting levelName on an adapter filters out all messages with a lower numeric level:

  • debug (0) — Verbose diagnostic information for troubleshooting.
  • info (1) — General informational messages about execution progress.
  • warning (2) — Potentially problematic situations that do not prevent execution.
  • error (3) — Errors that affect the current testcase or step.
  • fatal (4) — Critical failures that halt the entire run.

For example, setting levelName: 'warning' means the adapter will only process messages at warning level (2) and above, silently discarding debug and info messages.

Log Message Structure

Every log entry follows a consistent structure that includes metadata about the current execution context and the actual log data:

{
  meta: {
    run: { id, name, start },
    tc: { id, name, tcCountCurrent, tcCountAll },
    step: { id, name, stepCountCurrent, stepCountAll, type }
  },
  data: any,
  logLevel: 'info'
}

The meta object provides full context about where in the execution the message originated—which run, which testcase, and which step. The data field can contain any serializable value: a string, an object, an error, or structured diagnostic information. The logLevel field indicates the severity of the message.

Logging in Steps

Inside any step, use the built-in convenience methods to emit log messages at the appropriate level:

  • this.logDebug(data) — Debug-level message
  • this.logInfo(data) — Info-level message
  • this.logWarning(data) — Warning-level message
  • this.logError(data) — Error-level message
  • this.logFatal(data) — Fatal-level message

Each method accepts any object as the message data. The step automatically populates the meta context from its current execution state, so you only need to provide the payload.

class MyStep extends StepNormal {
  async run() {
    await this.logInfo('Starting processing')
    try {
      const result = await this.processData()
      await this.logDebug({ action: 'processData', result })
    } catch (err) {
      await this.logError({ message: 'Processing failed', error: err.message })
    }
  }
}

Custom Adapters

To send logs to a custom destination, implement the LogAdapterInterface. Your adapter must provide a log() method that receives the structured log message, a reset() method for cleanup, and the levelName and levelNumber properties for filtering.

import { LogAdapterInterface } from '@xhubio/bitdiver-runner'

class LogAdapterCustom implements LogAdapterInterface {
  levelName: string
  levelNumber: number

  constructor(opts: { levelName: string }) {
    this.levelName = opts.levelName
    this.levelNumber = this.levelToNumber(opts.levelName)
  }

  async log(message: any): Promise<void> {
    if (message.logLevel < this.levelNumber) return
    // Send to your custom destination
    await sendToExternalService(message)
  }

  async reset(): Promise<void> {
    // Clean up resources if needed
  }

  private levelToNumber(name: string): number {
    const levels: Record<string, number> = {
      debug: 0, info: 1, warning: 2, error: 3, fatal: 4
    }
    return levels[name] ?? 1
  }
}

Pass your custom adapter to the runner just like any built-in adapter. You can also compose multiple adapters by wrapping them in a dispatcher that fans out log messages to several destinations simultaneously.