Skip to content

Relay v2 parser output via otel log format for output parity - #16325

Open
aiguofer wants to merge 4 commits into
1.latestfrom
aiguofer/v2-parser-output-parity
Open

aiguofer wants to merge 4 commits into
1.latestfrom
aiguofer/v2-parser-output-parity

Conversation

@aiguofer

@aiguofer aiguofer commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Stacked on #16281 (rebrand-fusion-to-v2-parser). Review that one first; this PR's diff is only the commits stacked on top of it.

Previous behavior

--use-v2-parser shells out to the v2 (Fusion) binary and relays its log lines. The relay invoked it with --log-format json and forwarded every line as a flat Note event, keeping only the message text and the level. Consequences:

  • Duplicate banners and span noise. The json-compat layer renders events that v2's own TUI layer has no arm for — per-node span starts/ends, run banners — so v1 printed lines a user running v2 directly would never see. The two layers have independently drifted: v2's TUI applies several stateful gates before printing a node-processed line that json-compat applies none of.
  • Error/warning codes lost. v2 composes [Generic (dbt1000)]-style prefixes at render time; the json wire carries the code as a separate field, which the relay discarded.
  • No color. json-compat deliberately strips ANSI, so all relayed output was plain regardless of --use-colors.
  • Latent --warn-error abort. Unrecognized lines were relayed at WARN. Under --warn-error, WARN events are promoted to a raised EventCompilationError matched by event class name, so a single stray line of v2 stderr could abort an otherwise-successful parse.

New behavior

Invoke with --log-format otel and parse the JSONL envelope (SpanStart / SpanEnd / LogRecord).

Spans are dropped, with one deliberate exception (below). This is the load-bearing change: in the otel schema only LogRecord has a body field — spans structurally do not — so consuming just the message-shaped records both fixes the body problem and removes the span-derived noise with no hand-maintained denylist. v1 also stops being coupled to v2's span-visibility logic, which is where the layer drift lives.

Of the remaining log records, an allowlist is relayed:

event handling
LogMessage, UserLogMessage relay body, with the v2 error/warning code composed back into the message text ([Generic (dbt1000)]: ...)
ProgressMessage rendered v1-side, reproducing v2's {action} {target} ({description}) column formatting from the record's attributes

Severity comes from the OTLP severity_number, mapped onto EventLevel by band. WARN/ERROR styling uses v1's own dbt_common.ui tag helpers rather than trying to be byte-identical to v2 — the output looks native to v1 while carrying the same information, and it stays plain text that platform log ingestion parses unchanged.

Unparseable or unrecognized lines now relay at INFO, fixing the --warn-error abort above.

The end-of-run status line

v2's Finished 'parse' with 1 warning for target 'default' [1.2s] has no LogRecord: it exists only as a console-formatter rendering of the Invocation span's end attributes. Dropping every span would have silently deleted it, so the Invocation span end is the one span record the relay reads rather than discards, and the line is synthesized v1-side from its attributes.

The counts come from v2's own metric aggregator (metrics.total_warnings / total_errors), so they stay correct even for warnings whose LogRecords this relay filtered out. Mirrored from v2's formatters/invocation.rs: the success/warning/error phrasing and coloring (both counts red once any error is present), singular/plural labels, the for target '...' clause, the duration tiers from formatters/duration.rs, and the RESULT_LINE_OPT_OUT_COMMANDS set — so the relay withholds the line on exactly the commands (man, login) where v2 itself prints none.

Two wire details worth noting, both covered by tests: pbjson renders proto uint64 as a JSON string, and it omits optional fields entirely when unset — so both "12" and a missing key have to be handled.

The subprocess no longer writes its own dbt.log

--log-level-file off is now forwarded too. v2 defaults its file log to {--project-dir}/logs/dbt.log, and _build_argv forwards --project-dir but not --log-path — so on any run that doesn't redirect the log path (a plain local CLI run), that resolves to the same file dbt-core writes its own file log to. Both processes were appending the same relayed events to it, with no shared locking, producing a logs/dbt.log with every parser line doubled and the two writers' formats interleaved.

Nothing is lost by turning it off: everything the subprocess emits already reaches dbt-core over stdout and lands in dbt.log through dbt-core's own logger.

Note this is fixed explicitly rather than relying on the format switch. The otel format happens to install no file-log layer (config.rs, LogFormat::Otel => None), so the double-write would have disappeared on its own — but incidentally, and it would come back if a file renderer is added for otel or if a run passes --log-format-file. --log-level-file off short-circuits before the writer is even constructed, so no logs/ dir or empty file is created either.

This is unrelated to the duplicate-log display in Studio/IDE. That is an ide-server realtime-buffer-vs-finalized-artifact overlap, fixed separately by dbt-labs/ide-server#1488; platform redirects dbt-core's log path per task and never ingests the subprocess's file, so this change neither causes nor fixes that symptom.

