Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
2214e9f
Add GH action to auto build OCI image
nebula-it Mar 24, 2025
14daec0
Add GH action to auto build OCI image
nebula-it Mar 24, 2025
022a871
Add GH action to auto build OCI image
nebula-it Mar 24, 2025
84e80e7
Test building in docker
nebula-it Mar 24, 2025
f6de71e
Test building in docker
nebula-it Mar 24, 2025
42629cf
Test building in docker
nebula-it Mar 24, 2025
144a037
Test building in docker
nebula-it Mar 24, 2025
8ae66cc
Change default port to 8000
nebula-it Mar 24, 2025
3171dd2
fix: Update entrypoint as it was not picking up envvars
nebula-it Mar 24, 2025
015c4b4
Add healthcheck endpoint
nebula-it Mar 24, 2025
358f99e
Update examples
nebula-it Mar 24, 2025
3e35c07
Add kubernetes example
nebula-it Mar 24, 2025
de3e700
feat: Add ConvertLabelsToTags option to notification configuration
nebula-it Mar 25, 2025
e6d29e9
test a potential fix for ntfy "actions" not working
nebula-it Mar 25, 2025
3640bda
test a potential fix for ntfy "actions" not working
nebula-it Mar 25, 2025
7f939e3
Merge branch 'master' of https://github.com/nebula-it/alertmanager-ntfy
nebula-it Mar 25, 2025
a853cb9
test a potential fix for ntfy "actions" not working
nebula-it Mar 25, 2025
bb6effc
feat: add ntfy action support
nebula-it Mar 25, 2025
fdb7b94
feat: add ntfy action support
nebula-it Mar 25, 2025
b5bf457
Implement actions similar to tags
nebula-it Mar 25, 2025
a6253ba
feat: update action code to still process an alert even if actions ar…
nebula-it Mar 25, 2025
9650a41
chore: add debug logging for actions
nebula-it Mar 25, 2025
488c0dd
chore: add example of new actions
nebula-it Mar 25, 2025
564e6bc
Update go version
nebula-it Oct 9, 2025
e3e62ed
Update go version
nebula-it Oct 9, 2025
dec26bb
Update go version
nebula-it Oct 9, 2025
b404889
Update go version
nebula-it Oct 9, 2025
dd5ae9d
Update go version
nebula-it Oct 9, 2025
06fabb3
Revert dockerfile changes
nebula-it Oct 9, 2025
182b50a
Update go version
nebula-it Oct 9, 2025
a050c48
Update go version
nebula-it Oct 9, 2025
43beb3f
Update go version
nebula-it Oct 9, 2025
0a4c89e
Update go version
nebula-it Oct 9, 2025
67b4ef5
Merge branch 'master' of https://github.com/alexbakker/alertmanager-n…
nebula-it Jan 27, 2026
664469a
Remove kubernetes manifest
nebula-it Jan 27, 2026
9a00f58
Bring readme and example config in sync
nebula-it Jan 27, 2026
01778d3
Bring readme and example config in sync
nebula-it Jan 27, 2026
1600ed1
Merge pull request #3 from nebula-it/alexbakker-master
nebula-it Jan 27, 2026
4b4bff2
Remove dockerfile
nebula-it Jan 27, 2026
194873c
Update build file
nebula-it Jan 27, 2026
e992590
Update build file
nebula-it Jan 27, 2026
224a10b
Remove custom healthcheck and use upstream one
nebula-it Jan 27, 2026
bb4597b
Handle empty keys and Cleanup logging
nebula-it Feb 26, 2026
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
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ ntfy:
notification:
# The topic can either be a hardcoded string or a gval expression that evaluates to a string
topic: alertmanager
# true to send all labels as tags, false to only send labels that are explicitly defined in the tags section
convertLabelsToTags: true
# Priority reference: https://docs.ntfy.sh/publish/#message-priority
# Can either be a hardcoded string or a gval expression that evaluates to a string
priority: |
Expand All @@ -71,6 +73,16 @@ ntfy:
{{ if eq .Status "resolved" }}Resolved: {{ end }}{{ index .Annotations "summary" }}
description: |
{{ index .Annotations "description" }}
# Add ntfy actions https://docs.ntfy.sh/publish/?h=action#open-websiteapp
actions:
- action: "view"
label: "Dashboard"
url: "{{ .GeneratorURL }}"
condition: 'GeneratorURL != ""'
- action: "view"
label: "Runbook"
url: '{{ index .Annotations "runbook_url" }}'
condition: 'status == "firing" && annotations["runbook_url"] != ""'
headers:
X-Click: |
{{ .GeneratorURL }}
Expand Down
5 changes: 3 additions & 2 deletions cmd/alertmanager-ntfy/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,9 @@ var (
BaseURL: "https://ntfy.sh",
Timeout: 10 * time.Second,
Notification: config.Notification{
Topic: config.StringExpression{Text: getDefaultTopic()},
Priority: &config.StringExpression{Text: "default"},
Topic: config.StringExpression{Text: getDefaultTopic()},
Priority: &config.StringExpression{Text: "default"},
ConvertLabelsToTags: true,
},
},
Log: getDefaultLogConfig(zapcore.InfoLevel),
Expand Down
12 changes: 12 additions & 0 deletions config.example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ ntfy:
notification:
# The topic can either be a hardcoded string or a gval expression that evaluates to a string
topic: alertmanager
# true to send all labels as tags, false to only send labels that are explicitly defined in the tags section
convertLabelsToTags: true
# Priority reference: https://docs.ntfy.sh/publish/#message-priority
# Can either be a hardcoded string or a gval expression that evaluates to a string
priority: |
Expand All @@ -30,6 +32,16 @@ ntfy:
{{ if eq .Status "resolved" }}Resolved: {{ end }}{{ index .Annotations "summary" }}
description: |
{{ index .Annotations "description" }}
# Add ntfy actions https://docs.ntfy.sh/publish/?h=action#open-websiteapp
actions:
- action: "view"
label: "Dashboard"
url: "{{ .GeneratorURL }}"
condition: 'GeneratorURL != ""'
- action: "view"
label: "Runbook"
url: '{{ index .Annotations "runbook_url" }}'
condition: 'status == "firing" && annotations["runbook_url"] != ""'
headers:
X-Click: |
{{ .GeneratorURL }}
Expand Down
12 changes: 12 additions & 0 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
services:
alertmanager-ntfy:
image: "ghcr.io/alexbakker/alertmanager-ntfy:latest"
volumes:
- ./config:/config
environment:
- USER_ID=1000
- GROUP_ID=1000
# Defaults to "/config/config.yml". Multiple files can be specified with a comma-separated list.
# - CONFIG_FILES="/config/config.yaml,/secret/secret.yaml"
ports:
- "8000:8000"
5 changes: 5 additions & 0 deletions internal/alertmanager/format.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ type Alert struct {
}

