From 6020dd2a491a4d8c260f1e9dfccc0a5f6c0445e2 Mon Sep 17 00:00:00 2001 From: Codex Date: Mon, 7 Sep 2026 13:53:48 +0900 Subject: [PATCH 1/5] fix(telemetry): use supported OpenTelemetry logging handler --- AGENTS.md | 9 +++ docs/product-technical-gap-baseline.md | 35 ++++++++ lineageweave/observability.py | 3 +- pyproject.toml | 1 + tests/test_observability_telemetry.py | 13 ++- uv.lock | 106 +++++++++++++++++++++++++ 6 files changed, 165 insertions(+), 2 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 3af4169bc..a51f50eca 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -422,3 +422,12 @@ 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. diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index b5d31877b..6ea68bd7b 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -932,3 +932,38 @@ 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 diff --git a/lineageweave/observability.py b/lineageweave/observability.py index 9262a4135..cccb493d0 100644 --- a/lineageweave/observability.py +++ b/lineageweave/observability.py @@ -205,7 +205,8 @@ 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") diff --git a/pyproject.toml b/pyproject.toml index 7744aef87..1eb558a74 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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] diff --git a/tests/test_observability_telemetry.py b/tests/test_observability_telemetry.py index b99622fca..8561f71af 100644 --- a/tests/test_observability_telemetry.py +++ b/tests/test_observability_telemetry.py @@ -107,6 +107,10 @@ def test_configure_telemetry_success_installs_providers( import opentelemetry.metrics as otel_metrics import opentelemetry.trace as otel_trace + import warnings + + root_handlers = list(logging.getLogger().handlers) + record_factory = logging.getLogRecordFactory() trace_providers: list[object] = [] metric_providers: list[object] = [] log_providers: list[object] = [] @@ -118,7 +122,9 @@ def test_configure_telemetry_success_installs_providers( monkeypatch.setattr(otel_metrics, "set_meter_provider", metric_providers.append) monkeypatch.setattr(otel_logs, "set_logger_provider", log_providers.append) - observability.configure_telemetry("services/synthetic") + with warnings.catch_warnings(record=True) as notices: + warnings.simplefilter("always", DeprecationWarning) + observability.configure_telemetry("services/synthetic") assert observability._CONFIGURED is True assert observability._TRACE_PROVIDER is not None @@ -126,6 +132,10 @@ def test_configure_telemetry_success_installs_providers( assert metric_providers == [observability._METER_PROVIDER] assert log_providers == [observability._LOG_PROVIDER] assert isinstance(observability._LOG_HANDLER, logging.Handler) + assert observability._LOG_HANDLER.level == logging.WARNING + assert observability._LOG_HANDLER in observability._LOGGER.handlers + assert logging.getLogger().handlers == root_handlers + assert logging.getLogRecordFactory() is record_factory # Restore the module to a clean, unconfigured state for the rest of the suite. observability.shutdown_telemetry() @@ -134,6 +144,7 @@ def test_configure_telemetry_success_installs_providers( monkeypatch.setattr(observability, "_METER_PROVIDER", None) monkeypatch.setattr(observability, "_LOG_PROVIDER", None) monkeypatch.setattr(observability, "_LOG_HANDLER", None) + assert not [notice for notice in notices if issubclass(notice.category, DeprecationWarning)] def test_configure_telemetry_returns_when_sdk_disabled( diff --git a/uv.lock b/uv.lock index f94e79cf4..6239a78bd 100644 --- a/uv.lock +++ b/uv.lock @@ -692,6 +692,7 @@ dependencies = [ { name = "cryptography" }, { name = "opentelemetry-api" }, { name = "opentelemetry-exporter-otlp-proto-http" }, + { name = "opentelemetry-instrumentation-logging" }, { name = "opentelemetry-sdk" }, { name = "pillow" }, { name = "rankweave" }, @@ -732,6 +733,7 @@ requires-dist = [ { name = "mcp", marker = "extra == 'backend'", specifier = "==2.0.0" }, { name = "opentelemetry-api", specifier = ">=1.30.0" }, { name = "opentelemetry-exporter-otlp-proto-http", specifier = ">=1.30.0" }, + { name = "opentelemetry-instrumentation-logging", specifier = ">=0.65b0" }, { name = "opentelemetry-sdk", specifier = ">=1.30.0" }, { name = "pillow", specifier = ">=12.3.0" }, { name = "psycopg2-binary", marker = "extra == 'dev'", specifier = ">=2.9.12" }, @@ -900,6 +902,35 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cd/d0/fdeb1a98d8d3a6205f5f297c51b4a9bfe65126ab60339669bbe3dd54c2e2/opentelemetry_exporter_otlp_proto_http-1.44.0-py3-none-any.whl", hash = "sha256:838592fce774c1c8bb7b9a0a7facbfa82e17be5a8a4e94cef10cb84ae026bae3", size = 21850, upload-time = "2026-07-16T15:25:20.006Z" }, ] +[[package]] +name = "opentelemetry-instrumentation" +version = "0.65b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "packaging" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/13/91/3c58961cb0360cd60509064734f0be4275383c8681d73c580a40ca83ddce/opentelemetry_instrumentation-0.65b0.tar.gz", hash = "sha256:071d9d9eced9bd6460444ec3b0c77229870ed05a881c22c84fdede58e4eed09b", size = 42689, upload-time = "2026-07-16T15:25:50.275Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/7b/85eab1215f72adf0e68d3dc4a679b9bff993fa679ff34cd8dd378e2659fd/opentelemetry_instrumentation-0.65b0-py3-none-any.whl", hash = "sha256:ea967a72b9939b5fcfdad572753b4306c59dcb99e3f382d95dae04286805e137", size = 36717, upload-time = "2026-07-16T15:24:51.424Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-logging" +version = "0.65b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-semantic-conventions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/0a/b70a9cddbc7b314a783e62739dbb1184f8538c1f85e8ded6d340142b9b54/opentelemetry_instrumentation_logging-0.65b0.tar.gz", hash = "sha256:c0a50cade5d54db6c6af12e2c69227ecd26f2b3b779e99ff850561d3d8dd77e3", size = 19783, upload-time = "2026-07-16T15:26:09.853Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/8e/7577914681d77b180f8d6dcbac435be8e4ca6add6315da2d01ac4289eaa3/opentelemetry_instrumentation_logging-0.65b0-py3-none-any.whl", hash = "sha256:68365b31755c844f1e85f07dcd217839ff92f2d278a214bdf02d4dc806f9d915", size = 15727, upload-time = "2026-07-16T15:25:18.774Z" }, +] + [[package]] name = "opentelemetry-proto" version = "1.44.0" @@ -1821,3 +1852,78 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5b/24/a585e7573e128070605d003b5544729bcd58d9756c7e99d550818ca4b916/websockets-17.0.1-cp314-cp314t-win_arm64.whl", hash = "sha256:dbfae8e75b342e31fc6fd1a8bbb393b7cbb91d6cfd581650300a94381e7b7e2b", size = 213009, upload-time = "2026-07-31T11:31:16.776Z" }, { url = "https://files.pythonhosted.org/packages/09/ce/3929538b2b9918f5eee623fbf3346893973191f6df93f19bbda097bd7bb7/websockets-17.0.1-py3-none-any.whl", hash = "sha256:c6be9cba65c65cc76dfa3d4619e359ff02a4476c74e179b215236c11a0b32345", size = 206718, upload-time = "2026-07-31T11:31:26.037Z" }, ] + +[[package]] +name = "wrapt" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ba/8dc25478ed234dacc7d83c671634f347d0bdfb65bf0502f41879cf2f15a9/wrapt-2.4.0.tar.gz", hash = "sha256:7082fc1f94b020ac275870c4af71b09cff22876fe6e9c4c0ad01ea21d217b288", size = 161179, upload-time = "2026-08-30T04:41:51.424Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f0/22/581a0b44349d5babe526c958f365b8126e0fbd8fc2810e80446c47358050/wrapt-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ef4e2d6e399ce6eecc80179a6b9ef6544f121288f95fc132bc36c9d9503903af", size = 96374, upload-time = "2026-08-30T04:39:42.335Z" }, + { url = "https://files.pythonhosted.org/packages/5d/90/095984648cec62a786bb27c0b50f6cfa5856d1e073ba1006fe148d190084/wrapt-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b9b32d5e4f0a179cef5075cc79b79d6d3482c44c434c12969e48c6719e06d95", size = 96178, upload-time = "2026-08-30T04:39:43.789Z" }, + { url = "https://files.pythonhosted.org/packages/f8/fd/b20e3cb3cab35131b515edf18e8cd777dff680fc76fc00919481f4e536af/wrapt-2.4.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d7dbbdbfdacb85c2d962fa52db791c77943fd777d600d74c95af2d53b32f5a94", size = 227806, upload-time = "2026-08-30T04:39:45.264Z" }, + { url = "https://files.pythonhosted.org/packages/08/75/c8dfba5e0caf17cd0718a0cbbe76cb85e637a2d65183fb728232419f6fca/wrapt-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39cd68df4dff79f5336f9c745c06259d204bcb42d504040c9c91eac9e2abb39c", size = 229004, upload-time = "2026-08-30T04:39:47.068Z" }, + { url = "https://files.pythonhosted.org/packages/42/05/d4853fbd33e5860b10d5aec690f563547a92a82e61fb8bb2d4ece1ce3570/wrapt-2.4.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2a9f1a2f75bb95257cc5744e255e10a5a86e923f328b40ad3dbf9d8d03430013", size = 208934, upload-time = "2026-08-30T04:39:48.73Z" }, + { url = "https://files.pythonhosted.org/packages/a3/66/23d0e8de9b411fd198af5121627587563657370c8d509fbe5ea8adb3df79/wrapt-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8763ad01e3725b7751a4575f38bbcc19c0aa0822fec91c5c5bd21ce3ce7e1d2b", size = 225709, upload-time = "2026-08-30T04:39:50.287Z" }, + { url = "https://files.pythonhosted.org/packages/01/37/3b357bc90530d510ae59ae7ac48265c482ae899e47637ca4436645688b40/wrapt-2.4.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9125c6dbe8b88c00dd8ef4fc1e55757e8eb4720b6b2b2cc610a45bd32bd28c57", size = 207090, upload-time = "2026-08-30T04:39:51.78Z" }, + { url = "https://files.pythonhosted.org/packages/6d/0c/d8a5c6dbcc2d221308223bcea4130c6332454a855cb4dbd5dcb2360b13b2/wrapt-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:28f5de1526831b8f173889a436e289fe181ede8c66c9feb669d1aca8fd602eaf", size = 216269, upload-time = "2026-08-30T04:39:53.641Z" }, + { url = "https://files.pythonhosted.org/packages/92/93/cc9fc8fef1d3d25edaa1c2dc2337b556dc1d0613ddc1c4a6fe9ee08ad705/wrapt-2.4.0-cp312-cp312-win32.whl", hash = "sha256:a9ca1cdb3f7facb4990c7739ea5afbaceeb6728d066feedde03a4cfe83b29b03", size = 91187, upload-time = "2026-08-30T04:39:55.38Z" }, + { url = "https://files.pythonhosted.org/packages/ad/ec/a7b10705172bdb669b9687a8ff68bbe5f566437d2a49ad6d976af48b6d10/wrapt-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:8b464316489fb2fca0669ea0f8f07290054a0f26fc72982d3e4cf95469628ba9", size = 96423, upload-time = "2026-08-30T04:39:56.81Z" }, + { url = "https://files.pythonhosted.org/packages/83/7a/e838ac6463a1a1a1817b2f184ee2aa20c54692b80368c5063403c8d2461c/wrapt-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:db1285071ea09a7767fac608e7b5c7b03c09833b06186875a359905fbc659d29", size = 93003, upload-time = "2026-08-30T04:39:58.237Z" }, + { url = "https://files.pythonhosted.org/packages/19/86/f9de4e11582ff96ad2199eeeceaa17faa27bbdc599243f520070c4f3de07/wrapt-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5c5c4c728cd22a36e4b8bb5df4a7d3bccaa865d27725b36eeb3b6f18fb2e1bc2", size = 96041, upload-time = "2026-08-30T04:39:59.575Z" }, + { url = "https://files.pythonhosted.org/packages/c3/ab/1dbf50802bea3b46192fd0dc39bb0eb2e77a064c813b2bbd88d2888ad49f/wrapt-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7de5b8d94417e55c02be50cc226e0ae1209bbc73813bf691dff3979c94438115", size = 96269, upload-time = "2026-08-30T04:40:01.182Z" }, + { url = "https://files.pythonhosted.org/packages/cb/a3/a3b5cde1cd06e04b6e95134eb3187a0a7da607a530e7795b221d4e4fa819/wrapt-2.4.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6436e2bda993a3eb69a1b317fc831c8ebcafb5704c390859ebd49f81218c4bbb", size = 225787, upload-time = "2026-08-30T04:40:02.715Z" }, + { url = "https://files.pythonhosted.org/packages/cb/f7/d100f6c348b7669f19119cf890dcd4764623e2233af065586d110e0cd99e/wrapt-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e084558fbd112d2e1e34b0f5c71e45a3405bdad51a17150368a959bcf6697964", size = 226649, upload-time = "2026-08-30T04:40:04.647Z" }, + { url = "https://files.pythonhosted.org/packages/52/c6/3af8df515d5d7e92306957536f3468c6bdfecbe3659f99dbf09a468c2c4c/wrapt-2.4.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e78c947e18fadfd690c9420c30a96d221feeb93fc8f1cc00509b370ac16c3114", size = 206760, upload-time = "2026-08-30T04:40:06.332Z" }, + { url = "https://files.pythonhosted.org/packages/b9/c5/40d355552bd3eb6c5186e26051c19b573d24d7896de42caa7937d6b5ca9f/wrapt-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:08d8378c4514ac8dcc0ace76044cf87a873e6a52b5e6109834c8fb9037f4441b", size = 223467, upload-time = "2026-08-30T04:40:07.829Z" }, + { url = "https://files.pythonhosted.org/packages/40/ab/d198eebdb39f0d7e182e771e590a36673489cd58cebdad8aa273dcf28e04/wrapt-2.4.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:93180c2199784dd6a1075b33f9ed636bd0966821edbece6b3d5379b1c4f0bb7d", size = 205358, upload-time = "2026-08-30T04:40:09.344Z" }, + { url = "https://files.pythonhosted.org/packages/1c/0e/974a60672ad507d39a3d8a1c6351ef37fe65b07240d000ceba5d2b83e9e9/wrapt-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3d5e5eb76fb87e62752af751d2dcd9d1cd986b12037d2e1363d109ba716029e8", size = 214654, upload-time = "2026-08-30T04:40:10.923Z" }, + { url = "https://files.pythonhosted.org/packages/cc/5a/8b2db70206db0a4246758e0472ce344cb9636217113ef70640fc8d2ce874/wrapt-2.4.0-cp313-cp313-win32.whl", hash = "sha256:49bb5a572469e0e18163a8ec2aa972135a0929899ecbe627665f274506e1b5b4", size = 91171, upload-time = "2026-08-30T04:40:12.895Z" }, + { url = "https://files.pythonhosted.org/packages/fe/1e/e782b511c680dbe7369c92e7d981484aacca0cda584da1f28a84cd9a8e1a/wrapt-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:b1737f46b1e4a81eb93500a7f2854319e1c7a86e8863fb050b7b4daadd5a4178", size = 96178, upload-time = "2026-08-30T04:40:14.336Z" }, + { url = "https://files.pythonhosted.org/packages/9f/62/095ba31123fa5dd482d6183c05200b061314aabbd5442c010aba4b03ff1c/wrapt-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:f1e9e088094f4895f84ab043e7d59401df137d663efbf1e80c82144882960830", size = 92949, upload-time = "2026-08-30T04:40:15.935Z" }, + { url = "https://files.pythonhosted.org/packages/1f/dd/1f269e4daf0c992f675e1ca2de6b1683b761c6d0aeb6c7b4b412486823ea/wrapt-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:788e473d1a6786d29d577b1e2bd95e214c09cdafde84907c522c31069c9acfac", size = 96386, upload-time = "2026-08-30T04:40:17.584Z" }, + { url = "https://files.pythonhosted.org/packages/be/42/7ecef06d33c0121c68d66a8a695efe67ebaa57218c1c61c585eca2a6117a/wrapt-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:947bd4b3438167b3638bf5477cb83a068a586ffb6d331ac427f39839c2b93b3c", size = 96532, upload-time = "2026-08-30T04:40:19.116Z" }, + { url = "https://files.pythonhosted.org/packages/c2/e3/8fdc9eba0e6cbbfe8303e1e807d734691309a27970b2ea458d099f1a46b0/wrapt-2.4.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3a69161cae7f0dca44c89c1d14146b4a0508a0c3cad98b3f2db1f4e9016c94ba", size = 228775, upload-time = "2026-08-30T04:40:20.604Z" }, + { url = "https://files.pythonhosted.org/packages/f4/77/4ac5882abfb29bf9821c5fa5cf9f30241a194e0f47faa2682b9b29765278/wrapt-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0536f5d85ff6a157ebe7e0fe08c5479943742cf1ce59569075a66159efcbc495", size = 229029, upload-time = "2026-08-30T04:40:22.186Z" }, + { url = "https://files.pythonhosted.org/packages/c9/c5/8a3608311a02faf3e5c072da38d06a7c623150fc258e29f18fe377d91703/wrapt-2.4.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5f041ed6a4d571010944bd6cfad9072db463e1851877b6d3227467a44af37456", size = 210436, upload-time = "2026-08-30T04:40:23.953Z" }, + { url = "https://files.pythonhosted.org/packages/de/90/e0cbc43f435fd39df25460e9f173e7b96f3dac5c7f66be41c7227166f021/wrapt-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f7fed45dbadf5d98a52bfff9624d3cca00affeb9543d493c9632b7a53cdd35c9", size = 226586, upload-time = "2026-08-30T04:40:25.507Z" }, + { url = "https://files.pythonhosted.org/packages/81/6c/7e5f2143228635ec139ef6df733dc477049f7d96a0c49deb23944a73ed6a/wrapt-2.4.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5cc2e7c7b6032e11a2b367a9baadaf0c5241feff2d8205260d87f1aa6dbdf84b", size = 208880, upload-time = "2026-08-30T04:40:27.128Z" }, + { url = "https://files.pythonhosted.org/packages/10/16/1de84402bb7a0916e10739bf6586e031244172b299e87c8cff2a04baf9ff/wrapt-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:72826910a1cf5a081234720fd43011304b899acfee219af49148155b4d795533", size = 216689, upload-time = "2026-08-30T04:40:28.844Z" }, + { url = "https://files.pythonhosted.org/packages/20/19/cd6bd5050381a541b44be97c4e0994eed60c5f439f4314f95eb5777d6c1a/wrapt-2.4.0-cp314-cp314-win32.whl", hash = "sha256:0eca69c9e93518240abe8801fb9b2726116a6e48172e4564c2651a2e14521747", size = 91581, upload-time = "2026-08-30T04:40:30.592Z" }, + { url = "https://files.pythonhosted.org/packages/a4/f8/b642f3184619adde676ad449030bcbeae6cc78ea07a92f0b5fddeec4c4e6/wrapt-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:63b94f401d7ae3a9a3027472fd3a3ff38afd2ed293b2f0b3b84a6d133a9f99a3", size = 96510, upload-time = "2026-08-30T04:40:32.1Z" }, + { url = "https://files.pythonhosted.org/packages/4d/3b/3415a18b91221261eeac85bf8ee23dfb0e2a39d76b9703a797efca177439/wrapt-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:6b3e082d43f592fcd381aee46354a11ce887a813ce5bbcedd9766fd681723c09", size = 93648, upload-time = "2026-08-30T04:40:33.563Z" }, + { url = "https://files.pythonhosted.org/packages/ac/90/80cf6a09e9599a11249775928df9bb790b82471e4312b847a861ffb2c2ed/wrapt-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:09064c7be688c38c3ff125ce86bc26b69b5d78dd56062c3ddd9c814b2a25f1e1", size = 99615, upload-time = "2026-08-30T04:40:35.134Z" }, + { url = "https://files.pythonhosted.org/packages/b2/da/c1d3245abb911a42584f8f7e9781995bdc41345c7affba75cf7e376c85ac/wrapt-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:4f8ddff4bbb75916be36da5169b8b9d475b59a1bd24acdb7551bb2c71be9aaac", size = 100031, upload-time = "2026-08-30T04:40:36.641Z" }, + { url = "https://files.pythonhosted.org/packages/84/46/8ec4941d0abbb010df7caf0a34840ca0128177389843b0f5ef2f9ee48ac5/wrapt-2.4.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e9f8017443595870aa31f46125553a5c55ce95a26a267b96261baee6ba566d83", size = 269389, upload-time = "2026-08-30T04:40:38.212Z" }, + { url = "https://files.pythonhosted.org/packages/14/b5/a0ae1b431cc1f49a545d32b8b678a5788c50583ecf0ecb85dc0c7f95b4f6/wrapt-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:328eb2d978ca3a6ae25f8d8fe560bf8f4bc9778b5932e7b142664eef05b92e8f", size = 281081, upload-time = "2026-08-30T04:40:40.045Z" }, + { url = "https://files.pythonhosted.org/packages/c7/24/dfaf53dd3bdb0703524a9367b48e2a64ea86433fcc854b5f14be6a8e0e39/wrapt-2.4.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7a057d376d994da6bd1bbf955ecfda699aa7353826f98847f5605e1801abdfd4", size = 249637, upload-time = "2026-08-30T04:40:41.657Z" }, + { url = "https://files.pythonhosted.org/packages/3e/27/bdd82044d7503c2bfa78afcc89881f82a1b82b5d2013aabab853d339ce2a/wrapt-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3367a5212212c9393e0d3ca6ae029b3a8fa40c5896e4a985d43fe8a4b8322f0d", size = 275322, upload-time = "2026-08-30T04:40:43.408Z" }, + { url = "https://files.pythonhosted.org/packages/c4/82/04f4228eb3fb348d660dd1ea7225e53665b1809df2273ff4861d4d33b741/wrapt-2.4.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c4fca1e63af6675af3df7cdfcd5a0c878b5e655c7e48611ced9dc8d62183a11d", size = 247292, upload-time = "2026-08-30T04:40:45.457Z" }, + { url = "https://files.pythonhosted.org/packages/a2/20/67b2968fa9200458446c51b36a435adb6906083428b70fafb4caf92d4dc2/wrapt-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:694005fdc3002ade0f21641408c588028abde03c85961f3ba7727d8bead3ed6b", size = 264586, upload-time = "2026-08-30T04:40:47.079Z" }, + { url = "https://files.pythonhosted.org/packages/d7/fd/0db9ba03e08a7663f52455e95520c723f567bc037bffc6699950fcc456c4/wrapt-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:332d9bad7e9b718974bb2a576504c4956f45b4a0fcd7b3bb7827279167550464", size = 93752, upload-time = "2026-08-30T04:40:48.81Z" }, + { url = "https://files.pythonhosted.org/packages/3f/87/ced171220935c696b157207385fa6be5675558a74655479f071d95a00f1d/wrapt-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6d57264c9dfcf37d2bf0b0fbec68d0f6184fc5617267619ada04d03e8b0231f3", size = 99890, upload-time = "2026-08-30T04:40:50.407Z" }, + { url = "https://files.pythonhosted.org/packages/a3/af/4a10c9a6d3b7ae41f830978c28d33a59ceb29537bd6875d2abfe78db4b41/wrapt-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:f43af38a642c3d6062e9740d8f5cc0feb5dbe0da516702df892147393b8cb14d", size = 96033, upload-time = "2026-08-30T04:40:51.933Z" }, + { url = "https://files.pythonhosted.org/packages/a0/df/3a0b6225ab88bd47090df70391c059a3308057638f8fc0ae32e8ac9d1886/wrapt-2.4.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:430fde1a116df3ceb5c29035de1da6609b70e680d9b8ce3ee624422f3fe0978c", size = 96389, upload-time = "2026-08-30T04:40:53.555Z" }, + { url = "https://files.pythonhosted.org/packages/ea/6f/803b0d0e14de11781f0e938e6f7d6e29e79652139fe70d7513460357ac78/wrapt-2.4.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:7d28f8f35a02d49f75f57fa4e755db4ba33f65841c0de64cd65b253916f5bf06", size = 96557, upload-time = "2026-08-30T04:40:55.033Z" }, + { url = "https://files.pythonhosted.org/packages/aa/e8/46571e1218d0494604a7aadc4c898c738c4b179052327ee1e57e278cebd6/wrapt-2.4.0-cp315-cp315-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:efd9a4be6785295e471f71efdf5682bd11d5b822b9665e6e1b4844917cf2f7ac", size = 229230, upload-time = "2026-08-30T04:40:56.703Z" }, + { url = "https://files.pythonhosted.org/packages/78/2e/0cab15fcaec56096a5734feace3620bc01edc885653be04bd756f84a6784/wrapt-2.4.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:75529a2fb569a671cf162f762c1b576f569f571b55ec7f3481258ca842ba507f", size = 229444, upload-time = "2026-08-30T04:40:58.51Z" }, + { url = "https://files.pythonhosted.org/packages/e7/9e/a92c049371a2675f98a0381ab2951f984866d1ba4de0e0771d6a31fdaa2b/wrapt-2.4.0-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:66e7512c0d324cc37bba1def2be1fc365cbb685d3aa393a8f6f4d2d00202881d", size = 212482, upload-time = "2026-08-30T04:41:00.224Z" }, + { url = "https://files.pythonhosted.org/packages/ee/3b/8b5b57d0ff24edcd3421dbaeb4e94c89be3616824e47708f4e13f25ae3d7/wrapt-2.4.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:5f3bdfc35c83b562fcaebc0f24593045e5ed9f3b633adafd35222718a0ec38fa", size = 227017, upload-time = "2026-08-30T04:41:01.918Z" }, + { url = "https://files.pythonhosted.org/packages/0e/20/124b40bfd9585848db5a5aa6741d0c8dbf378dd995c6c2d95f090d9cf540/wrapt-2.4.0-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:d5f45bead708e2c0014be5e98531ce7202916b098a208c7be83c6ceb0a2559fa", size = 210498, upload-time = "2026-08-30T04:41:03.617Z" }, + { url = "https://files.pythonhosted.org/packages/4b/bf/89db9d5a80a9f2af52b24bdfdb5392be80bc0f0fd39fc39d1aab72afd0bd/wrapt-2.4.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:d294576fddac636589e4deccfe782e8f429da10f167c1985c4d51071de3672b7", size = 217046, upload-time = "2026-08-30T04:41:05.473Z" }, + { url = "https://files.pythonhosted.org/packages/3b/0b/021c9d6ce64c639894bffdaa7a895ddd4187abfefb2873ce55e536cd9d56/wrapt-2.4.0-cp315-cp315-win32.whl", hash = "sha256:0191d717dfbb8e519e7bfd4775e5b9bd57e359b3a09ab5db1ea47f6025b4d845", size = 91591, upload-time = "2026-08-30T04:41:07.086Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d3/6ebd944041cea0ac4a108a4739510ed2dc891a3f3216e4f7bf0650f5b5a6/wrapt-2.4.0-cp315-cp315-win_amd64.whl", hash = "sha256:e8df31a126a0a247c1aa379e30873839de03912dea09ca360c680f3625d815df", size = 96517, upload-time = "2026-08-30T04:41:08.671Z" }, + { url = "https://files.pythonhosted.org/packages/96/84/7c5e52e450f80ba76fd0282dccf7c79cd004ebd8ccabd0903064d3d2c56e/wrapt-2.4.0-cp315-cp315-win_arm64.whl", hash = "sha256:e9e7e94472f0e3f1447caf27e1939eb384d0e87972a35a05f5c2e0968e9c01af", size = 93652, upload-time = "2026-08-30T04:41:10.258Z" }, + { url = "https://files.pythonhosted.org/packages/35/89/f08ff45d7646de29750932805cc3b1e86b6ac3128015b293ed45fa8efe86/wrapt-2.4.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:8828369b7d3e93c547cc8ad931b5a57b4e8d174035c82762fb1091e7d05ac9f5", size = 99610, upload-time = "2026-08-30T04:41:11.933Z" }, + { url = "https://files.pythonhosted.org/packages/4b/c2/f9a3c40901a36c6bb7ecaff8e1e54af78fa7fa0b95a0e54d13d3a24c8a0a/wrapt-2.4.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:413e757dce7a43fcda8bb8441994b1127492ffac6a5803af777d44516df8c6e2", size = 100064, upload-time = "2026-08-30T04:41:13.492Z" }, + { url = "https://files.pythonhosted.org/packages/0b/e1/e2437f17f2a1ec292056e2fcafe1248269ebc39502f2ffe79424bf86f8a6/wrapt-2.4.0-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:75944792cf6b99262d649d55710bf5901f7013fbb212c7a1d736b97a20517607", size = 269421, upload-time = "2026-08-30T04:41:15.238Z" }, + { url = "https://files.pythonhosted.org/packages/c6/d0/c98d6548dc4c7d12ab9baa192234ca1a57e141afd283252b448faddbd9ef/wrapt-2.4.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:648d1d4f94e8a0a1656675c755f40d2f0ee5fe92c449ab45326f4ecc2738cbe8", size = 281452, upload-time = "2026-08-30T04:41:16.939Z" }, + { url = "https://files.pythonhosted.org/packages/a3/57/673168e00aa03725148ce621ed201b75df4e787a57acd48fecefd2725600/wrapt-2.4.0-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a112a1bfdd2621e4344cb0a32dbaab80636b32dac1b055d03fbb2a67d806d1db", size = 250358, upload-time = "2026-08-30T04:41:18.716Z" }, + { url = "https://files.pythonhosted.org/packages/78/0b/f2e576de5bf53ef5b578470104ea93f33e273a704c825131bc1719fffc42/wrapt-2.4.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:0972cd025f4c86fa2d8abd953d9f875779935343af58b4ce019ff89573fc65bd", size = 275654, upload-time = "2026-08-30T04:41:20.418Z" }, + { url = "https://files.pythonhosted.org/packages/33/7f/9347b2e236346b1ba4cb28b82b205b8a377bb2da9417cb81bbe3d25816d7/wrapt-2.4.0-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:c246aaed719dcdb62eeb7b8d9306a6237777226ef3baad35919c4ae134c91ce7", size = 248662, upload-time = "2026-08-30T04:41:22.371Z" }, + { url = "https://files.pythonhosted.org/packages/a5/36/3b84d9e1ac8393bf2c94272760a2d361dc394ac30301e6d6dbd6583ade2d/wrapt-2.4.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:1656de3835f760781c9b974bce07d8c04edb9c9ad7ad67264aee69cd68a1db09", size = 264813, upload-time = "2026-08-30T04:41:24.116Z" }, + { url = "https://files.pythonhosted.org/packages/6e/a2/de7b1de1702667b4a048318e301e26887268c17b07c8b9797cea06b10aee/wrapt-2.4.0-cp315-cp315t-win32.whl", hash = "sha256:d8e6e1e5dc684dfce7c33fc8b67a08ba2af94f3a45cfc70d5c1d6a839d2caf97", size = 93753, upload-time = "2026-08-30T04:41:25.793Z" }, + { url = "https://files.pythonhosted.org/packages/09/50/4e7ef58c4eb058861ceddc0d1f94a6ed87f62e1cb27783c60b2897ef7e58/wrapt-2.4.0-cp315-cp315t-win_amd64.whl", hash = "sha256:85ed3c67fd39e8d9a36c224758cb6f2f4eb277d07ea677930caa0008c18ec002", size = 99888, upload-time = "2026-08-30T04:41:27.305Z" }, + { url = "https://files.pythonhosted.org/packages/68/64/d15740c763dd0ddea2338ad42e3bd4a84f8702e16083e7ff61674c504a13/wrapt-2.4.0-cp315-cp315t-win_arm64.whl", hash = "sha256:36b56a4fba13b34ed8ff307557325fff215de0a58b5dbaef2c50e4d8aa39dbd1", size = 96039, upload-time = "2026-08-30T04:41:29.062Z" }, + { url = "https://files.pythonhosted.org/packages/79/c8/fafe0002f572ced999c792cfe8b05d39269c63d8193d15d25bd828bcad7a/wrapt-2.4.0-py3-none-any.whl", hash = "sha256:18aabd9301d06026f5900538051773d6f87f65ae02cdc60de482df978513dc0a", size = 73713, upload-time = "2026-08-30T04:41:49.805Z" }, +] From 84a9ffc95a0e48e22637e826bb8b7cc84f136e5a Mon Sep 17 00:00:00 2001 From: Codex Date: Mon, 7 Sep 2026 13:57:23 +0900 Subject: [PATCH 2/5] fix(telemetry): retain partially initialized provider for shutdown --- AGENTS.md | 5 ++++ docs/product-technical-gap-baseline.md | 18 +++++++++++++ lineageweave/observability.py | 2 +- tests/test_observability_telemetry.py | 35 +++++++++++++++++++++++++- 4 files changed, 58 insertions(+), 2 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index a51f50eca..f2bffa518 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -431,3 +431,8 @@ 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. diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 6ea68bd7b..c7d624f4b 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -967,3 +967,21 @@ https://opentelemetry-python-contrib.readthedocs.io/en/latest/instrumentation/lo 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. diff --git a/lineageweave/observability.py b/lineageweave/observability.py index cccb493d0..bcbeaa91a 100644 --- a/lineageweave/observability.py +++ b/lineageweave/observability.py @@ -213,6 +213,7 @@ def configure_telemetry(service_name: str = "lineageweave") -> None: return try: log_provider = LoggerProvider(resource=resource) + _LOG_PROVIDER = log_provider log_provider.add_log_record_processor( BatchLogRecordProcessor( OTLPLogExporter(endpoint=_otlp_log_endpoint(endpoint)) @@ -221,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") diff --git a/tests/test_observability_telemetry.py b/tests/test_observability_telemetry.py index 8561f71af..846b9be56 100644 --- a/tests/test_observability_telemetry.py +++ b/tests/test_observability_telemetry.py @@ -191,4 +191,37 @@ def test_shutdown_telemetry_removes_handler_and_nulls_providers( assert observability._TRACE_PROVIDER is None assert observability._METER_PROVIDER is None assert observability._LOG_PROVIDER is None - assert fake_handler not in logging.getLogger().handlers \ No newline at end of file + assert fake_handler not in logging.getLogger().handlers + +def test_failed_handler_keeps_provider_owned_for_shutdown(monkeypatch: pytest.MonkeyPatch) -> None: + """A handler failure must not orphan the log provider's batch worker.""" + from unittest.mock import Mock + + import opentelemetry._logs as otel_logs + import opentelemetry.instrumentation.logging.handler as handler_module + import opentelemetry.sdk._logs as sdk_logs + import opentelemetry.trace as otel_trace + + provider = sdk_logs.LoggerProvider() + shutdown = Mock(wraps=provider.shutdown) + monkeypatch.setattr(provider, "shutdown", shutdown) + monkeypatch.setattr(sdk_logs, "LoggerProvider", lambda **kwargs: provider) + monkeypatch.setattr(handler_module, "LoggingHandler", Mock(side_effect=RuntimeError("synthetic handler failure"))) + monkeypatch.setattr(otel_logs, "set_logger_provider", lambda provider: None) + monkeypatch.setattr(otel_trace, "set_tracer_provider", lambda provider: None) + monkeypatch.setattr(observability, "metrics", None) + monkeypatch.setattr(observability, "_CONFIGURED", False) + monkeypatch.setattr(observability, "_LOG_PROVIDER", None) + monkeypatch.setattr(observability, "_LOG_HANDLER", None) + monkeypatch.delenv("OTEL_SDK_DISABLED", raising=False) + monkeypatch.setenv("OTEL_EXPORTER_OTLP_ENDPOINT", "http://127.0.0.1:9") + try: + observability.configure_telemetry() + observability.shutdown_telemetry() + shutdown.assert_called_once_with() + assert observability._LOG_PROVIDER is None + assert observability._LOG_HANDLER is None + finally: + observability.shutdown_telemetry() + if not shutdown.called: + provider.shutdown() From cf09d6dd6f6e92f5dedb17c49bcccf81db2b61f9 Mon Sep 17 00:00:00 2001 From: Codex Date: Mon, 7 Sep 2026 14:02:33 +0900 Subject: [PATCH 3/5] fix(telemetry): isolate counter initialization failures --- AGENTS.md | 4 ++++ docs/product-technical-gap-baseline.md | 19 +++++++++++++++++++ lineageweave/observability.py | 10 +++++----- tests/test_observability_telemetry.py | 23 +++++++++++++++++++++++ 4 files changed, 51 insertions(+), 5 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index f2bffa518..39e4fe372 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -436,3 +436,7 @@ 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. diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index c7d624f4b..a803995f8 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -985,3 +985,22 @@ 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. diff --git a/lineageweave/observability.py b/lineageweave/observability.py index bcbeaa91a..c5d92aefc 100644 --- a/lineageweave/observability.py +++ b/lineageweave/observability.py @@ -317,9 +317,9 @@ 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, { @@ -327,8 +327,8 @@ def record_server_failure( "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 "" diff --git a/tests/test_observability_telemetry.py b/tests/test_observability_telemetry.py index 846b9be56..9535371a0 100644 --- a/tests/test_observability_telemetry.py +++ b/tests/test_observability_telemetry.py @@ -225,3 +225,26 @@ def test_failed_handler_keeps_provider_owned_for_shutdown(monkeypatch: pytest.Mo observability.shutdown_telemetry() if not shutdown.called: provider.shutdown() + + +def test_metric_initialization_failure_preserves_bounded_failure_log( + monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture, +) -> None: + """A broken metric provider cannot replace the original application failure.""" + from types import SimpleNamespace + from unittest.mock import Mock + + monkeypatch.setattr(observability, "_FAILURE_COUNTER", None) + monkeypatch.setattr(observability, "metrics", SimpleNamespace( + get_meter=Mock(side_effect=RuntimeError("synthetic private metric detail")), + )) + monkeypatch.setattr(observability, "trace", None) + with caplog.at_level(logging.WARNING, logger=observability.__name__): + observability.record_server_failure( + "global_ask", ValueError("synthetic private request detail"), + outcome="provider_unavailable", + ) + failure = next(record for record in caplog.records if record.msg == "lineageweave.server_failure") + assert failure.error_type == "ValueError" + assert failure.failure_outcome == "provider_unavailable" + assert "synthetic private" not in caplog.text From da5509598807a70b599839bd0d904f8c3a7b116f Mon Sep 17 00:00:00 2001 From: Codex Date: Mon, 7 Sep 2026 14:44:04 +0900 Subject: [PATCH 4/5] test(telemetry): exercise allowed-key privacy filtering --- AGENTS.md | 6 ++++++ docs/product-technical-gap-baseline.md | 18 ++++++++++++++++++ tests/test_observability_telemetry.py | 19 ++++++++++++++++++- 3 files changed, 42 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 39e4fe372..5ce29a600 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -440,3 +440,9 @@ 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. diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index a803995f8..541746284 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -1004,3 +1004,21 @@ 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. diff --git a/tests/test_observability_telemetry.py b/tests/test_observability_telemetry.py index 9535371a0..4ea219601 100644 --- a/tests/test_observability_telemetry.py +++ b/tests/test_observability_telemetry.py @@ -16,6 +16,20 @@ import lineageweave.observability as observability +def test_failure_log_drops_unknown_operation_without_a_metric_counter(monkeypatch, caplog) -> None: + """An unavailable metric channel must not admit unlisted operation content.""" + monkeypatch.setattr(observability, "_failure_counter", lambda: None) + monkeypatch.setattr(observability, "trace", None) + with caplog.at_level(logging.WARNING, logger=observability.__name__): + observability.record_server_failure( + "synthetic-private-operation", ValueError("private payload"), outcome="provider_unavailable" + ) + record = next(record for record in caplog.records if record.message == "lineageweave.server_failure") + assert record.operation_code == "unknown" + assert "synthetic-private-operation" not in str(vars(record)) + assert "private payload" not in str(vars(record)) + + def _signal_endpoint(endpoint: str, signal: str) -> str: """Thin wrapper so callers pass one helper under test.""" return observability._otlp_signal_endpoint(endpoint, signal) @@ -68,7 +82,8 @@ def test_metric_and_log_endpoint_helpers_route_to_their_signals() -> None: ) -def test_safe_attributes_skips_container_values_and_unknown_keys() -> None: +@pytest.mark.parametrize("invalid_value", [{"private": "content"}, ["content"], ("content",), {"content"}, None, object()]) +def test_safe_attributes_skips_container_values_and_unknown_keys(invalid_value) -> None: """Composite and unlisted attribute values never reach a span.""" sanitized = observability._safe_attributes( { @@ -77,11 +92,13 @@ def test_safe_attributes_skips_container_values_and_unknown_keys() -> None: "nested": {"a": 1}, "items": [1, 2, 3], "unlisted_key": "should-not-appear", + "http.response.status_code": invalid_value, } ) assert sanitized["lineageweave.operation_code"] == "http_post_json" assert sanitized["lineageweave.session_id"] == "post-123" assert "nested" not in sanitized + assert "http.response.status_code" not in sanitized assert "items" not in sanitized assert "unlisted_key" not in sanitized From 182d3c9d4c5f2a8ab2d63e77b8a9ced663a183f6 Mon Sep 17 00:00:00 2001 From: Codex Date: Mon, 7 Sep 2026 14:46:56 +0900 Subject: [PATCH 5/5] test(telemetry): verify degraded SDK failure preservation --- AGENTS.md | 5 +++++ docs/product-technical-gap-baseline.md | 16 ++++++++++++++++ tests/test_observability.py | 19 +++++++++++++++++++ tests/test_observability_telemetry.py | 26 ++++++++++++++++++++++++++ 4 files changed, 66 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 5ce29a600..f6da1226a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -446,3 +446,8 @@ 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. diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 541746284..b745962fe 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -1022,3 +1022,19 @@ or denominator change was made. Initial new-test failures were harness mistakes 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. diff --git a/tests/test_observability.py b/tests/test_observability.py index 16a29a2c6..67ccb68da 100644 --- a/tests/test_observability.py +++ b/tests/test_observability.py @@ -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) + 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 + 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 = {} diff --git a/tests/test_observability_telemetry.py b/tests/test_observability_telemetry.py index 4ea219601..c7901e947 100644 --- a/tests/test_observability_telemetry.py +++ b/tests/test_observability_telemetry.py @@ -10,12 +10,38 @@ from __future__ import annotations import logging +from types import SimpleNamespace import pytest import lineageweave.observability as observability +@pytest.mark.parametrize("current_span", [None, object()]) +def test_missing_span_context_does_not_invent_correlation(monkeypatch, current_span) -> None: + """An absent or incomplete span has no valid correlation identifiers.""" + monkeypatch.setattr(observability, "trace", SimpleNamespace(get_current_span=lambda: current_span)) + assert observability._current_trace_ids() == ("", "") + + +def test_unavailable_propagator_preserves_request_headers(monkeypatch) -> None: + """Missing propagation support must not change existing transport headers.""" + monkeypatch.setattr(observability, "_otel_inject", None) + carrier = {"content-type": "application/json"} + observability.inject_trace_context(carrier) + assert carrier == {"content-type": "application/json"} + + +def test_unlisted_failure_outcome_is_rejected_before_metric_creation(monkeypatch) -> None: + """Unlisted outcomes cannot create a new metric classification.""" + def unexpected_counter(): + pytest.fail("invalid classification reached metric creation") + + monkeypatch.setattr(observability, "_failure_counter", unexpected_counter) + with pytest.raises(ValueError, match="unsupported server failure outcome"): + observability.record_server_failure("post_chat", ValueError(), outcome="invented") + + def test_failure_log_drops_unknown_operation_without_a_metric_counter(monkeypatch, caplog) -> None: """An unavailable metric channel must not admit unlisted operation content.""" monkeypatch.setattr(observability, "_failure_counter", lambda: None)