Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

jiralert: Improved docs and config. #32

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* a unique name (matching the Alertmanager receiver name)
* 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),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* handful of required issue fields (such as the JIRA project and issue summary),
* 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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* some optional issue fields (e.g. priority) and a `fields` map for other (standard or custom) JIRA fields.
* optional issue fields (e.g. priority), including 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

Expand Down
23 changes: 2 additions & 21 deletions cmd/jiralert/content.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,6 @@ const (
<h2>Configuration</h2>
<pre>{{ .Config }}</pre>
{{- end }}

{{ define "content.error" -}}
<h2>Error</h2>
<pre>{{ .Err }}</pre>
{{- end }}
`
)

Expand All @@ -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 {
Expand All @@ -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,
})
}
Expand All @@ -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,
// })
//}
12 changes: 6 additions & 6 deletions cmd/jiralert/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}
Expand All @@ -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
Expand Down Expand Up @@ -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())

Expand Down
10 changes: 0 additions & 10 deletions examples/jiralert.tmpl

This file was deleted.

51 changes: 0 additions & 51 deletions examples/jiralert.yml

This file was deleted.

46 changes: 34 additions & 12 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd replace all this with

	// Name is a unique receiver name. It must match an Alertmanager receiver name. Required.

Name string `yaml:"name" json:"name"`

// API access fields
// APIURL specifies API URL for JIRA e.g https://<your org>.atlassian.net.
// Required.
APIURL string `yaml:"api_url" json:"api_url"`
// User specifies user to pass in basicauth against JIRA.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// User specifies user to pass in basicauth against JIRA.
// User is the JIRA user name, provided to JIRA via Basic HTTP Authentication. Required.

User string `yaml:"user" json:"user"`
// Password specifies password in baiscauth against JIRA.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Password specifies password in baiscauth against JIRA.
// Password is the user's JIRA password, provided to JIRA via Basic HTTP Authentication. Required.

Password Secret `yaml:"password" json:"password"`

// Required issue fields
// Required issue fields.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will this actually create a heading or something like that in the documentation? Or will it be ignored? If the latter, then I'd rather add required/optional notes to every field separately.


// Projects specifies in what project within org to create/reopen JIRA issues.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Projects specifies in what project within org to create/reopen JIRA issues.
// Project specifies the JIRA project key where issues are to be created.

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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Summary specifies Golang template invocation for generating the issue summary.
// Summary is a plain string; or Golang template invocation to generate 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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// This state has to exists for the chosen project.
// This state must exist for the chosen project and should ideally be reachable from all other states.

ReopenState string `yaml:"reopen_state" json:"reopen_state"`
// ReopenDuration specifies the time after being closed that an issue should be reopened, after which,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// ReopenDuration specifies the time after being closed that an issue should be reopened, after which,
// ReopenDuration specifies for how long after closing should an issue be reopened. After this,

// a new issue is created instead. Set to large value if you always want to reopen.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// a new issue is created instead. Set to large value if you always want to reopen.
// a new issue is created instead. Set to large value if you always want to reopen; and "0s" to always create a new issue.

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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Description specifies Golang template invocation for generating the issue description.
// Description is a plain string; or Golang template invocation to generate 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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Fields specifies standard or custom field values to set on created issue.
// Fields specifies standard or custom field values to set on created issue. E.g.
// * TextField: "Random text"
// * SelectList: { "value": "red" }
// * MultiSelect: [{"value": "red" }, {"value": "blue" }, {"value": "green" }]

//
// 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.
Expand All @@ -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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Default specifies default values to be used in place of any ReceiverConfig' empty field.
// 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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have an issue with defaulting to no template file: with no templating, JIRAlert will always create identical JIRA tickets (except the label, but that's not even intended for human consumption). Why would anyone want that at all? Much less as default behavior?

(I know about the deployment issue someone brought up, but I have a hard time believing there exists a widely used deployment tool that trips up on Golang templates and provides no workaround for that. What if the binary contains Golang templates as strings?)

Template string `yaml:"template" json:"template"`

// Catches all undefined fields and must be empty after parsing.
Expand Down Expand Up @@ -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")
}

Expand Down
16 changes: 11 additions & 5 deletions pkg/template/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
level.Info(logger).Log("msg", "no template was passed.")
level.Info(logger).Log("msg", "no template file provided")

return &Template{tmpl: template.New("")}, nil
}

func (t *Template) Err() error {
Expand Down