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

> Openship BuildLogger centralizes build telemetry, acting as a single source of truth for log emission from cloning to deployment, ensuring a unified and deterministic channel.

- Repository: [oblien/openship](https://github.com/oblien/openship)
- Tags: internals
- Published: 2026-08-19

---

**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`](https://github.com/oblien/openship/blob/main/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:

```typescript
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:

```typescript
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"`:

```typescript
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:

```typescript
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](https://github.com/oblien/openship/blob/main/packages/adapters/src/runtime/build-pipeline.ts#L94)) 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`](https://github.com/oblien/openship/blob/main/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:

```typescript
// 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`](https://github.com/oblien/openship/blob/main/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](https://github.com/oblien/openship/blob/main/packages/adapters/src/runtime/build-pipeline.ts#L22). 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:

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

```

Emit a simple informational message:

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

```

Record a specific step transition:

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

```

Wrap asynchronous operations with automatic status tracking:

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

```

Access the raw callback for streaming APIs:

```typescript
// 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`](https://github.com/oblien/openship/blob/main/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`](https://github.com/oblien/openship/blob/main/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`](https://github.com/oblien/openship/blob/main/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.