policyengine-observability 1.3.0__tar.gz → 1.3.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. {policyengine_observability-1.3.0 → policyengine_observability-1.3.2}/CHANGELOG.md +14 -0
  2. {policyengine_observability-1.3.0 → policyengine_observability-1.3.2}/PKG-INFO +7 -7
  3. {policyengine_observability-1.3.0 → policyengine_observability-1.3.2}/policyengine_observability/destinations/manager.py +37 -0
  4. {policyengine_observability-1.3.0 → policyengine_observability-1.3.2}/pyproject.toml +7 -7
  5. policyengine_observability-1.3.2/tests/test_destinations.py +210 -0
  6. {policyengine_observability-1.3.0 → policyengine_observability-1.3.2}/uv.lock +43 -43
  7. policyengine_observability-1.3.0/tests/test_destinations.py +0 -102
  8. {policyengine_observability-1.3.0 → policyengine_observability-1.3.2}/.github/bump_version.py +0 -0
  9. {policyengine_observability-1.3.0 → policyengine_observability-1.3.2}/.github/check-changelog.sh +0 -0
  10. {policyengine_observability-1.3.0 → policyengine_observability-1.3.2}/.github/copilot-instructions.md +0 -0
  11. {policyengine_observability-1.3.0 → policyengine_observability-1.3.2}/.github/fetch_version.py +0 -0
  12. {policyengine_observability-1.3.0 → policyengine_observability-1.3.2}/.github/get-changelog-diff.sh +0 -0
  13. {policyengine_observability-1.3.0 → policyengine_observability-1.3.2}/.github/publish-git-tag.sh +0 -0
  14. {policyengine_observability-1.3.0 → policyengine_observability-1.3.2}/.github/workflows/pr.yml +0 -0
  15. {policyengine_observability-1.3.0 → policyengine_observability-1.3.2}/.github/workflows/push.yml +0 -0
  16. {policyengine_observability-1.3.0 → policyengine_observability-1.3.2}/.gitignore +0 -0
  17. {policyengine_observability-1.3.0 → policyengine_observability-1.3.2}/AGENTS.md +0 -0
  18. {policyengine_observability-1.3.0 → policyengine_observability-1.3.2}/CLAUDE.md +0 -0
  19. {policyengine_observability-1.3.0 → policyengine_observability-1.3.2}/README.md +0 -0
  20. {policyengine_observability-1.3.0 → policyengine_observability-1.3.2}/docs/engineering/skills/README.md +0 -0
  21. {policyengine_observability-1.3.0 → policyengine_observability-1.3.2}/docs/engineering/skills/github-prs.md +0 -0
  22. {policyengine_observability-1.3.0 → policyengine_observability-1.3.2}/docs/engineering/skills/repository-guidance.md +0 -0
  23. {policyengine_observability-1.3.0 → policyengine_observability-1.3.2}/docs/operations/google-cloud-stage3-runbook.md +0 -0
  24. {policyengine_observability-1.3.0 → policyengine_observability-1.3.2}/policyengine_observability/__init__.py +0 -0
  25. {policyengine_observability-1.3.0 → policyengine_observability-1.3.2}/policyengine_observability/adapters/__init__.py +0 -0
  26. {policyengine_observability-1.3.0 → policyengine_observability-1.3.2}/policyengine_observability/adapters/fastapi.py +0 -0
  27. {policyengine_observability-1.3.0 → policyengine_observability-1.3.2}/policyengine_observability/adapters/flask.py +0 -0
  28. {policyengine_observability-1.3.0 → policyengine_observability-1.3.2}/policyengine_observability/config.py +0 -0
  29. {policyengine_observability-1.3.0 → policyengine_observability-1.3.2}/policyengine_observability/context.py +0 -0
  30. {policyengine_observability-1.3.0 → policyengine_observability-1.3.2}/policyengine_observability/destinations/__init__.py +0 -0
  31. {policyengine_observability-1.3.0 → policyengine_observability-1.3.2}/policyengine_observability/destinations/base.py +0 -0
  32. {policyengine_observability-1.3.0 → policyengine_observability-1.3.2}/policyengine_observability/destinations/google_cloud_logging.py +0 -0
  33. {policyengine_observability-1.3.0 → policyengine_observability-1.3.2}/policyengine_observability/destinations/stdout.py +0 -0
  34. {policyengine_observability-1.3.0 → policyengine_observability-1.3.2}/policyengine_observability/google_credentials.py +0 -0
  35. {policyengine_observability-1.3.0 → policyengine_observability-1.3.2}/policyengine_observability/integrations/__init__.py +0 -0
  36. {policyengine_observability-1.3.0 → policyengine_observability-1.3.2}/policyengine_observability/integrations/httpx.py +0 -0
  37. {policyengine_observability-1.3.0 → policyengine_observability-1.3.2}/policyengine_observability/logging.py +0 -0
  38. {policyengine_observability-1.3.0 → policyengine_observability-1.3.2}/policyengine_observability/runtime.py +0 -0
  39. {policyengine_observability-1.3.0 → policyengine_observability-1.3.2}/policyengine_observability/segments.py +0 -0
  40. {policyengine_observability-1.3.0 → policyengine_observability-1.3.2}/tests/test_fastapi_adapter.py +0 -0
  41. {policyengine_observability-1.3.0 → policyengine_observability-1.3.2}/tests/test_flask_adapter.py +0 -0
  42. {policyengine_observability-1.3.0 → policyengine_observability-1.3.2}/tests/test_google_credentials.py +0 -0
  43. {policyengine_observability-1.3.0 → policyengine_observability-1.3.2}/tests/test_public_api.py +0 -0
  44. {policyengine_observability-1.3.0 → policyengine_observability-1.3.2}/tests/test_release_scripts.py +0 -0
  45. {policyengine_observability-1.3.0 → policyengine_observability-1.3.2}/tests/test_runtime.py +0 -0
@@ -1,3 +1,17 @@
1
+ ## [1.3.2] - 2026-07-03
2
+
3
+ ### Fixed
4
+
5
+ - Disable a log destination after repeated consecutive emit failures, restoring stdout if none remain — an observability sink can no longer degrade its host service's request path.
6
+
7
+
8
+ ## [1.3.1] - 2026-07-02
9
+
10
+ ### Fixed
11
+
12
+ - Set coherent minimum versions across the OpenTelemetry dependency stack: `opentelemetry-api`, `-sdk`, `-exporter-otlp-proto-grpc`, and `-exporter-otlp-proto-http` to `>=1.43.0`, and `opentelemetry-instrumentation-fastapi` and `-httpx` to `>=0.64b0`. These forward-pin the whole stack to the coordinated OpenTelemetry release that fixes `AttributeError: '_IncludedRouter' object has no attribute 'path'` — which earlier `opentelemetry-instrumentation-fastapi` raised on FastAPI >= 0.137 `include_router` routing, turning every CORS preflight `OPTIONS` request into a 500 for consumers that enable FastAPI instrumentation.
13
+
14
+
1
15
  ## [1.3.0] - 2026-07-01
