First commit
This commit is contained in:
commit
d0388111ae
6 changed files with 275 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
uptimetracker
|
||||
uptimetracker.log
|
||||
77
README.md
Normal file
77
README.md
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
# uptimetracker
|
||||
|
||||
A lightweight HTTP uptime monitor written in Go. Checks a list of URLs at a configurable interval, logs results, and sends Discord alerts on state changes.
|
||||
|
||||
## Features
|
||||
|
||||
- Checks multiple targets concurrently
|
||||
- Alerts on DOWN and recovery via Discord webhook (fires once per state change, no spam)
|
||||
- Logs to file and stdout simultaneously
|
||||
|
||||
## Configuration
|
||||
|
||||
Edit `config.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"interval_seconds": 30,
|
||||
"discord_webhook": "https://discord.com/api/webhooks/...",
|
||||
"log_file": "uptimetracker.log",
|
||||
"targets": [
|
||||
{ "name": "My Site", "url": "https://example.com" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Required | Description |
|
||||
|---|---|---|
|
||||
| `targets` | yes | List of sites to monitor |
|
||||
| `interval_seconds` | no | Check frequency, defaults to 60 |
|
||||
| `discord_webhook` | no | Discord webhook URL for alerts |
|
||||
| `log_file` | no | Log file path, defaults to `uptimetracker.log` |
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
go build -o uptimetracker .
|
||||
./uptimetracker # uses config.json in current directory
|
||||
./uptimetracker /path/to/config.json
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
2026-02-25 18:00:00 started monitoring 1 target(s) every 30s
|
||||
2026-02-25 18:00:00 My Site UP 200 45ms
|
||||
2026-02-25 18:00:30 My Site DOWN 503 120ms
|
||||
2026-02-25 18:00:30 alert sent: My Site is DOWN
|
||||
```
|
||||
|
||||
## Deploy with systemd
|
||||
|
||||
Build a Linux binary:
|
||||
|
||||
```bash
|
||||
GOOS=linux GOARCH=amd64 go build -o uptimetracker .
|
||||
```
|
||||
|
||||
Copy to your server:
|
||||
|
||||
```bash
|
||||
scp uptimetracker config.json uptimetracker.service user@your-vps:~/uptimetracker/
|
||||
```
|
||||
|
||||
Install and start the service:
|
||||
|
||||
```bash
|
||||
# Update User= and paths in the service file to match your username
|
||||
sudo cp ~/uptimetracker/uptimetracker.service /etc/systemd/system/
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable --now uptimetracker
|
||||
```
|
||||
|
||||
View logs:
|
||||
|
||||
```bash
|
||||
sudo journalctl -fu uptimetracker
|
||||
```
|
||||
7
config.json
Normal file
7
config.json
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"interval_seconds": 30,
|
||||
"discord_webhook": "https://discord.com/api/webhooks/1476280778215264387/CCnXB03_MgwYUUKWQcw2PKFihny8UyDk4knqdPmnS85McjIFdokVW6MlkhBgLcv0yAZv",
|
||||
"targets": [
|
||||
{ "name": "OpenIssue", "url": "https://openissue.io" }
|
||||
]
|
||||
}
|
||||
3
go.mod
Normal file
3
go.mod
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
module uptimetracker
|
||||
|
||||
go 1.24.1
|
||||
174
main.go
Normal file
174
main.go
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"text/tabwriter"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Target struct {
|
||||
Name string `json:"name"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
type Result struct {
|
||||
Target Target
|
||||
Up bool
|
||||
StatusCode int
|
||||
Latency time.Duration
|
||||
CheckedAt time.Time
|
||||
Err string
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Targets []Target `json:"targets"`
|
||||
Interval int `json:"interval_seconds"`
|
||||
DiscordWebhook string `json:"discord_webhook"`
|
||||
LogFile string `json:"log_file"`
|
||||
}
|
||||
|
||||
func check(t Target, timeout time.Duration) Result {
|
||||
r := Result{Target: t, CheckedAt: time.Now()}
|
||||
client := &http.Client{Timeout: timeout}
|
||||
start := time.Now()
|
||||
resp, err := client.Get(t.URL)
|
||||
r.Latency = time.Since(start)
|
||||
if err != nil {
|
||||
r.Err = err.Error()
|
||||
return r
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
r.StatusCode = resp.StatusCode
|
||||
r.Up = resp.StatusCode >= 200 && resp.StatusCode < 400
|
||||
return r
|
||||
}
|
||||
|
||||
func sendDiscordAlert(webhookURL, message string) {
|
||||
payload, _ := json.Marshal(map[string]string{"content": message})
|
||||
resp, err := http.Post(webhookURL, "application/json", bytes.NewReader(payload))
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "discord alert failed: %v\n", err)
|
||||
return
|
||||
}
|
||||
resp.Body.Close()
|
||||
}
|
||||
|
||||
func printResults(w io.Writer, results []Result) {
|
||||
tw := tabwriter.NewWriter(w, 0, 0, 2, ' ', 0)
|
||||
for _, r := range results {
|
||||
status := "DOWN"
|
||||
if r.Up {
|
||||
status = "UP"
|
||||
}
|
||||
code := "-"
|
||||
if r.StatusCode != 0 {
|
||||
code = fmt.Sprintf("%d", r.StatusCode)
|
||||
}
|
||||
fmt.Fprintf(tw, "%s\t%s\t%s\t%s\t%s\n",
|
||||
r.CheckedAt.Format("2006-01-02 15:04:05"),
|
||||
r.Target.Name,
|
||||
status,
|
||||
code,
|
||||
r.Latency.Round(time.Millisecond),
|
||||
)
|
||||
}
|
||||
tw.Flush()
|
||||
}
|
||||
|
||||
func loadConfig(path string) (Config, error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
defer f.Close()
|
||||
var cfg Config
|
||||
if err := json.NewDecoder(f).Decode(&cfg); err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
if cfg.Interval <= 0 {
|
||||
cfg.Interval = 60
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func runOnce(cfg Config, prevState map[string]bool, out io.Writer) {
|
||||
results := make([]Result, len(cfg.Targets))
|
||||
done := make(chan struct{}, len(cfg.Targets))
|
||||
for i, t := range cfg.Targets {
|
||||
go func(i int, t Target) {
|
||||
results[i] = check(t, 10*time.Second)
|
||||
done <- struct{}{}
|
||||
}(i, t)
|
||||
}
|
||||
for range cfg.Targets {
|
||||
<-done
|
||||
}
|
||||
|
||||
printResults(out, results)
|
||||
|
||||
if cfg.DiscordWebhook != "" {
|
||||
for _, r := range results {
|
||||
wasUp, seen := prevState[r.Target.URL]
|
||||
if !r.Up && (!seen || wasUp) {
|
||||
msg := fmt.Sprintf("@here **%s** is DOWN (`%s`)", r.Target.Name, r.Target.URL)
|
||||
if r.Err != "" {
|
||||
msg += fmt.Sprintf("\nError: %s", r.Err)
|
||||
} else {
|
||||
msg += fmt.Sprintf("\nHTTP %d", r.StatusCode)
|
||||
}
|
||||
fmt.Fprintf(out, "%s\talert sent: %s is DOWN\n",
|
||||
r.CheckedAt.Format("2006-01-02 15:04:05"), r.Target.Name)
|
||||
sendDiscordAlert(cfg.DiscordWebhook, msg)
|
||||
} else if r.Up && seen && !wasUp {
|
||||
msg := fmt.Sprintf("**%s** is back UP (`%s`)", r.Target.Name, r.Target.URL)
|
||||
fmt.Fprintf(out, "%s\talert sent: %s is back UP\n",
|
||||
r.CheckedAt.Format("2006-01-02 15:04:05"), r.Target.Name)
|
||||
sendDiscordAlert(cfg.DiscordWebhook, msg)
|
||||
}
|
||||
prevState[r.Target.URL] = r.Up
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
cfgPath := "config.json"
|
||||
if len(os.Args) > 1 {
|
||||
cfgPath = os.Args[1]
|
||||
}
|
||||
|
||||
cfg, err := loadConfig(cfgPath)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error loading config: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
logPath := cfg.LogFile
|
||||
if logPath == "" {
|
||||
logPath = "uptimetracker.log"
|
||||
}
|
||||
lf, err := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error opening log file: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer lf.Close()
|
||||
|
||||
out := io.MultiWriter(os.Stdout, lf)
|
||||
|
||||
fmt.Fprintf(out, "%s\tstarted monitoring %d target(s) every %ds\n",
|
||||
time.Now().Format("2006-01-02 15:04:05"), len(cfg.Targets), cfg.Interval)
|
||||
|
||||
prevState := make(map[string]bool)
|
||||
runOnce(cfg, prevState, out)
|
||||
|
||||
ticker := time.NewTicker(time.Duration(cfg.Interval) * time.Second)
|
||||
defer ticker.Stop()
|
||||
for range ticker.C {
|
||||
runOnce(cfg, prevState, out)
|
||||
}
|
||||
}
|
||||
12
uptimetracker.service
Normal file
12
uptimetracker.service
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
[Unit]
|
||||
Description=Uptime Tracker
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
ExecStart=/home/user/uptimetracker/uptimetracker /home/user/uptimetracker/config.json
|
||||
WorkingDirectory=/home/user/uptimetracker
|
||||
Restart=always
|
||||
User=user
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
Loading…
Reference in a new issue