From dfea73923c49ffd58ab2854147fe6859fd87f593 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Mierzwa?= Date: Mon, 22 Apr 2019 19:23:37 +0100 Subject: [PATCH] refactor(backend): add support for OpenAPI based client for alertmanager --- go.mod | 1 + go.sum | 3 + internal/alertmanager/mapper.go | 11 ++-- internal/alertmanager/models.go | 109 ++++++++++++++++++------------- internal/mapper/headers.go | 22 +++++++ internal/mapper/mapper.go | 5 ++ internal/mapper/v016/Dockerfile | 2 +- internal/mapper/v016/alerts.go | 16 +++-- internal/mapper/v016/api.go | 62 ++++++++++++++---- internal/mapper/v016/silences.go | 42 ++++++++++++ internal/mapper/v04/alerts.go | 5 ++ internal/mapper/v04/silences.go | 14 +++- internal/mapper/v05/alerts.go | 5 ++ internal/mapper/v05/silences.go | 16 ++++- internal/mapper/v061/alerts.go | 5 ++ internal/mapper/v062/alerts.go | 7 +- internal/models/silence.go | 24 +++---- 17 files changed, 268 insertions(+), 81 deletions(-) create mode 100644 internal/mapper/headers.go create mode 100644 internal/mapper/v016/silences.go diff --git a/go.mod b/go.mod index 213ef4fe5..37e940a58 100644 --- a/go.mod +++ b/go.mod @@ -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 ) diff --git a/go.sum b/go.sum index 9e013ce09..fb04963cf 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/internal/alertmanager/mapper.go b/internal/alertmanager/mapper.go index 98ae27daf..8694e3b7f 100644 --- a/internal/alertmanager/mapper.go +++ b/internal/alertmanager/mapper.go @@ -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{}) } diff --git a/internal/alertmanager/models.go b/internal/alertmanager/models.go index e68a8d7c6..f8cb4f8a8 100644 --- a/internal/alertmanager/models.go +++ b/internal/alertmanager/models.go @@ -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)) diff --git a/internal/mapper/headers.go b/internal/mapper/headers.go new file mode 100644 index 000000000..ace810bdc --- /dev/null +++ b/internal/mapper/headers.go @@ -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) +} diff --git a/internal/mapper/mapper.go b/internal/mapper/mapper.go index 6d2153d50..72ed65bcf 100644 --- a/internal/mapper/mapper.go +++ b/internal/mapper/mapper.go @@ -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 diff --git a/internal/mapper/v016/Dockerfile b/internal/mapper/v016/Dockerfile index c3b31044a..c8d0e8ad9 100644 --- a/internal/mapper/v016/Dockerfile +++ b/internal/mapper/v016/Dockerfile @@ -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 diff --git a/internal/mapper/v016/alerts.go b/internal/mapper/v016/alerts.go index 95dfe9fe1..c7e27b604 100644 --- a/internal/mapper/v016/alerts.go +++ b/internal/mapper/v016/alerts.go @@ -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) } diff --git a/internal/mapper/v016/api.go b/internal/mapper/v016/api.go index f57f9aeb2..426eee548 100644 --- a/internal/mapper/v016/api.go +++ b/internal/mapper/v016/api.go @@ -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 +} diff --git a/internal/mapper/v016/silences.go b/internal/mapper/v016/silences.go new file mode 100644 index 000000000..e5e83fe22 --- /dev/null +++ b/internal/mapper/v016/silences.go @@ -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) +} diff --git a/internal/mapper/v04/alerts.go b/internal/mapper/v04/alerts.go index 85aa6e922..1f2dadd6f 100644 --- a/internal/mapper/v04/alerts.go +++ b/internal/mapper/v04/alerts.go @@ -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{} diff --git a/internal/mapper/v04/silences.go b/internal/mapper/v04/silences.go index a3cc6b68c..238bb5505 100644 --- a/internal/mapper/v04/silences.go +++ b/internal/mapper/v04/silences.go @@ -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 diff --git a/internal/mapper/v05/alerts.go b/internal/mapper/v05/alerts.go index 5d148855d..a7d8e3ca4 100644 --- a/internal/mapper/v05/alerts.go +++ b/internal/mapper/v05/alerts.go @@ -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{} diff --git a/internal/mapper/v05/silences.go b/internal/mapper/v05/silences.go index 35dfce99f..482aa43cf 100644 --- a/internal/mapper/v05/silences.go +++ b/internal/mapper/v05/silences.go @@ -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 diff --git a/internal/mapper/v061/alerts.go b/internal/mapper/v061/alerts.go index 20c652065..beb888497 100644 --- a/internal/mapper/v061/alerts.go +++ b/internal/mapper/v061/alerts.go @@ -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{} diff --git a/internal/mapper/v062/alerts.go b/internal/mapper/v062/alerts.go index 2050a2e92..2189acc47 100644 --- a/internal/mapper/v062/alerts.go +++ b/internal/mapper/v062/alerts.go @@ -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{} diff --git a/internal/models/silence.go b/internal/models/silence.go index 6422da38d..4187d3dba 100644 --- a/internal/models/silence.go +++ b/internal/models/silence.go @@ -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"`