DNF90 服务端与启动器完整源码(含 GM 工具功能增强)

服务端(Go):
- GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面
- GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新
- 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新)
- 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍
- 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0)
- 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算

启动器(Python/JS):
- 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭
- GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐)
- 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
This commit is contained in:
hanfuit committed 2026-11-04 14:31:55 +08:00
commit 645b64fe21
4388 files changed
+1405457

No files matched your search

@@ -0,0 +1,150 @@
package metrics
import (
"strings"
"time"
)
// 这些名称是框架层的最低共同语言,避免每个项目自造一套不可聚合的运营指标。
const (
REDRequestsMetric = "requests_total"
REDErrorsMetric = "errors_total"
REDDurationMetric = "request_duration"
OnlinePlayersMetric = "online_players"
LoginEventsMetric = "login_events_total"
PaymentEventsMetric = "payment_events_total"
PaymentAmountMicrosMetric = "payment_amount_micros_total"
RetentionEventsMetric = "retention_events_total"
)
// RED 把 rate、errors、duration 绑定到同一套 label 约束,避免 SLO 口径漂移。
type RED struct {
scope Scope
service string
duration *Histogram
}
type REDOptions struct {
Buckets []float64
}
func NewRED(scope Scope, service string, opts REDOptions) *RED {
service = strings.TrimSpace(service)
if service == "" {
service = "default"
}
baseLabels := map[string]string{"service": service}
histogram := NewHistogram(scope, REDDurationMetric, HistogramOptions{
Buckets: opts.Buckets,
Labels: baseLabels,
})
return &RED{
scope: scope.WithLabels(baseLabels),
service: service,
duration: histogram,
}
}
func (r *RED) Record(route string, code string, elapsed time.Duration) {
if r == nil {
return
}
route = sanitizeLabelValue(route, "unknown")
code = sanitizeLabelValue(code, "OK")
labels := map[string]string{"route": route, "code": code}
r.scope.Inc(REDRequestsMetric, labels)
if isErrorCode(code) {
r.scope.Inc(REDErrorsMetric, labels)
}
if elapsed < 0 {
elapsed = 0
}
if r.duration != nil {
r.duration.ObserveWithLabels(elapsed.Seconds(), labels)
}
}
func (r *RED) RecordError(route string, code string, elapsed time.Duration) {
if strings.TrimSpace(code) == "" {
code = "INTERNAL_ERR"
}
r.Record(route, code, elapsed)
}
func (r *RED) Service() string {
if r == nil {
return ""
}
return r.service
}
func isErrorCode(code string) bool {
upper := strings.ToUpper(strings.TrimSpace(code))
if upper == "" || upper == "OK" {
return false
}
if strings.HasPrefix(upper, "ERR") {
return true
}
switch upper {
case "FAIL", "FAILED", "INTERNAL_ERR", "TIMEOUT", "PANIC":
return true
}
return len(upper) == 3 && upper[0] == '5'
}
// OnlinePlayers 固定 segment、region、server_id 三个维度,避免在线人数指标被 user_id 拉爆。
type OnlinePlayers struct {
scope Scope
}
func NewOnlinePlayers(scope Scope, service string) *OnlinePlayers {
service = strings.TrimSpace(service)
if service == "" {
service = "default"
}
return &OnlinePlayers{
scope: scope.WithLabels(map[string]string{"service": service}),
}
}
func (o *OnlinePlayers) Set(segment, region, serverID string, count int64) {
if o == nil {
return
}
labels := map[string]string{
"segment": sanitizeLabelValue(segment, "all"),
"region": sanitizeLabelValue(region, "any"),
"server_id": sanitizeLabelValue(serverID, "any"),
}
o.scope.SetGauge(OnlinePlayersMetric, labels, count)
}
func (o *OnlinePlayers) Add(segment, region, serverID string, delta int64) {
if o == nil {
return
}
labels := map[string]string{
"segment": sanitizeLabelValue(segment, "all"),
"region": sanitizeLabelValue(region, "any"),
"server_id": sanitizeLabelValue(serverID, "any"),
}
o.scope.AddGauge(OnlinePlayersMetric, labels, delta)
}
func sanitizeLabelValue(value string, fallback string) string {
value = strings.TrimSpace(value)
if value == "" {
return fallback
}
if len(value) > 64 {
return fallback
}
for _, r := range value {
if r < 0x20 {
return fallback
}
}
return value
}
@@ -0,0 +1,11 @@
package metrics
import "math"
// Int64FromUint64 将 uint64 饱和转换为 int64,避免监控计数器溢出后回绕成负数。
func Int64FromUint64(value uint64) int64 {
if value > math.MaxInt64 {
return math.MaxInt64
}
return int64(value) //nolint:gosec // G115:前面已按 int64 最大值钳制。
}
@@ -0,0 +1,204 @@
package metrics
import (
"math"
"sort"
"strconv"
"strings"
"sync"
)
// DurationBucketsSec 匹配 Prometheus client_golang 的默认延迟桶布局。
// 适合 HTTP/RPC 调用、handler 分发、Profile 读写、Redis/MySQL 命令、Lua 限流等
// 大部分游戏服务端"亚秒级到秒级"的请求延迟。需要更长尾时显式传 Buckets 覆盖。
var DurationBucketsSec = []float64{
0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10,
}
// Histogram 是观察值(默认按秒)的分布统计;按 Prometheus 协议输出为
// <base>_bucket / <base>_count / <base>_sum 三类样本族。
//
// Histogram 自身只维护内存累加状态;最终样本通过注册的 Observer 在每次
// scrape 触发时写回 Registry。这使得 Histogram 可以承载高频 Observe 而不
// 每次都 touch Registry 的全局 map。
//
// 调用方应在启动时构造一次 Histogram 并复用,不要在每次请求里 New。
type Histogram struct {
registry *Registry
name string
buckets []float64
base map[string]string
mu sync.Mutex
series map[string]*histogramSeries
}
type histogramSeries struct {
labels map[string]string
bucketCnt []uint64
count uint64
sumSec float64
}
// HistogramOptions 用于构造 Histogram。空字段会回退到合理默认。
type HistogramOptions struct {
// Buckets 是上界(秒)切片,必须严格递增且为正;空切片回退到 DurationBucketsSec。
// 不需要手动加 +Inf 桶,Histogram 在序列化时自动追加。
Buckets []float64
// Labels 是该 Histogram 上所有样本共享的基础标签,常见用法是固定 service、node。
Labels map[string]string
}
// NewHistogram 构造一个 Histogram 并注册成该 Scope 对应 Registry 的 observer。
// observerKey 与 metric name 关联,重复构造同名 Histogram 会替换前一个 observer。
func NewHistogram(scope Scope, name string, opts HistogramOptions) *Histogram {
if scope.registry == nil {
return &Histogram{name: scope.Name(name)}
}
fullName := scope.Name(name)
if fullName == "" {
return &Histogram{}
}
buckets := normalizeBuckets(opts.Buckets)
h := &Histogram{
registry: scope.registry,
name: fullName,
buckets: buckets,
base: scope.Labels(opts.Labels),
series: make(map[string]*histogramSeries),
}
scope.registry.RegisterObserver("histogram:"+fullName, func(r *Registry) {
h.flush(r)
})
return h
}
// Buckets 返回该 Histogram 当前生效的上界(不含 +Inf)。仅用于调试 / 测试。
func (h *Histogram) Buckets() []float64 {
if h == nil {
return nil
}
out := make([]float64, len(h.buckets))
copy(out, h.buckets)
return out
}
// Observe 记录一次观察值,默认无附加 labels。
func (h *Histogram) Observe(seconds float64) {
h.ObserveWithLabels(seconds, nil)
}
// ObserveWithLabels 记录一次观察值并附加 per-observation labels。labels 会与
// HistogramOptions.Labels 合并;每组不同的 labels 都会成为独立的样本序列。
func (h *Histogram) ObserveWithLabels(seconds float64, labels map[string]string) {
if h == nil || h.registry == nil {
return
}
if math.IsNaN(seconds) || math.IsInf(seconds, 0) {
return
}
if seconds < 0 {
seconds = 0
}
merged := mergeLabels(h.base, labels)
key := labelsKey(merged)
h.mu.Lock()
series, ok := h.series[key]
if !ok {
series = &histogramSeries{
labels: cloneLabels(merged),
bucketCnt: make([]uint64, len(h.buckets)),
}
h.series[key] = series
}
series.count++
series.sumSec += seconds
for i, upper := range h.buckets {
if seconds <= upper {
series.bucketCnt[i]++
}
}
h.mu.Unlock()
}
// flush 把内存累加状态写回 Registry。在每次 scrape 由 RunObservers 触发。
// 我们以"绝对值"写回(SetHistogramSample 替换语义),避免与 normalizeSamples
// 的累加路径互相干扰。
func (h *Histogram) flush(registry *Registry) {
if h == nil || registry == nil {
return
}
h.mu.Lock()
snapshot := make([]*histogramSeries, 0, len(h.series))
for _, series := range h.series {
snapshot = append(snapshot, &histogramSeries{
labels: cloneLabels(series.labels),
bucketCnt: append([]uint64(nil), series.bucketCnt...),
count: series.count,
sumSec: series.sumSec,
})
}
h.mu.Unlock()
for _, series := range snapshot {
for i, upper := range h.buckets {
leLabels := withLabel(series.labels, "le", formatBucketBound(upper))
registry.SetHistogramSample(h.name+"_bucket", leLabels, Int64FromUint64(series.bucketCnt[i]), false, 0)
}
// +Inf 桶等价于 count,但 Prometheus 协议要求它显式出现。
infLabels := withLabel(series.labels, "le", "+Inf")
registry.SetHistogramSample(h.name+"_bucket", infLabels, Int64FromUint64(series.count), false, 0)
registry.SetHistogramSample(h.name+"_count", series.labels, Int64FromUint64(series.count), false, 0)
registry.SetHistogramSample(h.name+"_sum", series.labels, 0, true, series.sumSec)
}
}
// normalizeBuckets 校验 / 复制 / 排序 buckets。空输入返回默认;输入会被复制
// 以避免调用方后续修改影响 Histogram 内部状态。
func normalizeBuckets(in []float64) []float64 {
if len(in) == 0 {
out := make([]float64, len(DurationBucketsSec))
copy(out, DurationBucketsSec)
return out
}
cleaned := make([]float64, 0, len(in))
for _, b := range in {
if math.IsNaN(b) || math.IsInf(b, 0) || b <= 0 {
continue
}
cleaned = append(cleaned, b)
}
if len(cleaned) == 0 {
return normalizeBuckets(nil)
}
sort.Float64s(cleaned)
// 去重,避免 le 标签冲突。
deduped := cleaned[:0]
for i, b := range cleaned {
if i == 0 || b != cleaned[i-1] {
deduped = append(deduped, b)
}
}
out := make([]float64, len(deduped))
copy(out, deduped)
return out
}
func withLabel(labels map[string]string, key, value string) map[string]string {
out := make(map[string]string, len(labels)+1)
for k, v := range labels {
out[k] = v
}
out[key] = value
return out
}
func formatBucketBound(value float64) string {
// Prometheus 习惯把 0.005 显示成 "0.005" 而非 "5e-03"。用 %g 在大多数桶
// 上会给出干净小数;遇到极小或极大值时再退到科学计数。
s := strconv.FormatFloat(value, 'g', -1, 64)
// 进一步避免 "1" 这种没有小数点的形式造成与整数 bucket 混淆。
if !strings.ContainsAny(s, ".eE") {
s += ".0"
}
return s
}
+244
View File
@@ -0,0 +1,244 @@
package metrics
import (
"sort"
"strings"
"sync"
)
type Type string
const (
TypeCounter Type = "counter"
TypeGauge Type = "gauge"
TypeHistogram Type = "histogram"
)
// Sample 是 registry / observer 输出的最小单元。Value 用 int64 适合 counter / gauge
// 以及 histogram 的 bucket / count;当样本表示浮点值(如 histogram 的 sum)时,
// 设置 Float=true 并把数值写到 FloatValue。这种双字段写法兼容已有所有 counter/gauge
// 调用方,新增字段对存量 Sample 字面量保持零值,不影响 reflect.DeepEqual 比较。
type Sample struct {
Name string `json:"name"`
Type Type `json:"type"`
Labels map[string]string `json:"labels,omitempty"`
Value int64 `json:"value"`
Float bool `json:"float,omitempty"`
FloatValue float64 `json:"float_value,omitempty"`
}
type Observer func(*Registry)
type observerEntry struct {
name string
fn Observer
}
type Registry struct {
mu sync.RWMutex
samples map[string]Sample
observers []observerEntry
}
func New() *Registry {
return &Registry{samples: make(map[string]Sample)}
}
// RegisterObserver 注册 scrape 时执行的回调。
// Snapshot 向 Prometheus 客户端返回样本前会调用 observer;observer 应保持低成本
// (例如 O(1) Redis SCARD 或本地 atomics),并且必须能承受并发调用。
// 使用同名重新注册会替换旧回调;fn 为 nil 表示注销。
func (r *Registry) RegisterObserver(name string, fn Observer) {
if r == nil || strings.TrimSpace(name) == "" {
return
}
r.mu.Lock()
defer r.mu.Unlock()
for idx := range r.observers {
if r.observers[idx].name == name {
if fn == nil {
r.observers = append(r.observers[:idx], r.observers[idx+1:]...)
return
}
r.observers[idx].fn = fn
return
}
}
if fn == nil {
return
}
r.observers = append(r.observers, observerEntry{name: name, fn: fn})
}
// RunObservers 调用所有已注册 observer。
// Prometheus handler 和 /debug/metrics 会在序列化前调用它,让观测样本反映当前状态。
// observer 会顺序执行,不能长时间阻塞。
func (r *Registry) RunObservers() {
if r == nil {
return
}
r.mu.RLock()
entries := make([]observerEntry, len(r.observers))
copy(entries, r.observers)
r.mu.RUnlock()
for _, entry := range entries {
if entry.fn == nil {
continue
}
entry.fn(r)
}
}
func (r *Registry) Inc(name string, labels map[string]string) {
r.Add(name, labels, 1)
}
func (r *Registry) Add(name string, labels map[string]string, delta int64) {
if r == nil || name == "" {
return
}
r.mu.Lock()
defer r.mu.Unlock()
r.ensureSamplesLocked()
key := sampleKey(TypeCounter, name, labels)
sample := r.samples[key]
if sample.Name == "" {
sample = Sample{Name: name, Type: TypeCounter, Labels: cloneLabels(labels)}
}
sample.Value += delta
r.samples[key] = sample
}
func (r *Registry) SetCounter(name string, labels map[string]string, value int64) {
if r == nil || name == "" {
return
}
r.mu.Lock()
defer r.mu.Unlock()
r.ensureSamplesLocked()
key := sampleKey(TypeCounter, name, labels)
r.samples[key] = Sample{Name: name, Type: TypeCounter, Labels: cloneLabels(labels), Value: value}
}
func (r *Registry) SetGauge(name string, labels map[string]string, value int64) {
if r == nil || name == "" {
return
}
r.mu.Lock()
defer r.mu.Unlock()
r.ensureSamplesLocked()
key := sampleKey(TypeGauge, name, labels)
r.samples[key] = Sample{Name: name, Type: TypeGauge, Labels: cloneLabels(labels), Value: value}
}
// SetHistogramSample 写入或替换 histogram family 中的一项样本。
// Histogram family 会拆成多条共享 base 名的样本:
// - <base>_bucket{le="..."} 累计计数,int
// - <base>_count 总观察次数,int
// - <base>_sum 观察值总和(float 秒,set float=true)
//
// observer 会在 RunObservers 中调用它发布 histogram 的当前绝对状态;
// 和 SetGauge 一样,这里是替换语义,不做累加。Histogram.Observe 是公开 API,
// 调用方通常不直接使用 SetHistogramSample。
func (r *Registry) SetHistogramSample(name string, labels map[string]string, value int64, float bool, floatValue float64) {
if r == nil || name == "" {
return
}
r.mu.Lock()
defer r.mu.Unlock()
r.ensureSamplesLocked()
key := sampleKey(TypeHistogram, name, labels)
r.samples[key] = Sample{
Name: name,
Type: TypeHistogram,
Labels: cloneLabels(labels),
Value: value,
Float: float,
FloatValue: floatValue,
}
}
func (r *Registry) AddGauge(name string, labels map[string]string, delta int64) {
if r == nil || name == "" {
return
}
r.mu.Lock()
defer r.mu.Unlock()
r.ensureSamplesLocked()
key := sampleKey(TypeGauge, name, labels)
sample := r.samples[key]
if sample.Name == "" {
sample = Sample{Name: name, Type: TypeGauge, Labels: cloneLabels(labels)}
}
sample.Value += delta
r.samples[key] = sample
}
func (r *Registry) Snapshot() []Sample {
if r == nil {
return nil
}
r.mu.RLock()
out := make([]Sample, 0, len(r.samples))
for _, sample := range r.samples {
sample.Labels = cloneLabels(sample.Labels)
out = append(out, sample)
}
r.mu.RUnlock()
sort.Slice(out, func(i, j int) bool {
if out[i].Name != out[j].Name {
return out[i].Name < out[j].Name
}
if out[i].Type != out[j].Type {
return out[i].Type < out[j].Type
}
return labelsKey(out[i].Labels) < labelsKey(out[j].Labels)
})
return out
}
func (r *Registry) ensureSamplesLocked() {
if r.samples == nil {
r.samples = make(map[string]Sample)
}
}
func sampleKey(sampleType Type, name string, labels map[string]string) string {
return string(sampleType) + "\x00" + name + "\x00" + labelsKey(labels)
}
func labelsKey(labels map[string]string) string {
if len(labels) == 0 {
return ""
}
keys := make([]string, 0, len(labels))
for key := range labels {
keys = append(keys, key)
}
sort.Strings(keys)
var b strings.Builder
for _, key := range keys {
b.WriteString(key)
b.WriteByte('=')
b.WriteString(labels[key])
b.WriteByte('\x00')
}
return b.String()
}
func cloneLabels(labels map[string]string) map[string]string {
if len(labels) == 0 {
return nil
}
out := make(map[string]string, len(labels))
for key, value := range labels {
out[key] = value
}
return out
}
@@ -0,0 +1,193 @@
package metrics
import (
"fmt"
"net/http"
"regexp"
"sort"
"strings"
)
var metricNameRE = regexp.MustCompile(`[^a-zA-Z0-9_:]`)
var labelNameRE = regexp.MustCompile(`[^a-zA-Z0-9_]`)
type prometheusSample struct {
name string
typ Type
labels string
value int64
float bool
floatValue float64
}
func Handler(registry *Registry) http.HandlerFunc {
return func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "text/plain; version=0.0.4; charset=utf-8")
ObserveRuntime(registry)
registry.RunObservers()
WritePrometheus(w, registry.Snapshot())
}
}
func WritePrometheus(w interface{ Write([]byte) (int, error) }, samples []Sample) {
seenTypes := make(map[string]Type)
for _, sample := range normalizeSamples(samples) {
// Histogram 多个子样本共享同一 base 名字 (<base>_bucket / _count / _sum),
// TYPE 标注必须按 family 去重;其他类型按完整 metric 名去重保持原行为。
typeKey := sample.name
if sample.typ == TypeHistogram {
typeKey = histogramFamily(sample.name)
}
if existing, ok := seenTypes[typeKey]; ok {
if existing != sample.typ {
continue
}
} else {
seenTypes[typeKey] = sample.typ
_, _ = fmt.Fprintf(w, "# TYPE %s %s\n", typeKey, prometheusType(sample.typ))
}
if sample.float {
// 用 %g 而非 %f:histogram 的 sum 既要避免 1.0e-06 这种科学计数,
// 又要避免拖一长串无意义的零;%g 在大多数 latency 场景下输出可读小数。
_, _ = fmt.Fprintf(w, "%s%s %g\n", sample.name, sample.labels, sample.floatValue)
} else {
_, _ = fmt.Fprintf(w, "%s%s %d\n", sample.name, sample.labels, sample.value)
}
}
}
// histogramFamily 把 <base>_bucket / <base>_count / <base>_sum
// 还原成 <base>,用于 TYPE 标注去重。未识别后缀时返回原名,等价于"该样本自己就是 family"。
func histogramFamily(name string) string {
for _, suffix := range []string{"_sum", "_count", "_bucket"} {
if strings.HasSuffix(name, suffix) {
return strings.TrimSuffix(name, suffix)
}
}
return name
}
func normalizeSamples(samples []Sample) []prometheusSample {
merged := make(map[string]prometheusSample, len(samples))
for _, sample := range samples {
name := normalizeName(sample.Name)
if name == "" {
continue
}
labels := formatLabels(sample.Labels)
key := string(sample.Type) + "\x00" + name + "\x00" + labels
current := merged[key]
if current.name == "" {
current = prometheusSample{name: name, typ: sample.Type, labels: labels}
}
// Histogram observers 写入的是绝对值(替换语义,跟 gauge 一致),不能累加;
// 否则 bucket / count / sum 会被多次叠加。Gauge 同理。
switch sample.Type {
case TypeGauge, TypeHistogram:
current.value = sample.Value
current.float = sample.Float
current.floatValue = sample.FloatValue
default:
current.value += sample.Value
}
merged[key] = current
}
out := make([]prometheusSample, 0, len(merged))
for _, sample := range merged {
out = append(out, sample)
}
sort.Slice(out, func(i, j int) bool {
if out[i].name != out[j].name {
return out[i].name < out[j].name
}
if out[i].typ != out[j].typ {
return out[i].typ < out[j].typ
}
return out[i].labels < out[j].labels
})
return out
}
func prometheusType(t Type) string {
switch t {
case TypeGauge:
return "gauge"
case TypeHistogram:
return "histogram"
default:
return "counter"
}
}
func normalizeName(name string) string {
return normalizeIdentifier(name, metricNameRE, true)
}
func normalizeLabelName(name string) string {
return normalizeIdentifier(name, labelNameRE, false)
}
func normalizeIdentifier(name string, invalid *regexp.Regexp, allowColonFirst bool) string {
name = strings.TrimSpace(name)
name = invalid.ReplaceAllString(name, "_")
name = strings.Trim(name, "_")
if name == "" {
return ""
}
first := name[0]
if isAlpha(first) || first == '_' || (allowColonFirst && first == ':') {
return name
}
return "_" + name
}
func isAlpha(b byte) bool {
return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z')
}
func formatLabels(labels map[string]string) string {
if len(labels) == 0 {
return ""
}
normalized := make(map[string]string, len(labels))
keys := make([]string, 0, len(labels))
originalKeys := make([]string, 0, len(labels))
for key := range labels {
originalKeys = append(originalKeys, key)
}
sort.Strings(originalKeys)
for _, key := range originalKeys {
name := normalizeLabelName(key)
if name != "" {
if _, exists := normalized[name]; exists {
continue
}
normalized[name] = labels[key]
keys = append(keys, name)
}
}
sort.Strings(keys)
if len(keys) == 0 {
return ""
}
var b strings.Builder
b.WriteByte('{')
for idx, key := range keys {
if idx > 0 {
b.WriteByte(',')
}
b.WriteString(key)
b.WriteString(`="`)
b.WriteString(escapeLabel(normalized[key]))
b.WriteByte('"')
}
b.WriteByte('}')
return b.String()
}
func escapeLabel(value string) string {
value = strings.ReplaceAll(value, `\`, `\\`)
value = strings.ReplaceAll(value, "\n", `\n`)
value = strings.ReplaceAll(value, `"`, `\"`)
return value
}
@@ -0,0 +1,27 @@
package metrics
import "runtime"
func ObserveRuntime(registry *Registry) {
if registry == nil {
return
}
var stats runtime.MemStats
runtime.ReadMemStats(&stats)
registry.SetGauge("go_goroutines", nil, int64(runtime.NumGoroutine()))
registry.SetGauge("go_heap_alloc_bytes", nil, Int64FromUint64(stats.HeapAlloc))
registry.SetGauge("go_heap_inuse_bytes", nil, Int64FromUint64(stats.HeapInuse))
registry.SetGauge("go_heap_objects", nil, Int64FromUint64(stats.HeapObjects))
registry.SetGauge("go_gc_next_bytes", nil, Int64FromUint64(stats.NextGC))
registry.SetGauge("go_gc_cycles_total", nil, int64(stats.NumGC))
registry.SetGauge("go_gc_pause_total_ns", nil, Int64FromUint64(stats.PauseTotalNs))
registry.SetGauge("go_gc_last_pause_ns", nil, Int64FromUint64(lastPauseNS(stats)))
}
func lastPauseNS(stats runtime.MemStats) uint64 {
if stats.NumGC == 0 {
return 0
}
return stats.PauseNs[(stats.NumGC+255)%256]
}
+138
View File
@@ -0,0 +1,138 @@
package metrics
import (
"context"
"log/slog"
"strings"
)
type Scope struct {
registry *Registry
prefix string
labels map[string]string
}
type DumpOptions struct {
Level slog.Level
Message string
MaxSamples int
Labels map[string]string
}
func (r *Registry) Scope(parts ...string) Scope {
return Scope{registry: r, prefix: JoinName(parts...)}
}
func (s Scope) Scope(parts ...string) Scope {
prefix := JoinName(s.prefix, JoinName(parts...))
return Scope{registry: s.registry, prefix: prefix, labels: cloneLabels(s.labels)}
}
func (s Scope) WithLabels(labels map[string]string) Scope {
s.labels = mergeLabels(s.labels, labels)
return s
}
func (s Scope) Name(parts ...string) string {
return JoinName(s.prefix, JoinName(parts...))
}
func (s Scope) Labels(labels map[string]string) map[string]string {
return mergeLabels(s.labels, labels)
}
func (s Scope) Inc(name string, labels map[string]string) {
s.Add(name, labels, 1)
}
func (s Scope) Add(name string, labels map[string]string, delta int64) {
if s.registry == nil {
return
}
s.registry.Add(s.Name(name), s.Labels(labels), delta)
}
func (s Scope) SetCounter(name string, labels map[string]string, value int64) {
if s.registry == nil {
return
}
s.registry.SetCounter(s.Name(name), s.Labels(labels), value)
}
func (s Scope) SetGauge(name string, labels map[string]string, value int64) {
if s.registry == nil {
return
}
s.registry.SetGauge(s.Name(name), s.Labels(labels), value)
}
func (s Scope) AddGauge(name string, labels map[string]string, delta int64) {
if s.registry == nil {
return
}
s.registry.AddGauge(s.Name(name), s.Labels(labels), delta)
}
func JoinName(parts ...string) string {
out := make([]string, 0, len(parts))
for _, part := range parts {
for _, field := range strings.FieldsFunc(part, func(r rune) bool {
return r == '.' || r == '/' || r == '-' || r == ' ' || r == '\t' || r == '\n' || r == '\r'
}) {
field = strings.Trim(field, "_")
if field != "" {
out = append(out, field)
}
}
}
return strings.Join(out, "_")
}
func (r *Registry) DumpToLogger(ctx context.Context, logger *slog.Logger, opts DumpOptions) {
if r == nil {
return
}
if logger == nil {
logger = slog.Default()
}
msg := strings.TrimSpace(opts.Message)
if msg == "" {
msg = "metrics snapshot"
}
r.RunObservers()
samples := r.Snapshot()
sampleCount := len(samples)
if opts.MaxSamples > 0 && len(samples) > opts.MaxSamples {
samples = samples[:opts.MaxSamples]
}
attrs := []slog.Attr{
slog.Int("sample_count", sampleCount),
slog.Any("samples", samples),
}
for key, value := range opts.Labels {
key = strings.TrimSpace(key)
if key == "" {
continue
}
attrs = append(attrs, slog.String(key, value))
}
logger.LogAttrs(ctx, opts.Level, msg, attrs...)
}
func mergeLabels(base, extra map[string]string) map[string]string {
if len(base) == 0 && len(extra) == 0 {
return nil
}
out := make(map[string]string, len(base)+len(extra))
for key, value := range base {
if strings.TrimSpace(key) != "" {
out[key] = value
}
}
for key, value := range extra {
if strings.TrimSpace(key) != "" {
out[key] = value
}
}
return out
}
@@ -0,0 +1,25 @@
package metrics
import "time"
func (r *Registry) TimeStatsThreshold(name string, labels map[string]string, elapsed time.Duration, threshold time.Duration) {
if r == nil || name == "" {
return
}
if elapsed < 0 {
elapsed = 0
}
base := JoinName(name)
r.Inc(JoinName(base, "total"), labels)
r.SetGauge(JoinName(base, "last_ns"), labels, elapsed.Nanoseconds())
if threshold > 0 && elapsed >= threshold {
r.Inc(JoinName(base, "slow_total"), labels)
}
}
func (s Scope) TimeStatsThreshold(name string, labels map[string]string, elapsed time.Duration, threshold time.Duration) {
if s.registry == nil {
return
}
s.registry.TimeStatsThreshold(s.Name(name), s.Labels(labels), elapsed, threshold)
}