func (a *Alert) Map() map[string]interface{} {
// Ensure annotations is initialized to prevent nil map access
if a.Annotations == nil {
a.Annotations = make(map[string]string)
}

s := structs.New(a)
s.TagName = "json"
return s.Map()
Expand Down
17 changes: 13 additions & 4 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ type Expression struct {
type Templates struct {
Title *Template `yaml:"title"`
Description *Template `yaml:"description"`
Actions []Action `yaml:"actions,omitempty"`
Headers map[string]*Template `yaml:headers`
}

Expand All @@ -47,10 +48,11 @@ type StringExpression struct {
}

type Notification struct {
Topic StringExpression `yaml:"topic"`
Priority *StringExpression `yaml:"priority"`
Tags []*Tag `yaml:"tags"`
Templates *Templates `yaml:"templates"`
Topic StringExpression `yaml:"topic"`
Priority *StringExpression `yaml:"priority"`
Tags []*Tag `yaml:"tags"`
Templates *Templates `yaml:"templates"`
ConvertLabelsToTags bool `yaml:"convertLabelsToTags"`
}

type NtfyAuth struct {
Expand All @@ -77,6 +79,13 @@ type Config struct {
Log *zap.Config `yaml:"log"`
}

type Action struct {
Action string `yaml:"action"`
Label string `yaml:"label"`
URL string `yaml:"url"`
Condition *Expression `yaml:"condition,omitempty"`
}

// UnmarshalText implements the encoding.TextUnmarshaler interface.
func (t *Template) UnmarshalText(text []byte) error {
s := strings.TrimSpace(string(text))
Expand Down
88 changes: 87 additions & 1 deletion internal/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,20 @@ type Server struct {
http *http.Client
}

type ntfyAction struct {
Action string `json:"action"`
Label string `json:"label"`
URL string `json:"url"`
}

// HealthCheck handles health check requests
func HealthCheck(logger *zap.Logger) gin.HandlerFunc {
return func(c *gin.Context) {
logger.Debug("Health check request received")
c.String(http.StatusOK, "ok")
}
}

func New(logger *zap.Logger, cfg *config.Config) *Server {
gin.SetMode(gin.ReleaseMode)
e := gin.New()
Expand Down Expand Up @@ -190,14 +204,86 @@ func (s *Server) forwardAlert(logger *zap.Logger, alert *alertmanager.Alert) err

tags = append(tags, tag.Tag)
}
tags = append(tags, convertLabelsToTags(alert.Labels)...)
// Only convert labels to tags if enabled in config
if s.cfg.Ntfy.Notification.ConvertLabelsToTags {
tags = append(tags, convertLabelsToTags(alert.Labels)...)
}

if title != "" {
req.Header.Set("X-Title", title)
}
if len(tags) > 0 {
req.Header.Set("X-Tags", strings.Join(tags, tagSeparator))
}

// Add actions if configured
if len(s.cfg.Ntfy.Notification.Templates.Actions) > 0 {
var validActions []ntfyAction
alertMap := alert.Map()
for _, actionConfig := range s.cfg.Ntfy.Notification.Templates.Actions {
// Check condition if present
if actionConfig.Condition != nil {
match, err := actionConfig.Condition.Evaluable.EvalBool(context.Background(), alertMap)
// Log detailed action info
logger.Debug("Action condition evaluation details",
zap.String("action", actionConfig.Label),
zap.String("condition", actionConfig.Condition.Text),
zap.Any("match", match),
zap.Any("alert_map", alertMap),
zap.Error(err))
if err != nil {
logger.Warn("Action condition evaluation failed, skipping action",
zap.String("action", actionConfig.Label),
zap.String("condition", actionConfig.Condition.Text),
zap.Error(err))
continue // Skip this action but continue processing others
}
if !match {
logger.Debug("Action condition not met",
zap.String("action", actionConfig.Label),
zap.String("condition", actionConfig.Condition.Text))
continue
}
}

// Execute URL template
var urlBuf bytes.Buffer
urlTmpl, err := template.New("url").Option("missingkey=zero").Parse(actionConfig.URL)
if err != nil {
logger.Warn("Invalid URL template, skipping action",
zap.String("action", actionConfig.Label),
zap.Error(err))
continue // Skip this action but continue processing others
}

if err := urlTmpl.Execute(&urlBuf, alertMap); err != nil {
logger.Warn("Failed to render URL template, skipping action",
zap.String("action", actionConfig.Label),
zap.Error(err))
continue // Skip this action but continue processing others
}

// Add valid action to the list
validActions = append(validActions, ntfyAction{
Action: actionConfig.Action,
Label: actionConfig.Label,
URL: strings.TrimSpace(urlBuf.String()),
})
}

// Only set X-Actions header if we have valid actions
if len(validActions) > 0 {
if jsonActions, err := json.Marshal(validActions); err == nil {
req.Header.Set("X-Actions", string(jsonActions))
logger.Debug("Set X-Actions header",
zap.String("actions", string(jsonActions)))
} else {
logger.Warn("Failed to marshal actions, sending alert without actions",
zap.Error(err))
}
}
}

if s.cfg.Ntfy.Notification.Priority != nil {
priority, err := evalStringExpr(s.cfg.Ntfy.Notification.Priority, alert)
if err != nil {
Expand Down