From cb4d12822dd3ca28b112eff06aa8dc85d5682707 Mon Sep 17 00:00:00 2001 From: Aykhan Shahsuvarov Date: Sun, 17 May 2026 13:27:07 +0400 Subject: [PATCH] monitoring/crowdsec: add country flagss --- crowdsec/exporter/main.go | 24 +++++++++++++++---- grafana/data/grafana/dashboards/crowdsec.json | 12 ++++++---- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/crowdsec/exporter/main.go b/crowdsec/exporter/main.go index ac79f46..94c0bbe 100644 --- a/crowdsec/exporter/main.go +++ b/crowdsec/exporter/main.go @@ -31,6 +31,19 @@ func lookupCountry(ip string) string { return rec.Country.ISOCode } +// countryFlag converts a 2-letter ISO-3166-1 alpha-2 country code into its +// emoji flag using the Unicode regional indicator pairs trick (A-Z -> 0x1F1E6-FF). +func countryFlag(iso string) string { + if len(iso) != 2 { + return "" + } + a, b := iso[0], iso[1] + if a < 'A' || a > 'Z' || b < 'A' || b > 'Z' { + return "" + } + return string([]rune{rune(a-'A') + 0x1F1E6, rune(b-'A') + 0x1F1E6}) +} + var ( decisionsActive = prometheus.NewGaugeVec(prometheus.GaugeOpts{ Name: "crowdsec_decisions_active", @@ -50,7 +63,7 @@ var ( 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", "country", "scenario", "type"}) + }, []string{"origin", "ip", "country", "flag", "scenario", "type"}) lastFetchUnix = prometheus.NewGauge(prometheus.GaugeOpts{ Name: "crowdsec_exporter_last_fetch_unix", @@ -125,8 +138,8 @@ func fetchAndUpdate(client *http.Client, lapiURL, apiKey string) { ips := map[string]map[string]struct{}{} allIPs := map[string]struct{}{} type localDecision struct { - origin, ip, country, scenario, dtype string - remaining float64 + origin, ip, country, flag, scenario, dtype string + remaining float64 } var locals []localDecision @@ -156,7 +169,8 @@ func fetchAndUpdate(client *http.Client, lapiURL, apiKey string) { if perr != nil { dur = 0 } - locals = append(locals, localDecision{origin, d.Value, lookupCountry(d.Value), scenario, dtype, dur.Seconds()}) + c := lookupCountry(d.Value) + locals = append(locals, localDecision{origin, d.Value, c, countryFlag(c), scenario, dtype, dur.Seconds()}) } } @@ -171,7 +185,7 @@ func fetchAndUpdate(client *http.Client, lapiURL, apiKey string) { } decisionsUniqueIPsTotal.Set(float64(len(allIPs))) for _, l := range locals { - decisionRemaining.WithLabelValues(l.origin, l.ip, l.country, l.scenario, l.dtype).Set(l.remaining) + decisionRemaining.WithLabelValues(l.origin, l.ip, l.country, l.flag, l.scenario, l.dtype).Set(l.remaining) } fetchErrors.Set(0) diff --git a/grafana/data/grafana/dashboards/crowdsec.json b/grafana/data/grafana/dashboards/crowdsec.json index 636afab..decf1ca 100644 --- a/grafana/data/grafana/dashboards/crowdsec.json +++ b/grafana/data/grafana/dashboards/crowdsec.json @@ -428,13 +428,15 @@ "origin": true }, "indexByName": { - "ip": 0, - "country": 1, - "scenario": 2, - "type": 3, - "Value": 4 + "flag": 0, + "ip": 1, + "country": 2, + "scenario": 3, + "type": 4, + "Value": 5 }, "renameByName": { + "flag": "Flag", "ip": "IP", "country": "Country", "scenario": "Scenario",