hook up prometheus and fedi

This commit is contained in:
41666 2024-06-25 22:46:58 -04:00
parent f7ff87e88c
commit 947600efb7
7 changed files with 305 additions and 35 deletions

109
fedi.go Normal file
View file

@ -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)
}

12
go.mod
View file

@ -1,3 +1,15 @@
module git.sapphic.engineer/noe/plapkit module git.sapphic.engineer/noe/plapkit
go 1.22.3 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
)

18
go.sum Normal file
View file

@ -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=

View file

@ -6,34 +6,18 @@ import (
"net/http" "net/http"
) )
type HookPayload[D HookPayloadData] struct { type HookPayload struct {
Type string `json:"type"` Type string `json:"type"`
SigningToken string `json:"signing_token"` SigningToken string `json:"signing_token"`
SystemID string `json:"system_id"` SystemID string `json:"system_id"`
ID string `json:"id,omitempty"` ID string `json:"id,omitempty"`
Data D `json:"data,omitempty"` Data map[string]interface{} `json:"data,omitempty"`
} }
type Switch struct { type hookHandler func(h HookPayload)
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])
var ( var (
hookHandlers map[string]hookHandler[HookPayloadData] = map[string]hookHandler[HookPayloadData]{ hookHandlers map[string]hookHandler = map[string]hookHandler{
"CREATE_SWITCH": handleHookCreateSwitch, "CREATE_SWITCH": handleHookCreateSwitch,
"CREATE_MESSAGE": handleHookCreateMessage, "CREATE_MESSAGE": handleHookCreateMessage,
} }
@ -45,25 +29,29 @@ func postGetHookToken(rw http.ResponseWriter, req *http.Request) {
return return
} }
var payload HookPayload[HookPayloadData] var payload HookPayload
err := json.NewDecoder(req.Body).Decode(&payload) err := json.NewDecoder(req.Body).Decode(&payload)
if err != nil { if err != nil {
log.Println("[postGetHookToken] decode failed", err)
errBadRequest(rw) errBadRequest(rw)
return return
} }
if payload.SigningToken != SigningToken { if payload.SigningToken != SigningToken {
log.Println("[postGetHookToken] signing token did not match")
errUnauthorized(rw) errUnauthorized(rw)
return return
} }
if payload.Type == "PING" { if payload.Type == "PING" {
log.Println("[postGetHookToken] PING from PluralKit")
basicOk(rw) basicOk(rw)
return return
} }
handler, ok := hookHandlers[payload.Type] handler, ok := hookHandlers[payload.Type]
if !ok { if !ok {
log.Println("[postGetHookToken] no handler for event", payload.Type)
basicNoContent(rw) basicNoContent(rw)
return return
} }
@ -71,20 +59,44 @@ func postGetHookToken(rw http.ResponseWriter, req *http.Request) {
go handler(payload) go handler(payload)
} }
func handleHookCreateSwitch(h HookPayload[HookPayloadData]) { const SwitchOut = "00000000-0000-0000-0000-000000000000"
hook, ok := h.Data.(Switch)
func handleHookCreateSwitch(h HookPayload) {
members, ok := h.Data["members"].([]interface{})
if !ok { 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]) { // switch out, stop
hook, ok := h.Data.(Message) front := SwitchOut
if !ok { if len(members) != 0 {
log.Printf("[handleHookCreateMessage] could not cast hook payload data to Message, got: %v\n", h) 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)
} }

View file

@ -4,6 +4,8 @@ import (
"log" "log"
"net/http" "net/http"
"os" "os"
"github.com/prometheus/client_golang/prometheus/promhttp"
) )
var ( var (
@ -17,8 +19,13 @@ func main() {
} }
mux := http.NewServeMux() mux := http.NewServeMux()
// our routes
mux.HandleFunc("/hook/{token}", postGetHookToken) mux.HandleFunc("/hook/{token}", postGetHookToken)
// prometheus
mux.Handle("/metrics", promhttp.Handler())
log.Println("[main] http server listening on", listenAddr) log.Println("[main] http server listening on", listenAddr)
err := http.ListenAndServe(listenAddr, mux) err := http.ListenAndServe(listenAddr, mux)
if err != nil { if err != nil {

59
pluralkit.go Normal file
View file

@ -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
}

53
prometheus.go Normal file
View file

@ -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()
}