Skip to content

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:

Terminal window
diago render -debug spec.json > out.svg 2> debug.ndjson

Without -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.

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}

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.
Terminal window
# Only one stage's decisions
jq 'select(.phase == "route")' debug.ndjson
# Everything mentioning one node, across the differing key conventions
jq 'select(.node == "db" or .source == "db" or .subject == "db")' debug.ndjson
# Every output-rule violation -debug found in this render
jq 'select(.decision == "contract_violation")' debug.ndjson
# Every decision that cites one layout rule: copy the value from a record
jq --arg ref "<spec_ref value>" 'select(.spec_ref == $ref)' debug.ndjson
# What ran, and how much
jq -r '.phase' debug.ndjson | sort | uniq -c | sort -rn
jq -r '(.module // "-") + " | " + .phase + " | " + .decision' debug.ndjson | sort -u
  1. Save the failing spec to a file.
  2. Render it with -debug, sending stderr to a file.
  3. Find the misplaced node or edge in the output.
  4. Search the trace for its id.
  5. 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.