Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -422,3 +422,32 @@ columns). Do not silently rewrite either historical form. The SHACL
shapes graph (`docs/ontology/lineageweave-kg-shapes.ttl`) is the
closed-world data-validation boundary for DB-to-RDF projections and is
published beside the ontology.

## Bounded telemetry maintenance

When replacing an OpenTelemetry handler, preserve explicit OTLP opt-in and
attach it only to the product's bounded logger. Do not enable global automatic
instrumentation to silence a deprecation warning. Verify the root logger and
LogRecord factory are unchanged, retain the intended severity threshold, and
check warning absence after real provider setup and teardown. Backend diagnostic
tests require both the dev and backend extras in the isolated uv environment.

Register provider ownership immediately after allocation, before attaching
processors or handlers. A later optional-telemetry setup failure must still
leave the provider reachable by normal shutdown; test that failure path using
a real provider and verify shutdown rather than only catching the exception.

Optional telemetry failure boundaries must cover instrument acquisition as well
as recording. A failed meter/counter constructor must not replace the original
application failure or leak its exception message into bounded diagnostic logs.

Telemetry privacy tests must use allowed attribute names with disallowed values;
unlisted names are rejected earlier and cannot prove scalar-value filtering.
Inspect structured LogRecord fields as well as formatted text when checking
content exclusion. Preserve the full coverage denominator and record harness
failures separately from product assertions.

