Add middleware task
This commit is contained in:
parent
c2e212080c
commit
4b43a10f1c
12 changed files with 382 additions and 0 deletions
2
go.mod
2
go.mod
|
@ -33,6 +33,8 @@ require (
|
|||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // 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/jackc/chunkreader/v2 v2.0.1 // indirect
|
||||
github.com/jackc/pgconn v1.5.0 // indirect
|
||||
|
|
4
go.sum
4
go.sum
|
@ -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/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
|
||||
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/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
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/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0=
|
||||
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-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
|
|
9
middleware/README.md
Normal file
9
middleware/README.md
Normal file
|
@ -0,0 +1,9 @@
|
|||
# middleware
|
||||
|
||||
В это задаче, вам нужно научиться писать типичные middleware. Задача
|
||||
состоит из нескольких пакетов, все они независимы между собой. Чтобы
|
||||
решить задачу, нужно реализовать все пакеты.
|
||||
|
||||
- `auth` - реализует аутентификация
|
||||
- `requestlog` - логирует запросы
|
||||
- `httpgauge` - собирает статистики исполнения запросов
|
10
middleware/auth/README.md
Normal file
10
middleware/auth/README.md
Normal file
|
@ -0,0 +1,10 @@
|
|||
# auth
|
||||
|
||||
Напишите middleware проверяющую токен. Эта middleware должна проверять,
|
||||
что в заголовке `Authorization` прислали строку `Bearer TOKEN`, где `TOKEN` - валиден.
|
||||
|
||||
* Если токен не валиден, то middleware должна не пропускать запрос и возвращать `http.StatusUnauthorized`.
|
||||
* Если проверка токена завершилась ошибкой (тоесть не ясно, валиден он или нет),
|
||||
то запрос должен завершаться с кодом `StatusInternalServerError`.
|
||||
* Если токен валиден, то пользователь соответствующий токену, должен быть доступен следующим
|
||||
хендлерам через метод `ContextUser`.
|
28
middleware/auth/auth.go
Normal file
28
middleware/auth/auth.go
Normal 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")
|
||||
}
|
99
middleware/auth/auth_test.go
Normal file
99
middleware/auth/auth_test.go
Normal 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)
|
||||
})
|
||||
}
|
16
middleware/httpgauge/README.md
Normal file
16
middleware/httpgauge/README.md
Normal 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` - найдите сами)
|
23
middleware/httpgauge/httpgauge.go
Normal file
23
middleware/httpgauge/httpgauge.go
Normal 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")
|
||||
}
|
62
middleware/httpgauge/httpgauge_test.go
Normal file
62
middleware/httpgauge/httpgauge_test.go
Normal 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")
|
||||
}
|
31
middleware/requestlog/README.md
Normal file
31
middleware/requestlog/README.md
Normal 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 не должна глотать эту панику. Она должна только
|
||||
логировать, что паника случилась, и пробрасывать панику дальше.
|
13
middleware/requestlog/requestlog.go
Normal file
13
middleware/requestlog/requestlog.go
Normal 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")
|
||||
}
|
85
middleware/requestlog/requestlog_test.go
Normal file
85
middleware/requestlog/requestlog_test.go
Normal 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)
|
||||
}
|
Loading…
Reference in a new issue