Initial QSfera import
This commit is contained in:
+12
@@ -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
@@ -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
@@ -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
@@ -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,
|
||||
})
|
||||
+409
File diff suppressed because one or more lines are too long
+22
@@ -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
|
||||
Generated
Vendored
+178
@@ -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 }
|
||||
+205
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 }}
|
||||
+40
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user