From c9b4966914e96c002d8bc5939f39f7e2529f7fd7 Mon Sep 17 00:00:00 2001 From: Bartek Plotka Date: Fri, 12 Apr 2019 21:28:26 +0100 Subject: [PATCH] jiralert: Improved docs and config. Signed-off-by: Bartek Plotka --- README.md | 14 +++++++++-- cmd/jiralert/content.go | 23 ++---------------- cmd/jiralert/main.go | 12 +++++----- examples/jiralert.tmpl | 10 -------- examples/jiralert.yml | 51 ---------------------------------------- pkg/config/config.go | 46 ++++++++++++++++++++++++++---------- pkg/template/template.go | 16 +++++++++---- 7 files changed, 65 insertions(+), 107 deletions(-) delete mode 100644 examples/jiralert.tmpl delete mode 100644 examples/jiralert.yml diff --git a/README.md b/README.md index 7dc167ba..cf16dc50 100644 --- a/README.md +++ b/README.md @@ -52,9 +52,19 @@ $ curl -H "Content-type: application/json" -X POST \ ## Configuration -The configuration file is essentially a list of receivers matching 1-to-1 all Alertmanager receivers using JIRAlert; plus defaults (in the form of a partially defined receiver); and a pointer to the template file. +The configuration file is essentially a list of JiraAlert receivers plus defaults (in the form of a partially defined receiver); and a pointer to the template file. -Each receiver must have a unique name (matching the Alertmanager receiver name), JIRA API access fields (URL, username and password), a handful of required issue fields (such as the JIRA project and issue summary), some optional issue fields (e.g. priority) and a `fields` map for other (standard or custom) JIRA fields. Most of these may use [Go templating](https://golang.org/pkg/text/template/) to generate the actual field values based on the contents of the Alertmanager notification. The exact same data structures and functions as those defined in the [Alertmanager template reference](https://prometheus.io/docs/alerting/notifications/) are available in JIRAlert. +You can find more docs in [the configuration itself](/pkg/config/config.go) + +Each receiver must have: +* a unique name (matching the Alertmanager receiver name) +* JIRA API access fields (URL, username and password), +* handful of required issue fields (such as the JIRA project and issue summary), +* some optional issue fields (e.g. priority) and a `fields` map for other (standard or custom) JIRA fields. + +Most of these may use [Go templating](https://golang.org/pkg/text/template/) to generate the actual field values based on the contents of the Alertmanager notification. + +The exact same data structures and functions as those defined in the [Alertmanager template reference](https://prometheus.io/docs/alerting/notifications/) are available in JIRAlert. ## Alertmanager configuration diff --git a/cmd/jiralert/content.go b/cmd/jiralert/content.go index eead48ce..49051d74 100644 --- a/cmd/jiralert/content.go +++ b/cmd/jiralert/content.go @@ -53,11 +53,6 @@ const (

Configuration

{{ .Config }}
{{- end }} - - {{ define "content.error" -}} -

Error