2
16
 
3
17
  ### Added
@@ -1,15 +1,15 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: policyengine-observability
3
- Version: 1.3.0
3
+ Version: 1.3.2
4
4
  Summary: Shared PolicyEngine observability runtime for logs, timings, metrics, and OpenTelemetry.
5
5
  Author-email: PolicyEngine <hello@policyengine.org>
6
6
  Requires-Python: >=3.12
7
- Requires-Dist: opentelemetry-api
8
- Requires-Dist: opentelemetry-exporter-otlp-proto-grpc
9
- Requires-Dist: opentelemetry-exporter-otlp-proto-http
10
- Requires-Dist: opentelemetry-instrumentation-fastapi
11
- Requires-Dist: opentelemetry-instrumentation-httpx
12
- Requires-Dist: opentelemetry-sdk
7
+ Requires-Dist: opentelemetry-api>=1.43.0
8
+ Requires-Dist: opentelemetry-exporter-otlp-proto-grpc>=1.43.0
9
+ Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.43.0
10
+ Requires-Dist: opentelemetry-instrumentation-fastapi>=0.64b0
11
+ Requires-Dist: opentelemetry-instrumentation-httpx>=0.64b0
12
+ Requires-Dist: opentelemetry-sdk>=1.43.0
13
13
  Provides-Extra: all
14
14
  Requires-Dist: fastapi; extra == 'all'
15
15
  Requires-Dist: flask>=2.2; extra == 'all'
@@ -9,6 +9,13 @@ from .base import LogDestination
9
9
  from .google_cloud_logging import GoogleCloudLoggingDestination
10
10
  from .stdout import StdoutJsonDestination
11
11
 
12
+ # A destination that fails this many consecutive emits is disabled for the
13
+ # rest of the process. Emission is synchronous on the caller's (request) path,
14
+ # so a persistently failing destination — e.g. a credential exchange that
15
+ # errors after long internal retries — must not keep charging every request;
16
+ # an observability sink can never be allowed to degrade the host service.
17
+ DESTINATION_FAILURE_LIMIT = 3
18
+
12
19
 
13
20
  class LogDestinationManager:
14
21
  def __init__(
@@ -25,6 +32,7 @@ class LogDestinationManager:
25
32
  self.on_failure = on_failure
26
33
  self.destinations: list[LogDestination] = []
27
34
  self.configured = False
35
+ self._consecutive_failures: dict[int, int] = {}
28
36
 
29
37
  def configure(self) -> None:
30
38
  failures: list[tuple[str, BaseException]] = []
@@ -62,6 +70,7 @@ class LogDestinationManager:
62
70
  severity: str,
63
71
  ) -> None:
64
72
  emitted_payload = {**payload, "severity": severity}
73
+ tripped: list[LogDestination] = []
65
74
  for destination in self._ensure_destinations():
66
75
  try:
67
76
  destination.emit(
@@ -69,13 +78,23 @@ class LogDestinationManager:
69
78
  log_type=log_type,
70
79
  severity=severity,
71
80
  )
81
+ self._consecutive_failures.pop(id(destination), None)
72
82
  except BaseException as exc:
83
+ failures = (
84
+ self._consecutive_failures.get(id(destination), 0) + 1
85
+ )
86
+ self._consecutive_failures[id(destination)] = failures
87
+ if failures >= DESTINATION_FAILURE_LIMIT:
88
+ tripped.append(destination)
73
89
  self.on_failure(
74
90
  "logging.destination_emit",
75
91
  exc,
76
92
  destination=getattr(destination, "name", None),
77
93
  log_type=log_type,
94
+ consecutive_failures=failures,
78
95
  )
96
+ for destination in tripped:
97
+ self._disable_destination(destination)
79
98
 
80
99
  def _ensure_destinations(self) -> list[LogDestination]:
81
100
  if not self.configured:
@@ -87,6 +106,24 @@ class LogDestinationManager:
87
106
  self.on_failure("logging.destination_config", exc)
88
107
  return self.destinations
89
108
 
109
+ def _disable_destination(self, destination: LogDestination) -> None:
110
+ self.destinations = [
111
+ existing
112
+ for existing in self.destinations
113
+ if existing is not destination
114
+ ]
115
+ self._consecutive_failures.pop(id(destination), None)
116
+ self.on_failure(
117
+ "logging.destination_disabled",
118
+ RuntimeError(
119
+ "Disabling observability log destination after "
120
+ f"{DESTINATION_FAILURE_LIMIT} consecutive emit failures."
121
+ ),
122
+ destination=getattr(destination, "name", None),
123
+ )
124
+ if not self.destinations:
125
+ self.destinations.append(self._stdout_destination())
126
+
90
127
  def _build_destination(self, destination_name: str) -> LogDestination:
91
128
  normalized = destination_name.strip().lower().replace("-", "_")
92
129
  if normalized == "stdout":
@@ -1,17 +1,17 @@
1
1
  [project]
2
2
  name = "policyengine-observability"
3
- version = "1.3.0"
3
+ version = "1.3.2"
4
4
  description = "Shared PolicyEngine observability runtime for logs, timings, metrics, and OpenTelemetry."
5
5
  readme = "README.md"
6
6
  authors = [{ name = "PolicyEngine", email = "hello@policyengine.org" }]
7
7
  requires-python = ">=3.12"
8
8
  dependencies = [
9
- "opentelemetry-api",
10
- "opentelemetry-sdk",
11
- "opentelemetry-exporter-otlp-proto-grpc",
12
- "opentelemetry-exporter-otlp-proto-http",
13
- "opentelemetry-instrumentation-fastapi",
14
- "opentelemetry-instrumentation-httpx",
9
+ "opentelemetry-api>=1.43.0",
10
+ "opentelemetry-sdk>=1.43.0",
11
+ "opentelemetry-exporter-otlp-proto-grpc>=1.43.0",
12
+ "opentelemetry-exporter-otlp-proto-http>=1.43.0",
13
+ "opentelemetry-instrumentation-fastapi>=0.64b0",
14
+ "opentelemetry-instrumentation-httpx>=0.64b0",
15
15
  ]
16
16
 
17
17
  [project.optional-dependencies]
