diff --git a/crowdsec/exporter/main.go b/crowdsec/exporter/main.go index b24281b..3971690 100644 --- a/crowdsec/exporter/main.go +++ b/crowdsec/exporter/main.go @@ -28,6 +28,11 @@ var ( Help: "Unique IPs with at least one active decision across all origins.", }) + decisionRemaining = prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Name: "crowdsec_decision_remaining_seconds", + Help: "Remaining seconds before each local decision expires. Only origin=crowdsec is exposed to keep cardinality bounded (CAPI/lists can contain tens of thousands of IPs).", + }, []string{"origin", "ip", "scenario", "type"}) + lastFetchUnix = prometheus.NewGauge(prometheus.GaugeOpts{ Name: "crowdsec_exporter_last_fetch_unix", Help: "Unix timestamp of the last successful LAPI fetch.", @@ -44,6 +49,7 @@ type decision struct { Scenario string `json:"scenario"` Type string `json:"type"` Value string `json:"value"` + Duration string `json:"duration"` } func init() { @@ -51,6 +57,7 @@ func init() { decisionsActive, decisionsUniqueIPs, decisionsUniqueIPsTotal, + decisionRemaining, lastFetchUnix, fetchErrors, ) @@ -119,6 +126,7 @@ func fetchAndUpdate(client *http.Client, lapiURL, apiKey string) { decisionsActive.Reset() decisionsUniqueIPs.Reset() + decisionRemaining.Reset() for k, n := range counts { decisionsActive.WithLabelValues(k.origin, k.scenario, k.dtype).Set(float64(n)) } @@ -127,6 +135,17 @@ func fetchAndUpdate(client *http.Client, lapiURL, apiKey string) { } decisionsUniqueIPsTotal.Set(float64(len(allIPs))) + for _, d := range data { + if d.Origin != "crowdsec" { + continue + } + dur, err := time.ParseDuration(d.Duration) + if err != nil { + dur = 0 + } + decisionRemaining.WithLabelValues(d.Origin, d.Value, d.Scenario, d.Type).Set(dur.Seconds()) + } + fetchErrors.Set(0) lastFetchUnix.Set(float64(time.Now().Unix())) diff --git a/grafana/data/grafana/dashboards/crowdsec.json b/grafana/data/grafana/dashboards/crowdsec.json index 681de74..52c883d 100644 --- a/grafana/data/grafana/dashboards/crowdsec.json +++ b/grafana/data/grafana/dashboards/crowdsec.json @@ -428,6 +428,106 @@ "values": false } } + }, + { + "id": 15, + "type": "table", + "title": "Local bans detail", + "datasource": { + "type": "prometheus", + "uid": "prometheusdatasource" + }, + "gridPos": { + "x": 0, + "y": 24, + "w": 24, + "h": 10 + }, + "targets": [ + { + "expr": "crowdsec_decision_remaining_seconds{origin=\"crowdsec\"}", + "refId": "A", + "instant": true, + "format": "table", + "datasource": { + "type": "prometheus", + "uid": "prometheusdatasource" + } + } + ], + "transformations": [ + { + "id": "organize", + "options": { + "excludeByName": { + "Time": true, + "__name__": true, + "instance": true, + "job": true, + "origin": true + }, + "indexByName": { + "ip": 0, + "scenario": 1, + "type": 2, + "Value": 3 + }, + "renameByName": { + "ip": "IP", + "scenario": "Scenario", + "type": "Type", + "Value": "Time remaining" + } + } + } + ], + "fieldConfig": { + "defaults": { + "custom": { + "align": "auto", + "filterable": true, + "inspect": false + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Time remaining" + }, + "properties": [ + { + "id": "unit", + "value": "s" + }, + { + "id": "custom.cellOptions", + "value": { + "type": "color-text" + } + }, + { + "id": "color", + "value": { + "mode": "continuous-GrYlRd" + } + } + ] + } + ] + }, + "options": { + "showHeader": true, + "sortBy": [ + { + "displayName": "Time remaining", + "desc": true + } + ], + "footer": { + "show": false + } + } } ] } \ No newline at end of file