---
title: "Tracing Plugins"
description: "Time the content lifecycle — debug timelines in the console, or OpenTelemetry span forwarding."
canonical_url: "https://content.comark.dev/plugins/built-in/tracing"
---
# Tracing Plugins

> Time the content lifecycle — debug timelines in the console, or OpenTelemetry span forwarding.

Two plugins activate the [`content.perf`](https://content.comark.dev/reference/content/perf) lifecycle timing recorder — without one of them, `content.perf` is a no-op and instrumented code paths cost nothing:

- **`tracingDebug`** (`comark-content/plugins/tracing/debug`) — records spans in memory, logs a waterfall timeline for every top-level call, and adds a `Server-Timing` header to [`handler()`](https://content.comark.dev/reference/content/handler) responses.
- **`tracingOtel`** (`comark-content/plugins/tracing/otel`) — forwards spans to an OpenTelemetry tracer. No logs, no in-memory entries, no `Server-Timing`.

Only one tracing recorder can be active per Content instance — both plugins register under the `tracing` name, so the first one in the `plugins` array wins.

## Debug timelines

```ts [content.ts]
import { comarkContent } from 'comark-content'
import fs from 'comark-content/sources/fs'
import tracingDebug from 'comark-content/plugins/tracing/debug'

const content = comarkContent({
  source: fs('./content'),
  plugins: [tracingDebug()],
})

await content.navigation()
// [comark-content@perf]
// navigation timeline (16.4ms)
// navigation                  ██████████████████████████████████████████████████  16.4ms
//   list                      ███████████████████                                 6.2ms
//     init                    ███████████████████                                 6.2ms
//       cache:get              █                                                  0.2ms
//       source:load             █████████████████                                 5.5ms
//       source:keys             ███                                               0.9ms
//       parse:.md:partial ×4       █████████████                                  14.0ms
//         source:read ×4           ████████████                                   11.3ms
//       schema:infer                            █                                 0.2ms
//       cache:set                                █                                0.1ms
//   navigation:generate                          ███████████████████████████████  10.1ms
```

The plugin is always active when installed. To debug on demand, gate it yourself — falsy entries in the `plugins` array are skipped, so you can toggle it inline with an environment variable:

```ts
const content = comarkContent({
  source: fs('./content'),
  plugins: [process.env.COMARK_PERF_DEBUG && tracingDebug()],
})
```

It works the same in the browser (timelines go to the console, no `Server-Timing` headers). See [`content.perf`](https://content.comark.dev/reference/content/perf) for the recorder API, the timeline format, and the list of operation names.

## OpenTelemetry

Register the OTel SDK in the host app, then pass a tracer to the plugin:

```ts [content.ts]
import { trace } from '@opentelemetry/api'
import { comarkContent } from 'comark-content'
import fs from 'comark-content/sources/fs'
import tracingOtel from 'comark-content/plugins/tracing/otel'

const content = comarkContent({
  source: fs('./content'),
  plugins: [tracingOtel({ tracer: trace.getTracer('comark-content') })],
})
```

Lifecycle spans (`init`, `get`, `parse:.md`, `cache:get`, …) then appear as custom spans in your tracing backend (such as Vercel Session Tracing and Trace Drains). Span attributes are prefixed with `comark.` (for example `comark.key`, `comark.source`).

## Options

Each plugin takes its own options:

### `tracingDebug()`

No options. Timelines are logged through the instance [`logger`](https://content.comark.dev/reference/content/comark-content).

### `tracingOtel(options)`

| Option   | Type     | Description                                                                                               |
| -------- | -------- | --------------------------------------------------------------------------------------------------------- |
| `tracer` | `Tracer` | **Required.** An OpenTelemetry tracer from the host app, for example `trace.getTracer('comark-content')`. |

## Custom recorders

Both plugins are thin wrappers around `content.setPerf(recorder)` — the [`ContentPluginContext`](https://content.comark.dev/reference/plugins/define-content-plugin#contentplugincontext) method that installs the recorder behind `content.perf`. To forward spans to another APM, write your own plugin: spread the exported `noopPerf` so the debug-only members (`entries`, `report`, `timeline`, `startScope`, …) stay no-ops, and implement the span methods:

```ts [tracing-custom.ts]
import { defineContentPlugin, noopPerf, type Perf } from 'comark-content'
import { withSpan } from 'comark/utils/trace'
import { apm } from './apm' // your APM client — Datadog, New Relic, …

export default defineContentPlugin<void>(() => ({
  name: 'tracing', // same name as the built-ins, so only one recorder ever installs
  setup(content) {
    const recorder: Perf = {
      ...noopPerf,
      startSpan(name, options) {
        const start = performance.now()
        return { end: () => apm.timing(name, performance.now() - start, options?.attributes) }
      },
      startActiveSpan(name, optionsOrFn, fn?) {
        const run = typeof optionsOrFn === 'function' ? optionsOrFn : fn!
        const options = typeof optionsOrFn === 'function' ? undefined : optionsOrFn
        // Callers (and `withSpan`) end the span themselves, sync or async.
        return run(recorder.startSpan(name, options))
      },
      run: (name, fn, options) => withSpan(recorder, name, fn, options),
    }
    content.setPerf(recorder)
  },
}))
```

`setPerf` follows the same rule as the built-ins: the **first installed recorder wins**, and later calls log a warning and are ignored. Registering your plugin under the `tracing` name keeps it mutually exclusive with `tracingDebug` / `tracingOtel` at the plugin level too.

## What gets timed

Source loading, per-file reads and parses, hook dispatches, cache reads/writes, artifact building, navigation generation, and plugin operations like [`query`](https://content.comark.dev/plugins/built-in/sql-query) and [`search`](https://content.comark.dev/plugins/built-in/full-text-search) — plus [comark](https://comark.dev)'s own engine phases inside each markdown parse. The full list of operation names is documented on [`content.perf`](https://content.comark.dev/reference/content/perf#operation-names).


## Sitemap

See the full [sitemap](https://content.comark.dev/sitemap.md) for all pages.
