phlo-observe-plugin 0.2.0__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.
- phlo_observe_plugin-0.2.0/PKG-INFO +16 -0
- phlo_observe_plugin-0.2.0/README.md +50 -0
- phlo_observe_plugin-0.2.0/pyproject.toml +60 -0
- phlo_observe_plugin-0.2.0/setup.cfg +4 -0
- phlo_observe_plugin-0.2.0/src/phlo_observe_plugin/__init__.py +42 -0
- phlo_observe_plugin-0.2.0/src/phlo_observe_plugin/dagster_ext.py +161 -0
- phlo_observe_plugin-0.2.0/src/phlo_observe_plugin/db-setup.yaml +37 -0
- phlo_observe_plugin-0.2.0/src/phlo_observe_plugin/hooks_plugin.py +629 -0
- phlo_observe_plugin-0.2.0/src/phlo_observe_plugin/plugin.py +30 -0
- phlo_observe_plugin-0.2.0/src/phlo_observe_plugin/presentation.py +715 -0
- phlo_observe_plugin-0.2.0/src/phlo_observe_plugin/service.yaml +100 -0
- phlo_observe_plugin-0.2.0/src/phlo_observe_plugin.egg-info/PKG-INFO +16 -0
- phlo_observe_plugin-0.2.0/src/phlo_observe_plugin.egg-info/SOURCES.txt +19 -0
- phlo_observe_plugin-0.2.0/src/phlo_observe_plugin.egg-info/dependency_links.txt +1 -0
- phlo_observe_plugin-0.2.0/src/phlo_observe_plugin.egg-info/entry_points.txt +9 -0
- phlo_observe_plugin-0.2.0/src/phlo_observe_plugin.egg-info/requires.txt +7 -0
- phlo_observe_plugin-0.2.0/src/phlo_observe_plugin.egg-info/top_level.txt +1 -0
- phlo_observe_plugin-0.2.0/tests/test_dagster_ext.py +181 -0
- phlo_observe_plugin-0.2.0/tests/test_hooks_plugin.py +392 -0
- phlo_observe_plugin-0.2.0/tests/test_presentation.py +382 -0
- phlo_observe_plugin-0.2.0/tests/test_service_definitions.py +105 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: phlo-observe-plugin
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: phlo-observe SDK integration and phlo-observer service for Phlo
|
|
5
|
+
Author-email: Phlo Team <team@phlo.dev>
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.12
|
|
8
|
+
Description-Content-Type: text/plain
|
|
9
|
+
Requires-Dist: phlo<0.18,>=0.17.0
|
|
10
|
+
Requires-Dist: phlo-dagster>=0.3.2
|
|
11
|
+
Requires-Dist: phlo-observe<0.3,>=0.2.3
|
|
12
|
+
Provides-Extra: dev
|
|
13
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
14
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
15
|
+
|
|
16
|
+
Canonical event emission and observer service wiring for Phlo.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# phlo-observe-plugin
|
|
2
|
+
|
|
3
|
+
Canonical `phlo-observe` integration for Phlo.
|
|
4
|
+
|
|
5
|
+
The package installs the published `phlo-observe` SDK and its core and query
|
|
6
|
+
dependencies from PyPI.
|
|
7
|
+
|
|
8
|
+
## What it provides
|
|
9
|
+
|
|
10
|
+
- A hook plugin that translates pipeline events into canonical logs, metrics,
|
|
11
|
+
and operation events while preserving correlation.
|
|
12
|
+
- A Dagster extension that keeps physical execution IDs distinct from logical
|
|
13
|
+
retry-chain IDs.
|
|
14
|
+
- An opt-in `phlo-observer` service for ingest and query. ClickStack remains
|
|
15
|
+
Phlo's default observability backend.
|
|
16
|
+
|
|
17
|
+
Observability is fail-open: unavailable drains or observer services never stop
|
|
18
|
+
pipeline work.
|
|
19
|
+
|
|
20
|
+
## Use
|
|
21
|
+
|
|
22
|
+
Install the package, then enable the observability profile when a standalone
|
|
23
|
+
observer is wanted:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install phlo-observe-plugin
|
|
27
|
+
phlo services init --profile observability
|
|
28
|
+
phlo services start
|
|
29
|
+
phlo services start --profile observability
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Set `OBSERVE_HTTP_ENDPOINT` to the observer's `/v1/events` endpoint. For local
|
|
33
|
+
development, the service defaults to unauthenticated ingest. For hardened
|
|
34
|
+
deployments, configure matching `OBSERVE_HTTP_TOKEN` and
|
|
35
|
+
`PHLO_OBSERVER_INGEST_TOKENS` values.
|
|
36
|
+
|
|
37
|
+
The observability profile enables the concise terminal drain. Set
|
|
38
|
+
`PHLO_OBSERVE_PRETTY_VERBOSE=true` to include secondary diagnostic events and
|
|
39
|
+
the full framework log stream. Set `PHLO_OBSERVE_PRETTY=false` to keep
|
|
40
|
+
canonical events out of the terminal. Store either setting in the top-level
|
|
41
|
+
`env` block in `phlo.yaml`, then regenerate the service configuration:
|
|
42
|
+
|
|
43
|
+
```yaml
|
|
44
|
+
env:
|
|
45
|
+
PHLO_OBSERVE_PRETTY: "true"
|
|
46
|
+
PHLO_OBSERVE_PRETTY_VERBOSE: "false"
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The existing hook bus API remains compatible. The plugin translates its
|
|
50
|
+
events rather than replacing them.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
build-backend = "setuptools.build_meta"
|
|
3
|
+
requires = [
|
|
4
|
+
"setuptools>=45",
|
|
5
|
+
"wheel",
|
|
6
|
+
]
|
|
7
|
+
|
|
8
|
+
[project]
|
|
9
|
+
dependencies = [
|
|
10
|
+
"phlo>=0.17.0,<0.18",
|
|
11
|
+
"phlo-dagster>=0.3.2",
|
|
12
|
+
"phlo-observe>=0.2.3,<0.3",
|
|
13
|
+
]
|
|
14
|
+
description = "phlo-observe SDK integration and phlo-observer service for Phlo"
|
|
15
|
+
name = "phlo-observe-plugin"
|
|
16
|
+
requires-python = ">=3.12"
|
|
17
|
+
version = "0.2.0"
|
|
18
|
+
|
|
19
|
+
[[project.authors]]
|
|
20
|
+
email = "team@phlo.dev"
|
|
21
|
+
name = "Phlo Team"
|
|
22
|
+
|
|
23
|
+
[project.entry-points."phlo.plugins.dagster"]
|
|
24
|
+
observe = "phlo_observe_plugin.dagster_ext:ObserveDagsterExtension"
|
|
25
|
+
|
|
26
|
+
[project.entry-points."phlo.plugins.hooks"]
|
|
27
|
+
observe = "phlo_observe_plugin.hooks_plugin:ObserveHookPlugin"
|
|
28
|
+
|
|
29
|
+
[project.entry-points."phlo.plugins.services"]
|
|
30
|
+
phlo-observer = "phlo_observe_plugin.plugin:PhloObserverServicePlugin"
|
|
31
|
+
phlo-observer-db-setup = "phlo_observe_plugin.plugin:PhloObserverDbSetupPlugin"
|
|
32
|
+
|
|
33
|
+
[project.license]
|
|
34
|
+
text = "MIT"
|
|
35
|
+
|
|
36
|
+
[project.optional-dependencies]
|
|
37
|
+
dev = [
|
|
38
|
+
"pytest>=7.0",
|
|
39
|
+
"ruff>=0.1.0",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
[project.readme]
|
|
43
|
+
content-type = "text/plain"
|
|
44
|
+
text = "Canonical event emission and observer service wiring for Phlo."
|
|
45
|
+
|
|
46
|
+
[tool.ruff]
|
|
47
|
+
line-length = 100
|
|
48
|
+
target-version = "py312"
|
|
49
|
+
|
|
50
|
+
[tool.setuptools.package-data]
|
|
51
|
+
phlo_observe_plugin = [
|
|
52
|
+
"service.yaml",
|
|
53
|
+
"db-setup.yaml",
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
[tool.setuptools.package-dir]
|
|
57
|
+
"" = "src"
|
|
58
|
+
|
|
59
|
+
[tool.setuptools.packages.find]
|
|
60
|
+
where = ["src"]
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""phlo-observe integration for Phlo.
|
|
2
|
+
|
|
3
|
+
- ``ObserveHookPlugin`` translates hook-bus events into canonical
|
|
4
|
+
``phlo-observe`` events.
|
|
5
|
+
- ``ObserveDagsterExtension`` contributes a run-status sensor that closes each
|
|
6
|
+
physical Dagster run with a terminal ``pipeline.run`` event.
|
|
7
|
+
- ``PhloObserverServicePlugin`` / ``PhloObserverDbSetupPlugin`` declare the
|
|
8
|
+
observer compose service and its database provisioning step.
|
|
9
|
+
- ``presentation`` carries Phlo's ``PrettyRenderer`` rules and the ``pretty``
|
|
10
|
+
drain — imported lazily since it needs a PrettyRenderer-capable SDK.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"ObserveDagsterExtension",
|
|
17
|
+
"ObserveHookPlugin",
|
|
18
|
+
"PhloObserverDbSetupPlugin",
|
|
19
|
+
"PhloObserverServicePlugin",
|
|
20
|
+
"PrettyDrain",
|
|
21
|
+
"pretty_renderer",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def __getattr__(name: str) -> object:
|
|
26
|
+
if name == "ObserveHookPlugin":
|
|
27
|
+
from phlo_observe_plugin.hooks_plugin import ObserveHookPlugin
|
|
28
|
+
|
|
29
|
+
return ObserveHookPlugin
|
|
30
|
+
if name == "ObserveDagsterExtension":
|
|
31
|
+
from phlo_observe_plugin.dagster_ext import ObserveDagsterExtension
|
|
32
|
+
|
|
33
|
+
return ObserveDagsterExtension
|
|
34
|
+
if name in {"PhloObserverServicePlugin", "PhloObserverDbSetupPlugin"}:
|
|
35
|
+
from phlo_observe_plugin import plugin as _plugin
|
|
36
|
+
|
|
37
|
+
return getattr(_plugin, name)
|
|
38
|
+
if name in {"PrettyDrain", "pretty_renderer"}:
|
|
39
|
+
from phlo_observe_plugin import presentation as _presentation
|
|
40
|
+
|
|
41
|
+
return getattr(_presentation, name)
|
|
42
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""Dagster extension emitting terminal ``pipeline.run`` events per run.
|
|
2
|
+
|
|
3
|
+
Asset/step/check events are emitted inside the run worker by the adapter
|
|
4
|
+
instrumentation; this extension adds the run-boundary event. Run-status
|
|
5
|
+
sensors evaluate in the Dagster daemon, so the terminal ``pipeline.run`` is
|
|
6
|
+
emitted with explicit correlation rather than ambient context.
|
|
7
|
+
|
|
8
|
+
Run identity is the *physical* Dagster ``run_id`` — one observer run per
|
|
9
|
+
attempt. The logical Phlo run id (``phlo/run_id`` tag) and attempt number are
|
|
10
|
+
carried as attributes so retries group logically without violating the
|
|
11
|
+
observer's monotonic run-status precedence (a failed attempt must not pin a
|
|
12
|
+
later successful attempt to "failed").
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from datetime import UTC, datetime
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
import phlo.telemetry as phlo_observe
|
|
21
|
+
from phlo.logging import get_logger
|
|
22
|
+
from phlo.plugins.base import PluginMetadata
|
|
23
|
+
from phlo_dagster.dagster_ext import DagsterExtensionPlugin
|
|
24
|
+
|
|
25
|
+
logger = get_logger(__name__)
|
|
26
|
+
|
|
27
|
+
_WAP_RUN_ID_TAG = "phlo/run_id"
|
|
28
|
+
_WAP_ATTEMPT_TAG = "phlo/attempt"
|
|
29
|
+
_WAP_BRANCH_TAG = "phlo/wap_branch"
|
|
30
|
+
_WAP_CATALOG_SYSTEM_TAG = "phlo/catalog_system"
|
|
31
|
+
_PARTITION_TAG = "dagster/partition"
|
|
32
|
+
|
|
33
|
+
_STATUS_OUTCOME = {
|
|
34
|
+
"SUCCESS": "success",
|
|
35
|
+
"FAILURE": "failure",
|
|
36
|
+
"CANCELED": "cancelled",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _ts(epoch: Any) -> datetime | None:
|
|
41
|
+
try:
|
|
42
|
+
return datetime.fromtimestamp(float(epoch), UTC)
|
|
43
|
+
except (TypeError, ValueError, OverflowError):
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _emit_pipeline_run(context: Any, dagster_status: Any) -> None:
|
|
48
|
+
"""Emit the terminal ``pipeline.run`` event for one Dagster run."""
|
|
49
|
+
# enabled(), not available(): a disabled runtime should skip the run-stats
|
|
50
|
+
# query entirely, not do the work and drop the event at emit().
|
|
51
|
+
if not phlo_observe.enabled():
|
|
52
|
+
return
|
|
53
|
+
try:
|
|
54
|
+
run = context.dagster_run
|
|
55
|
+
tags = dict(run.tags or {})
|
|
56
|
+
run_id = run.run_id
|
|
57
|
+
partition_key = tags.get(_PARTITION_TAG)
|
|
58
|
+
branch = tags.get(_WAP_BRANCH_TAG)
|
|
59
|
+
# DagsterRunStatus is a plain Enum: str() gives 'DagsterRunStatus.X',
|
|
60
|
+
# so normalize through .value to the bare status name.
|
|
61
|
+
status_key = str(getattr(dagster_status, "value", dagster_status))
|
|
62
|
+
|
|
63
|
+
entities: dict[str, Any] = {"run": phlo_observe.run_entity_for("dagster", run_id)}
|
|
64
|
+
if branch:
|
|
65
|
+
# The staging ref is owned by the catalog the launch resolved —
|
|
66
|
+
# Nessie for the branch strategy, the snapshot catalog (e.g.
|
|
67
|
+
# polaris) for the snapshot strategy.
|
|
68
|
+
entities["branch"] = phlo_observe.branch_entity_id(
|
|
69
|
+
branch, system=tags.get(_WAP_CATALOG_SYSTEM_TAG) or "nessie"
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
started_at = None
|
|
73
|
+
ended_at = None
|
|
74
|
+
duration_ms = None
|
|
75
|
+
try:
|
|
76
|
+
# DagsterEvent carries no timestamp; run stats hold the real
|
|
77
|
+
# launch/start/end times for the physical attempt.
|
|
78
|
+
stats = context.instance.get_run_stats(run_id)
|
|
79
|
+
started_at = _ts(getattr(stats, "start_time", None)) or _ts(
|
|
80
|
+
getattr(stats, "launch_time", None)
|
|
81
|
+
)
|
|
82
|
+
ended_at = _ts(getattr(stats, "end_time", None))
|
|
83
|
+
if started_at and ended_at:
|
|
84
|
+
duration_ms = (ended_at - started_at).total_seconds() * 1000.0
|
|
85
|
+
except Exception: # noqa: BLE001 - timing is best-effort
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
attributes: dict[str, Any] = {
|
|
89
|
+
"job_name": run.job_name,
|
|
90
|
+
"dagster_status": status_key,
|
|
91
|
+
"phlo_run_id": tags.get(_WAP_RUN_ID_TAG),
|
|
92
|
+
"root_run_id": run.root_run_id,
|
|
93
|
+
"parent_run_id": run.parent_run_id,
|
|
94
|
+
}
|
|
95
|
+
attempt = tags.get(_WAP_ATTEMPT_TAG)
|
|
96
|
+
if attempt:
|
|
97
|
+
attributes["attempt"] = attempt
|
|
98
|
+
selection = getattr(run, "asset_selection", None)
|
|
99
|
+
if selection:
|
|
100
|
+
attributes["asset_keys"] = sorted(str(key) for key in selection)
|
|
101
|
+
|
|
102
|
+
phlo_observe.emit(
|
|
103
|
+
"pipeline.run",
|
|
104
|
+
category="pipeline",
|
|
105
|
+
outcome=_STATUS_OUTCOME.get(status_key, "unknown"),
|
|
106
|
+
severity="error" if status_key == "FAILURE" else "info",
|
|
107
|
+
attributes=attributes,
|
|
108
|
+
correlation={
|
|
109
|
+
"run_id": run_id,
|
|
110
|
+
"job_id": run.job_name,
|
|
111
|
+
"partition_key": partition_key,
|
|
112
|
+
"branch": branch,
|
|
113
|
+
},
|
|
114
|
+
entities=entities or None,
|
|
115
|
+
producer="dagster",
|
|
116
|
+
started_at=started_at,
|
|
117
|
+
ended_at=ended_at,
|
|
118
|
+
duration_ms=duration_ms,
|
|
119
|
+
)
|
|
120
|
+
except Exception: # noqa: BLE001 - telemetry must never fail a sensor tick
|
|
121
|
+
logger.debug("observe_run_status_emit_failed", exc_info=True)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class ObserveDagsterExtension(DagsterExtensionPlugin):
|
|
125
|
+
"""Contribute run-status sensors that close observer runs terminally."""
|
|
126
|
+
|
|
127
|
+
@property
|
|
128
|
+
def metadata(self) -> PluginMetadata:
|
|
129
|
+
"""Return plugin metadata for discovery and identification."""
|
|
130
|
+
return PluginMetadata(
|
|
131
|
+
name="observe",
|
|
132
|
+
version="0.1.0",
|
|
133
|
+
description="phlo-observe run-boundary sensors for Dagster",
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
def get_definitions(self) -> Any:
|
|
137
|
+
"""Return the run-status sensors as Dagster definitions."""
|
|
138
|
+
import dagster as dg
|
|
139
|
+
|
|
140
|
+
def _sensor(status: dg.DagsterRunStatus, suffix: str) -> Any:
|
|
141
|
+
@dg.run_status_sensor(
|
|
142
|
+
run_status=status,
|
|
143
|
+
name=f"observe_run_{suffix}",
|
|
144
|
+
monitor_all_code_locations=True,
|
|
145
|
+
# run_status_sensor defaults to STOPPED: without RUNNING the
|
|
146
|
+
# terminal pipeline.run events this extension exists to emit
|
|
147
|
+
# would never fire in a deployed stack.
|
|
148
|
+
default_status=dg.DefaultSensorStatus.RUNNING,
|
|
149
|
+
)
|
|
150
|
+
def _observe_run(context: Any) -> None:
|
|
151
|
+
_emit_pipeline_run(context, status)
|
|
152
|
+
|
|
153
|
+
return _observe_run
|
|
154
|
+
|
|
155
|
+
return dg.Definitions(
|
|
156
|
+
sensors=[
|
|
157
|
+
_sensor(dg.DagsterRunStatus.SUCCESS, "success"),
|
|
158
|
+
_sensor(dg.DagsterRunStatus.FAILURE, "failure"),
|
|
159
|
+
_sensor(dg.DagsterRunStatus.CANCELED, "canceled"),
|
|
160
|
+
]
|
|
161
|
+
)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
name: phlo-observer-db-setup
|
|
2
|
+
description: Create the phlo-observer database on the shared PostgreSQL instance
|
|
3
|
+
category: observability
|
|
4
|
+
default: false
|
|
5
|
+
profile: observability
|
|
6
|
+
|
|
7
|
+
# Same pinned image as the postgres service so no extra pull is needed.
|
|
8
|
+
image: postgres:18.4-alpine3.24@sha256:9a8afca54e7861fd90fab5fdf4c42477a6b1cb7d293595148e674e0a3181de15
|
|
9
|
+
|
|
10
|
+
depends_on:
|
|
11
|
+
- postgres
|
|
12
|
+
|
|
13
|
+
compose:
|
|
14
|
+
restart: "no"
|
|
15
|
+
entrypoint: /bin/sh
|
|
16
|
+
environment:
|
|
17
|
+
PGPASSWORD: ${POSTGRES_PASSWORD:-phlo}
|
|
18
|
+
PHLO_OBSERVER_DB: ${PHLO_OBSERVER_DB:-phlo_observer}
|
|
19
|
+
command:
|
|
20
|
+
# $$PHLO_OBSERVER_DB expands at container runtime, not compose-render
|
|
21
|
+
# time, so the value can never inject into this script's text; the case
|
|
22
|
+
# guard additionally restricts it to a safe SQL identifier.
|
|
23
|
+
- -c
|
|
24
|
+
- |
|
|
25
|
+
case "$$PHLO_OBSERVER_DB" in
|
|
26
|
+
*[!A-Za-z0-9_]*)
|
|
27
|
+
echo "phlo-observer-db-setup: PHLO_OBSERVER_DB must match [A-Za-z0-9_]+" >&2
|
|
28
|
+
exit 1
|
|
29
|
+
;;
|
|
30
|
+
esac
|
|
31
|
+
psql "postgresql://${POSTGRES_USER:-phlo}@postgres:5432/postgres" \
|
|
32
|
+
-v ON_ERROR_STOP=1 -tc \
|
|
33
|
+
"SELECT 1 FROM pg_database WHERE datname = '$$PHLO_OBSERVER_DB'" \
|
|
34
|
+
| grep -q 1 \
|
|
35
|
+
|| psql "postgresql://${POSTGRES_USER:-phlo}@postgres:5432/postgres" \
|
|
36
|
+
-v ON_ERROR_STOP=1 \
|
|
37
|
+
-c "CREATE DATABASE \"$$PHLO_OBSERVER_DB\""
|