Initial QSfera import

This commit is contained in:
Курнат Андрей
2026-06-07 10:20:04 +03:00
commit 2315f25754
16485 changed files with 4826827 additions and 0 deletions
+12
View File
@@ -0,0 +1,12 @@
Copyright Prometheus Team
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.
+201
View File
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
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.
+13
View File
@@ -0,0 +1,13 @@
Prometheus Alertmanager
Copyright 2013-2015 The Prometheus Authors
This product includes software developed at
SoundCloud Ltd. (http://soundcloud.com/).
The following components are included in this product:
Bootstrap
http://getbootstrap.com
Copyright 2011-2014 Twitter, Inc.
Licensed under the MIT License
+49
View File
@@ -0,0 +1,49 @@
// Copyright 2018 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.
//go:build dev
package asset
import (
"net/http"
"os"
"strings"
"github.com/shurcooL/httpfs/filter"
"github.com/shurcooL/httpfs/union"
)
var static http.FileSystem = filter.Keep(
http.Dir("../ui/app"),
func(path string, fi os.FileInfo) bool {
return path == "/" ||
path == "/script.js" ||
path == "/index.html" ||
path == "/favicon.ico" ||
strings.HasPrefix(path, "/lib")
},
)
var templates http.FileSystem = filter.Keep(
http.Dir("../template"),
func(path string, fi os.FileInfo) bool {
return path == "/" || path == "/default.tmpl" || path == "/email.tmpl"
},
)
// Assets contains the project's assets.
var Assets http.FileSystem = union.New(map[string]http.FileSystem{
"/templates": templates,
"/static": static,
})
File diff suppressed because one or more lines are too long
+22
View File
@@ -0,0 +1,22 @@
// Copyright 2018 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 asset provides the assets via a virtual filesystem.
package asset
import (
// The blank import is to make go modules happy.
_ "github.com/shurcooL/vfsgen"
)
//go:generate go run -tags=dev asset_generate.go
@@ -0,0 +1,178 @@
// Copyright 2023 Prometheus Team
// 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 featurecontrol
import (
"errors"
"fmt"
"log/slog"
"strings"
)
const (
FeatureAlertNamesInMetrics = "alert-names-in-metrics"
FeatureReceiverNameInMetrics = "receiver-name-in-metrics"
FeatureClassicMode = "classic-mode"
FeatureUTF8StrictMode = "utf8-strict-mode"
FeatureAutoGOMEMLIMIT = "auto-gomemlimit"
FeatureAutoGOMAXPROCS = "auto-gomaxprocs"
)
var AllowedFlags = []string{
FeatureAlertNamesInMetrics,
FeatureReceiverNameInMetrics,
FeatureClassicMode,
FeatureUTF8StrictMode,
FeatureAutoGOMEMLIMIT,
FeatureAutoGOMAXPROCS,
}
type Flagger interface {
EnableAlertNamesInMetrics() bool
EnableReceiverNamesInMetrics() bool
ClassicMode() bool
UTF8StrictMode() bool
EnableAutoGOMEMLIMIT() bool
EnableAutoGOMAXPROCS() bool
}
type Flags struct {
logger *slog.Logger
enableAlertNamesInMetrics bool
enableReceiverNamesInMetrics bool
classicMode bool
utf8StrictMode bool
enableAutoGOMEMLIMIT bool
enableAutoGOMAXPROCS bool
}
func (f *Flags) EnableAlertNamesInMetrics() bool {
return f.enableAlertNamesInMetrics
}
func (f *Flags) EnableReceiverNamesInMetrics() bool {
return f.enableReceiverNamesInMetrics
}
func (f *Flags) ClassicMode() bool {
return f.classicMode
}
func (f *Flags) UTF8StrictMode() bool {
return f.utf8StrictMode
}
func (f *Flags) EnableAutoGOMEMLIMIT() bool {
return f.enableAutoGOMEMLIMIT
}
func (f *Flags) EnableAutoGOMAXPROCS() bool {
return f.enableAutoGOMAXPROCS
}
type flagOption func(flags *Flags)
func enableReceiverNameInMetrics() flagOption {
return func(configs *Flags) {
configs.enableReceiverNamesInMetrics = true
}
}
func enableClassicMode() flagOption {
return func(configs *Flags) {
configs.classicMode = true
}
}
func enableUTF8StrictMode() flagOption {
return func(configs *Flags) {
configs.utf8StrictMode = true
}
}
func enableAutoGOMEMLIMIT() flagOption {
return func(configs *Flags) {
configs.enableAutoGOMEMLIMIT = true
}
}
func enableAutoGOMAXPROCS() flagOption {
return func(configs *Flags) {
configs.enableAutoGOMAXPROCS = true
}
}
func enableAlertNamesInMetrics() flagOption {
return func(configs *Flags) {
configs.enableAlertNamesInMetrics = true
}
}
func NewFlags(logger *slog.Logger, features string) (Flagger, error) {
fc := &Flags{logger: logger}
opts := []flagOption{}
if len(features) == 0 {
return NoopFlags{}, nil
}
for feature := range strings.SplitSeq(features, ",") {
switch feature {
case FeatureAlertNamesInMetrics:
opts = append(opts, enableAlertNamesInMetrics())
logger.Warn("Alert names in metrics enabled")
case FeatureReceiverNameInMetrics:
opts = append(opts, enableReceiverNameInMetrics())
logger.Warn("Experimental receiver name in metrics enabled")
case FeatureClassicMode:
opts = append(opts, enableClassicMode())
logger.Warn("Classic mode enabled")
case FeatureUTF8StrictMode:
opts = append(opts, enableUTF8StrictMode())
logger.Warn("UTF-8 strict mode enabled")
case FeatureAutoGOMEMLIMIT:
opts = append(opts, enableAutoGOMEMLIMIT())
logger.Warn("Automatically set GOMEMLIMIT to match the Linux container or system memory limit.")
case FeatureAutoGOMAXPROCS:
opts = append(opts, enableAutoGOMAXPROCS())
logger.Warn("Automatically set GOMAXPROCS to match Linux container CPU quota")
default:
return nil, fmt.Errorf("unknown option '%s' for --enable-feature", feature)
}
}
for _, opt := range opts {
opt(fc)
}
if fc.classicMode && fc.utf8StrictMode {
return nil, errors.New("cannot have both classic and UTF-8 modes enabled")
}
return fc, nil
}
type NoopFlags struct{}
func (n NoopFlags) EnableAlertNamesInMetrics() bool { return false }
func (n NoopFlags) EnableReceiverNamesInMetrics() bool { return false }
func (n NoopFlags) ClassicMode() bool { return false }
func (n NoopFlags) UTF8StrictMode() bool { return false }
func (n NoopFlags) EnableAutoGOMEMLIMIT() bool { return false }
func (n NoopFlags) EnableAutoGOMAXPROCS() bool { return false }
@@ -0,0 +1,205 @@
// Copyright 2023 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 compat
import (
"fmt"
"log/slog"
"reflect"
"strings"
"unicode/utf8"
"github.com/prometheus/common/model"
"github.com/prometheus/common/promslog"
"github.com/prometheus/alertmanager/featurecontrol"
"github.com/prometheus/alertmanager/matcher/parse"
"github.com/prometheus/alertmanager/pkg/labels"
)
var (
isValidLabelName = isValidClassicLabelName(promslog.NewNopLogger())
parseMatcher = ClassicMatcherParser(promslog.NewNopLogger())
parseMatchers = ClassicMatchersParser(promslog.NewNopLogger())
)
// IsValidLabelName returns true if the string is a valid label name.
func IsValidLabelName(name model.LabelName) bool {
return isValidLabelName(name)
}
type ParseMatcher func(input, origin string) (*labels.Matcher, error)
type ParseMatchers func(input, origin string) (labels.Matchers, error)
// Matcher parses the matcher in the input string. It returns an error
// if the input is invalid or contains two or more matchers.
func Matcher(input, origin string) (*labels.Matcher, error) {
return parseMatcher(input, origin)
}
// Matchers parses one or more matchers in the input string. It returns
// an error if the input is invalid.
func Matchers(input, origin string) (labels.Matchers, error) {
return parseMatchers(input, origin)
}
// InitFromFlags initializes the compat package from the flagger.
func InitFromFlags(l *slog.Logger, f featurecontrol.Flagger) {
if f.ClassicMode() {
isValidLabelName = isValidClassicLabelName(l)
parseMatcher = ClassicMatcherParser(l)
parseMatchers = ClassicMatchersParser(l)
} else if f.UTF8StrictMode() {
isValidLabelName = isValidUTF8LabelName(l)
parseMatcher = UTF8MatcherParser(l)
parseMatchers = UTF8MatchersParser(l)
} else {
isValidLabelName = isValidUTF8LabelName(l)
parseMatcher = FallbackMatcherParser(l)
parseMatchers = FallbackMatchersParser(l)
}
}
// ClassicMatcherParser uses the pkg/labels parser to parse the matcher in
// the input string.
func ClassicMatcherParser(l *slog.Logger) ParseMatcher {
return func(input, origin string) (matcher *labels.Matcher, err error) {
l.Debug("Parsing with classic matchers parser", "input", input, "origin", origin)
return labels.ParseMatcher(input)
}
}
// ClassicMatchersParser uses the pkg/labels parser to parse zero or more
// matchers in the input string. It returns an error if the input is invalid.
func ClassicMatchersParser(l *slog.Logger) ParseMatchers {
return func(input, origin string) (matchers labels.Matchers, err error) {
l.Debug("Parsing with classic matchers parser", "input", input, "origin", origin)
return labels.ParseMatchers(input)
}
}
// UTF8MatcherParser uses the new matcher/parse parser to parse the matcher
// in the input string. If this fails it does not revert to the pkg/labels parser.
func UTF8MatcherParser(l *slog.Logger) ParseMatcher {
return func(input, origin string) (matcher *labels.Matcher, err error) {
l.Debug("Parsing with UTF-8 matchers parser", "input", input, "origin", origin)
if strings.HasPrefix(input, "{") || strings.HasSuffix(input, "}") {
return nil, fmt.Errorf("unexpected open or close brace: %s", input)
}
return parse.Matcher(input)
}
}
// UTF8MatchersParser uses the new matcher/parse parser to parse zero or more
// matchers in the input string. If this fails it does not revert to the
// pkg/labels parser.
func UTF8MatchersParser(l *slog.Logger) ParseMatchers {
return func(input, origin string) (matchers labels.Matchers, err error) {
l.Debug("Parsing with UTF-8 matchers parser", "input", input, "origin", origin)
return parse.Matchers(input)
}
}
// FallbackMatcherParser uses the new matcher/parse parser to parse zero or more
// matchers in the string. If this fails it reverts to the pkg/labels parser and
// emits a warning log line.
func FallbackMatcherParser(l *slog.Logger) ParseMatcher {
return func(input, origin string) (matcher *labels.Matcher, err error) {
l.Debug("Parsing with UTF-8 matchers parser, with fallback to classic matchers parser", "input", input, "origin", origin)
if strings.HasPrefix(input, "{") || strings.HasSuffix(input, "}") {
return nil, fmt.Errorf("unexpected open or close brace: %s", input)
}
// Parse the input in both parsers to look for disagreement and incompatible
// inputs.
nMatcher, nErr := parse.Matcher(input)
cMatcher, cErr := labels.ParseMatcher(input)
if nErr != nil {
// If the input is invalid in both parsers, return the error.
if cErr != nil {
return nil, cErr
}
// The input is valid in the pkg/labels parser, but not the matcher/parse
// parser. This means the input is not forwards compatible.
suggestion := cMatcher.String()
l.Warn("Alertmanager is moving to a new parser for labels and matchers, and this input is incompatible. Alertmanager has instead parsed the input using the classic matchers parser as a fallback. To make this input compatible with the UTF-8 matchers parser please make sure all regular expressions and values are double-quoted and backslashes are escaped. If you are still seeing this message please open an issue.", "input", input, "origin", origin, "err", nErr, "suggestion", suggestion)
return cMatcher, nil
}
// If the input is valid in both parsers, but produces different results,
// then there is disagreement.
if cErr == nil && !reflect.DeepEqual(nMatcher, cMatcher) {
l.Warn("Matchers input has disagreement", "input", input, "origin", origin)
return cMatcher, nil
}
return nMatcher, nil
}
}
// FallbackMatchersParser uses the new matcher/parse parser to parse the
// matcher in the input string. If this fails it falls back to the pkg/labels
// parser and emits a warning log line.
func FallbackMatchersParser(l *slog.Logger) ParseMatchers {
return func(input, origin string) (matchers labels.Matchers, err error) {
l.Debug("Parsing with UTF-8 matchers parser, with fallback to classic matchers parser", "input", input, "origin", origin)
// Parse the input in both parsers to look for disagreement and incompatible
// inputs.
nMatchers, nErr := parse.Matchers(input)
cMatchers, cErr := labels.ParseMatchers(input)
if nErr != nil {
// If the input is invalid in both parsers, return the error.
if cErr != nil {
return nil, cErr
}
// The input is valid in the pkg/labels parser, but not the matcher/parse
// parser. This means the input is not forwards compatible.
var sb strings.Builder
for i, n := range cMatchers {
sb.WriteString(n.String())
if i < len(cMatchers)-1 {
sb.WriteRune(',')
}
}
suggestion := sb.String()
// The input is valid in the pkg/labels parser, but not the
// new matcher/parse parser.
l.Warn("Alertmanager is moving to a new parser for labels and matchers, and this input is incompatible. Alertmanager has instead parsed the input using the classic matchers parser as a fallback. To make this input compatible with the UTF-8 matchers parser please make sure all regular expressions and values are double-quoted and backslashes are escaped. If you are still seeing this message please open an issue.", "input", input, "origin", origin, "err", nErr, "suggestion", suggestion)
return cMatchers, nil
}
// If the input is valid in both parsers, but produces different results,
// then there is disagreement. We need to compare to labels.Matchers(cMatchers)
// as cMatchers is a []*labels.Matcher not labels.Matchers.
if cErr == nil && !reflect.DeepEqual(nMatchers, labels.Matchers(cMatchers)) {
l.Warn("Matchers input has disagreement", "input", input, "origin", origin)
return cMatchers, nil
}
return nMatchers, nil
}
}
// isValidClassicLabelName returns true if the string is a valid classic label name.
func isValidClassicLabelName(_ *slog.Logger) func(model.LabelName) bool {
return func(name model.LabelName) bool {
return model.LegacyValidation.IsValidLabelName(string(name))
}
}
// isValidUTF8LabelName returns true if the string is a valid UTF-8 label name.
func isValidUTF8LabelName(_ *slog.Logger) func(model.LabelName) bool {
return func(name model.LabelName) bool {
if len(name) == 0 {
return false
}
return utf8.ValidString(string(name))
}
}
+309
View File
@@ -0,0 +1,309 @@
// Copyright 2023 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 parse
import (
"fmt"
"strings"
"unicode"
"unicode/utf8"
)
const (
eof rune = -1
)
func isReserved(r rune) bool {
return unicode.IsSpace(r) || strings.ContainsRune("{}!=~,\\\"'`", r)
}
// expectedError is returned when the next rune does not match what is expected.
type expectedError struct {
position
input string
expected string
}
func (e expectedError) Error() string {
if e.offsetEnd >= len(e.input) {
return fmt.Sprintf("%d:%d: unexpected end of input, expected one of '%s'",
e.columnStart,
e.columnEnd,
e.expected,
)
}
return fmt.Sprintf("%d:%d: %s: expected one of '%s'",
e.columnStart,
e.columnEnd,
e.input[e.offsetStart:e.offsetEnd],
e.expected,
)
}
// invalidInputError is returned when the next rune in the input does not match
// the grammar of Prometheus-like matchers.
type invalidInputError struct {
position
input string
}
func (e invalidInputError) Error() string {
return fmt.Sprintf("%d:%d: %s: invalid input",
e.columnStart,
e.columnEnd,
e.input[e.offsetStart:e.offsetEnd],
)
}
// unterminatedError is returned when text in quotes does not have a closing quote.
type unterminatedError struct {
position
input string
quote rune
}
func (e unterminatedError) Error() string {
return fmt.Sprintf("%d:%d: %s: missing end %c",
e.columnStart,
e.columnEnd,
e.input[e.offsetStart:e.offsetEnd],
e.quote,
)
}
// lexer scans a sequence of tokens that match the grammar of Prometheus-like
// matchers. A token is emitted for each call to scan() which returns the
// next token in the input or an error if the input does not conform to the
// grammar. A token can be one of a number of kinds and corresponds to a
// subslice of the input. Once the input has been consumed successive calls to
// scan() return a tokenEOF token.
type lexer struct {
input string
err error
start int // The offset of the current token.
pos int // The position of the cursor in the input.
width int // The width of the last rune.
column int // The column offset of the current token.
cols int // The number of columns (runes) decoded from the input.
}
// Scans the next token in the input or an error if the input does not
// conform to the grammar. Once the input has been consumed successive
// calls scan() return a tokenEOF token.
func (l *lexer) scan() (token, error) {
t := token{}
// Do not attempt to emit more tokens if the input is invalid.
if l.err != nil {
return t, l.err
}
// Iterate over each rune in the input and either emit a token or an error.
for r := l.next(); r != eof; r = l.next() {
switch {
case r == '{':
t = l.emit(tokenOpenBrace)
return t, l.err
case r == '}':
t = l.emit(tokenCloseBrace)
return t, l.err
case r == ',':
t = l.emit(tokenComma)
return t, l.err
case r == '=' || r == '!':
l.rewind()
t, l.err = l.scanOperator()
return t, l.err
case r == '"':
l.rewind()
t, l.err = l.scanQuoted()
return t, l.err
case !isReserved(r):
l.rewind()
t, l.err = l.scanUnquoted()
return t, l.err
case unicode.IsSpace(r):
l.skip()
default:
l.err = invalidInputError{
position: l.position(),
input: l.input,
}
return t, l.err
}
}
return t, l.err
}
func (l *lexer) scanOperator() (token, error) {
// If the first rune is an '!' then it must be followed with either an
// '=' or '~' to not match a string or regex.
if l.accept("!") {
if l.accept("=") {
return l.emit(tokenNotEquals), nil
}
if l.accept("~") {
return l.emit(tokenNotMatches), nil
}
return token{}, expectedError{
position: l.position(),
input: l.input,
expected: "=~",
}
}
// If the first rune is an '=' then it can be followed with an optional
// '~' to match a regex.
if l.accept("=") {
if l.accept("~") {
return l.emit(tokenMatches), nil
}
return l.emit(tokenEquals), nil
}
return token{}, expectedError{
position: l.position(),
input: l.input,
expected: "!=",
}
}
func (l *lexer) scanQuoted() (token, error) {
if err := l.expect("\""); err != nil {
return token{}, err
}
var isEscaped bool
for r := l.next(); r != eof; r = l.next() {
if isEscaped {
isEscaped = false
} else if r == '\\' {
isEscaped = true
} else if r == '"' {
l.rewind()
break
}
}
if err := l.expect("\""); err != nil {
return token{}, unterminatedError{
position: l.position(),
input: l.input,
quote: '"',
}
}
return l.emit(tokenQuoted), nil
}
func (l *lexer) scanUnquoted() (token, error) {
for r := l.next(); r != eof; r = l.next() {
if isReserved(r) {
l.rewind()
break
}
}
return l.emit(tokenUnquoted), nil
}
// peek the next token in the input or an error if the input does not
// conform to the grammar. Once the input has been consumed successive
// calls peek() return a tokenEOF token.
func (l *lexer) peek() (token, error) {
start := l.start
pos := l.pos
width := l.width
column := l.column
cols := l.cols
// Do not reset l.err because we can return it on the next call to scan().
defer func() {
l.start = start
l.pos = pos
l.width = width
l.column = column
l.cols = cols
}()
return l.scan()
}
// position returns the position of the last emitted token.
func (l *lexer) position() position {
return position{
offsetStart: l.start,
offsetEnd: l.pos,
columnStart: l.column,
columnEnd: l.cols,
}
}
// accept consumes the next if its one of the valid runes.
// It returns true if the next rune was accepted, otherwise false.
func (l *lexer) accept(valid string) bool {
if strings.ContainsRune(valid, l.next()) {
return true
}
l.rewind()
return false
}
// expect consumes the next rune if its one of the valid runes.
// It returns nil if the next rune is valid, otherwise an expectedError
// error.
func (l *lexer) expect(valid string) error {
if strings.ContainsRune(valid, l.next()) {
return nil
}
l.rewind()
return expectedError{
position: l.position(),
input: l.input,
expected: valid,
}
}
// emits returns the scanned input as a token.
func (l *lexer) emit(kind tokenKind) token {
t := token{
kind: kind,
value: l.input[l.start:l.pos],
position: l.position(),
}
l.start = l.pos
l.column = l.cols
return t
}
// next returns the next rune in the input or eof.
func (l *lexer) next() rune {
if l.pos >= len(l.input) {
l.width = 0
return eof
}
r, width := utf8.DecodeRuneInString(l.input[l.pos:])
l.width = width
l.pos += width
l.cols++
return r
}
// rewind the last rune in the input. It should not be called more than once
// between consecutive calls of next.
func (l *lexer) rewind() {
l.pos -= l.width
// When the next rune in the input is eof the width is zero. This check
// prevents cols from being decremented when the next rune being accepted
// is instead eof.
if l.width > 0 {
l.cols--
}
}
// skip the scanned input between start and pos.
func (l *lexer) skip() {
l.start = l.pos
l.column = l.cols
}
+308
View File
@@ -0,0 +1,308 @@
// Copyright 2023 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 parse
import (
"errors"
"fmt"
"os"
"runtime/debug"
"github.com/prometheus/alertmanager/pkg/labels"
)
var (
errEOF = errors.New("end of input")
errExpectedEOF = errors.New("expected end of input")
errNoOpenBrace = errors.New("expected opening brace")
errNoCloseBrace = errors.New("expected close brace")
errNoLabelName = errors.New("expected label name")
errNoLabelValue = errors.New("expected label value")
errNoOperator = errors.New("expected an operator such as '=', '!=', '=~' or '!~'")
errExpectedComma = errors.New("expected a comma")
errExpectedCommaOrCloseBrace = errors.New("expected a comma or close brace")
errExpectedMatcherOrCloseBrace = errors.New("expected a matcher or close brace after comma")
)
// Matchers parses one or more matchers in the input string. It returns an error
// if the input is invalid.
func Matchers(input string) (matchers labels.Matchers, err error) {
defer func() {
if r := recover(); r != nil {
fmt.Fprintf(os.Stderr, "parser panic: %s, %s", r, debug.Stack())
err = errors.New("parser panic: this should never happen, check stderr for the stack trace")
}
}()
p := parser{lexer: lexer{input: input}}
return p.parse()
}
// Matcher parses the matcher in the input string. It returns an error
// if the input is invalid or contains two or more matchers.
func Matcher(input string) (*labels.Matcher, error) {
m, err := Matchers(input)
if err != nil {
return nil, err
}
switch len(m) {
case 1:
return m[0], nil
case 0:
return nil, fmt.Errorf("no matchers")
default:
return nil, fmt.Errorf("expected 1 matcher, found %d", len(m))
}
}
// parseFunc is state in the finite state automata.
type parseFunc func(l *lexer) (parseFunc, error)
// parser reads the sequence of tokens from the lexer and returns either a
// series of matchers or an error. It works as a finite state automata, where
// each state in the automata is a parseFunc. The finite state automata can move
// from one state to another by returning the next parseFunc. It terminates when
// a parseFunc returns nil as the next parseFunc, if the lexer attempts to scan
// input that does not match the expected grammar, or if the tokens returned from
// the lexer cannot be parsed into a complete series of matchers.
type parser struct {
matchers labels.Matchers
// Tracks if the input starts with an open brace and if we should expect to
// parse a close brace at the end of the input.
hasOpenBrace bool
lexer lexer
}
func (p *parser) parse() (labels.Matchers, error) {
var (
err error
fn = p.parseOpenBrace
l = &p.lexer
)
for {
if fn, err = fn(l); err != nil {
return nil, err
} else if fn == nil {
break
}
}
return p.matchers, nil
}
func (p *parser) parseOpenBrace(l *lexer) (parseFunc, error) {
var (
hasCloseBrace bool
err error
)
// Can start with an optional open brace.
p.hasOpenBrace, err = p.accept(l, tokenOpenBrace)
if err != nil {
if errors.Is(err, errEOF) {
return p.parseEOF, nil
}
return nil, err
}
// If the next token is a close brace there are no matchers in the input.
hasCloseBrace, err = p.acceptPeek(l, tokenCloseBrace)
if err != nil {
// If there is no more input after the open brace then parse the close brace
// so the error message contains ErrNoCloseBrace.
if errors.Is(err, errEOF) {
return p.parseCloseBrace, nil
}
return nil, err
}
if hasCloseBrace {
return p.parseCloseBrace, nil
}
return p.parseMatcher, nil
}
func (p *parser) parseCloseBrace(l *lexer) (parseFunc, error) {
if p.hasOpenBrace {
// If there was an open brace there must be a matching close brace.
if _, err := p.expect(l, tokenCloseBrace); err != nil {
return nil, fmt.Errorf("0:%d: %w: %w", l.position().columnEnd, err, errNoCloseBrace)
}
} else {
// If there was no open brace there must not be a close brace either.
if _, err := p.expect(l, tokenCloseBrace); err == nil {
return nil, fmt.Errorf("0:%d: }: %w", l.position().columnEnd, errNoOpenBrace)
}
}
return p.parseEOF, nil
}
func (p *parser) parseMatcher(l *lexer) (parseFunc, error) {
var (
err error
t token
matchName, matchValue string
matchTy labels.MatchType
)
// The first token should be the label name.
if t, err = p.expect(l, tokenQuoted, tokenUnquoted); err != nil {
return nil, fmt.Errorf("%w: %w", err, errNoLabelName)
}
matchName, err = t.unquote()
if err != nil {
return nil, fmt.Errorf("%d:%d: %s: invalid input", t.columnStart, t.columnEnd, t.value)
}
// The next token should be the operator.
if t, err = p.expect(l, tokenEquals, tokenNotEquals, tokenMatches, tokenNotMatches); err != nil {
return nil, fmt.Errorf("%w: %w", err, errNoOperator)
}
switch t.kind {
case tokenEquals:
matchTy = labels.MatchEqual
case tokenNotEquals:
matchTy = labels.MatchNotEqual
case tokenMatches:
matchTy = labels.MatchRegexp
case tokenNotMatches:
matchTy = labels.MatchNotRegexp
default:
panic(fmt.Sprintf("bad operator %s", t))
}
// The next token should be the match value. Like the match name, this too
// can be either double-quoted UTF-8 or unquoted UTF-8 without reserved characters.
if t, err = p.expect(l, tokenUnquoted, tokenQuoted); err != nil {
return nil, fmt.Errorf("%w: %w", err, errNoLabelValue)
}
matchValue, err = t.unquote()
if err != nil {
return nil, fmt.Errorf("%d:%d: %s: invalid input", t.columnStart, t.columnEnd, t.value)
}
m, err := labels.NewMatcher(matchTy, matchName, matchValue)
if err != nil {
return nil, fmt.Errorf("failed to create matcher: %w", err)
}
p.matchers = append(p.matchers, m)
return p.parseEndOfMatcher, nil
}
func (p *parser) parseEndOfMatcher(l *lexer) (parseFunc, error) {
t, err := p.expectPeek(l, tokenComma, tokenCloseBrace)
if err != nil {
if errors.Is(err, errEOF) {
// If this is the end of input we still need to check if the optional
// open brace has a matching close brace.
return p.parseCloseBrace, nil
}
return nil, fmt.Errorf("%w: %w", err, errExpectedCommaOrCloseBrace)
}
switch t.kind {
case tokenComma:
return p.parseComma, nil
case tokenCloseBrace:
return p.parseCloseBrace, nil
default:
panic(fmt.Sprintf("bad token %s", t))
}
}
func (p *parser) parseComma(l *lexer) (parseFunc, error) {
if _, err := p.expect(l, tokenComma); err != nil {
return nil, fmt.Errorf("%w: %w", err, errExpectedComma)
}
// The token after the comma can be another matcher, a close brace or end of input.
t, err := p.expectPeek(l, tokenCloseBrace, tokenUnquoted, tokenQuoted)
if err != nil {
if errors.Is(err, errEOF) {
// If this is the end of input we still need to check if the optional
// open brace has a matching close brace.
return p.parseCloseBrace, nil
}
return nil, fmt.Errorf("%w: %w", err, errExpectedMatcherOrCloseBrace)
}
if t.kind == tokenCloseBrace {
return p.parseCloseBrace, nil
}
return p.parseMatcher, nil
}
func (p *parser) parseEOF(l *lexer) (parseFunc, error) {
t, err := l.scan()
if err != nil {
return nil, fmt.Errorf("%w: %w", err, errExpectedEOF)
}
if !t.isEOF() {
return nil, fmt.Errorf("%d:%d: %s: %w", t.columnStart, t.columnEnd, t.value, errExpectedEOF)
}
return nil, nil
}
// nolint:godot
// accept returns true if the next token is one of the specified kinds,
// otherwise false. If the token is accepted it is consumed. tokenEOF is
// not an accepted kind and instead accept returns ErrEOF if there is no
// more input.
func (p *parser) accept(l *lexer, kinds ...tokenKind) (ok bool, err error) {
ok, err = p.acceptPeek(l, kinds...)
if ok {
if _, err = l.scan(); err != nil {
panic("failed to scan peeked token")
}
}
return ok, err
}
// nolint:godot
// acceptPeek returns true if the next token is one of the specified kinds,
// otherwise false. However, unlike accept, acceptPeek does not consume accepted
// tokens. tokenEOF is not an accepted kind and instead accept returns ErrEOF
// if there is no more input.
func (p *parser) acceptPeek(l *lexer, kinds ...tokenKind) (bool, error) {
t, err := l.peek()
if err != nil {
return false, err
}
if t.isEOF() {
return false, errEOF
}
return t.isOneOf(kinds...), nil
}
// nolint:godot
// expect returns the next token if it is one of the specified kinds, otherwise
// it returns an error. If the token is expected it is consumed. tokenEOF is not
// an accepted kind and instead expect returns ErrEOF if there is no more input.
func (p *parser) expect(l *lexer, kind ...tokenKind) (token, error) {
t, err := p.expectPeek(l, kind...)
if err != nil {
return t, err
}
if _, err = l.scan(); err != nil {
panic("failed to scan peeked token")
}
return t, nil
}
// nolint:godot
// expect returns the next token if it is one of the specified kinds, otherwise
// it returns an error. However, unlike expect, expectPeek does not consume tokens.
// tokenEOF is not an accepted kind and instead expect returns ErrEOF if there is no
// more input.
func (p *parser) expectPeek(l *lexer, kind ...tokenKind) (token, error) {
t, err := l.peek()
if err != nil {
return t, err
}
if t.isEOF() {
return t, errEOF
}
if !t.isOneOf(kind...) {
return t, fmt.Errorf("%d:%d: unexpected %s", t.columnStart, t.columnEnd, t.value)
}
return t, nil
}
+104
View File
@@ -0,0 +1,104 @@
// Copyright 2023 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 parse
import (
"errors"
"fmt"
"slices"
"strconv"
"unicode/utf8"
)
type tokenKind int
const (
tokenEOF tokenKind = iota
tokenOpenBrace
tokenCloseBrace
tokenComma
tokenEquals
tokenNotEquals
tokenMatches
tokenNotMatches
tokenQuoted
tokenUnquoted
)
func (k tokenKind) String() string {
switch k {
case tokenOpenBrace:
return "OpenBrace"
case tokenCloseBrace:
return "CloseBrace"
case tokenComma:
return "Comma"
case tokenEquals:
return "Equals"
case tokenNotEquals:
return "NotEquals"
case tokenMatches:
return "Matches"
case tokenNotMatches:
return "NotMatches"
case tokenQuoted:
return "Quoted"
case tokenUnquoted:
return "Unquoted"
default:
return "EOF"
}
}
type token struct {
kind tokenKind
value string
position
}
// isEOF returns true if the token is an end of file token.
func (t token) isEOF() bool {
return t.kind == tokenEOF
}
// isOneOf returns true if the token is one of the specified kinds.
func (t token) isOneOf(kinds ...tokenKind) bool {
return slices.Contains(kinds, t.kind)
}
// unquote the value in token. If unquoted returns it unmodified.
func (t token) unquote() (string, error) {
if t.kind == tokenQuoted {
unquoted, err := strconv.Unquote(t.value)
if err != nil {
return "", err
}
if !utf8.ValidString(unquoted) {
return "", errors.New("quoted string contains invalid UTF-8 code points")
}
return unquoted, nil
}
return t.value, nil
}
func (t token) String() string {
return fmt.Sprintf("(%s) '%s'", t.kind, t.value)
}
type position struct {
offsetStart int // The start position in the input.
offsetEnd int // The end position in the input.
columnStart int // The column number.
columnEnd int // The end of the column.
}
+216
View File
@@ -0,0 +1,216 @@
// Copyright 2017 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 labels
import (
"bytes"
"encoding/json"
"fmt"
"regexp"
"strconv"
"strings"
"unicode"
"github.com/prometheus/common/model"
)
// MatchType is an enum for label matching types.
type MatchType int
// Possible MatchTypes.
const (
MatchEqual MatchType = iota
MatchNotEqual
MatchRegexp
MatchNotRegexp
)
func (m MatchType) String() string {
typeToStr := map[MatchType]string{
MatchEqual: "=",
MatchNotEqual: "!=",
MatchRegexp: "=~",
MatchNotRegexp: "!~",
}
if str, ok := typeToStr[m]; ok {
return str
}
panic("unknown match type")
}
// Matcher models the matching of a label.
type Matcher struct {
Type MatchType
Name string
Value string
re *regexp.Regexp
}
// NewMatcher returns a matcher object.
func NewMatcher(t MatchType, n, v string) (*Matcher, error) {
m := &Matcher{
Type: t,
Name: n,
Value: v,
}
if t == MatchRegexp || t == MatchNotRegexp {
re, err := regexp.Compile("^(?:" + v + ")$")
if err != nil {
return nil, err
}
m.re = re
}
return m, nil
}
func (m *Matcher) String() string {
if strings.ContainsFunc(m.Name, isReserved) {
return fmt.Sprintf(`%s%s%s`, strconv.Quote(m.Name), m.Type, strconv.Quote(m.Value))
}
return fmt.Sprintf(`%s%s"%s"`, m.Name, m.Type, openMetricsEscape(m.Value))
}
// Matches returns whether the matcher matches the given string value.
func (m *Matcher) Matches(s string) bool {
switch m.Type {
case MatchEqual:
return s == m.Value
case MatchNotEqual:
return s != m.Value
case MatchRegexp:
return m.re.MatchString(s)
case MatchNotRegexp:
return !m.re.MatchString(s)
}
panic("labels.Matcher.Matches: invalid match type")
}
type apiV1Matcher struct {
Name string `json:"name"`
Value string `json:"value"`
IsRegex bool `json:"isRegex"`
IsEqual bool `json:"isEqual"`
}
// MarshalJSON retains backwards compatibility with types.Matcher for the v1 API.
func (m Matcher) MarshalJSON() ([]byte, error) {
return json.Marshal(apiV1Matcher{
Name: m.Name,
Value: m.Value,
IsRegex: m.Type == MatchRegexp || m.Type == MatchNotRegexp,
IsEqual: m.Type == MatchRegexp || m.Type == MatchEqual,
})
}
func (m *Matcher) UnmarshalJSON(data []byte) error {
v1m := apiV1Matcher{
IsEqual: true,
}
if err := json.Unmarshal(data, &v1m); err != nil {
return err
}
var t MatchType
switch {
case v1m.IsEqual && !v1m.IsRegex:
t = MatchEqual
case !v1m.IsEqual && !v1m.IsRegex:
t = MatchNotEqual
case v1m.IsEqual && v1m.IsRegex:
t = MatchRegexp
case !v1m.IsEqual && v1m.IsRegex:
t = MatchNotRegexp
}
matcher, err := NewMatcher(t, v1m.Name, v1m.Value)
if err != nil {
return err
}
*m = *matcher
return nil
}
// openMetricsEscape is similar to the usual string escaping, but more
// restricted. It merely replaces a new-line character with '\n', a double-quote
// character with '\"', and a backslash with '\\', which is the escaping used by
// OpenMetrics.
func openMetricsEscape(s string) string {
r := strings.NewReplacer(
`\`, `\\`,
"\n", `\n`,
`"`, `\"`,
)
return r.Replace(s)
}
// Matchers is a slice of Matchers that is sortable, implements Stringer, and
// provides a Matches method to match a LabelSet against all Matchers in the
// slice. Note that some users of Matchers might require it to be sorted.
type Matchers []*Matcher
func (ms Matchers) Len() int { return len(ms) }
func (ms Matchers) Swap(i, j int) { ms[i], ms[j] = ms[j], ms[i] }
func (ms Matchers) Less(i, j int) bool {
if ms[i].Name > ms[j].Name {
return false
}
if ms[i].Name < ms[j].Name {
return true
}
if ms[i].Value > ms[j].Value {
return false
}
if ms[i].Value < ms[j].Value {
return true
}
return ms[i].Type < ms[j].Type
}
// Matches checks whether all matchers are fulfilled against the given label set.
func (ms Matchers) Matches(lset model.LabelSet) bool {
for _, m := range ms {
if !m.Matches(string(lset[model.LabelName(m.Name)])) {
return false
}
}
return true
}
func (ms Matchers) String() string {
var buf bytes.Buffer
buf.WriteByte('{')
for i, m := range ms {
if i > 0 {
buf.WriteByte(',')
}
buf.WriteString(m.String())
}
buf.WriteByte('}')
return buf.String()
}
// This is copied from matcher/parse/lexer.go. It will be removed when
// the transition window from classic matchers to UTF-8 matchers is complete,
// as then we can use double quotes when printing the label name for all
// matchers. Until then, the classic parser does not understand double quotes
// around the label name, so we use this function as a heuristic to tell if
// the matcher was parsed with the UTF-8 parser or the classic parser.
func isReserved(r rune) bool {
return unicode.IsSpace(r) || strings.ContainsRune("{}!=~,\\\"'`", r)
}
+178
View File
@@ -0,0 +1,178 @@
// Copyright 2018 Prometheus Team
// 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 labels
import (
"fmt"
"regexp"
"strings"
"unicode/utf8"
)
var (
// '=~' has to come before '=' because otherwise only the '='
// will be consumed, and the '~' will be part of the 3rd token.
re = regexp.MustCompile(`^\s*([a-zA-Z_:][a-zA-Z0-9_:]*)\s*(=~|=|!=|!~)\s*((?s).*?)\s*$`)
typeMap = map[string]MatchType{
"=": MatchEqual,
"!=": MatchNotEqual,
"=~": MatchRegexp,
"!~": MatchNotRegexp,
}
)
// ParseMatchers parses a comma-separated list of Matchers. A leading '{' and/or
// a trailing '}' is optional and will be trimmed before further
// parsing. Individual Matchers are separated by commas outside of quoted parts
// of the input string. Those commas may be surrounded by whitespace. Parts of the
// string inside unescaped double quotes ('"…"') are considered quoted (and
// commas don't act as separators there). If double quotes are escaped with a
// single backslash ('\"'), they are ignored for the purpose of identifying
// quoted parts of the input string. If the input string, after trimming the
// optional trailing '}', ends with a comma, followed by optional whitespace,
// this comma and whitespace will be trimmed.
//
// Examples for valid input strings:
//
// {foo = "bar", dings != "bums", }
// foo=bar,dings!=bums
// foo=bar, dings!=bums
// {quote="She said: \"Hi, ladies! That's gender-neutral…\""}
// statuscode=~"5.."
//
// See ParseMatcher for details on how an individual Matcher is parsed.
func ParseMatchers(s string) ([]*Matcher, error) {
matchers := []*Matcher{}
s = strings.TrimPrefix(s, "{")
s = strings.TrimSuffix(s, "}")
var (
insideQuotes bool
escaped bool
token strings.Builder
tokens []string
)
for _, r := range s {
switch r {
case ',':
if !insideQuotes {
tokens = append(tokens, token.String())
token.Reset()
continue
}
case '"':
if !escaped {
insideQuotes = !insideQuotes
} else {
escaped = false
}
case '\\':
escaped = !escaped
default:
escaped = false
}
token.WriteRune(r)
}
if s := strings.TrimSpace(token.String()); s != "" {
tokens = append(tokens, s)
}
for _, token := range tokens {
m, err := ParseMatcher(token)
if err != nil {
return nil, err
}
matchers = append(matchers, m)
}
return matchers, nil
}
// ParseMatcher parses a matcher with a syntax inspired by PromQL and
// OpenMetrics. This syntax is convenient to describe filters and selectors in
// UIs and config files. To support the interactive nature of the use cases, the
// parser is in various aspects fairly tolerant.
//
// The syntax of a matcher consists of three tokens: (1) A valid Prometheus
// label name. (2) One of '=', '!=', '=~', or '!~', with the same meaning as
// known from PromQL selectors. (3) A UTF-8 string, which may be enclosed in
// double quotes. Before or after each token, there may be any amount of
// whitespace, which will be discarded. The 3rd token may be the empty
// string. Within the 3rd token, OpenMetrics escaping rules apply: '\"' for a
// double-quote, '\n' for a line feed, '\\' for a literal backslash. Unescaped
// '"' must not occur inside the 3rd token (only as the 1st or last
// character). However, literal line feed characters are tolerated, as are
// single '\' characters not followed by '\', 'n', or '"'. They act as a literal
// backslash in that case.
func ParseMatcher(s string) (_ *Matcher, err error) {
ms := re.FindStringSubmatch(s)
if len(ms) == 0 {
return nil, fmt.Errorf("bad matcher format: %s", s)
}
var (
rawValue = ms[3]
value strings.Builder
escaped bool
expectTrailingQuote bool
)
if after, ok := strings.CutPrefix(rawValue, "\""); ok {
rawValue = after
expectTrailingQuote = true
}
if !utf8.ValidString(rawValue) {
return nil, fmt.Errorf("matcher value not valid UTF-8: %s", ms[3])
}
// Unescape the rawValue.
for i, r := range rawValue {
if escaped {
escaped = false
switch r {
case 'n':
value.WriteByte('\n')
case '"', '\\':
value.WriteRune(r)
default:
// This was a spurious escape, so treat the '\' as literal.
value.WriteByte('\\')
value.WriteRune(r)
}
continue
}
switch r {
case '\\':
if i < len(rawValue)-1 {
escaped = true
continue
}
// '\' encountered as last byte. Treat it as literal.
value.WriteByte('\\')
case '"':
if !expectTrailingQuote || i < len(rawValue)-1 {
return nil, fmt.Errorf("matcher value contains unescaped double quote: %s", ms[3])
}
expectTrailingQuote = false
default:
value.WriteRune(r)
}
}
if expectTrailingQuote {
return nil, fmt.Errorf("matcher value contains unescaped double quote: %s", ms[3])
}
return NewMatcher(typeMap[ms[2]], ms[1], value.String())
}
+7
View File
@@ -0,0 +1,7 @@
FROM node:20-alpine
ENV NODE_PATH="/usr/local/lib/node_modules"
RUN npm install juice@10.0.1 -g
ENTRYPOINT [""]
+17
View File
@@ -0,0 +1,17 @@
DOCKER_IMG := alertmanager-template
DOCKER_RUN_CURRENT_USER := docker run --user=$(shell id -u $(USER)):$(shell id -g $(USER))
DOCKER_CMD := $(DOCKER_RUN_CURRENT_USER) --rm -t -v $(PWD):/app -w /app $(DOCKER_IMG)
ifeq ($(NO_DOCKER), true)
DOCKER_CMD=
endif
template-image:
@(if [ "$(NO_DOCKER)" != "true" ] ; then \
echo ">> build template docker image"; \
docker build -t $(DOCKER_IMG) . > /dev/null; \
fi; )
email.tmpl: template-image email.html
@echo ">> inline css for html email template"
$(DOCKER_CMD) ./inline-css.js
+230
View File
@@ -0,0 +1,230 @@
{{ define "__alertmanager" }}Alertmanager{{ end }}
{{ define "__alertmanagerURL" }}{{ .ExternalURL }}/#/alerts?receiver={{ .Receiver | urlquery }}{{ end }}
{{ define "__subject" }}[{{ .Status | toUpper }}{{ if eq .Status "firing" }}:{{ .Alerts.Firing | len }}{{ end }}] {{ .GroupLabels.SortedPairs.Values | join " " }} {{ if gt (len .CommonLabels) (len .GroupLabels) }}({{ with .CommonLabels.Remove .GroupLabels.Names }}{{ .Values | join " " }}{{ end }}){{ end }}{{ end }}
{{ define "__description" }}{{ end }}
{{ define "__text_alert_list" }}{{ range . }}Labels:
{{ range .Labels.SortedPairs }} - {{ .Name }} = {{ .Value }}
{{ end }}Annotations:
{{ range .Annotations.SortedPairs }} - {{ .Name }} = {{ .Value }}
{{ end }}Source: {{ .GeneratorURL }}
{{ end }}{{ end }}
{{ define "__text_alert_list_markdown" }}{{ range . }}
Labels:
{{ range .Labels.SortedPairs }} - {{ .Name }} = {{ .Value }}
{{ end }}
Annotations:
{{ range .Annotations.SortedPairs }} - {{ .Name }} = {{ .Value }}
{{ end }}
Source: {{ .GeneratorURL }}
{{ end }}
{{ end }}
{{ define "slack.default.title" }}{{ template "__subject" . }}{{ end }}
{{ define "slack.default.username" }}{{ template "__alertmanager" . }}{{ end }}
{{ define "slack.default.fallback" }}{{ template "slack.default.title" . }} | {{ template "slack.default.titlelink" . }}{{ end }}
{{ define "slack.default.callbackid" }}{{ end }}
{{ define "slack.default.pretext" }}{{ end }}
{{ define "slack.default.titlelink" }}{{ template "__alertmanagerURL" . }}{{ end }}
{{ define "slack.default.iconemoji" }}{{ end }}
{{ define "slack.default.iconurl" }}{{ end }}
{{ define "slack.default.text" }}{{ end }}
{{ define "slack.default.footer" }}{{ end }}
{{ define "pagerduty.default.description" }}{{ template "__subject" . }}{{ end }}
{{ define "pagerduty.default.client" }}{{ template "__alertmanager" . }}{{ end }}
{{ define "pagerduty.default.clientURL" }}{{ template "__alertmanagerURL" . }}{{ end }}
{{ define "pagerduty.default.instances" }}{{ template "__text_alert_list" . }}{{ end }}
{{ define "opsgenie.default.message" }}{{ template "__subject" . }}{{ end }}
{{ define "opsgenie.default.description" }}{{ .CommonAnnotations.SortedPairs.Values | join " " }}
{{ if gt (len .Alerts.Firing) 0 -}}
Alerts Firing:
{{ template "__text_alert_list" .Alerts.Firing }}
{{- end }}
{{ if gt (len .Alerts.Resolved) 0 -}}
Alerts Resolved:
{{ template "__text_alert_list" .Alerts.Resolved }}
{{- end }}
{{- end }}
{{ define "opsgenie.default.source" }}{{ template "__alertmanagerURL" . }}{{ end }}
{{ define "wechat.default.message" }}{{ template "__subject" . }}
{{ .CommonAnnotations.SortedPairs.Values | join " " }}
{{ if gt (len .Alerts.Firing) 0 -}}
Alerts Firing:
{{ template "__text_alert_list" .Alerts.Firing }}
{{- end }}
{{ if gt (len .Alerts.Resolved) 0 -}}
Alerts Resolved:
{{ template "__text_alert_list" .Alerts.Resolved }}
{{- end }}
AlertmanagerUrl:
{{ template "__alertmanagerURL" . }}
{{- end }}
{{ define "wechat.default.to_user" }}{{ end }}
{{ define "wechat.default.to_party" }}{{ end }}
{{ define "wechat.default.to_tag" }}{{ end }}
{{ define "wechat.default.agent_id" }}{{ end }}
{{ define "victorops.default.state_message" }}{{ .CommonAnnotations.SortedPairs.Values | join " " }}
{{ if gt (len .Alerts.Firing) 0 -}}
Alerts Firing:
{{ template "__text_alert_list" .Alerts.Firing }}
{{- end }}
{{ if gt (len .Alerts.Resolved) 0 -}}
Alerts Resolved:
{{ template "__text_alert_list" .Alerts.Resolved }}
{{- end }}
{{- end }}
{{ define "victorops.default.entity_display_name" }}{{ template "__subject" . }}{{ end }}
{{ define "victorops.default.monitoring_tool" }}{{ template "__alertmanager" . }}{{ end }}
{{ define "pushover.default.title" }}{{ template "__subject" . }}{{ end }}
{{ define "pushover.default.message" }}{{ .CommonAnnotations.SortedPairs.Values | join " " }}
{{ if gt (len .Alerts.Firing) 0 }}
Alerts Firing:
{{ template "__text_alert_list" .Alerts.Firing }}
{{ end }}
{{ if gt (len .Alerts.Resolved) 0 }}
Alerts Resolved:
{{ template "__text_alert_list" .Alerts.Resolved }}
{{ end }}
{{ end }}
{{ define "pushover.default.url" }}{{ template "__alertmanagerURL" . }}{{ end }}
{{ define "sns.default.subject" }}{{ template "__subject" . }}{{ end }}
{{ define "sns.default.message" }}{{ .CommonAnnotations.SortedPairs.Values | join " " }}
{{ if gt (len .Alerts.Firing) 0 }}
Alerts Firing:
{{ template "__text_alert_list" .Alerts.Firing }}
{{ end }}
{{ if gt (len .Alerts.Resolved) 0 }}
Alerts Resolved:
{{ template "__text_alert_list" .Alerts.Resolved }}
{{ end }}
{{ end }}
{{ define "telegram.default.message" }}
{{ if gt (len .Alerts.Firing) 0 }}
Alerts Firing:
{{ template "__text_alert_list" .Alerts.Firing }}
{{ end }}
{{ if gt (len .Alerts.Resolved) 0 }}
Alerts Resolved:
{{ template "__text_alert_list" .Alerts.Resolved }}
{{ end }}
{{ end }}
{{ define "discord.default.content" }}{{ end }}
{{ define "discord.default.title" }}{{ template "__subject" . }}{{ end }}
{{ define "discord.default.message" }}
{{ if gt (len .Alerts.Firing) 0 }}
Alerts Firing:
{{ template "__text_alert_list" .Alerts.Firing }}
{{ end }}
{{ if gt (len .Alerts.Resolved) 0 }}
Alerts Resolved:
{{ template "__text_alert_list" .Alerts.Resolved }}
{{ end }}
{{ end }}
{{ define "webex.default.message" }}{{ .CommonAnnotations.SortedPairs.Values | join " " }}
{{ if gt (len .Alerts.Firing) 0 }}
Alerts Firing:
{{ template "__text_alert_list" .Alerts.Firing }}
{{ end }}
{{ if gt (len .Alerts.Resolved) 0 }}
Alerts Resolved:
{{ template "__text_alert_list" .Alerts.Resolved }}
{{ end }}
{{ end }}
{{ define "msteams.default.summary" }}{{ template "__subject" . }}{{ end }}
{{ define "msteams.default.title" }}{{ template "__subject" . }}{{ end }}
{{ define "msteams.default.text" }}
{{ if gt (len .Alerts.Firing) 0 }}
# Alerts Firing:
{{ template "__text_alert_list_markdown" .Alerts.Firing }}
{{ end }}
{{ if gt (len .Alerts.Resolved) 0 }}
# Alerts Resolved:
{{ template "__text_alert_list_markdown" .Alerts.Resolved }}
{{ end }}
{{ end }}
{{ define "msteamsv2.default.title" }}{{ template "__subject" . }}{{ end }}
{{ define "msteamsv2.default.text" }}
{{ if gt (len .Alerts.Firing) 0 }}
# Alerts Firing:
{{ template "__text_alert_list_markdown" .Alerts.Firing }}
{{ end }}
{{ if gt (len .Alerts.Resolved) 0 }}
# Alerts Resolved:
{{ template "__text_alert_list_markdown" .Alerts.Resolved }}
{{ end }}
{{ end }}
{{ define "jira.default.summary" }}{{ template "__subject" . }}{{ end }}
{{ define "jira.default.description" }}
{{ if gt (len .Alerts.Firing) 0 }}
# Alerts Firing:
{{ template "__text_alert_list_markdown" .Alerts.Firing }}
{{ end }}
{{ if gt (len .Alerts.Resolved) 0 }}
# Alerts Resolved:
{{ template "__text_alert_list_markdown" .Alerts.Resolved }}
{{ end }}
{{ end }}
{{- define "jira.default.priority" -}}
{{- $priority := "" }}
{{- range .Alerts.Firing -}}
{{- $severity := index .Labels "severity" -}}
{{- if (eq $severity "critical") -}}
{{- $priority = "High" -}}
{{- else if (and (eq $severity "warning") (ne $priority "High")) -}}
{{- $priority = "Medium" -}}
{{- else if (and (eq $severity "info") (eq $priority "")) -}}
{{- $priority = "Low" -}}
{{- end -}}
{{- end -}}
{{- if eq $priority "" -}}
{{- range .Alerts.Resolved -}}
{{- $severity := index .Labels "severity" -}}
{{- if (eq $severity "critical") -}}
{{- $priority = "High" -}}
{{- else if (and (eq $severity "warning") (ne $priority "High")) -}}
{{- $priority = "Medium" -}}
{{- else if (and (eq $severity "info") (eq $priority "")) -}}
{{- $priority = "Low" -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- $priority -}}
{{- end -}}
{{ define "rocketchat.default.title" }}{{ template "__subject" . }}{{ end }}
{{ define "rocketchat.default.alias" }}{{ template "__alertmanager" . }}{{ end }}
{{ define "rocketchat.default.titlelink" }}{{ template "__alertmanagerURL" . }}{{ end }}
{{ define "rocketchat.default.emoji" }}{{ end }}
{{ define "rocketchat.default.iconurl" }}{{ end }}
{{ define "rocketchat.default.text" }}{{ end }}
{{ define "mattermost.default.text" }}
{{ if gt (len .Alerts.Firing) 0 }}
# Alerts Firing:
{{ template "__text_alert_list_markdown" .Alerts.Firing }}
{{ end }}
{{ if gt (len .Alerts.Resolved) 0 }}
# Alerts Resolved:
{{ template "__text_alert_list_markdown" .Alerts.Resolved }}
{{ end }}
{{ end }}
+412
View File
@@ -0,0 +1,412 @@
<!--
Style and HTML derived from https://github.com/mailgun/transactional-email-templates
The MIT License (MIT)
Copyright (c) 2014 Mailgun
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-->
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta name="viewport" content="width=device-width" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>{{ template "__subject" . }}</title>
<style>
/* -------------------------------------
GLOBAL
A very basic CSS reset
------------------------------------- */
* {
margin: 0;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
}
img {
max-width: 100%;
}
body {
-webkit-font-smoothing: antialiased;
-webkit-text-size-adjust: none;
width: 100% !important;
height: 100%;
line-height: 1.6em;
/* 1.6em * 14px = 22.4px, use px to get airier line-height also in Thunderbird, and Yahoo!, Outlook.com, AOL webmail clients */
/*line-height: 22px;*/
}
/* Let's make sure all tables have defaults */
table td {
vertical-align: top;
}
/* -------------------------------------
BODY & CONTAINER
------------------------------------- */
body {
background-color: #f6f6f6;
}
.body-wrap {
background-color: #f6f6f6;
width: 100%;
}
.container {
display: block !important;
max-width: 600px !important;
margin: 0 auto !important;
/* makes it centered */
clear: both !important;
}
.content {
max-width: 600px;
margin: 0 auto;
display: block;
padding: 20px;
}
/* -------------------------------------
HEADER, FOOTER, MAIN
------------------------------------- */
.main {
background-color: #fff;
border: 1px solid #e9e9e9;
border-radius: 3px;
}
.content-wrap {
padding: 30px;
}
.content-block {
padding: 0 0 20px;
}
.header {
width: 100%;
margin-bottom: 20px;
}
.footer {
width: 100%;
clear: both;
color: #999;
padding: 20px;
}
.footer p, .footer a, .footer td {
color: #999;
font-size: 12px;
}
/* -------------------------------------
TYPOGRAPHY
------------------------------------- */
h1, h2, h3 {
font-family: "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif;
color: #000;
margin: 40px 0 0;
line-height: 1.2em;
font-weight: 400;
}
h1 {
font-size: 32px;
font-weight: 500;
/* 1.2em * 32px = 38.4px, use px to get airier line-height also in Thunderbird, and Yahoo!, Outlook.com, AOL webmail clients */
/*line-height: 38px;*/
}
h2 {
font-size: 24px;
/* 1.2em * 24px = 28.8px, use px to get airier line-height also in Thunderbird, and Yahoo!, Outlook.com, AOL webmail clients */
/*line-height: 29px;*/
}
h3 {
font-size: 18px;
/* 1.2em * 18px = 21.6px, use px to get airier line-height also in Thunderbird, and Yahoo!, Outlook.com, AOL webmail clients */
/*line-height: 22px;*/
}
h4 {
font-size: 14px;
font-weight: 600;
}
p, ul, ol {
margin-bottom: 10px;
font-weight: normal;
}
p li, ul li, ol li {
margin-left: 5px;
list-style-position: inside;
}
/* -------------------------------------
LINKS & BUTTONS
------------------------------------- */
a {
color: #348eda;
text-decoration: underline;
}
.btn-primary {
text-decoration: none;
color: #FFF;
background-color: #348eda;
border: solid #348eda;
border-width: 10px 20px;
line-height: 2em;
/* 2em * 14px = 28px, use px to get airier line-height also in Thunderbird, and Yahoo!, Outlook.com, AOL webmail clients */
/*line-height: 28px;*/
font-weight: bold;
text-align: center;
cursor: pointer;
display: inline-block;
border-radius: 5px;
text-transform: capitalize;
}
/* -------------------------------------
OTHER STYLES THAT MIGHT BE USEFUL
------------------------------------- */
.last {
margin-bottom: 0;
}
.first {
margin-top: 0;
}
.aligncenter {
text-align: center;
}
.alignright {
text-align: right;
}
.alignleft {
text-align: left;
}
.clear {
clear: both;
}
/* -------------------------------------
ALERTS
Change the class depending on warning email, good email or bad email
------------------------------------- */
.alert {
font-size: 16px;
color: #fff;
font-weight: 500;
padding: 20px;
text-align: center;
border-radius: 3px 3px 0 0;
}
.alert a {
color: #fff;
text-decoration: none;
font-weight: 500;
font-size: 16px;
}
.alert.alert-warning {
background-color: #E6522C;
}
.alert.alert-bad {
background-color: #D0021B;
}
.alert.alert-good {
background-color: #68B90F;
}
/* -------------------------------------
INVOICE
Styles for the billing table
------------------------------------- */
.invoice {
margin: 40px auto;
text-align: left;
width: 80%;
}
.invoice td {
padding: 5px 0;
}
.invoice .invoice-items {
width: 100%;
}
.invoice .invoice-items td {
border-top: #eee 1px solid;
}
.invoice .invoice-items .total td {
border-top: 2px solid #333;
border-bottom: 2px solid #333;
font-weight: 700;
}
/* -------------------------------------
RESPONSIVE AND MOBILE FRIENDLY STYLES
------------------------------------- */
@media only screen and (max-width: 640px) {
body {
padding: 0 !important;
}
h1, h2, h3, h4 {
font-weight: 800 !important;
margin: 20px 0 5px !important;
}
h1 {
font-size: 22px !important;
}
h2 {
font-size: 18px !important;
}
h3 {
font-size: 16px !important;
}
.container {
padding: 0 !important;
width: 100% !important;
}
.content {
padding: 0 !important;
}
.content-wrap {
padding: 10px !important;
}
.invoice {
width: 100% !important;
}
}
</style>
</head>
<body itemscope itemtype="https://schema.org/EmailMessage">
<table class="body-wrap">
<tr>
<td></td>
<td class="container" width="600">
<div class="content">
<table class="main" width="100%" cellpadding="0" cellspacing="0">
<tr>
{{ if gt (len .Alerts.Firing) 0 }}
<td class="alert alert-warning">
{{ .Alerts | len }} alert{{ if gt (len .Alerts) 1 }}s{{ end }} for {{ range .GroupLabels.SortedPairs }}
{{ .Name }}={{ .Value }}
{{ end }}
</td>
{{ else }}
<td class="alert alert-good">
{{ .Alerts | len }} alert{{ if gt (len .Alerts) 1 }}s{{ end }} for {{ range .GroupLabels.SortedPairs }}
{{ .Name }}={{ .Value }}
{{ end }}
</td>
{{ end }}
</tr>
<tr>
<td class="content-wrap">
<table width="100%" cellpadding="0" cellspacing="0">
<tr>
<td class="content-block">
<a href='{{ template "__alertmanagerURL" . }}' class="btn-primary">View in {{ template "__alertmanager" . }}</a>
</td>
</tr>
{{ if gt (len .Alerts.Firing) 0 }}
<tr>
<td class="content-block">
<strong>[{{ .Alerts.Firing | len }}] Firing</strong>
</td>
</tr>
{{ end }}
{{ range .Alerts.Firing }}
<tr>
<td class="content-block">
<strong>Labels</strong><br />
{{ range .Labels.SortedPairs }}{{ .Name }} = {{ .Value }}<br />{{ end }}
{{ if gt (len .Annotations) 0 }}<strong>Annotations</strong><br />{{ end }}
{{ range .Annotations.SortedPairs }}{{ .Name }} = {{ .Value }}<br />{{ end }}
<a href="{{ .GeneratorURL }}">Source</a><br />
</td>
</tr>
{{ end }}
{{ if gt (len .Alerts.Resolved) 0 }}
{{ if gt (len .Alerts.Firing) 0 }}
<tr>
<td class="content-block">
<br />
<hr />
<br />
</td>
</tr>
{{ end }}
<tr>
<td class="content-block">
<strong>[{{ .Alerts.Resolved | len }}] Resolved</strong>
</td>
</tr>
{{ end }}
{{ range .Alerts.Resolved }}
<tr>
<td class="content-block">
<strong>Labels</strong><br />
{{ range .Labels.SortedPairs }}{{ .Name }} = {{ .Value }}<br />{{ end }}
{{ if gt (len .Annotations) 0 }}<strong>Annotations</strong><br />{{ end }}
{{ range .Annotations.SortedPairs }}{{ .Name }} = {{ .Value }}<br />{{ end }}
<a href="{{ .GeneratorURL }}">Source</a><br />
</td>
</tr>
{{ end }}
</table>
</td>
</tr>
</table>
<div class="footer">
<table width="100%">
<tr>
<td class="aligncenter content-block"><a href='{{ .ExternalURL }}'>Sent by {{ template "__alertmanager" . }}</a></td>
</tr>
</table>
</div></div>
</td>
<td></td>
</tr>
</table>
</body>
</html>
+179
View File
@@ -0,0 +1,179 @@
{{ define "email.default.subject" }}{{ template "__subject" . }}{{ end }}
{{ define "email.default.html" }}
<!--
Style and HTML derived from https://github.com/mailgun/transactional-email-templates
The MIT License (MIT)
Copyright (c) 2014 Mailgun
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-->
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
<head>
<meta name="viewport" content="width=device-width">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>{{ template "__subject" . }}</title>
<style>
@media only screen and (max-width: 640px) {
body {
padding: 0 !important;
}
h1,
h2,
h3,
h4 {
font-weight: 800 !important;
margin: 20px 0 5px !important;
}
h1 {
font-size: 22px !important;
}
h2 {
font-size: 18px !important;
}
h3 {
font-size: 16px !important;
}
.container {
padding: 0 !important;
width: 100% !important;
}
.content {
padding: 0 !important;
}
.content-wrap {
padding: 10px !important;
}
.invoice {
width: 100% !important;
}
}
</style>
</head>
<body itemscope itemtype="https://schema.org/EmailMessage" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; height: 100%; line-height: 1.6em; background-color: #f6f6f6; width: 100%;">
<table class="body-wrap" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; background-color: #f6f6f6; width: 100%;" width="100%" bgcolor="#f6f6f6">
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
<td style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top;" valign="top"></td>
<td class="container" width="600" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; display: block; max-width: 600px; margin: 0 auto; clear: both;" valign="top">
<div class="content" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; max-width: 600px; margin: 0 auto; display: block; padding: 20px;">
<table class="main" width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; background-color: #fff; border: 1px solid #e9e9e9; border-radius: 3px;" bgcolor="#fff">
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
{{ if gt (len .Alerts.Firing) 0 }}
<td class="alert alert-warning" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; vertical-align: top; font-size: 16px; color: #fff; font-weight: 500; padding: 20px; text-align: center; border-radius: 3px 3px 0 0; background-color: #E6522C;" valign="top" align="center" bgcolor="#E6522C">
{{ .Alerts | len }} alert{{ if gt (len .Alerts) 1 }}s{{ end }} for {{ range .GroupLabels.SortedPairs }}
{{ .Name }}={{ .Value }}
{{ end }}
</td>
{{ else }}
<td class="alert alert-good" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; vertical-align: top; font-size: 16px; color: #fff; font-weight: 500; padding: 20px; text-align: center; border-radius: 3px 3px 0 0; background-color: #68B90F;" valign="top" align="center" bgcolor="#68B90F">
{{ .Alerts | len }} alert{{ if gt (len .Alerts) 1 }}s{{ end }} for {{ range .GroupLabels.SortedPairs }}
{{ .Name }}={{ .Value }}
{{ end }}
</td>
{{ end }}
</tr>
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
<td class="content-wrap" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; padding: 30px;" valign="top">
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
<td class="content-block" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; padding: 0 0 20px;" valign="top">
<a href="{{ template "__alertmanagerURL" . }}" class="btn-primary" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; text-decoration: none; color: #FFF; background-color: #348eda; border: solid #348eda; border-width: 10px 20px; line-height: 2em; font-weight: bold; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; text-transform: capitalize;">View in {{ template "__alertmanager" . }}</a>
</td>
</tr>
{{ if gt (len .Alerts.Firing) 0 }}
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
<td class="content-block" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; padding: 0 0 20px;" valign="top">
<strong style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">[{{ .Alerts.Firing | len }}] Firing</strong>
</td>
</tr>
{{ end }}
{{ range .Alerts.Firing }}
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
<td class="content-block" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; padding: 0 0 20px;" valign="top">
<strong style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">Labels</strong><br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
{{ range .Labels.SortedPairs }}{{ .Name }} = {{ .Value }}<br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">{{ end }}
{{ if gt (len .Annotations) 0 }}<strong style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">Annotations</strong><br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">{{ end }}
{{ range .Annotations.SortedPairs }}{{ .Name }} = {{ .Value }}<br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">{{ end }}
<a href="{{ .GeneratorURL }}" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; color: #348eda; text-decoration: underline;">Source</a><br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
</td>
</tr>
{{ end }}
{{ if gt (len .Alerts.Resolved) 0 }}
{{ if gt (len .Alerts.Firing) 0 }}
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
<td class="content-block" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; padding: 0 0 20px;" valign="top">
<br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
<hr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
<br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
</td>
</tr>
{{ end }}
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
<td class="content-block" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; padding: 0 0 20px;" valign="top">
<strong style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">[{{ .Alerts.Resolved | len }}] Resolved</strong>
</td>
</tr>
{{ end }}
{{ range .Alerts.Resolved }}
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
<td class="content-block" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; padding: 0 0 20px;" valign="top">
<strong style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">Labels</strong><br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
{{ range .Labels.SortedPairs }}{{ .Name }} = {{ .Value }}<br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">{{ end }}
{{ if gt (len .Annotations) 0 }}<strong style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">Annotations</strong><br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">{{ end }}
{{ range .Annotations.SortedPairs }}{{ .Name }} = {{ .Value }}<br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">{{ end }}
<a href="{{ .GeneratorURL }}" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; color: #348eda; text-decoration: underline;">Source</a><br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
</td>
</tr>
{{ end }}
</table>
</td>
</tr>
</table>
<div class="footer" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; width: 100%; clear: both; color: #999; padding: 20px;">
<table width="100%" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
<td class="aligncenter content-block" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; vertical-align: top; padding: 0 0 20px; text-align: center; color: #999; font-size: 12px;" valign="top" align="center"><a href="{{ .ExternalURL }}" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; text-decoration: underline; color: #999; font-size: 12px;">Sent by {{ template "__alertmanager" . }}</a></td>
</tr>
</table>
</div></div>
</td>
<td style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top;" valign="top"></td>
</tr>
</table>
</body>
</html>
{{ end }}
@@ -0,0 +1,40 @@
#!/usr/bin/env node
// Copyright 2021 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.
const juice = require('juice')
const fs = require('fs')
const inputFile = 'email.html'
const outputFile = 'email.tmpl'
var inputData = ''
try {
inputData = fs.readFileSync(inputFile, 'utf8')
} catch (err) {
console.error(err)
process.exit(1)
}
var templateData = juice(inputData)
const outputData = `
{{ define "email.default.subject" }}{{ template "__subject" . }}{{ end }}
{{ define "email.default.html" }}
${templateData}
{{ end }}
`
fs.writeFileSync(outputFile, outputData)
+502
View File
@@ -0,0 +1,502 @@
// Copyright 2015 Prometheus Team
// 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"
"encoding/json"
tmplhtml "html/template"
"io"
"net/url"
"path"
"path/filepath"
"reflect"
"regexp"
"sort"
"strings"
tmpltext "text/template"
"time"
commonTemplates "github.com/prometheus/common/helpers/templates"
"github.com/prometheus/common/model"
"golang.org/x/text/cases"
"golang.org/x/text/language"
"gopkg.in/yaml.v2"
"github.com/prometheus/alertmanager/asset"
"github.com/prometheus/alertmanager/types"
)
// Template bundles a text and a html template instance.
type Template struct {
text *tmpltext.Template
html *tmplhtml.Template
ExternalURL *url.URL
}
// Option is generic modifier of the text and html templates used by a Template.
type Option func(text *tmpltext.Template, html *tmplhtml.Template)
// New returns a new Template with the DefaultFuncs added. The DefaultFuncs
// have precedence over any added custom functions. Options allow customization
// of the text and html templates in given order.
func New(options ...Option) (*Template, error) {
t := &Template{
text: tmpltext.New("").Option("missingkey=zero"),
html: tmplhtml.New("").Option("missingkey=zero"),
}
for _, o := range options {
o(t.text, t.html)
}
t.text.Funcs(tmpltext.FuncMap(DefaultFuncs))
t.html.Funcs(tmplhtml.FuncMap(DefaultFuncs))
return t, nil
}
// FromGlobs calls ParseGlob on all path globs provided and returns the
// resulting Template.
func FromGlobs(paths []string, options ...Option) (*Template, error) {
t, err := New(options...)
if err != nil {
return nil, err
}
defaultTemplates := []string{"default.tmpl", "email.tmpl"}
for _, file := range defaultTemplates {
f, err := asset.Assets.Open(path.Join("/templates", file))
if err != nil {
return nil, err
}
if err := t.Parse(f); err != nil {
f.Close()
return nil, err
}
f.Close()
}
for _, tp := range paths {
if err := t.FromGlob(tp); err != nil {
return nil, err
}
}
return t, nil
}
// Parse parses the given text into the template.
func (t *Template) Parse(r io.Reader) error {
b, err := io.ReadAll(r)
if err != nil {
return err
}
if t.text, err = t.text.Parse(string(b)); err != nil {
return err
}
if t.html, err = t.html.Parse(string(b)); err != nil {
return err
}
return nil
}
// FromGlob calls ParseGlob on given path glob provided and parses into the
// template.
func (t *Template) FromGlob(path string) error {
// ParseGlob in the template packages errors if not at least one file is
// matched. We want to allow empty matches that may be populated later on.
p, err := filepath.Glob(path)
if err != nil {
return err
}
if len(p) > 0 {
if t.text, err = t.text.ParseGlob(path); err != nil {
return err
}
if t.html, err = t.html.ParseGlob(path); err != nil {
return err
}
}
return nil
}
// ExecuteTextString needs a meaningful doc comment (TODO(fabxc)).
func (t *Template) ExecuteTextString(text string, data any) (string, error) {
if text == "" {
return "", nil
}
tmpl, err := t.text.Clone()
if err != nil {
return "", err
}
tmpl, err = tmpl.New("").Option("missingkey=zero").Parse(text)
if err != nil {
return "", err
}
var buf bytes.Buffer
err = tmpl.Execute(&buf, data)
return buf.String(), err
}
// ExecuteHTMLString needs a meaningful doc comment (TODO(fabxc)).
func (t *Template) ExecuteHTMLString(html string, data any) (string, error) {
if html == "" {
return "", nil
}
tmpl, err := t.html.Clone()
if err != nil {
return "", err
}
tmpl, err = tmpl.New("").Option("missingkey=zero").Parse(html)
if err != nil {
return "", err
}
var buf bytes.Buffer
err = tmpl.Execute(&buf, data)
return buf.String(), err
}
type FuncMap map[string]any
var DefaultFuncs = FuncMap{
"toUpper": strings.ToUpper,
"toLower": strings.ToLower,
"title": func(text string) string {
// Casers should not be shared between goroutines, instead
// create a new caser each time this function is called.
return cases.Title(language.AmericanEnglish).String(text)
},
"trimSpace": strings.TrimSpace,
// join is equal to strings.Join but inverts the argument order
// for easier pipelining in templates.
"join": func(sep string, s []string) string {
return strings.Join(s, sep)
},
"match": regexp.MatchString,
"safeHtml": func(text string) tmplhtml.HTML {
return tmplhtml.HTML(text)
},
"safeUrl": func(text string) tmplhtml.URL {
return tmplhtml.URL(text)
},
"urlUnescape": url.QueryUnescape,
"reReplaceAll": func(pattern, repl, text string) string {
re := regexp.MustCompile(pattern)
return re.ReplaceAllString(text, repl)
},
"stringSlice": func(s ...string) []string {
return s
},
// date returns the text representation of the time in the specified format.
"date": func(fmt string, t time.Time) string {
return t.Format(fmt)
},
// tz returns the time in the timezone.
"tz": func(name string, t time.Time) (time.Time, error) {
loc, err := time.LoadLocation(name)
if err != nil {
return time.Time{}, err
}
return t.In(loc), nil
},
"since": time.Since,
"humanizeDuration": commonTemplates.HumanizeDuration,
"toJson": func(v any) (string, error) {
bytes, err := json.Marshal(v)
if err != nil {
return "", err
}
return string(bytes), nil
},
}
// Pair is a key/value string pair.
type Pair struct {
Name, Value string
}
// Pairs is a list of key/value string pairs.
type Pairs []Pair
// Names returns a list of names of the pairs.
func (ps Pairs) Names() []string {
ns := make([]string, 0, len(ps))
for _, p := range ps {
ns = append(ns, p.Name)
}
return ns
}
// Values returns a list of values of the pairs.
func (ps Pairs) Values() []string {
vs := make([]string, 0, len(ps))
for _, p := range ps {
vs = append(vs, p.Value)
}
return vs
}
func (ps Pairs) String() string {
b := strings.Builder{}
for i, p := range ps {
b.WriteString(p.Name)
b.WriteRune('=')
b.WriteString(p.Value)
if i < len(ps)-1 {
b.WriteString(", ")
}
}
return b.String()
}
// KV is a set of key/value string pairs.
type KV map[string]string
// SortedPairs returns a sorted list of key/value pairs.
func (kv KV) SortedPairs() Pairs {
var (
pairs = make([]Pair, 0, len(kv))
keys = make([]string, 0, len(kv))
sortStart = 0
)
for k := range kv {
if k == string(model.AlertNameLabel) {
keys = append([]string{k}, keys...)
sortStart = 1
} else {
keys = append(keys, k)
}
}
sort.Strings(keys[sortStart:])
for _, k := range keys {
pairs = append(pairs, Pair{k, kv[k]})
}
return pairs
}
// Remove returns a copy of the key/value set without the given keys.
func (kv KV) Remove(keys []string) KV {
keySet := make(map[string]struct{}, len(keys))
for _, k := range keys {
keySet[k] = struct{}{}
}
res := KV{}
for k, v := range kv {
if _, ok := keySet[k]; !ok {
res[k] = v
}
}
return res
}
// Names returns the names of the label names in the LabelSet.
func (kv KV) Names() []string {
return kv.SortedPairs().Names()
}
// Values returns a list of the values in the LabelSet.
func (kv KV) Values() []string {
return kv.SortedPairs().Values()
}
func (kv KV) String() string {
return kv.SortedPairs().String()
}
// Data is the data passed to notification templates and webhook pushes.
//
// End-users should not be exposed to Go's type system, as this will confuse them and prevent
// simple things like simple equality checks to fail. Map everything to float64/string.
type Data struct {
Receiver string `json:"receiver"`
Status string `json:"status"`
Alerts Alerts `json:"alerts"`
GroupLabels KV `json:"groupLabels"`
CommonLabels KV `json:"commonLabels"`
CommonAnnotations KV `json:"commonAnnotations"`
ExternalURL string `json:"externalURL"`
}
// Alert holds one alert for notification templates.
type Alert struct {
Status string `json:"status"`
Labels KV `json:"labels"`
Annotations KV `json:"annotations"`
StartsAt time.Time `json:"startsAt"`
EndsAt time.Time `json:"endsAt"`
GeneratorURL string `json:"generatorURL"`
Fingerprint string `json:"fingerprint"`
}
// Alerts is a list of Alert objects.
type Alerts []Alert
// Firing returns the subset of alerts that are firing.
func (as Alerts) Firing() []Alert {
res := []Alert{}
for _, a := range as {
if a.Status == string(model.AlertFiring) {
res = append(res, a)
}
}
return res
}
// Resolved returns the subset of alerts that are resolved.
func (as Alerts) Resolved() []Alert {
res := []Alert{}
for _, a := range as {
if a.Status == string(model.AlertResolved) {
res = append(res, a)
}
}
return res
}
// Data assembles data for template expansion.
func (t *Template) Data(recv string, groupLabels model.LabelSet, alerts ...*types.Alert) *Data {
data := &Data{
Receiver: regexp.QuoteMeta(recv),
Status: string(types.Alerts(alerts...).Status()),
Alerts: make(Alerts, 0, len(alerts)),
GroupLabels: KV{},
CommonLabels: KV{},
CommonAnnotations: KV{},
ExternalURL: t.ExternalURL.String(),
}
// The call to types.Alert is necessary to correctly resolve the internal
// representation to the user representation.
for _, a := range types.Alerts(alerts...) {
alert := Alert{
Status: string(a.Status()),
Labels: make(KV, len(a.Labels)),
Annotations: make(KV, len(a.Annotations)),
StartsAt: a.StartsAt,
EndsAt: a.EndsAt,
GeneratorURL: a.GeneratorURL,
Fingerprint: a.Fingerprint().String(),
}
for k, v := range a.Labels {
alert.Labels[string(k)] = string(v)
}
for k, v := range a.Annotations {
alert.Annotations[string(k)] = string(v)
}
data.Alerts = append(data.Alerts, alert)
}
for k, v := range groupLabels {
data.GroupLabels[string(k)] = string(v)
}
if len(alerts) >= 1 {
var (
commonLabels = alerts[0].Labels.Clone()
commonAnnotations = alerts[0].Annotations.Clone()
)
for _, a := range alerts[1:] {
if len(commonLabels) == 0 && len(commonAnnotations) == 0 {
break
}
for ln, lv := range commonLabels {
if a.Labels[ln] != lv {
delete(commonLabels, ln)
}
}
for an, av := range commonAnnotations {
if a.Annotations[an] != av {
delete(commonAnnotations, an)
}
}
}
for k, v := range commonLabels {
data.CommonLabels[string(k)] = string(v)
}
for k, v := range commonAnnotations {
data.CommonAnnotations[string(k)] = string(v)
}
}
return data
}
type TemplateFunc func(string) (string, error)
// DeepCopyWithTemplate returns a deep copy of a map/slice/array/string/int/bool or combination thereof, executing the
// provided template (with the provided data) on all string keys or values. All maps are connverted to
// map[string]any, with all non-string keys discarded.
func DeepCopyWithTemplate(value any, tmplTextFunc TemplateFunc) (any, error) {
if value == nil {
return value, nil
}
valueMeta := reflect.ValueOf(value)
switch valueMeta.Kind() {
case reflect.String:
parsed, ok := tmplTextFunc(value.(string))
if ok == nil {
var inlineType any
err := yaml.Unmarshal([]byte(parsed), &inlineType)
if err != nil || (inlineType != nil && reflect.TypeOf(inlineType).Kind() == reflect.String) {
// ignore error, thus the string is not an interface
return parsed, ok
}
return DeepCopyWithTemplate(inlineType, tmplTextFunc)
}
return parsed, ok
case reflect.Array, reflect.Slice:
arrayLen := valueMeta.Len()
converted := make([]any, arrayLen)
for i := range arrayLen {
var err error
converted[i], err = DeepCopyWithTemplate(valueMeta.Index(i).Interface(), tmplTextFunc)
if err != nil {
return nil, err
}
}
return converted, nil
case reflect.Map:
keys := valueMeta.MapKeys()
converted := make(map[string]any, len(keys))
for _, keyMeta := range keys {
var err error
strKey, isString := keyMeta.Interface().(string)
if !isString {
continue
}
strKey, err = tmplTextFunc(strKey)
if err != nil {
return nil, err
}
converted[strKey], err = DeepCopyWithTemplate(valueMeta.MapIndex(keyMeta).Interface(), tmplTextFunc)
if err != nil {
return nil, err
}
}
return converted, nil
default:
return value, nil
}
}
+553
View File
@@ -0,0 +1,553 @@
// Copyright 2015 Prometheus Team
// 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 types
import (
"context"
"fmt"
"strings"
"sync"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/common/model"
"github.com/prometheus/alertmanager/matcher/compat"
"github.com/prometheus/alertmanager/pkg/labels"
)
// AlertState is used as part of AlertStatus.
type AlertState string
// Possible values for AlertState.
const (
AlertStateUnprocessed AlertState = "unprocessed"
AlertStateActive AlertState = "active"
AlertStateSuppressed AlertState = "suppressed"
)
// AlertStatus stores the state of an alert and, as applicable, the IDs of
// silences silencing the alert and of other alerts inhibiting the alert. Note
// that currently, SilencedBy is supposed to be the complete set of the relevant
// silences while InhibitedBy may contain only a subset of the inhibiting alerts
// in practice exactly one ID. (This somewhat confusing semantics might change
// in the future.)
type AlertStatus struct {
State AlertState `json:"state"`
SilencedBy []string `json:"silencedBy"`
InhibitedBy []string `json:"inhibitedBy"`
// For internal tracking, not exposed in the API.
pendingSilences []string
silencesVersion int
}
// groupStatus stores the state of the group, and, as applicable, the names
// of all active and mute time intervals that are muting it.
type groupStatus struct {
// mutedBy contains the names of all active and mute time intervals that
// are muting it.
mutedBy []string
}
// AlertMarker helps to mark alerts as silenced and/or inhibited.
// All methods are goroutine-safe.
type AlertMarker interface {
// SetActiveOrSilenced replaces the previous SilencedBy by the provided IDs of
// active and pending silences, including the version number of the
// silences state. The set of provided IDs is supposed to represent the
// complete set of relevant silences. If no active silence IDs are provided and
// InhibitedBy is already empty, it sets the provided alert to AlertStateActive.
// Otherwise, it sets the provided alert to AlertStateSuppressed.
SetActiveOrSilenced(alert model.Fingerprint, version int, activeSilenceIDs, pendingSilenceIDs []string)
// SetInhibited replaces the previous InhibitedBy by the provided IDs of
// alerts. In contrast to SetActiveOrSilenced, the set of provided IDs is not
// expected to represent the complete set of inhibiting alerts. (In
// practice, this method is only called with one or zero IDs. However,
// this expectation might change in the future. If no IDs are provided
// and InhibitedBy is already empty, it sets the provided alert to
// AlertStateActive. Otherwise, it sets the provided alert to
// AlertStateSuppressed.
SetInhibited(alert model.Fingerprint, alertIDs ...string)
// Count alerts of the given state(s). With no state provided, count all
// alerts.
Count(...AlertState) int
// Status of the given alert.
Status(model.Fingerprint) AlertStatus
// Delete the given alert.
Delete(model.Fingerprint)
// Various methods to inquire if the given alert is in a certain
// AlertState. Silenced also returns all the active and pending
// silences, while Inhibited may return only a subset of inhibiting
// alerts. Silenced also returns the version of the silences state the
// result is based on.
Unprocessed(model.Fingerprint) bool
Active(model.Fingerprint) bool
Silenced(model.Fingerprint) (activeIDs, pendingIDs []string, version int, silenced bool)
Inhibited(model.Fingerprint) ([]string, bool)
}
// GroupMarker helps to mark groups as active or muted.
// All methods are goroutine-safe.
//
// TODO(grobinson): routeID is used in Muted and SetMuted because groupKey
// is not unique (see #3817). Once groupKey uniqueness is fixed routeID can
// be removed from the GroupMarker interface.
type GroupMarker interface {
// Muted returns true if the group is muted, otherwise false. If the group
// is muted then it also returns the names of the time intervals that muted
// it.
Muted(routeID, groupKey string) ([]string, bool)
// SetMuted marks the group as muted, and sets the names of the time
// intervals that mute it. If the list of names is nil or the empty slice
// then the muted marker is removed.
SetMuted(routeID, groupKey string, timeIntervalNames []string)
// DeleteByGroupKey removes all markers for the GroupKey.
DeleteByGroupKey(routeID, groupKey string)
}
// NewMarker returns an instance of a AlertMarker implementation.
func NewMarker(r prometheus.Registerer) *MemMarker {
m := &MemMarker{
alerts: map[model.Fingerprint]*AlertStatus{},
groups: map[string]*groupStatus{},
}
m.registerMetrics(r)
return m
}
type MemMarker struct {
alerts map[model.Fingerprint]*AlertStatus
groups map[string]*groupStatus
mtx sync.RWMutex
}
// Muted implements GroupMarker.
func (m *MemMarker) Muted(routeID, groupKey string) ([]string, bool) {
m.mtx.Lock()
defer m.mtx.Unlock()
status, ok := m.groups[routeID+groupKey]
if !ok {
return nil, false
}
return status.mutedBy, len(status.mutedBy) > 0
}
// SetMuted implements GroupMarker.
func (m *MemMarker) SetMuted(routeID, groupKey string, timeIntervalNames []string) {
m.mtx.Lock()
defer m.mtx.Unlock()
status, ok := m.groups[routeID+groupKey]
if !ok {
status = &groupStatus{}
m.groups[routeID+groupKey] = status
}
status.mutedBy = timeIntervalNames
}
func (m *MemMarker) DeleteByGroupKey(routeID, groupKey string) {
m.mtx.Lock()
defer m.mtx.Unlock()
delete(m.groups, routeID+groupKey)
}
func (m *MemMarker) registerMetrics(r prometheus.Registerer) {
newMarkedAlertMetricByState := func(st AlertState) prometheus.GaugeFunc {
return prometheus.NewGaugeFunc(
prometheus.GaugeOpts{
Name: "alertmanager_marked_alerts",
Help: "How many alerts by state are currently marked in the Alertmanager regardless of their expiry.",
ConstLabels: prometheus.Labels{"state": string(st)},
},
func() float64 {
return float64(m.Count(st))
},
)
}
alertsActive := newMarkedAlertMetricByState(AlertStateActive)
alertsSuppressed := newMarkedAlertMetricByState(AlertStateSuppressed)
alertStateUnprocessed := newMarkedAlertMetricByState(AlertStateUnprocessed)
r.MustRegister(alertsActive)
r.MustRegister(alertsSuppressed)
r.MustRegister(alertStateUnprocessed)
}
// Count implements AlertMarker.
func (m *MemMarker) Count(states ...AlertState) int {
m.mtx.RLock()
defer m.mtx.RUnlock()
if len(states) == 0 {
return len(m.alerts)
}
var count int
for _, status := range m.alerts {
for _, state := range states {
if status.State == state {
count++
}
}
}
return count
}
// SetActiveOrSilenced implements AlertMarker.
func (m *MemMarker) SetActiveOrSilenced(alert model.Fingerprint, version int, activeIDs, pendingIDs []string) {
m.mtx.Lock()
defer m.mtx.Unlock()
s, found := m.alerts[alert]
if !found {
s = &AlertStatus{}
m.alerts[alert] = s
}
s.SilencedBy = activeIDs
s.pendingSilences = pendingIDs
s.silencesVersion = version
// If there are any silence or alert IDs associated with the
// fingerprint, it is suppressed. Otherwise, set it to
// AlertStateActive.
if len(activeIDs) == 0 && len(s.InhibitedBy) == 0 {
s.State = AlertStateActive
return
}
s.State = AlertStateSuppressed
}
// SetInhibited implements AlertMarker.
func (m *MemMarker) SetInhibited(alert model.Fingerprint, ids ...string) {
m.mtx.Lock()
defer m.mtx.Unlock()
s, found := m.alerts[alert]
if !found {
s = &AlertStatus{}
m.alerts[alert] = s
}
s.InhibitedBy = ids
// If there are any silence or alert IDs associated with the
// fingerprint, it is suppressed. Otherwise, set it to
// AlertStateActive.
if len(ids) == 0 && len(s.SilencedBy) == 0 {
s.State = AlertStateActive
return
}
s.State = AlertStateSuppressed
}
// Status implements AlertMarker.
func (m *MemMarker) Status(alert model.Fingerprint) AlertStatus {
m.mtx.RLock()
defer m.mtx.RUnlock()
if s, found := m.alerts[alert]; found {
return *s
}
return AlertStatus{
State: AlertStateUnprocessed,
SilencedBy: []string{},
InhibitedBy: []string{},
}
}
// Delete implements AlertMarker.
func (m *MemMarker) Delete(alert model.Fingerprint) {
m.mtx.Lock()
defer m.mtx.Unlock()
delete(m.alerts, alert)
}
// Unprocessed implements AlertMarker.
func (m *MemMarker) Unprocessed(alert model.Fingerprint) bool {
return m.Status(alert).State == AlertStateUnprocessed
}
// Active implements AlertMarker.
func (m *MemMarker) Active(alert model.Fingerprint) bool {
return m.Status(alert).State == AlertStateActive
}
// Inhibited implements AlertMarker.
func (m *MemMarker) Inhibited(alert model.Fingerprint) ([]string, bool) {
s := m.Status(alert)
return s.InhibitedBy,
s.State == AlertStateSuppressed && len(s.InhibitedBy) > 0
}
// Silenced returns whether the alert for the given Fingerprint is in the
// Silenced state, any associated silence IDs, and the silences state version
// the result is based on.
func (m *MemMarker) Silenced(alert model.Fingerprint) (activeIDs, pendingIDs []string, version int, silenced bool) {
s := m.Status(alert)
return s.SilencedBy, s.pendingSilences, s.silencesVersion,
s.State == AlertStateSuppressed && len(s.SilencedBy) > 0
}
// MultiError contains multiple errors and implements the error interface. Its
// zero value is ready to use. All its methods are goroutine safe.
type MultiError struct {
mtx sync.Mutex
errors []error
}
// Add adds an error to the MultiError.
func (e *MultiError) Add(err error) {
e.mtx.Lock()
defer e.mtx.Unlock()
e.errors = append(e.errors, err)
}
// Len returns the number of errors added to the MultiError.
func (e *MultiError) Len() int {
e.mtx.Lock()
defer e.mtx.Unlock()
return len(e.errors)
}
// Errors returns the errors added to the MuliError. The returned slice is a
// copy of the internal slice of errors.
func (e *MultiError) Errors() []error {
e.mtx.Lock()
defer e.mtx.Unlock()
return append(make([]error, 0, len(e.errors)), e.errors...)
}
func (e *MultiError) Error() string {
e.mtx.Lock()
defer e.mtx.Unlock()
es := make([]string, 0, len(e.errors))
for _, err := range e.errors {
es = append(es, err.Error())
}
return strings.Join(es, "; ")
}
// Alert wraps a model.Alert with additional information relevant
// to internal of the Alertmanager.
// The type is never exposed to external communication and the
// embedded alert has to be sanitized beforehand.
type Alert struct {
model.Alert
// The authoritative timestamp.
UpdatedAt time.Time
Timeout bool
}
func validateLs(ls model.LabelSet) error {
for ln, lv := range ls {
if !compat.IsValidLabelName(ln) {
return fmt.Errorf("invalid name %q", ln)
}
if !lv.IsValid() {
return fmt.Errorf("invalid value %q", lv)
}
}
return nil
}
// Validate overrides the same method in model.Alert to allow UTF-8 labels.
// This can be removed once prometheus/common has support for UTF-8.
func (a *Alert) Validate() error {
if a.StartsAt.IsZero() {
return fmt.Errorf("start time missing")
}
if !a.EndsAt.IsZero() && a.EndsAt.Before(a.StartsAt) {
return fmt.Errorf("start time must be before end time")
}
if len(a.Labels) == 0 {
return fmt.Errorf("at least one label pair required")
}
if err := validateLs(a.Labels); err != nil {
return fmt.Errorf("invalid label set: %w", err)
}
if err := validateLs(a.Annotations); err != nil {
return fmt.Errorf("invalid annotations: %w", err)
}
return nil
}
// AlertSlice is a sortable slice of Alerts.
type AlertSlice []*Alert
func (as AlertSlice) Less(i, j int) bool {
// Look at labels.job, then labels.instance.
for _, overrideKey := range [...]model.LabelName{"job", "instance"} {
iVal, iOk := as[i].Labels[overrideKey]
jVal, jOk := as[j].Labels[overrideKey]
if !iOk && !jOk {
continue
}
if !iOk {
return false
}
if !jOk {
return true
}
if iVal != jVal {
return iVal < jVal
}
}
return as[i].Labels.Before(as[j].Labels)
}
func (as AlertSlice) Swap(i, j int) { as[i], as[j] = as[j], as[i] }
func (as AlertSlice) Len() int { return len(as) }
// Alerts turns a sequence of internal alerts into a list of
// exposable model.Alert structures.
func Alerts(alerts ...*Alert) model.Alerts {
res := make(model.Alerts, 0, len(alerts))
for _, a := range alerts {
v := a.Alert
// If the end timestamp is not reached yet, do not expose it.
if !a.Resolved() {
v.EndsAt = time.Time{}
}
res = append(res, &v)
}
return res
}
// Merge merges the timespan of two alerts based and overwrites annotations
// based on the authoritative timestamp. A new alert is returned, the labels
// are assumed to be equal.
func (a *Alert) Merge(o *Alert) *Alert {
// Let o always be the younger alert.
if o.UpdatedAt.Before(a.UpdatedAt) {
return o.Merge(a)
}
res := *o
// Always pick the earliest starting time.
if a.StartsAt.Before(o.StartsAt) {
res.StartsAt = a.StartsAt
}
if o.Resolved() {
// The latest explicit resolved timestamp wins if both alerts are effectively resolved.
if a.Resolved() && a.EndsAt.After(o.EndsAt) {
res.EndsAt = a.EndsAt
}
} else {
// A non-timeout timestamp always rules if it is the latest.
if a.EndsAt.After(o.EndsAt) && !a.Timeout {
res.EndsAt = a.EndsAt
}
}
return &res
}
// A Muter determines whether a given label set is muted. Implementers that
// maintain an underlying AlertMarker are expected to update it during a call of
// Mutes.
type Muter interface {
Mutes(ctx context.Context, lset model.LabelSet) bool
}
// A TimeMuter determines if the time is muted by one or more active or mute
// time intervals. If the time is muted, it returns true and the names of the
// time intervals that muted it. Otherwise, it returns false and a nil slice.
type TimeMuter interface {
Mutes(timeIntervalNames []string, now time.Time) (bool, []string, error)
}
// A MuteFunc is a function that implements the Muter interface.
type MuteFunc func(ctx context.Context, lset model.LabelSet) bool
// Mutes implements the Muter interface.
func (f MuteFunc) Mutes(ctx context.Context, lset model.LabelSet) bool { return f(ctx, lset) }
// A Silence determines whether a given label set is muted.
type Silence struct {
// A unique identifier across all connected instances.
ID string `json:"id"`
// A set of matchers determining if a label set is affected
// by the silence.
Matchers labels.Matchers `json:"matchers"`
// Time range of the silence.
//
// * StartsAt must not be before creation time
// * EndsAt must be after StartsAt
// * Deleting a silence means to set EndsAt to now
// * Time range must not be modified in different ways
//
// TODO(fabxc): this may potentially be extended by
// creation and update timestamps.
StartsAt time.Time `json:"startsAt"`
EndsAt time.Time `json:"endsAt"`
// The last time the silence was updated.
UpdatedAt time.Time `json:"updatedAt"`
// Information about who created the silence for which reason.
CreatedBy string `json:"createdBy"`
Comment string `json:"comment,omitempty"`
Status SilenceStatus `json:"status"`
}
// Expired return if the silence is expired
// meaning that both StartsAt and EndsAt are equal.
func (s *Silence) Expired() bool {
return s.StartsAt.Equal(s.EndsAt)
}
// SilenceStatus stores the state of a silence.
type SilenceStatus struct {
State SilenceState `json:"state"`
}
// SilenceState is used as part of SilenceStatus.
type SilenceState string
// Possible values for SilenceState.
const (
SilenceStateExpired SilenceState = "expired"
SilenceStateActive SilenceState = "active"
SilenceStatePending SilenceState = "pending"
)
// CalcSilenceState returns the SilenceState that a silence with the given start
// and end time would have right now.
func CalcSilenceState(start, end time.Time) SilenceState {
current := time.Now()
if current.Before(start) {
return SilenceStatePending
}
if current.Before(end) {
return SilenceStateActive
}
return SilenceStateExpired
}