Files
onix/pkg/metrics/metrics.go
2025-11-13 13:16:16 +05:30

187 lines
4.7 KiB
Go

package metrics
import (
"context"
"errors"
"fmt"
"net/http"
"sync"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
otelprom "go.opentelemetry.io/otel/exporters/prometheus"
otelmetric "go.opentelemetry.io/otel/metric"
"go.opentelemetry.io/otel/sdk/metric"
"go.opentelemetry.io/otel/sdk/resource"
)
var (
mp *metric.MeterProvider
meter otelmetric.Meter
prometheusRegistry *prometheus.Registry
once sync.Once
shutdownFunc func(context.Context) error
ErrInvalidExporter = errors.New("invalid metrics exporter type")
ErrMetricsNotInit = errors.New("metrics not initialized")
)
// ExporterType represents the type of metrics exporter.
type ExporterType string
const (
// ExporterPrometheus exports metrics in Prometheus format.
ExporterPrometheus ExporterType = "prometheus"
)
// Config represents the configuration for metrics.
type Config struct {
Enabled bool `yaml:"enabled"`
ExporterType ExporterType `yaml:"exporterType"`
ServiceName string `yaml:"serviceName"`
ServiceVersion string `yaml:"serviceVersion"`
Prometheus PrometheusConfig `yaml:"prometheus"`
}
// PrometheusConfig represents Prometheus exporter configuration.
type PrometheusConfig struct {
Port string `yaml:"port"`
Path string `yaml:"path"`
}
// validate validates the metrics configuration.
func (c *Config) validate() error {
if !c.Enabled {
return nil
}
if c.ExporterType != ExporterPrometheus {
return fmt.Errorf("%w: %s", ErrInvalidExporter, c.ExporterType)
}
if c.ServiceName == "" {
c.ServiceName = "beckn-onix"
}
return nil
}
// InitMetrics initializes the OpenTelemetry metrics SDK.
func InitMetrics(cfg Config) error {
if !cfg.Enabled {
return nil
}
var initErr error
once.Do(func() {
if initErr = cfg.validate(); initErr != nil {
return
}
// Create resource with service information.
attrs := []attribute.KeyValue{
attribute.String("service.name", cfg.ServiceName),
}
if cfg.ServiceVersion != "" {
attrs = append(attrs, attribute.String("service.version", cfg.ServiceVersion))
}
res, err := resource.New(
context.Background(),
resource.WithAttributes(attrs...),
)
if err != nil {
initErr = fmt.Errorf("failed to create resource: %w", err)
return
}
// Always create Prometheus exporter for /metrics endpoint
// Create a custom registry for the exporter so we can use it for HTTP serving
promRegistry := prometheus.NewRegistry()
promExporter, err := otelprom.New(otelprom.WithRegisterer(promRegistry))
if err != nil {
initErr = fmt.Errorf("failed to create Prometheus exporter: %w", err)
return
}
prometheusRegistry = promRegistry
// Create readers based on configuration.
var readers []metric.Reader
// Always add Prometheus reader for /metrics endpoint
readers = append(readers, promExporter)
// Create meter provider with all readers
opts := []metric.Option{
metric.WithResource(res),
}
for _, reader := range readers {
opts = append(opts, metric.WithReader(reader))
}
mp = metric.NewMeterProvider(opts...)
// Set global meter provider.
otel.SetMeterProvider(mp)
// Create meter for this package.
meter = mp.Meter("github.com/beckn-one/beckn-onix")
// Store shutdown function.
shutdownFunc = func(ctx context.Context) error {
return mp.Shutdown(ctx)
}
})
return initErr
}
// GetMeter returns the global meter instance.
func GetMeter() otelmetric.Meter {
if meter == nil {
// Return a no-op meter if not initialized.
return otel.Meter("noop")
}
return meter
}
// Shutdown gracefully shuts down the metrics provider.
func Shutdown(ctx context.Context) error {
if shutdownFunc == nil {
return nil
}
return shutdownFunc(ctx)
}
// IsEnabled returns whether metrics are enabled.
func IsEnabled() bool {
return mp != nil
}
// MetricsHandler returns the HTTP handler for the /metrics endpoint.
// Returns nil if metrics are not enabled.
func MetricsHandler() http.Handler {
if prometheusRegistry == nil {
return nil
}
// Use promhttp to serve the Prometheus registry
return promhttp.HandlerFor(prometheusRegistry, promhttp.HandlerOpts{})
}
// InitAllMetrics initializes all metrics subsystems.
// This includes request metrics and runtime metrics.
// Returns an error if any initialization fails.
func InitAllMetrics() error {
if !IsEnabled() {
return nil
}
if err := InitRequestMetrics(); err != nil {
return fmt.Errorf("failed to initialize request metrics: %w", err)
}
if err := InitRuntimeMetrics(); err != nil {
return fmt.Errorf("failed to initialize runtime metrics: %w", err)
}
return nil
}