Skip to content

Commit

Permalink
feat: add googlechat notifier
Browse files Browse the repository at this point in the history
Signed-off-by: Philipp Born <git@pborn.eu>
  • Loading branch information
tamcore committed Jan 16, 2025
1 parent 6703b0a commit 9bc26dd
Show file tree
Hide file tree
Showing 9 changed files with 373 additions and 2 deletions.
4 changes: 2 additions & 2 deletions asset/assets_vfsdata.go

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

9 changes: 9 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,9 @@ func resolveFilepaths(baseDir string, cfg *Config) {
for _, cfg := range receiver.MSTeamsV2Configs {
cfg.HTTPConfig.SetDirectory(baseDir)
}
for _, cfg := range receiver.GoogleChatConfigs {
cfg.HTTPConfig.SetDirectory(baseDir)
}
for _, cfg := range receiver.JiraConfigs {
cfg.HTTPConfig.SetDirectory(baseDir)
}
Expand Down Expand Up @@ -599,6 +602,11 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error {
return errors.New("no msteamsv2 webhook URL or URLFile provided")
}
}
for _, googlechat := range rcv.GoogleChatConfigs {
if googlechat.HTTPConfig == nil {
googlechat.HTTPConfig = c.Global.HTTPConfig
}
}
for _, jira := range rcv.JiraConfigs {
if jira.HTTPConfig == nil {
jira.HTTPConfig = c.Global.HTTPConfig
Expand Down Expand Up @@ -1024,6 +1032,7 @@ type Receiver struct {
WebexConfigs []*WebexConfig `yaml:"webex_configs,omitempty" json:"webex_configs,omitempty"`
MSTeamsConfigs []*MSTeamsConfig `yaml:"msteams_configs,omitempty" json:"msteams_configs,omitempty"`
MSTeamsV2Configs []*MSTeamsV2Config `yaml:"msteamsv2_configs,omitempty" json:"msteamsv2_configs,omitempty"`
GoogleChatConfigs []*GoogleChatConfig `yaml:"googlechat_configs,omitempty" json:"googlechat_configs,omitempty"`
JiraConfigs []*JiraConfig `yaml:"jira_configs,omitempty" json:"jira_configs,omitempty"`
RocketchatConfigs []*RocketchatConfig `yaml:"rocketchat_configs,omitempty" json:"rocketchat_configs,omitempty"`
}
Expand Down
27 changes: 27 additions & 0 deletions config/notifiers.go
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,14 @@ var (
Text: `{{ template "msteamsv2.default.text" . }}`,
}

DefaultGoogleChatConfig = GoogleChatConfig{
NotifierConfig: NotifierConfig{
VSendResolved: true,
},
Message: `{{ template "googlechat.default.message" . }}`,
Threading: true,
}

DefaultJiraConfig = JiraConfig{
NotifierConfig: NotifierConfig{
VSendResolved: true,
Expand Down Expand Up @@ -892,6 +900,25 @@ func (c *MSTeamsV2Config) UnmarshalYAML(unmarshal func(interface{}) error) error
return nil
}

// GoogleChatConfig configures notifications via Discord.
type GoogleChatConfig struct {
NotifierConfig `yaml:",inline" json:",inline"`

HTTPConfig *commoncfg.HTTPClientConfig `yaml:"http_config,omitempty" json:"http_config,omitempty"`
URL *SecretURL `yaml:"url,omitempty" json:"url,omitempty"`
URLFile string `yaml:"url_file" json:"url_file"`

Message string `yaml:"message,omitempty" json:"message,omitempty"`
Threading bool `yaml:"threading,omitempty" json:"threading,omitempty"`
}

// UnmarshalYAML implements the yaml.Unmarshaler interface.
func (c *GoogleChatConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
*c = DefaultGoogleChatConfig
type plain GoogleChatConfig
return unmarshal((*plain)(c))
}

type JiraConfig struct {
NotifierConfig `yaml:",inline" json:",inline"`
HTTPConfig *commoncfg.HTTPClientConfig `yaml:"http_config,omitempty" json:"http_config,omitempty"`
Expand Down
4 changes: 4 additions & 0 deletions config/receiver/receiver.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"github.com/prometheus/alertmanager/notify/jira"
"github.com/prometheus/alertmanager/notify/msteams"
"github.com/prometheus/alertmanager/notify/msteamsv2"
"github.com/prometheus/alertmanager/notify/googlechat"
"github.com/prometheus/alertmanager/notify/opsgenie"
"github.com/prometheus/alertmanager/notify/pagerduty"
"github.com/prometheus/alertmanager/notify/pushover"
Expand Down Expand Up @@ -103,6 +104,9 @@ func BuildReceiverIntegrations(nc config.Receiver, tmpl *template.Template, logg
for i, c := range nc.MSTeamsV2Configs {
add("msteamsv2", i, c, func(l *slog.Logger) (notify.Notifier, error) { return msteamsv2.New(c, tmpl, l, httpOpts...) })
}
for i, c := range nc.GoogleChatConfigs {
add("googlechat", i, c, func(l log.Logger) (notify.Notifier, error) { return googlechat.New(c, tmpl, l) })

Check failure on line 108 in config/receiver/receiver.go

View workflow job for this annotation

GitHub Actions / lint

undefined: log (typecheck)
}
for i, c := range nc.JiraConfigs {
add("jira", i, c, func(l *slog.Logger) (notify.Notifier, error) { return jira.New(c, tmpl, l, httpOpts...) })
}
Expand Down
24 changes: 24 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -731,6 +731,8 @@ webhook_configs:
[ - <webhook_config>, ... ]
wechat_configs:
[ - <wechat_config>, ... ]
googlechat_configs:
[ - <googlechat_config>, ... ]
```

### `<http_config>`
Expand Down Expand Up @@ -1684,3 +1686,25 @@ room_id: <tmpl_string>
# The HTTP client's configuration. You must use this configuration to supply the bot token as part of the HTTP `Authorization` header.
[ http_config: <http_config> | default = global.http_config ]
```
### `<googlechat_config>`


```yaml
# Whether to notify about resolved alerts.
[ send_resolved: <boolean> | default = true ]
# Whether or not to enable threading.
# Enabling this, will cause resolved notifications to be posted in the same thread as the original message.
[ threading: <boolean> | default = true ]
# The Webhook URL for the Google Chat space.
# i.e. https://chat.googleapis.com/v1/spaces/XXXXXX/messages?key=YYYYYYY&token=ZZZZZZZ
[ url: <secret> ]
# Alternative to `url`. Allows the URL to be read from a file instead.
[ url_file: <filepath> ]

# Message template.
[ message: <tmpl_string> | default = '{{ template "googlechat.default.message" . }}' ]
```
131 changes: 131 additions & 0 deletions notify/googlechat/googlechat.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
// Copyright 2019 Prometheus Team
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package googlechat

import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"os"
"strings"

"github.com/go-kit/log"
commoncfg "github.com/prometheus/common/config"

"github.com/prometheus/alertmanager/config"
"github.com/prometheus/alertmanager/notify"
"github.com/prometheus/alertmanager/template"
"github.com/prometheus/alertmanager/types"
)

type Notifier struct {
conf *config.GoogleChatConfig
tmpl *template.Template
logger log.Logger
client *http.Client
retrier *notify.Retrier
}

func New(conf *config.GoogleChatConfig, t *template.Template, l log.Logger, httpOpts ...commoncfg.HTTPClientOption) (*Notifier, error) {
client, err := commoncfg.NewClientFromConfig(*conf.HTTPConfig, "googlechat", httpOpts...)
if err != nil {
return nil, err
}
return &Notifier{
conf: conf,
tmpl: t,
logger: l,
client: client,
retrier: &notify.Retrier{},
}, nil
}

// Message represents the structure for sending a
// Text message in Google Chat Webhook endpoint.
// https://developers.google.com/chat/api/guides/message-formats/basic
type Message struct {
Text string `json:"text"`
}

// Notify implements the Notifier interface.
func (n *Notifier) Notify(ctx context.Context, alert ...*types.Alert) (bool, error) {
key, err := notify.ExtractGroupKey(ctx)
if err != nil {
return false, err
}

var (
data = notify.GetTemplateData(ctx, n.tmpl, alert, n.logger)
tmpl = notify.TmplText(n.tmpl, data, &err)
)

message := tmpl(n.conf.Message)
if err != nil {
return false, err
}

msg := &Message{
Text: message,
}

var buf bytes.Buffer
if err := json.NewEncoder(&buf).Encode(msg); err != nil {
return false, err
}

var webhookURL string
if n.conf.URL != nil {
webhookURL = n.conf.URL.String()
} else {
content, err := os.ReadFile(n.conf.URLFile)
if err != nil {
return false, fmt.Errorf("read url_file: %w", err)
}
webhookURL = strings.TrimSpace(string(content))
}

// https://developers.google.com/chat/how-tos/webhooks#start_a_message_thread
// To post the first message of a thread with a webhook,
// append the threadKey and messageReplyOption parameters to the webhook URL.
// Set the threadKey to an arbitrary string, but remember what it is;
// you'll need to specify it again to post a reply to the thread.
// https://chat.googleapis.com/v1/spaces/SPACE_ID/messages?threadKey=ARBITRARY_STRING&messageReplyOption=REPLY_MESSAGE_FALLBACK_TO_NEW_THREAD

u, err := url.Parse(webhookURL)
if err != nil {
return false, fmt.Errorf("unable to parse googlechat url: %w", err)
}
q := u.Query()
if n.conf.Threading {
q.Set("threadKey", key.Hash())
q.Set("messageReplyOption", "REPLY_MESSAGE_FALLBACK_TO_NEW_THREAD")
}
u.RawQuery = q.Encode()
webhookURL = u.String()

resp, err := notify.PostJSON(ctx, n.client, webhookURL, &buf)
if err != nil {
return true, notify.RedactURL(err)
}
defer notify.Drain(resp)

shouldRetry, err := n.retrier.Check(resp.StatusCode, resp.Body)
if err != nil {
return shouldRetry, notify.NewErrorWithReason(notify.GetFailureReasonFromStatusCode(resp.StatusCode), err)
}
return shouldRetry, err
}
Loading

0 comments on commit 9bc26dd

Please sign in to comment.