Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(web/http): Add custom context, and a way to access request ID easily #19

Merged
merged 12 commits into from
Feb 8, 2023
Merged
43 changes: 43 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: ci

on:
pull_request:
push:
branches:
- main
tags:
- v*

permissions:
contents: write
packages: write

jobs:
lint:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
with:
go-version-file: go.mod
cache: false
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
with:
version: latest
skip-cache: true
args: -v

test:
runs-on: ubuntu-latest
needs: [lint]

steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
with:
go-version-file: go.mod
cache: false
- run: go mod download
- run: make test
37 changes: 37 additions & 0 deletions .golangci.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
run:
timeout: 5m
skip-dirs-use-default: true
modules-download-mode: readonly
allow-parallel-runners: true
go: '1.19'

output:
sort-results: true

linters:
disable-all: true
enable:
- gci
- revive

linters-settings:
gci:
sections:
- standard
- default
- prefix(github.com/suborbital)
- blank
- dot
custom-order: true
revive:
max-open-files: 2048
ignore-generated-header: true
enable-all-rules: false
confidence: 0.1
rules:
- name: import-shadowing
severity: warning
disabled: false
- name: duplicated-imports
severity: warning
disabled: false
9 changes: 9 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,2 +1,11 @@
tidy:
go mod tidy && go mod download && go mod vendor

test:
go test -v -count=1 ./...

lint:
golangci-lint run -v ./...

