mirror of
https://github.com/lukaszraczylo/graphql-monitoring-proxy.git
synced 2026-10-12 05:32:36 +00:00
get_metrics_name called os.Hostname on every metrics call and keyed a never-evicted cache by label values. The hostname is now resolved once and the cache is keyed by label names, so it cannot grow with high cardinality values. The metrics listener also honours a graceful shutdown context.
245 lines
8.5 KiB
Go
245 lines
8.5 KiB
Go
// Package libpack_monitoring provides Prometheus-compatible metrics collection
|
|
// and exposure using VictoriaMetrics. Supports counters, gauges, histograms,
|
|
// and custom metrics with labels.
|
|
package libpack_monitoring
|
|
|
|
import (
|
|
"context"
|
|
"flag"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/VictoriaMetrics/metrics"
|
|
"github.com/gofiber/fiber/v3"
|
|
"github.com/gookit/goutil/envutil"
|
|
libpack_config "github.com/lukaszraczylo/graphql-monitoring-proxy/config"
|
|
libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
|
|
)
|
|
|
|
type MetricsSetup struct {
|
|
metrics_set *metrics.Set
|
|
metrics_set_custom *metrics.Set
|
|
ic *InitConfig
|
|
metrics_prefix string
|
|
ctx context.Context
|
|
cancel context.CancelFunc
|
|
}
|
|
|
|
var log = libpack_logger.New().SetMinLogLevel(libpack_logger.LEVEL_INFO)
|
|
|
|
type InitConfig struct {
|
|
PurgeOnCrawl bool
|
|
PurgeEvery int
|
|
}
|
|
|
|
func NewMonitoring(ic *InitConfig) *MetricsSetup {
|
|
return NewMonitoringWithContext(context.Background(), ic)
|
|
}
|
|
|
|
// NewMonitoringWithContext creates a new monitoring instance with context for graceful shutdown
|
|
func NewMonitoringWithContext(ctx context.Context, ic *InitConfig) *MetricsSetup {
|
|
monCtx, cancel := context.WithCancel(ctx)
|
|
ms := &MetricsSetup{
|
|
ic: ic,
|
|
metrics_set: metrics.NewSet(),
|
|
metrics_set_custom: metrics.NewSet(),
|
|
ctx: monCtx,
|
|
cancel: cancel,
|
|
}
|
|
|
|
if flag.Lookup("test.v") == nil {
|
|
go ms.startPrometheusEndpoint()
|
|
|
|
if ic.PurgeEvery > 0 {
|
|
ticker := time.NewTicker(time.Duration(ic.PurgeEvery) * time.Second)
|
|
go func() {
|
|
defer ticker.Stop()
|
|
for {
|
|
select {
|
|
case <-ms.ctx.Done():
|
|
return
|
|
case <-ticker.C:
|
|
ms.PurgeMetrics()
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
}
|
|
|
|
return ms
|
|
}
|
|
|
|
// Shutdown stops the monitoring goroutines
|
|
func (ms *MetricsSetup) Shutdown() {
|
|
if ms.cancel != nil {
|
|
ms.cancel()
|
|
}
|
|
}
|
|
|
|
func (ms *MetricsSetup) startPrometheusEndpoint() {
|
|
app := fiber.New(fiber.Config{
|
|
AppName: fmt.Sprintf("GraphQL Monitoring Proxy - %s v%s", libpack_config.PKG_NAME, libpack_config.PKG_VERSION),
|
|
})
|
|
app.Get("/metrics", ms.metricsEndpoint)
|
|
// S6: bind address for the monitoring listener. This package does not
|
|
// import the main package's config, so it reads BIND_ADDRESS directly,
|
|
// the same way it already reads MONITORING_PORT. Empty default preserves
|
|
// today's "bind all interfaces" behavior (":port").
|
|
bindAddress := envutil.Getenv("BIND_ADDRESS", "")
|
|
if err := app.Listen(fmt.Sprintf("%s:%d", bindAddress, envutil.GetInt("MONITORING_PORT", 9393)), fiber.ListenConfig{DisableStartupMessage: true, GracefulContext: ms.ctx}); err != nil {
|
|
log.Critical(&libpack_logger.LogMessage{
|
|
Message: "Can't start the MONITORING service",
|
|
Pairs: map[string]any{"error": err},
|
|
})
|
|
}
|
|
}
|
|
|
|
func (ms *MetricsSetup) metricsEndpoint(c fiber.Ctx) error {
|
|
ms.metrics_set.WritePrometheus(c.Response().BodyWriter())
|
|
ms.metrics_set_custom.WritePrometheus(c.Response().BodyWriter())
|
|
|
|
if ms.ic.PurgeOnCrawl && ms.ic.PurgeEvery == 0 {
|
|
ms.PurgeMetrics()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (ms *MetricsSetup) AddMetricsPrefix(prefix string) {
|
|
ms.metrics_prefix = prefix
|
|
}
|
|
|
|
func (ms *MetricsSetup) ListActiveMetrics() []string {
|
|
return ms.metrics_set.ListMetricNames()
|
|
}
|
|
|
|
func (ms *MetricsSetup) RegisterMetricsGauge(metric_name string, labels map[string]string, val float64) *metrics.Gauge {
|
|
if ms == nil {
|
|
// Monitoring may not be initialized yet (e.g. during config parsing,
|
|
// before the monitoring server starts). Return a dummy to prevent panics.
|
|
return &metrics.Gauge{}
|
|
}
|
|
if err := validate_metrics_name(metric_name); err != nil {
|
|
log.Error(&libpack_logger.LogMessage{
|
|
Message: "RegisterMetricsGauge() error - invalid metric name",
|
|
Pairs: map[string]any{"error": err.Error(), "metric_name": metric_name},
|
|
})
|
|
// Return a dummy gauge instead of nil to prevent panics
|
|
return &metrics.Gauge{}
|
|
}
|
|
return ms.metrics_set_custom.GetOrCreateGauge(ms.get_metrics_name(metric_name, labels), func() float64 {
|
|
return val
|
|
})
|
|
}
|
|
|
|
// RegisterMetricsGaugeFunc registers a gauge with a callback function that is called on every scrape
|
|
// This is useful for metrics that need to return a dynamic value
|
|
func (ms *MetricsSetup) RegisterMetricsGaugeFunc(metric_name string, labels map[string]string, fn func() float64) *metrics.Gauge {
|
|
if ms == nil {
|
|
return &metrics.Gauge{}
|
|
}
|
|
if err := validate_metrics_name(metric_name); err != nil {
|
|
log.Error(&libpack_logger.LogMessage{
|
|
Message: "RegisterMetricsGaugeFunc() error - invalid metric name",
|
|
Pairs: map[string]any{"error": err.Error(), "metric_name": metric_name},
|
|
})
|
|
// Return a dummy gauge instead of nil to prevent panics
|
|
return &metrics.Gauge{}
|
|
}
|
|
return ms.metrics_set_custom.GetOrCreateGauge(ms.get_metrics_name(metric_name, labels), fn)
|
|
}
|
|
|
|
func (ms *MetricsSetup) RegisterMetricsCounter(metric_name string, labels map[string]string) *metrics.Counter {
|
|
if ms == nil {
|
|
return &metrics.Counter{}
|
|
}
|
|
if err := validate_metrics_name(metric_name); err != nil {
|
|
log.Error(&libpack_logger.LogMessage{
|
|
Message: "RegisterMetricsCounter() error - invalid metric name",
|
|
Pairs: map[string]any{"error": err.Error(), "metric_name": metric_name},
|
|
})
|
|
// Return a dummy counter instead of nil to prevent panics
|
|
return &metrics.Counter{}
|
|
}
|
|
if metric_name == MetricsSucceeded || metric_name == MetricsFailed || metric_name == MetricsSkipped {
|
|
return ms.metrics_set.GetOrCreateCounter(ms.get_metrics_name(metric_name, labels))
|
|
}
|
|
return ms.metrics_set_custom.GetOrCreateCounter(ms.get_metrics_name(metric_name, labels))
|
|
}
|
|
|
|
func (ms *MetricsSetup) RegisterFloatCounter(metric_name string, labels map[string]string) *metrics.FloatCounter {
|
|
if ms == nil {
|
|
return &metrics.FloatCounter{}
|
|
}
|
|
if err := validate_metrics_name(metric_name); err != nil {
|
|
log.Error(&libpack_logger.LogMessage{
|
|
Message: "RegisterFloatCounter() error - invalid metric name",
|
|
Pairs: map[string]any{"error": err.Error(), "metric_name": metric_name},
|
|
})
|
|
// Return a dummy float counter instead of nil to prevent panics
|
|
return &metrics.FloatCounter{}
|
|
}
|
|
return ms.metrics_set_custom.GetOrCreateFloatCounter(ms.get_metrics_name(metric_name, labels))
|
|
}
|
|
|
|
func (ms *MetricsSetup) RegisterMetricsSummary(metric_name string, labels map[string]string) *metrics.Summary {
|
|
if ms == nil {
|
|
return &metrics.Summary{}
|
|
}
|
|
if err := validate_metrics_name(metric_name); err != nil {
|
|
log.Error(&libpack_logger.LogMessage{
|
|
Message: "RegisterMetricsSummary() error - invalid metric name",
|
|
Pairs: map[string]any{"error": err.Error(), "metric_name": metric_name},
|
|
})
|
|
// Return a dummy summary instead of nil to prevent panics
|
|
return &metrics.Summary{}
|
|
}
|
|
return ms.metrics_set_custom.GetOrCreateSummary(ms.get_metrics_name(metric_name, labels))
|
|
}
|
|
|
|
func (ms *MetricsSetup) RegisterMetricsHistogram(metric_name string, labels map[string]string) *metrics.Histogram {
|
|
if ms == nil {
|
|
return &metrics.Histogram{}
|
|
}
|
|
if err := validate_metrics_name(metric_name); err != nil {
|
|
log.Error(&libpack_logger.LogMessage{
|
|
Message: "RegisterMetricsHistogram() error - invalid metric name",
|
|
Pairs: map[string]any{"error": err.Error(), "metric_name": metric_name},
|
|
})
|
|
// Return a dummy histogram instead of nil to prevent panics
|
|
return &metrics.Histogram{}
|
|
}
|
|
return ms.metrics_set_custom.GetOrCreateHistogram(ms.get_metrics_name(metric_name, labels))
|
|
}
|
|
|
|
func (ms *MetricsSetup) Increment(metric_name string, labels map[string]string) {
|
|
ms.RegisterMetricsCounter(metric_name, labels).Inc()
|
|
}
|
|
|
|
func (ms *MetricsSetup) IncrementFloat(metric_name string, labels map[string]string, value float64) {
|
|
ms.RegisterFloatCounter(metric_name, labels).Add(value)
|
|
}
|
|
|
|
func (ms *MetricsSetup) Set(metric_name string, labels map[string]string, value uint64) {
|
|
ms.RegisterMetricsCounter(metric_name, labels).Set(value)
|
|
}
|
|
|
|
func (ms *MetricsSetup) Update(metric_name string, labels map[string]string, value float64) {
|
|
ms.RegisterMetricsHistogram(metric_name, labels).Update(value)
|
|
}
|
|
|
|
func (ms *MetricsSetup) UpdateDuration(metric_name string, labels map[string]string, value time.Time) {
|
|
ms.RegisterMetricsHistogram(metric_name, labels).UpdateDuration(value)
|
|
}
|
|
|
|
func (ms *MetricsSetup) UpdateSummary(metric_name string, labels map[string]string, value float64) {
|
|
ms.RegisterMetricsSummary(metric_name, labels).Update(value)
|
|
}
|
|
|
|
func (ms *MetricsSetup) RemoveMetrics(metric_name string, labels map[string]string) {
|
|
ms.metrics_set_custom.UnregisterMetric(ms.get_metrics_name(metric_name, labels))
|
|
}
|
|
|
|
func (ms *MetricsSetup) PurgeMetrics() {
|
|
ms.metrics_set_custom.UnregisterAllMetrics()
|
|
}
|