...

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

Documentation: github.com/prometheus/alertmanager/notify

     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 notify
    15  
    16  import (
    17  	"context"
    18  	"crypto/sha256"
    19  	"fmt"
    20  	"io"
    21  	"net/http"
    22  	"net/url"
    23  	"strings"
    24  
    25  	"github.com/go-kit/log"
    26  	"github.com/go-kit/log/level"
    27  	"github.com/pkg/errors"
    28  	"github.com/prometheus/common/version"
    29  
    30  	"github.com/prometheus/alertmanager/template"
    31  	"github.com/prometheus/alertmanager/types"
    32  )
    33  
    34  // truncationMarker is the character used to represent a truncation.
    35  const truncationMarker = "…"
    36  
    37  // UserAgentHeader is the default User-Agent for notification requests
    38  var UserAgentHeader = fmt.Sprintf("Alertmanager/%s", version.Version)
    39  
    40  // RedactURL removes the URL part from an error of *url.Error type.
    41  func RedactURL(err error) error {
    42  	e, ok := err.(*url.Error)
    43  	if !ok {
    44  		return err
    45  	}
    46  	e.URL = "<redacted>"
    47  	return e
    48  }
    49  
    50  // Get sends a GET request to the given URL
    51  func Get(ctx context.Context, client *http.Client, url string) (*http.Response, error) {
    52  	return request(ctx, client, http.MethodGet, url, "", nil)
    53  }
    54  
    55  // PostJSON sends a POST request with JSON payload to the given URL.
    56  func PostJSON(ctx context.Context, client *http.Client, url string, body io.Reader) (*http.Response, error) {
    57  	return post(ctx, client, url, "application/json", body)
    58  }
    59  
    60  // PostText sends a POST request with text payload to the given URL.
    61  func PostText(ctx context.Context, client *http.Client, url string, body io.Reader) (*http.Response, error) {
    62  	return post(ctx, client, url, "text/plain", body)
    63  }
    64  
    65  func post(ctx context.Context, client *http.Client, url, bodyType string, body io.Reader) (*http.Response, error) {
    66  	return request(ctx, client, http.MethodPost, url, bodyType, body)
    67  }
    68  
    69  func request(ctx context.Context, client *http.Client, method, url, bodyType string, body io.Reader) (*http.Response, error) {
    70  	req, err := http.NewRequest(method, url, body)
    71  	if err != nil {
    72  		return nil, err
    73  	}
    74  	req.Header.Set("User-Agent", UserAgentHeader)
    75  	if bodyType != "" {
    76  		req.Header.Set("Content-Type", bodyType)
    77  	}
    78  	return client.Do(req.WithContext(ctx))
    79  }
    80  
    81  // Drain consumes and closes the response's body to make sure that the
    82  // HTTP client can reuse existing connections.
    83  func Drain(r *http.Response) {
    84  	io.Copy(io.Discard, r.Body)
    85  	r.Body.Close()
    86  }
    87  
    88  // TruncateInRunes truncates a string to fit the given size in Runes.
    89  func TruncateInRunes(s string, n int) (string, bool) {
    90  	r := []rune(s)
    91  	if len(r) <= n {
    92  		return s, false
    93  	}
    94  
    95  	if n <= 3 {
    96  		return string(r[:n]), true
    97  	}
    98  
    99  	return string(r[:n-1]) + truncationMarker, true
   100  }
   101  
   102  // TruncateInBytes truncates a string to fit the given size in Bytes.
   103  func TruncateInBytes(s string, n int) (string, bool) {
   104  	// First, measure the string the w/o a to-rune conversion.
   105  	if len(s) <= n {
   106  		return s, false
   107  	}
   108  
   109  	// The truncationMarker itself is 3 bytes, we can't return any part of the string when it's less than 3.
   110  	if n <= 3 {
   111  		switch n {
   112  		case 3:
   113  			return truncationMarker, true
   114  		default:
   115  			return strings.Repeat(".", n), true
   116  		}
   117  	}
   118  
   119  	// Now, to ensure we don't butcher the string we need to remove using runes.
   120  	r := []rune(s)
   121  	truncationTarget := n - 3
   122  
   123  	// Next, let's truncate the runes to the lower possible number.
   124  	truncatedRunes := r[:truncationTarget]
   125  	for len(string(truncatedRunes)) > truncationTarget {
   126  		truncatedRunes = r[:len(truncatedRunes)-1]
   127  	}
   128  
   129  	return string(truncatedRunes) + truncationMarker, true
   130  }
   131  
   132  // TmplText is using monadic error handling in order to make string templating
   133  // less verbose. Use with care as the final error checking is easily missed.
   134  func TmplText(tmpl *template.Template, data *template.Data, err *error) func(string) string {
   135  	return func(name string) (s string) {
   136  		if *err != nil {
   137  			return
   138  		}
   139  		s, *err = tmpl.ExecuteTextString(name, data)
   140  		return s
   141  	}
   142  }
   143  
   144  // TmplHTML is using monadic error handling in order to make string templating
   145  // less verbose. Use with care as the final error checking is easily missed.
   146  func TmplHTML(tmpl *template.Template, data *template.Data, err *error) func(string) string {
   147  	return func(name string) (s string) {
   148  		if *err != nil {
   149  			return
   150  		}
   151  		s, *err = tmpl.ExecuteHTMLString(name, data)
   152  		return s
   153  	}
   154  }
   155  
   156  // Key is a string that can be hashed.
   157  type Key string
   158  
   159  // ExtractGroupKey gets the group key from the context.
   160  func ExtractGroupKey(ctx context.Context) (Key, error) {
   161  	key, ok := GroupKey(ctx)
   162  	if !ok {
   163  		return "", errors.Errorf("group key missing")
   164  	}
   165  	return Key(key), nil
   166  }
   167  
   168  // Hash returns the sha256 for a group key as integrations may have
   169  // maximum length requirements on deduplication keys.
   170  func (k Key) Hash() string {
   171  	h := sha256.New()
   172  	// hash.Hash.Write never returns an error.
   173  	//nolint: errcheck
   174  	h.Write([]byte(string(k)))
   175  	return fmt.Sprintf("%x", h.Sum(nil))
   176  }
   177  
   178  func (k Key) String() string {
   179  	return string(k)
   180  }
   181  
   182  // GetTemplateData creates the template data from the context and the alerts.
   183  func GetTemplateData(ctx context.Context, tmpl *template.Template, alerts []*types.Alert, l log.Logger) *template.Data {
   184  	recv, ok := ReceiverName(ctx)
   185  	if !ok {
   186  		level.Error(l).Log("msg", "Missing receiver")
   187  	}
   188  	groupLabels, ok := GroupLabels(ctx)
   189  	if !ok {
   190  		level.Error(l).Log("msg", "Missing group labels")
   191  	}
   192  	return tmpl.Data(recv, groupLabels, alerts...)
   193  }
   194  
   195  func readAll(r io.Reader) string {
   196  	if r == nil {
   197  		return ""
   198  	}
   199  	bs, err := io.ReadAll(r)
   200  	if err != nil {
   201  		return ""
   202  	}
   203  	return string(bs)
   204  }
   205  
   206  // Retrier knows when to retry an HTTP request to a receiver. 2xx status codes
   207  // are successful, anything else is a failure and only 5xx status codes should
   208  // be retried.
   209  type Retrier struct {
   210  	// Function to return additional information in the error message.
   211  	CustomDetailsFunc func(code int, body io.Reader) string
   212  	// Additional HTTP status codes that should be retried.
   213  	RetryCodes []int
   214  }
   215  
   216  // Check returns a boolean indicating whether the request should be retried
   217  // and an optional error if the request has failed. If body is not nil, it will
   218  // be included in the error message.
   219  func (r *Retrier) Check(statusCode int, body io.Reader) (bool, error) {
   220  	// 2xx responses are considered to be always successful.
   221  	if statusCode/100 == 2 {
   222  		return false, nil
   223  	}
   224  
   225  	// 5xx responses are considered to be always retried.
   226  	retry := statusCode/100 == 5
   227  	if !retry {
   228  		for _, code := range r.RetryCodes {
   229  			if code == statusCode {
   230  				retry = true
   231  				break
   232  			}
   233  		}
   234  	}
   235  
   236  	s := fmt.Sprintf("unexpected status code %v", statusCode)
   237  	var details string
   238  	if r.CustomDetailsFunc != nil {
   239  		details = r.CustomDetailsFunc(statusCode, body)
   240  	} else {
   241  		details = readAll(body)
   242  	}
   243  	if details != "" {
   244  		s = fmt.Sprintf("%s: %s", s, details)
   245  	}
   246  	return retry, errors.New(s)
   247  }
   248  

View as plain text