Skip to content

Commit 090c6b4

Browse files
authored
Merge pull request #4 from gofurry/dev
feat: expose framework-neutral request metrics API
2 parents 0369211 + baafd4d commit 090c6b4

7 files changed

Lines changed: 380 additions & 16 deletions

File tree

.github/workflows/ci.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ on:
55
branches: [main, dev]
66
pull_request:
77

8+
permissions:
9+
contents: read
10+
811
jobs:
912
test:
1013
runs-on: ubuntu-latest

README.md

Lines changed: 133 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ curl -H "Accept: application/json" http://localhost:8080/monitor
7575

7676
## Fiber
7777

78-
`monitor` is built on `net/http`. Fiber is based on `fasthttp`, so the safest integration today is to create one monitor instance during startup and expose only the monitor endpoint through Fiber's official adaptor.
78+
`monitor` is built on `net/http`. Fiber is based on `fasthttp`, so create one monitor instance during startup, expose only the monitor endpoint through Fiber's official adaptor, and record business requests with Fiber-native middleware.
7979

8080
> Important: do not call `monitor.New` or `monitor.NewMonitor` inside `adaptor.HTTPMiddleware`.
8181
> Fiber executes that middleware factory for every request, while each monitor instance starts one background collector goroutine. Creating a monitor instance per request will leak collector goroutines.
@@ -85,9 +85,10 @@ package main
8585

8686
import (
8787
"net/http"
88+
"time"
8889

89-
"github.com/gofiber/fiber/v2"
90-
"github.com/gofiber/fiber/v2/middleware/adaptor"
90+
"github.com/gofiber/fiber/v3"
91+
"github.com/gofiber/fiber/v3/middleware/adaptor"
9192
"github.com/gofurry/monitor"
9293
)
9394

@@ -99,9 +100,29 @@ func main() {
99100
})
100101
defer m.Stop()
101102

103+
app.Use(func(c fiber.Ctx) error {
104+
if c.Path() == "/monitor" {
105+
return c.Next()
106+
}
107+
108+
started := time.Now()
109+
m.RequestStarted()
110+
err := c.Next()
111+
112+
status := c.Response().StatusCode()
113+
if err != nil {
114+
status = fiber.StatusInternalServerError
115+
if fiberErr, ok := err.(*fiber.Error); ok {
116+
status = fiberErr.Code
117+
}
118+
}
119+
m.RequestFinished(status, time.Since(started))
120+
return err
121+
})
122+
102123
app.All("/monitor", adaptor.HTTPHandler(m))
103124

