Files

337 lines
8.7 KiB
Go

/*
* 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"), ".")
}