Skip to content

Commit b619545

Browse files
Add Dagster observe-only mode for dbt Platform jobs (#121)
* feat: add dbt platform observe-only mode * fix: type dbt observe-only asset definitions
1 parent 6b27688 commit b619545

5 files changed

Lines changed: 140 additions & 20 deletions

File tree

.env.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,9 @@ DBT_PLATFORM_TOKEN=your-dbt-platform-service-token
117117
DBT_PLATFORM_ACCESS_URL=https://cloud.getdbt.com
118118
DBT_PLATFORM_ADHOC_JOB_NAME=economic-data-dagster-adhoc
119119
DBT_PLATFORM_POLL_INTERVAL_SECONDS=60
120+
# Set true in production when dbt Platform owns scheduled builds and Dagster
121+
# should only observe completed dbt Platform runs.
122+
DBT_PLATFORM_OBSERVE_ONLY=false
120123
# DBT_PROJECT_DIR=/path/to/dbt_project # Optional, auto-detected
121124

122125
# =============================================================================

docs/dbt_project/dbt-platform-fusion-onboarding.md

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ execution modes:
150150
| --- | --- | --- |
151151
| Local CLI | `local` | Uses `DbtCliResource` and the bundled `dbt_project/`. |
152152
| dbt Platform | `dbt_platform` | Uses `DbtCloudWorkspace` to load dbt Platform artifacts and launch ad hoc dbt Platform runs. |
153+
| dbt Platform observe-only | `dbt_platform` + `DBT_PLATFORM_OBSERVE_ONLY=true` | Loads dbt Platform asset specs and polls completed dbt Platform jobs, but does not register Dagster dbt jobs or launch ad hoc dbt Platform runs. |
153154

154155
Configure these Dagster environment variables after the dbt Platform project and
155156
environment exist:
@@ -159,11 +160,34 @@ environment exist:
159160
| `DBT_ORCHESTRATION_MODE` | Set to `dbt_platform` to switch Dagster from local dbt CLI execution to dbt Platform. |
160161
| `DBT_PLATFORM_ACCOUNT_ID` | dbt Platform account id. |
161162
| `DBT_PLATFORM_PROJECT_ID` | dbt Platform project id. |
162-
| `DBT_PLATFORM_ENVIRONMENT_ID` | dbt Platform environment id Dagster should use for ad hoc runs. |
163+
| `DBT_PLATFORM_ENVIRONMENT_ID` | dbt Platform environment id Dagster should load and observe. Use the production environment when dbt Platform owns scheduled production builds and freshness jobs. |
163164
| `DBT_PLATFORM_TOKEN` | dbt Platform service token. |
164165
| `DBT_PLATFORM_ACCESS_URL` | dbt Platform URL, default `https://cloud.getdbt.com`. |
165166
| `DBT_PLATFORM_ADHOC_JOB_NAME` | Optional ad hoc job name Dagster creates/uses in dbt Platform. |
166167
| `DBT_PLATFORM_POLL_INTERVAL_SECONDS` | Optional polling interval for external dbt Platform run observations. |
168+
| `DBT_PLATFORM_OBSERVE_ONLY` | Optional strict production mode. Set to `true` when Dagster should observe dbt Platform jobs without exposing Dagster dbt asset jobs. |
169+
170+
## Dagster Observe-Only Mode
171+
172+
Use observe-only mode for production once dbt Platform owns the scheduled dbt
173+
jobs:
174+
175+
```bash
176+
DBT_ORCHESTRATION_MODE=dbt_platform
177+
DBT_PLATFORM_ENVIRONMENT_ID=<production-environment-id>
178+
DBT_PLATFORM_OBSERVE_ONLY=true
179+
```
180+
181+
In this mode Dagster loads dbt model asset specs from the configured dbt
182+
Platform environment and registers the dbt Cloud polling sensor. The sensor
183+
polls completed dbt Platform runs for that project and environment, skips
184+
Dagster-created ad hoc jobs, reads each run's `run_results.json`, and records
185+
model-level Dagster asset materializations. Dagster does not register the
186+
`dbt_*_models_job` jobs in this mode, so scheduled `dbt build` and
187+
`dbt source freshness` runs remain owned by dbt Platform.
188+
189+
Keep observe-only disabled in local development or in a dedicated CI/ad hoc
190+
environment if you still want Dagster to trigger dbt Platform runs.
167191

168192
Cutover path:
169193

@@ -172,7 +196,9 @@ Cutover path:
172196
3. Set `DBT_ORCHESTRATION_MODE=dbt_platform` and configure the `DBT_PLATFORM_*` variables in Dagster.
173197
4. Validate Dagster code location load; Dagster should fetch the manifest from dbt Platform.
174198
5. Materialize a small dbt asset selection from Dagster to confirm ad hoc dbt Platform runs work.
175-
6. Remove local dbt project bundling and `dbt deps` fallback after the API path is stable.
199+
6. For production, set `DBT_PLATFORM_ENVIRONMENT_ID` to the production environment and `DBT_PLATFORM_OBSERVE_ONLY=true`.
200+
7. Confirm the `dbt_cloud_<account>_<project>_<environment>__run_status_sensor` sensor records asset materializations from the dbt Platform production build job.
201+
8. Remove local dbt project bundling and `dbt deps` fallback after the API path is stable.
176202

177203
## Remaining Manual Checklist
178204

@@ -186,5 +212,7 @@ Cutover path:
186212
- [ ] Configure PR, production build, and source freshness jobs.
187213
- [ ] Add dbt Platform PR check and `Dagster CI / Test Dagster` to GitHub branch protection.
188214
- [ ] Add dbt Platform GitHub repository secrets/variables for Dagster CI.
189-
- [ ] Decide final Dagster orchestration cutover date.
215+
- [ ] Set `DBT_PLATFORM_OBSERVE_ONLY=true` in production Dagster after dbt Platform schedules are live.
216+
- [ ] Record the production dbt Platform environment id, production build job id, and source freshness job id in deployment notes.
190217
- [ ] Validate Dagster can load dbt Platform asset specs and trigger an ad hoc run.
218+
- [ ] Validate Dagster observes a completed dbt Platform production build and records model-level asset materializations.

macro_agents/src/macro_agents/defs/transformation/__init__.py

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1+
from typing import cast
2+
13
import dagster as dg
24

35
from macro_agents.defs.transformation.dbt import (
46
dbt_cloud_polling_sensor,
7+
dbt_platform_observe_only,
58
dbt_resource,
69
full_dbt_assets,
710
)
@@ -11,6 +14,8 @@
1114
financial_conditions_index,
1215
)
1316

