Skip to content

Commit

Permalink
feat: httpmetrics; cookie -> httpcookie; add health endpoint to httph…
Browse files Browse the repository at this point in the history
…andler
  • Loading branch information
roman-vanesyan committed Feb 5, 2025
1 parent 4904511 commit 5306925
Show file tree
Hide file tree
Showing 16 changed files with 199 additions and 38 deletions.
2 changes: 1 addition & 1 deletion .envrc.local
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
if use flake; then
if nix flake show &>/dev/null; then
use flake
fi
6 changes: 2 additions & 4 deletions env/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,8 @@ import (
"go.inout.gg/foundations/must"
)

var (
// Validator is the default validator used to validate the configuration.
Validator = validator.New(validator.WithRequiredStructEnabled())
)
// Validator is the default validator used to validate the configuration.
var Validator = validator.New(validator.WithRequiredStructEnabled())

// Load loads the environment configuration into a struct T.
//
Expand Down
1 change: 0 additions & 1 deletion env/env_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ type Config struct {
}

func TestLoad(t *testing.T) {

t.Run("missing value", func(t *testing.T) {
// Make sure that the environment variables are not set.
os.Clearenv()
Expand Down
12 changes: 6 additions & 6 deletions flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
nodejs
sqlc
golangci-lint

mockgen
];
in
{
Expand All @@ -29,6 +31,7 @@

shellHook = ''
export GOTOOLCHAIN="local"
export GOFUMPT_SPLIT_LONG_LINES=true
'';

formatter = pkgs.nixfmt-rfc-style;
Expand Down
17 changes: 11 additions & 6 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,34 +1,39 @@
module go.inout.gg/foundations

go 1.21
go 1.22.0

toolchain go1.23.0
toolchain go1.23.4

require (
github.com/a-h/templ v0.2.747
github.com/caarlos0/env/v11 v11.1.0
github.com/felixge/httpsnoop v1.0.4
github.com/go-playground/form/v4 v4.2.1
github.com/go-playground/validator/v10 v10.22.0
github.com/google/uuid v1.6.0
github.com/jackc/pgx/v5 v5.6.0
github.com/jackc/puddle/v2 v2.2.1
github.com/joho/godotenv v1.5.1
github.com/stretchr/testify v1.9.0
github.com/stretchr/testify v1.10.0
go.opentelemetry.io/otel v1.33.0
go.opentelemetry.io/otel/metric v1.33.0
go.uber.org/mock v0.5.0
golang.org/x/crypto v0.26.0
golang.org/x/sync v0.8.0
)

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/kr/text v0.2.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rogpeppe/go-internal v1.12.0 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/otel/trace v1.33.0 // indirect
golang.org/x/net v0.27.0 // indirect
golang.org/x/sys v0.23.0 // indirect
golang.org/x/text v0.17.0 // indirect
Expand Down
32 changes: 23 additions & 9 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
github.com/a-h/templ v0.2.747 h1:D0dQ2lxC3W7Dxl6fxQ/1zZHBQslSkTSvl5FxP/CfdKg=
github.com/a-h/templ v0.2.747/go.mod h1:69ObQIbrcuwPCU32ohNaWce3Cb7qM5GMiqN1K+2yop4=
github.com/caarlos0/env/v11 v11.1.0 h1:a5qZqieE9ZfzdvbbdhTalRrHT5vu/4V1/ad1Ka6frhI=
github.com/caarlos0/env/v11 v11.1.0/go.mod h1:LwgkYk1kDvfGpHthrWWLof3Ny7PezzFwS4QrsJdHTMo=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
Expand All @@ -33,21 +37,31 @@ github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw=
go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I=
go.opentelemetry.io/otel/metric v1.33.0 h1:r+JOocAyeRVXD8lZpjdQjzMadVZp2M4WmQ+5WtEnklQ=
go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M=
go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s=
go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck=
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
Expand Down
2 changes: 1 addition & 1 deletion http/cookie/cookie.go → http/httpcookie/cookie.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package cookie
package httpcookie

import (
"net/http"
Expand Down
11 changes: 10 additions & 1 deletion http/httphandler/todo.go → http/httphandler/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ package httphandler

import "net/http"

var todoStr = []byte("todo")
var (
todoStr = []byte("todo")
okStr = []byte("ok")
)

// TODO returns an HTTP response with "todo" body and 200 status.
var TODO http.HandlerFunc = http.HandlerFunc(
Expand All @@ -11,3 +14,9 @@ var TODO http.HandlerFunc = http.HandlerFunc(
w.Write(todoStr)
},
)

// HealthCheck returns an HTTP response with "ok" body and 200 status.
func HealthCheck(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write(okStr)
}
44 changes: 44 additions & 0 deletions http/httphandler/handler_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package httphandler

import (
"io"
"net/http"
"net/http/httptest"
"testing"

"github.com/stretchr/testify/assert"
)

func TestTodo(t *testing.T) {
// Create a new request
req := httptest.NewRequest(http.MethodGet, "/", nil)
rr := httptest.NewRecorder()

// Call the handler
TODO.ServeHTTP(rr, req)

// Check status code
assert.Equal(t, http.StatusOK, rr.Code, "handler returned wrong status code")

// Check response body
body, err := io.ReadAll(rr.Body)
assert.NoError(t, err)
assert.Equal(t, "todo", string(body), "handler returned unexpected body")
}

func TestHealthCheck(t *testing.T) {
// Create a new request
req := httptest.NewRequest(http.MethodGet, "/health", nil)
rr := httptest.NewRecorder()

// Call the handler
HealthCheck(rr, req)

// Check status code
assert.Equal(t, http.StatusOK, rr.Code, "handler returned wrong status code")

// Check response body
body, err := io.ReadAll(rr.Body)
assert.NoError(t, err)
assert.Equal(t, "ok", string(body), "handler returned unexpected body")
}
48 changes: 48 additions & 0 deletions metrics/httpmetrics/httpmetrics.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package httpmetrics

import (
"net/http"

"github.com/felixge/httpsnoop"
"go.inout.gg/foundations/http/httpmiddleware"
"go.inout.gg/foundations/must"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/metric"
)

// Middleware returns a middleware that captures metrics for incoming HTTP requests.
func Middleware(p metric.MeterProvider) httpmiddleware.Middleware {
meter := p.Meter("foundations:httpmetrics")
requestDurationHisto := must.Must(
meter.Int64Histogram(
"request_duration_ms",
metric.WithDescription("The incoming request duration in milliseconds."),
metric.WithUnit("ms"),
metric.WithExplicitBucketBoundaries(1, 5, 10, 25, 50, 100, 200, 500, 1_000, 5_000, 10_000, 30_000, 60_000),
),
)
responseBodySizeHisto := must.Must(
meter.Int64Histogram(
"response_body_size_bytes",
metric.WithDescription("The outgoing response body size in bytes."),
metric.WithUnit("bytes"),
metric.WithExplicitBucketBoundaries(1, 10, 100, 1_000, 10_000, 100_000, 1_000_000, 10_000_000),
),
)

return httpmiddleware.MiddlewareFunc(func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()

metrics := httpsnoop.CaptureMetrics(next, w, r)
defaultAttributes := attribute.NewSet(
attribute.Int("code", metrics.Code),
attribute.String("method", r.Method),
attribute.String("path", r.URL.Path),
)

requestDurationHisto.Record(ctx, metrics.Duration.Milliseconds(), metric.WithAttributeSet(defaultAttributes))
responseBodySizeHisto.Record(ctx, metrics.Written, metric.WithAttributeSet(defaultAttributes))
})
})
}
6 changes: 4 additions & 2 deletions net/port/port.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ import (
"net"
)

const proto = "tcp"
const addr = ":0"
const (
proto = "tcp"
addr = ":0"
)

// Free returns a free port on the local machine.
func Free() (int, error) {
Expand Down
42 changes: 42 additions & 0 deletions sqldb/middleware_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package sqldb

import (
"context"
"net/http"
"net/http/httptest"
"testing"

"github.com/jackc/pgx/v5/pgxpool"
"github.com/stretchr/testify/assert"
)

func TestMiddleware(t *testing.T) {
mockPool := &pgxpool.Pool{}

t.Run("should bind pool to context", func(t *testing.T) {
testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Assert
pool, err := FromRequest(r)

assert.NoError(t, err)
assert.Equal(t, mockPool, pool)

pool, err = FromContext(r.Context())
assert.NoError(t, err)
assert.Equal(t, mockPool, pool)
})

// Arrange
middleware := Middleware(mockPool)

// Act
middleware(testHandler).ServeHTTP(httptest.NewRecorder(), httptest.NewRequest(http.MethodGet, "/", nil))
})

t.Run("should return error when pool not in context", func(t *testing.T) {
emptyCtx := context.Background()
_, err := FromContext(emptyCtx)
assert.Error(t, err)
assert.Equal(t, ErrDBPoolNotFound, err)
})
}
6 changes: 2 additions & 4 deletions sqldb/pool.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,8 @@ func NewPool(ctx context.Context, connString string, cfgs ...func(*pgxpool.Confi
return nil, fmt.Errorf("foundations/sqldb: failed to create a new database pool: %w", err)
}
defer func() {
if err != nil {
if pool != nil {
pool.Close()
}
if err != nil && pool != nil {
pool.Close()
}
}()

Expand Down
1 change: 1 addition & 0 deletions sqldb/pool_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package sqldb
4 changes: 1 addition & 3 deletions token/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@ import (
"strings"
)

var (
ErrMalformedToken = errors.New("foundations/token: invalid format")
)
var ErrMalformedToken = errors.New("foundations/token: invalid format")

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

0 comments on commit 5306925

Please sign in to comment.