Add middleware task

This commit is contained in:
Fedor Korotkiy 2023-03-25 03:08:06 +04:00
parent c2e212080c
commit 4b43a10f1c
12 changed files with 382 additions and 0 deletions

2
go.mod
View file

@ -33,6 +33,8 @@ require (
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/emirpasic/gods v1.12.0 // indirect github.com/emirpasic/gods v1.12.0 // indirect
github.com/felixge/httpsnoop v1.0.3 // indirect
github.com/go-chi/chi/v5 v5.0.8 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/jackc/chunkreader/v2 v2.0.1 // indirect github.com/jackc/chunkreader/v2 v2.0.1 // indirect
github.com/jackc/pgconn v1.5.0 // indirect github.com/jackc/pgconn v1.5.0 // indirect

4
go.sum
View file

@ -58,6 +58,8 @@ github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8
github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg= github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg=
github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk=
github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
@ -65,6 +67,8 @@ github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0= github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0=
github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
github.com/go-chi/chi/v5 v5.0.8 h1:lD+NLqFcAi1ovnVZpsnObHGW4xb4J8lNmoYVfECH1Y0=
github.com/go-chi/chi/v5 v5.0.8/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=

9
middleware/README.md Normal file
View file

@ -0,0 +1,9 @@
# middleware
В это задаче, вам нужно научиться писать типичные middleware. Задача
состоит из нескольких пакетов, все они независимы между собой. Чтобы
решить задачу, нужно реализовать все пакеты.
- `auth` - реализует аутентификация
- `requestlog` - логирует запросы
- `httpgauge` - собирает статистики исполнения запросов

10
middleware/auth/README.md Normal file
View file

@ -0,0 +1,10 @@
# auth
Напишите middleware проверяющую токен. Эта middleware должна проверять,
что в заголовке `Authorization` прислали строку `Bearer TOKEN`, где `TOKEN` - валиден.
* Если токен не валиден, то middleware должна не пропускать запрос и возвращать `http.StatusUnauthorized`.
* Если проверка токена завершилась ошибкой (тоесть не ясно, валиден он или нет),
то запрос должен завершаться с кодом `StatusInternalServerError`.
* Если токен валиден, то пользователь соответствующий токену, должен быть доступен следующим
хендлерам через метод `ContextUser`.

28
middleware/auth/auth.go Normal file
View file

@ -0,0 +1,28 @@
//go:build !solution
package auth
import (
"context"
"errors"
"net/http"
)
type User struct {
Name string
Email string
}
func ContextUser(ctx context.Context) (*User, bool) {
panic("not implemented")
}
var ErrInvalidToken = errors.New("invalid token")
type TokenChecker interface {
CheckToken(ctx context.Context, token string) (*User, error)
}
func CheckAuth(checker TokenChecker) func(next http.Handler) http.Handler {
panic("not implemented")
}

View file

@ -0,0 +1,99 @@
package auth_test
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"github.com/go-chi/chi/v5"
"github.com/stretchr/testify/require"
"gitlab.com/slon/shad-go/middleware/auth"
)
type fakeChecker map[string]struct {
user *auth.User
err error
}
func (c fakeChecker) CheckToken(ctx context.Context, token string) (*auth.User, error) {
res := c[token]
return res.user, res.err
}
func TestAuth(t *testing.T) {
m := chi.NewRouter()
c := fakeChecker{
"token0": {
user: &auth.User{Name: "Fedor", Email: "dartslon@gmail.com"},
},
"token1": {
err: fmt.Errorf("database offline"),
},
"token2": {
err: fmt.Errorf("token expired: %w", auth.ErrInvalidToken),
},
}
m.Use(auth.CheckAuth(c))
var (
lastUser *auth.User
lastUserOK bool
called bool
)
m.Get("/path/ok", func(w http.ResponseWriter, r *http.Request) {
called = true
lastUser, lastUserOK = auth.ContextUser(r.Context())
w.WriteHeader(http.StatusOK)
})
m.Get("/path/error", func(w http.ResponseWriter, r *http.Request) {
called = true
w.WriteHeader(http.StatusConflict)
})
t.Run("NoToken", func(t *testing.T) {
called = false
w := httptest.NewRecorder()
m.ServeHTTP(w, httptest.NewRequest("GET", "/path/ok", nil))
require.Equal(t, w.Code, http.StatusUnauthorized)
require.False(t, called)
})
t.Run("InvalidToken", func(t *testing.T) {
})
t.Run("DatabaseError", func(t *testing.T) {
})
t.Run("GoodToken", func(t *testing.T) {
called = false
w := httptest.NewRecorder()
r := httptest.NewRequest("GET", "/path/ok", nil)
r.Header.Add("authorization", "Bearer token0")
m.ServeHTTP(w, r)
require.Equal(t, w.Code, http.StatusOK)
require.True(t, called)
require.True(t, lastUserOK)
require.Equal(t, lastUser, &auth.User{Name: "Fedor", Email: "dartslon@gmail.com"})
called = false
w = httptest.NewRecorder()
r = httptest.NewRequest("GET", "/path/error", nil)
r.Header.Add("authorization", "Bearer token0")
m.ServeHTTP(w, r)
require.Equal(t, w.Code, http.StatusConflict)
require.True(t, called)
})
}

