From 04b3855874eda74183085f7f9c5c32eabb105889 Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Sun, 10 May 2026 18:11:21 -0500 Subject: [PATCH] k8s: add Starlink dish prometheus exporter + dashboard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit joshuasing/starlink_exporter 0.9.0 (Docker Hub; ghcr.io lags behind upstream releases). Targets the dish's gRPC management interface at 192.168.100.1:9200, reachable from cluster pods via 10.0.15.254 while the Starlink router is in bypass mode. Grafana dashboard "Starlink" covers throughput, PoP ping latency/drop, obstruction state + 24h total, GPS lock, and dish position/info — synced to /var/lib/grafana/dashboards/ by the existing grafana role. --- .../grafana/files/dashboards/starlink.json | 183 ++++++++++++++++++ .../argocd/apps/starlink-exporter.yaml | 23 +++ .../cluster/starlink-exporter/deployment.yaml | 55 ++++++ .../starlink-exporter/kustomization.yaml | 5 + home/cluster/starlink-exporter/service.yaml | 18 ++ 5 files changed, 284 insertions(+) create mode 100644 ansible/roles/grafana/files/dashboards/starlink.json create mode 100644 home/cluster/argocd/apps/starlink-exporter.yaml create mode 100644 home/cluster/starlink-exporter/deployment.yaml create mode 100644 home/cluster/starlink-exporter/kustomization.yaml create mode 100644 home/cluster/starlink-exporter/service.yaml diff --git a/ansible/roles/grafana/files/dashboards/starlink.json b/ansible/roles/grafana/files/dashboards/starlink.json new file mode 100644 index 0000000..df1f584 --- /dev/null +++ b/ansible/roles/grafana/files/dashboards/starlink.json @@ -0,0 +1,183 @@ +{ + "uid": "starlink", + "title": "Starlink", + "tags": ["network", "starlink"], + "schemaVersion": 38, + "version": 1, + "timezone": "", + "refresh": "30s", + "time": {"from": "now-6h", "to": "now"}, + "editable": true, + "graphTooltip": 1, + "templating": { + "list": [ + { + "name": "datasource", + "label": "Datasource", + "type": "datasource", + "query": "prometheus", + "current": {"text": "Prometheus", "value": "Prometheus", "selected": false}, + "hide": 0, + "includeAll": false, + "multi": false, + "refresh": 1, + "skipUrlSync": false + } + ] + }, + "panels": [ + {"type": "row", "id": 100, "title": "Overview", "collapsed": false, "gridPos": {"h": 1, "w": 24, "x": 0, "y": 0}}, + + {"type": "stat", "id": 1, "title": "Dish up", "datasource": "$datasource", + "gridPos": {"h": 4, "w": 4, "x": 0, "y": 1}, + "fieldConfig": {"defaults": { + "mappings": [{"type": "value", "options": {"0": {"text": "DOWN", "color": "red"}, "1": {"text": "UP", "color": "green"}}}], + "color": {"mode": "thresholds"}, + "thresholds": {"mode": "absolute", "steps": [{"color":"red","value":null},{"color":"green","value":1}]} + }}, + "options": {"reduceOptions":{"calcs":["lastNotNull"]}, "graphMode":"none","colorMode":"background","textMode":"value"}, + "targets": [{"expr": "starlink_dish_up", "refId":"A", "datasource":"$datasource"}]}, + + {"type": "stat", "id": 2, "title": "Uptime", "datasource": "$datasource", + "gridPos": {"h": 4, "w": 4, "x": 4, "y": 1}, + "fieldConfig": {"defaults": {"unit":"s","color":{"mode":"thresholds"}, + "thresholds":{"mode":"absolute","steps":[{"color":"red","value":null},{"color":"green","value":300}]}}}, + "options": {"reduceOptions":{"calcs":["lastNotNull"]}, "graphMode":"area","colorMode":"value"}, + "targets": [{"expr": "starlink_dish_uptime_seconds", "refId":"A", "datasource":"$datasource"}]}, + + {"type": "stat", "id": 3, "title": "Downlink", "datasource": "$datasource", + "gridPos": {"h": 4, "w": 4, "x": 8, "y": 1}, + "fieldConfig": {"defaults": {"unit":"bps","color":{"mode":"thresholds"}, + "thresholds":{"mode":"absolute","steps":[{"color":"red","value":null},{"color":"green","value":1000000}]}}}, + "options": {"reduceOptions":{"calcs":["lastNotNull"]}, "graphMode":"area","colorMode":"value"}, + "targets": [{"expr": "starlink_dish_downlink_throughput_bps", "refId":"A", "datasource":"$datasource"}]}, + + {"type": "stat", "id": 4, "title": "Uplink", "datasource": "$datasource", + "gridPos": {"h": 4, "w": 4, "x": 12, "y": 1}, + "fieldConfig": {"defaults": {"unit":"bps","color":{"mode":"thresholds"}, + "thresholds":{"mode":"absolute","steps":[{"color":"red","value":null},{"color":"green","value":100000}]}}}, + "options": {"reduceOptions":{"calcs":["lastNotNull"]}, "graphMode":"area","colorMode":"value"}, + "targets": [{"expr": "starlink_dish_uplink_throughput_bps", "refId":"A", "datasource":"$datasource"}]}, + + {"type": "stat", "id": 5, "title": "PoP ping", "datasource": "$datasource", + "gridPos": {"h": 4, "w": 4, "x": 16, "y": 1}, + "fieldConfig": {"defaults": {"unit":"s","color":{"mode":"thresholds"}, + "thresholds":{"mode":"absolute","steps":[{"color":"green","value":null},{"color":"yellow","value":0.05},{"color":"orange","value":0.1},{"color":"red","value":0.2}]}}}, + "options": {"reduceOptions":{"calcs":["lastNotNull"]}, "graphMode":"area","colorMode":"value"}, + "targets": [{"expr": "starlink_dish_pop_ping_latency_seconds", "refId":"A", "datasource":"$datasource"}]}, + + {"type": "stat", "id": 6, "title": "Power", "datasource": "$datasource", + "gridPos": {"h": 4, "w": 4, "x": 20, "y": 1}, + "fieldConfig": {"defaults": {"unit":"watt","color":{"mode":"thresholds"}, + "thresholds":{"mode":"absolute","steps":[{"color":"green","value":null},{"color":"yellow","value":80},{"color":"red","value":120}]}}}, + "options": {"reduceOptions":{"calcs":["lastNotNull"]}, "graphMode":"area","colorMode":"value"}, + "targets": [{"expr": "starlink_dish_power_input_watts", "refId":"A", "datasource":"$datasource"}]}, + + {"type": "row", "id": 101, "title": "Throughput", "collapsed": false, "gridPos": {"h": 1, "w": 24, "x": 0, "y": 5}}, + + {"type": "timeseries", "id": 10, "title": "Downlink (bps)", "datasource": "$datasource", + "gridPos": {"h": 8, "w": 12, "x": 0, "y": 6}, + "fieldConfig": {"defaults": {"unit":"bps","custom":{"lineWidth":1,"fillOpacity":15,"showPoints":"never"}}}, + "options": {"legend":{"showLegend":true,"placement":"bottom"},"tooltip":{"mode":"multi"}}, + "targets": [{"expr": "starlink_dish_downlink_throughput_bps", "legendFormat":"downlink", "refId":"A", "datasource":"$datasource"}]}, + + {"type": "timeseries", "id": 11, "title": "Uplink (bps)", "datasource": "$datasource", + "gridPos": {"h": 8, "w": 12, "x": 12, "y": 6}, + "fieldConfig": {"defaults": {"unit":"bps","custom":{"lineWidth":1,"fillOpacity":15,"showPoints":"never"}}}, + "options": {"legend":{"showLegend":true,"placement":"bottom"},"tooltip":{"mode":"multi"}}, + "targets": [{"expr": "starlink_dish_uplink_throughput_bps", "legendFormat":"uplink", "refId":"A", "datasource":"$datasource"}]}, + + {"type": "row", "id": 102, "title": "Latency & reliability", "collapsed": false, "gridPos": {"h": 1, "w": 24, "x": 0, "y": 14}}, + + {"type": "timeseries", "id": 20, "title": "PoP ping latency (s)", "datasource": "$datasource", + "description": "Round-trip latency to the Starlink point-of-presence. Anything under 50ms is healthy; sustained >100ms suggests congestion or weather.", + "gridPos": {"h": 8, "w": 12, "x": 0, "y": 15}, + "fieldConfig": {"defaults": {"unit":"s","custom":{"lineWidth":1,"fillOpacity":10,"showPoints":"never"}, + "thresholds":{"mode":"absolute","steps":[{"color":"green","value":null},{"color":"yellow","value":0.05},{"color":"red","value":0.1}]}}}, + "options": {"legend":{"showLegend":true,"placement":"bottom"},"tooltip":{"mode":"multi"}}, + "targets": [{"expr": "starlink_dish_pop_ping_latency_seconds", "legendFormat":"pop", "refId":"A", "datasource":"$datasource"}]}, + + {"type": "timeseries", "id": 21, "title": "PoP ping drop ratio", "datasource": "$datasource", + "description": "Fraction of pings dropped to the Starlink PoP — 0 is perfect, anything above 0 means packets are being lost upstream of the dish.", + "gridPos": {"h": 8, "w": 12, "x": 12, "y": 15}, + "fieldConfig": {"defaults": {"unit":"percentunit","min":0,"max":1,"custom":{"lineWidth":1,"fillOpacity":30,"showPoints":"never"}, + "thresholds":{"mode":"absolute","steps":[{"color":"green","value":null},{"color":"yellow","value":0.01},{"color":"red","value":0.05}]}}}, + "options": {"legend":{"showLegend":true,"placement":"bottom"},"tooltip":{"mode":"multi"}}, + "targets": [{"expr": "starlink_dish_pop_ping_drop_ratio", "legendFormat":"drop", "refId":"A", "datasource":"$datasource"}]}, + + {"type": "row", "id": 103, "title": "Obstruction & signal", "collapsed": false, "gridPos": {"h": 1, "w": 24, "x": 0, "y": 23}}, + + {"type": "stat", "id": 30, "title": "Currently obstructed", "datasource": "$datasource", + "gridPos": {"h": 4, "w": 4, "x": 0, "y": 24}, + "fieldConfig": {"defaults": { + "mappings":[{"type":"value","options":{"0":{"text":"CLEAR","color":"green"},"1":{"text":"OBSTRUCTED","color":"red"}}}], + "color":{"mode":"thresholds"}, + "thresholds":{"mode":"absolute","steps":[{"color":"green","value":null},{"color":"red","value":1}]}}}, + "options": {"reduceOptions":{"calcs":["lastNotNull"]},"graphMode":"none","colorMode":"background","textMode":"value"}, + "targets": [{"expr": "starlink_dish_currently_obstructed", "refId":"A", "datasource":"$datasource"}]}, + + {"type": "stat", "id": 31, "title": "Obstruction fraction", "datasource": "$datasource", + "description": "Fraction of the sky the dish believes is obstructed.", + "gridPos": {"h": 4, "w": 4, "x": 4, "y": 24}, + "fieldConfig": {"defaults": {"unit":"percentunit","min":0,"max":1,"color":{"mode":"thresholds"}, + "thresholds":{"mode":"absolute","steps":[{"color":"green","value":null},{"color":"yellow","value":0.05},{"color":"red","value":0.15}]}}}, + "options": {"reduceOptions":{"calcs":["lastNotNull"]},"graphMode":"area","colorMode":"value"}, + "targets": [{"expr": "starlink_dish_fraction_obstruction_ratio", "refId":"A", "datasource":"$datasource"}]}, + + {"type": "stat", "id": 32, "title": "Obstructed (last 24h)", "datasource": "$datasource", + "gridPos": {"h": 4, "w": 4, "x": 8, "y": 24}, + "fieldConfig": {"defaults": {"unit":"s","color":{"mode":"thresholds"}, + "thresholds":{"mode":"absolute","steps":[{"color":"green","value":null},{"color":"yellow","value":60},{"color":"red","value":600}]}}}, + "options": {"reduceOptions":{"calcs":["lastNotNull"]},"graphMode":"area","colorMode":"value"}, + "targets": [{"expr": "starlink_dish_last_24h_obstructed_seconds", "refId":"A", "datasource":"$datasource"}]}, + + {"type": "stat", "id": 33, "title": "GPS valid", "datasource": "$datasource", + "gridPos": {"h": 4, "w": 4, "x": 12, "y": 24}, + "fieldConfig": {"defaults": { + "mappings":[{"type":"value","options":{"0":{"text":"NO FIX","color":"red"},"1":{"text":"LOCKED","color":"green"}}}], + "color":{"mode":"thresholds"}, + "thresholds":{"mode":"absolute","steps":[{"color":"red","value":null},{"color":"green","value":1}]}}}, + "options": {"reduceOptions":{"calcs":["lastNotNull"]},"graphMode":"none","colorMode":"background","textMode":"value"}, + "targets": [{"expr": "starlink_dish_gps_valid", "refId":"A", "datasource":"$datasource"}]}, + + {"type": "stat", "id": 34, "title": "GPS satellites", "datasource": "$datasource", + "gridPos": {"h": 4, "w": 4, "x": 16, "y": 24}, + "fieldConfig": {"defaults": {"color":{"mode":"thresholds"}, + "thresholds":{"mode":"absolute","steps":[{"color":"red","value":null},{"color":"yellow","value":4},{"color":"green","value":8}]}}}, + "options": {"reduceOptions":{"calcs":["lastNotNull"]},"graphMode":"area","colorMode":"value"}, + "targets": [{"expr": "starlink_dish_gps_satellites", "refId":"A", "datasource":"$datasource"}]}, + + {"type": "stat", "id": 35, "title": "SNR above noise floor", "datasource": "$datasource", + "gridPos": {"h": 4, "w": 4, "x": 20, "y": 24}, + "fieldConfig": {"defaults": { + "mappings":[{"type":"value","options":{"0":{"text":"LOW","color":"red"},"1":{"text":"OK","color":"green"}}}], + "color":{"mode":"thresholds"}, + "thresholds":{"mode":"absolute","steps":[{"color":"red","value":null},{"color":"green","value":1}]}}}, + "options": {"reduceOptions":{"calcs":["lastNotNull"]},"graphMode":"none","colorMode":"background","textMode":"value"}, + "targets": [{"expr": "starlink_dish_snr_above_noise_floor", "refId":"A", "datasource":"$datasource"}]}, + + {"type": "row", "id": 104, "title": "Dish position & info", "collapsed": true, "gridPos": {"h": 1, "w": 24, "x": 0, "y": 28}, + "panels": [ + {"type": "stat", "id": 40, "title": "Tilt", "datasource": "$datasource", + "gridPos": {"h": 4, "w": 6, "x": 0, "y": 29}, + "fieldConfig": {"defaults": {"unit":"degree"}}, + "options": {"reduceOptions":{"calcs":["lastNotNull"]},"graphMode":"none"}, + "targets": [{"expr": "starlink_dish_tilt_angle_deg", "refId":"A", "datasource":"$datasource"}]}, + {"type": "stat", "id": 41, "title": "Boresight azimuth", "datasource": "$datasource", + "gridPos": {"h": 4, "w": 6, "x": 6, "y": 29}, + "fieldConfig": {"defaults": {"unit":"degree"}}, + "options": {"reduceOptions":{"calcs":["lastNotNull"]},"graphMode":"none"}, + "targets": [{"expr": "starlink_dish_boresight_azimuth_deg", "refId":"A", "datasource":"$datasource"}]}, + {"type": "stat", "id": 42, "title": "Boresight elevation", "datasource": "$datasource", + "gridPos": {"h": 4, "w": 6, "x": 12, "y": 29}, + "fieldConfig": {"defaults": {"unit":"degree"}}, + "options": {"reduceOptions":{"calcs":["lastNotNull"]},"graphMode":"none"}, + "targets": [{"expr": "starlink_dish_boresight_elevation_deg", "refId":"A", "datasource":"$datasource"}]}, + {"type": "table", "id": 43, "title": "Dish info", "datasource": "$datasource", + "gridPos": {"h": 6, "w": 24, "x": 0, "y": 33}, + "options": {"showHeader": true}, + "targets": [{"expr": "starlink_dish_info", "refId":"A", "instant": true, "format":"table", "datasource":"$datasource"}], + "transformations": [{"id":"organize","options":{"excludeByName":{"Time":true,"Value":true,"__name__":true,"app_kubernetes_io_instance":true,"app_kubernetes_io_name":true,"instance":true,"job":true,"namespace":true,"service":true}}}]} + ]} + ] +} diff --git a/home/cluster/argocd/apps/starlink-exporter.yaml b/home/cluster/argocd/apps/starlink-exporter.yaml new file mode 100644 index 0000000..af0c746 --- /dev/null +++ b/home/cluster/argocd/apps/starlink-exporter.yaml @@ -0,0 +1,23 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: starlink-exporter + namespace: argocd + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + project: default + source: + repoURL: https://codeberg.org/gmcintire/infra.git + targetRevision: main + path: home/cluster/starlink-exporter + destination: + server: https://kubernetes.default.svc + namespace: monitoring + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + - ServerSideApply=true diff --git a/home/cluster/starlink-exporter/deployment.yaml b/home/cluster/starlink-exporter/deployment.yaml new file mode 100644 index 0000000..452cbc9 --- /dev/null +++ b/home/cluster/starlink-exporter/deployment.yaml @@ -0,0 +1,55 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: starlink-exporter + namespace: monitoring + labels: + app.kubernetes.io/name: starlink-exporter +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: starlink-exporter + template: + metadata: + labels: + app.kubernetes.io/name: starlink-exporter + spec: + securityContext: + runAsNonRoot: true + runAsUser: 65532 + runAsGroup: 65532 + seccompProfile: + type: RuntimeDefault + containers: + - name: exporter + image: docker.io/joshuasing/starlink_exporter:0.9.0 + # Defaults already match this network: -dish 192.168.100.1:9200, + # -listen :9451. Dish is reachable via 10.0.15.254 (home router) + # while the Starlink router is in bypass mode. + ports: + - name: metrics + containerPort: 9451 + protocol: TCP + resources: + requests: + cpu: 10m + memory: 24Mi + limits: + memory: 96Mi + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: ["ALL"] + readinessProbe: + httpGet: + path: /metrics + port: metrics + periodSeconds: 15 + livenessProbe: + httpGet: + path: /metrics + port: metrics + periodSeconds: 30 + failureThreshold: 5 diff --git a/home/cluster/starlink-exporter/kustomization.yaml b/home/cluster/starlink-exporter/kustomization.yaml new file mode 100644 index 0000000..a33121c --- /dev/null +++ b/home/cluster/starlink-exporter/kustomization.yaml @@ -0,0 +1,5 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - deployment.yaml + - service.yaml diff --git a/home/cluster/starlink-exporter/service.yaml b/home/cluster/starlink-exporter/service.yaml new file mode 100644 index 0000000..b9b4463 --- /dev/null +++ b/home/cluster/starlink-exporter/service.yaml @@ -0,0 +1,18 @@ +apiVersion: v1 +kind: Service +metadata: + name: starlink-exporter + namespace: monitoring + labels: + app.kubernetes.io/name: starlink-exporter + annotations: + prometheus.io/scrape: "true" + prometheus.io/port: "9451" +spec: + selector: + app.kubernetes.io/name: starlink-exporter + ports: + - name: metrics + port: 9451 + targetPort: metrics + protocol: TCP