├── incidents.html
├── .dockerignore
├── .gitignore
├── .github
├── FUNDING.yml
└── dependabot.yml
├── go.mod
├── .deepsource.toml
├── Dockerfile
├── go.sum
├── checks.yaml
├── env.go
├── README.md
├── tmpl.go
├── main.go
└── LICENSE
/incidents.html:
--------------------------------------------------------------------------------
1 |
157 | {{ range $service, $entries := .history }}
158 |
159 |
{{ $service }}
160 | {{ range $entry := $entries }}
161 |
162 | {{ index (split $entry.Timestamp "T") 0 }} {{ slice (index (split $entry.Timestamp "T") 1) 0 8 }}
163 |
164 | {{ if $entry.Status }}Up{{ else }}Down{{ end }}
165 |
166 |
167 | {{ end }}
168 |
169 | {{ end }}
170 |
171 |
176 |
177 | `
178 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "fmt"
7 | "html/template"
8 | "log"
9 | "net"
10 | "net/http"
11 | "net/url"
12 | "os"
13 | "os/exec"
14 | "runtime"
15 | "sort"
16 | "strings"
17 | "sync"
18 | "time"
19 | )
20 |
21 | type Group struct {
22 | Title string `yaml:"title"`
23 | Checks []Check `yaml:"checks"`
24 | }
25 |
26 | type Check struct {
27 | Name string `yaml:"name"`
28 | Type string `yaml:"type"`
29 | Host string `yaml:"host"`
30 | Address string `yaml:"address"`
31 | Port int `yaml:"port"`
32 | ExpectedCode int `yaml:"expected_code"`
33 | }
34 |
35 | type HistoryEntry struct {
36 | Timestamp string `json:"timestamp"`
37 | Status bool `json:"status"`
38 | }
39 |
40 | type GroupCheckResult struct {
41 | Title string
42 | CheckResults []CheckResult
43 | }
44 |
45 | type CheckResult struct {
46 | Name string
47 | Status bool
48 | }
49 |
50 | func checkHTTP(url string, expectedCode int) bool {
51 | client := &http.Client{Timeout: time.Second * 5}
52 | resp, err := client.Get(url)
53 | if err != nil {
54 | return false
55 | }
56 | _ = resp.Body.Close()
57 | return resp.StatusCode == expectedCode
58 | }
59 |
60 | func pingIPv6(address string) bool {
61 | var cmd *exec.Cmd
62 | switch runtime.GOOS {
63 | case "windows":
64 | cmd = exec.Command("ping", "-n", "1", "-w", "5000", address)
65 | case "darwin":
66 | cmd = exec.Command("ping", "-c", "1", "-W", "5", address)
67 | default:
68 | cmd = exec.Command("ping", "-6", "-c", "1", "-W", "5", address)
69 | }
70 | return cmd.Run() == nil
71 | }
72 |
73 | func checkPing(host string) bool {
74 | var cmd *exec.Cmd
75 | switch runtime.GOOS {
76 | case "windows":
77 | cmd = exec.Command("ping", "-n", "1", "-w", "5000", host)
78 | default:
79 | cmd = exec.Command("ping", "-c", "1", "-W", "5", host)
80 | }
81 | return cmd.Run() == nil
82 | }
83 |
84 | func checkPort(host string, port int) bool {
85 | conn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%d", host, port), 5*time.Second)
86 | if err != nil {
87 | return false
88 | }
89 | _ = conn.Close()
90 | return true
91 | }
92 |
93 | func runChecks(groups []Group) []GroupCheckResult {
94 | numGroups := len(groups)
95 | results := make([]GroupCheckResult, numGroups)
96 | wg := sync.WaitGroup{}
97 | for idx, group := range groups {
98 | wg.Add(1)
99 | go func(g Group) {
100 | defer wg.Done()
101 | results[idx] = checkGroup(g)
102 | }(group)
103 | }
104 | wg.Wait()
105 | return results
106 | }
107 |
108 | func checkGroup(g Group) GroupCheckResult {
109 | numResults := len(g.Checks)
110 | checkResults := make([]CheckResult, numResults)
111 | wg := sync.WaitGroup{}
112 | for idx, check := range g.Checks {
113 | wg.Add(1)
114 | go func(c Check) {
115 | defer wg.Done()
116 | var status bool
117 | switch c.Type {
118 | case "http":
119 | status = checkHTTP(c.Host, c.ExpectedCode)
120 | case "ping":
121 | status = checkPing(c.Host)
122 | case "Port":
123 | status = checkPort(c.Host, c.Port)
124 | case "ipv6":
125 | status = pingIPv6(c.Address)
126 | }
127 | checkResults[idx] = CheckResult{c.Name, status}
128 | }(check)
129 | }
130 | wg.Wait()
131 | return GroupCheckResult{g.Title, checkResults}
132 | }
133 |
134 | func (c *Config) loadHistory() map[string][]HistoryEntry {
135 | file, err := os.Open(c.HistoryFile)
136 | if err != nil {
137 | return map[string][]HistoryEntry{}
138 | }
139 | defer func(file *os.File) {
140 | _ = file.Close()
141 | }(file)
142 | var history map[string][]HistoryEntry
143 | _ = json.NewDecoder(file).Decode(&history)
144 | if history == nil {
145 | history = make(map[string][]HistoryEntry)
146 | }
147 | return history
148 | }
149 |
150 | func (c *Config) saveHistory(history map[string][]HistoryEntry) {
151 | file, err := os.Create(c.HistoryFile)
152 | if err != nil {
153 | log.Println("Failed to save history:", err)
154 | return
155 | }
156 | defer func(file *os.File) {
157 | _ = file.Close()
158 | }(file)
159 | _ = json.NewEncoder(file).Encode(history)
160 | }
161 |
162 | func (c *Config) updateHistory(results []GroupCheckResult) {
163 | history := c.loadHistory()
164 | currentTime := time.Now().Format(time.RFC3339)
165 | for _, group := range results {
166 | for _, result := range group.CheckResults {
167 | name := result.Name
168 | if _, exists := history[name]; !exists {
169 | history[name] = []HistoryEntry{}
170 | }
171 | history[name] = append(history[name], HistoryEntry{currentTime, result.Status})
172 | sort.Slice(history[name], func(i, j int) bool {
173 | timeI, _ := time.Parse(time.RFC3339, history[name][i].Timestamp)
174 | timeJ, _ := time.Parse(time.RFC3339, history[name][j].Timestamp)
175 | return timeI.After(timeJ)
176 | })
177 | if len(history[name]) > c.MaxHistoryEntries {
178 | history[name] = history[name][:c.MaxHistoryEntries]
179 | }
180 | }
181 | }
182 | c.saveHistory(history)
183 | }
184 |
185 | func renderTemplate(data map[string]interface{}) string {
186 | tmpl, err := template.New("status").Parse(templateFile)
187 | if err != nil {
188 | log.Fatal(err)
189 | }
190 | var buf bytes.Buffer
191 | if err = tmpl.Execute(&buf, data); err != nil {
192 | log.Fatal(err)
193 | }
194 | return buf.String()
195 | }
196 |
197 | func (c *Config) generateHistoryPage() {
198 | history := c.loadHistory()
199 | tmpl, err := template.New("history").Funcs(template.FuncMap{
200 | "split": func(s, sep string) []string {
201 | return strings.Split(s, sep)
202 | },
203 | }).Parse(historyTemplateFile)
204 | if err != nil {
205 | log.Fatal("Failed to parse history template:", err)
206 | }
207 | data := map[string]interface{}{
208 | "history": history,
209 | "last_updated": time.Now().Format("2006-01-02 15:04:05"),
210 | }
211 | var buf bytes.Buffer
212 | if err = tmpl.Execute(&buf, data); err != nil {
213 | log.Fatal("Failed to execute history template:", err)
214 | }
215 | if err = os.WriteFile(c.HistoryHtmlFile(), buf.Bytes(), 0644); err != nil {
216 | log.Fatal("Failed to write history page:", err)
217 | }
218 | }
219 |
220 | func (c *Config) monitorServices() {
221 | for {
222 | groups := c.ReadChecks()
223 | // log.Printf("Groups: %+v", groups)
224 | results := runChecks(groups)
225 | c.updateHistory(results)
226 | data := map[string]interface{}{
227 | "groups": results,
228 | "incidents": template.HTML(c.ReadIncidentHtml()),
229 | "last_updated": time.Now().Format("2006-01-02 15:04:05"),
230 | }
231 | html := renderTemplate(data)
232 | if err := os.WriteFile(c.IndexHtmlFile(), []byte(html), 0644); err != nil {
233 | log.Fatal("Failed to write index.html:", err)
234 | }
235 | c.generateHistoryPage()
236 | log.Println("Status pages updated!")
237 | if c.Token != "" && c.Chatid != "" {
238 | log.Println("Notifying on telegram ...")
239 | for key, hdata := range c.loadHistory() {
240 | if total := len(hdata); total >= 2 {
241 | latestdata := hdata[:2]
242 | if latestdata[0].Status == latestdata[1].Status {
243 | continue
244 | }
245 | lastst := latestdata[1].Status
246 | newinterval := c.CheckInterval
247 | for x, y := range hdata {
248 | if x > 1 {
249 | if y.Status == lastst {
250 | newinterval += 60
251 | } else {
252 | break
253 | }
254 | }
255 | }
256 | tosend := fmt.Sprintf("