Skip to content

Commit

Permalink
Fix support for prometheus functions in initial template rendering
Browse files Browse the repository at this point in the history
  • Loading branch information
DRuggeri committed Mar 21, 2024
1 parent aeeaa5d commit 697a1ec
Show file tree
Hide file tree
Showing 4 changed files with 240 additions and 24 deletions.
26 changes: 4 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,27 +124,9 @@ All providers are back up
Now if the alert fires it would list the jobs that are down. Which information the `.Values` method contains can be inspected in the Grafana alertmanager when configuring an alert and clicking the `Preview Alert` button.

### Template Functions
The bridges Go templating supports several template functions. All template functions listed in the [Grafana template functions](https://grafana.com/docs/grafana/latest/alerting/fundamentals/annotation-label/template-functions/) are supported with the bridge, with usage examples.

NOTE: The externalURL function will only return a result when the message is sent from Grafana. Messages initiated through alertmanager will not contain an externalURL.

The bridge uses Prometheous's [template functions](https://prometheus.io/docs/prometheus/latest/configuration/template_reference/). Some of the template functions in [template.go](https://github.com/prometheus/prometheus/blob/main/template/template.go) are not supported in the bridge because of limitations. The chart below lists the additional functions not found in the [Grafana template functions](https://grafana.com/docs/grafana/latest/alerting/fundamentals/annotation-label/template-functions/) documentation, but can be called through the bridge.

| Function Name | Supported |
| --------------- |:---------:|
| query | no |
| first | no |
| label | no |
| value | no |
| strvalue | no |
| safeHtml | yes |
| sortByLabel | no |
| stripPort | yes |
| stripDomain | yes |
| toTime | yes |
| parseDuration | yes |

Grafana Example:
The bridge uses a subset of Prometheus's [template functions](https://prometheus.io/docs/prometheus/latest/configuration/template_reference/). Some of the template functions are not supported in the bridge. The file [prometheus_template_functions.go](prometheus_template_functions.go) contains the list of functions and how they are implemented in the bridge.

Example:
```go
{{ reReplaceAll ".+\\|" " " .Labels.log }}
```
Expand Down Expand Up @@ -183,7 +165,7 @@ curl http://127.0.0.1:8080/gotify_webhook -d '
'
```
### Bridge Message Templating
The bridge now supports user-defined templating for all inbound messages. The user-defined templating can be used for the title and/or message. Also, user-defined templating overrides the default title and message annotations. All keys and values in the JSON from alertmanager can be used in the user-defined template. Any failures in the templates will result in the bridge defaulting back to default alerting.
The bridge supports user-defined templating for all inbound messages. The user-defined templating can be used for the title and/or message. Also, user-defined templating overrides the default title and message annotations. All keys and values in the JSON from alertmanager can be used in the user-defined template. Any failures in the templates will result in the bridge falling back to default alerting.

#### Usage Notes:
- For Docker, you must bind your volume to your host to add user-defined templating.
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ require (
github.com/prometheus/client_golang v1.14.0
github.com/prometheus/common v0.39.0
github.com/prometheus/prometheus v0.42.0
golang.org/x/text v0.6.0
gopkg.in/alecthomas/kingpin.v2 v2.2.6
)
8 changes: 6 additions & 2 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"log"
"math"
"net/http"
Expand Down Expand Up @@ -240,7 +239,7 @@ func (svr *bridge) handleCall(w http.ResponseWriter, r *http.Request) {
}

/* Assume this will never fail */
b, _ := ioutil.ReadAll(r.Body)
b, _ := io.ReadAll(r.Body)

if *svr.debug {
log.Printf("bridge: Recieved request: %+v\n", r)
Expand Down Expand Up @@ -615,6 +614,11 @@ func parseUserTemplates(tmplPath string) (*ut.Template, error) {
"all templates with the file extension (.%s) will not function until the error is corrected", err, p)
}
}

if tmpl != nil {
tmpl.Funcs(fxns)
}

return tmpl, nil
}

Expand Down
229 changes: 229 additions & 0 deletions prometheus_template_functions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
// Incorporated from https://github.com/prometheus/prometheus/blob/v0.42.0/template/template.go#L49
// Please see URL for license

package main

import (
"errors"
"fmt"
"math"
"net"
"regexp"
"strconv"
"strings"
"time"

html_template "html/template"
text_template "text/template"

"github.com/prometheus/common/model"
"github.com/prometheus/prometheus/util/strutil"
"golang.org/x/text/cases"
)

var errNaNOrInf = errors.New("value is NaN or Inf")

var fxns = text_template.FuncMap{
"first": func(v []interface{}) (interface{}, error) {
if len(v) > 0 {
return v[0], nil
}
return nil, errors.New("first() called on interface with no elements")
},
"reReplaceAll": func(pattern, repl, text string) string {
re := regexp.MustCompile(pattern)
return re.ReplaceAllString(text, repl)
},
"safeHtml": func(text string) html_template.HTML {
return html_template.HTML(text)
},
"match": regexp.MatchString,
"title": cases.Title, // nolint:staticcheck
"toUpper": strings.ToUpper,
"toLower": strings.ToLower,
"graphLink": strutil.GraphLinkForExpression,
"tableLink": strutil.TableLinkForExpression,
"stripPort": func(hostPort string) string {
host, _, err := net.SplitHostPort(hostPort)
if err != nil {
return hostPort
}
return host
},
"stripDomain": func(hostPort string) string {
host, port, err := net.SplitHostPort(hostPort)
if err != nil {
host = hostPort
}
ip := net.ParseIP(host)
if ip != nil {
return hostPort
}
host = strings.Split(host, ".")[0]
if port != "" {
return net.JoinHostPort(host, port)
}
return host
},
"humanize": func(i interface{}) (string, error) {
v, err := convertToFloat(i)
if err != nil {
return "", err
}
if v == 0 || math.IsNaN(v) || math.IsInf(v, 0) {
return fmt.Sprintf("%.4g", v), nil
}
if math.Abs(v) >= 1 {
prefix := ""
for _, p := range []string{"k", "M", "G", "T", "P", "E", "Z", "Y"} {
if math.Abs(v) < 1000 {
break
}
prefix = p
v /= 1000
}
return fmt.Sprintf("%.4g%s", v, prefix), nil
}
prefix := ""
for _, p := range []string{"m", "u", "n", "p", "f", "a", "z", "y"} {
if math.Abs(v) >= 1 {
break
}
prefix = p
v *= 1000
}
return fmt.Sprintf("%.4g%s", v, prefix), nil
},
"humanize1024": func(i interface{}) (string, error) {
v, err := convertToFloat(i)
if err != nil {
return "", err
}
if math.Abs(v) <= 1 || math.IsNaN(v) || math.IsInf(v, 0) {
return fmt.Sprintf("%.4g", v), nil
}
prefix := ""
for _, p := range []string{"ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi", "Yi"} {
if math.Abs(v) < 1024 {
break
}
prefix = p
v /= 1024
}
return fmt.Sprintf("%.4g%s", v, prefix), nil
},
"humanizeDuration": func(i interface{}) (string, error) {
v, err := convertToFloat(i)
if err != nil {
return "", err
}
if math.IsNaN(v) || math.IsInf(v, 0) {
return fmt.Sprintf("%.4g", v), nil
}
if v == 0 {
return fmt.Sprintf("%.4gs", v), nil
}
if math.Abs(v) >= 1 {
sign := ""
if v < 0 {
sign = "-"
v = -v
}
duration := int64(v)
seconds := duration % 60
minutes := (duration / 60) % 60
hours := (duration / 60 / 60) % 24
days := duration / 60 / 60 / 24
// For days to minutes, we display seconds as an integer.
if days != 0 {
return fmt.Sprintf("%s%dd %dh %dm %ds", sign, days, hours, minutes, seconds), nil
}
if hours != 0 {
return fmt.Sprintf("%s%dh %dm %ds", sign, hours, minutes, seconds), nil
}
if minutes != 0 {
return fmt.Sprintf("%s%dm %ds", sign, minutes, seconds), nil
}
// For seconds, we display 4 significant digits.
return fmt.Sprintf("%s%.4gs", sign, v), nil
}
prefix := ""
for _, p := range []string{"m", "u", "n", "p", "f", "a", "z", "y"} {
if math.Abs(v) >= 1 {
break
}
prefix = p
v *= 1000
}
return fmt.Sprintf("%.4g%ss", v, prefix), nil
},
"humanizePercentage": func(i interface{}) (string, error) {
v, err := convertToFloat(i)
if err != nil {
return "", err
}
return fmt.Sprintf("%.4g%%", v*100), nil
},
"humanizeTimestamp": func(i interface{}) (string, error) {
v, err := convertToFloat(i)
if err != nil {
return "", err
}

tm, err := floatToTime(v)
switch {
case errors.Is(err, errNaNOrInf):
return fmt.Sprintf("%.4g", v), nil
case err != nil:
return "", err
}

return fmt.Sprint(tm), nil
},
"toTime": func(i interface{}) (*time.Time, error) {
v, err := convertToFloat(i)
if err != nil {
return nil, err
}

return floatToTime(v)
},
"parseDuration": func(d string) (float64, error) {
v, err := model.ParseDuration(d)
if err != nil {
return 0, err
}
return float64(time.Duration(v)) / float64(time.Second), nil
},
}

func convertToFloat(i interface{}) (float64, error) {
switch v := i.(type) {
case float64:
return v, nil
case string:
return strconv.ParseFloat(v, 64)
case int:
return float64(v), nil
case uint:
return float64(v), nil
case int64:
return float64(v), nil
case uint64:
return float64(v), nil
default:
return 0, fmt.Errorf("can't convert %T to float", v)
}
}

func floatToTime(v float64) (*time.Time, error) {
if math.IsNaN(v) || math.IsInf(v, 0) {
return nil, errNaNOrInf
}
timestamp := v * 1e9
if timestamp > math.MaxInt64 || timestamp < math.MinInt64 {
return nil, fmt.Errorf("%v cannot be represented as a nanoseconds timestamp since it overflows int64", v)
}
t := model.TimeFromUnixNano(int64(timestamp)).Time().UTC()
return &t, nil
}

0 comments on commit 697a1ec

Please sign in to comment.