Merge pull request #156 from prometheus/beorn7/review-dev

PR with changes after code review
This commit is contained in:
Björn Rabenstein 2015-11-23 18:27:32 +01:00
commit a4ffa9a64b
20 changed files with 334 additions and 227 deletions

View File

@ -54,8 +54,8 @@ This is an example configuration that should cover most relevant aspects of the
```yaml
global:
# The smarthost and SMTP sender used for mail notifications.
smarthost: 'localhost:25'
smtp_sender: 'alertmanager@example.org'
smtp_smarthost: 'localhost:25'
smtp_from: 'alertmanager@example.org'
# The directory from which notification templates are read.
templates:
@ -155,17 +155,17 @@ inhibit_rules:
receivers:
- name: 'team-X-mails'
email_configs:
- email: 'team-X+alerts@example.org'
- to: 'team-X+alerts@example.org'
- name: 'team-X-pager'
email_configs:
- email: 'team-X+alerts-critical@example.org'
- to: 'team-X+alerts-critical@example.org'
pagerduty_configs:
- service_key: <team-X-key>
- name: 'team-Y-mails'
email_configs:
- email: 'team-Y+alerts@example.org'
- to: 'team-Y+alerts@example.org'
- name: 'team-Y-pager'
pagerduty_configs:

View File

@ -14,6 +14,7 @@
package config
import (
"errors"
"fmt"
"io/ioutil"
"path/filepath"
@ -31,6 +32,13 @@ func Load(s string) (*Config, error) {
if err != nil {
return nil, err
}
// Check if we have a root route. We cannot check for it in the
// UnmarshalYAML method because it won't be called if the input is empty
// (e.g. the config file is empty or only contains whitespace).
if cfg.Route == nil {
return nil, errors.New("no route provided in config")
}
cfg.original = s
return cfg, nil
}
@ -134,7 +142,7 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error {
}
if ec.From == "" {
if c.Global.SMTPFrom == "" {
return fmt.Errorf("no global SMTP sender set")
return fmt.Errorf("no global SMTP from set")
}
ec.From = c.Global.SMTPFrom
}
@ -294,12 +302,12 @@ type Receiver struct {
// A unique identifier for this receiver.
Name string `yaml:"name"`
PagerdutyConfigs []*PagerdutyConfig `yaml:"pagerduty_configs"`
EmailConfigs []*EmailConfig `yaml:"email_configs"`
PushoverConfigs []*PushoverConfig `yaml:"pushover_configs"`
HipchatConfigs []*HipchatConfig `yaml:"hipchat_configs"`
SlackConfigs []*SlackConfig `yaml:"slack_configs"`
FlowdockConfigs []*FlowdockConfig `yaml:"flowdock_configs"`
HipchatConfigs []*HipchatConfig `yaml:"hipchat_configs"`
PagerdutyConfigs []*PagerdutyConfig `yaml:"pagerduty_configs"`
PushoverConfigs []*PushoverConfig `yaml:"pushover_configs"`
SlackConfigs []*SlackConfig `yaml:"slack_configs"`
WebhookConfigs []*WebhookConfig `yaml:"webhook_configs"`
// Catches all undefined fields and must be empty after parsing.

View File

@ -19,12 +19,25 @@ import (
)
var (
// DefaultEmailConfig defines default values for Email configurations.
DefaultEmailConfig = EmailConfig{
HTML: `{{template "email.default.html" .}}`,
}
// DefaultEmailSubject is a template used if no email subject has been provided.
DefaultEmailSubject = `{{template "email.default.subject" .}}`
// DefaultHipchatConfig defines default values for Hipchat configurations.
DefaultHipchatConfig = HipchatConfig{
Color: `{{ if eq .Status "firing" }}purple{{ else }}green{{ end }}`,
MessageFormat: HipchatFormatHTML,
}
// DefaultPagerdutyConfig defines default values for PagerDuty configurations.
DefaultPagerdutyConfig = PagerdutyConfig{
Description: `{{template "pagerduty.default.description" .}}`,
// TODO: Add a details field with all the alerts.
}
// DefaultSlackConfig defines default values for Slack configurations.
DefaultSlackConfig = SlackConfig{
Color: `{{ if eq .Status "firing" }}warning{{ else }}good{{ end }}`,
@ -34,42 +47,36 @@ var (
Text: `{{template "slack.default.text" . }}`,
Fallback: `{{template "slack.default.fallback" . }}`,
}
// DefaultEmailConfig defines default values for Email configurations.
DefaultEmailConfig = EmailConfig{
HTML: `{{template "email.default.html" .}}`,
}
DefaultEmailSubject = `{{template "email.default.subject" .}}`
// DefaultPagerdutyConfig defines default values for PagerDuty configurations.
DefaultPagerdutyConfig = PagerdutyConfig{
Description: `{{template "pagerduty.default.description" .}}`,
// TODO: Add a details field with all the alerts.
}
)
// PagerdutyConfig configures notifications via PagerDuty.
type PagerdutyConfig struct {
ServiceKey string `yaml:"service_key"`
URL string `yaml:"url"`
Description string `yaml:"description"`
Details map[string]string `yaml:"details"`
// FlowdockConfig configures notifications via Flowdock.
type FlowdockConfig struct {
// Flowdock flow API token.
APIToken string `yaml:"api_token"`
// Flowdock from_address.
FromAddress string `yaml:"from_address"`
// Flowdock flow tags.
Tags []string `yaml:"tags"`
// Catches all undefined fields and must be empty after parsing.
XXX map[string]interface{} `yaml:",inline"`
}
// UnmarshalYAML implements the yaml.Unmarshaler interface.
func (c *PagerdutyConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
*c = DefaultPagerdutyConfig
type plain PagerdutyConfig
func (c *FlowdockConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
type plain FlowdockConfig
if err := unmarshal((*plain)(c)); err != nil {
return err
}
if c.ServiceKey == "" {
return fmt.Errorf("missing service key in PagerDuty config")
if c.APIToken == "" {
return fmt.Errorf("missing API token in Flowdock config")
}
return checkOverflow(c.XXX, "pagerduty config")
if c.FromAddress == "" {
return fmt.Errorf("missing from address in Flowdock config")
}
return checkOverflow(c.XXX, "flowdock config")
}
// EmailConfig configures notifications via mail.
@ -97,15 +104,15 @@ func (c *EmailConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
}
// Header names are case-insensitive, check for collisions.
normalisedHeaders := map[string]string{}
normalizedHeaders := map[string]string{}
for h, v := range c.Headers {
normalised := strings.ToTitle(h)
if _, ok := normalisedHeaders[normalised]; ok {
return fmt.Errorf("duplicate header %q in email config", normalised)
normalized := strings.ToTitle(h)
if _, ok := normalizedHeaders[normalized]; ok {
return fmt.Errorf("duplicate header %q in email config", normalized)
}
normalisedHeaders[normalised] = v
normalizedHeaders[normalized] = v
}
c.Headers = normalisedHeaders
c.Headers = normalizedHeaders
if _, ok := c.Headers["Subject"]; !ok {
c.Headers["Subject"] = DefaultEmailSubject
}
@ -119,36 +126,10 @@ func (c *EmailConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
return checkOverflow(c.XXX, "email config")
}
// PushoverConfig configures notifications via PushOver.
type PushoverConfig struct {
// Pushover token.
Token string `yaml:"token"`
// Pushover user_key.
UserKey string `yaml:"user_key"`
// Catches all undefined fields and must be empty after parsing.
XXX map[string]interface{} `yaml:",inline"`
}
// UnmarshalYAML implements the yaml.Unmarshaler interface.
func (c *PushoverConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
type plain PushoverConfig
if err := unmarshal((*plain)(c)); err != nil {
return err
}
if c.Token == "" {
return fmt.Errorf("missing token in Pushover config")
}
if c.UserKey == "" {
return fmt.Errorf("missing user key in Pushover config")
}
return checkOverflow(c.XXX, "pushover config")
}
// HipchatFormat defines text formats for Hipchat.
type HipchatFormat string
// Possible values of HipchatFormat.
const (
HipchatFormatHTML HipchatFormat = "html"
HipchatFormatText HipchatFormat = "text"
@ -195,6 +176,57 @@ func (c *HipchatConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
return checkOverflow(c.XXX, "hipchat config")
}
// PagerdutyConfig configures notifications via PagerDuty.
type PagerdutyConfig struct {
ServiceKey string `yaml:"service_key"`
URL string `yaml:"url"`
Description string `yaml:"description"`
Details map[string]string `yaml:"details"`
// Catches all undefined fields and must be empty after parsing.
XXX map[string]interface{} `yaml:",inline"`
}
// UnmarshalYAML implements the yaml.Unmarshaler interface.
func (c *PagerdutyConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
*c = DefaultPagerdutyConfig
type plain PagerdutyConfig
if err := unmarshal((*plain)(c)); err != nil {
return err
}
if c.ServiceKey == "" {
return fmt.Errorf("missing service key in PagerDuty config")
}
return checkOverflow(c.XXX, "pagerduty config")
}
// PushoverConfig configures notifications via PushOver.
type PushoverConfig struct {
// Pushover token.
Token string `yaml:"token"`
// Pushover user_key.
UserKey string `yaml:"user_key"`
// Catches all undefined fields and must be empty after parsing.
XXX map[string]interface{} `yaml:",inline"`
}
// UnmarshalYAML implements the yaml.Unmarshaler interface.
func (c *PushoverConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
type plain PushoverConfig
if err := unmarshal((*plain)(c)); err != nil {
return err
}
if c.Token == "" {
return fmt.Errorf("missing token in Pushover config")
}
if c.UserKey == "" {
return fmt.Errorf("missing user key in Pushover config")
}
return checkOverflow(c.XXX, "pushover config")
}
// SlackConfig configures notifications via Slack.
type SlackConfig struct {
URL string `yaml:"url"`
@ -228,36 +260,6 @@ func (c *SlackConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
return checkOverflow(c.XXX, "slack config")
}
// FlowdockConfig configures notifications via Flowdock.
type FlowdockConfig struct {
// Flowdock flow API token.
APIToken string `yaml:"api_token"`
// Flowdock from_address.
FromAddress string `yaml:"from_address"`
// Flowdock flow tags.
Tags []string `yaml:"tags"`
// Catches all undefined fields and must be empty after parsing.
XXX map[string]interface{} `yaml:",inline"`
}
// UnmarshalYAML implements the yaml.Unmarshaler interface.
func (c *FlowdockConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
type plain FlowdockConfig
if err := unmarshal((*plain)(c)); err != nil {
return err
}
if c.APIToken == "" {
return fmt.Errorf("missing API token in Flowdock config")
}
if c.FromAddress == "" {
return fmt.Errorf("missing from address in Flowdock config")
}
return checkOverflow(c.XXX, "flowdock config")
}
// WebhookConfig configures notifications via a generic webhook.
type WebhookConfig struct {
// URL to send POST request to.

View File

@ -17,6 +17,7 @@ import (
// ResolveTimeout is the time after which an alert is declared resolved
// if it has not been updated.
// TODO(fabxc): Make it configurable.
const ResolveTimeout = 5 * time.Minute
// Dispatcher sorts incoming alerts into aggregation groups and
@ -152,7 +153,15 @@ func (d *Dispatcher) run(it provider.AlertIterator) {
for {
select {
case alert := <-it.Next():
case alert, ok := <-it.Next():
if !ok {
// Iterator exhausted for some reason.
if err := it.Err(); err != nil {
log.Errorf("Error on alert update: %s", err)
}
return
}
d.log.With("alert", alert).Debug("Received alert")
// Log errors but keep trying.
@ -278,7 +287,7 @@ func newAggrGroup(ctx context.Context, labels model.LabelSet, opts *RouteOpts) *
}
func (ag *aggrGroup) String() string {
return fmt.Sprintf("%v", ag.fingerprint())
return fmt.Sprint(ag.fingerprint())
}
func (ag *aggrGroup) alertSlice() []*types.Alert {
@ -352,8 +361,8 @@ func (ag *aggrGroup) fingerprint() model.Fingerprint {
return ag.labels.Fingerprint()
}
// insert the alert into the aggregation group. If the aggregation group
// is empty afterwards, true is returned.
// insert inserts the alert into the aggregation group. If the aggregation group
// is empty afterwards, it returns true.
func (ag *aggrGroup) insert(alert *types.Alert) {
ag.mtx.Lock()
defer ag.mtx.Unlock()

View File

@ -1,7 +1,7 @@
global:
# The smarthost and SMTP sender used for mail notifications.
smarthost: 'localhost:25'
smtp_sender: 'alertmanager@example.org'
smtp_smarthost: 'localhost:25'
smtp_from: 'alertmanager@example.org'
# The directory from which notification templates are read.
templates:
@ -96,17 +96,17 @@ inhibit_rules:
receivers:
- name: 'team-X-mails'
email_configs:
- email: 'team-X+alerts@example.org'
- to: 'team-X+alerts@example.org'
- name: 'team-X-pager'
email_configs:
- email: 'team-X+alerts-critical@example.org'
- to: 'team-X+alerts-critical@example.org'
pagerduty_configs:
- service_key: <team-X-key>
- name: 'team-Y-mails'
email_configs:
- email: 'team-Y+alerts@example.org'
- to: 'team-Y+alerts@example.org'
- name: 'team-Y-pager'
pagerduty_configs:

View File

@ -69,6 +69,9 @@ func (ih *Inhibitor) Mutes(lset model.LabelSet) bool {
}
}
}
if err := alerts.Err(); err != nil {
log.Errorf("Error after iterating alerts: %s", err)
}
ih.marker.SetInhibited(lset.Fingerprint(), false)
@ -103,24 +106,14 @@ func NewInhibitRule(cr *config.InhibitRule) *InhibitRule {
sourcem = append(sourcem, types.NewMatcher(model.LabelName(ln), lv))
}
for ln, lv := range cr.SourceMatchRE {
m, err := types.NewRegexMatcher(model.LabelName(ln), lv.String())
if err != nil {
// Must have been sanitized during config validation.
panic(err)
}
sourcem = append(sourcem, m)
sourcem = append(sourcem, types.NewRegexMatcher(model.LabelName(ln), lv.Regexp))
}
for ln, lv := range cr.TargetMatch {
targetm = append(targetm, types.NewMatcher(model.LabelName(ln), lv))
}
for ln, lv := range cr.TargetMatchRE {
m, err := types.NewRegexMatcher(model.LabelName(ln), lv.String())
if err != nil {
// Must have been sanitized during config validation.
panic(err)
}
targetm = append(targetm, m)
targetm = append(targetm, types.NewRegexMatcher(model.LabelName(ln), lv.Regexp))
}
equal := map[model.LabelName]struct{}{}

14
main.go
View File

@ -24,7 +24,7 @@ import (
"path/filepath"
"strings"
"syscall"
template_text "text/template"
tmpltext "text/template"
"github.com/prometheus/common/log"
"github.com/prometheus/common/route"
@ -40,10 +40,10 @@ import (
var (
showVersion = flag.Bool("version", false, "Print version information.")
configFile = flag.String("config.file", "alertmanager.yml", "The configuration file")
dataDir = flag.String("storage.path", "data/", "The data directory")
configFile = flag.String("config.file", "alertmanager.yml", "Alertmanager configuration file name.")
dataDir = flag.String("storage.path", "data/", "Base path for data storage.")
externalURL = flag.String("web.external-url", "", "The URL under which Prometheus is externally reachable (for example, if Prometheus is served via a reverse proxy). Used for generating relative and absolute links back to Prometheus itself. If the URL has a path portion, it will be used to prefix all HTTP endpoints served by Prometheus. If omitted, relevant URL components will be derived automatically.")
externalURL = flag.String("web.external-url", "", "The URL under which Alertmanager is externally reachable (for example, if Alertmanager is served via a reverse proxy). Used for generating relative and absolute links back to Alertmanager itself. If the URL has a path portion, it will be used to prefix all HTTP endpoints served by Alertmanager. If omitted, relevant URL components will be derived automatically.")
listenAddress = flag.String("web.listen-address", ":9093", "Address to listen on for the web interface and API.")
)
@ -71,7 +71,7 @@ func main() {
if err != nil {
log.Fatal(err)
}
notifies, err := provider.NewSQLNotifyInfo(db)
notifies, err := provider.NewSQLNotifies(db)
if err != nil {
log.Fatal(err)
}
@ -107,7 +107,7 @@ func main() {
}
router[name] = fo
}
var n notify.Notifier = router
n := notify.Notifier(router)
n = notify.Log(n, log.With("step", "route"))
n = notify.Mute(silences, n)
@ -185,7 +185,7 @@ alertmanager, version {{.version}} (branch: {{.branch}}, revision: {{.revision}}
`
func printVersion() {
t := template_text.Must(template_text.New("version").Parse(versionInfoTmpl))
t := tmpltext.Must(tmpltext.New("version").Parse(versionInfoTmpl))
var buf bytes.Buffer
if err := t.ExecuteTemplate(&buf, "version", version.Map); err != nil {

View File

@ -145,7 +145,7 @@ func (ns Fanout) Notify(ctx context.Context, alerts ...*types.Alert) error {
go func(n Notifier) {
if err := n.Notify(foCtx, alerts...); err != nil {
me = append(me, err)
me.Add(err)
log.Errorf("Error on notify: %s", err)
}
wg.Done()
@ -154,8 +154,8 @@ func (ns Fanout) Notify(ctx context.Context, alerts ...*types.Alert) error {
wg.Wait()
if len(me) > 0 {
return me
if me.Len() > 0 {
return &me
}
return nil
}
@ -340,6 +340,7 @@ func Mute(m types.Muter, n Notifier) *MutingNotifier {
return &MutingNotifier{Muter: m, notifier: n}
}
// Notify implements Notifier.
func (n *MutingNotifier) Notify(ctx context.Context, alerts ...*types.Alert) error {
var filtered []*types.Alert
for _, a := range alerts {

View File

@ -14,7 +14,6 @@
package provider
import (
"fmt"
"sync"
"github.com/prometheus/common/model"
@ -22,16 +21,14 @@ import (
"github.com/prometheus/alertmanager/types"
)
var (
ErrNotFound = fmt.Errorf("item not found")
)
// MemData contains the data backing MemAlerts and MemNotifies.
type MemData struct {
mtx sync.RWMutex
alerts map[model.Fingerprint]*types.Alert
notifies map[string]map[model.Fingerprint]*types.NotifyInfo
}
// NewMemData contains an empty but initialized MemData instance.
func NewMemData() *MemData {
return &MemData{
alerts: map[model.Fingerprint]*types.Alert{},
@ -39,19 +36,6 @@ func NewMemData() *MemData {
}
}
type memAlertIterator struct {
ch <-chan *types.Alert
done chan struct{}
err error
}
func (ai memAlertIterator) Next() <-chan *types.Alert {
return ai.ch
}
func (ai memAlertIterator) Err() error { return ai.err }
func (ai memAlertIterator) Close() { close(ai.done) }
// MemAlerts implements an Alerts provider based on in-memory data.
type MemAlerts struct {
data *MemData
@ -61,6 +45,7 @@ type MemAlerts struct {
next int
}
// NewMemAlerts returns a new MemAlerts based on the provided data.
func NewMemAlerts(data *MemData) *MemAlerts {
return &MemAlerts{
data: data,
@ -68,6 +53,7 @@ func NewMemAlerts(data *MemData) *MemAlerts {
}
}
// Subscribe implements the Alerts interface.
func (a *MemAlerts) Subscribe() AlertIterator {
a.mtx.Lock()
defer a.mtx.Unlock()
@ -104,12 +90,13 @@ func (a *MemAlerts) Subscribe() AlertIterator {
<-done
}()
return memAlertIterator{
return alertIterator{
ch: ch,
done: done,
}
}
// GetPending implements the Alerts interface.
func (a *MemAlerts) GetPending() AlertIterator {
a.mtx.Lock()
defer a.mtx.Unlock()
@ -134,7 +121,7 @@ func (a *MemAlerts) GetPending() AlertIterator {
}
}()
return memAlertIterator{
return alertIterator{
ch: ch,
done: done,
}
@ -163,6 +150,7 @@ func (a *MemAlerts) getPending() []*types.Alert {
return alerts
}
// Put implements the Alerts interface.
func (a *MemAlerts) Put(alerts ...*types.Alert) error {
a.mtx.Lock()
defer a.mtx.Unlock()
@ -187,6 +175,7 @@ func (a *MemAlerts) Put(alerts ...*types.Alert) error {
return nil
}
// Get implements the Alerts interface.
func (a *MemAlerts) Get(fp model.Fingerprint) (*types.Alert, error) {
a.data.mtx.RLock()
defer a.data.mtx.RUnlock()
@ -197,14 +186,17 @@ func (a *MemAlerts) Get(fp model.Fingerprint) (*types.Alert, error) {
return nil, ErrNotFound
}
// MemNotifies implements a Notifies provider based on in-memory data.
type MemNotifies struct {
data *MemData
}
// NewMemNotifies returns a new MemNotifies based on the provided data.
func NewMemNotifies(data *MemData) *MemNotifies {
return &MemNotifies{data: data}
}
// Set implements the Notifies interface.
func (n *MemNotifies) Set(ns ...*types.NotifyInfo) error {
n.data.mtx.Lock()
defer n.data.mtx.Unlock()
@ -223,6 +215,7 @@ func (n *MemNotifies) Set(ns ...*types.NotifyInfo) error {
return nil
}
// Get implements the Notifies interface.
func (n *MemNotifies) Get(dest string, fps ...model.Fingerprint) ([]*types.NotifyInfo, error) {
n.data.mtx.RLock()
defer n.data.mtx.RUnlock()
@ -241,17 +234,20 @@ func (n *MemNotifies) Get(dest string, fps ...model.Fingerprint) ([]*types.Notif
return res, nil
}
// MemSilences implements a Silences provider based on in-memory data.
type MemSilences struct {
mtx sync.RWMutex
silences map[uint64]*model.Silence
}
// NewMemSilences returns a new MemSilences.
func NewMemSilences() *MemSilences {
return &MemSilences{
silences: map[uint64]*model.Silence{},
}
}
// Mutes implements the Muter interface.
func (s *MemSilences) Mutes(lset model.LabelSet) bool {
s.mtx.RLock()
defer s.mtx.RUnlock()
@ -264,6 +260,7 @@ func (s *MemSilences) Mutes(lset model.LabelSet) bool {
return false
}
// All implements the Silences interface.
func (s *MemSilences) All() ([]*types.Silence, error) {
s.mtx.RLock()
defer s.mtx.RUnlock()
@ -275,6 +272,7 @@ func (s *MemSilences) All() ([]*types.Silence, error) {
return sils, nil
}
// Set impelements the Silences interface.
func (s *MemSilences) Set(sil *types.Silence) (uint64, error) {
s.mtx.Lock()
defer s.mtx.Unlock()
@ -291,6 +289,7 @@ func (s *MemSilences) Set(sil *types.Silence) (uint64, error) {
return sil.ID, nil
}
// Del implements the Silences interface.
func (s *MemSilences) Del(id uint64) error {
s.mtx.Lock()
defer s.mtx.Unlock()
@ -299,6 +298,7 @@ func (s *MemSilences) Del(id uint64) error {
return nil
}
// Get implements the Silences interface.
func (s *MemSilences) Get(id uint64) (*types.Silence, error) {
s.mtx.RLock()
defer s.mtx.RUnlock()

View File

@ -14,22 +14,56 @@
package provider
import (
"fmt"
"github.com/prometheus/common/model"
"github.com/prometheus/alertmanager/types"
)
var (
// ErrNotFound is returned if a provider cannot find a requested item.
ErrNotFound = fmt.Errorf("item not found")
)
// Iterator provides the functions common to all iterators. To be useful, a
// specific iterator interface (e.g. AlertIterator) has to be implemented that
// provides a Next method.
type Iterator interface {
// Err returns the current error. It is not safe to call it concurrently
// with other iterator methods or while reading from a channel returned
// by the iterator.
Err() error
// Close must be called to release resources once the iterator is not
// used anymore.
Close()
}
// AlertIterator is an Iterator for Alerts.
type AlertIterator interface {
Iterator
// Next returns a channel that will be closed once the iterator is
// exhausted. It is not necessary to exhaust the iterator but Close must
// be called in any case to release resources used by the iterator (even
// if the iterator is exhausted).
Next() <-chan *types.Alert
}
// Alerts gives access to a set of alerts.
// alertIterator implements AlertIterator. So far, this one fits all providers.
type alertIterator struct {
ch <-chan *types.Alert
done chan struct{}
err error
}
func (ai alertIterator) Next() <-chan *types.Alert {
return ai.ch
}
func (ai alertIterator) Err() error { return ai.err }
func (ai alertIterator) Close() { close(ai.done) }
// Alerts gives access to a set of alerts. All methods are goroutine-safe.
type Alerts interface {
// Subscribe returns an iterator over active alerts that have not been
// resolved and successfully notified about.
@ -44,7 +78,7 @@ type Alerts interface {
Put(...*types.Alert) error
}
// Silences gives access to silences.
// Silences gives access to silences. All methods are goroutine-safe.
type Silences interface {
// The Silences provider must implement the Muter interface
// for all its silences. The data provider may have access to an
@ -62,7 +96,7 @@ type Silences interface {
}
// Notifies provides information about pending and successful
// notifications.
// notifications. All methods are goroutine-safe.
type Notifies interface {
Get(dest string, fps ...model.Fingerprint) ([]*types.NotifyInfo, error)
// Set several notifies at once. All or none must succeed.

View File

@ -30,11 +30,13 @@ func init() {
ql.RegisterDriver()
}
type SQLNotifyInfo struct {
// SQLNotifies implements a Notifies provider based on a SQL DB.
type SQLNotifies struct {
db *sql.DB
}
func NewSQLNotifyInfo(db *sql.DB) (*SQLNotifyInfo, error) {
// NewSQLNotifies returns a new SQLNotifies based on the provided SQL DB.
func NewSQLNotifies(db *sql.DB) (*SQLNotifies, error) {
tx, err := db.Begin()
if err != nil {
return nil, err
@ -69,7 +71,7 @@ func NewSQLNotifyInfo(db *sql.DB) (*SQLNotifyInfo, error) {
tx.Commit()
return &SQLNotifyInfo{db: db}, nil
return &SQLNotifies{db: db}, nil
}
const createNotifyInfoTable = `
@ -85,7 +87,8 @@ CREATE INDEX IF NOT EXISTS notify_done
ON notify_info (resolved);
`
func (n *SQLNotifyInfo) Get(dest string, fps ...model.Fingerprint) ([]*types.NotifyInfo, error) {
// Get implements the Notifies interface.
func (n *SQLNotifies) Get(dest string, fps ...model.Fingerprint) ([]*types.NotifyInfo, error) {
var result []*types.NotifyInfo
for _, fp := range fps {
@ -120,7 +123,8 @@ func (n *SQLNotifyInfo) Get(dest string, fps ...model.Fingerprint) ([]*types.Not
return result, nil
}
func (n *SQLNotifyInfo) Set(ns ...*types.NotifyInfo) error {
// Set implements the Notifies interface.
func (n *SQLNotifies) Set(ns ...*types.NotifyInfo) error {
tx, err := n.db.Begin()
if err != nil {
return err
@ -167,6 +171,7 @@ func (n *SQLNotifyInfo) Set(ns ...*types.NotifyInfo) error {
return nil
}
// SQLAlerts implements an Alerts provider based on a SQL DB.
type SQLAlerts struct {
db *sql.DB
@ -175,6 +180,7 @@ type SQLAlerts struct {
next int
}
// NewSQLAlerts returns a new SQLAlerts based on the provided SQL DB.
func NewSQLAlerts(db *sql.DB) (*SQLAlerts, error) {
tx, err := db.Begin()
if err != nil {
@ -206,6 +212,7 @@ CREATE INDEX IF NOT EXISTS alerts_start ON alerts (starts_at);
CREATE INDEX IF NOT EXISTS alerts_end ON alerts (ends_at);
`
// Subscribe implements the Alerts interface.
func (a *SQLAlerts) Subscribe() AlertIterator {
var (
ch = make(chan *types.Alert, 200)
@ -239,13 +246,14 @@ func (a *SQLAlerts) Subscribe() AlertIterator {
<-done
}()
return memAlertIterator{
return alertIterator{
ch: ch,
done: done,
err: err,
}
}
// GetPending implements the Alerts interface.
func (a *SQLAlerts) GetPending() AlertIterator {
var (
ch = make(chan *types.Alert, 200)
@ -266,7 +274,7 @@ func (a *SQLAlerts) GetPending() AlertIterator {
}
}()
return memAlertIterator{
return alertIterator{
ch: ch,
done: done,
err: err,
@ -323,10 +331,12 @@ func (a *SQLAlerts) getPending() ([]*types.Alert, error) {
return alerts, nil
}
// Get implements the Alerts interface.
func (a *SQLAlerts) Get(model.Fingerprint) (*types.Alert, error) {
return nil, nil
}
// Put implements the Alerts interface.
func (a *SQLAlerts) Put(alerts ...*types.Alert) error {
tx, err := a.db.Begin()
if err != nil {
@ -473,11 +483,13 @@ func (a *SQLAlerts) Put(alerts ...*types.Alert) error {
return nil
}
// SQLSilences implements a Silences provider based on a SQL DB.
type SQLSilences struct {
db *sql.DB
marker types.Marker
}
// NewSQLSilences returns a new SQLSilences based on the provided SQL DB.
func NewSQLSilences(db *sql.DB, mk types.Marker) (*SQLSilences, error) {
tx, err := db.Begin()
if err != nil {
@ -506,6 +518,7 @@ CREATE INDEX IF NOT EXISTS silences_end ON silences (ends_at);
CREATE INDEX IF NOT EXISTS silences_id ON silences (id());
`
// Mutes implements the Muter interface.
func (s *SQLSilences) Mutes(lset model.LabelSet) bool {
sils, err := s.All()
if err != nil {
@ -525,6 +538,7 @@ func (s *SQLSilences) Mutes(lset model.LabelSet) bool {
return false
}
// All implements the Silences interface.
func (s *SQLSilences) All() ([]*types.Silence, error) {
rows, err := s.db.Query(`
SELECT id(), matchers, starts_at, ends_at, created_at, created_by, comment
@ -569,6 +583,7 @@ func (s *SQLSilences) All() ([]*types.Silence, error) {
return silences, nil
}
// Set impelements the Silences interface.
func (s *SQLSilences) Set(sil *types.Silence) (uint64, error) {
mb, err := json.Marshal(sil.Silence.Matchers)
if err != nil {
@ -607,6 +622,7 @@ func (s *SQLSilences) Set(sil *types.Silence) (uint64, error) {
return uint64(sid), nil
}
// Del implements the Silences interface.
func (s *SQLSilences) Del(sid uint64) error {
tx, err := s.db.Begin()
if err != nil {
@ -622,6 +638,7 @@ func (s *SQLSilences) Del(sid uint64) error {
return nil
}
// Get implements the Silences interface.
func (s *SQLSilences) Get(sid uint64) (*types.Silence, error) {
row := s.db.QueryRow(`
SELECT id(), matchers, starts_at, ends_at, created_at, created_by, comment

View File

@ -91,12 +91,7 @@ func NewRoute(cr *config.Route, parent *Route) *Route {
matchers = append(matchers, types.NewMatcher(model.LabelName(ln), lv))
}
for ln, lv := range cr.MatchRE {
m, err := types.NewRegexMatcher(model.LabelName(ln), lv.String())
if err != nil {
// Must have been sanitized during config validation.
panic(err)
}
matchers = append(matchers, m)
matchers = append(matchers, types.NewRegexMatcher(model.LabelName(ln), lv.Regexp))
}
route := &Route{

View File

@ -50,7 +50,7 @@ routes:
continue: true
- match_re:
env: "^produ.*$"
env: "produ.*"
receiver: 'notify-productionB'
group_wait: 30s
@ -60,7 +60,7 @@ routes:
- match_re:
owner: '^team-(B|C)$'
owner: 'team-(B|C)'
group_by: ['foo', 'bar']
group_wait: 2m

View File

@ -16,19 +16,22 @@ package template
import (
"bytes"
html_tmpl "html/template"
text_tmpl "text/template"
tmplhtml "html/template"
tmpltext "text/template"
)
// Template bundles a text and a html template instance.
type Template struct {
text *text_tmpl.Template
html *html_tmpl.Template
text *tmpltext.Template
html *tmplhtml.Template
}
// FromGlobs calls ParseGlob on all path globs provided and returns the
// resulting Template.
func FromGlobs(paths ...string) (*Template, error) {
t := &Template{
text: text_tmpl.New("").Option("missingkey=zero"),
html: html_tmpl.New("").Option("missingkey=zero"),
text: tmpltext.New("").Option("missingkey=zero"),
html: tmplhtml.New("").Option("missingkey=zero"),
}
var err error
@ -44,6 +47,7 @@ func FromGlobs(paths ...string) (*Template, error) {
return t, nil
}
// ExecuteTextString needs a meaningful doc comment (TODO(fabxc)).
func (t *Template) ExecuteTextString(text string, data interface{}) (string, error) {
tmpl, err := t.text.Clone()
if err != nil {
@ -58,6 +62,7 @@ func (t *Template) ExecuteTextString(text string, data interface{}) (string, err
return buf.String(), err
}
// ExecuteHTMLString needs a meaningful doc comment (TODO(fabxc)).
func (t *Template) ExecuteHTMLString(html string, data interface{}) (string, error) {
tmpl, err := t.html.Clone()
if err != nil {

View File

@ -263,7 +263,8 @@ func (am *Alertmanager) Start() {
time.Sleep(50 * time.Millisecond)
}
// kill the underlying Alertmanager process and remove intermediate data.
// Terminate kills the underlying Alertmanager process and remove intermediate
// data.
func (am *Alertmanager) Terminate() {
syscall.Kill(am.cmd.Process.Pid, syscall.SIGTERM)
}

View File

@ -68,7 +68,7 @@ func (c *Collector) latest() float64 {
return latest
}
// want declares that the Collector expects to receive the given alerts
// Want declares that the Collector expects to receive the given alerts
// within the given time boundaries.
func (c *Collector) Want(iv Interval, alerts ...*TestAlert) {
var nas model.Alerts

View File

@ -84,28 +84,28 @@ func (s *TestSilence) MatchRE(v ...string) *TestSilence {
// nativeSilence converts the declared test silence into a regular
// silence with resolved times.
func (sil *TestSilence) nativeSilence(opts *AcceptanceOpts) *model.Silence {
func (s *TestSilence) nativeSilence(opts *AcceptanceOpts) *model.Silence {
nsil := &model.Silence{}
for i := 0; i < len(sil.match); i += 2 {
for i := 0; i < len(s.match); i += 2 {
nsil.Matchers = append(nsil.Matchers, &model.Matcher{
Name: model.LabelName(sil.match[i]),
Value: sil.match[i+1],
Name: model.LabelName(s.match[i]),
Value: s.match[i+1],
})
}
for i := 0; i < len(sil.matchRE); i += 2 {
for i := 0; i < len(s.matchRE); i += 2 {
nsil.Matchers = append(nsil.Matchers, &model.Matcher{
Name: model.LabelName(sil.matchRE[i]),
Value: sil.matchRE[i+1],
Name: model.LabelName(s.matchRE[i]),
Value: s.matchRE[i+1],
IsRegex: true,
})
}
if sil.startsAt > 0 {
nsil.StartsAt = opts.expandTime(sil.startsAt)
if s.startsAt > 0 {
nsil.StartsAt = opts.expandTime(s.startsAt)
}
if sil.endsAt > 0 {
nsil.EndsAt = opts.expandTime(sil.endsAt)
if s.endsAt > 0 {
nsil.EndsAt = opts.expandTime(s.endsAt)
}
return nsil
}
@ -117,7 +117,7 @@ type TestAlert struct {
startsAt, endsAt float64
}
// alert creates a new alert declaration with the given key/value pairs
// Alert creates a new alert declaration with the given key/value pairs
// as identifying labels.
func Alert(keyval ...interface{}) *TestAlert {
if len(keyval)%2 == 1 {

View File

@ -17,6 +17,7 @@ import (
"encoding/json"
"fmt"
"regexp"
"strings"
"github.com/prometheus/common/model"
)
@ -32,11 +33,12 @@ type Matcher struct {
func (m *Matcher) String() string {
if m.isRegex {
return fmt.Sprintf("<RegexMatcher %s:%q>", m.Name, m.regex)
return fmt.Sprintf("<RegexMatcher %s:%q>", m.Name, m.Value)
}
return fmt.Sprintf("<Matcher %s:%q>", m.Name, m.Value)
}
// MarshalJSON implements json.Marshaler.
func (m *Matcher) MarshalJSON() ([]byte, error) {
v := struct {
Name model.LabelName `json:"name"`
@ -80,20 +82,21 @@ func NewMatcher(name model.LabelName, value string) *Matcher {
// NewRegexMatcher returns a new matcher that treats value as a regular
// expression which is used for matching.
func NewRegexMatcher(name model.LabelName, value string) (*Matcher, error) {
re, err := regexp.Compile(value)
if err != nil {
return nil, err
func NewRegexMatcher(name model.LabelName, re *regexp.Regexp) *Matcher {
value := strings.TrimSuffix(strings.TrimPrefix(re.String(), "^(?:"), ")$")
if len(re.String())-len(value) != 6 {
// Any non-anchored regexp is a bug.
panic(fmt.Errorf("regexp %q not properly anchored", re))
}
m := &Matcher{
return &Matcher{
Name: name,
Value: value,
isRegex: true,
regex: re,
}
return m, nil
}
// Matchers provides the Match and Fingerprint methods for a slice of Matchers.
type Matchers []*Matcher
// Match checks whether all matchers are fulfilled against the given label set.
@ -106,6 +109,7 @@ func (ms Matchers) Match(lset model.LabelSet) bool {
return true
}
// Fingerprint returns a quasi-unique fingerprint for the matchers.
func (ms Matchers) Fingerprint() model.Fingerprint {
lset := make(model.LabelSet, 3*len(ms))

View File

@ -16,6 +16,7 @@ package types
import (
"fmt"
"hash/fnv"
"regexp"
"strings"
"sync"
"time"
@ -23,6 +24,8 @@ import (
"github.com/prometheus/common/model"
)
// Marker helps to mark alerts as silenced and/or inhibited.
// All methods are goroutine-safe.
type Marker interface {
SetInhibited(alert model.Fingerprint, b bool)
SetSilenced(alert model.Fingerprint, sil ...uint64)
@ -31,6 +34,7 @@ type Marker interface {
Inhibited(alert model.Fingerprint) bool
}
// NewMarker returns an instance of a Marker implementation.
func NewMarker() Marker {
return &memMarker{
inhibited: map[model.Fingerprint]struct{}{},
@ -83,11 +87,44 @@ func (m *memMarker) SetSilenced(alert model.Fingerprint, sil ...uint64) {
}
}
type MultiError []error
// 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
}
func (e MultiError) Error() string {
var es []string
for _, err := range e {
// 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, "; ")
@ -105,6 +142,7 @@ type Alert struct {
Timeout bool `json:"-"`
}
// AlertSlice is a sortable slice of Alerts.
type AlertSlice []*Alert
func (as AlertSlice) Less(i, j int) bool { return as[i].UpdatedAt.Before(as[j].UpdatedAt) }
@ -114,7 +152,7 @@ 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 {
var res model.Alerts
res := make(model.Alerts, 0, len(alerts))
for _, a := range alerts {
v := a.Alert
// If the end timestamp was set as the expected value in case
@ -127,9 +165,9 @@ func Alerts(alerts ...*Alert) model.Alerts {
return res
}
// 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.
// 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) {
@ -152,13 +190,15 @@ func (a *Alert) Merge(o *Alert) *Alert {
return &res
}
// A Silencer determines whether a given label set is muted.
// A Muter determines whether a given label set is muted.
type Muter interface {
Mutes(model.LabelSet) bool
}
// A MuteFunc is a function that implements the Muter interface.
type MuteFunc func(model.LabelSet) bool
// Mutes implements the Muter interface.
func (f MuteFunc) Mutes(lset model.LabelSet) bool { return f(lset) }
// A Silence determines whether a given label set is muted
@ -186,16 +226,13 @@ func NewSilence(s *model.Silence) *Silence {
sil.Matchers = append(sil.Matchers, NewMatcher(m.Name, m.Value))
continue
}
rem, err := NewRegexMatcher(m.Name, m.Value)
if err != nil {
// Must have been sanitized beforehand.
panic(err)
}
rem := NewRegexMatcher(m.Name, regexp.MustCompile("^(?:"+m.Value+")$"))
sil.Matchers = append(sil.Matchers, rem)
}
return sil
}
// Mutes implements the Muter interface.
func (sil *Silence) Mutes(lset model.LabelSet) bool {
t := sil.timeFunc()
@ -221,6 +258,7 @@ func (n *NotifyInfo) String() string {
return fmt.Sprintf("<Notify:%q@%s to=%v res=%v>", n.Alert, n.Timestamp, n.Receiver, n.Resolved)
}
// Fingerprint returns a quasi-unique fingerprint for the NotifyInfo.
func (n *NotifyInfo) Fingerprint() model.Fingerprint {
h := fnv.New64a()
h.Write([]byte(n.Receiver))

File diff suppressed because one or more lines are too long