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