Skip to content

Commit 8917cad

Browse files
observability: phase 6.1 PR-C — 4 conformance fixtures (#24)
* observability: phase 6.1 PR-C conformance fillin Drive four spec observability conformance fixtures end-to-end through the existing test_observability harness: - 002-otel-subgraph-hierarchy: §4.5 synthetic dispatch span parents under invocation; inner-node spans parent under dispatch span with the nested ["outer_sub", "inner_x"] / ["outer_sub", "inner_y"] namespace. - 003-otel-error-status: §4.2 ERROR status mapping for the node_exception case (status_description == "node_exception", recorded exception event, openarmature.error.category attribute, ok_node stays OK). - 007-otel-retry-attempt-spans: both sub-cases. Three sibling attempt spans with distinct attempt_index 0..2, single shared invocation parent. ERROR/ERROR/OK on retry-succeeds-third; ERROR x3 on retry-exhausts. - 011-otel-determinism: signature-tuple comparison (name, status_code, sorted attrs minus the canonical non-deterministic-by-design ignore set: invocation_id + auto-generated correlation_id) across two runs. Two minor YAML-shape translations kept inline in the fixture-specific drivers so the existing adapter stays unchanged (the adapter is shared with other suites; this PR doesn't touch it): - Fixture 007 flaky.fail_count=N rewritten to the adapter's failure_sequence shape before build_graph. - Fixture 011 when:{field, gt: 0} edge syntax rewritten to the adapter's condition:{if_field, equals, then, else} shape via _translate_011_when_edges (gt:0 with the fixture's deterministic input is functionally equivalent to equals:1 because counter==1 always under that input). Map updates: - _SUPPORTED_FIXTURES grows from 4 to 8. - _DEFERRED_FIXTURES shrinks from 7 to 3 (004 / 006 / 010); each remaining note now spells out the specific gating-PR + spec/architectural rationale (PR-C.1 + proposal 0012 / PR-C.2 / PR-C.3) rather than the generic "deferred to Phase 6.1" placeholder. Module docstring rewritten to distinguish the 8 driven fixtures from the 3 deferred ones. No engine, observer, or correlation changes. Pure test-side work. 392 tests pass (was 388; net +4 from the new fixture drivers). 5 skipped (3 deferred conformance fixtures + 2 pre-existing). Pyright clean. * otel: address PR #24 review - Fix invocation span status propagation. The OTelObserver was unconditionally calling set_status(OK) on the invocation span in _close_invocation_span; OTel doesn't auto-propagate child status to parents, so the spec §4.2 / fixture 003 contract ("invocation span ends ERROR when a child errored") wasn't satisfied. _handle_completed now sets ERROR on the invocation span when a child errors; _close_invocation_span no longer forces OK (clean invocations end UNSET, which exporters map to OK; ERROR is preserved by OTel SDK status precedence). Driver _run_fixture_003 gains the explicit invocation-status ERROR assertion + docstring fix. - Fix _signature in _run_fixture_011 to include parent_name. Previously the docstring claimed "hierarchy" and the comment said "parent-by-name" but the signature ignored span.parent entirely. Now looks up parent.span_id in a per-run by-id map and includes the parent's name; a hierarchy regression where a node reparented to a different ancestor now surfaces as a signature divergence. - Fix comment drift in _run_fixture_011_case: the translated edge condition is "equals: 1" (matching the deterministic counter == 1 input), not "equals: True".
1 parent 5afd325 commit 8917cad

2 files changed

Lines changed: 525 additions & 47 deletions

File tree

src/openarmature/observability/otel/observer.py

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,17 @@ def _handle_completed(self, event: NodeEvent) -> None:
297297
span.set_status(Status(StatusCode.ERROR, description=event.error.category))
298298
span.record_exception(event.error)
299299
span.set_attribute("openarmature.error.category", event.error.category)
300+
# Per spec §4.2 / fixture 003: the invocation span MUST
301+
# end with ERROR status when any child node errors. OTel
302+
# doesn't auto-propagate child status to parents — we set
303+
# it explicitly here. The OTel SDK's status-precedence
304+
# rule preserves ERROR through any subsequent
305+
# ``set_status(OK)`` calls (only UNSET → OK transitions
306+
# are honoured), so the close path's UNSET-leave still
307+
# works for clean invocations.
308+
inv_open = self._invocation_span.get(invocation_id)
309+
if inv_open is not None:
310+
inv_open.span.set_status(Status(StatusCode.ERROR, description=event.error.category))
300311
else:
301312
span.set_status(Status(StatusCode.OK))
302313
span.end()
@@ -839,11 +850,18 @@ def _close_invocation_span(self, invocation_id: str) -> None:
839850
open_span = self._invocation_span.pop(invocation_id, None)
840851
if open_span is None:
841852
return
842-
# Status defaults to OK for completed invocations; if the
843-
# engine surfaced an error, the failing node's span
844-
# already carries it and OTel propagates ERROR up the
845-
# parent chain on its own.
846-
open_span.span.set_status(Status(StatusCode.OK))
853+
# Don't unconditionally call ``set_status(OK)`` here. OTel
854+
# doesn't auto-propagate child span status to parents, so
855+
# the spec §4.2 / fixture 003 contract ("invocation span
856+
# ends ERROR when a child errored") is satisfied by
857+
# ``_handle_completed`` setting ERROR on this span when an
858+
# error event fires. Calling ``set_status(OK)`` here would
859+
# be a no-op when ERROR was already set (OTel SDK
860+
# status-precedence preserves ERROR), but it's clearer to
861+
# leave the status UNSET in the clean-completion path —
862+
# exporters map UNSET to OK by convention, and the explicit
863+
# ERROR-set in ``_handle_completed`` handles the failure
864+
# path.
847865
open_span.span.end()
848866

849867
def shutdown(self) -> None:

0 commit comments

Comments
 (0)