...

Source file src/github.com/prometheus/alertmanager/notify/wechat/wechat.go

Documentation: github.com/prometheus/alertmanager/notify/wechat

     1  // Copyright 2019 Prometheus Team
     2  // Licensed under the Apache License, Version 2.0 (the "License");
     3  // you may not use this file except in compliance with the License.
     4  // You may obtain a copy of the License at
     5  //
     6  // http://www.apache.org/licenses/LICENSE-2.0
     7  //
     8  // Unless required by applicable law or agreed to in writing, software
     9  // distributed under the License is distributed on an "AS IS" BASIS,
    10  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    11  // See the License for the specific language governing permissions and
    12  // limitations under the License.
    13  
    14  package wechat
    15  
    16  import (
    17  	"bytes"
    18  	"context"
    19  	"encoding/json"
    20  	"fmt"
    21  	"io"
    22  	"net/http"
    23  	"net/url"
    24  	"time"
    25  
    26  	"github.com/go-kit/log"
    27  	"github.com/go-kit/log/level"
    28  	"github.com/pkg/errors"
    29  	commoncfg "github.com/prometheus/common/config"
    30  
    31  	"github.com/prometheus/alertmanager/config"
    32  	"github.com/prometheus/alertmanager/notify"
    33  	"github.com/prometheus/alertmanager/template"
    34  	"github.com/prometheus/alertmanager/types"
    35  )
    36  
    37  // Notifier implements a Notifier for wechat notifications.
    38  type Notifier struct {
    39  	conf   *config.WechatConfig
    40  	tmpl   *template.Template
    41  	logger log.Logger
    42  	client *http.Client
    43  
    44  	accessToken   string
    45  	accessTokenAt time.Time
    46  }
    47  
    48  // token is the AccessToken with corpid and corpsecret.
    49  type token struct {
    50  	AccessToken string `json:"access_token"`
    51  }
    52  
    53  type weChatMessage struct {
    54  	Text     weChatMessageContent `yaml:"text,omitempty" json:"text,omitempty"`
    55  	ToUser   string               `yaml:"touser,omitempty" json:"touser,omitempty"`
    56  	ToParty  string               `yaml:"toparty,omitempty" json:"toparty,omitempty"`
    57  	Totag    string               `yaml:"totag,omitempty" json:"totag,omitempty"`
    58  	AgentID  string               `yaml:"agentid,omitempty" json:"agentid,omitempty"`
    59  	Safe     string               `yaml:"safe,omitempty" json:"safe,omitempty"`
    60  	Type     string               `yaml:"msgtype,omitempty" json:"msgtype,omitempty"`
    61  	Markdown weChatMessageContent `yaml:"markdown,omitempty" json:"markdown,omitempty"`
    62  }
    63  
    64  type weChatMessageContent struct {
    65  	Content string `json:"content"`
    66  }
    67  
    68  type weChatResponse struct {
    69  	Code  int    `json:"code"`
    70  	Error string `json:"error"`
    71  }
    72  
    73  // New returns a new Wechat notifier.
    74  func New(c *config.WechatConfig, t *template.Template, l log.Logger, httpOpts ...commoncfg.HTTPClientOption) (*Notifier, error) {
    75  	client, err := commoncfg.NewClientFromConfig(*c.HTTPConfig, "wechat", httpOpts...)
    76  	if err != nil {
    77  		return nil, err
    78  	}
    79  
    80  	return &Notifier{conf: c, tmpl: t, logger: l, client: client}, nil
    81  }
    82  
    83  // Notify implements the Notifier interface.
    84  func (n *Notifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error) {
    85  	key, err := notify.ExtractGroupKey(ctx)
    86  	if err != nil {
    87  		return false, err
    88  	}
    89  
    90  	level.Debug(n.logger).Log("incident", key)
    91  	data := notify.GetTemplateData(ctx, n.tmpl, as, n.logger)
    92  
    93  	tmpl := notify.TmplText(n.tmpl, data, &err)
    94  	if err != nil {
    95  		return false, err
    96  	}
    97  
    98  	// Refresh AccessToken over 2 hours
    99  	if n.accessToken == "" || time.Since(n.accessTokenAt) > 2*time.Hour {
   100  		parameters := url.Values{}
   101  		parameters.Add("corpsecret", tmpl(string(n.conf.APISecret)))
   102  		parameters.Add("corpid", tmpl(string(n.conf.CorpID)))
   103  		if err != nil {
   104  			return false, fmt.Errorf("templating error: %s", err)
   105  		}
   106  
   107  		u := n.conf.APIURL.Copy()
   108  		u.Path += "gettoken"
   109  		u.RawQuery = parameters.Encode()
   110  
   111  		resp, err := notify.Get(ctx, n.client, u.String())
   112  		if err != nil {
   113  			return true, notify.RedactURL(err)
   114  		}
   115  		defer notify.Drain(resp)
   116  
   117  		var wechatToken token
   118  		if err := json.NewDecoder(resp.Body).Decode(&wechatToken); err != nil {
   119  			return false, err
   120  		}
   121  
   122  		if wechatToken.AccessToken == "" {
   123  			return false, fmt.Errorf("invalid APISecret for CorpID: %s", n.conf.CorpID)
   124  		}
   125  
   126  		// Cache accessToken
   127  		n.accessToken = wechatToken.AccessToken
   128  		n.accessTokenAt = time.Now()
   129  	}
   130  
   131  	msg := &weChatMessage{
   132  		ToUser:  tmpl(n.conf.ToUser),
   133  		ToParty: tmpl(n.conf.ToParty),
   134  		Totag:   tmpl(n.conf.ToTag),
   135  		AgentID: tmpl(n.conf.AgentID),
   136  		Type:    n.conf.MessageType,
   137  		Safe:    "0",
   138  	}
   139  
   140  	if msg.Type == "markdown" {
   141  		msg.Markdown = weChatMessageContent{
   142  			Content: tmpl(n.conf.Message),
   143  		}
   144  	} else {
   145  		msg.Text = weChatMessageContent{
   146  			Content: tmpl(n.conf.Message),
   147  		}
   148  	}
   149  	if err != nil {
   150  		return false, fmt.Errorf("templating error: %s", err)
   151  	}
   152  
   153  	var buf bytes.Buffer
   154  	if err := json.NewEncoder(&buf).Encode(msg); err != nil {
   155  		return false, err
   156  	}
   157  
   158  	postMessageURL := n.conf.APIURL.Copy()
   159  	postMessageURL.Path += "message/send"
   160  	q := postMessageURL.Query()
   161  	q.Set("access_token", n.accessToken)
   162  	postMessageURL.RawQuery = q.Encode()
   163  
   164  	resp, err := notify.PostJSON(ctx, n.client, postMessageURL.String(), &buf)
   165  	if err != nil {
   166  		return true, notify.RedactURL(err)
   167  	}
   168  	defer notify.Drain(resp)
   169  
   170  	if resp.StatusCode != 200 {
   171  		return true, fmt.Errorf("unexpected status code %v", resp.StatusCode)
   172  	}
   173  
   174  	body, err := io.ReadAll(resp.Body)
   175  	if err != nil {
   176  		return true, err
   177  	}
   178  	level.Debug(n.logger).Log("response", string(body), "incident", key)
   179  
   180  	var weResp weChatResponse
   181  	if err := json.Unmarshal(body, &weResp); err != nil {
   182  		return true, err
   183  	}
   184  
   185  	// https://work.weixin.qq.com/api/doc#10649
   186  	if weResp.Code == 0 {
   187  		return false, nil
   188  	}
   189  
   190  	// AccessToken is expired
   191  	if weResp.Code == 42001 {
   192  		n.accessToken = ""
   193  		return true, errors.New(weResp.Error)
   194  	}
   195  
   196  	return false, errors.New(weResp.Error)
   197  }
   198  

View as plain text