Documentation
¶
Overview ¶
Package monitors provides common functionality for monitor implementations. The BaseMonitor struct provides reusable functionality that concrete monitor implementations can embed or use as a foundation.
Package monitors provides a pluggable monitor registry system for Node Doctor.
This package implements a factory pattern that allows monitor implementations to self-register via func init() and provides thread-safe runtime discovery and instantiation. The registry serves as the central coordination point between monitor configurations and their implementations.
Architecture ¶
The monitor registry consists of several key components:
- Registry: Thread-safe storage and factory for monitor instances
- MonitorInfo: Registration metadata including factory and validator functions
- Package-level functions: Convenient API for the default global registry
Monitor Registration ¶
Monitor implementations register themselves during package initialization:
func init() {
monitors.Register(monitors.MonitorInfo{
Type: "system-disk-check",
Factory: NewSystemDiskMonitor,
Validator: ValidateSystemDiskConfig,
Description: "Monitors system disk usage and health",
})
}
The registration process:
- Happens at init time (single-threaded, no locking needed for registration)
- Panics on conflicts (fail-fast for development-time errors)
- Stores factory functions, not instances (enables on-demand creation)
- Includes optional validators for early configuration validation
Monitor Creation ¶
At runtime, monitors are created from configuration:
// Single monitor
config := types.MonitorConfig{
Name: "disk-monitor-1",
Type: "system-disk-check",
Enabled: true,
Config: map[string]interface{}{
"path": "/var/lib/kubelet",
"threshold": 85,
},
}
monitor, err := monitors.CreateMonitor(ctx, config)
if err != nil {
return fmt.Errorf("failed to create monitor: %w", err)
}
// Multiple monitors from configuration
allMonitors, err := monitors.CreateMonitorsFromConfigs(ctx, configs)
Thread Safety ¶
The registry is designed for high read concurrency with infrequent writes:
- Registration: Single-threaded (init time only)
- Read operations: Concurrent with RWMutex protection
- Monitor creation: Fully concurrent and thread-safe
- Factory functions: Must be thread-safe and stateless
Integration with Problem Detector ¶
The Problem Detector uses the registry to instantiate monitors:
// In pkg/detector/detector.go
monitors, err := monitors.CreateMonitorsFromConfigs(ctx, config.Monitors)
if err != nil {
return nil, fmt.Errorf("failed to create monitors: %w", err)
}
detector := &ProblemDetector{
monitors: monitors,
// ... other fields
}
Error Handling ¶
The registry provides different error handling strategies:
- Registration errors: Panic (development-time issues)
- Validation errors: Return error (configuration issues)
- Creation errors: Return error (runtime issues)
- Factory errors: Propagated to caller
Best Practices for Monitor Authors ¶
When implementing a new monitor:
- Register in init() function
- Provide a validator function for early config validation
- Make factory functions thread-safe and stateless
- Use context for cancellation in factory functions
- Handle configuration parsing within the factory
- Return meaningful error messages
Example monitor implementation:
package diskmonitor
import (
"context"
"fmt"
"github.com/supporttools/node-doctor/pkg/monitors"
"github.com/supporttools/node-doctor/pkg/types"
)
func init() {
monitors.Register(monitors.MonitorInfo{
Type: "system-disk-check",
Factory: NewDiskMonitor,
Validator: ValidateDiskConfig,
Description: "Monitors disk usage and health",
})
}
func NewDiskMonitor(ctx context.Context, config types.MonitorConfig) (types.Monitor, error) {
// Parse monitor-specific configuration
diskConfig, err := parseDiskConfig(config.Config)
if err != nil {
return nil, fmt.Errorf("invalid disk monitor config: %w", err)
}
return &DiskMonitor{
name: config.Name,
path: diskConfig.Path,
threshold: diskConfig.Threshold,
}, nil
}
func ValidateDiskConfig(config types.MonitorConfig) error {
if config.Config == nil {
return fmt.Errorf("config is required")
}
path, ok := config.Config["path"].(string)
if !ok || path == "" {
return fmt.Errorf("path is required and must be a string")
}
return nil
}
Testing ¶
The package provides comprehensive test coverage including:
- Unit tests for all public APIs
- Concurrent access testing with race detector
- Error condition testing
- Benchmark tests for performance validation
- Mock implementations for testing monitor consumers
Use the provided test utilities when testing monitor implementations:
func TestMyMonitor(t *testing.T) {
registry := monitors.NewRegistry()
registry.Register(monitors.MonitorInfo{
Type: "test-monitor",
Factory: NewMyMonitor,
})
config := types.MonitorConfig{
Name: "test",
Type: "test-monitor",
Enabled: true,
}
monitor, err := registry.CreateMonitor(context.Background(), config)
// ... test the monitor
}
Package monitors provides a pluggable monitor registry system for Node Doctor.
The registry allows monitors to self-register via func init() and provides thread-safe runtime discovery and instantiation of monitors. This enables a clean factory pattern where monitor implementations can be discovered and created without tight coupling.
Usage Example:
// Monitor registration (typically in monitor package init())
func init() {
monitors.MustRegister(monitors.MonitorInfo{
Type: "system-disk-check",
Factory: NewSystemDiskMonitor,
Validator: ValidateSystemDiskConfig,
Description: "Monitors system disk usage and health",
})
}
// Monitor creation at runtime
monitor, err := registry.CreateMonitor(ctx, config)
if err != nil {
return fmt.Errorf("failed to create monitor: %w", err)
}
Index ¶
- Variables
- func CreateMonitor(ctx context.Context, config types.MonitorConfig) (types.Monitor, error)
- func CreateMonitorsFromConfigs(ctx context.Context, configs []types.MonitorConfig) ([]types.Monitor, error)
- func GetRegisteredTypes() []string
- func IsRegistered(monitorType string) bool
- func MustRegister(info MonitorInfo)
- func Register(info MonitorInfo) error
- func ValidateConfig(config types.MonitorConfig) error
- type BaseMonitor
- func (b *BaseMonitor) GetInterval() time.Duration
- func (b *BaseMonitor) GetName() string
- func (b *BaseMonitor) GetTimeout() time.Duration
- func (b *BaseMonitor) IsRunning() bool
- func (b *BaseMonitor) SetCheckFunc(checkFunc CheckFunc) error
- func (b *BaseMonitor) SetLogger(logger Logger) error
- func (b *BaseMonitor) Start() (<-chan *types.Status, error)
- func (b *BaseMonitor) Stop()
- type CheckFunc
- type Logger
- type MonitorFactory
- type MonitorInfo
- type MonitorValidator
- type Registry
- func (r *Registry) CreateMonitor(ctx context.Context, config types.MonitorConfig) (types.Monitor, error)
- func (r *Registry) CreateMonitorsFromConfigs(ctx context.Context, configs []types.MonitorConfig) ([]types.Monitor, error)
- func (r *Registry) GetMonitorInfo(monitorType string) *MonitorInfo
- func (r *Registry) GetRegisteredTypes() []string
- func (r *Registry) GetRegistryStats() RegistryStats
- func (r *Registry) IsRegistered(monitorType string) bool
- func (r *Registry) MustRegister(info MonitorInfo)
- func (r *Registry) Register(info MonitorInfo) error
- func (r *Registry) ValidateConfig(config types.MonitorConfig) error
- type RegistryStats
Constants ¶
This section is empty.
Variables ¶
var DefaultRegistry = NewRegistry()
DefaultRegistry is the global monitor registry instance. Most applications should use this instance through the package-level functions rather than creating their own registry.
var ErrDuplicateMonitorType = errors.New("monitor type is already registered")
ErrDuplicateMonitorType is returned when attempting to register a monitor type that already exists.
var ErrEmptyMonitorType = errors.New("monitor type cannot be empty")
ErrEmptyMonitorType is returned when attempting to register a monitor with an empty type.
var ErrNilFactory = errors.New("monitor factory cannot be nil")
ErrNilFactory is returned when attempting to register a monitor with a nil factory.
Functions ¶
func CreateMonitor ¶
CreateMonitor creates a monitor instance using the default registry. See Registry.CreateMonitor for details.
func CreateMonitorsFromConfigs ¶
func CreateMonitorsFromConfigs(ctx context.Context, configs []types.MonitorConfig) ([]types.Monitor, error)
CreateMonitorsFromConfigs creates multiple monitors using the default registry. See Registry.CreateMonitorsFromConfigs for details.
func GetRegisteredTypes ¶
func GetRegisteredTypes() []string
GetRegisteredTypes returns all registered monitor types from the default registry. See Registry.GetRegisteredTypes for details.
func IsRegistered ¶
IsRegistered checks if a monitor type is registered in the default registry. See Registry.IsRegistered for details.
func MustRegister ¶ added in v1.2.0
func MustRegister(info MonitorInfo)
MustRegister registers a monitor type with the default registry and panics on error. See Registry.MustRegister for details.
func Register ¶
func Register(info MonitorInfo) error
Register registers a monitor type with the default registry. See Registry.Register for details.
func ValidateConfig ¶
func ValidateConfig(config types.MonitorConfig) error
ValidateConfig validates a monitor configuration using the default registry. See Registry.ValidateConfig for details.
Types ¶
type BaseMonitor ¶
type BaseMonitor struct {
// contains filtered or unexported fields
}
BaseMonitor provides common functionality for monitor implementations. It handles the standard monitor lifecycle (start/stop), status channel management, timeout enforcement, interval timing, and error handling. Concrete monitors can embed this struct or use it as a foundation for their implementation.
The BaseMonitor is designed to be thread-safe and provides graceful shutdown capabilities with proper resource cleanup.
Example usage:
monitor := NewBaseMonitor("disk-monitor", 30*time.Second, 10*time.Second)
monitor.SetCheckFunc(func(ctx context.Context) (*types.Status, error) {
// Perform monitoring check
return status, nil
})
statusCh, err := monitor.Start()
if err != nil {
log.Fatal(err)
}
// Process status updates from statusCh
monitor.Stop() // Graceful shutdown
func NewBaseMonitor ¶
func NewBaseMonitor(name string, interval, timeout time.Duration) (*BaseMonitor, error)
NewBaseMonitor creates a new BaseMonitor with the specified configuration. The monitor is created in a stopped state and must be started with Start().
Parameters:
- name: Unique identifier for this monitor instance
- interval: How often to perform monitoring checks
- timeout: Maximum time allowed for each check operation
Validation rules:
- name must not be empty
- interval must be positive
- timeout must be positive
- timeout must be less than interval
Returns an error if validation fails.
func (*BaseMonitor) GetInterval ¶
func (b *BaseMonitor) GetInterval() time.Duration
GetInterval returns the monitor's check interval. This method is thread-safe and can be called concurrently.
func (*BaseMonitor) GetName ¶
func (b *BaseMonitor) GetName() string
GetName returns the monitor's name. This method is thread-safe and can be called concurrently.
func (*BaseMonitor) GetTimeout ¶
func (b *BaseMonitor) GetTimeout() time.Duration
GetTimeout returns the monitor's check timeout. This method is thread-safe and can be called concurrently.
func (*BaseMonitor) IsRunning ¶
func (b *BaseMonitor) IsRunning() bool
IsRunning returns whether the monitor is currently running. This method is thread-safe and can be called concurrently.
func (*BaseMonitor) SetCheckFunc ¶
func (b *BaseMonitor) SetCheckFunc(checkFunc CheckFunc) error
SetCheckFunc sets the function that will be called to perform monitoring checks. This function must be set before calling Start().
The checkFunc will be called periodically according to the monitor's interval. It should perform the actual monitoring logic and return a Status describing the current state, or an error if the check fails.
The function receives a context that will be cancelled if the check exceeds the monitor's timeout. Implementations should respect this context.
Returns an error if the monitor is currently running.
func (*BaseMonitor) SetLogger ¶
func (b *BaseMonitor) SetLogger(logger Logger) error
SetLogger sets an optional logger for the monitor. If no logger is set, logging operations are silently ignored.
Returns an error if the monitor is currently running.
func (*BaseMonitor) Start ¶
func (b *BaseMonitor) Start() (<-chan *types.Status, error)
Start begins the monitoring process and returns a channel for status updates. The monitor runs asynchronously and sends Status updates through the channel until Stop() is called.
Start can be called multiple times to restart a stopped monitor. If the monitor was previously stopped, new channels are created to ensure safe restart.
Returns an error if:
- The monitor is already running
- No check function has been set
The returned channel will be closed when the monitor stops.
func (*BaseMonitor) Stop ¶
func (b *BaseMonitor) Stop()
Stop gracefully stops the monitor. This method blocks until the monitoring goroutine has completely stopped and all resources have been cleaned up, or until a timeout occurs.
Stop is safe to call multiple times. If the monitor is not running, this method returns immediately.
Stop has a built-in timeout of 30 seconds to prevent indefinite hangs. If the monitor doesn't stop within this time, a warning is logged but Stop returns to allow the application to continue.
After Stop returns:
- The status channel will be closed
- No more status updates will be sent
- The monitor can be restarted with Start()
- IsRunning() will return false
type CheckFunc ¶
CheckFunc is a function type that performs the actual monitoring check. It receives a context for cancellation/timeout and returns a Status or an error. If the function returns nil status and nil error, it's treated as a healthy check with no specific status to report.
type Logger ¶
type Logger interface {
// Infof logs an informational message with formatting.
Infof(format string, args ...interface{})
// Warnf logs a warning message with formatting.
Warnf(format string, args ...interface{})
// Errorf logs an error message with formatting.
Errorf(format string, args ...interface{})
}
Logger provides optional logging functionality for monitors. If a logger is not provided, logging operations are silently ignored.
type MonitorFactory ¶
MonitorFactory is a function that creates a new monitor instance. It takes a context for cancellation and a MonitorConfig containing monitor-specific configuration parameters.
type MonitorInfo ¶
type MonitorInfo struct {
// Type is the unique identifier for this monitor type.
// This should match the "type" field in MonitorConfig.
Type string
// Factory is the function used to create new instances of this monitor.
// The factory function should be thread-safe and stateless.
Factory MonitorFactory
// Validator is the function used to validate configuration for this monitor.
// This is optional but recommended for early validation.
Validator MonitorValidator
// Description provides human-readable documentation for this monitor type.
// This is used for help text, documentation, and debugging.
Description string
}
MonitorInfo contains metadata and factory functions for a monitor type. This is used to register monitor implementations with the registry.
func GetMonitorInfo ¶
func GetMonitorInfo(monitorType string) *MonitorInfo
GetMonitorInfo returns monitor info from the default registry. See Registry.GetMonitorInfo for details.
type MonitorValidator ¶
type MonitorValidator func(config types.MonitorConfig) error
MonitorValidator is a function that validates a monitor configuration. It should perform early validation of the configuration before attempting to create the monitor instance. This allows for fail-fast validation at configuration parse time.
type Registry ¶
type Registry struct {
// contains filtered or unexported fields
}
Registry manages the registration and creation of monitor instances. It provides thread-safe operations for registering monitor types at init time and creating monitor instances at runtime.
The registry uses a read-write mutex to optimize for the common case of many concurrent reads (monitor creation) with infrequent writes (monitor registration during init).
func NewRegistry ¶
func NewRegistry() *Registry
NewRegistry creates a new empty monitor registry. In most cases, you should use the DefaultRegistry instead of creating a new one. This function is primarily useful for testing or specialized use cases that require isolated registries.
func (*Registry) CreateMonitor ¶
func (r *Registry) CreateMonitor(ctx context.Context, config types.MonitorConfig) (types.Monitor, error)
CreateMonitor creates a new monitor instance of the specified type. The config parameter must contain a valid monitor type and any required configuration parameters for that monitor type.
This function is thread-safe and can be called concurrently.
Returns an error if:
- The monitor type is not registered
- The configuration is invalid
- The factory function returns an error
func (*Registry) CreateMonitorsFromConfigs ¶
func (r *Registry) CreateMonitorsFromConfigs(ctx context.Context, configs []types.MonitorConfig) ([]types.Monitor, error)
CreateMonitorsFromConfigs creates multiple monitor instances from a slice of configurations. This is a convenience function that calls CreateMonitor for each configuration.
If any monitor creation fails, this function returns an error and does not create any of the remaining monitors. The caller should handle partial failures appropriately (e.g., by cleaning up successfully created monitors).
For better error handling in production code, consider calling CreateMonitor individually and handling failures per monitor.
func (*Registry) GetMonitorInfo ¶
func (r *Registry) GetMonitorInfo(monitorType string) *MonitorInfo
GetMonitorInfo returns the registration information for a monitor type. This is useful for introspection, help text generation, and debugging.
Returns nil if the monitor type is not registered.
func (*Registry) GetRegisteredTypes ¶
GetRegisteredTypes returns a sorted list of all registered monitor types. This is useful for displaying available monitor types, validation, and debugging purposes.
The returned slice is a copy and can be safely modified by the caller.
func (*Registry) GetRegistryStats ¶
func (r *Registry) GetRegistryStats() RegistryStats
GetRegistryStats returns statistics about the current state of the registry. This is useful for monitoring, debugging, and health checks.
func (*Registry) IsRegistered ¶
IsRegistered checks whether a monitor type is registered. This is useful for early validation before attempting to create a monitor instance.
func (*Registry) MustRegister ¶ added in v1.2.0
func (r *Registry) MustRegister(info MonitorInfo)
MustRegister adds a new monitor type to the registry and panics on error. This is intended for use in init() functions where registration failures indicate programming errors that should be caught during development.
MustRegister panics if:
- info.Type is empty
- info.Factory is nil
- A monitor with the same type is already registered
func (*Registry) Register ¶
func (r *Registry) Register(info MonitorInfo) error
Register adds a new monitor type to the registry. This function is typically called from monitor package init() functions to self-register monitor implementations.
Register returns an error if:
- info.Type is empty
- info.Factory is nil
- A monitor with the same type is already registered
For use in init() functions where panicking on error is desired, use MustRegister instead.
func (*Registry) ValidateConfig ¶
func (r *Registry) ValidateConfig(config types.MonitorConfig) error
ValidateConfig validates a monitor configuration using the registered validator function. If no validator is registered for the monitor type, this function performs basic validation (type exists and is enabled).
This function should be called before CreateMonitor to enable fail-fast validation at configuration parse time.
type RegistryStats ¶
type RegistryStats struct {
// RegisteredTypes is the total number of registered monitor types
RegisteredTypes int
// TypesList is a sorted list of all registered monitor types
TypesList []string
}
RegistryStats contains statistics about the registry state.
func GetRegistryStats ¶
func GetRegistryStats() RegistryStats
GetRegistryStats returns statistics about the default registry. See Registry.GetRegistryStats for details.
Directories
¶
| Path | Synopsis |
|---|---|
|
Package example provides example monitor implementations that demonstrate the monitor registration and factory patterns used by Node Doctor.
|
Package example provides example monitor implementations that demonstrate the monitor registration and factory patterns used by Node Doctor. |
|
Package kubernetes provides Kubernetes component health monitoring capabilities.
|
Package kubernetes provides Kubernetes component health monitoring capabilities. |
|
Package network provides network health monitoring capabilities.
|
Package network provides network health monitoring capabilities. |
|
Package system provides system-level monitors for Node Doctor.
|
Package system provides system-level monitors for Node Doctor. |