From 5706fa625ecc45c78d70cc7a7685cb008a167811 Mon Sep 17 00:00:00 2001 From: Aryan Putta Date: Fri, 26 Jun 2026 04:43:45 -0400 Subject: [PATCH 1/6] feat(export): include accumulated warnings in output json Active TraceWarnings accumulated during processing are now emitted as "trace_issue" metadata (M) events during drain and flow through the pipeline. The JSON exporter captures those events and folds them into otherData.issues in the output file, so important warnings are not lost when console output is ignored or drowned out. This follows the meta-event approach introduced in #117: warnings ride through the pipeline as events instead of being tracked on the side, so no stage context needs to be kept alive past drain(). Resolves #113. Signed-off-by: Aryan Putta --- src/aiu_trace_analyzer/core/engine.py | 1 + src/aiu_trace_analyzer/core/processing.py | 9 +++ src/aiu_trace_analyzer/export/exporter.py | 6 ++ src/aiu_trace_analyzer/pipeline/context.py | 16 ++++- src/aiu_trace_analyzer/types.py | 5 ++ .../core/test_processing_issues.py | 60 +++++++++++++++++++ .../export/test_exporter.py | 52 ++++++++++++++++ .../pipeline/test_context.py | 19 +++++- 8 files changed, 166 insertions(+), 2 deletions(-) create mode 100644 tests/aiu_trace_analyzer/core/test_processing_issues.py create mode 100644 tests/aiu_trace_analyzer/export/test_exporter.py diff --git a/src/aiu_trace_analyzer/core/engine.py b/src/aiu_trace_analyzer/core/engine.py index 019406c..b3d3fea 100644 --- a/src/aiu_trace_analyzer/core/engine.py +++ b/src/aiu_trace_analyzer/core/engine.py @@ -30,6 +30,7 @@ def run(self) -> int: self.exporter.export(events) # drain the context buffers (if any) + # accumulated warnings ride along as trace_issue meta-events that the exporter captures drain = self.processor.drain() # export any events emitted during drain self.exporter.export(drain) diff --git a/src/aiu_trace_analyzer/core/processing.py b/src/aiu_trace_analyzer/core/processing.py index 7c2008a..9282d17 100644 --- a/src/aiu_trace_analyzer/core/processing.py +++ b/src/aiu_trace_analyzer/core/processing.py @@ -131,6 +131,7 @@ def drain(self) -> list[aiuev.AbstractEventType]: # walk through the registered pre-processing hooks for the event # split any returned list of events into single events for each next stage pre-processor next_event_list = [] + issue_events = [] while len(self.stages) > 0: # remove the first hook from the chain to drain and process any remaining/buffered events @@ -143,4 +144,12 @@ def drain(self) -> list[aiuev.AbstractEventType]: # then process the events that came back using the remaining pre-processing hooks + pipeline for event in pending: next_event_list += self.process(event) + + # collect any warnings the context accumulated (after its own drain) as meta-events + if drain_context: + issue_events += drain_context.emit_issue_events() + + # fold the accumulated warnings in as meta-events so the exporter can capture them into the output + if issue_events: + next_event_list += self.convert_events(issue_events) return next_event_list diff --git a/src/aiu_trace_analyzer/export/exporter.py b/src/aiu_trace_analyzer/export/exporter.py index 28d174d..b2446a1 100644 --- a/src/aiu_trace_analyzer/export/exporter.py +++ b/src/aiu_trace_analyzer/export/exporter.py @@ -10,6 +10,7 @@ import aiu_trace_analyzer.logger as aiulog import aiu_trace_analyzer.trace_view as tv +from aiu_trace_analyzer.types import TRACE_ISSUE_EVENT_NAME from aiu_trace_analyzer.verification.report import ( VERIFICATION_RESULT_NAME, VERIFICATION_TEST_RESULT_NAME, @@ -77,6 +78,11 @@ def __init__(self, target_uri, timescale="ms", settings=None) -> None: # take (a list) of events and append to the traceview def export(self, data: list[tv.AbstractEventType]): for event in data: + # trace_issue meta-events carry accumulated warnings: fold them into otherData + # instead of the trace event stream so problems stay visible in the output file + if event.ph == "M" and event.name == TRACE_ISSUE_EVENT_NAME: + self.traceview.other_data.setdefault("issues", {})[event.args["finding"]] = event.args["text"] + continue self.traceview.append_trace_event(event.json()) def export_meta(self, meta_data): diff --git a/src/aiu_trace_analyzer/pipeline/context.py b/src/aiu_trace_analyzer/pipeline/context.py index afab6bb..ab4567b 100644 --- a/src/aiu_trace_analyzer/pipeline/context.py +++ b/src/aiu_trace_analyzer/pipeline/context.py @@ -1,7 +1,7 @@ # Copyright 2024-2025 IBM Corporation import aiu_trace_analyzer.logger as aiulog -from aiu_trace_analyzer.types import TraceEvent, TraceWarning +from aiu_trace_analyzer.types import TraceEvent, TraceWarning, TRACE_ISSUE_EVENT_NAME class AbstractContext: @@ -52,6 +52,20 @@ def print_warnings(self) -> None: if w.has_warning(): aiulog.log(aiulog.WARN, w) + def emit_issue_events(self) -> list[TraceEvent]: + ''' + emit each active warning as a meta-event so the exporter can fold it into the output json. + this mirrors the verification-event mechanism (see _emit_verification_events) but targets the + regular trace output: the warnings ride through the pipeline as events instead of being tracked + on the side, so no context needs to be kept alive past drain(). + ''' + return [ + TraceEvent({"ph": "M", "ts": 0, "pid": 0, + "name": TRACE_ISSUE_EVENT_NAME, + "args": {"finding": name, "text": str(w)}}) + for name, w in self.warnings.items() if w.has_warning() + ] + def add_warning(self, warning: TraceWarning): self.warnings[warning.get_name()] = warning diff --git a/src/aiu_trace_analyzer/types.py b/src/aiu_trace_analyzer/types.py index a981b03..c4f8fc4 100644 --- a/src/aiu_trace_analyzer/types.py +++ b/src/aiu_trace_analyzer/types.py @@ -11,6 +11,11 @@ class TraceEvent(dict): pass +# name of the meta-event used to carry an accumulated warning through the pipeline +# so the exporter can fold it into the output json instead of losing it in the console +TRACE_ISSUE_EVENT_NAME = "trace_issue" + + class InputDialect: categories = set() dialect_map = {} diff --git a/tests/aiu_trace_analyzer/core/test_processing_issues.py b/tests/aiu_trace_analyzer/core/test_processing_issues.py new file mode 100644 index 0000000..c68c801 --- /dev/null +++ b/tests/aiu_trace_analyzer/core/test_processing_issues.py @@ -0,0 +1,60 @@ +# Copyright 2024-2026 IBM Corporation + +import json + +import pytest + +from aiu_trace_analyzer.core.processing import EventProcessor +from aiu_trace_analyzer.pipeline.context import AbstractContext +from aiu_trace_analyzer.types import TraceWarning, TRACE_ISSUE_EVENT_NAME +from aiu_trace_analyzer.export.exporter import JsonFileTraceExporter + + +@pytest.fixture +def warned_context() -> AbstractContext: + warning = TraceWarning( + name="long_dur", + text="OVC: Detected {d[count]} long event(s).", + data={"count": 0}, + update_fn={"count": int.__add__}, + auto_log=False, + ) + ctx = AbstractContext(warnings=[warning]) + ctx.enable() + return ctx + + +def _processor_with(context: AbstractContext) -> EventProcessor: + proc = EventProcessor() + # register the stage directly to avoid pulling in a full StageProfile for the test + proc.stages.append((lambda event, ctx: [event], context, {})) + return proc + + +def test_drain_emits_active_warning_as_meta_event(warned_context): + warned_context.issue_warning("long_dur", {"count": 3}) + + drained = _processor_with(warned_context).drain() + + issue_events = [e for e in drained if e.name == TRACE_ISSUE_EVENT_NAME] + assert len(issue_events) == 1 + assert issue_events[0].args == {"finding": "long_dur", + "text": "OVC: Detected 3 long event(s)."} + + +def test_drain_emits_nothing_when_no_warning(warned_context): + drained = _processor_with(warned_context).drain() + + assert [e for e in drained if e.name == TRACE_ISSUE_EVENT_NAME] == [] + + +def test_warning_reaches_exporter_other_data(warned_context): + warned_context.issue_warning("long_dur", {"count": 3}) + + drained = _processor_with(warned_context).drain() + exporter = JsonFileTraceExporter(target_uri="unused.json") + exporter.export(drained) + + output = json.loads(exporter.get_data()) + assert output["otherData"]["issues"] == {"long_dur": "OVC: Detected 3 long event(s)."} + assert output["traceEvents"] == [] diff --git a/tests/aiu_trace_analyzer/export/test_exporter.py b/tests/aiu_trace_analyzer/export/test_exporter.py new file mode 100644 index 0000000..530382c --- /dev/null +++ b/tests/aiu_trace_analyzer/export/test_exporter.py @@ -0,0 +1,52 @@ +# Copyright 2024-2026 IBM Corporation + +import json + +import pytest + +from aiu_trace_analyzer.types import TRACE_ISSUE_EVENT_NAME +from aiu_trace_analyzer.trace_view import AbstractEventType +from aiu_trace_analyzer.export.exporter import JsonFileTraceExporter + + +@pytest.fixture +def json_exporter() -> JsonFileTraceExporter: + return JsonFileTraceExporter(target_uri="unused.json") + + +def _issue_event(finding: str, text: str) -> AbstractEventType: + return AbstractEventType.from_dict({ + "ph": "M", "ts": 0, "pid": 0, + "name": TRACE_ISSUE_EVENT_NAME, + "args": {"finding": finding, "text": text}, + }) + + +def _instant_event() -> AbstractEventType: + return AbstractEventType.from_dict({ + "ph": "i", "ts": 1, "pid": 0, "tid": 0, "s": "g", + "name": "regular_event", "args": {}, + }) + + +def test_export_captures_issue_events(json_exporter): + text = "OVC: Detected 3 event(s) with long duration." + json_exporter.export([_issue_event("long_dur", text)]) + + other_data = json.loads(json_exporter.get_data())["otherData"] + assert other_data["issues"] == {"long_dur": text} + + +def test_export_issue_events_do_not_leak_into_trace(json_exporter): + json_exporter.export([_issue_event("long_dur", "text"), _instant_event()]) + + dumped = json.loads(json_exporter.get_data()) + names = [e["name"] for e in dumped["traceEvents"]] + assert TRACE_ISSUE_EVENT_NAME not in names + assert "regular_event" in names + + +def test_export_no_issue_section_when_absent(json_exporter): + json_exporter.export([_instant_event()]) + + assert "issues" not in json.loads(json_exporter.get_data())["otherData"] diff --git a/tests/aiu_trace_analyzer/pipeline/test_context.py b/tests/aiu_trace_analyzer/pipeline/test_context.py index 0c0c4ef..ebb57c5 100644 --- a/tests/aiu_trace_analyzer/pipeline/test_context.py +++ b/tests/aiu_trace_analyzer/pipeline/test_context.py @@ -3,7 +3,7 @@ import pytest import math -from aiu_trace_analyzer.types import TraceWarning +from aiu_trace_analyzer.types import TraceWarning, TRACE_ISSUE_EVENT_NAME from aiu_trace_analyzer.pipeline import AbstractContext from aiu_trace_analyzer.pipeline.context import AbstractVerificationContext @@ -135,6 +135,23 @@ def test_issue_warning(abstract_context): assert abstract_context.warnings["pytest"].__str__() == "A Warning with 2 args: 2 and 5.0" +def test_emit_issue_events_none_when_inactive(abstract_context): + abstract_context.warnings["pytest"].auto_log = False # disable auto-output for tests + assert abstract_context.emit_issue_events() == [] + + +def test_emit_issue_events(abstract_context): + abstract_context.warnings["pytest"].auto_log = False # disable auto-output for tests + abstract_context.issue_warning("pytest", {"count": 1, "max": 5.0}) + + events = abstract_context.emit_issue_events() + + assert len(events) == 1 + assert events[0]["ph"] == "M" + assert events[0]["name"] == TRACE_ISSUE_EVENT_NAME + assert events[0]["args"] == {"finding": "pytest", "text": "A Warning with 2 args: 1 and 5.0"} + + def test_drain(abstract_context): assert abstract_context.drain() == [] From 46c8765ee588916cce9e7704b0670da653677508 Mon Sep 17 00:00:00 2001 From: Aryan Putta Date: Thu, 16 Jul 2026 21:38:35 -0400 Subject: [PATCH 2/6] refactor(processing): route diagnostic events through drain Signed-off-by: Aryan Putta --- src/aiu_trace_analyzer/core/processing.py | 15 +++++------ src/aiu_trace_analyzer/pipeline/context.py | 27 +++++++++---------- src/aiu_trace_analyzer/types.py | 4 +++ .../core/test_processing_issues.py | 14 ++++++++++ 4 files changed, 37 insertions(+), 23 deletions(-) diff --git a/src/aiu_trace_analyzer/core/processing.py b/src/aiu_trace_analyzer/core/processing.py index 9282d17..b7b47f6 100644 --- a/src/aiu_trace_analyzer/core/processing.py +++ b/src/aiu_trace_analyzer/core/processing.py @@ -5,7 +5,7 @@ import aiu_trace_analyzer.trace_view as aiuev import aiu_trace_analyzer.pipeline.context as procCTX -from aiu_trace_analyzer.types import TraceEvent +from aiu_trace_analyzer.types import DiagnosticEvent, TraceEvent from aiu_trace_analyzer.core.duplicate_hold import IntermediateDuplicateAndHoldContext, duplicate_and_hold from aiu_trace_analyzer.export.exporter import JsonFileTraceExporter from aiu_trace_analyzer.core.stage_profile import StageProfile, StageProfileChecker @@ -81,6 +81,11 @@ def process(self, event: TraceEvent) -> list[aiuev.AbstractEventType]: # turn into a list, pre/post have do be able to expand single events into lists aiulog.log(aiulog.DEBUG, "Processing event:", event) + if isinstance(event, DiagnosticEvent): + output_event_list = self.convert_events([event]) + self.event_count += len(output_event_list) + return output_event_list + event_list = self.pre_process(event) output_event_list = self.convert_events(event_list) @@ -131,7 +136,6 @@ def drain(self) -> list[aiuev.AbstractEventType]: # walk through the registered pre-processing hooks for the event # split any returned list of events into single events for each next stage pre-processor next_event_list = [] - issue_events = [] while len(self.stages) > 0: # remove the first hook from the chain to drain and process any remaining/buffered events @@ -145,11 +149,4 @@ def drain(self) -> list[aiuev.AbstractEventType]: for event in pending: next_event_list += self.process(event) - # collect any warnings the context accumulated (after its own drain) as meta-events - if drain_context: - issue_events += drain_context.emit_issue_events() - - # fold the accumulated warnings in as meta-events so the exporter can capture them into the output - if issue_events: - next_event_list += self.convert_events(issue_events) return next_event_list diff --git a/src/aiu_trace_analyzer/pipeline/context.py b/src/aiu_trace_analyzer/pipeline/context.py index ab4567b..0ecb16c 100644 --- a/src/aiu_trace_analyzer/pipeline/context.py +++ b/src/aiu_trace_analyzer/pipeline/context.py @@ -1,7 +1,7 @@ # Copyright 2024-2025 IBM Corporation import aiu_trace_analyzer.logger as aiulog -from aiu_trace_analyzer.types import TraceEvent, TraceWarning, TRACE_ISSUE_EVENT_NAME +from aiu_trace_analyzer.types import DiagnosticEvent, TraceEvent, TraceWarning, TRACE_ISSUE_EVENT_NAME class AbstractContext: @@ -60,9 +60,9 @@ def emit_issue_events(self) -> list[TraceEvent]: on the side, so no context needs to be kept alive past drain(). ''' return [ - TraceEvent({"ph": "M", "ts": 0, "pid": 0, - "name": TRACE_ISSUE_EVENT_NAME, - "args": {"finding": name, "text": str(w)}}) + DiagnosticEvent({"ph": "M", "ts": 0, "pid": 0, + "name": TRACE_ISSUE_EVENT_NAME, + "args": {"finding": name, "text": str(w)}}) for name, w in self.warnings.items() if w.has_warning() ] @@ -86,13 +86,13 @@ def drain(self) -> list[TraceEvent]: a list of events. Events are drained following the sequence of registered processing functions. ''' - return [] + return self.emit_issue_events() def _emit_verification_events(self) -> list[TraceEvent]: return [ - TraceEvent({"ph": "M", "ts": 0, "pid": 0, - "name": "verification_data", - "args": w.to_verification_event_args()}) + DiagnosticEvent({"ph": "M", "ts": 0, "pid": 0, + "name": "verification_data", + "args": w.to_verification_event_args()}) for w in self.warnings.values() ] @@ -105,17 +105,16 @@ def _get_test_result_status(self) -> str: return "pass" def _emit_test_result_event(self, test_name: str) -> TraceEvent: - return TraceEvent({"ph": "M", "ts": 0, "pid": 0, - "name": "verification_test_result", - "args": {"test": test_name, - "result": self._get_test_result_status()}}) + return DiagnosticEvent({"ph": "M", "ts": 0, "pid": 0, + "name": "verification_test_result", + "args": {"test": test_name, + "result": self._get_test_result_status()}}) class AbstractVerificationContext(AbstractContext): test_name: str = "" def drain(self) -> list[TraceEvent]: - events = super().drain() - events += self._emit_verification_events() + events = self._emit_verification_events() events.append(self._emit_test_result_event(self.test_name)) return events diff --git a/src/aiu_trace_analyzer/types.py b/src/aiu_trace_analyzer/types.py index c4f8fc4..10fe97b 100644 --- a/src/aiu_trace_analyzer/types.py +++ b/src/aiu_trace_analyzer/types.py @@ -11,6 +11,10 @@ class TraceEvent(dict): pass +class DiagnosticEvent(TraceEvent): + pass + + # name of the meta-event used to carry an accumulated warning through the pipeline # so the exporter can fold it into the output json instead of losing it in the console TRACE_ISSUE_EVENT_NAME = "trace_issue" diff --git a/tests/aiu_trace_analyzer/core/test_processing_issues.py b/tests/aiu_trace_analyzer/core/test_processing_issues.py index c68c801..35aea2c 100644 --- a/tests/aiu_trace_analyzer/core/test_processing_issues.py +++ b/tests/aiu_trace_analyzer/core/test_processing_issues.py @@ -58,3 +58,17 @@ def test_warning_reaches_exporter_other_data(warned_context): output = json.loads(exporter.get_data()) assert output["otherData"]["issues"] == {"long_dur": "OVC: Detected 3 long event(s)."} assert output["traceEvents"] == [] + + +def test_drain_warning_bypasses_remaining_pipeline_stages(warned_context): + warned_context.issue_warning("long_dur", {"count": 3}) + proc = _processor_with(warned_context) + + def drop_everything(event, ctx): + return [] + + proc.stages.append((drop_everything, None, {})) + + drained = proc.drain() + + assert [e for e in drained if e.name == TRACE_ISSUE_EVENT_NAME] From bf1d7e513ffa305b5cdb7ca556ff4a2a046dc9d5 Mon Sep 17 00:00:00 2001 From: Aryan Putta Date: Wed, 29 Jul 2026 08:30:38 -0400 Subject: [PATCH 3/6] refactor(export): key trace issues by warning or error severity The trace_issue metadata events carried a generic "finding" key, which dropped the distinction the TraceWarning already tracks via is_error. The args key is now "warning" or "error", and the json exporter groups otherData.issues by that severity. Signed-off-by: Aryan Putta --- src/aiu_trace_analyzer/export/exporter.py | 7 ++++--- src/aiu_trace_analyzer/pipeline/context.py | 5 +---- src/aiu_trace_analyzer/types.py | 8 +++++++- .../core/test_processing_issues.py | 4 ++-- .../export/test_exporter.py | 15 +++++++++++--- .../pipeline/test_context.py | 20 ++++++++++++++++++- 6 files changed, 45 insertions(+), 14 deletions(-) diff --git a/src/aiu_trace_analyzer/export/exporter.py b/src/aiu_trace_analyzer/export/exporter.py index b2446a1..4011256 100644 --- a/src/aiu_trace_analyzer/export/exporter.py +++ b/src/aiu_trace_analyzer/export/exporter.py @@ -78,10 +78,11 @@ def __init__(self, target_uri, timescale="ms", settings=None) -> None: # take (a list) of events and append to the traceview def export(self, data: list[tv.AbstractEventType]): for event in data: - # trace_issue meta-events carry accumulated warnings: fold them into otherData - # instead of the trace event stream so problems stay visible in the output file + # trace_issue meta-events go into otherData, not into the trace event stream if event.ph == "M" and event.name == TRACE_ISSUE_EVENT_NAME: - self.traceview.other_data.setdefault("issues", {})[event.args["finding"]] = event.args["text"] + severity = "error" if "error" in event.args else "warning" + issues = self.traceview.other_data.setdefault("issues", {}) + issues.setdefault(severity, {})[event.args[severity]] = event.args["text"] continue self.traceview.append_trace_event(event.json()) diff --git a/src/aiu_trace_analyzer/pipeline/context.py b/src/aiu_trace_analyzer/pipeline/context.py index 0ecb16c..f127617 100644 --- a/src/aiu_trace_analyzer/pipeline/context.py +++ b/src/aiu_trace_analyzer/pipeline/context.py @@ -55,14 +55,11 @@ def print_warnings(self) -> None: def emit_issue_events(self) -> list[TraceEvent]: ''' emit each active warning as a meta-event so the exporter can fold it into the output json. - this mirrors the verification-event mechanism (see _emit_verification_events) but targets the - regular trace output: the warnings ride through the pipeline as events instead of being tracked - on the side, so no context needs to be kept alive past drain(). ''' return [ DiagnosticEvent({"ph": "M", "ts": 0, "pid": 0, "name": TRACE_ISSUE_EVENT_NAME, - "args": {"finding": name, "text": str(w)}}) + "args": {w.severity(): name, "text": str(w)}}) for name, w in self.warnings.items() if w.has_warning() ] diff --git a/src/aiu_trace_analyzer/types.py b/src/aiu_trace_analyzer/types.py index 10fe97b..c7589a2 100644 --- a/src/aiu_trace_analyzer/types.py +++ b/src/aiu_trace_analyzer/types.py @@ -301,13 +301,19 @@ def update(self, def has_warning(self) -> bool: return self.occurred + def is_error(self) -> bool: + return self.warn_level == aiulog.ERROR + + def severity(self) -> str: + return "error" if self.is_error() else "warning" + def add_instance(self, data: dict) -> None: self._instances.append(data) def to_verification_event_args(self) -> dict: return { "finding": self.name, - "is_error": self.warn_level == aiulog.ERROR, + "is_error": self.is_error(), "count": self.args_list.get("count", len(self._instances)), "instances": list(self._instances), } diff --git a/tests/aiu_trace_analyzer/core/test_processing_issues.py b/tests/aiu_trace_analyzer/core/test_processing_issues.py index 35aea2c..83941c6 100644 --- a/tests/aiu_trace_analyzer/core/test_processing_issues.py +++ b/tests/aiu_trace_analyzer/core/test_processing_issues.py @@ -38,7 +38,7 @@ def test_drain_emits_active_warning_as_meta_event(warned_context): issue_events = [e for e in drained if e.name == TRACE_ISSUE_EVENT_NAME] assert len(issue_events) == 1 - assert issue_events[0].args == {"finding": "long_dur", + assert issue_events[0].args == {"warning": "long_dur", "text": "OVC: Detected 3 long event(s)."} @@ -56,7 +56,7 @@ def test_warning_reaches_exporter_other_data(warned_context): exporter.export(drained) output = json.loads(exporter.get_data()) - assert output["otherData"]["issues"] == {"long_dur": "OVC: Detected 3 long event(s)."} + assert output["otherData"]["issues"] == {"warning": {"long_dur": "OVC: Detected 3 long event(s)."}} assert output["traceEvents"] == [] diff --git a/tests/aiu_trace_analyzer/export/test_exporter.py b/tests/aiu_trace_analyzer/export/test_exporter.py index 530382c..12216e0 100644 --- a/tests/aiu_trace_analyzer/export/test_exporter.py +++ b/tests/aiu_trace_analyzer/export/test_exporter.py @@ -14,11 +14,11 @@ def json_exporter() -> JsonFileTraceExporter: return JsonFileTraceExporter(target_uri="unused.json") -def _issue_event(finding: str, text: str) -> AbstractEventType: +def _issue_event(name: str, text: str, severity: str = "warning") -> AbstractEventType: return AbstractEventType.from_dict({ "ph": "M", "ts": 0, "pid": 0, "name": TRACE_ISSUE_EVENT_NAME, - "args": {"finding": finding, "text": text}, + "args": {severity: name, "text": text}, }) @@ -34,7 +34,16 @@ def test_export_captures_issue_events(json_exporter): json_exporter.export([_issue_event("long_dur", text)]) other_data = json.loads(json_exporter.get_data())["otherData"] - assert other_data["issues"] == {"long_dur": text} + assert other_data["issues"] == {"warning": {"long_dur": text}} + + +def test_export_separates_errors_from_warnings(json_exporter): + json_exporter.export([_issue_event("long_dur", "warn text"), + _issue_event("bad_ts", "error text", severity="error")]) + + other_data = json.loads(json_exporter.get_data())["otherData"] + assert other_data["issues"] == {"warning": {"long_dur": "warn text"}, + "error": {"bad_ts": "error text"}} def test_export_issue_events_do_not_leak_into_trace(json_exporter): diff --git a/tests/aiu_trace_analyzer/pipeline/test_context.py b/tests/aiu_trace_analyzer/pipeline/test_context.py index ebb57c5..0a02070 100644 --- a/tests/aiu_trace_analyzer/pipeline/test_context.py +++ b/tests/aiu_trace_analyzer/pipeline/test_context.py @@ -149,7 +149,25 @@ def test_emit_issue_events(abstract_context): assert len(events) == 1 assert events[0]["ph"] == "M" assert events[0]["name"] == TRACE_ISSUE_EVENT_NAME - assert events[0]["args"] == {"finding": "pytest", "text": "A Warning with 2 args: 1 and 5.0"} + assert events[0]["args"] == {"warning": "pytest", "text": "A Warning with 2 args: 1 and 5.0"} + + +def test_emit_issue_events_of_error_warning(): + error = TraceWarning( + name="pytest_err", + text="An Error with {d[count]} occurrence(s)", + data={"count": 0}, + update_fn={"count": int.__add__}, + auto_log=False, + is_error=True, + ) + context = AbstractContext(warnings=[error]) + context.issue_warning("pytest_err", {"count": 1}) + + events = context.emit_issue_events() + + assert len(events) == 1 + assert events[0]["args"] == {"error": "pytest_err", "text": "An Error with 1 occurrence(s)"} def test_drain(abstract_context): From 6f4b8a762ec59af237a0081df83c16240aee4784 Mon Sep 17 00:00:00 2001 From: Aryan Putta Date: Mon, 3 Aug 2026 13:32:57 -0400 Subject: [PATCH 4/6] fix(export): preserve verification issue metadata Signed-off-by: Aryan Putta --- src/aiu_trace_analyzer/pipeline/context.py | 3 ++- tests/aiu_trace_analyzer/pipeline/test_context.py | 12 ++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/aiu_trace_analyzer/pipeline/context.py b/src/aiu_trace_analyzer/pipeline/context.py index f127617..0a6bea0 100644 --- a/src/aiu_trace_analyzer/pipeline/context.py +++ b/src/aiu_trace_analyzer/pipeline/context.py @@ -112,6 +112,7 @@ class AbstractVerificationContext(AbstractContext): test_name: str = "" def drain(self) -> list[TraceEvent]: - events = self._emit_verification_events() + events = super().drain() + events += self._emit_verification_events() events.append(self._emit_test_result_event(self.test_name)) return events diff --git a/tests/aiu_trace_analyzer/pipeline/test_context.py b/tests/aiu_trace_analyzer/pipeline/test_context.py index 0a02070..ab37d7e 100644 --- a/tests/aiu_trace_analyzer/pipeline/test_context.py +++ b/tests/aiu_trace_analyzer/pipeline/test_context.py @@ -283,8 +283,14 @@ def test_v1_drain_no_warnings_produces_pass(verif_context_no_warnings): def test_v2_drain_warn_level_warning_produces_warn(verif_context_warn): verif_context_warn.warnings["test_w"].update({"count": 1}) events = verif_context_warn.drain() + issue_events = _find_events(events, TRACE_ISSUE_EVENT_NAME) test_result = _find_events(events, "verification_test_result")[0] assert test_result["args"]["result"] == "warn" + assert len(issue_events) == 1 + assert issue_events[0]["args"] == { + "warning": "test_w", + "text": "Found 1 issues", + } data_events = _find_events(events, "verification_data") assert len(data_events) == 1 assert data_events[0]["args"]["is_error"] is False @@ -293,8 +299,14 @@ def test_v2_drain_warn_level_warning_produces_warn(verif_context_warn): def test_v3_drain_error_level_warning_produces_fail(verif_context_error): verif_context_error.warnings["test_err"].update({"count": 1}) events = verif_context_error.drain() + issue_events = _find_events(events, TRACE_ISSUE_EVENT_NAME) test_result = _find_events(events, "verification_test_result")[0] assert test_result["args"]["result"] == "fail" + assert len(issue_events) == 1 + assert issue_events[0]["args"] == { + "error": "test_err", + "text": "Found 1 errors", + } data_events = _find_events(events, "verification_data") assert data_events[0]["args"]["is_error"] is True From 107e62ed368051749a2e14919a916bfa4e1747ee Mon Sep 17 00:00:00 2001 From: Aryan Putta Date: Mon, 3 Aug 2026 13:45:32 -0400 Subject: [PATCH 5/6] fix(export): align warning output with verification report Signed-off-by: Aryan Putta --- src/aiu_trace_analyzer/export/exporter.py | 7 ++++--- src/aiu_trace_analyzer/pipeline/context.py | 4 +++- .../core/test_processing_issues.py | 10 +++++++--- tests/aiu_trace_analyzer/export/test_exporter.py | 16 +++++++++------- .../aiu_trace_analyzer/pipeline/test_context.py | 14 ++++++++++---- 5 files changed, 33 insertions(+), 18 deletions(-) diff --git a/src/aiu_trace_analyzer/export/exporter.py b/src/aiu_trace_analyzer/export/exporter.py index 4011256..c338c56 100644 --- a/src/aiu_trace_analyzer/export/exporter.py +++ b/src/aiu_trace_analyzer/export/exporter.py @@ -80,9 +80,10 @@ def export(self, data: list[tv.AbstractEventType]): for event in data: # trace_issue meta-events go into otherData, not into the trace event stream if event.ph == "M" and event.name == TRACE_ISSUE_EVENT_NAME: - severity = "error" if "error" in event.args else "warning" - issues = self.traceview.other_data.setdefault("issues", {}) - issues.setdefault(severity, {})[event.args[severity]] = event.args["text"] + severity = "errors" if event.args["is_error"] else "warnings" + findings = self.traceview.other_data.setdefault(severity, []) + findings.append({"finding": event.args["finding"], + "text": event.args["text"]}) continue self.traceview.append_trace_event(event.json()) diff --git a/src/aiu_trace_analyzer/pipeline/context.py b/src/aiu_trace_analyzer/pipeline/context.py index 0a6bea0..a67246a 100644 --- a/src/aiu_trace_analyzer/pipeline/context.py +++ b/src/aiu_trace_analyzer/pipeline/context.py @@ -59,7 +59,9 @@ def emit_issue_events(self) -> list[TraceEvent]: return [ DiagnosticEvent({"ph": "M", "ts": 0, "pid": 0, "name": TRACE_ISSUE_EVENT_NAME, - "args": {w.severity(): name, "text": str(w)}}) + "args": {"finding": name, + "text": str(w), + "is_error": w.is_error()}}) for name, w in self.warnings.items() if w.has_warning() ] diff --git a/tests/aiu_trace_analyzer/core/test_processing_issues.py b/tests/aiu_trace_analyzer/core/test_processing_issues.py index 83941c6..0306396 100644 --- a/tests/aiu_trace_analyzer/core/test_processing_issues.py +++ b/tests/aiu_trace_analyzer/core/test_processing_issues.py @@ -38,8 +38,9 @@ def test_drain_emits_active_warning_as_meta_event(warned_context): issue_events = [e for e in drained if e.name == TRACE_ISSUE_EVENT_NAME] assert len(issue_events) == 1 - assert issue_events[0].args == {"warning": "long_dur", - "text": "OVC: Detected 3 long event(s)."} + assert issue_events[0].args == {"finding": "long_dur", + "text": "OVC: Detected 3 long event(s).", + "is_error": False} def test_drain_emits_nothing_when_no_warning(warned_context): @@ -56,7 +57,10 @@ def test_warning_reaches_exporter_other_data(warned_context): exporter.export(drained) output = json.loads(exporter.get_data()) - assert output["otherData"]["issues"] == {"warning": {"long_dur": "OVC: Detected 3 long event(s)."}} + assert output["otherData"]["warnings"] == [{ + "finding": "long_dur", + "text": "OVC: Detected 3 long event(s).", + }] assert output["traceEvents"] == [] diff --git a/tests/aiu_trace_analyzer/export/test_exporter.py b/tests/aiu_trace_analyzer/export/test_exporter.py index 12216e0..1126fc5 100644 --- a/tests/aiu_trace_analyzer/export/test_exporter.py +++ b/tests/aiu_trace_analyzer/export/test_exporter.py @@ -14,11 +14,11 @@ def json_exporter() -> JsonFileTraceExporter: return JsonFileTraceExporter(target_uri="unused.json") -def _issue_event(name: str, text: str, severity: str = "warning") -> AbstractEventType: +def _issue_event(name: str, text: str, is_error: bool = False) -> AbstractEventType: return AbstractEventType.from_dict({ "ph": "M", "ts": 0, "pid": 0, "name": TRACE_ISSUE_EVENT_NAME, - "args": {severity: name, "text": text}, + "args": {"finding": name, "text": text, "is_error": is_error}, }) @@ -34,16 +34,16 @@ def test_export_captures_issue_events(json_exporter): json_exporter.export([_issue_event("long_dur", text)]) other_data = json.loads(json_exporter.get_data())["otherData"] - assert other_data["issues"] == {"warning": {"long_dur": text}} + assert other_data["warnings"] == [{"finding": "long_dur", "text": text}] def test_export_separates_errors_from_warnings(json_exporter): json_exporter.export([_issue_event("long_dur", "warn text"), - _issue_event("bad_ts", "error text", severity="error")]) + _issue_event("bad_ts", "error text", is_error=True)]) other_data = json.loads(json_exporter.get_data())["otherData"] - assert other_data["issues"] == {"warning": {"long_dur": "warn text"}, - "error": {"bad_ts": "error text"}} + assert other_data["warnings"] == [{"finding": "long_dur", "text": "warn text"}] + assert other_data["errors"] == [{"finding": "bad_ts", "text": "error text"}] def test_export_issue_events_do_not_leak_into_trace(json_exporter): @@ -58,4 +58,6 @@ def test_export_issue_events_do_not_leak_into_trace(json_exporter): def test_export_no_issue_section_when_absent(json_exporter): json_exporter.export([_instant_event()]) - assert "issues" not in json.loads(json_exporter.get_data())["otherData"] + other_data = json.loads(json_exporter.get_data())["otherData"] + assert "warnings" not in other_data + assert "errors" not in other_data diff --git a/tests/aiu_trace_analyzer/pipeline/test_context.py b/tests/aiu_trace_analyzer/pipeline/test_context.py index ab37d7e..56101c4 100644 --- a/tests/aiu_trace_analyzer/pipeline/test_context.py +++ b/tests/aiu_trace_analyzer/pipeline/test_context.py @@ -149,7 +149,9 @@ def test_emit_issue_events(abstract_context): assert len(events) == 1 assert events[0]["ph"] == "M" assert events[0]["name"] == TRACE_ISSUE_EVENT_NAME - assert events[0]["args"] == {"warning": "pytest", "text": "A Warning with 2 args: 1 and 5.0"} + assert events[0]["args"] == {"finding": "pytest", + "text": "A Warning with 2 args: 1 and 5.0", + "is_error": False} def test_emit_issue_events_of_error_warning(): @@ -167,7 +169,9 @@ def test_emit_issue_events_of_error_warning(): events = context.emit_issue_events() assert len(events) == 1 - assert events[0]["args"] == {"error": "pytest_err", "text": "An Error with 1 occurrence(s)"} + assert events[0]["args"] == {"finding": "pytest_err", + "text": "An Error with 1 occurrence(s)", + "is_error": True} def test_drain(abstract_context): @@ -288,8 +292,9 @@ def test_v2_drain_warn_level_warning_produces_warn(verif_context_warn): assert test_result["args"]["result"] == "warn" assert len(issue_events) == 1 assert issue_events[0]["args"] == { - "warning": "test_w", + "finding": "test_w", "text": "Found 1 issues", + "is_error": False, } data_events = _find_events(events, "verification_data") assert len(data_events) == 1 @@ -304,8 +309,9 @@ def test_v3_drain_error_level_warning_produces_fail(verif_context_error): assert test_result["args"]["result"] == "fail" assert len(issue_events) == 1 assert issue_events[0]["args"] == { - "error": "test_err", + "finding": "test_err", "text": "Found 1 errors", + "is_error": True, } data_events = _find_events(events, "verification_data") assert data_events[0]["args"]["is_error"] is True From 69bb73b088f3a5731d2a958f6e65253862b85fdd Mon Sep 17 00:00:00 2001 From: Aryan Putta Date: Thu, 6 Aug 2026 13:34:23 -0400 Subject: [PATCH 6/6] fix(tests): correct continuation-line indentation in issue event asserts Signed-off-by: Aryan Putta --- tests/aiu_trace_analyzer/pipeline/test_context.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/aiu_trace_analyzer/pipeline/test_context.py b/tests/aiu_trace_analyzer/pipeline/test_context.py index 56101c4..377b5d6 100644 --- a/tests/aiu_trace_analyzer/pipeline/test_context.py +++ b/tests/aiu_trace_analyzer/pipeline/test_context.py @@ -150,8 +150,8 @@ def test_emit_issue_events(abstract_context): assert events[0]["ph"] == "M" assert events[0]["name"] == TRACE_ISSUE_EVENT_NAME assert events[0]["args"] == {"finding": "pytest", - "text": "A Warning with 2 args: 1 and 5.0", - "is_error": False} + "text": "A Warning with 2 args: 1 and 5.0", + "is_error": False} def test_emit_issue_events_of_error_warning(): @@ -170,8 +170,8 @@ def test_emit_issue_events_of_error_warning(): assert len(events) == 1 assert events[0]["args"] == {"finding": "pytest_err", - "text": "An Error with 1 occurrence(s)", - "is_error": True} + "text": "An Error with 1 occurrence(s)", + "is_error": True} def test_drain(abstract_context):