265 lines
9.0 KiB
Python
265 lines
9.0 KiB
Python
#!/usr/bin/env python3
|
|
"""Collect Debezium, Kafka CDC, and Spark metrics into postgres monitor schema."""
|
|
import json
|
|
import shlex
|
|
import subprocess
|
|
import urllib.request
|
|
from collections import Counter
|
|
from datetime import datetime, timezone
|
|
|
|
import psycopg2
|
|
|
|
PG_DSN = "host=10.0.21.51 dbname=postgres user=mo password=Dell2026!"
|
|
KAFKA = "10.0.21.36:9092"
|
|
DEBEZIUM = "http://localhost:8083" # Kafka Connect on kafka01; fallback lake01 :8083
|
|
SPARK_MASTER = "http://10.0.21.50:8080"
|
|
|
|
CDC_TOPICS = [
|
|
("PostgreSQL", "postgres-sales.public.sales_orders"),
|
|
("MongoDB", "mongodb-supplychain.supplychain.events"),
|
|
]
|
|
|
|
OP_LABELS = {"c": "INSERT", "u": "UPDATE", "d": "DELETE", "r": "SNAPSHOT", "i": "INSERT"}
|
|
|
|
|
|
def fetch_json(url, timeout=10):
|
|
with urllib.request.urlopen(url, timeout=timeout) as r:
|
|
return json.loads(r.read().decode())
|
|
|
|
|
|
def collect_debezium(cur):
|
|
connectors = fetch_json(f"{DEBEZIUM}/connectors")
|
|
cur.execute("DELETE FROM monitor.debezium_connectors")
|
|
now = datetime.now(timezone.utc)
|
|
for name in connectors:
|
|
try:
|
|
st = fetch_json(f"{DEBEZIUM}/connectors/{name}/status")
|
|
except Exception as e:
|
|
cur.execute(
|
|
"""INSERT INTO monitor.debezium_connectors
|
|
(connector_name, state, task_state, worker_id, checked_at)
|
|
VALUES (%s,%s,%s,%s,%s)""",
|
|
(name, "ERROR", str(e)[:32], "", now),
|
|
)
|
|
continue
|
|
conn_state = st.get("connector", {}).get("state", "UNKNOWN")
|
|
tasks = st.get("tasks") or []
|
|
task_state = tasks[0].get("state", "NONE") if tasks else "NONE"
|
|
worker = st.get("connector", {}).get("worker_id", "")
|
|
cur.execute(
|
|
"""INSERT INTO monitor.debezium_connectors
|
|
(connector_name, state, task_state, worker_id, checked_at)
|
|
VALUES (%s,%s,%s,%s,%s)""",
|
|
(name, conn_state, task_state, worker, now),
|
|
)
|
|
|
|
|
|
KAFKA_BIN = "/opt/kafka/bin"
|
|
USE_SSH_KAFKA = False # set True when running off-host
|
|
|
|
|
|
def _kafka_cmd(bin_name, args):
|
|
parts = [f"{KAFKA_BIN}/{bin_name}"] + list(args)
|
|
if USE_SSH_KAFKA:
|
|
remote = " ".join(shlex.quote(p) for p in parts)
|
|
full = f"ssh -o StrictHostKeyChecking=no root@10.0.21.36 {remote}"
|
|
return subprocess.check_output(full, shell=True, stderr=subprocess.DEVNULL, timeout=90, text=True)
|
|
return subprocess.check_output(parts, stderr=subprocess.DEVNULL, timeout=90, text=True)
|
|
|
|
|
|
def kafka_end_offsets(topic):
|
|
try:
|
|
out = _kafka_cmd(
|
|
"kafka-run-class.sh",
|
|
[
|
|
"kafka.tools.GetOffsetShell",
|
|
"--broker-list",
|
|
"localhost:9092",
|
|
"--topic",
|
|
topic,
|
|
],
|
|
)
|
|
except Exception:
|
|
return []
|
|
rows = []
|
|
for line in out.strip().splitlines():
|
|
parts = line.split(":")
|
|
if len(parts) >= 3:
|
|
rows.append((int(parts[1]), int(parts[2])))
|
|
return rows
|
|
|
|
|
|
def sample_topic_messages(topic, max_msgs=3000, tail=5000):
|
|
"""Sample recent messages using kafka-console-consumer from tail."""
|
|
offsets = kafka_end_offsets(topic)
|
|
if not offsets:
|
|
return []
|
|
# Pick partition 0 for sampling
|
|
part, end = offsets[0]
|
|
start = max(0, end - tail)
|
|
try:
|
|
out = _kafka_cmd(
|
|
"kafka-console-consumer.sh",
|
|
[
|
|
"--bootstrap-server",
|
|
"localhost:9092",
|
|
"--topic",
|
|
topic,
|
|
"--partition",
|
|
str(part),
|
|
"--offset",
|
|
str(start),
|
|
"--max-messages",
|
|
str(min(max_msgs, tail)),
|
|
"--timeout-ms",
|
|
"15000",
|
|
],
|
|
)
|
|
except Exception:
|
|
return []
|
|
return [ln for ln in out.strip().split("\n") if ln.strip()]
|
|
|
|
|
|
def parse_debezium_line(line):
|
|
try:
|
|
doc = json.loads(line)
|
|
payload = doc.get("payload") or doc
|
|
op = payload.get("op") or payload.get("operationType") or "?"
|
|
src = payload.get("source") or {}
|
|
table = src.get("table") or src.get("collection") or ""
|
|
ts_ms = payload.get("ts_ms") or src.get("ts_ms")
|
|
after = payload.get("after") or {}
|
|
before = payload.get("before") or {}
|
|
row = after if after else before
|
|
key = str(row.get("order_id") or row.get("event_id") or row.get("_id") or "")[:200]
|
|
detail = str(row.get("region") or row.get("type") or row.get("department") or "")[:200]
|
|
event_ts = None
|
|
if ts_ms:
|
|
event_ts = datetime.fromtimestamp(int(ts_ms) / 1000, tz=timezone.utc)
|
|
return op, table, key, detail, event_ts
|
|
except Exception:
|
|
return None
|
|
|
|
|
|
def collect_kafka_cdc(cur):
|
|
now = datetime.now(timezone.utc)
|
|
cur.execute("DELETE FROM monitor.kafka_topics")
|
|
cur.execute("DELETE FROM monitor.cdc_operations")
|
|
cur.execute("DELETE FROM monitor.cdc_recent_events")
|
|
|
|
for source, topic in CDC_TOPICS:
|
|
for part, end in kafka_end_offsets(topic):
|
|
cur.execute(
|
|
"""INSERT INTO monitor.kafka_topics (topic, partition_id, end_offset, checked_at)
|
|
VALUES (%s,%s,%s,%s)""",
|
|
(topic, part, end, now),
|
|
)
|
|
|
|
lines = sample_topic_messages(topic, max_msgs=2000, tail=3000)
|
|
ops = Counter()
|
|
recent = []
|
|
for line in lines:
|
|
parsed = parse_debezium_line(line)
|
|
if not parsed:
|
|
continue
|
|
op, table, key, detail, event_ts = parsed
|
|
ops[op] += 1
|
|
if len(recent) < 100:
|
|
recent.append((op, table, key, detail, event_ts))
|
|
|
|
for op, cnt in ops.items():
|
|
cur.execute(
|
|
"""INSERT INTO monitor.cdc_operations
|
|
(source_system, topic, operation, operation_label, event_count, checked_at)
|
|
VALUES (%s,%s,%s,%s,%s,%s)""",
|
|
(source, topic, op, OP_LABELS.get(op, op), cnt, now),
|
|
)
|
|
|
|
for op, table, key, detail, event_ts in recent[:50]:
|
|
cur.execute(
|
|
"""INSERT INTO monitor.cdc_recent_events
|
|
(source_system, topic, operation, operation_label, table_name,
|
|
record_key, detail, event_ts, sampled_at)
|
|
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s)""",
|
|
(
|
|
source,
|
|
topic,
|
|
op,
|
|
OP_LABELS.get(op, op),
|
|
table,
|
|
key,
|
|
detail,
|
|
event_ts,
|
|
now,
|
|
),
|
|
)
|
|
|
|
|
|
def collect_spark(cur):
|
|
now = datetime.now(timezone.utc)
|
|
cur.execute("DELETE FROM monitor.spark_applications")
|
|
try:
|
|
data = fetch_json(f"{SPARK_MASTER}/json/", timeout=5)
|
|
apps = []
|
|
if isinstance(data, dict):
|
|
# Standalone master JSON
|
|
for a in data.get("activeapps", []) or []:
|
|
apps.append(a)
|
|
for a in data.get("completedapps", []) or []:
|
|
apps.append(a)
|
|
for a in apps[:20]:
|
|
cur.execute(
|
|
"""INSERT INTO monitor.spark_applications
|
|
(app_id, app_name, state, cores, memory_mb, duration_sec, checked_at)
|
|
VALUES (%s,%s,%s,%s,%s,%s,%s)
|
|
ON CONFLICT (app_id) DO UPDATE SET
|
|
app_name=EXCLUDED.app_name, state=EXCLUDED.state,
|
|
cores=EXCLUDED.cores, memory_mb=EXCLUDED.memory_mb,
|
|
duration_sec=EXCLUDED.duration_sec, checked_at=EXCLUDED.checked_at""",
|
|
(
|
|
a.get("id", "unknown"),
|
|
a.get("name", "Spark App"),
|
|
"RUNNING" if "attempts" not in a else "COMPLETED",
|
|
int(a.get("cores", 0) or 0),
|
|
int((a.get("memory", 0) or 0) / 1024 / 1024),
|
|
int(a.get("duration", 0) / 1000) if a.get("duration") else 0,
|
|
now,
|
|
),
|
|
)
|
|
except Exception as e:
|
|
# Placeholder row so dashboard shows Spark host status
|
|
cur.execute(
|
|
"""INSERT INTO monitor.spark_applications
|
|
(app_id, app_name, state, cores, memory_mb, duration_sec, checked_at)
|
|
VALUES (%s,%s,%s,%s,%s,%s,%s)
|
|
ON CONFLICT (app_id) DO UPDATE SET state=EXCLUDED.state, checked_at=EXCLUDED.checked_at""",
|
|
(
|
|
"spark-master",
|
|
f"Spark Master @ {SPARK_MASTER}",
|
|
"REACHABLE" if "Connection" not in str(e) else "UNREACHABLE",
|
|
0,
|
|
0,
|
|
0,
|
|
now,
|
|
),
|
|
)
|
|
|
|
|
|
def main():
|
|
conn = psycopg2.connect(PG_DSN)
|
|
conn.autocommit = True
|
|
cur = conn.cursor()
|
|
print("Collecting Debezium...")
|
|
collect_debezium(cur)
|
|
print("Collecting Kafka CDC samples...")
|
|
collect_kafka_cdc(cur)
|
|
print("Collecting Spark...")
|
|
collect_spark(cur)
|
|
cur.close()
|
|
conn.close()
|
|
print("Done.")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|