104-
app.Get("/", func(c *fiber.Ctx) error {
125+
app.Get("/", func(c fiber.Ctx) error {
105126
return c.SendString("hello")
106127
})
107128

@@ -111,7 +132,114 @@ func main() {
111132

112133
Open `http://localhost:8080/monitor`.
113134

114-
This Fiber example safely serves the monitor page and JSON snapshot, but it does not wrap all Fiber routes. Therefore `http.total_requests` only reflects requests handled by this monitor handler. If you need full Fiber business request accounting, use a native Fiber adapter instead of wrapping `monitor.New` with `adaptor.HTTPMiddleware`.
135+
This Fiber example safely serves the monitor page and JSON snapshot, while `http.total_requests`, in-flight requests, status code classes, and latency are recorded from the native Fiber middleware.
136+
137+
## Gin
138+
139+
Gin runs on `net/http`, but you can still use the framework-neutral request lifecycle methods when you want monitor to stay outside Gin's handler chain.
140+
141+
```go
142+
package main
143+
144+
import (
145+
"net/http"
146+
"time"
147+
148+
"github.com/gin-gonic/gin"
149+
"github.com/gofurry/monitor"
150+
)
151+
152+
func main() {
153+
r := gin.New()
154+
r.Use(gin.Recovery())
155+
156+
m := monitor.NewMonitor(http.NotFoundHandler(), monitor.Config{
157+
Path: "/monitor",
158+
})
159+
defer m.Stop()
160+
161+
r.Use(func(c *gin.Context) {
162+
if c.Request.URL.Path == "/monitor" {
163+
c.Next()
164+
return
165+
}
166+
167+
started := time.Now()
168+
m.RequestStarted()
169+
c.Next()
170+
171+
status := c.Writer.Status()
172+
if status == 0 {
173+
status = http.StatusOK
174+
}
175+
m.RequestFinished(status, time.Since(started))
176+
})
177+
178+
r.GET("/monitor", gin.WrapH(m))
179+
r.GET("/", func(c *gin.Context) {
180+
c.String(http.StatusOK, "hello")
181+
})
182+
183+
_ = r.Run(":8080")
184+
}
185+
```
186+
187+
## Echo
188+
189+
Echo can also record requests with the same monitor lifecycle methods.
190+
191+
```go
192+
package main
193+
194+
import (
195+
"net/http"
196+
"time"
197+
198+
"github.com/gofurry/monitor"
199+
"github.com/labstack/echo/v4"
200+
)
201+
202+
func main() {
203+
e := echo.New()
204+
205+
m := monitor.NewMonitor(http.NotFoundHandler(), monitor.Config{
206+
Path: "/monitor",
207+
})
208+
defer m.Stop()
209+
210+
e.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
211+
return func(c echo.Context) error {
212+
if c.Request().URL.Path == "/monitor" {
213+
return next(c)
214+
}
215+
216+
started := time.Now()
217+
m.RequestStarted()
218+
err := next(c)
219+
220+
status := c.Response().Status
221+
if err != nil {
222+
status = http.StatusInternalServerError
223+
if echoErr, ok := err.(*echo.HTTPError); ok {
224+
status = echoErr.Code
225+
}
226+
}
227+
if status == 0 {
228+
status = http.StatusOK
229+
}
230+
m.RequestFinished(status, time.Since(started))
231+
return err
232+
}
233+
})
234+
235+
e.GET("/monitor", echo.WrapHandler(m))
236+
e.GET("/", func(c echo.Context) error {
237+
return c.String(http.StatusOK, "hello")
238+
})
239+
240+
_ = e.Start(":8080")
241+
}
242+
```
115243

116244
## Configuration
117245

doc.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
// exposes one monitor path, and serves metrics from a race-safe background
66
// snapshot. Requests to the monitor path are excluded from the HTTP request
77
// count so page refreshes and JSON polling do not inflate business traffic.
8+
// Frameworks that do not run on net/http can expose the monitor endpoint with
9+
// their own adaptor and record requests through RequestStarted,
10+
// RequestFinished, or ObserveRequest.
811
//
912
// Basic usage:
1013
//

docs/zh/README.md

Lines changed: 133 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ curl -H "Accept: application/json" http://localhost:8080/monitor
7272

7373
## Fiber
7474

75-
`monitor` 基于 `net/http`。Fiber 基于 `fasthttp`因此当前最安全的接入方式是在服务启动阶段只创建一个 monitor 实例,然后通过 Fiber 官方 adaptor 只暴露监控端点。
75+
`monitor` 基于 `net/http`。Fiber 基于 `fasthttp`因此推荐在服务启动阶段只创建一个 monitor 实例,通过 Fiber 官方 adaptor 只暴露监控端点,再用 Fiber 原生中间件记录业务请求
7676

7777
> 重要:不要在 `adaptor.HTTPMiddleware` 内部调用 `monitor.New``monitor.NewMonitor`
7878
> Fiber 会在每个请求里执行这个中间件工厂函数,而每个 monitor 实例都会启动一个后台采集 goroutine。如果每个请求都创建 monitor 实例,就会泄漏采集 goroutine。
@@ -82,9 +82,10 @@ package main
8282

8383
import (
8484
"net/http"
85+
"time"
8586

86-
"github.com/gofiber/fiber/v2"
87-
"github.com/gofiber/fiber/v2/middleware/adaptor"
87+
"github.com/gofiber/fiber/v3"
88+
"github.com/gofiber/fiber/v3/middleware/adaptor"
8889
"github.com/gofurry/monitor"
8990
)
9091

@@ -96,9 +97,29 @@ func main() {
9697
})
9798
defer m.Stop()
9899

100+
app.Use(func(c fiber.Ctx) error {
101+
if c.Path() == "/monitor" {
102+
return c.Next()
103+
}
104+
105+
started := time.Now()
106+
m.RequestStarted()
107+
err := c.Next()
108+
109+
status := c.Response().StatusCode()
110+
if err != nil {
111+
status = fiber.StatusInternalServerError
112+
if fiberErr, ok := err.(*fiber.Error); ok {
113+
status = fiberErr.Code
114+
}
115+
}
116+
m.RequestFinished(status, time.Since(started))
117+
return err
118+
})
119+
99120
app.All("/monitor", adaptor.HTTPHandler(m))
100121

101-
app.Get("/", func(c *fiber.Ctx) error {
122+
app.Get("/", func(c fiber.Ctx) error {
102123
return c.SendString("hello")
103124
})
104125

@@ -108,7 +129,114 @@ func main() {
108129

109130
打开 `http://localhost:8080/monitor`
110131

111-
这个 Fiber 示例可以安全地提供监控页面和 JSON 快照,但它不会包裹所有 Fiber 路由。因此 `http.total_requests` 只会反映这个 monitor handler 处理到的请求。如果你需要完整统计 Fiber 业务请求,请使用原生 Fiber adapter,而不是用 `adaptor.HTTPMiddleware` 包装 `monitor.New`
132+
这个 Fiber 示例可以安全地提供监控页面和 JSON 快照,同时通过原生 Fiber 中间件记录 `http.total_requests`、处理中请求、状态码分类和请求延迟。
133+
134+
## Gin
135+
136+
Gin 运行在 `net/http` 之上,但如果你希望 monitor 不直接包裹 Gin handler,也可以使用框架无关的请求生命周期方法。
137+
138+
```go
139+
package main
140+
141+
import (
142+
"net/http"
143+
"time"
144+
145+
"github.com/gin-gonic/gin"
146+
"github.com/gofurry/monitor"
147+
)
148+
149+
func main() {
150+
r := gin.New()
151+
r.Use(gin.Recovery())
152+
153+
m := monitor.NewMonitor(http.NotFoundHandler(), monitor.Config{
154+
Path: "/monitor",
155+
})
156+
defer m.Stop()
157+
158+
r.Use(func(c *gin.Context) {
159+
if c.Request.URL.Path == "/monitor" {
160+
c.Next()
161+
return
162+
}
163+
164+
started := time.Now()
165+
m.RequestStarted()
166+
c.Next()
167+
168+
status := c.Writer.Status()
169+
if status == 0 {
170+
status = http.StatusOK
171+
}
172+
m.RequestFinished(status, time.Since(started))
173+
})
174+
175+
r.GET("/monitor", gin.WrapH(m))
176+
r.GET("/", func(c *gin.Context) {
177+
c.String(http.StatusOK, "hello")
178+
})
179+
180+
_ = r.Run(":8080")
181+
}
182+
```
183+
184+
## Echo
185+
186+
Echo 也可以用同一组 monitor 生命周期方法记录请求。
187+
188+
```go
189+
package main
190+
191+
import (
192+
"net/http"
193+
"time"
194+
195+
"github.com/gofurry/monitor"
196+
"github.com/labstack/echo/v4"
197+
)
198+
199+
func main() {
200+
e := echo.New()
201+
202+
m := monitor.NewMonitor(http.NotFoundHandler(), monitor.Config{
203+
Path: "/monitor",
204+
})
205+
defer m.Stop()
206+
207+
e.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
208+
return func(c echo.Context) error {
209+
if c.Request().URL.Path == "/monitor" {
210+
return next(c)
211+
}
212+
213+
started := time.Now()
214+
m.RequestStarted()
215+
err := next(c)
216+
217+
status := c.Response().Status
218+
if err != nil {
219+
status = http.StatusInternalServerError
220+
if echoErr, ok := err.(*echo.HTTPError); ok {
221+
status = echoErr.Code
222+
}
223+
}
224+
if status == 0 {
225+
status = http.StatusOK
226+
}
227+
m.RequestFinished(status, time.Since(started))
228+
return err
229+
}
230+
})
231+
232+
e.GET("/monitor", echo.WrapHandler(m))
233+
e.GET("/", func(c echo.Context) error {
234+
return c.String(http.StatusOK, "hello")
235+
})
236+
237+
_ = e.Start(":8080")
238+
}
239+
```
112240

113241
## 配置
114242

0 commit comments

Comments
 (0)