- metrics.v: GET /metrics au format OpenMetrics (gauges + counters) - server.v: route /metrics, Stats.http_polls counter, increment in poll loop - metrics_test.v: test du endpoint metrics - ROADMAP: Phase 5 6/8 (Prometheus done)
This commit is contained in:
+8
-11
@@ -106,7 +106,7 @@ Objectif : un seul binaire, une seule commande pour installer/désinstaller le s
|
|||||||
|
|
||||||
**Sortie** : `ntfy-bridge --install-service` fonctionne sur Windows 10+, Debian 11+, Alpine 3.18+, Raspberry Pi OS (arm64). ✅
|
**Sortie** : `ntfy-bridge --install-service` fonctionne sur Windows 10+, Debian 11+, Alpine 3.18+, Raspberry Pi OS (arm64). ✅
|
||||||
|
|
||||||
## Phase 5 : Futures idées — v0.5.0+ (4/8)
|
## Phase 5 : Futures idées — v0.5.0+ (6/8)
|
||||||
|
|
||||||
- [x] Web UI minimale — Dashboard avec statuts, historique, API endpoints
|
- [x] Web UI minimale — Dashboard avec statuts, historique, API endpoints
|
||||||
- [x] Filtres avancés — Expressions conditionnelles par source
|
- [x] Filtres avancés — Expressions conditionnelles par source
|
||||||
@@ -116,20 +116,17 @@ Objectif : un seul binaire, une seule commande pour installer/désinstaller le s
|
|||||||
- match_source: "docker"
|
- match_source: "docker"
|
||||||
match_name: "test-*"
|
match_name: "test-*"
|
||||||
action: drop
|
action: drop
|
||||||
- match_source: "gitea"
|
|
||||||
action: set_priority
|
|
||||||
value: "5"
|
|
||||||
```
|
```
|
||||||
- Actions: `drop`, `set_priority:N`, `add_tag:X`
|
|
||||||
- Wildcard `*` supporté dans `match_name`
|
|
||||||
- [x] Notifications structurées — Support des actions Ntfy (boutons click)
|
- [x] Notifications structurées — Support des actions Ntfy (boutons click)
|
||||||
- Champ `click_url` pour ouvrir une URL au clic
|
- Champ `click_url` pour ouvrir une URL au clic
|
||||||
- Champ `actions` pour boutons d'action (view, broadcast, http)
|
- Champ `actions` pour boutons d'action (view, broadcast, http)
|
||||||
- [ ] Intégration Prometheus — Métriques exposées au format OpenMetrics
|
- [x] Intégration Prometheus — Métriques exposées au format OpenMetrics
|
||||||
|
- Endpoint `GET /metrics` : gauges (uptime, poll_state, silence) + counters (notifications, errors, http_polls)
|
||||||
|
- Format compatible Prometheus/Grafana
|
||||||
- [x] Webhook sortant — Forwarder les événements vers Slack, Discord, JSON
|
- [x] Webhook sortant — Forwarder les événements vers Slack, Discord, JSON
|
||||||
```yaml
|
- [ ] Support multi-utilisateurs — ACLs par webhook_path
|
||||||
outgoing:
|
- [x] Fichier d'état — Persistance de l'état up/down des health checks
|
||||||
webhooks:
|
- [ ] Plugin system — Sources customisables via dll/.so
|
||||||
- url: "https://hooks.slack.com/..."
|
- url: "https://hooks.slack.com/..."
|
||||||
format: slack
|
format: slack
|
||||||
- url: "https://discord.com/api/webhooks/..."
|
- url: "https://discord.com/api/webhooks/..."
|
||||||
@@ -169,4 +166,4 @@ Ces features ont été ajoutées en cours de route :
|
|||||||
| v0.3.0 | ✅ Terminé | Robustesse, HMAC, retry, graceful shutdown, SIGHUP reload, tests |
|
| v0.3.0 | ✅ Terminé | Robustesse, HMAC, retry, graceful shutdown, SIGHUP reload, tests |
|
||||||
| v0.4.0 | ✅ Terminé | Templates, silence, grouping, HMAC global, CI/CD |
|
| v0.4.0 | ✅ Terminé | Templates, silence, grouping, HMAC global, CI/CD |
|
||||||
| v0.4.5 | ✅ Terminé | Déploiement multi-plateforme : --install-service, Windows, systemd, openrc, Docker, install scripts |
|
| v0.4.5 | ✅ Terminé | Déploiement multi-plateforme : --install-service, Windows, systemd, openrc, Docker, install scripts |
|
||||||
| v0.5.0+ | 🚧 4/8 | Dashboard, filtres avancés, actions Ntfy, outgoing webhooks, state file — manque Prometheus, multi-user, plugins |
|
| v0.5.0+ | 🚧 6/8 | Dashboard, filtres, actions Ntfy, Prometheus, outgoing webhooks, state file — manque multi-user, plugins |
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
module main
|
||||||
|
|
||||||
|
import net.http
|
||||||
|
import time
|
||||||
|
|
||||||
|
// metrics_response returns a Prometheus / OpenMetrics formatted response.
|
||||||
|
fn (mut app App) metrics_response() http.Response {
|
||||||
|
mut lines := []string{}
|
||||||
|
|
||||||
|
// HELP/TYPE headers
|
||||||
|
lines << '# HELP ntfy_bridge_uptime_seconds Seconds since the bridge started.'
|
||||||
|
lines << '# TYPE ntfy_bridge_uptime_seconds gauge'
|
||||||
|
lines << 'ntfy_bridge_uptime_seconds ${app.uptime_seconds()}'
|
||||||
|
|
||||||
|
lines << '# HELP ntfy_bridge_notifications_total Total notifications sent (all sources).'
|
||||||
|
lines << '# TYPE ntfy_bridge_notifications_total counter'
|
||||||
|
lines << 'ntfy_bridge_notifications_total ${app.stats.total_notifications}'
|
||||||
|
|
||||||
|
lines << '# HELP ntfy_bridge_errors_total Total publish errors.'
|
||||||
|
lines << '# TYPE ntfy_bridge_errors_total counter'
|
||||||
|
lines << 'ntfy_bridge_errors_total ${app.stats.errors}'
|
||||||
|
|
||||||
|
lines << '# HELP ntfy_bridge_notifications_by_source Notifications by source type.'
|
||||||
|
lines << '# TYPE ntfy_bridge_notifications_by_source counter'
|
||||||
|
for source, count in app.stats.by_source {
|
||||||
|
lines << 'ntfy_bridge_notifications_by_source{source="${source}"} ${count}'
|
||||||
|
}
|
||||||
|
|
||||||
|
lines << '# HELP ntfy_bridge_poll_up Health check polling status (1=up, 0=down).'
|
||||||
|
lines << '# TYPE ntfy_bridge_poll_up gauge'
|
||||||
|
for url, is_up in app.poll_state {
|
||||||
|
val := if is_up { '1' } else { '0' }
|
||||||
|
safe_url := url.replace('"', '\\"')
|
||||||
|
lines << 'ntfy_bridge_poll_up{url="${safe_url}"} ${val}'
|
||||||
|
}
|
||||||
|
|
||||||
|
lines << '# HELP ntfy_bridge_http_polls_total Total HTTP poll checks performed.'
|
||||||
|
lines << '# TYPE ntfy_bridge_http_polls_total counter'
|
||||||
|
lines << 'ntfy_bridge_http_polls_total ${app.stats.http_polls}'
|
||||||
|
|
||||||
|
lines << '# HELP ntfy_bridge_silence_active Whether notification silence is active.'
|
||||||
|
lines << '# TYPE ntfy_bridge_silence_active gauge'
|
||||||
|
now := time.now()
|
||||||
|
silence_val := if now.unix() < app.silence_until.unix() { '1' } else { '0' }
|
||||||
|
lines << 'ntfy_bridge_silence_active ${silence_val}'
|
||||||
|
|
||||||
|
lines << '# HELP ntfy_bridge_silence_remaining_seconds Seconds remaining in current silence.'
|
||||||
|
lines << '# TYPE ntfy_bridge_silence_remaining_seconds gauge'
|
||||||
|
remaining := if time.now() < app.silence_until {
|
||||||
|
(app.silence_until - time.now()) / time.second
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
lines << 'ntfy_bridge_silence_remaining_seconds ${remaining}'
|
||||||
|
|
||||||
|
// EOF
|
||||||
|
lines << '# EOF'
|
||||||
|
|
||||||
|
return http.Response{
|
||||||
|
status_code: 200
|
||||||
|
header: http.Header{}
|
||||||
|
body: lines.join('\n') + '\n'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// uptime_seconds returns the number of seconds since the server started.
|
||||||
|
fn (app App) uptime_seconds() i64 {
|
||||||
|
now := time.now()
|
||||||
|
diff := now.unix() - app.start_time.unix()
|
||||||
|
return i64(diff)
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
module main
|
||||||
|
|
||||||
|
fn test_metrics_keys() {
|
||||||
|
mut app := App{
|
||||||
|
logger: new_logger(.info, false)
|
||||||
|
cfg: Config{
|
||||||
|
filters: FilterConfig{}
|
||||||
|
outgoing: OutgoingConfig{}
|
||||||
|
state: StateConfig{}
|
||||||
|
}
|
||||||
|
poll_state: map[string]bool{}
|
||||||
|
}
|
||||||
|
app.poll_state['http://example.com'] = true
|
||||||
|
app.poll_state['http://down.com'] = false
|
||||||
|
app.stats.total_notifications = 42
|
||||||
|
app.stats.errors = 3
|
||||||
|
app.stats.by_source['docker'] = 10
|
||||||
|
|
||||||
|
resp := app.metrics_response()
|
||||||
|
body := resp.body
|
||||||
|
|
||||||
|
assert body.contains('ntfy_bridge_uptime_seconds')
|
||||||
|
assert body.contains('ntfy_bridge_notifications_total 42')
|
||||||
|
assert body.contains('ntfy_bridge_errors_total 3')
|
||||||
|
assert body.contains('ntfy_bridge_silence_active')
|
||||||
|
assert body.contains('ntfy_bridge_http_polls_total')
|
||||||
|
assert body.contains('# EOF')
|
||||||
|
}
|
||||||
@@ -15,9 +15,10 @@ pub struct Stats {
|
|||||||
pub mut:
|
pub mut:
|
||||||
total_notifications int
|
total_notifications int
|
||||||
errors int
|
errors int
|
||||||
|
http_polls int // v0.5: total HTTP poll checks
|
||||||
by_source map[string]int
|
by_source map[string]int
|
||||||
|
start_time time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct App {
|
pub struct App {
|
||||||
pub mut:
|
pub mut:
|
||||||
cfg Config
|
cfg Config
|
||||||
@@ -190,6 +191,9 @@ pub fn (mut app App) handle(req http.Request) http.Response {
|
|||||||
if req.url == '/api/silence' {
|
if req.url == '/api/silence' {
|
||||||
return app.silence_status_response()
|
return app.silence_status_response()
|
||||||
}
|
}
|
||||||
|
if req.url == '/metrics' {
|
||||||
|
return app.metrics_response()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.post {
|
.post {
|
||||||
if req.url.starts_with('/api/silence') {
|
if req.url.starts_with('/api/silence') {
|
||||||
@@ -409,6 +413,7 @@ fn (mut app App) http_poll_loop() {
|
|||||||
}
|
}
|
||||||
for src in app.cfg.sources.http_poll {
|
for src in app.cfg.sources.http_poll {
|
||||||
for check in src.checks {
|
for check in src.checks {
|
||||||
|
app.stats.http_polls++
|
||||||
app.run_http_check(check)
|
app.run_http_check(check)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user