package metrics import ( "errors" "fmt" "sort" "sync" "github.com/safing/portbase/modules" ) var ( module *modules.Module registry []Metric registryLock sync.RWMutex 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 init() { module = modules.Register("metrics", prep, start, stop, "config", "database", "api") } func prep() error { return prepConfig() } func start() error { // Add metric instance name as global variable if set. if instanceOption() != "" { if err := AddGlobalLabel("instance", instanceOption()); err != nil { return err } } 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 err := registerAPI(); err != nil { return err } if pushOption() != "" { module.StartServiceWorker("metric pusher", 0, 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)) // Set flag that first metric is now registered. firstMetricRegistered = true if module.Status() < modules.StatusStarting { return fmt.Errorf("registering metric %q too early", m.ID()) } 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] }