Skip to content
This repository was archived by the owner on Mar 29, 2025. It is now read-only.

Commit fda9007

Browse files
committed
Added metrics server support
1 parent e6f4cdf commit fda9007

File tree

13 files changed

+795
-5
lines changed

13 files changed

+795
-5
lines changed

METRICS.md

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# Health and Metrics controller
2+
3+
## Usage with example
4+
5+
```golang
6+
package example
7+
8+
import (
9+
"encoding/json"
10+
"math/rand"
11+
12+
"github.com/randlabs/go-webserver/v2/metrics"
13+
)
14+
15+
func main() {
16+
// Create a new health & metrics controller with a web server
17+
srvOpts := metrics.Options{
18+
Address: "127.0.0.1",
19+
Port: 3000,
20+
HealthCallback: healthCallback, // Setup our health check callback
21+
EnableDebugProfiles: true,
22+
}
23+
mc, err := metrics.CreateController(srvOpts)
24+
if err != nil {
25+
// handle error
26+
}
27+
defer mc.Stop()
28+
29+
// Create a custom prometheus counter
30+
err = mc.NewCounterWithCallback(
31+
"random_counter", "A random counter",
32+
func() float64 {
33+
// Return the counter value.
34+
// The common scenario is to have a shared set of variables you regularly update with the current
35+
// state of your application.
36+
return rand.Float64()
37+
},
38+
)
39+
40+
// Start health & metrics web server
41+
err = mc.Start()
42+
if err != nil {
43+
// handle error
44+
}
45+
46+
// your app code may go here
47+
}
48+
49+
// Health output will be in JSON format.
50+
type exampleHealthOutput struct {
51+
Status string `json:"status"`
52+
}
53+
54+
// Our health callback routine.
55+
func healthCallback() string {
56+
state := exampleHealthOutput{
57+
Status: "ok",
58+
}
59+
60+
j, _ := json.Marshal(state)
61+
return string(j)
62+
}
63+
```

go.mod

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
module github.com/randlabs/go-webserver/v2
22

3-
go 1.20
3+
go 1.21
4+
5+
toolchain go1.22.2
46

