1// Copyright 2013 The Prometheus Authors 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 14package template 15 16import ( 17 "bytes" 18 "context" 19 "fmt" 20 html_template "html/template" 21 "math" 22 "net/url" 23 "regexp" 24 "sort" 25 "strconv" 26 "strings" 27 text_template "text/template" 28 "time" 29 30 "github.com/pkg/errors" 31 "github.com/prometheus/client_golang/prometheus" 32 "github.com/prometheus/common/model" 33 34 "github.com/prometheus/prometheus/promql" 35 "github.com/prometheus/prometheus/util/strutil" 36) 37 38var ( 39 templateTextExpansionFailures = prometheus.NewCounter(prometheus.CounterOpts{ 40 Name: "prometheus_template_text_expansion_failures_total", 41 Help: "The total number of template text expansion failures.", 42 }) 43 templateTextExpansionTotal = prometheus.NewCounter(prometheus.CounterOpts{ 44 Name: "prometheus_template_text_expansions_total", 45 Help: "The total number of template text expansions.", 46 }) 47) 48 49func init() { 50 prometheus.MustRegister(templateTextExpansionFailures) 51 prometheus.MustRegister(templateTextExpansionTotal) 52} 53 54// A version of vector that's easier to use from templates. 55type sample struct { 56 Labels map[string]string 57 Value float64 58} 59type queryResult []*sample 60 61type queryResultByLabelSorter struct { 62 results queryResult 63 by string 64} 65 66func (q queryResultByLabelSorter) Len() int { 67 return len(q.results) 68} 69 70func (q queryResultByLabelSorter) Less(i, j int) bool { 71 return q.results[i].Labels[q.by] < q.results[j].Labels[q.by] 72} 73 74func (q queryResultByLabelSorter) Swap(i, j int) { 75 q.results[i], q.results[j] = q.results[j], q.results[i] 76} 77 78// QueryFunc executes a PromQL query at the given time. 79type QueryFunc func(context.Context, string, time.Time) (promql.Vector, error) 80 81func query(ctx context.Context, q string, ts time.Time, queryFn QueryFunc) (queryResult, error) { 82 vector, err := queryFn(ctx, q, ts) 83 if err != nil { 84 return nil, err 85 } 86 87 // promql.Vector is hard to work with in templates, so convert to 88 // base data types. 89 // TODO(fabxc): probably not true anymore after type rework. 90 var result = make(queryResult, len(vector)) 91 for n, v := range vector { 92 s := sample{ 93 Value: v.V, 94 Labels: v.Metric.Map(), 95 } 96 result[n] = &s 97 } 98 return result, nil 99} 100 101func convertToFloat(i interface{}) (float64, error) { 102 switch v := i.(type) { 103 case float64: 104 return v, nil 105 case string: 106 return strconv.ParseFloat(v, 64) 107 default: 108 return 0, fmt.Errorf("can't convert %T to float", v) 109 } 110} 111 112// Expander executes templates in text or HTML mode with a common set of Prometheus template functions. 113type Expander struct { 114 text string 115 name string 116 data interface{} 117 funcMap text_template.FuncMap 118} 119 120// NewTemplateExpander returns a template expander ready to use. 121func NewTemplateExpander( 122 ctx context.Context, 123 text string, 124 name string, 125 data interface{}, 126 timestamp model.Time, 127 queryFunc QueryFunc, 128 externalURL *url.URL, 129) *Expander { 130 return &Expander{ 131 text: text, 132 name: name, 133 data: data, 134 funcMap: text_template.FuncMap{ 135 "query": func(q string) (queryResult, error) { 136 return query(ctx, q, timestamp.Time(), queryFunc) 137 }, 138 "first": func(v queryResult) (*sample, error) { 139 if len(v) > 0 { 140 return v[0], nil 141 } 142 return nil, errors.New("first() called on vector with no elements") 143 }, 144 "label": func(label string, s *sample) string { 145 return s.Labels[label] 146 }, 147 "value": func(s *sample) float64 { 148 return s.Value 149 }, 150 "strvalue": func(s *sample) string { 151 return s.Labels["__value__"] 152 }, 153 "args": func(args ...interface{}) map[string]interface{} { 154 result := make(map[string]interface{}) 155 for i, a := range args { 156 result[fmt.Sprintf("arg%d", i)] = a 157 } 158 return result 159 }, 160 "reReplaceAll": func(pattern, repl, text string) string { 161 re := regexp.MustCompile(pattern) 162 return re.ReplaceAllString(text, repl) 163 }, 164 "safeHtml": func(text string) html_template.HTML { 165 return html_template.HTML(text) 166 }, 167 "match": regexp.MatchString, 168 "title": strings.Title, 169 "toUpper": strings.ToUpper, 170 "toLower": strings.ToLower, 171 "graphLink": strutil.GraphLinkForExpression, 172 "tableLink": strutil.TableLinkForExpression, 173 "sortByLabel": func(label string, v queryResult) queryResult { 174 sorter := queryResultByLabelSorter{v[:], label} 175 sort.Stable(sorter) 176 return v 177 }, 178 "humanize": func(i interface{}) (string, error) { 179 v, err := convertToFloat(i) 180 if err != nil { 181 return "", err 182 } 183 if v == 0 || math.IsNaN(v) || math.IsInf(v, 0) { 184 return fmt.Sprintf("%.4g", v), nil 185 } 186 if math.Abs(v) >= 1 { 187 prefix := "" 188 for _, p := range []string{"k", "M", "G", "T", "P", "E", "Z", "Y"} { 189 if math.Abs(v) < 1000 { 190 break 191 } 192 prefix = p 193 v /= 1000 194 } 195 return fmt.Sprintf("%.4g%s", v, prefix), nil 196 } 197 prefix := "" 198 for _, p := range []string{"m", "u", "n", "p", "f", "a", "z", "y"} { 199 if math.Abs(v) >= 1 { 200 break 201 } 202 prefix = p 203 v *= 1000 204 } 205 return fmt.Sprintf("%.4g%s", v, prefix), nil 206 }, 207 "humanize1024": func(i interface{}) (string, error) { 208 v, err := convertToFloat(i) 209 if err != nil { 210 return "", err 211 } 212 if math.Abs(v) <= 1 || math.IsNaN(v) || math.IsInf(v, 0) { 213 return fmt.Sprintf("%.4g", v), nil 214 } 215 prefix := "" 216 for _, p := range []string{"ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi", "Yi"} { 217 if math.Abs(v) < 1024 { 218 break 219 } 220 prefix = p 221 v /= 1024 222 } 223 return fmt.Sprintf("%.4g%s", v, prefix), nil 224 }, 225 "humanizeDuration": func(i interface{}) (string, error) { 226 v, err := convertToFloat(i) 227 if err != nil { 228 return "", err 229 } 230 if math.IsNaN(v) || math.IsInf(v, 0) { 231 return fmt.Sprintf("%.4g", v), nil 232 } 233 if v == 0 { 234 return fmt.Sprintf("%.4gs", v), nil 235 } 236 if math.Abs(v) >= 1 { 237 sign := "" 238 if v < 0 { 239 sign = "-" 240 v = -v 241 } 242 seconds := int64(v) % 60 243 minutes := (int64(v) / 60) % 60 244 hours := (int64(v) / 60 / 60) % 24 245 days := int64(v) / 60 / 60 / 24 246 // For days to minutes, we display seconds as an integer. 247 if days != 0 { 248 return fmt.Sprintf("%s%dd %dh %dm %ds", sign, days, hours, minutes, seconds), nil 249 } 250 if hours != 0 { 251 return fmt.Sprintf("%s%dh %dm %ds", sign, hours, minutes, seconds), nil 252 } 253 if minutes != 0 { 254 return fmt.Sprintf("%s%dm %ds", sign, minutes, seconds), nil 255 } 256 // For seconds, we display 4 significant digits. 257 return fmt.Sprintf("%s%.4gs", sign, v), nil 258 } 259 prefix := "" 260 for _, p := range []string{"m", "u", "n", "p", "f", "a", "z", "y"} { 261 if math.Abs(v) >= 1 { 262 break 263 } 264 prefix = p 265 v *= 1000 266 } 267 return fmt.Sprintf("%.4g%ss", v, prefix), nil 268 }, 269 "humanizePercentage": func(i interface{}) (string, error) { 270 v, err := convertToFloat(i) 271 if err != nil { 272 return "", err 273 } 274 return fmt.Sprintf("%.4g%%", v*100), nil 275 }, 276 "humanizeTimestamp": func(i interface{}) (string, error) { 277 v, err := convertToFloat(i) 278 if err != nil { 279 return "", err 280 } 281 if math.IsNaN(v) || math.IsInf(v, 0) { 282 return fmt.Sprintf("%.4g", v), nil 283 } 284 t := model.TimeFromUnixNano(int64(v * 1e9)).Time().UTC() 285 return fmt.Sprint(t), nil 286 }, 287 "pathPrefix": func() string { 288 return externalURL.Path 289 }, 290 "externalURL": func() string { 291 return externalURL.String() 292 }, 293 }, 294 } 295} 296 297// AlertTemplateData returns the interface to be used in expanding the template. 298func AlertTemplateData(labels map[string]string, externalLabels map[string]string, externalURL string, value float64) interface{} { 299 return struct { 300 Labels map[string]string 301 ExternalLabels map[string]string 302 ExternalURL string 303 Value float64 304 }{ 305 Labels: labels, 306 ExternalLabels: externalLabels, 307 ExternalURL: externalURL, 308 Value: value, 309 } 310} 311 312// Funcs adds the functions in fm to the Expander's function map. 313// Existing functions will be overwritten in case of conflict. 314func (te Expander) Funcs(fm text_template.FuncMap) { 315 for k, v := range fm { 316 te.funcMap[k] = v 317 } 318} 319 320// Expand expands a template in text (non-HTML) mode. 321func (te Expander) Expand() (result string, resultErr error) { 322 // It'd better to have no alert description than to kill the whole process 323 // if there's a bug in the template. 324 defer func() { 325 if r := recover(); r != nil { 326 var ok bool 327 resultErr, ok = r.(error) 328 if !ok { 329 resultErr = errors.Errorf("panic expanding template %v: %v", te.name, r) 330 } 331 } 332 if resultErr != nil { 333 templateTextExpansionFailures.Inc() 334 } 335 }() 336 337 templateTextExpansionTotal.Inc() 338 339 tmpl, err := text_template.New(te.name).Funcs(te.funcMap).Option("missingkey=zero").Parse(te.text) 340 if err != nil { 341 return "", errors.Wrapf(err, "error parsing template %v", te.name) 342 } 343 var buffer bytes.Buffer 344 err = tmpl.Execute(&buffer, te.data) 345 if err != nil { 346 return "", errors.Wrapf(err, "error executing template %v", te.name) 347 } 348 return buffer.String(), nil 349} 350 351// ExpandHTML expands a template with HTML escaping, with templates read from the given files. 352func (te Expander) ExpandHTML(templateFiles []string) (result string, resultErr error) { 353 defer func() { 354 if r := recover(); r != nil { 355 var ok bool 356 resultErr, ok = r.(error) 357 if !ok { 358 resultErr = errors.Errorf("panic expanding template %s: %v", te.name, r) 359 } 360 } 361 }() 362 363 tmpl := html_template.New(te.name).Funcs(html_template.FuncMap(te.funcMap)) 364 tmpl.Option("missingkey=zero") 365 tmpl.Funcs(html_template.FuncMap{ 366 "tmpl": func(name string, data interface{}) (html_template.HTML, error) { 367 var buffer bytes.Buffer 368 err := tmpl.ExecuteTemplate(&buffer, name, data) 369 return html_template.HTML(buffer.String()), err 370 }, 371 }) 372 tmpl, err := tmpl.Parse(te.text) 373 if err != nil { 374 return "", errors.Wrapf(err, "error parsing template %v", te.name) 375 } 376 if len(templateFiles) > 0 { 377 _, err = tmpl.ParseFiles(templateFiles...) 378 if err != nil { 379 return "", errors.Wrapf(err, "error parsing template files for %v", te.name) 380 } 381 } 382 var buffer bytes.Buffer 383 err = tmpl.Execute(&buffer, te.data) 384 if err != nil { 385 return "", errors.Wrapf(err, "error executing template %v", te.name) 386 } 387 return buffer.String(), nil 388} 389 390// ParseTest parses the templates and returns the error if any. 391func (te Expander) ParseTest() error { 392 _, err := text_template.New(te.name).Funcs(te.funcMap).Option("missingkey=zero").Parse(te.text) 393 if err != nil { 394 return err 395 } 396 return nil 397} 398