lintfix:
golangci-lint run -v --fix ./...
4 changes: 4 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ require (
github.com/labstack/echo/v4 v4.10.0
github.com/pkg/errors v0.9.1
github.com/rs/zerolog v1.29.0
github.com/stretchr/testify v1.8.1
go.opentelemetry.io/otel v1.12.0
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.35.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.12.0
Expand All @@ -20,6 +21,7 @@ require (

require (
github.com/cenkalti/backoff/v4 v4.2.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-logr/logr v1.2.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
Expand All @@ -28,6 +30,7 @@ require (
github.com/labstack/gommon v0.4.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.12.0 // indirect
Expand All @@ -41,4 +44,5 @@ require (
golang.org/x/time v0.3.0 // indirect
google.golang.org/genproto v0.0.0-20230202175211-008b39050e57 // indirect
google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
9 changes: 9 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -142,8 +142,10 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/labstack/echo/v4 v4.10.0 h1:5CiyngihEO4HXsz3vVsJn7f8xAlWwRr3aY6Ih280ZKA=
github.com/labstack/echo/v4 v4.10.0/go.mod h1:S/T/5fy/GigaXnHTkh0ZGe4LpkkQysvRjFMSUTkDRNQ=
Expand All @@ -169,10 +171,15 @@ github.com/rs/zerolog v1.29.0 h1:Zes4hju04hjbvkVkOhdl2HpZa+0PmVwigmo8XoORE5w=
github.com/rs/zerolog v1.29.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
Expand Down Expand Up @@ -471,13 +478,15 @@ google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
Expand Down
120 changes: 83 additions & 37 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,57 +60,55 @@ Both Honeycomb and the collector versions use a grpc connection. There's a `Grpc

## Web

There are three configurable echo middlewares included in the kit.

### CORS

Provides good enough defaults with a simple call signature for ease of use:
There are four middlewares included in the kit, three of them configurable. The order of the middlewares should be the following:
```go
func main() {
e := echo.New()
e.Use(
mid.CORS("*"),
)
}
e.Use(
mid.CustomContext(),
mid.UUIDRequestID(),
mid.Logger(logger),
mid.CORS("*"),
// anything else
)
```

In case it's needed, you can configure additional domains, additional allowed headers, and a skipper function in case there's a route you don't want the middleware to be applied to.
There's also a standalone function called `RID(c echo.Context)` which takes a standard echo context, as opposed to one of our custom contexts, and returns the request ID stored in the response header if present.

The upside is that we don't need to go through the hoops of adding the custom context middleware, and then asserting that an incoming echo context is actually a custom context so we can use the method on it.

To use this:
```go
func main() {
e := echo.New()
e.Use(
mid.CORS(
"domainone.com",
mid.WithDomains("domaintwo.org", "domainthree.exe"),
mid.WithHeaders("X-Suborbital-State"),
mid.WithSkipper(func(c echo.Context) bool {
return c.Path() != "/dont/cors/this"
}),
),
)
import "github.com/suborbital/go-kit/web/http"

func SomeHandler(c echo.Context) error {
rid := http.RID(c)

return c.String(http.StatusOK, rid)
}
```

### Logger
Provides a middleware that will log when a request comes in and when the same request goes out. Error handling happens before the response is logged, which means neither the logger, nor any other middleware up the chain can further modify the response status code / body.
### Custom Context
[Echo's Context interface can be extended](), so one excellent use for that is to provide convenience functions for things that might be repetitive, such as grabbing the request ID from it.

Important! The requestID logger needs to be outside of this middleware. In practical terms, it needs to be in the list passed to `e.Use` earlier.
This needs to be the first middleware to be registered, but as the documentation page says, cannot be put in the `Pre` stack of middlewares.

It uses rs/zerolog.
Here's how to use it in a handler after it's registered:
```go
func main() {
logger := zerolog.New(os.Stderr).With().Str("service", "myservice").Logger()

e := echo.New()
e.Use(
// requestID middleware goes here somewhere. As long as it's above the logger.
mid.Logger(logger),
)
import "github.com/suborbital/go-kit/web/http"

func SomeHandler(c echo.Context) error {
cc, ok := c.(*http.Context)
if !ok {
return echo.NewHTTPError(http.StatusInternalServerError, "custom context is not enabled")
}

rid := cc.RequestID()
// do the thing with the request ID

return c.JSON(http.StatusOK, data)
}
```

### RequestID
### UUIDRequestID

The `UUIDRequestID` middleware configures echo's built in request ID middleware to use UUIDv4s instead of a random twenty-something character string.

Expand Down Expand Up @@ -143,6 +141,54 @@ func Handler() echo.HandlerFunc {
}
```

### Logger
Provides a middleware that will log when a request comes in and when the same request goes out. Error handling happens before the response is logged, which means neither the logger, nor any other middleware up the chain can further modify the response status code / body.

Important! The requestID logger needs to be outside of this middleware. In practical terms, it needs to be in the list passed to `e.Use` earlier.

It uses rs/zerolog.
```go
func main() {
logger := zerolog.New(os.Stderr).With().Str("service", "myservice").Logger()

e := echo.New()
e.Use(
// requestID middleware goes here somewhere. As long as it's above the logger.
mid.Logger(logger),
)
}
```

### CORS

Provides good enough defaults with a simple call signature for ease of use:
```go
func main() {
e := echo.New()
e.Use(
mid.CORS("*"),
)
}
```

In case it's needed, you can configure additional domains, additional allowed headers, and a skipper function in case there's a route you don't want the middleware to be applied to.

```go
func main() {
e := echo.New()
e.Use(
mid.CORS(
"domainone.com",
mid.WithDomains("domaintwo.org", "domainthree.exe"),
mid.WithHeaders("X-Suborbital-State"),
mid.WithSkipper(func(c echo.Context) bool {
return c.Path() != "/dont/cors/this"
}),
),
)
}
```

### Tracing

OpenTelemetry contrib already has an echo tracing middleware, best to use that one. You still need to configure it beforehand.
Expand Down
35 changes: 35 additions & 0 deletions web/http/context.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package http

import (
"github.com/labstack/echo/v4"
)

const noRequestID = "unknown-request"

type Context struct {
echo.Context
}

// RequestID will return the request ID stored in the echo.HeaderXRequestID header on the http.Response, or
// "unknown-request" if the value of the header was an empty string.
//
// Importantly this is on the response header because the request headers are expected to come from outside the service.
func (c *Context) RequestID() string {
rid := c.Response().Header().Get(echo.HeaderXRequestID)
if rid == "" {
return noRequestID
}

return rid
}

// RID is a short function that the echo context can be passed into, which returns the request ID as it grabs it from
// the response header. The upside of this over the custom context is that a custom context is not required.
func RID(c echo.Context) string {
rid := c.Response().Header().Get(echo.HeaderXRequestID)
if rid == "" {
return noRequestID
}

return rid
}