Repository: prometheus/alertmanager
Pull request #4362 (2025-04-22 20:29:45)

Pushover: Add monospace message formatting, add documentation for existing HTML styling
Hey all,

this is my first contibution to alertmanager. Please let me know if I missed something.

There are two changes in this PR, but hey seemed very related:

- Document the (already existing) feature of enabling HTML message formatting when using Pushover
- Add the possibility to use monospace formatting for messages in Pushover (+documentation, +test)

API documentation can be found [here](https://pushover.net/api#html).


Patch:
diff --git a/config/config.go b/config/config.go
index 82885b23..6f54c523 100644
--- a/config/config.go
+++ b/config/config.go
@@ -924,7 +924,7 @@ func (r *Route) UnmarshalYAML(unmarshal func(interface{}) error) error {
 	}
 
 	if len(r.GroupBy) > 0 && r.GroupByAll {
-		return errors.New("cannot have wildcard group_by (`...`) and other labels at the same time")
+		return errors.New("cannot have wildcard group_by (`...`) and other other labels at the same time")
 	}
 
 	groupBy := map[model.LabelName]struct{}{}
diff --git a/config/notifiers.go b/config/notifiers.go
index 87f806aa..71e404e3 100644
--- a/config/notifiers.go
+++ b/config/notifiers.go
@@ -741,7 +741,8 @@ type PushoverConfig struct {
 	Retry       duration `yaml:"retry,omitempty" json:"retry,omitempty"`
 	Expire      duration `yaml:"expire,omitempty" json:"expire,omitempty"`
 	TTL         duration `yaml:"ttl,omitempty" json:"ttl,omitempty"`
-	HTML        bool     `yaml:"html" json:"html,omitempty"`
+	HTML        bool     `yaml:"html,omitempty" json:"html,omitempty"`
+	Monospace   bool     `yaml:"monospace,omitempty" json:"monospace,omitempty"`
 }
 
 // UnmarshalYAML implements the yaml.Unmarshaler interface.
@@ -763,6 +764,9 @@ func (c *PushoverConfig) UnmarshalYAML(unmarshal func(interface{}) error) error
 	if c.Token != "" && c.TokenFile != "" {
 		return errors.New("at most one of token & token_file must be configured")
 	}
+	if c.HTML && c.Monospace {
+		return errors.New("at most one of monospace & html must be configured")
+	}
 	return nil
 }
 
diff --git a/docs/configuration.md b/docs/configuration.md
index 731437f7..5e26c87c 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -1330,6 +1330,11 @@ token_file: <filepath>
 # Optional time to live (TTL) to use for notification, see https://pushover.net/api#ttl
 [ ttl: <duration> ]
 
+# Optional HTML/monospace formatting for the message, see https://pushover.net/api#html
+# html and monospace formatting are mutually exclusive.
+[ html: <boolean> | default = false ]
+[ monospace: <boolean> | default = false ]
+
 # The HTTP client's configuration.
 [ http_config: <http_config> | default = global.http_config ]
 ```
diff --git a/notify/pushover/pushover.go b/notify/pushover/pushover.go
index 5445b977..1032de99 100644
--- a/notify/pushover/pushover.go
+++ b/notify/pushover/pushover.go
@@ -124,6 +124,10 @@ func (n *Notifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error)
 		message = tmpl(n.conf.Message)
 	}
 
+	if n.conf.Monospace {
+		parameters.Add("monospace", "1")
+	}
+
 	message, truncated = notify.TruncateInRunes(message, maxMessageLenRunes)
 	if truncated {
 		n.logger.Warn("Truncated message", "incident", key, "max_runes", maxMessageLenRunes)


Test patch:
diff --git a/config/config_test.go b/config/config_test.go
index a609b9f5..a9b77f17 100644
--- a/config/config_test.go
+++ b/config/config_test.go
@@ -300,7 +300,7 @@ receivers:
 `
 	_, err := Load(in)
 
-	expected := "cannot have wildcard group_by (`...`) and other labels at the same time"
+	expected := "cannot have wildcard group_by (`...`) and other other labels at the same time"
 
 	if err == nil {
 		t.Fatalf("no error returned, expected:\n%q", expected)
diff --git a/config/notifiers_test.go b/config/notifiers_test.go
index af348b9f..74a452bf 100644
--- a/config/notifiers_test.go
+++ b/config/notifiers_test.go
@@ -516,6 +516,26 @@ user_key: 'user key'
 	}
 }
 
+func TestPushoverHTMLOrMonospace(t *testing.T) {
+	in := `
+token: 'pushover token'
+user_key: 'user key'
+html: true
+monospace: true
+`
+	var cfg PushoverConfig
+	err := yaml.UnmarshalStrict([]byte(in), &cfg)
+
+	expected := "at most one of monospace & html must be configured"
+
+	if err == nil {
+		t.Fatalf("no error returned, expected:\n%v", expected)
+	}
+	if err.Error() != expected {
+		t.Errorf("\nexpected:\n%v\ngot:\n%v", expected, err.Error())
+	}
+}
+
 func TestLoadSlackConfiguration(t *testing.T) {
 	tests := []struct {
 		in       string
diff --git a/notify/pushover/pushover_test.go b/notify/pushover/pushover_test.go
index 30565d07..1381a793 100644
--- a/notify/pushover/pushover_test.go
+++ b/notify/pushover/pushover_test.go
@@ -14,6 +14,7 @@
 package pushover
 
 import (
+	"net/http"
 	"os"
 	"testing"
 
@@ -22,7 +23,9 @@ import (
 	"github.com/stretchr/testify/require"
 
 	"github.com/prometheus/alertmanager/config"
+	"github.com/prometheus/alertmanager/notify"
 	"github.com/prometheus/alertmanager/notify/test"
+	"github.com/prometheus/alertmanager/types"
 )
 
 func TestPushoverRetry(t *testing.T) {
@@ -109,3 +112,27 @@ func TestPushoverReadingTokenFromFile(t *testing.T) {
 
 	test.AssertNotifyLeaksNoSecret(ctx, t, notifier, token)
 }
+
+func TestPushoverMonospaceParameter(t *testing.T) {
+	ctx, apiURL, fn := test.GetContextWithCancelingURL(func(w http.ResponseWriter, r *http.Request) {
+		require.NoError(t, r.ParseForm())
+		require.Equal(t, "1", r.FormValue("monospace"), `expected monospace parameter to be set to "1"`)
+	})
+	defer fn()
+
+	notifier, err := New(
+		&config.PushoverConfig{
+			UserKey:    config.Secret("user_key"),
+			Token:      config.Secret("token"),
+			Monospace:  true,
+			HTTPConfig: &commoncfg.HTTPClientConfig{},
+		},
+		test.CreateTmpl(t),
+		promslog.NewNopLogger(),
+	)
+	notifier.apiURL = apiURL.String()
+	require.NoError(t, err)
+
+	_, err = notifier.Notify(notify.WithGroupKey(ctx, "1"), &types.Alert{})
+	require.NoError(t, err)
+}