17+
DagsterAssetDefinition = dg.AssetsDefinition | dg.AssetSpec | dg.SourceAsset
18+
1419

1520
dbt_models_job = dg.define_asset_job(
1621
name="dbt_models_job",
@@ -108,11 +113,16 @@
108113
if dbt_cloud_polling_sensor is not None:
109114
transformation_sensors.append(dbt_cloud_polling_sensor)
110115

116+
dbt_asset_definitions: list[DagsterAssetDefinition] = (
117+
cast(list[DagsterAssetDefinition], full_dbt_assets)
118+
if isinstance(full_dbt_assets, list)
119+
else [cast(DagsterAssetDefinition, full_dbt_assets)]
120+
)
111121

112-
defs = dg.Definitions(
113-
assets=[full_dbt_assets, financial_conditions_index, fci_weights_config],
114-
asset_checks=transformation_checks,
115-
jobs=[
122+
dbt_jobs = (
123+
[]
124+
if dbt_platform_observe_only
125+
else [
116126
dbt_models_job,
117127
dbt_staging_models_job,
118128
dbt_staging_telemetry_models_job,
@@ -124,7 +134,18 @@
124134
dbt_agents_preprocess_models_job,
125135
dbt_data_quality_models_job,
126136
dbt_backtesting_models_job,
137+
]
138+
)
139+
140+
141+
defs = dg.Definitions(
142+
assets=[
143+
*dbt_asset_definitions,
144+
financial_conditions_index,
145+
fci_weights_config,
127146
],
147+
asset_checks=transformation_checks,
148+
jobs=dbt_jobs,
128149
resources={"dbt": dbt_resource},
129150
sensors=transformation_sensors,
130151
)

macro_agents/src/macro_agents/defs/transformation/dbt.py

Lines changed: 42 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,11 @@ def _first_env(*names: str) -> str | None:
6565
return None
6666

6767

68+
def _truthy_env(*names: str) -> bool:
69+
value = _first_env(*names)
70+
return value is not None and value.lower() in {"1", "true", "t", "yes", "y", "on"}
71+
72+
6873
def _required_int_env(*names: str) -> int:
6974
value = _first_env(*names)
7075
if value is None:
@@ -86,7 +91,18 @@ def _required_env_var(*names: str) -> dg.EnvVar:
8691
raise RuntimeError(f"Set {joined_names} to enable dbt Platform orchestration.")
8792

8893

89-
if orchestration_mode in {"dbt_platform", "dbt_cloud", "platform", "cloud"}:
94+
dbt_platform_mode = orchestration_mode in {
95+
"dbt_platform",
96+
"dbt_cloud",
97+
"platform",
98+
"cloud",
99+
}
100+
dbt_platform_observe_only = dbt_platform_mode and _truthy_env(
101+
"DBT_PLATFORM_OBSERVE_ONLY", "DBT_CLOUD_OBSERVE_ONLY"
102+
)
103+
104+
105+
if dbt_platform_mode:
90106
dbt_cloud_credentials = DbtCloudCredentials(
91107
account_id=_required_int_env("DBT_PLATFORM_ACCOUNT_ID", "DBT_CLOUD_ACCOUNT_ID"),
92108
token=_required_env_var("DBT_PLATFORM_TOKEN", "DBT_CLOUD_API_TOKEN"),
@@ -105,21 +121,34 @@ def _required_env_var(*names: str) -> dg.EnvVar:
105121
),
106122
)
107123

108-
@dbt_cloud_assets(
109-
workspace=dbt_resource,
110-
exclude=DBT_NASDAQ_EXCLUDE,
111-
dagster_dbt_translator=CustomizedDagsterDbtTranslator(),
112-
)
113-
def full_dbt_assets(context: dg.AssetExecutionContext, dbt: DbtCloudWorkspace):
114-
yield from dbt.cli(
115-
["build"],
116-
dagster_dbt_translator=CustomizedDagsterDbtTranslator(),
117-
context=context,
118-
).wait()
124+
dbt_platform_translator = CustomizedDagsterDbtTranslator()
125+
126+
if dbt_platform_observe_only:
127+
full_dbt_assets = list(
128+
dbt_resource.load_asset_specs(
129+
select="fqn:*",
130+
exclude=DBT_NASDAQ_EXCLUDE,
131+
selector="",
132+
dagster_dbt_translator=dbt_platform_translator,
133+
)
134+
)
135+
else:
136+
137+
@dbt_cloud_assets(
138+
workspace=dbt_resource,
139+
exclude=DBT_NASDAQ_EXCLUDE,
140+
dagster_dbt_translator=dbt_platform_translator,
141+
)
142+
def full_dbt_assets(context: dg.AssetExecutionContext, dbt: DbtCloudWorkspace):
143+
yield from dbt.cli(
144+
["build"],
145+
dagster_dbt_translator=dbt_platform_translator,
146+
context=context,
147+
).wait()
119148

120149
dbt_cloud_polling_sensor = build_dbt_cloud_polling_sensor(
121150
workspace=dbt_resource,
122-
dagster_dbt_translator=CustomizedDagsterDbtTranslator(),
151+
dagster_dbt_translator=dbt_platform_translator,
123152
minimum_interval_seconds=int(
124153
os.getenv("DBT_PLATFORM_POLL_INTERVAL_SECONDS", "60")
125154
),
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import importlib
2+
import sys
3+
4+
import dagster as dg
5+
from dagster_dbt import DbtCloudWorkspace
6+
7+
8+
def test_dbt_platform_observe_only_registers_specs_without_dbt_jobs(monkeypatch):
9+
monkeypatch.setenv("DBT_ORCHESTRATION_MODE", "dbt_platform")
10+
monkeypatch.setenv("DBT_PLATFORM_OBSERVE_ONLY", "true")
11+
monkeypatch.setenv("DBT_PLATFORM_ACCOUNT_ID", "1")
12+
monkeypatch.setenv("DBT_PLATFORM_PROJECT_ID", "2")
13+
monkeypatch.setenv("DBT_PLATFORM_ENVIRONMENT_ID", "3")
14+
monkeypatch.setenv("DBT_PLATFORM_TOKEN", "test-token")
15+
16+
def fake_load_asset_specs(self, **kwargs):
17+
return [
18+
dg.AssetSpec(
19+
key=dg.AssetKey("observed_dbt_model"),
20+
group_name="staging",
21+
kinds={"dbtcloud"},
22+
)
23+
]
24+
25+
monkeypatch.setattr(DbtCloudWorkspace, "load_asset_specs", fake_load_asset_specs)
26+
27+
for module_name in [
28+
"macro_agents.defs.transformation",
29+
"macro_agents.defs.transformation.dbt",
30+
]:
31+
sys.modules.pop(module_name, None)
32+
33+
transformation = importlib.import_module("macro_agents.defs.transformation")
34+
35+
assert transformation.dbt_jobs == []
36+
assert transformation.dbt_asset_definitions[0].key == dg.AssetKey(
37+
"observed_dbt_model"
38+
)
39+
assert transformation.dbt_cloud_polling_sensor is not None

0 commit comments

Comments
 (0)