Files
akvorado/common/reporter/metrics/root.go
2025-11-12 22:37:44 +01:00

120 lines
3.7 KiB
Go

// SPDX-FileCopyrightText: 2022 Free Mobile
// SPDX-License-Identifier: AGPL-3.0-only
// Package metrics handles metrics for akvorado
//
// This is a wrapper around Prometheus Go client.
package metrics
import (
"fmt"
"net/http"
"regexp"
"strings"
"sync"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/collectors"
"github.com/prometheus/client_golang/prometheus/promhttp"
"akvorado/common/reporter/logger"
"akvorado/common/reporter/stack"
)
// Metrics represents the internal state of the metric subsystem.
type Metrics struct {
logger logger.Logger
config Configuration
registry *prometheus.Registry
factoryCache map[string]*Factory
factoryCacheLock sync.RWMutex
}
// New creates a new metric registry and setup the appropriate
// exporters. The provided prefix is used for system-wide metrics.
func New(logger logger.Logger, configuration Configuration) (*Metrics, error) {
reg := prometheus.NewRegistry()
reg.MustRegister(collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}))
reg.MustRegister(collectors.NewGoCollector(
collectors.WithGoCollectorRuntimeMetrics(
collectors.GoRuntimeMetricsRule{Matcher: regexp.MustCompile("/.*")})))
m := Metrics{
logger: logger,
config: configuration,
registry: reg,
factoryCache: make(map[string]*Factory, 0),
}
return &m, nil
}
// HTTPHandler returns an handler to server Prometheus metrics.
func (m *Metrics) HTTPHandler() http.Handler {
return promhttp.HandlerFor(m.registry, promhttp.HandlerOpts{
ErrorLog: promHTTPLogger{m.logger},
})
}
func getPrefix(module string) string {
var moduleName string
if !strings.HasPrefix(module, stack.ModuleName) {
moduleName = stack.ModuleName
} else {
moduleName = strings.SplitN(module, ".", 2)[0]
}
moduleName = strings.ReplaceAll(moduleName, "/", "_")
moduleName = strings.ReplaceAll(moduleName, ".", "_")
moduleName = fmt.Sprintf("%s_", moduleName)
return moduleName
}
// Factory returns a factory to register new metrics with promauto. It
// includes the module as an automatic prefix. This method is expected
// to be called only from our own module to avoid walking the stack
// too often. It uses a cache to speedup things a little bit.
func (m *Metrics) Factory(skipCallstack int) *Factory {
callStack := stack.Callers()
call := callStack[1+skipCallstack] // Trial and error, there is a test to check it works
module := call.Info().FunctionName()
// Hotpath
if factory := func() *Factory {
m.factoryCacheLock.RLock()
defer m.factoryCacheLock.RUnlock()
if factory, ok := m.factoryCache[module]; ok {
return factory
}
return nil
}(); factory != nil {
return factory
}
// Slow path
m.factoryCacheLock.Lock()
defer m.factoryCacheLock.Unlock()
moduleName := getPrefix(module)
factory := Factory{
prefix: moduleName,
registry: m.registry,
}
m.factoryCache[module] = &factory
return &factory
}
// RegisterCollector register a custom collector and prefix
// everything with the module name.
func (m *Metrics) RegisterCollector(skipCallStack int, c prometheus.Collector) {
callStack := stack.Callers()
call := callStack[1+skipCallStack] // Should be the same as above !
prefix := getPrefix(call.Info().FunctionName())
prometheus.WrapRegistererWithPrefix(prefix, m.registry).MustRegister(c)
}
// UnregisterCollector unregister a previously registered custom collector. It should be called from the same module!
func (m *Metrics) UnregisterCollector(skipCallStack int, c prometheus.Collector) {
callStack := stack.Callers()
call := callStack[1+skipCallStack] // Should be the same as above !
prefix := getPrefix(call.Info().FunctionName())
prometheus.WrapRegistererWithPrefix(prefix, m.registry).Unregister(c)
}