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.
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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\""