chore: merge dev into main [skip ci] #19
@@ -1,186 +0,0 @@
|
||||
networks:
|
||||
monitoring:
|
||||
driver: bridge
|
||||
volumes:
|
||||
prometheus_data: null
|
||||
grafana_data: null
|
||||
services:
|
||||
prometheus:
|
||||
image: prom/prometheus:latest
|
||||
container_name: prometheus
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 127.0.0.1:9091:9090
|
||||
volumes:
|
||||
- ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
|
||||
- ./targets:/etc/prometheus/targets:ro
|
||||
- prometheus_data:/prometheus
|
||||
command:
|
||||
- --config.file=/etc/prometheus/prometheus.yml
|
||||
- --storage.tsdb.path=/prometheus
|
||||
- --storage.tsdb.retention.time=90d
|
||||
- --web.enable-lifecycle
|
||||
extra_hosts:
|
||||
- host.docker.internal:host-gateway
|
||||
networks:
|
||||
- monitoring
|
||||
healthcheck:
|
||||
test:
|
||||
- CMD
|
||||
- wget
|
||||
- -qO-
|
||||
- http://localhost:9090/-/healthy
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
node-exporter:
|
||||
image: prom/node-exporter:latest
|
||||
container_name: node-exporter
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 127.0.0.1:9100:9100
|
||||
volumes:
|
||||
- /proc:/host/proc:ro
|
||||
- /sys:/host/sys:ro
|
||||
- /:/rootfs:ro
|
||||
- /var/run/dbus/system_bus_socket:/var/run/dbus/system_bus_socket:ro
|
||||
- /var/lib/prometheus/node-exporter:/textfile:ro
|
||||
command:
|
||||
- --path.procfs=/host/proc
|
||||
- --path.sysfs=/host/sys
|
||||
- --path.rootfs=/rootfs
|
||||
- --collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)
|
||||
- --collector.netclass.ignored-devices=^(veth.*|br-.*|docker.*)$$
|
||||
- --collector.diskstats.device-exclude=^(ram|loop|fd|dm-)\d+$$
|
||||
- --collector.systemd
|
||||
- --collector.systemd.unit-include=.+
|
||||
- --collector.textfile.directory=/textfile
|
||||
pid: host
|
||||
security_opt:
|
||||
- apparmor:unconfined
|
||||
networks:
|
||||
- monitoring
|
||||
healthcheck:
|
||||
test:
|
||||
- CMD
|
||||
- wget
|
||||
- --spider
|
||||
- -q
|
||||
- http://localhost:9100/metrics
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
cadvisor:
|
||||
image: gcr.io/cadvisor/cadvisor:latest
|
||||
container_name: cadvisor
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 127.0.0.1:8082:8080
|
||||
volumes:
|
||||
- /:/rootfs:ro
|
||||
- /var/run:/var/run:ro
|
||||
- /sys:/sys:ro
|
||||
- /var/lib/docker/:/var/lib/docker:ro
|
||||
- /dev/disk/:/dev/disk:ro
|
||||
privileged: true
|
||||
devices:
|
||||
- /dev/kmsg
|
||||
networks:
|
||||
- monitoring
|
||||
healthcheck:
|
||||
test:
|
||||
- CMD
|
||||
- wget
|
||||
- --spider
|
||||
- -q
|
||||
- http://localhost:8080/healthz
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
nginx-exporter:
|
||||
image: nginx/nginx-prometheus-exporter:latest
|
||||
container_name: nginx-exporter
|
||||
restart: unless-stopped
|
||||
network_mode: host
|
||||
command:
|
||||
- --nginx.scrape-uri=http://127.0.0.1:8888/nginx_status
|
||||
- --web.listen-address=0.0.0.0:9113
|
||||
grafana:
|
||||
image: grafana/grafana:latest
|
||||
container_name: grafana
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 127.0.0.1:3001:3000
|
||||
environment:
|
||||
- GF_SECURITY_ADMIN_USER=jmiller
|
||||
- GF_SECURITY_ADMIN_PASSWORD=#2918HeatherfieldDrive
|
||||
- GF_SERVER_ROOT_URL=https://bench.mokoconsulting.tech/
|
||||
- GF_SERVER_DOMAIN=bench.mokoconsulting.tech
|
||||
- GF_AUTH_ANONYMOUS_ENABLED=true
|
||||
- GF_AUTH_ANONYMOUS_ORG_ROLE=Viewer
|
||||
- GF_AUTH_ANONYMOUS_ORG_ID=1
|
||||
- GF_USERS_ALLOW_SIGN_UP=false
|
||||
- GF_USERS_ALLOW_ORG_CREATE=false
|
||||
- GF_SECURITY_COOKIE_SECURE=true
|
||||
- GF_SECURITY_STRICT_TRANSPORT_SECURITY=true
|
||||
- GF_SECURITY_X_CONTENT_TYPE_OPTIONS=true
|
||||
- GF_SECURITY_X_XSS_PROTECTION=true
|
||||
- GF_LOG_MODE=console
|
||||
- GF_LOG_LEVEL=debug
|
||||
- GF_USERS_DEFAULT_THEME=dark
|
||||
- GF_BRANDING_APP_TITLE=Moko Bench
|
||||
- GF_BRANDING_LOGIN_TITLE=Moko Consulting
|
||||
- GF_BRANDING_LOGIN_SUBTITLE=Server Performance Dashboard
|
||||
- GF_DATE_FORMATS_FULL_DATE=YYYY-MM-DD HH:mm:ss
|
||||
- GF_INSTALL_PLUGINS=grafana-clock-panel,grafana-polystat-panel,yesoreyeram-infinity-datasource,natel-discrete-panel
|
||||
- GF_AUTH_GOOGLE_ENABLED=true
|
||||
- GF_AUTH_GOOGLE_CLIENT_ID=349391103517-oiq974b2gq4r3t9f9cf43im31gtruhml.apps.googleusercontent.com
|
||||
- GF_AUTH_GOOGLE_CLIENT_SECRET=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
|
||||
- GF_AUTH_GOOGLE_SCOPES=openid email profile
|
||||
- GF_AUTH_GOOGLE_AUTH_URL=https://accounts.google.com/o/oauth2/v2/auth
|
||||
- GF_AUTH_GOOGLE_TOKEN_URL=https://oauth2.googleapis.com/token
|
||||
- GF_AUTH_GOOGLE_ALLOWED_DOMAINS=mokoconsulting.tech
|
||||
- GF_AUTH_GOOGLE_ALLOW_SIGN_UP=true
|
||||
- GF_AUTH_GOOGLE_AUTO_LOGIN=false
|
||||
- GF_AUTH_GOOGLE_SKIP_ORG_ROLE_SYNC=true
|
||||
- GF_USERS_AUTO_ASSIGN_ORG_ROLE=Admin
|
||||
volumes:
|
||||
- grafana_data:/var/lib/grafana
|
||||
- ./grafana/provisioning:/etc/grafana/provisioning:ro
|
||||
- ./grafana/custom.ini:/etc/grafana/grafana.ini:ro
|
||||
- ./grafana/dashboards:/var/lib/grafana/dashboards:ro
|
||||
networks:
|
||||
- monitoring
|
||||
depends_on:
|
||||
prometheus:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test:
|
||||
- CMD
|
||||
- wget
|
||||
- --spider
|
||||
- -q
|
||||
- http://localhost:3000/api/health
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
mysqld-exporter:
|
||||
image: prom/mysqld-exporter:latest
|
||||
container_name: mysqld-exporter
|
||||
restart: unless-stopped
|
||||
network_mode: host
|
||||
volumes:
|
||||
- /opt/gitea-server-setup/docker/monitoring/.mysqld-exporter.cnf:/cfg/.my.cnf:ro
|
||||
environment:
|
||||
MYSQLD_EXPORTER_PASSWORD: exporter_moko_2026
|
||||
command:
|
||||
- --config.my-cnf=/cfg/.my.cnf
|
||||
- --web.listen-address=127.0.0.1:9104
|
||||
healthcheck:
|
||||
test:
|
||||
- CMD-SHELL
|
||||
- wget -q --spider http://localhost:9104/metrics || exit 1
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 15s
|
||||
@@ -1,89 +0,0 @@
|
||||
{
|
||||
"name": "CPU Usage",
|
||||
"kind": 1,
|
||||
"model": {
|
||||
"description": "",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"custom": {
|
||||
"fillOpacity": 20,
|
||||
"gradientMode": "scheme",
|
||||
"lineWidth": 2
|
||||
},
|
||||
"max": 1,
|
||||
"min": 0,
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "yellow",
|
||||
"value": 0.7
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 0.9
|
||||
}
|
||||
]
|
||||
},
|
||||
"unit": "percentunit"
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"matcher": {
|
||||
"id": "byName",
|
||||
"options": "Yesterday CPU"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "custom.lineStyle",
|
||||
"value": {
|
||||
"dash": [
|
||||
10,
|
||||
10
|
||||
],
|
||||
"fill": "dash"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "custom.fillOpacity",
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"id": "custom.lineWidth",
|
||||
"value": 1
|
||||
},
|
||||
{
|
||||
"id": "color",
|
||||
"value": {
|
||||
"fixedColor": "rgba(255,255,255,0.35)",
|
||||
"mode": "fixed"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"expr": "1 - avg(rate(node_cpu_seconds_total{mode=\"idle\"}[5m]))",
|
||||
"legendFormat": "Total CPU",
|
||||
"refId": "A"
|
||||
},
|
||||
{
|
||||
"expr": "1 - avg(rate(node_cpu_seconds_total{mode=\"idle\"}[5m] offset 1d))",
|
||||
"legendFormat": "Yesterday CPU",
|
||||
"refId": "B"
|
||||
}
|
||||
],
|
||||
"options": {
|
||||
"legend": { "displayMode": "list", "placement": "right", "calcs": [] },
|
||||
"tooltip": { "mode": "multi" }
|
||||
},
|
||||
"title": "CPU Usage %",
|
||||
"type": "timeseries"
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
{
|
||||
"name": "Disk Usage",
|
||||
"kind": 1,
|
||||
"model": {
|
||||
"description": "",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"max": 1,
|
||||
"min": 0,
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "yellow",
|
||||
"value": 0.75
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 0.9
|
||||
}
|
||||
]
|
||||
},
|
||||
"unit": "percentunit"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"expr": "1 - (node_filesystem_avail_bytes{mountpoint=\"/\",fstype!=\"tmpfs\"} / node_filesystem_size_bytes{mountpoint=\"/\",fstype!=\"tmpfs\"})",
|
||||
"legendFormat": "/ (root)",
|
||||
"refId": "A"
|
||||
},
|
||||
{
|
||||
"expr": "1 - (node_filesystem_avail_bytes{mountpoint=\"/mnt/backup\",fstype!=\"tmpfs\"} / node_filesystem_size_bytes{mountpoint=\"/mnt/backup\",fstype!=\"tmpfs\"})",
|
||||
"legendFormat": "/mnt/backup",
|
||||
"refId": "B"
|
||||
}
|
||||
],
|
||||
"title": "Filesystem Usage",
|
||||
"type": "gauge"
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
{
|
||||
"name": "Docker Container CPU",
|
||||
"kind": 1,
|
||||
"model": {
|
||||
"description": "",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"custom": {
|
||||
"fillOpacity": 20,
|
||||
"lineWidth": 2,
|
||||
"stacking": {
|
||||
"mode": "normal"
|
||||
}
|
||||
},
|
||||
"unit": "percentunit"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"expr": "rate(container_cpu_usage_seconds_total{name!=\"\",name!~\".*POD.*\"}[5m])",
|
||||
"legendFormat": "{{name}}",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"options": {
|
||||
"legend": { "displayMode": "list", "placement": "right", "calcs": [] },
|
||||
"tooltip": { "mode": "multi" }
|
||||
},
|
||||
"title": "CPU Usage by Container",
|
||||
"type": "timeseries"
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
{
|
||||
"name": "Docker Container Memory",
|
||||
"kind": 1,
|
||||
"model": {
|
||||
"description": "",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"custom": {
|
||||
"fillOpacity": 20,
|
||||
"lineWidth": 2,
|
||||
"stacking": {
|
||||
"mode": "normal"
|
||||
}
|
||||
},
|
||||
"unit": "bytes"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"expr": "container_memory_usage_bytes{name!=\"\",name!~\".*POD.*\"}",
|
||||
"legendFormat": "{{name}}",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"options": {
|
||||
"legend": { "displayMode": "list", "placement": "right", "calcs": [] },
|
||||
"tooltip": { "mode": "multi" }
|
||||
},
|
||||
"title": "Memory Usage by Container",
|
||||
"type": "timeseries"
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
{
|
||||
"name": "Memory Usage",
|
||||
"kind": 1,
|
||||
"model": {
|
||||
"description": "",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"custom": {
|
||||
"fillOpacity": 30,
|
||||
"lineWidth": 2,
|
||||
"stacking": {
|
||||
"group": "A",
|
||||
"mode": "normal"
|
||||
}
|
||||
},
|
||||
"unit": "bytes"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"expr": "node_memory_MemTotal_bytes - node_memory_MemFree_bytes - node_memory_Buffers_bytes - node_memory_Cached_bytes - node_memory_SReclaimable_bytes",
|
||||
"legendFormat": "Used",
|
||||
"refId": "A"
|
||||
},
|
||||
{
|
||||
"expr": "node_memory_Buffers_bytes",
|
||||
"legendFormat": "Buffers",
|
||||
"refId": "B"
|
||||
},
|
||||
{
|
||||
"expr": "node_memory_Cached_bytes + node_memory_SReclaimable_bytes",
|
||||
"legendFormat": "Cached",
|
||||
"refId": "C"
|
||||
},
|
||||
{
|
||||
"expr": "node_memory_MemFree_bytes",
|
||||
"legendFormat": "Free",
|
||||
"refId": "D"
|
||||
}
|
||||
],
|
||||
"options": {
|
||||
"legend": { "displayMode": "list", "placement": "right", "calcs": [] },
|
||||
"tooltip": { "mode": "multi" }
|
||||
},
|
||||
"title": "Memory Usage",
|
||||
"type": "timeseries"
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
{
|
||||
"name": "MySQL Connections",
|
||||
"kind": 1,
|
||||
"model": {
|
||||
"description": "",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"custom": {
|
||||
"fillOpacity": 15,
|
||||
"lineWidth": 2
|
||||
}
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"expr": "mysql_global_status_threads_connected",
|
||||
"legendFormat": "Connected",
|
||||
"refId": "A"
|
||||
},
|
||||
{
|
||||
"expr": "mysql_global_status_threads_running",
|
||||
"legendFormat": "Running",
|
||||
"refId": "B"
|
||||
},
|
||||
{
|
||||
"expr": "mysql_global_variables_max_connections",
|
||||
"legendFormat": "Max",
|
||||
"refId": "C"
|
||||
}
|
||||
],
|
||||
"options": {
|
||||
"legend": { "displayMode": "list", "placement": "right", "calcs": [] },
|
||||
"tooltip": { "mode": "multi" }
|
||||
},
|
||||
"title": "Connections",
|
||||
"type": "timeseries"
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
{
|
||||
"name": "MySQL Queries per Second",
|
||||
"kind": 1,
|
||||
"model": {
|
||||
"description": "",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"custom": {
|
||||
"fillOpacity": 20,
|
||||
"lineWidth": 2
|
||||
},
|
||||
"unit": "ops"
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"matcher": {
|
||||
"id": "byName",
|
||||
"options": "Yesterday"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "custom.lineStyle",
|
||||
"value": {
|
||||
"dash": [
|
||||
10,
|
||||
10
|
||||
],
|
||||
"fill": "dash"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "custom.fillOpacity",
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"id": "custom.lineWidth",
|
||||
"value": 1
|
||||
},
|
||||
{
|
||||
"id": "color",
|
||||
"value": {
|
||||
"fixedColor": "rgba(255,255,255,0.35)",
|
||||
"mode": "fixed"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"expr": "rate(mysql_global_status_queries[5m])",
|
||||
"legendFormat": "Queries/s",
|
||||
"refId": "A"
|
||||
},
|
||||
{
|
||||
"expr": "rate(mysql_global_status_queries[5m] offset 1d)",
|
||||
"legendFormat": "Yesterday",
|
||||
"refId": "B"
|
||||
}
|
||||
],
|
||||
"options": {
|
||||
"legend": { "displayMode": "list", "placement": "right", "calcs": [] },
|
||||
"tooltip": { "mode": "multi" }
|
||||
},
|
||||
"title": "Queries per Second",
|
||||
"type": "timeseries"
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
{
|
||||
"name": "Network Traffic",
|
||||
"kind": 1,
|
||||
"model": {
|
||||
"description": "",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"custom": {
|
||||
"fillOpacity": 20,
|
||||
"lineWidth": 2
|
||||
},
|
||||
"unit": "bps"
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"matcher": {
|
||||
"id": "byRegexp",
|
||||
"options": "/Yesterday.*/"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "custom.lineStyle",
|
||||
"value": {
|
||||
"dash": [
|
||||
10,
|
||||
10
|
||||
],
|
||||
"fill": "dash"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "custom.fillOpacity",
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"id": "custom.lineWidth",
|
||||
"value": 1
|
||||
},
|
||||
{
|
||||
"id": "color",
|
||||
"value": {
|
||||
"fixedColor": "rgba(255,255,255,0.35)",
|
||||
"mode": "fixed"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"expr": "rate(node_network_receive_bytes_total{device!~\"lo|veth.*|br-.*|docker.*\"}[5m]) * 8",
|
||||
"legendFormat": "RX {{device}}",
|
||||
"refId": "A"
|
||||
},
|
||||
{
|
||||
"expr": "-rate(node_network_transmit_bytes_total{device!~\"lo|veth.*|br-.*|docker.*\"}[5m]) * 8",
|
||||
"legendFormat": "TX {{device}}",
|
||||
"refId": "B"
|
||||
},
|
||||
{
|
||||
"expr": "rate(node_network_receive_bytes_total{device!~\"lo|veth.*|br-.*|docker.*\"}[5m] offset 1d) * 8",
|
||||
"legendFormat": "Yesterday RX {{device}}",
|
||||
"refId": "C"
|
||||
},
|
||||
{
|
||||
"expr": "-rate(node_network_transmit_bytes_total{device!~\"lo|veth.*|br-.*|docker.*\"}[5m] offset 1d) * 8",
|
||||
"legendFormat": "Yesterday TX {{device}}",
|
||||
"refId": "D"
|
||||
}
|
||||
],
|
||||
"options": {
|
||||
"legend": { "displayMode": "list", "placement": "right", "calcs": [] },
|
||||
"tooltip": { "mode": "multi" }
|
||||
},
|
||||
"title": "Network Traffic",
|
||||
"type": "timeseries"
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
{
|
||||
"name": "Nginx Connections",
|
||||
"kind": 1,
|
||||
"model": {
|
||||
"description": "",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"custom": {
|
||||
"fillOpacity": 20,
|
||||
"lineWidth": 2
|
||||
}
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"expr": "nginx_connections_active",
|
||||
"legendFormat": "Active",
|
||||
"refId": "A"
|
||||
},
|
||||
{
|
||||
"expr": "nginx_connections_reading",
|
||||
"legendFormat": "Reading",
|
||||
"refId": "B"
|
||||
},
|
||||
{
|
||||
"expr": "nginx_connections_writing",
|
||||
"legendFormat": "Writing",
|
||||
"refId": "C"
|
||||
},
|
||||
{
|
||||
"expr": "nginx_connections_waiting",
|
||||
"legendFormat": "Waiting",
|
||||
"refId": "D"
|
||||
}
|
||||
],
|
||||
"options": {
|
||||
"legend": { "displayMode": "list", "placement": "right", "calcs": [] },
|
||||
"tooltip": { "mode": "multi" }
|
||||
},
|
||||
"title": "Connections Over Time",
|
||||
"type": "timeseries"
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
{
|
||||
"name": "Nginx Request Rate",
|
||||
"kind": 1,
|
||||
"model": {
|
||||
"description": "",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"custom": {
|
||||
"fillOpacity": 25,
|
||||
"gradientMode": "scheme",
|
||||
"lineWidth": 2
|
||||
},
|
||||
"thresholds": {
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "yellow",
|
||||
"value": 50
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 200
|
||||
}
|
||||
]
|
||||
},
|
||||
"unit": "reqps"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"expr": "rate(nginx_http_requests_total[5m])",
|
||||
"legendFormat": "Requests/s",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"options": {
|
||||
"legend": { "displayMode": "list", "placement": "right", "calcs": [] },
|
||||
"tooltip": { "mode": "multi" }
|
||||
},
|
||||
"title": "Request Rate",
|
||||
"type": "timeseries"
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
{
|
||||
"name": "Response Time",
|
||||
"kind": 1,
|
||||
"model": {
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "PBFA97CFB590B2093"
|
||||
},
|
||||
"description": "",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "s"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"legend": {
|
||||
"displayMode": "list",
|
||||
"placement": "right"
|
||||
},
|
||||
"tooltip": {
|
||||
"mode": "multi"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "PBFA97CFB590B2093"
|
||||
},
|
||||
"expr": "probe_http_duration_seconds{site_name=~\"$site\", job=\"blackbox-http\", phase=\"transfer\"}",
|
||||
"legendFormat": "{{site_name}} transfer"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "PBFA97CFB590B2093"
|
||||
},
|
||||
"expr": "probe_http_duration_seconds{site_name=~\"$site\", job=\"blackbox-http\", phase=\"processing\"}",
|
||||
"legendFormat": "{{site_name}} processing"
|
||||
}
|
||||
],
|
||||
"title": "Response Time",
|
||||
"type": "timeseries"
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
{
|
||||
"name": "Service Health",
|
||||
"kind": 1,
|
||||
"model": {
|
||||
"description": "",
|
||||
"fieldConfig": {
|
||||
"defaults": {},
|
||||
"overrides": [
|
||||
{
|
||||
"matcher": {
|
||||
"id": "byName",
|
||||
"options": "Value"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "mappings",
|
||||
"value": [
|
||||
{
|
||||
"options": {
|
||||
"0": {
|
||||
"color": "red",
|
||||
"text": "DOWN"
|
||||
},
|
||||
"1": {
|
||||
"color": "green",
|
||||
"text": "UP"
|
||||
}
|
||||
},
|
||||
"type": "value"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"expr": "up",
|
||||
"format": "table",
|
||||
"instant": true,
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Scrape Target Health",
|
||||
"transformations": [
|
||||
{
|
||||
"id": "organize",
|
||||
"options": {
|
||||
"excludeByName": {
|
||||
"Time": true,
|
||||
"__name__": true
|
||||
},
|
||||
"renameByName": {
|
||||
"Value": "Status",
|
||||
"instance": "Instance",
|
||||
"job": "Service"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"type": "table"
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
{
|
||||
"name": "SSL Certificate Expiry",
|
||||
"kind": 1,
|
||||
"model": {
|
||||
"description": "",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"custom": {
|
||||
"drawStyle": "line",
|
||||
"fillOpacity": 10,
|
||||
"lineWidth": 2,
|
||||
"spanNulls": false,
|
||||
"thresholdsStyle": {
|
||||
"mode": "area"
|
||||
}
|
||||
},
|
||||
"decimals": 0,
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "red",
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "yellow",
|
||||
"value": 7
|
||||
},
|
||||
{
|
||||
"color": "green",
|
||||
"value": 30
|
||||
}
|
||||
]
|
||||
},
|
||||
"unit": "short"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [
|
||||
"lastNotNull",
|
||||
"min"
|
||||
],
|
||||
"displayMode": "table",
|
||||
"placement": "right"
|
||||
},
|
||||
"tooltip": {
|
||||
"mode": "multi",
|
||||
"sort": "desc"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"expr": "moko_ssl_cert_expiry_seconds{domain=\"git.mokoconsulting.tech\"} / 86400",
|
||||
"legendFormat": "git.mokoconsulting.tech \u2014 days",
|
||||
"refId": "A"
|
||||
},
|
||||
{
|
||||
"expr": "moko_ssl_cert_expiry_seconds{domain=\"ntfy.mokoconsulting.tech\"} / 86400",
|
||||
"legendFormat": "ntfy.mokoconsulting.tech \u2014 days",
|
||||
"refId": "B"
|
||||
}
|
||||
],
|
||||
"title": "Certificate Expiry Timeline",
|
||||
"type": "timeseries"
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
{
|
||||
"name": "Uptime Availability",
|
||||
"kind": 1,
|
||||
"model": {
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "PBFA97CFB590B2093"
|
||||
},
|
||||
"description": "",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"custom": {
|
||||
"fillOpacity": 10,
|
||||
"lineWidth": 2
|
||||
},
|
||||
"max": 1,
|
||||
"min": 0,
|
||||
"thresholds": {
|
||||
"steps": [
|
||||
{
|
||||
"color": "red",
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"color": "yellow",
|
||||
"value": 0.95
|
||||
},
|
||||
{
|
||||
"color": "green",
|
||||
"value": 0.99
|
||||
}
|
||||
]
|
||||
},
|
||||
"unit": "percentunit"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"legend": {
|
||||
"displayMode": "list",
|
||||
"placement": "right",
|
||||
"calcs": []
|
||||
},
|
||||
"tooltip": {
|
||||
"mode": "single"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "PBFA97CFB590B2093"
|
||||
},
|
||||
"expr": "avg_over_time(probe_success{site_name=~\"$site\", job=\"blackbox-http\"}[1h])",
|
||||
"legendFormat": "{{site_name}}"
|
||||
}
|
||||
],
|
||||
"title": "Availability (30d)",
|
||||
"type": "timeseries"
|
||||
}
|
||||
}
|
||||
@@ -1,394 +0,0 @@
|
||||
{
|
||||
"description": "MokoWaaS endpoint monitoring — site health, Joomla core & extensions, backups, performance, and uptime",
|
||||
"editable": true,
|
||||
"panels": [
|
||||
{
|
||||
"collapsed": false,
|
||||
"gridPos": {
|
||||
"h": 1,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"id": 100,
|
||||
"title": "Site Status",
|
||||
"type": "row"
|
||||
},
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" },
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"noValue": "—",
|
||||
"custom": { "align": "center", "cellOptions": { "type": "auto" } }
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"matcher": { "id": "byName", "options": "Site" },
|
||||
"properties": [
|
||||
{ "id": "custom.width", "value": 300 },
|
||||
{ "id": "custom.align", "value": "left" },
|
||||
{ "id": "links", "value": [{ "title": "Open Site", "url": "${__value.text}", "targetBlank": true }] }
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": { "id": "byName", "options": "Online" },
|
||||
"properties": [
|
||||
{ "id": "mappings", "value": [{ "options": { "0": { "color": "orange", "text": "OFFLINE" }, "1": { "color": "green", "text": "ONLINE" } }, "type": "value" }] },
|
||||
{ "id": "thresholds", "value": { "mode": "absolute", "steps": [{ "color": "orange", "value": null }, { "color": "green", "value": 1 }] } },
|
||||
{ "id": "custom.cellOptions", "value": { "type": "color-background", "mode": "basic" } }
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": { "id": "byName", "options": "Status" },
|
||||
"properties": [
|
||||
{ "id": "mappings", "value": [{ "options": { "0": { "color": "red", "text": "DOWN" }, "1": { "color": "green", "text": "UP" } }, "type": "value" }] },
|
||||
{ "id": "thresholds", "value": { "mode": "absolute", "steps": [{ "color": "red", "value": null }, { "color": "green", "value": 1 }] } },
|
||||
{ "id": "custom.cellOptions", "value": { "type": "color-background", "mode": "basic" } }
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": { "id": "byName", "options": "HTTP" },
|
||||
"properties": [
|
||||
{ "id": "thresholds", "value": { "mode": "absolute", "steps": [{ "color": "red", "value": null }, { "color": "green", "value": 200 }, { "color": "yellow", "value": 300 }, { "color": "orange", "value": 400 }, { "color": "red", "value": 500 }] } },
|
||||
{ "id": "custom.cellOptions", "value": { "type": "color-background", "mode": "basic" } },
|
||||
{ "id": "links", "value": [{ "title": "HTTP Status Code Reference", "url": "https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#${__value.raw}", "targetBlank": true }] }
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": { "id": "byName", "options": "API" },
|
||||
"properties": [
|
||||
{ "id": "mappings", "value": [{ "options": { "0": { "color": "red", "text": "NO" }, "1": { "color": "green", "text": "OK" } }, "type": "value" }] },
|
||||
{ "id": "thresholds", "value": { "mode": "absolute", "steps": [{ "color": "red", "value": null }, { "color": "green", "value": 1 }] } },
|
||||
{ "id": "custom.cellOptions", "value": { "type": "color-background", "mode": "basic" } }
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": { "id": "byName", "options": "SSL" },
|
||||
"properties": [
|
||||
{ "id": "decimals", "value": 0 },
|
||||
{ "id": "unit", "value": "d" },
|
||||
{ "id": "thresholds", "value": { "mode": "absolute", "steps": [{ "color": "red", "value": null }, { "color": "yellow", "value": 14 }, { "color": "green", "value": 30 }] } },
|
||||
{ "id": "custom.cellOptions", "value": { "type": "color-text" } }
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": { "id": "byName", "options": "Last Scrape" },
|
||||
"properties": [
|
||||
{ "id": "unit", "value": "dateTimeFromNow" }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"gridPos": { "x": 0, "y": 1, "w": 24, "h": 8 },
|
||||
"id": 1,
|
||||
"options": { "showHeader": true, "cellHeight": "sm", "footer": { "show": false } },
|
||||
"targets": [
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" },
|
||||
"expr": "probe_success{site_name=~\"$site\", job=\"blackbox-http\"}",
|
||||
"instant": true, "format": "table", "refId": "STATUS"
|
||||
},
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" },
|
||||
"expr": "label_replace(probe_http_status_code{site_name=~\"$site\", job=\"blackbox-http\"}, \"site_url\", \"$1\", \"instance\", \"(.+)\")",
|
||||
"instant": true, "format": "table", "refId": "HTTP"
|
||||
},
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" },
|
||||
"expr": "label_replace(joomla_site_online{site=~\"$site\"}, \"site_name\", \"$1\", \"site\", \"(.+)\")",
|
||||
"instant": true, "format": "table", "refId": "ONLINE"
|
||||
},
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" },
|
||||
"expr": "label_replace(joomla_site_api_reachable{site=~\"$site\"}, \"site_name\", \"$1\", \"site\", \"(.+)\")",
|
||||
"instant": true, "format": "table", "refId": "API"
|
||||
},
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" },
|
||||
"expr": "(probe_ssl_earliest_cert_expiry{site_name=~\"$site\"} - time()) / 86400",
|
||||
"instant": true, "format": "table", "refId": "SSL"
|
||||
},
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" },
|
||||
"expr": "label_replace(joomla_monitor_last_scrape{site=~\"$site\"} * 1000, \"site_name\", \"$1\", \"site\", \"(.+)\")",
|
||||
"instant": true, "format": "table", "refId": "LASTSCRAPE"
|
||||
}
|
||||
],
|
||||
"transformations": [
|
||||
{ "id": "joinByField", "options": { "byField": "site_name", "mode": "outer" } },
|
||||
{
|
||||
"id": "filterFieldsByName",
|
||||
"options": {
|
||||
"include": { "pattern": "^(site_url|Value #).*" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "organize",
|
||||
"options": {
|
||||
"renameByName": {
|
||||
"site_url": "Site",
|
||||
"Value #ONLINE": "Online",
|
||||
"Value #STATUS": "Status",
|
||||
"Value #HTTP": "HTTP",
|
||||
"Value #API": "API",
|
||||
"Value #SSL": "SSL",
|
||||
"Value #LASTSCRAPE": "Last Scrape"
|
||||
},
|
||||
"indexByName": {
|
||||
"site_url": 0,
|
||||
"Value #ONLINE": 1,
|
||||
"Value #STATUS": 2,
|
||||
"Value #HTTP": 3,
|
||||
"Value #API": 4,
|
||||
"Value #SSL": 5,
|
||||
"Value #LASTSCRAPE": 6
|
||||
}
|
||||
}
|
||||
},
|
||||
{ "id": "sortBy", "options": { "sort": [{ "field": "Site", "desc": false }] } }
|
||||
],
|
||||
"title": "Site Health",
|
||||
"type": "table"
|
||||
},
|
||||
{
|
||||
"collapsed": true,
|
||||
"gridPos": {
|
||||
"h": 1,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 25
|
||||
},
|
||||
"id": 101,
|
||||
"title": "Joomla Core & Updates",
|
||||
"type": "row",
|
||||
"panels": [
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" },
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"noValue": "—",
|
||||
"custom": { "align": "center", "cellOptions": { "type": "auto" } }
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"matcher": { "id": "byName", "options": "Site" },
|
||||
"properties": [
|
||||
{ "id": "custom.width", "value": 300 },
|
||||
{ "id": "custom.align", "value": "left" },
|
||||
{ "id": "links", "value": [{ "title": "Open Admin", "url": "${__value.text}/administrator/", "targetBlank": true }] }
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": { "id": "byName", "options": "System" },
|
||||
"properties": [
|
||||
{ "id": "mappings", "value": [{ "options": { "0": { "color": "green", "text": "CURRENT" }, "1": { "color": "red", "text": "UPDATE" } }, "type": "value" }] },
|
||||
{ "id": "thresholds", "value": { "mode": "absolute", "steps": [{ "color": "green", "value": null }, { "color": "red", "value": 1 }] } },
|
||||
{ "id": "custom.cellOptions", "value": { "type": "color-background", "mode": "basic" } }
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": { "id": "byName", "options": "Ext Updates" },
|
||||
"properties": [
|
||||
{ "id": "thresholds", "value": { "mode": "absolute", "steps": [{ "color": "green", "value": null }, { "color": "red", "value": 1 }] } },
|
||||
{ "id": "custom.cellOptions", "value": { "type": "color-text" } },
|
||||
{ "id": "mappings", "value": [{ "options": { "0": { "text": "—" } }, "type": "value" }] }
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": { "id": "byName", "options": "Total" },
|
||||
"properties": [
|
||||
{ "id": "mappings", "value": [{ "options": { "0": { "text": "—" } }, "type": "value" }] }
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": { "id": "byName", "options": "Enabled" },
|
||||
"properties": [
|
||||
{ "id": "mappings", "value": [{ "options": { "0": { "text": "—" } }, "type": "value" }] },
|
||||
{ "id": "thresholds", "value": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] } },
|
||||
{ "id": "custom.cellOptions", "value": { "type": "color-text" } }
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": { "id": "byName", "options": "Disabled" },
|
||||
"properties": [
|
||||
{ "id": "mappings", "value": [{ "options": { "0": { "text": "—" } }, "type": "value" }] },
|
||||
{ "id": "thresholds", "value": { "mode": "absolute", "steps": [{ "color": "green", "value": null }, { "color": "yellow", "value": 5 }, { "color": "orange", "value": 20 }] } },
|
||||
{ "id": "custom.cellOptions", "value": { "type": "color-text" } }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"gridPos": { "x": 0, "y": 26, "w": 24, "h": 8 },
|
||||
"id": 10,
|
||||
"options": { "showHeader": true, "cellHeight": "sm", "footer": { "show": false } },
|
||||
"targets": [
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" },
|
||||
"expr": "label_replace(joomla_core_version_info{site=~\"$site\"}, \"site_url\", \"$1\", \"exported_instance\", \"(.+)\")",
|
||||
"instant": true, "format": "table", "refId": "VERSION"
|
||||
},
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" },
|
||||
"expr": "max by (site) (joomla_core_update_available{site=~\"$site\"})",
|
||||
"instant": true, "format": "table", "refId": "SYSTEM"
|
||||
},
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" },
|
||||
"expr": "joomla_extensions_update_count{site=~\"$site\"} or joomla_extensions_total{site=~\"$site\"} * 0",
|
||||
"instant": true, "format": "table", "refId": "EXTUPDATES"
|
||||
},
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" },
|
||||
"expr": "joomla_extensions_total{site=~\"$site\"}",
|
||||
"instant": true, "format": "table", "refId": "TOTAL"
|
||||
},
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" },
|
||||
"expr": "joomla_extensions_enabled{site=~\"$site\"}",
|
||||
"instant": true, "format": "table", "refId": "ENABLED"
|
||||
},
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" },
|
||||
"expr": "joomla_extensions_disabled{site=~\"$site\"}",
|
||||
"instant": true, "format": "table", "refId": "DISABLED"
|
||||
}
|
||||
],
|
||||
"transformations": [
|
||||
{ "id": "joinByField", "options": { "byField": "site", "mode": "outer" } },
|
||||
{ "id": "filterFieldsByName", "options": { "include": { "pattern": "^(site_url|version|Value #(SYSTEM|EXTUPDATES|TOTAL|ENABLED|DISABLED))$" } } },
|
||||
{
|
||||
"id": "organize",
|
||||
"options": {
|
||||
"renameByName": {
|
||||
"site_url": "Site",
|
||||
"version": "Version",
|
||||
"Value #SYSTEM": "System",
|
||||
"Value #EXTUPDATES": "Ext Updates",
|
||||
"Value #TOTAL": "Total",
|
||||
"Value #ENABLED": "Enabled",
|
||||
"Value #DISABLED": "Disabled"
|
||||
}
|
||||
}
|
||||
},
|
||||
{ "id": "sortBy", "options": { "sort": [{ "field": "Site", "desc": false }] } }
|
||||
],
|
||||
"title": "Joomla Core & Extensions",
|
||||
"type": "table"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"collapsed": false,
|
||||
"gridPos": {
|
||||
"h": 1,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 46
|
||||
},
|
||||
"id": 102,
|
||||
"title": "Performance",
|
||||
"type": "row"
|
||||
},
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" },
|
||||
"fieldConfig": { "defaults": { "unit": "s" } },
|
||||
"gridPos": { "h": 8, "w": 16, "x": 0, "y": 47 },
|
||||
"id": 20,
|
||||
"options": { "legend": { "displayMode": "list", "placement": "bottom" }, "tooltip": { "mode": "multi" } },
|
||||
"targets": [
|
||||
{ "datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" }, "expr": "probe_http_duration_seconds{site_name=~\"$site\", job=\"blackbox-http\", phase=\"transfer\"}", "legendFormat": "{{site_name}} transfer" },
|
||||
{ "datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" }, "expr": "probe_http_duration_seconds{site_name=~\"$site\", job=\"blackbox-http\", phase=\"processing\"}", "legendFormat": "{{site_name}} processing" }
|
||||
],
|
||||
"title": "Response Time",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" },
|
||||
"fieldConfig": { "defaults": { "max": 10, "min": 0, "thresholds": { "steps": [{ "color": "green", "value": 0 }, { "color": "yellow", "value": 2 }, { "color": "red", "value": 5 }] }, "unit": "s" } },
|
||||
"gridPos": { "h": 8, "w": 8, "x": 16, "y": 47 },
|
||||
"id": 22,
|
||||
"targets": [
|
||||
{ "datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" }, "expr": "probe_duration_seconds{site_name=~\"$site\", job=\"blackbox-http\"}", "legendFormat": "{{site_name}}" }
|
||||
],
|
||||
"title": "Total Duration",
|
||||
"type": "gauge"
|
||||
},
|
||||
{
|
||||
"collapsed": false,
|
||||
"gridPos": { "h": 1, "w": 24, "x": 0, "y": 55 },
|
||||
"id": 104,
|
||||
"title": "Backup Status",
|
||||
"type": "row"
|
||||
},
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" },
|
||||
"fieldConfig": {
|
||||
"defaults": { "noValue": "—", "custom": { "align": "center", "cellOptions": { "type": "auto" } } },
|
||||
"overrides": [
|
||||
{ "matcher": { "id": "byName", "options": "Site" }, "properties": [{ "id": "custom.width", "value": 300 }, { "id": "custom.align", "value": "left" }, { "id": "links", "value": [{ "title": "Manage Backups", "url": "${__value.text}/administrator/index.php?option=com_akeebabackup&view=Manage", "targetBlank": true }] }] },
|
||||
{ "matcher": { "id": "byName", "options": "Status" }, "properties": [{ "id": "mappings", "value": [{ "options": { "0": { "color": "red", "text": "FAILED" }, "1": { "color": "green", "text": "OK" } }, "type": "value" }] }, { "id": "thresholds", "value": { "mode": "absolute", "steps": [{ "color": "red", "value": null }, { "color": "green", "value": 1 }] } }, { "id": "custom.cellOptions", "value": { "type": "color-background", "mode": "basic" } }] },
|
||||
{ "matcher": { "id": "byName", "options": "Age" }, "properties": [{ "id": "unit", "value": "s" }, { "id": "thresholds", "value": { "mode": "absolute", "steps": [{ "color": "green", "value": null }, { "color": "yellow", "value": 172800 }, { "color": "red", "value": 604800 }] } }, { "id": "custom.cellOptions", "value": { "type": "color-text" } }, { "id": "mappings", "value": [{ "options": { "-1": { "text": "—" } }, "type": "value" }] }] },
|
||||
{ "matcher": { "id": "byName", "options": "Records" }, "properties": [{ "id": "mappings", "value": [{ "options": { "0": { "text": "—" } }, "type": "value" }] }] }
|
||||
]
|
||||
},
|
||||
"gridPos": { "x": 0, "y": 56, "w": 24, "h": 8 },
|
||||
"id": 40,
|
||||
"options": { "showHeader": true, "cellHeight": "sm", "footer": { "show": false } },
|
||||
"targets": [
|
||||
{ "datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" }, "expr": "label_replace(max by (site, exported_instance) (joomla_backup_status{site=~\"$site\"}), \"site_url\", \"$1\", \"exported_instance\", \"(.+)\")", "instant": true, "format": "table", "refId": "STATUS" },
|
||||
{ "datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" }, "expr": "joomla_backup_age_seconds{site=~\"$site\"}", "instant": true, "format": "table", "refId": "AGE" },
|
||||
{ "datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" }, "expr": "joomla_backup_records_total{site=~\"$site\"}", "instant": true, "format": "table", "refId": "RECORDS" }
|
||||
],
|
||||
"transformations": [
|
||||
{ "id": "joinByField", "options": { "byField": "site", "mode": "outer" } },
|
||||
{ "id": "filterFieldsByName", "options": { "include": { "pattern": "^(site_url|Value #).*" } } },
|
||||
{ "id": "organize", "options": { "renameByName": { "site_url": "Site", "Value #STATUS": "Status", "Value #AGE": "Age", "Value #RECORDS": "Records" } } },
|
||||
{ "id": "sortBy", "options": { "sort": [{ "field": "Site", "desc": false }] } }
|
||||
],
|
||||
"title": "Backup Status",
|
||||
"type": "table"
|
||||
},
|
||||
{
|
||||
"collapsed": false,
|
||||
"gridPos": { "h": 1, "w": 24, "x": 0, "y": 68 },
|
||||
"id": 103,
|
||||
"title": "Uptime History",
|
||||
"type": "row"
|
||||
},
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" },
|
||||
"fieldConfig": { "defaults": { "custom": { "fillOpacity": 10, "lineWidth": 2 }, "max": 1, "min": 0, "thresholds": { "steps": [{ "color": "red", "value": 0 }, { "color": "yellow", "value": 0.95 }, { "color": "green", "value": 0.99 }] }, "unit": "percentunit" } },
|
||||
"gridPos": { "h": 8, "w": 24, "x": 0, "y": 69 },
|
||||
"id": 30,
|
||||
"options": { "legend": { "displayMode": "list", "placement": "bottom" }, "tooltip": { "mode": "single" } },
|
||||
"targets": [
|
||||
{ "datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" }, "expr": "avg_over_time(probe_success{site_name=~\"$site\", job=\"blackbox-http\"}[1h])", "legendFormat": "{{site_name}}" }
|
||||
],
|
||||
"title": "Availability (30d)",
|
||||
"type": "timeseries"
|
||||
}
|
||||
],
|
||||
"refresh": "5m",
|
||||
"tags": ["mokowaas", "joomla", "endpoints", "monitoring"],
|
||||
"templating": {
|
||||
"list": [
|
||||
{
|
||||
"current": { "text": "All", "value": "$__all" },
|
||||
"datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" },
|
||||
"includeAll": true,
|
||||
"label": "Site",
|
||||
"multi": true,
|
||||
"name": "site",
|
||||
"query": "label_values(probe_success{job=\"blackbox-http\", site_type=~\"mokowaas.*\"}, site_name)",
|
||||
"refresh": 2,
|
||||
"sort": 1,
|
||||
"type": "query"
|
||||
}
|
||||
]
|
||||
},
|
||||
"time": { "from": "now-24h", "to": "now" },
|
||||
"timezone": "browser",
|
||||
"title": "MokoWaaS",
|
||||
"uid": "mokowaas",
|
||||
"version": 1
|
||||
}
|
||||
@@ -1,330 +0,0 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
*
|
||||
* This file is part of a Moko Consulting project.
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*
|
||||
* FILE INFORMATION
|
||||
* DEFGROUP: MokoStandards.Scripts.Monitoring
|
||||
* INGROUP: MokoStandards
|
||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||
* PATH: /monitoring/joomla-version-audit.php
|
||||
* VERSION: 01.00.00
|
||||
* BRIEF: Audit Joomla core and extension versions across sites via the Joomla API
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
final class JoomlaVersionAudit
|
||||
{
|
||||
/** @var array<string, mixed> */
|
||||
private array $args = [];
|
||||
|
||||
/** @var array<array{url: string, joomlaVersion: string, behind: bool, extensions: array<array{name: string, version: string}>, error: string}> */
|
||||
private array $results = [];
|
||||
|
||||
public function run(): int
|
||||
{
|
||||
$this->args = $this->parseArgs();
|
||||
|
||||
$sites = $this->resolveSites();
|
||||
|
||||
if (count($sites) === 0) {
|
||||
$this->log('No sites provided. Use --sites <json-file>.');
|
||||
return 1;
|
||||
}
|
||||
|
||||
$latestVersion = $this->args['latest'] ?? null;
|
||||
|
||||
foreach ($sites as $site) {
|
||||
$this->log("Auditing: {$site['url']}");
|
||||
$entry = $this->auditSite($site, $latestVersion);
|
||||
$this->results[] = $entry;
|
||||
}
|
||||
|
||||
if (!empty($this->args['json'])) {
|
||||
$this->outputJson();
|
||||
} else {
|
||||
$this->outputTable();
|
||||
}
|
||||
|
||||
$hasBehind = false;
|
||||
foreach ($this->results as $r) {
|
||||
if ($r['behind'] || $r['error'] !== '') {
|
||||
$hasBehind = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $hasBehind ? 1 : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function parseArgs(): array
|
||||
{
|
||||
$args = [
|
||||
'sites' => null,
|
||||
'json' => false,
|
||||
'latest' => null,
|
||||
];
|
||||
|
||||
$argv = $_SERVER['argv'] ?? [];
|
||||
$argc = count($argv);
|
||||
|
||||
for ($i = 1; $i < $argc; $i++) {
|
||||
switch ($argv[$i]) {
|
||||
case '--sites':
|
||||
$args['sites'] = $argv[++$i] ?? null;
|
||||
break;
|
||||
case '--json':
|
||||
$args['json'] = true;
|
||||
break;
|
||||
case '--latest':
|
||||
$args['latest'] = $argv[++$i] ?? null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $args;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array{url: string, token: string}>
|
||||
*/
|
||||
private function resolveSites(): array
|
||||
{
|
||||
$file = $this->args['sites'] ?? null;
|
||||
|
||||
if (empty($file)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!is_file($file) || !is_readable($file)) {
|
||||
$this->log("Cannot read sites file: {$file}");
|
||||
return [];
|
||||
}
|
||||
|
||||
$raw = file_get_contents($file);
|
||||
if ($raw === false) {
|
||||
$this->log("Failed to read sites file: {$file}");
|
||||
return [];
|
||||
}
|
||||
|
||||
$data = json_decode($raw, true);
|
||||
if (!is_array($data)) {
|
||||
$this->log("Sites file is not valid JSON: {$file}");
|
||||
return [];
|
||||
}
|
||||
|
||||
$sites = [];
|
||||
foreach ($data as $item) {
|
||||
if (is_array($item) && isset($item['url'], $item['token'])) {
|
||||
$sites[] = [
|
||||
'url' => rtrim((string) $item['url'], '/'),
|
||||
'token' => (string) $item['token'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $sites;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{url: string, token: string} $site
|
||||
* @return array{url: string, joomlaVersion: string, behind: bool, extensions: array<array{name: string, version: string}>, error: string}
|
||||
*/
|
||||
private function auditSite(array $site, ?string $latestVersion): array
|
||||
{
|
||||
$entry = [
|
||||
'url' => $site['url'],
|
||||
'joomlaVersion' => '',
|
||||
'behind' => false,
|
||||
'extensions' => [],
|
||||
'error' => '',
|
||||
];
|
||||
|
||||
// Fetch Joomla version from config/application endpoint
|
||||
$configData = $this->apiGet($site['url'] . '/api/index.php/v1/config/application', $site['token']);
|
||||
|
||||
if ($configData === null) {
|
||||
$entry['error'] = 'Failed to fetch site configuration';
|
||||
return $entry;
|
||||
}
|
||||
|
||||
// The Joomla API returns config attributes; extract version from the response
|
||||
$joomlaVersion = $this->extractJoomlaVersion($configData);
|
||||
$entry['joomlaVersion'] = $joomlaVersion;
|
||||
|
||||
// Compare against latest known version
|
||||
if ($latestVersion !== null && $joomlaVersion !== '') {
|
||||
if (version_compare($joomlaVersion, $latestVersion, '<')) {
|
||||
$entry['behind'] = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch extensions list
|
||||
$extData = $this->apiGet($site['url'] . '/api/index.php/v1/extensions', $site['token']);
|
||||
|
||||
if ($extData !== null && isset($extData['data']) && is_array($extData['data'])) {
|
||||
foreach ($extData['data'] as $ext) {
|
||||
$attrs = $ext['attributes'] ?? $ext;
|
||||
$name = $attrs['name'] ?? $attrs['element'] ?? 'unknown';
|
||||
$version = $attrs['version'] ?? '?';
|
||||
|
||||
// Filter to meaningful extensions (components, modules, plugins, templates)
|
||||
$type = $attrs['type'] ?? '';
|
||||
if (in_array($type, ['component', 'module', 'plugin', 'template', 'package', 'library'], true)) {
|
||||
$entry['extensions'][] = [
|
||||
'name' => (string) $name,
|
||||
'version' => (string) $version,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $entry;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
private function apiGet(string $url, string $token): ?array
|
||||
{
|
||||
$context = stream_context_create([
|
||||
'http' => [
|
||||
'method' => 'GET',
|
||||
'timeout' => 30,
|
||||
'ignore_errors' => true,
|
||||
'follow_location' => 1,
|
||||
'max_redirects' => 3,
|
||||
'header' => implode("\r\n", [
|
||||
"X-Joomla-Token: {$token}",
|
||||
'Accept: application/vnd.api+json',
|
||||
'Content-Type: application/json',
|
||||
]),
|
||||
],
|
||||
'ssl' => [
|
||||
'verify_peer' => true,
|
||||
'verify_peer_name' => true,
|
||||
],
|
||||
]);
|
||||
|
||||
$body = @file_get_contents($url, false, $context);
|
||||
if ($body === false) {
|
||||
$this->log(" API request failed: {$url}");
|
||||
return null;
|
||||
}
|
||||
|
||||
$data = json_decode($body, true);
|
||||
if (!is_array($data)) {
|
||||
$this->log(" Invalid JSON response from: {$url}");
|
||||
return null;
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $configData
|
||||
*/
|
||||
private function extractJoomlaVersion(array $configData): string
|
||||
{
|
||||
// Joomla's Web Services API returns config data in a JSON:API structure.
|
||||
// The version may appear in data.attributes or in meta.
|
||||
if (isset($configData['meta']['cms-version'])) {
|
||||
return (string) $configData['meta']['cms-version'];
|
||||
}
|
||||
|
||||
// Fallback: look through data attributes for a version-like field
|
||||
if (isset($configData['data']) && is_array($configData['data'])) {
|
||||
foreach ($configData['data'] as $item) {
|
||||
$attrs = $item['attributes'] ?? [];
|
||||
// Some Joomla API responses put the version in the attributes
|
||||
if (isset($attrs['cms-version'])) {
|
||||
return (string) $attrs['cms-version'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Second fallback: look for X-Powered-By or similar in response
|
||||
if (isset($configData['data']['attributes'])) {
|
||||
$attrs = $configData['data']['attributes'];
|
||||
foreach ($attrs as $key => $value) {
|
||||
if (stripos($key, 'version') !== false && is_string($value)) {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
private function outputTable(): void
|
||||
{
|
||||
$latestVersion = $this->args['latest'] ?? null;
|
||||
|
||||
foreach ($this->results as $r) {
|
||||
$this->log('');
|
||||
$this->log(str_repeat('=', 70));
|
||||
$this->log("Site: {$r['url']}");
|
||||
|
||||
if ($r['error'] !== '') {
|
||||
$this->log(" Error: {$r['error']}");
|
||||
continue;
|
||||
}
|
||||
|
||||
$versionDisplay = $r['joomlaVersion'];
|
||||
if ($r['behind'] && $latestVersion !== null) {
|
||||
$versionDisplay .= " (BEHIND, latest: {$latestVersion})";
|
||||
}
|
||||
$this->log(" Joomla Version: {$versionDisplay}");
|
||||
|
||||
if (count($r['extensions']) === 0) {
|
||||
$this->log(' Extensions: none found');
|
||||
continue;
|
||||
}
|
||||
|
||||
// Extension table
|
||||
$colName = 4;
|
||||
$colVersion = 7;
|
||||
|
||||
foreach ($r['extensions'] as $ext) {
|
||||
$colName = max($colName, strlen($ext['name']));
|
||||
$colVersion = max($colVersion, strlen($ext['version']));
|
||||
}
|
||||
|
||||
$colName = min($colName, 50);
|
||||
$colVersion = min($colVersion, 20);
|
||||
|
||||
$fmt = " %-{$colName}s | %-{$colVersion}s";
|
||||
|
||||
$this->log('');
|
||||
$this->log(sprintf($fmt, 'Name', 'Version'));
|
||||
$this->log(' ' . str_repeat('-', $colName + $colVersion + 4));
|
||||
|
||||
foreach ($r['extensions'] as $ext) {
|
||||
$nameDisplay = strlen($ext['name']) > 50 ? substr($ext['name'], 0, 47) . '...' : $ext['name'];
|
||||
$this->log(sprintf($fmt, $nameDisplay, $ext['version']));
|
||||
}
|
||||
}
|
||||
|
||||
$this->log('');
|
||||
}
|
||||
|
||||
private function outputJson(): void
|
||||
{
|
||||
echo json_encode($this->results, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n";
|
||||
}
|
||||
|
||||
private function log(string $message): void
|
||||
{
|
||||
fwrite(STDERR, $message . "\n");
|
||||
}
|
||||
}
|
||||
|
||||
$audit = new JoomlaVersionAudit();
|
||||
exit($audit->run());
|
||||
@@ -1,312 +0,0 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
*
|
||||
* This file is part of a Moko Consulting project.
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*
|
||||
* FILE INFORMATION
|
||||
* DEFGROUP: MokoStandards.Scripts.Monitoring
|
||||
* INGROUP: MokoStandards
|
||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||
* PATH: /monitoring/ssl-check.php
|
||||
* VERSION: 01.00.00
|
||||
* BRIEF: Check SSL certificate expiry dates for a list of domains
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
final class SslCheck
|
||||
{
|
||||
/** @var array<string, mixed> */
|
||||
private array $args = [];
|
||||
|
||||
/** @var array<array{domain: string, issuer: string, expires: string, daysLeft: int, status: string, error: string}> */
|
||||
private array $results = [];
|
||||
|
||||
public function run(): int
|
||||
{
|
||||
$this->args = $this->parseArgs();
|
||||
|
||||
$domains = $this->resolveDomains();
|
||||
|
||||
if (count($domains) === 0) {
|
||||
$this->log('No domains provided. Use --domains <file> or --domain <single>.');
|
||||
return 1;
|
||||
}
|
||||
|
||||
$warnDays = (int) ($this->args['warn-days'] ?? 30);
|
||||
|
||||
foreach ($domains as $domain) {
|
||||
$this->log("Checking: {$domain}");
|
||||
$entry = $this->checkCert($domain, $warnDays);
|
||||
$this->results[] = $entry;
|
||||
}
|
||||
|
||||
$hasIssue = false;
|
||||
foreach ($this->results as $r) {
|
||||
if ($r['status'] !== 'OK') {
|
||||
$hasIssue = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($this->args['json'])) {
|
||||
$this->outputJson();
|
||||
} else {
|
||||
$this->outputTable();
|
||||
}
|
||||
|
||||
if ($hasIssue && !empty($this->args['notify'])) {
|
||||
$this->sendNotification();
|
||||
}
|
||||
|
||||
return $hasIssue ? 1 : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function parseArgs(): array
|
||||
{
|
||||
$args = [
|
||||
'domains' => null,
|
||||
'domain' => null,
|
||||
'warn-days' => 30,
|
||||
'notify' => null,
|
||||
'json' => false,
|
||||
];
|
||||
|
||||
$argv = $_SERVER['argv'] ?? [];
|
||||
$argc = count($argv);
|
||||
|
||||
for ($i = 1; $i < $argc; $i++) {
|
||||
switch ($argv[$i]) {
|
||||
case '--domains':
|
||||
$args['domains'] = $argv[++$i] ?? null;
|
||||
break;
|
||||
case '--domain':
|
||||
$args['domain'] = $argv[++$i] ?? null;
|
||||
break;
|
||||
case '--warn-days':
|
||||
$args['warn-days'] = (int) ($argv[++$i] ?? 30);
|
||||
break;
|
||||
case '--notify':
|
||||
$args['notify'] = $argv[++$i] ?? null;
|
||||
break;
|
||||
case '--json':
|
||||
$args['json'] = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $args;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
private function resolveDomains(): array
|
||||
{
|
||||
$domains = [];
|
||||
|
||||
if (!empty($this->args['domains'])) {
|
||||
$file = $this->args['domains'];
|
||||
if (!is_file($file) || !is_readable($file)) {
|
||||
$this->log("Cannot read domains file: {$file}");
|
||||
return [];
|
||||
}
|
||||
$lines = file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||
if ($lines !== false) {
|
||||
foreach ($lines as $line) {
|
||||
$line = trim($line);
|
||||
if ($line !== '' && $line[0] !== '#') {
|
||||
$domains[] = $line;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($this->args['domain'])) {
|
||||
$domains[] = trim($this->args['domain']);
|
||||
}
|
||||
|
||||
return $domains;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{domain: string, issuer: string, expires: string, daysLeft: int, status: string, error: string}
|
||||
*/
|
||||
private function checkCert(string $domain, int $warnDays): array
|
||||
{
|
||||
$entry = [
|
||||
'domain' => $domain,
|
||||
'issuer' => '',
|
||||
'expires' => '',
|
||||
'daysLeft' => 0,
|
||||
'status' => 'EXPIRED',
|
||||
'error' => '',
|
||||
];
|
||||
|
||||
$context = stream_context_create([
|
||||
'ssl' => [
|
||||
'capture_peer_cert' => true,
|
||||
'verify_peer' => true,
|
||||
'verify_peer_name' => true,
|
||||
'SNI_enabled' => true,
|
||||
'peer_name' => $domain,
|
||||
],
|
||||
]);
|
||||
|
||||
$errno = 0;
|
||||
$errstr = '';
|
||||
$client = @stream_socket_client(
|
||||
"ssl://{$domain}:443",
|
||||
$errno,
|
||||
$errstr,
|
||||
15,
|
||||
STREAM_CLIENT_CONNECT,
|
||||
$context
|
||||
);
|
||||
|
||||
if ($client === false) {
|
||||
$entry['error'] = "Connection failed: {$errstr} ({$errno})";
|
||||
return $entry;
|
||||
}
|
||||
|
||||
$params = stream_context_get_params($client);
|
||||
fclose($client);
|
||||
|
||||
if (!isset($params['options']['ssl']['peer_certificate'])) {
|
||||
$entry['error'] = 'No peer certificate captured';
|
||||
return $entry;
|
||||
}
|
||||
|
||||
$certResource = $params['options']['ssl']['peer_certificate'];
|
||||
$certInfo = openssl_x509_parse($certResource);
|
||||
|
||||
if ($certInfo === false) {
|
||||
$entry['error'] = 'Failed to parse certificate';
|
||||
return $entry;
|
||||
}
|
||||
|
||||
// Issuer
|
||||
$issuerParts = $certInfo['issuer'] ?? [];
|
||||
$entry['issuer'] = $issuerParts['O'] ?? $issuerParts['CN'] ?? 'Unknown';
|
||||
|
||||
// Expiry
|
||||
$validTo = $certInfo['validTo_time_t'] ?? 0;
|
||||
$entry['expires'] = date('Y-m-d', $validTo);
|
||||
|
||||
$now = time();
|
||||
$daysLeft = (int) floor(($validTo - $now) / 86400);
|
||||
$entry['daysLeft'] = $daysLeft;
|
||||
|
||||
if ($daysLeft < 0) {
|
||||
$entry['status'] = 'EXPIRED';
|
||||
} elseif ($daysLeft <= $warnDays) {
|
||||
$entry['status'] = 'WARN';
|
||||
} else {
|
||||
$entry['status'] = 'OK';
|
||||
}
|
||||
|
||||
return $entry;
|
||||
}
|
||||
|
||||
private function outputTable(): void
|
||||
{
|
||||
$colDomain = 6;
|
||||
$colIssuer = 6;
|
||||
$colExpires = 10;
|
||||
$colDays = 9;
|
||||
$colStatus = 7;
|
||||
|
||||
foreach ($this->results as $r) {
|
||||
$colDomain = max($colDomain, strlen($r['domain']));
|
||||
$colIssuer = max($colIssuer, strlen($r['issuer']));
|
||||
}
|
||||
|
||||
$colDomain = min($colDomain, 40);
|
||||
$colIssuer = min($colIssuer, 30);
|
||||
|
||||
$fmt = "%-{$colDomain}s | %-{$colIssuer}s | %-{$colExpires}s | %-{$colDays}s | %-{$colStatus}s";
|
||||
|
||||
$this->log('');
|
||||
$this->log(sprintf($fmt, 'Domain', 'Issuer', 'Expires', 'Days Left', 'Status'));
|
||||
$this->log(str_repeat('-', $colDomain + $colIssuer + $colExpires + $colDays + $colStatus + 13));
|
||||
|
||||
foreach ($this->results as $r) {
|
||||
$domainDisplay = strlen($r['domain']) > 40 ? substr($r['domain'], 0, 37) . '...' : $r['domain'];
|
||||
$issuerDisplay = strlen($r['issuer']) > 30 ? substr($r['issuer'], 0, 27) . '...' : $r['issuer'];
|
||||
$daysStr = $r['error'] !== '' ? 'N/A' : (string) $r['daysLeft'];
|
||||
$expiresStr = $r['expires'] ?: 'N/A';
|
||||
|
||||
$this->log(sprintf($fmt, $domainDisplay, $issuerDisplay, $expiresStr, $daysStr, $r['status']));
|
||||
|
||||
if ($r['error'] !== '') {
|
||||
$this->log(" Error: {$r['error']}");
|
||||
}
|
||||
}
|
||||
|
||||
$this->log('');
|
||||
}
|
||||
|
||||
private function outputJson(): void
|
||||
{
|
||||
echo json_encode($this->results, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n";
|
||||
}
|
||||
|
||||
private function sendNotification(): void
|
||||
{
|
||||
$ntfyUrl = $this->args['notify'];
|
||||
if (empty($ntfyUrl)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$issues = [];
|
||||
foreach ($this->results as $r) {
|
||||
if ($r['status'] !== 'OK') {
|
||||
$issues[] = $r['domain'] . ' — ' . $r['status']
|
||||
. ($r['daysLeft'] > 0 ? " ({$r['daysLeft']} days left)" : '')
|
||||
. ($r['error'] !== '' ? " [{$r['error']}]" : '');
|
||||
}
|
||||
}
|
||||
|
||||
$message = "SSL certificate issues:\n" . implode("\n", $issues);
|
||||
|
||||
$ch = curl_init($ntfyUrl);
|
||||
if ($ch === false) {
|
||||
$this->log('Failed to initialise curl for notification.');
|
||||
return;
|
||||
}
|
||||
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => $message,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT => 10,
|
||||
CURLOPT_HTTPHEADER => [
|
||||
'Title: SSL Certificate Alert',
|
||||
'Priority: high',
|
||||
],
|
||||
]);
|
||||
|
||||
$result = curl_exec($ch);
|
||||
if ($result === false) {
|
||||
$this->log('Notification failed: ' . curl_error($ch));
|
||||
} else {
|
||||
$this->log('Notification sent.');
|
||||
}
|
||||
|
||||
curl_close($ch);
|
||||
}
|
||||
|
||||
private function log(string $message): void
|
||||
{
|
||||
fwrite(STDERR, $message . "\n");
|
||||
}
|
||||
}
|
||||
|
||||
$check = new SslCheck();
|
||||
exit($check->run());
|
||||
@@ -1,295 +0,0 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
*
|
||||
* This file is part of a Moko Consulting project.
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*
|
||||
* FILE INFORMATION
|
||||
* DEFGROUP: MokoStandards.Scripts.Monitoring
|
||||
* INGROUP: MokoStandards
|
||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||
* PATH: /monitoring/uptime-probe.php
|
||||
* VERSION: 01.00.00
|
||||
* BRIEF: Check uptime and response time for a list of URLs
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
final class UptimeProbe
|
||||
{
|
||||
/** @var array<string, mixed> */
|
||||
private array $args = [];
|
||||
|
||||
/** @var array<array{url: string, status: int, time: float, result: string, error: string}> */
|
||||
private array $results = [];
|
||||
|
||||
public function run(): int
|
||||
{
|
||||
$this->args = $this->parseArgs();
|
||||
|
||||
$urls = $this->resolveUrls();
|
||||
|
||||
if (count($urls) === 0) {
|
||||
$this->log('No URLs provided. Use --urls <file> or --url <single-url>.');
|
||||
return 1;
|
||||
}
|
||||
|
||||
$timeout = (int) ($this->args['timeout'] ?? 15);
|
||||
|
||||
foreach ($urls as $url) {
|
||||
$this->log("Probing: {$url}");
|
||||
$entry = $this->probe($url, $timeout);
|
||||
$this->results[] = $entry;
|
||||
}
|
||||
|
||||
$hasFailure = false;
|
||||
foreach ($this->results as $r) {
|
||||
if ($r['result'] === 'FAIL') {
|
||||
$hasFailure = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($this->args['json'])) {
|
||||
$this->outputJson();
|
||||
} else {
|
||||
$this->outputTable();
|
||||
}
|
||||
|
||||
if ($hasFailure && !empty($this->args['notify'])) {
|
||||
$this->sendNotification();
|
||||
}
|
||||
|
||||
return $hasFailure ? 1 : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function parseArgs(): array
|
||||
{
|
||||
$args = [
|
||||
'urls' => null,
|
||||
'url' => null,
|
||||
'timeout' => 15,
|
||||
'notify' => null,
|
||||
'json' => false,
|
||||
];
|
||||
|
||||
$argv = $_SERVER['argv'] ?? [];
|
||||
$argc = count($argv);
|
||||
|
||||
for ($i = 1; $i < $argc; $i++) {
|
||||
switch ($argv[$i]) {
|
||||
case '--urls':
|
||||
$args['urls'] = $argv[++$i] ?? null;
|
||||
break;
|
||||
case '--url':
|
||||
$args['url'] = $argv[++$i] ?? null;
|
||||
break;
|
||||
case '--timeout':
|
||||
$args['timeout'] = (int) ($argv[++$i] ?? 15);
|
||||
break;
|
||||
case '--notify':
|
||||
$args['notify'] = $argv[++$i] ?? null;
|
||||
break;
|
||||
case '--json':
|
||||
$args['json'] = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $args;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
private function resolveUrls(): array
|
||||
{
|
||||
$urls = [];
|
||||
|
||||
if (!empty($this->args['urls'])) {
|
||||
$file = $this->args['urls'];
|
||||
if (!is_file($file) || !is_readable($file)) {
|
||||
$this->log("Cannot read URL file: {$file}");
|
||||
return [];
|
||||
}
|
||||
$lines = file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||
if ($lines !== false) {
|
||||
foreach ($lines as $line) {
|
||||
$line = trim($line);
|
||||
if ($line !== '' && $line[0] !== '#') {
|
||||
$urls[] = $line;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($this->args['url'])) {
|
||||
$urls[] = trim($this->args['url']);
|
||||
}
|
||||
|
||||
return $urls;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{url: string, status: int, time: float, result: string, error: string}
|
||||
*/
|
||||
private function probe(string $url, int $timeout): array
|
||||
{
|
||||
$entry = [
|
||||
'url' => $url,
|
||||
'status' => 0,
|
||||
'time' => 0.0,
|
||||
'result' => 'FAIL',
|
||||
'error' => '',
|
||||
];
|
||||
|
||||
$context = stream_context_create([
|
||||
'http' => [
|
||||
'method' => 'GET',
|
||||
'timeout' => $timeout,
|
||||
'follow_location' => 1,
|
||||
'max_redirects' => 5,
|
||||
'ignore_errors' => true,
|
||||
],
|
||||
'ssl' => [
|
||||
'verify_peer' => true,
|
||||
'verify_peer_name' => true,
|
||||
],
|
||||
]);
|
||||
|
||||
$start = microtime(true);
|
||||
$body = @file_get_contents($url, false, $context);
|
||||
$elapsed = round(microtime(true) - $start, 3);
|
||||
$entry['time'] = $elapsed;
|
||||
|
||||
if ($body === false) {
|
||||
$entry['error'] = 'Connection failed or timed out';
|
||||
return $entry;
|
||||
}
|
||||
|
||||
// Parse status code from $http_response_header
|
||||
$statusCode = 0;
|
||||
if (isset($http_response_header) && is_array($http_response_header)) {
|
||||
foreach ($http_response_header as $header) {
|
||||
if (preg_match('/^HTTP\/[\d.]+ (\d{3})/', $header, $m)) {
|
||||
$statusCode = (int) $m[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
$entry['status'] = $statusCode;
|
||||
|
||||
// Check for PHP fatal errors in the response body
|
||||
$fatalPatterns = [
|
||||
'Fatal error:',
|
||||
'Parse error:',
|
||||
'Uncaught Exception',
|
||||
'Uncaught Error',
|
||||
'Stack trace:',
|
||||
];
|
||||
|
||||
foreach ($fatalPatterns as $pattern) {
|
||||
if (stripos($body, $pattern) !== false) {
|
||||
$entry['error'] = "PHP fatal error detected in response body";
|
||||
return $entry;
|
||||
}
|
||||
}
|
||||
|
||||
if ($statusCode >= 200 && $statusCode < 400) {
|
||||
$entry['result'] = 'PASS';
|
||||
} else {
|
||||
$entry['error'] = "HTTP {$statusCode}";
|
||||
}
|
||||
|
||||
return $entry;
|
||||
}
|
||||
|
||||
private function outputTable(): void
|
||||
{
|
||||
$colUrl = 4;
|
||||
$colStatus = 6;
|
||||
$colTime = 6;
|
||||
$colResult = 6;
|
||||
|
||||
foreach ($this->results as $r) {
|
||||
$colUrl = max($colUrl, strlen($r['url']));
|
||||
}
|
||||
|
||||
$colUrl = min($colUrl, 60);
|
||||
|
||||
$fmt = "%-{$colUrl}s | %-{$colStatus}s | %-{$colTime}s | %-{$colResult}s";
|
||||
|
||||
$this->log('');
|
||||
$this->log(sprintf($fmt, 'URL', 'Status', 'Time', 'Result'));
|
||||
$this->log(str_repeat('-', $colUrl + $colStatus + $colTime + $colResult + 10));
|
||||
|
||||
foreach ($this->results as $r) {
|
||||
$urlDisplay = strlen($r['url']) > 60 ? substr($r['url'], 0, 57) . '...' : $r['url'];
|
||||
$timeStr = $r['time'] . 's';
|
||||
$statusStr = $r['status'] > 0 ? (string) $r['status'] : 'ERR';
|
||||
$this->log(sprintf($fmt, $urlDisplay, $statusStr, $timeStr, $r['result']));
|
||||
}
|
||||
|
||||
$this->log('');
|
||||
}
|
||||
|
||||
private function outputJson(): void
|
||||
{
|
||||
echo json_encode($this->results, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n";
|
||||
}
|
||||
|
||||
private function sendNotification(): void
|
||||
{
|
||||
$ntfyUrl = $this->args['notify'];
|
||||
if (empty($ntfyUrl)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$failures = [];
|
||||
foreach ($this->results as $r) {
|
||||
if ($r['result'] === 'FAIL') {
|
||||
$failures[] = $r['url'] . ' (' . ($r['error'] ?: 'HTTP ' . $r['status']) . ')';
|
||||
}
|
||||
}
|
||||
|
||||
$message = "Uptime probe failures:\n" . implode("\n", $failures);
|
||||
|
||||
$ch = curl_init($ntfyUrl);
|
||||
if ($ch === false) {
|
||||
$this->log('Failed to initialise curl for notification.');
|
||||
return;
|
||||
}
|
||||
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => $message,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT => 10,
|
||||
CURLOPT_HTTPHEADER => [
|
||||
'Title: Uptime Probe Alert',
|
||||
'Priority: high',
|
||||
],
|
||||
]);
|
||||
|
||||
$result = curl_exec($ch);
|
||||
if ($result === false) {
|
||||
$this->log('Notification failed: ' . curl_error($ch));
|
||||
} else {
|
||||
$this->log('Notification sent.');
|
||||
}
|
||||
|
||||
curl_close($ch);
|
||||
}
|
||||
|
||||
private function log(string $message): void
|
||||
{
|
||||
fwrite(STDERR, $message . "\n");
|
||||
}
|
||||
}
|
||||
|
||||
$probe = new UptimeProbe();
|
||||
exit($probe->run());
|
||||
Reference in New Issue
Block a user