refactor(backend): add support for OpenAPI based client for alertmanager

This commit is contained in:
Łukasz Mierzwa
2019-04-22 19:23:37 +01:00
parent f27849aed2
commit dfea73923c
17 changed files with 268 additions and 81 deletions

1
go.mod
View File

@@ -31,6 +31,7 @@ require (
github.com/spf13/pflag v1.0.3
github.com/spf13/viper v1.3.2
github.com/terinjokes/bakelite v0.2.0
golang.org/x/arch v0.0.0-20190312162104-788fe5ffcd8c // indirect
gopkg.in/go-playground/colors.v1 v1.2.0
gopkg.in/yaml.v2 v2.2.2
)

3
go.sum
View File

@@ -283,6 +283,8 @@ github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljT
github.com/ugorji/go/codec v0.0.0-20181209151446-772ced7fd4c2 h1:EICbibRW4JNKMcY+LsWmuwob+CRS1BmdRdjphAm9mH4=
github.com/ugorji/go/codec v0.0.0-20181209151446-772ced7fd4c2/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
golang.org/x/arch v0.0.0-20190312162104-788fe5ffcd8c h1:Rx/HTKi09myZ25t1SOlDHmHOy/mKxNAcu0hP1oPX9qM=
golang.org/x/arch v0.0.0-20190312162104-788fe5ffcd8c/go.mod h1:flIaEI6LNU6xOCD5PaJvn9wGP0agmIOqjrtsKGRguv4=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
@@ -348,6 +350,7 @@ mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b h1:DxJ5nJdkhDlLok9K6qO+5290kphD
mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b/go.mod h1:2odslEg/xrtNQqCYg2/jCoyKnw3vv5biOc3JnIcYfL4=
mvdan.cc/unparam v0.0.0-20190124213536-fbb59629db34 h1:B1LAOfRqg2QUyCdzfjf46quTSYUTAK5OCwbh6pljHbM=
mvdan.cc/unparam v0.0.0-20190124213536-fbb59629db34/go.mod h1:H6SUd1XjIs+qQCyskXg5OFSrilMRUkD8ePJpHKDPaeY=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
sourcegraph.com/sourcegraph/go-diff v0.5.1-0.20190210232911-dee78e514455 h1:qoQ5Kt+Zm+GXBtz49YwD3juBhr/E0U25jO6bBzxW6NI=
sourcegraph.com/sourcegraph/go-diff v0.5.1-0.20190210232911-dee78e514455/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck=
sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4 h1:JPJh2pk3+X4lXAkZIk2RuE/7/FoK9maXw+TNPJhVS/c=

View File

@@ -2,10 +2,11 @@ package alertmanager
import (
"github.com/prymitive/karma/internal/mapper"
"github.com/prymitive/karma/internal/mapper/v04"
"github.com/prymitive/karma/internal/mapper/v05"
"github.com/prymitive/karma/internal/mapper/v061"
"github.com/prymitive/karma/internal/mapper/v062"
v016 "github.com/prymitive/karma/internal/mapper/v016"
v04 "github.com/prymitive/karma/internal/mapper/v04"
v05 "github.com/prymitive/karma/internal/mapper/v05"
v061 "github.com/prymitive/karma/internal/mapper/v061"
v062 "github.com/prymitive/karma/internal/mapper/v062"
)
// initialize all mappers
@@ -16,4 +17,6 @@ func init() {
mapper.RegisterAlertMapper(v062.AlertMapper{})
mapper.RegisterSilenceMapper(v04.SilenceMapper{})
mapper.RegisterSilenceMapper(v05.SilenceMapper{})
mapper.RegisterAlertMapper(v016.AlertMapper{})
mapper.RegisterSilenceMapper(v016.SilenceMapper{})
}

View File

