created docker-prometheus compose file and edited the Prometheus yml file
This commit is contained in:
325
prom/template/template.go
Normal file
325
prom/template/template.go
Normal file
@ -0,0 +1,325 @@
|
||||
// Copyright 2013 The Prometheus Authors
|
||||
// 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 template
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
html_template "html/template"
|
||||
text_template "text/template"
|
||||
|
||||
clientmodel "github.com/prometheus/client_golang/model"
|
||||
|
||||
"github.com/prometheus/prometheus/promql"
|
||||
"github.com/prometheus/prometheus/util/strutil"
|
||||
)
|
||||
|
||||
// A version of vector that's easier to use from templates.
|
||||
type sample struct {
|
||||
Labels map[string]string
|
||||
Value float64
|
||||
}
|
||||
type queryResult []*sample
|
||||
|
||||
type queryResultByLabelSorter struct {
|
||||
results queryResult
|
||||
by string
|
||||
}
|
||||
|
||||
func (q queryResultByLabelSorter) Len() int {
|
||||
return len(q.results)
|
||||
}
|
||||
|
||||
func (q queryResultByLabelSorter) Less(i, j int) bool {
|
||||
return q.results[i].Labels[q.by] < q.results[j].Labels[q.by]
|
||||
}
|
||||
|
||||
func (q queryResultByLabelSorter) Swap(i, j int) {
|
||||
q.results[i], q.results[j] = q.results[j], q.results[i]
|
||||
}
|
||||
|
||||
func query(q string, timestamp clientmodel.Timestamp, queryEngine *promql.Engine) (queryResult, error) {
|
||||
query, err := queryEngine.NewInstantQuery(q, timestamp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res := query.Exec()
|
||||
if res.Err != nil {
|
||||
return nil, res.Err
|
||||
}
|
||||
var vector promql.Vector
|
||||
|
||||
switch v := res.Value.(type) {
|
||||
case promql.Matrix:
|
||||
return nil, errors.New("matrix return values not supported")
|
||||
case promql.Vector:
|
||||
vector = v
|
||||
case *promql.Scalar:
|
||||
vector = promql.Vector{&promql.Sample{
|
||||
Value: v.Value,
|
||||
Timestamp: v.Timestamp,
|
||||
}}
|
||||
case *promql.String:
|
||||
vector = promql.Vector{&promql.Sample{
|
||||
Metric: clientmodel.COWMetric{
|
||||
Metric: clientmodel.Metric{"__value__": clientmodel.LabelValue(v.Value)},
|
||||
Copied: true,
|
||||
},
|
||||
Timestamp: v.Timestamp,
|
||||
}}
|
||||
default:
|
||||
panic("template.query: unhandled result value type")
|
||||
}
|
||||
|
||||
// promql.Vector is hard to work with in templates, so convert to
|
||||
// base data types.
|
||||
var result = make(queryResult, len(vector))
|
||||
for n, v := range vector {
|
||||
s := sample{
|
||||
Value: float64(v.Value),
|
||||
Labels: make(map[string]string),
|
||||
}
|
||||
for label, value := range v.Metric.Metric {
|
||||
s.Labels[string(label)] = string(value)
|
||||
}
|
||||
result[n] = &s
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
type templateExpander struct {
|
||||
text string
|
||||
name string
|
||||
data interface{}
|
||||
funcMap text_template.FuncMap
|
||||
}
|
||||
|
||||
// NewTemplateExpander returns a template expander ready to use.
|
||||
func NewTemplateExpander(text string, name string, data interface{}, timestamp clientmodel.Timestamp, queryEngine *promql.Engine, pathPrefix string) *templateExpander {
|
||||
return &templateExpander{
|
||||
text: text,
|
||||
name: name,
|
||||
data: data,
|
||||
funcMap: text_template.FuncMap{
|
||||
"query": func(q string) (queryResult, error) {
|
||||
return query(q, timestamp, queryEngine)
|
||||
},
|
||||
"first": func(v queryResult) (*sample, error) {
|
||||
if len(v) > 0 {
|
||||
return v[0], nil
|
||||
}
|
||||
return nil, errors.New("first() called on vector with no elements")
|
||||
},
|
||||
"label": func(label string, s *sample) string {
|
||||
return s.Labels[label]
|
||||
},
|
||||
"value": func(s *sample) float64 {
|
||||
return s.Value
|
||||
},
|
||||
"strvalue": func(s *sample) string {
|
||||
return s.Labels["__value__"]
|
||||
},
|
||||
"args": func(args ...interface{}) map[string]interface{} {
|
||||
result := make(map[string]interface{})
|
||||
for i, a := range args {
|
||||
result[fmt.Sprintf("arg%d", i)] = a
|
||||
}
|
||||
return result
|
||||
},
|
||||
"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": strings.Title,
|
||||
"graphLink": strutil.GraphLinkForExpression,
|
||||
"tableLink": strutil.TableLinkForExpression,
|
||||
"sortByLabel": func(label string, v queryResult) queryResult {
|
||||
sorter := queryResultByLabelSorter{v[:], label}
|
||||
sort.Stable(sorter)
|
||||
return v
|
||||
},
|
||||
"humanize": func(v float64) string {
|
||||
if v == 0 || math.IsNaN(v) || math.IsInf(v, 0) {
|
||||
return fmt.Sprintf("%.4g", v)
|
||||
}
|
||||
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)
|
||||
}
|
||||
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)
|
||||
},
|
||||
"humanize1024": func(v float64) string {
|
||||
if math.Abs(v) <= 1 || math.IsNaN(v) || math.IsInf(v, 0) {
|
||||
return fmt.Sprintf("%.4g", v)
|
||||
}
|
||||
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)
|
||||
},
|
||||
"humanizeDuration": func(v float64) string {
|
||||
if math.IsNaN(v) || math.IsInf(v, 0) {
|
||||
return fmt.Sprintf("%.4g", v)
|
||||
}
|
||||
if v == 0 {
|
||||
return fmt.Sprintf("%.4gs", v)
|
||||
}
|
||||
if math.Abs(v) >= 1 {
|
||||
sign := ""
|
||||
if v < 0 {
|
||||
sign = "-"
|
||||
v = -v
|
||||
}
|
||||
seconds := int64(v) % 60
|
||||
minutes := (int64(v) / 60) % 60
|
||||
hours := (int64(v) / 60 / 60) % 24
|
||||
days := (int64(v) / 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)
|
||||
}
|
||||
if hours != 0 {
|
||||
return fmt.Sprintf("%s%dh %dm %ds", sign, hours, minutes, seconds)
|
||||
}
|
||||
if minutes != 0 {
|
||||
return fmt.Sprintf("%s%dm %ds", sign, minutes, seconds)
|
||||
}
|
||||
// For seconds, we display 4 significant digts.
|
||||
return fmt.Sprintf("%s%.4gs", sign, v)
|
||||
}
|
||||
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)
|
||||
},
|
||||
"humanizeTimestamp": func(v float64) string {
|
||||
if math.IsNaN(v) || math.IsInf(v, 0) {
|
||||
return fmt.Sprintf("%.4g", v)
|
||||
}
|
||||
t := clientmodel.TimestampFromUnixNano(int64(v * 1e9)).Time().UTC()
|
||||
return fmt.Sprint(t)
|
||||
},
|
||||
"pathPrefix": func() string {
|
||||
return pathPrefix
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Funcs adds the functions in fm to the templateExpander's function map.
|
||||
// Existing functions will be overwritten in case of conflict.
|
||||
func (te templateExpander) Funcs(fm text_template.FuncMap) {
|
||||
for k, v := range fm {
|
||||
te.funcMap[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
// Expand a template.
|
||||
func (te templateExpander) Expand() (result string, resultErr error) {
|
||||
// It'd better to have no alert description than to kill the whole process
|
||||
// if there's a bug in the template.
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
var ok bool
|
||||
resultErr, ok = r.(error)
|
||||
if !ok {
|
||||
resultErr = fmt.Errorf("panic expanding template %v: %v", te.name, r)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
var buffer bytes.Buffer
|
||||
tmpl, err := text_template.New(te.name).Funcs(te.funcMap).Parse(te.text)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error parsing template %v: %v", te.name, err)
|
||||
}
|
||||
err = tmpl.Execute(&buffer, te.data)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error executing template %v: %v", te.name, err)
|
||||
}
|
||||
return buffer.String(), nil
|
||||
}
|
||||
|
||||
// Expand a template with HTML escaping, with templates read from the given files.
|
||||
func (te templateExpander) ExpandHTML(templateFiles []string) (result string, resultErr error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
var ok bool
|
||||
resultErr, ok = r.(error)
|
||||
if !ok {
|
||||
resultErr = fmt.Errorf("panic expanding template %v: %v", te.name, r)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
var buffer bytes.Buffer
|
||||
tmpl := html_template.New(te.name).Funcs(html_template.FuncMap(te.funcMap))
|
||||
tmpl.Funcs(html_template.FuncMap{
|
||||
"tmpl": func(name string, data interface{}) (html_template.HTML, error) {
|
||||
var buffer bytes.Buffer
|
||||
err := tmpl.ExecuteTemplate(&buffer, name, data)
|
||||
return html_template.HTML(buffer.String()), err
|
||||
},
|
||||
})
|
||||
tmpl, err := tmpl.Parse(te.text)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error parsing template %v: %v", te.name, err)
|
||||
}
|
||||
if len(templateFiles) > 0 {
|
||||
_, err = tmpl.ParseFiles(templateFiles...)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error parsing template files for %v: %v", te.name, err)
|
||||
}
|
||||
}
|
||||
err = tmpl.Execute(&buffer, te.data)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error executing template %v: %v", te.name, err)
|
||||
}
|
||||
return buffer.String(), nil
|
||||
}
|
219
prom/template/template_test.go
Normal file
219
prom/template/template_test.go
Normal file
@ -0,0 +1,219 @@
|
||||
// Copyright 2014 The Prometheus Authors
|
||||
// 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 template
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
|
||||
clientmodel "github.com/prometheus/client_golang/model"
|
||||
|
||||
"github.com/prometheus/prometheus/promql"
|
||||
"github.com/prometheus/prometheus/storage/local"
|
||||
)
|
||||
|
||||
type testTemplatesScenario struct {
|
||||
text string
|
||||
output string
|
||||
input interface{}
|
||||
shouldFail bool
|
||||
html bool
|
||||
}
|
||||
|
||||
func TestTemplateExpansion(t *testing.T) {
|
||||
scenarios := []testTemplatesScenario{
|
||||
{
|
||||
// No template.
|
||||
text: "plain text",
|
||||
output: "plain text",
|
||||
},
|
||||
{
|
||||
// Simple value.
|
||||
text: "{{ 1 }}",
|
||||
output: "1",
|
||||
},
|
||||
{
|
||||
// HTML escaping.
|
||||
text: "{{ \"<b>\" }}",
|
||||
output: "<b>",
|
||||
html: true,
|
||||
},
|
||||
{
|
||||
// Disabling HTML escaping.
|
||||
text: "{{ \"<b>\" | safeHtml }}",
|
||||
output: "<b>",
|
||||
html: true,
|
||||
},
|
||||
{
|
||||
// HTML escaping doesn't apply to non-html.
|
||||
text: "{{ \"<b>\" }}",
|
||||
output: "<b>",
|
||||
},
|
||||
{
|
||||
// Pass multiple arguments to templates.
|
||||
text: "{{define \"x\"}}{{.arg0}} {{.arg1}}{{end}}{{template \"x\" (args 1 \"2\")}}",
|
||||
output: "1 2",
|
||||
},
|
||||
{
|
||||
text: "{{ query \"1.5\" | first | value }}",
|
||||
output: "1.5",
|
||||
},
|
||||
{
|
||||
// Get value from scalar query.
|
||||
text: "{{ query \"scalar(count(metric))\" | first | value }}",
|
||||
output: "2",
|
||||
},
|
||||
{
|
||||
// Get value from query.
|
||||
text: "{{ query \"metric{instance='a'}\" | first | value }}",
|
||||
output: "11",
|
||||
},
|
||||
{
|
||||
// Get label from query.
|
||||
text: "{{ query \"metric{instance='a'}\" | first | label \"instance\" }}",
|
||||
output: "a",
|
||||
},
|
||||
{
|
||||
// Range over query and sort by label.
|
||||
text: "{{ range query \"metric\" | sortByLabel \"instance\" }}{{.Labels.instance}}:{{.Value}}: {{end}}",
|
||||
output: "a:11: b:21: ",
|
||||
},
|
||||
{
|
||||
// Unparsable template.
|
||||
text: "{{",
|
||||
shouldFail: true,
|
||||
},
|
||||
{
|
||||
// Error in function.
|
||||
text: "{{ query \"missing\" | first }}",
|
||||
shouldFail: true,
|
||||
},
|
||||
{
|
||||
// Panic.
|
||||
text: "{{ (query \"missing\").banana }}",
|
||||
shouldFail: true,
|
||||
},
|
||||
{
|
||||
// Regex replacement.
|
||||
text: "{{ reReplaceAll \"(a)b\" \"x$1\" \"ab\" }}",
|
||||
output: "xa",
|
||||
},
|
||||
{
|
||||
// Humanize.
|
||||
text: "{{ range . }}{{ humanize . }}:{{ end }}",
|
||||
input: []float64{0.0, 1.0, 1234567.0, .12},
|
||||
output: "0:1:1.235M:120m:",
|
||||
},
|
||||
{
|
||||
// Humanize1024.
|
||||
text: "{{ range . }}{{ humanize1024 . }}:{{ end }}",
|
||||
input: []float64{0.0, 1.0, 1048576.0, .12},
|
||||
output: "0:1:1Mi:0.12:",
|
||||
},
|
||||
{
|
||||
// HumanizeDuration - seconds.
|
||||
text: "{{ range . }}{{ humanizeDuration . }}:{{ end }}",
|
||||
input: []float64{0, 1, 60, 3600, 86400, 86400 + 3600, -(86400*2 + 3600*3 + 60*4 + 5), 899.99},
|
||||
output: "0s:1s:1m 0s:1h 0m 0s:1d 0h 0m 0s:1d 1h 0m 0s:-2d 3h 4m 5s:14m 59s:",
|
||||
},
|
||||
{
|
||||
// HumanizeDuration - subsecond and fractional seconds.
|
||||
text: "{{ range . }}{{ humanizeDuration . }}:{{ end }}",
|
||||
input: []float64{.1, .0001, .12345, 60.1, 60.5, 1.2345, 12.345},
|
||||
output: "100ms:100us:123.5ms:1m 0s:1m 0s:1.234s:12.35s:",
|
||||
},
|
||||
{
|
||||
// Humanize* Inf and NaN.
|
||||
text: "{{ range . }}{{ humanize . }}:{{ humanize1024 . }}:{{ humanizeDuration . }}:{{humanizeTimestamp .}}:{{ end }}",
|
||||
input: []float64{math.Inf(1), math.Inf(-1), math.NaN()},
|
||||
output: "+Inf:+Inf:+Inf:+Inf:-Inf:-Inf:-Inf:-Inf:NaN:NaN:NaN:NaN:",
|
||||
},
|
||||
{
|
||||
// HumanizeTimestamp - clientmodel.SampleValue input.
|
||||
text: "{{ 1435065584.128 | humanizeTimestamp }}",
|
||||
output: "2015-06-23 13:19:44.128 +0000 UTC",
|
||||
},
|
||||
{
|
||||
// Title.
|
||||
text: "{{ \"aa bb CC\" | title }}",
|
||||
output: "Aa Bb CC",
|
||||
},
|
||||
{
|
||||
// Match.
|
||||
text: "{{ match \"a+\" \"aa\" }} {{ match \"a+\" \"b\" }}",
|
||||
output: "true false",
|
||||
},
|
||||
{
|
||||
// graphLink.
|
||||
text: "{{ graphLink \"up\" }}",
|
||||
output: "/graph#%5B%7B%22expr%22%3A%22up%22%2C%22tab%22%3A0%7D%5D",
|
||||
},
|
||||
{
|
||||
// tableLink.
|
||||
text: "{{ tableLink \"up\" }}",
|
||||
output: "/graph#%5B%7B%22expr%22%3A%22up%22%2C%22tab%22%3A1%7D%5D",
|
||||
},
|
||||
{
|
||||
// tmpl.
|
||||
text: "{{ define \"a\" }}x{{ end }}{{ $name := \"a\"}}{{ tmpl $name . }}",
|
||||
output: "x",
|
||||
html: true,
|
||||
},
|
||||
}
|
||||
|
||||
time := clientmodel.Timestamp(0)
|
||||
|
||||
storage, closer := local.NewTestStorage(t, 1)
|
||||
defer closer.Close()
|
||||
storage.Append(&clientmodel.Sample{
|
||||
Metric: clientmodel.Metric{
|
||||
clientmodel.MetricNameLabel: "metric",
|
||||
"instance": "a"},
|
||||
Value: 11,
|
||||
})
|
||||
storage.Append(&clientmodel.Sample{
|
||||
Metric: clientmodel.Metric{
|
||||
clientmodel.MetricNameLabel: "metric",
|
||||
"instance": "b"},
|
||||
Value: 21,
|
||||
})
|
||||
storage.WaitForIndexing()
|
||||
|
||||
engine := promql.NewEngine(storage, nil)
|
||||
|
||||
for i, s := range scenarios {
|
||||
var result string
|
||||
var err error
|
||||
expander := NewTemplateExpander(s.text, "test", s.input, time, engine, "")
|
||||
if s.html {
|
||||
result, err = expander.ExpandHTML(nil)
|
||||
} else {
|
||||
result, err = expander.Expand()
|
||||
}
|
||||
if s.shouldFail {
|
||||
if err == nil {
|
||||
t.Fatalf("%d. Error not returned from %v", i, s.text)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("%d. Error returned from %v: %v", i, s.text, err)
|
||||
continue
|
||||
}
|
||||
if result != s.output {
|
||||
t.Fatalf("%d. Error in result from %v: Expected '%v' Got '%v'", i, s.text, s.output, result)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user