Debug logging
When a diagram comes out wrong, such as a node in an odd place, a wire that detours or a label
beside the wrong edge, -debug shows how the layout got there. It writes one JSON Lines record per
layout and routing decision on stderr, so a run separates cleanly into the artifact on stdout and
the trace on stderr:
diago render -debug spec.json > out.svg 2> debug.ndjsonWithout -debug, stderr stays empty on a clean render. diago diff takes -debug too. A
sequence diagram emits no records. This page is for someone reporting or investigating a layout
bug: attaching the trace to a report tells the maintainers which decision went wrong.
The record
Section titled “The record”One JSON object per line. Always present: time, level (DEBUG), msg (always
layout decision), phase, decision (snake_case) and module (diago). A record may carry
spec_ref, the identifier of the layout rule the decision followed: a letter and a number that
name an internal rule. Quote it in a report; it is not meant to be looked up by a user. The
remaining keys vary per decision. This is a real record, from rendering the flow spec on the
flow spec page with its spec_ref left out:
{"time":"2026-10-07T21:51:13.755443+02:00","level":"DEBUG","msg":"layout decision","decision":"layers_assigned","phase":"rank","module":"diago","layers":3,"span_longest_path":2,"span":2}Phases
Section titled “Phases”The phase says which stage emitted the record. In pipeline order:
| Phase | Stage |
|---|---|
size |
Sizing each node and class box from its label and members. |
cycle |
Breaking cycles, so every edge can point the same way; reversed edges are recorded. |
anchoring |
Reusing a previous layout. Runs per level, before rank and again before order, and only when a previous layout is given through render -previous or diago diff. It emits previous_read once, when the previous carrier is read, and previous_ranked once too, but only when the carrier lists flat edges that fell back. |
rank |
Assigning nodes to layers. |
equalize |
Evening out sibling groups. |
lgraph |
Building the layered graph the next stages work on. |
order |
Ordering the nodes within each layer. |
place |
Giving every node its coordinates. |
route |
Routing every edge. |
nest |
Laying groups out level by level. |
congruence |
Copying a layout between groups that have the same shape. |
flat |
Routing the flat edges on the finished layout: flat_edge_routed, flat_edge_unrouted, and flat_edge_ranked when one falls back to an ordinary edge and the layout is redone, whose stages then emit their records again. |
labels |
Placing edge labels and cardinalities. |
frame |
Framing groups and fitting the title. |
contract |
Checking the final layout against the output rules, in the profile rendered, screen or text. Each contract_violation record has spec_ref the violated rule, subject the edge, node or group id it applies to, and detail the checker’s message. Emitted only with -debug. |
Useful queries
Section titled “Useful queries”# Only one stage's decisionsjq 'select(.phase == "route")' debug.ndjson
# Everything mentioning one node, across the differing key conventionsjq 'select(.node == "db" or .source == "db" or .subject == "db")' debug.ndjson
# Every output-rule violation -debug found in this renderjq 'select(.decision == "contract_violation")' debug.ndjson
# Every decision that cites one layout rule: copy the value from a recordjq --arg ref "<spec_ref value>" 'select(.spec_ref == $ref)' debug.ndjson
# What ran, and how muchjq -r '.phase' debug.ndjson | sort | uniq -c | sort -rnjq -r '(.module // "-") + " | " + .phase + " | " + .decision' debug.ndjson | sort -uWorkflow
Section titled “Workflow”- Save the failing spec to a file.
- Render it with
-debug, sending stderr to a file. - Find the misplaced node or edge in the output.
- Search the trace for its id.
- Walk the decision chain backward to the phase that made the wrong call.
If no record explains what you see, the gap is the bug report: tell the maintainers which decision
has no record. If you work on diago itself, the fix for that gap is to extend the
internal/layoutdbg.Decision call sites at that spot, with the same gating as the rest, and never
to add ad-hoc fmt.Printf instrumentation.
When debug is off, each call site costs one slog.Logger.Enabled(LevelDebug) comparison and
allocates nothing. When it is on, expect thousands of lines for a large diagram, so always
redirect stderr to a file. The contract records come from running the output-rule checker on the
final layout, which -debug arms; without it that check does not run.