Monitoring Pipeline Recovery Runbook¶
When to use: Grafana dashboards show no data / stale data / alerts not firing. Use this to systematically triage which layer broke.
Estimated time: 5–15 minutes for full triage.
Architecture recap — 2 pipelines¶
Pipeline 1: Network Flows (ntopng DPI)¶
1. Source mik-main (NetFlow v9 :12055 → netflow2ng)
2. DPI engine ntopng (:3004, custom Lua endpoint)
3. Scraper scrape_ntopng.py (mikrotik-tools, every 5min)
4. Push API dp-chat-adapter (:8093) → Kafka platform.logs
5. Streaming RisingWave (:4566) — 13 MVs
6. Visualization Grafana (:3002) — 3 dashboards, 29 panels
Pipeline 2: Router Metrics (Prometheus)¶
1. Source mik-main (REST :80, API :8728)
2. Collector mktxp-exporter (:49090, scraped by otel)
3. TSDB VictoriaMetrics (:8428)
4. Alerting vmalert (:8880) → apprise-bridge → Apprise (:8008)
5. Meta-monitor mikrotik-tools scheduler (18 jobs)
Failure at any layer cascades downstream. Identify the lowest broken layer first.
Triage in 4 steps¶
Step 1 — Identify scope of missing data¶
| Symptom | Likely pipeline + layer |
|---|---|
| ntopng Grafana dashboard empty | Pipeline 1: scraper or Push API or RisingWave |
| Router metrics dashboard empty | Pipeline 2: mktxp or VM or otel |
| Single dashboard empty, others fine | Grafana datasource (check type = grafana-postgresql-datasource) |
| All dashboards empty | Grafana down OR both pipelines broken |
| Apprise silent during outage | Pipeline 2 Layer 4: vmalert/apprise-bridge |
| Apprise flood | Alert flapping OR vpn-tunnel-monitor threshold too low |
Step 2 — Verify Pipeline 1 (Network Flows)¶
# Layer 6: Grafana datasource OK?
curl -s http://192.168.100.59:3002/api/datasources/uid/risingwave/health
# Layer 5: RisingWave has data?
PGPASSWORD=postgres psql -h 192.168.100.31 -p 4566 -U root -d dev -c \
"SELECT count(*) FROM platform_logs WHERE provider='ntopng' AND timestamp > NOW() - INTERVAL '10 min'"
# Layer 4: Push API healthy?
curl -s http://192.168.100.31:8093/api/v1/health
# Layer 3: Scraper running?
docker logs mikrotik-tools 2>&1 | grep "ntopng-scrape" | tail -3
# Layer 2: ntopng has flows?
docker exec ntopng curl -s http://localhost:3004/lua/custom_flow_export.lua | python3 -c "import sys,json; d=json.load(sys.stdin); print(f'flows: {len(d.get(\"flows\",[]))}')"
# Layer 1: MikroTik sending NetFlow?
sshpass -p 'minhluc1' ssh lucndm@192.168.100.1 '/ip traffic-flow target print'
Step 3 — Verify Pipeline 2 (Router Metrics)¶
# Layer 4: alerts firing?
curl -s http://192.168.100.59:8880/api/v1/alerts | python3 -m json.tool | head -30
# Layer 3: VM responding?
curl -s 'http://192.168.100.59:8428/api/v1/query?query=up'
curl -s 'http://192.168.100.59:8428/api/v1/query?query=mktxp_system_cpu_load'
# Layer 2: exporters running?
docker ps --filter name=mktxp --format '{{.Names}}: {{.Status}}'
docker logs mktxp-exporter --tail 10
# Layer 1: mik-main alive?
ping 192.168.100.1
Step 4 — Match symptom to known failure mode¶
| Symptom | Cause | Fix |
|---|---|---|
| RisingWave MVs show NULL timestamps | Scraper sent epoch string instead of ISO 8601 | Check scrape_ntopng.py — timestamp must be datetime.isoformat() |
| Grafana shows "No data" on all panels | Datasource type wrong (postgres vs grafana-postgresql-datasource) |
Fix via Grafana API: see .claude/rules/grafana-testing.md |
| Push API returns 400 | metadata sent as JSON string instead of dict |
Check schemas.py — metadata must be Dict[str, Any] |
| Scraper log "Pushed 0 flow logs" | ntopng Lua endpoint down or ntopng not receiving NetFlow | Check ntopng container + netflow2ng + MikroTik traffic-flow target |
| mktxp logs "No network connection" | API user deleted or API service disabled | Verify /user print has mktxp and /ip service has api enabled |
| VPN failover not working | check_vpn_tunnel.py state machine stuck | Check /data/vpn_failover_state.json — may need manual reset |
| VPN route flapping | PING_COUNT too low or FAIL_THRESHOLD too low | Current: PING_COUNT=3, FAIL_THRESHOLD=2 |
Common recovery procedures¶
"ntopng scraper not pushing data"¶
# Run scraper manually
docker exec mikrotik-tools python3 -u /app/scripts/scrape_ntopng.py
# Check Push API accepted
docker exec dp-chat-adapter tail -5 /tmp/push_api.log
# Check Kafka has data
docker exec dp-chat-adapter python3 -c "
import asyncio
from aiokafka import AIOKafkaConsumer, TopicPartition
async def c():
con = AIOKafkaConsumer('platform.logs', bootstrap_servers='redpanda:9092')
await con.start()
tp = TopicPartition('platform.logs', 0)
e = await con.end_offsets([tp])
print(f'HW: {e[tp]}')
await con.stop()
asyncio.run(c())
"
"RisingWave source shows empty metadata"¶
-- Source was created with VARCHAR instead of JSONB
DROP SOURCE platform_logs CASCADE;
-- Recreate with JSONB (see risingwave-init.sql)
"VPN failover stuck on WAN"¶
# Check state
docker exec mikrotik-tools cat /data/vpn_failover_state.json
# If tier=wan, manually trigger recovery:
docker exec mikrotik-tools python3 -c "
import json
with open('/data/vpn_failover_state.json','w') as f:
json.dump({'tier':'wan','fail_count':0,'recovery_counter':5}, f)
"
# Run check — will try VN6 recovery
docker exec mikrotik-tools python3 -u /app/scripts/check_vpn_tunnel.py
Prevention checklist (post-incident)¶
- mikrotik-tools container has
restart: unless-stopped+ Docker socket mounted - Scheduler running (check:
docker logs mikrotik-tools | grep "scheduler") - Push API healthy (
curl http://192.168.100.31:8093/api/v1/health) - RisingWave healthy (
curl http://192.168.100.31:4566/health) - Grafana datasource type =
grafana-postgresql-datasource(notpostgres) - dbt models deployed (
scripts/deploy-dbt.sh streaming) - VPN failover state not stuck (check
/data/vpn_failover_state.json) - ProtonVPN IP ranges in address-list (
188.214.152.0/24,149.50.211.0/24)
Related¶
- Grafana Dashboard Testing Rule — browser-verify panels before reporting done
- Deploy Checklist — full lifecycle checklist
- VPN Routing Playbook — VPN routing changes
- ADR: Gravitino → Lakekeeper — catalog migration decision