-
{{ .Err }}
- {{- end }} ` ) @@ -66,16 +61,12 @@ type tdata struct { // `/config` only Config string - - // `/error` only - Err error } var ( allTemplates = template.Must(template.New("").Parse(templates)) homeTemplate = pageTemplate("home") configTemplate = pageTemplate("config") - // errorTemplate = pageTemplate("error") ) func pageTemplate(name string) *template.Template { @@ -86,7 +77,7 @@ func pageTemplate(name string) *template.Template { // HomeHandlerFunc is the HTTP handler for the home page (`/`). func HomeHandlerFunc() func(http.ResponseWriter, *http.Request) { return func(w http.ResponseWriter, r *http.Request) { - homeTemplate.Execute(w, &tdata{ + _ = homeTemplate.Execute(w, &tdata{ DocsUrl: docsUrl, }) } @@ -95,19 +86,9 @@ func HomeHandlerFunc() func(http.ResponseWriter, *http.Request) { // ConfigHandlerFunc is the HTTP handler for the `/config` page. It outputs the configuration marshaled in YAML format. func ConfigHandlerFunc(config *config.Config) func(http.ResponseWriter, *http.Request) { return func(w http.ResponseWriter, r *http.Request) { - configTemplate.Execute(w, &tdata{ + _ = configTemplate.Execute(w, &tdata{ DocsUrl: docsUrl, Config: config.String(), }) } } - -// HandleError is an error handler that other handlers defer to in case of error. It is important to not have written -// anything to w before calling HandleError(), or the 500 status code won't be set (and the content might be mixed up). -//func HandleError(err error, metricsPath string, w http.ResponseWriter, r *http.Request) { -// w.WriteHeader(http.StatusInternalServerError) -// errorTemplate.Execute(w, &tdata{ -// DocsUrl: docsUrl, -// Err: err, -// }) -//} diff --git a/cmd/jiralert/main.go b/cmd/jiralert/main.go index b6881a1a..a52ef995 100644 --- a/cmd/jiralert/main.go +++ b/cmd/jiralert/main.go @@ -48,21 +48,21 @@ func main() { var logger = setupLogger(*logLevel, *logFormat) level.Info(logger).Log("msg", "starting JIRAlert", "version", Version) - config, _, err := config.LoadFile(*configFile, logger) + cfg, _, err := config.LoadFile(*configFile, logger) if err != nil { level.Error(logger).Log("msg", "error loading configuration", "path", *configFile, "err", err) os.Exit(1) } - tmpl, err := template.LoadTemplate(config.Template, logger) + tmpl, err := template.LoadTemplate(cfg.Template, logger) if err != nil { - level.Error(logger).Log("msg", "error loading templates", "path", config.Template, "err", err) + level.Error(logger).Log("msg", "error loading templates", "path", cfg.Template, "err", err) os.Exit(1) } http.HandleFunc("/alert", func(w http.ResponseWriter, req *http.Request) { level.Debug(logger).Log("msg", "handling /alert webhook request") - defer req.Body.Close() + defer func() { _ = req.Body.Close() }() // https://godoc.org/github.com/prometheus/alertmanager/template#Data data := alertmanager.Data{} @@ -71,7 +71,7 @@ func main() { return } - conf := config.ReceiverByName(data.Receiver) + conf := cfg.ReceiverByName(data.Receiver) if conf == nil { errorHandler(w, http.StatusNotFound, fmt.Errorf("receiver missing: %s", data.Receiver), unknownReceiver, &data, logger) return @@ -107,7 +107,7 @@ func main() { }) http.HandleFunc("/", HomeHandlerFunc()) - http.HandleFunc("/config", ConfigHandlerFunc(config)) + http.HandleFunc("/cfg", ConfigHandlerFunc(cfg)) http.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) { http.Error(w, "OK", http.StatusOK) }) http.Handle("/metrics", promhttp.Handler()) diff --git a/examples/jiralert.tmpl b/examples/jiralert.tmpl deleted file mode 100644 index a5690573..00000000 --- a/examples/jiralert.tmpl +++ /dev/null @@ -1,10 +0,0 @@ -{{ define "jira.summary" }}[{{ .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 "jira.description" }}{{ range .Alerts.Firing }}Labels: -{{ range .Labels.SortedPairs }} - {{ .Name }} = {{ .Value }} -{{ end }} -Annotations: -{{ range .Annotations.SortedPairs }} - {{ .Name }} = {{ .Value }} -{{ end }} -Source: {{ .GeneratorURL }} -{{ end }}{{ end }} diff --git a/examples/jiralert.yml b/examples/jiralert.yml deleted file mode 100644 index ef634189..00000000 --- a/examples/jiralert.yml +++ /dev/null @@ -1,51 +0,0 @@ -# Global defaults, applied to all receivers where not explicitly overridden. Optional. -defaults: - # API access fields. - api_url: https://jiralert.atlassian.net - user: jiralert - password: 'JIRAlert' - - # The type of JIRA issue to create. Required. - issue_type: Bug - # Issue priority. Optional. - priority: Critical - # Go template invocation for generating the summary. Required. - summary: '{{ template "jira.summary" . }}' - # Go template invocation for generating the description. Optional. - description: '{{ template "jira.description" . }}' - # State to transition into when reopening a closed issue. Required. - reopen_state: "To Do" - # Do not reopen issues with this resolution. Optional. - wont_fix_resolution: "Won't Fix" - # Amount of time after being closed that an issue should be reopened, after which, a new issue is created. - # Optional (default: always reopen) - reopen_duration: 0h - -# Receiver definitions. At least one must be defined. -receivers: - # Must match the Alertmanager receiver name. Required. - - name: 'jira-ab' - # JIRA project to create the issue in. Required. - project: AB - # Copy all Prometheus labels into separate JIRA labels. Optional (default: false). - add_group_labels: false - - - name: 'jira-xy' - project: XY - # Overrides default. - issue_type: Task - # JIRA components. Optional. - components: [ 'Operations' ] - # Standard or custom field values to set on created issue. Optional. - # - # See https://developer.atlassian.com/server/jira/platform/jira-rest-api-examples/#setting-custom-field-data-for-other-field-types for further examples. - fields: - # TextField - customfield_10001: "Random text" - # SelectList - customfield_10002: { "value": "red" } - # MultiSelect - customfield_10003: [{"value": "red" }, {"value": "blue" }, {"value": "green" }] - -# File containing template definitions. Required. -template: jiralert.tmpl diff --git a/pkg/config/config.go b/pkg/config/config.go index 8498864f..02dde652 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -74,31 +74,52 @@ func resolveFilepaths(baseDir string, cfg *Config, logger log.Logger) { cfg.Template = join(cfg.Template) } -// ReceiverConfig is the configuration for one receiver. It has a unique name and includes API access fields (URL, user -// and password) and issue fields (required -- e.g. project, issue type -- and optional -- e.g. priority). +// ReceiverConfig is the configuration for one receiver. type ReceiverConfig struct { + // Name represents unique name for a receiver. + // If Iiralert is used with Alertmanager, name it as Alertmanager receiver that sends alert via webhook to Jiralert for + // desired propagation. Name string `yaml:"name" json:"name"` - // API access fields + // APIURL specifies API URL for JIRA e.g https://.atlassian.net. + // Required. APIURL string `yaml:"api_url" json:"api_url"` + // User specifies user to pass in basicauth against JIRA. User string `yaml:"user" json:"user"` + // Password specifies password in baiscauth against JIRA. Password Secret `yaml:"password" json:"password"` - // Required issue fields + // Required issue fields. + + // Projects specifies in what project within org to create/reopen JIRA issues. Project string `yaml:"project" json:"project"` + // IssueType specifies what type of the issue to use for new JIRA issues. IssueType string `yaml:"issue_type" json:"issue_type"` + // Summary specifies Golang template invocation for generating the issue summary. Summary string `yaml:"summary" json:"summary"` + // ReopenState specifies the state to transition into when reopening a closed issue. + // This state has to exists for the chosen project. ReopenState string `yaml:"reopen_state" json:"reopen_state"` + // ReopenDuration specifies the time after being closed that an issue should be reopened, after which, + // a new issue is created instead. Set to large value if you always want to reopen. + ReopenDuration *Duration `yaml:"reopen_duration" json:"reopen_duration"` + + // Optional issue fields. - // Optional issue fields + // Priority represents issue priority. Priority string `yaml:"priority" json:"priority"` + // Description specifies Golang template invocation for generating the issue description. Description string `yaml:"description" json:"description"` + // WontFixResolution specifies to not reopen issues with this resolution. Useful for silencing alerts. WontFixResolution string `yaml:"wont_fix_resolution" json:"wont_fix_resolution"` + // Fields specifies standard or custom field values to set on created issue. + // + // See https://developer.atlassian.com/server/jira/platform/jira-rest-api-examples/#setting-custom-field-data-for-other-field-types for further examples. Fields map[string]interface{} `yaml:"fields" json:"fields"` + // Components specifies issue components. Sometimes required, depending on JIRA project. Components []string `yaml:"components" json:"components"` - ReopenDuration *Duration `yaml:"reopen_duration" json:"reopen_duration"` - - // Label copy settings + // AddGroupLabels specifies if all Prometheus labels should be copied into separate JIRA labels. + // Default: false. AddGroupLabels bool `yaml:"add_group_labels" json:"add_group_labels"` // Catches all undefined fields and must be empty after parsing. @@ -123,8 +144,13 @@ func (rc *ReceiverConfig) UnmarshalYAML(unmarshal func(interface{}) error) error // Config is the top-level configuration for JIRAlert's config file. type Config struct { + // Default specifies default values to be used in place of any ReceiverConfig' empty field. Defaults *ReceiverConfig `yaml:"defaults,omitempty" json:"defaults,omitempty"` + + // Receivers contains configuration per each receiver. Receivers []*ReceiverConfig `yaml:"receivers,omitempty" json:"receivers,omitempty"` + + // Template specifies an optional file with template definitions. Template string `yaml:"template" json:"template"` // Catches all undefined fields and must be empty after parsing. @@ -232,10 +258,6 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error { return fmt.Errorf("no receivers defined") } - if c.Template == "" { - return fmt.Errorf("missing template file") - } - return checkOverflow(c.XXX, "config") } diff --git a/pkg/template/template.go b/pkg/template/template.go index 02f11b29..dc322ec1 100644 --- a/pkg/template/template.go +++ b/pkg/template/template.go @@ -32,13 +32,19 @@ var funcs = template.FuncMap{ } // LoadTemplate reads and parses all templates defined in the given file and constructs a jiralert.Template. +// If file is empty, or no file is passed, empty file template is returned. func LoadTemplate(path string, logger log.Logger) (*Template, error) { - level.Debug(logger).Log("msg", "loading templates", "path", path) - tmpl, err := template.New("").Option("missingkey=zero").Funcs(funcs).ParseFiles(path) - if err != nil { - return nil, err + if len(path) > 0 { + level.Debug(logger).Log("msg", "loading templates", "path", path) + tmpl, err := template.New("").Option("missingkey=zero").Funcs(funcs).ParseFiles(path) + if err != nil { + return nil, err + } + return &Template{tmpl: tmpl}, nil } - return &Template{tmpl: tmpl}, nil + + level.Info(logger).Log("msg", "no template was passed.") + return &Template{tmpl: template.New("")}, nil } func (t *Template) Err() error {