mirror of
https://github.com/Dvorinka/beszel.git
synced 2026-06-03 21:02:56 +00:00
Initial commit: Beszel fork with Domain Locker integration
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
package container
|
||||
|
||||
import "time"
|
||||
|
||||
// Docker container info from /containers/json
|
||||
type ApiInfo struct {
|
||||
Id string
|
||||
IdShort string
|
||||
Names []string
|
||||
Status string
|
||||
State string
|
||||
Image string
|
||||
Health struct {
|
||||
Status string
|
||||
// FailingStreak int
|
||||
}
|
||||
Ports []struct {
|
||||
// PrivatePort uint16
|
||||
PublicPort uint16
|
||||
IP string
|
||||
// Type string
|
||||
}
|
||||
// ImageID string
|
||||
// Command string
|
||||
// Created int64
|
||||
// SizeRw int64 `json:",omitempty"`
|
||||
// SizeRootFs int64 `json:",omitempty"`
|
||||
// Labels map[string]string
|
||||
// HostConfig struct {
|
||||
// NetworkMode string `json:",omitempty"`
|
||||
// Annotations map[string]string `json:",omitempty"`
|
||||
// }
|
||||
// NetworkSettings *SummaryNetworkSettings
|
||||
// Mounts []MountPoint
|
||||
}
|
||||
|
||||
// Docker container resources from /containers/{id}/stats
|
||||
type ApiStats struct {
|
||||
Read time.Time `json:"read"` // Time of stats generation
|
||||
NumProcs uint32 `json:"num_procs,omitzero"` // Windows specific, not populated on Linux.
|
||||
Networks map[string]NetworkStats
|
||||
CPUStats CPUStats `json:"cpu_stats"`
|
||||
MemoryStats MemoryStats `json:"memory_stats"`
|
||||
}
|
||||
|
||||
// Docker system info from /info API endpoint
|
||||
type HostInfo struct {
|
||||
OperatingSystem string `json:"OperatingSystem"`
|
||||
KernelVersion string `json:"KernelVersion"`
|
||||
NCPU int `json:"NCPU"`
|
||||
MemTotal uint64 `json:"MemTotal"`
|
||||
}
|
||||
|
||||
func (s *ApiStats) CalculateCpuPercentLinux(prevCpuContainer uint64, prevCpuSystem uint64) float64 {
|
||||
cpuDelta := s.CPUStats.CPUUsage.TotalUsage - prevCpuContainer
|
||||
systemDelta := s.CPUStats.SystemUsage - prevCpuSystem
|
||||
|
||||
// Avoid division by zero and handle first run case
|
||||
if systemDelta == 0 || prevCpuContainer == 0 {
|
||||
return 0.0
|
||||
}
|
||||
|
||||
return float64(cpuDelta) / float64(systemDelta) * 100.0
|
||||
}
|
||||
|
||||
// from: https://github.com/docker/cli/blob/master/cli/command/container/stats_helpers.go#L185
|
||||
func (s *ApiStats) CalculateCpuPercentWindows(prevCpuUsage uint64, prevRead time.Time) float64 {
|
||||
// Max number of 100ns intervals between the previous time read and now
|
||||
possIntervals := uint64(s.Read.Sub(prevRead).Nanoseconds())
|
||||
possIntervals /= 100 // Convert to number of 100ns intervals
|
||||
possIntervals *= uint64(s.NumProcs) // Multiple by the number of processors
|
||||
|
||||
// Intervals used
|
||||
intervalsUsed := s.CPUStats.CPUUsage.TotalUsage - prevCpuUsage
|
||||
|
||||
// Percentage avoiding divide-by-zero
|
||||
if possIntervals > 0 {
|
||||
return float64(intervalsUsed) / float64(possIntervals) * 100.0
|
||||
}
|
||||
return 0.00
|
||||
}
|
||||
|
||||
type CPUStats struct {
|
||||
// CPU Usage. Linux and Windows.
|
||||
CPUUsage CPUUsage `json:"cpu_usage"`
|
||||
// System Usage. Linux only.
|
||||
SystemUsage uint64 `json:"system_cpu_usage,omitempty"`
|
||||
}
|
||||
|
||||
type CPUUsage struct {
|
||||
// Total CPU time consumed.
|
||||
// Units: nanoseconds (Linux)
|
||||
// Units: 100's of nanoseconds (Windows)
|
||||
TotalUsage uint64 `json:"total_usage"`
|
||||
}
|
||||
|
||||
type MemoryStats struct {
|
||||
// current res_counter usage for memory
|
||||
Usage uint64 `json:"usage,omitempty"`
|
||||
// all the stats exported via memory.stat.
|
||||
Stats MemoryStatsStats `json:"stats"`
|
||||
// private working set (Windows only)
|
||||
PrivateWorkingSet uint64 `json:"privateworkingset,omitempty"`
|
||||
}
|
||||
|
||||
type MemoryStatsStats struct {
|
||||
Cache uint64 `json:"cache,omitempty"`
|
||||
InactiveFile uint64 `json:"inactive_file,omitempty"`
|
||||
}
|
||||
|
||||
type NetworkStats struct {
|
||||
// Bytes received. Windows and Linux.
|
||||
RxBytes uint64 `json:"rx_bytes"`
|
||||
// Bytes sent. Windows and Linux.
|
||||
TxBytes uint64 `json:"tx_bytes"`
|
||||
}
|
||||
|
||||
type prevNetStats struct {
|
||||
Sent uint64
|
||||
Recv uint64
|
||||
}
|
||||
|
||||
type DockerHealth = uint8
|
||||
|
||||
const (
|
||||
DockerHealthNone DockerHealth = iota
|
||||
DockerHealthStarting
|
||||
DockerHealthHealthy
|
||||
DockerHealthUnhealthy
|
||||
)
|
||||
|
||||
var DockerHealthStrings = map[string]DockerHealth{
|
||||
"none": DockerHealthNone,
|
||||
"starting": DockerHealthStarting,
|
||||
"healthy": DockerHealthHealthy,
|
||||
"unhealthy": DockerHealthUnhealthy,
|
||||
}
|
||||
|
||||
// Docker container stats
|
||||
type Stats struct {
|
||||
Name string `json:"n" cbor:"0,keyasint"`
|
||||
Cpu float64 `json:"c" cbor:"1,keyasint"`
|
||||
Mem float64 `json:"m" cbor:"2,keyasint"`
|
||||
NetworkSent float64 `json:"ns,omitzero" cbor:"3,keyasint,omitzero"` // deprecated 0.18.3 (MB) - keep field for old agents/records
|
||||
NetworkRecv float64 `json:"nr,omitzero" cbor:"4,keyasint,omitzero"` // deprecated 0.18.3 (MB) - keep field for old agents/records
|
||||
Bandwidth [2]uint64 `json:"b,omitzero" cbor:"9,keyasint,omitzero"` // [sent bytes, recv bytes]
|
||||
|
||||
Health DockerHealth `json:"-" cbor:"5,keyasint"`
|
||||
Status string `json:"-" cbor:"6,keyasint"`
|
||||
Id string `json:"-" cbor:"7,keyasint"`
|
||||
Image string `json:"-" cbor:"8,keyasint"`
|
||||
Ports string `json:"-" cbor:"10,keyasint"`
|
||||
// PrevCpu [2]uint64 `json:"-"`
|
||||
CpuSystem uint64 `json:"-"`
|
||||
CpuContainer uint64 `json:"-"`
|
||||
PrevNet prevNetStats `json:"-"`
|
||||
PrevReadTime time.Time `json:"-"`
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
package domain
|
||||
|
||||
import "time"
|
||||
|
||||
// Domain represents a tracked domain with WHOIS and DNS information
|
||||
type Domain struct {
|
||||
ID string `json:"id" db:"id"`
|
||||
DomainName string `json:"domain_name" db:"domain_name"`
|
||||
Status string `json:"status" db:"status"`
|
||||
Active bool `json:"active" db:"active"`
|
||||
|
||||
// Dates
|
||||
ExpiryDate *time.Time `json:"expiry_date" db:"expiry_date"`
|
||||
CreationDate *time.Time `json:"creation_date" db:"creation_date"`
|
||||
UpdatedDate *time.Time `json:"updated_date" db:"updated_date"`
|
||||
|
||||
// Registrar
|
||||
RegistrarName string `json:"registrar_name" db:"registrar_name"`
|
||||
RegistrarID string `json:"registrar_id" db:"registrar_id"`
|
||||
RegistrarURL string `json:"registrar_url" db:"registrar_url"`
|
||||
RegistryDomainID string `json:"registry_domain_id" db:"registry_domain_id"`
|
||||
|
||||
// DNS
|
||||
DNSSEC string `json:"dnssec" db:"dnssec"`
|
||||
NameServers []string `json:"name_servers" db:"name_servers"`
|
||||
MXRecords []string `json:"mx_records" db:"mx_records"`
|
||||
TXTRecords []string `json:"txt_records" db:"txt_records"`
|
||||
|
||||
// IP Addresses
|
||||
IPv4Addresses []string `json:"ipv4_addresses" db:"ipv4_addresses"`
|
||||
IPv6Addresses []string `json:"ipv6_addresses" db:"ipv6_addresses"`
|
||||
|
||||
// SSL Certificate
|
||||
SSLIssuer string `json:"ssl_issuer" db:"ssl_issuer"`
|
||||
SSLIssuerCountry string `json:"ssl_issuer_country" db:"ssl_issuer_country"`
|
||||
SSLValidFrom *time.Time `json:"ssl_valid_from" db:"ssl_valid_from"`
|
||||
SSLValidTo *time.Time `json:"ssl_valid_to" db:"ssl_valid_to"`
|
||||
SSLSubject string `json:"ssl_subject" db:"ssl_subject"`
|
||||
SSLFingerprint string `json:"ssl_fingerprint" db:"ssl_fingerprint"`
|
||||
SSLKeySize int `json:"ssl_key_size" db:"ssl_key_size"`
|
||||
SSLSignatureAlgo string `json:"ssl_signature_algo" db:"ssl_signature_algo"`
|
||||
|
||||
// Host Info
|
||||
HostCountry string `json:"host_country" db:"host_country"`
|
||||
HostRegion string `json:"host_region" db:"host_region"`
|
||||
HostCity string `json:"host_city" db:"host_city"`
|
||||
HostISP string `json:"host_isp" db:"host_isp"`
|
||||
HostOrg string `json:"host_org" db:"host_org"`
|
||||
HostAS string `json:"host_as" db:"host_as"`
|
||||
HostLat float64 `json:"host_lat" db:"host_lat"`
|
||||
HostLon float64 `json:"host_lon" db:"host_lon"`
|
||||
|
||||
// Valuation
|
||||
PurchasePrice float64 `json:"purchase_price" db:"purchase_price"`
|
||||
CurrentValue float64 `json:"current_value" db:"current_value"`
|
||||
RenewalCost float64 `json:"renewal_cost" db:"renewal_cost"`
|
||||
AutoRenew bool `json:"auto_renew" db:"auto_renew"`
|
||||
|
||||
// Registrant Contact (from WHOIS)
|
||||
RegistrantName string `json:"registrant_name" db:"registrant_name"`
|
||||
RegistrantOrg string `json:"registrant_org" db:"registrant_org"`
|
||||
RegistrantStreet string `json:"registrant_street" db:"registrant_street"`
|
||||
RegistrantCity string `json:"registrant_city" db:"registrant_city"`
|
||||
RegistrantState string `json:"registrant_state" db:"registrant_state"`
|
||||
RegistrantCountry string `json:"registrant_country" db:"registrant_country"`
|
||||
RegistrantPostal string `json:"registrant_postal" db:"registrant_postal"`
|
||||
|
||||
// Abuse Contact (from WHOIS)
|
||||
AbuseEmail string `json:"abuse_email" db:"abuse_email"`
|
||||
AbusePhone string `json:"abuse_phone" db:"abuse_phone"`
|
||||
|
||||
// Metadata
|
||||
Tags []string `json:"tags" db:"tags"`
|
||||
Notes string `json:"notes" db:"notes"`
|
||||
FaviconURL string `json:"favicon_url" db:"favicon_url"`
|
||||
|
||||
// Ownership
|
||||
UserID string `json:"user" db:"user"`
|
||||
Created time.Time `json:"created" db:"created"`
|
||||
Updated time.Time `json:"updated" db:"updated"`
|
||||
LastChecked time.Time `json:"last_checked" db:"last_checked"`
|
||||
|
||||
// Alert settings
|
||||
AlertDaysBefore int `json:"alert_days_before" db:"alert_days_before"` // Days before expiry to alert
|
||||
SSLAlertEnabled bool `json:"ssl_alert_enabled" db:"ssl_alert_enabled"`
|
||||
}
|
||||
|
||||
// DomainHistory tracks changes to a domain over time
|
||||
type DomainHistory struct {
|
||||
ID string `json:"id" db:"id"`
|
||||
DomainID string `json:"domain" db:"domain"`
|
||||
ChangeType string `json:"change_type" db:"change_type"` // expiry, ssl, dns, registrar, ip, etc.
|
||||
FieldName string `json:"field_name" db:"field_name"`
|
||||
OldValue string `json:"old_value" db:"old_value"`
|
||||
NewValue string `json:"new_value" db:"new_value"`
|
||||
UserID string `json:"user" db:"user"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
}
|
||||
|
||||
// WHOISData represents parsed WHOIS information
|
||||
type WHOISData struct {
|
||||
DomainName string `json:"domain_name"`
|
||||
Status []string `json:"status"`
|
||||
DNSSEC string `json:"dnssec"`
|
||||
Dates WHOISDates `json:"dates"`
|
||||
Registrar WHOISRegistrar `json:"registrar"`
|
||||
Registrant WHOISContact `json:"registrant"`
|
||||
Abuse WHOISAbuse `json:"abuse"`
|
||||
}
|
||||
|
||||
type WHOISDates struct {
|
||||
ExpiryDate *time.Time `json:"expiry_date"`
|
||||
CreationDate *time.Time `json:"creation_date"`
|
||||
UpdatedDate *time.Time `json:"updated_date"`
|
||||
}
|
||||
|
||||
type WHOISRegistrar struct {
|
||||
Name string `json:"name"`
|
||||
ID string `json:"id"`
|
||||
URL string `json:"url"`
|
||||
RegistryDomainID string `json:"registry_domain_id"`
|
||||
}
|
||||
|
||||
type WHOISContact struct {
|
||||
Name string `json:"name"`
|
||||
Organization string `json:"organization"`
|
||||
Street string `json:"street"`
|
||||
City string `json:"city"`
|
||||
State string `json:"state"`
|
||||
Country string `json:"country"`
|
||||
PostalCode string `json:"postal_code"`
|
||||
}
|
||||
|
||||
type WHOISAbuse struct {
|
||||
Email string `json:"email"`
|
||||
Phone string `json:"phone"`
|
||||
}
|
||||
|
||||
// SSLInfo represents SSL certificate information
|
||||
type SSLInfo struct {
|
||||
Issuer string `json:"issuer"`
|
||||
IssuerCountry string `json:"issuer_country"`
|
||||
ValidFrom time.Time `json:"valid_from"`
|
||||
ValidTo time.Time `json:"valid_to"`
|
||||
Subject string `json:"subject"`
|
||||
Fingerprint string `json:"fingerprint"`
|
||||
KeySize int `json:"key_size"`
|
||||
SignatureAlgo string `json:"signature_algo"`
|
||||
}
|
||||
|
||||
// DNSInfo represents DNS records
|
||||
type DNSInfo struct {
|
||||
NameServers []string `json:"name_servers"`
|
||||
MXRecords []string `json:"mx_records"`
|
||||
TXTRecords []string `json:"txt_records"`
|
||||
DNSSEC string `json:"dnssec"`
|
||||
}
|
||||
|
||||
// HostInfo represents host/geolocation information
|
||||
type HostInfo struct {
|
||||
Country string `json:"country"`
|
||||
Region string `json:"region"`
|
||||
City string `json:"city"`
|
||||
Lat float64 `json:"lat"`
|
||||
Lon float64 `json:"lon"`
|
||||
ISP string `json:"isp"`
|
||||
Org string `json:"org"`
|
||||
ASNumber string `json:"as_number"`
|
||||
}
|
||||
|
||||
// IPInfo represents IP address information
|
||||
type IPInfo struct {
|
||||
IPv4 []string `json:"ipv4"`
|
||||
IPv6 []string `json:"ipv6"`
|
||||
}
|
||||
|
||||
// ChangeType constants for domain history
|
||||
const (
|
||||
ChangeTypeExpiry = "expiry"
|
||||
ChangeTypeSSL = "ssl"
|
||||
ChangeTypeDNS = "dns"
|
||||
ChangeTypeRegistrar = "registrar"
|
||||
ChangeTypeIP = "ip"
|
||||
ChangeTypeHost = "host"
|
||||
ChangeTypeStatus = "status"
|
||||
)
|
||||
|
||||
// Domain status constants
|
||||
const (
|
||||
DomainStatusActive = "active"
|
||||
DomainStatusExpiring = "expiring" // Within alert_days_before
|
||||
DomainStatusExpired = "expired"
|
||||
DomainStatusUnknown = "unknown"
|
||||
DomainStatusPaused = "paused"
|
||||
)
|
||||
|
||||
// IsExpiring returns true if the domain is expiring within the alert window
|
||||
func (d *Domain) IsExpiring() bool {
|
||||
if d.ExpiryDate == nil || d.AlertDaysBefore <= 0 {
|
||||
return false
|
||||
}
|
||||
alertDate := time.Now().AddDate(0, 0, d.AlertDaysBefore)
|
||||
return d.ExpiryDate.Before(alertDate) && d.ExpiryDate.After(time.Now())
|
||||
}
|
||||
|
||||
// IsExpired returns true if the domain has expired
|
||||
func (d *Domain) IsExpired() bool {
|
||||
if d.ExpiryDate == nil {
|
||||
return false
|
||||
}
|
||||
return d.ExpiryDate.Before(time.Now())
|
||||
}
|
||||
|
||||
// DaysUntilExpiry returns the number of days until expiry
|
||||
func (d *Domain) DaysUntilExpiry() int {
|
||||
if d.ExpiryDate == nil {
|
||||
return -1
|
||||
}
|
||||
return int(time.Until(*d.ExpiryDate).Hours() / 24)
|
||||
}
|
||||
|
||||
// SSLDaysUntilExpiry returns days until SSL expiry
|
||||
func (d *Domain) SSLDaysUntilExpiry() int {
|
||||
if d.SSLValidTo == nil {
|
||||
return -1
|
||||
}
|
||||
return int(time.Until(*d.SSLValidTo).Hours() / 24)
|
||||
}
|
||||
|
||||
// GetStatus returns the current domain status
|
||||
func (d *Domain) GetStatus() string {
|
||||
if !d.Active {
|
||||
return DomainStatusPaused
|
||||
}
|
||||
if d.IsExpired() {
|
||||
return DomainStatusExpired
|
||||
}
|
||||
if d.IsExpiring() {
|
||||
return DomainStatusExpiring
|
||||
}
|
||||
if d.ExpiryDate == nil {
|
||||
return DomainStatusUnknown
|
||||
}
|
||||
return DomainStatusActive
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package incident
|
||||
|
||||
import "time"
|
||||
|
||||
// Incident represents a monitoring incident
|
||||
type Incident struct {
|
||||
ID string `json:"id" db:"id"`
|
||||
Title string `json:"title" db:"title"`
|
||||
Description string `json:"description" db:"description"`
|
||||
Type string `json:"type" db:"type"` // monitor_down, domain_expiring, ssl_expiring, etc.
|
||||
Severity string `json:"severity" db:"severity"` // critical, high, medium, low
|
||||
Status string `json:"status" db:"status"` // open, acknowledged, resolved, closed
|
||||
|
||||
// Related entities
|
||||
MonitorID *string `json:"monitor,omitempty" db:"monitor"`
|
||||
DomainID *string `json:"domain,omitempty" db:"domain"`
|
||||
SystemID *string `json:"system,omitempty" db:"system"`
|
||||
|
||||
// Assignment
|
||||
AssignedTo *string `json:"assigned_to,omitempty" db:"assigned_to"`
|
||||
|
||||
// Timestamps
|
||||
StartedAt time.Time `json:"started_at" db:"started_at"`
|
||||
AcknowledgedAt *time.Time `json:"acknowledged_at,omitempty" db:"acknowledged_at"`
|
||||
ResolvedAt *time.Time `json:"resolved_at,omitempty" db:"resolved_at"`
|
||||
ClosedAt *time.Time `json:"closed_at,omitempty" db:"closed_at"`
|
||||
|
||||
// Resolution
|
||||
Resolution string `json:"resolution,omitempty" db:"resolution"`
|
||||
RootCause string `json:"root_cause,omitempty" db:"root_cause"`
|
||||
|
||||
// Metadata
|
||||
UserID string `json:"user" db:"user"`
|
||||
Created time.Time `json:"created" db:"created"`
|
||||
Updated time.Time `json:"updated" db:"updated"`
|
||||
}
|
||||
|
||||
// IncidentUpdate represents an update/note added to an incident
|
||||
type IncidentUpdate struct {
|
||||
ID string `json:"id" db:"id"`
|
||||
IncidentID string `json:"incident" db:"incident"`
|
||||
Message string `json:"message" db:"message"`
|
||||
UpdateType string `json:"update_type" db:"update_type"` // note, status_change, assignment
|
||||
OldStatus *string `json:"old_status,omitempty" db:"old_status"`
|
||||
NewStatus *string `json:"new_status,omitempty" db:"new_status"`
|
||||
CreatedBy string `json:"created_by" db:"created_by"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
}
|
||||
|
||||
// IncidentMetrics represents incident statistics
|
||||
type IncidentMetrics struct {
|
||||
TotalIncidents int `json:"total_incidents"`
|
||||
OpenIncidents int `json:"open_incidents"`
|
||||
AcknowledgedIncidents int `json:"acknowledged_incidents"`
|
||||
ResolvedIncidents int `json:"resolved_incidents"`
|
||||
AvgResolutionTime string `json:"avg_resolution_time"`
|
||||
MTTR float64 `json:"mttr_hours"` // Mean Time To Resolve
|
||||
}
|
||||
|
||||
// Constants
|
||||
const (
|
||||
// Incident Types
|
||||
TypeMonitorDown = "monitor_down"
|
||||
TypeMonitorUp = "monitor_up"
|
||||
TypeDomainExpiring = "domain_expiring"
|
||||
TypeDomainExpired = "domain_expired"
|
||||
TypeSSLExpiring = "ssl_expiring"
|
||||
TypeSystemOffline = "system_offline"
|
||||
TypeSystemOnline = "system_online"
|
||||
|
||||
// Severity Levels
|
||||
SeverityCritical = "critical"
|
||||
SeverityHigh = "high"
|
||||
SeverityMedium = "medium"
|
||||
SeverityLow = "low"
|
||||
|
||||
// Status
|
||||
StatusOpen = "open"
|
||||
StatusAcknowledged = "acknowledged"
|
||||
StatusResolved = "resolved"
|
||||
StatusClosed = "closed"
|
||||
)
|
||||
|
||||
// IsOpen returns true if incident is not resolved/closed
|
||||
func (i *Incident) IsOpen() bool {
|
||||
return i.Status == StatusOpen || i.Status == StatusAcknowledged
|
||||
}
|
||||
|
||||
// Duration returns how long the incident has been open
|
||||
func (i *Incident) Duration() time.Duration {
|
||||
if i.ResolvedAt != nil {
|
||||
return i.ResolvedAt.Sub(i.StartedAt)
|
||||
}
|
||||
return time.Since(i.StartedAt)
|
||||
}
|
||||
|
||||
// GetSeverityColor returns CSS color class for severity
|
||||
func GetSeverityColor(severity string) string {
|
||||
switch severity {
|
||||
case SeverityCritical:
|
||||
return "bg-red-600"
|
||||
case SeverityHigh:
|
||||
return "bg-orange-500"
|
||||
case SeverityMedium:
|
||||
return "bg-yellow-500"
|
||||
case SeverityLow:
|
||||
return "bg-blue-500"
|
||||
default:
|
||||
return "bg-gray-500"
|
||||
}
|
||||
}
|
||||
|
||||
// GetStatusColor returns CSS color class for status
|
||||
func GetStatusColor(status string) string {
|
||||
switch status {
|
||||
case StatusOpen:
|
||||
return "bg-red-500"
|
||||
case StatusAcknowledged:
|
||||
return "bg-yellow-500"
|
||||
case StatusResolved:
|
||||
return "bg-green-500"
|
||||
case StatusClosed:
|
||||
return "bg-gray-500"
|
||||
default:
|
||||
return "bg-gray-500"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
package monitor
|
||||
|
||||
import "time"
|
||||
|
||||
// Status constants for monitors
|
||||
type Status string
|
||||
|
||||
const (
|
||||
StatusUp Status = "up"
|
||||
StatusDown Status = "down"
|
||||
StatusPending Status = "pending"
|
||||
StatusPaused Status = "paused"
|
||||
StatusMaintenance Status = "maintenance"
|
||||
)
|
||||
|
||||
// Monitor types
|
||||
const (
|
||||
TypeHTTP = "http"
|
||||
TypeHTTPS = "https"
|
||||
TypeTCP = "tcp"
|
||||
TypePing = "ping"
|
||||
TypeDNS = "dns"
|
||||
TypeKeyword = "keyword"
|
||||
TypeJSONQuery = "json-query"
|
||||
TypeDocker = "docker"
|
||||
)
|
||||
|
||||
// HTTPMethod constants
|
||||
const (
|
||||
MethodGET = "GET"
|
||||
MethodPOST = "POST"
|
||||
MethodPUT = "PUT"
|
||||
MethodDELETE = "DELETE"
|
||||
MethodHEAD = "HEAD"
|
||||
MethodOPTIONS = "OPTIONS"
|
||||
MethodPATCH = "PATCH"
|
||||
)
|
||||
|
||||
// Monitor represents a website/service monitor configuration
|
||||
type Monitor struct {
|
||||
ID string `json:"id" db:"id"`
|
||||
Name string `json:"name" db:"name"`
|
||||
Type string `json:"type" db:"type"`
|
||||
URL string `json:"url" db:"url"`
|
||||
Hostname string `json:"hostname" db:"hostname"`
|
||||
Port int `json:"port" db:"port"`
|
||||
Method string `json:"method" db:"method"`
|
||||
Headers string `json:"headers" db:"headers"`
|
||||
Body string `json:"body" db:"body"`
|
||||
Interval int `json:"interval" db:"interval"`
|
||||
Timeout int `json:"timeout" db:"timeout"`
|
||||
Retries int `json:"retries" db:"retries"`
|
||||
RetryInterval int `json:"retry_interval" db:"retry_interval"`
|
||||
MaxRedirects int `json:"max_redirects" db:"max_redirects"`
|
||||
Keyword string `json:"keyword" db:"keyword"`
|
||||
JSONQuery string `json:"json_query" db:"json_query"`
|
||||
ExpectedValue string `json:"expected_value" db:"expected_value"`
|
||||
InvertKeyword bool `json:"invert_keyword" db:"invert_keyword"`
|
||||
DNSResolveServer string `json:"dns_resolve_server" db:"dns_resolve_server"`
|
||||
DNSResolverMode string `json:"dns_resolver_mode" db:"dns_resolver_mode"`
|
||||
Status Status `json:"status" db:"status"`
|
||||
Active bool `json:"active" db:"active"`
|
||||
UserID string `json:"user" db:"user"`
|
||||
Tags []string `json:"tags" db:"tags"`
|
||||
Created time.Time `json:"created" db:"created"`
|
||||
Updated time.Time `json:"updated" db:"updated"`
|
||||
LastCheck time.Time `json:"last_check" db:"last_check"`
|
||||
UptimeStats map[string]float64 `json:"uptime_stats" db:"uptime_stats"`
|
||||
Description string `json:"description" db:"description"`
|
||||
CertExpiryNotification bool `json:"cert_expiry_notification" db:"cert_expiry_notification"`
|
||||
CertExpiryDays int `json:"cert_expiry_days" db:"cert_expiry_days"`
|
||||
ProxyID string `json:"proxy" db:"proxy"`
|
||||
IgnoreTLSError bool `json:"ignore_tls_error" db:"ignore_tls_error"`
|
||||
}
|
||||
|
||||
// Heartbeat represents a single monitor check result
|
||||
type Heartbeat struct {
|
||||
ID string `json:"id" db:"id"`
|
||||
MonitorID string `json:"monitor" db:"monitor"`
|
||||
Status Status `json:"status" db:"status"`
|
||||
Ping int `json:"ping" db:"ping"`
|
||||
Msg string `json:"msg" db:"msg"`
|
||||
Time time.Time `json:"time" db:"time"`
|
||||
CertExpiry int `json:"cert_expiry" db:"cert_expiry"`
|
||||
CertValid bool `json:"cert_valid" db:"cert_valid"`
|
||||
}
|
||||
|
||||
// UptimeStats holds calculated uptime statistics
|
||||
type UptimeStats struct {
|
||||
Total int `json:"total"`
|
||||
Up int `json:"up"`
|
||||
Down int `json:"down"`
|
||||
Uptime24h float64 `json:"uptime_24h"`
|
||||
Uptime7d float64 `json:"uptime_7d"`
|
||||
Uptime30d float64 `json:"uptime_30d"`
|
||||
}
|
||||
|
||||
// CheckResult holds the result of a monitor check
|
||||
type CheckResult struct {
|
||||
Status Status
|
||||
Ping int
|
||||
Msg string
|
||||
CertExpiry int
|
||||
CertValid bool
|
||||
Error error
|
||||
}
|
||||
|
||||
// CheckRequest holds parameters for a monitor check
|
||||
type CheckRequest struct {
|
||||
Monitor *Monitor
|
||||
Timeout time.Duration
|
||||
}
|
||||
|
||||
// ToPublicJSON returns a monitor object suitable for public display
|
||||
func (m *Monitor) ToPublicJSON() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"id": m.ID,
|
||||
"name": m.Name,
|
||||
"type": m.Type,
|
||||
"status": m.Status,
|
||||
"uptime": m.UptimeStats,
|
||||
}
|
||||
}
|
||||
|
||||
// IsUp returns true if monitor status is up
|
||||
func (m *Monitor) IsUp() bool {
|
||||
return m.Status == StatusUp
|
||||
}
|
||||
|
||||
// IsDown returns true if monitor status is down
|
||||
func (m *Monitor) IsDown() bool {
|
||||
return m.Status == StatusDown
|
||||
}
|
||||
|
||||
// GetUptimePercent calculates uptime percentage for given time range
|
||||
func (m *Monitor) GetUptimePercent(hours int) float64 {
|
||||
key := "uptime_24h"
|
||||
if hours == 168 {
|
||||
key = "uptime_7d"
|
||||
} else if hours == 720 {
|
||||
key = "uptime_30d"
|
||||
}
|
||||
if val, ok := m.UptimeStats[key]; ok {
|
||||
return val
|
||||
}
|
||||
return 100.0
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
package notification
|
||||
|
||||
import "time"
|
||||
|
||||
// Provider types
|
||||
const (
|
||||
ProviderEmail = "email"
|
||||
ProviderWebhook = "webhook"
|
||||
ProviderDiscord = "discord"
|
||||
ProviderSlack = "slack"
|
||||
ProviderTelegram = "telegram"
|
||||
ProviderGotify = "gotify"
|
||||
ProviderPushover = "pushover"
|
||||
)
|
||||
|
||||
// Notification represents a notification provider configuration
|
||||
type Notification struct {
|
||||
ID string `json:"id" db:"id"`
|
||||
Name string `json:"name" db:"name"`
|
||||
Type string `json:"type" db:"type"`
|
||||
IsDefault bool `json:"is_default" db:"is_default"`
|
||||
Settings map[string]interface{} `json:"settings" db:"settings"`
|
||||
UserID string `json:"user" db:"user"`
|
||||
Active bool `json:"active" db:"active"`
|
||||
Created time.Time `json:"created" db:"created"`
|
||||
Updated time.Time `json:"updated" db:"updated"`
|
||||
}
|
||||
|
||||
// MonitorNotification links monitors to notifications
|
||||
type MonitorNotification struct {
|
||||
ID string `json:"id" db:"id"`
|
||||
MonitorID string `json:"monitor" db:"monitor"`
|
||||
NotificationID string `json:"notification" db:"notification"`
|
||||
UserID string `json:"user" db:"user"`
|
||||
Created time.Time `json:"created" db:"created"`
|
||||
}
|
||||
|
||||
// NotificationMessage represents a message to be sent
|
||||
type NotificationMessage struct {
|
||||
Title string
|
||||
Body string
|
||||
MonitorName string
|
||||
MonitorURL string
|
||||
Status string
|
||||
Timestamp time.Time
|
||||
Ping int
|
||||
Message string
|
||||
}
|
||||
|
||||
// EmailSettings for SMTP email notifications
|
||||
type EmailSettings struct {
|
||||
SMTPHost string `json:"smtp_host"`
|
||||
SMTPPort int `json:"smtp_port"`
|
||||
SMTPUser string `json:"smtp_user"`
|
||||
SMTPPassword string `json:"smtp_password"`
|
||||
FromEmail string `json:"from_email"`
|
||||
ToEmail string `json:"to_email"`
|
||||
UseTLS bool `json:"use_tls"`
|
||||
}
|
||||
|
||||
// WebhookSettings for webhook notifications
|
||||
type WebhookSettings struct {
|
||||
URL string `json:"url"`
|
||||
Method string `json:"method"`
|
||||
Headers map[string]string `json:"headers"`
|
||||
BodyTemplate string `json:"body_template"`
|
||||
}
|
||||
|
||||
// DiscordSettings for Discord webhook notifications
|
||||
type DiscordSettings struct {
|
||||
WebhookURL string `json:"webhook_url"`
|
||||
Username string `json:"username"`
|
||||
AvatarURL string `json:"avatar_url"`
|
||||
}
|
||||
|
||||
// SlackSettings for Slack webhook notifications
|
||||
type SlackSettings struct {
|
||||
WebhookURL string `json:"webhook_url"`
|
||||
Channel string `json:"channel"`
|
||||
Username string `json:"username"`
|
||||
}
|
||||
|
||||
// TelegramSettings for Telegram bot notifications
|
||||
type TelegramSettings struct {
|
||||
BotToken string `json:"bot_token"`
|
||||
ChatID string `json:"chat_id"`
|
||||
}
|
||||
|
||||
// GotifySettings for Gotify notifications
|
||||
type GotifySettings struct {
|
||||
ServerURL string `json:"server_url"`
|
||||
AppToken string `json:"app_token"`
|
||||
Priority int `json:"priority"`
|
||||
}
|
||||
|
||||
// PushoverSettings for Pushover notifications
|
||||
type PushoverSettings struct {
|
||||
AppToken string `json:"app_token"`
|
||||
UserKey string `json:"user_key"`
|
||||
Priority int `json:"priority"`
|
||||
Device string `json:"device"`
|
||||
}
|
||||
|
||||
// Provider interface for notification implementations
|
||||
type Provider interface {
|
||||
Send(message *NotificationMessage) error
|
||||
Validate() error
|
||||
}
|
||||
|
||||
// GetSettings returns typed settings based on provider type
|
||||
func (n *Notification) GetSettings() interface{} {
|
||||
switch n.Type {
|
||||
case ProviderEmail:
|
||||
var settings EmailSettings
|
||||
if m, ok := n.Settings["smtp_host"].(string); ok {
|
||||
settings.SMTPHost = m
|
||||
}
|
||||
if m, ok := n.Settings["smtp_port"].(float64); ok {
|
||||
settings.SMTPPort = int(m)
|
||||
}
|
||||
if m, ok := n.Settings["smtp_user"].(string); ok {
|
||||
settings.SMTPUser = m
|
||||
}
|
||||
if m, ok := n.Settings["smtp_password"].(string); ok {
|
||||
settings.SMTPPassword = m
|
||||
}
|
||||
if m, ok := n.Settings["from_email"].(string); ok {
|
||||
settings.FromEmail = m
|
||||
}
|
||||
if m, ok := n.Settings["to_email"].(string); ok {
|
||||
settings.ToEmail = m
|
||||
}
|
||||
if m, ok := n.Settings["use_tls"].(bool); ok {
|
||||
settings.UseTLS = m
|
||||
}
|
||||
return settings
|
||||
case ProviderWebhook:
|
||||
var settings WebhookSettings
|
||||
if m, ok := n.Settings["url"].(string); ok {
|
||||
settings.URL = m
|
||||
}
|
||||
if m, ok := n.Settings["method"].(string); ok {
|
||||
settings.Method = m
|
||||
}
|
||||
if m, ok := n.Settings["headers"].(map[string]interface{}); ok {
|
||||
settings.Headers = make(map[string]string)
|
||||
for k, v := range m {
|
||||
if s, ok := v.(string); ok {
|
||||
settings.Headers[k] = s
|
||||
}
|
||||
}
|
||||
}
|
||||
if m, ok := n.Settings["body_template"].(string); ok {
|
||||
settings.BodyTemplate = m
|
||||
}
|
||||
return settings
|
||||
case ProviderDiscord:
|
||||
var settings DiscordSettings
|
||||
if m, ok := n.Settings["webhook_url"].(string); ok {
|
||||
settings.WebhookURL = m
|
||||
}
|
||||
if m, ok := n.Settings["username"].(string); ok {
|
||||
settings.Username = m
|
||||
}
|
||||
if m, ok := n.Settings["avatar_url"].(string); ok {
|
||||
settings.AvatarURL = m
|
||||
}
|
||||
return settings
|
||||
case ProviderSlack:
|
||||
var settings SlackSettings
|
||||
if m, ok := n.Settings["webhook_url"].(string); ok {
|
||||
settings.WebhookURL = m
|
||||
}
|
||||
if m, ok := n.Settings["channel"].(string); ok {
|
||||
settings.Channel = m
|
||||
}
|
||||
if m, ok := n.Settings["username"].(string); ok {
|
||||
settings.Username = m
|
||||
}
|
||||
return settings
|
||||
case ProviderTelegram:
|
||||
var settings TelegramSettings
|
||||
if m, ok := n.Settings["bot_token"].(string); ok {
|
||||
settings.BotToken = m
|
||||
}
|
||||
if m, ok := n.Settings["chat_id"].(string); ok {
|
||||
settings.ChatID = m
|
||||
}
|
||||
return settings
|
||||
case ProviderGotify:
|
||||
var settings GotifySettings
|
||||
if m, ok := n.Settings["server_url"].(string); ok {
|
||||
settings.ServerURL = m
|
||||
}
|
||||
if m, ok := n.Settings["app_token"].(string); ok {
|
||||
settings.AppToken = m
|
||||
}
|
||||
if m, ok := n.Settings["priority"].(float64); ok {
|
||||
settings.Priority = int(m)
|
||||
}
|
||||
return settings
|
||||
case ProviderPushover:
|
||||
var settings PushoverSettings
|
||||
if m, ok := n.Settings["app_token"].(string); ok {
|
||||
settings.AppToken = m
|
||||
}
|
||||
if m, ok := n.Settings["user_key"].(string); ok {
|
||||
settings.UserKey = m
|
||||
}
|
||||
if m, ok := n.Settings["priority"].(float64); ok {
|
||||
settings.Priority = int(m)
|
||||
}
|
||||
if m, ok := n.Settings["device"].(string); ok {
|
||||
settings.Device = m
|
||||
}
|
||||
return settings
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// NotificationEvent represents a notification sent event
|
||||
type NotificationEvent struct {
|
||||
ID string `json:"id" db:"id"`
|
||||
NotificationID string `json:"notification" db:"notification"`
|
||||
MonitorID string `json:"monitor" db:"monitor"`
|
||||
Status string `json:"status" db:"status"`
|
||||
Message string `json:"message" db:"message"`
|
||||
SentAt time.Time `json:"sent_at" db:"sent_at"`
|
||||
Error string `json:"error" db:"error"`
|
||||
}
|
||||
@@ -0,0 +1,543 @@
|
||||
package smart
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Common types
|
||||
type VersionInfo [2]int
|
||||
|
||||
type SmartctlInfo struct {
|
||||
Version VersionInfo `json:"version"`
|
||||
SvnRevision string `json:"svn_revision"`
|
||||
PlatformInfo string `json:"platform_info"`
|
||||
BuildInfo string `json:"build_info"`
|
||||
Argv []string `json:"argv"`
|
||||
ExitStatus int `json:"exit_status"`
|
||||
}
|
||||
|
||||
type DeviceInfo struct {
|
||||
Name string `json:"name"`
|
||||
InfoName string `json:"info_name"`
|
||||
Type string `json:"type"`
|
||||
Protocol string `json:"protocol"`
|
||||
}
|
||||
|
||||
type UserCapacity struct {
|
||||
Blocks uint64 `json:"blocks"`
|
||||
Bytes uint64 `json:"bytes"`
|
||||
}
|
||||
|
||||
// type LocalTime struct {
|
||||
// TimeT int64 `json:"time_t"`
|
||||
// Asctime string `json:"asctime"`
|
||||
// }
|
||||
|
||||
// type WwnInfo struct {
|
||||
// Naa int `json:"naa"`
|
||||
// Oui int `json:"oui"`
|
||||
// ID int `json:"id"`
|
||||
// }
|
||||
|
||||
// type FormFactorInfo struct {
|
||||
// AtaValue int `json:"ata_value"`
|
||||
// Name string `json:"name"`
|
||||
// }
|
||||
|
||||
// type TrimInfo struct {
|
||||
// Supported bool `json:"supported"`
|
||||
// }
|
||||
|
||||
// type AtaVersionInfo struct {
|
||||
// String string `json:"string"`
|
||||
// MajorValue int `json:"major_value"`
|
||||
// MinorValue int `json:"minor_value"`
|
||||
// }
|
||||
|
||||
// type VersionStringInfo struct {
|
||||
// String string `json:"string"`
|
||||
// Value int `json:"value"`
|
||||
// }
|
||||
|
||||
// type SpeedInfo struct {
|
||||
// SataValue int `json:"sata_value"`
|
||||
// String string `json:"string"`
|
||||
// UnitsPerSecond int `json:"units_per_second"`
|
||||
// BitsPerUnit int `json:"bits_per_unit"`
|
||||
// }
|
||||
|
||||
// type InterfaceSpeedInfo struct {
|
||||
// Max SpeedInfo `json:"max"`
|
||||
// Current SpeedInfo `json:"current"`
|
||||
// }
|
||||
|
||||
type SmartStatusInfo struct {
|
||||
Passed bool `json:"passed"`
|
||||
}
|
||||
|
||||
type StatusInfo struct {
|
||||
Value int `json:"value"`
|
||||
String string `json:"string"`
|
||||
Passed bool `json:"passed"`
|
||||
}
|
||||
|
||||
type PollingMinutes struct {
|
||||
Short int `json:"short"`
|
||||
Extended int `json:"extended"`
|
||||
}
|
||||
|
||||
type CapabilitiesInfo struct {
|
||||
Values []int `json:"values"`
|
||||
ExecOfflineImmediateSupported bool `json:"exec_offline_immediate_supported"`
|
||||
OfflineIsAbortedUponNewCmd bool `json:"offline_is_aborted_upon_new_cmd"`
|
||||
OfflineSurfaceScanSupported bool `json:"offline_surface_scan_supported"`
|
||||
SelfTestsSupported bool `json:"self_tests_supported"`
|
||||
ConveyanceSelfTestSupported bool `json:"conveyance_self_test_supported"`
|
||||
SelectiveSelfTestSupported bool `json:"selective_self_test_supported"`
|
||||
AttributeAutosaveEnabled bool `json:"attribute_autosave_enabled"`
|
||||
ErrorLoggingSupported bool `json:"error_logging_supported"`
|
||||
GpLoggingSupported bool `json:"gp_logging_supported"`
|
||||
}
|
||||
|
||||
// type AtaSmartData struct {
|
||||
// OfflineDataCollection OfflineDataCollectionInfo `json:"offline_data_collection"`
|
||||
// SelfTest SelfTestInfo `json:"self_test"`
|
||||
// Capabilities CapabilitiesInfo `json:"capabilities"`
|
||||
// }
|
||||
|
||||
// type OfflineDataCollectionInfo struct {
|
||||
// Status StatusInfo `json:"status"`
|
||||
// CompletionSeconds int `json:"completion_seconds"`
|
||||
// }
|
||||
|
||||
// type SelfTestInfo struct {
|
||||
// Status StatusInfo `json:"status"`
|
||||
// PollingMinutes PollingMinutes `json:"polling_minutes"`
|
||||
// }
|
||||
|
||||
// type AtaSctCapabilities struct {
|
||||
// Value int `json:"value"`
|
||||
// ErrorRecoveryControlSupported bool `json:"error_recovery_control_supported"`
|
||||
// FeatureControlSupported bool `json:"feature_control_supported"`
|
||||
// DataTableSupported bool `json:"data_table_supported"`
|
||||
// }
|
||||
|
||||
type SummaryInfo struct {
|
||||
Revision int `json:"revision"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
type AtaSmartAttributes struct {
|
||||
Table []AtaSmartAttribute `json:"table"`
|
||||
}
|
||||
|
||||
type AtaDeviceStatistics struct {
|
||||
Pages []AtaDeviceStatisticsPage `json:"pages"`
|
||||
}
|
||||
|
||||
type AtaDeviceStatisticsPage struct {
|
||||
Number uint8 `json:"number"`
|
||||
Table []AtaDeviceStatisticsEntry `json:"table"`
|
||||
}
|
||||
|
||||
type AtaDeviceStatisticsEntry struct {
|
||||
Name string `json:"name"`
|
||||
Value *int64 `json:"value,omitempty"`
|
||||
}
|
||||
|
||||
type AtaSmartAttribute struct {
|
||||
ID uint16 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Value uint16 `json:"value"`
|
||||
Worst uint16 `json:"worst"`
|
||||
Thresh uint16 `json:"thresh"`
|
||||
WhenFailed string `json:"when_failed"`
|
||||
// Flags AttributeFlags `json:"flags"`
|
||||
Raw RawValue `json:"raw"`
|
||||
}
|
||||
|
||||
// type AttributeFlags struct {
|
||||
// Value int `json:"value"`
|
||||
// String string `json:"string"`
|
||||
// Prefailure bool `json:"prefailure"`
|
||||
// UpdatedOnline bool `json:"updated_online"`
|
||||
// Performance bool `json:"performance"`
|
||||
// ErrorRate bool `json:"error_rate"`
|
||||
// EventCount bool `json:"event_count"`
|
||||
// AutoKeep bool `json:"auto_keep"`
|
||||
// }
|
||||
|
||||
type RawValue struct {
|
||||
Value SmartRawValue `json:"value"`
|
||||
String string `json:"string"`
|
||||
}
|
||||
|
||||
func (r *RawValue) UnmarshalJSON(data []byte) error {
|
||||
var tmp struct {
|
||||
Value json.RawMessage `json:"value"`
|
||||
String string `json:"string"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(data, &tmp); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(tmp.Value) > 0 {
|
||||
if err := r.Value.UnmarshalJSON(tmp.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
r.Value = 0
|
||||
}
|
||||
|
||||
r.String = tmp.String
|
||||
|
||||
if parsed, ok := ParseSmartRawValueString(tmp.String); ok {
|
||||
r.Value = SmartRawValue(parsed)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type SmartRawValue uint64
|
||||
|
||||
// handles when drives report strings like "0h+0m+0.000s" or "7344 (253d 8h)" for power on hours
|
||||
func (v *SmartRawValue) UnmarshalJSON(data []byte) error {
|
||||
trimmed := strings.TrimSpace(string(data))
|
||||
if len(trimmed) == 0 || trimmed == "null" {
|
||||
*v = 0
|
||||
return nil
|
||||
}
|
||||
|
||||
if trimmed[0] == '"' {
|
||||
valueStr, err := strconv.Unquote(trimmed)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
parsed, ok := ParseSmartRawValueString(valueStr)
|
||||
if ok {
|
||||
*v = SmartRawValue(parsed)
|
||||
return nil
|
||||
}
|
||||
*v = 0
|
||||
return nil
|
||||
}
|
||||
|
||||
if parsed, err := strconv.ParseUint(trimmed, 0, 64); err == nil {
|
||||
*v = SmartRawValue(parsed)
|
||||
return nil
|
||||
}
|
||||
|
||||
if parsed, ok := ParseSmartRawValueString(trimmed); ok {
|
||||
*v = SmartRawValue(parsed)
|
||||
return nil
|
||||
}
|
||||
|
||||
*v = 0
|
||||
return nil
|
||||
}
|
||||
|
||||
// ParseSmartRawValueString attempts to extract a numeric value from the raw value
|
||||
// strings emitted by smartctl, which sometimes include human-friendly annotations
|
||||
// like "7344 (253d 8h)" or "0h+0m+0.000s". It returns the parsed value and a
|
||||
// boolean indicating success.
|
||||
func ParseSmartRawValueString(value string) (uint64, bool) {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
if parsed, err := strconv.ParseUint(value, 0, 64); err == nil {
|
||||
return parsed, true
|
||||
}
|
||||
|
||||
if idx := strings.IndexRune(value, 'h'); idx > 0 {
|
||||
hoursPart := strings.TrimSpace(value[:idx])
|
||||
if hoursPart != "" {
|
||||
if parsed, err := strconv.ParseFloat(hoursPart, 64); err == nil {
|
||||
return uint64(parsed), true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for i := 0; i < len(value); i++ {
|
||||
if value[i] < '0' || value[i] > '9' {
|
||||
continue
|
||||
}
|
||||
end := i + 1
|
||||
for end < len(value) && value[end] >= '0' && value[end] <= '9' {
|
||||
end++
|
||||
}
|
||||
digits := value[i:end]
|
||||
if parsed, err := strconv.ParseUint(digits, 10, 64); err == nil {
|
||||
return parsed, true
|
||||
}
|
||||
i = end
|
||||
}
|
||||
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// type PowerOnTimeInfo struct {
|
||||
// Hours uint32 `json:"hours"`
|
||||
// }
|
||||
|
||||
type TemperatureInfo struct {
|
||||
Current uint8 `json:"current"`
|
||||
}
|
||||
|
||||
type TemperatureInfoScsi struct {
|
||||
Current uint8 `json:"current"`
|
||||
DriveTrip uint8 `json:"drive_trip"`
|
||||
}
|
||||
|
||||
// type SelectiveSelfTestTable struct {
|
||||
// LbaMin int `json:"lba_min"`
|
||||
// LbaMax int `json:"lba_max"`
|
||||
// Status StatusInfo `json:"status"`
|
||||
// }
|
||||
|
||||
// type SelectiveSelfTestFlags struct {
|
||||
// Value int `json:"value"`
|
||||
// RemainderScanEnabled bool `json:"remainder_scan_enabled"`
|
||||
// }
|
||||
|
||||
// type AtaSmartSelectiveSelfTestLog struct {
|
||||
// Revision int `json:"revision"`
|
||||
// Table []SelectiveSelfTestTable `json:"table"`
|
||||
// Flags SelectiveSelfTestFlags `json:"flags"`
|
||||
// PowerUpScanResumeMinutes int `json:"power_up_scan_resume_minutes"`
|
||||
// }
|
||||
|
||||
// BaseSmartInfo contains common fields shared between SATA and NVMe drives
|
||||
// type BaseSmartInfo struct {
|
||||
// Device DeviceInfo `json:"device"`
|
||||
// ModelName string `json:"model_name"`
|
||||
// SerialNumber string `json:"serial_number"`
|
||||
// FirmwareVersion string `json:"firmware_version"`
|
||||
// UserCapacity UserCapacity `json:"user_capacity"`
|
||||
// LogicalBlockSize int `json:"logical_block_size"`
|
||||
// LocalTime LocalTime `json:"local_time"`
|
||||
// }
|
||||
|
||||
type SmartctlInfoLegacy struct {
|
||||
Version VersionInfo `json:"version"`
|
||||
SvnRevision string `json:"svn_revision"`
|
||||
PlatformInfo string `json:"platform_info"`
|
||||
BuildInfo string `json:"build_info"`
|
||||
Argv []string `json:"argv"`
|
||||
ExitStatus int `json:"exit_status"`
|
||||
}
|
||||
|
||||
type SmartInfoForSata struct {
|
||||
// JSONFormatVersion VersionInfo `json:"json_format_version"`
|
||||
Smartctl SmartctlInfoLegacy `json:"smartctl"`
|
||||
Device DeviceInfo `json:"device"`
|
||||
// ModelFamily string `json:"model_family"`
|
||||
ModelName string `json:"model_name"`
|
||||
SerialNumber string `json:"serial_number"`
|
||||
// Wwn WwnInfo `json:"wwn"`
|
||||
FirmwareVersion string `json:"firmware_version"`
|
||||
UserCapacity UserCapacity `json:"user_capacity"`
|
||||
ScsiVendor string `json:"scsi_vendor"`
|
||||
ScsiProduct string `json:"scsi_product"`
|
||||
// LogicalBlockSize int `json:"logical_block_size"`
|
||||
// PhysicalBlockSize int `json:"physical_block_size"`
|
||||
// RotationRate int `json:"rotation_rate"`
|
||||
// FormFactor FormFactorInfo `json:"form_factor"`
|
||||
// Trim TrimInfo `json:"trim"`
|
||||
// InSmartctlDatabase bool `json:"in_smartctl_database"`
|
||||
// AtaVersion AtaVersionInfo `json:"ata_version"`
|
||||
// SataVersion VersionStringInfo `json:"sata_version"`
|
||||
// InterfaceSpeed InterfaceSpeedInfo `json:"interface_speed"`
|
||||
// LocalTime LocalTime `json:"local_time"`
|
||||
SmartStatus SmartStatusInfo `json:"smart_status"`
|
||||
// AtaSmartData AtaSmartData `json:"ata_smart_data"`
|
||||
// AtaSctCapabilities AtaSctCapabilities `json:"ata_sct_capabilities"`
|
||||
AtaSmartAttributes AtaSmartAttributes `json:"ata_smart_attributes"`
|
||||
AtaDeviceStatistics json.RawMessage `json:"ata_device_statistics"`
|
||||
// PowerOnTime PowerOnTimeInfo `json:"power_on_time"`
|
||||
// PowerCycleCount uint16 `json:"power_cycle_count"`
|
||||
Temperature TemperatureInfo `json:"temperature"`
|
||||
// AtaSmartErrorLog AtaSmartErrorLog `json:"ata_smart_error_log"`
|
||||
// AtaSmartSelfTestLog AtaSmartSelfTestLog `json:"ata_smart_self_test_log"`
|
||||
// AtaSmartSelectiveSelfTestLog AtaSmartSelectiveSelfTestLog `json:"ata_smart_selective_self_test_log"`
|
||||
}
|
||||
|
||||
type ScsiErrorCounter struct {
|
||||
ErrorsCorrectedByECCFast uint64 `json:"errors_corrected_by_eccfast"`
|
||||
ErrorsCorrectedByECCDelayed uint64 `json:"errors_corrected_by_eccdelayed"`
|
||||
ErrorsCorrectedByRereadsRewrites uint64 `json:"errors_corrected_by_rereads_rewrites"`
|
||||
TotalErrorsCorrected uint64 `json:"total_errors_corrected"`
|
||||
CorrectionAlgorithmInvocations uint64 `json:"correction_algorithm_invocations"`
|
||||
GigabytesProcessed string `json:"gigabytes_processed"`
|
||||
TotalUncorrectedErrors uint64 `json:"total_uncorrected_errors"`
|
||||
}
|
||||
|
||||
type ScsiErrorCounterLog struct {
|
||||
Read ScsiErrorCounter `json:"read"`
|
||||
Write ScsiErrorCounter `json:"write"`
|
||||
Verify ScsiErrorCounter `json:"verify"`
|
||||
}
|
||||
|
||||
type ScsiStartStopCycleCounter struct {
|
||||
YearOfManufacture string `json:"year_of_manufacture"`
|
||||
WeekOfManufacture string `json:"week_of_manufacture"`
|
||||
SpecifiedCycleCountOverDeviceLifetime uint64 `json:"specified_cycle_count_over_device_lifetime"`
|
||||
AccumulatedStartStopCycles uint64 `json:"accumulated_start_stop_cycles"`
|
||||
SpecifiedLoadUnloadCountOverDeviceLifetime uint64 `json:"specified_load_unload_count_over_device_lifetime"`
|
||||
AccumulatedLoadUnloadCycles uint64 `json:"accumulated_load_unload_cycles"`
|
||||
}
|
||||
|
||||
type PowerOnTimeScsi struct {
|
||||
Hours uint64 `json:"hours"`
|
||||
Minutes uint64 `json:"minutes"`
|
||||
}
|
||||
|
||||
type SmartInfoForScsi struct {
|
||||
Smartctl SmartctlInfoLegacy `json:"smartctl"`
|
||||
Device DeviceInfo `json:"device"`
|
||||
ScsiVendor string `json:"scsi_vendor"`
|
||||
ScsiProduct string `json:"scsi_product"`
|
||||
ScsiModelName string `json:"scsi_model_name"`
|
||||
ScsiRevision string `json:"scsi_revision"`
|
||||
ScsiVersion string `json:"scsi_version"`
|
||||
SerialNumber string `json:"serial_number"`
|
||||
UserCapacity UserCapacity `json:"user_capacity"`
|
||||
Temperature TemperatureInfoScsi `json:"temperature"`
|
||||
SmartStatus SmartStatusInfo `json:"smart_status"`
|
||||
PowerOnTime PowerOnTimeScsi `json:"power_on_time"`
|
||||
ScsiStartStopCycleCounter ScsiStartStopCycleCounter `json:"scsi_start_stop_cycle_counter"`
|
||||
ScsiGrownDefectList uint64 `json:"scsi_grown_defect_list"`
|
||||
ScsiErrorCounterLog ScsiErrorCounterLog `json:"scsi_error_counter_log"`
|
||||
}
|
||||
|
||||
// type AtaSmartErrorLog struct {
|
||||
// Summary SummaryInfo `json:"summary"`
|
||||
// }
|
||||
|
||||
// type AtaSmartSelfTestLog struct {
|
||||
// Standard SummaryInfo `json:"standard"`
|
||||
// }
|
||||
|
||||
type SmartctlInfoNvme struct {
|
||||
Version VersionInfo `json:"version"`
|
||||
SVNRevision string `json:"svn_revision"`
|
||||
PlatformInfo string `json:"platform_info"`
|
||||
BuildInfo string `json:"build_info"`
|
||||
Argv []string `json:"argv"`
|
||||
ExitStatus int `json:"exit_status"`
|
||||
}
|
||||
|
||||
// type NVMePCIVendor struct {
|
||||
// ID int `json:"id"`
|
||||
// SubsystemID int `json:"subsystem_id"`
|
||||
// }
|
||||
|
||||
// type SizeCapacityInfo struct {
|
||||
// Blocks uint64 `json:"blocks"`
|
||||
// Bytes uint64 `json:"bytes"`
|
||||
// }
|
||||
|
||||
// type EUI64Info struct {
|
||||
// OUI int `json:"oui"`
|
||||
// ExtID int `json:"ext_id"`
|
||||
// }
|
||||
|
||||
// type NVMeNamespace struct {
|
||||
// ID uint32 `json:"id"`
|
||||
// Size SizeCapacityInfo `json:"size"`
|
||||
// Capacity SizeCapacityInfo `json:"capacity"`
|
||||
// Utilization SizeCapacityInfo `json:"utilization"`
|
||||
// FormattedLBASize uint32 `json:"formatted_lba_size"`
|
||||
// EUI64 EUI64Info `json:"eui64"`
|
||||
// }
|
||||
|
||||
type SmartStatusInfoNvme struct {
|
||||
Passed bool `json:"passed"`
|
||||
NVMe SmartStatusNVMe `json:"nvme"`
|
||||
}
|
||||
|
||||
type SmartStatusNVMe struct {
|
||||
Value int `json:"value"`
|
||||
}
|
||||
|
||||
type NVMeSmartHealthInformationLog struct {
|
||||
CriticalWarning uint `json:"critical_warning"`
|
||||
Temperature uint8 `json:"temperature"`
|
||||
AvailableSpare uint `json:"available_spare"`
|
||||
AvailableSpareThreshold uint `json:"available_spare_threshold"`
|
||||
PercentageUsed uint8 `json:"percentage_used"`
|
||||
DataUnitsRead uint64 `json:"data_units_read"`
|
||||
DataUnitsWritten uint64 `json:"data_units_written"`
|
||||
HostReads uint `json:"host_reads"`
|
||||
HostWrites uint `json:"host_writes"`
|
||||
ControllerBusyTime uint `json:"controller_busy_time"`
|
||||
PowerCycles uint16 `json:"power_cycles"`
|
||||
PowerOnHours uint32 `json:"power_on_hours"`
|
||||
UnsafeShutdowns uint16 `json:"unsafe_shutdowns"`
|
||||
MediaErrors uint `json:"media_errors"`
|
||||
NumErrLogEntries uint `json:"num_err_log_entries"`
|
||||
WarningTempTime uint `json:"warning_temp_time"`
|
||||
CriticalCompTime uint `json:"critical_comp_time"`
|
||||
TemperatureSensors []uint8 `json:"temperature_sensors"`
|
||||
}
|
||||
|
||||
type SmartInfoForNvme struct {
|
||||
// JSONFormatVersion VersionInfo `json:"json_format_version"`
|
||||
Smartctl SmartctlInfoNvme `json:"smartctl"`
|
||||
Device DeviceInfo `json:"device"`
|
||||
ModelName string `json:"model_name"`
|
||||
SerialNumber string `json:"serial_number"`
|
||||
FirmwareVersion string `json:"firmware_version"`
|
||||
// NVMePCIVendor NVMePCIVendor `json:"nvme_pci_vendor"`
|
||||
// NVMeIEEEOUIIdentifier uint32 `json:"nvme_ieee_oui_identifier"`
|
||||
NVMeTotalCapacity uint64 `json:"nvme_total_capacity"`
|
||||
// NVMeUnallocatedCapacity uint64 `json:"nvme_unallocated_capacity"`
|
||||
// NVMeControllerID uint16 `json:"nvme_controller_id"`
|
||||
// NVMeVersion VersionStringInfo `json:"nvme_version"`
|
||||
// NVMeNumberOfNamespaces uint8 `json:"nvme_number_of_namespaces"`
|
||||
// NVMeNamespaces []NVMeNamespace `json:"nvme_namespaces"`
|
||||
UserCapacity UserCapacity `json:"user_capacity"`
|
||||
// LogicalBlockSize int `json:"logical_block_size"`
|
||||
// LocalTime LocalTime `json:"local_time"`
|
||||
SmartStatus SmartStatusInfoNvme `json:"smart_status"`
|
||||
NVMeSmartHealthInformationLog NVMeSmartHealthInformationLog `json:"nvme_smart_health_information_log"`
|
||||
Temperature TemperatureInfoNvme `json:"temperature"`
|
||||
PowerCycleCount uint16 `json:"power_cycle_count"`
|
||||
PowerOnTime PowerOnTimeInfoNvme `json:"power_on_time"`
|
||||
}
|
||||
|
||||
type TemperatureInfoNvme struct {
|
||||
Current int `json:"current"`
|
||||
}
|
||||
|
||||
type PowerOnTimeInfoNvme struct {
|
||||
Hours int `json:"hours"`
|
||||
}
|
||||
|
||||
type SmartData struct {
|
||||
// ModelFamily string `json:"mf,omitempty" cbor:"0,keyasint,omitempty"`
|
||||
ModelName string `json:"mn,omitempty" cbor:"1,keyasint,omitempty"`
|
||||
SerialNumber string `json:"sn,omitempty" cbor:"2,keyasint,omitempty"`
|
||||
FirmwareVersion string `json:"fv,omitempty" cbor:"3,keyasint,omitempty"`
|
||||
Capacity uint64 `json:"c,omitempty" cbor:"4,keyasint,omitempty"`
|
||||
SmartStatus string `json:"s,omitempty" cbor:"5,keyasint,omitempty"`
|
||||
DiskName string `json:"dn,omitempty" cbor:"6,keyasint,omitempty"`
|
||||
DiskType string `json:"dt,omitempty" cbor:"7,keyasint,omitempty"`
|
||||
Temperature uint8 `json:"t,omitempty" cbor:"8,keyasint,omitempty"`
|
||||
Attributes []*SmartAttribute `json:"a,omitempty" cbor:"9,keyasint,omitempty"`
|
||||
}
|
||||
|
||||
type SmartAttribute struct {
|
||||
ID uint16 `json:"id,omitempty" cbor:"0,keyasint,omitempty"`
|
||||
Name string `json:"n" cbor:"1,keyasint"`
|
||||
Value uint16 `json:"v,omitempty" cbor:"2,keyasint,omitempty"`
|
||||
Worst uint16 `json:"w,omitempty" cbor:"3,keyasint,omitempty"`
|
||||
Threshold uint16 `json:"t,omitempty" cbor:"4,keyasint,omitempty"`
|
||||
RawValue uint64 `json:"rv" cbor:"5,keyasint"`
|
||||
RawString string `json:"rs,omitempty" cbor:"6,keyasint,omitempty"`
|
||||
WhenFailed string `json:"wf,omitempty" cbor:"7,keyasint,omitempty"`
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package smart
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestSmartRawValueUnmarshalDuration(t *testing.T) {
|
||||
input := []byte(`{"value":"62312h+33m+50.907s","string":"62312h+33m+50.907s"}`)
|
||||
var raw RawValue
|
||||
err := json.Unmarshal(input, &raw)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.EqualValues(t, 62312, raw.Value)
|
||||
}
|
||||
|
||||
func TestSmartRawValueUnmarshalNumericString(t *testing.T) {
|
||||
input := []byte(`{"value":"7344","string":"7344"}`)
|
||||
var raw RawValue
|
||||
err := json.Unmarshal(input, &raw)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.EqualValues(t, 7344, raw.Value)
|
||||
}
|
||||
|
||||
func TestSmartRawValueUnmarshalParenthetical(t *testing.T) {
|
||||
input := []byte(`{"value":"39925 (212 206 0)","string":"39925 (212 206 0)"}`)
|
||||
var raw RawValue
|
||||
err := json.Unmarshal(input, &raw)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.EqualValues(t, 39925, raw.Value)
|
||||
}
|
||||
|
||||
func TestSmartRawValueUnmarshalDurationWithFractions(t *testing.T) {
|
||||
input := []byte(`{"value":"2748h+31m+49.560s","string":"2748h+31m+49.560s"}`)
|
||||
var raw RawValue
|
||||
err := json.Unmarshal(input, &raw)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.EqualValues(t, 2748, raw.Value)
|
||||
}
|
||||
|
||||
func TestSmartRawValueUnmarshalParentheticalRawValue(t *testing.T) {
|
||||
input := []byte(`{"value":57891864217128,"string":"39925 (212 206 0)"}`)
|
||||
var raw RawValue
|
||||
err := json.Unmarshal(input, &raw)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.EqualValues(t, 39925, raw.Value)
|
||||
}
|
||||
|
||||
func TestSmartRawValueUnmarshalDurationRawValue(t *testing.T) {
|
||||
input := []byte(`{"value":57891864217128,"string":"2748h+31m+49.560s"}`)
|
||||
var raw RawValue
|
||||
err := json.Unmarshal(input, &raw)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.EqualValues(t, 2748, raw.Value)
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
package statuspage
|
||||
|
||||
import "time"
|
||||
|
||||
// StatusPage represents a public status page configuration
|
||||
type StatusPage struct {
|
||||
ID string `json:"id" db:"id"`
|
||||
Name string `json:"name" db:"name"`
|
||||
Slug string `json:"slug" db:"slug"`
|
||||
Title string `json:"title" db:"title"`
|
||||
Description string `json:"description" db:"description"`
|
||||
Logo string `json:"logo" db:"logo"`
|
||||
Favicon string `json:"favicon" db:"favicon"`
|
||||
Theme string `json:"theme" db:"theme"` // light, dark, auto
|
||||
CustomCSS string `json:"custom_css" db:"custom_css"`
|
||||
Public bool `json:"public" db:"public"`
|
||||
ShowUptime bool `json:"show_uptime" db:"show_uptime"`
|
||||
UserID string `json:"user" db:"user"`
|
||||
Created time.Time `json:"created" db:"created"`
|
||||
Updated time.Time `json:"updated" db:"updated"`
|
||||
}
|
||||
|
||||
// StatusPageMonitor links monitors to a status page
|
||||
type StatusPageMonitor struct {
|
||||
ID string `json:"id" db:"id"`
|
||||
StatusPageID string `json:"status_page" db:"status_page"`
|
||||
MonitorID string `json:"monitor" db:"monitor"`
|
||||
DisplayName string `json:"display_name" db:"display_name"`
|
||||
Group string `json:"group" db:"group"`
|
||||
SortOrder int `json:"sort_order" db:"sort_order"`
|
||||
UserID string `json:"user" db:"user"`
|
||||
}
|
||||
|
||||
// PublicStatusPage represents a status page for public viewing
|
||||
type PublicStatusPage struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Logo string `json:"logo"`
|
||||
Favicon string `json:"favicon"`
|
||||
Theme string `json:"theme"`
|
||||
CustomCSS string `json:"custom_css,omitempty"`
|
||||
Monitors []PublicMonitorStatus `json:"monitors"`
|
||||
OverallStatus string `json:"overall_status"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// PublicMonitorStatus represents a monitor's status for public display
|
||||
type PublicMonitorStatus struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
DisplayName string `json:"display_name"`
|
||||
Group string `json:"group"`
|
||||
Status string `json:"status"`
|
||||
Uptime24h float64 `json:"uptime_24h"`
|
||||
Uptime7d float64 `json:"uptime_7d"`
|
||||
Uptime30d float64 `json:"uptime_30d"`
|
||||
LastCheck time.Time `json:"last_check"`
|
||||
}
|
||||
|
||||
// CreateStatusPageRequest represents a status page creation request
|
||||
type CreateStatusPageRequest struct {
|
||||
Name string `json:"name"`
|
||||
Slug string `json:"slug"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Logo string `json:"logo,omitempty"`
|
||||
Favicon string `json:"favicon,omitempty"`
|
||||
Theme string `json:"theme,omitempty"`
|
||||
CustomCSS string `json:"custom_css,omitempty"`
|
||||
Public bool `json:"public"`
|
||||
ShowUptime bool `json:"show_uptime,omitempty"`
|
||||
}
|
||||
|
||||
// UpdateStatusPageRequest represents a status page update request
|
||||
type UpdateStatusPageRequest struct {
|
||||
Name *string `json:"name,omitempty"`
|
||||
Title *string `json:"title,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
Logo *string `json:"logo,omitempty"`
|
||||
Favicon *string `json:"favicon,omitempty"`
|
||||
Theme *string `json:"theme,omitempty"`
|
||||
CustomCSS *string `json:"custom_css,omitempty"`
|
||||
Public *bool `json:"public,omitempty"`
|
||||
ShowUptime *bool `json:"show_uptime,omitempty"`
|
||||
}
|
||||
|
||||
// StatusPageMonitorRequest represents adding a monitor to a status page
|
||||
type StatusPageMonitorRequest struct {
|
||||
MonitorID string `json:"monitor"`
|
||||
DisplayName string `json:"display_name,omitempty"`
|
||||
Group string `json:"group,omitempty"`
|
||||
SortOrder int `json:"sort_order,omitempty"`
|
||||
}
|
||||
|
||||
// StatusPageResponse represents a status page response
|
||||
type StatusPageResponse struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Slug string `json:"slug"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Logo string `json:"logo"`
|
||||
Favicon string `json:"favicon"`
|
||||
Theme string `json:"theme"`
|
||||
Public bool `json:"public"`
|
||||
ShowUptime bool `json:"show_uptime"`
|
||||
MonitorCount int `json:"monitor_count"`
|
||||
Created string `json:"created"`
|
||||
Updated string `json:"updated"`
|
||||
}
|
||||
|
||||
// Overall status constants
|
||||
const (
|
||||
StatusOperational = "operational"
|
||||
StatusDegraded = "degraded"
|
||||
StatusPartial = "partial_outage"
|
||||
StatusMajor = "major_outage"
|
||||
)
|
||||
|
||||
// Theme constants
|
||||
const (
|
||||
ThemeLight = "light"
|
||||
ThemeDark = "dark"
|
||||
ThemeAuto = "auto"
|
||||
)
|
||||
|
||||
// ValidateTheme validates and returns a theme
|
||||
func ValidateTheme(theme string) string {
|
||||
switch theme {
|
||||
case ThemeLight, ThemeDark, ThemeAuto:
|
||||
return theme
|
||||
default:
|
||||
return ThemeAuto
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
package system
|
||||
|
||||
// TODO: this is confusing, make common package with common/types common/helpers etc
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/henrygd/beszel/internal/entities/container"
|
||||
"github.com/henrygd/beszel/internal/entities/systemd"
|
||||
)
|
||||
|
||||
type Stats struct {
|
||||
Cpu float64 `json:"cpu" cbor:"0,keyasint"`
|
||||
MaxCpu float64 `json:"cpum,omitempty" cbor:"-"`
|
||||
Mem float64 `json:"m" cbor:"2,keyasint"`
|
||||
MaxMem float64 `json:"mm,omitempty" cbor:"-"`
|
||||
MemUsed float64 `json:"mu" cbor:"3,keyasint"`
|
||||
MemPct float64 `json:"mp" cbor:"4,keyasint"`
|
||||
MemBuffCache float64 `json:"mb" cbor:"5,keyasint"`
|
||||
MemZfsArc float64 `json:"mz,omitempty" cbor:"6,keyasint,omitempty"` // ZFS ARC memory
|
||||
Swap float64 `json:"s,omitempty" cbor:"7,keyasint,omitempty"`
|
||||
SwapUsed float64 `json:"su,omitempty" cbor:"8,keyasint,omitempty"`
|
||||
DiskTotal float64 `json:"d" cbor:"9,keyasint"`
|
||||
DiskUsed float64 `json:"du" cbor:"10,keyasint"`
|
||||
DiskPct float64 `json:"dp" cbor:"11,keyasint"`
|
||||
DiskReadPs float64 `json:"dr,omitzero" cbor:"12,keyasint,omitzero"`
|
||||
DiskWritePs float64 `json:"dw,omitzero" cbor:"13,keyasint,omitzero"`
|
||||
MaxDiskReadPs float64 `json:"drm,omitempty" cbor:"-"`
|
||||
MaxDiskWritePs float64 `json:"dwm,omitempty" cbor:"-"`
|
||||
NetworkSent float64 `json:"ns,omitzero" cbor:"16,keyasint,omitzero"`
|
||||
NetworkRecv float64 `json:"nr,omitzero" cbor:"17,keyasint,omitzero"`
|
||||
MaxNetworkSent float64 `json:"nsm,omitempty" cbor:"-"`
|
||||
MaxNetworkRecv float64 `json:"nrm,omitempty" cbor:"-"`
|
||||
Temperatures map[string]float64 `json:"t,omitempty" cbor:"20,keyasint,omitempty"`
|
||||
ExtraFs map[string]*FsStats `json:"efs,omitempty" cbor:"21,keyasint,omitempty"`
|
||||
GPUData map[string]GPUData `json:"g,omitempty" cbor:"22,keyasint,omitempty"`
|
||||
// LoadAvg1 float64 `json:"l1,omitempty" cbor:"23,keyasint,omitempty"`
|
||||
// LoadAvg5 float64 `json:"l5,omitempty" cbor:"24,keyasint,omitempty"`
|
||||
// LoadAvg15 float64 `json:"l15,omitempty" cbor:"25,keyasint,omitempty"`
|
||||
Bandwidth [2]uint64 `json:"b,omitzero" cbor:"26,keyasint,omitzero"` // [sent bytes, recv bytes]
|
||||
MaxBandwidth [2]uint64 `json:"bm,omitzero" cbor:"-"` // [sent bytes, recv bytes]
|
||||
// TODO: remove other load fields in future release in favor of load avg array
|
||||
LoadAvg [3]float64 `json:"la,omitempty" cbor:"28,keyasint"`
|
||||
Battery [2]uint8 `json:"bat,omitzero" cbor:"29,keyasint,omitzero"` // [percent, charge state, current]
|
||||
NetworkInterfaces map[string][4]uint64 `json:"ni,omitempty" cbor:"31,keyasint,omitempty"` // [upload bytes, download bytes, total upload, total download]
|
||||
DiskIO [2]uint64 `json:"dio,omitzero" cbor:"32,keyasint,omitzero"` // [read bytes, write bytes]
|
||||
MaxDiskIO [2]uint64 `json:"diom,omitzero" cbor:"-"` // [max read bytes, max write bytes]
|
||||
CpuBreakdown []float64 `json:"cpub,omitempty" cbor:"33,keyasint,omitempty"` // [user, system, iowait, steal, idle]
|
||||
CpuCoresUsage Uint8Slice `json:"cpus,omitempty" cbor:"34,keyasint,omitempty"` // per-core busy usage [CPU0..]
|
||||
DiskIoStats [6]float64 `json:"dios,omitzero" cbor:"35,keyasint,omitzero"` // [read time %, write time %, io utilization %, r_await ms, w_await ms, weighted io %]
|
||||
MaxDiskIoStats [6]float64 `json:"diosm,omitzero" cbor:"-"` // max values for DiskIoStats
|
||||
}
|
||||
|
||||
// Uint8Slice wraps []uint8 to customize JSON encoding while keeping CBOR efficient.
|
||||
// JSON: encodes as array of numbers (avoids base64 string).
|
||||
// CBOR: falls back to default handling for []uint8 (byte string), keeping payload small.
|
||||
type Uint8Slice []uint8
|
||||
|
||||
func (s Uint8Slice) MarshalJSON() ([]byte, error) {
|
||||
if s == nil {
|
||||
return []byte("null"), nil
|
||||
}
|
||||
// Convert to wider ints to force array-of-numbers encoding.
|
||||
arr := make([]uint16, len(s))
|
||||
for i, v := range s {
|
||||
arr[i] = uint16(v)
|
||||
}
|
||||
return json.Marshal(arr)
|
||||
}
|
||||
|
||||
type GPUData struct {
|
||||
Name string `json:"n" cbor:"0,keyasint"`
|
||||
Temperature float64 `json:"-"`
|
||||
MemoryUsed float64 `json:"mu,omitempty,omitzero" cbor:"1,keyasint,omitempty,omitzero"`
|
||||
MemoryTotal float64 `json:"mt,omitempty,omitzero" cbor:"2,keyasint,omitempty,omitzero"`
|
||||
Usage float64 `json:"u" cbor:"3,keyasint,omitempty"`
|
||||
Power float64 `json:"p,omitempty" cbor:"4,keyasint,omitempty"`
|
||||
Count float64 `json:"-"`
|
||||
Engines map[string]float64 `json:"e,omitempty" cbor:"5,keyasint,omitempty"`
|
||||
PowerPkg float64 `json:"pp,omitempty" cbor:"6,keyasint,omitempty"`
|
||||
}
|
||||
|
||||
type FsStats struct {
|
||||
Time time.Time `json:"-"`
|
||||
Root bool `json:"-"`
|
||||
Mountpoint string `json:"-"`
|
||||
Name string `json:"-"`
|
||||
DiskTotal float64 `json:"d" cbor:"0,keyasint"`
|
||||
DiskUsed float64 `json:"du" cbor:"1,keyasint"`
|
||||
TotalRead uint64 `json:"-"`
|
||||
TotalWrite uint64 `json:"-"`
|
||||
DiskReadPs float64 `json:"r" cbor:"2,keyasint"`
|
||||
DiskWritePs float64 `json:"w" cbor:"3,keyasint"`
|
||||
MaxDiskReadPS float64 `json:"rm,omitempty" cbor:"-"`
|
||||
MaxDiskWritePS float64 `json:"wm,omitempty" cbor:"-"`
|
||||
// TODO: remove DiskReadPs and DiskWritePs in future release in favor of DiskReadBytes and DiskWriteBytes
|
||||
DiskReadBytes uint64 `json:"rb" cbor:"6,keyasint,omitempty"`
|
||||
DiskWriteBytes uint64 `json:"wb" cbor:"7,keyasint,omitempty"`
|
||||
MaxDiskReadBytes uint64 `json:"rbm,omitempty" cbor:"-"`
|
||||
MaxDiskWriteBytes uint64 `json:"wbm,omitempty" cbor:"-"`
|
||||
DiskIoStats [6]float64 `json:"dios,omitzero" cbor:"8,keyasint,omitzero"` // [read time %, write time %, io utilization %, r_await ms, w_await ms, weighted io %]
|
||||
MaxDiskIoStats [6]float64 `json:"diosm,omitzero" cbor:"-"` // max values for DiskIoStats
|
||||
}
|
||||
|
||||
type NetIoStats struct {
|
||||
BytesRecv uint64
|
||||
BytesSent uint64
|
||||
Time time.Time
|
||||
Name string
|
||||
}
|
||||
|
||||
type Os = uint8
|
||||
|
||||
const (
|
||||
Linux Os = iota
|
||||
Darwin
|
||||
Windows
|
||||
Freebsd
|
||||
)
|
||||
|
||||
type ConnectionType = uint8
|
||||
|
||||
const (
|
||||
ConnectionTypeNone ConnectionType = iota
|
||||
ConnectionTypeSSH
|
||||
ConnectionTypeWebSocket
|
||||
)
|
||||
|
||||
// Core system data that is needed in All Systems table
|
||||
type Info struct {
|
||||
Hostname string `json:"h,omitempty" cbor:"0,keyasint,omitempty"` // deprecated - moved to Details struct
|
||||
KernelVersion string `json:"k,omitempty" cbor:"1,keyasint,omitempty"` // deprecated - moved to Details struct
|
||||
Cores int `json:"c,omitzero" cbor:"2,keyasint,omitzero"` // deprecated - moved to Details struct
|
||||
// Threads is needed in Info struct to calculate load average thresholds
|
||||
Threads int `json:"t,omitempty" cbor:"3,keyasint,omitempty"`
|
||||
CpuModel string `json:"m,omitempty" cbor:"4,keyasint,omitempty"` // deprecated - moved to Details struct
|
||||
Uptime uint64 `json:"u" cbor:"5,keyasint"`
|
||||
Cpu float64 `json:"cpu" cbor:"6,keyasint"`
|
||||
MemPct float64 `json:"mp" cbor:"7,keyasint"`
|
||||
DiskPct float64 `json:"dp" cbor:"8,keyasint"`
|
||||
Bandwidth float64 `json:"b,omitzero" cbor:"9,keyasint"` // deprecated in favor of BandwidthBytes
|
||||
AgentVersion string `json:"v" cbor:"10,keyasint"`
|
||||
Podman bool `json:"p,omitempty" cbor:"11,keyasint,omitempty"` // deprecated - moved to Details struct
|
||||
GpuPct float64 `json:"g,omitempty" cbor:"12,keyasint,omitempty"`
|
||||
DashboardTemp float64 `json:"dt,omitempty" cbor:"13,keyasint,omitempty"`
|
||||
Os Os `json:"os,omitempty" cbor:"14,keyasint,omitempty"` // deprecated - moved to Details struct
|
||||
// LoadAvg1 float64 `json:"l1,omitempty" cbor:"15,keyasint,omitempty"` // deprecated - use `la` array instead
|
||||
// LoadAvg5 float64 `json:"l5,omitempty" cbor:"16,keyasint,omitempty"` // deprecated - use `la` array instead
|
||||
// LoadAvg15 float64 `json:"l15,omitempty" cbor:"17,keyasint,omitempty"` // deprecated - use `la` array instead
|
||||
|
||||
BandwidthBytes uint64 `json:"bb" cbor:"18,keyasint"`
|
||||
LoadAvg [3]float64 `json:"la,omitempty" cbor:"19,keyasint"`
|
||||
ConnectionType ConnectionType `json:"ct,omitempty" cbor:"20,keyasint,omitempty,omitzero"`
|
||||
ExtraFsPct map[string]float64 `json:"efs,omitempty" cbor:"21,keyasint,omitempty"`
|
||||
Services []uint16 `json:"sv,omitempty" cbor:"22,keyasint,omitempty"` // [totalServices, numFailedServices]
|
||||
Battery [2]uint8 `json:"bat,omitzero" cbor:"23,keyasint,omitzero"` // [percent, charge state]
|
||||
}
|
||||
|
||||
// Data that does not change during process lifetime and is not needed in All Systems table
|
||||
type Details struct {
|
||||
Hostname string `cbor:"0,keyasint"`
|
||||
Kernel string `cbor:"1,keyasint,omitempty"`
|
||||
Cores int `cbor:"2,keyasint"`
|
||||
Threads int `cbor:"3,keyasint"`
|
||||
CpuModel string `cbor:"4,keyasint"`
|
||||
Os Os `cbor:"5,keyasint"`
|
||||
OsName string `cbor:"6,keyasint"`
|
||||
Arch string `cbor:"7,keyasint"`
|
||||
Podman bool `cbor:"8,keyasint,omitempty"`
|
||||
MemoryTotal uint64 `cbor:"9,keyasint"`
|
||||
SmartInterval time.Duration `cbor:"10,keyasint,omitempty"`
|
||||
}
|
||||
|
||||
// Final data structure to return to the hub
|
||||
type CombinedData struct {
|
||||
Stats Stats `json:"stats" cbor:"0,keyasint"`
|
||||
Info Info `json:"info" cbor:"1,keyasint"`
|
||||
Containers []*container.Stats `json:"container" cbor:"2,keyasint"`
|
||||
SystemdServices []*systemd.Service `json:"systemd,omitempty" cbor:"3,keyasint,omitempty"`
|
||||
Details *Details `cbor:"4,keyasint,omitempty"`
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package systemd
|
||||
|
||||
import (
|
||||
"math"
|
||||
"runtime"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ServiceState represents the status of a systemd service
|
||||
type ServiceState uint8
|
||||
|
||||
const (
|
||||
StatusActive ServiceState = iota
|
||||
StatusInactive
|
||||
StatusFailed
|
||||
StatusActivating
|
||||
StatusDeactivating
|
||||
StatusReloading
|
||||
)
|
||||
|
||||
// ServiceSubState represents the sub status of a systemd service
|
||||
type ServiceSubState uint8
|
||||
|
||||
const (
|
||||
SubStateDead ServiceSubState = iota
|
||||
SubStateRunning
|
||||
SubStateExited
|
||||
SubStateFailed
|
||||
SubStateUnknown
|
||||
)
|
||||
|
||||
// ParseServiceStatus converts a string status to a ServiceStatus enum value
|
||||
func ParseServiceStatus(status string) ServiceState {
|
||||
switch status {
|
||||
case "active":
|
||||
return StatusActive
|
||||
case "inactive":
|
||||
return StatusInactive
|
||||
case "failed":
|
||||
return StatusFailed
|
||||
case "activating":
|
||||
return StatusActivating
|
||||
case "deactivating":
|
||||
return StatusDeactivating
|
||||
case "reloading":
|
||||
return StatusReloading
|
||||
default:
|
||||
return StatusInactive
|
||||
}
|
||||
}
|
||||
|
||||
// ParseServiceSubState converts a string sub status to a ServiceSubState enum value
|
||||
func ParseServiceSubState(subState string) ServiceSubState {
|
||||
switch subState {
|
||||
case "dead":
|
||||
return SubStateDead
|
||||
case "running":
|
||||
return SubStateRunning
|
||||
case "exited":
|
||||
return SubStateExited
|
||||
case "failed":
|
||||
return SubStateFailed
|
||||
default:
|
||||
return SubStateUnknown
|
||||
}
|
||||
}
|
||||
|
||||
// Service represents a single systemd service with its stats.
|
||||
type Service struct {
|
||||
Name string `json:"n" cbor:"0,keyasint"`
|
||||
State ServiceState `json:"s" cbor:"1,keyasint"`
|
||||
Cpu float64 `json:"c" cbor:"2,keyasint"`
|
||||
Mem uint64 `json:"m" cbor:"3,keyasint"`
|
||||
MemPeak uint64 `json:"mp" cbor:"4,keyasint"`
|
||||
Sub ServiceSubState `json:"ss" cbor:"5,keyasint"`
|
||||
CpuPeak float64 `json:"cp" cbor:"6,keyasint"`
|
||||
PrevCpuUsage uint64 `json:"-"`
|
||||
PrevReadTime time.Time `json:"-"`
|
||||
}
|
||||
|
||||
// UpdateCPUPercent calculates the CPU usage percentage for the service.
|
||||
func (s *Service) UpdateCPUPercent(cpuUsage uint64) {
|
||||
now := time.Now()
|
||||
|
||||
if s.PrevReadTime.IsZero() || cpuUsage < s.PrevCpuUsage {
|
||||
s.Cpu = 0
|
||||
s.PrevCpuUsage = cpuUsage
|
||||
s.PrevReadTime = now
|
||||
return
|
||||
}
|
||||
|
||||
duration := now.Sub(s.PrevReadTime).Nanoseconds()
|
||||
if duration <= 0 {
|
||||
s.PrevCpuUsage = cpuUsage
|
||||
s.PrevReadTime = now
|
||||
return
|
||||
}
|
||||
|
||||
coreCount := int64(runtime.NumCPU())
|
||||
duration *= coreCount
|
||||
|
||||
usageDelta := cpuUsage - s.PrevCpuUsage
|
||||
cpuPercent := float64(usageDelta) / float64(duration)
|
||||
s.Cpu = twoDecimals(cpuPercent * 100)
|
||||
|
||||
if s.Cpu > s.CpuPeak {
|
||||
s.CpuPeak = s.Cpu
|
||||
}
|
||||
|
||||
s.PrevCpuUsage = cpuUsage
|
||||
s.PrevReadTime = now
|
||||
}
|
||||
|
||||
func twoDecimals(value float64) float64 {
|
||||
return math.Round(value*100) / 100
|
||||
}
|
||||
|
||||
// ServiceDependency represents a unit that the service depends on.
|
||||
type ServiceDependency struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description,omitempty"`
|
||||
ActiveState string `json:"activeState,omitempty"`
|
||||
SubState string `json:"subState,omitempty"`
|
||||
}
|
||||
|
||||
// ServiceDetails contains extended information about a systemd service.
|
||||
type ServiceDetails map[string]any
|
||||
@@ -0,0 +1,113 @@
|
||||
//go:build testing
|
||||
|
||||
package systemd_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/henrygd/beszel/internal/entities/systemd"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestParseServiceStatus(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
expected systemd.ServiceState
|
||||
}{
|
||||
{"active", systemd.StatusActive},
|
||||
{"inactive", systemd.StatusInactive},
|
||||
{"failed", systemd.StatusFailed},
|
||||
{"activating", systemd.StatusActivating},
|
||||
{"deactivating", systemd.StatusDeactivating},
|
||||
{"reloading", systemd.StatusReloading},
|
||||
{"unknown", systemd.StatusInactive}, // default case
|
||||
{"", systemd.StatusInactive}, // default case
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.input, func(t *testing.T) {
|
||||
result := systemd.ParseServiceStatus(test.input)
|
||||
assert.Equal(t, test.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseServiceSubState(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
expected systemd.ServiceSubState
|
||||
}{
|
||||
{"dead", systemd.SubStateDead},
|
||||
{"running", systemd.SubStateRunning},
|
||||
{"exited", systemd.SubStateExited},
|
||||
{"failed", systemd.SubStateFailed},
|
||||
{"unknown", systemd.SubStateUnknown},
|
||||
{"other", systemd.SubStateUnknown}, // default case
|
||||
{"", systemd.SubStateUnknown}, // default case
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.input, func(t *testing.T) {
|
||||
result := systemd.ParseServiceSubState(test.input)
|
||||
assert.Equal(t, test.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServiceUpdateCPUPercent(t *testing.T) {
|
||||
t.Run("initial call sets CPU to 0", func(t *testing.T) {
|
||||
service := &systemd.Service{}
|
||||
service.UpdateCPUPercent(1000)
|
||||
assert.Equal(t, 0.0, service.Cpu)
|
||||
assert.Equal(t, uint64(1000), service.PrevCpuUsage)
|
||||
assert.False(t, service.PrevReadTime.IsZero())
|
||||
})
|
||||
|
||||
t.Run("subsequent call calculates CPU percentage", func(t *testing.T) {
|
||||
service := &systemd.Service{}
|
||||
service.PrevCpuUsage = 1000
|
||||
service.PrevReadTime = time.Now().Add(-time.Second)
|
||||
|
||||
service.UpdateCPUPercent(8000000000) // 8 seconds of CPU time
|
||||
|
||||
// CPU usage should be positive and reasonable
|
||||
assert.Greater(t, service.Cpu, 0.0, "CPU usage should be positive")
|
||||
assert.LessOrEqual(t, service.Cpu, 100.0, "CPU usage should not exceed 100%")
|
||||
assert.Equal(t, uint64(8000000000), service.PrevCpuUsage)
|
||||
assert.Greater(t, service.CpuPeak, 0.0, "CPU peak should be set")
|
||||
})
|
||||
|
||||
t.Run("CPU peak updates only when higher", func(t *testing.T) {
|
||||
service := &systemd.Service{}
|
||||
service.PrevCpuUsage = 1000
|
||||
service.PrevReadTime = time.Now().Add(-time.Second)
|
||||
service.UpdateCPUPercent(8000000000) // Set initial peak to ~50%
|
||||
initialPeak := service.CpuPeak
|
||||
|
||||
// Now try with much lower CPU usage - should not update peak
|
||||
service.PrevReadTime = time.Now().Add(-time.Second)
|
||||
service.UpdateCPUPercent(1000000) // Much lower usage
|
||||
assert.Equal(t, initialPeak, service.CpuPeak, "Peak should not update for lower CPU usage")
|
||||
})
|
||||
|
||||
t.Run("handles zero duration", func(t *testing.T) {
|
||||
service := &systemd.Service{}
|
||||
service.PrevCpuUsage = 1000
|
||||
now := time.Now()
|
||||
service.PrevReadTime = now
|
||||
// Mock time.Now() to return the same time to ensure zero duration
|
||||
// Since we can't mock time in Go easily, we'll check the logic manually
|
||||
// The zero duration case happens when duration <= 0
|
||||
assert.Equal(t, 0.0, service.Cpu, "CPU should start at 0")
|
||||
})
|
||||
|
||||
t.Run("handles CPU usage wraparound", func(t *testing.T) {
|
||||
service := &systemd.Service{}
|
||||
// Simulate wraparound where new usage is less than previous
|
||||
service.PrevCpuUsage = 1000
|
||||
service.PrevReadTime = time.Now().Add(-time.Second)
|
||||
service.UpdateCPUPercent(500) // Less than previous, should reset
|
||||
assert.Equal(t, 0.0, service.Cpu)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user