safing-portmaster/base/metrics/module.go
2024-11-27 16:14:34 +01:00

216 lines
5.1 KiB
Go

package metrics
import (
"errors"
"fmt"
"sort"
"sync"
"sync/atomic"
"github.com/safing/portmaster/service/mgr"
)
type Metrics struct {
mgr *mgr.Manager
instance instance
metricTicker *mgr.SleepyTicker
}
func (met *Metrics) Manager() *mgr.Manager {
return met.mgr
}
func (met *Metrics) Start() error {
return start()
}
func (met *Metrics) Stop() error {
return stop()
}
func (met *Metrics) SetSleep(enabled bool) {
if met.metricTicker != nil {
met.metricTicker.SetSleep(enabled)
}
}
var (
module *Metrics
shimLoaded atomic.Bool
registry []Metric
registryLock sync.RWMutex
readyToRegister bool
firstMetricRegistered bool
metricNamespace string
globalLabels = make(map[string]string)
// ErrAlreadyStarted is returned when an operation is only valid before the
// first metric is registered, and is called after.
ErrAlreadyStarted = errors.New("can only be changed before first metric is registered")
// ErrAlreadyRegistered is returned when a metric with the same ID is
// registered again.
ErrAlreadyRegistered = errors.New("metric already registered")
// ErrAlreadySet is returned when a value is already set and cannot be changed.
ErrAlreadySet = errors.New("already set")
// ErrInvalidOptions is returned when invalid options where provided.
ErrInvalidOptions = errors.New("invalid options")
)
func start() error {
// Add metric instance name as global variable if set.
if instanceOption() != "" {
if err := AddGlobalLabel("instance", instanceOption()); err != nil {
return err
}
}
// Mark registry as ready to register metrics.
func() {
registryLock.Lock()
defer registryLock.Unlock()
readyToRegister = true
}()
if err := registerInfoMetric(); err != nil {
return err
}
if err := registerRuntimeMetric(); err != nil {
return err
}
if err := registerHostMetrics(); err != nil {
return err
}
if err := registerLogMetrics(); err != nil {
return err
}
if pushOption() != "" {
module.mgr.Go("metric pusher", metricsWriter)
}
return nil
}
func stop() error {
// Wait until the metrics pusher is done, as it may have started reporting
// and may report a higher number than we store to disk. For persistent
// metrics it can then happen that the first report is lower than the
// previous report, making prometheus think that all that happened since the
// last report, due to the automatic restart detection.
// The registry is read locked when writing metrics.
// Write lock the registry to make sure all writes are finished.
registryLock.Lock()
registryLock.Unlock() //nolint:staticcheck
storePersistentMetrics()
return nil
}
func register(m Metric) error {
registryLock.Lock()
defer registryLock.Unlock()
// Check if metric ID is already registered.
for _, registeredMetric := range registry {
if m.LabeledID() == registeredMetric.LabeledID() {
return ErrAlreadyRegistered
}
if m.Opts().InternalID != "" &&
m.Opts().InternalID == registeredMetric.Opts().InternalID {
return fmt.Errorf("%w with this internal ID", ErrAlreadyRegistered)
}
}
// Add new metric to registry and sort it.
registry = append(registry, m)
sort.Sort(byLabeledID(registry))
// Check if we can already register.
if !readyToRegister {
return fmt.Errorf("registering metric %q too early", m.ID())
}
// Set flag that first metric is now registered.
firstMetricRegistered = true
return nil
}
// SetNamespace sets the namespace for all metrics. It is prefixed to all
// metric IDs.
// It must be set before any metric is registered.
// Does not affect golang runtime metrics.
func SetNamespace(namespace string) error {
// Lock registry and check if a first metric is already registered.
registryLock.Lock()
defer registryLock.Unlock()
if firstMetricRegistered {
return ErrAlreadyStarted
}
// Check if the namespace is already set.
if metricNamespace != "" {
return ErrAlreadySet
}
metricNamespace = namespace
return nil
}
// AddGlobalLabel adds a global label to all metrics.
// Global labels must be added before any metric is registered.
// Does not affect golang runtime metrics.
func AddGlobalLabel(name, value string) error {
// Lock registry and check if a first metric is already registered.
registryLock.Lock()
defer registryLock.Unlock()
if firstMetricRegistered {
return ErrAlreadyStarted
}
// Check format.
if !prometheusFormat.MatchString(name) {
return fmt.Errorf("metric label name %q must match %s", name, PrometheusFormatRequirement)
}
globalLabels[name] = value
return nil
}
type byLabeledID []Metric
func (r byLabeledID) Len() int { return len(r) }
func (r byLabeledID) Less(i, j int) bool { return r[i].LabeledID() < r[j].LabeledID() }
func (r byLabeledID) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
func New(instance instance) (*Metrics, error) {
if !shimLoaded.CompareAndSwap(false, true) {
return nil, errors.New("only one instance allowed")
}
m := mgr.New("Metrics")
module = &Metrics{
mgr: m,
instance: instance,
}
if err := prepConfig(); err != nil {
return nil, err
}
if err := registerAPI(); err != nil {
return nil, err
}
return module, nil
}
type instance interface{}