57
require (
68
github.com/VictoriaMetrics/fastcache v1.12.2
@@ -10,11 +12,18 @@ require (
1012

1113
require (
1214
github.com/andybalholm/brotli v1.1.0 // indirect
15+
github.com/beorn7/perks v1.0.1 // indirect
1316
github.com/cespare/xxhash/v2 v2.3.0 // indirect
1417
github.com/golang/snappy v0.0.4 // indirect
1518
github.com/klauspost/compress v1.17.8 // indirect
19+
github.com/prometheus/client_golang v1.19.0 // indirect
20+
github.com/prometheus/client_model v0.6.1 // indirect
21+
github.com/prometheus/common v0.53.0 // indirect
22+
github.com/prometheus/procfs v0.14.0 // indirect
23+
github.com/randlabs/rundown-protection v1.1.1 // indirect
1624
github.com/savsgio/gotils v0.0.0-20240303185622-093b76447511 // indirect
1725
github.com/valyala/bytebufferpool v1.0.0 // indirect
1826
github.com/valyala/tcplisten v1.0.0 // indirect
19-
golang.org/x/sys v0.19.0 // indirect
27+
golang.org/x/sys v0.20.0 // indirect
28+
google.golang.org/protobuf v1.34.0 // indirect
2029
)

go.sum

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156 h1:eMwmnE/GDgah
44
github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM=
55
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
66
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
7+
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
8+
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
79
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
810
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
911
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
@@ -16,6 +18,16 @@ github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEW
1618
github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU=
1719
github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
1820
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
21+
github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU=
22+
github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k=
23+
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
24+
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
25+
github.com/prometheus/common v0.53.0 h1:U2pL9w9nmJwJDa4qqLQ3ZaePJ6ZTwt7cMD3AG3+aLCE=
26+
github.com/prometheus/common v0.53.0/go.mod h1:BrxBKv3FWBIGXw89Mg1AeBq7FSyRzXWI3l3e7W3RN5U=
27+
github.com/prometheus/procfs v0.14.0 h1:Lw4VdGGoKEZilJsayHf0B+9YgLGREba2C6xr+Fdfq6s=
28+
github.com/prometheus/procfs v0.14.0/go.mod h1:XL+Iwz8k8ZabyZfMFHPiilCniixqQarAy5Mu67pHlNQ=
29+
github.com/randlabs/rundown-protection v1.1.1 h1:M0ltneeZ0efBPERxWOLC5JbCP0I3TZce41IUQoQNNJA=
30+
github.com/randlabs/rundown-protection v1.1.1/go.mod h1:rCfZhLfCKhedcQ0uQvpZbvyaCsPTsreynI2FqwuesGA=
1931
github.com/savsgio/gotils v0.0.0-20240303185622-093b76447511 h1:KanIMPX0QdEdB4R3CiimCAbxFrhB3j7h0/OvpYGVQa8=
2032
github.com/savsgio/gotils v0.0.0-20240303185622-093b76447511/go.mod h1:sM7Mt7uEoCeFSCBM+qBrqvEo+/9vdmj19wzp3yzUhmg=
2133
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@@ -29,3 +41,7 @@ github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7Fw
2941
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
3042
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
3143
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
44+
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
45+
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
46+
google.golang.org/protobuf v1.34.0 h1:Qo/qEd2RZPCf2nKuorzksSknv0d3ERwp1vFG38gSmH4=
47+
google.golang.org/protobuf v1.34.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=

helpers_test/helpers.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@ func RunWebServer(t *testing.T, initCB func(srv *webserver.Server) error) *TestW
4949
}
5050

5151
// Add some dummy endpoints
52-
tws.Server.OPTIONS("/api/version", renderApiVersion)
5352
tws.Server.GET("/api/version", renderApiVersion)
5453
tws.Server.POST("/api/version", renderApiVersion)
5554

@@ -139,8 +138,8 @@ func QueryApiVersion(doPost bool, queryParams map[string]string, headers http.He
139138
return resp.StatusCode, resp.Header, nil
140139
}
141140

142-
func OpenBrowser() {
143-
rawUrl := "http://127.0.0.1:3000/"
141+
func OpenBrowser(path string) {
142+
rawUrl := "http://127.0.0.1:3000" + path
144143
switch runtime.GOOS {
145144
case "linux":
146145
_ = exec.Command("xdg-open", rawUrl).Start()

metrics/helpers.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package metrics
2+
3+
// -----------------------------------------------------------------------------
4+
5+
func isJSON(s string) bool {
6+
// An official (?) method but a plain text is also considered a valid JSON
7+
// var js interface{}
8+
// return json.Unmarshal([]byte(s), &js) == nil
9+
10+
// Our quick approach
11+
startIdx := 0
12+
endIdx := len(s)
13+
14+
// Skip blanks at the beginning and the end
15+
for startIdx < endIdx && isBlank(s[startIdx]) {
16+
startIdx += 1
17+
}
18+
for endIdx > startIdx && isBlank(s[endIdx-1]) {
19+
endIdx -= 1
20+
}
21+
22+
return startIdx < endIdx &&
23+
((s[startIdx] == '{' && s[endIdx-1] == '}') ||
24+
(s[startIdx] == '[' && s[endIdx-1] == ']'))
25+
}
26+
27+
func isBlank(ch byte) bool {
28+
return ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n'
29+
}

metrics/metric_handlers.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package metrics
2+
3+
import (
4+
"strconv"
5+
6+
"github.com/prometheus/client_golang/prometheus/promhttp"
7+
webserver "github.com/randlabs/go-webserver/v2"
8+
"github.com/randlabs/go-webserver/v2/util"
9+
)
10+
11+
// -----------------------------------------------------------------------------
12+
13+
func (mws *Controller) getHealthHandler() webserver.HandlerFunc {
14+
return func(req *webserver.RequestContext) error {
15+
// Get current health status from callback
16+
status := mws.healthCallback()
17+
18+
// Send output
19+
respHdrs := req.ResponseHeaders()
20+
if isJSON(status) {
21+
respHdrs.SetBytesKV(util.HeaderContentType, util.ContentTypeApplicationJSON)
22+
} else {
23+
respHdrs.SetBytesKV(util.HeaderContentType, util.ContentTypeTextPlain)
24+
}
25+
26+
if !req.IsHead() {
27+
_, _ = req.WriteString(status)
28+
} else {
29+
respHdrs.SetBytesK(util.HeaderContentLength, strconv.FormatUint(uint64(int64(len(status))), 10))
30+
}
31+
32+
// Done
33+
req.Success()
34+
return nil
35+
}
36+
}
37+
38+
func (mws *Controller) getMetricsHandler() webserver.HandlerFunc {
39+
return webserver.NewHandlerFromHttpHandler(promhttp.HandlerFor(
40+
mws.registry,
41+
promhttp.HandlerOpts{
42+
EnableOpenMetrics: true,
43+
MaxRequestsInFlight: 5,
44+
},
45+
))
46+
}

0 commit comments

Comments
 (0)