Skip to content

Commit 5306925

Browse files
feat: httpmetrics; cookie -> httpcookie; add health endpoint to httphandler
1 parent 4904511 commit 5306925

File tree

16 files changed

+199
-38
lines changed

16 files changed

+199
-38
lines changed

.envrc.local

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
if use flake; then
1+
if nix flake show &>/dev/null; then
22
use flake
33
fi

env/env.go

+2-4
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,8 @@ import (
1010
"go.inout.gg/foundations/must"
1111
)
1212

13-
var (
14-
// Validator is the default validator used to validate the configuration.
15-
Validator = validator.New(validator.WithRequiredStructEnabled())
16-
)
13+
// Validator is the default validator used to validate the configuration.
14+
var Validator = validator.New(validator.WithRequiredStructEnabled())
1715

1816
// Load loads the environment configuration into a struct T.
1917
//

env/env_test.go

-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ type Config struct {
1414
}
1515

1616
func TestLoad(t *testing.T) {
17-
1817
t.Run("missing value", func(t *testing.T) {
1918
// Make sure that the environment variables are not set.
2019
os.Clearenv()

flake.lock

+6-6
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

flake.nix

+3
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
nodejs
2121
sqlc
2222
golangci-lint
23+
24+
mockgen
2325
];
2426
in
2527
{
@@ -29,6 +31,7 @@
2931

3032
shellHook = ''
3133
export GOTOOLCHAIN="local"
34+
export GOFUMPT_SPLIT_LONG_LINES=true
3235
'';
3336

3437
formatter = pkgs.nixfmt-rfc-style;

go.mod

+11-6
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,39 @@
11
module go.inout.gg/foundations
22

3-
go 1.21
3+
go 1.22.0
44

5-
toolchain go1.23.0
5+
toolchain go1.23.4
66

77
require (
8-
github.com/a-h/templ v0.2.747
98
github.com/caarlos0/env/v11 v11.1.0
9+
github.com/felixge/httpsnoop v1.0.4
1010
github.com/go-playground/form/v4 v4.2.1
1111
github.com/go-playground/validator/v10 v10.22.0
1212
github.com/google/uuid v1.6.0
1313
github.com/jackc/pgx/v5 v5.6.0
1414
github.com/jackc/puddle/v2 v2.2.1
1515
github.com/joho/godotenv v1.5.1
16-
github.com/stretchr/testify v1.9.0
16+
github.com/stretchr/testify v1.10.0
17+
go.opentelemetry.io/otel v1.33.0
18+
go.opentelemetry.io/otel/metric v1.33.0
19+
go.uber.org/mock v0.5.0
1720
golang.org/x/crypto v0.26.0
1821
golang.org/x/sync v0.8.0
1922
)
2023

2124
require (
2225
github.com/davecgh/go-spew v1.1.1 // indirect
2326
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
27+
github.com/go-logr/logr v1.4.2 // indirect
28+
github.com/go-logr/stdr v1.2.2 // indirect
2429
github.com/go-playground/locales v0.14.1 // indirect
2530
github.com/go-playground/universal-translator v0.18.1 // indirect
2631
github.com/jackc/pgpassfile v1.0.0 // indirect
2732
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
28-
github.com/kr/text v0.2.0 // indirect
2933
github.com/leodido/go-urn v1.4.0 // indirect
3034
github.com/pmezard/go-difflib v1.0.0 // indirect
31-
github.com/rogpeppe/go-internal v1.12.0 // indirect
35+
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
36+
go.opentelemetry.io/otel/trace v1.33.0 // indirect
3237
golang.org/x/net v0.27.0 // indirect
3338
golang.org/x/sys v0.23.0 // indirect
3439
golang.org/x/text v0.17.0 // indirect

go.sum

+23-9
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
1-
github.com/a-h/templ v0.2.747 h1:D0dQ2lxC3W7Dxl6fxQ/1zZHBQslSkTSvl5FxP/CfdKg=
2-
github.com/a-h/templ v0.2.747/go.mod h1:69ObQIbrcuwPCU32ohNaWce3Cb7qM5GMiqN1K+2yop4=
31
github.com/caarlos0/env/v11 v11.1.0 h1:a5qZqieE9ZfzdvbbdhTalRrHT5vu/4V1/ad1Ka6frhI=
42
github.com/caarlos0/env/v11 v11.1.0/go.mod h1:LwgkYk1kDvfGpHthrWWLof3Ny7PezzFwS4QrsJdHTMo=
5-
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
63
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
74
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
85
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
6+
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
7+
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
98
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
109
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
10+
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
11+
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
12+
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
13+
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
14+
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
1115
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
1216
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
1317
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
@@ -33,21 +37,31 @@ github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk
3337
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
3438
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
3539
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
36-
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
37-
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
40+
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
41+
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
3842
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
3943
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
4044
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
4145
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
4246
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
4347
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
44-
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
45-
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
48+
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
49+
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
4650
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
4751
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
4852
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
49-
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
50-
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
53+
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
54+
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
55+
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
56+
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
57+
go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw=
58+
go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I=
59+
go.opentelemetry.io/otel/metric v1.33.0 h1:r+JOocAyeRVXD8lZpjdQjzMadVZp2M4WmQ+5WtEnklQ=
60+
go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M=
61+
go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s=
62+
go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck=
63+
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
64+
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
5165
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
5266
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
5367
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=

http/cookie/cookie.go http/httpcookie/cookie.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package cookie
1+
package httpcookie
22

33
import (
44
"net/http"

http/httphandler/todo.go http/httphandler/handler.go

+10-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ package httphandler
22

33
import "net/http"
44

5-
var todoStr = []byte("todo")
5+
var (
6+
todoStr = []byte("todo")
7+
okStr = []byte("ok")
8+
)
69

710
// TODO returns an HTTP response with "todo" body and 200 status.
811
var TODO http.HandlerFunc = http.HandlerFunc(
@@ -11,3 +14,9 @@ var TODO http.HandlerFunc = http.HandlerFunc(
1114
w.Write(todoStr)
1215
},
1316
)
17+
18+
// HealthCheck returns an HTTP response with "ok" body and 200 status.
19+
func HealthCheck(w http.ResponseWriter, r *http.Request) {
20+
w.WriteHeader(http.StatusOK)
21+
w.Write(okStr)
22+
}

http/httphandler/handler_test.go

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package httphandler
2+
3+
import (
4+
"io"
5+
"net/http"
6+
"net/http/httptest"
7+
"testing"
8+
9+
"github.com/stretchr/testify/assert"
10+
)
11+
12+
func TestTodo(t *testing.T) {
13+
// Create a new request
14+
req := httptest.NewRequest(http.MethodGet, "/", nil)
15+
rr := httptest.NewRecorder()
16+
17+
// Call the handler
18+
TODO.ServeHTTP(rr, req)
19+
20+
// Check status code
21+
assert.Equal(t, http.StatusOK, rr.Code, "handler returned wrong status code")
22+
23+
// Check response body
24+
body, err := io.ReadAll(rr.Body)
25+
assert.NoError(t, err)
26+
assert.Equal(t, "todo", string(body), "handler returned unexpected body")
27+
}
28+
29+
func TestHealthCheck(t *testing.T) {
30+
// Create a new request
31+
req := httptest.NewRequest(http.MethodGet, "/health", nil)
32+
rr := httptest.NewRecorder()
33+
34+
// Call the handler
35+
HealthCheck(rr, req)
36+
37+
// Check status code
38+
assert.Equal(t, http.StatusOK, rr.Code, "handler returned wrong status code")
39+
40+
// Check response body
41+
body, err := io.ReadAll(rr.Body)
42+
assert.NoError(t, err)
43+
assert.Equal(t, "ok", string(body), "handler returned unexpected body")
44+
}

metrics/httpmetrics/httpmetrics.go

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package httpmetrics
2+
3+
import (
4+
"net/http"
5+
6+
"github.com/felixge/httpsnoop"
7+
"go.inout.gg/foundations/http/httpmiddleware"
8+
"go.inout.gg/foundations/must"
9+
"go.opentelemetry.io/otel/attribute"
10+
"go.opentelemetry.io/otel/metric"
11+
)
12+
13+
// Middleware returns a middleware that captures metrics for incoming HTTP requests.
14+
func Middleware(p metric.MeterProvider) httpmiddleware.Middleware {
15+
meter := p.Meter("foundations:httpmetrics")
16+
requestDurationHisto := must.Must(
17+
meter.Int64Histogram(
18+
"request_duration_ms",
19+
metric.WithDescription("The incoming request duration in milliseconds."),
20+
metric.WithUnit("ms"),
21+
metric.WithExplicitBucketBoundaries(1, 5, 10, 25, 50, 100, 200, 500, 1_000, 5_000, 10_000, 30_000, 60_000),
22+
),
23+
)
24+
responseBodySizeHisto := must.Must(
25+
meter.Int64Histogram(
26+
"response_body_size_bytes",
27+
metric.WithDescription("The outgoing response body size in bytes."),
28+
metric.WithUnit("bytes"),
29+
metric.WithExplicitBucketBoundaries(1, 10, 100, 1_000, 10_000, 100_000, 1_000_000, 10_000_000),
30+
),
31+
)
32+
33+
return httpmiddleware.MiddlewareFunc(func(next http.Handler) http.Handler {
34+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
35+
ctx := r.Context()
36+
37+
metrics := httpsnoop.CaptureMetrics(next, w, r)
38+
defaultAttributes := attribute.NewSet(
39+
attribute.Int("code", metrics.Code),
40+
attribute.String("method", r.Method),
41+
attribute.String("path", r.URL.Path),
42+
)
43+
44+
requestDurationHisto.Record(ctx, metrics.Duration.Milliseconds(), metric.WithAttributeSet(defaultAttributes))
45+
responseBodySizeHisto.Record(ctx, metrics.Written, metric.WithAttributeSet(defaultAttributes))
46+
})
47+
})
48+
}

net/port/port.go

+4-2
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ import (
55
"net"
66
)
77

8-
const proto = "tcp"
9-
const addr = ":0"
8+
const (
9+
proto = "tcp"
10+
addr = ":0"
11+
)
1012

1113
// Free returns a free port on the local machine.
1214
func Free() (int, error) {

sqldb/middleware_test.go

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package sqldb
2+
3+
import (
4+
"context"
5+
"net/http"
6+
"net/http/httptest"
7+
"testing"
8+
9+
"github.com/jackc/pgx/v5/pgxpool"
10+
"github.com/stretchr/testify/assert"
11+
)
12+
13+
func TestMiddleware(t *testing.T) {
14+
mockPool := &pgxpool.Pool{}
15+
16+
t.Run("should bind pool to context", func(t *testing.T) {
17+
testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
18+
// Assert
19+
pool, err := FromRequest(r)
20+
21+
assert.NoError(t, err)
22+
assert.Equal(t, mockPool, pool)
23+
24+
pool, err = FromContext(r.Context())
25+
assert.NoError(t, err)
26+
assert.Equal(t, mockPool, pool)
27+
})
28+
29+
// Arrange
30+
middleware := Middleware(mockPool)
31+
32+
// Act
33+
middleware(testHandler).ServeHTTP(httptest.NewRecorder(), httptest.NewRequest(http.MethodGet, "/", nil))
34+
})
35+
36+
t.Run("should return error when pool not in context", func(t *testing.T) {
37+
emptyCtx := context.Background()
38+
_, err := FromContext(emptyCtx)
39+
assert.Error(t, err)
40+
assert.Equal(t, ErrDBPoolNotFound, err)
41+
})
42+
}

sqldb/pool.go

+2-4
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,8 @@ func NewPool(ctx context.Context, connString string, cfgs ...func(*pgxpool.Confi
5151
return nil, fmt.Errorf("foundations/sqldb: failed to create a new database pool: %w", err)
5252
}
5353
defer func() {
54-
if err != nil {
55-
if pool != nil {
56-
pool.Close()
57-
}
54+
if err != nil && pool != nil {
55+
pool.Close()
5856
}
5957
}()
6058

sqldb/pool_test.go

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
package sqldb

token/token.go

+1-3
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,7 @@ import (
55
"strings"
66
)
77

8-
var (
9-
ErrMalformedToken = errors.New("foundations/token: invalid format")
10-
)
8+
var ErrMalformedToken = errors.New("foundations/token: invalid format")
119

1210
// TokenFromBearerString returns the token from a bearer token string.
1311
func TokenFromBearerString(str string) (string, error) {

0 commit comments

Comments
 (0)