diff --git a/docs/diagrams/architecture.mmd b/docs/diagrams/architecture.mmd deleted file mode 100644 index a14a6dd..0000000 --- a/docs/diagrams/architecture.mmd +++ /dev/null @@ -1,17 +0,0 @@ -flowchart TB - Declared[Declared architecture\n(Compose + docs)] - Runtime[Observed runtime\n(Prometheus inventory)] - Declared --> Runtime - - subgraph Monitoring["Prometheus observed jobs"] - job_container_updates["container-updates"] - job_kuma["kuma"] - job_node["node"] - job_pihole["pihole"] - job_prometheus["prometheus"] - job_proxmox_storage["proxmox-storage"] - job_telegraf["telegraf"] - job_traefik["traefik"] - end - - Runtime --> Monitoring diff --git a/docs/diagrams/monitoring-coverage.mmd b/docs/diagrams/monitoring-coverage.mmd deleted file mode 100644 index 7d18e3f..0000000 --- a/docs/diagrams/monitoring-coverage.mmd +++ /dev/null @@ -1,90 +0,0 @@ -flowchart LR - Prom[Prometheus] - - classDef scrape stroke-dasharray: 5 5; - subgraph host_docker_update_exporter["Host: docker-update-exporter"] - container_updates_docker_update_exporter_9105["container-updates\ndocker-update-exporter:9105"] - end - subgraph host_kuma_lan_ddnsgeek_com["Host: kuma.lan.ddnsgeek.com"] - kuma_kuma_lan_ddnsgeek_com["kuma\nkuma.lan.ddnsgeek.com"] - end - subgraph host_monitor_kuma["Host: monitor-kuma"] - kuma_monitor_kuma_3001["kuma\nmonitor-kuma:3001"] - end - subgraph host_nix_cache["Host: nix-cache"] - node_nix_cache_9100["node\nnix-cache:9100"] - end - subgraph host_node_exporter["Host: node-exporter"] - node_node_exporter_9100["node\nnode-exporter:9100"] - end - subgraph host_pbs_sweet_home["Host: pbs.sweet.home"] - node_pbs_sweet_home_9100["node\npbs.sweet.home:9100"] - proxmox_storage_pbs_sweet_home_9102["proxmox-storage\npbs.sweet.home:9102"] - end - subgraph host_pihole["Host: pihole"] - node_pihole_9100["node\npihole:9100"] - end - subgraph host_pihole_exporter["Host: pihole-exporter"] - pihole_pihole_exporter_9617["pihole\npihole-exporter:9617"] - end - subgraph host_prometheus["Host: prometheus"] - prometheus_prometheus_9090["prometheus\nprometheus:9090"] - end - subgraph host_pve_sweet_home["Host: pve.sweet.home"] - node_pve_sweet_home_9100["node\npve.sweet.home:9100"] - proxmox_storage_pve_sweet_home_9101["proxmox-storage\npve.sweet.home:9101"] - end - subgraph host_raspberrypi_tail13f623_ts_net["Host: raspberrypi.tail13f623.ts.net"] - container_updates_raspberrypi_tail13f623_ts_net_9105["container-updates\nraspberrypi.tail13f623.ts.net:9105"] - node_raspberrypi_tail13f623_ts_net_9100["node\nraspberrypi.tail13f623.ts.net:9100"] - telegraf_raspberrypi_tail13f623_ts_net_9273["telegraf\nraspberrypi.tail13f623.ts.net:9273"] - traefik_raspberrypi_tail13f623_ts_net_8080["traefik\nraspberrypi.tail13f623.ts.net:8080"] - end - subgraph host_server["Host: server"] - node_server_9100["node\nserver:9100"] - end - subgraph host_telegraf["Host: telegraf"] - telegraf_telegraf_9273["telegraf\ntelegraf:9273"] - end - subgraph host_traefik_lan_ddnsgeek_com["Host: traefik.lan.ddnsgeek.com"] - traefik_traefik_lan_ddnsgeek_com_8080["traefik\ntraefik.lan.ddnsgeek.com:8080"] - end - - Prom -. scrape .-> container_updates_docker_update_exporter_9105 - class container_updates_docker_update_exporter_9105 scrape; - Prom -. scrape .-> container_updates_raspberrypi_tail13f623_ts_net_9105 - class container_updates_raspberrypi_tail13f623_ts_net_9105 scrape; - Prom -. scrape .-> kuma_kuma_lan_ddnsgeek_com - class kuma_kuma_lan_ddnsgeek_com scrape; - Prom -. scrape .-> kuma_monitor_kuma_3001 - class kuma_monitor_kuma_3001 scrape; - Prom -. scrape .-> node_nix_cache_9100 - class node_nix_cache_9100 scrape; - Prom -. scrape .-> node_node_exporter_9100 - class node_node_exporter_9100 scrape; - Prom -. scrape .-> node_pbs_sweet_home_9100 - class node_pbs_sweet_home_9100 scrape; - Prom -. scrape .-> node_pihole_9100 - class node_pihole_9100 scrape; - Prom -. scrape .-> node_pve_sweet_home_9100 - class node_pve_sweet_home_9100 scrape; - Prom -. scrape .-> node_raspberrypi_tail13f623_ts_net_9100 - class node_raspberrypi_tail13f623_ts_net_9100 scrape; - Prom -. scrape .-> node_server_9100 - class node_server_9100 scrape; - Prom -. scrape .-> pihole_pihole_exporter_9617 - class pihole_pihole_exporter_9617 scrape; - Prom -. scrape .-> prometheus_prometheus_9090 - class prometheus_prometheus_9090 scrape; - Prom -. scrape .-> proxmox_storage_pbs_sweet_home_9102 - class proxmox_storage_pbs_sweet_home_9102 scrape; - Prom -. scrape .-> proxmox_storage_pve_sweet_home_9101 - class proxmox_storage_pve_sweet_home_9101 scrape; - Prom -. scrape .-> telegraf_raspberrypi_tail13f623_ts_net_9273 - class telegraf_raspberrypi_tail13f623_ts_net_9273 scrape; - Prom -. scrape .-> telegraf_telegraf_9273 - class telegraf_telegraf_9273 scrape; - Prom -. scrape .-> traefik_raspberrypi_tail13f623_ts_net_8080 - class traefik_raspberrypi_tail13f623_ts_net_8080 scrape; - Prom -. scrape .-> traefik_traefik_lan_ddnsgeek_com_8080 - class traefik_traefik_lan_ddnsgeek_com_8080 scrape; diff --git a/docs/monitoring-coverage.md b/docs/monitoring-coverage.md deleted file mode 100644 index 1144008..0000000 --- a/docs/monitoring-coverage.md +++ /dev/null @@ -1,77 +0,0 @@ -# Monitoring Coverage - -## Overview - -This page is generated from Prometheus-observed runtime inventory. It supplements declared architecture docs and does not replace static source-of-truth configuration. - -- Inventory timestamp: `2026-04-13T06:36:45Z` -- Prometheus URL: `http://prometheus:9090` -- Active scrape targets observed: `19` -- Unhealthy scrape targets observed: `0` - -## Coverage by job - -| job | active targets | unhealthy targets | -| --- | --- | --- | -| container-updates | 2 | 0 | -| kuma | 2 | 0 | -| node | 7 | 0 | -| pihole | 1 | 0 | -| prometheus | 1 | 0 | -| proxmox-storage | 2 | 0 | -| telegraf | 2 | 0 | -| traefik | 2 | 0 | - -## Coverage by instance - -| instance | jobs | health | -| --- | --- | --- | -| docker-update-exporter:9105 | container-updates | 1/1 up | -| kuma.lan.ddnsgeek.com | kuma | 1/1 up | -| monitor-kuma:3001 | kuma | 1/1 up | -| nix-cache:9100 | node | 1/1 up | -| node-exporter:9100 | node | 1/1 up | -| pbs.sweet.home:9100 | node | 1/1 up | -| pbs.sweet.home:9102 | proxmox-storage | 1/1 up | -| pihole-exporter:9617 | pihole | 1/1 up | -| pihole:9100 | node | 1/1 up | -| prometheus:9090 | prometheus | 1/1 up | -| pve.sweet.home:9100 | node | 1/1 up | -| pve.sweet.home:9101 | proxmox-storage | 1/1 up | -| raspberrypi.tail13f623.ts.net:8080 | traefik | 1/1 up | -| raspberrypi.tail13f623.ts.net:9100 | node | 1/1 up | -| raspberrypi.tail13f623.ts.net:9105 | container-updates | 1/1 up | -| raspberrypi.tail13f623.ts.net:9273 | telegraf | 1/1 up | -| server:9100 | node | 1/1 up | -| telegraf:9273 | telegraf | 1/1 up | -| traefik.lan.ddnsgeek.com:8080 | traefik | 1/1 up | - -## Coverage by service - -| service | instances | health | -| --- | --- | --- | -| unknown | docker-update-exporter:9105, kuma.lan.ddnsgeek.com, monitor-kuma:3001, nix-cache:9100, node-exporter:9100, pbs.sweet.home:9100, pbs.sweet.home:9102, pihole-exporter:9617, pihole:9100, prometheus:9090, pve.sweet.home:9100, pve.sweet.home:9101, raspberrypi.tail13f623.ts.net:8080, raspberrypi.tail13f623.ts.net:9100, raspberrypi.tail13f623.ts.net:9105, raspberrypi.tail13f623.ts.net:9273, server:9100, telegraf:9273, traefik.lan.ddnsgeek.com:8080 | 19/19 up | - -## Unhealthy targets - -| job | instance | scrape URL | health | last error | -| --- | --- | --- | --- | --- | -| none | | | | | - -## Unknowns / missing metadata - -| label | targets missing | -| --- | --- | -| exposure | 19 | -| hostname | 19 | -| hypervisor | 19 | -| network | 19 | -| service | 19 | - -Unknown or missing metadata is treated as `unknown` in generated summaries to avoid over-claiming topology. - -## Regeneration instructions - -```bash -python3 scripts/render_prometheus_docs.py --inventory-file docs/runtime/prometheus-inventory.json -``` diff --git a/docs/network.md b/docs/network.md deleted file mode 100644 index 725642e..0000000 --- a/docs/network.md +++ /dev/null @@ -1,52 +0,0 @@ -# Network and Exposure View (Prometheus Observed) - -## Overview - -This document is generated from Prometheus scrape metadata and endpoint URLs. It is an observed monitoring view and not a physical network map. - -- Inventory timestamp: `2026-04-13T06:36:45Z` -- Physical topology, VLAN mapping, and bridge membership remain unknown unless explicitly documented elsewhere. - -## Observed scrape endpoints - -| job | instance | scrape URL | network label | exposure label | -| --- | --- | --- | --- | --- | -| container-updates | docker-update-exporter:9105 | http://docker-update-exporter:9105/metrics | unknown | unknown | -| container-updates | raspberrypi.tail13f623.ts.net:9105 | http://raspberrypi.tail13f623.ts.net:9105/metrics | unknown | unknown | -| kuma | kuma.lan.ddnsgeek.com | http://kuma.lan.ddnsgeek.com/metrics | unknown | unknown | -| kuma | monitor-kuma:3001 | http://monitor-kuma:3001/metrics | unknown | unknown | -| node | nix-cache:9100 | http://nix-cache:9100/metrics | unknown | unknown | -| node | node-exporter:9100 | http://node-exporter:9100/metrics | unknown | unknown | -| node | pbs.sweet.home:9100 | http://pbs.sweet.home:9100/metrics | unknown | unknown | -| node | pihole:9100 | http://pihole:9100/metrics | unknown | unknown | -| node | pve.sweet.home:9100 | http://pve.sweet.home:9100/metrics | unknown | unknown | -| node | raspberrypi.tail13f623.ts.net:9100 | http://raspberrypi.tail13f623.ts.net:9100/metrics | unknown | unknown | -| node | server:9100 | http://server:9100/metrics | unknown | unknown | -| pihole | pihole-exporter:9617 | http://pihole-exporter:9617/metrics | unknown | unknown | -| prometheus | prometheus:9090 | http://prometheus:9090/metrics | unknown | unknown | -| proxmox-storage | pbs.sweet.home:9102 | http://pbs.sweet.home:9102/metrics | unknown | unknown | -| proxmox-storage | pve.sweet.home:9101 | http://pve.sweet.home:9101/metrics | unknown | unknown | -| telegraf | raspberrypi.tail13f623.ts.net:9273 | http://raspberrypi.tail13f623.ts.net:9273/metrics | unknown | unknown | -| telegraf | telegraf:9273 | http://telegraf:9273/metrics | unknown | unknown | -| traefik | raspberrypi.tail13f623.ts.net:8080 | http://raspberrypi.tail13f623.ts.net:8080/metrics | unknown | unknown | -| traefik | traefik.lan.ddnsgeek.com:8080 | http://traefik.lan.ddnsgeek.com:8080/metrics | unknown | unknown | - -## Internal vs public indicators - -| exposure label | targets | -| --- | --- | -| unknown | 19 | - -All indicators above are label-derived. Missing labels are rendered as `unknown`. - -## Monitoring paths - -| metrics path | observed targets | -| --- | --- | -| /metrics | 19 | - -## Unknowns and limits - -- Prometheus can confirm scrape reachability but not ownership or placement boundaries. -- No VLAN, switch, or hypervisor placement is inferred unless present in inventory labels. -- Treat this as runtime evidence to pair with declared architecture docs. diff --git a/docs/prometheus-inventory.md b/docs/prometheus-inventory.md deleted file mode 100644 index 8c8a84d..0000000 --- a/docs/prometheus-inventory.md +++ /dev/null @@ -1,125 +0,0 @@ -# Prometheus Runtime Inventory - -## 1. Overview - -- Generated at: `2026-04-13T06:36:45Z` -- Prometheus URL: `http://prometheus:9090` -- Total active targets: `19` -- Unhealthy targets: `0` -- Source type: Observed runtime telemetry (not sole source of truth). - -## 2. Scrape jobs - -| Job | Observed target count | -| --- | --- | -| container-updates | 2 | -| kuma | 2 | -| node | 7 | -| pihole | 1 | -| prometheus | 1 | -| proxmox-storage | 2 | -| telegraf | 2 | -| traefik | 2 | - -## 3. Active targets - -| Job | Instance | Health | Scrape URL | -| --- | --- | --- | --- | -| container-updates | docker-update-exporter:9105 | up | http://docker-update-exporter:9105/metrics | -| container-updates | raspberrypi.tail13f623.ts.net:9105 | up | http://raspberrypi.tail13f623.ts.net:9105/metrics | -| kuma | kuma.lan.ddnsgeek.com | up | http://kuma.lan.ddnsgeek.com/metrics | -| kuma | monitor-kuma:3001 | up | http://monitor-kuma:3001/metrics | -| node | nix-cache:9100 | up | http://nix-cache:9100/metrics | -| node | node-exporter:9100 | up | http://node-exporter:9100/metrics | -| node | pbs.sweet.home:9100 | up | http://pbs.sweet.home:9100/metrics | -| node | pihole:9100 | up | http://pihole:9100/metrics | -| node | pve.sweet.home:9100 | up | http://pve.sweet.home:9100/metrics | -| node | raspberrypi.tail13f623.ts.net:9100 | up | http://raspberrypi.tail13f623.ts.net:9100/metrics | -| node | server:9100 | up | http://server:9100/metrics | -| pihole | pihole-exporter:9617 | up | http://pihole-exporter:9617/metrics | -| prometheus | prometheus:9090 | up | http://prometheus:9090/metrics | -| proxmox-storage | pbs.sweet.home:9102 | up | http://pbs.sweet.home:9102/metrics | -| proxmox-storage | pve.sweet.home:9101 | up | http://pve.sweet.home:9101/metrics | -| telegraf | raspberrypi.tail13f623.ts.net:9273 | up | http://raspberrypi.tail13f623.ts.net:9273/metrics | -| telegraf | telegraf:9273 | up | http://telegraf:9273/metrics | -| traefik | raspberrypi.tail13f623.ts.net:8080 | up | http://raspberrypi.tail13f623.ts.net:8080/metrics | -| traefik | traefik.lan.ddnsgeek.com:8080 | up | http://traefik.lan.ddnsgeek.com:8080/metrics | - -## 4. Unhealthy targets - -| Job | Instance | Health | Last error | -| --- | --- | --- | --- | -| None | | | | - -## 5. Hosts / instances observed - -| Job | Instance | Series count | -| --- | --- | --- | -| container-updates | docker-update-exporter:9105 | 1 | -| container-updates | raspberrypi.tail13f623.ts.net:9105 | 1 | -| kuma | kuma.lan.ddnsgeek.com | 1 | -| kuma | monitor-kuma:3001 | 1 | -| node | nix-cache:9100 | 1 | -| node | node-exporter:9100 | 1 | -| node | pbs.sweet.home:9100 | 1 | -| node | pihole:9100 | 1 | -| node | pve.sweet.home:9100 | 1 | -| node | raspberrypi.tail13f623.ts.net:9100 | 1 | -| node | server:9100 | 1 | -| pihole | pihole-exporter:9617 | 1 | -| prometheus | prometheus:9090 | 1 | -| proxmox-storage | pbs.sweet.home:9102 | 1 | -| proxmox-storage | pve.sweet.home:9101 | 1 | -| telegraf | raspberrypi.tail13f623.ts.net:9273 | 1 | -| telegraf | telegraf:9273 | 1 | -| traefik | raspberrypi.tail13f623.ts.net:8080 | 1 | -| traefik | traefik.lan.ddnsgeek.com:8080 | 1 | - -## 6. Services observed - -| Service | Series count | -| --- | --- | -| | 19 | - -## 7. Network / exposure metadata observed - -| Category | Label | Series count | -| --- | --- | --- | -| network | | 19 | -| exposure | | 19 | -| role | backup | 2 | -| role | cache | 1 | -| role | docker | 6 | -| role | pihole | 2 | -| role | prometheus | 1 | -| role | proxmox | 2 | -| role | raspberrypi | 4 | -| role | server | 1 | -| hypervisor | | 19 | - -## 8. Unknowns / missing metadata - -| Missing label | Targets missing | -| --- | --- | -| exposure | 19 | -| hostname | 19 | -| hypervisor | 19 | -| network | 19 | -| service | 19 | - -Notes: - -- Prometheus runtime data is observational and not authoritative for placement/topology. -- Do not infer Proxmox host placement, VM placement, VLAN layout, or public/internal boundaries without explicit labels or additional inventory sources. - -## 9. Regeneration instructions - -```bash -export PROMETHEUS_URL="https://prometheus.example.com" -# Optional auth: -# export PROMETHEUS_BEARER_TOKEN="..." -# or export PROMETHEUS_USERNAME="..."; export PROMETHEUS_PASSWORD="..." -python3 scripts/export_prometheus_inventory.py --output-dir docs/runtime -``` - -This inventory feeds documentation and diagram workflows as an observed-runtime input alongside static repo configuration. diff --git a/docs/runtime/prometheus-inventory.json b/docs/runtime/prometheus-inventory.json deleted file mode 100644 index 2f7a523..0000000 --- a/docs/runtime/prometheus-inventory.json +++ /dev/null @@ -1,519 +0,0 @@ -{ - "exposures": { - "": 19 - }, - "generated_at": "2026-04-13T06:36:45Z", - "hypervisors": { - "": 19 - }, - "instances": { - "container-updates": { - "docker-update-exporter:9105": 1, - "raspberrypi.tail13f623.ts.net:9105": 1 - }, - "kuma": { - "kuma.lan.ddnsgeek.com": 1, - "monitor-kuma:3001": 1 - }, - "node": { - "nix-cache:9100": 1, - "node-exporter:9100": 1, - "pbs.sweet.home:9100": 1, - "pihole:9100": 1, - "pve.sweet.home:9100": 1, - "raspberrypi.tail13f623.ts.net:9100": 1, - "server:9100": 1 - }, - "pihole": { - "pihole-exporter:9617": 1 - }, - "prometheus": { - "prometheus:9090": 1 - }, - "proxmox-storage": { - "pbs.sweet.home:9102": 1, - "pve.sweet.home:9101": 1 - }, - "telegraf": { - "raspberrypi.tail13f623.ts.net:9273": 1, - "telegraf:9273": 1 - }, - "traefik": { - "raspberrypi.tail13f623.ts.net:8080": 1, - "traefik.lan.ddnsgeek.com:8080": 1 - } - }, - "jobs": { - "container-updates": 2, - "kuma": 2, - "node": 7, - "pihole": 1, - "prometheus": 1, - "proxmox-storage": 2, - "telegraf": 2, - "traefik": 2 - }, - "networks": { - "": 19 - }, - "notes": [ - "The `up` query indicates scrape success from Prometheus perspective only.", - "Use static repository architecture docs and deployment configs with this runtime export for complete diagrams." - ], - "prometheus_url": "http://prometheus:9090", - "query_observations": { - "job_count": 8, - "up_series_count": 19 - }, - "roles": { - "backup": 2, - "cache": 1, - "docker": 6, - "pihole": 2, - "prometheus": 1, - "proxmox": 2, - "raspberrypi": 4, - "server": 1 - }, - "services": { - "": 19 - }, - "targets": [ - { - "discovered_labels": { - "__address__": "docker-update-exporter:9105", - "__metrics_path__": "/metrics", - "__scheme__": "http", - "__scrape_interval__": "15s", - "__scrape_timeout__": "10s", - "job": "container-updates", - "role": "docker" - }, - "health": "up", - "instance": "docker-update-exporter:9105", - "job": "container-updates", - "labels": { - "instance": "docker-update-exporter:9105", - "job": "container-updates", - "role": "docker" - }, - "last_error": "", - "scrape_pool": "container-updates", - "scrape_url": "http://docker-update-exporter:9105/metrics" - }, - { - "discovered_labels": { - "__address__": "raspberrypi.tail13f623.ts.net:9105", - "__metrics_path__": "/metrics", - "__scheme__": "http", - "__scrape_interval__": "15s", - "__scrape_timeout__": "10s", - "job": "container-updates", - "role": "raspberrypi" - }, - "health": "up", - "instance": "raspberrypi.tail13f623.ts.net:9105", - "job": "container-updates", - "labels": { - "instance": "raspberrypi.tail13f623.ts.net:9105", - "job": "container-updates", - "role": "raspberrypi" - }, - "last_error": "", - "scrape_pool": "container-updates", - "scrape_url": "http://raspberrypi.tail13f623.ts.net:9105/metrics" - }, - { - "discovered_labels": { - "__address__": "kuma.lan.ddnsgeek.com", - "__metrics_path__": "/metrics", - "__scheme__": "http", - "__scrape_interval__": "30s", - "__scrape_timeout__": "10s", - "job": "kuma", - "role": "raspberrypi" - }, - "health": "up", - "instance": "kuma.lan.ddnsgeek.com", - "job": "kuma", - "labels": { - "instance": "kuma.lan.ddnsgeek.com", - "job": "kuma", - "role": "raspberrypi" - }, - "last_error": "", - "scrape_pool": "kuma", - "scrape_url": "http://kuma.lan.ddnsgeek.com/metrics" - }, - { - "discovered_labels": { - "__address__": "monitor-kuma:3001", - "__metrics_path__": "/metrics", - "__scheme__": "http", - "__scrape_interval__": "30s", - "__scrape_timeout__": "10s", - "job": "kuma", - "role": "docker" - }, - "health": "up", - "instance": "monitor-kuma:3001", - "job": "kuma", - "labels": { - "instance": "monitor-kuma:3001", - "job": "kuma", - "role": "docker" - }, - "last_error": "", - "scrape_pool": "kuma", - "scrape_url": "http://monitor-kuma:3001/metrics" - }, - { - "discovered_labels": { - "__address__": "nix-cache:9100", - "__metrics_path__": "/metrics", - "__scheme__": "http", - "__scrape_interval__": "15s", - "__scrape_timeout__": "10s", - "job": "node", - "role": "cache" - }, - "health": "up", - "instance": "nix-cache:9100", - "job": "node", - "labels": { - "instance": "nix-cache:9100", - "job": "node", - "role": "cache" - }, - "last_error": "", - "scrape_pool": "node", - "scrape_url": "http://nix-cache:9100/metrics" - }, - { - "discovered_labels": { - "__address__": "node-exporter:9100", - "__metrics_path__": "/metrics", - "__scheme__": "http", - "__scrape_interval__": "15s", - "__scrape_timeout__": "10s", - "job": "node", - "role": "docker" - }, - "health": "up", - "instance": "node-exporter:9100", - "job": "node", - "labels": { - "instance": "node-exporter:9100", - "job": "node", - "role": "docker" - }, - "last_error": "", - "scrape_pool": "node", - "scrape_url": "http://node-exporter:9100/metrics" - }, - { - "discovered_labels": { - "__address__": "pbs.sweet.home:9100", - "__metrics_path__": "/metrics", - "__scheme__": "http", - "__scrape_interval__": "15s", - "__scrape_timeout__": "10s", - "job": "node", - "role": "backup" - }, - "health": "up", - "instance": "pbs.sweet.home:9100", - "job": "node", - "labels": { - "instance": "pbs.sweet.home:9100", - "job": "node", - "role": "backup" - }, - "last_error": "", - "scrape_pool": "node", - "scrape_url": "http://pbs.sweet.home:9100/metrics" - }, - { - "discovered_labels": { - "__address__": "pihole:9100", - "__metrics_path__": "/metrics", - "__scheme__": "http", - "__scrape_interval__": "15s", - "__scrape_timeout__": "10s", - "job": "node", - "role": "pihole" - }, - "health": "up", - "instance": "pihole:9100", - "job": "node", - "labels": { - "instance": "pihole:9100", - "job": "node", - "role": "pihole" - }, - "last_error": "", - "scrape_pool": "node", - "scrape_url": "http://pihole:9100/metrics" - }, - { - "discovered_labels": { - "__address__": "pve.sweet.home:9100", - "__metrics_path__": "/metrics", - "__scheme__": "http", - "__scrape_interval__": "15s", - "__scrape_timeout__": "10s", - "job": "node", - "role": "proxmox" - }, - "health": "up", - "instance": "pve.sweet.home:9100", - "job": "node", - "labels": { - "instance": "pve.sweet.home:9100", - "job": "node", - "role": "proxmox" - }, - "last_error": "", - "scrape_pool": "node", - "scrape_url": "http://pve.sweet.home:9100/metrics" - }, - { - "discovered_labels": { - "__address__": "raspberrypi.tail13f623.ts.net:9100", - "__metrics_path__": "/metrics", - "__scheme__": "http", - "__scrape_interval__": "15s", - "__scrape_timeout__": "10s", - "job": "node", - "role": "raspberrypi" - }, - "health": "up", - "instance": "raspberrypi.tail13f623.ts.net:9100", - "job": "node", - "labels": { - "instance": "raspberrypi.tail13f623.ts.net:9100", - "job": "node", - "role": "raspberrypi" - }, - "last_error": "", - "scrape_pool": "node", - "scrape_url": "http://raspberrypi.tail13f623.ts.net:9100/metrics" - }, - { - "discovered_labels": { - "__address__": "server:9100", - "__metrics_path__": "/metrics", - "__scheme__": "http", - "__scrape_interval__": "15s", - "__scrape_timeout__": "10s", - "job": "node", - "role": "server" - }, - "health": "up", - "instance": "server:9100", - "job": "node", - "labels": { - "instance": "server:9100", - "job": "node", - "role": "server" - }, - "last_error": "", - "scrape_pool": "node", - "scrape_url": "http://server:9100/metrics" - }, - { - "discovered_labels": { - "__address__": "pihole-exporter:9617", - "__metrics_path__": "/metrics", - "__scheme__": "http", - "__scrape_interval__": "15s", - "__scrape_timeout__": "10s", - "job": "pihole", - "role": "pihole" - }, - "health": "up", - "instance": "pihole-exporter:9617", - "job": "pihole", - "labels": { - "instance": "pihole-exporter:9617", - "job": "pihole", - "role": "pihole" - }, - "last_error": "", - "scrape_pool": "pihole", - "scrape_url": "http://pihole-exporter:9617/metrics" - }, - { - "discovered_labels": { - "__address__": "prometheus:9090", - "__metrics_path__": "/metrics", - "__scheme__": "http", - "__scrape_interval__": "15s", - "__scrape_timeout__": "10s", - "job": "prometheus", - "role": "prometheus" - }, - "health": "up", - "instance": "prometheus:9090", - "job": "prometheus", - "labels": { - "instance": "prometheus:9090", - "job": "prometheus", - "role": "prometheus" - }, - "last_error": "", - "scrape_pool": "prometheus", - "scrape_url": "http://prometheus:9090/metrics" - }, - { - "discovered_labels": { - "__address__": "pbs.sweet.home:9102", - "__metrics_path__": "/metrics", - "__scheme__": "http", - "__scrape_interval__": "15s", - "__scrape_timeout__": "10s", - "job": "proxmox-storage", - "role": "backup", - "storage": "datastore" - }, - "health": "up", - "instance": "pbs.sweet.home:9102", - "job": "proxmox-storage", - "labels": { - "instance": "pbs.sweet.home:9102", - "job": "proxmox-storage", - "role": "backup", - "storage": "datastore" - }, - "last_error": "", - "scrape_pool": "proxmox-storage", - "scrape_url": "http://pbs.sweet.home:9102/metrics" - }, - { - "discovered_labels": { - "__address__": "pve.sweet.home:9101", - "__metrics_path__": "/metrics", - "__scheme__": "http", - "__scrape_interval__": "15s", - "__scrape_timeout__": "10s", - "job": "proxmox-storage", - "role": "proxmox", - "storage": "lvm" - }, - "health": "up", - "instance": "pve.sweet.home:9101", - "job": "proxmox-storage", - "labels": { - "instance": "pve.sweet.home:9101", - "job": "proxmox-storage", - "role": "proxmox", - "storage": "lvm" - }, - "last_error": "", - "scrape_pool": "proxmox-storage", - "scrape_url": "http://pve.sweet.home:9101/metrics" - }, - { - "discovered_labels": { - "__address__": "raspberrypi.tail13f623.ts.net:9273", - "__metrics_path__": "/metrics", - "__scheme__": "http", - "__scrape_interval__": "15s", - "__scrape_timeout__": "10s", - "job": "telegraf", - "role": "docker" - }, - "health": "up", - "instance": "raspberrypi.tail13f623.ts.net:9273", - "job": "telegraf", - "labels": { - "instance": "raspberrypi.tail13f623.ts.net:9273", - "job": "telegraf", - "role": "docker" - }, - "last_error": "", - "scrape_pool": "telegraf", - "scrape_url": "http://raspberrypi.tail13f623.ts.net:9273/metrics" - }, - { - "discovered_labels": { - "__address__": "telegraf:9273", - "__metrics_path__": "/metrics", - "__scheme__": "http", - "__scrape_interval__": "15s", - "__scrape_timeout__": "10s", - "job": "telegraf", - "role": "docker" - }, - "health": "up", - "instance": "telegraf:9273", - "job": "telegraf", - "labels": { - "instance": "telegraf:9273", - "job": "telegraf", - "role": "docker" - }, - "last_error": "", - "scrape_pool": "telegraf", - "scrape_url": "http://telegraf:9273/metrics" - }, - { - "discovered_labels": { - "__address__": "raspberrypi.tail13f623.ts.net:8080", - "__metrics_path__": "/metrics", - "__scheme__": "http", - "__scrape_interval__": "15s", - "__scrape_timeout__": "10s", - "job": "traefik", - "role": "raspberrypi" - }, - "health": "up", - "instance": "raspberrypi.tail13f623.ts.net:8080", - "job": "traefik", - "labels": { - "instance": "raspberrypi.tail13f623.ts.net:8080", - "job": "traefik", - "role": "raspberrypi" - }, - "last_error": "", - "scrape_pool": "traefik", - "scrape_url": "http://raspberrypi.tail13f623.ts.net:8080/metrics" - }, - { - "discovered_labels": { - "__address__": "traefik.lan.ddnsgeek.com:8080", - "__metrics_path__": "/metrics", - "__scheme__": "http", - "__scrape_interval__": "15s", - "__scrape_timeout__": "10s", - "job": "traefik", - "role": "docker" - }, - "health": "up", - "instance": "traefik.lan.ddnsgeek.com:8080", - "job": "traefik", - "labels": { - "instance": "traefik.lan.ddnsgeek.com:8080", - "job": "traefik", - "role": "docker" - }, - "last_error": "", - "scrape_pool": "traefik", - "scrape_url": "http://traefik.lan.ddnsgeek.com:8080/metrics" - } - ], - "unhealthy_targets": [], - "unknowns": { - "missing_label_counts": { - "exposure": 19, - "hostname": 19, - "hypervisor": 19, - "network": 19, - "service": 19 - }, - "notes": [ - "Prometheus runtime data is observational and not authoritative for placement/topology.", - "Do not infer Proxmox host placement, VM placement, VLAN layout, or public/internal boundaries without explicit labels or additional inventory sources." - ] - } -} diff --git a/docs/runtime/prometheus-query-exposure.json b/docs/runtime/prometheus-query-exposure.json deleted file mode 100644 index b5133b2..0000000 --- a/docs/runtime/prometheus-query-exposure.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "data": { - "result": [ - { - "metric": {}, - "value": [ - 1776062205.716, - "19" - ] - } - ], - "resultType": "vector" - }, - "status": "success" -} diff --git a/docs/runtime/prometheus-query-hypervisor.json b/docs/runtime/prometheus-query-hypervisor.json deleted file mode 100644 index 5fe744a..0000000 --- a/docs/runtime/prometheus-query-hypervisor.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "data": { - "result": [ - { - "metric": {}, - "value": [ - 1776062205.714, - "19" - ] - } - ], - "resultType": "vector" - }, - "status": "success" -} diff --git a/docs/runtime/prometheus-query-job-instance.json b/docs/runtime/prometheus-query-job-instance.json deleted file mode 100644 index 613b008..0000000 --- a/docs/runtime/prometheus-query-job-instance.json +++ /dev/null @@ -1,198 +0,0 @@ -{ - "data": { - "result": [ - { - "metric": { - "instance": "pbs.sweet.home:9100", - "job": "node" - }, - "value": [ - 1776062205.712, - "1" - ] - }, - { - "metric": { - "instance": "raspberrypi.tail13f623.ts.net:8080", - "job": "traefik" - }, - "value": [ - 1776062205.712, - "1" - ] - }, - { - "metric": { - "instance": "traefik.lan.ddnsgeek.com:8080", - "job": "traefik" - }, - "value": [ - 1776062205.712, - "1" - ] - }, - { - "metric": { - "instance": "pve.sweet.home:9101", - "job": "proxmox-storage" - }, - "value": [ - 1776062205.712, - "1" - ] - }, - { - "metric": { - "instance": "raspberrypi.tail13f623.ts.net:9273", - "job": "telegraf" - }, - "value": [ - 1776062205.712, - "1" - ] - }, - { - "metric": { - "instance": "docker-update-exporter:9105", - "job": "container-updates" - }, - "value": [ - 1776062205.712, - "1" - ] - }, - { - "metric": { - "instance": "pihole:9100", - "job": "node" - }, - "value": [ - 1776062205.712, - "1" - ] - }, - { - "metric": { - "instance": "pihole-exporter:9617", - "job": "pihole" - }, - "value": [ - 1776062205.712, - "1" - ] - }, - { - "metric": { - "instance": "raspberrypi.tail13f623.ts.net:9105", - "job": "container-updates" - }, - "value": [ - 1776062205.712, - "1" - ] - }, - { - "metric": { - "instance": "pve.sweet.home:9100", - "job": "node" - }, - "value": [ - 1776062205.712, - "1" - ] - }, - { - "metric": { - "instance": "prometheus:9090", - "job": "prometheus" - }, - "value": [ - 1776062205.712, - "1" - ] - }, - { - "metric": { - "instance": "telegraf:9273", - "job": "telegraf" - }, - "value": [ - 1776062205.712, - "1" - ] - }, - { - "metric": { - "instance": "raspberrypi.tail13f623.ts.net:9100", - "job": "node" - }, - "value": [ - 1776062205.712, - "1" - ] - }, - { - "metric": { - "instance": "node-exporter:9100", - "job": "node" - }, - "value": [ - 1776062205.712, - "1" - ] - }, - { - "metric": { - "instance": "pbs.sweet.home:9102", - "job": "proxmox-storage" - }, - "value": [ - 1776062205.712, - "1" - ] - }, - { - "metric": { - "instance": "nix-cache:9100", - "job": "node" - }, - "value": [ - 1776062205.712, - "1" - ] - }, - { - "metric": { - "instance": "server:9100", - "job": "node" - }, - "value": [ - 1776062205.712, - "1" - ] - }, - { - "metric": { - "instance": "kuma.lan.ddnsgeek.com", - "job": "kuma" - }, - "value": [ - 1776062205.712, - "1" - ] - }, - { - "metric": { - "instance": "monitor-kuma:3001", - "job": "kuma" - }, - "value": [ - 1776062205.712, - "1" - ] - } - ], - "resultType": "vector" - }, - "status": "success" -} diff --git a/docs/runtime/prometheus-query-jobs.json b/docs/runtime/prometheus-query-jobs.json deleted file mode 100644 index f527c7f..0000000 --- a/docs/runtime/prometheus-query-jobs.json +++ /dev/null @@ -1,80 +0,0 @@ -{ - "data": { - "result": [ - { - "metric": { - "job": "node" - }, - "value": [ - 1776062205.711, - "7" - ] - }, - { - "metric": { - "job": "traefik" - }, - "value": [ - 1776062205.711, - "2" - ] - }, - { - "metric": { - "job": "proxmox-storage" - }, - "value": [ - 1776062205.711, - "2" - ] - }, - { - "metric": { - "job": "telegraf" - }, - "value": [ - 1776062205.711, - "2" - ] - }, - { - "metric": { - "job": "container-updates" - }, - "value": [ - 1776062205.711, - "2" - ] - }, - { - "metric": { - "job": "pihole" - }, - "value": [ - 1776062205.711, - "1" - ] - }, - { - "metric": { - "job": "prometheus" - }, - "value": [ - 1776062205.711, - "1" - ] - }, - { - "metric": { - "job": "kuma" - }, - "value": [ - 1776062205.711, - "2" - ] - } - ], - "resultType": "vector" - }, - "status": "success" -} diff --git a/docs/runtime/prometheus-query-network.json b/docs/runtime/prometheus-query-network.json deleted file mode 100644 index 50d5465..0000000 --- a/docs/runtime/prometheus-query-network.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "data": { - "result": [ - { - "metric": {}, - "value": [ - 1776062205.715, - "19" - ] - } - ], - "resultType": "vector" - }, - "status": "success" -} diff --git a/docs/runtime/prometheus-query-role.json b/docs/runtime/prometheus-query-role.json deleted file mode 100644 index 74b919a..0000000 --- a/docs/runtime/prometheus-query-role.json +++ /dev/null @@ -1,80 +0,0 @@ -{ - "data": { - "result": [ - { - "metric": { - "role": "backup" - }, - "value": [ - 1776062205.714, - "2" - ] - }, - { - "metric": { - "role": "raspberrypi" - }, - "value": [ - 1776062205.714, - "4" - ] - }, - { - "metric": { - "role": "docker" - }, - "value": [ - 1776062205.714, - "6" - ] - }, - { - "metric": { - "role": "proxmox" - }, - "value": [ - 1776062205.714, - "2" - ] - }, - { - "metric": { - "role": "pihole" - }, - "value": [ - 1776062205.714, - "2" - ] - }, - { - "metric": { - "role": "prometheus" - }, - "value": [ - 1776062205.714, - "1" - ] - }, - { - "metric": { - "role": "cache" - }, - "value": [ - 1776062205.714, - "1" - ] - }, - { - "metric": { - "role": "server" - }, - "value": [ - 1776062205.714, - "1" - ] - } - ], - "resultType": "vector" - }, - "status": "success" -} diff --git a/docs/runtime/prometheus-query-service.json b/docs/runtime/prometheus-query-service.json deleted file mode 100644 index 337f0ee..0000000 --- a/docs/runtime/prometheus-query-service.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "data": { - "result": [ - { - "metric": {}, - "value": [ - 1776062205.713, - "19" - ] - } - ], - "resultType": "vector" - }, - "status": "success" -} diff --git a/docs/runtime/prometheus-query-up.json b/docs/runtime/prometheus-query-up.json deleted file mode 100644 index 12e91a5..0000000 --- a/docs/runtime/prometheus-query-up.json +++ /dev/null @@ -1,238 +0,0 @@ -{ - "data": { - "result": [ - { - "metric": { - "__name__": "up", - "instance": "pbs.sweet.home:9100", - "job": "node", - "role": "backup" - }, - "value": [ - 1776062205.71, - "1" - ] - }, - { - "metric": { - "__name__": "up", - "instance": "raspberrypi.tail13f623.ts.net:8080", - "job": "traefik", - "role": "raspberrypi" - }, - "value": [ - 1776062205.71, - "1" - ] - }, - { - "metric": { - "__name__": "up", - "instance": "traefik.lan.ddnsgeek.com:8080", - "job": "traefik", - "role": "docker" - }, - "value": [ - 1776062205.71, - "1" - ] - }, - { - "metric": { - "__name__": "up", - "instance": "pve.sweet.home:9101", - "job": "proxmox-storage", - "role": "proxmox", - "storage": "lvm" - }, - "value": [ - 1776062205.71, - "1" - ] - }, - { - "metric": { - "__name__": "up", - "instance": "raspberrypi.tail13f623.ts.net:9273", - "job": "telegraf", - "role": "docker" - }, - "value": [ - 1776062205.71, - "1" - ] - }, - { - "metric": { - "__name__": "up", - "instance": "docker-update-exporter:9105", - "job": "container-updates", - "role": "docker" - }, - "value": [ - 1776062205.71, - "1" - ] - }, - { - "metric": { - "__name__": "up", - "instance": "pihole:9100", - "job": "node", - "role": "pihole" - }, - "value": [ - 1776062205.71, - "1" - ] - }, - { - "metric": { - "__name__": "up", - "instance": "pihole-exporter:9617", - "job": "pihole", - "role": "pihole" - }, - "value": [ - 1776062205.71, - "1" - ] - }, - { - "metric": { - "__name__": "up", - "instance": "raspberrypi.tail13f623.ts.net:9105", - "job": "container-updates", - "role": "raspberrypi" - }, - "value": [ - 1776062205.71, - "1" - ] - }, - { - "metric": { - "__name__": "up", - "instance": "pve.sweet.home:9100", - "job": "node", - "role": "proxmox" - }, - "value": [ - 1776062205.71, - "1" - ] - }, - { - "metric": { - "__name__": "up", - "instance": "prometheus:9090", - "job": "prometheus", - "role": "prometheus" - }, - "value": [ - 1776062205.71, - "1" - ] - }, - { - "metric": { - "__name__": "up", - "instance": "telegraf:9273", - "job": "telegraf", - "role": "docker" - }, - "value": [ - 1776062205.71, - "1" - ] - }, - { - "metric": { - "__name__": "up", - "instance": "raspberrypi.tail13f623.ts.net:9100", - "job": "node", - "role": "raspberrypi" - }, - "value": [ - 1776062205.71, - "1" - ] - }, - { - "metric": { - "__name__": "up", - "instance": "node-exporter:9100", - "job": "node", - "role": "docker" - }, - "value": [ - 1776062205.71, - "1" - ] - }, - { - "metric": { - "__name__": "up", - "instance": "pbs.sweet.home:9102", - "job": "proxmox-storage", - "role": "backup", - "storage": "datastore" - }, - "value": [ - 1776062205.71, - "1" - ] - }, - { - "metric": { - "__name__": "up", - "instance": "nix-cache:9100", - "job": "node", - "role": "cache" - }, - "value": [ - 1776062205.71, - "1" - ] - }, - { - "metric": { - "__name__": "up", - "instance": "server:9100", - "job": "node", - "role": "server" - }, - "value": [ - 1776062205.71, - "1" - ] - }, - { - "metric": { - "__name__": "up", - "instance": "kuma.lan.ddnsgeek.com", - "job": "kuma", - "role": "raspberrypi" - }, - "value": [ - 1776062205.71, - "1" - ] - }, - { - "metric": { - "__name__": "up", - "instance": "monitor-kuma:3001", - "job": "kuma", - "role": "docker" - }, - "value": [ - 1776062205.71, - "1" - ] - } - ], - "resultType": "vector" - }, - "status": "success" -} diff --git a/docs/runtime/prometheus-targets.json b/docs/runtime/prometheus-targets.json deleted file mode 100644 index 119b6dd..0000000 --- a/docs/runtime/prometheus-targets.json +++ /dev/null @@ -1,497 +0,0 @@ -{ - "data": { - "activeTargets": [ - { - "discoveredLabels": { - "__address__": "raspberrypi.tail13f623.ts.net:9105", - "__metrics_path__": "/metrics", - "__scheme__": "http", - "__scrape_interval__": "15s", - "__scrape_timeout__": "10s", - "job": "container-updates", - "role": "raspberrypi" - }, - "globalUrl": "http://raspberrypi.tail13f623.ts.net:9105/metrics", - "health": "up", - "labels": { - "instance": "raspberrypi.tail13f623.ts.net:9105", - "job": "container-updates", - "role": "raspberrypi" - }, - "lastError": "", - "lastScrape": "2026-04-13T06:36:38.412018212Z", - "lastScrapeDuration": 0.119818002, - "scrapeInterval": "15s", - "scrapePool": "container-updates", - "scrapeTimeout": "10s", - "scrapeUrl": "http://raspberrypi.tail13f623.ts.net:9105/metrics" - }, - { - "discoveredLabels": { - "__address__": "docker-update-exporter:9105", - "__metrics_path__": "/metrics", - "__scheme__": "http", - "__scrape_interval__": "15s", - "__scrape_timeout__": "10s", - "job": "container-updates", - "role": "docker" - }, - "globalUrl": "http://docker-update-exporter:9105/metrics", - "health": "up", - "labels": { - "instance": "docker-update-exporter:9105", - "job": "container-updates", - "role": "docker" - }, - "lastError": "", - "lastScrape": "2026-04-13T06:36:35.654237456Z", - "lastScrapeDuration": 0.002254582, - "scrapeInterval": "15s", - "scrapePool": "container-updates", - "scrapeTimeout": "10s", - "scrapeUrl": "http://docker-update-exporter:9105/metrics" - }, - { - "discoveredLabels": { - "__address__": "monitor-kuma:3001", - "__metrics_path__": "/metrics", - "__scheme__": "http", - "__scrape_interval__": "30s", - "__scrape_timeout__": "10s", - "job": "kuma", - "role": "docker" - }, - "globalUrl": "http://monitor-kuma:3001/metrics", - "health": "up", - "labels": { - "instance": "monitor-kuma:3001", - "job": "kuma", - "role": "docker" - }, - "lastError": "", - "lastScrape": "2026-04-13T06:36:27.982541752Z", - "lastScrapeDuration": 0.069706065, - "scrapeInterval": "30s", - "scrapePool": "kuma", - "scrapeTimeout": "10s", - "scrapeUrl": "http://monitor-kuma:3001/metrics" - }, - { - "discoveredLabels": { - "__address__": "kuma.lan.ddnsgeek.com", - "__metrics_path__": "/metrics", - "__scheme__": "http", - "__scrape_interval__": "30s", - "__scrape_timeout__": "10s", - "job": "kuma", - "role": "raspberrypi" - }, - "globalUrl": "http://kuma.lan.ddnsgeek.com/metrics", - "health": "up", - "labels": { - "instance": "kuma.lan.ddnsgeek.com", - "job": "kuma", - "role": "raspberrypi" - }, - "lastError": "", - "lastScrape": "2026-04-13T06:36:39.961722007Z", - "lastScrapeDuration": 1.470526665, - "scrapeInterval": "30s", - "scrapePool": "kuma", - "scrapeTimeout": "10s", - "scrapeUrl": "http://kuma.lan.ddnsgeek.com/metrics" - }, - { - "discoveredLabels": { - "__address__": "pve.sweet.home:9100", - "__metrics_path__": "/metrics", - "__scheme__": "http", - "__scrape_interval__": "15s", - "__scrape_timeout__": "10s", - "job": "node", - "role": "proxmox" - }, - "globalUrl": "http://pve.sweet.home:9100/metrics", - "health": "up", - "labels": { - "instance": "pve.sweet.home:9100", - "job": "node", - "role": "proxmox" - }, - "lastError": "", - "lastScrape": "2026-04-13T06:36:45.136284048Z", - "lastScrapeDuration": 0.077552327, - "scrapeInterval": "15s", - "scrapePool": "node", - "scrapeTimeout": "10s", - "scrapeUrl": "http://pve.sweet.home:9100/metrics" - }, - { - "discoveredLabels": { - "__address__": "pbs.sweet.home:9100", - "__metrics_path__": "/metrics", - "__scheme__": "http", - "__scrape_interval__": "15s", - "__scrape_timeout__": "10s", - "job": "node", - "role": "backup" - }, - "globalUrl": "http://pbs.sweet.home:9100/metrics", - "health": "up", - "labels": { - "instance": "pbs.sweet.home:9100", - "job": "node", - "role": "backup" - }, - "lastError": "", - "lastScrape": "2026-04-13T06:36:43.953992414Z", - "lastScrapeDuration": 0.042913089, - "scrapeInterval": "15s", - "scrapePool": "node", - "scrapeTimeout": "10s", - "scrapeUrl": "http://pbs.sweet.home:9100/metrics" - }, - { - "discoveredLabels": { - "__address__": "pihole:9100", - "__metrics_path__": "/metrics", - "__scheme__": "http", - "__scrape_interval__": "15s", - "__scrape_timeout__": "10s", - "job": "node", - "role": "pihole" - }, - "globalUrl": "http://pihole:9100/metrics", - "health": "up", - "labels": { - "instance": "pihole:9100", - "job": "node", - "role": "pihole" - }, - "lastError": "", - "lastScrape": "2026-04-13T06:36:42.158765958Z", - "lastScrapeDuration": 0.027103133, - "scrapeInterval": "15s", - "scrapePool": "node", - "scrapeTimeout": "10s", - "scrapeUrl": "http://pihole:9100/metrics" - }, - { - "discoveredLabels": { - "__address__": "server:9100", - "__metrics_path__": "/metrics", - "__scheme__": "http", - "__scrape_interval__": "15s", - "__scrape_timeout__": "10s", - "job": "node", - "role": "server" - }, - "globalUrl": "http://server:9100/metrics", - "health": "up", - "labels": { - "instance": "server:9100", - "job": "node", - "role": "server" - }, - "lastError": "", - "lastScrape": "2026-04-13T06:36:31.985355223Z", - "lastScrapeDuration": 0.010159325, - "scrapeInterval": "15s", - "scrapePool": "node", - "scrapeTimeout": "10s", - "scrapeUrl": "http://server:9100/metrics" - }, - { - "discoveredLabels": { - "__address__": "nix-cache:9100", - "__metrics_path__": "/metrics", - "__scheme__": "http", - "__scrape_interval__": "15s", - "__scrape_timeout__": "10s", - "job": "node", - "role": "cache" - }, - "globalUrl": "http://nix-cache:9100/metrics", - "health": "up", - "labels": { - "instance": "nix-cache:9100", - "job": "node", - "role": "cache" - }, - "lastError": "", - "lastScrape": "2026-04-13T06:36:32.054207846Z", - "lastScrapeDuration": 0.011150377, - "scrapeInterval": "15s", - "scrapePool": "node", - "scrapeTimeout": "10s", - "scrapeUrl": "http://nix-cache:9100/metrics" - }, - { - "discoveredLabels": { - "__address__": "node-exporter:9100", - "__metrics_path__": "/metrics", - "__scheme__": "http", - "__scrape_interval__": "15s", - "__scrape_timeout__": "10s", - "job": "node", - "role": "docker" - }, - "globalUrl": "http://node-exporter:9100/metrics", - "health": "up", - "labels": { - "instance": "node-exporter:9100", - "job": "node", - "role": "docker" - }, - "lastError": "", - "lastScrape": "2026-04-13T06:36:39.839919677Z", - "lastScrapeDuration": 0.076223114, - "scrapeInterval": "15s", - "scrapePool": "node", - "scrapeTimeout": "10s", - "scrapeUrl": "http://node-exporter:9100/metrics" - }, - { - "discoveredLabels": { - "__address__": "raspberrypi.tail13f623.ts.net:9100", - "__metrics_path__": "/metrics", - "__scheme__": "http", - "__scrape_interval__": "15s", - "__scrape_timeout__": "10s", - "job": "node", - "role": "raspberrypi" - }, - "globalUrl": "http://raspberrypi.tail13f623.ts.net:9100/metrics", - "health": "up", - "labels": { - "instance": "raspberrypi.tail13f623.ts.net:9100", - "job": "node", - "role": "raspberrypi" - }, - "lastError": "", - "lastScrape": "2026-04-13T06:36:36.961379652Z", - "lastScrapeDuration": 0.292301106, - "scrapeInterval": "15s", - "scrapePool": "node", - "scrapeTimeout": "10s", - "scrapeUrl": "http://raspberrypi.tail13f623.ts.net:9100/metrics" - }, - { - "discoveredLabels": { - "__address__": "pihole-exporter:9617", - "__metrics_path__": "/metrics", - "__scheme__": "http", - "__scrape_interval__": "15s", - "__scrape_timeout__": "10s", - "job": "pihole", - "role": "pihole" - }, - "globalUrl": "http://pihole-exporter:9617/metrics", - "health": "up", - "labels": { - "instance": "pihole-exporter:9617", - "job": "pihole", - "role": "pihole" - }, - "lastError": "", - "lastScrape": "2026-04-13T06:36:43.038435285Z", - "lastScrapeDuration": 0.49729699, - "scrapeInterval": "15s", - "scrapePool": "pihole", - "scrapeTimeout": "10s", - "scrapeUrl": "http://pihole-exporter:9617/metrics" - }, - { - "discoveredLabels": { - "__address__": "prometheus:9090", - "__metrics_path__": "/metrics", - "__scheme__": "http", - "__scrape_interval__": "15s", - "__scrape_timeout__": "10s", - "job": "prometheus", - "role": "prometheus" - }, - "globalUrl": "http://prometheus:9090/metrics", - "health": "up", - "labels": { - "instance": "prometheus:9090", - "job": "prometheus", - "role": "prometheus" - }, - "lastError": "", - "lastScrape": "2026-04-13T06:36:39.065876441Z", - "lastScrapeDuration": 0.004656092, - "scrapeInterval": "15s", - "scrapePool": "prometheus", - "scrapeTimeout": "10s", - "scrapeUrl": "http://prometheus:9090/metrics" - }, - { - "discoveredLabels": { - "__address__": "pve.sweet.home:9101", - "__metrics_path__": "/metrics", - "__scheme__": "http", - "__scrape_interval__": "15s", - "__scrape_timeout__": "10s", - "job": "proxmox-storage", - "role": "proxmox", - "storage": "lvm" - }, - "globalUrl": "http://pve.sweet.home:9101/metrics", - "health": "up", - "labels": { - "instance": "pve.sweet.home:9101", - "job": "proxmox-storage", - "role": "proxmox", - "storage": "lvm" - }, - "lastError": "", - "lastScrape": "2026-04-13T06:36:44.889795345Z", - "lastScrapeDuration": 0.003225566, - "scrapeInterval": "15s", - "scrapePool": "proxmox-storage", - "scrapeTimeout": "10s", - "scrapeUrl": "http://pve.sweet.home:9101/metrics" - }, - { - "discoveredLabels": { - "__address__": "pbs.sweet.home:9102", - "__metrics_path__": "/metrics", - "__scheme__": "http", - "__scrape_interval__": "15s", - "__scrape_timeout__": "10s", - "job": "proxmox-storage", - "role": "backup", - "storage": "datastore" - }, - "globalUrl": "http://pbs.sweet.home:9102/metrics", - "health": "up", - "labels": { - "instance": "pbs.sweet.home:9102", - "job": "proxmox-storage", - "role": "backup", - "storage": "datastore" - }, - "lastError": "", - "lastScrape": "2026-04-13T06:36:32.748484232Z", - "lastScrapeDuration": 0.002049134, - "scrapeInterval": "15s", - "scrapePool": "proxmox-storage", - "scrapeTimeout": "10s", - "scrapeUrl": "http://pbs.sweet.home:9102/metrics" - }, - { - "discoveredLabels": { - "__address__": "raspberrypi.tail13f623.ts.net:9273", - "__metrics_path__": "/metrics", - "__scheme__": "http", - "__scrape_interval__": "15s", - "__scrape_timeout__": "10s", - "job": "telegraf", - "role": "docker" - }, - "globalUrl": "http://raspberrypi.tail13f623.ts.net:9273/metrics", - "health": "up", - "labels": { - "instance": "raspberrypi.tail13f623.ts.net:9273", - "job": "telegraf", - "role": "docker" - }, - "lastError": "", - "lastScrape": "2026-04-13T06:36:36.677793239Z", - "lastScrapeDuration": 0.151265608, - "scrapeInterval": "15s", - "scrapePool": "telegraf", - "scrapeTimeout": "10s", - "scrapeUrl": "http://raspberrypi.tail13f623.ts.net:9273/metrics" - }, - { - "discoveredLabels": { - "__address__": "telegraf:9273", - "__metrics_path__": "/metrics", - "__scheme__": "http", - "__scrape_interval__": "15s", - "__scrape_timeout__": "10s", - "job": "telegraf", - "role": "docker" - }, - "globalUrl": "http://telegraf:9273/metrics", - "health": "up", - "labels": { - "instance": "telegraf:9273", - "job": "telegraf", - "role": "docker" - }, - "lastError": "", - "lastScrape": "2026-04-13T06:36:44.055413407Z", - "lastScrapeDuration": 0.152691132, - "scrapeInterval": "15s", - "scrapePool": "telegraf", - "scrapeTimeout": "10s", - "scrapeUrl": "http://telegraf:9273/metrics" - }, - { - "discoveredLabels": { - "__address__": "traefik.lan.ddnsgeek.com:8080", - "__metrics_path__": "/metrics", - "__scheme__": "http", - "__scrape_interval__": "15s", - "__scrape_timeout__": "10s", - "job": "traefik", - "role": "docker" - }, - "globalUrl": "http://traefik.lan.ddnsgeek.com:8080/metrics", - "health": "up", - "labels": { - "instance": "traefik.lan.ddnsgeek.com:8080", - "job": "traefik", - "role": "docker" - }, - "lastError": "", - "lastScrape": "2026-04-13T06:36:34.549111604Z", - "lastScrapeDuration": 0.004579596, - "scrapeInterval": "15s", - "scrapePool": "traefik", - "scrapeTimeout": "10s", - "scrapeUrl": "http://traefik.lan.ddnsgeek.com:8080/metrics" - }, - { - "discoveredLabels": { - "__address__": "raspberrypi.tail13f623.ts.net:8080", - "__metrics_path__": "/metrics", - "__scheme__": "http", - "__scrape_interval__": "15s", - "__scrape_timeout__": "10s", - "job": "traefik", - "role": "raspberrypi" - }, - "globalUrl": "http://raspberrypi.tail13f623.ts.net:8080/metrics", - "health": "up", - "labels": { - "instance": "raspberrypi.tail13f623.ts.net:8080", - "job": "traefik", - "role": "raspberrypi" - }, - "lastError": "", - "lastScrape": "2026-04-13T06:36:35.550082255Z", - "lastScrapeDuration": 0.062750046, - "scrapeInterval": "15s", - "scrapePool": "traefik", - "scrapeTimeout": "10s", - "scrapeUrl": "http://raspberrypi.tail13f623.ts.net:8080/metrics" - } - ], - "droppedTargetCounts": { - "container-updates": 0, - "kuma": 0, - "node": 0, - "pihole": 0, - "prometheus": 0, - "proxmox-storage": 0, - "telegraf": 0, - "traefik": 0 - }, - "droppedTargets": [] - }, - "status": "success" -} diff --git a/scripts/export_prometheus_inventory.py b/scripts/export_prometheus_inventory.py deleted file mode 100755 index f266960..0000000 --- a/scripts/export_prometheus_inventory.py +++ /dev/null @@ -1,475 +0,0 @@ -#!/usr/bin/env python3 -"""Export Prometheus runtime inventory for documentation/diagram workflows.""" - -from __future__ import annotations - -import argparse -import json -import os -import ssl -import sys -from collections import Counter, defaultdict -from dataclasses import dataclass -from datetime import datetime, timezone -from pathlib import Path -from typing import Any -from urllib import error, parse, request - -DEFAULT_TIMEOUT = 10.0 - - -class ExportError(RuntimeError): - """Raised for expected hard failures in export flow.""" - - -@dataclass -class Config: - prometheus_url: str - output_dir: Path - timeout: float - verify_tls: bool - bearer_token: str | None - username: str | None - password: str | None - verbose: bool - - -def parse_bool(value: str | None, default: bool = True) -> bool: - if value is None: - return default - normalized = value.strip().lower() - if normalized in {"1", "true", "yes", "on"}: - return True - if normalized in {"0", "false", "no", "off"}: - return False - raise ExportError(f"Invalid boolean value: {value!r}") - - -def parse_args(argv: list[str]) -> argparse.Namespace: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--output-dir", default="docs/runtime", help="Directory for JSON artifacts") - parser.add_argument("--prometheus-url", help="Prometheus base URL") - parser.add_argument("--timeout", type=float, help="HTTP timeout in seconds") - parser.add_argument("--insecure", action="store_true", help="Disable TLS verification") - parser.add_argument("--verbose", action="store_true", help="Print progress details") - return parser.parse_args(argv) - - -def load_config(args: argparse.Namespace) -> Config: - prometheus_url = args.prometheus_url or os.environ.get("PROMETHEUS_URL") - if not prometheus_url: - raise ExportError( - "PROMETHEUS_URL is required. Set PROMETHEUS_URL or pass --prometheus-url." - ) - - timeout_value = args.timeout - if timeout_value is None: - timeout_raw = os.environ.get("PROMETHEUS_TIMEOUT") - timeout_value = float(timeout_raw) if timeout_raw else DEFAULT_TIMEOUT - - verify_tls = parse_bool(os.environ.get("PROMETHEUS_VERIFY_TLS"), default=True) - if args.insecure: - verify_tls = False - - return Config( - prometheus_url=prometheus_url.rstrip("/"), - output_dir=Path(args.output_dir), - timeout=timeout_value, - verify_tls=verify_tls, - bearer_token=os.environ.get("PROMETHEUS_BEARER_TOKEN"), - username=os.environ.get("PROMETHEUS_USERNAME"), - password=os.environ.get("PROMETHEUS_PASSWORD"), - verbose=args.verbose, - ) - - -def build_opener(config: Config) -> request.OpenerDirector: - handlers: list[Any] = [] - - if config.username and config.password and not config.bearer_token: - password_manager = request.HTTPPasswordMgrWithDefaultRealm() - password_manager.add_password(None, config.prometheus_url, config.username, config.password) - handlers.append(request.HTTPBasicAuthHandler(password_manager)) - - if config.prometheus_url.startswith("https://") and not config.verify_tls: - insecure_context = ssl.create_default_context() - insecure_context.check_hostname = False - insecure_context.verify_mode = ssl.CERT_NONE - handlers.append(request.HTTPSHandler(context=insecure_context)) - - return request.build_opener(*handlers) - - -def api_get_json( - opener: request.OpenerDirector, - config: Config, - endpoint: str, - params: dict[str, str] | None = None, -) -> dict[str, Any]: - url = f"{config.prometheus_url}{endpoint}" - if params: - url = f"{url}?{parse.urlencode(params)}" - - req = request.Request(url) - req.add_header("Accept", "application/json") - if config.bearer_token: - req.add_header("Authorization", f"Bearer {config.bearer_token}") - - try: - with opener.open(req, timeout=config.timeout) as response: - body = response.read().decode("utf-8") - except error.HTTPError as exc: - detail = exc.read().decode("utf-8", errors="replace") - raise ExportError(f"Prometheus API error for {endpoint}: HTTP {exc.code} - {detail}") from exc - except error.URLError as exc: - raise ExportError( - f"Failed to reach Prometheus at {config.prometheus_url}: {exc.reason}" - ) from exc - except TimeoutError as exc: - raise ExportError( - f"Timed out reaching Prometheus at {config.prometheus_url} after {config.timeout}s" - ) from exc - - try: - payload = json.loads(body) - except json.JSONDecodeError as exc: - raise ExportError(f"Invalid JSON returned by {endpoint}") from exc - - if payload.get("status") != "success": - raise ExportError(f"Prometheus API returned non-success status for {endpoint}: {payload}") - - return payload - - -def write_json(path: Path, payload: dict[str, Any]) -> None: - path.parent.mkdir(parents=True, exist_ok=True) - with path.open("w", encoding="utf-8") as handle: - json.dump(payload, handle, indent=2, sort_keys=True) - handle.write("\n") - - -def extract_vector(query_response: dict[str, Any]) -> list[dict[str, Any]]: - data = query_response.get("data", {}) - if data.get("resultType") != "vector": - return [] - return data.get("result", []) - - -def counter_from_vector(results: list[dict[str, Any]], label: str) -> dict[str, int]: - output: dict[str, int] = {} - for item in results: - metric = item.get("metric", {}) - key = metric.get(label, "") - value_raw = item.get("value", [None, "0"])[1] - try: - output[key] = int(float(value_raw)) - except (TypeError, ValueError): - output[key] = 0 - return dict(sorted(output.items(), key=lambda kv: kv[0])) - - -def nested_counter_from_vector( - results: list[dict[str, Any]], - parent_label: str, - child_label: str, -) -> dict[str, dict[str, int]]: - nested: dict[str, dict[str, int]] = defaultdict(dict) - for item in results: - metric = item.get("metric", {}) - parent = metric.get(parent_label, "") - child = metric.get(child_label, "") - value_raw = item.get("value", [None, "0"])[1] - try: - value = int(float(value_raw)) - except (TypeError, ValueError): - value = 0 - nested[parent][child] = value - - return { - parent: dict(sorted(children.items(), key=lambda kv: kv[0])) - for parent, children in sorted(nested.items(), key=lambda kv: kv[0]) - } - - -def summarize_targets(targets_payload: dict[str, Any]) -> tuple[list[dict[str, Any]], list[dict[str, Any]], Counter]: - data = targets_payload.get("data", {}) - active_targets = data.get("activeTargets", []) - - normalized_targets: list[dict[str, Any]] = [] - unhealthy_targets: list[dict[str, Any]] = [] - missing_labels = Counter() - - important_labels = ["hostname", "service", "role", "hypervisor", "network", "exposure"] - - for target in sorted(active_targets, key=lambda t: (t.get("labels", {}).get("job", ""), t.get("scrapeUrl", ""))): - labels = target.get("labels", {}) - discovered = target.get("discoveredLabels", {}) - - merged = dict(discovered) - merged.update(labels) - - for label in important_labels: - if label not in merged: - missing_labels[label] += 1 - - normalized = { - "job": labels.get("job", ""), - "instance": labels.get("instance", ""), - "health": target.get("health", "unknown"), - "scrape_pool": target.get("scrapePool"), - "scrape_url": target.get("scrapeUrl"), - "last_error": target.get("lastError") or "", - "labels": labels, - "discovered_labels": discovered, - } - normalized_targets.append(normalized) - - if target.get("health") != "up": - unhealthy_targets.append(normalized) - - return normalized_targets, unhealthy_targets, missing_labels - - -def build_inventory( - config: Config, - targets_payload: dict[str, Any], - query_payloads: dict[str, dict[str, Any]], -) -> dict[str, Any]: - now = datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z") - - targets, unhealthy_targets, missing_labels = summarize_targets(targets_payload) - - up_results = extract_vector(query_payloads["up"]) - jobs_results = extract_vector(query_payloads["jobs"]) - job_instance_results = extract_vector(query_payloads["job_instance"]) - - optional = { - "services": counter_from_vector(extract_vector(query_payloads["service"]), "service"), - "roles": counter_from_vector(extract_vector(query_payloads["role"]), "role"), - "hypervisors": counter_from_vector(extract_vector(query_payloads["hypervisor"]), "hypervisor"), - "networks": counter_from_vector(extract_vector(query_payloads["network"]), "network"), - "exposures": counter_from_vector(extract_vector(query_payloads["exposure"]), "exposure"), - } - - unknowns = { - "missing_label_counts": dict(sorted(missing_labels.items(), key=lambda kv: kv[0])), - "notes": [ - "Prometheus runtime data is observational and not authoritative for placement/topology.", - "Do not infer Proxmox host placement, VM placement, VLAN layout, or public/internal boundaries without explicit labels or additional inventory sources.", - ], - } - - return { - "generated_at": now, - "prometheus_url": config.prometheus_url, - "targets": targets, - "jobs": counter_from_vector(jobs_results, "job"), - "instances": nested_counter_from_vector(job_instance_results, "job", "instance"), - "services": optional["services"], - "roles": optional["roles"], - "hypervisors": optional["hypervisors"], - "networks": optional["networks"], - "exposures": optional["exposures"], - "unhealthy_targets": unhealthy_targets, - "unknowns": unknowns, - "notes": [ - "The `up` query indicates scrape success from Prometheus perspective only.", - "Use static repository architecture docs and deployment configs with this runtime export for complete diagrams.", - ], - "query_observations": { - "up_series_count": len(up_results), - "job_count": len(counter_from_vector(jobs_results, "job")), - }, - } - - -def markdown_table(headers: list[str], rows: list[list[str]]) -> str: - table = ["| " + " | ".join(headers) + " |", "| " + " | ".join(["---"] * len(headers)) + " |"] - for row in rows: - table.append("| " + " | ".join(row) + " |") - return "\n".join(table) - - -def generate_markdown(inventory: dict[str, Any]) -> str: - jobs_rows = [[job, str(count)] for job, count in inventory["jobs"].items()] or [["", "0"]] - - targets_rows = [ - [target["job"], target["instance"], target["health"], target.get("scrape_url") or ""] - for target in inventory["targets"] - ] or [["", "", "unknown", ""]] - - unhealthy_rows = [ - [target["job"], target["instance"], target["health"], target.get("last_error", "")] - for target in inventory["unhealthy_targets"] - ] or [["None", "", "", ""]] - - host_rows: list[list[str]] = [] - for job, instances in inventory["instances"].items(): - for instance, count in instances.items(): - host_rows.append([job, instance, str(count)]) - if not host_rows: - host_rows = [["", "", "0"]] - - service_rows = [[name, str(value)] for name, value in inventory["services"].items()] or [["", "0"]] - - network_rows: list[list[str]] = [] - for section in ["networks", "exposures", "roles", "hypervisors"]: - for name, value in inventory[section].items(): - network_rows.append([section[:-1], name, str(value)]) - if not network_rows: - network_rows = [["", "", "0"]] - - unknown_rows = [ - [label, str(count)] - for label, count in inventory["unknowns"].get("missing_label_counts", {}).items() - ] or [["", "0"]] - - lines = [ - "# Prometheus Runtime Inventory", - "", - "## 1. Overview", - "", - f"- Generated at: `{inventory['generated_at']}`", - f"- Prometheus URL: `{inventory['prometheus_url']}`", - f"- Total active targets: `{len(inventory['targets'])}`", - f"- Unhealthy targets: `{len(inventory['unhealthy_targets'])}`", - "- Source type: Observed runtime telemetry (not sole source of truth).", - "", - "## 2. Scrape jobs", - "", - markdown_table(["Job", "Observed target count"], jobs_rows), - "", - "## 3. Active targets", - "", - markdown_table(["Job", "Instance", "Health", "Scrape URL"], targets_rows), - "", - "## 4. Unhealthy targets", - "", - markdown_table(["Job", "Instance", "Health", "Last error"], unhealthy_rows), - "", - "## 5. Hosts / instances observed", - "", - markdown_table(["Job", "Instance", "Series count"], host_rows), - "", - "## 6. Services observed", - "", - markdown_table(["Service", "Series count"], service_rows), - "", - "## 7. Network / exposure metadata observed", - "", - markdown_table(["Category", "Label", "Series count"], network_rows), - "", - "## 8. Unknowns / missing metadata", - "", - markdown_table(["Missing label", "Targets missing"], unknown_rows), - "", - "Notes:", - "", - ] - - for note in inventory["unknowns"].get("notes", []): - lines.append(f"- {note}") - - lines.extend( - [ - "", - "## 9. Regeneration instructions", - "", - "```bash", - "export PROMETHEUS_URL=\"https://prometheus.example.com\"", - "# Optional auth:", - "# export PROMETHEUS_BEARER_TOKEN=\"...\"", - "# or export PROMETHEUS_USERNAME=\"...\"; export PROMETHEUS_PASSWORD=\"...\"", - "python3 scripts/export_prometheus_inventory.py --output-dir docs/runtime", - "```", - "", - "This inventory feeds documentation and diagram workflows as an observed-runtime input alongside static repo configuration.", - "", - ] - ) - - return "\n".join(lines) - - -def write_markdown(path: Path, text: str) -> None: - path.parent.mkdir(parents=True, exist_ok=True) - path.write_text(text, encoding="utf-8") - - -def log(config: Config, message: str) -> None: - if config.verbose: - print(message, file=sys.stderr) - - -def main(argv: list[str]) -> int: - try: - args = parse_args(argv) - config = load_config(args) - opener = build_opener(config) - - output_dir = config.output_dir - output_dir.mkdir(parents=True, exist_ok=True) - - endpoint_map = { - "targets": ("/api/v1/targets", None, "prometheus-targets.json"), - "up": ("/api/v1/query", {"query": "up"}, "prometheus-query-up.json"), - "jobs": ( - "/api/v1/query", - {"query": "count by (job) (up)"}, - "prometheus-query-jobs.json", - ), - "job_instance": ( - "/api/v1/query", - {"query": "count by (job, instance) (up)"}, - "prometheus-query-job-instance.json", - ), - "service": ( - "/api/v1/query", - {"query": "count by (service) (up)"}, - "prometheus-query-service.json", - ), - "role": ( - "/api/v1/query", - {"query": "count by (role) (up)"}, - "prometheus-query-role.json", - ), - "hypervisor": ( - "/api/v1/query", - {"query": "count by (hypervisor) (up)"}, - "prometheus-query-hypervisor.json", - ), - "network": ( - "/api/v1/query", - {"query": "count by (network) (up)"}, - "prometheus-query-network.json", - ), - "exposure": ( - "/api/v1/query", - {"query": "count by (exposure) (up)"}, - "prometheus-query-exposure.json", - ), - } - - payloads: dict[str, dict[str, Any]] = {} - for name, (endpoint, params, filename) in endpoint_map.items(): - log(config, f"Querying {name}: {endpoint}") - payload = api_get_json(opener, config, endpoint, params=params) - payloads[name] = payload - write_json(output_dir / filename, payload) - - inventory = build_inventory(config, payloads["targets"], payloads) - write_json(output_dir / "prometheus-inventory.json", inventory) - - markdown = generate_markdown(inventory) - markdown_path = output_dir.parent / "prometheus-inventory.md" - write_markdown(markdown_path, markdown) - - log(config, f"Wrote inventory artifacts to {output_dir} and {markdown_path}") - return 0 - except ExportError as exc: - print(f"ERROR: {exc}", file=sys.stderr) - return 1 - - -if __name__ == "__main__": - sys.exit(main(sys.argv[1:])) diff --git a/scripts/render_prometheus_docs.py b/scripts/render_prometheus_docs.py deleted file mode 100755 index 18cfc2a..0000000 --- a/scripts/render_prometheus_docs.py +++ /dev/null @@ -1,427 +0,0 @@ -#!/usr/bin/env python3 -"""Render Prometheus inventory into documentation and Mermaid diagrams.""" - -from __future__ import annotations - -import argparse -import json -import re -from collections import defaultdict -from pathlib import Path -from typing import Any -from urllib.parse import urlparse - -GENERATED_BEGIN = "" -GENERATED_END = "" - - -def parse_args() -> argparse.Namespace: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument( - "--inventory-file", - default="docs/runtime/prometheus-inventory.json", - help="Path to normalized Prometheus inventory JSON.", - ) - parser.add_argument("--docs-dir", default="docs", help="Documentation directory.") - parser.add_argument("--diagrams-dir", default="docs/diagrams", help="Diagram output directory.") - parser.add_argument("--readme-file", default="README.md", help="README path for regeneration notes.") - parser.add_argument("--architecture-file", default="docs/architecture.md", help="Architecture markdown path.") - parser.add_argument("--network-file", default="docs/network.md", help="Network markdown path.") - parser.add_argument("--coverage-file", default="docs/monitoring-coverage.md", help="Coverage markdown path.") - parser.add_argument("--dry-run", action="store_true", help="Print changes instead of writing files.") - parser.add_argument("--verbose", action="store_true", help="Print detailed processing output.") - return parser.parse_args() - - -def load_json(path: Path) -> dict[str, Any]: - with path.open("r", encoding="utf-8") as handle: - data = json.load(handle) - if not isinstance(data, dict): - raise ValueError(f"Inventory must be a JSON object: {path}") - return data - - -def merged_labels(target: dict[str, Any]) -> dict[str, str]: - discovered = target.get("discovered_labels") or {} - labels = target.get("labels") or {} - merged = {k: str(v) for k, v in discovered.items()} - merged.update({k: str(v) for k, v in labels.items()}) - return merged - - -def normalize_targets(inventory: dict[str, Any]) -> list[dict[str, Any]]: - normalized: list[dict[str, Any]] = [] - for target in inventory.get("targets") or []: - if not isinstance(target, dict): - continue - labels = merged_labels(target) - parsed = urlparse(str(target.get("scrape_url") or "")) - host = parsed.hostname or labels.get("hostname") or "unknown" - endpoint = parsed.path or "/metrics" - normalized.append( - { - "job": str(target.get("job") or labels.get("job") or ""), - "instance": str(target.get("instance") or labels.get("instance") or ""), - "health": str(target.get("health") or "unknown"), - "scrape_url": str(target.get("scrape_url") or ""), - "last_error": str(target.get("last_error") or ""), - "host": host, - "endpoint": endpoint, - "service": labels.get("service", "unknown"), - "role": labels.get("role", "unknown"), - "hypervisor": labels.get("hypervisor", "unknown"), - "network": labels.get("network", "unknown"), - "exposure": labels.get("exposure", "unknown"), - } - ) - normalized.sort(key=lambda t: (t["job"], t["instance"], t["scrape_url"])) - return normalized - - -def markdown_table(headers: list[str], rows: list[list[str]]) -> str: - line = "| " + " | ".join(headers) + " |" - sep = "| " + " | ".join(["---"] * len(headers)) + " |" - body = ["| " + " | ".join(row) + " |" for row in rows] - return "\n".join([line, sep, *body]) - - -def summarize_targets(targets: list[dict[str, Any]], unhealthy: list[dict[str, Any]]) -> dict[str, Any]: - by_job: dict[str, dict[str, int]] = defaultdict(lambda: {"active": 0, "unhealthy": 0}) - by_instance: dict[str, dict[str, Any]] = defaultdict(lambda: {"jobs": set(), "unhealthy": 0, "total": 0}) - by_service: dict[str, dict[str, Any]] = defaultdict(lambda: {"instances": set(), "unhealthy": 0, "total": 0}) - by_exposure: dict[str, int] = defaultdict(int) - - unhealthy_set = {(u["job"], u["instance"], u["scrape_url"]) for u in unhealthy} - - for target in targets: - job = target["job"] - instance = target["instance"] - service = target["service"] if target["service"] != "" else "unknown" - by_job[job]["active"] += 1 - key = (target["job"], target["instance"], target["scrape_url"]) - if key in unhealthy_set or target["health"] != "up": - by_job[job]["unhealthy"] += 1 - by_instance[instance]["unhealthy"] += 1 - by_service[service]["unhealthy"] += 1 - - by_instance[instance]["jobs"].add(job) - by_instance[instance]["total"] += 1 - by_service[service]["instances"].add(instance) - by_service[service]["total"] += 1 - by_exposure[target["exposure"]] += 1 - - return { - "by_job": dict(sorted(by_job.items())), - "by_instance": { - key: { - "jobs": sorted(value["jobs"]), - "unhealthy": value["unhealthy"], - "total": value["total"], - } - for key, value in sorted(by_instance.items()) - }, - "by_service": { - key: { - "instances": sorted(value["instances"]), - "unhealthy": value["unhealthy"], - "total": value["total"], - } - for key, value in sorted(by_service.items()) - }, - "by_exposure": dict(sorted(by_exposure.items())), - } - - -def render_monitoring_coverage(inventory: dict[str, Any], targets: list[dict[str, Any]]) -> str: - unhealthy = normalize_targets({"targets": inventory.get("unhealthy_targets") or []}) - summaries = summarize_targets(targets, unhealthy) - missing = (inventory.get("unknowns") or {}).get("missing_label_counts") or {} - - lines = [ - "# Monitoring Coverage", - "", - "## Overview", - "", - "This page is generated from Prometheus-observed runtime inventory. It supplements declared architecture docs and does not replace static source-of-truth configuration.", - "", - f"- Inventory timestamp: `{inventory.get('generated_at', 'unknown')}`", - f"- Prometheus URL: `{inventory.get('prometheus_url', 'unknown')}`", - f"- Active scrape targets observed: `{len(targets)}`", - f"- Unhealthy scrape targets observed: `{len(unhealthy)}`", - "", - "## Coverage by job", - "", - ] - - job_rows = [ - [job, str(data["active"]), str(data["unhealthy"])] - for job, data in summaries["by_job"].items() - ] or [["none", "0", "0"]] - lines.append(markdown_table(["job", "active targets", "unhealthy targets"], job_rows)) - - lines.extend(["", "## Coverage by instance", ""]) - instance_rows = [ - [instance, ", ".join(data["jobs"]), f"{data['total'] - data['unhealthy']}/{data['total']} up"] - for instance, data in summaries["by_instance"].items() - ] or [["none", "", ""]] - lines.append(markdown_table(["instance", "jobs", "health"], instance_rows)) - - lines.extend(["", "## Coverage by service", ""]) - service_rows = [ - [service, ", ".join(data["instances"]), f"{data['total'] - data['unhealthy']}/{data['total']} up"] - for service, data in summaries["by_service"].items() - ] or [["unknown", "", ""]] - lines.append(markdown_table(["service", "instances", "health"], service_rows)) - - lines.extend(["", "## Unhealthy targets", ""]) - unhealthy_rows = [ - [u["job"], u["instance"], u["scrape_url"], u["health"], u["last_error"] or "none"] - for u in unhealthy - ] or [["none", "", "", "", ""]] - lines.append(markdown_table(["job", "instance", "scrape URL", "health", "last error"], unhealthy_rows)) - - lines.extend(["", "## Unknowns / missing metadata", ""]) - missing_rows = [[k, str(v)] for k, v in sorted(missing.items())] or [["none", "0"]] - lines.append(markdown_table(["label", "targets missing"], missing_rows)) - lines.extend( - [ - "", - "Unknown or missing metadata is treated as `unknown` in generated summaries to avoid over-claiming topology.", - "", - "## Regeneration instructions", - "", - "```bash", - "python3 scripts/render_prometheus_docs.py --inventory-file docs/runtime/prometheus-inventory.json", - "```", - "", - ] - ) - return "\n".join(lines) - - -def render_network_doc(inventory: dict[str, Any], targets: list[dict[str, Any]]) -> str: - summaries = summarize_targets(targets, normalize_targets({"targets": inventory.get("unhealthy_targets") or []})) - - endpoint_rows = [[t["job"], t["instance"], t["scrape_url"], t["network"], t["exposure"]] for t in targets] - endpoint_rows = endpoint_rows or [["none", "", "", "", ""]] - - exposure_rows = [[exp, str(count)] for exp, count in summaries["by_exposure"].items()] or [["unknown", "0"]] - - paths = sorted({t["endpoint"] for t in targets}) - path_rows = [[path, str(sum(1 for t in targets if t["endpoint"] == path))] for path in paths] or [["/metrics", "0"]] - - lines = [ - "# Network and Exposure View (Prometheus Observed)", - "", - "## Overview", - "", - "This document is generated from Prometheus scrape metadata and endpoint URLs. It is an observed monitoring view and not a physical network map.", - "", - f"- Inventory timestamp: `{inventory.get('generated_at', 'unknown')}`", - "- Physical topology, VLAN mapping, and bridge membership remain unknown unless explicitly documented elsewhere.", - "", - "## Observed scrape endpoints", - "", - markdown_table(["job", "instance", "scrape URL", "network label", "exposure label"], endpoint_rows), - "", - "## Internal vs public indicators", - "", - markdown_table(["exposure label", "targets"], exposure_rows), - "", - "All indicators above are label-derived. Missing labels are rendered as `unknown`.", - "", - "## Monitoring paths", - "", - markdown_table(["metrics path", "observed targets"], path_rows), - "", - "## Unknowns and limits", - "", - "- Prometheus can confirm scrape reachability but not ownership or placement boundaries.", - "- No VLAN, switch, or hypervisor placement is inferred unless present in inventory labels.", - "- Treat this as runtime evidence to pair with declared architecture docs.", - "", - ] - return "\n".join(lines) - - -def render_architecture_section(inventory: dict[str, Any], targets: list[dict[str, Any]]) -> str: - summaries = summarize_targets(targets, normalize_targets({"targets": inventory.get("unhealthy_targets") or []})) - notes = inventory.get("notes") or [] - - lines = [ - "## Runtime visibility from Prometheus", - "", - GENERATED_BEGIN, - "", - "Prometheus inventory provides **observed runtime coverage** of scrape targets. It complements (but does not replace) declared architecture in Compose files and static docs.", - "", - f"- Inventory timestamp: `{inventory.get('generated_at', 'unknown')}`", - f"- Observed jobs: `{len(summaries['by_job'])}`", - f"- Observed instances: `{len(summaries['by_instance'])}`", - f"- Observed services (label-derived): `{len(summaries['by_service'])}`", - "", - "### Observed monitoring view", - "", - markdown_table( - ["job", "targets", "unhealthy"], - [[job, str(data["active"]), str(data["unhealthy"])] for job, data in summaries["by_job"].items()] or [["none", "0", "0"]], - ), - "", - "### Data sources", - "", - "- `docs/runtime/prometheus-inventory.json` (normalized runtime export)", - "- Prometheus scrape metadata (`targets` + label sets)", - "- Existing repository architecture docs for declared topology", - ] - if notes: - lines.extend(["", "### Notes from inventory", ""]) - for note in notes: - lines.append(f"- {note}") - lines.extend(["", GENERATED_END, ""]) - return "\n".join(lines) - - -def upsert_generated_section(path: Path, section_markdown: str, dry_run: bool, verbose: bool) -> None: - existing = path.read_text(encoding="utf-8") if path.exists() else "" - section_body = section_markdown - - if GENERATED_BEGIN in existing and GENERATED_END in existing: - pattern = re.compile( - rf"{re.escape(GENERATED_BEGIN)}.*?{re.escape(GENERATED_END)}", - re.DOTALL, - ) - replacement = "\n".join( - line for line in section_body.splitlines() if line.strip() not in {"## Runtime visibility from Prometheus"} - ) - updated = pattern.sub(replacement.strip(), existing) - else: - updated = existing.rstrip() + "\n\n" + section_body.strip() + "\n" - - write_file(path, updated, dry_run=dry_run, verbose=verbose) - - -def mermaid_safe_id(value: str) -> str: - safe = re.sub(r"[^a-zA-Z0-9_]", "_", value) - safe = re.sub(r"_+", "_", safe).strip("_") - return safe or "unknown" - - -def render_monitoring_mermaid(targets: list[dict[str, Any]]) -> str: - by_host: dict[str, list[dict[str, Any]]] = defaultdict(list) - for target in targets: - by_host[target["host"]].append(target) - - lines = [ - "flowchart LR", - " Prom[Prometheus]", - "", - " classDef scrape stroke-dasharray: 5 5;", - ] - - for host, host_targets in sorted(by_host.items()): - host_id = mermaid_safe_id(f"host_{host}") - lines.append(f' subgraph {host_id}["Host: {host}"]') - for target in sorted(host_targets, key=lambda t: (t["job"], t["instance"])): - tid = mermaid_safe_id(f"{target['job']}_{target['instance']}") - label = f"{target['job']}\\n{target['instance']}" - lines.append(f' {tid}["{label}"]') - lines.append(" end") - - lines.append("") - for target in targets: - tid = mermaid_safe_id(f"{target['job']}_{target['instance']}") - lines.append(f" Prom -. scrape .-> {tid}") - lines.append(f" class {tid} scrape;") - - return "\n".join(lines) + "\n" - - -def render_architecture_mermaid(targets: list[dict[str, Any]]) -> str: - jobs = sorted({t["job"] for t in targets}) - lines = [ - "flowchart TB", - " Declared[Declared architecture\\n(Compose + docs)]", - " Runtime[Observed runtime\\n(Prometheus inventory)]", - " Declared --> Runtime", - "", - ' subgraph Monitoring["Prometheus observed jobs"]', - ] - for job in jobs: - jid = mermaid_safe_id(f"job_{job}") - lines.append(f' {jid}["{job}"]') - lines.extend([" end", "", " Runtime --> Monitoring", ""]) - return "\n".join(lines) - - -def write_file(path: Path, content: str, dry_run: bool, verbose: bool) -> None: - if dry_run: - print(f"[DRY RUN] Would write: {path}") - return - path.parent.mkdir(parents=True, exist_ok=True) - path.write_text(content, encoding="utf-8") - if verbose: - print(f"Wrote {path}") - - -def update_readme(path: Path, dry_run: bool, verbose: bool) -> None: - if not path.exists(): - return - existing = path.read_text(encoding="utf-8") - marker = "## Prometheus Runtime Inventory Export" - snippet = ( - "\nRegenerate derived docs/diagrams from inventory:\n\n" - "```bash\n" - "python3 scripts/render_prometheus_docs.py --inventory-file docs/runtime/prometheus-inventory.json\n" - "```\n" - ) - if marker in existing and "scripts/render_prometheus_docs.py" not in existing: - updated = existing.replace(marker, marker + snippet) - write_file(path, updated, dry_run=dry_run, verbose=verbose) - - -def main() -> int: - args = parse_args() - inventory_path = Path(args.inventory_file) - docs_dir = Path(args.docs_dir) - diagrams_dir = Path(args.diagrams_dir) - - inventory = load_json(inventory_path) - targets = normalize_targets(inventory) - - coverage_path = Path(args.coverage_file) - network_path = Path(args.network_file) - architecture_path = Path(args.architecture_file) - - coverage_md = render_monitoring_coverage(inventory, targets) - network_md = render_network_doc(inventory, targets) - architecture_section = render_architecture_section(inventory, targets) - monitoring_mmd = render_monitoring_mermaid(targets) - architecture_mmd = render_architecture_mermaid(targets) - - def resolve_doc_path(path: Path) -> Path: - if path.is_absolute(): - return path - if len(path.parts) == 1: - return docs_dir / path - return path - - write_file(resolve_doc_path(coverage_path), coverage_md, args.dry_run, args.verbose) - write_file(resolve_doc_path(network_path), network_md, args.dry_run, args.verbose) - upsert_generated_section( - resolve_doc_path(architecture_path), - architecture_section, - args.dry_run, - args.verbose, - ) - - write_file(diagrams_dir / "monitoring-coverage.mmd", monitoring_mmd, args.dry_run, args.verbose) - write_file(diagrams_dir / "architecture.mmd", architecture_mmd, args.dry_run, args.verbose) - update_readme(Path(args.readme_file), args.dry_run, args.verbose) - - if args.verbose: - print(f"Processed {len(targets)} targets from {inventory_path}") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main())