From 0fdf7f4d31caffef45607f41d8bff3962c392085 Mon Sep 17 00:00:00 2001 From: Arata Nakafushiki <274594+raydive@users.noreply.github.com> Date: Tue, 1 Sep 2026 14:32:22 +0900 Subject: [PATCH] Add span attributes and per-PostProcessor spans to markdown tracing Beta trace analysis showed that post_process dominates typical parse time (p50: 8.1ms of 9.0ms) while the inlines phase dominates the tail (p99: 303ms), but the existing spans carried no detail to explain either. Enrich the instrumentation: - markdown.parse: markdown.input.length attribute, to correlate tail latency with input size - markdown.render: markdown.node.type, markdown.input.length, and markdown.output.length (when the output is a CharSequence) - markdown.parse.post_processor: new child span per PostProcessor with a markdown.post_processor.class attribute, so the cost of post processing is attributable to individual processors Span.current() and GlobalOpenTelemetry come from opentelemetry-api, already a transitive compile dependency via opentelemetry-instrumentation-annotations; both are no-ops when no agent or SDK is wired. Co-Authored-By: Claude Fable 5 --- .../vladsch/flexmark/html/HtmlRenderer.java | 9 +++ .../com/vladsch/flexmark/parser/Parser.java | 5 ++ .../parser/internal/PostProcessorManager.java | 67 +++++++++++++------ 3 files changed, 62 insertions(+), 19 deletions(-) diff --git a/flexmark/src/main/java/com/vladsch/flexmark/html/HtmlRenderer.java b/flexmark/src/main/java/com/vladsch/flexmark/html/HtmlRenderer.java index 7a41e81cf..78d75baea 100644 --- a/flexmark/src/main/java/com/vladsch/flexmark/html/HtmlRenderer.java +++ b/flexmark/src/main/java/com/vladsch/flexmark/html/HtmlRenderer.java @@ -18,6 +18,7 @@ import com.vladsch.flexmark.util.sequence.Escaping; import com.vladsch.flexmark.util.sequence.LineAppendable; import com.vladsch.flexmark.util.sequence.TagRange; +import io.opentelemetry.api.trace.Span; import io.opentelemetry.instrumentation.annotations.WithSpan; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -206,6 +207,10 @@ public void render(@NotNull Node node, @NotNull Appendable output) { */ @WithSpan("markdown.render") public void render(@NotNull Node node, @NotNull Appendable output, int maxTrailingBlankLines) { + Span span = Span.current(); + span.setAttribute("markdown.node.type", node.getClass().getSimpleName()); + span.setAttribute("markdown.input.length", node.getChars().length()); + HtmlWriter htmlWriter = new HtmlWriter(output, htmlOptions.indentSize, htmlOptions.formatFlags, !htmlOptions.htmlBlockOpenTagEol, !htmlOptions.htmlBlockCloseTagEol); MainNodeRenderer renderer = new MainNodeRenderer(options, htmlWriter, node.getDocument()); if (renderer.htmlIdGenerator != HtmlIdGenerator.NULL && !(node instanceof Document)) { @@ -218,6 +223,10 @@ public void render(@NotNull Node node, @NotNull Appendable output, int maxTraili // resolve any unresolved tracked offsets that are outside elements which resolve their own TrackedOffsetUtils.resolveTrackedOffsets(node.getChars(), htmlWriter, TRACKED_OFFSETS.get(renderer.getDocument()), maxTrailingBlankLines, SharedDataKeys.RUNNING_TESTS.get(options)); renderer.dispose(); + + if (output instanceof CharSequence) { + span.setAttribute("markdown.output.length", ((CharSequence) output).length()); + } } /** diff --git a/flexmark/src/main/java/com/vladsch/flexmark/parser/Parser.java b/flexmark/src/main/java/com/vladsch/flexmark/parser/Parser.java index 17ccc6ea5..de01481fb 100644 --- a/flexmark/src/main/java/com/vladsch/flexmark/parser/Parser.java +++ b/flexmark/src/main/java/com/vladsch/flexmark/parser/Parser.java @@ -17,6 +17,7 @@ import com.vladsch.flexmark.util.sequence.BasedSequence; import com.vladsch.flexmark.util.sequence.ReplacedBasedSequence; import com.vladsch.flexmark.util.sequence.mappers.SpecialLeadInHandler; +import io.opentelemetry.api.trace.Span; import io.opentelemetry.instrumentation.annotations.WithSpan; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -356,6 +357,8 @@ public static Builder builder(DataHolder options) { */ @WithSpan("markdown.parse") public @NotNull Document parse(@NotNull BasedSequence input) { + Span.current().setAttribute("markdown.input.length", input.length()); + // NOTE: parser can only handle contiguous sequences with no out of base characters if (input instanceof ReplacedBasedSequence) { throw new IllegalArgumentException("" + @@ -383,6 +386,8 @@ public static Builder builder(DataHolder options) { */ @WithSpan("markdown.parse") public @NotNull Document parse(@NotNull String input) { + Span.current().setAttribute("markdown.input.length", input.length()); + DocumentParser documentParser = new DocumentParser(options , blockParserFactories , paragraphPreProcessorFactories diff --git a/flexmark/src/main/java/com/vladsch/flexmark/parser/internal/PostProcessorManager.java b/flexmark/src/main/java/com/vladsch/flexmark/parser/internal/PostProcessorManager.java index 8021db187..765fe3fdd 100644 --- a/flexmark/src/main/java/com/vladsch/flexmark/parser/internal/PostProcessorManager.java +++ b/flexmark/src/main/java/com/vladsch/flexmark/parser/internal/PostProcessorManager.java @@ -12,6 +12,10 @@ import com.vladsch.flexmark.util.dependency.DependencyResolver; import com.vladsch.flexmark.util.dependency.DependentItem; import com.vladsch.flexmark.util.dependency.DependentItemMap; +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.context.Scope; import io.opentelemetry.instrumentation.annotations.WithSpan; import java.util.*; @@ -72,7 +76,16 @@ public Document postProcess(Document document) { boolean hadGlobal = false; for (PostProcessorFactory dependent : stage.dependents) { if (dependent.affectsGlobalScope()) { - document = dependent.apply(document).processDocument(document); + PostProcessor postProcessor = dependent.apply(document); + Span span = startPostProcessorSpan(postProcessor); + try (Scope ignored = span.makeCurrent()) { + document = postProcessor.processDocument(document); + } catch (Throwable t) { + span.setStatus(StatusCode.ERROR, t.getClass().getSimpleName() + ": " + t.getMessage()); + throw t; + } finally { + span.end(); + } hadGlobal = true; // assume it no longer reflects reality; classifyingNodeTracker = null; @@ -94,28 +107,36 @@ public Document postProcess(Document document) { } ReversibleIterable nodes = classifyingNodeTracker.getCategoryItems(Node.class, dependentNodeTypes.keySet()); - for (Node node : nodes) { - if (node.getParent() == null) continue; // was already removed - // now we need to get the bitset for the excluded ancestors of the node, then intersect it with the actual ancestors of this factory - int index; - BitSet nodeAncestors; - BitSet nodeExclusions; - Set> excluded = dependentNodeTypes.get(node.getClass()); - if (excluded != null) { - index = classifyingNodeTracker.getItems().indexOf(node); - if (index != -1) { - nodeAncestors = classifyingNodeTracker.getNodeAncestryMap().get(index); - if (nodeAncestors != null) { - nodeExclusions = classifyingNodeTracker.getExclusionSet().indexBitSet(excluded); - nodeExclusions.and(nodeAncestors); - if (!nodeExclusions.isEmpty()) { - // has excluded ancestor - continue; + Span span = startPostProcessorSpan(postProcessor); + try (Scope ignored = span.makeCurrent()) { + for (Node node : nodes) { + if (node.getParent() == null) continue; // was already removed + // now we need to get the bitset for the excluded ancestors of the node, then intersect it with the actual ancestors of this factory + int index; + BitSet nodeAncestors; + BitSet nodeExclusions; + Set> excluded = dependentNodeTypes.get(node.getClass()); + if (excluded != null) { + index = classifyingNodeTracker.getItems().indexOf(node); + if (index != -1) { + nodeAncestors = classifyingNodeTracker.getNodeAncestryMap().get(index); + if (nodeAncestors != null) { + nodeExclusions = classifyingNodeTracker.getExclusionSet().indexBitSet(excluded); + nodeExclusions.and(nodeAncestors); + if (!nodeExclusions.isEmpty()) { + // has excluded ancestor + continue; + } } } } + postProcessor.process(classifyingNodeTracker, node); } - postProcessor.process(classifyingNodeTracker, node); + } catch (Throwable t) { + span.setStatus(StatusCode.ERROR, t.getClass().getSimpleName() + ": " + t.getMessage()); + throw t; + } finally { + span.end(); } } } @@ -125,6 +146,14 @@ public Document postProcess(Document document) { return document; } + private static Span startPostProcessorSpan(PostProcessor postProcessor) { + // no-op span unless a Java agent (or the application) has wired GlobalOpenTelemetry + return GlobalOpenTelemetry.getTracer("com.vladsch.flexmark") + .spanBuilder("markdown.parse.post_processor") + .setAttribute("markdown.post_processor.class", postProcessor.getClass().getName()) + .startSpan(); + } + static DependentItemMap prioritizePostProcessors(DependentItemMap dependentMap) { // put globals last List, DependentItem>> prioritized = dependentMap.entries();