@@ -0,0 +1,210 @@
1
+ from __future__ import annotations
2
+
3
+ from policyengine_observability.destinations import (
4
+ GoogleCloudLoggingDestination,
5
+ google_cloud_logging,
6
+ normalize_payload,
7
+ )
8
+
9
+
10
+ class Unprintable:
11
+ def __str__(self) -> str:
12
+ raise RuntimeError("cannot stringify")
13
+
14
+
15
+ class FakeLogger:
16
+ def __init__(self) -> None:
17
+ self.calls = []
18
+
19
+ def log_struct(self, payload, **kwargs) -> None:
20
+ self.calls.append((payload, kwargs))
21
+
22
+
23
+ class FakeClient:
24
+ def __init__(self) -> None:
25
+ self.project = "resolved-project"
26
+ self.fake_logger = FakeLogger()
27
+ self.log_names = []
28
+
29
+ def logger(self, log_name: str) -> FakeLogger:
30
+ self.log_names.append(log_name)
31
+ return self.fake_logger
32
+
33
+
34
+ def test_normalize_payload_recursively_stringifies_unsafe_values() -> None:
35
+ normalized = normalize_payload(
36
+ {
37
+ "keep": "value",
38
+ "drop_none": None,
39
+ "bytes": b"value",
40
+ "list": [1, Unprintable()],
41
+ "nested": {"object": object()},
42
+ }
43
+ )
44
+
45
+ assert normalized["keep"] == "value"
46
+ assert normalized["drop_none"] is None
47
+ assert normalized["bytes"] == "value"
48
+ assert normalized["list"] == [1, "<unprintable Unprintable>"]
49
+ assert normalized["nested"]["object"].startswith("<object object at ")
50
+
51
+
52
+ def test_google_destination_writes_structured_log_with_bounded_labels(
53
+ monkeypatch,
54
+ ) -> None:
55
+ monkeypatch.setattr(
56
+ google_cloud_logging,
57
+ "load_google_credentials",
58
+ lambda *, prefer_workload_identity: None,
59
+ )
60
+ monkeypatch.setattr(
61
+ google_cloud_logging,
62
+ "configure_google_application_credentials",
63
+ lambda: None,
64
+ )
65
+ client = FakeClient()
66
+ destination = GoogleCloudLoggingDestination(
67
+ project=None,
68
+ log_name="policyengine-observability",
69
+ client_factory=lambda _project, _credentials: client,
70
+ )
71
+
72
+ destination.emit(
73
+ {
74
+ "schema_version": "policyengine.observability.request.v1",
75
+ "service_name": "svc",
76
+ "service_role": "api",
77
+ "environment": "production",
78
+ "request_id": "request-1",
79
+ "trace_id": "abc123",
80
+ "span_id": "def456",
81
+ "path": "/calculate",
82
+ "object": object(),
83
+ },
84
+ log_type="request",
85
+ severity="ERROR",
86
+ )
87
+
88
+ payload, kwargs = client.fake_logger.calls[0]
89
+ assert client.log_names == ["policyengine-observability"]
90
+ assert payload["object"].startswith("<object object at ")
91
+ assert kwargs["severity"] == "ERROR"
92
+ assert kwargs["trace"] == "projects/resolved-project/traces/abc123"
93
+ assert kwargs["span_id"] == "def456"
94
+ assert kwargs["labels"] == {
95
+ "log_type": "request",
96
+ "service_name": "svc",
97
+ "service_role": "api",
98
+ "environment": "production",
99
+ "schema_version": "policyengine.observability.request.v1",
100
+ }
101
+ assert "request_id" not in kwargs["labels"]
102
+ assert "path" not in kwargs["labels"]
103
+
104
+
105
+ # ── Destination circuit breaker ──────────────────────────────────────────
106
+
107
+
108
+ class FlakyDestination:
109
+ name = "flaky"
110
+
111
+ def __init__(self, fail_first: int | None = None) -> None:
112
+ self.calls = 0
113
+ self.fail_first = fail_first
114
+
115
+ def emit(self, payload, *, log_type, severity) -> None:
116
+ self.calls += 1
117
+ if self.fail_first is None or self.calls <= self.fail_first:
118
+ raise RuntimeError("emit failed")
119
+
120
+
121
+ class RecordingDestination:
122
+ name = "recording"
123
+
124
+ def __init__(self) -> None:
125
+ self.payloads = []
126
+
127
+ def emit(self, payload, *, log_type, severity) -> None:
128
+ self.payloads.append(payload)
129
+
130
+
131
+ def _manager(destinations):
132
+ import json
133
+ import logging
134
+
135
+ from policyengine_observability.config import ObservabilityConfig
136
+ from policyengine_observability.destinations.manager import (
137
+ LogDestinationManager,
138
+ )
139
+
140
+ failures = []
141
+ manager = LogDestinationManager(
142
+ config=ObservabilityConfig(),
143
+ loggers={"event": logging.getLogger("test-destinations")},
144
+ serializer=json.dumps,
145
+ on_failure=lambda operation, exc, **fields: failures.append(
146
+ (operation, fields)
147
+ ),
148
+ )
149
+ manager.destinations = list(destinations)
150
+ manager.configured = True
151
+ return manager, failures
152
+
153
+
154
+ def test_destination_disabled_after_consecutive_emit_failures() -> None:
155
+ from policyengine_observability.destinations.manager import (
156
+ DESTINATION_FAILURE_LIMIT,
157
+ )
158
+
159
+ flaky = FlakyDestination()
160
+ healthy = RecordingDestination()
161
+ manager, failures = _manager([flaky, healthy])
162
+
163
+ for _ in range(DESTINATION_FAILURE_LIMIT + 2):
164
+ manager.emit({"event": "x"}, log_type="event", severity="INFO")
165
+
166
+ assert flaky.calls == DESTINATION_FAILURE_LIMIT
167
+ assert flaky not in manager.destinations
168
+ assert len(healthy.payloads) == DESTINATION_FAILURE_LIMIT + 2
169
+ assert any(op == "logging.destination_disabled" for op, _ in failures)
170
+
171
+
172
+ def test_emit_success_resets_the_failure_counter() -> None:
173
+ from policyengine_observability.destinations.manager import (
174
+ DESTINATION_FAILURE_LIMIT,
175
+ )
176
+
177
+ flaky = FlakyDestination(fail_first=DESTINATION_FAILURE_LIMIT - 1)
178
+ manager, failures = _manager([flaky])
179
+
180
+ for _ in range(DESTINATION_FAILURE_LIMIT + 2):
181
+ manager.emit({"event": "x"}, log_type="event", severity="INFO")
182
+
183
+ assert flaky in manager.destinations
184
+ counts = [
185
+ fields["consecutive_failures"]
186
+ for op, fields in failures
187
+ if op == "logging.destination_emit"
188
+ ]
189
+ assert max(counts) == DESTINATION_FAILURE_LIMIT - 1
190
+
191
+
192
+ def test_sole_disabled_destination_falls_back_to_stdout() -> None:
193
+ from policyengine_observability.destinations.manager import (
194
+ DESTINATION_FAILURE_LIMIT,
195
+ )
196
+ from policyengine_observability.destinations.stdout import (
197
+ StdoutJsonDestination,
198
+ )
199
+
200
+ flaky = FlakyDestination()
201
+ manager, failures = _manager([flaky])
202
+
203
+ for _ in range(DESTINATION_FAILURE_LIMIT + 1):
204
+ manager.emit({"event": "x"}, log_type="event", severity="INFO")
205
+
206
+ assert flaky not in manager.destinations
207
+ assert any(
208
+ isinstance(destination, StdoutJsonDestination)
209
+ for destination in manager.destinations
210
+ )
@@ -707,31 +707,31 @@ wheels = [
707
707
 
708
708
  [[package]]
709
709
  name = "opentelemetry-api"
710
- version = "1.42.1"
710
+ version = "1.43.0"
711
711
  source = { registry = "https://pypi.org/simple" }
712
712
  dependencies = [
713
713
  { name = "typing-extensions" },
714
714
  ]
715
- sdist = { url = "https://files.pythonhosted.org/packages/b4/1c/125e1c936c0873796771b7f04f6c93b9f1bf5d424cea90fda94a99f61da8/opentelemetry_api-1.42.1.tar.gz", hash = "sha256:56c63bea9f77b62856be8c47600474acad853b2924b99b1687c4cb6297166716", size = 72296, upload-time = "2026-05-21T16:32:49.335Z" }
715
+ sdist = { url = "https://files.pythonhosted.org/packages/ae/cc/e4c9584181f86494df0f6bdec1a4f3280c50db44704dc2a407e994fc87bb/opentelemetry_api-1.43.0.tar.gz", hash = "sha256:107d0d03857ea8fc7c5fcbbbd83f800c281f0d560553d61c1d675fccfd1761c1", size = 73476, upload-time = "2026-06-24T15:19:55.323Z" }
716
716
  wheels = [
717
- { url = "https://files.pythonhosted.org/packages/a3/ca/9520cc1f3dfbbd03ac5903bbf55833e257bc64b1cf30fa8b0d6df374d821/opentelemetry_api-1.42.1-py3-none-any.whl", hash = "sha256:51a69edacadbc03a8950ace1c4c21099cacc538820ac2c9e36277e78cebba714", size = 61311, upload-time = "2026-05-21T16:32:28.822Z" },
717
+ { url = "https://files.pythonhosted.org/packages/17/83/6dba32b85f31868400440dc7ad2ca1eab94cbbf3a7b0459ed39f8311a9e2/opentelemetry_api-1.43.0-py3-none-any.whl", hash = "sha256:20acf45e9b21851926835292e4045d290acade1edd2ff3de86d2f069687ba1fd", size = 61912, upload-time = "2026-06-24T15:19:35.434Z" },
718
718
  ]
719
719
 
720
720
  [[package]]
721
721
  name = "opentelemetry-exporter-otlp-proto-common"
722
- version = "1.42.1"
722
+ version = "1.43.0"
723
723
  source = { registry = "https://pypi.org/simple" }
724
724
  dependencies = [
725
725
  { name = "opentelemetry-proto" },
726
726
  ]
727
- sdist = { url = "https://files.pythonhosted.org/packages/0e/9c/216acfeaedadf2e1937f4373929b20f73197c5c4a2546d4f584b7fa63813/opentelemetry_exporter_otlp_proto_common-1.42.1.tar.gz", hash = "sha256:04f1f01fb597c4249dfcd7f8b861c902c2102369d376d9d346ff38de4469a2ee", size = 21433, upload-time = "2026-05-21T16:32:55.526Z" }
727
+ sdist = { url = "https://files.pythonhosted.org/packages/55/c1/e8098490ab15abf116dcaf9fa89ededcb35547c7d08d4b5a62f573dc1e63/opentelemetry_exporter_otlp_proto_common-1.43.0.tar.gz", hash = "sha256:c4e32ba6d6b13bdb2b8f6764c4fd28d00192826561aa04f6d14eedfce7ac076f", size = 20197, upload-time = "2026-06-24T15:20:00.247Z" }
728
728
  wheels = [
729
- { url = "https://files.pythonhosted.org/packages/d6/43/2375e7612e1121a4518c17603b6e0b03ad94f565aafad53f464dc5be2bf6/opentelemetry_exporter_otlp_proto_common-1.42.1-py3-none-any.whl", hash = "sha256:f48d395ab815b444da118868977e9798ea354c25737d5cf39578ae894011c140", size = 17327, upload-time = "2026-05-21T16:32:33.387Z" },
729
+ { url = "https://files.pythonhosted.org/packages/d0/b2/41ebc74ae1d5859901f1b69305de58724bf043381103d6ef413521cbc35a/opentelemetry_exporter_otlp_proto_common-1.43.0-py3-none-any.whl", hash = "sha256:123c3f9cc87218562490c63b36f497bf3a722faf174a515d1443f31ababa6264", size = 17048, upload-time = "2026-06-24T15:19:41.264Z" },
730
730
  ]
731
731
 
732
732
  [[package]]
733
733
  name = "opentelemetry-exporter-otlp-proto-grpc"
734
- version = "1.42.1"
734
+ version = "1.43.0"
735
735
  source = { registry = "https://pypi.org/simple" }
736
736
  dependencies = [
737
737
  { name = "googleapis-common-protos" },
@@ -742,14 +742,14 @@ dependencies = [
742
742
  { name = "opentelemetry-sdk" },
743
743
  { name = "typing-extensions" },
744
744
  ]
745
- sdist = { url = "https://files.pythonhosted.org/packages/87/87/ca7fc790dfdbcf4f9e9aab14a39ef1b7508ead13707e283de0b3131478d2/opentelemetry_exporter_otlp_proto_grpc-1.42.1.tar.gz", hash = "sha256:975c4461f167dd8ed8857d68d3b6b25f3d272eab896f6a9470d0f5b90e2faf15", size = 27140, upload-time = "2026-05-21T16:32:56.162Z" }
745
+ sdist = { url = "https://files.pythonhosted.org/packages/e2/1d/6336453716ca0a240d4417d19e6d5b77a5e7163e5670ec4f7ec4d3ede7bf/opentelemetry_exporter_otlp_proto_grpc-1.43.0.tar.gz", hash = "sha256:1b3e0627daa9bc21884d4a13946807c255eb558bfe5bdd543dffb6f4c9faee0d", size = 27213, upload-time = "2026-06-24T15:20:00.907Z" }
746
746
  wheels = [
747
- { url = "https://files.pythonhosted.org/packages/89/2b/28ba5b128f47fe8c3bab541000d6feb4b5a9bd26623ca013406f01c0fb60/opentelemetry_exporter_otlp_proto_grpc-1.42.1-py3-none-any.whl", hash = "sha256:0ae1177e2038b18a929b3098215243631ef91136cba26b7e2b12790ceb7e87cc", size = 19617, upload-time = "2026-05-21T16:32:34.278Z" },
747
+ { url = "https://files.pythonhosted.org/packages/6d/74/2700b5d5c946bf2dba87073fce3dfc198c46bc92ea3d5693f54bc51c90b1/opentelemetry_exporter_otlp_proto_grpc-1.43.0-py3-none-any.whl", hash = "sha256:6a10d1feacffffda19acacbf277b736094b1e2f4dbb98c90ccb2c6e1962e2ec6", size = 19626, upload-time = "2026-06-24T15:19:42.233Z" },
748
748
  ]
749
749
 
750
750
  [[package]]
751
751
  name = "opentelemetry-exporter-otlp-proto-http"
752
- version = "1.42.1"
752
+ version = "1.43.0"
753
753
  source = { registry = "https://pypi.org/simple" }
754
754
  dependencies = [
755
755
  { name = "googleapis-common-protos" },
@@ -760,14 +760,14 @@ dependencies = [
760
760
  { name = "requests" },
761
761
  { name = "typing-extensions" },
762
762
  ]
763
- sdist = { url = "https://files.pythonhosted.org/packages/77/32/826bfa1d80ecea24f47808de03cd4a0d13c17ecc07712f45123f0f61e4ac/opentelemetry_exporter_otlp_proto_http-1.42.1.tar.gz", hash = "sha256:bf142a21035d7571ac3a09cb2e5639f49886f243972883cfe777ed3bf02b734d", size = 25406, upload-time = "2026-05-21T16:32:56.807Z" }
763
+ sdist = { url = "https://files.pythonhosted.org/packages/fc/92/0b9f56412483a8891d4843890294796c9df8ab42417bd9bad8035d840cb3/opentelemetry_exporter_otlp_proto_http-1.43.0.tar.gz", hash = "sha256:fa8a42bb7d00ee5391f4c0b04d8e6a46c03caa437903296ab73a81dc11ba118f", size = 25406, upload-time = "2026-06-24T15:20:01.515Z" }
764
764
  wheels = [
765
- { url = "https://files.pythonhosted.org/packages/d3/96/82cb223a1502f0787d4bbff12907f5f8d870a50731febcd5818d93ef9555/opentelemetry_exporter_otlp_proto_http-1.42.1-py3-none-any.whl", hash = "sha256:00a16da1b312a1d6c7233d600d557c91df71125af73020f3b9a7765bd699d59d", size = 21793, upload-time = "2026-05-21T16:32:35.277Z" },
765
+ { url = "https://files.pythonhosted.org/packages/b3/20/b685ed7af2e17c29ffc8af56f1fa8bc2033258fc30fb0d2b722f49d13ba0/opentelemetry_exporter_otlp_proto_http-1.43.0-py3-none-any.whl", hash = "sha256:647f603aa8efdbdb4dbff842e0729d0406a6fff26b295a72d3d60e7d963b2610", size = 21795, upload-time = "2026-06-24T15:19:43.164Z" },
766
766
  ]
767
767
 
768
768
  [[package]]
769
769
  name = "opentelemetry-instrumentation"
770
- version = "0.63b1"
770
+ version = "0.64b0"
771
771
  source = { registry = "https://pypi.org/simple" }
772
772
  dependencies = [
773
773
  { name = "opentelemetry-api" },
@@ -775,14 +775,14 @@ dependencies = [
775
775
  { name = "packaging" },
776
776
  { name = "wrapt" },
777
777
  ]
778
- sdist = { url = "https://files.pythonhosted.org/packages/da/6d/4de72d97ff54db1ed270c7a59c9b904b917c0ac7af429c086c388b824ddb/opentelemetry_instrumentation-0.63b1.tar.gz", hash = "sha256:32368d6ae52c8de20aa790a6ad86b10a76f09956092337ae37d675773990e541", size = 41081, upload-time = "2026-05-21T16:36:14.206Z" }
778
+ sdist = { url = "https://files.pythonhosted.org/packages/7e/97/02fe6e1c8b1ffac42d0b429c18080edb24e0e0d18c86612edf72b5752382/opentelemetry_instrumentation-0.64b0.tar.gz", hash = "sha256:b47d528dead6271d7743114417eb67fc915bd9258111c48dbf9a4951d2efa88d", size = 41935, upload-time = "2026-06-24T15:19:12.951Z" }
779
779
  wheels = [
780
- { url = "https://files.pythonhosted.org/packages/35/a1/9314e621c143e4d82a5bf7a43c2ff7a745d31023506336857607c8c543cc/opentelemetry_instrumentation-0.63b1-py3-none-any.whl", hash = "sha256:f1986716d52cc316ea5f60189098726a9071d8ecc0eee96c9ed110be08bade9c", size = 35577, upload-time = "2026-05-21T16:34:56.818Z" },
780
+ { url = "https://files.pythonhosted.org/packages/d2/0c/cb9fe342de5299c7af24582eb7d788661cc53a1c4b904da92309caaa9417/opentelemetry_instrumentation-0.64b0-py3-none-any.whl", hash = "sha256:133ab7ffca796557aec059bf6be3190a34b6dea987f25be3d9409e230cbdad8b", size = 35880, upload-time = "2026-06-24T15:18:17.277Z" },
781
781
  ]
782
782
 
783
783
  [[package]]
784
784
  name = "opentelemetry-instrumentation-asgi"
785
- version = "0.63b1"
785
+ version = "0.64b0"
786
786
  source = { registry = "https://pypi.org/simple" }
787
787
  dependencies = [
788
788
  { name = "asgiref" },
@@ -791,14 +791,14 @@ dependencies = [
791
791
  { name = "opentelemetry-semantic-conventions" },
792
792
  { name = "opentelemetry-util-http" },
793
793
  ]
794
- sdist = { url = "https://files.pythonhosted.org/packages/a0/b5/7ea3a9fd1b80e89786c14250bfaecf32a753c3fd08232690f4da8dc16e29/opentelemetry_instrumentation_asgi-0.63b1.tar.gz", hash = "sha256:267b422416d768f3c7f4054883b41d9c3a7c943d86d20032b738c99a3dbb5862", size = 26151, upload-time = "2026-05-21T16:36:18.368Z" }
794
+ sdist = { url = "https://files.pythonhosted.org/packages/85/0c/71c696fccb86d37af383ea1604af4729fabad0af2fabaf203e4c79c1e859/opentelemetry_instrumentation_asgi-0.64b0.tar.gz", hash = "sha256:4dd3eee566a4303f8e6b9b84f2a0a7abc57a6640df768926c68a3868bf5b2090", size = 26136, upload-time = "2026-06-24T15:19:17.003Z" }
795
795
  wheels = [
796
- { url = "https://files.pythonhosted.org/packages/57/7e/83986f27b421de04fab1e1a84e892621dac42e6432a9c66779505f4d1381/opentelemetry_instrumentation_asgi-0.63b1-py3-none-any.whl", hash = "sha256:1a22453dfa965f14799b10a674b8acbcb897a8a75c79136060af54214cc7886e", size = 15906, upload-time = "2026-05-21T16:35:04.162Z" },
796
+ { url = "https://files.pythonhosted.org/packages/51/20/218b65a63d847a7ed28d1bea84c39234689160b74480b8702272e37f4240/opentelemetry_instrumentation_asgi-0.64b0-py3-none-any.whl", hash = "sha256:e0840b66e15303a9254b0540946010bd008aa0504f4d89b8e1b7fb63490a36f0", size = 15906, upload-time = "2026-06-24T15:18:23.107Z" },
797
797
  ]
798
798
 
799
799
  [[package]]
800
800
  name = "opentelemetry-instrumentation-fastapi"
801
- version = "0.63b1"
801
+ version = "0.64b0"
802
802
  source = { registry = "https://pypi.org/simple" }
803
803
  dependencies = [
804
804
  { name = "opentelemetry-api" },
@@ -807,14 +807,14 @@ dependencies = [
807
807
  { name = "opentelemetry-semantic-conventions" },
808
808
  { name = "opentelemetry-util-http" },
809
809
  ]
810
- sdist = { url = "https://files.pythonhosted.org/packages/32/d6/0c128fac2e34b7d526a8d3c6edc45b875a97f8a987861b00511151b6337d/opentelemetry_instrumentation_fastapi-0.63b1.tar.gz", hash = "sha256:cc42dff56c96d0a2921510c4abab2a4c2e27fe64b26dc1254727fb550df100ba", size = 25387, upload-time = "2026-05-21T16:36:32.071Z" }
810
+ sdist = { url = "https://files.pythonhosted.org/packages/db/a1/52282e2cc5c08f4df13b087896d9907258fe2ff4f34035d3b21aa92684c3/opentelemetry_instrumentation_fastapi-0.64b0.tar.gz", hash = "sha256:05f75149929e433c1630de381688e650bf651c1e1cce7f9a7b649a807dac8a98", size = 26236, upload-time = "2026-06-24T15:19:27.219Z" }
811
811
  wheels = [
812
- { url = "https://files.pythonhosted.org/packages/b1/3d/2eae63f13f36d7a8ab5bf03d06ecaf169c2069b524547f24947be6d92094/opentelemetry_instrumentation_fastapi-0.63b1-py3-none-any.whl", hash = "sha256:52ee2cde9a2ac094bdd45d79f85860e03a972928a2553006071fe61d94cf7281", size = 12795, upload-time = "2026-05-21T16:35:28.68Z" },
812
+ { url = "https://files.pythonhosted.org/packages/72/2d/4f48dc2d6f289f2febc5b871940dbea9f3b04ab9186d23342581b49cb984/opentelemetry_instrumentation_fastapi-0.64b0-py3-none-any.whl", hash = "sha256:43cbbfb2d3079dc81104478a2950ae93ac6d0e90a5020fa3987a236f8f2bdef1", size = 13263, upload-time = "2026-06-24T15:18:38.553Z" },
813
813
  ]
814
814
 
815
815
  [[package]]
816
816
  name = "opentelemetry-instrumentation-httpx"
817
- version = "0.63b1"
817
+ version = "0.64b0"
818
818
  source = { registry = "https://pypi.org/simple" }
819
819
  dependencies = [
820
820
  { name = "opentelemetry-api" },
@@ -823,57 +823,57 @@ dependencies = [
823
823
  { name = "opentelemetry-util-http" },
824
824
  { name = "wrapt" },
825
825
  ]
826
- sdist = { url = "https://files.pythonhosted.org/packages/02/27/c2b4335bca030e893acbe5ff2b4f434868773bf94508be7e6bf5af981b24/opentelemetry_instrumentation_httpx-0.63b1.tar.gz", hash = "sha256:f41ec82f25c3abcdada621052db3e5fd648e3b43d55eec4b9c0c5d3ecb7b4ff4", size = 23557, upload-time = "2026-05-21T16:36:34.583Z" }
826
+ sdist = { url = "https://files.pythonhosted.org/packages/0d/2a/2893a8781b93894f1e8014904c0342da7e4302de6597c2d5c0cb6c1a552e/opentelemetry_instrumentation_httpx-0.64b0.tar.gz", hash = "sha256:c2cfcd03d3665762860ebd0c28038c6e47fbb48d7942dec31dd75fc634d25c92", size = 23555, upload-time = "2026-06-24T15:19:29.107Z" }
827
827
  wheels = [
828
- { url = "https://files.pythonhosted.org/packages/ba/b8/f536780996195c3b9f2354998554671e05a7a262df8c043f63fe9e5a6f0b/opentelemetry_instrumentation_httpx-0.63b1-py3-none-any.whl", hash = "sha256:14df6e99d81be9a8cd238f6639b6fa52404c4d3ce219058fcb5dc8c0f2211f86", size = 16336, upload-time = "2026-05-21T16:35:32.221Z" },
828
+ { url = "https://files.pythonhosted.org/packages/06/29/a20309bd3f5a8051b61ca475e78623410c3b077e3deccae19aa4f8b5b9a2/opentelemetry_instrumentation_httpx-0.64b0-py3-none-any.whl", hash = "sha256:04829e5723941b5ceb0c88b44d63983e226b5c75b2b2e34a57739fdd0e060608", size = 16336, upload-time = "2026-06-24T15:18:41.412Z" },
829
829
  ]
830
830
 
831
831
  [[package]]
832
832
  name = "opentelemetry-proto"
833
- version = "1.42.1"
833
+ version = "1.43.0"
834
834
  source = { registry = "https://pypi.org/simple" }
835
835
  dependencies = [
836
836
  { name = "protobuf" },
837
837
  ]
838
- sdist = { url = "https://files.pythonhosted.org/packages/b4/55/63eac3e1089b768ba014091fdd2ae8a9a440c821ef5e2b786909c94c8836/opentelemetry_proto-1.42.1.tar.gz", hash = "sha256:c6a51e6b4f05ae63565f3a113217f3d2bfaec68f78c02d7a6c85f9010d1cfca6", size = 45839, upload-time = "2026-05-21T16:33:03.937Z" }
838
+ sdist = { url = "https://files.pythonhosted.org/packages/e0/b9/d357faefb40bda1d4799913e6af611171ff22a2dedcb93576bc92242d056/opentelemetry_proto-1.43.0.tar.gz", hash = "sha256:224778df17e1f3fafeaaa21d874236ca5f6ffc2f86e0899298ec7351aac27924", size = 46481, upload-time = "2026-06-24T15:20:07.625Z" }
839
839
  wheels = [
840
- { url = "https://files.pythonhosted.org/packages/41/9d/171c02c84a76940b7e601805b3bb536985aded9168fbcc9ba52f0a730fa2/opentelemetry_proto-1.42.1-py3-none-any.whl", hash = "sha256:dedb74cba2886c59c7789b227a7a670613025a07489040050aedff6e5c0fb43c", size = 71782, upload-time = "2026-05-21T16:32:44.867Z" },
840
+ { url = "https://files.pythonhosted.org/packages/ed/a7/3e5308cf548b8f72529c7db1afdb3a404211982376a12927fd7759f77bf3/opentelemetry_proto-1.43.0-py3-none-any.whl", hash = "sha256:c58f1f7ef84bc7dc2834016c0c37fe0081dde7ca9f6339be1970fbf9cdaaa90d", size = 72489, upload-time = "2026-06-24T15:19:51.164Z" },
841
841
  ]
842
842
 
843
843
  [[package]]
844
844
  name = "opentelemetry-sdk"
845
- version = "1.42.1"
845
+ version = "1.43.0"
846
846
  source = { registry = "https://pypi.org/simple" }
847
847
  dependencies = [
848
848
  { name = "opentelemetry-api" },
849
849
  { name = "opentelemetry-semantic-conventions" },
850
850
  { name = "typing-extensions" },
851
851
  ]
852
- sdist = { url = "https://files.pythonhosted.org/packages/40/f7/b390bd9bfd703bf98a68fea1f27786c6872331fd617164a54b8a59bdc008/opentelemetry_sdk-1.42.1.tar.gz", hash = "sha256:8c834e8f8c9ba4171d4ec843d0cb8a67e4c7394d3f9e9297e582cbd9456ddbf7", size = 239262, upload-time = "2026-05-21T16:33:04.641Z" }
852
+ sdist = { url = "https://files.pythonhosted.org/packages/3e/eb/5041074274ac0956b03637cc039d434569112468e875eddfcc9a0674ce06/opentelemetry_sdk-1.43.0.tar.gz", hash = "sha256:d8187c81c162df9913e4003dd6485f7390d9a24fc17026ec7387b8b8218b08e9", size = 254744, upload-time = "2026-06-24T15:20:08.467Z" }
853
853
  wheels = [
854
- { url = "https://files.pythonhosted.org/packages/8f/6b/4287766cfbde577ae2272e8884abac325aeaac0d64f41c61d5b8cc595105/opentelemetry_sdk-1.42.1-py3-none-any.whl", hash = "sha256:083cd4bbfaa5aa7b5a9e552430d9951219967cfb27aa61feb13a77aba1fc839d", size = 170907, upload-time = "2026-05-21T16:32:45.894Z" },
854
+ { url = "https://files.pythonhosted.org/packages/49/e3/b17be23af124201c9f52eececd4cc8ddfed1597d37b4ee771895d325805c/opentelemetry_sdk-1.43.0-py3-none-any.whl", hash = "sha256:d1323a547c1ce69d6a069a17a44b7da82bb8b332051ecb074041f87642c86823", size = 178852, upload-time = "2026-06-24T15:19:52.169Z" },
855
855
  ]
856
856
 
857
857
  [[package]]
858
858
  name = "opentelemetry-semantic-conventions"
859
- version = "0.63b1"
859
+ version = "0.64b0"
860
860
  source = { registry = "https://pypi.org/simple" }
861
861
  dependencies = [
862
862
  { name = "opentelemetry-api" },
863
863
  { name = "typing-extensions" },
864
864
  ]
865
- sdist = { url = "https://files.pythonhosted.org/packages/93/99/4d7dd6df64795951413ce6e815f8cf1eb191daf7196ae86574589643d5f3/opentelemetry_semantic_conventions-0.63b1.tar.gz", hash = "sha256:3daf963611334b365e98a57438183eb012d3bfb40b2d931a9af613476b8701a9", size = 148340, upload-time = "2026-05-21T16:33:05.455Z" }
865
+ sdist = { url = "https://files.pythonhosted.org/packages/5a/30/5f26df29509eccd86b99b481ac9ffa39da49ba9577cc69071c552ae30447/opentelemetry_semantic_conventions-0.64b0.tar.gz", hash = "sha256:72f76fb2d1582d9d033dd1fcd84532e961e6ff3d90d24ba6fabc72975a83864c", size = 148340, upload-time = "2026-06-24T15:20:09.267Z" }
866
866
  wheels = [
867
- { url = "https://files.pythonhosted.org/packages/cb/7a/7fe66f5f3682b1dd47d88cc4e11f1c6c0966b737de2d16671146e23c39a5/opentelemetry_semantic_conventions-0.63b1-py3-none-any.whl", hash = "sha256:dfe5ef4dee82586b746f522b818ceb298d00b3d59f660042bd79404bff8d0682", size = 203713, upload-time = "2026-05-21T16:32:47.016Z" },
867
+ { url = "https://files.pythonhosted.org/packages/f2/ca/23ba87a221b574a7c5a99d48849d80bfe8b047624681357e2b002e566187/opentelemetry_semantic_conventions-0.64b0-py3-none-any.whl", hash = "sha256:ea77e85e354b8f604ddbe5f3d9135216f982fa4d77e5859ac30f6d8a50505aa6", size = 203713, upload-time = "2026-06-24T15:19:53.339Z" },
868
868
  ]
869
869
 
870
870
  [[package]]
871
871
  name = "opentelemetry-util-http"
872
- version = "0.63b1"
872
+ version = "0.64b0"
873
873
  source = { registry = "https://pypi.org/simple" }
874
- sdist = { url = "https://files.pythonhosted.org/packages/6c/d8/7bf5e4cec0578ac3c28c18eb7b88f34279139cbc8c568d6aa02b9c5ae53e/opentelemetry_util_http-0.63b1.tar.gz", hash = "sha256:ba1268f00922ee522dba2ae38458060f99486e7385a8056985901ca9685adfff", size = 11102, upload-time = "2026-05-21T16:36:56.675Z" }
874
+ sdist = { url = "https://files.pythonhosted.org/packages/c3/1b/1029a805fd7242f7dfce91633b244c3b14a94d703232878f71e01ce862b1/opentelemetry_util_http-0.64b0.tar.gz", hash = "sha256:8a86a220dbfc56d736f47f1e5c4e7932a21fcf69052312e1bcf166444dc79322", size = 11102, upload-time = "2026-06-24T15:19:48.974Z" }
875
875
  wheels = [
876
- { url = "https://files.pythonhosted.org/packages/e5/f1/34e047e8f6a3c67e5220acf1af7b9f62868c25d77791bca74457bd2180a6/opentelemetry_util_http-0.63b1-py3-none-any.whl", hash = "sha256:6284194028c59cd439f8acfe388145069a6127f11dc077e1344a2094adacc3f8", size = 8205, upload-time = "2026-05-21T16:36:09.736Z" },
876
+ { url = "https://files.pythonhosted.org/packages/1c/c7/5f8ec5b30546f2dc22cd5fc5759bce2ab5be6e89a2e710a405ac9ef64ed3/opentelemetry_util_http-0.64b0-py3-none-any.whl", hash = "sha256:c1e5350d25507c1afcd6076cf9ac062485a0a4f79cd9971366996fd3056bacdb", size = 8204, upload-time = "2026-06-24T15:19:09.02Z" },
877
877
  ]
878
878
 
879
879
  [[package]]
@@ -896,7 +896,7 @@ wheels = [
896
896
 
897
897
  [[package]]
898
898
  name = "policyengine-observability"
899
- version = "1.3.0"
899
+ version = "1.3.2"
900
900
  source = { editable = "." }
901
901
  dependencies = [
902
902
  { name = "opentelemetry-api" },
@@ -946,12 +946,12 @@ requires-dist = [
946
946
  { name = "google-cloud-logging", marker = "extra == 'google'", specifier = ">=3.15.0" },
947
947
  { name = "httpx", marker = "extra == 'all'" },
948
948
  { name = "httpx", marker = "extra == 'httpx'" },
949
- { name = "opentelemetry-api" },
950
- { name = "opentelemetry-exporter-otlp-proto-grpc" },
951
- { name = "opentelemetry-exporter-otlp-proto-http" },
952
- { name = "opentelemetry-instrumentation-fastapi" },
953
- { name = "opentelemetry-instrumentation-httpx" },
954
- { name = "opentelemetry-sdk" },
949
+ { name = "opentelemetry-api", specifier = ">=1.43.0" },
950
+ { name = "opentelemetry-exporter-otlp-proto-grpc", specifier = ">=1.43.0" },
951
+ { name = "opentelemetry-exporter-otlp-proto-http", specifier = ">=1.43.0" },
952
+ { name = "opentelemetry-instrumentation-fastapi", specifier = ">=0.64b0" },
953
+ { name = "opentelemetry-instrumentation-httpx", specifier = ">=0.64b0" },
954
+ { name = "opentelemetry-sdk", specifier = ">=1.43.0" },
955
955
  { name = "pytest", marker = "extra == 'dev'" },
956
956
  { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.9.0" },
957
957
  { name = "towncrier", marker = "extra == 'dev'", specifier = ">=24.8.0" },
@@ -1,102 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from policyengine_observability.destinations import (
4
- GoogleCloudLoggingDestination,
5
- google_cloud_logging,
6
- normalize_payload,
7
- )
8
-
9
-
10
- class Unprintable:
11
- def __str__(self) -> str:
12
- raise RuntimeError("cannot stringify")
13
-
14
-
15
- class FakeLogger:
16
- def __init__(self) -> None:
17
- self.calls = []
18
-
19
- def log_struct(self, payload, **kwargs) -> None:
20
- self.calls.append((payload, kwargs))
21
-
22
-
23
- class FakeClient:
24
- def __init__(self) -> None:
25
- self.project = "resolved-project"
26
- self.fake_logger = FakeLogger()
27
- self.log_names = []
28
-
29
- def logger(self, log_name: str) -> FakeLogger:
30
- self.log_names.append(log_name)
31
- return self.fake_logger
32
-
33
-
34
- def test_normalize_payload_recursively_stringifies_unsafe_values() -> None:
35
- normalized = normalize_payload(
36
- {
37
- "keep": "value",
38
- "drop_none": None,
39
- "bytes": b"value",
40
- "list": [1, Unprintable()],
41
- "nested": {"object": object()},
42
- }
43
- )
44
-
45
- assert normalized["keep"] == "value"
46
- assert normalized["drop_none"] is None
47
- assert normalized["bytes"] == "value"
48
- assert normalized["list"] == [1, "<unprintable Unprintable>"]
49
- assert normalized["nested"]["object"].startswith("<object object at ")
50
-
51
-
52
- def test_google_destination_writes_structured_log_with_bounded_labels(
53
- monkeypatch,
54
- ) -> None:
55
- monkeypatch.setattr(
56
- google_cloud_logging,
57
- "load_google_credentials",
58
- lambda *, prefer_workload_identity: None,
59
- )
60
- monkeypatch.setattr(
61
- google_cloud_logging,
62
- "configure_google_application_credentials",
63
- lambda: None,
64
- )
65
- client = FakeClient()
66
- destination = GoogleCloudLoggingDestination(
67
- project=None,
68
- log_name="policyengine-observability",
69
- client_factory=lambda _project, _credentials: client,
70
- )
71
-
72
- destination.emit(
73
- {
74
- "schema_version": "policyengine.observability.request.v1",
75
- "service_name": "svc",
76
- "service_role": "api",
77
- "environment": "production",
78
- "request_id": "request-1",
79
- "trace_id": "abc123",
80
- "span_id": "def456",
81
- "path": "/calculate",
82
- "object": object(),
83
- },
84
- log_type="request",
85
- severity="ERROR",
86
- )
87
-
88
- payload, kwargs = client.fake_logger.calls[0]
89
- assert client.log_names == ["policyengine-observability"]
90
- assert payload["object"].startswith("<object object at ")
91
- assert kwargs["severity"] == "ERROR"
92
- assert kwargs["trace"] == "projects/resolved-project/traces/abc123"
93
- assert kwargs["span_id"] == "def456"
94
- assert kwargs["labels"] == {
95
- "log_type": "request",
96
- "service_name": "svc",
97
- "service_role": "api",
98
- "environment": "production",
99
- "schema_version": "policyengine.observability.request.v1",
100
- }
101
- assert "request_id" not in kwargs["labels"]
102
- assert "path" not in kwargs["labels"]