Compare commits
2 Commits
4111ce9153
...
6b962ea509
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b962ea509 | ||
|
|
b1d93a7698 |
10
config.go
10
config.go
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
48
main.go
@@ -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 {
|
||||||
|
|||||||
46
main_test.go
46
main_test.go
@@ -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 {
|
||||||
|
|||||||
70
message.go
70
message.go
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
34
storage.go
34
storage.go
@@ -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))
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user