Openship BuildLogger Log Emission Single Source of Truth: Centralizing Build Telemetry

The BuildLogger class in the oblien/openship repository serves as the single source of truth for all log emission, ensuring every step—from repository cloning through deployment—streams through one unified, deterministic channel.

In complex deployment pipelines spanning local shells, SSH connections, and Docker containers, fragmented logging creates debugging nightmares. The OpenShip project solves this by implementing a centralized BuildLogger that eliminates conflicting log streams and provides consistent telemetry across the entire build and deploy lifecycle according to the source code analysis.

Why a Single Source of Truth Matters

When runtime adapters for local shells, SSH, Docker, and Oblien cloud services all receive the same BuildLogger instance, every emitted log entry routes through one place. This architectural choice eliminates duplicated streams and makes troubleshooting deterministic.

The pattern ensures that whether you are executing commands in a local environment or streaming output from a remote Docker daemon, the consuming UI receives consistently formatted entries with uniform timestamps, log levels, and step statuses.

Core Implementation and API

The BuildLogger implementation resides in [packages/adapters/src/runtime/build-pipeline.ts at line 32](https://github.com/oblien/openship/blob/main/packages/adapters/src/runtime/build-pipeline.ts#L32). The class constructor accepts a single optional callback that receives all log entries:

export class BuildLogger {
  constructor(private readonly onLog?: LogCallback) {}
  // ...
}

The Three Primary Logging Methods

The logger exposes three distinct APIs for different emission patterns:

1. Raw Log Lines with log() Use for ad-hoc messages. The method signature includes optional metadata for service identification:

log(
  message: string, 
  level: LogEntry["level"] = "info", 
  meta?: Pick<LogEntry, "serviceName" | "serviceId">
)

2. Structured Step Events with step() Emits lifecycle events for discrete build phases. The status parameter accepts "running", "completed", "failed", or "skipped":

step(step: BuildStep, status: NonNullable<LogEntry["stepStatus"]>, message: string)

3. Automatic Step Wrapping with runStep() Wraps async operations with automatic status transitions. This helper emits running before execution and completed or failed afterward:

async runStep(step: BuildStep, label: string, fn: () => Promise<void>)

Usage Across the OpenShip System

The BuildLogger propagates through every layer of the deployment stack, maintaining the single source of truth pattern.

Build Pipeline Integration

The runBuildPipeline function (starting at line 94 in build-pipeline.ts) receives a BuildLogger instance from the service layer and injects it into every phase—clone, install, and build. This ensures that dependency installation failures and compilation errors all emit through the same channel.

Runtime Adapters (Docker, SSH, and Local)

Runtime adapters forward the logger to their execution functions so that command output streams through the central channel. In the Docker adapter ([packages/adapters/src/runtime/docker.ts lines 1066-1075](https://github.com/oblien/openship/blob/main/packages/adapters/src/runtime/docker.ts#L1066-L1075)), the logger captures both step initialization and real-time build output streaming:

// Example pattern from Docker adapter
logger.step(step, "running", `[${step}] Starting Docker build`);
// Docker output streams through logger.callback

Transfer verification utilities also rely on the logger, as seen in test implementations where file count verifications emit diagnostic messages before and after operations.

Deploy Pipeline Continuity

After the build succeeds, the deploy pipeline continues using the same logger instance. The code at [packages/adapters/src/runtime/deploy-pipeline.ts lines 299-307](https://github.com/oblien/openship/blob/main/packages/adapters/src/runtime/deploy-pipeline.ts#L299-L307) demonstrates how deployment phases—from stopping previous versions to applying routing rules—emit through the established logger, creating an unbroken audit trail from initial clone to final traffic routing.

Log Level Intelligence and Error Handling

The BuildLogger automatically determines message severity through the parseLogLevel function located at line 22 of build-pipeline.ts. This ensures that errors, warnings, and informational messages receive appropriate UI styling without manual level assignment.

For cancellation scenarios, the system distinguishes between genuine failures and BuildCancelledError events. When a build aborts, the logger records the cancellation as a distinct status rather than a failure, keeping UI output clean and accurate for users who manually terminate builds.

Practical Implementation Examples

Create a logger instance that forwards to your UI or console:

const logger = new BuildLogger((entry) => {
  console.log(`[${entry.timestamp}] ${entry.level}: ${entry.message}`);
});

Emit a simple informational message:

logger.log("Starting build pipeline...", "info");

Record a specific step transition:

logger.step("clone", "running", "Cloning repository from GitHub");
logger.step("clone", "completed", "Repository cloned successfully");

Wrap asynchronous operations with automatic status tracking:

await logger.runStep("install", "Installing npm dependencies", async () => {
  await exec("npm ci", logger.callback);
});

Access the raw callback for streaming APIs:

// Pass to functions that expect a LogCallback signature
spawnProcess(childProcess, logger.callback);

Summary

  • Centralized Architecture: The BuildLogger class at packages/adapters/src/runtime/build-pipeline.ts provides the single source of truth for all OpenShip log emission.
  • Three-Method API: Use log() for raw messages, step() for lifecycle events, and runStep() for wrapped async operations.
  • Universal Propagation: The same logger instance flows through build pipelines, Docker adapters, SSH connections, and deploy phases.
  • Automatic Severity Detection: The parseLogLevel function at line 22 ensures proper log level assignment without manual intervention.
  • Clean Cancellation Handling: Distinguishes between build failures and user-initiated cancellations for accurate status reporting.

Frequently Asked Questions

How does the BuildLogger ensure logs don't get duplicated across different runtime adapters?

The service layer instantiates a single BuildLogger and passes it by reference to every runtime adapter including Docker, SSH, and local shell executors. Since all adapters receive the identical instance rather than creating their own loggers, every message routes through the same onLog callback, eliminating duplicate or conflicting streams according to the implementation in packages/adapters/src/runtime/build-pipeline.ts.

What is the difference between logger.log() and logger.step() in the OpenShip codebase?

Use logger.log() for unstructured, ad-hoc messages with arbitrary log levels (info, warn, error), while logger.step() emits structured lifecycle events tied to specific build phases like "clone" or "build". The step() method automatically sets the log level based on status—emitting "error" for failed steps and "info" for completed ones—as implemented in the class definition at line 32.

How does the BuildLogger handle errors in async operations?

The runStep() method wraps async functions in a try-catch block. If the operation succeeds, it emits a "completed" status with the label plus "- done". If it throws, the method catches the error, extracts a safe message using safeErrorMessage(), emits a "failed" status, and re-throws the error for upstream handling. This pattern appears in the BuildLogger implementation and ensures consistent error visibility.

Can the BuildLogger be used outside of the standard build pipeline?

Yes. The class is designed for reuse across any OpenShip component requiring telemetry. The deploy pipeline at packages/adapters/src/runtime/deploy-pipeline.ts uses the same logger instance post-build, and test utilities use it for transfer verification. Any code receiving a BuildLogger instance can emit through the single source of truth by calling logger.log() or accessing logger.callback for streaming APIs.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →