Compare commits

...

2 Commits

Author SHA1 Message Date
bel
6b962ea509 parse datacenter from Tags field 2024-04-13 10:24:02 -06:00
bel
b1d93a7698 add /events and /eventnames 2024-04-13 10:15:29 -06:00
8 changed files with 179 additions and 40 deletions

View File

@@ -26,6 +26,7 @@ type Config struct {
LocalCheckpoint string LocalCheckpoint string
LocalTokenizer string LocalTokenizer string
AssetPattern string AssetPattern string
DatacenterPattern string
storage Storage storage Storage
queue Queue queue Queue
driver Driver driver Driver
@@ -38,9 +39,10 @@ func newConfig(ctx context.Context) (Config, error) {
func newConfigFromEnv(ctx context.Context, getEnv func(string) string) (Config, error) { func newConfigFromEnv(ctx context.Context, getEnv func(string) string) (Config, error) {
def := Config{ def := Config{
Port: 38080, Port: 38080,
OllamaModel: "gemma:2b", OllamaModel: "gemma:2b",
AssetPattern: `(dpg|svc|red)-[a-z0-9-]*`, AssetPattern: `(dpg|svc|red)-[a-z0-9-]*`,
DatacenterPattern: `[a-z]{4}[a-z]*-[0-9]`,
} }
var m map[string]any var m map[string]any
@@ -105,7 +107,7 @@ func newConfigFromEnv(ctx context.Context, getEnv func(string) string) (Config,
result.driver = pg result.driver = pg
} }
if result.FillWithTestdata { if result.FillWithTestdata {
if err := FillWithTestdata(ctx, result.driver, result.AssetPattern); err != nil { if err := FillWithTestdata(ctx, result.driver, result.AssetPattern, result.DatacenterPattern); err != nil {
return Config{}, err return Config{}, err
} }
} }

View File

@@ -23,7 +23,7 @@ type Driver interface {
Set(context.Context, string, string, []byte) error Set(context.Context, string, string, []byte) error
} }
func FillWithTestdata(ctx context.Context, driver Driver, assetPattern string) error { func FillWithTestdata(ctx context.Context, driver Driver, assetPattern, datacenterPattern string) error {
d := "./testdata/slack_events" d := "./testdata/slack_events"
entries, err := os.ReadDir(d) entries, err := os.ReadDir(d)
if err != nil { if err != nil {
@@ -37,7 +37,7 @@ func FillWithTestdata(ctx context.Context, driver Driver, assetPattern string) e
if err != nil { if err != nil {
return err return err
} }
m, err := ParseSlack(b, assetPattern) m, err := ParseSlack(b, assetPattern, datacenterPattern)
if errors.Is(err, ErrIrrelevantMessage) { if errors.Is(err, ErrIrrelevantMessage) {
continue continue
} else if err != nil { } else if err != nil {

View File

@@ -17,7 +17,7 @@ func TestFillTestdata(t *testing.T) {
defer can() defer can()
ram := NewRAM() ram := NewRAM()
if err := FillWithTestdata(ctx, ram, renderAssetPattern); err != nil { if err := FillWithTestdata(ctx, ram, renderAssetPattern, renderDatacenterPattern); err != nil {
t.Fatal(err) t.Fatal(err)
} }
n := 0 n := 0

48
main.go
View File

@@ -65,6 +65,8 @@ func newHandler(cfg Config) http.HandlerFunc {
mux := http.NewServeMux() mux := http.NewServeMux()
mux.Handle("POST /api/v1/events/slack", http.HandlerFunc(newHandlerPostAPIV1EventsSlack(cfg))) mux.Handle("POST /api/v1/events/slack", http.HandlerFunc(newHandlerPostAPIV1EventsSlack(cfg)))
mux.Handle("GET /api/v1/eventnames", http.HandlerFunc(newHandlerGetAPIV1EventNames(cfg)))
mux.Handle("GET /api/v1/events", http.HandlerFunc(newHandlerGetAPIV1Events(cfg)))
mux.Handle("GET /api/v1/messages", http.HandlerFunc(newHandlerGetAPIV1Messages(cfg))) mux.Handle("GET /api/v1/messages", http.HandlerFunc(newHandlerGetAPIV1Messages(cfg)))
mux.Handle("GET /api/v1/threads", http.HandlerFunc(newHandlerGetAPIV1Threads(cfg))) mux.Handle("GET /api/v1/threads", http.HandlerFunc(newHandlerGetAPIV1Threads(cfg)))
mux.Handle("GET /api/v1/threads/{thread}", http.HandlerFunc(newHandlerGetAPIV1ThreadsThread(cfg))) mux.Handle("GET /api/v1/threads/{thread}", http.HandlerFunc(newHandlerGetAPIV1ThreadsThread(cfg)))
@@ -80,6 +82,50 @@ func newHandler(cfg Config) http.HandlerFunc {
} }
} }
func newHandlerGetAPIV1EventNames(cfg Config) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if !basicAuth(cfg, w, r) {
return
}
since, err := parseSince(r.URL.Query().Get("since"))
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
eventNames, err := cfg.storage.EventNamesSince(r.Context(), since)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
encodeResponse(w, r, map[string]any{"eventNames": eventNames})
}
}
func newHandlerGetAPIV1Events(cfg Config) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if !basicAuth(cfg, w, r) {
return
}
since, err := parseSince(r.URL.Query().Get("since"))
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
events, err := cfg.storage.EventsSince(r.Context(), since)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
encodeResponse(w, r, map[string]any{"events": events})
}
}
func newHandlerGetAPIV1Messages(cfg Config) http.HandlerFunc { func newHandlerGetAPIV1Messages(cfg Config) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
if !basicAuth(cfg, w, r) { if !basicAuth(cfg, w, r) {
@@ -200,7 +246,7 @@ func _newHandlerPostAPIV1EventsSlack(cfg Config) http.HandlerFunc {
return return
} }
m, err := ParseSlack(b, cfg.AssetPattern) m, err := ParseSlack(b, cfg.AssetPattern, cfg.DatacenterPattern)
if errors.Is(err, ErrIrrelevantMessage) { if errors.Is(err, ErrIrrelevantMessage) {
return return
} else if err != nil { } else if err != nil {

View File

@@ -104,6 +104,52 @@ func TestRun(t *testing.T) {
} }
}) })
t.Run("GET /api/v1/eventnames", func(t *testing.T) {
resp, err := http.Get(fmt.Sprintf("%s/api/v1/eventnames", u))
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
b, _ := io.ReadAll(resp.Body)
t.Fatalf("(%d) %s", resp.StatusCode, b)
}
var result struct {
EventNames []string
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
t.Fatal(err)
} else if result.EventNames[0] != "[Oregon-1] Wal Receive Count Alert" {
t.Fatal(result.EventNames)
} else {
t.Logf("%+v", result)
}
})
t.Run("GET /api/v1/events", func(t *testing.T) {
resp, err := http.Get(fmt.Sprintf("%s/api/v1/events", u))
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
b, _ := io.ReadAll(resp.Body)
t.Fatalf("(%d) %s", resp.StatusCode, b)
}
var result struct {
Events []string
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
t.Fatal(err)
} else if result.Events[0] != "11067" {
t.Fatal(result.Events)
} else {
t.Logf("%+v", result)
}
})
t.Run("GET /api/v1/threads", func(t *testing.T) { t.Run("GET /api/v1/threads", func(t *testing.T) {
resp, err := http.Get(fmt.Sprintf("%s/api/v1/threads", u)) resp, err := http.Get(fmt.Sprintf("%s/api/v1/threads", u))
if err != nil { if err != nil {

View File

@@ -14,16 +14,17 @@ var (
) )
type Message struct { type Message struct {
ID string ID string
TS uint64 TS uint64
Source string Source string
Channel string Channel string
Thread string Thread string
EventName string EventName string
Event string Event string
Plaintext string Plaintext string
Asset string Asset string
Resolved bool Resolved bool
Datacenter string
} }
func (m Message) Empty() bool { func (m Message) Empty() bool {
@@ -106,8 +107,9 @@ type (
slackAction struct{} slackAction struct{}
) )
func ParseSlack(b []byte, assetPattern string) (Message, error) { func ParseSlack(b []byte, assetPattern, datacenterPattern string) (Message, error) {
asset := regexp.MustCompile(assetPattern) asset := regexp.MustCompile(assetPattern)
datacenter := regexp.MustCompile(datacenterPattern)
s, err := parseSlack(b) s, err := parseSlack(b)
if err != nil { if err != nil {
@@ -120,17 +122,24 @@ func ParseSlack(b []byte, assetPattern string) (Message, error) {
} else if !strings.Contains(s.Event.Attachments[0].Title, ": Firing: ") { } else if !strings.Contains(s.Event.Attachments[0].Title, ": Firing: ") {
return Message{}, ErrIrrelevantMessage return Message{}, ErrIrrelevantMessage
} }
var tagsField string
for _, field := range s.Event.Attachments[0].Fields {
if field.Title == "Tags" {
tagsField = field.Value
}
}
return Message{ return Message{
ID: fmt.Sprintf("%s/%v", s.Event.ID, s.TS), ID: fmt.Sprintf("%s/%v", s.Event.ID, s.TS),
TS: s.TS, TS: s.TS,
Source: fmt.Sprintf(`https://renderinc.slack.com/archives/%s/p%s`, s.Event.Channel, strings.ReplaceAll(s.Event.ID, ".", "")), Source: fmt.Sprintf(`https://renderinc.slack.com/archives/%s/p%s`, s.Event.Channel, strings.ReplaceAll(s.Event.ID, ".", "")),
Channel: s.Event.Channel, Channel: s.Event.Channel,
Thread: s.Event.ID, Thread: s.Event.ID,
EventName: strings.Split(s.Event.Attachments[0].Title, ": Firing: ")[1], EventName: strings.Split(s.Event.Attachments[0].Title, ": Firing: ")[1],
Event: strings.TrimPrefix(strings.Split(s.Event.Attachments[0].Title, ":")[0], "#"), Event: strings.TrimPrefix(strings.Split(s.Event.Attachments[0].Title, ":")[0], "#"),
Plaintext: s.Event.Attachments[0].Text, Plaintext: s.Event.Attachments[0].Text,
Asset: asset.FindString(s.Event.Attachments[0].Text), Asset: asset.FindString(s.Event.Attachments[0].Text),
Resolved: !strings.HasPrefix(s.Event.Attachments[0].Color, "F"), Resolved: !strings.HasPrefix(s.Event.Attachments[0].Color, "F"),
Datacenter: datacenter.FindString(tagsField),
}, nil }, nil
} }
@@ -138,15 +147,16 @@ func ParseSlack(b []byte, assetPattern string) (Message, error) {
return Message{}, ErrIrrelevantMessage return Message{}, ErrIrrelevantMessage
} }
return Message{ return Message{
ID: fmt.Sprintf("%s/%v", s.Event.ParentID, s.TS), ID: fmt.Sprintf("%s/%v", s.Event.ParentID, s.TS),
TS: s.TS, TS: s.TS,
Source: fmt.Sprintf(`https://renderinc.slack.com/archives/%s/p%s`, s.Event.Channel, strings.ReplaceAll(s.Event.ParentID, ".", "")), Source: fmt.Sprintf(`https://renderinc.slack.com/archives/%s/p%s`, s.Event.Channel, strings.ReplaceAll(s.Event.ParentID, ".", "")),
Channel: s.Event.Channel, Channel: s.Event.Channel,
Thread: s.Event.ParentID, Thread: s.Event.ParentID,
EventName: "", EventName: "",
Event: "", Event: "",
Plaintext: s.Event.Text, Plaintext: s.Event.Text,
Asset: asset.FindString(s.Event.Text), Asset: asset.FindString(s.Event.Text),
Datacenter: datacenter.FindString(s.Event.Text),
}, nil }, nil
} }

View File

@@ -8,7 +8,8 @@ import (
) )
var ( var (
renderAssetPattern = `(dpg|svc|red)-[a-z0-9-]*[a-z0-9]` renderAssetPattern = `(dpg|svc|red)-[a-z0-9-]*[a-z0-9]`
renderDatacenterPattern = `[a-z]{4}[a-z]*-[0-9]`
) )
func TestParseSlackTestdata(t *testing.T) { func TestParseSlackTestdata(t *testing.T) {
@@ -139,7 +140,7 @@ func TestParseSlackTestdata(t *testing.T) {
}) })
t.Run("ParseSlack", func(t *testing.T) { t.Run("ParseSlack", func(t *testing.T) {
got, err := ParseSlack(b, renderAssetPattern) got, err := ParseSlack(b, renderAssetPattern, renderDatacenterPattern)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

@@ -25,6 +25,40 @@ func (s Storage) MessagesSince(ctx context.Context, t time.Time) ([]Message, err
}) })
} }
func (s Storage) EventNamesSince(ctx context.Context, t time.Time) ([]string, error) {
messages, err := s.MessagesSince(ctx, t)
if err != nil {
return nil, err
}
names := map[string]struct{}{}
for _, m := range messages {
names[m.EventName] = struct{}{}
}
result := make([]string, 0, len(names))
for k := range names {
result = append(result, k)
}
sort.Strings(result)
return result, nil
}
func (s Storage) EventsSince(ctx context.Context, t time.Time) ([]string, error) {
messages, err := s.MessagesSince(ctx, t)
if err != nil {
return nil, err
}
events := map[string]struct{}{}
for _, m := range messages {
events[m.Event] = struct{}{}
}
result := make([]string, 0, len(events))
for k := range events {
result = append(result, k)
}
sort.Strings(result)
return result, nil
}
func (s Storage) Threads(ctx context.Context) ([]string, error) { func (s Storage) Threads(ctx context.Context) ([]string, error) {
return s.ThreadsSince(ctx, time.Unix(0, 0)) return s.ThreadsSince(ctx, time.Unix(0, 0))
} }