Compare commits
3 Commits
fix/empty-
...
e8cd0705a7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e8cd0705a7 | ||
| 0a74971ade | |||
| e882e7d237 |
14
README.md
14
README.md
@@ -52,10 +52,6 @@ without requiring an external database.
|
|||||||
responding again.
|
responding again.
|
||||||
- **Inconsistency detected**: Two nameservers that previously agreed
|
- **Inconsistency detected**: Two nameservers that previously agreed
|
||||||
now return different record sets for the same hostname.
|
now return different record sets for the same hostname.
|
||||||
- **Inconsistency resolved**: Nameservers that previously disagreed
|
|
||||||
are now back in agreement.
|
|
||||||
- **Empty response**: A nameserver that previously returned records
|
|
||||||
now returns an authoritative empty response (NODATA/NXDOMAIN).
|
|
||||||
|
|
||||||
### TCP Port Monitoring
|
### TCP Port Monitoring
|
||||||
|
|
||||||
@@ -136,8 +132,6 @@ dnswatcher exposes a lightweight HTTP API for operational visibility:
|
|||||||
|---------------------------------------|--------------------------------|
|
|---------------------------------------|--------------------------------|
|
||||||
| `GET /health` | Health check (JSON) |
|
| `GET /health` | Health check (JSON) |
|
||||||
| `GET /api/v1/status` | Current monitoring state |
|
| `GET /api/v1/status` | Current monitoring state |
|
||||||
| `GET /api/v1/domains` | Configured domains and status |
|
|
||||||
| `GET /api/v1/hostnames` | Configured hostnames and status|
|
|
||||||
| `GET /metrics` | Prometheus metrics (optional) |
|
| `GET /metrics` | Prometheus metrics (optional) |
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -210,6 +204,12 @@ the following precedence (highest to lowest):
|
|||||||
| `DNSWATCHER_METRICS_USERNAME` | Basic auth username for /metrics | `""` |
|
| `DNSWATCHER_METRICS_USERNAME` | Basic auth username for /metrics | `""` |
|
||||||
| `DNSWATCHER_METRICS_PASSWORD` | Basic auth password for /metrics | `""` |
|
| `DNSWATCHER_METRICS_PASSWORD` | Basic auth password for /metrics | `""` |
|
||||||
|
|
||||||
|
**`DNSWATCHER_TARGETS` is required.** dnswatcher will refuse to start if no
|
||||||
|
monitoring targets are configured. A monitoring daemon with nothing to monitor
|
||||||
|
is a misconfiguration, so dnswatcher fails fast with a clear error message
|
||||||
|
rather than running silently. Set `DNSWATCHER_TARGETS` to a comma-separated
|
||||||
|
list of DNS names before starting.
|
||||||
|
|
||||||
### Example `.env`
|
### Example `.env`
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
@@ -319,8 +319,6 @@ tracks reachability:
|
|||||||
|-------------|-------------------------------------------------|
|
|-------------|-------------------------------------------------|
|
||||||
| `ok` | Query succeeded, records are current |
|
| `ok` | Query succeeded, records are current |
|
||||||
| `error` | Query failed (timeout, SERVFAIL, network error) |
|
| `error` | Query failed (timeout, SERVFAIL, network error) |
|
||||||
| `nxdomain` | Authoritative NXDOMAIN response |
|
|
||||||
| `nodata` | Authoritative empty response (NODATA) |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,11 @@ const (
|
|||||||
defaultTLSExpiryWarning = 7
|
defaultTLSExpiryWarning = 7
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ErrNoTargets is returned when no monitoring targets are configured.
|
||||||
|
var ErrNoTargets = errors.New(
|
||||||
|
"no monitoring targets configured: set DNSWATCHER_TARGETS environment variable",
|
||||||
|
)
|
||||||
|
|
||||||
// Params contains dependencies for Config.
|
// Params contains dependencies for Config.
|
||||||
type Params struct {
|
type Params struct {
|
||||||
fx.In
|
fx.In
|
||||||
@@ -132,11 +137,9 @@ func buildConfig(
|
|||||||
tlsInterval = defaultTLSInterval
|
tlsInterval = defaultTLSInterval
|
||||||
}
|
}
|
||||||
|
|
||||||
domains, hostnames, err := ClassifyTargets(
|
domains, hostnames, err := parseAndValidateTargets()
|
||||||
parseCSV(viper.GetString("TARGETS")),
|
|
||||||
)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("invalid targets configuration: %w", err)
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg := &Config{
|
cfg := &Config{
|
||||||
@@ -162,6 +165,23 @@ func buildConfig(
|
|||||||
return cfg, nil
|
return cfg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseAndValidateTargets() ([]string, []string, error) {
|
||||||
|
domains, hostnames, err := ClassifyTargets(
|
||||||
|
parseCSV(viper.GetString("TARGETS")),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf(
|
||||||
|
"invalid targets configuration: %w", err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(domains) == 0 && len(hostnames) == 0 {
|
||||||
|
return nil, nil, ErrNoTargets
|
||||||
|
}
|
||||||
|
|
||||||
|
return domains, hostnames, nil
|
||||||
|
}
|
||||||
|
|
||||||
func parseCSV(input string) []string {
|
func parseCSV(input string) []string {
|
||||||
if input == "" {
|
if input == "" {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
1130
internal/notify/delivery_test.go
Normal file
1130
internal/notify/delivery_test.go
Normal file
File diff suppressed because it is too large
Load Diff
75
internal/notify/export_test.go
Normal file
75
internal/notify/export_test.go
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
package notify
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NtfyPriority exports ntfyPriority for testing.
|
||||||
|
func NtfyPriority(priority string) string {
|
||||||
|
return ntfyPriority(priority)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SlackColor exports slackColor for testing.
|
||||||
|
func SlackColor(priority string) string {
|
||||||
|
return slackColor(priority)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRequestForTest exports newRequest for testing.
|
||||||
|
func NewRequestForTest(
|
||||||
|
ctx context.Context,
|
||||||
|
method string,
|
||||||
|
target *url.URL,
|
||||||
|
body io.Reader,
|
||||||
|
) *http.Request {
|
||||||
|
return newRequest(ctx, method, target, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTestService creates a Service suitable for unit testing.
|
||||||
|
// It discards log output and uses the given transport.
|
||||||
|
func NewTestService(transport http.RoundTripper) *Service {
|
||||||
|
return &Service{
|
||||||
|
log: slog.New(slog.DiscardHandler),
|
||||||
|
transport: transport,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetNtfyURL sets the ntfy URL on a Service for testing.
|
||||||
|
func (svc *Service) SetNtfyURL(u *url.URL) {
|
||||||
|
svc.ntfyURL = u
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetSlackWebhookURL sets the Slack webhook URL on a
|
||||||
|
// Service for testing.
|
||||||
|
func (svc *Service) SetSlackWebhookURL(u *url.URL) {
|
||||||
|
svc.slackWebhookURL = u
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetMattermostWebhookURL sets the Mattermost webhook URL on
|
||||||
|
// a Service for testing.
|
||||||
|
func (svc *Service) SetMattermostWebhookURL(u *url.URL) {
|
||||||
|
svc.mattermostWebhookURL = u
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendNtfy exports sendNtfy for testing.
|
||||||
|
func (svc *Service) SendNtfy(
|
||||||
|
ctx context.Context,
|
||||||
|
topicURL *url.URL,
|
||||||
|
title, message, priority string,
|
||||||
|
) error {
|
||||||
|
return svc.sendNtfy(ctx, topicURL, title, message, priority)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendSlack exports sendSlack for testing.
|
||||||
|
func (svc *Service) SendSlack(
|
||||||
|
ctx context.Context,
|
||||||
|
webhookURL *url.URL,
|
||||||
|
title, message, priority string,
|
||||||
|
) error {
|
||||||
|
return svc.sendSlack(
|
||||||
|
ctx, webhookURL, title, message, priority,
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user