package service import ( "sync" "github.com/prometheus/client_golang/prometheus" "go.uber.org/zap" ) const ( singleRequestLifecycleMetric = "iop_edge_single_request_lifecycle_total" singleRequestDurationMetric = "iop_edge_single_request_duration_seconds" singleRequestObservationLogKey = "edge_single_request_observation" ) // singleRequestMetrics contains only fixed-label collectors. Correlation is // intentionally a log-only field and is never admitted as a metric label. type singleRequestMetrics struct { lifecycle *prometheus.CounterVec duration *prometheus.HistogramVec } type singleRequestObservability struct { metrics *singleRequestMetrics mu sync.RWMutex logger *zap.Logger } var defaultSingleRequestMetrics struct { once sync.Once metrics *singleRequestMetrics } func defaultSingleRequestCollectorSet() *singleRequestMetrics { defaultSingleRequestMetrics.once.Do(func() { defaultSingleRequestMetrics.metrics = newSingleRequestMetrics(prometheus.DefaultRegisterer) }) return defaultSingleRequestMetrics.metrics } func newSingleRequestObservability(reg prometheus.Registerer, logger *zap.Logger) *singleRequestObservability { if logger == nil { logger = zap.NewNop() } return &singleRequestObservability{metrics: newSingleRequestMetrics(reg), logger: logger} } func newDefaultSingleRequestObservability(logger *zap.Logger) *singleRequestObservability { if logger == nil { logger = zap.NewNop() } return &singleRequestObservability{metrics: defaultSingleRequestCollectorSet(), logger: logger} } func newSingleRequestMetrics(reg prometheus.Registerer) *singleRequestMetrics { labels := []string{"event_class", "stage", "operation", "outcome", "error_class"} metrics := &singleRequestMetrics{ lifecycle: prometheus.NewCounterVec(prometheus.CounterOpts{ Name: singleRequestLifecycleMetric, Help: "Closed Edge single-request lifecycle observations.", }, labels), duration: prometheus.NewHistogramVec(prometheus.HistogramOpts{ Name: singleRequestDurationMetric, Help: "Closed Edge single-request lifecycle durations.", Buckets: prometheus.DefBuckets, }, labels), } if reg == nil { return metrics } metrics.lifecycle = registerSingleRequestCounter(reg, metrics.lifecycle) metrics.duration = registerSingleRequestHistogram(reg, metrics.duration) return metrics } func registerSingleRequestCounter(reg prometheus.Registerer, collector *prometheus.CounterVec) *prometheus.CounterVec { if err := reg.Register(collector); err != nil { if existing, ok := err.(prometheus.AlreadyRegisteredError); ok { if counter, ok := existing.ExistingCollector.(*prometheus.CounterVec); ok { return counter } } } return collector } func registerSingleRequestHistogram(reg prometheus.Registerer, collector *prometheus.HistogramVec) *prometheus.HistogramVec { if err := reg.Register(collector); err != nil { if existing, ok := err.(prometheus.AlreadyRegisteredError); ok { if histogram, ok := existing.ExistingCollector.(*prometheus.HistogramVec); ok { return histogram } } } return collector } // SetSingleRequestObservationLogger installs the bounded production observer. // It is called during Edge bootstrap before input servers are constructed. func (s *Service) SetSingleRequestObservationLogger(logger *zap.Logger) { if s == nil { return } s.SetSingleRequestObserver(newDefaultSingleRequestObservability(logger)) } // SetSingleRequestObservationLoggerForTesting installs an isolated collector // registry for cross-package integration tests. Production bootstrap must use // SetSingleRequestObservationLogger and the process-default collector set. func (s *Service) SetSingleRequestObservationLoggerForTesting(reg prometheus.Registerer, logger *zap.Logger) { if s == nil { return } s.SetSingleRequestObserver(newSingleRequestObservability(reg, logger)) } // SingleRequestObservationConfigured is a narrow bootstrap test seam. It // exposes only whether a non-noop observer is present, never the observer or // any request data. func (s *Service) SingleRequestObservationConfigured() bool { if s == nil { return false } s.mu.RLock() defer s.mu.RUnlock() return s.singleRequestObserver != nil } func (o *singleRequestObservability) Emit(dto singleRequestDTO) error { if o == nil || o.metrics == nil || !singleRequestDTOIsValid(dto) { return nil } labels := singleRequestMetricLabels(dto) o.metrics.lifecycle.WithLabelValues(labels...).Inc() if dto.DurationMS >= 0 { o.metrics.duration.WithLabelValues(labels...).Observe(float64(dto.DurationMS) / 1000) } o.mu.RLock() logger := o.logger o.mu.RUnlock() if logger == nil { return nil } logger.Info(singleRequestObservationLogKey, zap.String("correlation", singleRequestSanitizeString(dto.Correlation)), zap.String("event_class", string(dto.EventClass)), zap.String("stage", singleRequestMetricStage(dto.Stage)), zap.String("operation", singleRequestMetricOperation(dto.Operation)), zap.String("outcome", singleRequestMetricOutcome(dto.Outcome)), zap.String("error_class", singleRequestMetricErrorClass(dto.ErrorClass)), zap.Int64("duration_ms", dto.DurationMS), zap.Int("tool_count", dto.ToolCount), zap.Bool("has_result", dto.HasResult), ) return nil } func singleRequestMetricLabels(dto singleRequestDTO) []string { return []string{ string(dto.EventClass), singleRequestMetricStage(dto.Stage), singleRequestMetricOperation(dto.Operation), singleRequestMetricOutcome(dto.Outcome), singleRequestMetricErrorClass(dto.ErrorClass), } } func singleRequestMetricStage(value singleRequestStage) string { if singleRequestStageIsValid(value) { return string(value) } return "none" } func singleRequestMetricOperation(value singleRequestOperation) string { if singleRequestOperationIsValid(value) { return string(value) } return "none" } func singleRequestMetricOutcome(value singleRequestOutcome) string { if singleRequestOutcomeIsValid(value) { return string(value) } return "none" } func singleRequestMetricErrorClass(value singleRequestErrorClass) string { if singleRequestErrorClassIsValid(value) { return string(value) } return "none" }