hook up prometheus and fedi
This commit is contained in:
parent
f7ff87e88c
commit
947600efb7
7 changed files with 305 additions and 35 deletions
109
fedi.go
Normal file
109
fedi.go
Normal 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
12
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
|
||||
)
|
||||
|
|
18
go.sum
Normal file
18
go.sum
Normal 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=
|
|
@ -6,34 +6,18 @@ import (
|
|||
"net/http"
|
||||
)
|
||||
|
||||
type HookPayload[D HookPayloadData] struct {
|
||||
type HookPayload 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"`
|
||||
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)
|
||||
// switch out, stop
|
||||
front := SwitchOut
|
||||
if len(members) != 0 {
|
||||
front, ok = members[0].(string)
|
||||
if !ok {
|
||||
log.Printf("[handleHookCreateMessage] could not cast hook payload data to Message, got: %v\n", h)
|
||||
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)
|
||||
}
|
||||
|
|
7
main.go
7
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 {
|
||||
|
|
59
pluralkit.go
Normal file
59
pluralkit.go
Normal 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
53
prometheus.go
Normal 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()
|
||||
}
|
Loading…
Add table
Reference in a new issue