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 span method must return the result of fn to preserve operation semantics
  • Errors thrown by fn should be re-thrown after logging to maintain stack traces
  • The WorkspaceSpan object 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 noopObserver performs no allocations or async work
  • Lazy evaluation: Attributes are passed as objects, not serialized unless your observer reads them
  • No internal buffering: The @cloudflare/computer core never queues spans; your observer controls buffering strategy

Summary

  • Custom observability hooks trace workspace operations via the WorkspaceObserver interface defined in packages/computer/src/observe.ts
  • Inject your observer through the observer constructor option; all workspace methods automatically wrap their work in span() calls
  • Implement three methods: span() for operation tracing, plus optional WorkspaceSpan handlers for error marking
  • Reference implementations: Start with makeRecorder for tests, createCloudflareObserver for Cloudflare deployment, or noopObserver for 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:

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 →