From 48848e2f08dea1071a6cfe8e37733adf5ce4ce23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Mierzwa?= Date: Wed, 5 Apr 2017 22:41:59 -0700 Subject: [PATCH] Add mapper module allowing to pick Alertmanager schema handlers at runtime This modules allows to have a dedicated path for unmarshaling Alertmanager reponse per Alertmanager version. It returns data in format handled internally by unsee (which is Alertmanager data + some additional attributes) --- mapper/mapper.go | 59 ++++++++++++++++++++++++ mapper/v04/alerts.go | 101 +++++++++++++++++++++++++++++++++++++++++ mapper/v04/silences.go | 93 +++++++++++++++++++++++++++++++++++++ mapper/v05/alerts.go | 97 +++++++++++++++++++++++++++++++++++++++ mapper/v05/silences.go | 84 ++++++++++++++++++++++++++++++++++ 5 files changed, 434 insertions(+) create mode 100644 mapper/mapper.go create mode 100644 mapper/v04/alerts.go create mode 100644 mapper/v04/silences.go create mode 100644 mapper/v05/alerts.go create mode 100644 mapper/v05/silences.go diff --git a/mapper/mapper.go b/mapper/mapper.go new file mode 100644 index 000000000..68e79bc71 --- /dev/null +++ b/mapper/mapper.go @@ -0,0 +1,59 @@ +package mapper + +import ( + "fmt" + + "github.com/cloudflare/unsee/models" +) + +var ( + alertMappers = []AlertMapper{} + silenceMappers = []SilenceMapper{} +) + +// AlertMapper implements Alertmanager -> unsee alert data mapping that works +// for a specific range of Alertmanager versions +type AlertMapper interface { + IsSupported(version string) bool + GetAlerts() ([]models.UnseeAlertGroup, error) +} + +// RegisterAlertMapper allows to register mapper implementing alert data +// handling for specific Alertmanager versions +func RegisterAlertMapper(m AlertMapper) { + alertMappers = append(alertMappers, m) +} + +// GetAlertMapper returns mapper for given version +func GetAlertMapper(version string) (AlertMapper, error) { + for _, m := range alertMappers { + if m.IsSupported(version) { + return m, nil + } + } + return nil, fmt.Errorf("Can't find alert mapper for Alertmanager %s", version) +} + +// SilenceMapper implements Alertmanager -> unsee silence data mapping that +// works for a specific range of Alertmanager versions +type SilenceMapper interface { + Release() string + IsSupported(version string) bool + GetSilences() ([]models.UnseeSilence, error) +} + +// RegisterSilenceMapper allows to register mapper implementing silence data +// handling for specific Alertmanager versions +func RegisterSilenceMapper(m SilenceMapper) { + silenceMappers = append(silenceMappers, m) +} + +// GetSilenceMapper returns mapper for given version +func GetSilenceMapper(version string) (SilenceMapper, error) { + for _, m := range silenceMappers { + if m.IsSupported(version) { + return m, nil + } + } + return nil, fmt.Errorf("Can't find silence mapper for Alertmanager %s", version) +} diff --git a/mapper/v04/alerts.go b/mapper/v04/alerts.go new file mode 100644 index 000000000..39f0d8402 --- /dev/null +++ b/mapper/v04/alerts.go @@ -0,0 +1,101 @@ +// Package v04 package implements support for interacting with Alertmanager 0.4 +// Collected data will be mapped to unsee internal schema defined the +// unsee/models package +// This file defines Alertmanager alerts mapping +package v04 + +import ( + "errors" + "time" + + "github.com/blang/semver" + "github.com/cloudflare/unsee/config" + "github.com/cloudflare/unsee/mapper" + "github.com/cloudflare/unsee/models" + "github.com/cloudflare/unsee/transport" +) + +// AlertmanagerAlert is vanilla alert object from Alertmanager 0.4 +type alert struct { + Annotations map[string]string `json:"annotations"` + Labels map[string]string `json:"labels"` + StartsAt time.Time `json:"startsAt"` + EndsAt time.Time `json:"endsAt"` + GeneratorURL string `json:"generatorURL"` + Inhibited bool `json:"inhibited"` + Silenced int `json:"silenced"` +} + +// alertsGroupsV04 is vanilla group object from Alertmanager, exposed under api/v1/alerts/groups +type alertsGroups struct { + Labels map[string]string `json:"labels"` + Blocks []struct { + Alerts []alert `json:"alerts"` + } `json:"blocks"` +} + +type alertsGroupsAPISchema struct { + Status string `json:"status"` + Groups []alertsGroups `json:"data"` + Error string `json:"error"` +} + +// AlertMapper implements Alertmanager 0.4 API schema +type AlertMapper struct { + mapper.AlertMapper +} + +// IsSupported returns true if given version string is supported +func (m AlertMapper) IsSupported(version string) bool { + versionRange := semver.MustParseRange(">=0.4.0 <0.5.0") + return versionRange(semver.MustParse(version)) +} + +// GetAlerts will make a request to Alertmanager API and parse the response +// It will only return alerts or error (if any) +func (m AlertMapper) GetAlerts() ([]models.UnseeAlertGroup, error) { + groups := []models.UnseeAlertGroup{} + resp := alertsGroupsAPISchema{} + + url, err := transport.JoinURL(config.Config.AlertmanagerURI, "api/v1/alerts/groups") + if err != nil { + return groups, err + } + + err = transport.GetJSONFromURL(url, config.Config.AlertmanagerTimeout, &resp) + if err != nil { + return groups, err + } + + if resp.Status != "success" { + return groups, errors.New(resp.Error) + } + + for _, g := range resp.Groups { + alertList := models.UnseeAlertList{} + for _, b := range g.Blocks { + for _, a := range b.Alerts { + us := models.UnseeAlert{ + AlertmanagerAlert: models.AlertmanagerAlert{ + Annotations: a.Annotations, + Labels: a.Labels, + StartsAt: a.StartsAt, + EndsAt: a.EndsAt, + GeneratorURL: a.GeneratorURL, + Inhibited: a.Inhibited, + }, + } + if a.Silenced > 0 { + us.Silenced = string(a.Silenced) + } + alertList = append(alertList, us) + } + } + ug := models.UnseeAlertGroup{ + Labels: g.Labels, + Alerts: alertList, + } + groups = append(groups, ug) + } + return groups, nil +} diff --git a/mapper/v04/silences.go b/mapper/v04/silences.go new file mode 100644 index 000000000..cebd13571 --- /dev/null +++ b/mapper/v04/silences.go @@ -0,0 +1,93 @@ +// Package v04 package implements support for interacting with Alertmanager 0.4 +// Collected data will be mapped to unsee internal schema defined the +// unsee/models package +// This file defines Alertmanager silences mapping +package v04 + +import ( + "errors" + "fmt" + "math" + "time" + + "github.com/blang/semver" + "github.com/cloudflare/unsee/config" + "github.com/cloudflare/unsee/mapper" + "github.com/cloudflare/unsee/models" + "github.com/cloudflare/unsee/transport" +) + +// Alertmanager 0.4 silence format +type silence struct { + ID int `json:"id"` + Matchers []struct { + Name string `json:"name"` + Value string `json:"value"` + IsRegex bool `json:"isRegex"` + } `json:"matchers"` + StartsAt time.Time `json:"startsAt"` + EndsAt time.Time `json:"endsAt"` + CreatedAt time.Time `json:"createdAt"` + CreatedBy string `json:"createdBy"` + Comment string `json:"comment"` +} + +// silenceAPIResponseV04 is what Alertmanager 0.4 API returns +type silenceAPISchema struct { + Status string `json:"status"` + Data struct { + Silences []silence `json:"silences"` + TotalSilences int `json:"totalSilences"` + } `json:"data"` + Error string `json:"error"` +} + +// SilenceMapper implements Alertmanager 0.4 API schema +type SilenceMapper struct { + mapper.SilenceMapper +} + +// IsSupported returns true if given version string is supported +func (m SilenceMapper) IsSupported(version string) bool { + versionRange := semver.MustParseRange(">=0.4.0 <0.5.0") + return versionRange(semver.MustParse(version)) +} + +// GetSilences will make a request to Alertmanager API and parse the response +// It will only return silences or error (if any) +func (m SilenceMapper) GetSilences() ([]models.UnseeSilence, error) { + silences := []models.UnseeSilence{} + resp := silenceAPISchema{} + + url, err := transport.JoinURL(config.Config.AlertmanagerURI, "api/v1/silences") + if err != nil { + return silences, err + } + + // Alertmanager 0.4 uses pagination for silences + url = fmt.Sprintf("%s?limit=%d", url, math.MaxUint32) + err = transport.GetJSONFromURL(url, config.Config.AlertmanagerTimeout, &resp) + if err != nil { + return silences, err + } + + if resp.Status != "success" { + return silences, errors.New(resp.Error) + } + + for _, s := range resp.Data.Silences { + us := models.UnseeSilence{ + AlertmanagerSilence: models.AlertmanagerSilence{ + ID: string(s.ID), + Matchers: s.Matchers, + StartsAt: s.StartsAt, + EndsAt: s.EndsAt, + CreatedAt: s.CreatedAt, + CreatedBy: s.CreatedBy, + Comment: s.Comment, + }, + } + silences = append(silences, us) + } + return silences, nil +} diff --git a/mapper/v05/alerts.go b/mapper/v05/alerts.go new file mode 100644 index 000000000..1865fb540 --- /dev/null +++ b/mapper/v05/alerts.go @@ -0,0 +1,97 @@ +// Package v05 package implements support for interacting with Alertmanager 0.5 +// Collected data will be mapped to unsee internal schema defined the +// unsee/models package +// This file defines Alertmanager alerts mapping +package v05 + +import ( + "errors" + "time" + + "github.com/blang/semver" + "github.com/cloudflare/unsee/config" + "github.com/cloudflare/unsee/mapper" + "github.com/cloudflare/unsee/models" + "github.com/cloudflare/unsee/transport" +) + +type alert struct { + Annotations map[string]string `json:"annotations"` + Labels map[string]string `json:"labels"` + StartsAt time.Time `json:"startsAt"` + EndsAt time.Time `json:"endsAt"` + GeneratorURL string `json:"generatorURL"` + Inhibited bool `json:"inhibited"` + Silenced string `json:"silenced"` +} + +type alertsGroups struct { + Labels map[string]string `json:"labels"` + Blocks []struct { + Alerts []alert `json:"alerts"` + } `json:"blocks"` +} + +type alertsGroupsAPISchema struct { + Status string `json:"status"` + Groups []alertsGroups `json:"data"` + Error string `json:"error"` +} + +// AlertMapper implements Alertmanager 0.5 API schema +type AlertMapper struct { + mapper.AlertMapper +} + +// IsSupported returns true if given version string is supported +func (m AlertMapper) IsSupported(version string) bool { + versionRange := semver.MustParseRange(">=0.5.0") + return versionRange(semver.MustParse(version)) +} + +// GetAlerts will make a request to Alertmanager API and parse the response +// It will only return alerts or error (if any) +func (m AlertMapper) GetAlerts() ([]models.UnseeAlertGroup, error) { + groups := []models.UnseeAlertGroup{} + resp := alertsGroupsAPISchema{} + + url, err := transport.JoinURL(config.Config.AlertmanagerURI, "api/v1/alerts/groups") + if err != nil { + return groups, err + } + + err = transport.GetJSONFromURL(url, config.Config.AlertmanagerTimeout, &resp) + if err != nil { + return groups, err + } + + if resp.Status != "success" { + return groups, errors.New(resp.Error) + } + + for _, g := range resp.Groups { + alertList := models.UnseeAlertList{} + for _, b := range g.Blocks { + for _, a := range b.Alerts { + us := models.UnseeAlert{ + AlertmanagerAlert: models.AlertmanagerAlert{ + Annotations: a.Annotations, + Labels: a.Labels, + StartsAt: a.StartsAt, + EndsAt: a.EndsAt, + GeneratorURL: a.GeneratorURL, + Inhibited: a.Inhibited, + Silenced: a.Silenced, + }, + } + alertList = append(alertList, us) + } + } + ug := models.UnseeAlertGroup{ + Labels: g.Labels, + Alerts: alertList, + } + groups = append(groups, ug) + } + return groups, nil +} diff --git a/mapper/v05/silences.go b/mapper/v05/silences.go new file mode 100644 index 000000000..572e5f186 --- /dev/null +++ b/mapper/v05/silences.go @@ -0,0 +1,84 @@ +// Package v05 package implements support for interacting with Alertmanager 0.5 +// Collected data will be mapped to unsee internal schema defined the +// unsee/models package +// This file defines Alertmanager alerts mapping +package v05 + +import ( + "errors" + "time" + + "github.com/blang/semver" + "github.com/cloudflare/unsee/config" + "github.com/cloudflare/unsee/mapper" + "github.com/cloudflare/unsee/models" + "github.com/cloudflare/unsee/transport" +) + +type silence struct { + ID string `json:"id"` + Matchers []struct { + Name string `json:"name"` + Value string `json:"value"` + IsRegex bool `json:"isRegex"` + } `json:"matchers"` + StartsAt time.Time `json:"startsAt"` + EndsAt time.Time `json:"endsAt"` + CreatedAt time.Time `json:"createdAt"` + CreatedBy string `json:"createdBy"` + Comment string `json:"comment"` +} + +type silenceAPISchema struct { + Status string `json:"status"` + Data []silence `json:"data"` + Error string `json:"error"` +} + +// SilenceMapper implements Alertmanager 0.4 API schema +type SilenceMapper struct { + mapper.SilenceMapper +} + +// IsSupported returns true if given version string is supported +func (m SilenceMapper) IsSupported(version string) bool { + versionRange := semver.MustParseRange(">=0.5.0") + return versionRange(semver.MustParse(version)) +} + +// GetSilences will make a request to Alertmanager API and parse the response +// It will only return silences or error (if any) +func (m SilenceMapper) GetSilences() ([]models.UnseeSilence, error) { + silences := []models.UnseeSilence{} + resp := silenceAPISchema{} + + url, err := transport.JoinURL(config.Config.AlertmanagerURI, "api/v1/silences") + if err != nil { + return silences, err + } + + err = transport.GetJSONFromURL(url, config.Config.AlertmanagerTimeout, &resp) + if err != nil { + return silences, err + } + + if resp.Status != "success" { + return silences, errors.New(resp.Error) + } + + for _, s := range resp.Data { + us := models.UnseeSilence{ + AlertmanagerSilence: models.AlertmanagerSilence{ + ID: s.ID, + Matchers: s.Matchers, + StartsAt: s.StartsAt, + EndsAt: s.EndsAt, + CreatedAt: s.CreatedAt, + CreatedBy: s.CreatedBy, + Comment: s.Comment, + }, + } + silences = append(silences, us) + } + return silences, nil +}