Skip to content

Commit e850aff

Browse files
committed
feat: tracer option and trace utils
1 parent 0514cec commit e850aff

8 files changed

Lines changed: 83 additions & 81 deletions

File tree

docs/content/5.api/1.parse.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -365,18 +365,18 @@ Both `parseMarkdown()` and `createMarkdownParser()` accept the same `ParserOptio
365365
| `headingIds` | `boolean` | `true` | Auto-generate `id` attributes for `h1``h6` headings. Set `false` to disable |
366366
| `registerDefaultPlugins` | `boolean` | `true` | Register the built-in default plugins (`frontmatter`, `html`, `alert`, `task-list`, `components`, `attributes`). Set `false` to disable. Also can be used to configure default plugins like `components` in conjunction with `plugins` |
367367
| `plugins` | `ComarkPlugin[]` | `[]` | Array of plugins to apply |
368-
| `perf` | `ComarkPerf` | `undefined` | Timing recorder for the parse pipeline — see [Timing the parse](#timing-the-parse) |
368+
| `tracer` | `ComarkTracer` | `undefined` | Timing recorder for the parse pipeline — see [Timing the parse](#timing-the-parse) |
369369

370370
### Timing the parse
371371

372-
Pass a `perf` recorder to time each phase of the pipeline and every plugin hook, so you can see where parse time goes (e.g. a slow `post` highlight hook). The contract is a structural subset of [OpenTelemetry](https://opentelemetry.io/docs/languages/js/instrumentation/#creating-spans) `Tracer``startSpan` and `startActiveSpan` — so a real OTel tracer works as-is:
372+
Pass a `tracer` to time each phase of the pipeline and every plugin hook, so you can see where parse time goes (e.g. a slow `post` highlight hook). The contract is a structural subset of [OpenTelemetry](https://opentelemetry.io/docs/languages/js/instrumentation/#creating-spans) `Tracer``startSpan` and `startActiveSpan` — so a real OTel tracer works as-is:
373373

374374
```ts
375375
import { trace } from '@opentelemetry/api'
376376

