How Openship BuildLogger Provides a Single Source of Truth for Log Emission
The Openship BuildLogger class centralizes all build and deployment output through a unified API that ensures consistent formatting, stream capture, and progress tracking across the entire pipeline.
The oblien/openship repository implements a centralized logging architecture through the BuildLogger class to eliminate fragmented log sources during container builds and remote deployments. Defined in packages/adapters/src/runtime/build-pipeline.ts, this class funnels every log entry through a single instance, maintaining strict consistency in timestamp formatting, error handling, and execution state reporting across asynchronous operations.
Centralizing Log Emission Through BuildLogger
The BuildLogger class serves as the exclusive authority for all log output within the Openship runtime. Rather than scattering console.log statements throughout the codebase, every runtime module accepts an optional BuildLogger parameter that routes output through centralized methods. When a caller does not provide a logger instance, runtimes automatically instantiate a default logger using new BuildLogger(). This pattern ensures that every execution path, whether invoked from the CLI or programmatically, routes through the same logging infrastructure without requiring manual initialization at every call site.
Core API Methods for Unified Logging
The BuildLogger exposes three primary methods that establish a strict contract for log emission across the system:
log(message, level?)– Formats entries with consistent timestamps and level tags (info,warn,error), replacing ad-hoc formatting throughout the codebase.step(stepId, status, message?)– Records execution phases such asdeploy,build, ortransfer, enabling the UI to render coherent progress indicators.callback(entry)– Returns a function reference that streams raw output directly from subprocesses and remote executors back into the logger.
This API design establishes a single source of truth where all observability data originates from one instance, eliminating the risk of duplicate or missing log entries during complex multi-step operations.
Integrating BuildLogger with Runtime Executors
Runtime modules throughout the packages/adapters/src/runtime/ directory accept the logger instance to capture output from low-level operations. For example, file transfer operations in transfer.ts and Docker builds in docker.ts receive the logger's callback to stream real-time output.
Consider how the logger integrates with executor methods:
import { BuildLogger } from "./runtime/build-pipeline";
async function transferFiles(target, localPath, logger?: BuildLogger) {
const log = logger ?? new BuildLogger();
// Stream subprocess output directly into the centralized logger
await target.executor.transferIn(
localPath,
target.path,
log.callback, // All raw output funnels here
{ /* options */ }
);
}
By passing logger.callback to low-level executors responsible for SSH transfers or Docker daemon communication, Openship ensures that even binary stream output from external processes enters the same log sequence as application-level messages.
Ensuring Consistent Formatting and Error Handling
The BuildLogger enforces uniform message formatting by intercepting every log entry before emission. When developers invoke logger.log("Message", "error"), the class automatically prefixes the output with timestamp and severity indicators, ensuring that error logs maintain the same structural consistency as informational messages.
This centralized approach to error handling prevents discrepancy between console output and programmatic logs. Rather than calling console.error directly, runtimes emit failures through:
logger.log(`Docker build failed: ${msg}\n`, "error");
Because all errors route through the logger's log method, downstream consumers—including test suites in packages/adapters/test/deploy-pipeline.test.ts and monitoring dashboards—receive identical error strings without format drift.
Default Logger Pattern for Guaranteed Coverage
The codebase implements a defensive instantiation pattern that guarantees logging coverage even when callers omit the logger parameter. Each runtime function accepts an optional BuildLogger argument and immediately applies the nullish coalescing operator to create a default instance:
async function buildImage(config: BuildConfig, logger?: BuildLogger) {
const log = logger ?? new BuildLogger();
log.log("Starting Docker build…\n");
// ... build logic
}
This pattern ensures that every code path, whether tested in isolation or invoked through the deployment pipeline, maintains observability without requiring repetitive setup code. The optional parameter design allows dependency injection during testing while guaranteeing production logging through automatic instantiation.
Summary
The Openship BuildLogger establishes a single source of truth for log emission by:
- Centralizing output through the
BuildLoggerclass inpackages/adapters/src/runtime/build-pipeline.ts, eliminating fragmentedconsolecalls across runtimes - Standardizing formatting via the
log()method, which applies consistent timestamps and severity tags to every entry - Capturing subprocess streams by supplying
logger.callbackto low-level executors intransfer.tsanddocker.ts - Tracking execution state through the
step()method, which records pipeline phases for UI progress indicators - Ensuring universal coverage through default instantiation patterns that create a logger instance when none is provided
Frequently Asked Questions
What happens if I don't pass a BuildLogger to a runtime function?
If you omit the BuildLogger parameter when calling runtime functions like those in packages/adapters/src/runtime/docker.ts, the function automatically creates a default instance using new BuildLogger(). This ensures all execution paths maintain observability without requiring manual logger initialization at every call site, while still allowing dependency injection for testing scenarios.
How does BuildLogger capture output from Docker and SSH processes?
The logger.callback method provides a function reference that streams raw output directly from subprocesses back into the centralized log. When executing Docker builds or file transfers, runtimes pass this callback to low-level executors, ensuring that stdout and stderr from external processes enter the same formatted log stream as application messages.
Can BuildLogger track the progress of multi-step deployments?
Yes. The step(stepId, status, message?) method records specific pipeline phases such as build, transfer, or deploy. By calling logger.step("deploy", "running") at phase initiation and logger.step("deploy", "completed") upon finish, the logger maintains a centralized record of execution state that UI components can query to render real-time progress indicators.
How does centralizing logs through BuildLogger improve error debugging?
By routing all errors through logger.log(errorMessage, "error") rather than disparate console.error calls, the BuildLogger ensures that error messages include consistent timestamps and severity tags. This eliminates format drift between development console output and production logs, allowing automated monitoring tools and test suites in packages/adapters/test/deploy-pipeline.test.ts to parse failures reliably using a single schema.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →