Improve service checks and system health details
This commit is contained in:
138
services.go
138
services.go
@@ -30,15 +30,36 @@ type MonitoredService struct {
|
||||
|
||||
type ServiceStatus struct {
|
||||
MonitoredService
|
||||
Up bool `json:"up"`
|
||||
CheckedAt int64 `json:"checkedAt"`
|
||||
LatencyMS float64 `json:"latencyMs"`
|
||||
StatusCode int `json:"statusCode"`
|
||||
Error string `json:"error,omitempty"`
|
||||
TLSExpiry *int64 `json:"tlsExpiry,omitempty"`
|
||||
TLSDays *int `json:"tlsDays,omitempty"`
|
||||
UptimeDay float64 `json:"uptimeDay"`
|
||||
UptimeWeek float64 `json:"uptimeWeek"`
|
||||
Endpoints []ServiceEndpointStatus `json:"endpoints"`
|
||||
Up bool `json:"up"`
|
||||
CheckedAt int64 `json:"checkedAt"`
|
||||
LatencyMS float64 `json:"latencyMs"`
|
||||
StatusCode int `json:"statusCode"`
|
||||
Error string `json:"error,omitempty"`
|
||||
TLSExpiry *int64 `json:"tlsExpiry,omitempty"`
|
||||
TLSDays *int `json:"tlsDays,omitempty"`
|
||||
UptimeDay float64 `json:"uptimeDay"`
|
||||
UptimeWeek float64 `json:"uptimeWeek"`
|
||||
}
|
||||
|
||||
type ServiceEndpointStatus struct {
|
||||
Kind string `json:"kind"`
|
||||
Label string `json:"label"`
|
||||
Address string `json:"address"`
|
||||
Up bool `json:"up"`
|
||||
CheckedAt int64 `json:"checkedAt"`
|
||||
LatencyMS float64 `json:"latencyMs"`
|
||||
StatusCode int `json:"statusCode"`
|
||||
Error string `json:"error,omitempty"`
|
||||
TLSExpiry *int64 `json:"tlsExpiry,omitempty"`
|
||||
TLSDays *int `json:"tlsDays,omitempty"`
|
||||
TLSSubject string `json:"tlsSubject,omitempty"`
|
||||
TLSIssuer string `json:"tlsIssuer,omitempty"`
|
||||
TLSDNSNames []string `json:"tlsDnsNames,omitempty"`
|
||||
TLSHostnameValid bool `json:"tlsHostnameValid"`
|
||||
TLSHostnameError string `json:"tlsHostnameError,omitempty"`
|
||||
UptimeDay float64 `json:"uptimeDay"`
|
||||
UptimeWeek float64 `json:"uptimeWeek"`
|
||||
}
|
||||
|
||||
type ServicesMetrics struct {
|
||||
@@ -84,13 +105,14 @@ func validateService(v MonitoredService) error {
|
||||
return fmt.Errorf("таймаут должен быть от 1 до 30 секунд")
|
||||
}
|
||||
if v.CheckType == "http" {
|
||||
target := v.URL
|
||||
if target == "" {
|
||||
target = v.LocalAddress
|
||||
}
|
||||
u, err := url.Parse(target)
|
||||
if err != nil || (u.Scheme != "http" && u.Scheme != "https") || u.Host == "" {
|
||||
return fmt.Errorf("укажите полный HTTP/HTTPS-адрес")
|
||||
for _, target := range []string{v.URL, v.LocalAddress} {
|
||||
if target == "" {
|
||||
continue
|
||||
}
|
||||
u, err := url.Parse(target)
|
||||
if err != nil || (u.Scheme != "http" && u.Scheme != "https") || u.Host == "" {
|
||||
return fmt.Errorf("оба адреса должны начинаться с http:// или https://")
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@@ -121,17 +143,17 @@ func (s *Store) DeleteService(id int64) error {
|
||||
_, err := s.db.Exec(`DELETE FROM services WHERE id=?`, id)
|
||||
return err
|
||||
}
|
||||
func (s *Store) addServiceCheck(id int64, v ServiceStatus) {
|
||||
func (s *Store) addServiceCheck(id int64, endpoint string, v ServiceEndpointStatus) {
|
||||
var expiry any
|
||||
if v.TLSExpiry != nil {
|
||||
expiry = *v.TLSExpiry
|
||||
}
|
||||
_, _ = s.db.Exec(`INSERT INTO service_checks(service_id,ts,up,latency_ms,status_code,error,tls_expiry) VALUES(?,?,?,?,?,?,?)`, id, v.CheckedAt, v.Up, v.LatencyMS, v.StatusCode, v.Error, expiry)
|
||||
_, _ = s.db.Exec(`INSERT INTO service_checks(service_id,ts,up,latency_ms,status_code,error,tls_expiry,endpoint) VALUES(?,?,?,?,?,?,?,?)`, id, v.CheckedAt, v.Up, v.LatencyMS, v.StatusCode, v.Error, expiry, endpoint)
|
||||
_, _ = s.db.Exec(`DELETE FROM service_checks WHERE ts<?`, time.Now().Add(-historyRetention).Unix())
|
||||
}
|
||||
func (s *Store) serviceUptime(id int64, since time.Time) float64 {
|
||||
func (s *Store) serviceUptime(id int64, endpoint string, since time.Time) float64 {
|
||||
var value sql.NullFloat64
|
||||
_ = s.db.QueryRow(`SELECT AVG(up)*100 FROM service_checks WHERE service_id=? AND ts>=?`, id, since.Unix()).Scan(&value)
|
||||
_ = s.db.QueryRow(`SELECT AVG(up)*100 FROM service_checks WHERE service_id=? AND endpoint=? AND ts>=?`, id, endpoint, since.Unix()).Scan(&value)
|
||||
if value.Valid {
|
||||
return value.Float64
|
||||
}
|
||||
@@ -195,9 +217,16 @@ func (c *serviceCollector) checkAll() {
|
||||
continue
|
||||
}
|
||||
st := checkService(v)
|
||||
st.UptimeDay = c.store.serviceUptime(v.ID, time.Now().Add(-24*time.Hour))
|
||||
st.UptimeWeek = c.store.serviceUptime(v.ID, time.Now().Add(-7*24*time.Hour))
|
||||
c.store.addServiceCheck(v.ID, st)
|
||||
for i := range st.Endpoints {
|
||||
ep := &st.Endpoints[i]
|
||||
ep.UptimeDay = c.store.serviceUptime(v.ID, ep.Kind, time.Now().Add(-24*time.Hour))
|
||||
ep.UptimeWeek = c.store.serviceUptime(v.ID, ep.Kind, time.Now().Add(-7*24*time.Hour))
|
||||
c.store.addServiceCheck(v.ID, ep.Kind, *ep)
|
||||
}
|
||||
if len(st.Endpoints) > 0 {
|
||||
st.UptimeDay = st.Endpoints[0].UptimeDay
|
||||
st.UptimeWeek = st.Endpoints[0].UptimeWeek
|
||||
}
|
||||
m.Total++
|
||||
if st.Up {
|
||||
m.Up++
|
||||
@@ -210,16 +239,41 @@ func (c *serviceCollector) checkAll() {
|
||||
}
|
||||
|
||||
func checkService(v MonitoredService) ServiceStatus {
|
||||
st := ServiceStatus{MonitoredService: v, CheckedAt: time.Now().Unix()}
|
||||
st := ServiceStatus{MonitoredService: v, CheckedAt: time.Now().Unix(), Up: true}
|
||||
if v.URL != "" {
|
||||
st.Endpoints = append(st.Endpoints, checkServiceEndpoint(v, "public", "Внешний адрес", v.URL))
|
||||
}
|
||||
if v.LocalAddress != "" {
|
||||
st.Endpoints = append(st.Endpoints, checkServiceEndpoint(v, "local", "Локальный адрес", v.LocalAddress))
|
||||
}
|
||||
for _, ep := range st.Endpoints {
|
||||
if !ep.Up {
|
||||
st.Up = false
|
||||
}
|
||||
st.LatencyMS += ep.LatencyMS
|
||||
if st.StatusCode == 0 {
|
||||
st.StatusCode = ep.StatusCode
|
||||
}
|
||||
if st.Error == "" && ep.Error != "" {
|
||||
st.Error = ep.Label + ": " + ep.Error
|
||||
}
|
||||
}
|
||||
if len(st.Endpoints) > 0 {
|
||||
st.LatencyMS /= float64(len(st.Endpoints))
|
||||
ep := st.Endpoints[0]
|
||||
st.TLSExpiry = ep.TLSExpiry
|
||||
st.TLSDays = ep.TLSDays
|
||||
}
|
||||
return st
|
||||
}
|
||||
|
||||
func checkServiceEndpoint(v MonitoredService, kind, label, target string) ServiceEndpointStatus {
|
||||
st := ServiceEndpointStatus{Kind: kind, Label: label, Address: target, CheckedAt: time.Now().Unix()}
|
||||
start := time.Now()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(v.TimeoutSeconds)*time.Second)
|
||||
defer cancel()
|
||||
switch v.CheckType {
|
||||
case "tcp":
|
||||
target := v.URL
|
||||
if target == "" {
|
||||
target = v.LocalAddress
|
||||
}
|
||||
d := net.Dialer{}
|
||||
conn, err := d.DialContext(ctx, "tcp", target)
|
||||
if err == nil {
|
||||
@@ -229,10 +283,6 @@ func checkService(v MonitoredService) ServiceStatus {
|
||||
st.Error = err.Error()
|
||||
}
|
||||
case "ping":
|
||||
target := v.URL
|
||||
if target == "" {
|
||||
target = v.LocalAddress
|
||||
}
|
||||
host := strings.TrimPrefix(strings.TrimPrefix(target, "https://"), "http://")
|
||||
host = strings.Split(host, "/")[0]
|
||||
host = strings.Split(host, ":")[0]
|
||||
@@ -246,13 +296,13 @@ func checkService(v MonitoredService) ServiceStatus {
|
||||
st.Error = strings.TrimSpace(string(out))
|
||||
}
|
||||
default:
|
||||
target := v.URL
|
||||
if target == "" {
|
||||
target = v.LocalAddress
|
||||
}
|
||||
tr := &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true, MinVersion: tls.VersionTLS12}}
|
||||
client := &http.Client{Transport: tr, Timeout: time.Duration(v.TimeoutSeconds) * time.Second}
|
||||
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, target, nil)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, target, nil)
|
||||
if err != nil {
|
||||
st.Error = err.Error()
|
||||
break
|
||||
}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
st.Error = err.Error()
|
||||
@@ -269,10 +319,20 @@ func checkService(v MonitoredService) ServiceStatus {
|
||||
st.Error = fmt.Sprintf("HTTP %d, ожидался %d", resp.StatusCode, v.ExpectedCode)
|
||||
}
|
||||
if resp.TLS != nil && len(resp.TLS.PeerCertificates) > 0 {
|
||||
expiry := resp.TLS.PeerCertificates[0].NotAfter.Unix()
|
||||
days := int(time.Until(resp.TLS.PeerCertificates[0].NotAfter).Hours() / 24)
|
||||
cert := resp.TLS.PeerCertificates[0]
|
||||
expiry := cert.NotAfter.Unix()
|
||||
days := int(time.Until(cert.NotAfter).Hours() / 24)
|
||||
st.TLSExpiry = &expiry
|
||||
st.TLSDays = &days
|
||||
st.TLSSubject = cert.Subject.CommonName
|
||||
st.TLSIssuer = cert.Issuer.CommonName
|
||||
st.TLSDNSNames = cert.DNSNames
|
||||
host := req.URL.Hostname()
|
||||
if err := cert.VerifyHostname(host); err == nil {
|
||||
st.TLSHostnameValid = true
|
||||
} else {
|
||||
st.TLSHostnameError = err.Error()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user