Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add alert metadata #3764

Merged
merged 49 commits into from
Apr 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
5f1d9af
alert metadata WIP
Forfold Mar 11, 2024
da7e2c9
add alert metada changes
shivanishendge Mar 21, 2024
e6e48ea
add alert type
shivanishendge Mar 21, 2024
d1deb73
add validation
shivanishendge Mar 21, 2024
3ca69c1
generic api handler correction
shivanishendge Mar 21, 2024
f772f32
add meta field to webhook notification
shivanishendge Mar 21, 2024
19a49d6
remove log statements
shivanishendge Mar 21, 2024
e25626c
resolve go lint issues
shivanishendge Mar 21, 2024
acc9b12
Update migrate/migrations/20240212125328-alert-metadata.sql
shivanishendge Mar 26, 2024
53e8839
Update graphql2/schema.graphql
shivanishendge Mar 26, 2024
c6026b0
Update graphql2/graphqlapp/alert.go
shivanishendge Mar 26, 2024
2ed2bd6
change as per review
shivanishendge Mar 26, 2024
1eea8c2
Add createMetaTx method
mastercactapus Mar 26, 2024
cd9d54b
fix urlform values
shivanishendge Mar 26, 2024
88decb8
fix generic api tests
shivanishendge Mar 26, 2024
ab33b35
Merge branch 'master' into add-alertMetadata
shivanishendge Mar 26, 2024
a9a00a6
fix lint error
shivanishendge Mar 26, 2024
2ee7763
remove commented code
mastercactapus Mar 26, 2024
6b4641d
move metadata out of CreateOrUpdate
mastercactapus Mar 26, 2024
3a46df7
update SetMetadata to support service permissions validation
mastercactapus Mar 26, 2024
3ed9354
fix type for manymetadata and add single query
mastercactapus Mar 26, 2024
63bb1d7
regen sqlc
mastercactapus Mar 26, 2024
f0191dc
move store methods to metadata.go
mastercactapus Mar 26, 2024
166a01e
move validation to metadata.go
mastercactapus Mar 26, 2024
f4bb587
missing sig. updates
mastercactapus Mar 26, 2024
aef1cc3
add ServiceNullUUID method to permission pkg
mastercactapus Mar 26, 2024
bd84139
update grafana call
mastercactapus Mar 26, 2024
9d6e758
regen sqlc
mastercactapus Mar 26, 2024
5511efb
fix CreateOrUpdate call
mastercactapus Mar 26, 2024
230510d
add db in call for metadata
mastercactapus Mar 26, 2024
303ceed
fix edge case handling in MetaValue
mastercactapus Mar 26, 2024
57d1371
move map handling out of Tx function
mastercactapus Mar 26, 2024
e628cd5
add create CreateOrUpdateWithMeta store method and update generic api…
shivanishendge Mar 28, 2024
56de5ce
add comment
shivanishendge Mar 28, 2024
d5add2f
Merge branch 'master' into add-alertMetadata
mastercactapus Apr 1, 2024
8f3d384
remove unused queries
mastercactapus Apr 1, 2024
40f8f29
clairify comment
mastercactapus Apr 1, 2024
26e067e
tweak error message
mastercactapus Apr 1, 2024
1effb27
cleanup graphql code
mastercactapus Apr 1, 2024
36e627d
update documentation
mastercactapus Apr 1, 2024
3a05169
remove changes to existing tests (will create new)
mastercactapus Apr 1, 2024
610dc1e
add metadata smoke test
mastercactapus Apr 1, 2024
c3f57d9
add dataloader for batching metadata lookups
mastercactapus Apr 1, 2024
940b669
use dataloader
mastercactapus Apr 1, 2024
aeb3444
Merge remote-tracking branch 'origin/master' into add-alertMetadata
mastercactapus Apr 1, 2024
37c716e
regen
mastercactapus Apr 1, 2024
695fa9f
add id column for switchover
mastercactapus Apr 1, 2024
c400820
Merge branch 'master' into add-alertMetadata
mastercactapus Apr 2, 2024
6fe5702
regen sqlc
mastercactapus Apr 2, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions alert/alert.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ func (a Alert) Normalize() (*Alert, error) {
}
a.Summary = strings.Replace(a.Summary, "\n", " ", -1)
a.Summary = strings.Replace(a.Summary, " ", " ", -1)

