feat(openmetadata): populate Data Insights dashboard
Assign owners to all 177 tables and descriptions + tier tags to the 18 business tables, then trigger SearchIndexing + DataInsights apps so the platform Data Insights page shows real totals, description coverage and tier distribution instead of empty plots.
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
"""Populate OpenMetadata Data Insights: assign owners (all tables),
|
||||
descriptions + tier tags (business tables), then the DataInsights app can
|
||||
produce a meaningful Data Assets dashboard (coverage > 0)."""
|
||||
import json, os, time, urllib.request, urllib.parse, urllib.error
|
||||
|
||||
OM = "http://openmetadata-server:8585/api"
|
||||
H = {"Authorization": "Bearer " + os.environ["OM_TOKEN"]}
|
||||
MO = "f0fc4958-5a46-4088-b5d0-9be137dae95d"
|
||||
BART = "e2849c82-4d42-4e85-b8c7-802181e688be"
|
||||
|
||||
|
||||
def req(method, path, body=None, ct="application/json"):
|
||||
data = json.dumps(body).encode() if body is not None else None
|
||||
h = dict(H)
|
||||
if data is not None:
|
||||
h["Content-Type"] = ct
|
||||
r = urllib.request.Request(OM + path, data=data, headers=h, method=method)
|
||||
try:
|
||||
with urllib.request.urlopen(r, timeout=60) as resp:
|
||||
return resp.status, json.loads(resp.read().decode() or "{}")
|
||||
except urllib.error.HTTPError as e:
|
||||
return e.code, e.read().decode()[:200]
|
||||
|
||||
|
||||
def patch(eid, ops):
|
||||
return req("PATCH", "/v1/tables/" + eid, ops, "application/json-patch+json")
|
||||
|
||||
|
||||
# ---- map all tables: fqn -> id --------------------------------------------
|
||||
st, d = req("GET", "/v1/search/query?q=*&index=table_search_index&size=400")
|
||||
fqn2id = {}
|
||||
for h in d["hits"]["hits"]:
|
||||
fqn2id[h["_source"]["fullyQualifiedName"]] = h["_id"]
|
||||
print("tables found:", len(fqn2id))
|
||||
|
||||
# ---- 1) owner on ALL tables (ownership coverage) --------------------------
|
||||
owned = 0
|
||||
for fqn, eid in fqn2id.items():
|
||||
st, r = patch(eid, [{"op": "add", "path": "/owners",
|
||||
"value": [{"id": MO, "type": "user"}]}])
|
||||
if st == 200:
|
||||
owned += 1
|
||||
elif st == 400: # owners already present -> replace
|
||||
st2, _ = patch(eid, [{"op": "replace", "path": "/owners",
|
||||
"value": [{"id": MO, "type": "user"}]}])
|
||||
owned += 1 if st2 == 200 else 0
|
||||
print("owners set on:", owned)
|
||||
|
||||
# ---- 2) descriptions + tier on business tables ----------------------------
|
||||
BIZ = {
|
||||
"atc_postgres.postgres.public.sales_orders": (
|
||||
"Operational sales orders - source of truth in PostgreSQL. Continuously "
|
||||
"change-captured by Debezium into Kafka and curated into the lakehouse.", "Tier.Tier1"),
|
||||
"atc_mysql.default.hr.employee_events": (
|
||||
"HR employee lifecycle events (hire/leave/transfer) in MySQL. CDC source, "
|
||||
"masked for PII in the curated layer.", "Tier.Tier1"),
|
||||
"atc_mongodb.supplychain.supplychain.events": (
|
||||
"Supply-chain events stream in MongoDB. CDC source feeding Kafka and S3.", "Tier.Tier1"),
|
||||
"atc_trino.iceberg.curated_masked.sales_orders_masked": (
|
||||
"Curated, PII-masked sales orders (Iceberg on S3). Governed analytics layer.", "Tier.Tier1"),
|
||||
"atc_trino.iceberg.curated_masked.employee_events_masked": (
|
||||
"Curated, PII-masked HR employee events (Iceberg on S3).", "Tier.Tier1"),
|
||||
"atc_trino.iceberg.hadoop.historical_sales": (
|
||||
"Historical sales offloaded from Hadoop/HDFS into Iceberg for long-term analytics.", "Tier.Tier2"),
|
||||
"atc_trino.iceberg.hadoop.historical_sales_hdfs": (
|
||||
"Historical sales materialized from HDFS via the hadoop_to_trino pipeline.", "Tier.Tier2"),
|
||||
"atc_trino.cassandra_telemetry.telemetry.device_metrics": (
|
||||
"Device telemetry time-series in Cassandra, queryable via Trino.", "Tier.Tier2"),
|
||||
'atc_trino.kafka.default."postgres-sales.public.sales_orders"': (
|
||||
"Debezium CDC stream of sales_orders (Kafka), queryable via Trino.", "Tier.Tier2"),
|
||||
'atc_trino.kafka.default."mongodb-supplychain.supplychain.events"': (
|
||||
"Debezium CDC stream of supply-chain events (Kafka), queryable via Trino.", "Tier.Tier2"),
|
||||
"atc_trino.postgres_sales.public.sales_orders": (
|
||||
"Trino read-through view of the PostgreSQL sales_orders source.", "Tier.Tier2"),
|
||||
"atc_trino.mysql_hr.hr.employee_events": (
|
||||
"Trino read-through view of the MySQL employee_events source.", "Tier.Tier2"),
|
||||
"atc_trino.mongodb_supplychain.supplychain.events": (
|
||||
"Trino read-through view of the MongoDB supply-chain events source.", "Tier.Tier2"),
|
||||
"atc_trino.postgres_sales.monitor.cdc_operations": (
|
||||
"Operational view: CDC operation counts per table.", "Tier.Tier3"),
|
||||
"atc_trino.postgres_sales.monitor.cdc_recent_events": (
|
||||
"Operational view: most recent CDC events.", "Tier.Tier3"),
|
||||
"atc_trino.postgres_sales.monitor.debezium_connectors": (
|
||||
"Operational view: Debezium connector status.", "Tier.Tier3"),
|
||||
"atc_trino.postgres_sales.monitor.kafka_topics": (
|
||||
"Operational view: Kafka topic inventory and lag.", "Tier.Tier3"),
|
||||
"atc_trino.postgres_sales.monitor.spark_applications": (
|
||||
"Operational view: Spark application run history.", "Tier.Tier3"),
|
||||
}
|
||||
done = 0
|
||||
for fqn, (desc, tier) in BIZ.items():
|
||||
eid = fqn2id.get(fqn)
|
||||
if not eid:
|
||||
print(" ! not found:", fqn); continue
|
||||
st, t = req("GET", "/v1/tables/" + eid + "?fields=tags,owners")
|
||||
ops = [{"op": "add", "path": "/description", "value": desc}]
|
||||
tags = [tg for tg in (t.get("tags") or []) if not tg.get("tagFQN", "").startswith("Tier.")]
|
||||
tags.append({"tagFQN": tier, "source": "Classification",
|
||||
"labelType": "Manual", "state": "Confirmed"})
|
||||
ops.append({"op": "add" if "tags" not in t else "replace", "path": "/tags", "value": tags})
|
||||
st2, r2 = patch(eid, ops)
|
||||
print(" biz %-55s desc+%s -> %s" % (fqn.split(".")[-1][:50], tier, st2))
|
||||
done += 1 if st2 == 200 else 0
|
||||
print("business enriched:", done)
|
||||
@@ -0,0 +1,22 @@
|
||||
import json, os, time, urllib.request, urllib.error
|
||||
|
||||
OM = "http://openmetadata-server:8585/api"
|
||||
H = {"Authorization": "Bearer " + os.environ["OM_TOKEN"]}
|
||||
|
||||
|
||||
def req(method, path, body=None):
|
||||
data = json.dumps(body).encode() if body is not None else None
|
||||
h = dict(H)
|
||||
if data is not None:
|
||||
h["Content-Type"] = "application/json"
|
||||
r = urllib.request.Request(OM + path, data=data, headers=h, method=method)
|
||||
try:
|
||||
with urllib.request.urlopen(r, timeout=60) as resp:
|
||||
return resp.status, (json.loads(resp.read().decode() or "{}"))
|
||||
except urllib.error.HTTPError as e:
|
||||
return e.code, e.read().decode()[:250]
|
||||
|
||||
|
||||
for app in ["SearchIndexingApplication", "DataInsightsApplication"]:
|
||||
st, r = req("POST", "/v1/apps/trigger/" + app, {})
|
||||
print("trigger %-30s -> %s %s" % (app, st, str(r)[:120]))
|
||||
@@ -0,0 +1,31 @@
|
||||
import json, os, time, urllib.request, urllib.parse, urllib.error
|
||||
|
||||
OM = "http://openmetadata-server:8585/api"
|
||||
H = {"Authorization": "Bearer " + os.environ["OM_TOKEN"]}
|
||||
now = int(time.time() * 1000)
|
||||
start = now - 7 * 86400000
|
||||
|
||||
|
||||
def get(path):
|
||||
r = urllib.request.Request(OM + path, headers=H)
|
||||
try:
|
||||
with urllib.request.urlopen(r, timeout=30) as resp:
|
||||
return resp.status, json.loads(resp.read().decode())
|
||||
except urllib.error.HTTPError as e:
|
||||
return e.code, e.read().decode()[:160]
|
||||
|
||||
|
||||
charts = [
|
||||
"total_data_assets",
|
||||
"percentage_of_data_asset_with_owner",
|
||||
"percentage_of_data_asset_with_description",
|
||||
"total_data_assets_by_tier",
|
||||
]
|
||||
for c in charts:
|
||||
p = "/v1/analytics/dataInsights/system/charts/name/%s/data?start=%d&end=%d" % (c, start, now)
|
||||
st, r = get(p)
|
||||
summary = r
|
||||
if isinstance(r, dict):
|
||||
res = r.get("results") or r.get("data") or []
|
||||
summary = "results=%d e.g. %s" % (len(res), json.dumps(res[:2])[:220])
|
||||
print("%-45s %s %s" % (c, st, summary))
|
||||
Reference in New Issue
Block a user