When testing degraded telemetry support, verify that the original application
exception survives and that exported events plus structured logs omit its value.
A coverage percentage alone does not prove either invariant; retain the same
statement/branch denominator when comparing improvements.
106 changes: 106 additions & 0 deletions docs/product-technical-gap-baseline.md
Original file line number Diff line number Diff line change
Expand Up @@ -932,3 +932,109 @@ The ONET rows stacked into base branches (#743/#745/#746/#740/#732) reached
`main` together through the #759 promotion; their per-base merge records are
historical evidence only. The job-architecture artifact ship originally via
#749 is now re-verified on `main` from the promotion.


## OpenTelemetry log-handler compatibility (2026-09-07)

Hosted Tests run 34082387676 reported a deprecated SDK LoggingHandler during
provider configuration. The existing provider test was strengthened to capture
DeprecationWarning, then assert that none occurred after shutdown; it failed
on the SDK handler. It also checks the WARNING threshold, dedicated logger
attachment, unchanged root handlers, and unchanged global LogRecord factory.

The adapter now imports LoggingHandler from the official
opentelemetry-instrumentation-logging package and retains manual construction
with its existing LoggerProvider. The lock pins 0.65b0 alongside SDK 1.44.0
and semantic conventions 0.65b0. Only the new instrumentation package, its base
instrumentation dependency, and wrapt are added; no existing locked versions
were changed. No LoggingInstrumentor, global record-factory patch, root handler,
or new exporter setting is enabled. ADR 0122 remains the governing opt-in and
bounded-content contract. The replacement's default omits optional code-location
attributes; the ADR-defined operation/session/error evidence remains unchanged.

Final local verification used the frozen dev and backend extras on Python 3.14.6:
30 observability and server-diagnostic tests passed without warnings. Module
statement/branch coverage is 92%, not 100%; uncovered paths remain a tracked
gap. The first combined run lacked backend extras and stopped during collection
on missing asyncpg; its 15% coverage report is not test acceptance evidence.
No warning suppression, real-record fixture, collector deployment, or protected
merge is claimed.

References:

OpenTelemetry Authors. (n.d.). *OpenTelemetry logging instrumentation*.
https://opentelemetry-python-contrib.readthedocs.io/en/latest/instrumentation/logging/logging.html

OpenTelemetry Authors. (2026). *LoggingHandler implementation (v0.65b0)*.
https://github.com/open-telemetry/opentelemetry-python-contrib/blob/v0.65b0/instrumentation/opentelemetry-instrumentation-logging/src/opentelemetry/instrumentation/logging/handler.py


### Partial telemetry initialization ownership (2026-09-07)

Handler initialization could fail after the log provider acquired a batch worker
but before the module saved the provider for shutdown. A regression with a real
LoggerProvider and a deliberately failing handler proved normal shutdown called
the provider zero times. Test cleanup explicitly closed the orphan afterward.

The provider is now registered with module ownership immediately after creation.
The same failure remains fail-open, while normal shutdown closes the partially
initialized provider exactly once and clears module state. This moves one
existing assignment and introduces no new lifecycle abstraction.

All 31 observability/server-diagnostic tests passed in 1.97 s without warnings.
The same module coverage increased from 92% to 95% (197 statements, 68 branches);
100% remains unmet. This is local failure-path evidence, not live collector or
protected-merge acceptance.


### Metric initialization failure boundary (2026-09-07)

The counter add operation was guarded, but acquiring the counter was outside
the guard. A synthetic metrics-provider failure escaped record_server_failure
and replaced the application's original failure. The regression failed with
the injected RuntimeError before the repair.

Counter acquisition now belongs to the existing metric-recording exception
boundary. The original ValueError classification and provider-unavailable
outcome still reach bounded logs, while neither the injected metric exception
message nor the original request exception message appears. No fallback metric
value, alternate provider, or additional wrapper is introduced.

The complete three-file observability/server-diagnostic selection passed 32
tests in 34.74 s without warnings. Module statement/branch coverage increased
from 95% to 96%; the remaining paths are not claimed covered. This is local
failure-isolation evidence, not protected deployment or full-goal completion.

### Telemetry attribute privacy coverage follow-up (2026-09-07)

Hosted run 34087122183 actually executed 32 tests successfully and reported 96%
statement/branch coverage for observability; earlier harness failures were not
coverage measurements. The container-value test had used only unlisted keys,
which bypassed the allowed-key scalar filter it purported to test. It now checks
an allowed key with dictionary, list, tuple, set, null, and unsupported-object
values. A separate unavailable-metric test verifies unknown operation content and
exception payload stay out of structured log fields.

The corrected focused suite passes 38 tests in 2.43 s and reports 97% (197
statements, 68 branches, 2 missed statements and 5 partial branches). No exclusion
or denominator change was made. Initial new-test failures were harness mistakes
(keyword-only outcome called positionally, then checking formatted text instead
of structured LogRecord attributes); neither is claimed as a product bug.
The 100% target, current-head hosted tests, independent review, and deployment
remain incomplete; keep the PR Draft.

### Telemetry measured coverage completion (2026-09-07; local module scope)

The remaining telemetry regressions exercise absent/incomplete span context,
unavailable propagation without header mutation, rejected outcome classification
before metric creation, and partial status support. The latter uses the existing
in-memory SDK exporter and verifies original-exception identity, retained bounded
exception type, unset status, and absence of exception content in both events and
structured logs. No production guard or coverage exclusion was removed.

The focused suite passed 44 tests in 34.20 s. `coverage report --fail-under=100`
passed with 197 statements, 68 branches, zero missed statements and zero partial
branches: **100% for lineageweave/observability.py**, on the same denominator as
96% and 97% observations above. This supersedes the local module coverage gap;
it does not establish repository-wide 100% coverage, current-head hosted tests,
independent review, collector deployment, or real-source runtime acceptance.
15 changes: 8 additions & 7 deletions lineageweave/observability.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,13 +205,15 @@ def configure_telemetry(service_name: str = "lineageweave") -> None:
from opentelemetry.exporter.otlp.proto.http._log_exporter import (
OTLPLogExporter,
)
from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler
from opentelemetry.instrumentation.logging.handler import LoggingHandler
from opentelemetry.sdk._logs import LoggerProvider
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
except ImportError: # pragma: no cover - guarded by the runtime extra
_LOGGER.warning("OpenTelemetry log SDK/exporter is unavailable")
return
try:
log_provider = LoggerProvider(resource=resource)
_LOG_PROVIDER = log_provider
log_provider.add_log_record_processor(
BatchLogRecordProcessor(
OTLPLogExporter(endpoint=_otlp_log_endpoint(endpoint))
Expand All @@ -220,7 +222,6 @@ def configure_telemetry(service_name: str = "lineageweave") -> None:
set_logger_provider(log_provider)
handler = LoggingHandler(level=logging.WARNING, logger_provider=log_provider)
_LOGGER.addHandler(handler)
_LOG_PROVIDER = log_provider
_LOG_HANDLER = handler
except Exception: # noqa: BLE001 - export must stay fail-open
_LOGGER.warning("OpenTelemetry log exporter is unavailable")
Expand Down Expand Up @@ -316,18 +317,18 @@ def record_server_failure(
bounded_operation = "unknown"
error_type = type(exc).__name__[:128]
session_id = current_session_id() or ""
counter = _failure_counter()
if counter is not None:
try:
try:
counter = _failure_counter()
if counter is not None:
counter.add(
1,
{
"lineageweave.operation_code": bounded_operation,
"lineageweave.failure_outcome": outcome,
},
)
except Exception: # noqa: BLE001 # telemetry failure must not mask API failure
_LOGGER.warning("telemetry.metric_recording_failed")
except Exception: # noqa: BLE001 # telemetry failure must not mask API failure
_LOGGER.warning("telemetry.metric_recording_failed")

stack_trace = (
_stack_trace_without_exception(exc) if outcome == "internal_error" else ""
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ dependencies = [
"opentelemetry-api>=1.30.0",
"opentelemetry-sdk>=1.30.0",
"opentelemetry-exporter-otlp-proto-http>=1.30.0",
"opentelemetry-instrumentation-logging>=0.65b0",
]

[build-system]
Expand Down
19 changes: 19 additions & 0 deletions tests/test_observability.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,25 @@ def attach_inmemory_tracer(monkeypatch: pytest.MonkeyPatch) -> InMemorySpanExpor
return exporter


@pytest.mark.parametrize("missing_symbol", ["Status", "StatusCode"])
def test_missing_status_support_preserves_failure_and_safe_evidence(monkeypatch, caplog, missing_symbol):
"""Partial status support must neither replace application errors nor leak content."""
exporter = attach_inmemory_tracer(monkeypatch)
Comment thread
seonghobae marked this conversation as resolved.
monkeypatch.setattr(observability, missing_symbol, None)
original = ValueError("synthetic private payload")
with caplog.at_level(logging.WARNING), pytest.raises(ValueError) as raised:
with traced("lineageweave.test.partial_status"):
record_server_failure("post_chat", original, outcome="provider_unavailable")
raise original
assert raised.value is original
Comment thread
seonghobae marked this conversation as resolved.
spans = exporter.get_finished_spans()
assert len(spans) == 1
assert spans[0].status.status_code == StatusCode.UNSET
assert spans[0].events[0].attributes["exception.type"] == "ValueError"
assert "synthetic private payload" not in str(spans[0].events)
assert "synthetic private payload" not in str([vars(record) for record in caplog.records])


def test_post_json_sends_post_session_header(monkeypatch):
"""One post session reaches the orchestrator as a transport header."""
captured = {}
Expand Down
Loading
Loading