diff --git a/cmd/agent/main.go b/cmd/agent/agent.go similarity index 100% rename from cmd/agent/main.go rename to cmd/agent/agent.go diff --git a/cmd/server/main.go b/cmd/server/main.go deleted file mode 100644 index 38dd16d..0000000 --- a/cmd/server/main.go +++ /dev/null @@ -1,3 +0,0 @@ -package main - -func main() {} diff --git a/cmd/server/server.go b/cmd/server/server.go new file mode 100644 index 0000000..e58fd60 --- /dev/null +++ b/cmd/server/server.go @@ -0,0 +1,11 @@ +package main + +import ( + "github.com/StasMerzlyakov/go-metrics/internal/server" +) + +func main() { + if err := server.CreateServer(); err != nil { + panic(err) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..c82c449 --- /dev/null +++ b/go.mod @@ -0,0 +1,10 @@ +module github.com/StasMerzlyakov/go-metrics + +go 1.19 + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/stretchr/testify v1.8.4 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..8cf6655 --- /dev/null +++ b/go.sum @@ -0,0 +1,9 @@ +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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/common.go b/internal/common.go new file mode 100644 index 0000000..95b0690 --- /dev/null +++ b/internal/common.go @@ -0,0 +1,32 @@ +package internal + +import ( + "fmt" + "net/http" +) + +func BadRequestHandler(w http.ResponseWriter, req *http.Request) { + http.Error(w, "BadRequest", http.StatusBadRequest) +} + +func TodoResponse(res http.ResponseWriter, message string) { + res.Header().Set("Content-Type", "application/json") + res.WriteHeader(http.StatusNotImplemented) + fmt.Fprintf(res, ` + { + "response": { + "text": "%v" + }, + "version": "1.0" + } + `, message) +} + +type Middleware func(http.Handler) http.Handler + +func Conveyor(h http.Handler, middlewares ...Middleware) http.Handler { + for _, middleware := range middlewares { + h = middleware(h) + } + return h +} diff --git a/internal/server/mem_storage.go b/internal/server/mem_storage.go new file mode 100644 index 0000000..bbf043c --- /dev/null +++ b/internal/server/mem_storage.go @@ -0,0 +1,58 @@ +package server + +import "sync" + +type MemValue interface { + int64 | float64 +} + +func NewFloat64Storage() MemStorage[float64] { + return &memStorage[float64]{} +} + +func NewInt64Storage() MemStorage[int64] { + return &memStorage[int64]{} +} + +type MemStorage[T MemValue] interface { + Set(key string, value T) + Add(key string, value T) + Get(key string) (T, bool) +} + +type memStorage[T MemValue] struct { + mtx sync.Mutex + storage map[string]T +} + +func (ms *memStorage[T]) Set(key string, value T) { + ms.mtx.Lock() + defer ms.mtx.Unlock() + if ms.storage == nil { + ms.storage = make(map[string]T) + } + ms.storage[key] = value +} + +func (ms *memStorage[T]) Add(key string, value T) { + ms.mtx.Lock() + defer ms.mtx.Unlock() + if ms.storage == nil { + ms.storage = make(map[string]T) + } + if curVal, ok := ms.storage[key]; ok { + ms.storage[key] = curVal + value + } else { + ms.storage[key] = value + } +} + +func (ms *memStorage[T]) Get(key string) (T, bool) { + ms.mtx.Lock() + defer ms.mtx.Unlock() + if ms.storage == nil { + ms.storage = make(map[string]T) + } + curVal, ok := ms.storage[key] + return curVal, ok +} diff --git a/internal/server/mem_storage_test.go b/internal/server/mem_storage_test.go new file mode 100644 index 0000000..7186ade --- /dev/null +++ b/internal/server/mem_storage_test.go @@ -0,0 +1,45 @@ +package server + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestInt64MemStorage(t *testing.T) { + ms := NewInt64Storage() + ms.Add("m", 1) + ms.Add("m", 2) + k, v := ms.Get("m") + assert.True(t, v) + assert.Equal(t, int64(3), k) + + ms.Set("m", 5) + k, v = ms.Get("m") + assert.True(t, v) + assert.Equal(t, int64(5), k) + + _, v = ms.Get("m2") + assert.False(t, v) +} + +func TestFloat64MemStorage(t *testing.T) { + ms := NewFloat64Storage() + ms.Add("m", 1) + ms.Add("m", 2) + k, v := ms.Get("m") + assert.True(t, v) + assert.Equal(t, float64(3), k) + + ms.Set("m", 5) + k, v = ms.Get("m") + assert.True(t, v) + assert.Equal(t, float64(5), k) + + ms.Set("m", 7) + k, v = ms.Get("m") + assert.True(t, v) + assert.Equal(t, float64(7), k) + + _, v = ms.Get("m2") + assert.False(t, v) +} diff --git a/internal/server/server.go b/internal/server/server.go new file mode 100644 index 0000000..0a7a672 --- /dev/null +++ b/internal/server/server.go @@ -0,0 +1,104 @@ +package server + +import ( + "fmt" + "github.com/StasMerzlyakov/go-metrics/internal" + "net/http" + "regexp" + "strconv" + "strings" +) + +// CreateServer +// TODO configuration +func CreateServer() error { + counterStorage := NewInt64Storage() + gaugeStorage := NewFloat64Storage() + sux := http.NewServeMux() + sux.HandleFunc("/", internal.BadRequestHandler) + sux.Handle("/update/gauge/", http.StripPrefix("/update/gauge", CreateGaugeConveyor(gaugeStorage))) + sux.Handle("/update/counter/", http.StripPrefix("/update/counter", CreateCounterConveyor(counterStorage))) + + return http.ListenAndServe(`:8080`, sux) +} + +func CreateCounterConveyor(storage MemStorage[int64]) http.Handler { + return internal.Conveyor(CounterHandlerCreator(storage), CheckInputMiddleware) +} + +func CreateGaugeConveyor(storage MemStorage[float64]) http.Handler { + return internal.Conveyor(GaugeHandlerCreator(storage), CheckInputMiddleware) +} + +func GetURLRegexp() *regexp.Regexp { + return regexp.MustCompile("^/[a-zA-Z][a-zA-Z0-9_]*/-?([1-9][0-9]*|0)([.][0-9]+)?$") +} + +// CheckInputMiddleware +// принимаем только: +// - POST +// - Content-Type: text/plain +// - /<ИМЯ_МЕТРИКИ>/<ЗНАЧЕНИЕ_МЕТРИКИ> +func CheckInputMiddleware(next http.Handler) http.Handler { + pathPattern := GetURLRegexp() + return http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodPost { + http.Error(res, "only post methods", http.StatusMethodNotAllowed) + return + } + contentType := req.Header.Get("Content-Type") + if contentType != "" && !strings.HasPrefix(contentType, "text/plain") { + http.Error(res, "only post methods", http.StatusUnsupportedMediaType) + return + } + + url := req.URL.Path + + if len(strings.Split(url, "/")) != 3 { + http.Error(res, "wrong url", http.StatusNotFound) + return + } + + if !pathPattern.MatchString(url) { + http.Error(res, "wrong url", http.StatusBadRequest) + return + } + + next.ServeHTTP(res, req) + }) +} + +// nameValueExtractor +// разбивает сроку /<ИМЯ_МЕТРИКИ>/<ЗНАЧЕНИЕ_МЕТРИКИ> на имя и значение +// url уже валидирована!! +func nameValueExtractor(req *http.Request) (string, string) { + url := req.URL.Path + res := strings.Split(url, "/")[1:] + return res[0], res[1] +} + +func GaugeHandlerCreator(storage MemStorage[float64]) http.HandlerFunc { + return func(res http.ResponseWriter, req *http.Request) { + name, valueStr := nameValueExtractor(req) + value, err := strconv.ParseFloat(valueStr, 64) + if err != nil { + http.Error(res, fmt.Sprintf("wrong float64 value: %v", valueStr), http.StatusBadRequest) + return + } + storage.Set(name, value) + res.WriteHeader(http.StatusOK) + } +} + +func CounterHandlerCreator(storage MemStorage[int64]) http.HandlerFunc { + return func(res http.ResponseWriter, req *http.Request) { + name, valueStr := nameValueExtractor(req) + value, err := strconv.ParseInt(valueStr, 10, 64) + if err != nil { + http.Error(res, fmt.Sprintf("wrong int64 value: %v", valueStr), http.StatusBadRequest) + return + } + storage.Add(name, value) + res.WriteHeader(http.StatusOK) + } +} diff --git a/internal/server/server_test.go b/internal/server/server_test.go new file mode 100644 index 0000000..399b602 --- /dev/null +++ b/internal/server/server_test.go @@ -0,0 +1,278 @@ +package server + +import ( + "github.com/stretchr/testify/assert" + "net/http" + "net/http/httptest" + "testing" +) + +type TestCounterCases struct { + url string + method string + contentType string + expectedCode int + expectedKey string + expectedValue int64 +} + +func TestServerCounter(t *testing.T) { + counterStorage := NewInt64Storage() + handler := CreateCounterConveyor(counterStorage) + testCases := []TestCounterCases{ + { + "/m1/123", + http.MethodGet, + "text/plain", + http.StatusMethodNotAllowed, + "", + -1, + }, + { + "/m1/123", + http.MethodPost, + "application/json", + http.StatusUnsupportedMediaType, + "", + -1, + }, + { + "/m1/123_", + http.MethodPost, + "text/plain; charset=utf-8", + http.StatusBadRequest, + "", + -1, + }, + { + "/m1/123.05", + http.MethodPost, + "text/plain; charset=utf-8", + http.StatusBadRequest, + "", + -1, + }, + { + "/123_", + http.MethodPost, + "text/plain; charset=utf-8", + http.StatusNotFound, + "", + -1, + }, + { + "/m1/123", + http.MethodPost, + "text/plain; charset=utf-8", + http.StatusOK, + "m1", + 123, + }, + { + "/m1/-123", + http.MethodPost, + "text/plain; charset=utf-8", + http.StatusOK, + "m1", + 0, + }, + } + + for _, test := range testCases { + req, _ := http.NewRequest(test.method, test.url, nil) + req.Header.Add("Content-Type", test.contentType) + w := httptest.NewRecorder() + handler.ServeHTTP(w, req) + res := w.Result() + res.Body.Close() + assert.Equal(t, test.expectedCode, res.StatusCode) + if res.StatusCode == http.StatusOK { + v, ok := counterStorage.Get(test.expectedKey) + assert.True(t, ok) + assert.Equal(t, test.expectedValue, v) + } + } +} + +type TestGaugeCases struct { + url string + method string + contentType string + expectedCode int + expectedKey string + expectedValue float64 +} + +func TestGaugeCounter(t *testing.T) { + gaugeStorage := NewFloat64Storage() + handler := CreateGaugeConveyor(gaugeStorage) + testCases := []TestGaugeCases{ + { + "/m1/123", + http.MethodGet, + "text/plain", + http.StatusMethodNotAllowed, + "", + -1, + }, + { + "/m1/123", + http.MethodPost, + "application/json", + http.StatusUnsupportedMediaType, + "", + -1, + }, + { + "/m1/123_", + http.MethodPost, + "text/plain; charset=utf-8", + http.StatusBadRequest, + "", + -1, + }, + { + "/m1/123.05", + http.MethodPost, + "text/plain; charset=utf-8", + http.StatusOK, + "m1", + 123.05, + }, + { + "/123_", + http.MethodPost, + "text/plain; charset=utf-8", + http.StatusNotFound, + "", + -1, + }, + { + "/m1/123", + http.MethodPost, + "text/plain; charset=utf-8", + http.StatusOK, + "m1", + 123, + }, + { + "/m1/-123.5", + http.MethodPost, + "text/plain; charset=utf-8", + http.StatusOK, + "m1", + -123.5, + }, + { + "/m1/-1", + http.MethodPost, + "", + http.StatusOK, + "m1", + -1, + }, + } + + for _, test := range testCases { + req, _ := http.NewRequest(test.method, test.url, nil) + if test.contentType != "" { + req.Header.Add("Content-Type", test.contentType) + } + + w := httptest.NewRecorder() + handler.ServeHTTP(w, req) + res := w.Result() + res.Body.Close() + assert.Equal(t, test.expectedCode, res.StatusCode) + if res.StatusCode == http.StatusOK { + v, ok := gaugeStorage.Get(test.expectedKey) + assert.True(t, ok) + assert.Equal(t, test.expectedValue, v) + } + } +} + +type URLTestCases struct { + input string + result bool +} + +func TestGetURLRegexp(t *testing.T) { + reg := GetURLRegexp() + testCases := []URLTestCases{ + { + "/a1s_asd1_1", + false, + }, + { + "/a1s_asd1_1/00.123", + false, + }, + { + "/a1s_asd1_1/0.123", + true, + }, + { + "/a1s_asd1_1/-0.123", + true, + }, + { + "/a1s_asd1_1/0.123?channel=fs&client=ubuntu", + false, + }, + { + "/1_/0.123", + false, + }, + { + "/_m1_/0.123", + false, + }, + { + "/m1_/123", + true, + }, + { + "/m1_/-123", + true, + }, + { + "//m1_/123", + false, + }, + { + "/m1_/123/", + false, + }, + { + "/m1_//123", + false, + }, + { + "/m1_/123.", + false, + }, + { + "/m1_/123.123.1", + false, + }, + { + "/m1_/123.123.", + false, + }, + { + "/m1_/123.123", + true, + }, + { + "/a1s_asd1_1/0.123/asdsavb", + false, + }, + } + + for _, test := range testCases { + res := reg.MatchString(test.input) + assert.Equal(t, test.result, res) + } + +}