Tracing Plugins
Two plugins activate the 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 aServer-Timingheader tohandler()responses.tracingOtel(comark-content/plugins/tracing/otel) — forwards spans to an OpenTelemetry tracer. No logs, no in-memory entries, noServer-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
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.1msThe 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:
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 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:
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 (e.g. Vercel Session Tracing and Trace Drains). Span attributes are prefixed with comark. (e.g. comark.key, comark.source).
Options
tracingDebug()
No options. Timelines are logged through the instance logger.
tracingOtel(options)
| Option | Type | Description |
|---|---|---|
tracer | Tracer | Required. An OpenTelemetry tracer from the host app, e.g. trace.getTracer('comark-content'). |
Custom recorders
Both plugins are thin wrappers around content.setPerf(recorder) — the 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:
import { defineContentPlugin, noopPerf, type Perf } from 'comark-content'
import { withSpan } from 'comark/utils/trace'
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: () => myApm.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 and search — plus comark's own engine phases inside each markdown parse. The full list of operation names is documented on content.perf.