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
|
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
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"
|
"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)
|
||||||
}
|
}
|
||||||
|
|
7
main.go
7
main.go
|
@ -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
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