服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
245 lines
6.3 KiB
Go
245 lines
6.3 KiB
Go
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
|
||
}
|