Compare commits
14 Commits
2911b2f884
...
02331752fe
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
02331752fe | ||
|
|
0d3910829d | ||
|
|
8f288cf12e | ||
|
|
1b148092b9 | ||
|
|
24628f4ebb | ||
|
|
c51e580e09 | ||
|
|
82fc7526d0 | ||
|
|
02fecb7eb2 | ||
|
|
6ba23e61c3 | ||
|
|
8883856a63 | ||
|
|
634255725a | ||
|
|
233ba8ae2b | ||
|
|
ccee4a49da | ||
|
|
4080af952d |
26
config.go
26
config.go
@@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
@@ -8,6 +9,7 @@ import (
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
@@ -16,13 +18,17 @@ type Config struct {
|
||||
InitializeSlack bool
|
||||
SlackToken string
|
||||
SlackChannels string
|
||||
PostgresConn string
|
||||
storage Storage
|
||||
queue Queue
|
||||
driver Driver
|
||||
}
|
||||
|
||||
func newConfig() (Config, error) {
|
||||
return newConfigFromEnv(os.Getenv)
|
||||
func newConfig(ctx context.Context) (Config, error) {
|
||||
return newConfigFromEnv(ctx, os.Getenv)
|
||||
}
|
||||
|
||||
func newConfigFromEnv(getEnv func(string) string) (Config, error) {
|
||||
func newConfigFromEnv(ctx context.Context, getEnv func(string) string) (Config, error) {
|
||||
def := Config{
|
||||
Port: 8080,
|
||||
}
|
||||
@@ -73,5 +79,19 @@ func newConfigFromEnv(getEnv func(string) string) (Config, error) {
|
||||
} else if err := json.Unmarshal(b, &result); err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
|
||||
result.driver = NewRAM()
|
||||
if result.PostgresConn != "" {
|
||||
ctx, can := context.WithTimeout(ctx, time.Second*10)
|
||||
defer can()
|
||||
pg, err := NewPostgres(ctx, result.PostgresConn)
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
result.driver = pg
|
||||
}
|
||||
result.storage = NewStorage(result.driver)
|
||||
result.queue = NewQueue(result.driver)
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
package main
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewConfig(t *testing.T) {
|
||||
if got, err := newConfigFromEnv(func(k string) string {
|
||||
if got, err := newConfigFromEnv(context.Background(), func(k string) string {
|
||||
t.Logf("getenv(%s)", k)
|
||||
switch k {
|
||||
case "PORT":
|
||||
|
||||
195
driver.go
195
driver.go
@@ -2,11 +2,17 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"go.etcd.io/bbolt"
|
||||
|
||||
_ "github.com/lib/pq"
|
||||
)
|
||||
|
||||
type Driver interface {
|
||||
@@ -16,16 +22,197 @@ type Driver interface {
|
||||
Set(context.Context, string, string, []byte) error
|
||||
}
|
||||
|
||||
type Postgres struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func NewPostgres(ctx context.Context, conn string) (Postgres, error) {
|
||||
db, err := sql.Open("postgres", conn)
|
||||
if err != nil {
|
||||
return Postgres{}, err
|
||||
}
|
||||
|
||||
pg := Postgres{db: db}
|
||||
if err := pg.setup(ctx); err != nil {
|
||||
pg.Close()
|
||||
return Postgres{}, fmt.Errorf("failed setup: %w", err)
|
||||
}
|
||||
|
||||
return pg, nil
|
||||
}
|
||||
|
||||
func (pg Postgres) setup(ctx context.Context) error {
|
||||
tableQ, err := pg.table("q")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tableM, err := pg.table("m")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := pg.db.ExecContext(ctx, fmt.Sprintf(`
|
||||
CREATE TABLE IF NOT EXISTS %s (
|
||||
id TEXT NOT NULL,
|
||||
v JSONB NOT NULL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS %s (
|
||||
id TEXT NOT NULL,
|
||||
v JSONB NOT NULL
|
||||
);
|
||||
ALTER TABLE %s DROP CONSTRAINT IF EXISTS %s_id_unique;
|
||||
ALTER TABLE %s ADD CONSTRAINT %s_id_unique UNIQUE (id);
|
||||
ALTER TABLE %s DROP CONSTRAINT IF EXISTS %s_id_unique;
|
||||
ALTER TABLE %s ADD CONSTRAINT %s_id_unique UNIQUE (id);
|
||||
`, tableQ,
|
||||
tableM,
|
||||
tableQ, tableQ,
|
||||
tableQ, tableQ,
|
||||
tableM, tableM,
|
||||
tableM, tableM,
|
||||
)); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pg Postgres) table(s string) (string, error) {
|
||||
switch s {
|
||||
case "q":
|
||||
return "spoc_bot_vr_q", nil
|
||||
case "m":
|
||||
return "spoc_bot_vr_messages", nil
|
||||
}
|
||||
return "", errors.New("invalid table " + s)
|
||||
}
|
||||
|
||||
func (pg Postgres) Close() error {
|
||||
return pg.db.Close()
|
||||
}
|
||||
|
||||
func (pg Postgres) ForEach(ctx context.Context, ns string, cb func(string, []byte) error) error {
|
||||
table, err := pg.table(ns)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rows, err := pg.db.QueryContext(ctx, fmt.Sprintf(`SELECT id, v FROM %s;`, table))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var id string
|
||||
var v []byte
|
||||
if err := rows.Scan(&id, &v); err != nil {
|
||||
return err
|
||||
} else if err := cb(id, v); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
func (pg Postgres) Get(ctx context.Context, ns, id string) ([]byte, error) {
|
||||
table, err := pg.table(ns)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
row := pg.db.QueryRowContext(ctx, fmt.Sprintf(`SELECT v FROM %s WHERE id='%s';`, table, id))
|
||||
if err := row.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var v []byte
|
||||
if err := row.Scan(&v); err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func (pg Postgres) Set(ctx context.Context, ns, id string, v []byte) error {
|
||||
table, err := pg.table(ns)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if v == nil {
|
||||
_, err = pg.db.ExecContext(ctx, fmt.Sprintf(`DELETE FROM %s WHERE id='%s';`, table, id))
|
||||
return err
|
||||
|
||||
}
|
||||
|
||||
_, err = pg.db.ExecContext(ctx, fmt.Sprintf(`INSERT INTO %s (id, v) VALUES ('%s', '%s') ON CONFLICT (id) DO UPDATE SET v = '%s'`, table, id, v, v))
|
||||
return err
|
||||
}
|
||||
|
||||
type RAM struct {
|
||||
m map[string]map[string][]byte
|
||||
lock *sync.RWMutex
|
||||
}
|
||||
|
||||
func NewRAM() RAM {
|
||||
return RAM{
|
||||
m: make(map[string]map[string][]byte),
|
||||
lock: &sync.RWMutex{},
|
||||
}
|
||||
}
|
||||
|
||||
func (ram RAM) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ram RAM) ForEach(ctx context.Context, ns string, cb func(string, []byte) error) error {
|
||||
ram.lock.RLock()
|
||||
defer ram.lock.RUnlock()
|
||||
|
||||
for k, v := range ram.m[ns] {
|
||||
if ctx.Err() != nil {
|
||||
break
|
||||
}
|
||||
if err := cb(k, v); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
func (ram RAM) Get(_ context.Context, ns, id string) ([]byte, error) {
|
||||
ram.lock.RLock()
|
||||
defer ram.lock.RUnlock()
|
||||
|
||||
if _, ok := ram.m[ns]; !ok {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return ram.m[ns][id], nil
|
||||
}
|
||||
|
||||
func (ram RAM) Set(_ context.Context, ns, id string, v []byte) error {
|
||||
ram.lock.Lock()
|
||||
defer ram.lock.Unlock()
|
||||
|
||||
if _, ok := ram.m[ns]; !ok {
|
||||
ram.m[ns] = map[string][]byte{}
|
||||
}
|
||||
ram.m[ns][id] = v
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type BBolt struct {
|
||||
db *bbolt.DB
|
||||
}
|
||||
|
||||
func NewTestDB() BBolt {
|
||||
d, err := ioutil.TempDir(os.TempDir(), "test-db-*")
|
||||
func NewTestDBIn(d string) BBolt {
|
||||
d, err := ioutil.TempDir(d, "test-db-*")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
db, err := NewDB(d)
|
||||
db, err := NewDB(path.Join(d, "bb"))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
84
driver_test.go
Normal file
84
driver_test.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestPostgres(t *testing.T) {
|
||||
ctx, can := context.WithTimeout(context.Background(), time.Second*15)
|
||||
defer can()
|
||||
|
||||
conn := os.Getenv("INTEGRATION_POSTGRES_CONN")
|
||||
if conn == "" {
|
||||
t.Skip()
|
||||
}
|
||||
pg, err := NewPostgres(ctx, conn)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
testDriver(t, pg)
|
||||
}
|
||||
|
||||
func TestDriverRAM(t *testing.T) {
|
||||
testDriver(t, NewRAM())
|
||||
}
|
||||
|
||||
func TestDriverBBolt(t *testing.T) {
|
||||
testDriver(t, NewTestDBIn(t.TempDir()))
|
||||
}
|
||||
|
||||
func testDriver(t *testing.T, d Driver) {
|
||||
ctx, can := context.WithTimeout(context.Background(), time.Second*15)
|
||||
defer can()
|
||||
|
||||
defer d.Close()
|
||||
|
||||
if b, err := d.Get(ctx, "m", "id"); err != nil {
|
||||
t.Error("cannot get from empty:", err)
|
||||
} else if b != nil {
|
||||
t.Error("got fake from empty")
|
||||
}
|
||||
|
||||
if err := d.ForEach(ctx, "m", func(string, []byte) error {
|
||||
return errors.New("should have no hits")
|
||||
}); err != nil {
|
||||
t.Error("failed to forEach empty:", err)
|
||||
}
|
||||
|
||||
if err := d.Set(ctx, "m", "id", []byte(`"hello world"`)); err != nil {
|
||||
t.Error("cannot set from empty:", err)
|
||||
}
|
||||
|
||||
if b, err := d.Get(ctx, "m", "id"); err != nil {
|
||||
t.Error("cannot get from full:", err)
|
||||
} else if string(b) != `"hello world"` {
|
||||
t.Error("got fake from full")
|
||||
}
|
||||
|
||||
if err := d.ForEach(ctx, "m", func(id string, v []byte) error {
|
||||
if id != "id" {
|
||||
t.Error("for each id weird:", id)
|
||||
}
|
||||
if string(v) != `"hello world"` {
|
||||
t.Error("for each value weird:", string(v))
|
||||
}
|
||||
return io.EOF
|
||||
}); err != io.EOF {
|
||||
t.Error("failed to forEach full:", err)
|
||||
}
|
||||
|
||||
if err := d.Set(ctx, "m", "id", nil); err != nil {
|
||||
t.Error("cannot set from full:", err)
|
||||
}
|
||||
|
||||
if b, err := d.Get(ctx, "m", "id"); err != nil {
|
||||
t.Error("cannot get from deleted:", err)
|
||||
} else if b != nil {
|
||||
t.Error("got fake from deleted")
|
||||
}
|
||||
}
|
||||
5
go.mod
5
go.mod
@@ -7,4 +7,7 @@ require (
|
||||
go.etcd.io/bbolt v1.3.9
|
||||
)
|
||||
|
||||
require golang.org/x/sys v0.4.0 // indirect
|
||||
require (
|
||||
github.com/lib/pq v1.10.9 // indirect
|
||||
golang.org/x/sys v0.4.0 // indirect
|
||||
)
|
||||
|
||||
2
go.sum
2
go.sum
@@ -1,5 +1,7 @@
|
||||
github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk=
|
||||
github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
|
||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
go.etcd.io/bbolt v1.3.9 h1:8x7aARPEXiXbHmtUwAIv7eV2fQFHrLLavdiJ3uzJXoI=
|
||||
go.etcd.io/bbolt v1.3.9/go.mod h1:zaO32+Ti0PK1ivdPtgMESzuzL2VPoIG1PCQNvOdo/dE=
|
||||
golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18=
|
||||
|
||||
15
main.go
15
main.go
@@ -19,10 +19,11 @@ func main() {
|
||||
ctx, can := signal.NotifyContext(context.Background(), syscall.SIGINT)
|
||||
defer can()
|
||||
|
||||
cfg, err := newConfig()
|
||||
cfg, err := newConfig(ctx)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer cfg.driver.Close()
|
||||
|
||||
if err := run(ctx, cfg); err != nil && ctx.Err() == nil {
|
||||
panic(err)
|
||||
@@ -115,7 +116,15 @@ func _newHandlerPostAPIV1EventsSlack(cfg Config) http.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("slack event: %s", b)
|
||||
http.Error(w, "not impl", http.StatusNotImplemented)
|
||||
m, err := ParseSlack(b)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err := cfg.storage.Upsert(r.Context(), m); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
98
message.go
98
message.go
@@ -1,6 +1,11 @@
|
||||
package main
|
||||
|
||||
import "encoding/json"
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Message struct {
|
||||
ID string
|
||||
@@ -39,3 +44,94 @@ func Deserialize(b []byte) (Message, error) {
|
||||
err := json.Unmarshal(b, &m)
|
||||
return m, err
|
||||
}
|
||||
|
||||
type (
|
||||
slackMessage struct {
|
||||
TS uint64 `json:"event_time"`
|
||||
Event slackEvent
|
||||
}
|
||||
|
||||
slackEvent struct {
|
||||
ID string `json:"event_ts"`
|
||||
Channel string
|
||||
// human
|
||||
ParentID string `json:"thread_ts"`
|
||||
Text string
|
||||
Blocks []slackBlock
|
||||
// bot
|
||||
Bot slackBot `json:"bot_profile"`
|
||||
Attachments []slackAttachment
|
||||
}
|
||||
|
||||
slackBlock struct {
|
||||
Elements []slackElement
|
||||
}
|
||||
|
||||
slackElement struct {
|
||||
Elements []slackElement
|
||||
RichText string `json:"text"`
|
||||
}
|
||||
|
||||
slackBot struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
slackAttachment struct {
|
||||
Color string
|
||||
Title string
|
||||
Text string
|
||||
Fields []slackField
|
||||
Actions []slackAction
|
||||
}
|
||||
|
||||
slackField struct {
|
||||
Value string
|
||||
Title string
|
||||
}
|
||||
|
||||
slackAction struct{}
|
||||
)
|
||||
|
||||
func ParseSlack(b []byte) (Message, error) {
|
||||
s, err := parseSlack(b)
|
||||
if err != nil {
|
||||
return Message{}, err
|
||||
}
|
||||
|
||||
if s.Event.Bot.Name != "" {
|
||||
if len(s.Event.Attachments) == 0 {
|
||||
return Message{}, errors.New("bot message has no attachments")
|
||||
} else if !strings.Contains(s.Event.Attachments[0].Title, ": Firing: ") {
|
||||
return Message{}, errors.New("bot message attachment is not Firing")
|
||||
}
|
||||
return Message{
|
||||
ID: fmt.Sprintf("%s/%v", s.Event.ID, s.TS),
|
||||
TS: s.TS,
|
||||
Source: fmt.Sprintf(`https://renderinc.slack.com/archives/%s/p%s`, s.Event.Channel, strings.ReplaceAll(s.Event.ID, ".", "")),
|
||||
Channel: s.Event.Channel,
|
||||
Thread: s.Event.ID,
|
||||
EventName: strings.Split(s.Event.Attachments[0].Title, ": Firing: ")[1],
|
||||
Event: strings.Split(s.Event.Attachments[0].Title, ":")[0],
|
||||
Plaintext: s.Event.Attachments[0].Text,
|
||||
Asset: "TODO",
|
||||
}, nil
|
||||
}
|
||||
|
||||
return Message{
|
||||
ID: fmt.Sprintf("%s/%v", s.Event.ParentID, s.TS),
|
||||
TS: s.TS,
|
||||
Source: fmt.Sprintf(`https://renderinc.slack.com/archives/%s/p%s`, s.Event.Channel, strings.ReplaceAll(s.Event.ParentID, ".", "")),
|
||||
Channel: s.Event.Channel,
|
||||
Thread: s.Event.ParentID,
|
||||
EventName: "TODO",
|
||||
Event: "TODO",
|
||||
Plaintext: s.Event.Text,
|
||||
Asset: "TODO",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func parseSlack(b []byte) (slackMessage, error) {
|
||||
var result slackMessage
|
||||
err := json.Unmarshal(b, &result)
|
||||
return result, err
|
||||
}
|
||||
|
||||
113
message_test.go
Normal file
113
message_test.go
Normal file
@@ -0,0 +1,113 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseSlackTestdata(t *testing.T) {
|
||||
cases := map[string]struct {
|
||||
slackMessage slackMessage
|
||||
message Message
|
||||
}{
|
||||
"human_thread_message_from_opsgenie_alert.json": {
|
||||
slackMessage: slackMessage{
|
||||
TS: 1712930706,
|
||||
Event: slackEvent{
|
||||
ID: "1712930706.598629",
|
||||
Channel: "C06U1DDBBU4",
|
||||
ParentID: "1712927439.728409",
|
||||
Text: "I gotta do this",
|
||||
Blocks: []slackBlock{{
|
||||
Elements: []slackElement{{
|
||||
Elements: []slackElement{{
|
||||
RichText: "I gotta do this",
|
||||
}},
|
||||
}},
|
||||
}},
|
||||
Bot: slackBot{
|
||||
Name: "",
|
||||
},
|
||||
Attachments: []slackAttachment{},
|
||||
},
|
||||
},
|
||||
message: Message{
|
||||
ID: "1712927439.728409/1712930706",
|
||||
TS: 1712930706,
|
||||
Source: "https://renderinc.slack.com/archives/C06U1DDBBU4/p1712927439728409",
|
||||
Channel: "C06U1DDBBU4",
|
||||
Thread: "1712927439.728409",
|
||||
EventName: "TODO",
|
||||
Event: "TODO",
|
||||
Plaintext: "I gotta do this",
|
||||
Asset: "TODO",
|
||||
},
|
||||
},
|
||||
"opsgenie_alert.json": {
|
||||
slackMessage: slackMessage{
|
||||
TS: 1712927439,
|
||||
Event: slackEvent{
|
||||
ID: "1712927439.728409",
|
||||
Channel: "C06U1DDBBU4",
|
||||
Bot: slackBot{
|
||||
Name: "Opsgenie for Alert Management",
|
||||
},
|
||||
Attachments: []slackAttachment{{
|
||||
Color: "F4511E",
|
||||
Title: "#11071: [Grafana]: Firing: Alertconfig Workflow Failed",
|
||||
Text: "At least one alertconfig run has failed unexpectedly.\nDashboard: <https://grafana.render.com/d/VLZU83YVk?orgId=1>\nPanel: <https://grafana.render.com/d/VLZU83YVk?orgId=1&viewPanel=17>\nSource: <https://grafana.render.com/alerting/grafana/fa7b06b8-b4d8-4979-bce7-5e1c432edd81/view?orgId=1>",
|
||||
Fields: []slackField{
|
||||
{Value: "P3", Title: "Priority"},
|
||||
{Value: "alertname:Alertconfig Workflow Failed, grafana_folder:Datastores, rule_uid:a7639f7e-6950-41be-850a-b22119f74cbb", Title: "Tags"},
|
||||
{Value: "Datastores Non-Critical", Title: "Routed Teams"},
|
||||
},
|
||||
Actions: []slackAction{{}, {}, {}},
|
||||
}},
|
||||
},
|
||||
},
|
||||
message: Message{
|
||||
ID: "1712927439.728409/1712927439",
|
||||
TS: 1712927439,
|
||||
Source: "https://renderinc.slack.com/archives/C06U1DDBBU4/p1712927439728409",
|
||||
Channel: "C06U1DDBBU4",
|
||||
Thread: "1712927439.728409",
|
||||
EventName: "Alertconfig Workflow Failed",
|
||||
Event: "#11071",
|
||||
Plaintext: "At least one alertconfig run has failed unexpectedly.\nDashboard: <https://grafana.render.com/d/VLZU83YVk?orgId=1>\nPanel: <https://grafana.render.com/d/VLZU83YVk?orgId=1&viewPanel=17>\nSource: <https://grafana.render.com/alerting/grafana/fa7b06b8-b4d8-4979-bce7-5e1c432edd81/view?orgId=1>",
|
||||
Asset: "TODO",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, d := range cases {
|
||||
want := d
|
||||
t.Run(name, func(t *testing.T) {
|
||||
b, err := os.ReadFile(path.Join("testdata", "slack_events", name))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Run("parseSlack", func(t *testing.T) {
|
||||
got, err := parseSlack(b)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if fmt.Sprintf("%+v", got) != fmt.Sprintf("%+v", want.slackMessage) {
|
||||
t.Errorf("wanted \n\t%+v, got\n\t%+v", want.slackMessage, got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ParseSlack", func(t *testing.T) {
|
||||
got, err := ParseSlack(b)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got != want.message {
|
||||
t.Errorf("wanted \n\t%+v, got\n\t%+v", want.message, got)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
4
queue.go
4
queue.go
@@ -11,10 +11,6 @@ type Queue struct {
|
||||
driver Driver
|
||||
}
|
||||
|
||||
func NewTestQueue() Queue {
|
||||
return Queue{driver: NewTestDB()}
|
||||
}
|
||||
|
||||
func NewQueue(driver Driver) Queue {
|
||||
return Queue{driver: driver}
|
||||
}
|
||||
|
||||
37
queue_test.go
Normal file
37
queue_test.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestQueue(t *testing.T) {
|
||||
ctx, can := context.WithTimeout(context.Background(), time.Second*10)
|
||||
defer can()
|
||||
|
||||
db := NewTestDBIn(t.TempDir())
|
||||
defer db.Close()
|
||||
|
||||
q := NewQueue(db)
|
||||
|
||||
for i := 0; i < 39; i++ {
|
||||
if err := q.Push(ctx, Message{ID: strconv.Itoa(i), TS: uint64(i)}); err != nil {
|
||||
t.Fatal(i, err)
|
||||
}
|
||||
}
|
||||
|
||||
found := map[uint64]struct{}{}
|
||||
for i := 0; i < 39; i++ {
|
||||
if m, err := q.PeekFirst(ctx); err != nil {
|
||||
t.Fatal(i, err)
|
||||
} else if _, ok := found[m.TS]; ok {
|
||||
t.Error(i, m.TS)
|
||||
} else if err := q.Ack(ctx, m.ID); err != nil {
|
||||
t.Fatal(i, err)
|
||||
} else {
|
||||
found[m.TS] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,10 +13,6 @@ type Storage struct {
|
||||
driver Driver
|
||||
}
|
||||
|
||||
func NewTestStorage() Storage {
|
||||
return Storage{driver: NewTestDB()}
|
||||
}
|
||||
|
||||
func NewStorage(driver Driver) Storage {
|
||||
return Storage{driver: driver}
|
||||
}
|
||||
|
||||
36
storage_test.go
Normal file
36
storage_test.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestStorage(t *testing.T) {
|
||||
ctx, can := context.WithTimeout(context.Background(), time.Second)
|
||||
defer can()
|
||||
|
||||
db := NewTestDBIn(t.TempDir())
|
||||
defer db.Close()
|
||||
|
||||
s := NewStorage(db)
|
||||
|
||||
if _, err := s.Get(ctx, "id"); err != ErrNotFound {
|
||||
t.Error("failed to get 404", err)
|
||||
}
|
||||
|
||||
m := Message{
|
||||
ID: "id",
|
||||
TS: 1,
|
||||
}
|
||||
|
||||
if err := s.Upsert(ctx, m); err != nil {
|
||||
t.Error("failed to upsert", err)
|
||||
}
|
||||
|
||||
if m2, err := s.Get(ctx, "id"); err != nil {
|
||||
t.Error("failed to get", err)
|
||||
} else if m != m2 {
|
||||
t.Error(m2)
|
||||
}
|
||||
}
|
||||
8
testdata/slack_events.json
vendored
8
testdata/slack_events.json
vendored
File diff suppressed because one or more lines are too long
50
testdata/slack_events/human_message.json
vendored
Normal file
50
testdata/slack_events/human_message.json
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"hint": "a message",
|
||||
"token": "redacted",
|
||||
"team_id": "T9RQLQ0KV",
|
||||
"context_team_id": "T9RQLQ0KV",
|
||||
"context_enterprise_id": null,
|
||||
"api_app_id": "A06TYH7CALB",
|
||||
"event": {
|
||||
"user": "U06868T6ADV",
|
||||
"type": "message",
|
||||
"ts": "1712878479.415559",
|
||||
"client_msg_id": "fce57841-0446-4720-aa1d-557e162c7667",
|
||||
"text": "BOTH OF YOU GET IN HERE HEEEHEEEHEEE",
|
||||
"team": "T9RQLQ0KV",
|
||||
"blocks": [
|
||||
{
|
||||
"type": "rich_text",
|
||||
"block_id": "6G5BZ",
|
||||
"elements": [
|
||||
{
|
||||
"type": "rich_text_section",
|
||||
"elements": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "BOTH OF YOU GET IN HERE HEEEHEEEHEEE"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"channel": "C06U1DDBBU4",
|
||||
"event_ts": "1712878479.415559",
|
||||
"channel_type": "channel"
|
||||
},
|
||||
"type": "event_callback",
|
||||
"event_id": "Ev06U1F01XMJ",
|
||||
"event_time": 1712878479,
|
||||
"authorizations": [
|
||||
{
|
||||
"enterprise_id": null,
|
||||
"team_id": "T9RQLQ0KV",
|
||||
"user_id": "U06TS9M7ABG",
|
||||
"is_bot": true,
|
||||
"is_enterprise_install": false
|
||||
}
|
||||
],
|
||||
"is_ext_shared_channel": false,
|
||||
"event_context": "4-eyJldCI6Im1lc3NhZ2UiLCJ0aWQiOiJUOVJRTFEwS1YiLCJhaWQiOiJBMDZUWUg3Q0FMQiIsImNpZCI6IkMwNlUxRERCQlU0In0"
|
||||
}
|
||||
52
testdata/slack_events/human_thread_message.json
vendored
Normal file
52
testdata/slack_events/human_thread_message.json
vendored
Normal file
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"hint": "a thread message",
|
||||
"token": "redacted",
|
||||
"team_id": "T9RQLQ0KV",
|
||||
"context_team_id": "T9RQLQ0KV",
|
||||
"context_enterprise_id": null,
|
||||
"api_app_id": "A06TYH7CALB",
|
||||
"event": {
|
||||
"user": "U06868T6ADV",
|
||||
"type": "message",
|
||||
"ts": "1712878566.650149",
|
||||
"client_msg_id": "5d9d586b-eee0-40f8-b15e-66245c073a4c",
|
||||
"text": "in a thread",
|
||||
"team": "T9RQLQ0KV",
|
||||
"thread_ts": "1712877772.926539",
|
||||
"parent_user_id": "U06868T6ADV",
|
||||
"blocks": [
|
||||
{
|
||||
"type": "rich_text",
|
||||
"block_id": "KHSCu",
|
||||
"elements": [
|
||||
{
|
||||
"type": "rich_text_section",
|
||||
"elements": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "in a thread"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"channel": "C06U1DDBBU4",
|
||||
"event_ts": "1712878566.650149",
|
||||
"channel_type": "channel"
|
||||
},
|
||||
"type": "event_callback",
|
||||
"event_id": "Ev06TW3WE58V",
|
||||
"event_time": 1712878566,
|
||||
"authorizations": [
|
||||
{
|
||||
"enterprise_id": null,
|
||||
"team_id": "T9RQLQ0KV",
|
||||
"user_id": "U06TS9M7ABG",
|
||||
"is_bot": true,
|
||||
"is_enterprise_install": false
|
||||
}
|
||||
],
|
||||
"is_ext_shared_channel": false,
|
||||
"event_context": "4-eyJldCI6Im1lc3NhZ2UiLCJ0aWQiOiJUOVJRTFEwS1YiLCJhaWQiOiJBMDZUWUg3Q0FMQiIsImNpZCI6IkMwNlUxRERCQlU0In0"
|
||||
}
|
||||
52
testdata/slack_events/human_thread_message_from_opsgenie_alert.json
vendored
Normal file
52
testdata/slack_events/human_thread_message_from_opsgenie_alert.json
vendored
Normal file
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"hint": "a thread reply to an alert",
|
||||
"token": "redacted",
|
||||
"team_id": "T9RQLQ0KV",
|
||||
"context_team_id": "T9RQLQ0KV",
|
||||
"context_enterprise_id": null,
|
||||
"api_app_id": "A06TYH7CALB",
|
||||
"event": {
|
||||
"user": "U06868T6ADV",
|
||||
"type": "message",
|
||||
"ts": "1712930706.598629",
|
||||
"client_msg_id": "880fc69f-de95-4c1e-90a7-aae701a40c21",
|
||||
"text": "I gotta do this",
|
||||
"team": "T9RQLQ0KV",
|
||||
"thread_ts": "1712927439.728409",
|
||||
"parent_user_id": "U03RUK7FBUY",
|
||||
"blocks": [
|
||||
{
|
||||
"type": "rich_text",
|
||||
"block_id": "Cp0IU",
|
||||
"elements": [
|
||||
{
|
||||
"type": "rich_text_section",
|
||||
"elements": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "I gotta do this"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"channel": "C06U1DDBBU4",
|
||||
"event_ts": "1712930706.598629",
|
||||
"channel_type": "channel"
|
||||
},
|
||||
"type": "event_callback",
|
||||
"event_id": "Ev06UF3U1LM7",
|
||||
"event_time": 1712930706,
|
||||
"authorizations": [
|
||||
{
|
||||
"enterprise_id": null,
|
||||
"team_id": "T9RQLQ0KV",
|
||||
"user_id": "U06TS9M7ABG",
|
||||
"is_bot": true,
|
||||
"is_enterprise_install": false
|
||||
}
|
||||
],
|
||||
"is_ext_shared_channel": false,
|
||||
"event_context": "4-eyJldCI6Im1lc3NhZ2UiLCJ0aWQiOiJUOVJRTFEwS1YiLCJhaWQiOiJBMDZUWUg3Q0FMQiIsImNpZCI6IkMwNlUxRERCQlU0In0"
|
||||
}
|
||||
127
testdata/slack_events/opsgenie_alert.json
vendored
Normal file
127
testdata/slack_events/opsgenie_alert.json
vendored
Normal file
@@ -0,0 +1,127 @@
|
||||
{
|
||||
"hint": "a new alert from opsgenie",
|
||||
"token": "redacted",
|
||||
"team_id": "T9RQLQ0KV",
|
||||
"context_team_id": "T9RQLQ0KV",
|
||||
"context_enterprise_id": null,
|
||||
"api_app_id": "A06TYH7CALB",
|
||||
"event": {
|
||||
"user": "U03RUK7FBUY",
|
||||
"type": "message",
|
||||
"ts": "1712927439.728409",
|
||||
"bot_id": "B03RHGBPH2M",
|
||||
"app_id": "A286WATV2",
|
||||
"text": "",
|
||||
"team": "T9RQLQ0KV",
|
||||
"bot_profile": {
|
||||
"id": "B03RHGBPH2M",
|
||||
"deleted": false,
|
||||
"name": "Opsgenie for Alert Management",
|
||||
"updated": 1658887059,
|
||||
"app_id": "A286WATV2",
|
||||
"icons": {
|
||||
"image_36": "https://avatars.slack-edge.com/2019-05-30/652285939191_7831939cc30ef7159561_36.png",
|
||||
"image_48": "https://avatars.slack-edge.com/2019-05-30/652285939191_7831939cc30ef7159561_48.png",
|
||||
"image_72": "https://avatars.slack-edge.com/2019-05-30/652285939191_7831939cc30ef7159561_72.png"
|
||||
},
|
||||
"team_id": "T9RQLQ0KV"
|
||||
},
|
||||
"attachments": [
|
||||
{
|
||||
"id": 1,
|
||||
"color": "F4511E",
|
||||
"fallback": "New alert: \"[Grafana]: Firing: Alertconfig Workflow Failed\" <https://opsg.in/a/i/render/38152bc5-bc5d-411d-9feb-d285af5b6481-1712927439305|11071>\nTags: alertname:Alertconfig Workflow Failed, grafana_folder:Datastores, rule_uid:a7639f7e-6950-41be-850a-b22119f74cbb",
|
||||
"text": "At least one alertconfig run has failed unexpectedly.\nDashboard: <https://grafana.render.com/d/VLZU83YVk?orgId=1>\nPanel: <https://grafana.render.com/d/VLZU83YVk?orgId=1&viewPanel=17>\nSource: <https://grafana.render.com/alerting/grafana/fa7b06b8-b4d8-4979-bce7-5e1c432edd81/view?orgId=1>",
|
||||
"title": "#11071: [Grafana]: Firing: Alertconfig Workflow Failed",
|
||||
"title_link": "https://opsg.in/a/i/render/38152bc5-bc5d-411d-9feb-d285af5b6481-1712927439305",
|
||||
"author_name": "New Alert created via Grafana - Datastore Slack",
|
||||
"callback_id": "bbd4a269-08a9-470e-ba79-ce238ac03dc7_05fa2e9b-bec4-4a7e-842d-36043d267a13_11071",
|
||||
"fields": [
|
||||
{
|
||||
"value": "P3",
|
||||
"title": "Priority",
|
||||
"short": true
|
||||
},
|
||||
{
|
||||
"value": "alertname:Alertconfig Workflow Failed, grafana_folder:Datastores, rule_uid:a7639f7e-6950-41be-850a-b22119f74cbb",
|
||||
"title": "Tags",
|
||||
"short": true
|
||||
},
|
||||
{
|
||||
"value": "Datastores Non-Critical",
|
||||
"title": "Routed Teams",
|
||||
"short": true
|
||||
}
|
||||
],
|
||||
"mrkdwn_in": [
|
||||
"pretext",
|
||||
"text"
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"id": "1",
|
||||
"name": "acknowledge",
|
||||
"text": "Acknowledge",
|
||||
"type": "button",
|
||||
"value": "ack",
|
||||
"style": "default"
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"name": "close",
|
||||
"text": "Close",
|
||||
"type": "button",
|
||||
"value": "close",
|
||||
"style": "primary"
|
||||
},
|
||||
{
|
||||
"id": "3",
|
||||
"name": "action",
|
||||
"text": "Other actions...",
|
||||
"type": "select",
|
||||
"data_source": "static",
|
||||
"options": [
|
||||
{
|
||||
"text": "Assign",
|
||||
"value": "assign"
|
||||
},
|
||||
{
|
||||
"text": "Take Ownership",
|
||||
"value": "own"
|
||||
},
|
||||
{
|
||||
"text": "Snooze",
|
||||
"value": "snooze"
|
||||
},
|
||||
{
|
||||
"text": "Add Note",
|
||||
"value": "addNote"
|
||||
},
|
||||
{
|
||||
"text": "Update Priority",
|
||||
"value": "updatePriority"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"channel": "C06U1DDBBU4",
|
||||
"event_ts": "1712927439.728409",
|
||||
"channel_type": "channel"
|
||||
},
|
||||
"type": "event_callback",
|
||||
"event_id": "Ev06UEPF5BSM",
|
||||
"event_time": 1712927439,
|
||||
"authorizations": [
|
||||
{
|
||||
"enterprise_id": null,
|
||||
"team_id": "T9RQLQ0KV",
|
||||
"user_id": "U06TS9M7ABG",
|
||||
"is_bot": true,
|
||||
"is_enterprise_install": false
|
||||
}
|
||||
],
|
||||
"is_ext_shared_channel": false,
|
||||
"event_context": "4-eyJldCI6Im1lc3NhZ2UiLCJ0aWQiOiJUOVJRTFEwS1YiLCJhaWQiOiJBMDZUWUg3Q0FMQiIsImNpZCI6IkMwNlUxRERCQlU0In0"
|
||||
}
|
||||
192
testdata/slack_events/opsgenie_alert_2.json
vendored
Normal file
192
testdata/slack_events/opsgenie_alert_2.json
vendored
Normal file
@@ -0,0 +1,192 @@
|
||||
{
|
||||
"hint": "a new alert from opsgenie",
|
||||
"token": "redacted",
|
||||
"team_id": "T9RQLQ0KV",
|
||||
"context_team_id": "T9RQLQ0KV",
|
||||
"context_enterprise_id": null,
|
||||
"api_app_id": "A06TYH7CALB",
|
||||
"event": {
|
||||
"type": "message",
|
||||
"subtype": "message_changed",
|
||||
"message": {
|
||||
"user": "U03RUK7FBUY",
|
||||
"type": "message",
|
||||
"edited": {
|
||||
"user": "B03RHGBPH2M",
|
||||
"ts": "1712925631.000000"
|
||||
},
|
||||
"bot_id": "B03RHGBPH2M",
|
||||
"app_id": "A286WATV2",
|
||||
"text": "",
|
||||
"team": "T9RQLQ0KV",
|
||||
"bot_profile": {
|
||||
"id": "B03RHGBPH2M",
|
||||
"app_id": "A286WATV2",
|
||||
"name": "Opsgenie for Alert Management",
|
||||
"icons": {
|
||||
"image_36": "https://avatars.slack-edge.com/2019-05-30/652285939191_7831939cc30ef7159561_36.png",
|
||||
"image_48": "https://avatars.slack-edge.com/2019-05-30/652285939191_7831939cc30ef7159561_48.png",
|
||||
"image_72": "https://avatars.slack-edge.com/2019-05-30/652285939191_7831939cc30ef7159561_72.png"
|
||||
},
|
||||
"deleted": false,
|
||||
"updated": 1658887059,
|
||||
"team_id": "T9RQLQ0KV"
|
||||
},
|
||||
"attachments": [
|
||||
{
|
||||
"id": 1,
|
||||
"color": "2ecc71",
|
||||
"fallback": "\"[Grafana]: Firing: Alertconfig Workflow Failed\" <https://opsg.in/a/i/render/ec6e7c4b-90a6-4c3c-94e9-901f6b7ada47-1712922031403|11070>\nTags: alertname:Alertconfig Workflow Failed, grafana_folder:Datastores, rule_uid:a7639f7e-6950-41be-850a-b22119f74cbb",
|
||||
"text": "At least one alertconfig run has failed unexpectedly.\nDashboard: <https://grafana.render.com/d/VLZU83YVk?orgId=1>\nPanel: <https://grafana.render.com/d/VLZU83YVk?orgId=1&viewPanel=17>\nSource: <https://grafana.render.com/alerting/grafana/fa7b06b8-b4d8-4979-bce7-5e1c432edd81/view?orgId=1>",
|
||||
"title": "#11070: [Grafana]: Firing: Alertconfig Workflow Failed",
|
||||
"title_link": "https://opsg.in/a/i/render/ec6e7c4b-90a6-4c3c-94e9-901f6b7ada47-1712922031403",
|
||||
"callback_id": "bbd4a269-08a9-470e-ba79-ce238ac03dc7_05fa2e9b-bec4-4a7e-842d-36043d267a13_11070",
|
||||
"fields": [
|
||||
{
|
||||
"value": "P3",
|
||||
"title": "Priority",
|
||||
"short": true
|
||||
},
|
||||
{
|
||||
"value": "alertname:Alertconfig Workflow Failed, grafana_folder:Datastores, rule_uid:a7639f7e-6950-41be-850a-b22119f74cbb",
|
||||
"title": "Tags",
|
||||
"short": true
|
||||
},
|
||||
{
|
||||
"value": "Datastores Non-Critical",
|
||||
"title": "Routed Teams",
|
||||
"short": true
|
||||
}
|
||||
],
|
||||
"mrkdwn_in": [
|
||||
"text"
|
||||
]
|
||||
}
|
||||
],
|
||||
"ts": "1712922031.821339",
|
||||
"source_team": "T9RQLQ0KV",
|
||||
"user_team": "T9RQLQ0KV"
|
||||
},
|
||||
"previous_message": {
|
||||
"user": "U03RUK7FBUY",
|
||||
"type": "message",
|
||||
"ts": "1712922031.821339",
|
||||
"bot_id": "B03RHGBPH2M",
|
||||
"app_id": "A286WATV2",
|
||||
"text": "",
|
||||
"team": "T9RQLQ0KV",
|
||||
"bot_profile": {
|
||||
"id": "B03RHGBPH2M",
|
||||
"app_id": "A286WATV2",
|
||||
"name": "Opsgenie for Alert Management",
|
||||
"icons": {
|
||||
"image_36": "https://avatars.slack-edge.com/2019-05-30/652285939191_7831939cc30ef7159561_36.png",
|
||||
"image_48": "https://avatars.slack-edge.com/2019-05-30/652285939191_7831939cc30ef7159561_48.png",
|
||||
"image_72": "https://avatars.slack-edge.com/2019-05-30/652285939191_7831939cc30ef7159561_72.png"
|
||||
},
|
||||
"deleted": false,
|
||||
"updated": 1658887059,
|
||||
"team_id": "T9RQLQ0KV"
|
||||
},
|
||||
"attachments": [
|
||||
{
|
||||
"id": 1,
|
||||
"color": "F4511E",
|
||||
"fallback": "New alert: \"[Grafana]: Firing: Alertconfig Workflow Failed\" <https://opsg.in/a/i/render/ec6e7c4b-90a6-4c3c-94e9-901f6b7ada47-1712922031403|11070>\nTags: alertname:Alertconfig Workflow Failed, grafana_folder:Datastores, rule_uid:a7639f7e-6950-41be-850a-b22119f74cbb",
|
||||
"text": "At least one alertconfig run has failed unexpectedly.\nDashboard: <https://grafana.render.com/d/VLZU83YVk?orgId=1>\nPanel: <https://grafana.render.com/d/VLZU83YVk?orgId=1&viewPanel=17>\nSource: <https://grafana.render.com/alerting/grafana/fa7b06b8-b4d8-4979-bce7-5e1c432edd81/view?orgId=1>",
|
||||
"title": "#11070: [Grafana]: Firing: Alertconfig Workflow Failed",
|
||||
"title_link": "https://opsg.in/a/i/render/ec6e7c4b-90a6-4c3c-94e9-901f6b7ada47-1712922031403",
|
||||
"author_name": "New Alert created via Grafana - Datastore Slack",
|
||||
"callback_id": "bbd4a269-08a9-470e-ba79-ce238ac03dc7_05fa2e9b-bec4-4a7e-842d-36043d267a13_11070",
|
||||
"fields": [
|
||||
{
|
||||
"value": "P3",
|
||||
"title": "Priority",
|
||||
"short": true
|
||||
},
|
||||
{
|
||||
"value": "alertname:Alertconfig Workflow Failed, grafana_folder:Datastores, rule_uid:a7639f7e-6950-41be-850a-b22119f74cbb",
|
||||
"title": "Tags",
|
||||
"short": true
|
||||
},
|
||||
{
|
||||
"value": "Datastores Non-Critical",
|
||||
"title": "Routed Teams",
|
||||
"short": true
|
||||
}
|
||||
],
|
||||
"mrkdwn_in": [
|
||||
"pretext",
|
||||
"text"
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"id": "1",
|
||||
"name": "acknowledge",
|
||||
"text": "Acknowledge",
|
||||
"type": "button",
|
||||
"value": "ack",
|
||||
"style": "default"
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"name": "close",
|
||||
"text": "Close",
|
||||
"type": "button",
|
||||
"value": "close",
|
||||
"style": "primary"
|
||||
},
|
||||
{
|
||||
"id": "3",
|
||||
"name": "action",
|
||||
"text": "Other actions...",
|
||||
"type": "select",
|
||||
"data_source": "static",
|
||||
"options": [
|
||||
{
|
||||
"text": "Assign",
|
||||
"value": "assign"
|
||||
},
|
||||
{
|
||||
"text": "Take Ownership",
|
||||
"value": "own"
|
||||
},
|
||||
{
|
||||
"text": "Snooze",
|
||||
"value": "snooze"
|
||||
},
|
||||
{
|
||||
"text": "Add Note",
|
||||
"value": "addNote"
|
||||
},
|
||||
{
|
||||
"text": "Update Priority",
|
||||
"value": "updatePriority"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"channel": "C06U1DDBBU4",
|
||||
"hidden": true,
|
||||
"ts": "1712925631.000400",
|
||||
"event_ts": "1712925631.000400",
|
||||
"channel_type": "channel"
|
||||
},
|
||||
"type": "event_callback",
|
||||
"event_id": "Ev06UQNCJKNC",
|
||||
"event_time": 1712925631,
|
||||
"authorizations": [
|
||||
{
|
||||
"enterprise_id": null,
|
||||
"team_id": "T9RQLQ0KV",
|
||||
"user_id": "U06TS9M7ABG",
|
||||
"is_bot": true,
|
||||
"is_enterprise_install": false
|
||||
}
|
||||
],
|
||||
"is_ext_shared_channel": false,
|
||||
"event_context": "4-eyJldCI6Im1lc3NhZ2UiLCJ0aWQiOiJUOVJRTFEwS1YiLCJhaWQiOiJBMDZUWUg3Q0FMQiIsImNpZCI6IkMwNlUxRERCQlU0In0"
|
||||
}
|
||||
127
testdata/slack_events/opsgenie_alert_3.json
vendored
Normal file
127
testdata/slack_events/opsgenie_alert_3.json
vendored
Normal file
@@ -0,0 +1,127 @@
|
||||
{
|
||||
"hint": "an alert firing that has a render_id",
|
||||
"token": "redacted",
|
||||
"team_id": "T9RQLQ0KV",
|
||||
"context_team_id": "T9RQLQ0KV",
|
||||
"context_enterprise_id": null,
|
||||
"api_app_id": "A06TYH7CALB",
|
||||
"event": {
|
||||
"user": "U03RUK7FBUY",
|
||||
"type": "message",
|
||||
"ts": "1712911957.023359",
|
||||
"bot_id": "B03RHGBPH2M",
|
||||
"app_id": "A286WATV2",
|
||||
"text": "",
|
||||
"team": "T9RQLQ0KV",
|
||||
"bot_profile": {
|
||||
"id": "B03RHGBPH2M",
|
||||
"deleted": false,
|
||||
"name": "Opsgenie for Alert Management",
|
||||
"updated": 1658887059,
|
||||
"app_id": "A286WATV2",
|
||||
"icons": {
|
||||
"image_36": "https://avatars.slack-edge.com/2019-05-30/652285939191_7831939cc30ef7159561_36.png",
|
||||
"image_48": "https://avatars.slack-edge.com/2019-05-30/652285939191_7831939cc30ef7159561_48.png",
|
||||
"image_72": "https://avatars.slack-edge.com/2019-05-30/652285939191_7831939cc30ef7159561_72.png"
|
||||
},
|
||||
"team_id": "T9RQLQ0KV"
|
||||
},
|
||||
"attachments": [
|
||||
{
|
||||
"id": 1,
|
||||
"color": "F4511E",
|
||||
"fallback": "New alert: \"[Grafana]: Firing: [Oregon-1] Wal Receive Count Alert\" <https://opsg.in/a/i/render/c1565736-aac4-4a78-b7db-65e4d90a59f2-1712911956739|11067>\nTags: alertname:[oregon-1] WAL Receive Count alert, business_hours:true, cluster:oregon-1, contacts:\"OpsGenie - Datastore\",\"Slack - Datastore, grafana_folder:[Generated] oregon-1, template-source:ab1da08c-129f-4f08-b505-db44929eb8",
|
||||
"text": "Replica for dpg-cn7te6nsc6pc73ak5pig-b cannot start streaming replication, because the primary has already deleted the needed WAL segment.\n\nSync the replica from scratch using the runbook.\nRunbook: <https://slab.render.com/posts/runbook-postgres-replica-too-far-behind-pr084dj4>\nSource: <https://grafana.render.com/alerting/grafana/aecdfbcf-5298-4679-bd20-d9dec32ee2a0/view?orgId=1>",
|
||||
"title": "#11067: [Grafana]: Firing: [Oregon-1] Wal Receive Count Alert",
|
||||
"title_link": "https://opsg.in/a/i/render/c1565736-aac4-4a78-b7db-65e4d90a59f2-1712911956739",
|
||||
"author_name": "New Alert created via Grafana - Datastore Slack",
|
||||
"callback_id": "bbd4a269-08a9-470e-ba79-ce238ac03dc7_05fa2e9b-bec4-4a7e-842d-36043d267a13_11067",
|
||||
"fields": [
|
||||
{
|
||||
"value": "P3",
|
||||
"title": "Priority",
|
||||
"short": true
|
||||
},
|
||||
{
|
||||
"value": "alertname:[oregon-1] WAL Receive Count alert, business_hours:true, cluster:oregon-1, contacts:\"OpsGenie - Datastore\",\"Slack - Datastore, grafana_folder:[Generated] oregon-1, template-source:ab1da08c-129f-4f08-b505-db44929eb8",
|
||||
"title": "Tags",
|
||||
"short": true
|
||||
},
|
||||
{
|
||||
"value": "Datastores Non-Critical",
|
||||
"title": "Routed Teams",
|
||||
"short": true
|
||||
}
|
||||
],
|
||||
"mrkdwn_in": [
|
||||
"pretext",
|
||||
"text"
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"id": "1",
|
||||
"name": "acknowledge",
|
||||
"text": "Acknowledge",
|
||||
"type": "button",
|
||||
"value": "ack",
|
||||
"style": "default"
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"name": "close",
|
||||
"text": "Close",
|
||||
"type": "button",
|
||||
"value": "close",
|
||||
"style": "primary"
|
||||
},
|
||||
{
|
||||
"id": "3",
|
||||
"name": "action",
|
||||
"text": "Other actions...",
|
||||
"type": "select",
|
||||
"data_source": "static",
|
||||
"options": [
|
||||
{
|
||||
"text": "Assign",
|
||||
"value": "assign"
|
||||
},
|
||||
{
|
||||
"text": "Take Ownership",
|
||||
"value": "own"
|
||||
},
|
||||
{
|
||||
"text": "Snooze",
|
||||
"value": "snooze"
|
||||
},
|
||||
{
|
||||
"text": "Add Note",
|
||||
"value": "addNote"
|
||||
},
|
||||
{
|
||||
"text": "Update Priority",
|
||||
"value": "updatePriority"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"channel": "C06U1DDBBU4",
|
||||
"event_ts": "1712911957.023359",
|
||||
"channel_type": "channel"
|
||||
},
|
||||
"type": "event_callback",
|
||||
"event_id": "Ev06U0TNBXV0",
|
||||
"event_time": 1712911957,
|
||||
"authorizations": [
|
||||
{
|
||||
"enterprise_id": null,
|
||||
"team_id": "T9RQLQ0KV",
|
||||
"user_id": "U06TS9M7ABG",
|
||||
"is_bot": true,
|
||||
"is_enterprise_install": false
|
||||
}
|
||||
],
|
||||
"is_ext_shared_channel": false,
|
||||
"event_context": "4-eyJldCI6Im1lc3NhZ2UiLCJ0aWQiOiJUOVJRTFEwS1YiLCJhaWQiOiJBMDZUWUg3Q0FMQiIsImNpZCI6IkMwNlUxRERCQlU0In0"
|
||||
}
|
||||
192
testdata/slack_events/opsgenie_alert_resolved.json
vendored
Normal file
192
testdata/slack_events/opsgenie_alert_resolved.json
vendored
Normal file
@@ -0,0 +1,192 @@
|
||||
{
|
||||
"hint": "an edit to resolve an alert from opsgenie",
|
||||
"token": "redacted",
|
||||
"team_id": "T9RQLQ0KV",
|
||||
"context_team_id": "T9RQLQ0KV",
|
||||
"context_enterprise_id": null,
|
||||
"api_app_id": "A06TYH7CALB",
|
||||
"event": {
|
||||
"type": "message",
|
||||
"subtype": "message_changed",
|
||||
"message": {
|
||||
"user": "U03RUK7FBUY",
|
||||
"type": "message",
|
||||
"edited": {
|
||||
"user": "B03RHGBPH2M",
|
||||
"ts": "1712916339.000000"
|
||||
},
|
||||
"bot_id": "B03RHGBPH2M",
|
||||
"app_id": "A286WATV2",
|
||||
"text": "",
|
||||
"team": "T9RQLQ0KV",
|
||||
"bot_profile": {
|
||||
"id": "B03RHGBPH2M",
|
||||
"app_id": "A286WATV2",
|
||||
"name": "Opsgenie for Alert Management",
|
||||
"icons": {
|
||||
"image_36": "https://avatars.slack-edge.com/2019-05-30/652285939191_7831939cc30ef7159561_36.png",
|
||||
"image_48": "https://avatars.slack-edge.com/2019-05-30/652285939191_7831939cc30ef7159561_48.png",
|
||||
"image_72": "https://avatars.slack-edge.com/2019-05-30/652285939191_7831939cc30ef7159561_72.png"
|
||||
},
|
||||
"deleted": false,
|
||||
"updated": 1658887059,
|
||||
"team_id": "T9RQLQ0KV"
|
||||
},
|
||||
"attachments": [
|
||||
{
|
||||
"id": 1,
|
||||
"color": "2ecc71",
|
||||
"fallback": "\"[Grafana]: Firing: Alertconfig Workflow Failed\" <https://opsg.in/a/i/render/ba6f8300-b869-4aba-9d02-c142237ae59e-1712912739431|11069>\nTags: alertname:Alertconfig Workflow Failed, grafana_folder:Datastores, rule_uid:a7639f7e-6950-41be-850a-b22119f74cbb",
|
||||
"text": "At least one alertconfig run has failed unexpectedly.\nDashboard: <https://grafana.render.com/d/VLZU83YVk?orgId=1>\nPanel: <https://grafana.render.com/d/VLZU83YVk?orgId=1&viewPanel=17>\nSource: <https://grafana.render.com/alerting/grafana/fa7b06b8-b4d8-4979-bce7-5e1c432edd81/view?orgId=1>",
|
||||
"title": "#11069: [Grafana]: Firing: Alertconfig Workflow Failed",
|
||||
"title_link": "https://opsg.in/a/i/render/ba6f8300-b869-4aba-9d02-c142237ae59e-1712912739431",
|
||||
"callback_id": "bbd4a269-08a9-470e-ba79-ce238ac03dc7_05fa2e9b-bec4-4a7e-842d-36043d267a13_11069",
|
||||
"fields": [
|
||||
{
|
||||
"value": "P3",
|
||||
"title": "Priority",
|
||||
"short": true
|
||||
},
|
||||
{
|
||||
"value": "alertname:Alertconfig Workflow Failed, grafana_folder:Datastores, rule_uid:a7639f7e-6950-41be-850a-b22119f74cbb",
|
||||
"title": "Tags",
|
||||
"short": true
|
||||
},
|
||||
{
|
||||
"value": "Datastores Non-Critical",
|
||||
"title": "Routed Teams",
|
||||
"short": true
|
||||
}
|
||||
],
|
||||
"mrkdwn_in": [
|
||||
"text"
|
||||
]
|
||||
}
|
||||
],
|
||||
"ts": "1712912739.723049",
|
||||
"source_team": "T9RQLQ0KV",
|
||||
"user_team": "T9RQLQ0KV"
|
||||
},
|
||||
"previous_message": {
|
||||
"user": "U03RUK7FBUY",
|
||||
"type": "message",
|
||||
"ts": "1712912739.723049",
|
||||
"bot_id": "B03RHGBPH2M",
|
||||
"app_id": "A286WATV2",
|
||||
"text": "",
|
||||
"team": "T9RQLQ0KV",
|
||||
"bot_profile": {
|
||||
"id": "B03RHGBPH2M",
|
||||
"app_id": "A286WATV2",
|
||||
"name": "Opsgenie for Alert Management",
|
||||
"icons": {
|
||||
"image_36": "https://avatars.slack-edge.com/2019-05-30/652285939191_7831939cc30ef7159561_36.png",
|
||||
"image_48": "https://avatars.slack-edge.com/2019-05-30/652285939191_7831939cc30ef7159561_48.png",
|
||||
"image_72": "https://avatars.slack-edge.com/2019-05-30/652285939191_7831939cc30ef7159561_72.png"
|
||||
},
|
||||
"deleted": false,
|
||||
"updated": 1658887059,
|
||||
"team_id": "T9RQLQ0KV"
|
||||
},
|
||||
"attachments": [
|
||||
{
|
||||
"id": 1,
|
||||
"color": "F4511E",
|
||||
"fallback": "New alert: \"[Grafana]: Firing: Alertconfig Workflow Failed\" <https://opsg.in/a/i/render/ba6f8300-b869-4aba-9d02-c142237ae59e-1712912739431|11069>\nTags: alertname:Alertconfig Workflow Failed, grafana_folder:Datastores, rule_uid:a7639f7e-6950-41be-850a-b22119f74cbb",
|
||||
"text": "At least one alertconfig run has failed unexpectedly.\nDashboard: <https://grafana.render.com/d/VLZU83YVk?orgId=1>\nPanel: <https://grafana.render.com/d/VLZU83YVk?orgId=1&viewPanel=17>\nSource: <https://grafana.render.com/alerting/grafana/fa7b06b8-b4d8-4979-bce7-5e1c432edd81/view?orgId=1>",
|
||||
"title": "#11069: [Grafana]: Firing: Alertconfig Workflow Failed",
|
||||
"title_link": "https://opsg.in/a/i/render/ba6f8300-b869-4aba-9d02-c142237ae59e-1712912739431",
|
||||
"author_name": "New Alert created via Grafana - Datastore Slack",
|
||||
"callback_id": "bbd4a269-08a9-470e-ba79-ce238ac03dc7_05fa2e9b-bec4-4a7e-842d-36043d267a13_11069",
|
||||
"fields": [
|
||||
{
|
||||
"value": "P3",
|
||||
"title": "Priority",
|
||||
"short": true
|
||||
},
|
||||
{
|
||||
"value": "alertname:Alertconfig Workflow Failed, grafana_folder:Datastores, rule_uid:a7639f7e-6950-41be-850a-b22119f74cbb",
|
||||
"title": "Tags",
|
||||
"short": true
|
||||
},
|
||||
{
|
||||
"value": "Datastores Non-Critical",
|
||||
"title": "Routed Teams",
|
||||
"short": true
|
||||
}
|
||||
],
|
||||
"mrkdwn_in": [
|
||||
"pretext",
|
||||
"text"
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"id": "1",
|
||||
"name": "acknowledge",
|
||||
"text": "Acknowledge",
|
||||
"type": "button",
|
||||
"value": "ack",
|
||||
"style": "default"
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"name": "close",
|
||||
"text": "Close",
|
||||
"type": "button",
|
||||
"value": "close",
|
||||
"style": "primary"
|
||||
},
|
||||
{
|
||||
"id": "3",
|
||||
"name": "action",
|
||||
"text": "Other actions...",
|
||||
"type": "select",
|
||||
"data_source": "static",
|
||||
"options": [
|
||||
{
|
||||
"text": "Assign",
|
||||
"value": "assign"
|
||||
},
|
||||
{
|
||||
"text": "Take Ownership",
|
||||
"value": "own"
|
||||
},
|
||||
{
|
||||
"text": "Snooze",
|
||||
"value": "snooze"
|
||||
},
|
||||
{
|
||||
"text": "Add Note",
|
||||
"value": "addNote"
|
||||
},
|
||||
{
|
||||
"text": "Update Priority",
|
||||
"value": "updatePriority"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"channel": "C06U1DDBBU4",
|
||||
"hidden": true,
|
||||
"ts": "1712916339.000300",
|
||||
"event_ts": "1712916339.000300",
|
||||
"channel_type": "channel"
|
||||
},
|
||||
"type": "event_callback",
|
||||
"event_id": "Ev06TLMY8FJB",
|
||||
"event_time": 1712916339,
|
||||
"authorizations": [
|
||||
{
|
||||
"enterprise_id": null,
|
||||
"team_id": "T9RQLQ0KV",
|
||||
"user_id": "U06TS9M7ABG",
|
||||
"is_bot": true,
|
||||
"is_enterprise_install": false
|
||||
}
|
||||
],
|
||||
"is_ext_shared_channel": false,
|
||||
"event_context": "4-eyJldCI6Im1lc3NhZ2UiLCJ0aWQiOiJUOVJRTFEwS1YiLCJhaWQiOiJBMDZUWUg3Q0FMQiIsImNpZCI6IkMwNlUxRERCQlU0In0"
|
||||
}
|
||||
55
testdata/slack_events/opsgenie_alert_resolved_fyi.json
vendored
Normal file
55
testdata/slack_events/opsgenie_alert_resolved_fyi.json
vendored
Normal file
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"hint": "a teeny closed alert message",
|
||||
"token": "redacted",
|
||||
"team_id": "T9RQLQ0KV",
|
||||
"context_team_id": "T9RQLQ0KV",
|
||||
"context_enterprise_id": null,
|
||||
"api_app_id": "A06TYH7CALB",
|
||||
"event": {
|
||||
"user": "U03RUK7FBUY",
|
||||
"type": "message",
|
||||
"ts": "1712925631.682559",
|
||||
"bot_id": "B03RHGBPH2M",
|
||||
"app_id": "A286WATV2",
|
||||
"text": "",
|
||||
"team": "T9RQLQ0KV",
|
||||
"bot_profile": {
|
||||
"id": "B03RHGBPH2M",
|
||||
"deleted": false,
|
||||
"name": "Opsgenie for Alert Management",
|
||||
"updated": 1658887059,
|
||||
"app_id": "A286WATV2",
|
||||
"icons": {
|
||||
"image_36": "https://avatars.slack-edge.com/2019-05-30/652285939191_7831939cc30ef7159561_36.png",
|
||||
"image_48": "https://avatars.slack-edge.com/2019-05-30/652285939191_7831939cc30ef7159561_48.png",
|
||||
"image_72": "https://avatars.slack-edge.com/2019-05-30/652285939191_7831939cc30ef7159561_72.png"
|
||||
},
|
||||
"team_id": "T9RQLQ0KV"
|
||||
},
|
||||
"attachments": [
|
||||
{
|
||||
"id": 1,
|
||||
"color": "2ecc71",
|
||||
"fallback": "Alert API closed alert <https://opsg.in/a/i/render/ec6e7c4b-90a6-4c3c-94e9-901f6b7ada47-1712922031403|#11070> \"[Grafana]: Firing: Alertconfig Workflow Failed\"",
|
||||
"text": "Alert API closed alert <https://opsg.in/a/i/render/ec6e7c4b-90a6-4c3c-94e9-901f6b7ada47-1712922031403|#11070> \"[Grafana]: Firing: Alertconfig Workflow Failed\""
|
||||
}
|
||||
],
|
||||
"channel": "C06U1DDBBU4",
|
||||
"event_ts": "1712925631.682559",
|
||||
"channel_type": "channel"
|
||||
},
|
||||
"type": "event_callback",
|
||||
"event_id": "Ev06U1R10TPV",
|
||||
"event_time": 1712925631,
|
||||
"authorizations": [
|
||||
{
|
||||
"enterprise_id": null,
|
||||
"team_id": "T9RQLQ0KV",
|
||||
"user_id": "U06TS9M7ABG",
|
||||
"is_bot": true,
|
||||
"is_enterprise_install": false
|
||||
}
|
||||
],
|
||||
"is_ext_shared_channel": false,
|
||||
"event_context": "4-eyJldCI6Im1lc3NhZ2UiLCJ0aWQiOiJUOVJRTFEwS1YiLCJhaWQiOiJBMDZUWUg3Q0FMQiIsImNpZCI6IkMwNlUxRERCQlU0In0"
|
||||
}
|
||||
Reference in New Issue
Block a user