mirror of https://github.com/grafana/grafana
Alerting: QoL improvements to the unified alerting multi-replica devenv (#64907)
parent
e22e12455d
commit
406431df4e
@ -0,0 +1,12 @@ |
|||||||
|
FROM golang:1.19 |
||||||
|
|
||||||
|
ADD webhook-listener.go /go/src/webhook/webhook-listener.go |
||||||
|
|
||||||
|
WORKDIR /go/src/webhook |
||||||
|
|
||||||
|
RUN mkdir /tmp/logs |
||||||
|
RUN go build -o /bin webhook-listener.go |
||||||
|
|
||||||
|
ENV PORT 8080 |
||||||
|
|
||||||
|
ENTRYPOINT [ "/bin/webhook-listener" ] |
@ -0,0 +1,23 @@ |
|||||||
|
services: |
||||||
|
grafana: |
||||||
|
image: grafana/grafana-dev:3a22eba17f23b18faa27436ab2f9c3ea977b550b |
||||||
|
volumes: |
||||||
|
- ./grafana/provisioning/:/etc/grafana/provisioning/ |
||||||
|
environment: |
||||||
|
- VIRTUAL_HOST=grafana.loc |
||||||
|
- GF_FEATURE_TOGGLES_ENABLE=ngalert |
||||||
|
- GF_UNIFIED_ALERTING_HA_PEERS=ha-test-unified-alerting-grafana2-1:9094,ha-test-unified-alerting-grafana1-1:9094,ha-test-unified-alerting-grafana3-1:9094,ha-test-unified-alerting-grafana4-1:9094 |
||||||
|
- GF_SERVER_ROOT_URL=http://grafana.loc |
||||||
|
- GF_DATABASE_NAME=grafana |
||||||
|
- GF_DATABASE_USER=grafana |
||||||
|
- GF_DATABASE_PASSWORD=password |
||||||
|
- GF_DATABASE_TYPE=mysql |
||||||
|
- GF_DATABASE_HOST=db:3306 |
||||||
|
- GF_DATABASE_MAX_OPEN_CONN=300 |
||||||
|
- GF_SESSION_PROVIDER=mysql |
||||||
|
- GF_SESSION_PROVIDER_CONFIG=grafana:password@tcp(db:3306)/grafana?allowNativePasswords=true |
||||||
|
healthcheck: |
||||||
|
test: timeout 10s bash -c ':> /dev/tcp/127.0.0.1/3000' || exit 1 |
||||||
|
interval: 5s |
||||||
|
timeout: 15s |
||||||
|
retries: 3 |
@ -0,0 +1,181 @@ |
|||||||
|
package main |
||||||
|
|
||||||
|
import ( |
||||||
|
"encoding/json" |
||||||
|
"flag" |
||||||
|
"fmt" |
||||||
|
"io/ioutil" |
||||||
|
"log" |
||||||
|
"net/http" |
||||||
|
"os" |
||||||
|
"path" |
||||||
|
"path/filepath" |
||||||
|
"sync" |
||||||
|
"time" |
||||||
|
) |
||||||
|
|
||||||
|
var ( |
||||||
|
fingerprints = make(Fingerprints) |
||||||
|
mu sync.Mutex |
||||||
|
waitSeconds int |
||||||
|
logFile bool |
||||||
|
logFileName = filepath.Join(os.TempDir(), "/logs/webhook-listener.log") |
||||||
|
dumpDir = filepath.Join(os.TempDir(), "/logs/dumps") |
||||||
|
) |
||||||
|
|
||||||
|
type Alert struct { |
||||||
|
Fingerprint string `json:"fingerprint"` |
||||||
|
StartsAt time.Time `json:"startsAt"` |
||||||
|
Status string `json:"status"` |
||||||
|
} |
||||||
|
|
||||||
|
type Data struct { |
||||||
|
Receiver string `json:"receiver"` |
||||||
|
Status string `json:"status"` |
||||||
|
Alerts []Alert `json:"alerts"` |
||||||
|
} |
||||||
|
|
||||||
|
// Fingerprints keeps track of the number of alerts received
|
||||||
|
// by fingerprint and StartsAt time.
|
||||||
|
type Fingerprints map[string]map[time.Time]tracker |
||||||
|
|
||||||
|
type tracker struct { |
||||||
|
Updates int `json:"updates"` |
||||||
|
Statuses []string `json:"statuses"` |
||||||
|
} |
||||||
|
|
||||||
|
func updateFingerprints(v Data) { |
||||||
|
mu.Lock() |
||||||
|
defer mu.Unlock() |
||||||
|
for _, alert := range v.Alerts { |
||||||
|
m, ok := fingerprints[alert.Fingerprint] |
||||||
|
if !ok { |
||||||
|
m = make(map[time.Time]tracker) |
||||||
|
} |
||||||
|
|
||||||
|
t, ok := m[alert.StartsAt] |
||||||
|
if !ok { |
||||||
|
t = tracker{ |
||||||
|
Updates: 0, |
||||||
|
Statuses: []string{}, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
t.Updates += 1 |
||||||
|
t.Statuses = append(t.Statuses, alert.Status) |
||||||
|
|
||||||
|
m[alert.StartsAt] = t |
||||||
|
fingerprints[alert.Fingerprint] = m |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func parseFlags() { |
||||||
|
flag.BoolVar(&logFile, "log-file", true, "Whether to log to file") |
||||||
|
flag.IntVar(&waitSeconds, "wait-seconds", 0, "The number of seconds to wait before sending an HTTP response") |
||||||
|
flag.Parse() |
||||||
|
} |
||||||
|
|
||||||
|
func saveDump(data []byte) { |
||||||
|
if !logFile { |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
if len(data) == 0 { |
||||||
|
fmt.Println("empty dump - not saving") |
||||||
|
return |
||||||
|
} |
||||||
|
ts := time.Now().UnixNano() |
||||||
|
name := path.Join(dumpDir, fmt.Sprintf("%d.json", ts)) |
||||||
|
for i := 1; i <= 1000; i++ { |
||||||
|
if _, err := os.Stat(name); os.IsNotExist(err) { |
||||||
|
break |
||||||
|
} |
||||||
|
name = path.Join(dumpDir, fmt.Sprintf("%d_%04d.json", ts, i)) |
||||||
|
} |
||||||
|
log.Printf("saving dump to %s", name) |
||||||
|
err := os.WriteFile(name, data, os.ModePerm) |
||||||
|
if err != nil { |
||||||
|
log.Printf("cannot save to file %s: %s\n", name, err) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func main() { |
||||||
|
parseFlags() |
||||||
|
|
||||||
|
_, err := os.Stat(dumpDir) |
||||||
|
if os.IsNotExist(err) { |
||||||
|
err = os.MkdirAll(dumpDir, os.ModePerm) |
||||||
|
if err != nil { |
||||||
|
log.Panicf("can't create directory '%s'", dumpDir) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if logFile { |
||||||
|
//create your file with desired read/write permissions
|
||||||
|
f, err := os.OpenFile(logFileName, os.O_WRONLY|os.O_CREATE|os.O_APPEND, os.ModePerm) |
||||||
|
if err != nil { |
||||||
|
log.Fatal(err) |
||||||
|
} |
||||||
|
defer f.Close() |
||||||
|
log.SetOutput(f) |
||||||
|
} |
||||||
|
|
||||||
|
waitDuration := time.Duration(waitSeconds) * time.Second |
||||||
|
http.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) { |
||||||
|
|
||||||
|
writer.WriteHeader(http.StatusOK) |
||||||
|
writer.Write([]byte(landingPage)) |
||||||
|
}) |
||||||
|
|
||||||
|
http.HandleFunc("/listen", func(w http.ResponseWriter, r *http.Request) { |
||||||
|
log.Printf("got submission from: %s\n", r.RemoteAddr) |
||||||
|
b, err := ioutil.ReadAll(r.Body) |
||||||
|
if err != nil { |
||||||
|
log.Println(err) |
||||||
|
w.WriteHeader(http.StatusBadRequest) |
||||||
|
return |
||||||
|
} |
||||||
|
saveDump(b) |
||||||
|
v := Data{} |
||||||
|
if err := json.Unmarshal(b, &v); err != nil { |
||||||
|
log.Println(err) |
||||||
|
w.WriteHeader(http.StatusBadRequest) |
||||||
|
return |
||||||
|
} |
||||||
|
fmt.Printf("receiver: %s, status: %s\n", v.Receiver, v.Status) |
||||||
|
updateFingerprints(v) |
||||||
|
<-time.After(waitDuration) |
||||||
|
}) |
||||||
|
http.HandleFunc("/fingerprints", func(w http.ResponseWriter, r *http.Request) { |
||||||
|
b, err := func() ([]byte, error) { |
||||||
|
mu.Lock() |
||||||
|
defer mu.Unlock() |
||||||
|
return json.Marshal(fingerprints) |
||||||
|
}() |
||||||
|
if err != nil { |
||||||
|
log.Println(err) |
||||||
|
w.WriteHeader(http.StatusInternalServerError) |
||||||
|
return |
||||||
|
} |
||||||
|
w.Header().Add("Content-Type", "application/json") |
||||||
|
w.Write(b) |
||||||
|
}) |
||||||
|
log.Println("Listening") |
||||||
|
log.Printf("Wait Duration %v\n", waitDuration) |
||||||
|
http.ListenAndServe("0.0.0.0:8080", nil) |
||||||
|
} |
||||||
|
|
||||||
|
const landingPage = ` |
||||||
|
<!doctype html> |
||||||
|
<html> |
||||||
|
<head> |
||||||
|
<title>Webhook listener</title> |
||||||
|
</head> |
||||||
|
<body> |
||||||
|
<h1>Webhook Listener<h1> |
||||||
|
|
||||||
|
<p> For setup, please point your webhook configuration to the "/listen" endpoint. </p> |
||||||
|
<p> For debugging, please use the "/fingerprints" endpoint. </p> |
||||||
|
</body> |
||||||
|
</html> |
||||||
|
` |
Loading…
Reference in new issue