policyengine-observability 3.0.1__tar.gz → 3.0.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.
- {policyengine_observability-3.0.1 → policyengine_observability-3.0.2}/CHANGELOG.md +7 -0
- {policyengine_observability-3.0.1 → policyengine_observability-3.0.2}/PKG-INFO +31 -4
- {policyengine_observability-3.0.1 → policyengine_observability-3.0.2}/README.md +30 -3
- {policyengine_observability-3.0.1 → policyengine_observability-3.0.2}/docs/engineering/skills/repository-guidance.md +2 -2
- {policyengine_observability-3.0.1 → policyengine_observability-3.0.2}/policyengine_observability/__init__.py +2 -0
- policyengine_observability-3.0.2/policyengine_observability/identity.py +36 -0
- {policyengine_observability-3.0.1 → policyengine_observability-3.0.2}/policyengine_observability/runtime.py +1 -0
- {policyengine_observability-3.0.1 → policyengine_observability-3.0.2}/pyproject.toml +1 -1
- policyengine_observability-3.0.2/tests/test_identity.py +38 -0
- {policyengine_observability-3.0.1 → policyengine_observability-3.0.2}/tests/test_runtime.py +56 -0
- {policyengine_observability-3.0.1 → policyengine_observability-3.0.2}/uv.lock +1 -1
- {policyengine_observability-3.0.1 → policyengine_observability-3.0.2}/.github/bump_version.py +0 -0
- {policyengine_observability-3.0.1 → policyengine_observability-3.0.2}/.github/check-changelog.sh +0 -0
- {policyengine_observability-3.0.1 → policyengine_observability-3.0.2}/.github/copilot-instructions.md +0 -0
- {policyengine_observability-3.0.1 → policyengine_observability-3.0.2}/.github/fetch_version.py +0 -0
- {policyengine_observability-3.0.1 → policyengine_observability-3.0.2}/.github/get-changelog-diff.sh +0 -0
- {policyengine_observability-3.0.1 → policyengine_observability-3.0.2}/.github/publish-git-tag.sh +0 -0
- {policyengine_observability-3.0.1 → policyengine_observability-3.0.2}/.github/workflows/pr.yml +0 -0
- {policyengine_observability-3.0.1 → policyengine_observability-3.0.2}/.github/workflows/push.yml +0 -0
- {policyengine_observability-3.0.1 → policyengine_observability-3.0.2}/.gitignore +0 -0
- {policyengine_observability-3.0.1 → policyengine_observability-3.0.2}/AGENTS.md +0 -0
- {policyengine_observability-3.0.1 → policyengine_observability-3.0.2}/CLAUDE.md +0 -0
- {policyengine_observability-3.0.1 → policyengine_observability-3.0.2}/LICENSE +0 -0
- {policyengine_observability-3.0.1 → policyengine_observability-3.0.2}/docs/engineering/skills/README.md +0 -0
- {policyengine_observability-3.0.1 → policyengine_observability-3.0.2}/docs/engineering/skills/github-prs.md +0 -0
- {policyengine_observability-3.0.1 → policyengine_observability-3.0.2}/policyengine_observability/adapters/__init__.py +0 -0
- {policyengine_observability-3.0.1 → policyengine_observability-3.0.2}/policyengine_observability/adapters/fastapi.py +0 -0
- {policyengine_observability-3.0.1 → policyengine_observability-3.0.2}/policyengine_observability/adapters/flask.py +0 -0
- {policyengine_observability-3.0.1 → policyengine_observability-3.0.2}/policyengine_observability/config.py +0 -0
- {policyengine_observability-3.0.1 → policyengine_observability-3.0.2}/policyengine_observability/delivery.py +0 -0
- {policyengine_observability-3.0.1 → policyengine_observability-3.0.2}/policyengine_observability/destinations/__init__.py +0 -0
- {policyengine_observability-3.0.1 → policyengine_observability-3.0.2}/policyengine_observability/destinations/base.py +0 -0
- {policyengine_observability-3.0.1 → policyengine_observability-3.0.2}/policyengine_observability/destinations/google_cloud.py +0 -0
- {policyengine_observability-3.0.1 → policyengine_observability-3.0.2}/policyengine_observability/destinations/stdout.py +0 -0
- {policyengine_observability-3.0.1 → policyengine_observability-3.0.2}/policyengine_observability/diagnostics.py +0 -0
- {policyengine_observability-3.0.1 → policyengine_observability-3.0.2}/policyengine_observability/google_auth.py +0 -0
- {policyengine_observability-3.0.1 → policyengine_observability-3.0.2}/policyengine_observability/google_credentials.py +0 -0
- {policyengine_observability-3.0.1 → policyengine_observability-3.0.2}/policyengine_observability/integrations/__init__.py +0 -0
- {policyengine_observability-3.0.1 → policyengine_observability-3.0.2}/policyengine_observability/integrations/httpx.py +0 -0
- {policyengine_observability-3.0.1 → policyengine_observability-3.0.2}/policyengine_observability/otel.py +0 -0
- {policyengine_observability-3.0.1 → policyengine_observability-3.0.2}/policyengine_observability/schema.py +0 -0
- {policyengine_observability-3.0.1 → policyengine_observability-3.0.2}/tests/conftest.py +0 -0
- {policyengine_observability-3.0.1 → policyengine_observability-3.0.2}/tests/test_adapters.py +0 -0
- {policyengine_observability-3.0.1 → policyengine_observability-3.0.2}/tests/test_config_schema.py +0 -0
- {policyengine_observability-3.0.1 → policyengine_observability-3.0.2}/tests/test_delivery.py +0 -0
- {policyengine_observability-3.0.1 → policyengine_observability-3.0.2}/tests/test_destination_strategies.py +0 -0
- {policyengine_observability-3.0.1 → policyengine_observability-3.0.2}/tests/test_diagnostics_public_api.py +0 -0
- {policyengine_observability-3.0.1 → policyengine_observability-3.0.2}/tests/test_google_credentials.py +0 -0
- {policyengine_observability-3.0.1 → policyengine_observability-3.0.2}/tests/test_otel.py +0 -0
- {policyengine_observability-3.0.1 → policyengine_observability-3.0.2}/tests/test_otel_destinations.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: policyengine-observability
|
|
3
|
-
Version: 3.0.
|
|
3
|
+
Version: 3.0.2
|
|
4
4
|
Summary: Shared PolicyEngine observability runtime for logs, timings, metrics, and OpenTelemetry.
|
|
5
5
|
Author-email: PolicyEngine <hello@policyengine.org>
|
|
6
6
|
License-File: LICENSE
|
|
@@ -374,15 +374,42 @@ def worker(payload, *, observability_context=None):
|
|
|
374
374
|
PolicyEngine request ID, and scalar attributes named by
|
|
375
375
|
`dispatch_attribute_keys`. Starting the remote operation restores only those
|
|
376
376
|
configured dispatch attributes. They remain available to nested
|
|
377
|
-
`capture_context()` calls and are attached to logs
|
|
378
|
-
the operation. They are never added to metric labels
|
|
379
|
-
included in `metric_attribute_keys`.
|
|
377
|
+
`capture_context()` calls and are attached to logs, nested operations, and
|
|
378
|
+
nested spans inside the operation. They are never added to metric labels
|
|
379
|
+
unless separately included in `metric_attribute_keys`.
|
|
380
380
|
|
|
381
381
|
Keep this context separate from the application payload. Invalid or stale
|
|
382
382
|
trace context can reduce correlation, but it does not prevent the observed
|
|
383
383
|
application code from running. A recent direct dispatch continues the trace;
|
|
384
384
|
delayed, retry, and aggregate work starts a trace linked to the dispatch span.
|
|
385
385
|
|
|
386
|
+
## Process identity
|
|
387
|
+
|
|
388
|
+
OpenTelemetry resource attributes require `service.instance.id` to identify
|
|
389
|
+
one telemetry-producing process. Deployment revisions identify code shared by
|
|
390
|
+
multiple containers and workers, so they must not be used alone as the process
|
|
391
|
+
identity. Construct the deployment identity after the application process has
|
|
392
|
+
started:
|
|
393
|
+
|
|
394
|
+
```python
|
|
395
|
+
import os
|
|
396
|
+
|
|
397
|
+
from policyengine_observability import DeploymentIdentity, process_instance_id
|
|
398
|
+
|
|
399
|
+
service_name = "example-api"
|
|
400
|
+
deployment = DeploymentIdentity(
|
|
401
|
+
environment="production",
|
|
402
|
+
platform="google_cloud_run",
|
|
403
|
+
region="us-central1",
|
|
404
|
+
instance_id=process_instance_id(service_name, os.getenv("K_REVISION")),
|
|
405
|
+
)
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
The helper returns one stable value for a service within the current process.
|
|
409
|
+
It combines the optional platform identifier with a process ID and random UUID,
|
|
410
|
+
so separate workers and containers cannot publish cumulative metrics under the
|
|
411
|
+
same resource identity. A child process receives a new value on its first call.
|
|
412
|
+
|
|
386
413
|
## Process restoration and shutdown
|
|
387
414
|
|
|
388
415
|
After a process image or memory snapshot is restored, rebuild process-local
|
|
@@ -328,15 +328,42 @@ def worker(payload, *, observability_context=None):
|
|
|
328
328
|
PolicyEngine request ID, and scalar attributes named by
|
|
329
329
|
`dispatch_attribute_keys`. Starting the remote operation restores only those
|
|
330
330
|
configured dispatch attributes. They remain available to nested
|
|
331
|
-
`capture_context()` calls and are attached to logs
|
|
332
|
-
the operation. They are never added to metric labels
|
|
333
|
-
included in `metric_attribute_keys`.
|
|
331
|
+
`capture_context()` calls and are attached to logs, nested operations, and
|
|
332
|
+
nested spans inside the operation. They are never added to metric labels
|
|
333
|
+
unless separately included in `metric_attribute_keys`.
|
|
334
334
|
|
|
335
335
|
Keep this context separate from the application payload. Invalid or stale
|
|
336
336
|
trace context can reduce correlation, but it does not prevent the observed
|
|
337
337
|
application code from running. A recent direct dispatch continues the trace;
|
|
338
338
|
delayed, retry, and aggregate work starts a trace linked to the dispatch span.
|
|
339
339
|
|
|
340
|
+
## Process identity
|
|
341
|
+
|
|
342
|
+
OpenTelemetry resource attributes require `service.instance.id` to identify
|
|
343
|
+
one telemetry-producing process. Deployment revisions identify code shared by
|
|
344
|
+
multiple containers and workers, so they must not be used alone as the process
|
|
345
|
+
identity. Construct the deployment identity after the application process has
|
|
346
|
+
started:
|
|
347
|
+
|
|
348
|
+
```python
|
|
349
|
+
import os
|
|
350
|
+
|
|
351
|
+
from policyengine_observability import DeploymentIdentity, process_instance_id
|
|
352
|
+
|
|
353
|
+
service_name = "example-api"
|
|
354
|
+
deployment = DeploymentIdentity(
|
|
355
|
+
environment="production",
|
|
356
|
+
platform="google_cloud_run",
|
|
357
|
+
region="us-central1",
|
|
358
|
+
instance_id=process_instance_id(service_name, os.getenv("K_REVISION")),
|
|
359
|
+
)
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
The helper returns one stable value for a service within the current process.
|
|
363
|
+
It combines the optional platform identifier with a process ID and random UUID,
|
|
364
|
+
so separate workers and containers cannot publish cumulative metrics under the
|
|
365
|
+
same resource identity. A child process receives a new value on its first call.
|
|
366
|
+
|
|
340
367
|
## Process restoration and shutdown
|
|
341
368
|
|
|
342
369
|
After a process image or memory snapshot is restored, rebuild process-local
|
|
@@ -67,8 +67,8 @@ uv run --extra dev towncrier check --compare-with origin/main
|
|
|
67
67
|
`remote_context`; do not insert it into business request models.
|
|
68
68
|
- Restore only attributes explicitly listed in `dispatch_attribute_keys`.
|
|
69
69
|
Those attributes must remain available to nested dispatches and structured
|
|
70
|
-
logs and must be attached to nested spans, but must not become
|
|
71
|
-
unless independently allowlisted in `metric_attribute_keys`.
|
|
70
|
+
logs and must be attached to nested operations and spans, but must not become
|
|
71
|
+
metric labels unless independently allowlisted in `metric_attribute_keys`.
|
|
72
72
|
- Accept explicitly supplied safe scalar attributes in local logs and spans by
|
|
73
73
|
default. Use `application_attribute_keys` only when a consumer requires a
|
|
74
74
|
strict local allowlist. Do not use that optional local policy to decide what
|
|
@@ -21,6 +21,7 @@ from .destinations import (
|
|
|
21
21
|
StdoutLogDestination,
|
|
22
22
|
)
|
|
23
23
|
from .google_auth import GoogleIdTokenAuth
|
|
24
|
+
from .identity import process_instance_id
|
|
24
25
|
from .integrations import instrument_httpx
|
|
25
26
|
from .runtime import (
|
|
26
27
|
REQUEST_ID_HEADER,
|
|
@@ -61,4 +62,5 @@ __all__ = [
|
|
|
61
62
|
"instrument_flask",
|
|
62
63
|
"instrument_httpx",
|
|
63
64
|
"instrument_logging",
|
|
65
|
+
"process_instance_id",
|
|
64
66
|
]
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Stable identities for individual telemetry-producing processes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from uuid import uuid4
|
|
7
|
+
|
|
8
|
+
_PROCESS_IDENTITIES: dict[tuple[int, str, str], str] = {}
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def process_instance_id(
|
|
12
|
+
service_name: str,
|
|
13
|
+
platform_instance_id: str | None = None,
|
|
14
|
+
) -> str:
|
|
15
|
+
"""Return a stable, unique identity for one service process.
|
|
16
|
+
|
|
17
|
+
Platform identifiers such as a Cloud Run revision identify deployed code,
|
|
18
|
+
not an individual process. The process ID distinguishes local workers and
|
|
19
|
+
the UUID distinguishes processes in separate containers that reuse the
|
|
20
|
+
same operating-system process ID. Including ``os.getpid()`` in the cache
|
|
21
|
+
key causes a forked child to receive a new identity on its first call.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
service = service_name.strip()
|
|
25
|
+
if not service:
|
|
26
|
+
raise ValueError("service_name must be non-empty")
|
|
27
|
+
platform_id = (platform_instance_id or "unassigned").strip()
|
|
28
|
+
if not platform_id:
|
|
29
|
+
platform_id = "unassigned"
|
|
30
|
+
process_id = os.getpid()
|
|
31
|
+
key = (process_id, service, platform_id)
|
|
32
|
+
existing = _PROCESS_IDENTITIES.get(key)
|
|
33
|
+
if existing is not None:
|
|
34
|
+
return existing
|
|
35
|
+
generated = f"{platform_id}:{process_id}:{uuid4().hex}"
|
|
36
|
+
return _PROCESS_IDENTITIES.setdefault(key, generated)
|
|
@@ -548,6 +548,7 @@ class ObservabilityRuntime:
|
|
|
548
548
|
if link is not None:
|
|
549
549
|
links.append(link)
|
|
550
550
|
parent = self._otel.empty_context()
|
|
551
|
+
safe = {**safe, **self._active_dispatch_attributes()}
|
|
551
552
|
active_request = self._request_state.get()
|
|
552
553
|
if request_id is None and active_request is not None:
|
|
553
554
|
request_id = active_request.request_id
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "policyengine-observability"
|
|
3
|
-
version = "3.0.
|
|
3
|
+
version = "3.0.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" }]
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from policyengine_observability import identity, process_instance_id
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_process_instance_id_is_stable_within_one_process() -> None:
|
|
11
|
+
first = process_instance_id("example-api", "revision-1")
|
|
12
|
+
second = process_instance_id("example-api", "revision-1")
|
|
13
|
+
|
|
14
|
+
assert first == second
|
|
15
|
+
assert re.fullmatch(r"revision-1:\d+:[0-9a-f]{32}", first)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_process_instance_id_separates_services_and_deployments() -> None:
|
|
19
|
+
first = process_instance_id("example-api", "revision-1")
|
|
20
|
+
|
|
21
|
+
assert process_instance_id("other-api", "revision-1") != first
|
|
22
|
+
assert process_instance_id("example-api", "revision-2") != first
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_process_instance_id_changes_after_process_duplication(
|
|
26
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
27
|
+
) -> None:
|
|
28
|
+
monkeypatch.setattr(identity.os, "getpid", lambda: 10_001)
|
|
29
|
+
parent = process_instance_id("example-api", "revision-1")
|
|
30
|
+
monkeypatch.setattr(identity.os, "getpid", lambda: 10_002)
|
|
31
|
+
child = process_instance_id("example-api", "revision-1")
|
|
32
|
+
|
|
33
|
+
assert parent != child
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_process_instance_id_rejects_empty_service_name() -> None:
|
|
37
|
+
with pytest.raises(ValueError, match="service_name must be non-empty"):
|
|
38
|
+
process_instance_id(" ")
|
|
@@ -270,6 +270,62 @@ def test_nested_span_inherits_active_dispatch_attributes(monkeypatch) -> None:
|
|
|
270
270
|
observed.shutdown()
|
|
271
271
|
|
|
272
272
|
|
|
273
|
+
def test_nested_operation_inherits_active_dispatch_attributes(
|
|
274
|
+
monkeypatch,
|
|
275
|
+
) -> None:
|
|
276
|
+
observability_id = "00000000-0000-4000-8000-000000000001"
|
|
277
|
+
observed, output = make_runtime(
|
|
278
|
+
application_attribute_keys=frozenset({"simulation_role"}),
|
|
279
|
+
dispatch_attribute_keys=frozenset({"observability_id"}),
|
|
280
|
+
)
|
|
281
|
+
operation_span_attributes = []
|
|
282
|
+
|
|
283
|
+
with observed.operation(
|
|
284
|
+
"simulation.run",
|
|
285
|
+
remote_context={
|
|
286
|
+
"captured_at": "not-a-date",
|
|
287
|
+
"observability_id": observability_id,
|
|
288
|
+
},
|
|
289
|
+
):
|
|
290
|
+
monkeypatch.setattr(
|
|
291
|
+
observed._otel,
|
|
292
|
+
"start_span",
|
|
293
|
+
lambda _name, **kwargs: operation_span_attributes.append(
|
|
294
|
+
kwargs["attributes"]
|
|
295
|
+
),
|
|
296
|
+
)
|
|
297
|
+
with observed.operation(
|
|
298
|
+
"simulation.plan",
|
|
299
|
+
attributes={
|
|
300
|
+
"simulation_role": "baseline",
|
|
301
|
+
"observability_id": "00000000-0000-4000-8000-000000000099",
|
|
302
|
+
},
|
|
303
|
+
):
|
|
304
|
+
assert observed.capture_context()["observability_id"] == (
|
|
305
|
+
observability_id
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
emitted = records(output)
|
|
309
|
+
nested_completion = next(
|
|
310
|
+
item
|
|
311
|
+
for item in emitted
|
|
312
|
+
if item.get("operation.name") == "simulation.plan"
|
|
313
|
+
)
|
|
314
|
+
assert nested_completion["attributes"] == {
|
|
315
|
+
"simulation_role": "baseline",
|
|
316
|
+
"observability_id": observability_id,
|
|
317
|
+
}
|
|
318
|
+
assert operation_span_attributes == [
|
|
319
|
+
{
|
|
320
|
+
"operation.name": "simulation.plan",
|
|
321
|
+
"operation.kind": "operation",
|
|
322
|
+
"simulation_role": "baseline",
|
|
323
|
+
"observability_id": observability_id,
|
|
324
|
+
}
|
|
325
|
+
]
|
|
326
|
+
observed.shutdown()
|
|
327
|
+
|
|
328
|
+
|
|
273
329
|
def test_local_operation_attributes_override_remote_dispatch_values() -> None:
|
|
274
330
|
observed, output = make_runtime(
|
|
275
331
|
dispatch_attribute_keys=frozenset({"job_id"})
|
{policyengine_observability-3.0.1 → policyengine_observability-3.0.2}/.github/bump_version.py
RENAMED
|
File without changes
|
{policyengine_observability-3.0.1 → policyengine_observability-3.0.2}/.github/check-changelog.sh
RENAMED
|
File without changes
|
|
File without changes
|
{policyengine_observability-3.0.1 → policyengine_observability-3.0.2}/.github/fetch_version.py
RENAMED
|
File without changes
|
{policyengine_observability-3.0.1 → policyengine_observability-3.0.2}/.github/get-changelog-diff.sh
RENAMED
|
File without changes
|
{policyengine_observability-3.0.1 → policyengine_observability-3.0.2}/.github/publish-git-tag.sh
RENAMED
|
File without changes
|
{policyengine_observability-3.0.1 → policyengine_observability-3.0.2}/.github/workflows/pr.yml
RENAMED
|
File without changes
|
{policyengine_observability-3.0.1 → policyengine_observability-3.0.2}/.github/workflows/push.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{policyengine_observability-3.0.1 → policyengine_observability-3.0.2}/tests/test_adapters.py
RENAMED
|
File without changes
|
{policyengine_observability-3.0.1 → policyengine_observability-3.0.2}/tests/test_config_schema.py
RENAMED
|
File without changes
|
{policyengine_observability-3.0.1 → policyengine_observability-3.0.2}/tests/test_delivery.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|