Skip to content
This repository was archived by the owner on Oct 14, 2024. It is now read-only.

Commit 40ae320

Browse files
committed
Initial commit
1 parent d9627e7 commit 40ae320

13 files changed

+1239
-0
lines changed

.gitattributes

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Handle line endings automatically for files detected as text
2+
# and leave all files detected as binary untouched.
3+
* text=auto
4+
5+
# Force the following filetypes to have unix eols, so Windows does not break them
6+
*.* text eol=lf
7+
8+
# Windows forced line-endings
9+
/.idea/* text eol=crlf
10+
11+
#
12+
## These files are binary and should be left untouched
13+
#
14+
15+
# (binary is a macro for -text -diff)
16+
*.png binary
17+
*.jpg binary
18+
*.jpeg binary
19+
*.gif binary
20+
*.ico binary
21+
*.mov binary
22+
*.mp4 binary
23+
*.mp3 binary
24+
*.flv binary
25+
*.fla binary
26+
*.swf binary
27+
*.gz binary
28+
*.zip binary
29+
*.7z binary
30+
*.ttf binary
31+
*.eot binary
32+
*.woff binary
33+
*.pyc binary
34+
*.pdf binary
35+
*.ez binary
36+
*.bz2 binary
37+
*.swp binary

.gitignore

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
*.~*
2+
3+
*.log
4+
*.swp
5+
.idea
6+
.vscode
7+
*.patch
8+
### Go template
9+
# Binaries for programs and plugins
10+
*.exe
11+
*.exe~
12+
*.dll
13+
*.so
14+
*.dylib
15+
16+
# Test binary, build with `go test -c`
17+
*.test
18+
19+
# Output of the go coverage tool, specifically when used with LiteIDE
20+
*.out
21+
.DS_Store
22+
app
23+
demo
24+
25+
vendor/*
26+
27+
/**/*_test.json

LICENSE

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
Copyright (C) RandLabs.IO - All Rights Reserved.
2+
Unauthorized copying of this file, via any medium is strictly prohibited.
3+
Proprietary and confidential.
4+
5+
Only explicitly authorized companies or individual can copy, modify,
6+
publish, use, compile, sell, or distribute this software,
7+
either in source code form or as a compiled binary, for any purpose,
8+
commercial or non-commercial, and by any means.

README.md

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

go.mod

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
module github.com/randlabs/go-metrics
2+
3+
go 1.17
4+
5+
require (
6+
github.com/prometheus/client_golang v1.12.1
7+
github.com/prometheus/client_model v0.2.0
8+
github.com/randlabs/go-webserver v1.0.0
9+
github.com/valyala/fasthttp v1.34.0
10+
google.golang.org/protobuf v1.27.1
11+
)
12+
13+
require (
14+
github.com/andybalholm/brotli v1.0.4 // indirect
15+
github.com/beorn7/perks v1.0.1 // indirect
16+
github.com/buaazp/fasthttprouter v0.1.1 // indirect
17+
github.com/cespare/xxhash/v2 v2.1.2 // indirect
18+
github.com/golang/protobuf v1.5.2 // indirect
19+
github.com/klauspost/compress v1.15.1 // indirect
20+
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
21+
github.com/prometheus/common v0.32.1 // indirect
22+
github.com/prometheus/procfs v0.7.3 // indirect
23+
github.com/valyala/bytebufferpool v1.0.0 // indirect
24+
github.com/valyala/tcplisten v1.0.0 // indirect
25+
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5 // indirect
26+
)

go.sum

+498
Large diffs are not rendered by default.

handlers.go

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package metrics
2+
3+
import (
4+
"github.com/prometheus/client_golang/prometheus/promhttp"
5+
webserver "github.com/randlabs/go-webserver"
6+
"github.com/valyala/fasthttp"
7+
)
8+
9+
// -----------------------------------------------------------------------------
10+
11+
func (mws *MetricsWebServer) getHealthHandler() fasthttp.RequestHandler {
12+
return func (ctx *webserver.RequestCtx) {
13+
webserver.EnableCORS(ctx)
14+
webserver.DisableCache(ctx)
15+
16+
// Get current state from callback
17+
state := mws.healthCallback()
18+
19+
// Encode and send output
20+
webserver.SendJSON(ctx, state)
21+
}
22+
}
23+
24+
func (mws *MetricsWebServer) getMetricsHandler() fasthttp.RequestHandler {
25+
return webserver.FastHttpHandlerFromHttpHandler(promhttp.HandlerFor(
26+
mws.registry,
27+
promhttp.HandlerOpts{
28+
EnableOpenMetrics: true,
29+
},
30+
))
31+
}

