Skip to content

Add parser diagnostic observability infrastructure (#304) - #305

Open
leynos wants to merge 4 commits into
mainfrom
issue-304-add-parser-diagnostic-observability-infrastructure
Open

Add parser diagnostic observability infrastructure (#304)#305
leynos wants to merge 4 commits into
mainfrom
issue-304-add-parser-diagnostic-observability-infrastructure

Conversation

@leynos

@leynos leynos commented Jul 24, 2026

Copy link
Copy Markdown
Owner

Summary

This branch establishes repository-wide, backend-neutral parser observability
before scanner-specific telemetry expands. It adds a stable diagnostic
taxonomy, an opt-in observer API, an optional tracing and metrics adapter,
deterministic event tests, and downstream export and alerting guidance.

Existing parse() behaviour remains unchanged through the no-op observer, and
the parser library does not install or require a telemetry runtime.

Closes #304.

Review walkthrough

Validation

  • cargo check --no-default-features: passed.
  • cargo check --all-features: passed.
  • cargo test --no-default-features --test parser_observability: 10 passed.
  • make check-fmt: passed.
  • make lint: passed, including Clippy, Whitaker, and spelling.
  • make test: passed for all targets and all features.
  • make markdownlint: passed.
  • make nixie: passed.
  • mbake validate Makefile: passed.
  • coderabbit review --agent: zero findings after each of the three
    implementation milestones.

Notes

  • The observability feature is opt-in and is absent from default features.
  • Metrics use only stable code, category, and severity labels. Message
    text and spans remain structured logging fields.
  • Comprehensive code assignment for non-relation scanners remains future work;
    those diagnostics are observable through their stable categories.

References

leynos added 3 commits July 24, 2026 18:08
Introduce typed diagnostic categories and freeze the eight relation
diagnostic identifiers as an ADR-001 compatibility surface. Centralize
relation message text so scanner output and typed codes share one contract.

Keep the full gate clean under the current Whitaker policy by replacing
test-helper `expect` calls with explicit cardinality assertions.
Expose an opt-in `ParseObserver` entry point for deterministic parser,
scanner, and diagnostic events while preserving the existing `parse` API
through a no-op observer.

Add an optional adapter for the `tracing` and `metrics` facades. Keep
message text and spans out of metric labels, and leave recorder,
subscriber, and exporter installation to host applications.
Record parser callbacks in integration tests and assert all relation
diagnostic codes, source spans, parser categories, event ordering, and
repeated-run determinism. Verify the observer path returns the same
syntax tree and errors as the no-op entry point.

Document custom and facade-backed host integration, stable metrics and
logging fields, exporter ownership, and rate-based alerting guidance.

@sourcery-ai sourcery-ai Bot 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.

Sorry @leynos, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Summary

  • Added backend-neutral parser observability with opt-in ParseObserver support and deterministic parse_with_observer() instrumentation.
  • Introduced stable diagnostic categories and relation codes D-REL-001D-REL-008, including structured diagnostic context.
  • Added optional tracing/metrics integration with bounded labels via the observability feature.
  • Expanded public parser exports while preserving existing parse() behaviour through a no-op observer.
  • Added deterministic integration tests covering diagnostic events, spans, ordering and repeatability.
  • Documented downstream logging, metrics, exporting and alerting integration, aligned with issue #304 and the parser conformance register.
  • Inlined DiagnosticContext construction to resolve the function-argument-count finding without changing behaviour.

Walkthrough

Add backend-neutral parser observability with stable diagnostic codes, observer callbacks, optional tracing/metrics integration, public re-exports, documentation, and deterministic integration tests.

Changes

Parser observability

Layer / File(s) Summary
Stable diagnostic taxonomy
src/parser/diagnostics/*, src/parser/error_messages.rs, src/parser/span_scanners/relations*
Define stable categories and D-REL-001D-REL-008 codes, centralize relation messages, and expose diagnostic types.
Observed parser pipeline
src/parser/mod.rs, src/parser/observability/mod.rs, src/parser/span_scanner.rs, src/parser/cst_builder/tree.rs, src/parser/tests/*
Add parse_with_observer, observer contexts, no-op observation, and lifecycle reporting across parser and scanner stages.
Telemetry adapter and public API
Cargo.toml, src/lib.rs, src/parser/observability/telemetry.rs
Add the optional observability feature and forward parser events to metrics counters and tracing events.
Documentation and validation
docs/*, tests/parser_observability.rs, tests/support/observability.rs
Document observer and telemetry integration, diagnostic contracts, alerting guidance, and validate event metadata, ordering, and determinism.

Sequence Diagram(s)

sequenceDiagram
  participant Host
  participant Parser
  participant Observer
  participant TelemetryObserver
  Host->>Parser: parse_with_observer(source, observer)
  Parser->>Observer: report attempt lifecycle
  Parser->>Observer: emit diagnostic context
  Observer->>TelemetryObserver: forward callbacks
  TelemetryObserver-->>Host: tracing events and metrics counters
  Parser-->>Host: return Parsed
Loading

Possibly related PRs

  • leynos/ddlint#263: Modifies the relation diagnostic surface and D-REL-* error handling used by this observability contract.

Suggested labels: Issue

Poem

Stable codes hum through parser halls,
Observers catch each rise and fall.
Metrics bloom, tracing lights the way,
No-op silence keeps plain parses grey.
D-REL stars mark errors bright—
Deterministic events set things right.


Caution

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

  • Ignore

❌ Failed checks (1 error, 4 warnings)

Check name Status Explanation Resolution
Testing (Overall) ❌ Error Leave TelemetryObserver untested and never assert non-zero attempt counts; current tests cover observer events, but not the metrics/tracing adapter or completion counts on failing parses. Add direct tests for TelemetryObserver behind the observability feature, asserting counter names/labels and emitted tracing fields; assert ParseAttemptContext counts on error cases.
User-Facing Documentation ⚠️ Warning docs/users-guide.md still documents only log-based warnings; it omits parse_with_observer, the observability feature, and telemetry export guidance. Update docs/users-guide.md to explain the new parser observability API, optional observability feature, and how to start using TelemetryObserver.
Developer Documentation ⚠️ Warning docs/developers-guide.md still covers only the old parser module map; it never mentions parse_with_observer, ParseObserver, or the observability feature. Add the new observer API, telemetry adapter, and feature-flag boundary to docs/developers-guide.md, then sync any linked roadmap or ADR text if you close an item.
Testing (Property / Proof) ⚠️ Warning The PR adds range-wide invariants (no-op parse equivalence and deterministic observer ordering) but only example-driven tests; no proptest or proof covers the general case. Add a proptest over arbitrary parser inputs asserting parse_with_observer(..., NoopParseObserver) matches parse() and event order/count stay stable; keep finite code mappings as unit tests.
Testing (Compile-Time / Ui) ⚠️ Warning No trybuild/compile-fail tests exist for the new feature-gated and non_exhaustive parser API; the added tests only cover runtime observer events. Add a trybuild-equivalent test suite for feature-gated exports and non_exhaustive API stability; keep the current runtime integration tests alongside it.
✅ Passed checks (15 passed)
Check name Status Explanation
Title check ✅ Passed The title matches the observability infrastructure change and includes the linked issue reference (#304).
Description check ✅ Passed The description clearly describes the parser observability work and stays on topic.
Linked Issues check ✅ Passed The PR covers the required backend-neutral API, stable codes/categories, no-op behaviour, tests, and docs for #304.
Out of Scope Changes check ✅ Passed No clear out-of-scope changes stand out; the extra docs, tests, and helper refactors support the observability work.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Module-Level Documentation ✅ Passed PASS: every touched Rust module and test module begins with //! docs, and the new observability/diagnostics modules explain purpose and relationships.
Testing (Unit And Behavioural) ✅ Passed PASS: The PR adds unit tests for stable diagnostic mappings and lexer error paths, plus integration tests that drive the public parse_with_observer boundary and verify failure, ordering, and determ...
Unit Architecture ✅ Passed Parse() stays pure via NoopParseObserver; telemetry is isolated behind explicit ParseObserver injection and an opt-in feature, with deterministic tests covering both paths.
Domain Architecture ✅ Passed Parser core depends only on ParseObserver; tracing/metrics stay in an optional adapter behind the observability feature, so domain logic remains isolated.
Observability ✅ Passed Feature-gated parser observability adds stable code/category/severity labels, structured failure context, and ratio-based alert guidance without unbounded metrics.
Security And Privacy ✅ Passed No secrets, credentials, or auth changes found; telemetry is opt-in and only logs bounded codes/categories plus structured messages/spans, not raw source text or env values.
Performance And Resource Use ✅ Passed Hot-path work stays linear in input/error count; observer and telemetry overhead are fixed-size and feature-gated, with no unbounded buffering or blocking I/O.
Concurrency And State ✅ Passed PASS: the new observability path is synchronous and stateless; no locks, awaits, or background tasks appear, and only a local RefCell in tests holds mutable state.
Architectural Complexity And Maintainability ✅ Passed PASS: The observer seam is explicit and immediately used by parse/span scanning/tests; docs explain host-owned telemetry, and the optional adapter is feature-gated with no global state.
Rust Compiler Lint Integrity ✅ Passed No new lint suppressions or fake usage anchors appear; the added clones are justified snapshots/shared rowan ownership, not dead-code or ownership masking.
📋 Issue Planner

Built with CodeRabbit's Coding Plans for faster development and fewer bugs.

View plan used: #304

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-304-add-parser-diagnostic-observability-infrastructure

Comment @coderabbitai help to get the list of available commands.

codescene-access[bot]

This comment was marked as outdated.

codescene-access[bot]

This comment was marked as outdated.

@leynos

leynos commented Jul 24, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix. Include the file and symbol names indicated in the issue at the head of your response. Ensure that this is validated against the current version of the codegraph.

If further refinement to address this finding would be deleterious, please supply a clear explanatory one to two paragraph markdown message I can paste into the CodeScene web ui's diagnostic suppression function so this diagnostic can be silenced.

src/parser/observability/mod.rs

Comment on lines +57 to +71

    pub const fn new(
        code: Option<DiagnosticCode>,
        category: DiagnosticCategory,
        span: Span,
        severity: DiagnosticSeverity,
        message: &'a str,
    ) -> Self {
        Self {
            code,
            category,
            span,
            severity,
            message,
        }
    }

❌ New issue: Excess Number of Function Arguments
DiagnosticContext.new has 5 arguments, max arguments = 4

@coderabbitai

This comment was marked as resolved.

@leynos
leynos marked this pull request as ready for review July 26, 2026 02:18
Remove the `DiagnosticContext::new` constructor to clear the CodeScene
"Excess Number of Function Arguments" finding. The sole call site in
`report_diagnostics` now builds the struct with a field literal carrying
identical values, preserving field visibility, accessors, lifetimes, and
observer callback behaviour.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
codescene-access[bot]

This comment was marked as outdated.

@coderabbitai coderabbitai Bot added the Issue label Jul 26, 2026

@codescene-access codescene-access Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

No quality gates enabled for this code.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/parser-observability.md`:
- Around line 80-91: Add descriptive captions immediately before both tables in
docs/parser-observability.md, including the table describing TelemetryObserver
counters and the additional table around the referenced section. Also add the
required Triage paragraph for this documentation-style change, preserving the
existing table content.

In `@src/parser/diagnostics/code.rs`:
- Around line 76-94: The DiagnosticCode enum in src/parser/diagnostics/code.rs
(lines 76-94) must be marked non-exhaustive by adding the appropriate attribute
so future variants remain additive for downstream consumers. No direct changes
are required in docs/parser-conformance-register.md (lines 198-203) or
docs/parser-observability.md (lines 119-122); they are affected sites covered by
the enum-level fix.

In `@src/parser/observability/mod.rs`:
- Around line 21-42: Mark the public DiagnosticSeverity enum as
#[non_exhaustive] so downstream matches must remain forward-compatible with
future severity variants. Leave its existing Error variant and as_str/Display
implementations unchanged.

In `@src/parser/observability/telemetry.rs`:
- Around line 1-56: Add focused tests for TelemetryObserver that install a
metrics_util::debugging::DebuggingRecorder, construct TelemetryObserver::new(),
invoke its ParseObserver methods directly, and assert the emitted counter names,
label keys, and category/code/severity values. Include the relevant
completion/diagnostic paths exposed by TelemetryObserver, and ensure recorder
setup is isolated so the test does not conflict with global metrics state.
- Around line 57-83: Change the diagnostic logging call in diagnostic_emitted
from tracing::error! to tracing::warn!, preserving its existing target, fields,
and message. Keep the metric emission unchanged so alerting can continue to use
sustained diagnostic rates or ratios rather than individual error-level logs.

In `@src/parser/span_scanner.rs`:
- Around line 108-122: Update the Ok arm of the span_result match to call
complete_attempt with DiagnosticCategory::SpanBuilder and an empty span-error
collection, matching the existing Err arm’s completion flow. Remove the direct
ParseAttemptContext construction and ensure the successful spans are still
returned unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 7e1c6cba-a0c4-4d05-a32f-65e812559306

📥 Commits

Reviewing files that changed from the base of the PR and between 3889f3b and 97b7fb7.

📒 Files selected for processing (23)
  • Cargo.toml
  • docs/contents.md
  • docs/parser-conformance-register.md
  • docs/parser-implementation-notes.md
  • docs/parser-observability.md
  • src/lib.rs
  • src/parser/cst_builder/tree.rs
  • src/parser/diagnostics/code.rs
  • src/parser/diagnostics/mod.rs
  • src/parser/error_messages.rs
  • src/parser/mod.rs
  • src/parser/observability/mod.rs
  • src/parser/observability/telemetry.rs
  • src/parser/span_scanner.rs
  • src/parser/span_scanners/relations.rs
  • src/parser/span_scanners/relations/cursor.rs
  • src/parser/span_scanners/relations/preamble.rs
  • src/parser/span_scanners/tests/mod.rs
  • src/parser/tests/helpers.rs
  • src/parser/tests/rules/aggregations.rs
  • src/parser/tests/rules/body_terms.rs
  • tests/parser_observability.rs
  • tests/support/observability.rs
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • leynos/shared-actions (auto-detected)

Comment on lines +80 to +91
`TelemetryObserver` emits the following counters:

| Metric | Labels | Meaning |
| --------------------------------- | ------------------------------ | ---------------------------------- |
| `ddlint_parser_attempts_total` | `category` | Parser or scanner attempts started |
| `ddlint_parser_diagnostics_total` | `code`, `category`, `severity` | Diagnostics emitted |

The `code` label is the stable diagnostic code or `uncoded`. Categories and
severity are bounded enums. The adapter emits attempt start/completion events at
`DEBUG` and diagnostics at `ERROR` under the `ddlint::parser` tracing target.
Diagnostic events include `code`, `category`, `severity`, `span_start`,
`span_end`, and `message` fields.

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Caption both tables.

Add a descriptive caption immediately before each table.

Triage: [type:docstyle]

As per coding guidelines, “Caption every table.” As per path instructions, documentation-style findings require a Triage paragraph.

Also applies to: 124-138

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/parser-observability.md` around lines 80 - 91, Add descriptive captions
immediately before both tables in docs/parser-observability.md, including the
table describing TelemetryObserver counters and the additional table around the
referenced section. Also add the required Triage paragraph for this
documentation-style change, preserving the existing table content.

Sources: Coding guidelines, Path instructions

Comment on lines +76 to +94
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum DiagnosticCode {
/// A relation kind appears before its role keyword.
RelationKindBeforeRole,
/// A relation declaration contains more than one role keyword.
RelationDuplicateRole,
/// A relation declaration contains more than one kind keyword.
RelationDuplicateKind,
/// A bracket-form relation declares a primary-key clause.
RelationBracketPrimaryKey,
/// A bracket-form relation does not contain exactly one element type.
RelationInvalidBracketElementType,
/// A non-input relation declares a primary-key clause.
RelationPrimaryKeyOnNonInput,
/// A relation contains an unexpected or malformed primary-key clause.
RelationMalformedPrimaryKey,
/// A relation uses an unsupported bracket-wrapped primary-key clause.
RelationBracketWrappedPrimaryKey,
}

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.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== code.rs ==\n'
sed -n '1,180p' src/parser/diagnostics/code.rs

printf '\n== conformance register ==\n'
sed -n '185,210p' docs/parser-conformance-register.md

printf '\n== observability docs ==\n'
sed -n '110,130p' docs/parser-observability.md

Repository: leynos/ddlint

Length of output: 8944


Mark DiagnosticCode as non-exhaustive. Add #[non_exhaustive]; otherwise every future variant is a source-breaking change for downstream exhaustive matches, which undermines the additive compatibility contract.

📍 Affects 3 files
  • src/parser/diagnostics/code.rs#L76-L94 (this comment)
  • docs/parser-conformance-register.md#L198-L203
  • docs/parser-observability.md#L119-L122
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/parser/diagnostics/code.rs` around lines 76 - 94, The DiagnosticCode enum
in src/parser/diagnostics/code.rs (lines 76-94) must be marked non-exhaustive by
adding the appropriate attribute so future variants remain additive for
downstream consumers. No direct changes are required in
docs/parser-conformance-register.md (lines 198-203) or
docs/parser-observability.md (lines 119-122); they are affected sites covered by
the enum-level fix.

Comment on lines +21 to +42
/// Severity of a parser diagnostic.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum DiagnosticSeverity {
/// The parser could not accept or validate part of the source.
Error,
}

impl DiagnosticSeverity {
/// Return the stable, low-cardinality severity label.
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Error => "error",
}
}
}

impl fmt::Display for DiagnosticSeverity {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}

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.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Mark DiagnosticSeverity #[non_exhaustive].

The doc comment frames this as "severity of a parser diagnostic" with a single variant today, implying growth (Warning, perhaps). Downstream ParseObserver implementations that match exhaustively on this enum will break the moment a second variant lands. Mark it #[non_exhaustive] now, whilst the API is new, to make that future addition non-breaking.

🔧 Proposed fix
 /// Severity of a parser diagnostic.
 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+#[non_exhaustive]
 pub enum DiagnosticSeverity {
     /// The parser could not accept or validate part of the source.
     Error,
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/// Severity of a parser diagnostic.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum DiagnosticSeverity {
/// The parser could not accept or validate part of the source.
Error,
}
impl DiagnosticSeverity {
/// Return the stable, low-cardinality severity label.
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Error => "error",
}
}
}
impl fmt::Display for DiagnosticSeverity {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
/// Severity of a parser diagnostic.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum DiagnosticSeverity {
/// The parser could not accept or validate part of the source.
Error,
}
impl DiagnosticSeverity {
/// Return the stable, low-cardinality severity label.
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Error => "error",
}
}
}
impl fmt::Display for DiagnosticSeverity {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/parser/observability/mod.rs` around lines 21 - 42, Mark the public
DiagnosticSeverity enum as #[non_exhaustive] so downstream matches must remain
forward-compatible with future severity variants. Leave its existing Error
variant and as_str/Display implementations unchanged.

Comment on lines +1 to +56
//! Optional `tracing` and `metrics` observer adapter.

use metrics::{counter, describe_counter};

use super::{DiagnosticContext, ParseAttemptContext, ParseObserver};
use crate::parser::diagnostics::DiagnosticCategory;

const ATTEMPT_METRIC: &str = "ddlint_parser_attempts_total";
const DIAGNOSTIC_METRIC: &str = "ddlint_parser_diagnostics_total";

/// Observer that forwards parser events to the `tracing` and `metrics` facades.
///
/// Constructing this adapter describes its counters but does not install a
/// global tracing subscriber, metrics recorder, or exporter.
#[derive(Debug, Clone, Copy)]
#[non_exhaustive]
pub struct TelemetryObserver;

impl TelemetryObserver {
/// Construct the optional telemetry adapter.
#[must_use]
pub fn new() -> Self {
describe_counter!(ATTEMPT_METRIC, "Parser and scanner attempts");
describe_counter!(DIAGNOSTIC_METRIC, "Parser diagnostics emitted");
Self
}
}

impl Default for TelemetryObserver {
fn default() -> Self {
Self::new()
}
}

impl ParseObserver for TelemetryObserver {
fn parse_attempt_started(&self, category: DiagnosticCategory) {
let category = category.as_str();
counter!(ATTEMPT_METRIC, "category" => category).increment(1);
tracing::debug!(
target: "ddlint::parser",
category,
"parser attempt started"
);
}

fn parse_attempt_completed(&self, context: ParseAttemptContext) {
let category = context.category().as_str();
let diagnostic_count = context.diagnostic_count();
tracing::debug!(
target: "ddlint::parser",
category,
diagnostic_count,
"parser attempt completed"
);
}

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.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a test asserting TelemetryObserver's emitted counters/fields.

This adapter's label-mapping logic (category/code/severity strings, counter names) is untouched by any provided test — RecordingParseObserver covers the generic observer contract, but nothing exercises TelemetryObserver itself. A metrics_util::debugging::DebuggingRecorder snapshot (or a captured tracing subscriber) run against TelemetryObserver::new() plus direct trait-method calls would catch label typos or cardinality regressions before they ship silently in an optional, hard-to-manually-verify path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/parser/observability/telemetry.rs` around lines 1 - 56, Add focused tests
for TelemetryObserver that install a metrics_util::debugging::DebuggingRecorder,
construct TelemetryObserver::new(), invoke its ParseObserver methods directly,
and assert the emitted counter names, label keys, and category/code/severity
values. Include the relevant completion/diagnostic paths exposed by
TelemetryObserver, and ensure recorder setup is isolated so the test does not
conflict with global metrics state.

Source: Coding guidelines

Comment on lines +57 to +83
fn diagnostic_emitted(&self, context: &DiagnosticContext<'_>) {
let code = context.code().map_or("uncoded", |code| code.as_str());
let category = context.category().as_str();
let severity = context.severity().as_str();
let span_start = context.span().start;
let span_end = context.span().end;
let message = context.message();

counter!(
DIAGNOSTIC_METRIC,
"code" => code,
"category" => category,
"severity" => severity
)
.increment(1);
tracing::error!(
target: "ddlint::parser",
code,
category,
severity,
span_start,
span_end,
message,
"parser diagnostic"
);
}
}

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.

🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Reconsider hard-coding tracing::error! for every diagnostic.

A parser diagnostic represents expected, potentially very frequent, user-input mistakes (a syntax typo whilst editing), not a system fault. Hosts that treat ERROR-level logs as actionable will get paged or alert-fatigued on ordinary typos. The retrieved guidance for this repo is explicit that diagnostics shouldn't trigger alerts on existence alone, only on sustained rate/ratio — emitting at error! unconditionally works against that by default. Emit at tracing::warn! (or select the level from context.severity() once more variants exist) so hosts aren't nudged toward per-occurrence alerting.

Based on learnings, "Configure alerts on sustained diagnostic-to-parser-attempt rates or ratios after a minimum traffic threshold; do not page solely because a diagnostic exists, since invalid user input may be expected."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/parser/observability/telemetry.rs` around lines 57 - 83, Change the
diagnostic logging call in diagnostic_emitted from tracing::error! to
tracing::warn!, preserving its existing target, fields, and message. Keep the
metric emission unchanged so alerting can continue to use sustained diagnostic
rates or ratios rather than individual error-level logs.

Source: Learnings

Comment on lines 108 to 122
let spans = match span_result {
Ok(spans) => spans,
Ok(spans) => {
observer.parse_attempt_completed(super::observability::ParseAttemptContext::new(
DiagnosticCategory::SpanBuilder,
0,
));
spans
}
Err(err) => {
all_errors.push(chumsky::error::Simple::custom(0..0, err.to_string()));
let span_errors = [chumsky::error::Simple::custom(0..0, err.to_string())];
complete_attempt(observer, DiagnosticCategory::SpanBuilder, &span_errors);
all_errors.extend(span_errors);
ParsedSpans::default()
}
};

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.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Route the SpanBuilder success path through complete_attempt for consistency.

Every other attempt in this file (including the Err arm directly below) reports completion via complete_attempt, which also drives report_diagnostics. The Ok arm instead hand-constructs ParseAttemptContext via a fully-qualified path, since ParseAttemptContext isn't imported here. This is harmless today (zero errors on the happy path) but diverges silently if complete_attempt's behaviour ever changes, and the fully-qualified path is avoidable.

♻️ Proposed fix
-use super::observability::{ParseObserver, complete_attempt};
+use super::observability::{ParseAttemptContext, ParseObserver, complete_attempt};
     let spans = match span_result {
         Ok(spans) => {
-            observer.parse_attempt_completed(super::observability::ParseAttemptContext::new(
-                DiagnosticCategory::SpanBuilder,
-                0,
-            ));
+            complete_attempt(observer, DiagnosticCategory::SpanBuilder, &[]);
             spans
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/parser/span_scanner.rs` around lines 108 - 122, Update the Ok arm of the
span_result match to call complete_attempt with DiagnosticCategory::SpanBuilder
and an empty span-error collection, matching the existing Err arm’s completion
flow. Remove the direct ParseAttemptContext construction and ensure the
successful spans are still returned unchanged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add parser diagnostic observability infrastructure

1 participant