@@ -170,31 +170,40 @@ func (am *Alertmanager) pullSilences(version string) error {
return err
}
// generate full URL to collect silences from
url, err := mapper.AbsoluteURL(am.URI)
if err != nil {
log.Errorf("[%s] Failed to generate silences endpoint URL: %s", am.Name, err)
return err
}
// append query args if mapper needs those
queryArgs := mapper.QueryArgs()
if queryArgs != "" {
url = fmt.Sprintf("%s?%s", url, queryArgs)
}
var silences []models.Silence
start := time.Now()
// read raw body from the source
source, err := am.reader.Read(url, am.HTTPHeaders)
if err != nil {
log.Errorf("[%s] %s request failed: %s", am.Name, uri.SanitizeURI(url), err)
return err
}
defer source.Close()
if mapper.IsOpenAPI() {
silences, err = mapper.Collect(am.URI, am.HTTPHeaders, am.RequestTimeout, am.HTTPTransport)
if err != nil {
return err
}
} else {
// generate full URL to collect silences from
url, err := mapper.AbsoluteURL(am.URI)
if err != nil {
log.Errorf("[%s] Failed to generate silences endpoint URL: %s", am.Name, err)
return err
}
// append query args if mapper needs those
queryArgs := mapper.QueryArgs()
if queryArgs != "" {
url = fmt.Sprintf("%s?%s", url, queryArgs)
}
// decode body text
silences, err := mapper.Decode(source)
if err != nil {
return err
// read raw body from the source
source, err := am.reader.Read(url, am.HTTPHeaders)
if err != nil {
log.Errorf("[%s] %s request failed: %s", am.Name, uri.SanitizeURI(url), err)
return err
}
defer source.Close()
// decode body text
silences, err = mapper.Decode(source)
if err != nil {
return err
}
}
log.Infof("[%s] Got %d silences(s) in %s", am.Name, len(silences), time.Since(start))
@@ -234,32 +243,42 @@ func (am *Alertmanager) pullAlerts(version string) error {
return err
}
// generate full URL to collect alerts from
url, err := mapper.AbsoluteURL(am.URI)
if err != nil {
log.Errorf("[%s] Failed to generate alerts endpoint URL: %s", am.Name, err)
return err
}
// append query args if mapper needs those
queryArgs := mapper.QueryArgs()
if queryArgs != "" {
url = fmt.Sprintf("%s?%s", url, queryArgs)
}
var groups []models.AlertGroup
start := time.Now()
// read raw body from the source
source, err := am.reader.Read(url, am.HTTPHeaders)
if err != nil {
log.Errorf("[%s] %s request failed: %s", am.Name, uri.SanitizeURI(url), err)
return err
}
defer source.Close()
if mapper.IsOpenAPI() {
groups, err = mapper.Collect(am.URI, am.HTTPHeaders, am.RequestTimeout, am.HTTPTransport)
if err != nil {
return err
}
} else {
// decode body text
groups, err := mapper.Decode(source)
if err != nil {
return err
// generate full URL to collect alerts from
url, err := mapper.AbsoluteURL(am.URI)
if err != nil {
log.Errorf("[%s] Failed to generate alerts endpoint URL: %s", am.Name, err)
return err
}
// append query args if mapper needs those
queryArgs := mapper.QueryArgs()
if queryArgs != "" {
url = fmt.Sprintf("%s?%s", url, queryArgs)
}
// read raw body from the source
source, err := am.reader.Read(url, am.HTTPHeaders)
if err != nil {
log.Errorf("[%s] %s request failed: %s", am.Name, uri.SanitizeURI(url), err)
return err
}
defer source.Close()
// decode body text
groups, err = mapper.Decode(source)
if err != nil {
return err
}
}
log.Infof("[%s] Got %d alert group(s) in %s", am.Name, len(groups), time.Since(start))

View File

@@ -0,0 +1,22 @@
package mapper
import "net/http"
func SetHeaders(inner http.RoundTripper, headers map[string]string) http.RoundTripper {
return &headersRoundTripper{
inner: inner,
Headers: headers,
}
}
type headersRoundTripper struct {
inner http.RoundTripper
Headers map[string]string
}
func (hrt *headersRoundTripper) RoundTrip(r *http.Request) (*http.Response, error) {
for k, v := range hrt.Headers {
r.Header.Set(k, v)
}
return hrt.inner.RoundTrip(r)
}

View File

@@ -3,6 +3,8 @@ package mapper
import (
"fmt"
"io"
"net/http"
"time"
"github.com/prymitive/karma/internal/models"
)
@@ -17,18 +19,21 @@ type Mapper interface {
IsSupported(version string) bool
AbsoluteURL(baseURI string) (string, error)
QueryArgs() string
IsOpenAPI() bool
}
// AlertMapper handles mapping of Alertmanager alert information to karma AlertGroup models
type AlertMapper interface {
Mapper
Decode(io.ReadCloser) ([]models.AlertGroup, error)
Collect(string, map[string]string, time.Duration, http.RoundTripper) ([]models.AlertGroup, error)
}
// SilenceMapper handles mapping of Alertmanager silence information to karma Silence models
type SilenceMapper interface {
Mapper
Decode(io.ReadCloser) ([]models.Silence, error)
Collect(string, map[string]string, time.Duration, http.RoundTripper) ([]models.Silence, error)
}
// RegisterAlertMapper allows to register mapper implementing alert data

View File

@@ -1,4 +1,4 @@
FROM quay.io/goswagger/swagger:v0.18.0
FROM quay.io/goswagger/swagger:v0.19.0
RUN apk add --update curl

View File

@@ -1,9 +1,11 @@
package v016
import (
"io"
"net/http"
"time"
"github.com/blang/semver"
"github.com/prymitive/karma/internal/mapper"
"github.com/prymitive/karma/internal/models"
"github.com/prymitive/karma/internal/uri"
@@ -30,8 +32,12 @@ func (m AlertMapper) IsSupported(version string) bool {
return versionRange(semver.MustParse(version))
}
// Decode Alertmanager API response body and return karma model instances
func (m AlertMapper) Decode(source io.ReadCloser) ([]models.AlertGroup, error) {
defer source.Close()
return Groups()
// IsOpenAPI returns true is remote Alertmanager uses OpenAPI
func (m AlertMapper) IsOpenAPI() bool {
return true
}
func (m AlertMapper) Collect(uri string, headers map[string]string, timeout time.Duration, httpTransport http.RoundTripper) ([]models.AlertGroup, error) {
c := newClient(uri, headers, httpTransport)
return groups(c, timeout)
}

View File

@@ -1,28 +1,38 @@
package v016
import (
"net/http"
"net/url"
"path"
"sort"
"time"
httptransport "github.com/go-openapi/runtime/client"
"github.com/prymitive/karma/internal/mapper"
"github.com/prymitive/karma/internal/mapper/v016/client"
"github.com/prymitive/karma/internal/mapper/v016/client/alertgroup"
"github.com/prymitive/karma/internal/mapper/v016/client/silence"
"github.com/prymitive/karma/internal/models"
)
func newClient(uri string, headers map[string]string, httpTransport http.RoundTripper) *client.Alertmanager {
u, _ := url.Parse(uri)
transport := httptransport.New(u.Host, path.Join(u.Path, "/api/v2"), []string{u.Scheme})
if httpTransport != nil {
transport.Transport = mapper.SetHeaders(httpTransport, headers)
} else {
transport.Transport = mapper.SetHeaders(transport.Transport, headers)
}
c := client.New(transport, nil)
return c
}
// Alerts will fetch all alert groups from the API
func Groups() ([]models.AlertGroup, error) {
func groups(c *client.Alertmanager, timeout time.Duration) ([]models.AlertGroup, error) {
ret := []models.AlertGroup{}
transport := client.TransportConfig{
Host: "localhost:9093",
BasePath: "/api/v2",
Schemes: []string{"http"},
}
cli := client.NewHTTPClientWithConfig(nil, &transport)
timeout := time.Second * 30
groups, err := cli.Alertgroup.GetAlertGroups(alertgroup.NewGetAlertGroupsParamsWithTimeout(timeout))
groups, err := c.Alertgroup.GetAlertGroups(alertgroup.NewGetAlertGroupsParamsWithTimeout(timeout))
if err != nil {
return []models.AlertGroup{}, err
}
@@ -53,3 +63,33 @@ func Groups() ([]models.AlertGroup, error) {
return ret, nil
}
func silences(c *client.Alertmanager, timeout time.Duration) ([]models.Silence, error) {
ret := []models.Silence{}
silences, err := c.Silence.GetSilences(silence.NewGetSilencesParamsWithTimeout(timeout))
if err != nil {
return ret, err
}
for _, s := range silences.Payload {
us := models.Silence{
ID: *s.ID,
StartsAt: time.Time(*s.StartsAt),
EndsAt: time.Time(*s.EndsAt),
CreatedBy: *s.CreatedBy,
Comment: *s.Comment,
}
for _, m := range s.Matchers {
sm := models.SilenceMatcher{
Name: *m.Name,
Value: *m.Value,
IsRegex: *m.IsRegex,
}
us.Matchers = append(us.Matchers, sm)
}
ret = append(ret, us)
}
return ret, nil
}

View File

@@ -0,0 +1,42 @@
package v016
import (
"net/http"
"time"
"github.com/blang/semver"
"github.com/prymitive/karma/internal/mapper"
"github.com/prymitive/karma/internal/models"
"github.com/prymitive/karma/internal/uri"
)
// SilenceMapper implements Alertmanager 0.4 API schema
type SilenceMapper struct {
mapper.SilenceMapper
}
// AbsoluteURL for silences API endpoint this mapper supports
func (m SilenceMapper) AbsoluteURL(baseURI string) (string, error) {
return uri.JoinURL(baseURI, "api/v2/silences")
}
// QueryArgs for HTTP requests send to the Alertmanager API endpoint
func (m SilenceMapper) QueryArgs() string {
return ""
}
// IsSupported returns true if given version string is supported
func (m SilenceMapper) IsSupported(version string) bool {
versionRange := semver.MustParseRange(">=0.16.0")
return versionRange(semver.MustParse(version))
}
// IsOpenAPI returns true is remote Alertmanager uses OpenAPI
func (m SilenceMapper) IsOpenAPI() bool {
return true
}
func (m SilenceMapper) Collect(uri string, headers map[string]string, timeout time.Duration, httpTransport http.RoundTripper) ([]models.Silence, error) {
c := newClient(uri, headers, httpTransport)
return silences(c, timeout)
}

View File

@@ -70,6 +70,11 @@ func (m AlertMapper) IsSupported(version string) bool {
return versionRange(semver.MustParse(version))
}
// IsOpenAPI returns true is remote Alertmanager uses OpenAPI
func (m AlertMapper) IsOpenAPI() bool {
return false
}
// Decode Alertmanager API response body and return karma model instances
func (m AlertMapper) Decode(source io.ReadCloser) ([]models.AlertGroup, error) {
groups := []models.AlertGroup{}

View File

@@ -67,6 +67,11 @@ func (m SilenceMapper) IsSupported(version string) bool {
return versionRange(semver.MustParse(version))
}
// IsOpenAPI returns true is remote Alertmanager uses OpenAPI
func (m SilenceMapper) IsOpenAPI() bool {
return false
}
// Decode Alertmanager API response body and return karma model instances
func (m SilenceMapper) Decode(source io.ReadCloser) ([]models.Silence, error) {
silences := []models.Silence{}
@@ -85,13 +90,20 @@ func (m SilenceMapper) Decode(source io.ReadCloser) ([]models.Silence, error) {
for _, s := range resp.Data.Silences {
us := models.Silence{
ID: strconv.Itoa(s.ID),
Matchers: s.Matchers,
StartsAt: s.StartsAt,
EndsAt: s.EndsAt,
CreatedAt: s.CreatedAt,
CreatedBy: s.CreatedBy,
Comment: s.Comment,
}
for _, m := range s.Matchers {
sm := models.SilenceMatcher{
Name: m.Name,
Value: m.Value,
IsRegex: m.IsRegex,
}
us.Matchers = append(us.Matchers, sm)
}
silences = append(silences, us)
}
return silences, nil

View File

@@ -69,6 +69,11 @@ func (m AlertMapper) IsSupported(version string) bool {
return versionRange(semver.MustParse(version))
}
// IsOpenAPI returns true is remote Alertmanager uses OpenAPI
func (m AlertMapper) IsOpenAPI() bool {
return false
}
// Decode Alertmanager API response body and return karma model instances
func (m AlertMapper) Decode(source io.ReadCloser) ([]models.AlertGroup, error) {
groups := []models.AlertGroup{}

View File

@@ -53,10 +53,15 @@ func (m SilenceMapper) QueryArgs() string {
// IsSupported returns true if given version string is supported
func (m SilenceMapper) IsSupported(version string) bool {
versionRange := semver.MustParseRange(">=0.5.0")
versionRange := semver.MustParseRange(">=0.5.0 <0.16.0")
return versionRange(semver.MustParse(version))
}
// IsOpenAPI returns true is remote Alertmanager uses OpenAPI
func (m SilenceMapper) IsOpenAPI() bool {
return false
}
// Decode Alertmanager API response body and return karma model instances
func (m SilenceMapper) Decode(source io.ReadCloser) ([]models.Silence, error) {
silences := []models.Silence{}
@@ -75,13 +80,20 @@ func (m SilenceMapper) Decode(source io.ReadCloser) ([]models.Silence, error) {
for _, s := range resp.Data {
us := models.Silence{
ID: s.ID,
Matchers: s.Matchers,
StartsAt: s.StartsAt,
EndsAt: s.EndsAt,
CreatedAt: s.CreatedAt,
CreatedBy: s.CreatedBy,
Comment: s.Comment,
}
for _, m := range s.Matchers {
sm := models.SilenceMatcher{
Name: m.Name,
Value: m.Value,
IsRegex: m.IsRegex,
}
us.Matchers = append(us.Matchers, sm)
}
silences = append(silences, us)
}
return silences, nil

View File

@@ -71,6 +71,11 @@ func (m AlertMapper) IsSupported(version string) bool {
return versionRange(semver.MustParse(version))
}
// IsOpenAPI returns true is remote Alertmanager uses OpenAPI
func (m AlertMapper) IsOpenAPI() bool {
return false
}
// Decode Alertmanager API response body and return karma model instances
func (m AlertMapper) Decode(source io.ReadCloser) ([]models.AlertGroup, error) {
groups := []models.AlertGroup{}

View File

@@ -71,10 +71,15 @@ func (m AlertMapper) QueryArgs() string {
// IsSupported returns true if given version string is supported
func (m AlertMapper) IsSupported(version string) bool {
versionRange := semver.MustParseRange(">=0.6.2")
versionRange := semver.MustParseRange(">=0.6.2 <0.16.0")
return versionRange(semver.MustParse(version))
}
// IsOpenAPI returns true is remote Alertmanager uses OpenAPI
func (m AlertMapper) IsOpenAPI() bool {
return false
}
// Decode Alertmanager API response body and return karma model instances
func (m AlertMapper) Decode(source io.ReadCloser) ([]models.AlertGroup, error) {
groups := []models.AlertGroup{}

View File

@@ -2,22 +2,24 @@ package models
import "time"
type SilenceMatcher struct {
Name string `json:"name"`
Value string `json:"value"`
IsRegex bool `json:"isRegex"`
}
// Silence is vanilla silence + some additional attributes
// karma adds JIRA support, it can extract JIRA IDs from comments
// extracted ID is used to generate link to JIRA issue
// this means karma needs to store additional fields for each silence
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"`
ID string `json:"id"`
Matchers []SilenceMatcher `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"`
// karma fields
JiraID string `json:"jiraID"`
JiraURL string `json:"jiraURL"`