Skip to content

Commit

Permalink
Merge branch 'main' into fix-linting-errors
Browse files Browse the repository at this point in the history
  • Loading branch information
whiskeyjimbo committed Jan 14, 2025
2 parents d0a172e + 95b97ac commit 515f27f
Show file tree
Hide file tree
Showing 9 changed files with 271 additions and 142 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ DISCLAIMER: This is a personal project and is not meant to be used in a producti
- Flexible notification system
- Service tagging system
- TLS certificate expiration monitoring
- Extensible design for easy protocol additions

### High Availability Monitoring

Expand Down
123 changes: 118 additions & 5 deletions internal/checkers/checker.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,17 @@ package checkers

import (
"context"
"errors"
"sync"
"time"
)

const (
GlobalMinTimeout = 2 * time.Second
GlobalMaxTimeout = 20 * time.Second
GlobalDefaultTimeout = 10 * time.Second
)

type CheckResult struct {
Error error
Metadata map[string]interface{}
Expand All @@ -32,18 +40,43 @@ type HostCheckResult struct {
CheckResult
}

// Checker defines the interface for all protocol checkers
type Checker interface {
Protocol() Protocol
Check(ctx context.Context, hosts []string, port string) []HostCheckResult
GetTimeout() time.Duration
SetTimeout(timeout time.Duration) error
}

type TimeoutBounds struct {
Min time.Duration
Max time.Duration
Default time.Duration
}

// BaseChecker provides common functionality for all checkers
type BaseChecker struct {
timeout time.Duration
bounds TimeoutBounds
mu sync.RWMutex
}

func NewBaseChecker(bounds TimeoutBounds) BaseChecker {
if bounds.Min == 0 {
bounds.Min = GlobalMinTimeout
}
if bounds.Max == 0 {
bounds.Max = GlobalMaxTimeout
}
if bounds.Default == 0 {
bounds.Default = GlobalDefaultTimeout
}

return BaseChecker{
timeout: bounds.Default,
bounds: bounds,
}
}

func (b *BaseChecker) checkHost(ctx context.Context, host string, checkFn func() error) HostCheckResult {
func (b *BaseChecker) checkHost(ctx context.Context, host string, checkFn func() (map[string]interface{}, error)) HostCheckResult {
start := time.Now()
result := HostCheckResult{
Host: host,
Expand All @@ -58,11 +91,91 @@ func (b *BaseChecker) checkHost(ctx context.Context, host string, checkFn func()
return result
}

if err := checkFn(); err != nil {
metadata, err := checkFn()
if err != nil {
result.Error = err
result.Success = false
}

result.Metadata = metadata
result.ResponseTime = time.Since(start)
return result
}

func (b *BaseChecker) GetTimeout() time.Duration {
b.mu.RLock()
defer b.mu.RUnlock()
return b.timeout
}

func (b *BaseChecker) SetTimeout(timeout time.Duration) error {
b.mu.Lock()
defer b.mu.Unlock()
validTimeout, err := b.ValidateTimeout(timeout)
if err != nil {
return err
}
b.timeout = validTimeout
return nil
}

func (b *BaseChecker) GetTimeoutBounds() TimeoutBounds {
return b.bounds
}

func (b *BaseChecker) ValidateTimeout(timeout time.Duration) (time.Duration, error) {
if timeout == 0 {
return b.bounds.Default, nil
}
if timeout < b.bounds.Min {
return b.bounds.Min, errors.New("timeout is less than the minimum allowed")
}
if timeout > b.bounds.Max {
return b.bounds.Max, errors.New("timeout is greater than the maximum allowed")
}
return timeout, nil
}

func (b *BaseChecker) Check(ctx context.Context, hosts []string, port string, checkFn func(ctx context.Context, host string, port string) (map[string]interface{}, error)) []HostCheckResult {
return b.parallelCheck(ctx, hosts, port, checkFn)
}

func (b *BaseChecker) parallelCheck(ctx context.Context, hosts []string, port string, checkFn func(ctx context.Context, host string, port string) (map[string]interface{}, error)) []HostCheckResult {
checkCtx, cancel := context.WithTimeout(ctx, b.GetTimeout())
defer cancel()

results := make([]HostCheckResult, len(hosts))
for i, host := range hosts {
results[i].Host = host
}

var wg sync.WaitGroup
wg.Add(len(hosts))

for i, host := range hosts {
go func(index int, host string) {
defer wg.Done()
results[index] = b.checkHost(checkCtx, host, func() (map[string]interface{}, error) {
return checkFn(checkCtx, host, port)
})
}(i, host)
}

wg.Wait()
return results
}

type CheckError struct {
err error
metadata map[string]interface{}
}

func (e *CheckError) Error() string {
if e.err != nil {
return e.err.Error()
}
return ""
}

func (e *CheckError) Metadata() map[string]interface{} {
return e.metadata
}
54 changes: 25 additions & 29 deletions internal/checkers/dns.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,26 +17,31 @@ package checkers

import (
"context"
"errors"
"fmt"
"net"
"sync"
"time"
)

const (
defaultDNSTimeout = 5 * time.Second
dnsMinTimeout = 500 * time.Millisecond
dnsMaxTimeout = 5 * time.Second
dnsDefaultTimeout = 2 * time.Second
)

type DNSChecker struct {
BaseChecker
resolver *net.Resolver
mu sync.RWMutex
}

func NewDNSChecker() *DNSChecker {
return &DNSChecker{
BaseChecker: BaseChecker{
timeout: defaultDNSTimeout,
},
BaseChecker: NewBaseChecker(TimeoutBounds{
Min: dnsMinTimeout,
Max: dnsMaxTimeout,
Default: dnsDefaultTimeout,
}),
resolver: net.DefaultResolver,
}
}
Expand All @@ -45,35 +50,26 @@ func (c *DNSChecker) Protocol() Protocol {
return "DNS"
}

func (c *DNSChecker) Check(ctx context.Context, hosts []string, _ string) []HostCheckResult {
results := make([]HostCheckResult, 0, len(hosts))
func (c *DNSChecker) Check(ctx context.Context, hosts []string, port string) []HostCheckResult {
return c.BaseChecker.Check(ctx, hosts, port, c.checkDNS)
}

for _, host := range hosts {
var ips []net.IP
result := c.checkHost(ctx, host, func() error {
ctx, cancel := context.WithTimeout(ctx, c.timeout)
defer cancel()
func (c *DNSChecker) checkDNS(ctx context.Context, host string, port string) (map[string]interface{}, error) {
lookupCtx, cancel := context.WithTimeout(ctx, c.GetTimeout())
defer cancel()

var err error
ips, err = c.resolver.LookupIP(ctx, "ip4", host)
if err != nil {
return fmt.Errorf("dns lookup failed: %w", err)
}
ips, err := c.resolver.LookupIP(lookupCtx, "ip4", host)
if err != nil {
return nil, fmt.Errorf("dns lookup failed: %w", err)
}

if len(ips) == 0 {
return errors.New("no IP addresses found for host")
}
return nil
})
if len(ips) > 0 {
result.Metadata = map[string]interface{}{
"ips": ips,
}
}
results = append(results, result)
if len(ips) == 0 {
return nil, fmt.Errorf("no IP addresses found for host")
}

return results
return map[string]interface{}{
"ips": ips,
}, nil
}

func init() {
Expand Down
51 changes: 25 additions & 26 deletions internal/checkers/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ import (
)

const (
defaultHTTPTimeout = 10 * time.Second
httpMinTimeout = 2 * time.Second
httpMaxTimeout = 20 * time.Second
httpDefaultTimeout = 10 * time.Second
)

type HTTPChecker struct {
Expand All @@ -33,11 +35,13 @@ type HTTPChecker struct {

func NewHTTPChecker() *HTTPChecker {
return &HTTPChecker{
BaseChecker: BaseChecker{
timeout: defaultHTTPTimeout,
},
BaseChecker: NewBaseChecker(TimeoutBounds{
Min: httpMinTimeout,
Max: httpMaxTimeout,
Default: httpDefaultTimeout,
}),
client: &http.Client{
Timeout: defaultHTTPTimeout,
Timeout: httpDefaultTimeout,
},
}
}
Expand All @@ -47,31 +51,26 @@ func (c *HTTPChecker) Protocol() Protocol {
}

func (c *HTTPChecker) Check(ctx context.Context, hosts []string, port string) []HostCheckResult {
results := make([]HostCheckResult, 0, len(hosts))

for _, host := range hosts {
url := fmt.Sprintf("http://%s:%s", host, port)
result := c.checkHost(ctx, host, func() error {
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}
return c.BaseChecker.Check(ctx, hosts, port, c.checkHTTP)
}

resp, err := c.client.Do(req)
if err != nil {
return fmt.Errorf("http request failed: %w", err)
}
defer resp.Body.Close()
func (c *HTTPChecker) checkHTTP(ctx context.Context, host string, port string) (map[string]interface{}, error) {
url := fmt.Sprintf("http://%s:%s", host, port)
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}

if resp.StatusCode >= 400 {
return fmt.Errorf("http status error: %d", resp.StatusCode)
}
return nil
})
results = append(results, result)
resp, err := c.client.Do(req)
if err != nil {
return nil, fmt.Errorf("http request failed: %w", err)
}
defer resp.Body.Close()

return results
if resp.StatusCode >= 400 {
return nil, fmt.Errorf("http status error: %d", resp.StatusCode)
}
return nil, nil
}

func init() {
Expand Down
Loading

0 comments on commit 515f27f

Please sign in to comment.