From 24c26296998086e9729244bcb37d4a147054f5d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=93=D1=80=D0=B8=D0=B3=D0=BE=D1=80=D0=B8=D0=B9=20=D0=A1?= =?UTF-8?q?=D0=B0=D1=84=D1=80=D0=BE=D0=BD=D0=BE=D0=B2?= Date: Sun, 20 Sep 2026 22:09:49 +0000 Subject: [PATCH] Add internal/metrics/prometheus.go --- internal/metrics/prometheus.go | 337 +++++++++++++++++++++++++++++++++ 1 file changed, 337 insertions(+) create mode 100644 internal/metrics/prometheus.go diff --git a/internal/metrics/prometheus.go b/internal/metrics/prometheus.go new file mode 100644 index 0000000..bb82e65 --- /dev/null +++ b/internal/metrics/prometheus.go @@ -0,0 +1,337 @@ +/* + * Copyright 2026 Safronov Grigorii + * + * Licensed under the CDDL, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * + * You may obtain a copy of the License at + * https://opensource.org/licenses/CDDL-1.0 + */ + +// Файл: internal/metrics/prometheus.go +// Назначение: Экспорт метрик в формате Prometheus (text exposition format) +// без внешних зависимостей. Совместимо с Linux и OpenIndiana (illumos). +// Поддерживает типы: counter, gauge, histogram (упрощённый). +// +// ИСПРАВЛЕНО: вместо небезопасного приведения через unsafePointer +// используются стандартные math.Float64bits / math.Float64frombits, +// что полностью убирает зависимость от пакета unsafe и работает +// одинаково на всех платформах. + +package metrics + +import ( + "fmt" + "io" + "math" + "sort" + "strings" + "sync" + "sync/atomic" + "time" +) + +// MetricType тип метрики +type MetricType string + +const ( + CounterType MetricType = "counter" + GaugeType MetricType = "gauge" + HistogramType MetricType = "histogram" + SummaryType MetricType = "summary" +) + +// LabelPair пара ключ-значение +type LabelPair struct { + Name string + Value string +} + +// MetricFamily семейство метрик с одинаковым именем +type MetricFamily struct { + Name string + Help string + Type MetricType + mu sync.RWMutex + metrics map[string]*Metric // key: сериализованные labels +} + +// Metric конкретная метрика с набором labels +type Metric struct { + Labels []LabelPair + Value atomic.Uint64 // для counter/gauge храним float64 как биты + ValueF atomic.Value // float64 + Buckets []float64 // для histogram + Counts []atomic.Uint64 + Sum atomic.Uint64 // float64 как биты + Count atomic.Uint64 +} + +// Registry реестр метрик +type Registry struct { + families sync.Map // map[string]*MetricFamily + start time.Time +} + +var defaultRegistry = NewRegistry() + +// NewRegistry создаёт новый реестр метрик +func NewRegistry() *Registry { + return &Registry{start: time.Now()} +} + +// DefaultRegistry возвращает глобальный реестр +func DefaultRegistry() *Registry { return defaultRegistry } + +// RegisterCounter регистрирует counter-метрику +func (r *Registry) RegisterCounter(name, help string) *MetricFamily { + return r.register(name, help, CounterType) +} + +// RegisterGauge регистрирует gauge-метрику +func (r *Registry) RegisterGauge(name, help string) *MetricFamily { + return r.register(name, help, GaugeType) +} + +// RegisterHistogram регистрирует histogram-метрику +func (r *Registry) RegisterHistogram(name, help string, buckets []float64) *MetricFamily { + mf := r.register(name, help, HistogramType) + if len(buckets) == 0 { + buckets = []float64{0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10} + } + mf.mu.Lock() + mf.metrics["__buckets__"] = &Metric{Buckets: buckets} + mf.mu.Unlock() + return mf +} + +func (r *Registry) register(name, help string, t MetricType) *MetricFamily { + if mf, ok := r.families.Load(name); ok { + return mf.(*MetricFamily) + } + mf := &MetricFamily{ + Name: name, + Help: help, + Type: t, + metrics: make(map[string]*Metric), + } + actual, _ := r.families.LoadOrStore(name, mf) + return actual.(*MetricFamily) +} + +func labelsKey(labels []LabelPair) string { + if len(labels) == 0 { + return "" + } + sorted := make([]LabelPair, len(labels)) + copy(sorted, labels) + sort.Slice(sorted, func(i, j int) bool { return sorted[i].Name < sorted[j].Name }) + var b strings.Builder + for i, l := range sorted { + if i > 0 { + b.WriteByte(',') + } + b.WriteString(l.Name) + b.WriteByte('=') + b.WriteString(l.Value) + } + return b.String() +} + +// WithLabels возвращает метрику с указанными labels (создаёт при необходимости) +func (mf *MetricFamily) WithLabels(labels ...LabelPair) *Metric { + key := labelsKey(labels) + mf.mu.RLock() + if m, ok := mf.metrics[key]; ok { + mf.mu.RUnlock() + return m + } + mf.mu.RUnlock() + + mf.mu.Lock() + defer mf.mu.Unlock() + if m, ok := mf.metrics[key]; ok { + return m + } + m := &Metric{Labels: labels} + m.ValueF.Store(float64(0)) + if mf.Type == HistogramType { + if bm, ok := mf.metrics["__buckets__"]; ok { + m.Buckets = bm.Buckets + m.Counts = make([]atomic.Uint64, len(m.Buckets)) + } + } + mf.metrics[key] = m + return m +} + +// Inc увеличивает counter на 1 +func (m *Metric) Inc() { m.Add(1) } + +// Add добавляет delta к значению +func (m *Metric) Add(delta float64) { + for { + old := m.ValueF.Load() + var oldF float64 + if old != nil { + oldF = old.(float64) + } + if m.ValueF.CompareAndSwap(old, oldF+delta) { + return + } + } +} + +// Set устанавливает значение (для gauge) +func (m *Metric) Set(v float64) { + m.ValueF.Store(v) +} + +// Observe добавляет наблюдение в histogram +func (m *Metric) Observe(v float64) { + if m.Buckets == nil { + return + } + for i, b := range m.Buckets { + if v <= b { + m.Counts[i].Add(1) + } + } + m.Count.Add(1) + for { + old := m.Sum.Load() + oldF := float64FromBits(old) + newF := oldF + v + if m.Sum.CompareAndSwap(old, float64ToBits(newF)) { + break + } + } +} + +// float64ToBits и float64FromBits — стандартные обёртки над math. +// Используются для атомарного хранения float64 в atomic.Uint64. +func float64ToBits(f float64) uint64 { + return math.Float64bits(f) +} + +func float64FromBits(b uint64) float64 { + return math.Float64frombits(b) +} + +// WriteTo записывает метрики в формате Prometheus text exposition +func (r *Registry) WriteTo(w io.Writer) (int64, error) { + var total int64 + var writeErr error + + write := func(s string) { + if writeErr != nil { + return + } + n, err := io.WriteString(w, s) + total += int64(n) + if err != nil { + writeErr = err + } + } + + // Process metrics + write("# HELP futriis_up Whether futriis is up (1) or not (0)\n") + write("# TYPE futriis_up gauge\n") + write("futriis_up 1\n") + + write("# HELP futriis_uptime_seconds Uptime in seconds\n") + write("# TYPE futriis_uptime_seconds gauge\n") + write(fmt.Sprintf("futriis_uptime_seconds %.3f\n", time.Since(r.start).Seconds())) + + // Сортируем имена семейств для детерминированного вывода + names := make([]string, 0) + r.families.Range(func(k, v interface{}) bool { + names = append(names, k.(string)) + return true + }) + sort.Strings(names) + + for _, name := range names { + mfVal, _ := r.families.Load(name) + mf := mfVal.(*MetricFamily) + + write(fmt.Sprintf("# HELP %s %s\n", mf.Name, mf.Help)) + write(fmt.Sprintf("# TYPE %s %s\n", mf.Name, mf.Type)) + + mf.mu.RLock() + keys := make([]string, 0, len(mf.metrics)) + for k := range mf.metrics { + if k == "__buckets__" { + continue + } + keys = append(keys, k) + } + sort.Strings(keys) + + for _, k := range keys { + m := mf.metrics[k] + labelStr := formatLabels(m.Labels) + switch mf.Type { + case CounterType, GaugeType: + v := m.ValueF.Load() + var f float64 + if v != nil { + f = v.(float64) + } + write(fmt.Sprintf("%s%s %.6f\n", mf.Name, labelStr, f)) + case HistogramType: + cum := uint64(0) + for i, b := range m.Buckets { + cum += m.Counts[i].Load() + le := formatFloat(b) + write(fmt.Sprintf("%s_bucket%s le=\"%s\"} %d\n", mf.Name, labelStrWithBrace(labelStr), le, cum)) + } + write(fmt.Sprintf("%s_bucket%s le=\"+Inf\"} %d\n", mf.Name, labelStrWithBrace(labelStr), m.Count.Load())) + sumF := float64FromBits(m.Sum.Load()) + write(fmt.Sprintf("%s_sum%s %.6f\n", mf.Name, labelStr, sumF)) + write(fmt.Sprintf("%s_count%s %d\n", mf.Name, labelStr, m.Count.Load())) + } + } + mf.mu.RUnlock() + } + + return total, writeErr +} + +func formatLabels(labels []LabelPair) string { + if len(labels) == 0 { + return "" + } + var b strings.Builder + b.WriteByte('{') + for i, l := range labels { + if i > 0 { + b.WriteByte(',') + } + b.WriteString(l.Name) + b.WriteString("=\"") + b.WriteString(escapeLabelValue(l.Value)) + b.WriteByte('"') + } + b.WriteByte('}') + return b.String() +} + +// labelStrWithBrace превращает "{a=\"b\"}" в "{a=\"b\"," для добавления le="..." +func labelStrWithBrace(labelStr string) string { + if labelStr == "" { + return "{" + } + return labelStr[:len(labelStr)-1] + "," +} + +func escapeLabelValue(s string) string { + s = strings.ReplaceAll(s, "\\", "\\\\") + s = strings.ReplaceAll(s, "\"", "\\\"") + s = strings.ReplaceAll(s, "\n", "\\n") + return s +} + +func formatFloat(f float64) string { + return strings.TrimRight(strings.TrimRight(fmt.Sprintf("%f", f), "0"), ".") +} \ No newline at end of file