err := validate.Many(
validate.Text("Summary", a.Summary, 1, MaxSummaryLength),
validate.Text("Details", a.Details, 0, MaxDetailsLength),
Expand Down
158 changes: 158 additions & 0 deletions alert/metadata.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
package alert

import (
"context"
"database/sql"
"encoding/json"
"errors"

"github.com/sqlc-dev/pqtype"
"github.com/target/goalert/gadb"
"github.com/target/goalert/permission"
"github.com/target/goalert/validation"
"github.com/target/goalert/validation/validate"
)

const metaV1 = "alert_meta_v1"

type metadataDBFormat struct {
Type string
AlertMetaV1 map[string]string
}

// Metadata returns the metadata for a single alert. If err == nil, meta is guaranteed to be non-nil. If the alert has no metadata, an empty map is returned.
func (s *Store) Metadata(ctx context.Context, db gadb.DBTX, alertID int) (meta map[string]string, err error) {
err = permission.LimitCheckAny(ctx, permission.System, permission.User)
if err != nil {
return nil, err
}

md, err := gadb.New(db).AlertMetadata(ctx, int64(alertID))
if errors.Is(err, sql.ErrNoRows) || !md.Valid {
return map[string]string{}, nil
}
if err != nil {
return nil, err
}

var doc metadataDBFormat
err = json.Unmarshal(md.RawMessage, &doc)
if err != nil {
return nil, err
}

if doc.Type != metaV1 || doc.AlertMetaV1 == nil {
return nil, errors.New("unsupported metadata type")
}

return doc.AlertMetaV1, nil
}

type MetadataAlertID struct {
// AlertID is the ID of the alert.
ID int64
Meta map[string]string
}

func (s Store) FindManyMetadata(ctx context.Context, db gadb.DBTX, alertIDs []int) ([]MetadataAlertID, error) {
err := permission.LimitCheckAny(ctx, permission.System, permission.User)
if err != nil {
return nil, err
}

err = validate.Range("AlertIDs", len(alertIDs), 1, maxBatch)
if err != nil {
return nil, err
}
ids := make([]int64, len(alertIDs))
for i, id := range alertIDs {
ids[i] = int64(id)
}

rows, err := gadb.New(db).AlertManyMetadata(ctx, ids)
if errors.Is(err, sql.ErrNoRows) {
return nil, nil
}
if err != nil {
return nil, err
}

res := make([]MetadataAlertID, len(rows))
for i, r := range rows {
var doc metadataDBFormat
err = json.Unmarshal(r.Metadata.RawMessage, &doc)
if err != nil {
return nil, err
}

if doc.Type != metaV1 || doc.AlertMetaV1 == nil {
return nil, errors.New("unsupported metadata type")
}

res[i] = MetadataAlertID{
ID: r.AlertID,
Meta: doc.AlertMetaV1,
}
}

return res, nil
}

func (s Store) SetMetadataTx(ctx context.Context, db gadb.DBTX, alertID int, meta map[string]string) error {
err := permission.LimitCheckAny(ctx, permission.User, permission.Service)
if err != nil {
return err
}

err = ValidateMetadata(meta)
if err != nil {
return err
}

var doc metadataDBFormat
doc.Type = metaV1
doc.AlertMetaV1 = meta

md, err := json.Marshal(&doc)
if err != nil {
return err
}

rowCount, err := gadb.New(db).AlertSetMetadata(ctx, gadb.AlertSetMetadataParams{
ID: int64(alertID),
ServiceID: permission.ServiceNullUUID(ctx), // only provide service_id restriction if request is from a service
Metadata: pqtype.NullRawMessage{Valid: true, RawMessage: json.RawMessage(md)},
})
if err != nil {
return err
}

if rowCount == 0 {
// shouldn't happen, but just in case
return permission.NewAccessDenied("alert closed, invalid, or wrong service")
}

return nil
}

func ValidateMetadata(m map[string]string) error {
if m == nil {
return validation.NewFieldError("Meta", "cannot be nil")
}

var totalSize int
for k, v := range m {
err := validate.ASCII("Meta[<key>]", k, 1, 255)
if err != nil {
return err
}

totalSize += len(k) + len(v)
}

if totalSize > 32768 {
return validation.NewFieldError("Meta", "cannot exceed 32KiB in size")
}

return nil
}
35 changes: 35 additions & 0 deletions alert/queries.sql
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,38 @@ ON CONFLICT (alert_id)
RETURNING
alert_id;

-- name: AlertMetadata :one
SELECT
metadata
FROM
alert_data
WHERE
alert_id = $1;

-- name: AlertManyMetadata :many
SELECT
alert_id,
metadata
FROM
alert_data
WHERE
alert_id = ANY (@alert_ids::bigint[]);

-- name: AlertSetMetadata :execrows
INSERT INTO alert_data(alert_id, metadata)
SELECT
a.id,
$2
FROM
alerts a
WHERE
a.id = $1
AND a.status != 'closed'
AND (a.service_id = $3
OR $3 IS NULL) -- ensure the alert is associated with the service, if coming from an integration
ON CONFLICT (alert_id)
DO UPDATE SET
metadata = $2
WHERE
alert_data.alert_id = $1;

18 changes: 18 additions & 0 deletions alert/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -610,6 +610,15 @@ func (s *Store) CreateOrUpdateTx(ctx context.Context, tx *sql.Tx, a *Alert) (*Al
// In the case that Status is closed but a matching alert is not present, nil is returned.
// Otherwise the current alert is returned.
func (s *Store) CreateOrUpdate(ctx context.Context, a *Alert) (*Alert, bool, error) {
return s.createOrUpdate(ctx, a, nil)
}

// CreateOrUpdateWithMeta behaves the same as CreateOrUpdate, but also sets metadata on the alert if it is new.
func (s *Store) CreateOrUpdateWithMeta(ctx context.Context, a *Alert, meta map[string]string) (*Alert, bool, error) {
return s.createOrUpdate(ctx, a, meta)
}

func (s *Store) createOrUpdate(ctx context.Context, a *Alert, meta map[string]string) (*Alert, bool, error) {
err := permission.LimitCheckAny(ctx,
permission.System,
permission.Admin,
Expand All @@ -631,10 +640,19 @@ func (s *Store) CreateOrUpdate(ctx context.Context, a *Alert) (*Alert, bool, err
return nil, false, err
}

// Set metadata only if meta is not nil and isNew is true
if meta != nil && isNew {
err = s.SetMetadataTx(ctx, tx, n.ID, meta)
if err != nil {
return nil, false, err
}
}

err = tx.Commit()
if err != nil {
return nil, false, err
}

if n == nil {
return nil, false, nil
}
Expand Down
5 changes: 5 additions & 0 deletions engine/sendmessage.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ func (p *Engine) sendMessage(ctx context.Context, msg *message.Message) (*notifi
// set to nil if it's the current message
stat = nil
}
meta, err := p.a.Metadata(ctx, p.b.db, msg.AlertID)
if err != nil {
return nil, errors.Wrap(err, "lookup alert metadata")
}
notifMsg = notification.Alert{
Dest: msg.Dest,
AlertID: msg.AlertID,
Expand All @@ -79,6 +83,7 @@ func (p *Engine) sendMessage(ctx context.Context, msg *message.Message) (*notifi
CallbackID: msg.ID,
ServiceID: a.ServiceID,
ServiceName: name,
Meta: meta,

OriginalStatus: stat,
}
Expand Down
6 changes: 6 additions & 0 deletions gadb/models.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

87 changes: 87 additions & 0 deletions gadb/queries.sql.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading