How to Implement Custom Observability Hooks to Trace Workspace Operations
The @cloudflare/computer package exposes a WorkspaceObserver interface that lets you trace every workspace operation by injecting a custom observer into the Workspace constructor.
Custom observability hooks in @cloudflare/computer allow you to capture timing, attributes, and errors for file system operations, process execution, and RPC calls without modifying internal code. The hook system centers on a single interface defined in packages/computer/src/observe.ts that all workspace methods invoke automatically.
The WorkspaceObserver Interface
The core contract is the WorkspaceObserver interface. At minimum, your implementation must provide a span method with this signature:
span<T>(
name: string,
attributes: Record<string, unknown>,
fn: (span: WorkspaceSpan) => Promise<T>
): Promise<T>
The WorkspaceSpan parameter passed to your callback includes methods like error(msg: string) to mark spans as failed. Every internal workspace operation—readFile, writeFile, exec, push—wraps its execution in observer.span(), ensuring consistent hook coverage.
Implementing a Minimal Custom Observer
Here's a console-logging observer that records start time, attributes, duration, and errors:
import { Workspace, type WorkspaceObserver, type WorkspaceSpan } from "@cloudflare/computer";
class ConsoleObserver implements WorkspaceObserver {
async span<T>(
name: string,
attrs: Record<string, unknown>,
fn: (span: WorkspaceSpan) => Promise<T>
): Promise<T> {
const start = Date.now();
console.log(`→ ${name}`, attrs);
try {
const result = await fn({
error: (msg) => console.error(`✗ ${name}`, msg),
});
console.log(`← ${name} – ${Date.now() - start} ms`);
return result;
} catch (e) {
console.error(`✗ ${name} – threw`, e);
throw e;
}
}
}
Key implementation details:
- The
spanmethod must return the result offnto preserve operation semantics - Errors thrown by
fnshould be re-thrown after logging to maintain stack traces - The
WorkspaceSpanobject lets you attach error markers that downstream systems can detect
Injecting the Observer
Pass your observer via the observer option when constructing a Workspace:
const ws = new Workspace({
// ...other required options like namespace and id
observer: new ConsoleObserver(),
});
// All subsequent operations are traced
await ws.fs.readFile("/hello.txt");
await ws.exec("git", ["status"]);
The Workspace constructor in packages/computer/src/workspace.ts stores the observer in a private #observer field, defaulting to noopObserver when none is provided.
Built-In Reference Implementations
The repository ships three observer implementations you can copy or extend:
| Implementation | Location | Purpose |
|---|---|---|
noopObserver |
observe.ts |
Zero-cost default when observability is disabled |
createCloudflareObserver |
observe/cloudflare.ts |
Forwards spans to Cloudflare's tracing infrastructure |
makeRecorder |
observe-recorder.ts |
In-memory span capture for testing and debugging |
Using the Recording Observer for Tests
The makeRecorder function creates an observer that stores every span for later inspection:
import { makeRecorder } from "@cloudflare/computer/src/observe-recorder";
const recorder = makeRecorder();
const ws = new Workspace({
namespace: "test",
id: "test-workspace",
observer: recorder
});
await ws.fs.writeFile("/tmp.txt", "data");
await ws.fs.readFile("/tmp.txt");
// Assert on recorded operations
console.log(recorder.spans.map(s => s.name));
// ['workspace.fs.writeFile', 'workspace.fs.readFile', ...]
Each recorded span includes name, attributes, startTime, endTime, and error fields.
Advanced: Wiring to OpenTelemetry
To forward spans to OpenTelemetry, implement WorkspaceObserver to create Otel spans:
import { trace, SpanStatusCode } from "@opentelemetry/api";
import type { WorkspaceObserver, WorkspaceSpan } from "@cloudflare/computer";
const tracer = trace.getTracer("@cloudflare/computer");
class OpenTelemetryObserver implements WorkspaceObserver {
async span<T>(
name: string,
attrs: Record<string, unknown>,
fn: (span: WorkspaceSpan) => Promise<T>
): Promise<T> {
return tracer.startActiveSpan(name, async (otelSpan) => {
Object.entries(attrs).forEach(([k, v]) => otelSpan.setAttribute(k, v));
try {
const result = await fn({
error: (msg) => otelSpan.setStatus({ code: SpanStatusCode.ERROR, message: msg }),
});
otelSpan.setStatus({ code: SpanStatusCode.OK });
return result;
} catch (e) {
otelSpan.recordException(e as Error);
otelSpan.setStatus({ code: SpanStatusCode.ERROR });
throw e;
} finally {
otelSpan.end();
}
});
}
}
This pattern works with any observability backend: Datadog, Honeycomb, Grafana Tempo, or custom logging pipelines.
Performance and Overhead
The observability system is designed for production use:
- Zero cost when disabled: The default
noopObserverperforms no allocations or async work - Lazy evaluation: Attributes are passed as objects, not serialized unless your observer reads them
- No internal buffering: The
@cloudflare/computercore never queues spans; your observer controls buffering strategy
Summary
- Custom observability hooks trace workspace operations via the
WorkspaceObserverinterface defined inpackages/computer/src/observe.ts - Inject your observer through the
observerconstructor option; all workspace methods automatically wrap their work inspan()calls - Implement three methods:
span()for operation tracing, plus optionalWorkspaceSpanhandlers for error marking - Reference implementations: Start with
makeRecorderfor tests,createCloudflareObserverfor Cloudflare deployment, ornoopObserverfor zero-cost defaults - Production-ready: No overhead when disabled; full control over span payload, sampling, and export in your implementation
Frequently Asked Questions
What operations can I trace with a custom observer?
Every high-level workspace operation: file reads and writes (ws.fs.readFile, ws.fs.writeFile), process execution (ws.exec), RPC pushes (ws.push), and directory operations. The span name parameter identifies the operation type (e.g., "workspace.fs.readFile", "workspace.exec").
Does implementing a custom observer require modifying the Computer source code?
No. The hook is entirely external. You implement WorkspaceObserver in your own codebase and inject it at Workspace construction time. The packages/computer/src/workspace.ts source shows that options.observer is read once during initialization and stored privately.
How do I test that my observer receives the expected spans?
Use makeRecorder from packages/computer/src/observe-recorder.ts. Create a recorder, pass it as the observer, perform workspace operations, then inspect recorder.spans. Each span exposes name, attributes, timing fields, and error state for assertions.
Can I use multiple observers simultaneously?
The interface accepts a single observer. To fan out to multiple systems, implement a composite observer that delegates span() calls to multiple backends, or chain observers manually by wrapping one inside another.
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 →