mirror of
https://github.com/airlabspl/uptimemonitor.git
synced 2025-08-15 12:49:15 +02:00
259 lines
5.4 KiB
Go
259 lines
5.4 KiB
Go
package service
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"text/template"
|
|
"time"
|
|
"uptimemonitor"
|
|
"uptimemonitor/store"
|
|
)
|
|
|
|
type CheckService struct {
|
|
Store *store.Store
|
|
}
|
|
|
|
func (s *CheckService) StartCheck() chan uptimemonitor.Monitor {
|
|
ch := make(chan uptimemonitor.Monitor, 10)
|
|
|
|
go func() {
|
|
for m := range ch {
|
|
s.handleCheck(m)
|
|
}
|
|
}()
|
|
|
|
return ch
|
|
}
|
|
|
|
func (s *CheckService) RunCheck(ctx context.Context, ch chan uptimemonitor.Monitor) error {
|
|
monitors, err := s.Store.ListMonitors(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, m := range monitors {
|
|
ch <- m
|
|
}
|
|
|
|
go func() {
|
|
s.Cleanup()
|
|
}()
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *CheckService) handleCheck(m uptimemonitor.Monitor) {
|
|
c, cancel := context.WithTimeout(context.Background(), time.Minute)
|
|
defer cancel()
|
|
|
|
start := time.Now()
|
|
var customBody io.Reader
|
|
|
|
if m.HttpBody != "" {
|
|
customBody = strings.NewReader(m.HttpBody)
|
|
}
|
|
|
|
req, err := http.NewRequest(
|
|
m.HttpMethod,
|
|
m.Url,
|
|
customBody,
|
|
)
|
|
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
if m.HttpHeaders != "" {
|
|
customHeaders := map[string]string{}
|
|
err = json.Unmarshal([]byte(m.HttpHeaders), &customHeaders)
|
|
if err == nil {
|
|
for k, v := range customHeaders {
|
|
req.Header.Add(k, v)
|
|
}
|
|
}
|
|
}
|
|
|
|
// todo: add timeout
|
|
res, err := http.DefaultClient.Do(req)
|
|
elapsed := time.Since(start)
|
|
|
|
if err != nil {
|
|
check, err := s.Store.CreateCheck(c, uptimemonitor.Check{
|
|
MonitorID: m.ID,
|
|
Monitor: m,
|
|
StatusCode: http.StatusNotFound,
|
|
ResponseTimeMs: elapsed.Milliseconds(),
|
|
})
|
|
|
|
if err != nil || check.ID == 0 {
|
|
return
|
|
}
|
|
|
|
if err := s.createIncident(m, check, elapsed.Milliseconds(), http.StatusNotFound, "", ""); err != nil {
|
|
return
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
statusCode := res.StatusCode
|
|
if res.Request.Response != nil {
|
|
statusCode = res.Request.Response.StatusCode
|
|
}
|
|
|
|
check, err := s.Store.CreateCheck(c, uptimemonitor.Check{
|
|
MonitorID: m.ID,
|
|
Monitor: m,
|
|
StatusCode: statusCode,
|
|
ResponseTimeMs: elapsed.Milliseconds(),
|
|
})
|
|
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
if statusCode < 300 {
|
|
open, err := s.Store.ListMonitorOpenIncidents(c, m.ID)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
for _, i := range open {
|
|
s.Store.ResolveIncident(c, i)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
var body string
|
|
var headers string
|
|
|
|
if res.Request.Response != nil {
|
|
var resHeaders string
|
|
for k, v := range res.Request.Response.Header {
|
|
resHeaders += fmt.Sprintf("%s: %s\n", k, v)
|
|
}
|
|
|
|
resBody, _ := io.ReadAll(res.Request.Response.Body)
|
|
defer res.Request.Response.Body.Close()
|
|
body = string(resBody)
|
|
headers = resHeaders
|
|
} else {
|
|
var resHeaders string
|
|
for k, v := range res.Header {
|
|
resHeaders += fmt.Sprintf("%s: %s\n", k, v)
|
|
}
|
|
|
|
resBody, _ := io.ReadAll(res.Body)
|
|
defer res.Body.Close()
|
|
body = string(resBody)
|
|
headers = resHeaders
|
|
}
|
|
|
|
s.createIncident(m, check, elapsed.Milliseconds(), statusCode, string(body), headers)
|
|
}
|
|
|
|
func (s *CheckService) createIncident(m uptimemonitor.Monitor, check uptimemonitor.Check, responseTimeMs int64, statusCode int, body string, headers string) error {
|
|
reqURL := m.Url
|
|
reqMethod := m.HttpMethod
|
|
reqHeaders := m.HttpHeaders
|
|
reqBody := m.HttpBody
|
|
|
|
if exists, latest := s.incidentAlreadyExists(context.Background(), m, statusCode); exists {
|
|
return s.Store.UpdateIncidentBodyAndHeaders(context.Background(), latest, body, headers, reqMethod, reqURL, reqHeaders, reqBody)
|
|
}
|
|
|
|
s.Store.ResolveMonitorIncidents(context.Background(), m)
|
|
|
|
incident := uptimemonitor.Incident{
|
|
MonitorID: m.ID,
|
|
StatusCode: check.StatusCode,
|
|
ResponseTimeMs: responseTimeMs,
|
|
Body: body,
|
|
Headers: headers,
|
|
ReqUrl: reqURL,
|
|
ReqHeaders: reqHeaders,
|
|
ReqBody: reqBody,
|
|
ReqMethod: reqMethod,
|
|
}
|
|
|
|
saved, err := s.Store.CreateIncident(context.Background(), incident)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create incident for monitor %d: %w", m.ID, err)
|
|
}
|
|
|
|
if m.WebhookUrl != "" {
|
|
s.callWebhook(m, saved)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *CheckService) incidentAlreadyExists(ctx context.Context, m uptimemonitor.Monitor, statusCode int) (bool, uptimemonitor.Incident) {
|
|
latest, err := s.Store.LastIncidentByStatusCode(ctx, m.ID, uptimemonitor.IncidentStatusOpen, statusCode)
|
|
if err != nil {
|
|
return false, uptimemonitor.Incident{}
|
|
}
|
|
|
|
return latest.StatusCode == statusCode, latest
|
|
}
|
|
|
|
func (s *CheckService) callWebhook(m uptimemonitor.Monitor, i uptimemonitor.Incident) {
|
|
var customBody io.Reader
|
|
|
|
// todo: parse url
|
|
if m.WebhookBody != "" {
|
|
t, err := template.New("webhook").Parse(m.WebhookBody)
|
|
if err != nil {
|
|
customBody = strings.NewReader(m.WebhookBody)
|
|
} else {
|
|
var buf bytes.Buffer
|
|
err = t.Execute(&buf, struct {
|
|
Url string
|
|
StatusCode int
|
|
}{
|
|
Url: m.Url,
|
|
StatusCode: i.StatusCode,
|
|
})
|
|
if err != nil {
|
|
customBody = strings.NewReader(m.WebhookBody)
|
|
} else {
|
|
customBody = strings.NewReader(buf.String())
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
req, err := http.NewRequest(
|
|
m.WebhookMethod,
|
|
m.WebhookUrl,
|
|
customBody,
|
|
)
|
|
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
if m.WebhookHeaders != "" {
|
|
customHeaders := map[string]string{}
|
|
err = json.Unmarshal([]byte(m.WebhookHeaders), &customHeaders)
|
|
if err == nil {
|
|
for k, v := range customHeaders {
|
|
req.Header.Add(k, v)
|
|
}
|
|
}
|
|
}
|
|
|
|
http.DefaultClient.Do(req)
|
|
}
|
|
|
|
func (s *CheckService) Cleanup() {
|
|
s.Store.DeleteOldChecks(context.Background())
|
|
s.Store.DeleteOldIncidents(context.Background())
|
|
}
|