handlers_access.go

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package metrics
2+
3+
import (
4+
"crypto/subtle"
5+
"strings"
6+
7+
webserver "github.com/randlabs/go-webserver"
8+
"github.com/valyala/fasthttp"
9+
)
10+
11+
// -----------------------------------------------------------------------------
12+
// Private methods
13+
14+
func (mws *MetricsWebServer) checkAccessToken(ctx *webserver.RequestCtx) bool {
15+
if len(mws.accessToken) == 0 {
16+
return true
17+
}
18+
19+
var token []byte
20+
21+
// Get X-Access-Token header
22+
header := ctx.Request.Header.Peek("X-Access-Token")
23+
if len(header) > 0 {
24+
token = header
25+
} else {
26+
// If no token provided, try with Authorization: Bearer XXX header
27+
header = ctx.Request.Header.Peek("Authorization")
28+
if len(header) > 0 {
29+
auth := strings.SplitN(string(header), " ", 2)
30+
if len(auth) == 2 && strings.EqualFold("Bearer", auth[0]) {
31+
token = []byte(auth[1])
32+
}
33+
}
34+
}
35+
36+
//Check token
37+
if len(token) > 0 && subtle.ConstantTimeCompare(mws.accessToken, token) != 0 {
38+
return true
39+
}
40+
41+
// Deny access
42+
return false
43+
}
44+
45+
func (mws *MetricsWebServer) protectedHandler(handler fasthttp.RequestHandler) fasthttp.RequestHandler {
46+
return func(ctx *fasthttp.RequestCtx) {
47+
// Check access token
48+
if mws.checkAccessToken(ctx) {
49+
handler(ctx)
50+
} else {
51+
webserver.EnableCORS(ctx)
52+
webserver.DisableCache(ctx)
53+
webserver.SendAccessDenied(ctx, "403 forbidden")
54+
}
55+
}
56+
}

metrics.go

+122
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
package metrics
2+
3+
// -----------------------------------------------------------------------------
4+
5+
import (
6+
"crypto/tls"
7+
"errors"
8+
"fmt"
9+
10+
"github.com/prometheus/client_golang/prometheus"
11+
webserver "github.com/randlabs/go-webserver"
12+
)
13+
14+
// -----------------------------------------------------------------------------
15+
16+
// MetricsWebServer is the metrics web server object
17+
type MetricsWebServer struct {
18+
httpsrv *webserver.Server
19+
registry *prometheus.Registry
20+
healthCallback HealthCallback
21+
accessToken []byte
22+
}
23+
24+
// Options specifies metrics web server initialization options.
25+
type Options struct {
26+
// Address is the bind address to attach the server listener.
27+
Address string
28+
29+
// Port is the port number the server will listen.
30+
Port uint16
31+
32+
// TLSConfig optionally provides a TLS configuration for use.
33+
TLSConfig *tls.Config
34+
35+
// AccessToken is an optional access token required to access the status endpoints.
36+
AccessToken string
37+
38+
// HealthCallback indicates a function that returns an object that will be returned as a JSON output.
39+
HealthCallback HealthCallback
40+
}
41+
42+
// HealthCallback indicates a function that returns an object that will be returned as a JSON output.
43+
type HealthCallback func() interface{}
44+
45+
// -----------------------------------------------------------------------------
46+
47+
// CreateMetricsWebServer initializes and creates a new web server
48+
func CreateMetricsWebServer(opts Options) (*MetricsWebServer, error) {
49+
var err error
50+
51+
if opts.HealthCallback == nil {
52+
return nil, errors.New("invalid health callback")
53+
}
54+
55+
// Create metrics object
56+
mws := MetricsWebServer{
57+
healthCallback: opts.HealthCallback,
58+
accessToken: []byte(opts.AccessToken),
59+
}
60+
61+
// Create webserver
62+
mws.httpsrv, err = webserver.Create(webserver.Options{
63+
Address: opts.Address,
64+
Port: opts.Port,
65+
EnableCompression: false,
66+
TLSConfig: opts.TLSConfig,
67+
})
68+
if err != nil {
69+
mws.Stop()
70+
return nil, fmt.Errorf("unable to create metrics web server [err=%v]", err)
71+
}
72+
73+
// Create Prometheus handler
74+
err = mws.createPrometheusRegistry()
75+
if err != nil {
76+
mws.Stop()
77+
return nil, err
78+
}
79+
80+
// Add webserver handlers
81+
mws.httpsrv.Router.GET("/health", mws.protectedHandler(mws.getHealthHandler()))
82+
mws.httpsrv.Router.GET("/metrics", mws.protectedHandler(mws.getMetricsHandler()))
83+
mws.httpsrv.AddProfilerHandlers("/debug/pprof", mws.checkAccessToken)
84+
85+
// Done
86+
return &mws, nil
87+
}
88+
89+
// Start starts the metrics web server
90+
func (mws *MetricsWebServer) Start() error {
91+
if mws.httpsrv == nil {
92+
return errors.New("metrics webserver not initialized")
93+
}
94+
return mws.httpsrv.Start()
95+
}
96+
97+
// Stop shuts down the metrics web server
98+
func (mws *MetricsWebServer) Stop() {
99+
// Stop web server
100+
if mws.httpsrv != nil {
101+
mws.httpsrv.Stop()
102+
mws.httpsrv = nil
103+
}
104+
mws.registry = nil
105+
106+
mws.cleanAccessToken()
107+
mws.healthCallback = nil
108+
}
109+
110+
// Registry returns the prometheus registry object
111+
func (mws *MetricsWebServer) Registry() *prometheus.Registry {
112+
return mws.registry
113+
}
114+
115+
// -----------------------------------------------------------------------------
116+
117+
func (mws *MetricsWebServer) cleanAccessToken() {
118+
tokenLen := len(mws.accessToken)
119+
for idx := 0; idx < tokenLen; idx++ {
120+
mws.accessToken[idx] = 0
121+
}
122+
}

0 commit comments

Comments
 (0)