├── assets
├── output.png
└── slack.png
├── go.mod
├── .dockerignore
├── go.sum
├── Dockerfile
├── .gitignore
├── config.yaml
├── internal
├── util
│ ├── validator.go
│ ├── notifier_test.go
│ └── notifier.go
├── monitor
│ └── monitor.go
├── checker
│ ├── checker_test.go
│ └── checker.go
└── config
│ └── config.go
├── Makefile
├── LICENSE
├── cmd
└── uwdog
│ └── main.go
├── CONTRIBUTING.md
└── README.md
/assets/output.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/seponik/uptime-watchdog/HEAD/assets/output.png
--------------------------------------------------------------------------------
/assets/slack.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/seponik/uptime-watchdog/HEAD/assets/slack.png
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/seponik/uptime-watchdog
2 |
3 | go 1.24.5
4 |
5 | require gopkg.in/yaml.v3 v3.0.1 // indirect
6 |
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | config.yaml
2 |
3 | Dockerfile
4 | docker-compose.yml
5 |
6 | Makefile
7 |
8 | README.md
9 | CONTRIBUTING.md
10 | LICENSE
11 | .gitignore
12 |
13 | assets
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
2 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
3 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
4 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM golang:1.24-alpine AS builder
2 |
3 | WORKDIR /build
4 | COPY go.mod go.sum ./
5 | RUN go mod download
6 | COPY . .
7 | RUN CGO_ENABLED=0 go build -ldflags='-s -w' -o uwdog cmd/uwdog/main.go
8 |
9 | FROM alpine:latest
10 | COPY --from=builder /build/uwdog /uwdog/uwdog
11 | WORKDIR /uwdog
12 | ENTRYPOINT ["./uwdog"]
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Binaries for programs and plugins
2 | *.exe
3 | *.exe~
4 | *.dll
5 | *.so
6 | *.dylib
7 |
8 | # Test binary, built with `go test -c`
9 | *.test
10 |
11 | # Code coverage profiles and other test artifacts
12 | *.out
13 | coverage.*
14 | *.coverprofile
15 | profile.cov
16 |
17 | # Go workspace file
18 | go.work
19 | go.work.sum
20 |
21 | # env file
22 | .env
--------------------------------------------------------------------------------
/config.yaml:
--------------------------------------------------------------------------------
1 | webhook_url: https://hooks.slack.com/services/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
2 |
3 | endpoints:
4 | - name: Portfolio
5 | url: https://me.seponik.dev
6 | timeout: 5s
7 | interval: 1h
8 |
9 | - name: Example API
10 | url: https://api.example.com/health
11 | timeout: 5s
12 | interval: 30m
13 | expected_status: 200
14 |
15 | - name: Example Website
16 | url: https://www.example.com
17 | timeout: 10s
18 | interval: 1h
19 | expected_status: 200
--------------------------------------------------------------------------------
/internal/util/validator.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/seponik/uptime-watchdog/internal/config"
7 | )
8 |
9 | func ValidateConfig(cfg *config.Config) error {
10 | if cfg.WebhookURL == "" {
11 | return fmt.Errorf("webhook URL is not set in the configuration")
12 | }
13 |
14 | if len(cfg.Endpoints) == 0 {
15 | return fmt.Errorf("no endpoints configured to monitor")
16 | }
17 |
18 | for index, endpoint := range cfg.Endpoints {
19 | if endpoint.Name == "" {
20 | return fmt.Errorf("endpoint name is not set for URL: %d", index)
21 | }
22 |
23 | if endpoint.URL == "" {
24 | return fmt.Errorf("endpoint URL is not set for endpoint: %d", index)
25 | }
26 | }
27 |
28 | return nil
29 | }
30 |
--------------------------------------------------------------------------------
/internal/monitor/monitor.go:
--------------------------------------------------------------------------------
1 | package monitor
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/seponik/uptime-watchdog/internal/checker"
7 | "github.com/seponik/uptime-watchdog/internal/config"
8 | "github.com/seponik/uptime-watchdog/internal/util"
9 | )
10 |
11 | func Monitor(endpoint config.Endpoint, notifier util.Notifier) {
12 |
13 | if endpoint.Interval <= 0 {
14 | endpoint.Interval = config.Duration(time.Minute * 5) // Default to 5 min if not set
15 | }
16 |
17 | if endpoint.Timeout <= 0 {
18 | endpoint.Timeout = config.Duration(time.Second * 5) // Default to 5 seconds if not set
19 | }
20 |
21 | if endpoint.ExpectedStatus <= 0 {
22 | endpoint.ExpectedStatus = 200 // Default to 200 if not set
23 | }
24 |
25 | checker := checker.NewChecker(endpoint)
26 |
27 | for {
28 | result := checker.Check()
29 |
30 | notifier.ProcessResult(result)
31 |
32 | time.Sleep(time.Duration(endpoint.Interval))
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/internal/util/notifier_test.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import (
4 | "encoding/json"
5 | "net/http"
6 | "net/http/httptest"
7 | "testing"
8 |
9 | "github.com/seponik/uptime-watchdog/internal/config"
10 | )
11 |
12 | func newWebhookServer() *httptest.Server {
13 | handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
14 | var payload map[string]string
15 |
16 | json.NewDecoder(r.Body).Decode(&payload)
17 |
18 | if payload["text"] == "" {
19 | http.Error(w, "missing text", http.StatusBadRequest)
20 | return
21 | }
22 |
23 | w.WriteHeader(http.StatusOK)
24 | })
25 |
26 | return httptest.NewServer(handler)
27 | }
28 |
29 | func TestSendAlert(t *testing.T) {
30 | server := newWebhookServer()
31 | defer server.Close()
32 |
33 | endpoint := config.Endpoint{
34 | Name: "Test Endpoint",
35 | URL: "http://test.org",
36 | }
37 |
38 | err := sendAlert(server.URL, endpoint)
39 | if err != nil {
40 | t.Errorf("expected no error, got %v", err)
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | PROJECT := uwdog
2 |
3 | OS ?= linux
4 | ARCH ?= amd64
5 |
6 | PLATFORMS := \
7 | linux/amd64 \
8 | linux/arm64 \
9 | windows/amd64 \
10 | windows/arm64 \
11 | darwin/amd64 \
12 | darwin/arm64
13 |
14 |
15 | run:
16 | go run cmd/uwdog/main.go
17 |
18 | test:
19 | go test -cover ./...
20 |
21 | fmt:
22 | go fmt ./...
23 |
24 | clean:
25 | rm -rf build/
26 |
27 | build:
28 | @OUTPUT=build/$(OS)/$(ARCH)/$(PROJECT); \
29 | if [ "$(OS)" = "windows" ]; then OUTPUT="$$OUTPUT.exe"; fi; \
30 | echo "Building $$OUTPUT ..."; \
31 | CGO_ENABLED=0 GOOS=$(OS) GOARCH=$(ARCH) go build -ldflags='-s -w' -o $$OUTPUT cmd/uwdog/main.go
32 |
33 | build-all:
34 | @for platform in $(PLATFORMS); do \
35 | OS=$${platform%/*}; \
36 | ARCH=$${platform#*/}; \
37 | OUTPUT="build/$${OS}/$${ARCH}/$(PROJECT)"; \
38 | if [ "$$OS" = "windows" ]; then OUTPUT="$$OUTPUT.exe"; fi; \
39 | echo "Building $$OUTPUT ..."; \
40 | CGO_ENABLED=0 GOOS=$$OS GOARCH=$$ARCH go build -ldflags='-s -w' -o $$OUTPUT cmd/uwdog/main.go; \
41 | done
42 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 Sepehr Bostandoust
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/cmd/uwdog/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "flag"
5 | "fmt"
6 | "os"
7 | "sync"
8 |
9 | "github.com/seponik/uptime-watchdog/internal/config"
10 | "github.com/seponik/uptime-watchdog/internal/monitor"
11 | "github.com/seponik/uptime-watchdog/internal/util"
12 | )
13 |
14 | func main() {
15 | cfgPath := flag.String("config", "/uwdog/config.yaml", "Path to the configuration file")
16 | flag.Parse()
17 |
18 | cfg, err := config.Load(*cfgPath)
19 | if err != nil {
20 | fmt.Println("[ERR] Failed to find/read the config file.")
21 | os.Exit(1)
22 | }
23 |
24 | err = util.ValidateConfig(cfg)
25 | if err != nil {
26 | fmt.Printf("[ERR] Configuration validation failed: %v\n", err)
27 | os.Exit(1)
28 | }
29 |
30 | fmt.Println("[INFO] Starting uptime watchdog...")
31 |
32 | fmt.Println("[INFO] Loaded configuration successfully.")
33 |
34 | notifier := util.NewNotifier(cfg.WebhookURL)
35 |
36 | var wg sync.WaitGroup
37 |
38 | for _, endpoint := range cfg.Endpoints {
39 | wg.Add(1)
40 |
41 | go func(ep config.Endpoint, nt util.Notifier) {
42 | defer wg.Done()
43 |
44 | monitor.Monitor(ep, nt)
45 | }(endpoint, notifier)
46 | }
47 |
48 | wg.Wait()
49 | }
50 |
--------------------------------------------------------------------------------
/internal/checker/checker_test.go:
--------------------------------------------------------------------------------
1 | package checker
2 |
3 | import (
4 | "net/http"
5 | "net/http/httptest"
6 | "testing"
7 | "time"
8 |
9 | "github.com/seponik/uptime-watchdog/internal/config"
10 | )
11 |
12 | func newTestServer(status int, delay time.Duration) *httptest.Server {
13 | return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
14 | time.Sleep(delay)
15 | w.WriteHeader(status)
16 | }))
17 | }
18 |
19 | func TestChecker(t *testing.T) {
20 | upSrv := newTestServer(200, 50*time.Millisecond)
21 | defer upSrv.Close()
22 |
23 | downSrv := newTestServer(500, 3*time.Second)
24 | defer downSrv.Close()
25 |
26 | endpointUp := config.Endpoint{
27 | URL: upSrv.URL,
28 | Timeout: config.Duration(2 * time.Second),
29 | }
30 |
31 | endpointDown := config.Endpoint{
32 | URL: downSrv.URL,
33 | Timeout: config.Duration(2 * time.Second),
34 | }
35 |
36 | checkerUp := NewChecker(endpointUp)
37 | resultUp := checkerUp.Check()
38 |
39 | checkerDown := NewChecker(endpointDown)
40 | resultDown := checkerDown.Check()
41 |
42 | if resultUp.StatusCode != 200 || resultUp.Error != nil {
43 | t.Errorf("expected UP result, got %+v", resultUp)
44 | }
45 |
46 | if resultDown.Error == nil {
47 | t.Errorf("expected DOWN result, got %+v", resultDown)
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/internal/config/config.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "os"
5 | "time"
6 |
7 | "gopkg.in/yaml.v3"
8 | )
9 |
10 | // Endpoint represents a single URL endpoint to monitor.
11 | type Endpoint struct {
12 | Name string `yaml:"name"` // Name of the endpoint.
13 | URL string `yaml:"url"` // URL to monitor.
14 | Timeout Duration `yaml:"timeout"` // Timeout for the request.
15 | Interval Duration `yaml:"interval"` // Interval to check the endpoint.
16 | ExpectedStatus int `yaml:"expected_status"` // Expected HTTP status code.
17 | }
18 |
19 | // Config holds the configuration for the uptime watchdog.
20 | type Config struct {
21 | WebhookURL string `yaml:"webhook_url"` // Slack webhook url for notifications.
22 | Endpoints []Endpoint `yaml:"endpoints"` // List of target endpoints to monitor.
23 | }
24 |
25 | type Duration time.Duration
26 |
27 | // UnmarshalYAML implements the yaml.Unmarshaler interface for Duration parsing.
28 | func (d *Duration) UnmarshalYAML(value *yaml.Node) error {
29 | var durationStr string
30 |
31 | if err := value.Decode(&durationStr); err != nil {
32 | return err
33 | }
34 |
35 | duration, err := time.ParseDuration(durationStr)
36 | if err != nil {
37 | return err
38 | }
39 |
40 | *d = Duration(duration)
41 | return nil
42 | }
43 |
44 | // Load reads and parses the config file and returns Config.
45 | func Load(path string) (*Config, error) {
46 | data, err := os.ReadFile(path)
47 | if err != nil {
48 | return nil, err
49 | }
50 |
51 | var cfg Config
52 | err = yaml.Unmarshal(data, &cfg)
53 | if err != nil {
54 | return nil, err
55 | }
56 |
57 | return &cfg, nil
58 | }
59 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to Uptime Watchdog
2 |
3 | Thanks for your interest in contributing! 🚀
4 |
5 | ## 🚀 Quick Start
6 |
7 | 1. **Fork** this repository
8 |
9 | 2. **Clone** your fork: `git clone https://github.com/YOUR_USERNAME/uptime-watchdog.git`
10 |
11 | 3. **Create a branch**: `git checkout -b feature-your-feature`
12 |
13 | 4. **Make changes** and test them
14 |
15 | 5. **Submit a pull request**
16 |
17 | ## 🐛 Bug Reports
18 |
19 | - Check [existing issues](https://github.com/seponik/uptime-watchdog/issues) first
20 |
21 | - Use a clear title and description
22 |
23 | - Include steps to reproduce
24 |
25 | - Add your environment details (OS, Go version)
26 |
27 | ## 💡 Feature Requests
28 |
29 | - Open an [issue](https://github.com/seponik/uptime-watchdog/issues)
30 |
31 | - Describe the feature and why it's useful
32 |
33 | - Provide examples if possible
34 |
35 | ## 🛠️ Development
36 |
37 | ### Setup
38 |
39 | ```bash
40 | go mod download
41 | make test
42 | ```
43 |
44 | ### Running
45 |
46 | ```bash
47 | make run
48 | ```
49 |
50 | ### Testing
51 |
52 | ```bash
53 | make test
54 | ```
55 |
56 | ## 📝 Pull Requests
57 |
58 | ### Before submitting:
59 |
60 | - [ ] Tests pass: `make test`
61 | - [ ] Code is formatted: `make fmt`
62 | - [ ] Add tests for new features
63 | - [ ] Update documentation if needed
64 |
65 | ### Commit messages:
66 |
67 | ```
68 | feat: add Discord notifications
69 | fix: handle timeout errors
70 | docs: update README
71 | ```
72 |
73 | ## 🎨 Code Style
74 |
75 | - Follow standard Go conventions
76 |
77 | - Use `gofmt` to format code
78 |
79 | - Write clear variable names
80 |
81 | - Add comments for exported functions
82 |
83 | ## 📞 Need Help?
84 |
85 | - Open an [issue](https://github.com/seponik/uptime-watchdog/issues)
86 |
87 | ---
88 |
89 | **Every contribution matters!** ✨
--------------------------------------------------------------------------------
/internal/checker/checker.go:
--------------------------------------------------------------------------------
1 | package checker
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 | "strings"
7 | "time"
8 |
9 | "github.com/seponik/uptime-watchdog/internal/config"
10 | )
11 |
12 | type URLCheckResult struct {
13 | Endpoint config.Endpoint // The endpoint that was checked.
14 | StatusCode int // The HTTP status code received from the server.
15 | Delay time.Duration // The time it took to get a response.
16 | Error error // Any error that occurred during the check.
17 | }
18 |
19 | type Checker struct {
20 | endpoint config.Endpoint // The endpoint to check.
21 | client *http.Client // HTTP client used to send requests.
22 | }
23 |
24 | // NewChecker creates a new Checker instance for the given URL endpoint.
25 | func NewChecker(endpoint config.Endpoint) *Checker {
26 | return &Checker{
27 | endpoint: endpoint,
28 | client: &http.Client{
29 | Timeout: time.Duration(endpoint.Timeout),
30 | },
31 | }
32 | }
33 |
34 | // CheckURL sends a GET request to the endpoint's URL and returns the result.
35 | // Returns a URLCheckResult, which includes the Endpoint, status code, delay, and an error if something went wrong.
36 | func (c *Checker) Check() URLCheckResult {
37 | start := time.Now()
38 | response, err := c.client.Get(c.endpoint.URL)
39 | delay := time.Since(start)
40 |
41 | if err != nil {
42 | err = parseError(err, c.client.Timeout)
43 |
44 | return URLCheckResult{
45 | Endpoint: c.endpoint,
46 | Delay: delay,
47 | Error: err,
48 | }
49 | }
50 | defer response.Body.Close()
51 |
52 | return URLCheckResult{
53 | Endpoint: c.endpoint,
54 | StatusCode: response.StatusCode,
55 | Delay: delay,
56 | }
57 | }
58 |
59 | func parseError(err error, timeout time.Duration) error {
60 | errStr := err.Error()
61 |
62 | if strings.Contains(errStr, "deadline exceeded") {
63 | return fmt.Errorf("request timed out after %v", timeout)
64 | }
65 |
66 | if strings.Contains(errStr, "no such host") {
67 | return fmt.Errorf("host not found")
68 | }
69 |
70 | if strings.Contains(errStr, "connection refused") {
71 | return fmt.Errorf("connection refused by server")
72 | }
73 |
74 | return err
75 | }
76 |
--------------------------------------------------------------------------------
/internal/util/notifier.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "fmt"
7 | "log"
8 | "net/http"
9 | "time"
10 |
11 | "github.com/seponik/uptime-watchdog/internal/checker"
12 | "github.com/seponik/uptime-watchdog/internal/config"
13 | )
14 |
15 | type Notifier struct {
16 | webhookURL string // Slack webhook URL for notifications
17 | }
18 |
19 | // NewNotifier creates a new Notifier instance with the given webhook URL.
20 | func NewNotifier(webhookURL string) Notifier {
21 | return Notifier{webhookURL: webhookURL}
22 | }
23 |
24 | // ProcessResult processes the URL check result and sends an alert if necessary.
25 | func (n *Notifier) ProcessResult(result checker.URLCheckResult) {
26 | printResult(result)
27 | if result.Error != nil || result.StatusCode != result.Endpoint.ExpectedStatus {
28 | if err := sendAlert(n.webhookURL, result.Endpoint); err != nil {
29 | log.Printf("[ERR] Failed to send alert: %v", err)
30 | }
31 | }
32 | }
33 |
34 | // printResults logs each Endpoints's check result: UP if OK, DOWN if ERROR or Unexpected Status code.
35 | func printResult(result checker.URLCheckResult) {
36 | if result.Error != nil || result.StatusCode != result.Endpoint.ExpectedStatus {
37 | log.Printf("[DOWN] %-20s error: %-20v",
38 | result.Endpoint.Name,
39 | result.Error,
40 | )
41 |
42 | return
43 | }
44 |
45 | log.Printf("[UP] %-20s status: %-4d (%.2vms)",
46 | result.Endpoint.Name,
47 | result.StatusCode,
48 | result.Delay.Milliseconds(),
49 | )
50 | }
51 |
52 | // sendAlert sends a alert to the given slack webhook if a URL is down.
53 | // Returns an error if something went wrong.
54 | func sendAlert(webhookURL string, endpoint config.Endpoint) error {
55 | timestamp := time.Now().UTC().Format("2006-01-02 15:04:05")
56 | alertMessage := fmt.Sprintf("🚨 ALERT: %s is DOWN as of %s UTC.", endpoint.Name, timestamp)
57 |
58 | payload := map[string]string{"text": alertMessage}
59 |
60 | body, err := json.Marshal(payload)
61 | if err != nil {
62 | return fmt.Errorf("failed to marshal alert payload: %v", err)
63 | }
64 |
65 | resp, err := http.Post(webhookURL, "application/json", bytes.NewBuffer(body))
66 | if err != nil {
67 | return fmt.Errorf("failed to send alert: %v", err)
68 | }
69 | defer resp.Body.Close()
70 |
71 | if resp.StatusCode < 200 || resp.StatusCode >= 300 {
72 | return fmt.Errorf("unexpected status code from webhook: %d", resp.StatusCode)
73 | }
74 |
75 | return nil
76 | }
77 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 📈 Uptime Watchdog
2 |
3 | [](https://opensource.org/licenses/MIT)
4 | [](https://golang.org/)
5 | [](https://goreportcard.com/report/github.com/seponik/uptime-watchdog)
6 |
7 | A **lightweight**, **concurrent**, and **reliable** uptime monitoring tool built in Go. Monitor multiple endpoints simultaneously and get instant Slack notifications when services go down.
8 |
9 | ## ✨ Features
10 |
11 | - 🔄 **Concurrent Monitoring** - Check multiple URLs simultaneously for optimal performance
12 |
13 | - ⚡ **Lightning Fast** - Built with Go's goroutines for maximum efficiency
14 |
15 | - 📱 **Slack Integration** - Instant notifications to your team channels
16 |
17 | - 📋 **YAML Configuration** - Simple, human-readable configuration files
18 |
19 | - 🎯 **Minimal Resource Usage** - Designed to be lightweight and efficient
20 |
21 | - 📊 **Response Time Tracking** - Monitor performance metrics
22 |
23 | ## 🚀 Quick Start
24 |
25 | ### Installation
26 |
27 | 1. Go to the [Releases](https://github.com/seponik/uptime-watchdog/releases) section of this repository.
28 |
29 | 2. Download the latest release for your operating system.
30 |
31 | ### Configuration
32 |
33 | Create a `config.yaml` file:
34 |
35 | ```yaml
36 | # Uptime Watchdog Configuration
37 | webhook_url: https://hooks.slack.com/services/X/Y/Z
38 |
39 | endpoints:
40 | - name: Portfolio
41 | url: https://me.seponik.dev
42 | timeout: 5s
43 | interval: 1h
44 |
45 | - name: Example API
46 | url: https://api.example.com/health
47 | timeout: 5s
48 | interval: 30m
49 | expected_status: 200
50 |
51 | - name: Example Website
52 | url: https://www.example.com
53 | timeout: 10s
54 | interval: 1h
55 | expected_status: 200
56 | ```
57 |
58 | ### Usage
59 |
60 | ```bash
61 | ./uwdog -config YOUR_CONFIG.yaml
62 | ```
63 |
64 | ## 📊 Sample Output
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 | ## 🔧 Configuration Options
73 |
74 | ### Application Configuration
75 |
76 | | Parameter | Description | Required |
77 | |-----------|-------------|----------|
78 | | `webhook_url` | Slack webhook URL | ✅ |
79 |
80 | ### Endpoint Configuration
81 |
82 | | Parameter | Description | Default | Required |
83 | |-----------|-------------|---------|----------|
84 | | `name` | Friendly name for the endpoint | - | ✅ |
85 | | `url` | URL to monitor | - | ✅ |
86 | | `interval` | Check interval (e.g., "30s", "2m") | "5m" | ❌ |
87 | | `timeout` | Request timeout | "5s" | ❌ |
88 | | `expected_status` | Expected HTTP status code | 200 | ❌ |
89 |
90 |
91 |
92 | ## 🐳 Docker Support
93 |
94 | ```bash
95 | # Pull and run the latest version
96 | docker run -v $(pwd)/config.yaml:/uwdog/config.yaml seponik/uwdog:latest
97 | ```
98 |
99 | ### Docker Compose
100 |
101 | ```yaml
102 | version: '3.8'
103 | services:
104 | uptime-watchdog:
105 | image: seponik/uwdog:latest
106 | volumes:
107 | - ./config.yaml:/uwdog/config.yaml
108 | restart: unless-stopped
109 | ```
110 |
111 | ## 🏗️ Building from Source
112 |
113 | Clone the repository and build for your platform:
114 |
115 | ```bash
116 | git clone https://github.com/seponik/uptime-watchdog.git
117 | cd uptime-watchdog
118 | make build OS=linux ARCH=amd64
119 | ```
120 |
121 | ### Supported Platforms
122 |
123 | | OS | Architecture |
124 | |---|---|
125 | | `linux` | `amd64`, `arm64` |
126 | | `windows` | `amd64`, `arm64` |
127 | | `darwin` | `amd64`, `arm64` |
128 |
129 | ### Examples
130 |
131 | ```bash
132 | # Linux x64
133 | make build OS=linux ARCH=amd64
134 |
135 | # macOS Apple Silicon
136 | make build OS=darwin ARCH=arm64
137 |
138 | # Windows x64
139 | make build OS=windows ARCH=amd64
140 | ```
141 |
142 | ## 🤝 Contributing
143 |
144 | We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details.
145 | ## 📝 License
146 |
147 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
--------------------------------------------------------------------------------