From 947600efb7a30a5e3a0d8667f99518b8066d525a Mon Sep 17 00:00:00 2001 From: noe Date: Tue, 25 Jun 2024 22:46:58 -0400 Subject: [PATCH] hook up prometheus and fedi --- fedi.go | 109 ++++++++++++++++++++++++++++++++++++++++++++++++ go.mod | 12 ++++++ go.sum | 18 ++++++++ hook_handler.go | 82 ++++++++++++++++++++---------------- main.go | 7 ++++ pluralkit.go | 59 ++++++++++++++++++++++++++ prometheus.go | 53 +++++++++++++++++++++++ 7 files changed, 305 insertions(+), 35 deletions(-) create mode 100644 fedi.go create mode 100644 go.sum create mode 100644 pluralkit.go create mode 100644 prometheus.go diff --git a/fedi.go b/fedi.go new file mode 100644 index 0000000..6ec8018 --- /dev/null +++ b/fedi.go @@ -0,0 +1,109 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "net/url" + "os" + "time" +) + +var ( + dsiApi = NewIceshrimpAPI("https://dis.sociat.ing/api", os.Getenv("DSI_TOKEN"), os.Getenv("DSI_FIELD_NAME")) +) + +type IceshrimpAPI struct { + Client http.Client + Token string + BaseURL string + FieldName string +} + +func NewIceshrimpAPI(baseURL string, token string, fieldName string) IceshrimpAPI { + return IceshrimpAPI{ + Client: http.Client{Timeout: time.Second * 30}, + BaseURL: baseURL, + Token: token, + FieldName: fieldName, + } +} + +func (i IceshrimpAPI) UpdateFrontField(newValue string) { + log.Println("UpdateFrontField") + + headers := http.Header{} + headers.Add("authorization", fmt.Sprintf("Bearer %s", i.Token)) + + url, err := url.Parse(fmt.Sprintf("%s/i", i.BaseURL)) + if err != nil { + log.Printf("[IceshrimpAPI] %s/i is not a valid URL\n%v\n", i.BaseURL, err) + return + } + + resp, err := i.Client.Do(&http.Request{ + URL: url, + Header: headers, + Method: "POST", + }) + if err != nil || resp.StatusCode != 200 { + // todo: better error for non-200 + log.Printf("[IceshrimpAPI] %s/i failed\n%v\n", i.BaseURL, err) + return + } + + type field struct { + Name string `json:"name"` + Value string `json:"value"` + } + + type profileFields struct { + Fields []field `json:"fields"` + } + + var fields profileFields + err = json.NewDecoder(resp.Body).Decode(&fields) + if err != nil { + log.Println("[IceshrimpAPI] json decode failed", err) + return + } + + log.Println(fields) + + for idx, field := range fields.Fields { + if field.Name == i.FieldName { + fields.Fields[idx].Value = newValue + break + } + } + + url, err = url.Parse(fmt.Sprintf("%s/i/update", i.BaseURL)) + if err != nil { + log.Printf("[IceshrimpAPI] %s/i/update is not a valid URL\n%v\n", i.BaseURL, err) + return + } + + buf := bytes.Buffer{} + err = json.NewEncoder(&buf).Encode(fields) + if err != nil { + log.Println("[IceshrimpAPI] json encode failed", err) + return + } + + log.Println(buf.String()) + + resp, err = i.Client.Do(&http.Request{ + URL: url, + Method: "POST", + Header: headers, + Body: io.NopCloser(&buf), + }) + if err != nil { + log.Println("[IceshrimpAPI] update call failed", err) + } + + json.NewDecoder(os.Stderr).Decode(resp.Body) +} diff --git a/go.mod b/go.mod index 52fa425..8048edb 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,15 @@ module git.sapphic.engineer/noe/plapkit go 1.22.3 + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect + github.com/prometheus/client_golang v1.19.1 // indirect + github.com/prometheus/client_model v0.5.0 // indirect + github.com/prometheus/common v0.48.0 // indirect + github.com/prometheus/procfs v0.12.0 // indirect + golang.org/x/sys v0.17.0 // indirect + google.golang.org/protobuf v1.33.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..5a54ad4 --- /dev/null +++ b/go.sum @@ -0,0 +1,18 @@ +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= +github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= +github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= +github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= +github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE= +github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= +github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= +github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= +golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= diff --git a/hook_handler.go b/hook_handler.go index b5e3d4b..f400a76 100644 --- a/hook_handler.go +++ b/hook_handler.go @@ -6,34 +6,18 @@ import ( "net/http" ) -type HookPayload[D HookPayloadData] struct { - Type string `json:"type"` - SigningToken string `json:"signing_token"` - SystemID string `json:"system_id"` - ID string `json:"id,omitempty"` - Data D `json:"data,omitempty"` +type HookPayload struct { + Type string `json:"type"` + SigningToken string `json:"signing_token"` + SystemID string `json:"system_id"` + ID string `json:"id,omitempty"` + Data map[string]interface{} `json:"data,omitempty"` } -type Switch struct { - ID string `json:"id"` - Timestamp string `json:"timestamp"` - Members []string `json:"members"` -} - -type Message struct { - ID string `json:"id"` - Timestamp string `json:"timestamp"` - Member string `json:"member"` -} - -type HookPayloadData interface { - Switch | Message | interface{} -} - -type hookHandler[D HookPayloadData] func(h HookPayload[D]) +type hookHandler func(h HookPayload) var ( - hookHandlers map[string]hookHandler[HookPayloadData] = map[string]hookHandler[HookPayloadData]{ + hookHandlers map[string]hookHandler = map[string]hookHandler{ "CREATE_SWITCH": handleHookCreateSwitch, "CREATE_MESSAGE": handleHookCreateMessage, } @@ -45,25 +29,29 @@ func postGetHookToken(rw http.ResponseWriter, req *http.Request) { return } - var payload HookPayload[HookPayloadData] + var payload HookPayload err := json.NewDecoder(req.Body).Decode(&payload) if err != nil { + log.Println("[postGetHookToken] decode failed", err) errBadRequest(rw) return } if payload.SigningToken != SigningToken { + log.Println("[postGetHookToken] signing token did not match") errUnauthorized(rw) return } if payload.Type == "PING" { + log.Println("[postGetHookToken] PING from PluralKit") basicOk(rw) return } handler, ok := hookHandlers[payload.Type] if !ok { + log.Println("[postGetHookToken] no handler for event", payload.Type) basicNoContent(rw) return } @@ -71,20 +59,44 @@ func postGetHookToken(rw http.ResponseWriter, req *http.Request) { go handler(payload) } -func handleHookCreateSwitch(h HookPayload[HookPayloadData]) { - hook, ok := h.Data.(Switch) +const SwitchOut = "00000000-0000-0000-0000-000000000000" + +func handleHookCreateSwitch(h HookPayload) { + members, ok := h.Data["members"].([]interface{}) if !ok { - log.Printf("[handleHookCreateSwitch] could not cast hook payload data to Switch, got: %v\n", h) + log.Println("[handleHookCreateSwitch] ERR rejected, missing members", h.Data) + return } - log.Printf("[handleHookCreateSwitch] got hook: %v\n", hook) -} + log.Println("[handleHookCreateSwitch] got switch", members, h) -func handleHookCreateMessage(h HookPayload[HookPayloadData]) { - hook, ok := h.Data.(Message) - if !ok { - log.Printf("[handleHookCreateMessage] could not cast hook payload data to Message, got: %v\n", h) + // switch out, stop + front := SwitchOut + if len(members) != 0 { + front, ok = members[0].(string) + if !ok { + log.Println("[handleHookCreateSwitch] ERR rejected, first member wasn't a string", members) + return + } } - log.Printf("[handleHookCreateMessage] got hook: %v\n", hook) + member, err := pkApi.GetMember(front) + if err != nil { + log.Printf("[handleHookCreateSwitch] pk member fetch failed, skipping named requirements") + } else { + go dsiApi.UpdateFrontField(member.Name) + } + + go promCountSwitches(h, members) + // TODO: discord nickname updates +} + +func handleHookCreateMessage(h HookPayload) { + _, ok := h.Data["member"].(map[string]interface{}) + if !ok { + log.Println("[handleHookCreateMessage] rejected, missing member", h.Data) + return + } + + go promCountMessage(h) } diff --git a/main.go b/main.go index 361aea0..11c00f0 100644 --- a/main.go +++ b/main.go @@ -4,6 +4,8 @@ import ( "log" "net/http" "os" + + "github.com/prometheus/client_golang/prometheus/promhttp" ) var ( @@ -17,8 +19,13 @@ func main() { } mux := http.NewServeMux() + + // our routes mux.HandleFunc("/hook/{token}", postGetHookToken) + // prometheus + mux.Handle("/metrics", promhttp.Handler()) + log.Println("[main] http server listening on", listenAddr) err := http.ListenAndServe(listenAddr, mux) if err != nil { diff --git a/pluralkit.go b/pluralkit.go new file mode 100644 index 0000000..4b93361 --- /dev/null +++ b/pluralkit.go @@ -0,0 +1,59 @@ +package main + +import ( + "encoding/json" + "fmt" + "net/http" + "time" + + "github.com/hashicorp/golang-lru/v2/expirable" +) + +var ( + pkApi = NewPluralKit() +) + +type PluralKitMember struct { + UUID string `json:"uuid"` + Name string `json:"name"` +} + +type PluralKit struct { + Client http.Client + Cache *expirable.LRU[string, *PluralKitMember] + BaseURL string +} + +func NewPluralKit() PluralKit { + return PluralKit{ + BaseURL: "https://api.pluralkit.me/v2", + Client: http.Client{Timeout: time.Second * 30}, + Cache: expirable.NewLRU[string, *PluralKitMember](20, nil, time.Hour), + } +} + +func (pk *PluralKit) GetMember(id string) (*PluralKitMember, error) { + cached, ok := pk.Cache.Get(id) + if ok { + return cached, nil + } + + resp, err := pk.Client.Get(fmt.Sprintf("%s/members/%s", pk.BaseURL, id)) + if err != nil { + return nil, fmt.Errorf("fetching pluralkit member %s failed: %w", id, err) + } + + if resp.StatusCode != 200 { + return nil, fmt.Errorf("pluralkit did not find member %ss", id) + } + + var member PluralKitMember + err = json.NewDecoder(resp.Body).Decode(&member) + if err != nil { + return nil, fmt.Errorf("malformed json: %w", err) + } + + pk.Cache.Add(id, &member) + + return &member, nil +} diff --git a/prometheus.go b/prometheus.go new file mode 100644 index 0000000..b897101 --- /dev/null +++ b/prometheus.go @@ -0,0 +1,53 @@ +package main + +import ( + "log" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +var ( + switchCounter *prometheus.CounterVec = promauto.NewCounterVec(prometheus.CounterOpts{ + Namespace: "plapkit", + Name: "switches", + }, []string{"system", "member", "member_display", "role"}) + + messageCounter *prometheus.CounterVec = promauto.NewCounterVec(prometheus.CounterOpts{ + Namespace: "plapkit", + Name: "messages", + }, []string{"system", "member", "member_display"}) +) + +func promCountSwitches(h HookPayload, members []interface{}) { + for idx, id := range members { + role := "cofront" + if idx == 0 { + role = "front" + } + + member := &PluralKitMember{ + Name: "switched out", + } + + member2, err := pkApi.GetMember(id.(string)) + if err != nil { + member.Name = "%%lookup failed%%" + log.Println("[promCountSwitches] WARN lookup failed,", err) + } else { + member = member2 + } + + switchCounter.WithLabelValues(h.SystemID, id.(string), member.Name, role).Inc() + } +} + +func promCountMessage(h HookPayload) { + member, ok := h.Data["member"].(map[string]interface{}) + if !ok { + log.Println("[promCountMessage] failed to get member from data") + return + } + + messageCounter.WithLabelValues(h.SystemID, member["uuid"].(string), member["name"].(string)).Inc() +}