View file

@ -0,0 +1,16 @@
# httpgauge
Напишите middleware, считающий метрики числа запросов. Обычно, метрики измеряют
с помощью библиотеки [prometheus](https://github.com/prometheus/client_golang).
Но мы будем писать упрошённую версию, которая будет хранить метрики в `map[string]int`.
Ключём в такой `map` можно было бы сделать `Path` запроса, но это не очень хорошее решение.
В пути часто встречаются изменяемые параметры. Например `/user/0`, `/user/1`, etc. В этом случае,
мы не хотим отдельно измерять, что пришло 5 запросов к `/user/0` и 10 запросов к `/user/1`.
В этом случае, для мониторинга за сервером нам нужно знать, что пришло 15 запросов к пути `/user/{userID}`.
Чтобы получить такой паттерн (`/user/{userID}`) вместо полного пути, нужно использовать библиотеку-роутер.
Стандартная библиотека такого не умеет. В этой задаче мы будем использовать библиоетку [chi](https://github.com/go-chi/chi).
Как получить такой паттерн из библиотеки `chi` - найдите сами)

View file

@ -0,0 +1,23 @@
//go:build !solution
package httpgauge
import "net/http"
type Gauge struct{}
func New() *Gauge {
panic("not implemented")
}
func (g *Gauge) Snapshot() map[string]int {
panic("not implemented")
}
func (g *Gauge) ServeHTTP(w http.ResponseWriter, r *http.Request) {
panic("not implemented")
}
func (g *Gauge) Wrap(next http.Handler) http.Handler {
panic("not implemented")
}

View file

@ -0,0 +1,62 @@
package httpgauge_test
import (
"fmt"
"net/http"
"net/http/httptest"
"sync"
"testing"
"github.com/go-chi/chi/v5"
"github.com/stretchr/testify/require"
"gitlab.com/slon/shad-go/middleware/httpgauge"
)
func TestMiddleware(t *testing.T) {
g := httpgauge.New()
m := chi.NewRouter()
m.Use(g.Wrap)
m.Get("/simple", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
m.Get("/panic", func(w http.ResponseWriter, r *http.Request) {
panic("bug")
})
m.Get("/user/{userID}", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
m.ServeHTTP(httptest.NewRecorder(), httptest.NewRequest("GET", "/simple", nil))
m.ServeHTTP(httptest.NewRecorder(), httptest.NewRequest("GET", "/simple", nil))
require.Panics(t, func() {
m.ServeHTTP(httptest.NewRecorder(), httptest.NewRequest("GET", "/panic", nil))
})
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < 1000; j++ {
m.ServeHTTP(httptest.NewRecorder(), httptest.NewRequest("GET", fmt.Sprintf("/user/%d", j), nil))
}
}()
}
wg.Wait()
require.Equal(t, g.Snapshot(), map[string]int{
"/simple": 2,
"/panic": 1,
"/user/{userID}": 10000,
})
w := httptest.NewRecorder()
g.ServeHTTP(w, httptest.NewRequest("GET", "/", nil))
require.Equal(t, w.Body.String(), "/panic 1\n/simple 2\n/user/{userID} 10000\n")
}

View file

@ -0,0 +1,31 @@
# requestlog
Реализуйте middleware логирующую запросы.
Для логирования вам нужно использовать библиотеку zap. Прочитайте [README](https://github.com/uber-go/zap)
на библиотеку, этого должно быть достаточно для выполнения задания. Вам нужно использовать `structured`
формат логов.
```go
logger.Info("failed to fetch URL",
// Structured context as strongly typed Field values.
zap.String("url", url),
zap.Int("attempt", 3),
zap.Duration("backoff", time.Second),
)
```
* Каждый запрос должен писать в лог две строчки. Одну строчку до начала обработки запроса и одну строчку после
конца обработки запроса.
* Обе строчки должны содержать поля `path` и `method`, соответствующие полям запроса.
* Так же обе строчки должны содержать поле `request_id` с удинальным идентификатором запроса. Как вы сгенерируете
этот идентификатор не важно, главное чтобы он был уникальным и совпадал. Это поле нужно, чтобы по первой строчке
можно было найти вторую и по второй - первую.
* Вторая строчка должна содержать поле `duration` с временем исполнения запроса.
* Вторая строчка должна содержать поле `status_code` со кодом возврата. Чтобы сохранить это поле,
вам нужно написать обёртку над `http.ResponseWriter`. Можете сделать это сами, а можете
воспользоваться библиотекой [httpsnoop](https://github.com/felixge/httpsnoop).
* Сообщение в первой строчке всегда должно быть `request started`
* Сообщение во второй строчке должно быть либо `request finished`, либо `request panicked`.
Если обработчик запроса паниковал, ваша middleware не должна глотать эту панику. Она должна только
логировать, что паника случилась, и пробрасывать панику дальше.

View file

@ -0,0 +1,13 @@
//go:build !solution
package requestlog
import (
"net/http"
"go.uber.org/zap"
)
func Log(l *zap.Logger) func(next http.Handler) http.Handler {
panic("not implemented")
}

View file

@ -0,0 +1,85 @@
package requestlog_test
import (
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/go-chi/chi/v5"
"github.com/stretchr/testify/require"
"go.uber.org/zap"
"go.uber.org/zap/zaptest/observer"
"gitlab.com/slon/shad-go/middleware/requestlog"
)
func TestRequestLog(t *testing.T) {
core, obs := observer.New(zap.DebugLevel)
m := chi.NewRouter()
m.Use(requestlog.Log(zap.New(core)))
m.Get("/simple", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
m.Post("/post", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
m.Get("/forbidden", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusForbidden)
})
m.Get("/slow", func(w http.ResponseWriter, r *http.Request) {
time.Sleep(time.Millisecond * 200)
w.WriteHeader(http.StatusOK)
})
m.Get("/forgetful", func(w http.ResponseWriter, r *http.Request) {
// TODO
})
m.Get("/buggy", func(w http.ResponseWriter, r *http.Request) {
panic("bug")
})
m.ServeHTTP(httptest.NewRecorder(), httptest.NewRequest("GET", "/simple", nil))
m.ServeHTTP(httptest.NewRecorder(), httptest.NewRequest("POST", "/post", nil))
m.ServeHTTP(httptest.NewRecorder(), httptest.NewRequest("GET", "/forbidden", nil))
m.ServeHTTP(httptest.NewRecorder(), httptest.NewRequest("GET", "/slow", nil))
m.ServeHTTP(httptest.NewRecorder(), httptest.NewRequest("GET", "/forgetful", nil))
require.Panics(t, func() {
m.ServeHTTP(httptest.NewRecorder(), httptest.NewRequest("GET", "/buggy", nil))
})
checkEntries := func(path string, panic bool, code int) {
entries := obs.FilterField(zap.String("path", path)).All()
require.Len(t, entries, 2)
require.Equal(t, entries[0].Message, "request started")
require.Contains(t, entries[0].ContextMap(), "request_id")
var requestID zap.Field
for _, f := range entries[0].Context {
if f.Key == "request_id" {
requestID = f
}
}
if !panic {
require.Equal(t, entries[1].Message, "request finished")
require.Contains(t, entries[1].Context, zap.Int("status_code", code))
require.Contains(t, entries[1].ContextMap(), "duration")
require.Contains(t, entries[1].Context, requestID)
} else {
require.Equal(t, entries[1].Message, "request panicked")
require.Contains(t, entries[1].Context, requestID)
}
}
checkEntries("/simple", false, http.StatusOK)
checkEntries("/post", false, http.StatusOK)
checkEntries("/forbidden", false, http.StatusForbidden)
checkEntries("/slow", false, http.StatusOK)
checkEntries("/forgetful", false, http.StatusInternalServerError)
checkEntries("/buggy", true, http.StatusInternalServerError)
}