package m2mcore import ( "fmt" "io" "sort" "strings" "sync" "time" ) // Recorder — интерфейс записи метрик. NoopRecorder используется по // умолчанию; реальная реализация (Prometheus) подключается в M2, когда // внешние Go-зависимости становятся доступны. type Recorder interface { StageDuration(stage State, d time.Duration) IncDeal(state State) IncSLABreach(stage State, budget string) } // NoopRecorder ничего не пишет. type NoopRecorder struct{} // StageDuration — no-op. func (NoopRecorder) StageDuration(State, time.Duration) {} // IncDeal — no-op. func (NoopRecorder) IncDeal(State) {} // IncSLABreach — no-op. func (NoopRecorder) IncSLABreach(State, string) {} // MemoryRecorder — простая in-memory реализация Recorder, удобная для // тестов и для /metrics-endpoint в M1 (Prometheus text format). type MemoryRecorder struct { mu sync.Mutex // stageDurations: stage -> сумма длительностей и счётчик stageDurations map[State]struct { Sum time.Duration Count uint64 } dealsByState map[State]uint64 slaBreaches map[string]uint64 // ключ: "stage|budget" } // NewMemoryRecorder создаёт MemoryRecorder с пустыми счётчиками. func NewMemoryRecorder() *MemoryRecorder { return &MemoryRecorder{ stageDurations: make(map[State]struct { Sum time.Duration Count uint64 }), dealsByState: make(map[State]uint64), slaBreaches: make(map[string]uint64), } } // StageDuration фиксирует длительность этапа FSM. func (m *MemoryRecorder) StageDuration(stage State, d time.Duration) { m.mu.Lock() defer m.mu.Unlock() v := m.stageDurations[stage] v.Sum += d v.Count++ m.stageDurations[stage] = v } // IncDeal увеличивает счётчик сделок в заданном итоговом состоянии. func (m *MemoryRecorder) IncDeal(state State) { m.mu.Lock() defer m.mu.Unlock() m.dealsByState[state]++ } // IncSLABreach фиксирует превышение SLA-бюджета на этапе. func (m *MemoryRecorder) IncSLABreach(stage State, budget string) { m.mu.Lock() defer m.mu.Unlock() m.slaBreaches[fmt.Sprintf("%s|%s", stage, budget)]++ } // WritePrometheus сериализует накопленные метрики в формате Prometheus // text exposition (HELP/TYPE + значения). Подходит для /metrics. func (m *MemoryRecorder) WritePrometheus(w io.Writer) error { m.mu.Lock() defer m.mu.Unlock() fmt.Fprintf(w, "# HELP m2m_stage_duration_seconds_sum Сумма длительностей этапа в секундах\n") fmt.Fprintf(w, "# TYPE m2m_stage_duration_seconds_sum counter\n") stages := sortedStates(m.stageDurations) for _, s := range stages { v := m.stageDurations[s] fmt.Fprintf(w, "m2m_stage_duration_seconds_sum{stage=%q} %f\n", s, v.Sum.Seconds()) fmt.Fprintf(w, "m2m_stage_duration_seconds_count{stage=%q} %d\n", s, v.Count) } fmt.Fprintf(w, "# HELP m2m_deals_total Сделки по итоговому состоянию\n") fmt.Fprintf(w, "# TYPE m2m_deals_total counter\n") dstates := sortedStatesCount(m.dealsByState) for _, s := range dstates { fmt.Fprintf(w, "m2m_deals_total{state=%q} %d\n", s, m.dealsByState[s]) } fmt.Fprintf(w, "# HELP m2m_sla_breaches_total Превышения SLA-бюджета\n") fmt.Fprintf(w, "# TYPE m2m_sla_breaches_total counter\n") keys := make([]string, 0, len(m.slaBreaches)) for k := range m.slaBreaches { keys = append(keys, k) } sort.Strings(keys) for _, k := range keys { stage, budget, ok := strings.Cut(k, "|") if !ok { continue } fmt.Fprintf(w, "m2m_sla_breaches_total{stage=%q,budget=%q} %d\n", stage, budget, m.slaBreaches[k]) } return nil } // sortedStates возвращает состояния как отсортированный слайс для // детерминированного вывода. func sortedStates(m map[State]struct { Sum time.Duration Count uint64 }) []State { out := make([]State, 0, len(m)) for k := range m { out = append(out, k) } sort.Slice(out, func(i, j int) bool { return out[i] < out[j] }) return out } func sortedStatesCount(m map[State]uint64) []State { out := make([]State, 0, len(m)) for k := range m { out = append(out, k) } sort.Slice(out, func(i, j int) bool { return out[i] < out[j] }) return out }