mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-04-29 03:50:18 +00:00
Implements structured logging package with LOG_LEVEL/LOG_FORMAT env support, debug level guards for hot paths, enriched error messages with actionable context, and stack trace capture for production debugging. Improves observability and reduces log overhead in high-frequency polling loops.
193 lines
6.4 KiB
Go
193 lines
6.4 KiB
Go
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"net"
|
|
"strconv"
|
|
)
|
|
|
|
// Settings represents the complete application configuration
|
|
type Settings struct {
|
|
Server ServerSettings `json:"server" yaml:"server" mapstructure:"server"`
|
|
Monitoring MonitoringSettings `json:"monitoring" yaml:"monitoring" mapstructure:"monitoring"`
|
|
Logging LoggingSettings `json:"logging" yaml:"logging" mapstructure:"logging"`
|
|
Security SecuritySettings `json:"security" yaml:"security" mapstructure:"security"`
|
|
}
|
|
|
|
// ServerSettings contains all server-related configuration
|
|
type ServerSettings struct {
|
|
Backend PortSettings `json:"backend" yaml:"backend" mapstructure:"backend"`
|
|
Frontend PortSettings `json:"frontend" yaml:"frontend" mapstructure:"frontend"`
|
|
}
|
|
|
|
// PortSettings defines configuration for a network service
|
|
type PortSettings struct {
|
|
Port int `json:"port" yaml:"port" mapstructure:"port"`
|
|
Host string `json:"host" yaml:"host" mapstructure:"host"`
|
|
}
|
|
|
|
// MonitoringSettings contains monitoring-related configuration
|
|
type MonitoringSettings struct {
|
|
PollingInterval int `json:"pollingInterval" yaml:"pollingInterval" mapstructure:"pollingInterval"` // milliseconds
|
|
ConcurrentPolling bool `json:"concurrentPolling" yaml:"concurrentPolling" mapstructure:"concurrentPolling"`
|
|
BackupPollingCycles int `json:"backupPollingCycles" yaml:"backupPollingCycles" mapstructure:"backupPollingCycles"` // How often to poll backups
|
|
BackupPollingIntervalMs int `json:"backupPollingIntervalMs" yaml:"backupPollingIntervalMs" mapstructure:"backupPollingIntervalMs"` // 0 = use cycle-based scheduling
|
|
BackupPollingEnabled bool `json:"backupPollingEnabled" yaml:"backupPollingEnabled" mapstructure:"backupPollingEnabled"`
|
|
MetricsRetentionDays int `json:"metricsRetentionDays" yaml:"metricsRetentionDays" mapstructure:"metricsRetentionDays"`
|
|
}
|
|
|
|
// LoggingSettings contains logging configuration
|
|
type LoggingSettings struct {
|
|
Level string `json:"level" yaml:"level" mapstructure:"level"` // debug, info, warn, error
|
|
Format string `json:"format" yaml:"format" mapstructure:"format"` // json, console, auto
|
|
File string `json:"file" yaml:"file" mapstructure:"file"` // log file path
|
|
MaxSize int `json:"maxSize" yaml:"maxSize" mapstructure:"maxSize"` // MB
|
|
MaxBackups int `json:"maxBackups" yaml:"maxBackups" mapstructure:"maxBackups"`
|
|
MaxAge int `json:"maxAge" yaml:"maxAge" mapstructure:"maxAge"` // days
|
|
Compress bool `json:"compress" yaml:"compress" mapstructure:"compress"`
|
|
}
|
|
|
|
// SecuritySettings contains security-related configuration
|
|
type SecuritySettings struct {
|
|
APIToken string `json:"apiToken" yaml:"apiToken" mapstructure:"apiToken"`
|
|
AllowedOrigins []string `json:"allowedOrigins" yaml:"allowedOrigins" mapstructure:"allowedOrigins"`
|
|
IframeEmbedding string `json:"iframeEmbedding" yaml:"iframeEmbedding" mapstructure:"iframeEmbedding"` // DENY, SAMEORIGIN, or URL
|
|
EnableAuthentication bool `json:"enableAuthentication" yaml:"enableAuthentication" mapstructure:"enableAuthentication"`
|
|
}
|
|
|
|
// DefaultSettings returns the default configuration
|
|
func DefaultSettings() *Settings {
|
|
return &Settings{
|
|
Server: ServerSettings{
|
|
Backend: PortSettings{
|
|
Port: 3000,
|
|
Host: "0.0.0.0",
|
|
},
|
|
Frontend: PortSettings{
|
|
Port: 7655,
|
|
Host: "0.0.0.0",
|
|
},
|
|
},
|
|
Monitoring: MonitoringSettings{
|
|
PollingInterval: 5000, // 5 seconds
|
|
ConcurrentPolling: true,
|
|
BackupPollingCycles: 10, // Poll backups every 10 cycles
|
|
BackupPollingIntervalMs: 0,
|
|
BackupPollingEnabled: true,
|
|
MetricsRetentionDays: 7,
|
|
},
|
|
Logging: LoggingSettings{
|
|
Level: "info",
|
|
Format: "auto",
|
|
File: "/opt/pulse/pulse.log",
|
|
MaxSize: 100, // 100MB
|
|
MaxBackups: 5,
|
|
MaxAge: 30, // 30 days
|
|
Compress: true,
|
|
},
|
|
Security: SecuritySettings{
|
|
AllowedOrigins: []string{"*"},
|
|
IframeEmbedding: "SAMEORIGIN",
|
|
EnableAuthentication: false,
|
|
},
|
|
}
|
|
}
|
|
|
|
// Validate checks if the settings are valid
|
|
func (s *Settings) Validate() error {
|
|
// Validate backend port
|
|
if err := validatePort(s.Server.Backend.Port); err != nil {
|
|
return fmt.Errorf("invalid backend port: %w", err)
|
|
}
|
|
|
|
// Validate frontend port
|
|
if err := validatePort(s.Server.Frontend.Port); err != nil {
|
|
return fmt.Errorf("invalid frontend port: %w", err)
|
|
}
|
|
|
|
// Ensure ports are different
|
|
if s.Server.Backend.Port == s.Server.Frontend.Port {
|
|
return fmt.Errorf("backend and frontend ports must be different")
|
|
}
|
|
|
|
// Validate hosts
|
|
if err := validateHost(s.Server.Backend.Host); err != nil {
|
|
return fmt.Errorf("invalid backend host: %w", err)
|
|
}
|
|
|
|
if err := validateHost(s.Server.Frontend.Host); err != nil {
|
|
return fmt.Errorf("invalid frontend host: %w", err)
|
|
}
|
|
|
|
// Validate monitoring settings
|
|
if s.Monitoring.PollingInterval < 1000 {
|
|
return fmt.Errorf("polling interval must be at least 1000ms (1 second)")
|
|
}
|
|
|
|
if s.Monitoring.BackupPollingCycles < 0 {
|
|
return fmt.Errorf("backup polling cycles cannot be negative")
|
|
}
|
|
|
|
if s.Monitoring.BackupPollingIntervalMs < 0 {
|
|
return fmt.Errorf("backup polling interval cannot be negative")
|
|
}
|
|
|
|
// Validate logging level
|
|
validLevels := map[string]bool{"debug": true, "info": true, "warn": true, "error": true}
|
|
if !validLevels[s.Logging.Level] {
|
|
return fmt.Errorf("invalid log level: %s", s.Logging.Level)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// validatePort checks if a port number is valid
|
|
func validatePort(port int) error {
|
|
if port < 1 || port > 65535 {
|
|
return fmt.Errorf("port must be between 1 and 65535")
|
|
}
|
|
|
|
// Warn about privileged ports
|
|
if port < 1024 {
|
|
// This is just a warning in the validation
|
|
// The actual binding will fail if not root
|
|
return nil
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// validateHost checks if a host string is valid
|
|
func validateHost(host string) error {
|
|
if host == "" {
|
|
return fmt.Errorf("host cannot be empty")
|
|
}
|
|
|
|
// Special cases
|
|
if host == "0.0.0.0" || host == "::" || host == "localhost" {
|
|
return nil
|
|
}
|
|
|
|
// Try to parse as IP
|
|
if net.ParseIP(host) != nil {
|
|
return nil
|
|
}
|
|
|
|
// Try to parse as hostname (basic check)
|
|
if len(host) > 0 && len(host) <= 253 {
|
|
return nil
|
|
}
|
|
|
|
return fmt.Errorf("invalid host: %s", host)
|
|
}
|
|
|
|
// IsPortAvailable checks if a port is available for binding
|
|
func IsPortAvailable(host string, port int) bool {
|
|
address := net.JoinHostPort(host, strconv.Itoa(port))
|
|
listener, err := net.Listen("tcp", address)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
listener.Close()
|
|
return true
|
|
}
|