forked from target/goalert
-
Notifications
You must be signed in to change notification settings - Fork 0
Add native AWS CloudWatch and Azure Monitor alert ingress, plus alert write-back mutations #2
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
Open
sarora-eightfold
wants to merge
13
commits into
master
Choose a base branch
from
add-aws-support
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
52960b4
Add native AWS CloudWatch (via SNS) alert ingress
b481b3e
Add native Azure Monitor alert ingress
fbf74d8
minor lint change
sarora-eightfold 3b9b779
Add setAlertMetadata mutation to support write-back from external sys…
sarora-eightfold 4a9d017
Render a source console/portal link in alert details
sarora-eightfold d2cdd32
Derive alarm region from the alarm's own ARN, not the SNS topic's
sarora-eightfold 627a8a0
Add appendAlertDetails mutation for external write-back into alert De…
sarora-eightfold 39805ef
cloudwatch: return error instead of panic for unknown signed field
sarora-eightfold 76f237d
ingress: make transient failures retryable, address review comments
sarora-eightfold 4b3c3ac
address remaining claude[bot] review comments
sarora-eightfold 33b3cd7
minor
sarora-eightfold 161fd61
azuremonitor: link to the alert page, not the AI investigation agent
sarora-eightfold 983c602
add status support
sarora-eightfold File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| package alert | ||
|
|
||
| import ( | ||
| "context" | ||
|
|
||
| "github.com/target/goalert/gadb" | ||
| "github.com/target/goalert/permission" | ||
| "github.com/target/goalert/validation/validate" | ||
| ) | ||
|
|
||
| // LockDetailsTx returns an alert's current Details, locking its row for the rest | ||
| // of the transaction. | ||
| // | ||
| // This exists so an append can be done as a read-modify-write without losing a | ||
| // concurrent one: the lock makes a second appender wait for the first to commit | ||
| // and then read the value it wrote, instead of both reading the same original and | ||
| // one overwriting the other. | ||
| func (s Store) LockDetailsTx(ctx context.Context, db gadb.DBTX, alertID int) (string, error) { | ||
| err := permission.LimitCheckAny(ctx, permission.User, permission.Service) | ||
| if err != nil { | ||
| return "", err | ||
| } | ||
|
|
||
| return gadb.New(db).Alert_LockOneAlertDetails(ctx, gadb.Alert_LockOneAlertDetailsParams{ | ||
| ID: int64(alertID), | ||
| ServiceID: permission.ServiceNullUUID(ctx), // only provide service_id restriction if request is from a service | ||
| }) | ||
| } | ||
|
|
||
| // SetDetailsTx sets the Details for an existing alert, replacing it wholesale. | ||
| // | ||
| // Unlike SetMetadataTx this does not merge: Details is free text with no | ||
| // key/value structure to merge on, so a caller wanting to preserve existing | ||
| // content must read it first and pass the full text it wants stored. | ||
| func (s Store) SetDetailsTx(ctx context.Context, db gadb.DBTX, alertID int, details string) error { | ||
| err := permission.LimitCheckAny(ctx, permission.User, permission.Service) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| err = validate.Text("Details", details, 0, MaxDetailsLength) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| rowCount, err := gadb.New(db).Alert_SetDetails(ctx, gadb.Alert_SetDetailsParams{ | ||
| ID: int64(alertID), | ||
| Details: details, | ||
| ServiceID: permission.ServiceNullUUID(ctx), // only provide service_id restriction if request is from a service | ||
| }) | ||
| 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 | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,140 @@ | ||
| // Package azuremonitor implements ingress for Azure Monitor alerts delivered by | ||
| // an action-group webhook receiver. | ||
| // | ||
| // # Trust model | ||
| // | ||
| // Azure signs nothing. Unlike the sibling cloudwatch integration -- where SNS | ||
| // signs every message with a verifiable certificate chain -- the integration key | ||
| // in the query string is the only credential, so the webhook URL is | ||
| // credential-grade: anyone holding it can create arbitrary alerts on the service | ||
| // the key identifies. There is no public key to verify against, so asymmetric | ||
| // verification is not possible; anything built would be a shared secret with | ||
| // extra steps. | ||
| // | ||
| // The handler makes no outbound requests at all, so there is no SSRF surface and | ||
| // nothing analogous to cloudwatch's host allowlist or certificate cache. | ||
| // | ||
| // # Service resolution | ||
| // | ||
| // The integration key selects the GoAlert service. Nothing here knows or can know | ||
| // which action group, subscription, or team it is serving -- the only inputs are | ||
| // the request body and the key. That is what lets one deployed handler serve any | ||
| // number of action groups with no routing table and no per-group code. | ||
| package azuremonitor | ||
|
|
||
| import ( | ||
| "errors" | ||
| "io" | ||
| "net/http" | ||
| "time" | ||
|
|
||
| "github.com/target/goalert/alert" | ||
| "github.com/target/goalert/integrationkey" | ||
| "github.com/target/goalert/permission" | ||
| "github.com/target/goalert/retry" | ||
| "github.com/target/goalert/util/errutil" | ||
| "github.com/target/goalert/util/log" | ||
| ) | ||
|
|
||
| // maxBodyBytes bounds the request body independently of the global | ||
| // maxBodySizeMiddleware, which is disabled when MaxReqBodyBytes is 0. | ||
| const maxBodyBytes = 256 * 1024 | ||
|
|
||
| // Config configures a Handler. | ||
| type Config struct { | ||
| AlertStore *alert.Store | ||
| IntegrationKeyStore *integrationkey.Store | ||
| } | ||
|
|
||
| // Handler serves the Azure Monitor ingress endpoint. | ||
| type Handler struct { | ||
| cfg Config | ||
| } | ||
|
|
||
| // NewHandler returns a Handler for the given config. | ||
| func NewHandler(cfg Config) *Handler { return &Handler{cfg: cfg} } | ||
|
|
||
| // ServeIncoming handles an Azure Monitor action-group webhook delivery. | ||
| func (h *Handler) ServeIncoming(w http.ResponseWriter, r *http.Request) { | ||
| ctx := r.Context() | ||
|
|
||
| err := permission.LimitCheckAny(ctx, permission.Service) | ||
| if errutil.HTTPError(ctx, w, err) { | ||
| return | ||
| } | ||
|
|
||
| // MaxBytesReader rather than io.LimitReader: errutil.HTTPError maps | ||
| // *http.MaxBytesError to a clean 413, whereas silent truncation would surface | ||
| // as a confusing 400. | ||
| r.Body = http.MaxBytesReader(w, r.Body, maxBodyBytes) | ||
| data, err := io.ReadAll(r.Body) | ||
| if errutil.HTTPErrorRetry(ctx, w, err) { | ||
| return | ||
| } | ||
|
|
||
| a, meta, info, err := buildAlert(data) | ||
| switch { | ||
| case errors.Is(err, errLegacySchema): | ||
| // Deliberately not degraded to the best-effort path: a legacy-schema | ||
| // payload would yield alerts with no useful content and no indication | ||
| // why. 400 rather than 5xx so Azure does not retry a receiver that is | ||
| // permanently misconfigured. | ||
| log.Logf(ctx, "azuremonitor: %v", err) | ||
| http.Error(w, err.Error(), http.StatusBadRequest) | ||
| return | ||
| case err != nil: | ||
| log.Debugf(ctx, "azuremonitor: bad request body: %v", err) | ||
| http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) | ||
| return | ||
| } | ||
|
|
||
| // signalType alone does not identify the payload shape -- Platform and | ||
| // Prometheus metric alerts share it, as do Log Alerts V2 and Azure Backup -- | ||
| // so log the whole triple that actually determines parsing. | ||
| ctx = log.WithFields(ctx, log.Fields{ | ||
| "AzureSignalType": info.SignalType, | ||
| "AzureMonitorService": info.MonitorService, | ||
| "AzureConditionType": info.ConditionType, | ||
| "AzureMonitorCondition": meta["monitor_condition"], | ||
| }) | ||
|
|
||
| // Nothing recognised alertContext, so this alert carries essentials only. That | ||
| // is a deliberate graceful degradation rather than a failure, but it is also | ||
| // how a newly-routed Azure alert type announces itself -- otherwise the only | ||
| // symptom is thin alerts that nobody notices. | ||
| if !info.ContextRendered { | ||
| log.Logf(ctx, "azuremonitor: unrecognised alertContext, alert built from essentials only") | ||
| } | ||
|
|
||
| a.ServiceID = permission.ServiceID(ctx) | ||
| if len(meta) == 0 { | ||
| meta = nil | ||
| } | ||
|
|
||
| var created *alert.Alert | ||
| err = retry.DoTemporaryError(func(int) error { | ||
| var err error | ||
| created, _, err = h.cfg.AlertStore.CreateOrUpdateWithMeta(ctx, &a, meta) | ||
| return err | ||
| }, | ||
| retry.Log(ctx), | ||
| retry.Limit(5), | ||
| retry.FibBackoff(250*time.Millisecond), | ||
| ) | ||
| // HTTPErrorRetry, not HTTPError: Azure's retryable set is 408, 429, 503 and 504 | ||
| // -- 500 is absent, so mapping an exhausted database failure to 500 would drop | ||
| // the alert with no retries at all, which is exactly the case most worth | ||
| // retrying (an Aurora failover). 503 is retried by Azure and by SNS. | ||
| if errutil.HTTPErrorRetry(ctx, w, err) { | ||
| return | ||
| } | ||
|
|
||
| // created is nil with a nil error when the status was closed and no open alert | ||
| // held this dedup key -- a Resolved delivery for an alert we never opened, or | ||
| // already closed. Normal, so info level and a 2xx. | ||
| if created == nil { | ||
| log.Logf(ctx, "azuremonitor: no open alert to close") | ||
| } | ||
|
|
||
| w.WriteHeader(http.StatusNoContent) | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.