Skip to main content

Integrate with DataHub

Status: shipped — DataHub outbound + inbound (issue #89). Atlan, Data.world, and Alation now ship as first-class connectors too (issues #403 / #404 / #415) — see Integrate with Atlan, Integrate with Data.world, and Integrate with Alation.

Measured Cloud ships a first-party connector for DataHub that does two things:

  • Outbound — pushes every ValidationResult to DataHub as an assertion aspect so the dataset page shows a quality badge backed by your Measured Cloud validations.
  • Inbound — walks DataHub's GraphQL catalog every hour, JIT-creates Measured Cloud DataAsset rows for any dataset on a datasource you've already connected, and propagates owner / domain / tag / description.

The connector talks to DataHub's GMS endpoint directly over HTTP — no acryl-datahub SDK dependency in the worker image.

Prerequisites​

  • A DataHub deployment you control (OSS or Acryl Cloud) — Measured Cloud needs the GMS endpoint reachable from the worker process.
  • A Personal Access Token if your DataHub has METADATA_SERVICE_AUTH_ENABLED=true. Local-dev clusters without auth can skip this.

Configure the integration​

The catalog-integration row lives in public.catalog_integrations (one row per (org, kind)). A super admin or technical admin configures it through the settings API:

curl -X POST "$BASE_URL/api/v1/organizations/$ORG_ID/catalog-integrations" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"kind": "datahub",
"endpoint": "https://gms.example.com:8080",
"auth_token": "eyJhbGciOiJIUzI1NiJ9..."
}'

The auth token is AES-256-GCM-encrypted under the org's DEK before it hits Postgres; the API never returns it back. The endpoint goes through the same SSRF guard as alert webhooks (no 169.254.169.254, no RFC1918 unless your DATASOURCE_NETWORK_ALLOWLIST permits it).

To rotate the token or change the endpoint, PATCH the row — fields that you omit stay unchanged, an explicit null clears them:

curl -X PATCH "$BASE_URL/api/v1/organizations/$ORG_ID/catalog-integrations/$ID" \
-H "Authorization: Bearer $TOKEN" \
-d '{"auth_token": "eyJ...new..."}'

To pause outbound + inbound temporarily without losing the row:

curl -X PATCH "$BASE_URL/api/v1/organizations/$ORG_ID/catalog-integrations/$ID" \
-d '{"enabled": false}'

What outbound looks like in DataHub​

After every ValidationResult commit, the emit_to_datahub ARQ job posts two aspects against urn:li:assertion:<dataset_urn>::<key>, one assertion per data asset. <key> is the id of the asset's rule suite for an asset created before rule suites were retired (so its existing assertion keeps receiving runs), else the data asset's id:

  • assertionInfo — the static definition of what's being checked (the validation's name + each rule it checked on the asset, as (rule_type, kwargs)).
  • assertionRunEvent — the run record. Carries pass / total via actualAggValue / expectedAggValue plus the operator-friendly nativeResults breakdown ({total, passed, failed}).

DataHub renders the most recent assertionRunEvent as the dataset's quality badge. Every run of the asset, from any validation, re-emits against the same assertion and overwrites it in place — Measured Cloud doesn't duplicate the assertion every run.

The emit is conditional: a run only enqueues the emit_to_datahub job when the org has at least one enabled catalog_integrations row, so orgs that haven't opted in pay no extra ARQ overhead per result.

How the asset URN is built​

The outbound emitter and the inbound discoverer agree on the same URN shape:

urn:li:dataset:(urn:li:dataPlatform:<platform>,<name>,<env>)
  • <platform> — postgres, snowflake, bigquery, redshift, mysql, mssql, databricks, athena. Anything else falls through to the lowercased datasource type.
  • <name> — the Measured Cloud asset's name field. The convention is <database>.<schema>.<table> to match DataHub's, but Measured Cloud doesn't enforce three-part names.
  • <env> — PROD by default. Set DataAsset.config["catalog"]["datahub_env"] to STAGING, DEV, etc. to override per-asset; the discoverer match-back respects the env tag so a STAGING URN won't collide with a PROD-tagged asset.

The first time outbound emits successfully for an asset, Measured Cloud stamps the URN onto DataAsset.external_urn["datahub"] so the inbound discoverer can match it back on rule 1 (external URN containment) without falling through to the rule 2 fall-back.

Inbound discovery​

The hourly cron at minute 7 enumerates every enabled integration row and fans out one run_inbound_for_integration job per row. Each job:

  1. Switches to the integration's tenant schema.
  2. Pages through DataHub's GraphQL search(input: {type: DATASET, ...}) query.
  3. For each dataset, applies the three-rule match-back:
    • External URN match — external_urn["datahub"] @> <urn>.
    • (platform, name, env) match — find an asset whose datasource type maps to the same DataHub platform key, whose name matches, AND whose configured env matches.
    • JIT-create — otherwise, create a new DataAsset row on the oldest matching Datasource (one with type == platform). If no matching datasource exists, the dataset is skipped and a datahub_discover_no_matching_datasource log line lands so an operator can see why nothing got picked up.

Each pass overwrites owner_email, tags, description, and writes the domain name into config["catalog"]["datahub_domain"]. DataHub is the source of truth for those fields when the integration is enabled.

To trigger an out-of-cycle walk for one integration (useful when you've just connected a new datasource and want DataHub's assets to land immediately):

curl -X POST "$BASE_URL/api/v1/organizations/$ORG_ID/catalog-integrations/$ID/sync"

The endpoint returns 202 and the walk runs asynchronously. Watch last_synced_at on the GET response to see when it completes — this is scoped to the one integration you specified, so other tenants' rows are untouched.

DataHub deployment notes​

  • Auth disabled (local dev) — leave auth_token unset and DataHub will accept the writes without an Authorization header.
  • GMS behind nginx — the connector uses standard HTTP Bearer auth and the v1 /aspects?action=ingestProposal ingest path, which most reverse-proxy configs pass through unchanged.
  • DataHub Cloud (Acryl) — point endpoint at the GMS URL Acryl gave you (the one ending in .acryl.io); the API surface is identical to OSS DataHub.

Other native catalogs​

The catalog-integration framework now backs four first-party connectors. They share the same catalog_integrations table, the same encrypt-at-rest auth-token handling, the same SSRF guard, and the same "enqueue only when an enabled integration exists" gate — only the per-vendor wire shape differs:

  • Atlan (issue #415) — pushes assetDqStatus + a custom-metadata pass-rate attribute on the table asset. See Integrate with Atlan.
  • Data.world (issue #403) — posts a quality insight to the dataset's activity stream. See Integrate with Data.world.
  • Alation (issue #404) — pushes a DataQuality value (pass/fail status + pass-rate score) to the table object. See Integrate with Alation.

DataHub is the only one with inbound discovery today; the other three are outbound-only (their issues ask only to push results).

Follow-ups​

  • datasetProfile aspect emission — the aspect builder (build_dataset_profile_aspect) and unit tests are wired, but the worker hook that fires it after every ProfileRun write is a small follow-up (one dispatch entry, one _org_has_catalog_integration gate).
  • Atlan inbound discovery — the outbound emitter ships now; an inbound walk (mirroring DataHub's discoverer) is a follow-up.