Repository navigation
FEAT: Fetch trace evidence from a remote OTLP/JSON backend - #4153
Open
WatchTree-19 wants to merge 1 commit into
Open
WatchTree-19 wants to merge 1 commit into
WatchTree-19 wants to merge 1 commit into
Conversation
Add OtlpJsonTraceClient, a TraceClient that fetches each requested trace by ID from an HTTP endpoint returning OTLP/JSON, such as Grafana Tempo's trace-by-ID API. It uses httpx, which PyRIT already depends on, accepts an injected httpx.AsyncClient, and keeps headers out of its identifier. A remote backend cannot say when every span has arrived, so coverage is never complete: an observed tool call stands and an absent one stays undetermined. A trace the backend does not have is unavailable. Failed requests, error statuses, timeouts and unreadable responses raise TraceAcquisitionError when nothing was retrieved, and leave coverage partial when another trace was. Timestamps are truncated, never rounded, to the microseconds a datetime holds.
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
Malformed OTLP timestamps can abort scoring or produce false-positive tool-call verdicts, while exception logging can expose private trace data.
3 open issues
What changed in this PR
Adds OtlpJsonTraceClient, a remote trace-retrieval adapter for PyRIT’s existing observation and tool-call scoring pipeline, without adding dependencies.
Changes:
- Fetches and converts OTLP/JSON traces over HTTP, with partial-coverage reporting and configurable client ownership.
- Exposes the adapter through lazy public exports and adds mocked retrieval and scoring tests.
- Documents remote trace acquisition with a runnable example.
| File | Description |
|---|---|
tests/unit/score/test_otlp_json_trace_client.py |
Tests parsing, failures, coverage, scoring, and client ownership. |
tests/unit/common/test_lazy_package_imports.py |
Extends export-parity and no-SDK-import checks. |
pyrit/score/observation/otlp_json_trace_client.py |
Implements HTTP retrieval and OTLP/JSON conversion. |
pyrit/score/observation/__init__.py |
Adds the observation-package lazy export. |
pyrit/score/__init__.py |
Adds the scoring-package lazy export. |
doc/code/scoring/5_tool_call_scorer.py |
Adds a remote-backend example using an in-process transport. |
doc/code/scoring/5_tool_call_scorer.ipynb |
Adds the matching notebook example and outputs. |
doc/code/framework.md |
Describes the adapter’s role and incomplete-coverage semantics. |
Review effort: Balanced
| try: | ||
| trace_spans = _parse_trace_document(response.json(), trace_id=trace_id) | ||
| except (ValueError, TypeError) as error: | ||
| logger.warning("Trace backend response could not be read (%s).", error) |
Comment on lines
+237
to
+242
| return TraceSpan( | ||
| trace_id=_hex_id(raw.get("traceId"), size=16), | ||
| span_id=_hex_id(raw.get("spanId"), size=8), | ||
| parent_span_id=None if parent is None else _hex_id(parent, size=8), | ||
| start_time=_timestamp(_nanos(raw.get("startTimeUnixNano"))), | ||
| end_time=_timestamp(end_nanos) if end_nanos else None, |
Comment on lines
+279
to
+280
| if nanos < 0: | ||
| raise ValueError("A span timestamp must not be negative.") |
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Description
Phase 5 of the scorer contract proposal separated retrieval (
TraceClient), acquisition (OtelTraceSource) and evaluation (OtelToolCallScorer), shipped the localInMemoryTraceClient, and deferred remote adapters. This adds one:OtlpJsonTraceClient, aTraceClientthat fetches each requested trace by ID from an HTTP endpoint returning OTLP/JSON.It is generic rather than a vendor SDK: OTLP/JSON is the OpenTelemetry wire format, and the client uses
httpx, which PyRIT already depends on. No dependency is added. It reads spans underresourceSpans(or the olderbatcheskey), at the top level or inside aresultortraceobject, with hex identities as the OTLP/JSON specification requires or base64 identities as some backends emit. Attribute values convert asInMemoryTraceExporterconverts them, bytes included. Timestamps are converted from integer nanoseconds exactly and truncated, never rounded, so conversion cannot reorder two spans. Callers can pass their ownhttpx.AsyncClientfor proxies, certificates or tests; otherwise each query opens and closes one throughget_httpx_client.The verdict rules stay as they are; the client only reports what it saw:
remote_completion_unknown). An observed tool call stands; a missing one is undetermined, never false.available=False, so the observation isUNAVAILABLE.TraceAcquisitionError, whichOtelTraceSourcealready maps toERROR. If another requested trace was retrieved, its spans are kept and coverage is partial, so a positive found there is not thrown away. A failed trace contributes no spans at all, rather than its readable remainder.TraceQuery.limitis applied, withquery_limit_exceededwhen it truncates. The source's own timeout still applies on top of the per-request timeout.The identifier records the endpoint without its query string, fragment or headers, which may hold credentials, and URLs with embedded credentials are rejected.
Tests and Documentation
tests/unit/score/test_otlp_json_trace_client.py(41 tests). Every request goes throughhttpx.MockTransportor a patched client factory; no test opens a network connection. Covered: span and attribute conversion, every accepted layout and identity encoding, unfinished spans, truncation of nanosecond timestamps, incomplete coverage, a remote positive and an undetermined absence throughOtelToolCallScorer, missing traces as unavailable, each failure kind as an acquisition error, one failed trace among several as partial evidence, duplicate spans, the query limit, a slow backend hitting the source timeout, headers and per-request timeout, client ownership, the identifier, and configuration validation.tests/unit/common/test_lazy_package_imports.py: the new export is added to the existing export-parity and no-SDK-import checks.doc/code/scoring/5_tool_call_scorer.pyand.ipynb: a section that reads a trace through an in-process transport standing in for a backend.doc/code/framework.mdmentions the client.How it was tested
Rebased on main 935afa8:
pytest tests/unit/score tests/unit/models tests/unit/common tests/unit/_code_execution tests/unit/output tests/unit/executor/attack -n 2: 11582 passed, 65 skipped. Ruff, format, ty and docs validation are clean. The notebook was executed and its outputs match the.pycells.The response shapes were checked against fixtures built from the OTLP/JSON format, not against a running backend.
Mutations, each applied alone and then restored, with the number of failing tests (22 of 22 caught):
result/tracewrappers ignoredbatcheskey ignored