Skip to content
This repository was archived by the owner on Jul 7, 2025. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 pkg/action/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ An Action is an element of a workflow that is executed within an AlertChain. By
- [http](./http)
- [otx](./otx)
- [bigquery](./bigquery)
- [incident.io](./incident_io/)
26 changes: 14 additions & 12 deletions pkg/action/action.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"github.com/m-mizutani/alertchain/pkg/action/chatgpt"
"github.com/m-mizutani/alertchain/pkg/action/github"
"github.com/m-mizutani/alertchain/pkg/action/http"
"github.com/m-mizutani/alertchain/pkg/action/incident_io"
"github.com/m-mizutani/alertchain/pkg/action/jira"
"github.com/m-mizutani/alertchain/pkg/action/opsgenie"
"github.com/m-mizutani/alertchain/pkg/action/otx"
Expand All @@ -14,18 +15,19 @@ import (
)

var actionMap = map[types.ActionName]interfaces.RunAction{
"github.create_issue": github.CreateIssue,
"github.create_comment": github.CreateComment,
"jira.create_issue": jira.CreateIssue,
"jira.add_comment": jira.AddComment,
"jira.add_attachment": jira.AddAttachment,
`opsgenie.create_alert`: opsgenie.CreateAlert,
"chatgpt.query": chatgpt.Query,
"slack.post": slack.Post,
"http.fetch": http.Fetch,
"otx.indicator": otx.Indicator,
"bigquery.insert_alert": bigquery.InsertAlert,
"bigquery.insert_data": bigquery.InsertData,
"github.create_issue": github.CreateIssue,
"github.create_comment": github.CreateComment,
"jira.create_issue": jira.CreateIssue,
"jira.add_comment": jira.AddComment,
"jira.add_attachment": jira.AddAttachment,
`opsgenie.create_alert`: opsgenie.CreateAlert,
"chatgpt.query": chatgpt.Query,
"slack.post": slack.Post,
"http.fetch": http.Fetch,
"otx.indicator": otx.Indicator,
"bigquery.insert_alert": bigquery.InsertAlert,
"bigquery.insert_data": bigquery.InsertData,
"incident_io.create_alert": incident_io.CreateAlert,
}