Notes

  • Relayed message text is not scrubbed for credential-bearing URLs on either wire. The cloud-cli path gets this from fusion-client-python's scrub_message(); a v1 relay has no equivalent. Not a regression introduced here, and I have not confirmed such URLs can actually appear in relayed text, but it is worth a second opinion before this ships widely.

Example outputs

CLI:
image

Studio:
image

@aiguofer
aiguofer requested a review from a team as a code owner September 15, 2026 20:16
Copilot AI lite review requested due to automatic review settings September 15, 2026 20:16
@cla-bot cla-bot Bot added the cla:yes label Sep 15, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Record validation/fallback and coded UserLogMessage handling need correction.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Updates v2 parser output relaying from JSON compatibility logs to OTEL JSONL for cleaner, richer parity.

Changes:

  • Filters spans and relays supported log records.
  • Preserves severity, codes, progress formatting, and native styling.
  • Adds unit coverage and a changelog entry.
File summaries
File Reviewed changes
tests/unit/parser/test_v2.py Tests OTEL filtering, formatting, severity, and fallback behavior.
core/dbt/parser/v2.py Implements OTEL parsing and event relaying.
.changes/unreleased/Fixes-20260911-155947.yaml Documents the fix.
Review details

Suppressed comments (1)

core/dbt/parser/v2.py:547

  • The stated relay behavior groups LogMessage and UserLogMessage together for restoring the v2 code/code_name prefix, but this condition only applies _prefix_log_message_code to LogMessage. Any coded UserLogMessage will therefore lose its [Name (dbt####)] text. Apply the same prefix helper to both message event types (it is a no-op when the attributes have no code).
                if event_type == _EVENT_TYPE_LOG_MESSAGE:
  • Files reviewed: 3/3 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread core/dbt/parser/v2.py
@aiguofer
aiguofer force-pushed the aiguofer/v2-parser-output-parity branch from ff1f84a to 1d92bae Compare September 15, 2026 21:20
@aiguofer
aiguofer force-pushed the aiguofer/v2-parser-output-parity branch from ef9cb98 to c09ba12 Compare September 16, 2026 17:54
@aiguofer
aiguofer added this pull request to stack #16339 September 16, 2026 18:02
mishamsk
mishamsk previously approved these changes Sep 16, 2026

@mishamsk mishamsk left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm. see 2 nits

Comment thread core/dbt/parser/v2.py Outdated
Comment thread core/dbt/parser/v2.py

@mishamsk mishamsk left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

second time as charm!

Base automatically changed from rebrand-fusion-to-v2-parser to 1.latest September 18, 2026 16:56
aiguofer and others added 4 commits September 18, 2026 09:56
… parity

The subprocess relay for `--use-v2-parser` shelled out with `--log-format
json` and forwarded every line as a flat, unstructured Note event,
producing duplicate banners, per-node span noise, and losing the
original fusion error/warning codes.

Switch to `--log-format otel` and parse the JSONL envelope (SpanStart/
SpanEnd/LogRecord). Spans are dropped unconditionally, which structurally
eliminates the span-derived noise without a hand-maintained denylist.
Only an allowlist of LogRecord event types is relayed: LogMessage and
UserLogMessage relay their body (with the fusion error/warning code
composed back into the message text), StdoutMessage/StderrMessage relay
as-is, and ProgressMessage is rendered v1-side to reproduce fusion's
"{action} {target} ({description})" formatting. Severity is mapped from
the OTLP severity_number onto dbt-core's EventLevel.

Unparseable or unrecognized lines now relay at INFO instead of WARN,
fixing a latent bug where a stray fusion stderr line could get promoted
to a raised EventCompilationError under --warn-error and abort the run.

Also forward --log-level-file off. The v2 parser defaults its file log to
{--project-dir}/logs/dbt.log, which is the same path dbt-core writes its
own file log to when --log-path isn't redirected, so both processes were
appending the same relayed events to one file. Everything the subprocess
emits already reaches dbt-core over stdout and lands in dbt.log through
dbt-core's own logger.
The `# type: ignore[arg-type]` only covered one of the codes mypy emits for
int(object) depending on version, so it failed under mantle's mypy with
call-overload. An isinstance narrow needs no ignore and keeps the relay
identical between dbt-core and mantle.
The status line ('Finished 'parse' with N warnings and M errors') has no
LogRecord of its own: the v2 parser renders it in its console formatter
out of Invocation span-end attributes. The relay dropped every span
wholesale, so the line went missing from --use-v2-parser output.

Read the aggregate counts off the Invocation span end instead, and
re-render the line with dbt-core's own ui color helpers. Every other span
is still dropped.
These event types exist for console output only and are not intended to
reach the otel stream, so allowlisting them for forward-compatibility was
speculative. Restore the TODO about switching to the Python OTel decoding
library once it is released.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@tauhid621
tauhid621 force-pushed the aiguofer/v2-parser-output-parity branch from e891d83 to 88fac75 Compare September 18, 2026 16:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants