Compare commits
2 Commits
5916e32ff3
...
feature/un
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f8d5a8f6cc | ||
|
|
73e01c7664 |
@@ -1,5 +1,7 @@
|
||||
# dnswatcher
|
||||
|
||||
> ⚠️ Pre-1.0 software. APIs, configuration, and behavior may change without notice.
|
||||
|
||||
dnswatcher is a production DNS and infrastructure monitoring daemon written in
|
||||
Go. It watches configured DNS domains and hostnames for changes, monitors TCP
|
||||
port availability, tracks TLS certificate expiry, and delivers real-time
|
||||
@@ -196,8 +198,6 @@ the following precedence (highest to lowest):
|
||||
| `DNSWATCHER_DEBUG` | Enable debug logging | `false` |
|
||||
| `DNSWATCHER_DATA_DIR` | Directory for state file | `./data` |
|
||||
| `DNSWATCHER_TARGETS` | Comma-separated DNS names (auto-classified via PSL) | `""` |
|
||||
| `DNSWATCHER_DOMAINS` | *(deprecated)* Comma-separated apex domains | `""` |
|
||||
| `DNSWATCHER_HOSTNAMES` | *(deprecated)* Comma-separated hostnames | `""` |
|
||||
| `DNSWATCHER_SLACK_WEBHOOK` | Slack incoming webhook URL | `""` |
|
||||
| `DNSWATCHER_MATTERMOST_WEBHOOK` | Mattermost incoming webhook URL | `""` |
|
||||
| `DNSWATCHER_NTFY_TOPIC` | ntfy topic URL | `""` |
|
||||
|
||||
@@ -90,8 +90,6 @@ func setupViper(name string) {
|
||||
viper.SetDefault("DEBUG", false)
|
||||
viper.SetDefault("DATA_DIR", "./data")
|
||||
viper.SetDefault("TARGETS", "")
|
||||
viper.SetDefault("DOMAINS", "")
|
||||
viper.SetDefault("HOSTNAMES", "")
|
||||
viper.SetDefault("SLACK_WEBHOOK", "")
|
||||
viper.SetDefault("MATTERMOST_WEBHOOK", "")
|
||||
viper.SetDefault("NTFY_TOPIC", "")
|
||||
@@ -134,7 +132,9 @@ func buildConfig(
|
||||
tlsInterval = defaultTLSInterval
|
||||
}
|
||||
|
||||
domains, hostnames, err := resolveTargets(log)
|
||||
domains, hostnames, err := ClassifyTargets(
|
||||
parseCSV(viper.GetString("TARGETS")),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid targets configuration: %w", err)
|
||||
}
|
||||
@@ -187,56 +187,6 @@ func configureDebugLogging(cfg *Config, params Params) {
|
||||
}
|
||||
}
|
||||
|
||||
// resolveTargets merges DNSWATCHER_TARGETS with the deprecated
|
||||
// DNSWATCHER_DOMAINS and DNSWATCHER_HOSTNAMES variables. When TARGETS
|
||||
// is set, names are automatically classified using the Public Suffix
|
||||
// List. Legacy variables are merged in and a deprecation warning is
|
||||
// logged when they are used.
|
||||
func resolveTargets(log *slog.Logger) ([]string, []string, error) {
|
||||
targets := parseCSV(viper.GetString("TARGETS"))
|
||||
legacyDomains := parseCSV(viper.GetString("DOMAINS"))
|
||||
legacyHostnames := parseCSV(viper.GetString("HOSTNAMES"))
|
||||
|
||||
if len(legacyDomains) > 0 || len(legacyHostnames) > 0 {
|
||||
log.Warn(
|
||||
"DNSWATCHER_DOMAINS and DNSWATCHER_HOSTNAMES are deprecated; use DNSWATCHER_TARGETS instead",
|
||||
)
|
||||
}
|
||||
|
||||
var domains, hostnames []string
|
||||
|
||||
if len(targets) > 0 {
|
||||
var err error
|
||||
|
||||
domains, hostnames, err = ClassifyTargets(targets)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
domains = mergeUnique(domains, legacyDomains)
|
||||
hostnames = mergeUnique(hostnames, legacyHostnames)
|
||||
|
||||
return domains, hostnames, nil
|
||||
}
|
||||
|
||||
// mergeUnique appends items from b into a, skipping duplicates.
|
||||
func mergeUnique(a, b []string) []string {
|
||||
seen := make(map[string]bool, len(a))
|
||||
for _, v := range a {
|
||||
seen[v] = true
|
||||
}
|
||||
|
||||
for _, v := range b {
|
||||
if !seen[v] {
|
||||
a = append(a, v)
|
||||
seen[v] = true
|
||||
}
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
// StatePath returns the full path to the state JSON file.
|
||||
func (c *Config) StatePath() string {
|
||||
return c.DataDir + "/state.json"
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"go.uber.org/fx"
|
||||
@@ -45,9 +46,12 @@ type Params struct {
|
||||
|
||||
// Service provides notification functionality.
|
||||
type Service struct {
|
||||
log *slog.Logger
|
||||
client *http.Client
|
||||
config *config.Config
|
||||
log *slog.Logger
|
||||
client *http.Client
|
||||
config *config.Config
|
||||
ntfyURL *url.URL
|
||||
slackWebhookURL *url.URL
|
||||
mattermostWebhookURL *url.URL
|
||||
}
|
||||
|
||||
// New creates a new notify Service.
|
||||
@@ -55,13 +59,44 @@ func New(
|
||||
_ fx.Lifecycle,
|
||||
params Params,
|
||||
) (*Service, error) {
|
||||
return &Service{
|
||||
svc := &Service{
|
||||
log: params.Logger.Get(),
|
||||
client: &http.Client{
|
||||
Timeout: httpClientTimeout,
|
||||
},
|
||||
config: params.Config,
|
||||
}, nil
|
||||
}
|
||||
|
||||
if params.Config.NtfyTopic != "" {
|
||||
u, err := url.ParseRequestURI(params.Config.NtfyTopic)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid ntfy topic URL: %w", err)
|
||||
}
|
||||
|
||||
svc.ntfyURL = u
|
||||
}
|
||||
|
||||
if params.Config.SlackWebhook != "" {
|
||||
u, err := url.ParseRequestURI(params.Config.SlackWebhook)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid slack webhook URL: %w", err)
|
||||
}
|
||||
|
||||
svc.slackWebhookURL = u
|
||||
}
|
||||
|
||||
if params.Config.MattermostWebhook != "" {
|
||||
u, err := url.ParseRequestURI(params.Config.MattermostWebhook)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(
|
||||
"invalid mattermost webhook URL: %w", err,
|
||||
)
|
||||
}
|
||||
|
||||
svc.mattermostWebhookURL = u
|
||||
}
|
||||
|
||||
return svc, nil
|
||||
}
|
||||
|
||||
// SendNotification sends a notification to all configured endpoints.
|
||||
@@ -69,13 +104,13 @@ func (svc *Service) SendNotification(
|
||||
ctx context.Context,
|
||||
title, message, priority string,
|
||||
) {
|
||||
if svc.config.NtfyTopic != "" {
|
||||
if svc.ntfyURL != nil {
|
||||
go func() {
|
||||
notifyCtx := context.WithoutCancel(ctx)
|
||||
|
||||
err := svc.sendNtfy(
|
||||
notifyCtx,
|
||||
svc.config.NtfyTopic,
|
||||
svc.ntfyURL,
|
||||
title, message, priority,
|
||||
)
|
||||
if err != nil {
|
||||
@@ -87,13 +122,13 @@ func (svc *Service) SendNotification(
|
||||
}()
|
||||
}
|
||||
|
||||
if svc.config.SlackWebhook != "" {
|
||||
if svc.slackWebhookURL != nil {
|
||||
go func() {
|
||||
notifyCtx := context.WithoutCancel(ctx)
|
||||
|
||||
err := svc.sendSlack(
|
||||
notifyCtx,
|
||||
svc.config.SlackWebhook,
|
||||
svc.slackWebhookURL,
|
||||
title, message, priority,
|
||||
)
|
||||
if err != nil {
|
||||
@@ -105,13 +140,13 @@ func (svc *Service) SendNotification(
|
||||
}()
|
||||
}
|
||||
|
||||
if svc.config.MattermostWebhook != "" {
|
||||
if svc.mattermostWebhookURL != nil {
|
||||
go func() {
|
||||
notifyCtx := context.WithoutCancel(ctx)
|
||||
|
||||
err := svc.sendSlack(
|
||||
notifyCtx,
|
||||
svc.config.MattermostWebhook,
|
||||
svc.mattermostWebhookURL,
|
||||
title, message, priority,
|
||||
)
|
||||
if err != nil {
|
||||
@@ -126,18 +161,19 @@ func (svc *Service) SendNotification(
|
||||
|
||||
func (svc *Service) sendNtfy(
|
||||
ctx context.Context,
|
||||
topic, title, message, priority string,
|
||||
topicURL *url.URL,
|
||||
title, message, priority string,
|
||||
) error {
|
||||
svc.log.Debug(
|
||||
"sending ntfy notification",
|
||||
"topic", topic,
|
||||
"topic", topicURL.String(),
|
||||
"title", title,
|
||||
)
|
||||
|
||||
request, err := http.NewRequestWithContext(
|
||||
ctx,
|
||||
http.MethodPost,
|
||||
topic,
|
||||
topicURL.String(),
|
||||
bytes.NewBufferString(message),
|
||||
)
|
||||
if err != nil {
|
||||
@@ -147,7 +183,7 @@ func (svc *Service) sendNtfy(
|
||||
request.Header.Set("Title", title)
|
||||
request.Header.Set("Priority", ntfyPriority(priority))
|
||||
|
||||
resp, err := svc.client.Do(request)
|
||||
resp, err := svc.client.Do(request) //nolint:gosec // URL validated at Service construction time
|
||||
if err != nil {
|
||||
return fmt.Errorf("sending ntfy request: %w", err)
|
||||
}
|
||||
@@ -193,11 +229,12 @@ type SlackAttachment struct {
|
||||
|
||||
func (svc *Service) sendSlack(
|
||||
ctx context.Context,
|
||||
webhookURL, title, message, priority string,
|
||||
webhookURL *url.URL,
|
||||
title, message, priority string,
|
||||
) error {
|
||||
svc.log.Debug(
|
||||
"sending webhook notification",
|
||||
"url", webhookURL,
|
||||
"url", webhookURL.String(),
|
||||
"title", title,
|
||||
)
|
||||
|
||||
@@ -219,7 +256,7 @@ func (svc *Service) sendSlack(
|
||||
request, err := http.NewRequestWithContext(
|
||||
ctx,
|
||||
http.MethodPost,
|
||||
webhookURL,
|
||||
webhookURL.String(),
|
||||
bytes.NewBuffer(body),
|
||||
)
|
||||
if err != nil {
|
||||
@@ -228,7 +265,7 @@ func (svc *Service) sendSlack(
|
||||
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := svc.client.Do(request)
|
||||
resp, err := svc.client.Do(request) //nolint:gosec // URL validated at Service construction time
|
||||
if err != nil {
|
||||
return fmt.Errorf("sending webhook request: %w", err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user