Initial commit: Beszel fork with Domain Locker integration

This commit is contained in:
Tomas Dvorak
2026-04-21 15:39:43 +02:00
commit 363d708e91
440 changed files with 160889 additions and 0 deletions
+158
View File
@@ -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:"-"`
}
+245
View File
@@ -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
}
+127
View File
@@ -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"
}
}
+147
View File
@@ -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"`
}
+543
View File
@@ -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"`
}
+62
View File
@@ -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)
}
+137
View File
@@ -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
}
}
+182
View File
@@ -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"`
}
+127
View File
@@ -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
+113
View File
@@ -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)
})
}