Add internal/metrics/prometheus.go
This commit is contained in:
1 parent
c660a67d6d
commit
24c2629699
1 file changed
+337
@@ -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"), ".")
|
||||
}
|
||||
Reference in new issue
Block a user