You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
go-zero/core/stat/metrics.go

227 lines
5.0 KiB
Go

package stat
import (
"os"
"sync"
"time"
"github.com/zeromicro/go-zero/core/executors"
"github.com/zeromicro/go-zero/core/logx"
"github.com/zeromicro/go-zero/core/syncx"
)
var (
logInterval = time.Minute
writerLock sync.Mutex
reportWriter Writer = nil
logEnabled = syncx.ForAtomicBool(true)
)
type (
// Writer interface wraps the Write method.
Writer interface {
Write(report *StatReport) error
}
// A StatReport is a stat report entry.
StatReport struct {
Name string `json:"name"`
Timestamp int64 `json:"tm"`
Pid int `json:"pid"`
ReqsPerSecond float32 `json:"qps"`
Drops int `json:"drops"`
Average float32 `json:"avg"`
Median float32 `json:"med"`
Top90th float32 `json:"t90"`
Top99th float32 `json:"t99"`
Top99p9th float32 `json:"t99p9"`
}
// A Metrics is used to log and report stat reports.
Metrics struct {
executor *executors.PeriodicalExecutor
container *metricsContainer
}
)
// DisableLog disables logs of stats.
func DisableLog() {
logEnabled.Set(false)
}
// SetReportWriter sets the report writer.
func SetReportWriter(writer Writer) {
writerLock.Lock()
reportWriter = writer
writerLock.Unlock()
}
// NewMetrics returns a Metrics.
func NewMetrics(name string) *Metrics {
container := &metricsContainer{
name: name,
pid: os.Getpid(),
}
return &Metrics{
executor: executors.NewPeriodicalExecutor(logInterval, container),
container: container,
}
}
// Add adds task to m.
func (m *Metrics) Add(task Task) {
m.executor.Add(task)
}
// AddDrop adds a drop to m.
func (m *Metrics) AddDrop() {
m.executor.Add(Task{
Drop: true,
})
}
// SetName sets the name of m.
func (m *Metrics) SetName(name string) {
m.executor.Sync(func() {
m.container.name = name
})
}
type (
tasksDurationPair struct {
tasks []Task
duration time.Duration
drops int
}
metricsContainer struct {
name string
pid int
tasks []Task
duration time.Duration
drops int
}
)
func (c *metricsContainer) AddTask(v any) bool {
if task, ok := v.(Task); ok {
if task.Drop {
c.drops++
} else {
c.tasks = append(c.tasks, task)
c.duration += task.Duration
}
}
return false
}
func (c *metricsContainer) Execute(v any) {
pair := v.(tasksDurationPair)
tasks := pair.tasks
duration := pair.duration
drops := pair.drops
size := len(tasks)
report := &StatReport{
Name: c.name,
Timestamp: time.Now().Unix(),
Pid: c.pid,
ReqsPerSecond: float32(size) / float32(logInterval/time.Second),
Drops: drops,
}
if size > 0 {
report.Average = float32(duration/time.Millisecond) / float32(size)
fiftyPercent := size >> 1
if fiftyPercent > 0 {
top50pTasks := topK(tasks, fiftyPercent)
medianTask := top50pTasks[0]
report.Median = float32(medianTask.Duration) / float32(time.Millisecond)
tenPercent := fiftyPercent / 5
if tenPercent > 0 {
top10pTasks := topK(top50pTasks, tenPercent)
task90th := top10pTasks[0]
report.Top90th = float32(task90th.Duration) / float32(time.Millisecond)
onePercent := tenPercent / 10
if onePercent > 0 {
top1pTasks := topK(top10pTasks, onePercent)
task99th := top1pTasks[0]
report.Top99th = float32(task99th.Duration) / float32(time.Millisecond)
pointOnePercent := onePercent / 10
if pointOnePercent > 0 {
topPointOneTasks := topK(top1pTasks, pointOnePercent)
task99Point9th := topPointOneTasks[0]
report.Top99p9th = float32(task99Point9th.Duration) / float32(time.Millisecond)
} else {
report.Top99p9th = getTopDuration(top1pTasks)
}
} else {
mostDuration := getTopDuration(top10pTasks)
report.Top99th = mostDuration
report.Top99p9th = mostDuration
}
} else {
mostDuration := getTopDuration(top50pTasks)
report.Top90th = mostDuration
report.Top99th = mostDuration
report.Top99p9th = mostDuration
}
} else {
mostDuration := getTopDuration(tasks)
report.Median = mostDuration
report.Top90th = mostDuration
report.Top99th = mostDuration
report.Top99p9th = mostDuration
}
}
log(report)
}
func (c *metricsContainer) RemoveAll() any {
tasks := c.tasks
duration := c.duration
drops := c.drops
c.tasks = nil
c.duration = 0
c.drops = 0
return tasksDurationPair{
tasks: tasks,
duration: duration,
drops: drops,
}
}
func getTopDuration(tasks []Task) float32 {
top := topK(tasks, 1)
if len(top) < 1 {
return 0
}
return float32(top[0].Duration) / float32(time.Millisecond)
}
func log(report *StatReport) {
writeReport(report)
if logEnabled.True() {
logx.Statf("(%s) - qps: %.1f/s, drops: %d, avg time: %.1fms, med: %.1fms, "+
"90th: %.1fms, 99th: %.1fms, 99.9th: %.1fms",
report.Name, report.ReqsPerSecond, report.Drops, report.Average, report.Median,
report.Top90th, report.Top99th, report.Top99p9th)
}
}
func writeReport(report *StatReport) {
writerLock.Lock()
defer writerLock.Unlock()
if reportWriter != nil {
if err := reportWriter.Write(report); err != nil {
logx.Error(err)
}
}
}