diff --git a/changelog.d/1018.fixed.md b/changelog.d/1018.fixed.md new file mode 100644 index 000000000..eb419414b --- /dev/null +++ b/changelog.d/1018.fixed.md @@ -0,0 +1 @@ +Records the gateway and the entrypoint write while an endpoint runs, such as exception records and events, now carry the endpoint's route template as `http.route` (for example `/jobs/{job_id}`) instead of ``, so the daily telemetry error report groups them by route. Every service now requires policyengine-observability 3.0.6, which reads the matched route each time it writes a record. diff --git a/docs/engineering/skills/observability.md b/docs/engineering/skills/observability.md index c7ad2ac63..5332de28a 100644 --- a/docs/engineering/skills/observability.md +++ b/docs/engineering/skills/observability.md @@ -80,11 +80,22 @@ appropriate `StagePlan` and call `plan.name(Stage.VALUE)`. Do not add span name string literals in services or workers. Open a span with `runtime.span(plan.name(Stage.VALUE))`, and an operation with -`runtime.operation(...)`. policyengine-observability 3.0.4 types both scopes -correctly: their `__exit__` and `__aexit__` return `Literal[False]`, so Pyright -knows an exception raised in the block propagates; see +`runtime.operation(...)`. policyengine-observability 3.0.4 and later type both +scopes correctly: their `__exit__` and `__aexit__` return `Literal[False]`, so +Pyright knows an exception raised in the block propagates; see `docs/engineering/skills/type-checking.md`. +In every app instrumented with `init_simulation_observability` (the gateway, +the entrypoint and the executor's local FastAPI app), every record written +while a FastAPI endpoint runs, from `record_exception`, `event` or `log`, +carries the endpoint's route template as `http.route`, such as +`/jobs/{job_id}`, as the request's `request.completed` record does. +policyengine-observability 3.0.5 and later read the route when each record is +written; 3.0.4 wrote `` on every record written before the request +ended, because its middleware begins the request before FastAPI matches the +route. A record written before routing, such as one from a middleware, and a +request that matches no route still carry ``. + When adding a run configuration or runtime stage: 1. add it to `RunConfiguration` or `Stage`; diff --git a/docs/engineering/skills/type-checking.md b/docs/engineering/skills/type-checking.md index 56a444b58..1e0ff5ea2 100644 --- a/docs/engineering/skills/type-checking.md +++ b/docs/engineering/skills/type-checking.md @@ -174,11 +174,11 @@ section is still in the registry fails the guard on every pull request. - If an error shows a real bug, first write a test that demonstrates it, then fix it. - Open observability spans and operations with `runtime.span(...)` and - `runtime.operation(...)`. policyengine-observability 3.0.4 types them - correctly: their `__exit__` and `__aexit__` return `Literal[False]`, so + `runtime.operation(...)`. policyengine-observability 3.0.4 and later type + them correctly: their `__exit__` and `__aexit__` return `Literal[False]`, so Pyright knows they never suppress an exception and doesn't report a variable the block assigns as possibly unbound after it. Every package that depends - on policyengine-observability requires 3.0.4 or later, and + on policyengine-observability requires 3.0.6 or later, and `libs/policyengine-simulation-observability/tests/test_scope_exit_typing.py` fails if an older release is installed. - Before reading a simulation's output frames, narrow diff --git a/libs/policyengine-simulation-contract/uv.lock b/libs/policyengine-simulation-contract/uv.lock index 3fa7e7d15..6188bee12 100644 --- a/libs/policyengine-simulation-contract/uv.lock +++ b/libs/policyengine-simulation-contract/uv.lock @@ -1069,11 +1069,11 @@ wheels = [ [[package]] name = "policyengine-observability" -version = "3.0.4" +version = "3.0.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c3/d0/09b91aca37fb3efd493c164082097e85705fcac2062d2b5ef8a8c4e2be04/policyengine_observability-3.0.4.tar.gz", hash = "sha256:7e43a5d0e9b9e5f683623643efca7c47e651baaad7b557a50ec8b344602dc61f", size = 127972, upload-time = "2026-10-27T17:23:24.343Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/1e/3d7f7e22a415b27d6fc19739035df979dc9365d1e8b2fe39043cf55f25f5/policyengine_observability-3.0.6.tar.gz", hash = "sha256:648824176568342f0b048321cfc44e36098be49f143e52c567614add8c11fc2b", size = 129369, upload-time = "2026-11-04T10:22:28.886Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/00/00/d9c4850a12b4d0bef72034316909aac97228de302384af8936a958b7dc8e/policyengine_observability-3.0.4-py3-none-any.whl", hash = "sha256:1b3ebbfd00c138abcc2ec9580b13aa55679b91745783c5567cc73f8b2bbaaaf7", size = 42204, upload-time = "2026-10-27T17:23:22.641Z" }, + { url = "https://files.pythonhosted.org/packages/4e/d8/2e3b75e5b46d95af30891802260bc04ef11882732539ac31a09e9ae4d33e/policyengine_observability-3.0.6-py3-none-any.whl", hash = "sha256:364585ddfa2a9af14154049661c0d9c931185c42409bf3292d736a12d662cda0", size = 42575, upload-time = "2026-11-04T10:22:27.359Z" }, ] [package.optional-dependencies] @@ -1145,7 +1145,7 @@ requires-dist = [ { name = "black", marker = "extra == 'build'", specifier = ">=25.1.0" }, { name = "fastapi", specifier = ">=0.115.0" }, { name = "httpx2", marker = "extra == 'test'" }, - { name = "policyengine-observability", extras = ["fastapi", "google", "otlp-grpc"], specifier = ">=3.0.4,<4" }, + { name = "policyengine-observability", extras = ["fastapi", "google", "otlp-grpc"], specifier = ">=3.0.6,<4" }, { name = "pydantic", specifier = ">=2.0" }, { name = "pyright", marker = "extra == 'build'", specifier = ">=1.1.401" }, { name = "pytest", marker = "extra == 'test'", specifier = ">=8.3.4" }, diff --git a/libs/policyengine-simulation-observability/pyproject.toml b/libs/policyengine-simulation-observability/pyproject.toml index d7cf62251..0c203e3db 100644 --- a/libs/policyengine-simulation-observability/pyproject.toml +++ b/libs/policyengine-simulation-observability/pyproject.toml @@ -10,7 +10,7 @@ requires-python = ">=3.13" dependencies = [ "fastapi>=0.115.0", "pydantic>=2.0", - "policyengine-observability[fastapi,google,otlp-grpc]>=3.0.4,<4", + "policyengine-observability[fastapi,google,otlp-grpc]>=3.0.6,<4", ] [project.optional-dependencies] diff --git a/libs/policyengine-simulation-observability/uv.lock b/libs/policyengine-simulation-observability/uv.lock index 8e971eb08..6daa706b1 100644 --- a/libs/policyengine-simulation-observability/uv.lock +++ b/libs/policyengine-simulation-observability/uv.lock @@ -720,11 +720,11 @@ wheels = [ [[package]] name = "policyengine-observability" -version = "3.0.4" +version = "3.0.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c3/d0/09b91aca37fb3efd493c164082097e85705fcac2062d2b5ef8a8c4e2be04/policyengine_observability-3.0.4.tar.gz", hash = "sha256:7e43a5d0e9b9e5f683623643efca7c47e651baaad7b557a50ec8b344602dc61f", size = 127972, upload-time = "2026-10-27T17:23:24.343Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/1e/3d7f7e22a415b27d6fc19739035df979dc9365d1e8b2fe39043cf55f25f5/policyengine_observability-3.0.6.tar.gz", hash = "sha256:648824176568342f0b048321cfc44e36098be49f143e52c567614add8c11fc2b", size = 129369, upload-time = "2026-11-04T10:22:28.886Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/00/00/d9c4850a12b4d0bef72034316909aac97228de302384af8936a958b7dc8e/policyengine_observability-3.0.4-py3-none-any.whl", hash = "sha256:1b3ebbfd00c138abcc2ec9580b13aa55679b91745783c5567cc73f8b2bbaaaf7", size = 42204, upload-time = "2026-10-27T17:23:22.641Z" }, + { url = "https://files.pythonhosted.org/packages/4e/d8/2e3b75e5b46d95af30891802260bc04ef11882732539ac31a09e9ae4d33e/policyengine_observability-3.0.6-py3-none-any.whl", hash = "sha256:364585ddfa2a9af14154049661c0d9c931185c42409bf3292d736a12d662cda0", size = 42575, upload-time = "2026-11-04T10:22:27.359Z" }, ] [package.optional-dependencies] @@ -768,7 +768,7 @@ requires-dist = [ { name = "black", marker = "extra == 'build'", specifier = ">=25.1.0" }, { name = "fastapi", specifier = ">=0.115.0" }, { name = "httpx2", marker = "extra == 'test'" }, - { name = "policyengine-observability", extras = ["fastapi", "google", "otlp-grpc"], specifier = ">=3.0.4,<4" }, + { name = "policyengine-observability", extras = ["fastapi", "google", "otlp-grpc"], specifier = ">=3.0.6,<4" }, { name = "pydantic", specifier = ">=2.0" }, { name = "pyright", marker = "extra == 'build'", specifier = ">=1.1.401" }, { name = "pytest", marker = "extra == 'test'", specifier = ">=8.3.4" }, diff --git a/libs/policyengine-stage12-persistence/uv.lock b/libs/policyengine-stage12-persistence/uv.lock index 20d14c19b..dda65fe69 100644 --- a/libs/policyengine-stage12-persistence/uv.lock +++ b/libs/policyengine-stage12-persistence/uv.lock @@ -566,11 +566,11 @@ wheels = [ [[package]] name = "policyengine-observability" -version = "3.0.4" +version = "3.0.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c3/d0/09b91aca37fb3efd493c164082097e85705fcac2062d2b5ef8a8c4e2be04/policyengine_observability-3.0.4.tar.gz", hash = "sha256:7e43a5d0e9b9e5f683623643efca7c47e651baaad7b557a50ec8b344602dc61f", size = 127972, upload-time = "2026-10-27T17:23:24.343Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/1e/3d7f7e22a415b27d6fc19739035df979dc9365d1e8b2fe39043cf55f25f5/policyengine_observability-3.0.6.tar.gz", hash = "sha256:648824176568342f0b048321cfc44e36098be49f143e52c567614add8c11fc2b", size = 129369, upload-time = "2026-11-04T10:22:28.886Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/00/00/d9c4850a12b4d0bef72034316909aac97228de302384af8936a958b7dc8e/policyengine_observability-3.0.4-py3-none-any.whl", hash = "sha256:1b3ebbfd00c138abcc2ec9580b13aa55679b91745783c5567cc73f8b2bbaaaf7", size = 42204, upload-time = "2026-10-27T17:23:22.641Z" }, + { url = "https://files.pythonhosted.org/packages/4e/d8/2e3b75e5b46d95af30891802260bc04ef11882732539ac31a09e9ae4d33e/policyengine_observability-3.0.6-py3-none-any.whl", hash = "sha256:364585ddfa2a9af14154049661c0d9c931185c42409bf3292d736a12d662cda0", size = 42575, upload-time = "2026-11-04T10:22:27.359Z" }, ] [package.optional-dependencies] @@ -627,7 +627,7 @@ requires-dist = [ { name = "black", marker = "extra == 'build'", specifier = ">=25.1.0" }, { name = "fastapi", specifier = ">=0.115.0" }, { name = "httpx2", marker = "extra == 'test'" }, - { name = "policyengine-observability", extras = ["fastapi", "google", "otlp-grpc"], specifier = ">=3.0.4,<4" }, + { name = "policyengine-observability", extras = ["fastapi", "google", "otlp-grpc"], specifier = ">=3.0.6,<4" }, { name = "pydantic", specifier = ">=2.0" }, { name = "pyright", marker = "extra == 'build'", specifier = ">=1.1.401" }, { name = "pytest", marker = "extra == 'test'", specifier = ">=8.3.4" }, diff --git a/projects/policyengine-simulation-entry/pyproject.toml b/projects/policyengine-simulation-entry/pyproject.toml index 1b43b4f1d..e1330304a 100644 --- a/projects/policyengine-simulation-entry/pyproject.toml +++ b/projects/policyengine-simulation-entry/pyproject.toml @@ -16,7 +16,7 @@ dependencies = [ "fastapi>=0.115.0,<1", "httpx>=0.28.0,<1", "modal>=1.4,<2", - "policyengine-observability[fastapi,google,httpx,otlp-grpc]>=3.0.4,<4", + "policyengine-observability[fastapi,google,httpx,otlp-grpc]>=3.0.6,<4", "policyengine-simulation-contract", "policyengine-simulation-observability", "policyengine-stage12-persistence", diff --git a/projects/policyengine-simulation-entry/tests/test_app.py b/projects/policyengine-simulation-entry/tests/test_app.py index 6c0443123..9598e9206 100644 --- a/projects/policyengine-simulation-entry/tests/test_app.py +++ b/projects/policyengine-simulation-entry/tests/test_app.py @@ -789,6 +789,40 @@ def test_job_status_records_structured_backend_telemetry( assert attributes["job_id"] == "fc-123" +def test_event_recorded_while_a_route_runs_carries_the_matched_route( + capsys: pytest.CaptureFixture[str], +): + backend = FakeBackend() + backend.responses[("GET", "/jobs/fc-123")] = response(202, {"status": "running"}) + # The runtime keeps the standard output it finds when it is built, so the + # app is built after capsys replaces it. + app = create_app( + settings=make_settings(), backend=backend, auth_dependency=lambda: None + ) + + from fastapi.testclient import TestClient + + with TestClient(app) as test_client: + capsys.readouterr() + job_response = test_client.get("/jobs/fc-123") + captured_output = capsys.readouterr().out + + assert job_response.status_code == 202 + routes_by_event_name: dict[object, list[object]] = {} + for output_line in captured_output.splitlines(): + try: + record = json.loads(output_line) + except json.JSONDecodeError: + continue + routes_by_event_name.setdefault(record.get("event.name"), []).append( + record.get("http.route") + ) + assert routes_by_event_name["simulation_entry_backend_response"] == [ + "/jobs/{job_id}" + ] + assert routes_by_event_name["request.completed"] == ["/jobs/{job_id}"] + + def test_budget_window_routes_use_original_batch_id(client, backend): backend.responses[("POST", "/simulate/economy/budget-window")] = response( 202, diff --git a/projects/policyengine-simulation-entry/uv.lock b/projects/policyengine-simulation-entry/uv.lock index e9a587c24..0eb687564 100644 --- a/projects/policyengine-simulation-entry/uv.lock +++ b/projects/policyengine-simulation-entry/uv.lock @@ -840,11 +840,11 @@ wheels = [ [[package]] name = "policyengine-observability" -version = "3.0.4" +version = "3.0.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c3/d0/09b91aca37fb3efd493c164082097e85705fcac2062d2b5ef8a8c4e2be04/policyengine_observability-3.0.4.tar.gz", hash = "sha256:7e43a5d0e9b9e5f683623643efca7c47e651baaad7b557a50ec8b344602dc61f", size = 127972, upload-time = "2026-10-27T17:23:24.343Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/1e/3d7f7e22a415b27d6fc19739035df979dc9365d1e8b2fe39043cf55f25f5/policyengine_observability-3.0.6.tar.gz", hash = "sha256:648824176568342f0b048321cfc44e36098be49f143e52c567614add8c11fc2b", size = 129369, upload-time = "2026-11-04T10:22:28.886Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/00/00/d9c4850a12b4d0bef72034316909aac97228de302384af8936a958b7dc8e/policyengine_observability-3.0.4-py3-none-any.whl", hash = "sha256:1b3ebbfd00c138abcc2ec9580b13aa55679b91745783c5567cc73f8b2bbaaaf7", size = 42204, upload-time = "2026-10-27T17:23:22.641Z" }, + { url = "https://files.pythonhosted.org/packages/4e/d8/2e3b75e5b46d95af30891802260bc04ef11882732539ac31a09e9ae4d33e/policyengine_observability-3.0.6-py3-none-any.whl", hash = "sha256:364585ddfa2a9af14154049661c0d9c931185c42409bf3292d736a12d662cda0", size = 42575, upload-time = "2026-11-04T10:22:27.359Z" }, ] [package.optional-dependencies] @@ -925,7 +925,7 @@ requires-dist = [ { name = "httpx", specifier = ">=0.28.0,<1" }, { name = "modal", specifier = ">=1.4,<2" }, { name = "openapi-python-client", marker = "extra == 'build'", specifier = ">=0.21.6" }, - { name = "policyengine-observability", extras = ["fastapi", "google", "httpx", "otlp-grpc"], specifier = ">=3.0.4,<4" }, + { name = "policyengine-observability", extras = ["fastapi", "google", "httpx", "otlp-grpc"], specifier = ">=3.0.6,<4" }, { name = "policyengine-simulation-contract", editable = "../../libs/policyengine-simulation-contract" }, { name = "policyengine-simulation-observability", editable = "../../libs/policyengine-simulation-observability" }, { name = "policyengine-stage12-persistence", editable = "../../libs/policyengine-stage12-persistence" }, @@ -954,7 +954,7 @@ requires-dist = [ { name = "black", marker = "extra == 'build'", specifier = ">=25.1.0" }, { name = "fastapi", specifier = ">=0.115.0" }, { name = "httpx2", marker = "extra == 'test'" }, - { name = "policyengine-observability", extras = ["fastapi", "google", "otlp-grpc"], specifier = ">=3.0.4,<4" }, + { name = "policyengine-observability", extras = ["fastapi", "google", "otlp-grpc"], specifier = ">=3.0.6,<4" }, { name = "pydantic", specifier = ">=2.0" }, { name = "pyright", marker = "extra == 'build'", specifier = ">=1.1.401" }, { name = "pytest", marker = "extra == 'test'", specifier = ">=8.3.4" }, diff --git a/projects/policyengine-simulation-executor/pyproject.toml b/projects/policyengine-simulation-executor/pyproject.toml index faf357acd..759254cd3 100644 --- a/projects/policyengine-simulation-executor/pyproject.toml +++ b/projects/policyengine-simulation-executor/pyproject.toml @@ -24,7 +24,7 @@ dependencies = [ "policyengine[models]==6.12.1", "tables>=3.10.2", "modal>=0.73.0", - "policyengine-observability[fastapi,google,otlp-grpc]>=3.0.4,<4", + "policyengine-observability[fastapi,google,otlp-grpc]>=3.0.6,<4", # Imported directly by the artifact store client; do not rely on the # transitive pin via policyengine. "google-cloud-storage>=2", @@ -49,7 +49,7 @@ modal-simulation-image = [ "policyengine[models]==6.12.1", "fastapi>=0.115.0", "tables>=3.10.2", - "policyengine-observability[fastapi,google,otlp-grpc]>=3.0.4,<4", + "policyengine-observability[fastapi,google,otlp-grpc]>=3.0.6,<4", # The artifact fetch layer and store client read GCS. The lib is also a # transitive dependency of policyengine, but the image must not depend # on an upstream package keeping it. diff --git a/projects/policyengine-simulation-executor/uv.lock b/projects/policyengine-simulation-executor/uv.lock index c74de3a42..58d9baca7 100644 --- a/projects/policyengine-simulation-executor/uv.lock +++ b/projects/policyengine-simulation-executor/uv.lock @@ -1840,11 +1840,11 @@ provides-extras = ["test", "build"] [[package]] name = "policyengine-observability" -version = "3.0.4" +version = "3.0.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c3/d0/09b91aca37fb3efd493c164082097e85705fcac2062d2b5ef8a8c4e2be04/policyengine_observability-3.0.4.tar.gz", hash = "sha256:7e43a5d0e9b9e5f683623643efca7c47e651baaad7b557a50ec8b344602dc61f", size = 127972, upload-time = "2026-10-27T17:23:24.343Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/1e/3d7f7e22a415b27d6fc19739035df979dc9365d1e8b2fe39043cf55f25f5/policyengine_observability-3.0.6.tar.gz", hash = "sha256:648824176568342f0b048321cfc44e36098be49f143e52c567614add8c11fc2b", size = 129369, upload-time = "2026-11-04T10:22:28.886Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/00/00/d9c4850a12b4d0bef72034316909aac97228de302384af8936a958b7dc8e/policyengine_observability-3.0.4-py3-none-any.whl", hash = "sha256:1b3ebbfd00c138abcc2ec9580b13aa55679b91745783c5567cc73f8b2bbaaaf7", size = 42204, upload-time = "2026-10-27T17:23:22.641Z" }, + { url = "https://files.pythonhosted.org/packages/4e/d8/2e3b75e5b46d95af30891802260bc04ef11882732539ac31a09e9ae4d33e/policyengine_observability-3.0.6-py3-none-any.whl", hash = "sha256:364585ddfa2a9af14154049661c0d9c931185c42409bf3292d736a12d662cda0", size = 42575, upload-time = "2026-11-04T10:22:27.359Z" }, ] [package.optional-dependencies] @@ -1953,7 +1953,7 @@ requires-dist = [ { name = "opentelemetry-instrumentation-sqlalchemy", specifier = ">=0.65b0,<0.66" }, { name = "policyengine", extras = ["models"], specifier = "==6.12.1" }, { name = "policyengine-fastapi", editable = "../../libs/policyengine-fastapi" }, - { name = "policyengine-observability", extras = ["fastapi", "google", "otlp-grpc"], specifier = ">=3.0.4,<4" }, + { name = "policyengine-observability", extras = ["fastapi", "google", "otlp-grpc"], specifier = ">=3.0.6,<4" }, { name = "policyengine-simulation-contract", extras = ["modal"], editable = "../../libs/policyengine-simulation-contract" }, { name = "policyengine-simulation-observability", editable = "../../libs/policyengine-simulation-observability" }, { name = "policyengine-stage12-persistence", editable = "../../libs/policyengine-stage12-persistence" }, @@ -1974,7 +1974,7 @@ modal-simulation-image = [ { name = "google-cloud-storage", specifier = ">=2" }, { name = "huggingface-hub", specifier = ">=1,<2" }, { name = "policyengine", extras = ["models"], specifier = "==6.12.1" }, - { name = "policyengine-observability", extras = ["fastapi", "google", "otlp-grpc"], specifier = ">=3.0.4,<4" }, + { name = "policyengine-observability", extras = ["fastapi", "google", "otlp-grpc"], specifier = ">=3.0.6,<4" }, { name = "psycopg", extras = ["binary"], specifier = ">=3.2,<4" }, { name = "pyarrow", specifier = ">=20,<24" }, { name = "sqlalchemy", specifier = ">=2,<3" }, @@ -2004,7 +2004,7 @@ requires-dist = [ { name = "modal", specifier = ">=1.4,<2" }, { name = "openapi-python-client", marker = "extra == 'build'", specifier = ">=0.21.6" }, { name = "packaging", specifier = ">=24" }, - { name = "policyengine-observability", extras = ["fastapi", "google", "otlp-grpc"], specifier = ">=3.0.4,<4" }, + { name = "policyengine-observability", extras = ["fastapi", "google", "otlp-grpc"], specifier = ">=3.0.6,<4" }, { name = "pydantic", specifier = ">=2.0" }, { name = "pyjwt", specifier = ">=2.10.1,<3.0.0" }, { name = "pyright", marker = "extra == 'build'", specifier = ">=1.1.401" }, @@ -2036,7 +2036,7 @@ requires-dist = [ { name = "black", marker = "extra == 'build'", specifier = ">=25.1.0" }, { name = "fastapi", specifier = ">=0.115.0" }, { name = "httpx2", marker = "extra == 'test'" }, - { name = "policyengine-observability", extras = ["fastapi", "google", "otlp-grpc"], specifier = ">=3.0.4,<4" }, + { name = "policyengine-observability", extras = ["fastapi", "google", "otlp-grpc"], specifier = ">=3.0.6,<4" }, { name = "pydantic", specifier = ">=2.0" }, { name = "pyright", marker = "extra == 'build'", specifier = ">=1.1.401" }, { name = "pytest", marker = "extra == 'test'", specifier = ">=8.3.4" }, diff --git a/projects/policyengine-simulation-gateway/pyproject.toml b/projects/policyengine-simulation-gateway/pyproject.toml index 4518faa1e..4f5343dcc 100644 --- a/projects/policyengine-simulation-gateway/pyproject.toml +++ b/projects/policyengine-simulation-gateway/pyproject.toml @@ -25,7 +25,7 @@ dependencies = [ "pyjwt>=2.10.1,<3.0.0", # JWTDecoder (policyengine-fastapi lib) needs cryptography at runtime. "cryptography>=41.0.0", - "policyengine-observability[fastapi,google,otlp-grpc]>=3.0.4,<4", + "policyengine-observability[fastapi,google,otlp-grpc]>=3.0.6,<4", "modal>=1.4,<2", ] diff --git a/projects/policyengine-simulation-gateway/tests/test_observability.py b/projects/policyengine-simulation-gateway/tests/test_observability.py index 68b4930a1..ffe290059 100644 --- a/projects/policyengine-simulation-gateway/tests/test_observability.py +++ b/projects/policyengine-simulation-gateway/tests/test_observability.py @@ -1,10 +1,26 @@ """Gateway observability identity and request correlation tests.""" +import json + +import pytest from fastapi.testclient import TestClient from policyengine_observability import REQUEST_ID_HEADER from policyengine_simulation_gateway.testing import create_gateway_app +def _records_written(captured_output: str) -> list[dict[str, object]]: + """Return the JSON records the runtime wrote to standard output, in order.""" + records: list[dict[str, object]] = [] + for output_line in captured_output.splitlines(): + try: + record = json.loads(output_line) + except json.JSONDecodeError: + continue + if isinstance(record, dict): + records.append(record) + return records + + def test_gateway_preserves_policyengine_request_id(): app = create_gateway_app() response = TestClient(app).get( @@ -36,3 +52,31 @@ def test_gateway_response_survives_unavailable_collector(monkeypatch): assert response.status_code == 200 assert response.json() == {"status": "healthy"} + + +def test_exception_recorded_while_a_route_runs_carries_the_matched_route( + capsys: pytest.CaptureFixture[str], mock_modal +): + # The runtime keeps the standard output it finds when it is built, so the + # app is built after capsys replaces it. + client = TestClient(create_gateway_app(), raise_server_exceptions=False) + submit_response = client.post( + "/simulate/economy/comparison", + json={"country": "us", "scope": "macro", "reform": {}}, + ) + job_id = submit_response.json()["job_id"] + mock_modal["function_call"].from_id_errors[job_id] = RuntimeError( + "Modal control plane unavailable" + ) + capsys.readouterr() + + response = client.get(f"/jobs/{job_id}") + + assert response.status_code == 500 + routes_by_event_name: dict[object, list[object]] = {} + for record in _records_written(capsys.readouterr().out): + routes_by_event_name.setdefault(record["event.name"], []).append( + record.get("http.route") + ) + assert routes_by_event_name["exception.recorded"] == ["/jobs/{job_id}"] + assert routes_by_event_name["request.completed"] == ["/jobs/{job_id}"] diff --git a/projects/policyengine-simulation-gateway/uv.lock b/projects/policyengine-simulation-gateway/uv.lock index 8e709799d..90d70cb8d 100644 --- a/projects/policyengine-simulation-gateway/uv.lock +++ b/projects/policyengine-simulation-gateway/uv.lock @@ -1261,11 +1261,11 @@ provides-extras = ["test", "build"] [[package]] name = "policyengine-observability" -version = "3.0.4" +version = "3.0.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c3/d0/09b91aca37fb3efd493c164082097e85705fcac2062d2b5ef8a8c4e2be04/policyengine_observability-3.0.4.tar.gz", hash = "sha256:7e43a5d0e9b9e5f683623643efca7c47e651baaad7b557a50ec8b344602dc61f", size = 127972, upload-time = "2026-10-27T17:23:24.343Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/1e/3d7f7e22a415b27d6fc19739035df979dc9365d1e8b2fe39043cf55f25f5/policyengine_observability-3.0.6.tar.gz", hash = "sha256:648824176568342f0b048321cfc44e36098be49f143e52c567614add8c11fc2b", size = 129369, upload-time = "2026-11-04T10:22:28.886Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/00/00/d9c4850a12b4d0bef72034316909aac97228de302384af8936a958b7dc8e/policyengine_observability-3.0.4-py3-none-any.whl", hash = "sha256:1b3ebbfd00c138abcc2ec9580b13aa55679b91745783c5567cc73f8b2bbaaaf7", size = 42204, upload-time = "2026-10-27T17:23:22.641Z" }, + { url = "https://files.pythonhosted.org/packages/4e/d8/2e3b75e5b46d95af30891802260bc04ef11882732539ac31a09e9ae4d33e/policyengine_observability-3.0.6-py3-none-any.whl", hash = "sha256:364585ddfa2a9af14154049661c0d9c931185c42409bf3292d736a12d662cda0", size = 42575, upload-time = "2026-11-04T10:22:27.359Z" }, ] [package.optional-dependencies] @@ -1355,7 +1355,7 @@ requires-dist = [ { name = "modal", specifier = ">=1.4,<2" }, { name = "openapi-python-client", marker = "extra == 'build'", specifier = ">=0.21.6" }, { name = "packaging", specifier = ">=24" }, - { name = "policyengine-observability", extras = ["fastapi", "google", "otlp-grpc"], specifier = ">=3.0.4,<4" }, + { name = "policyengine-observability", extras = ["fastapi", "google", "otlp-grpc"], specifier = ">=3.0.6,<4" }, { name = "pydantic", specifier = ">=2.0" }, { name = "pyjwt", specifier = ">=2.10.1,<3.0.0" }, { name = "pyright", marker = "extra == 'build'", specifier = ">=1.1.401" }, @@ -1387,7 +1387,7 @@ requires-dist = [ { name = "black", marker = "extra == 'build'", specifier = ">=25.1.0" }, { name = "fastapi", specifier = ">=0.115.0" }, { name = "httpx2", marker = "extra == 'test'" }, - { name = "policyengine-observability", extras = ["fastapi", "google", "otlp-grpc"], specifier = ">=3.0.4,<4" }, + { name = "policyengine-observability", extras = ["fastapi", "google", "otlp-grpc"], specifier = ">=3.0.6,<4" }, { name = "pydantic", specifier = ">=2.0" }, { name = "pyright", marker = "extra == 'build'", specifier = ">=1.1.401" }, { name = "pytest", marker = "extra == 'test'", specifier = ">=8.3.4" },