Feature: Add consumption and observability to dbt Artifacts - #562
Open
mtcarlone wants to merge 18 commits into
Open
Feature: Add consumption and observability to dbt Artifacts#562mtcarlone wants to merge 18 commits into
mtcarlone wants to merge 18 commits into
Conversation
specs/ is a local-only living record for the consumption+observability v1 effort; keep it out of version control.
Cross-adapter macro converting a stored run timestamp to its UTC calendar date. default__ and snowflake__ both plain-cast to date: run_started_at is stored TIMESTAMP_NTZ holding dbt's UTC value, so the cast is session-TZ invariant. Shared infra for consumption + observability daily-grain marts.
ANSI percentile_cont(fraction) within group ordered-set aggregates, usable in GROUP BY queries; Snowflake supports them natively so no snowflake__ override needed. p95() is thin sugar over percentile(col, 0.95). For the observability performance-baseline math.
Dispatched case-expression classifying invocations as deployment/development per the design precedence table (cloud job -> deployment targets -> optional env-var rule -> fallback), with a count_all_invocations escape hatch. Rule 3 (env_vars JSON) lives only in snowflake__ (env_vars is OBJECT via type_json); default__ raises a clear compile error if rule 3 is requested off Snowflake. Additive: does not modify stg_dbt__invocations or any existing contract.
New view exploding depends_on_nodes (native ARRAY on Snowflake) into one row per parent x child edge from the latest graph state, across models/snapshots/ tests. Snowflake-gated (enabled = target.type == 'snowflake'); JSON-array explode via new dispatched flatten_json_array() helper whose default__ raises a clear compile error naming the adapter (fast-follow O-12/C-11). Adds an idiomatic lineage_edge_id surrogate key so grain is testable without a dbt_utils dependency. Additive; no existing model/contract touched.
Core consumption marts over existing staging. _detail is the base grain (day x meter x materialization x target_name); _daily is a strict sum roll-up of _detail so the two reconcile by construction. Meters: 'smb' (deployment model successes on run/build/retry) and 'active_target_tables' (distinct deployment-active nodes per target, DATT upper-bound proxy). Uses C-01 cast_to_utc_date + C-02 classify_invocation_billing. Additive views only.
Per-model monthly SMB burn, cadence, runtime and dbt State ROI estimate (distinct_days_built x datt_price). Same SMB filter as consumption_daily so monthly totals reconcile across marts. Adds a reusable dependency-free generic range test dbt_artifacts.is_between (used here for pct_of_month_smb in [0,1]; reused by O-tier marts). Additive views only.
Per-UTC-day run-health rollup across all invocations (no billing filter): invocation/command counts, node success/failure/error/skip counts across models+seeds+snapshots+tests, success_rate, runtime, first/last timestamps, failed-invocation count. _detail adds a target_name cut. success_rate guarded with dbt_artifacts.is_between. Additive views only.
Per day x node runtime stats vs a trailing 28d same-DOW baseline. median/p95/ rows over successful non-full-refresh executions; full_refresh_executions exposed separately. baseline_runtime = median of same-node same-ISO-DOW prior median_runtime within run_rate_days, excluding current day. is_regressed gated on regression_threshold (1.5) AND >= regression_min_samples (3) same-DOW days. Enhances is_between to allow open-ended bounds (ratio >= 0 test). Additive.
Per parent model x UTC day: measured wall-clock stall its children spent waiting (child.compile_started_at - binding parent.query_completed_at, clamped >=0). Binding parent = latest-completing model-parent per (invocation, child) via qualify. Roll-up counts only gating rows (stall>0). Detail view for drill-down. Depends on dim_dbt__lineage_edges (O-02); Snowflake-gated. Descendant counts/blocking scores parked (v1 scope). Additive views only.
Detects tests that flip fail/error -> pass on the same UTC day with no parent model rebuilt between (retry-until-green signal), via lag() over test executions + dim_dbt__lineage_edges. parent_rebuilt_between is false (parents exist, none rebuilt) or null (no model parents); candidates with a parent rebuild between are legit fixes and excluded. Rollup test x month with flips, flake_rate, is_flaky. Snowflake-gated. Additive views only.
Month-end forecast per billing_month x meter: MTD, weekday-aware projected month-end (trailing run_rate_days per-ISO-DOW average incl. zero days), projected allowance-breach date, pct of allowance. Allowance via get_smb_allowance() (explicit var -> plan default -> none). Remaining-day series generated with a Jinja loop (no seed, no dbt_utils). Snowflake-gated.
These marts use Snowflake-specific SQL (dayofweekiso, datediff, last_day, date_trunc semantics, etc.). dbt_artifacts is installed on 7 adapters; an un-gated Snowflake-only model would break a non-Snowflake consumer's dbt run. Gating enabled = target.type == 'snowflake' keeps v1 strictly additive: other adapters get no new models (identical behavior). Multi-adapter support with portable dispatch is fast-follow (C-11/O-12). Completes gating for C-03/C-05/ O-03/O-04 (O-02/O-05/O-06/C-04 already gated).
dbt renders disabled models' Jinja during parse, so raise_compiler_error in
flatten_json_array/classify default__ broke 'dbt parse' on postgres/etc even
though the models are Snowflake-gated. Guard the raises with {% if execute %}
(parse has execute==false) and emit a parse-safe placeholder. Non-Snowflake
consumers' dbt parse/run is now unaffected; a real build on an unsupported
adapter still raises a clear error. Found via O-07's postgres-parse check.
Sets dbt_artifacts_billing_plan/deployment_targets in the integration project so its invocations class as deployment, and adds Snowflake-gated singular tests: consumption_daily has smb>0, forecast has one current-month smb row with non-null mtd+allowance, by_model reconciles to daily.
Snowflake-gated singular tests: run_health_daily has invocations>0, every model-with-deps has a lineage edge, model_performance is consistent and non-empty, flaky_tests/dag_bottlenecks return valid (possibly empty) results.
…-end Previously the 'already exceeded' branch returned as_of_date, which collapses to month_end for fully-elapsed months (e.g. June showed 06-30). Add a historical_breach CTE: the first elapsed day whose ACTUAL running total reaches the allowance, and use coalesce(historical, projected_future). Now a past or already-over month reports the true day it crossed. Verified: integration April 2025 -> 2025-04-09 (cumulative crosses 50 there), not 04-30.
Model, macro and test comments cited internal ticket IDs and design-doc paths that don't exist in this repository, so they read as dangling references. Replace them with the information they were standing in for: dependency names instead of ticket IDs, "planned" instead of named follow-up tickets, and plain descriptions instead of doc paths. Also reword the flatten_json_array() unsupported-adapter error, which surfaced a ticket ID to end users. Comment-only apart from that error string; no behaviour change.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Add consumption and run-health marts (Snowflake)
Background
dbt_artifactsalready captures everything needed to answer two questions teams ask constantly, but it ships no models that answer either:"How much of our dbt platform allowance have we burned, and when do we blow through it?" dbt bills on Successful Models Built — every model built in a deployment environment via orchestration, where views, tables and incrementals count equally and reruns count again. That usage is visible in the dbt platform UI, but not in the warehouse alongside every other cost metric a team tracks, and it is not projected forward. The package has recorded every model execution and invocation since day one; the meter is a
selectaway."Why is the nightly job slower, and which tests do we just retry until they pass?" Runtime, status and timing are already captured per node execution, but every team that wants regression detection, flake identification or a run-health dashboard builds the same SQL by hand.
Both are pure analytics over existing source tables. This PR adds those marts. No new upload datasets, no changes to the on-run-end hook, and no changes to any existing model — the column-order contract between the upload macros, source models and column-name lists is untouched.
Scope is deliberately Snowflake-only for this first cut. Every new model is gated with
config(enabled = target.type == "snowflake")and the one helper without portable SQL degrades safely (see below), so consumers on the other six adapters see no behavioural change at all. Cross-adapter implementations are a follow-up.Key Changes
Consumption marts
fct_dbt__consumption_daily(+_detail) — one row per UTC day × meter. Two meters ship:smb(Successful Models Built, encoding dbt's published rules — success status, deployment invocations only,run/build/retrycommands, all materializations counted equally, reruns counted again) andactive_target_tables(distinct models, seeds, snapshots and tests active per UTC day — the upper bound of what a table-day meter would count). Meters are rows rather than columns, so additional meters are additive. The daily model is a strict roll-up of the detail model, so the two grains always reconcile.fct_dbt__consumption_forecast— one row per billing month × meter: month-to-date quantity, plan allowance, weekday-aware run rate, projected month-end total, projected allowance-breach date, and percent of allowance consumed. The projection averages per ISO day-of-week over a trailing window (including zero-days) rather than a straight mean, so weekly seasonality does not distort it.forecast_exceeded_datereports the real crossing day in both directions: for elapsed months it finds the first day the actual running total reached the allowance, rather than collapsing to month-end.fct_dbt__consumption_by_model— one row per billing month × model: SMB burn, share of monthly total, build cadence, distinct days built, total runtime, and an estimated cost of reusing rather than rebuilding, for teams evaluating state-aware builds.Observability marts
fct_dbt__run_health_daily(+_detail) — per-day invocation counts, node successes/failures/errors/skips across models, seeds, snapshots and tests, success rate, runtime totals, and failed-invocation counts. Unlike the consumption marts this covers all invocations, not just deployment ones.fct_dbt__model_performance— per day × model runtime statistics against a trailing weekday-aware baseline, with a regression ratio and anis_regressedflag. Full-refresh executions are excluded from both current-day stats and the baseline (a full refresh is not a regression) and surfaced as a separate count, and the flag requires a minimum baseline sample so new models do not fire spuriously.fct_dbt__flaky_tests(+_detail) — tests that flip from failing to passing with no successful rebuild of the attached model in between, rolled up monthly with a flake rate. Where parentage cannot be resolved the flip is still reported with the uncertainty exposed as a column rather than silently dropped.fct_dbt__dag_bottlenecks(+_detail) — measured stall time: within an invocation, how long each model waited on the latest-completing parent that gated it, attributed to that binding parent. This is observed wall-clock gating from execution timestamps, not graph estimation. Descendant counts and blocking scores are intentionally out of scope here.dim_dbt__lineage_edges— one row per parent → child edge, derived from thedepends_on_nodesarrays already captured for models, snapshots and tests. Turns lineage that was only queryable as a nested array into a joinable table.Supporting macros
cast_to_utc_date(),median()/percentile()/p95(), andflatten_json_array()inmacros/database_specific_helpers/, all dispatched in the package's existing pattern.classify_invocation_billing()— resolves an invocation todeploymentordevelopmentby documented precedence (dbt Cloud job id present, then configurable target names, then an optional environment-variable marker), since only deployment builds are billed.get_smb_allowance()— resolves the monthly allowance from an explicit override, else a plan-name default, else none.is_between()generic test for range assertions on the new models.Configuration
All new behaviour is var-driven with working defaults, so the package behaves identically for anyone who sets nothing:
dbt_artifacts_billing_plan,dbt_artifacts_smb_allowance,dbt_artifacts_deployment_targets,dbt_artifacts_deployment_env_var,dbt_artifacts_count_all_invocations,dbt_artifacts_datt_price,dbt_artifacts_run_rate_days,dbt_artifacts_regression_threshold,dbt_artifacts_regression_min_samples,dbt_artifacts_flaky_min_flips.Backwards compatibility
flatten_json_array()has no portable implementation yet. Its default emits a parse-safe placeholder whenexecuteis false, sodbt parseanddbt compilesucceed unchanged on every adapter, and raises a clear compiler error only if a model using it is actually built on an unsupported adapter — which the gating already prevents.Tests
Eight singular tests in the integration project cover both mart families: SMB rows present and positive, forecast returning exactly one row per meter for the current billing month with a resolved allowance, by-model totals reconciling to the daily grain, lineage edges covering every model with dependencies, and validity/range assertions on the performance, flaky-test and bottleneck marts. The integration project sets a billing plan and deployment-target list so the consumption marts have classified invocations to aggregate; those vars are inert on non-Snowflake targets.
Not included
Package version bump and README documentation for the new models and vars. These marts are new views, so consumers will need to re-run
dbt run --select dbt_artifactsafter upgrading — the release should be a minor version, and the notes should carry that instruction.