Compare commits
No commits in common. "3bbde04774cdea84a9b9acfdb3224496e11df2ac" and "e1ae4d2e438d2d1ba8408f2df37af10e1cfcb389" have entirely different histories.
3bbde04774
...
e1ae4d2e43
19 changed files with 429 additions and 829 deletions
20
.build.yml
20
.build.yml
|
@ -1,28 +1,28 @@
|
||||||
image: archlinux
|
image: archlinux
|
||||||
packages:
|
packages:
|
||||||
- docker
|
- docker
|
||||||
- docker-buildx
|
|
||||||
- go
|
- go
|
||||||
- just
|
|
||||||
- revive
|
- revive
|
||||||
- staticcheck
|
- staticcheck
|
||||||
secrets:
|
secrets:
|
||||||
- 8da1d834-8c97-4da0-a7ae-dd755b702691
|
- 2b44595c-3464-4c2d-a8df-c5dea8e90c4c
|
||||||
sources:
|
sources:
|
||||||
- https://git.xenrox.net/~xenrox/ntfy-alertmanager
|
- https://git.xenrox.net/~xenrox/ntfy-alertmanager
|
||||||
tasks:
|
tasks:
|
||||||
- test: |
|
- test: |
|
||||||
cd ntfy-alertmanager
|
cd ntfy-alertmanager
|
||||||
just test
|
go test -v ./...
|
||||||
- lint: |
|
- lint: |
|
||||||
cd ntfy-alertmanager
|
cd ntfy-alertmanager
|
||||||
just lint
|
go vet ./...
|
||||||
|
staticcheck ./...
|
||||||
|
revive ./...
|
||||||
- build: |
|
- build: |
|
||||||
cd ntfy-alertmanager
|
cd ntfy-alertmanager
|
||||||
just build
|
go build
|
||||||
- gofmt: |
|
- gofmt: |
|
||||||
cd ntfy-alertmanager
|
cd ntfy-alertmanager
|
||||||
just gofmt
|
test -z $(gofmt -l .)
|
||||||
- dev-image: |
|
- dev-image: |
|
||||||
cd ntfy-alertmanager/docker
|
cd ntfy-alertmanager/docker
|
||||||
if [ "$BUILD_SUBMITTER" != "git.sr.ht" ] || [ "$(git rev-parse master)" != "$(git rev-parse HEAD)" ]
|
if [ "$BUILD_SUBMITTER" != "git.sr.ht" ] || [ "$(git rev-parse master)" != "$(git rev-parse HEAD)" ]
|
||||||
|
@ -30,10 +30,6 @@ tasks:
|
||||||
complete-build
|
complete-build
|
||||||
fi
|
fi
|
||||||
sudo systemctl start docker
|
sudo systemctl start docker
|
||||||
~/.local/bin/docker_login
|
~/.local/bin/dockerhub_login
|
||||||
docker build -f Dockerfile-dev -t xenrox/ntfy-alertmanager:dev ./..
|
docker build -f Dockerfile-dev -t xenrox/ntfy-alertmanager:dev ./..
|
||||||
docker push xenrox/ntfy-alertmanager:dev
|
docker push xenrox/ntfy-alertmanager:dev
|
||||||
docker tag xenrox/ntfy-alertmanager:dev code.xenrox.net/xenrox/ntfy-alertmanager:dev
|
|
||||||
docker push code.xenrox.net/xenrox/ntfy-alertmanager:dev
|
|
||||||
docker tag xenrox/ntfy-alertmanager:dev codeberg.org/xenrox/ntfy-alertmanager:dev
|
|
||||||
docker push codeberg.org/xenrox/ntfy-alertmanager:dev
|
|
||||||
|
|
49
.justfile
49
.justfile
|
@ -1,49 +0,0 @@
|
||||||
version := "0.4.0"
|
|
||||||
|
|
||||||
default:
|
|
||||||
@just --choose
|
|
||||||
|
|
||||||
test:
|
|
||||||
go test -v ./...
|
|
||||||
|
|
||||||
lint:
|
|
||||||
go vet ./...
|
|
||||||
staticcheck ./...
|
|
||||||
revive ./...
|
|
||||||
|
|
||||||
gofmt:
|
|
||||||
gofmt -l .
|
|
||||||
@test -z $(gofmt -l .)
|
|
||||||
|
|
||||||
build:
|
|
||||||
go build
|
|
||||||
|
|
||||||
release-docker:
|
|
||||||
docker build -t xenrox/ntfy-alertmanager:latest docker
|
|
||||||
docker tag xenrox/ntfy-alertmanager:latest xenrox/ntfy-alertmanager:{{version}}
|
|
||||||
docker push xenrox/ntfy-alertmanager:latest
|
|
||||||
docker push xenrox/ntfy-alertmanager:{{version}}
|
|
||||||
|
|
||||||
docker tag xenrox/ntfy-alertmanager:latest code.xenrox.net/xenrox/ntfy-alertmanager:latest
|
|
||||||
docker tag xenrox/ntfy-alertmanager:latest code.xenrox.net/xenrox/ntfy-alertmanager:{{version}}
|
|
||||||
docker push code.xenrox.net/xenrox/ntfy-alertmanager:latest
|
|
||||||
docker push code.xenrox.net/xenrox/ntfy-alertmanager:{{version}}
|
|
||||||
|
|
||||||
docker tag xenrox/ntfy-alertmanager:latest codeberg.org/xenrox/ntfy-alertmanager:latest
|
|
||||||
docker tag xenrox/ntfy-alertmanager:latest codeberg.org/xenrox/ntfy-alertmanager:{{version}}
|
|
||||||
docker push codeberg.org/xenrox/ntfy-alertmanager:latest
|
|
||||||
docker push codeberg.org/xenrox/ntfy-alertmanager:{{version}}
|
|
||||||
|
|
||||||
upgrade-deps:
|
|
||||||
go get -u ./...
|
|
||||||
go mod tidy
|
|
||||||
|
|
||||||
@run:
|
|
||||||
go run . --config ./devconfig.scfg
|
|
||||||
|
|
||||||
@curl:
|
|
||||||
curl --user "user:pass" -X 'POST' \
|
|
||||||
'127.0.0.1:8080' \
|
|
||||||
-H 'Content-Type: application/json' \
|
|
||||||
-d @contrib/test_payload.json
|
|
||||||
|
|
29
README.md
29
README.md
|
@ -6,7 +6,7 @@ A bridge between ntfy and Alertmanager.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
Simply use go build or the [docker image] ([Codeberg image mirror]) with [docker compose file].
|
Simply use go build or the [docker image] with [docker-compose file].
|
||||||
`ntfy-alertmanager:latest` is built from the latest tagged release while
|
`ntfy-alertmanager:latest` is built from the latest tagged release while
|
||||||
`ntfy-alertmanager:dev` is built from the master branch.
|
`ntfy-alertmanager:dev` is built from the master branch.
|
||||||
On Arch Linux you can install the [aur package] as well.
|
On Arch Linux you can install the [aur package] as well.
|
||||||
|
@ -19,30 +19,29 @@ of this file is [scfg] and there is an [example configuration file] in this repo
|
||||||
Furthermore you can take a look at [my deployment].
|
Furthermore you can take a look at [my deployment].
|
||||||
|
|
||||||
ntfy-alertmanager has support for setting ntfy [priority], [tags], [icon], [action buttons]
|
ntfy-alertmanager has support for setting ntfy [priority], [tags], [icon], [action buttons]
|
||||||
(which can be used to e.g. create an Alertmanager silence or open the alert's Prometheus URL), [email notifications] and [phone calls].
|
(which can be used to create an Alertmanager silence), [email notifications] and [phone calls].
|
||||||
Define a decreasing order of labels in the config file and map those labels to tags, priority, an icon, an email address or an alternative ntfy topic.
|
Define a decreasing order of labels in the config file and map those labels to tags, priority, an icon or an email address.
|
||||||
|
|
||||||
- For priority and icon the first found value will be chosen. Settings for "resolved" alerts will take precedence.
|
- For priority and icon the first found value will be chosen. Settings for "resolved" alerts will take precedence.
|
||||||
- Tags are added together.
|
- Tags are added together.
|
||||||
|
|
||||||
### Alertmanager config
|
### Alertmanager config
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
receivers:
|
receivers:
|
||||||
- name: "ntfy"
|
- name: "ntfy"
|
||||||
webhook_configs:
|
webhook_configs:
|
||||||
- url: "http://127.0.0.1:8080"
|
- url: "http://127.0.0.1:8080"
|
||||||
http_config:
|
http_config:
|
||||||
basic_auth:
|
basic_auth:
|
||||||
username: "webhookUser"
|
username: "webhookUser"
|
||||||
password: "webhookPass"
|
password: "webhookPass"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
Report bugs on the [issue tracker], send patches/ask questions on the [mailing list]
|
Report bugs on the [issue tracker], send patches/ask questions on the [mailing list]
|
||||||
or write to me directly on matrix [@xenrox:xenrox.net].
|
or write to me directly on matrix [@xenrox:xenrox.net].
|
||||||
There is a [mirror on Codeberg] as well, where you can create issues or open pull requests.
|
|
||||||
|
|
||||||
[ntfy-alertmanager]: https://hub.xenrox.net/~xenrox/ntfy-alertmanager/
|
[ntfy-alertmanager]: https://hub.xenrox.net/~xenrox/ntfy-alertmanager/
|
||||||
[scfg]: https://git.sr.ht/~emersion/scfg
|
[scfg]: https://git.sr.ht/~emersion/scfg
|
||||||
|
@ -56,9 +55,7 @@ There is a [mirror on Codeberg] as well, where you can create issues or open pul
|
||||||
[mailing list]: https://lists.xenrox.net/~xenrox/public-inbox
|
[mailing list]: https://lists.xenrox.net/~xenrox/public-inbox
|
||||||
[@xenrox:xenrox.net]: https://matrix.to/#/@xenrox:xenrox.net
|
[@xenrox:xenrox.net]: https://matrix.to/#/@xenrox:xenrox.net
|
||||||
[docker image]: https://hub.docker.com/r/xenrox/ntfy-alertmanager
|
[docker image]: https://hub.docker.com/r/xenrox/ntfy-alertmanager
|
||||||
[Codeberg image mirror]: https://codeberg.org/xenrox/-/packages/container/ntfy-alertmanager
|
[docker-compose file]: https://git.xenrox.net/~xenrox/ntfy-alertmanager/tree/master/item/docker/docker-compose.yml
|
||||||
[docker compose file]: https://git.xenrox.net/~xenrox/ntfy-alertmanager/tree/master/item/docker/compose.yaml
|
|
||||||
[example configuration file]: https://git.xenrox.net/~xenrox/ntfy-alertmanager/tree/master/item/config.scfg
|
[example configuration file]: https://git.xenrox.net/~xenrox/ntfy-alertmanager/tree/master/item/config.scfg
|
||||||
[my deployment]: https://git.xenrox.net/~xenrox/ansible/tree/master/item/roles/alertmanager/templates/ntfy-alertmanager.j2
|
[my deployment]: https://git.xenrox.net/~xenrox/ansible/tree/master/item/roles/alertmanager/templates/ntfy-alertmanager.j2
|
||||||
[aur package]: https://aur.archlinux.org/packages/ntfy-alertmanager
|
[aur package]: https://aur.archlinux.org/packages/ntfy-alertmanager
|
||||||
[mirror on Codeberg]: https://codeberg.org/xenrox/ntfy-alertmanager
|
|
||||||
|
|
9
cache/cache.go
vendored
9
cache/cache.go
vendored
|
@ -2,7 +2,6 @@
|
||||||
package cache
|
package cache
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
@ -11,18 +10,18 @@ import (
|
||||||
|
|
||||||
// Cache is the interface that describes a cache for ntfy-alertmanager.
|
// Cache is the interface that describes a cache for ntfy-alertmanager.
|
||||||
type Cache interface {
|
type Cache interface {
|
||||||
Set(ctx context.Context, fingerprint string, status string) error
|
Set(fingerprint string, status string) error
|
||||||
Contains(ctx context.Context, fingerprint string, status string) (bool, error)
|
Contains(fingerprint string, status string) (bool, error)
|
||||||
Cleanup()
|
Cleanup()
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewCache reads the cache configuration cfg and creates the cache.
|
// NewCache reads the cache configuration cfg and creates the cache.
|
||||||
func NewCache(ctx context.Context, cfg config.CacheConfig) (Cache, error) {
|
func NewCache(cfg config.CacheConfig) (Cache, error) {
|
||||||
switch strings.ToLower(cfg.Type) {
|
switch strings.ToLower(cfg.Type) {
|
||||||
case "memory":
|
case "memory":
|
||||||
return NewMemoryCache(cfg.Duration), nil
|
return NewMemoryCache(cfg.Duration), nil
|
||||||
case "redis":
|
case "redis":
|
||||||
return NewRedisCache(ctx, cfg.RedisURL, cfg.Duration)
|
return NewRedisCache(cfg.RedisURL, cfg.Duration)
|
||||||
case "disabled":
|
case "disabled":
|
||||||
return NewDisabledCache()
|
return NewDisabledCache()
|
||||||
default:
|
default:
|
||||||
|
|
6
cache/disabled.go
vendored
6
cache/disabled.go
vendored
|
@ -1,7 +1,5 @@
|
||||||
package cache
|
package cache
|
||||||
|
|
||||||
import "context"
|
|
||||||
|
|
||||||
// DisabledCache is the disabled cache.
|
// DisabledCache is the disabled cache.
|
||||||
type DisabledCache struct{}
|
type DisabledCache struct{}
|
||||||
|
|
||||||
|
@ -12,12 +10,12 @@ func NewDisabledCache() (Cache, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set is an empty function to implement the interface.
|
// Set is an empty function to implement the interface.
|
||||||
func (c *DisabledCache) Set(_ context.Context, _ string, _ string) error {
|
func (c *DisabledCache) Set(_ string, _ string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Contains is an empty function to implement the interface.
|
// Contains is an empty function to implement the interface.
|
||||||
func (c *DisabledCache) Contains(_ context.Context, _ string, _ string) (bool, error) {
|
func (c *DisabledCache) Contains(_ string, _ string) (bool, error) {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
5
cache/memory.go
vendored
5
cache/memory.go
vendored
|
@ -1,7 +1,6 @@
|
||||||
package cache
|
package cache
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
@ -28,7 +27,7 @@ func NewMemoryCache(d time.Duration) Cache {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set saves an alert in the cache.
|
// Set saves an alert in the cache.
|
||||||
func (c *MemoryCache) Set(_ context.Context, fingerprint string, status string) error {
|
func (c *MemoryCache) Set(fingerprint string, status string) error {
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
defer c.mu.Unlock()
|
defer c.mu.Unlock()
|
||||||
alert := new(cachedAlert)
|
alert := new(cachedAlert)
|
||||||
|
@ -41,7 +40,7 @@ func (c *MemoryCache) Set(_ context.Context, fingerprint string, status string)
|
||||||
|
|
||||||
// Contains checks if an alert with a given fingerprint is in the cache
|
// Contains checks if an alert with a given fingerprint is in the cache
|
||||||
// and if the status matches.
|
// and if the status matches.
|
||||||
func (c *MemoryCache) Contains(_ context.Context, fingerprint string, status string) (bool, error) {
|
func (c *MemoryCache) Contains(fingerprint string, status string) (bool, error) {
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
defer c.mu.Unlock()
|
defer c.mu.Unlock()
|
||||||
alert, ok := c.alerts[fingerprint]
|
alert, ok := c.alerts[fingerprint]
|
||||||
|
|
12
cache/redis.go
vendored
12
cache/redis.go
vendored
|
@ -16,7 +16,7 @@ type RedisCache struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewRedisCache creates a new redis cache/client.
|
// NewRedisCache creates a new redis cache/client.
|
||||||
func NewRedisCache(ctx context.Context, redisURL string, d time.Duration) (Cache, error) {
|
func NewRedisCache(redisURL string, d time.Duration) (Cache, error) {
|
||||||
c := new(RedisCache)
|
c := new(RedisCache)
|
||||||
ropts, err := redis.ParseURL(redisURL)
|
ropts, err := redis.ParseURL(redisURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -24,7 +24,7 @@ func NewRedisCache(ctx context.Context, redisURL string, d time.Duration) (Cache
|
||||||
}
|
}
|
||||||
|
|
||||||
rdb := redis.NewClient(ropts)
|
rdb := redis.NewClient(ropts)
|
||||||
ctx, cancel := context.WithTimeout(ctx, redisTimeout)
|
ctx, cancel := context.WithTimeout(context.TODO(), redisTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
err = rdb.Ping(ctx).Err()
|
err = rdb.Ping(ctx).Err()
|
||||||
|
@ -38,8 +38,8 @@ func NewRedisCache(ctx context.Context, redisURL string, d time.Duration) (Cache
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set saves an alert in the cache.
|
// Set saves an alert in the cache.
|
||||||
func (c *RedisCache) Set(ctx context.Context, fingerprint string, status string) error {
|
func (c *RedisCache) Set(fingerprint string, status string) error {
|
||||||
ctx, cancel := context.WithTimeout(ctx, redisTimeout)
|
ctx, cancel := context.WithTimeout(context.TODO(), redisTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
return c.client.SetEx(ctx, fingerprint, status, c.duration).Err()
|
return c.client.SetEx(ctx, fingerprint, status, c.duration).Err()
|
||||||
|
@ -47,8 +47,8 @@ func (c *RedisCache) Set(ctx context.Context, fingerprint string, status string)
|
||||||
|
|
||||||
// Contains checks if an alert with a given fingerprint is in the cache
|
// Contains checks if an alert with a given fingerprint is in the cache
|
||||||
// and if the status matches.
|
// and if the status matches.
|
||||||
func (c *RedisCache) Contains(ctx context.Context, fingerprint string, status string) (bool, error) {
|
func (c *RedisCache) Contains(fingerprint string, status string) (bool, error) {
|
||||||
ctx, cancel := context.WithTimeout(ctx, redisTimeout)
|
ctx, cancel := context.WithTimeout(context.TODO(), redisTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
val, err := c.client.Get(ctx, fingerprint).Result()
|
val, err := c.client.Get(ctx, fingerprint).Result()
|
||||||
|
|
72
config.scfg
72
config.scfg
|
@ -1,36 +1,17 @@
|
||||||
# Absolute path to another scfg configuration file which will be included.
|
# Public facing base URL of the service (e.g. https://ntfy-alertmanager.xenrox.net)
|
||||||
# This directive can be specified multiple times in the main configuration,
|
|
||||||
# but only the last occurrence of a setting will be used. Settings from
|
|
||||||
# the main configuration will take precedence.
|
|
||||||
# Default: unset
|
|
||||||
include /etc/ntfy-alertmanager/ntfy.scfg
|
|
||||||
# Public facing base URL of the service (e.g. https://ntfy-alertmanager.example.com)
|
|
||||||
# This setting is required for the "Silence" feature.
|
# This setting is required for the "Silence" feature.
|
||||||
# Default: ""
|
base-url https://ntfy-alertmanager.xenrox.net
|
||||||
base-url https://ntfy-alertmanager.example.com
|
|
||||||
# http listen address
|
# http listen address
|
||||||
# Default: 127.0.0.1:8080
|
|
||||||
http-address :8080
|
http-address :8080
|
||||||
# Log level
|
# Log level (either debug, info, warning, error)
|
||||||
# Options: debug, info, warning, error
|
|
||||||
# Default: info
|
|
||||||
log-level info
|
log-level info
|
||||||
# Log format
|
# Log format (either text or json)
|
||||||
# Options: text, json
|
|
||||||
# Default: text
|
|
||||||
log-format text
|
log-format text
|
||||||
# Write logs to this file. If unset, the logs will be written to stderr.
|
|
||||||
# Default: ""
|
|
||||||
log-file /var/log/ntfy-alertmanager.log
|
|
||||||
# When multiple alerts are grouped together by Alertmanager, they can either be sent
|
# When multiple alerts are grouped together by Alertmanager, they can either be sent
|
||||||
# each on their own (single mode) or be kept together (multi mode)
|
# each on their own (single mode) or be kept together (multi mode) (either single or multi; default is multi)
|
||||||
# Options: single, multi
|
|
||||||
# Default: multi
|
|
||||||
alert-mode single
|
alert-mode single
|
||||||
# Optionally protect with HTTP basic authentication
|
# Optionally protect with HTTP basic authentication
|
||||||
# Default: ""
|
|
||||||
user webhookUser
|
user webhookUser
|
||||||
# Default: ""
|
|
||||||
password webhookPass
|
password webhookPass
|
||||||
|
|
||||||
labels {
|
labels {
|
||||||
|
@ -39,9 +20,9 @@ labels {
|
||||||
severity "critical" {
|
severity "critical" {
|
||||||
priority 5
|
priority 5
|
||||||
tags "rotating_light"
|
tags "rotating_light"
|
||||||
icon "https://example.com/critical.png"
|
icon "https://foo.com/critical.png"
|
||||||
# Forward messages which severity "critical" to the specified email address.
|
# Forward messages which severity "critical" to the specified email address.
|
||||||
email-address foo@example.com
|
email-address foo@bar.com
|
||||||
# Call the specified number. Use `yes` to pick the first of your verified numbers.
|
# Call the specified number. Use `yes` to pick the first of your verified numbers.
|
||||||
call yes
|
call yes
|
||||||
}
|
}
|
||||||
|
@ -51,7 +32,6 @@ labels {
|
||||||
}
|
}
|
||||||
|
|
||||||
instance "example.com" {
|
instance "example.com" {
|
||||||
topic homeserver
|
|
||||||
tags "computer,example"
|
tags "computer,example"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -59,47 +39,29 @@ labels {
|
||||||
# Settings for resolved alerts
|
# Settings for resolved alerts
|
||||||
resolved {
|
resolved {
|
||||||
tags "resolved,partying_face"
|
tags "resolved,partying_face"
|
||||||
icon "https://example.com/resolved.png"
|
icon "https://foo.com/resolved.png"
|
||||||
priority 1
|
priority 1
|
||||||
}
|
}
|
||||||
|
|
||||||
ntfy {
|
ntfy {
|
||||||
# URL of the ntfy server.
|
# URL of the ntfy topic - required
|
||||||
# Default: ""
|
topic https://ntfy.sh/alertmanager-alerts
|
||||||
server https://ntfy.sh
|
|
||||||
# Name of the ntfy topic. For backwards compatibility you can specify the full URL of the
|
|
||||||
# topic (e.g. https://ntfy.sh/alertmanager-alerts) and the server will be parsed from it.
|
|
||||||
# Furthermore the topic name can be optionally set by using URL parameters with the webhook
|
|
||||||
# endpoint: https://ntfy-alertmanager.example.com/?topic=foobar
|
|
||||||
# This setting is required.
|
|
||||||
# Default: ""
|
|
||||||
topic alertmanager-alerts
|
|
||||||
# ntfy authentication via Basic Auth (https://docs.ntfy.sh/publish/#username-password)
|
# ntfy authentication via Basic Auth (https://docs.ntfy.sh/publish/#username-password)
|
||||||
# Default: ""
|
|
||||||
user user
|
user user
|
||||||
# Default: ""
|
|
||||||
password pass
|
password pass
|
||||||
# ntfy authentication via access tokens (https://docs.ntfy.sh/publish/#access-tokens)
|
# ntfy authentication via access tokens (https://docs.ntfy.sh/publish/#access-tokens)
|
||||||
# Either access-token or a user/password combination can be used - not both.
|
# Either access-token or a user/password combination can be used - not both.
|
||||||
# Default: ""
|
|
||||||
access-token foobar
|
access-token foobar
|
||||||
# When using (self signed) certificates that cannot be verified, you can instead specify
|
# When using (self signed) certificates that cannot be verified, you can instead specify
|
||||||
# the SHA512 fingerprint.
|
# the SHA512 fingerprint.
|
||||||
# openssl can be used to obtain it:
|
# openssl can be used to obtain it:
|
||||||
# openssl s_client -connect HOST:PORT | openssl x509 -fingerprint -sha512 -noout
|
# openssl s_client -connect HOST:PORT | openssl x509 -fingerprint -sha512 -noout
|
||||||
# For convenience ntfy-alertmanager will convert the certificate to lower case and remove all colons.
|
# For convenience ntfy-alertmanager will convert the certificate to lower case and remove all colons.
|
||||||
# Default: ""
|
|
||||||
certificate-fingerprint 13:6D:2B:88:9C:57:36:D0:81:B4:B2:9C:79:09:27:62:92:CF:B8:6A:6B:D3:AD:46:35:CB:70:17:EB:99:6E:28:08:2A:B8:C6:79:4B:F6:2E:81:79:41:98:1D:53:C8:07:B3:5C:24:5F:B1:8E:B6:FB:66:B5:DD:B4:D0:5C:29:91
|
certificate-fingerprint 13:6D:2B:88:9C:57:36:D0:81:B4:B2:9C:79:09:27:62:92:CF:B8:6A:6B:D3:AD:46:35:CB:70:17:EB:99:6E:28:08:2A:B8:C6:79:4B:F6:2E:81:79:41:98:1D:53:C8:07:B3:5C:24:5F:B1:8E:B6:FB:66:B5:DD:B4:D0:5C:29:91
|
||||||
# Forward all messages to the specified email address.
|
# Forward all messages to the specified email address.
|
||||||
# Default: ""
|
email-address foo@bar.com
|
||||||
email-address foo@example.com
|
|
||||||
# Call the specified number for all alerts. Use `yes` to pick the first of your verified numbers.
|
# Call the specified number for all alerts. Use `yes` to pick the first of your verified numbers.
|
||||||
# Default: ""
|
|
||||||
call +123456789
|
call +123456789
|
||||||
# Add a button that will open the alert's generator/Prometheus URL with the following label.
|
|
||||||
# This only works for the "single" alert-mode.
|
|
||||||
# Default: ""
|
|
||||||
generator-url-label source
|
|
||||||
}
|
}
|
||||||
|
|
||||||
alertmanager {
|
alertmanager {
|
||||||
|
@ -111,36 +73,28 @@ alertmanager {
|
||||||
# When alert-mode is set to "single" all alert labels will be used to create the silence.
|
# When alert-mode is set to "single" all alert labels will be used to create the silence.
|
||||||
# When it is "multi" common labels between all the alerts will be used. WARNING: This
|
# When it is "multi" common labels between all the alerts will be used. WARNING: This
|
||||||
# could silence unwanted alerts.
|
# could silence unwanted alerts.
|
||||||
# Default: ""
|
|
||||||
silence-duration 24h
|
silence-duration 24h
|
||||||
# Basic authentication (https://prometheus.io/docs/alerting/latest/https/)
|
# Basic authentication (https://prometheus.io/docs/alerting/latest/https/)
|
||||||
# Default: ""
|
|
||||||
user user
|
user user
|
||||||
# Default: ""
|
|
||||||
password pass
|
password pass
|
||||||
# By default the Alertmanager URL gets parsed from the webhook. In case that
|
# By default the Alertmanager URL gets parsed from the webhook. In case that
|
||||||
# Alertmanger is not reachable under that URL, it can be overwritten here.
|
# Alertmanger is not reachable under that URL, it can be overwritten here.
|
||||||
url https://alertmanager.example.com
|
url https://alertmanager.xenrox.net
|
||||||
}
|
}
|
||||||
|
|
||||||
# When the alert-mode is set to single, ntfy-alertmanager will cache each single alert
|
# When the alert-mode is set to single, ntfy-alertmanager will cache each single alert
|
||||||
# to avoid sending recurrences.
|
# to avoid sending recurrences.
|
||||||
cache {
|
cache {
|
||||||
# The type of cache that will be used
|
# The type of cache that will be used (either disabled, memory or redis; default is disabled).
|
||||||
# Options: disabled, memory, redis
|
|
||||||
# Default: disabled
|
|
||||||
type memory
|
type memory
|
||||||
# How long messages stay in the cache for
|
# How long messages stay in the cache for
|
||||||
# Default: 24h
|
|
||||||
duration 24h
|
duration 24h
|
||||||
|
|
||||||
# Memory cache settings
|
# Memory cache settings
|
||||||
# Interval in which the cache is cleaned up
|
# Interval in which the cache is cleaned up
|
||||||
# Default: 1h
|
|
||||||
cleanup-interval 1h
|
cleanup-interval 1h
|
||||||
|
|
||||||
# Redis cache settings
|
# Redis cache settings
|
||||||
# URL to connect to redis (default: redis://localhost:6379)
|
# URL to connect to redis (default: redis://localhost:6379)
|
||||||
# Default: redis://localhost:6379
|
|
||||||
redis-url redis://user:password@localhost:6789/3
|
redis-url redis://user:password@localhost:6789/3
|
||||||
}
|
}
|
||||||
|
|
664
config/config.go
664
config/config.go
|
@ -25,7 +25,6 @@ type Config struct {
|
||||||
HTTPAddress string
|
HTTPAddress string
|
||||||
LogLevel string
|
LogLevel string
|
||||||
LogFormat string
|
LogFormat string
|
||||||
LogFile string
|
|
||||||
AlertMode AlertMode
|
AlertMode AlertMode
|
||||||
User string
|
User string
|
||||||
Password string
|
Password string
|
||||||
|
@ -37,15 +36,13 @@ type Config struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type ntfyConfig struct {
|
type ntfyConfig struct {
|
||||||
Server string
|
Topic string
|
||||||
Topic string
|
User string
|
||||||
User string
|
Password string
|
||||||
Password string
|
AccessToken string
|
||||||
AccessToken string
|
CertFingerprint string
|
||||||
CertFingerprint string
|
EmailAddress string
|
||||||
EmailAddress string
|
Call string
|
||||||
Call string
|
|
||||||
GeneratorURLLabel string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type labels struct {
|
type labels struct {
|
||||||
|
@ -59,7 +56,6 @@ type labelConfig struct {
|
||||||
Icon string
|
Icon string
|
||||||
EmailAddress string
|
EmailAddress string
|
||||||
Call string
|
Call string
|
||||||
Topic string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CacheConfig is the configuration of the cache.
|
// CacheConfig is the configuration of the cache.
|
||||||
|
@ -86,338 +82,8 @@ type resolvedConfig struct {
|
||||||
Priority string
|
Priority string
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseBlock(block scfg.Block, config *Config) error {
|
// ReadConfig reads an scfg formatted file and returns the configuration struct.
|
||||||
d := block.Get("log-level")
|
func ReadConfig(path string) (*Config, error) {
|
||||||
if d != nil {
|
|
||||||
if err := d.ParseParams(&config.LogLevel); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
d = block.Get("log-format")
|
|
||||||
if d != nil {
|
|
||||||
if err := d.ParseParams(&config.LogFormat); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
d = block.Get("log-file")
|
|
||||||
if d != nil {
|
|
||||||
if err := d.ParseParams(&config.LogFile); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
d = block.Get("http-address")
|
|
||||||
if d != nil {
|
|
||||||
if err := d.ParseParams(&config.HTTPAddress); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
d = block.Get("base-url")
|
|
||||||
if d != nil {
|
|
||||||
if err := d.ParseParams(&config.BaseURL); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
d = block.Get("alert-mode")
|
|
||||||
if d != nil {
|
|
||||||
var mode string
|
|
||||||
if err := d.ParseParams(&mode); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
switch strings.ToLower(mode) {
|
|
||||||
case "single":
|
|
||||||
config.AlertMode = Single
|
|
||||||
|
|
||||||
case "multi":
|
|
||||||
config.AlertMode = Multi
|
|
||||||
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("%q directive: illegal mode %q", d.Name, mode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
d = block.Get("user")
|
|
||||||
if d != nil {
|
|
||||||
if err := d.ParseParams(&config.User); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
d = block.Get("password")
|
|
||||||
if d != nil {
|
|
||||||
if err := d.ParseParams(&config.Password); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
labelsDir := block.Get("labels")
|
|
||||||
if labelsDir != nil {
|
|
||||||
d = labelsDir.Children.Get("order")
|
|
||||||
if d != nil {
|
|
||||||
var order string
|
|
||||||
if err := d.ParseParams(&order); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
config.Labels.Order = strings.Split(order, ",")
|
|
||||||
}
|
|
||||||
|
|
||||||
labels := make(map[string]labelConfig)
|
|
||||||
for _, labelName := range config.Labels.Order {
|
|
||||||
for _, labelDir := range labelsDir.Children.GetAll(labelName) {
|
|
||||||
labelConfig := new(labelConfig)
|
|
||||||
var name string
|
|
||||||
|
|
||||||
if err := labelDir.ParseParams(&name); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
d = labelDir.Children.Get("priority")
|
|
||||||
if d != nil {
|
|
||||||
if err := d.ParseParams(&labelConfig.Priority); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
d = labelDir.Children.Get("tags")
|
|
||||||
if d != nil {
|
|
||||||
var tags string
|
|
||||||
if err := d.ParseParams(&tags); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
labelConfig.Tags = strings.Split(tags, ",")
|
|
||||||
}
|
|
||||||
|
|
||||||
d = labelDir.Children.Get("icon")
|
|
||||||
if d != nil {
|
|
||||||
if err := d.ParseParams(&labelConfig.Icon); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
d = labelDir.Children.Get("email-address")
|
|
||||||
if d != nil {
|
|
||||||
if err := d.ParseParams(&labelConfig.EmailAddress); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
d = labelDir.Children.Get("call")
|
|
||||||
if d != nil {
|
|
||||||
if err := d.ParseParams(&labelConfig.Call); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
d = labelDir.Children.Get("topic")
|
|
||||||
if d != nil {
|
|
||||||
if err := d.ParseParams(&labelConfig.Topic); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
labels[fmt.Sprintf("%s:%s", labelName, name)] = *labelConfig
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
config.Labels.Label = labels
|
|
||||||
}
|
|
||||||
|
|
||||||
ntfyDir := block.Get("ntfy")
|
|
||||||
if ntfyDir != nil {
|
|
||||||
d = ntfyDir.Children.Get("server")
|
|
||||||
if d != nil {
|
|
||||||
if err := d.ParseParams(&config.Ntfy.Server); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
d = ntfyDir.Children.Get("topic")
|
|
||||||
if d != nil {
|
|
||||||
if err := d.ParseParams(&config.Ntfy.Topic); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
d = ntfyDir.Children.Get("user")
|
|
||||||
if d != nil {
|
|
||||||
if err := d.ParseParams(&config.Ntfy.User); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
d = ntfyDir.Children.Get("password")
|
|
||||||
if d != nil {
|
|
||||||
if err := d.ParseParams(&config.Ntfy.Password); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
d = ntfyDir.Children.Get("access-token")
|
|
||||||
if d != nil {
|
|
||||||
if err := d.ParseParams(&config.Ntfy.AccessToken); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
d = ntfyDir.Children.Get("certificate-fingerprint")
|
|
||||||
if d != nil {
|
|
||||||
if err := d.ParseParams(&config.Ntfy.CertFingerprint); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// hex.EncodeToString outputs a lower case string
|
|
||||||
config.Ntfy.CertFingerprint = strings.ToLower(strings.ReplaceAll(config.Ntfy.CertFingerprint, ":", ""))
|
|
||||||
}
|
|
||||||
|
|
||||||
d = ntfyDir.Children.Get("email-address")
|
|
||||||
if d != nil {
|
|
||||||
if err := d.ParseParams(&config.Ntfy.EmailAddress); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
d = ntfyDir.Children.Get("call")
|
|
||||||
if d != nil {
|
|
||||||
if err := d.ParseParams(&config.Ntfy.Call); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
d = ntfyDir.Children.Get("generator-url-label")
|
|
||||||
if d != nil {
|
|
||||||
if err := d.ParseParams(&config.Ntfy.GeneratorURLLabel); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cacheDir := block.Get("cache")
|
|
||||||
if cacheDir != nil {
|
|
||||||
d = cacheDir.Children.Get("type")
|
|
||||||
if d != nil {
|
|
||||||
if err := d.ParseParams(&config.Cache.Type); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var durationString string
|
|
||||||
d = cacheDir.Children.Get("duration")
|
|
||||||
if d != nil {
|
|
||||||
if err := d.ParseParams(&durationString); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
duration, err := time.ParseDuration(durationString)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
config.Cache.Duration = duration
|
|
||||||
}
|
|
||||||
|
|
||||||
// memory
|
|
||||||
var cleanupIntervalString string
|
|
||||||
d = cacheDir.Children.Get("cleanup-interval")
|
|
||||||
if d != nil {
|
|
||||||
if err := d.ParseParams(&cleanupIntervalString); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
interval, err := time.ParseDuration(cleanupIntervalString)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
config.Cache.CleanupInterval = interval
|
|
||||||
}
|
|
||||||
|
|
||||||
// redis
|
|
||||||
d = cacheDir.Children.Get("redis-url")
|
|
||||||
if d != nil {
|
|
||||||
if err := d.ParseParams(&config.Cache.RedisURL); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
amDir := block.Get("alertmanager")
|
|
||||||
if amDir != nil {
|
|
||||||
var durationString string
|
|
||||||
d = amDir.Children.Get("silence-duration")
|
|
||||||
if d != nil {
|
|
||||||
if err := d.ParseParams(&durationString); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
duration, err := time.ParseDuration(durationString)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
config.Am.SilenceDuration = duration
|
|
||||||
}
|
|
||||||
|
|
||||||
d = amDir.Children.Get("user")
|
|
||||||
if d != nil {
|
|
||||||
if err := d.ParseParams(&config.Am.User); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
d = amDir.Children.Get("password")
|
|
||||||
if d != nil {
|
|
||||||
if err := d.ParseParams(&config.Am.Password); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
d = amDir.Children.Get("url")
|
|
||||||
if d != nil {
|
|
||||||
if err := d.ParseParams(&config.Am.URL); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
resolvedDir := block.Get("resolved")
|
|
||||||
if resolvedDir != nil {
|
|
||||||
d = resolvedDir.Children.Get("tags")
|
|
||||||
if d != nil {
|
|
||||||
var tags string
|
|
||||||
if err := d.ParseParams(&tags); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
config.Resolved.Tags = strings.Split(tags, ",")
|
|
||||||
}
|
|
||||||
|
|
||||||
d = resolvedDir.Children.Get("icon")
|
|
||||||
if d != nil {
|
|
||||||
if err := d.ParseParams(&config.Resolved.Icon); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
d = resolvedDir.Children.Get("priority")
|
|
||||||
if d != nil {
|
|
||||||
if err := d.ParseParams(&config.Resolved.Priority); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read reads an scfg formatted file and returns the configuration struct.
|
|
||||||
func Read(path string) (*Config, error) {
|
|
||||||
cfg, err := scfg.Load(path)
|
cfg, err := scfg.Load(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -437,50 +103,326 @@ func Read(path string) (*Config, error) {
|
||||||
// redis
|
// redis
|
||||||
config.Cache.RedisURL = "redis://localhost:6379"
|
config.Cache.RedisURL = "redis://localhost:6379"
|
||||||
|
|
||||||
includeDirs := cfg.GetAll("include")
|
d := cfg.Get("log-level")
|
||||||
for _, d := range includeDirs {
|
if d != nil {
|
||||||
var includePath string
|
if err := d.ParseParams(&config.LogLevel); err != nil {
|
||||||
if err := d.ParseParams(&includePath); err != nil {
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
d = cfg.Get("log-format")
|
||||||
|
if d != nil {
|
||||||
|
if err := d.ParseParams(&config.LogFormat); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
d = cfg.Get("http-address")
|
||||||
|
if d != nil {
|
||||||
|
if err := d.ParseParams(&config.HTTPAddress); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
d = cfg.Get("base-url")
|
||||||
|
if d != nil {
|
||||||
|
if err := d.ParseParams(&config.BaseURL); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
d = cfg.Get("alert-mode")
|
||||||
|
if d != nil {
|
||||||
|
var mode string
|
||||||
|
if err := d.ParseParams(&mode); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
block, err := scfg.Load(includePath)
|
switch strings.ToLower(mode) {
|
||||||
if err != nil {
|
case "single":
|
||||||
return nil, fmt.Errorf("cannot load included config file %q: %v", includePath, err)
|
config.AlertMode = Single
|
||||||
}
|
|
||||||
|
|
||||||
if err := parseBlock(block, config); err != nil {
|
case "multi":
|
||||||
return nil, fmt.Errorf("cannot parse included config file %q: %v", includePath, err)
|
config.AlertMode = Multi
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("%q directive: illegal mode %q", d.Name, mode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = parseBlock(cfg, config)
|
d = cfg.Get("user")
|
||||||
if err != nil {
|
if d != nil {
|
||||||
return nil, err
|
if err := d.ParseParams(&config.User); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
d = cfg.Get("password")
|
||||||
|
if d != nil {
|
||||||
|
if err := d.ParseParams(&config.Password); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check settings
|
|
||||||
if (config.Password != "" && config.User == "") ||
|
if (config.Password != "" && config.User == "") ||
|
||||||
(config.Password == "" && config.User != "") {
|
(config.Password == "" && config.User != "") {
|
||||||
return nil, errors.New("user and password have to be set together")
|
return nil, errors.New("user and password have to be set together")
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.Ntfy.Topic == "" {
|
labelsDir := cfg.Get("labels")
|
||||||
|
if labelsDir != nil {
|
||||||
|
d = labelsDir.Children.Get("order")
|
||||||
|
if d != nil {
|
||||||
|
var order string
|
||||||
|
if err := d.ParseParams(&order); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
config.Labels.Order = strings.Split(order, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
labels := make(map[string]labelConfig)
|
||||||
|
for _, labelName := range config.Labels.Order {
|
||||||
|
for _, labelDir := range labelsDir.Children.GetAll(labelName) {
|
||||||
|
labelConfig := new(labelConfig)
|
||||||
|
var name string
|
||||||
|
|
||||||
|
if err := labelDir.ParseParams(&name); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
d = labelDir.Children.Get("priority")
|
||||||
|
if d != nil {
|
||||||
|
if err := d.ParseParams(&labelConfig.Priority); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
d = labelDir.Children.Get("tags")
|
||||||
|
if d != nil {
|
||||||
|
var tags string
|
||||||
|
if err := d.ParseParams(&tags); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
labelConfig.Tags = strings.Split(tags, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
d = labelDir.Children.Get("icon")
|
||||||
|
if d != nil {
|
||||||
|
if err := d.ParseParams(&labelConfig.Icon); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
d = labelDir.Children.Get("email-address")
|
||||||
|
if d != nil {
|
||||||
|
if err := d.ParseParams(&labelConfig.EmailAddress); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
d = labelDir.Children.Get("call")
|
||||||
|
if d != nil {
|
||||||
|
if err := d.ParseParams(&labelConfig.Call); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
labels[fmt.Sprintf("%s:%s", labelName, name)] = *labelConfig
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
config.Labels.Label = labels
|
||||||
|
}
|
||||||
|
|
||||||
|
ntfyDir := cfg.Get("ntfy")
|
||||||
|
if ntfyDir == nil {
|
||||||
|
return nil, fmt.Errorf("%q directive missing", "ntfy")
|
||||||
|
}
|
||||||
|
|
||||||
|
d = ntfyDir.Children.Get("topic")
|
||||||
|
if d == nil {
|
||||||
return nil, fmt.Errorf("%q missing from %q directive", "topic", "ntfy")
|
return nil, fmt.Errorf("%q missing from %q directive", "topic", "ntfy")
|
||||||
}
|
}
|
||||||
|
if err := d.ParseParams(&config.Ntfy.Topic); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
d = ntfyDir.Children.Get("user")
|
||||||
|
if d != nil {
|
||||||
|
if err := d.ParseParams(&config.Ntfy.User); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
d = ntfyDir.Children.Get("password")
|
||||||
|
if d != nil {
|
||||||
|
if err := d.ParseParams(&config.Ntfy.Password); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (config.Ntfy.Password != "" && config.Ntfy.User == "") ||
|
if (config.Ntfy.Password != "" && config.Ntfy.User == "") ||
|
||||||
(config.Ntfy.Password == "" && config.Ntfy.User != "") {
|
(config.Ntfy.Password == "" && config.Ntfy.User != "") {
|
||||||
return nil, errors.New("ntfy: user and password have to be set together")
|
return nil, errors.New("ntfy: user and password have to be set together")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
d = ntfyDir.Children.Get("access-token")
|
||||||
|
if d != nil {
|
||||||
|
if err := d.ParseParams(&config.Ntfy.AccessToken); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if config.Ntfy.User != "" && config.Ntfy.AccessToken != "" {
|
if config.Ntfy.User != "" && config.Ntfy.AccessToken != "" {
|
||||||
return nil, errors.New("ntfy: cannot use both an access-token and a user/password at the same time")
|
return nil, errors.New("ntfy: cannot use both an access-token and a user/password at the same time")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.Am.Password != "" && config.Am.User == "") ||
|
d = ntfyDir.Children.Get("certificate-fingerprint")
|
||||||
(config.Am.Password == "" && config.Am.User != "") {
|
if d != nil {
|
||||||
return nil, errors.New("alertmanager: user and password have to be set together")
|
if err := d.ParseParams(&config.Ntfy.CertFingerprint); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// hex.EncodeToString outputs a lower case string
|
||||||
|
config.Ntfy.CertFingerprint = strings.ToLower(strings.ReplaceAll(config.Ntfy.CertFingerprint, ":", ""))
|
||||||
|
}
|
||||||
|
|
||||||
|
d = ntfyDir.Children.Get("email-address")
|
||||||
|
if d != nil {
|
||||||
|
if err := d.ParseParams(&config.Ntfy.EmailAddress); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
d = ntfyDir.Children.Get("call")
|
||||||
|
if d != nil {
|
||||||
|
if err := d.ParseParams(&config.Ntfy.Call); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheDir := cfg.Get("cache")
|
||||||
|
|
||||||
|
if cacheDir != nil {
|
||||||
|
d = cacheDir.Children.Get("type")
|
||||||
|
if d != nil {
|
||||||
|
if err := d.ParseParams(&config.Cache.Type); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var durationString string
|
||||||
|
d = cacheDir.Children.Get("duration")
|
||||||
|
if d != nil {
|
||||||
|
if err := d.ParseParams(&durationString); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
duration, err := time.ParseDuration(durationString)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
config.Cache.Duration = duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// memory
|
||||||
|
var cleanupIntervalString string
|
||||||
|
d = cacheDir.Children.Get("cleanup-interval")
|
||||||
|
if d != nil {
|
||||||
|
if err := d.ParseParams(&cleanupIntervalString); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
interval, err := time.ParseDuration(cleanupIntervalString)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
config.Cache.CleanupInterval = interval
|
||||||
|
}
|
||||||
|
|
||||||
|
// redis
|
||||||
|
d = cacheDir.Children.Get("redis-url")
|
||||||
|
if d != nil {
|
||||||
|
if err := d.ParseParams(&config.Cache.RedisURL); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
amDir := cfg.Get("alertmanager")
|
||||||
|
|
||||||
|
if amDir != nil {
|
||||||
|
var durationString string
|
||||||
|
d = amDir.Children.Get("silence-duration")
|
||||||
|
if d != nil {
|
||||||
|
if err := d.ParseParams(&durationString); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
duration, err := time.ParseDuration(durationString)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
config.Am.SilenceDuration = duration
|
||||||
|
}
|
||||||
|
|
||||||
|
d = amDir.Children.Get("user")
|
||||||
|
if d != nil {
|
||||||
|
if err := d.ParseParams(&config.Am.User); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
d = amDir.Children.Get("password")
|
||||||
|
if d != nil {
|
||||||
|
if err := d.ParseParams(&config.Am.Password); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.Am.Password != "" && config.Am.User == "") ||
|
||||||
|
(config.Am.Password == "" && config.Am.User != "") {
|
||||||
|
return nil, errors.New("alertmanager: user and password have to be set together")
|
||||||
|
}
|
||||||
|
|
||||||
|
d = amDir.Children.Get("url")
|
||||||
|
if d != nil {
|
||||||
|
if err := d.ParseParams(&config.Am.URL); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resolvedDir := cfg.Get("resolved")
|
||||||
|
if resolvedDir != nil {
|
||||||
|
d = resolvedDir.Children.Get("tags")
|
||||||
|
if d != nil {
|
||||||
|
var tags string
|
||||||
|
if err := d.ParseParams(&tags); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
config.Resolved.Tags = strings.Split(tags, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
d = resolvedDir.Children.Get("icon")
|
||||||
|
if d != nil {
|
||||||
|
if err := d.ParseParams(&config.Resolved.Icon); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
d = resolvedDir.Children.Get("priority")
|
||||||
|
if d != nil {
|
||||||
|
if err := d.ParseParams(&config.Resolved.Priority); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return config, nil
|
return config, nil
|
||||||
|
|
|
@ -10,11 +10,10 @@ import (
|
||||||
|
|
||||||
func TestReadConfig(t *testing.T) {
|
func TestReadConfig(t *testing.T) {
|
||||||
configContent := `
|
configContent := `
|
||||||
base-url https://ntfy-alertmanager.example.com
|
base-url https://ntfy-alertmanager.xenrox.net
|
||||||
http-address :8080
|
http-address :8080
|
||||||
log-level info
|
log-level info
|
||||||
log-format json
|
log-format json
|
||||||
log-file /var/log/ntfy-alertmanager.log
|
|
||||||
alert-mode multi
|
alert-mode multi
|
||||||
user webhookUser
|
user webhookUser
|
||||||
password webhookPass
|
password webhookPass
|
||||||
|
@ -25,8 +24,8 @@ labels {
|
||||||
severity "critical" {
|
severity "critical" {
|
||||||
priority 5
|
priority 5
|
||||||
tags "rotating_light"
|
tags "rotating_light"
|
||||||
icon "https://example.com/critical.png"
|
icon "https://foo.com/critical.png"
|
||||||
email-address foo@example.com
|
email-address foo@bar.com
|
||||||
call yes
|
call yes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,30 +35,27 @@ labels {
|
||||||
|
|
||||||
instance "example.com" {
|
instance "example.com" {
|
||||||
tags "computer,example"
|
tags "computer,example"
|
||||||
topic homeserver
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
resolved {
|
resolved {
|
||||||
tags "resolved,partying_face"
|
tags "resolved,partying_face"
|
||||||
icon "https://example.com/resolved.png"
|
icon "https://foo.com/resolved.png"
|
||||||
priority 1
|
priority 1
|
||||||
}
|
}
|
||||||
|
|
||||||
ntfy {
|
ntfy {
|
||||||
server https://ntfy.sh
|
|
||||||
topic https://ntfy.sh/alertmanager-alerts
|
topic https://ntfy.sh/alertmanager-alerts
|
||||||
certificate-fingerprint 13:6D:2B:88:9C:57:36:D0:81:B4:B2:9C:79:09:27:62:92:CF:B8:6A:6B:D3:AD:46:35:CB:70:17:EB:99:6E:28:08:2A:B8:C6:79:4B:F6:2E:81:79:41:98:1D:53:C8:07:B3:5C:24:5F:B1:8E:B6:FB:66:B5:DD:B4:D0:5C:29:91
|
certificate-fingerprint 13:6D:2B:88:9C:57:36:D0:81:B4:B2:9C:79:09:27:62:92:CF:B8:6A:6B:D3:AD:46:35:CB:70:17:EB:99:6E:28:08:2A:B8:C6:79:4B:F6:2E:81:79:41:98:1D:53:C8:07:B3:5C:24:5F:B1:8E:B6:FB:66:B5:DD:B4:D0:5C:29:91
|
||||||
user user
|
user user
|
||||||
password pass
|
password pass
|
||||||
generator-url-label source
|
|
||||||
}
|
}
|
||||||
|
|
||||||
alertmanager {
|
alertmanager {
|
||||||
silence-duration 24h
|
silence-duration 24h
|
||||||
user user
|
user user
|
||||||
password pass
|
password pass
|
||||||
url https://alertmanager.example.com
|
url https://alertmanager.xenrox.net
|
||||||
}
|
}
|
||||||
|
|
||||||
cache {
|
cache {
|
||||||
|
@ -70,35 +66,30 @@ cache {
|
||||||
`
|
`
|
||||||
|
|
||||||
expectedCfg := &Config{
|
expectedCfg := &Config{
|
||||||
BaseURL: "https://ntfy-alertmanager.example.com",
|
BaseURL: "https://ntfy-alertmanager.xenrox.net",
|
||||||
HTTPAddress: ":8080",
|
HTTPAddress: ":8080",
|
||||||
LogLevel: "info",
|
LogLevel: "info",
|
||||||
LogFormat: "json",
|
LogFormat: "json",
|
||||||
LogFile: "/var/log/ntfy-alertmanager.log",
|
|
||||||
AlertMode: Multi,
|
AlertMode: Multi,
|
||||||
User: "webhookUser",
|
User: "webhookUser",
|
||||||
Password: "webhookPass",
|
Password: "webhookPass",
|
||||||
Ntfy: ntfyConfig{
|
Ntfy: ntfyConfig{
|
||||||
Server: "https://ntfy.sh",
|
Topic: "https://ntfy.sh/alertmanager-alerts",
|
||||||
Topic: "https://ntfy.sh/alertmanager-alerts",
|
User: "user",
|
||||||
User: "user",
|
Password: "pass",
|
||||||
Password: "pass",
|
CertFingerprint: "136d2b889c5736d081b4b29c7909276292cfb86a6bd3ad4635cb7017eb996e28082ab8c6794bf62e817941981d53c807b35c245fb18eb6fb66b5ddb4d05c2991",
|
||||||
CertFingerprint: "136d2b889c5736d081b4b29c7909276292cfb86a6bd3ad4635cb7017eb996e28082ab8c6794bf62e817941981d53c807b35c245fb18eb6fb66b5ddb4d05c2991",
|
|
||||||
GeneratorURLLabel: "source",
|
|
||||||
},
|
},
|
||||||
Labels: labels{Order: []string{"severity", "instance"},
|
Labels: labels{Order: []string{"severity", "instance"},
|
||||||
Label: map[string]labelConfig{
|
Label: map[string]labelConfig{
|
||||||
"severity:critical": {
|
"severity:critical": {
|
||||||
Priority: "5",
|
Priority: "5",
|
||||||
Tags: []string{"rotating_light"},
|
Tags: []string{"rotating_light"},
|
||||||
Icon: "https://example.com/critical.png",
|
Icon: "https://foo.com/critical.png",
|
||||||
EmailAddress: "foo@example.com",
|
EmailAddress: "foo@bar.com",
|
||||||
Call: "yes",
|
Call: "yes",
|
||||||
},
|
},
|
||||||
"severity:info": {Priority: "1"},
|
"severity:info": {Priority: "1"},
|
||||||
"instance:example.com": {
|
"instance:example.com": {Tags: []string{"computer", "example"}},
|
||||||
Tags: []string{"computer", "example"},
|
|
||||||
Topic: "homeserver"},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Cache: CacheConfig{
|
Cache: CacheConfig{
|
||||||
|
@ -111,11 +102,11 @@ cache {
|
||||||
SilenceDuration: time.Hour * 24,
|
SilenceDuration: time.Hour * 24,
|
||||||
User: "user",
|
User: "user",
|
||||||
Password: "pass",
|
Password: "pass",
|
||||||
URL: "https://alertmanager.example.com",
|
URL: "https://alertmanager.xenrox.net",
|
||||||
},
|
},
|
||||||
Resolved: resolvedConfig{
|
Resolved: resolvedConfig{
|
||||||
Tags: []string{"resolved", "partying_face"},
|
Tags: []string{"resolved", "partying_face"},
|
||||||
Icon: "https://example.com/resolved.png",
|
Icon: "https://foo.com/resolved.png",
|
||||||
Priority: "1",
|
Priority: "1",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -126,7 +117,7 @@ cache {
|
||||||
t.Errorf("failed to write config file: %v", err)
|
t.Errorf("failed to write config file: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg, err := Read(configPath)
|
cfg, err := ReadConfig(configPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("failed to read config file: %v", err)
|
t.Errorf("failed to read config file: %v", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,80 +0,0 @@
|
||||||
{
|
|
||||||
"receiver": "test-receiver",
|
|
||||||
"status": "firing",
|
|
||||||
"alerts": [
|
|
||||||
{
|
|
||||||
"status": "firing",
|
|
||||||
"labels": {
|
|
||||||
"alertname": "systemd_unit_failed",
|
|
||||||
"instance": "example.com",
|
|
||||||
"job": "node_exporter",
|
|
||||||
"name": "example.service",
|
|
||||||
"severity": "critical",
|
|
||||||
"state": "failed",
|
|
||||||
"type": "oneshot"
|
|
||||||
},
|
|
||||||
"annotations": {
|
|
||||||
"description": "Instance example.com: Service example.service failed",
|
|
||||||
"summary": "Systemd unit failed"
|
|
||||||
},
|
|
||||||
"startsAt": "2022-10-09T08:31:47.929Z",
|
|
||||||
"endsAt": "0001-01-01T00:00:00Z",
|
|
||||||
"generatorURL": "http://example.com:9090/graph?g0.expr=node_systemd_unit_state%7Bstate%3D%22failed%22%7D+%3E+0\u0026g0.tab=1",
|
|
||||||
"fingerprint": "6f1c1a905a91802b"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"status": "firing",
|
|
||||||
"labels": {
|
|
||||||
"alertname": "systemd_unit_failed",
|
|
||||||
"instance": "example.com",
|
|
||||||
"job": "node_exporter",
|
|
||||||
"name": "bar.service",
|
|
||||||
"severity": "critical",
|
|
||||||
"state": "failed",
|
|
||||||
"type": "oneshot"
|
|
||||||
},
|
|
||||||
"annotations": {
|
|
||||||
"description": "Instance example.com: Service bar.service failed",
|
|
||||||
"summary": "Systemd unit failed"
|
|
||||||
},
|
|
||||||
"startsAt": "2022-10-06T12:48:47.929Z",
|
|
||||||
"endsAt": "0001-01-01T00:00:00Z",
|
|
||||||
"generatorURL": "http://example.com:9090/graph?g0.expr=node_systemd_unit_state%7Bstate%3D%22failed%22%7D+%3E+0\u0026g0.tab=1",
|
|
||||||
"fingerprint": "0b17134bedc094da"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"status": "firing",
|
|
||||||
"labels": {
|
|
||||||
"alertname": "systemd_unit_failed",
|
|
||||||
"instance": "example.com",
|
|
||||||
"job": "node_exporter",
|
|
||||||
"name": "foo.service",
|
|
||||||
"severity": "critical",
|
|
||||||
"state": "failed",
|
|
||||||
"type": "oneshot"
|
|
||||||
},
|
|
||||||
"annotations": {
|
|
||||||
"description": "Instance example.com: Service foo.service failed",
|
|
||||||
"summary": "Systemd unit failed"
|
|
||||||
},
|
|
||||||
"startsAt": "2022-10-09T00:03:47.929Z",
|
|
||||||
"endsAt": "0001-01-01T00:00:00Z",
|
|
||||||
"generatorURL": "http://example.com:9090/graph?g0.expr=node_systemd_unit_state%7Bstate%3D%22failed%22%7D+%3E+0\u0026g0.tab=1",
|
|
||||||
"fingerprint": "aaaabf78228dbbc6"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"groupLabels": { "instance": "example.com", "severity": "critical" },
|
|
||||||
"commonLabels": {
|
|
||||||
"alertname": "systemd_unit_failed",
|
|
||||||
"instance": "example.com",
|
|
||||||
"job": "node_exporter",
|
|
||||||
"severity": "critical",
|
|
||||||
"state": "failed",
|
|
||||||
"type": "oneshot"
|
|
||||||
},
|
|
||||||
"commonAnnotations": { "summary": "Systemd unit failed" },
|
|
||||||
"externalURL": "http://example.com:9093",
|
|
||||||
"version": "4",
|
|
||||||
"groupKey": "{}:{instance=\"example.com\", severity=\"critical\"}",
|
|
||||||
"truncatedAlerts": 0
|
|
||||||
}
|
|
|
@ -1,80 +0,0 @@
|
||||||
{
|
|
||||||
"receiver": "test-receiver",
|
|
||||||
"status": "firing",
|
|
||||||
"alerts": [
|
|
||||||
{
|
|
||||||
"status": "resolved",
|
|
||||||
"labels": {
|
|
||||||
"alertname": "systemd_unit_failed",
|
|
||||||
"instance": "example.com",
|
|
||||||
"job": "node_exporter",
|
|
||||||
"name": "example.service",
|
|
||||||
"severity": "critical",
|
|
||||||
"state": "failed",
|
|
||||||
"type": "oneshot"
|
|
||||||
},
|
|
||||||
"annotations": {
|
|
||||||
"description": "Instance example.com: Service example.service failed",
|
|
||||||
"summary": "Systemd unit failed"
|
|
||||||
},
|
|
||||||
"startsAt": "2022-10-09T08:31:47.929Z",
|
|
||||||
"endsAt": "0001-01-01T00:00:00Z",
|
|
||||||
"generatorURL": "http://example.com:9090/graph?g0.expr=node_systemd_unit_state%7Bstate%3D%22failed%22%7D+%3E+0\u0026g0.tab=1",
|
|
||||||
"fingerprint": "6f1c1a905a91802b"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"status": "firing",
|
|
||||||
"labels": {
|
|
||||||
"alertname": "systemd_unit_failed",
|
|
||||||
"instance": "example.com",
|
|
||||||
"job": "node_exporter",
|
|
||||||
"name": "bar.service",
|
|
||||||
"severity": "critical",
|
|
||||||
"state": "failed",
|
|
||||||
"type": "oneshot"
|
|
||||||
},
|
|
||||||
"annotations": {
|
|
||||||
"description": "Instance example.com: Service bar.service failed",
|
|
||||||
"summary": "Systemd unit failed"
|
|
||||||
},
|
|
||||||
"startsAt": "2022-10-06T12:48:47.929Z",
|
|
||||||
"endsAt": "0001-01-01T00:00:00Z",
|
|
||||||
"generatorURL": "http://example.com:9090/graph?g0.expr=node_systemd_unit_state%7Bstate%3D%22failed%22%7D+%3E+0\u0026g0.tab=1",
|
|
||||||
"fingerprint": "0b17134bedc094da"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"status": "firing",
|
|
||||||
"labels": {
|
|
||||||
"alertname": "systemd_unit_failed",
|
|
||||||
"instance": "example.com",
|
|
||||||
"job": "node_exporter",
|
|
||||||
"name": "foo.service",
|
|
||||||
"severity": "critical",
|
|
||||||
"state": "failed",
|
|
||||||
"type": "oneshot"
|
|
||||||
},
|
|
||||||
"annotations": {
|
|
||||||
"description": "Instance example.com: Service foo.service failed",
|
|
||||||
"summary": "Systemd unit failed"
|
|
||||||
},
|
|
||||||
"startsAt": "2022-10-09T00:03:47.929Z",
|
|
||||||
"endsAt": "0001-01-01T00:00:00Z",
|
|
||||||
"generatorURL": "http://example.com:9090/graph?g0.expr=node_systemd_unit_state%7Bstate%3D%22failed%22%7D+%3E+0\u0026g0.tab=1",
|
|
||||||
"fingerprint": "aaaabf78228dbbc6"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"groupLabels": { "instance": "example.com", "severity": "critical" },
|
|
||||||
"commonLabels": {
|
|
||||||
"alertname": "systemd_unit_failed",
|
|
||||||
"instance": "example.com",
|
|
||||||
"job": "node_exporter",
|
|
||||||
"severity": "critical",
|
|
||||||
"state": "failed",
|
|
||||||
"type": "oneshot"
|
|
||||||
},
|
|
||||||
"commonAnnotations": { "summary": "Systemd unit failed" },
|
|
||||||
"externalURL": "http://example.com:9093",
|
|
||||||
"version": "4",
|
|
||||||
"groupKey": "{}:{instance=\"example.com\", severity=\"critical\"}",
|
|
||||||
"truncatedAlerts": 0
|
|
||||||
}
|
|
|
@ -1,6 +1,6 @@
|
||||||
FROM golang:alpine AS build
|
FROM golang:alpine as build
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
ARG VERSION=0.4.0
|
ARG VERSION=0.3.0
|
||||||
|
|
||||||
RUN wget https://git.xenrox.net/~xenrox/ntfy-alertmanager/refs/download/v${VERSION}/ntfy-alertmanager-${VERSION}.tar.gz -O latest.tar.gz && \
|
RUN wget https://git.xenrox.net/~xenrox/ntfy-alertmanager/refs/download/v${VERSION}/ntfy-alertmanager-${VERSION}.tar.gz -O latest.tar.gz && \
|
||||||
tar -zxf latest.tar.gz
|
tar -zxf latest.tar.gz
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
FROM golang:alpine AS build
|
FROM golang:alpine as build
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
version: "3"
|
||||||
services:
|
services:
|
||||||
ntfy-alertmanager:
|
ntfy-alertmanager:
|
||||||
image: xenrox/ntfy-alertmanager:latest
|
image: xenrox/ntfy-alertmanager:latest
|
||||||
|
@ -8,10 +9,10 @@ services:
|
||||||
- 127.0.0.1:8080:8080
|
- 127.0.0.1:8080:8080
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
# Uncomment if you want to use valkey as cache
|
# Uncomment if you want to use redis as cache
|
||||||
# valkey:
|
# redis:
|
||||||
# image: valkey/valkey
|
# image: redis:alpine
|
||||||
# container_name: valkey
|
# container_name: redis
|
||||||
# restart: unless-stopped
|
# restart: unless-stopped
|
||||||
# volumes:
|
# volumes:
|
||||||
# - ./cache:/data
|
# - ./cache:/data
|
12
go.mod
12
go.mod
|
@ -1,15 +1,15 @@
|
||||||
module git.xenrox.net/~xenrox/ntfy-alertmanager
|
module git.xenrox.net/~xenrox/ntfy-alertmanager
|
||||||
|
|
||||||
go 1.22.0
|
go 1.21.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
git.sr.ht/~emersion/go-scfg v0.0.0-20240128091534-2ae16e782082
|
git.sr.ht/~emersion/go-scfg v0.0.0-20230601130942-e042ab15616e
|
||||||
git.xenrox.net/~xenrox/go-utils v0.0.0-20230813142628-a8bdc9211a98
|
git.xenrox.net/~xenrox/go-utils v0.0.0-20230813135846-959a68fe51de
|
||||||
github.com/redis/go-redis/v9 v9.7.0
|
github.com/redis/go-redis/v9 v9.0.5
|
||||||
golang.org/x/text v0.20.0
|
golang.org/x/text v0.12.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||||
)
|
)
|
||||||
|
|
28
go.sum
28
go.sum
|
@ -1,18 +1,18 @@
|
||||||
git.sr.ht/~emersion/go-scfg v0.0.0-20240128091534-2ae16e782082 h1:9Udx5fm4vRtmgDIBjy2ef5QioHbzpw5oHabbhpAUyEw=
|
git.sr.ht/~emersion/go-scfg v0.0.0-20230601130942-e042ab15616e h1:42zyo0ZFxHGkysM1B9EM7PnQNO0TEzPm+bw/2Zontyg=
|
||||||
git.sr.ht/~emersion/go-scfg v0.0.0-20240128091534-2ae16e782082/go.mod h1:ybgvEJTIx5XbaspSviB3KNa6OdPmAZqDoSud7z8fFlw=
|
git.sr.ht/~emersion/go-scfg v0.0.0-20230601130942-e042ab15616e/go.mod h1:ybgvEJTIx5XbaspSviB3KNa6OdPmAZqDoSud7z8fFlw=
|
||||||
git.xenrox.net/~xenrox/go-utils v0.0.0-20230813142628-a8bdc9211a98 h1:c6B8yMLiPWj8Fqp3AeLBB86gKhdz2hfgAupaNpmMRMo=
|
git.xenrox.net/~xenrox/go-utils v0.0.0-20230813135846-959a68fe51de h1:nTecM544Gf8hxxGVSn1dB8LGtGrwr5YAxOEbcJLLjVQ=
|
||||||
git.xenrox.net/~xenrox/go-utils v0.0.0-20230813142628-a8bdc9211a98/go.mod h1:BM4sMPD0fqFB6eG1T/7rGgEUiqZsMpHvq4PGE861Sfk=
|
git.xenrox.net/~xenrox/go-utils v0.0.0-20230813135846-959a68fe51de/go.mod h1:BM4sMPD0fqFB6eG1T/7rGgEUiqZsMpHvq4PGE861Sfk=
|
||||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
github.com/bsm/ginkgo/v2 v2.7.0 h1:ItPMPH90RbmZJt5GtkcNvIRuGEdwlBItdNVoyzaNQao=
|
||||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
github.com/bsm/ginkgo/v2 v2.7.0/go.mod h1:AiKlXPm7ItEHNc/2+OkrNG4E0ITzojb9/xWzvQ9XZ9w=
|
||||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
github.com/bsm/gomega v1.26.0 h1:LhQm+AFcgV2M0WyKroMASzAzCAJVpAxQXv4SaI9a69Y=
|
||||||
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
github.com/bsm/gomega v1.26.0/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||||
github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E=
|
github.com/redis/go-redis/v9 v9.0.5 h1:CuQcn5HIEeK7BgElubPP8CGtE0KakrnbBSTLjathl5o=
|
||||||
github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw=
|
github.com/redis/go-redis/v9 v9.0.5/go.mod h1:WqMKv5vnQbRuZstUwxQI195wHy+t4PuXDOjzMvcuQHk=
|
||||||
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
|
golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc=
|
||||||
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
|
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
|
|
125
main.go
125
main.go
|
@ -14,10 +14,8 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"slices"
|
"slices"
|
||||||
|
@ -34,8 +32,6 @@ import (
|
||||||
|
|
||||||
var version = "dev"
|
var version = "dev"
|
||||||
|
|
||||||
const maxNTFYActions = 3
|
|
||||||
|
|
||||||
type bridge struct {
|
type bridge struct {
|
||||||
cfg *config.Config
|
cfg *config.Config
|
||||||
logger *slog.Logger
|
logger *slog.Logger
|
||||||
|
@ -53,11 +49,10 @@ type payload struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type alert struct {
|
type alert struct {
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Labels map[string]string `json:"labels"`
|
Labels map[string]string `json:"labels"`
|
||||||
Annotations map[string]string `json:"annotations"`
|
Annotations map[string]string `json:"annotations"`
|
||||||
GeneratorURL string `json:"generatorURL"`
|
Fingerprint string `json:"fingerprint"`
|
||||||
Fingerprint string `json:"fingerprint"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type notification struct {
|
type notification struct {
|
||||||
|
@ -71,18 +66,16 @@ type notification struct {
|
||||||
silenceBody string
|
silenceBody string
|
||||||
fingerprint string
|
fingerprint string
|
||||||
status string
|
status string
|
||||||
generatorURL string
|
|
||||||
topic string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ntfyError struct {
|
type ntfyError struct {
|
||||||
Error string `json:"error"`
|
Error string `json:"error"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (br *bridge) singleAlertNotifications(ctx context.Context, p *payload) []*notification {
|
func (br *bridge) singleAlertNotifications(p *payload) []*notification {
|
||||||
var notifications []*notification
|
var notifications []*notification
|
||||||
for _, alert := range p.Alerts {
|
for _, alert := range p.Alerts {
|
||||||
contains, err := br.cache.Contains(ctx, alert.Fingerprint, alert.Status)
|
contains, err := br.cache.Contains(alert.Fingerprint, alert.Status)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
br.logger.Error("Failed to lookup alert in cache",
|
br.logger.Error("Failed to lookup alert in cache",
|
||||||
slog.String("fingerprint", alert.Fingerprint),
|
slog.String("fingerprint", alert.Fingerprint),
|
||||||
|
@ -97,7 +90,6 @@ func (br *bridge) singleAlertNotifications(ctx context.Context, p *payload) []*n
|
||||||
n := new(notification)
|
n := new(notification)
|
||||||
n.fingerprint = alert.Fingerprint
|
n.fingerprint = alert.Fingerprint
|
||||||
n.status = alert.Status
|
n.status = alert.Status
|
||||||
n.generatorURL = alert.GeneratorURL
|
|
||||||
|
|
||||||
// create title
|
// create title
|
||||||
n.title = fmt.Sprintf("[%s]", strings.ToUpper(alert.Status))
|
n.title = fmt.Sprintf("[%s]", strings.ToUpper(alert.Status))
|
||||||
|
@ -158,10 +150,6 @@ func (br *bridge) singleAlertNotifications(ctx context.Context, p *payload) []*n
|
||||||
n.call = labelConfig.Call
|
n.call = labelConfig.Call
|
||||||
}
|
}
|
||||||
|
|
||||||
if n.topic == "" {
|
|
||||||
n.topic = labelConfig.Topic
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, val := range labelConfig.Tags {
|
for _, val := range labelConfig.Tags {
|
||||||
if !slices.Contains(tags, val) {
|
if !slices.Contains(tags, val) {
|
||||||
tags = append(tags, val)
|
tags = append(tags, val)
|
||||||
|
@ -272,10 +260,6 @@ func (br *bridge) multiAlertNotification(p *payload) *notification {
|
||||||
n.call = labelConfig.Call
|
n.call = labelConfig.Call
|
||||||
}
|
}
|
||||||
|
|
||||||
if n.topic == "" {
|
|
||||||
n.topic = labelConfig.Topic
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, val := range labelConfig.Tags {
|
for _, val := range labelConfig.Tags {
|
||||||
if !slices.Contains(tags, val) {
|
if !slices.Contains(tags, val) {
|
||||||
tags = append(tags, val)
|
tags = append(tags, val)
|
||||||
|
@ -304,24 +288,12 @@ func (br *bridge) multiAlertNotification(p *payload) *notification {
|
||||||
return n
|
return n
|
||||||
}
|
}
|
||||||
|
|
||||||
func (br *bridge) publish(n *notification, topicParam string) error {
|
func (br *bridge) publish(n *notification) error {
|
||||||
// precedence: topicParam > n.topic > cfg.Ntfy.Topic
|
req, err := http.NewRequest(http.MethodPost, br.cfg.Ntfy.Topic, strings.NewReader(n.body))
|
||||||
if topicParam == "" {
|
|
||||||
topicParam = n.topic
|
|
||||||
}
|
|
||||||
|
|
||||||
url, err := br.topicURL(topicParam)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
req, err := http.NewRequest(http.MethodPost, url, strings.NewReader(n.body))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var actions []string
|
|
||||||
|
|
||||||
// ntfy authentication
|
// ntfy authentication
|
||||||
if br.cfg.Ntfy.Password != "" && br.cfg.Ntfy.User != "" {
|
if br.cfg.Ntfy.Password != "" && br.cfg.Ntfy.User != "" {
|
||||||
req.SetBasicAuth(br.cfg.Ntfy.User, br.cfg.Ntfy.Password)
|
req.SetBasicAuth(br.cfg.Ntfy.User, br.cfg.Ntfy.Password)
|
||||||
|
@ -360,26 +332,13 @@ func (br *bridge) publish(n *notification, topicParam string) error {
|
||||||
authString = fmt.Sprintf(", headers.Authorization=Basic %s", auth)
|
authString = fmt.Sprintf(", headers.Authorization=Basic %s", auth)
|
||||||
}
|
}
|
||||||
|
|
||||||
actions = append(actions, fmt.Sprintf("http, Silence, %s, method=POST, body=%s%s", url, n.silenceBody, authString))
|
req.Header.Set("Actions", fmt.Sprintf("http, Silence, %s, method=POST, body=%s%s", url, n.silenceBody, authString))
|
||||||
}
|
}
|
||||||
|
|
||||||
if br.cfg.Ntfy.GeneratorURLLabel != "" && n.generatorURL != "" {
|
|
||||||
actions = append(actions, fmt.Sprintf("view, %s, %s", br.cfg.Ntfy.GeneratorURLLabel, n.generatorURL))
|
|
||||||
}
|
|
||||||
|
|
||||||
nActions := len(actions)
|
|
||||||
if nActions > maxNTFYActions {
|
|
||||||
br.logger.Warn(fmt.Sprintf("Publish: Too many actions (%d), ntfy only supports up to %d - removing surplus actions.", nActions, maxNTFYActions))
|
|
||||||
br.logger.Debug("Action list",
|
|
||||||
slog.Any("actions", actions))
|
|
||||||
actions = actions[:maxNTFYActions]
|
|
||||||
}
|
|
||||||
req.Header.Set("Actions", strings.Join(actions, ";"))
|
|
||||||
|
|
||||||
configFingerprint := br.cfg.Ntfy.CertFingerprint
|
configFingerprint := br.cfg.Ntfy.CertFingerprint
|
||||||
if configFingerprint != "" {
|
if configFingerprint != "" {
|
||||||
tlsCfg := &tls.Config{}
|
tlsCfg := &tls.Config{}
|
||||||
tlsCfg.VerifyPeerCertificate = func(rawCerts [][]byte, _ [][]*x509.Certificate) error {
|
tlsCfg.VerifyPeerCertificate = func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
|
||||||
for _, rawCert := range rawCerts {
|
for _, rawCert := range rawCerts {
|
||||||
hash := sha512.Sum512(rawCert)
|
hash := sha512.Sum512(rawCert)
|
||||||
if hex.EncodeToString(hash[:]) == configFingerprint {
|
if hex.EncodeToString(hash[:]) == configFingerprint {
|
||||||
|
@ -427,9 +386,14 @@ func (br *bridge) publish(n *notification, topicParam string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (br *bridge) handleWebhooks(w http.ResponseWriter, r *http.Request) {
|
func (br *bridge) handleWebhooks(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := r.Context()
|
|
||||||
logger := br.logger.With(slog.String("handler", "/"))
|
logger := br.logger.With(slog.String("handler", "/"))
|
||||||
|
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
http.Error(w, "Only POST allowed", http.StatusMethodNotAllowed)
|
||||||
|
logger.Debug(fmt.Sprintf("Illegal HTTP method: expected %q, got %q", "POST", r.Method))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
contentType := r.Header.Get("Content-Type")
|
contentType := r.Header.Get("Content-Type")
|
||||||
if contentType != "application/json" {
|
if contentType != "application/json" {
|
||||||
http.Error(w, "Only application/json allowed", http.StatusUnsupportedMediaType)
|
http.Error(w, "Only application/json allowed", http.StatusUnsupportedMediaType)
|
||||||
|
@ -437,8 +401,6 @@ func (br *bridge) handleWebhooks(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
topicParam := r.URL.Query().Get("topic")
|
|
||||||
|
|
||||||
var event payload
|
var event payload
|
||||||
if err := json.NewDecoder(r.Body).Decode(&event); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&event); err != nil {
|
||||||
http.Error(w, "Failed to parse payload", http.StatusInternalServerError)
|
http.Error(w, "Failed to parse payload", http.StatusInternalServerError)
|
||||||
|
@ -451,14 +413,14 @@ func (br *bridge) handleWebhooks(w http.ResponseWriter, r *http.Request) {
|
||||||
slog.Any("payload", event))
|
slog.Any("payload", event))
|
||||||
|
|
||||||
if br.cfg.AlertMode == config.Single {
|
if br.cfg.AlertMode == config.Single {
|
||||||
notifications := br.singleAlertNotifications(ctx, &event)
|
notifications := br.singleAlertNotifications(&event)
|
||||||
for _, n := range notifications {
|
for _, n := range notifications {
|
||||||
err := br.publish(n, topicParam)
|
err := br.publish(n)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("Failed to publish notification",
|
logger.Error("Failed to publish notification",
|
||||||
slog.String("error", err.Error()))
|
slog.String("error", err.Error()))
|
||||||
} else {
|
} else {
|
||||||
if err := br.cache.Set(ctx, n.fingerprint, n.status); err != nil {
|
if err := br.cache.Set(n.fingerprint, n.status); err != nil {
|
||||||
logger.Error("Failed to cache alert",
|
logger.Error("Failed to cache alert",
|
||||||
slog.String("fingerprint", n.fingerprint),
|
slog.String("fingerprint", n.fingerprint),
|
||||||
slog.String("error", err.Error()))
|
slog.String("error", err.Error()))
|
||||||
|
@ -467,7 +429,7 @@ func (br *bridge) handleWebhooks(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
notification := br.multiAlertNotification(&event)
|
notification := br.multiAlertNotification(&event)
|
||||||
err := br.publish(notification, topicParam)
|
err := br.publish(notification)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("Failed to publish notification",
|
logger.Error("Failed to publish notification",
|
||||||
slog.String("error", err.Error()))
|
slog.String("error", err.Error()))
|
||||||
|
@ -536,28 +498,14 @@ func main() {
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg, err := config.Read(configPath)
|
cfg, err := config.ReadConfig(configPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Failed to read config",
|
slog.Error("Failed to read config",
|
||||||
slog.String("error", err.Error()))
|
slog.String("error", err.Error()))
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
var logOutput io.Writer
|
logger, err := logging.New(cfg.LogLevel, cfg.LogFormat, os.Stderr)
|
||||||
if cfg.LogFile != "" {
|
|
||||||
f, err := os.OpenFile(cfg.LogFile, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0600)
|
|
||||||
if err != nil {
|
|
||||||
slog.Error("Failed to open logfile",
|
|
||||||
slog.String("error", err.Error()))
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
logOutput = f
|
|
||||||
} else {
|
|
||||||
logOutput = os.Stderr
|
|
||||||
}
|
|
||||||
|
|
||||||
logger, err := logging.New(cfg.LogLevel, cfg.LogFormat, logOutput)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Failed to create logger",
|
slog.Error("Failed to create logger",
|
||||||
slog.String("error", err.Error()))
|
slog.String("error", err.Error()))
|
||||||
|
@ -569,7 +517,7 @@ func main() {
|
||||||
|
|
||||||
client := &httpClient{&http.Client{Timeout: time.Second * 3}}
|
client := &httpClient{&http.Client{Timeout: time.Second * 3}}
|
||||||
|
|
||||||
c, err := cache.NewCache(ctx, cfg.Cache)
|
c, err := cache.NewCache(cfg.Cache)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("Failed to create cache",
|
logger.Error("Failed to create cache",
|
||||||
slog.String("error", err.Error()))
|
slog.String("error", err.Error()))
|
||||||
|
@ -580,8 +528,8 @@ func main() {
|
||||||
logger.Info(fmt.Sprintf("Listening on %s, ntfy-alertmanager %s", cfg.HTTPAddress, version))
|
logger.Info(fmt.Sprintf("Listening on %s, ntfy-alertmanager %s", cfg.HTTPAddress, version))
|
||||||
|
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
mux.HandleFunc("POST /", bridge.handleWebhooks)
|
mux.HandleFunc("/", bridge.handleWebhooks)
|
||||||
mux.HandleFunc("POST /silences", bridge.handleSilences)
|
mux.HandleFunc("/silences", bridge.handleSilences)
|
||||||
|
|
||||||
httpServer := &http.Server{
|
httpServer := &http.Server{
|
||||||
Addr: cfg.HTTPAddress,
|
Addr: cfg.HTTPAddress,
|
||||||
|
@ -619,26 +567,3 @@ func main() {
|
||||||
slog.String("error", err.Error()))
|
slog.String("error", err.Error()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (br *bridge) topicURL(topic string) (string, error) {
|
|
||||||
if topic == "" {
|
|
||||||
topic = br.cfg.Ntfy.Topic
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the configured topic name already contains the ntfy server
|
|
||||||
i := strings.Index(topic, "://")
|
|
||||||
if i != -1 {
|
|
||||||
return topic, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if br.cfg.Ntfy.Server == "" {
|
|
||||||
return "", errors.New("cannot set topic: no ntfy server set")
|
|
||||||
}
|
|
||||||
|
|
||||||
s, err := url.JoinPath(br.cfg.Ntfy.Server, topic)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return s, nil
|
|
||||||
}
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
@ -39,6 +40,12 @@ type silenceResponse struct {
|
||||||
func (br *bridge) handleSilences(w http.ResponseWriter, r *http.Request) {
|
func (br *bridge) handleSilences(w http.ResponseWriter, r *http.Request) {
|
||||||
logger := br.logger.With(slog.String("handler", "/silences"))
|
logger := br.logger.With(slog.String("handler", "/silences"))
|
||||||
|
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
http.Error(w, "Only POST allowed", http.StatusMethodNotAllowed)
|
||||||
|
logger.Debug(fmt.Sprintf("Illegal HTTP method: expected %q, got %q", "POST", r.Method))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
b, err := io.ReadAll(r.Body)
|
b, err := io.ReadAll(r.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("Failed to read body",
|
logger.Error("Failed to read body",
|
||||||
|
|
Loading…
Add table
Reference in a new issue