├── 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 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 4 | [![Go Version](https://img.shields.io/badge/Go-1.24+-00ADD8?style=flat&logo=go)](https://golang.org/) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/seponik/uptime-watchdog)](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. --------------------------------------------------------------------------------