Skip to content

Commit 1b81c54

Browse files
anth-volkclaude
andcommitted
Log the safety check's passes through the observability runtime
From the extra review of the reworked 1.06: the job configured logging with logging.basicConfig, whose plain format drops every field of a log record, so a pass's counts never reached the job's logs. The job now starts the process observability runtime, as the Modal workers do, logs JSON like the web service, and shuts the runtime down on exit. The pass summary carries its counts as policyengine_attributes, the only record fields the observability log handler keeps. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 444acbf commit 1b81c54

2 files changed

Lines changed: 78 additions & 8 deletions

File tree

‎projects/policyengine-simulation-entry/src/policyengine_simulation_entry/safety_check.py‎

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@
2929
from dataclasses import asdict
3030
from typing import TYPE_CHECKING, Protocol
3131

32+
from policyengine_simulation_observability.observability import (
33+
init_process_observability,
34+
)
35+
3236
from policyengine_simulation_entry.modal_calls import OWN_PROCESS_SIGNALS
3337
from policyengine_simulation_entry.simulation_database import SimulationDatabase
3438

@@ -44,6 +48,23 @@
4448
EXIT_NOT_CONFIGURED = 2
4549

4650

51+
class Telemetry(Protocol):
52+
"""The observability runtime's part the job uses."""
53+
54+
def shutdown(self) -> None:
55+
"""Flush and stop telemetry."""
56+
...
57+
58+
59+
def _start_observability(environment: str) -> Telemetry:
60+
return init_process_observability(
61+
service_name="policyengine-simulation-safety-check",
62+
service_role="simulation_safety_check",
63+
platform="google_cloud_run",
64+
environment=environment,
65+
)
66+
67+
4768
class DueOutcomeRecorder(Protocol):
4869
"""Run one recording pass (``RunRecorder``)."""
4970

@@ -60,13 +81,25 @@ def main(
6081
*,
6182
environment: Mapping[str, str] | None = None,
6283
build_recorder: Callable[[str], DueOutcomeRecorder] = _database_recorder,
84+
start_observability: Callable[[str], Telemetry] = _start_observability,
6385
) -> int:
6486
"""Run one pass and return the process exit status.
6587
66-
Only a signal to this process propagates; any other error fails the
88+
Logs go through the observability runtime, as the web service's do. Only
89+
a signal to this process propagates; any other error fails the
6790
execution, and the next scheduled execution tries again.
6891
"""
6992
values = os.environ if environment is None else environment
93+
telemetry = start_observability(values.get("APP_ENVIRONMENT", "local"))
94+
try:
95+
return _run_pass(values, build_recorder)
96+
finally:
97+
telemetry.shutdown()
98+
99+
100+
def _run_pass(
101+
values: Mapping[str, str], build_recorder: Callable[[str], DueOutcomeRecorder]
102+
) -> int:
70103
database_url = values.get("SIMULATION_DATABASE_URL", "")
71104
if not database_url:
72105
logger.error("safety_check_not_configured")
@@ -81,10 +114,13 @@ def main(
81114
"safety_check_pass_failed", extra={"error_type": type(error).__name__}
82115
)
83116
return EXIT_PASS_FAILED
84-
logger.info("safety_check_pass", extra=asdict(recording))
117+
# The observability log handler keeps a record's policyengine_attributes.
118+
logger.info(
119+
"safety_check_pass", extra={"policyengine_attributes": asdict(recording)}
120+
)
85121
return EXIT_PASSED
86122

87123

88124
if __name__ == "__main__":
89-
logging.basicConfig(level=logging.INFO)
125+
logging.getLogger().setLevel(logging.INFO)
90126
sys.exit(main())

‎projects/policyengine-simulation-entry/tests/test_safety_check.py‎

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,34 @@ async def record_due_outcomes(self) -> RecordingPass:
3232
return self.result
3333

3434

35-
def _run(environment, recorder):
35+
class _Telemetry:
36+
"""Stands in for the observability runtime the job starts and shuts down."""
37+
38+
def __init__(self) -> None:
39+
self.environments = []
40+
self.shut_down = False
41+
42+
def start(self, environment):
43+
self.environments.append(environment)
44+
return self
45+
46+
def shutdown(self) -> None:
47+
self.shut_down = True
48+
49+
50+
def _run(environment, recorder, telemetry=None):
3651
built_for = []
3752

3853
def build(database_url):
3954
built_for.append(database_url)
4055
return recorder
4156

42-
status = safety_check.main(environment=environment, build_recorder=build)
57+
telemetry = telemetry or _Telemetry()
58+
status = safety_check.main(
59+
environment=environment,
60+
build_recorder=build,
61+
start_observability=telemetry.start,
62+
)
4363
return status, built_for
4464

4565

@@ -62,9 +82,23 @@ def test_the_pass_is_logged_with_what_it_found(caplog) -> None:
6282
(record,) = [
6383
record for record in caplog.records if record.message == "safety_check_pass"
6484
]
65-
assert record.checked == 4
66-
assert record.succeeded == 1
67-
assert record.abandoned == 2
85+
# The observability log handler keeps only these attributes of a record.
86+
assert record.policyengine_attributes["checked"] == 4
87+
assert record.policyengine_attributes["succeeded"] == 1
88+
assert record.policyengine_attributes["abandoned"] == 2
89+
90+
91+
def test_the_job_logs_through_the_observability_runtime_and_shuts_it_down() -> None:
92+
telemetry = _Telemetry()
93+
94+
_run(
95+
{"SIMULATION_DATABASE_URL": DATABASE_URL, "APP_ENVIRONMENT": "production"},
96+
_OnePass(RecordingPass()),
97+
telemetry,
98+
)
99+
100+
assert telemetry.environments == ["production"]
101+
assert telemetry.shut_down
68102

69103

70104
def test_an_execution_without_the_database_url_fails_without_a_pass() -> None:

0 commit comments

Comments
 (0)