377377
const parse = createMarkdownParser({
378378
// Uses the OpenTelemetry provider registered by your app or hosting platform.
379-
perf: trace.getTracer('comark'),
379+
tracer: trace.getTracer('comark'),
380380
plugins: [highlight()],
381381
})
382382
```
@@ -393,20 +393,20 @@ Or a minimal recorder:
393393

394394
```ts
395395
const spans: { name: string, duration: number }[] = []
396-
const perf = {
396+
const tracer = {
397397
startSpan(name) {
398398
const start = performance.now()
399399
return { end: () => spans.push({ name, duration: performance.now() - start }) }
400400
},
401401
startActiveSpan(name, optionsOrFn, maybeFn) {
402402
const fn = typeof optionsOrFn === 'function' ? optionsOrFn : maybeFn
403-
const span = perf.startSpan(name)
403+
const span = tracer.startSpan(name)
404404
// Nested startActiveSpan/startSpan calls become children via your context/stack.
405405
return fn(span) // caller (comark) ends the span
406406
},
407407
}
408408

409-
const parse = createMarkdownParser({ perf, plugins: [highlight()] })
409+
const parse = createMarkdownParser({ tracer, plugins: [highlight()] })
410410
await parse(markdown)
411411
// spans → comark:parse
412412
// ├─ comark:autoclose
@@ -418,7 +418,7 @@ await parse(markdown)
418418
//
419419
```
420420

421-
Recorded spans: a root `comark:parse` active span enclosing `comark:autoclose`, `comark:tokenize` (markdown parsing), `comark:nodes` (token → AST conversion and unwrapping), and `comark:pre:<name>` / `comark:post:<name>` for each plugin hook. Nested `startActiveSpan` calls form the parent → child hierarchy (OTel active context, or a stack in a simple recorder). There is no timing overhead when `perf` is omitted, and no Node-specific API is used — it works in the browser too.
421+
Recorded spans: a root `comark:parse` active span enclosing `comark:autoclose`, `comark:tokenize` (markdown parsing), `comark:nodes` (token → AST conversion and unwrapping), and `comark:pre:<name>` / `comark:post:<name>` for each plugin hook. Nested `startActiveSpan` calls form the parent → child hierarchy (OTel active context, or a stack in a simple recorder). There is no timing overhead when `tracer` is omitted, and no Node-specific API is used — it works in the browser too.
422422

423423
### Inline rendering
424424

examples/3.cli/perf-trace/index.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
* Or from this directory:
1212
* pnpm start
1313
*/
14-
import { createMarkdownParser, type ComarkPerf, type ComarkSpan, type ComarkSpanOptions } from 'comark'
14+
import { createMarkdownParser, type ComarkTracer, type ComarkSpan, type ComarkSpanOptions } from 'comark'
1515
import rangi from 'comark/plugins/rangi'
1616
import toc from 'comark/plugins/toc'
1717
import security from 'comark/plugins/security'
@@ -29,7 +29,7 @@ interface TraceNode {
2929
children: TraceNode[]
3030
}
3131

32-
function createTracePerf() {
32+
function createTraceTracer() {
3333
const entries: PerfEntry[] = []
3434
const stack: string[] = []
3535
let nextId = 0
@@ -52,7 +52,7 @@ function createTracePerf() {
5252
return { span, id, parent, start }
5353
}
5454

55-
const perf: ComarkPerf = {
55+
const tracer: ComarkTracer = {
5656
startSpan(name) {
5757
// Non-active child of the current active span (if any).
5858
return makeSpan(name).span
@@ -69,7 +69,7 @@ function createTracePerf() {
6969
},
7070
}
7171

72-
return { perf, entries }
72+
return { tracer, entries }
7373
}
7474

7575
/** Rebuild a tree from parent → id links. */
@@ -171,9 +171,9 @@ Nested component body with **bold**.
171171
- item two
172172
`
173173

174-
const { perf, entries } = createTracePerf()
174+
const { tracer, entries } = createTraceTracer()
175175
const parse = createMarkdownParser({
176-
perf,
176+
tracer,
177177
plugins: [
178178
rangi(),
179179
toc({ depth: 3 }),

examples/3.cli/perf-trace/otel.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ const provider = new NodeTracerProvider({
6969
provider.register()
7070

7171
const parse = createMarkdownParser({
72-
perf: trace.getTracer('comark'),
72+
tracer: trace.getTracer('comark'),
7373
plugins: [
7474
rangi(),
7575
toc({ depth: 3 }),

packages/comark/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
".": "./dist/index.js",
4343
"./plugins/*": "./dist/plugins/*.js",
4444
"./utils": "./dist/utils/index.js",
45+
"./utils/trace": "./dist/utils/trace.js",
4546
"./parse": "./dist/parse.js",
4647
"./render": "./dist/render.js"
4748
},

packages/comark/src/parse.ts

Lines changed: 8 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,6 @@ import type {
55
MarkdownExitPlugin,
66
MergePluginFrontmatter,
77
MergePluginMeta,
8-
ComarkPerf,
9-
ComarkSpan,
10-
ComarkSpanOptions,
118
ParserOptions,
129
ResolvedFrontmatter,
1310
ResolvedMeta,
@@ -27,29 +24,14 @@ import { marmdownItTokensToMarkdownDocument } from './internal/parse/token-proce
2724
import { autoCloseMarkdown } from './internal/parse/auto-close/index.ts'
2825
import { extractReusableNodes } from './internal/parse/incremental.ts'
2926
import { createSerializedTask, dedupePlugins } from './utils/helpers.ts'
27+
import { noopTracer, withSpan } from './utils/trace.ts'
3028

3129
// Re-export frontmatter utilities
3230
export { parseFrontmatter } from './internal/frontmatter.ts'
3331

3432
// Re-export plugin utilities
3533
export { defineComarkPlugin } from './utils/helpers.ts'
3634

37-
/** No-op span used by {@link noopPerf}. */
38-
const noopSpan: ComarkSpan = { end: () => {} }
39-
40-
/** No-op recorder used when no `perf` option is provided — zero overhead. */
41-
const noopPerf: ComarkPerf = {
42-
startSpan: () => noopSpan,
43-
startActiveSpan: (
44-
_name: string,
45-
optionsOrFn: ComarkSpanOptions | ((span: ComarkSpan) => unknown),
46-
fn?: (span: ComarkSpan) => unknown
47-
) => {
48-
const run = typeof optionsOrFn === 'function' ? optionsOrFn : fn!
49-
return run(noopSpan)
50-
},
51-
}
52-
5335
/**
5436
* Creates a parser function for Comark content.
5537
*
@@ -81,7 +63,7 @@ const noopPerf: ComarkPerf = {
8163
export function createMarkdownParser<const TPlugins extends readonly ComarkPlugin<any, any>[] = []>(
8264
options: ParserOptions<TPlugins> = {} as ParserOptions<TPlugins>
8365
): ComarkParseFn<ResolvedMeta<MergePluginMeta<TPlugins>>, ResolvedFrontmatter<MergePluginFrontmatter<TPlugins>>> {
84-
const { autoUnwrap = true, autoClose = true, perf = noopPerf } = options
66+
const { autoUnwrap = true, autoClose = true, tracer = noopTracer } = options
8567
// Tag set to strip from the top level of the tree (MDC `unwrap`). Resolved once.
8668
const unwrapTags = resolveUnwrapTags(options.unwrap)
8769

@@ -124,29 +106,12 @@ export function createMarkdownParser<const TPlugins extends readonly ComarkPlugi
124106
let lastOutput: MarkdownDocument | null = null
125107
let lastInput: string | null = null
126108

127-
/** Run `fn` inside an active span; always ends the span (incl. async). */
128-
function withSpan<T>(name: string, fn: () => T): T {
129-
return perf.startActiveSpan(name, (span) => {
130-
try {
131-
const result = fn()
132-
if (result instanceof Promise) {
133-
return result.finally(() => span.end()) as T
134-
}
135-
span.end()
136-
return result
137-
} catch (error) {
138-
span.end()
139-
throw error
140-
}
141-
})
142-
}
143-
144109
const parseFn: ComarkParseFn = async (markdown, opts = {}) => {
145110
// Root active span for the full parse pipeline. Nested startActiveSpan /
146111
// startSpan calls become children (OTel active context, or a stack in a
147112
// simple recorder) → hierarchical trace:
148113
// comark:parse → autoclose / pre / tokenize / nodes / post
149-
return await withSpan('comark:parse', async () => {
114+
return await withSpan(tracer, 'comark:parse', async () => {
150115
const state = {
151116
options,
152117
tokens: [] as unknown[],
@@ -175,7 +140,7 @@ export function createMarkdownParser<const TPlugins extends readonly ComarkPlugi
175140
}
176141

177142
if (autoClose) {
178-
state.markdown = withSpan('comark:autoclose', () =>
143+
state.markdown = withSpan(tracer, 'comark:autoclose', () =>
179144
autoCloseMarkdown(state.markdown, {
180145
frontmatter: hasPlugin('frontmatter') && opts.streaming,
181146
syntax: hasPlugin('components'),
@@ -185,11 +150,11 @@ export function createMarkdownParser<const TPlugins extends readonly ComarkPlugi
185150

186151
for (const plugin of plugins) {
187152
if (!plugin.pre) continue
188-
await withSpan(`comark:pre:${plugin.name}`, () => plugin.pre!(state))
153+
await withSpan(tracer, `comark:pre:${plugin.name}`, () => plugin.pre!(state))
189154
}
190155

191156
try {
192-
state.tokens = withSpan('comark:tokenize', () => parser.parse(state.markdown, {}))
157+
state.tokens = withSpan(tracer, 'comark:tokenize', () => parser.parse(state.markdown, {}))
193158
} catch (e) {
194159
// in case of streaming, return the previous output if parsing fails
195160
// This is to avoid resetting the tree to an empty state on failure
@@ -201,7 +166,7 @@ export function createMarkdownParser<const TPlugins extends readonly ComarkPlugi
201166
}
202167

203168
// Convert tokens to Comark structure
204-
const nodesSpan = perf.startSpan('comark:nodes')
169+
const nodesSpan = tracer.startSpan('comark:nodes')
205170
let nodes = marmdownItTokensToMarkdownDocument(state.tokens, {
206171
startLine: state.parsedLines,
207172
preservePositions: opts.streaming ?? false,
@@ -242,7 +207,7 @@ export function createMarkdownParser<const TPlugins extends readonly ComarkPlugi
242207

243208
for (const plugin of plugins) {
244209
if (!plugin.post) continue
245-
await withSpan(`comark:post:${plugin.name}`, () => plugin.post!(state as ComarkParsePostState))
210+
await withSpan(tracer, `comark:post:${plugin.name}`, () => plugin.post!(state as ComarkParsePostState))
246211
}
247212

248213
return state.tree

packages/comark/src/types.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -322,17 +322,17 @@ export interface ComarkSpanOptions {
322322
*
323323
* ```ts
324324
* import { trace } from '@opentelemetry/api'
325-
* const parse = createMarkdownParser({ perf: trace.getTracer('comark') })
325+
* const parse = createMarkdownParser({ tracer: trace.getTracer('comark') })
326326
* ```
327327
*
328328
* Nested `startActiveSpan` calls form a parent → child hierarchy via the
329329
* active context (OTel) or an internal stack (simple recorders). Universal
330330
* by design — no Node-specific APIs — so it works in the browser too.
331331
*
332332
* Like OTel, callers must `span.end()` (including in `finally` / promise
333-
* settlement). See {@link ParserOptions.perf}.
333+
* settlement). See {@link ParserOptions.tracer}.
334334
*/
335-
export interface ComarkPerf {
335+
export interface ComarkTracer {
336336
/**
337337
* Start a span without making it active. Call `span.end()` when done.
338338
* Compatible with OTel `Tracer.startSpan(name, options?)`.
@@ -505,17 +505,17 @@ export interface ParserOptions<TPlugins extends readonly ComarkPlugin<any, any>[
505505
plugins?: TPlugins
506506

507507
/**
508-
* Timing recorder for the parse pipeline — see {@link ComarkPerf}.
508+
* Timing recorder for the parse pipeline — see {@link ComarkTracer}.
509509
* Structural subset of OpenTelemetry `Tracer`, so an OTel tracer works as-is:
510-
* `createMarkdownParser({ perf: trace.getTracer('comark') })`.
510+
* `createMarkdownParser({ tracer: trace.getTracer('comark') })`.
511511
*
512512
* When provided, the full parse is a root `comark:parse` active span containing
513513
* child phases (`comark:autoclose`, `comark:tokenize`, `comark:nodes`) and plugin
514514
* hooks (`comark:pre:<name>`, `comark:post:<name>`). Nested active spans form
515515
* the hierarchy. No timing overhead when omitted.
516516
* @default undefined
517517
*/
518-
perf?: ComarkPerf
518+
tracer?: ComarkTracer
519519
}
520520

521521
/**

packages/comark/src/utils/trace.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import type { ComarkSpan, ComarkSpanOptions, ComarkTracer } from '../types.ts'
2+
3+
/** No-op span used by {@link noopTracer}. */
4+
const noopSpan: ComarkSpan = { end: () => {} }
5+
6+
/** No-op recorder used when no `tracer` option is provided — zero overhead. */
7+
export const noopTracer: ComarkTracer = {
8+
startSpan: () => noopSpan,
9+
startActiveSpan: (
10+
_name: string,
11+
optionsOrFn: ComarkSpanOptions | ((span: ComarkSpan) => unknown),
12+
fn?: (span: ComarkSpan) => unknown
13+
) => {
14+
const run = typeof optionsOrFn === 'function' ? optionsOrFn : fn!
15+
return run(noopSpan)
16+
},
17+
}
18+
19+
/**
20+
* Run `fn` inside an active span on `tracer`; always ends the span (incl. async).
21+
*/
22+
export function withSpan<T>(tracer: ComarkTracer, name: string, fn: () => T): T {
23+
return tracer.startActiveSpan(name, (span) => {
24+
try {
25+
const result = fn()
26+
if (result instanceof Promise) {
27+
return result.finally(() => span.end()) as T
28+
}
29+
span.end()
30+
return result
31+
} catch (error) {
32+
span.end()
33+
throw error
34+
}
35+
})
36+
}

0 commit comments

Comments
 (0)