# How to Implement Custom Observability Hooks to Trace Workspace Operations

> Implement custom observability hooks to trace workspace operations with Cloudflare's computer package. Inject a WorkspaceObserver for detailed insights into every operation.

- Repository: [Cloudflare/computer](https://github.com/cloudflare/computer)
- Tags: how-to-guide
- Published: 2026-08-16

---

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

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

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

```typescript
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`](https://github.com/cloudflare/computer/blob/main/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`](https://github.com/cloudflare/computer/blob/main/observe.ts) | Zero-cost default when observability is disabled |
| `createCloudflareObserver` | [`observe/cloudflare.ts`](https://github.com/cloudflare/computer/blob/main/observe/cloudflare.ts) | Forwards spans to Cloudflare's tracing infrastructure |
| `makeRecorder` | [`observe-recorder.ts`](https://github.com/cloudflare/computer/blob/main/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:

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

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