phlo-observe-plugin 0.2.0__py3-none-any.whl
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/__init__.py +42 -0
- phlo_observe_plugin/dagster_ext.py +161 -0
- phlo_observe_plugin/db-setup.yaml +37 -0
- phlo_observe_plugin/hooks_plugin.py +629 -0
- phlo_observe_plugin/plugin.py +30 -0
- phlo_observe_plugin/presentation.py +715 -0
- phlo_observe_plugin/service.yaml +100 -0
- phlo_observe_plugin-0.2.0.dist-info/METADATA +16 -0
- phlo_observe_plugin-0.2.0.dist-info/RECORD +12 -0
- phlo_observe_plugin-0.2.0.dist-info/WHEEL +5 -0
- phlo_observe_plugin-0.2.0.dist-info/entry_points.txt +9 -0
- phlo_observe_plugin-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -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\""
|