diff --git a/CHANGELOG.md b/CHANGELOG.md index 49741ee..3f4ca86 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ Versioning](http://semver.org/spec/v2.0.0.html). ## Unreleased +## [1.0.4] - 2021-04-14 +### Added +- flags `--heartbeat` and `--hearbeat-map` to enable this handler to send an heartbeat request to opsgenie instead opening an alert. This can be used as keepalived handler or just to check if one integration is working fine. + +### Changed +- Update Readme + ## [1.0.3] - 2021-04-01 ### Added - flag `--tagTemplate` based on fork from [nixwiz](https://github.com/nixwiz/sensu-opsgenie-handler/commit/73208fff8c51234814758e8aafed114751daeff9). Remove short option due conflict with titlePrettify flag. diff --git a/README.md b/README.md index e15a6af..2459721 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,22 @@ ![Go Test](https://github.com/betorvs/sensu-opsgenie-handler/workflows/Go%20Test/badge.svg) [![Sensu Bonsai Asset](https://img.shields.io/badge/Bonsai-Download%20Me-brightgreen.svg?colorB=89C967&logo=sensu)](https://bonsai.sensu.io/assets/betorvs/sensu-opsgenie-handler) +## Table of Contents +- [Overview](#overview) +- [Configuration](#configuration) +- [Usage examples](#usage-examples) +- [Others Configurations](#others-configurations) + - [To use Opsgenie Priority from Entity or Check](#to-use-opsgenie-priority-from-entity-or-check) + - [Argument Annotations](#argument-annotations) + - [Asset registration](#asset-registration) +- [Installation from source](#installation-from-source) +- [Additional notes](#additional-notes) + - [Option remediation handler](#option-remediation-handler) + - [Option keepalived handler](#option-keepalived-handler) +- [Contributing](#contributing) + +## Overview + The Sensu Go OpsGenie Handler is a [Sensu Event Handler][3] which manages [OpsGenie][2] incidents, for alerting operators. With this handler, [Sensu][1] can trigger OpsGenie incidents. @@ -10,15 +26,6 @@ This handler was inspired by [pagerduty plugin][6]. After version 1.0.0 we changed opsgenie [sdk][7] to [sdk-v2][8]. -## Installation - -Download the latest version of the sensu-opsgenie-handler from [releases][4], -or create an executable script from this source. - -From the local path of the sensu-opsgenie-handler repository: -``` -go build -o /usr/local/bin/sensu-opsgenie-handler main.go -``` ## Configuration @@ -105,6 +112,8 @@ Flags: -d, --descriptionTemplate string The template for the description to be sent (default "{{.Check.Output}}") --escalation-team string The OpsGenie Escalation Responders Team, use default from OPSGENIE_ESCALATION_TEAM env var -F, --fullDetails Include the more details to send to OpsGenie like proxy_entity_name, occurrences and agent details arch and os + --hearbeat-map string Map of entity/check to heartbeat name. E. entity/check=heartbeat_name,entity1/check1=heartbeat + --heartbeat Enable Heartbeat Events -h, --help help for sensu-opsgenie-handler -i, --includeEventInNote Include the event JSON in the payload sent to OpsGenie -l, --messageLimit int The maximum length of the message field (default 130) @@ -130,6 +139,8 @@ Use "sensu-opsgenie-handler [command] --help" for more information about a comma To configure OpsGenie Sensu Integration follow these first part in [OpsGenie Docs][5]. +## Others Configurations + ### To use Opsgenie Priority from Entity or Check Please add this annotations inside sensu-agent: @@ -179,7 +190,7 @@ metadata: ``` -### Asset creation +### Asset registration The easiest way to get this handler added to your Sensu environment, is to add it as an asset from Bonsai: @@ -189,8 +200,25 @@ sensuctl asset add betorvs/sensu-opsgenie-handler --rename sensu-opsgenie-handle See `sensuctl asset --help` for details on how to specify version. +## Installation from source + +Download the latest version of the sensu-opsgenie-handler from [releases][4], +or create an executable script from this source. + +From the local path of the sensu-opsgenie-handler repository: +``` +go build -o /usr/local/bin/sensu-opsgenie-handler main.go +``` + + ## Additional notes +Both options presented here changes how this handler works, only use this for specific cases and remember to not apply any filter, because in both cases, they will send only in case of status `!= 0`. + +### Option remediation handler + +Using this option we enable opsgenie handler to add extra properties in a previous alert created with remediation actions (checks). + Flags: `--remediation-events` and `--remediation-event-alias`. More info about [remediation][11]. Proposed Workflow: @@ -241,6 +269,31 @@ spec: More ideas about remediation try this [plugin][12]. +### Option keepalived handler + +This option enable opsgenie plugin to send heatbeat pings instead creating new alerts. This options could fit in keepalive for important network assets or important integrations (like [alert manager plugin][14]). + +Flags `--heartbeat` and `--hearbeat-map` can map a entity/check to a [heartbeat][13] in opsgenie. + +And Handler: +```yml +type: Handler +api_version: core/v2 +metadata: + name: opsgenie_heartbeat + namespace: default +spec: + type: pipe + command: sensu-opsgenie-handler --heartbeat --hearbeat-map webserver01/check-nginx=heartbeat_webserver01_nginx + env_vars: + - OPSGENIE_REGION=us + timeout: 10 + runtime_assets: + - betorvs/sensu-opsgenie-handler + filters: null +``` + + ## Contributing See https://github.com/sensu/sensu-go/blob/master/CONTRIBUTING.md @@ -257,3 +310,5 @@ See https://github.com/sensu/sensu-go/blob/master/CONTRIBUTING.md [10]: https://docs.sensu.io/sensu-go/latest/guides/secrets-management/#use-env-for-secrets-management [11]: https://github.com/sensu/sensu-remediation-handler [12]: https://github.com/betorvs/sensu-dynamic-check-mutator +[13]: https://docs.opsgenie.com/docs/heartbeat-api +[14]: https://github.com/betorvs/sensu-alertmanager-events \ No newline at end of file diff --git a/main.go b/main.go index 1ca3515..e044f8f 100644 --- a/main.go +++ b/main.go @@ -9,6 +9,7 @@ import ( "github.com/opsgenie/opsgenie-go-sdk-v2/alert" "github.com/opsgenie/opsgenie-go-sdk-v2/client" + "github.com/opsgenie/opsgenie-go-sdk-v2/heartbeat" "github.com/sensu-community/sensu-plugin-sdk/sensu" "github.com/sensu-community/sensu-plugin-sdk/templates" @@ -44,6 +45,8 @@ type Config struct { TagsTemplates []string RemediationEvents bool RemediationEventAlias string + HeartbeatEvents bool + HeartbeatMap string } var ( @@ -246,6 +249,24 @@ var ( Usage: "Replace opsgenie alias with this value and add only output as node in opsgenie. Should be used with auto remediation checks", Value: &plugin.RemediationEventAlias, }, + { + Path: "heartbeat", + Env: "", + Argument: "heartbeat", + Shorthand: "", + Default: false, + Usage: "Enable Heartbeat Events", + Value: &plugin.HeartbeatEvents, + }, + { + Path: "hearbeat-map", + Env: "", + Argument: "hearbeat-map", + Shorthand: "", + Default: "", + Usage: "Map of entity/check to heartbeat name. E. entity/check=heartbeat_name,entity1/check1=heartbeat", + Value: &plugin.HeartbeatMap, + }, } ) @@ -448,12 +469,12 @@ func executeHandler(event *types.Event) error { if err != nil { return fmt.Errorf("failed to create opsgenie client: %s", err) } - + // always create an alert in opsgenie if status != 0 if event.Check.Status != 0 { return createIncident(alertClient, event) } - // if RemediationEvents true + // if RemediationEvents true: change behaviour of opsgenie plugin if plugin.RemediationEvents && event.Check.Status == 0 { hasAlert, _ := getAlert(alertClient, plugin.RemediationEventAlias) details := make(map[string]string) @@ -464,6 +485,33 @@ func executeHandler(event *types.Event) error { notes := fmt.Sprintf("%s ", event.Check.Output) return updateAlert(alertClient, notes, hasAlert, details) } + // if heartbeat true: match entity/check with heartbeat + if plugin.HeartbeatEvents && event.Check.Status == 0 && plugin.HeartbeatMap != "" { + heartbeats, err := parseHeartbeatMap(plugin.HeartbeatMap) + if err != nil { + return err + } + entity_check := fmt.Sprintf("%s/%s", event.Entity.Name, event.Check.Name) + if heartbeats[entity_check] != "" { + fmt.Printf("Pinging heartbeat %s \n", heartbeats[entity_check]) + errPing := pingHeartbeat(heartbeats[entity_check]) + if errPing != nil { + return errPing + } + } + if heartbeats["all"] != "" { + // ping all alerts + fmt.Printf("Pinging heartbeat %s without entity/check defined\n", heartbeats["all"]) + errPing := pingHeartbeat(heartbeats["all"]) + if errPing != nil { + return errPing + } + } + if len(heartbeats) != 0 { + fmt.Println("Not pinging any heartbeat because entity/check defined do not match") + } + return nil + } // check if event has a alert _, alias, _ := parseEventKeyTags(event) @@ -660,3 +708,71 @@ func updateAlert(alertClient *alert.Client, notes string, alertid string, detail } return nil } + +func pingHeartbeat(name string) error { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + heartbeatClient, err := heartbeat.NewClient(&client.Config{ + ApiKey: plugin.AuthToken, + OpsGenieAPIURL: switchOpsgenieRegion(), + }) + if err != nil { + return err + } + + hearbeatResult, err := heartbeatClient.Ping(ctx, name) + if err != nil { + return err + } + fmt.Printf("Heartbeat %s was requested with %s and response time %v and message %s", name, hearbeatResult.RequestId, hearbeatResult.ResponseTime, hearbeatResult.Message) + + return nil +} + +func parseHeartbeatMap(s string) (map[string]string, error) { + // entity1/check1=heartbeat1,entity2/check2=heartbeat2 + heartbeats := make(map[string]string) + temp := makeMap(s) + for k, v := range temp { + if strings.Contains(v, "/") { + return heartbeats, fmt.Errorf("hearbeat wrong format: entity/check=heartbeat_name") + } + if k != "" && v != "" { + key := k + if !strings.Contains(k, "/") { + key = "all" + } + heartbeats[key] = v + } + } + return heartbeats, nil +} + +func makeMap(s string) map[string]string { + temp := make(map[string]string) + if strings.Contains(s, ",") { + splited := strings.Split(s, ",") + for _, v := range splited { + a, b := splitString(v, "=") + if a != "" && b != "" { + temp[a] = b + } + } + } else { + a, b := splitString(s, "=") + if a != "" && b != "" { + temp[a] = b + } + } + return temp +} + +func splitString(s, div string) (string, string) { + if div != "" { + splited := strings.Split(s, div) + if len(splited) == 2 { + return splited[0], splited[1] + } + } + return "", "" +} diff --git a/main_test.go b/main_test.go index 3469a58..7dc20b8 100644 --- a/main_test.go +++ b/main_test.go @@ -176,3 +176,47 @@ func TestVisibilityTeams(t *testing.T) { res1 := visibilityTeams() assert.Equal(t, res1, test1) } + +func TestSplitString(t *testing.T) { + test1 := "key1=value1" + res1 := "key1" + res2 := "value1" + val1, val2 := splitString(test1, "=") + assert.Equal(t, res1, val1) + assert.Equal(t, res2, val2) + assert.NotEqual(t, res1, val2) +} + +func TestMakeRewriteAnnotation(t *testing.T) { + test1 := "key1=value1,key2=value2" + res1 := map[string]string{"key1": "value1", "key2": "value2"} + val1 := makeMap(test1) + assert.Equal(t, res1, val1) + test2 := "key1=value1,key2/subkey2=value2" + res2 := map[string]string{"key1": "value1", "key2/subkey2": "value2"} + val2 := makeMap(test2) + assert.Equal(t, res2, val2) + test3 := "key1=value1,key2=value2," + res3 := map[string]string{"key1": "value1", "key2": "value2"} + val3 := makeMap(test3) + assert.Equal(t, res3, val3) +} + +func TestParseHeartbeatMap(t *testing.T) { + test1 := "entity1/check1=heartbeat1,entity2/check2=heartbeat2" + expected1 := map[string]string{"entity1/check1": "heartbeat1", "entity2/check2": "heartbeat2"} + res1, err1 := parseHeartbeatMap(test1) + assert.Equal(t, expected1, res1) + assert.NoError(t, err1) + + test2 := "entity1/check1=heartbeat1," + expected2 := map[string]string{"entity1/check1": "heartbeat1"} + res2, err2 := parseHeartbeatMap(test2) + assert.Equal(t, expected2, res2) + assert.NoError(t, err2) + + test3 := "heartbeat1=entity1/check1," + _, err3 := parseHeartbeatMap(test3) + assert.Error(t, err3) + +}