승인된 execution preset을 Edge 조정 경계와 Node workspace/tool 실행 경계로 연결해 단일 요청 수명주기와 관측 계약을 일관되게 처리한다.
184 lines
5.6 KiB
Go
184 lines
5.6 KiB
Go
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))
|
|
}
|
|
|
|
// 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"
|
|
}
|