func Map() map[types.ActionName]interfaces.RunAction {
Expand Down
53 changes: 53 additions & 0 deletions pkg/action/incident_io/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# incident.io

The `incident.io` action is used to create incidents in the incident.io platform.

## Pre-requisites

- Create an account in the incident.io platform
- `Team` or higher plan (for API access)
- Create Alert source and Alert route. See [the article](https://incident.io/changelog/automatically-create-incidents-with-alerts) for more information.
- **API Token**: Generate an API token in the incident.io platform
- **Alert Source Config ID**: Get the Alert Source Config ID from the Alert Source settings

## `incident_io.create_alert`

This action creates an alert in the incident.io platform. It calls [CreateHTTP Alert Event V2](https://api-docs.incident.io/tag/Alert-Events-V2#operation/Alert%20Events%20V2_CreateHTTP) API.

### Arguments

Example policy:

```rego
run[job] {
job := {
id: "your-action",
uses: "incident_io.create_alert",
args: {
"secret_api_token": input.env.INCIDENT_IO_API_TOKEN,
"alert_source_config_id": "B2BJGP4XSC4ZNWUY8FB5KIOV4SLGRS8N",
"title": "Alert title",
"description": "Alert description",
"status": "firing",
"deduplication_key": "dedup-key",
"metadata": {
"key1": "value1",
"key2": "value2",
},
},
},
}
```

- `secret_api_token` (string, required): Specifies the API token of the incident.io platform.
- `alert_source_config_id` (string, required): Specifies the Alert Source Config ID.
- `title` (string, optional): Specifies the title of the alert. Default is `title` of [Alert](../../../docs/policy.md#alert).
- `description` (string, optional): Specifies the description of the alert. Default is `description` of [Alert](../../../docs/policy.md#alert).
- `status` (`firing` or `resolved`, optional): Specifies the status of the alert. Default is `firing`.
- `deduplication_key` (string, optional): Specifies the deduplication key of the alert. Default is `id` of [Alert](../../../docs/policy.md#alert).
- `metadata` (object, optional): Specifies the metadata of the alert. Default is built from `attrs` of [Alert](../../../docs/policy.md#alert) and provided `metadata` will be merged into default metadata.
- `source_url`: A link to the alert in the upstream system

### Response

See the response part of [CreateHTTP Alert Event V2](https://api-docs.incident.io/tag/Alert-Events-V2#operation/Alert%20Events%20V2_CreateHTTP).
132 changes: 132 additions & 0 deletions pkg/action/incident_io/alert.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package incident_io

import (
"bytes"
"encoding/json"
"io"
"net/http"
"net/http/httptest"

"github.com/m-mizutani/alertchain/pkg/domain/model"
"github.com/m-mizutani/goerr"
)

type createAlertRequest struct {
// Required
Status string `json:"status"`
Title string `json:"title"`

// Optional
DeduplicationKey string `json:"deduplication_key"`
Description string `json:"description,omitempty"`
MetaData map[string]interface{} `json:"metadata,omitempty"`
SourceURL string `json:"source_url,omitempty"`
}

const (
baseURL = "https://api.incident.io"
)

type httpClient interface {
Do(req *http.Request) (*http.Response, error)
}

func CreateAlert(ctx *model.Context, alert model.Alert, args model.ActionArgs) (any, error) {
var (
apiToken string
alertSourceConfigID string
)
metaData := map[string]any{}
request := &createAlertRequest{
Title: alert.Title,
Status: "firing",
DeduplicationKey: alert.ID.String(),
Description: alert.Description,
MetaData: map[string]any{},
}
for _, attr := range alert.Attrs {
request.MetaData[attr.Key.String()] = attr.Value
}

if err := args.Parse(
model.ArgDef("secret_api_token", &apiToken),
model.ArgDef("alert_source_config_id", &alertSourceConfigID),

model.ArgDef("status", &request.Status, model.ArgOptional()),
model.ArgDef("title", &request.Title, model.ArgOptional()),
model.ArgDef("description", &request.Description, model.ArgOptional()),
model.ArgDef("deduplication_key", &request.DeduplicationKey, model.ArgOptional()),
model.ArgDef("metadata", &metaData, model.ArgOptional()),
model.ArgDef("source_url", &request.SourceURL, model.ArgOptional()),
); err != nil {
return nil, err
}

for k, v := range metaData {
request.MetaData[k] = v
}

url := baseURL + "/v2/alert_events/http/" + alertSourceConfigID
raw, err := json.Marshal(request)
if err != nil {
return nil, goerr.Wrap(err, "fail to marshal incident.io request")
}
reqBody := bytes.NewReader(raw)

req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, reqBody)
if err != nil {
return nil, goerr.Wrap(err, "fail to create incident.io http request")
}
req.Header.Set("Authorization", "Bearer "+apiToken)

var client httpClient = http.DefaultClient
if ctx.Test() {
client = sink
}

resp, err := client.Do(req)
if err != nil {
return nil, goerr.Wrap(err, "fail to send incident.io request")
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusAccepted {
respBody, _ := io.ReadAll(resp.Body)
return nil, goerr.Wrap(err, "unexpected status code from incident.io").With("code", resp.StatusCode).With("body", string(respBody))
}

var respBody struct {
DeduplicationKey string `json:"deduplication_key"`
Message string `json:"message"`
Status string `json:"status"`
}

data, err := io.ReadAll(resp.Body)
if err != nil {
return nil, goerr.Wrap(err, "fail to read incident.io response")
}
if err := json.Unmarshal(data, &respBody); err != nil {
return nil, goerr.Wrap(err, "fail to decode incident.io response").With("body", string(data))
}

return &respBody, nil
}

// For testing
type sinkHTTPClient struct {
Requests []*http.Request
}

func (x *sinkHTTPClient) Reset() {
x.Requests = nil
}

func (x *sinkHTTPClient) Do(req *http.Request) (*http.Response, error) {
x.Requests = append(x.Requests, req)
resp := httptest.NewRecorder()
resp.WriteHeader(http.StatusAccepted)
_, _ = resp.Write([]byte(`{"deduplication_key":"test_key","message":"test_message","status":"test_status"}`))
return resp.Result(), nil
}

var sink = &sinkHTTPClient{}
Loading