7.06 Middleware-Prometheus
本节课工程结构如下:
(base) yanglei@yuanhong 04-prometheus % tree ./
./
├── context.go
├── context_test.go
├── go.mod
├── go.sum
├── handleFunc.go
├── httpServer.go
├── httpServer_test.go
├── matchNode.go
├── middleware.go
├── middleware_test.go
├── middlewares
│ ├── access_log
│ │ ├── accessLog.go
│ │ ├── accessLog_test.go
│ │ └── middlewareBuilder.go
│ └── open_telemetry
│ ├── docker-compose.yaml
│ ├── middlewareBuilder.go
│ └── middleware_test.go
├── node.go
├── option.go
├── router.go
├── router_test.go
├── safeContext.go
├── serverInterface.go
└── stringValue.go
3 directories, 23 files
PART1. middlewareBuilder
按照惯例,和之前写access_log
和open_telemetry
一样,写prometheus的中间件也是从middlewareBuilder
开始的
middlewares/prometheus/middlewareBuilder.go
:
package prometheus
import "web"
// MiddlewareBuilder prometheus中间件构建器
type MiddlewareBuilder struct {
}
// Build 构建中间件
func (m *MiddlewareBuilder) Build() web.Middleware {
return func(next web.HandleFunc) web.HandleFunc {
return func(ctx *web.Context) {
next(ctx)
}
}
}
1.1 创建vector
middlewares/prometheus/middlewareBuilder.go
:
package prometheus
import (
"github.com/prometheus/client_golang/prometheus"
"web"
)
// MiddlewareBuilder prometheus中间件构建器
type MiddlewareBuilder struct {
Namespace string // Namespace APP名称
Subsystem string // Subsystem 子系统/模块名称
Name string // Name 指标名称
Help string // Help 指标的描述信息
}
// Build 构建中间件
func (m *MiddlewareBuilder) Build() web.Middleware {
labels := []string{
"pattern", // pattern 命中的路由
"method", // method 请求方法
"status", // status 响应状态码
}
vector := prometheus.NewSummaryVec(prometheus.SummaryOpts{
Namespace: m.Namespace,
Subsystem: m.Subsystem,
Name: m.Name,
Help: m.Help,
}, labels)
// 注册指标
prometheus.MustRegister(vector)
return func(next web.HandleFunc) web.HandleFunc {
return func(ctx *web.Context) {
next(ctx)
}
}
}
这里不要忘记注册指标
1.2 记录指标
很明显,我们要上报指标,首先得有指标才能上报.想要有指标,那么一定是在HandleFunc执行完毕之后,才能拿到指标的数据(例如响应时长).
middlewares/prometheus/middlewareBuilder.go
:
package prometheus
import (
"github.com/prometheus/client_golang/prometheus"
"strconv"
"time"
"web"
)
// MiddlewareBuilder prometheus中间件构建器
type MiddlewareBuilder struct {
Namespace string // Namespace APP名称
Subsystem string // Subsystem 子系统/模块名称
Name string // Name 指标名称
Help string // Help 指标的描述信息
}
// Build 构建中间件
func (m *MiddlewareBuilder) Build() web.Middleware {
labels := []string{
"pattern", // pattern 命中的路由
"method", // method 请求方法
"status", // status 响应状态码
}
vector := prometheus.NewSummaryVec(prometheus.SummaryOpts{
Namespace: m.Namespace,
Subsystem: m.Subsystem,
Name: m.Name,
Help: m.Help,
}, labels)
// 注册指标
prometheus.MustRegister(vector)
return func(next web.HandleFunc) web.HandleFunc {
return func(ctx *web.Context) {
startTime := time.Now()
// 为防止后续的中间件和HandleFunc中发生panic导致指标不记录
// 故在此处将指标记录的代码放在defer中
defer func() {
// 响应时长
duration := time.Since(startTime).Milliseconds()
vector.WithLabelValues(
// 命中的路由
ctx.MatchRoute,
// 请求方法
ctx.Req.Method,
// 响应状态码
strconv.Itoa(ctx.RespStatusCode),
).Observe(float64(duration))
}()
next(ctx)
}
}
}
这里由于需要等到HandleFunc执行完毕,响应从中间件链上回来的时候,才能拿到各种指标(响应时长、命中的路由、请求方法、响应状态码),因此为了防止在中间件链上发生panic导致无法记录指标,因此我们将记录指标的代码放在defer
中执行
注意:这里defer
中的代码也可以放在一个goroutine中以异步的方式执行,这样就可以不阻塞主线程了.可能有人会问:那如果中间件责任链上发生了panic或者突然宕机了,那么异步采集的指标不就丢失了吗?
其实这是一个伪命题.因为你正常的业务都panic了或者突然宕机,你不可能把关注点放在一个指标是否采集成功这件事上,正常人想的都是突然宕机或者发生了意料之外的panic,我的业务是否还安好.
TODO:试一下异步的goroutine方案
1.3 设置百分比和误差
middlewares/prometheus/middlewareBuilder.go
:
package prometheus
import (
"github.com/prometheus/client_golang/prometheus"
"strconv"
"time"
"web"
)
// MiddlewareBuilder prometheus中间件构建器
type MiddlewareBuilder struct {
Namespace string // Namespace APP名称
Subsystem string // Subsystem 子系统/模块名称
Name string // Name 指标名称
Help string // Help 指标的描述信息
}
// Build 构建中间件
func (m *MiddlewareBuilder) Build() web.Middleware {
labels := []string{
"pattern", // pattern 命中的路由
"method", // method 请求方法
"status", // status 响应状态码
}
vector := prometheus.NewSummaryVec(prometheus.SummaryOpts{
Namespace: m.Namespace,
Subsystem: m.Subsystem,
Name: m.Name,
Help: m.Help,
// 采样率 即百分比和误差
Objectives: map[float64]float64{
0.5: 0.01,
0.75: 0.01,
0.90: 0.005,
// 99线
0.99: 0.001,
// 999线
0.999: 0.0001,
},
}, labels)
// 注册指标
prometheus.MustRegister(vector)
return func(next web.HandleFunc) web.HandleFunc {
return func(ctx *web.Context) {
startTime := time.Now()
// 为防止后续的中间件和HandleFunc中发生panic导致指标不记录
// 故在此处将指标记录的代码放在defer中
defer func() {
// 响应时长
duration := time.Since(startTime).Milliseconds()
vector.WithLabelValues(
// 命中的路由
ctx.MatchRoute,
// 请求方法
ctx.Req.Method,
// 响应状态码
strconv.Itoa(ctx.RespStatusCode),
).Observe(float64(duration))
}()
next(ctx)
}
}
}
关于误差的描述可以参见此处
PART2. 编写测试用例
2.1 创建Server和Middleware
middlewares/prometheus/middleware_test.go
:
package prometheus
import (
"math/rand"
"net/http"
"testing"
"time"
"web"
)
// Test_MiddlewareBuilder 测试MiddlewareBuilder
func Test_MiddlewareBuilder(t *testing.T) {
// 创建中间件构建器
builder := &MiddlewareBuilder{
// Tips: 此处的Namespace、Subsystem、Name、Help的值均不可以出现-
Namespace: "my_framework",
Subsystem: "web",
Name: "http_response",
Help: "metric_help",
}
// 创建中间件Option
options := web.ServerWithMiddleware(builder.Build())
// 创建服务器
server := web.NewHTTPServer(options)
// 创建HandleFunc
handleFunc := func(ctx *web.Context) {
// 随机sleep 1-1001ms 模拟请求时长
randomValue := rand.Intn(1000) + 1
time.Sleep(time.Duration(randomValue) * time.Millisecond)
ctx.RespJSON(http.StatusAccepted, &User{Name: "Tom"})
}
// 注册路由并启动服务器
server.GET("/user", handleFunc)
server.Start(":8080")
}
type User struct {
Name string
}
这部分代码都比较常规,之前都写过很多遍了,不解释.
2.2 暴露指标
middlewares/prometheus/middleware_test.go
:
package prometheus
import (
"github.com/prometheus/client_golang/prometheus/promhttp"
"math/rand"
"net/http"
"testing"
"time"
"web"
)
// Test_MiddlewareBuilder 测试MiddlewareBuilder
func Test_MiddlewareBuilder(t *testing.T) {
// 创建中间件构建器
builder := &MiddlewareBuilder{
// Tips: 此处的Namespace、Subsystem、Name、Help的值均不可以出现-
Namespace: "my_framework",
Subsystem: "web",
Name: "http_response",
Help: "metric_help",
}
// 创建中间件Option
options := web.ServerWithMiddleware(builder.Build())
// 创建服务器
server := web.NewHTTPServer(options)
// 创建HandleFunc
handleFunc := func(ctx *web.Context) {
// 随机sleep 1-1001ms 模拟请求时长
randomValue := rand.Intn(1000) + 1
time.Sleep(time.Duration(randomValue) * time.Millisecond)
ctx.RespJSON(http.StatusAccepted, &User{Name: "Tom"})
}
// 监听另外一个端口 该端口用于暴露指标让prometheus采集
go func() {
http.Handle("/metrics", promhttp.Handler())
// 通常用于采集指标的端口不会和业务端口混合在一起
// 因为业务端口通常是需要暴露到外网的 但是指标采集端口通常是不需要的
http.ListenAndServe(":8082", nil)
}()
// 注册路由并启动服务器
server.GET("/user", handleFunc)
server.Start(":8080")
}
type User struct {
Name string
}
注意:通常暴露指标的端口和业务端口不会混用的.因为业务端口要暴露到外网,而暴露指标的端口大概率是不需要暴露到外网的
2.3 查看结果
Last updated