From 5a46c41cb605c84e206a5a46d0987b8698862f41 Mon Sep 17 00:00:00 2001 From: Stas Merzlyakov Date: Mon, 18 Mar 2024 14:54:21 +0400 Subject: [PATCH] =?UTF-8?q?=D0=B8=D0=B7-=D0=B7=D0=B0=20=D0=BF=D0=BE=D0=B7?= =?UTF-8?q?=D0=B4=D0=BD=D0=B5=D0=B9=20=D1=81=D0=B4=D0=B0=D1=87=D0=B8=20?= =?UTF-8?q?=D0=B2=D0=BE=D0=B7=D0=BD=D0=B8=D0=BA=D0=B0=D1=8E=D1=82=20=D0=BA?= =?UTF-8?q?=D0=BE=D0=BD=D1=84=D0=BB=D0=B8=D0=BA=D1=82=D1=8B=20=D1=81=20?= =?UTF-8?q?=D0=B2=D0=B5=D1=82=D0=BA=D0=BE=D0=B9=20iter13=20Revert=20"Iter9?= =?UTF-8?q?=20(#12)"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit fa730323072afb68dd5b415ae415ae3a3aea84fa. --- .gitattributes | 1 - Arch_Idea.png | 3 - Questions.md | 17 - cmd/agent/agent.go | 31 +- cmd/server/server.go | 72 +-- go.mod | 17 +- go.sum | 19 - internal/agent/agent.go | 101 ++-- internal/agent/api.go | 15 + .../agent.go => agent/configuration.go} | 8 +- internal/agent/domain_model.go | 15 - internal/agent/http_result_sender.go | 74 +-- internal/agent/memstats_storage_test.go | 70 --- ...memstats_storage.go => runtime_metrics.go} | 44 +- internal/agent/runtime_metrics_test.go | 53 ++ internal/common.go | 43 ++ internal/config/server.go | 59 --- internal/server/adapter/fs/formatter/json.go | 90 ---- .../server/adapter/fs/formatter/json_test.go | 89 ---- .../server/adapter/http/handler/handler.go | 384 -------------- .../adapter/http/handler/handler_test.go | 501 ------------------ .../server/adapter/http/middleware/common.go | 12 - .../compress_gzip_buffer_response_mw.go | 62 --- .../compress_gzip_buffer_response_mw_test.go | 119 ----- .../compress/compress_gzip_response_mw.go | 57 -- .../compress_gzip_response_mw_test.go | 119 ----- .../compress/compress_utils_test.go | 46 -- .../compress/uncompress_gzip_request_mw.go | 41 -- .../uncompress_gzip_request_mw_test.go | 73 --- .../logging/logging_request_info_mw.go | 30 -- .../logging/logging_response_info_mw.go | 53 -- internal/server/configuration.go | 41 ++ internal/server/controller.go | 76 +++ internal/server/domain/common_types.go | 5 - internal/server/domain/entities.go | 15 - internal/server/domain/errors.go | 8 - internal/server/domain/usefull.go | 9 - internal/server/handlers.go | 120 +++++ internal/server/middleware.go | 62 +++ internal/server/middleware_test.go | 170 ++++++ internal/server/server.go | 140 ++--- internal/server/server_test.go | 112 ++++ internal/server/storage/memory/storage.go | 141 ----- .../server/storage/memory/storage_test.go | 174 ------ internal/server/usecase/backup.go | 56 -- internal/server/usecase/metrics.go | 145 ----- internal/server/usecase/metrics_test.go | 145 ----- internal/server/utils.go | 40 ++ internal/server/utils_test.go | 292 ++++++++++ internal/storage/api.go | 12 + internal/storage/mem_storage.go | 53 ++ internal/storage/mem_storage_test.go | 46 ++ 52 files changed, 1275 insertions(+), 2905 deletions(-) delete mode 100644 .gitattributes delete mode 100644 Arch_Idea.png delete mode 100644 Questions.md create mode 100644 internal/agent/api.go rename internal/{config/agent.go => agent/configuration.go} (82%) delete mode 100644 internal/agent/domain_model.go delete mode 100644 internal/agent/memstats_storage_test.go rename internal/agent/{memstats_storage.go => runtime_metrics.go} (65%) create mode 100644 internal/agent/runtime_metrics_test.go create mode 100644 internal/common.go delete mode 100644 internal/config/server.go delete mode 100644 internal/server/adapter/fs/formatter/json.go delete mode 100644 internal/server/adapter/fs/formatter/json_test.go delete mode 100644 internal/server/adapter/http/handler/handler.go delete mode 100644 internal/server/adapter/http/handler/handler_test.go delete mode 100644 internal/server/adapter/http/middleware/common.go delete mode 100644 internal/server/adapter/http/middleware/compress/compress_gzip_buffer_response_mw.go delete mode 100644 internal/server/adapter/http/middleware/compress/compress_gzip_buffer_response_mw_test.go delete mode 100644 internal/server/adapter/http/middleware/compress/compress_gzip_response_mw.go delete mode 100644 internal/server/adapter/http/middleware/compress/compress_gzip_response_mw_test.go delete mode 100644 internal/server/adapter/http/middleware/compress/compress_utils_test.go delete mode 100644 internal/server/adapter/http/middleware/compress/uncompress_gzip_request_mw.go delete mode 100644 internal/server/adapter/http/middleware/compress/uncompress_gzip_request_mw_test.go delete mode 100644 internal/server/adapter/http/middleware/logging/logging_request_info_mw.go delete mode 100644 internal/server/adapter/http/middleware/logging/logging_response_info_mw.go create mode 100644 internal/server/configuration.go create mode 100644 internal/server/controller.go delete mode 100644 internal/server/domain/common_types.go delete mode 100644 internal/server/domain/entities.go delete mode 100644 internal/server/domain/errors.go delete mode 100644 internal/server/domain/usefull.go create mode 100644 internal/server/handlers.go create mode 100644 internal/server/middleware.go create mode 100644 internal/server/middleware_test.go delete mode 100644 internal/server/storage/memory/storage.go delete mode 100644 internal/server/storage/memory/storage_test.go delete mode 100644 internal/server/usecase/backup.go delete mode 100644 internal/server/usecase/metrics.go delete mode 100644 internal/server/usecase/metrics_test.go create mode 100644 internal/server/utils.go create mode 100644 internal/server/utils_test.go create mode 100644 internal/storage/api.go create mode 100644 internal/storage/mem_storage.go create mode 100644 internal/storage/mem_storage_test.go diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index 24a8e87..0000000 --- a/.gitattributes +++ /dev/null @@ -1 +0,0 @@ -*.png filter=lfs diff=lfs merge=lfs -text diff --git a/Arch_Idea.png b/Arch_Idea.png deleted file mode 100644 index fd5438f..0000000 --- a/Arch_Idea.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f30243b2c9b7168a9fde6cd0732a365e9c8d0313dec612b54657bcb6edbc54a3 -size 397679 diff --git a/Questions.md b/Questions.md deleted file mode 100644 index e1d134d..0000000 --- a/Questions.md +++ /dev/null @@ -1,17 +0,0 @@ -- АРХИТЕКТУРА!!! - -- chi.Use хочет handler - можно ли подставить в chi http.HandlerFunc (с handlerFunc работать удобней, чем с обычным http.Handler; см compress_utils_test.go); есть ли преобразование HandlerFunc в Handler (ссылка на пример, если есть) - -- Куда спрятать доп.функции для тестирования (compress_utils_test.go) - -- Формат логов - что писать, где писть, как писать, КАК ПРОВЕРЯТЬ что разработчики не косячат и как автоматически поддерживать - -- w.WriteHeader(http.StatusOK) - где писать, надо ли вообще писать? (судя по всему не надо; мало ли какие мидлы дальше будут работать) - -- Загрузка конфигурации (flag, env) (см internal/config/server.go) - можно ли покрыть тестами (ссылка на пример, если есть; надо ли вообще? :) ) - -- Передача logger и инициализация в тестах log := logger.Sugar() (нормальна ли завязка на конкрентый логгер; есть ли какой-то вариант из zap для использования в тестах с упрощенной инициализацией см handler_test/TestPostUpdate.go) - -- BoolVar - работает хитро - значение по-умолчанию "true" ????!!! (сделал через flag.Var - не усложняю ли) - -- Вопрос по api - "net/http".Server.Shutdown(ctx context.Context) для чего передается контекст? для возможности cancel контекста? (например по по таймауту) diff --git a/cmd/agent/agent.go b/cmd/agent/agent.go index 6d98ba3..b4ad5f6 100644 --- a/cmd/agent/agent.go +++ b/cmd/agent/agent.go @@ -8,38 +8,25 @@ import ( "syscall" "github.com/StasMerzlyakov/go-metrics/internal/agent" - "github.com/StasMerzlyakov/go-metrics/internal/config" ) -type Agent interface { - Start(ctx context.Context) - Wait() -} - func main() { - agentCfg, err := config.LoadAgentConfig() + agentCfg, err := agent.LoadConfig() if err != nil { log.Fatal(err) } - metricStorage := agent.NewMemStatsStorage() - resultSender := agent.NewHTTPResultSender(agentCfg.ServerAddr) - - var agnt Agent = agent.Create(agentCfg, - resultSender, - metricStorage, - ) - // Взято отсюда: "Реализация Graceful Shutdown в Go"(https://habr.com/ru/articles/771626/) // Сейчас выглядит избыточным - оставил как задел на будущее для сервера - ctx, cancelFn := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(context.Background()) exit := make(chan os.Signal, 1) signal.Notify(exit, os.Interrupt, syscall.SIGTERM) - agnt.Start(ctx) - defer func() { - cancelFn() - agnt.Wait() - }() - <-exit + if agent, err := agent.CreateAgent(ctx, agentCfg); err != nil { + panic(err) + } else { + <-exit + cancel() + agent.Wait() // ожидаение завершения go-рутин в агенте + } } diff --git a/cmd/server/server.go b/cmd/server/server.go index 6590cf7..9ff5d1c 100644 --- a/cmd/server/server.go +++ b/cmd/server/server.go @@ -1,81 +1,19 @@ package main import ( - "context" - "net/http" - "os" - "os/signal" - "syscall" + "log" - "github.com/StasMerzlyakov/go-metrics/internal/config" "github.com/StasMerzlyakov/go-metrics/internal/server" - "github.com/StasMerzlyakov/go-metrics/internal/server/adapter/fs/formatter" - "github.com/StasMerzlyakov/go-metrics/internal/server/adapter/http/handler" - "github.com/StasMerzlyakov/go-metrics/internal/server/adapter/http/middleware/compress" - "github.com/StasMerzlyakov/go-metrics/internal/server/adapter/http/middleware/logging" - "github.com/StasMerzlyakov/go-metrics/internal/server/storage/memory" - "github.com/StasMerzlyakov/go-metrics/internal/server/usecase" - "go.uber.org/zap" ) -type Server interface { - Start(ctx context.Context) - WaitDone() -} - -func createMiddleWareList(log *zap.SugaredLogger) []func(http.Handler) http.Handler { - return []func(http.Handler) http.Handler{ - logging.NewLoggingResponseMW(log), - compress.NewCompressGZIPResponseMW(log), //compress.NewCompressGZIPBufferResponseMW(log), - compress.NewUncompressGZIPRequestMW(log), - logging.NewLoggingRequestMW(log), - } -} - func main() { - // Конфигурация - srvConf, err := config.LoadServerConfig() + srvConf, err := server.LoadConfig() if err != nil { - panic(err) + log.Fatal(err) } - // Создаем логгер - logger, err := zap.NewDevelopment() - if err != nil { - // вызываем панику, если ошибка - panic("cannot initialize zap") + if err := server.CreateServer(srvConf); err != nil { + panic(err) } - defer logger.Sync() - - sugarLog := logger.Sugar() - - // Сборка сервера - storage := memory.NewStorage() - - backupFomratter := formatter.NewJSON(sugarLog, srvConf.FileStoragePath) - backup := usecase.NewBackup(sugarLog, storage, backupFomratter) - - metrics := usecase.NewMetrics(storage) - - mwList := createMiddleWareList(sugarLog) - httpHandler := handler.NewHTTP(metrics, sugarLog, mwList...) - - var server Server = server.NewMetricsServer(srvConf, - sugarLog, - httpHandler, - metrics, - backup) - - // Запуск сервера - ctx, cancelFn := context.WithCancel(context.Background()) - exit := make(chan os.Signal, 1) - signal.Notify(exit, os.Interrupt, syscall.SIGTERM) - - server.Start(ctx) - defer func() { - cancelFn() - server.WaitDone() - }() - <-exit } diff --git a/go.mod b/go.mod index d2835d2..cf4a363 100644 --- a/go.mod +++ b/go.mod @@ -3,21 +3,12 @@ module github.com/StasMerzlyakov/go-metrics go 1.19 require ( - github.com/caarlos0/env v3.5.0+incompatible - github.com/go-chi/chi/v5 v5.0.11 - github.com/go-resty/resty/v2 v2.11.0 - github.com/pkg/errors v0.9.1 - github.com/sirupsen/logrus v1.9.3 - github.com/stretchr/testify v1.8.4 - github.com/ungerik/go-pool v0.0.0-20140720100922-d102a2c7872a - go.uber.org/zap v1.27.0 -) - -require ( + github.com/caarlos0/env v3.5.0+incompatible // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/go-chi/chi/v5 v5.0.11 // indirect + github.com/go-resty/resty/v2 v2.11.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - go.uber.org/multierr v1.10.0 // indirect + github.com/stretchr/testify v1.8.4 // indirect golang.org/x/net v0.17.0 // indirect - golang.org/x/sys v0.13.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 7f22f50..844aa83 100644 --- a/go.sum +++ b/go.sum @@ -1,30 +1,16 @@ github.com/caarlos0/env v3.5.0+incompatible h1:Yy0UN8o9Wtr/jGHZDpCBLpNrzcFLLM2yixi/rBrKyJs= github.com/caarlos0/env v3.5.0+incompatible/go.mod h1:tdCsowwCzMLdkqRYDlHpZCp2UooDD3MspDBjZ2AD02Y= -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/go-chi/chi/v5 v5.0.11 h1:BnpYbFZ3T3S1WMpD79r7R5ThWX40TaFB7L31Y8xqSwA= github.com/go-chi/chi/v5 v5.0.11/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-resty/resty/v2 v2.11.0 h1:i7jMfNOJYMp69lq7qozJP+bjgzfAzeOhuGlyDrqxT/8= github.com/go-resty/resty/v2 v2.11.0/go.mod h1:iiP/OpA0CkcL3IGt1O0+/SIItFUbkkyw5BGXiVdTu+A= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/ungerik/go-pool v0.0.0-20140720100922-d102a2c7872a h1:Lfu+d2ZpWyfH6HyuQrGI3om39+yNuvk2OWT0UptrFAM= -github.com/ungerik/go-pool v0.0.0-20140720100922-d102a2c7872a/go.mod h1:DcjcKDwgMN/tbplnqJjxJNonhx4QvgPsv5W3N+h6sFU= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= -go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= -go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= -go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= @@ -44,11 +30,9 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -61,15 +45,12 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/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= diff --git a/internal/agent/agent.go b/internal/agent/agent.go index 9783dc3..f413ff9 100644 --- a/internal/agent/agent.go +++ b/internal/agent/agent.go @@ -2,89 +2,88 @@ package agent import ( "context" - "log" + "fmt" "sync" "time" - "github.com/StasMerzlyakov/go-metrics/internal/config" - "github.com/sirupsen/logrus" + "github.com/StasMerzlyakov/go-metrics/internal/storage" ) -type ResultSender interface { - SendMetrics(metrics []Metrics) error +type Agent interface { + Wait() } -type MetricStorage interface { - Refresh() error - GetMetrics() []Metrics -} - -func Create(config *config.AgentConfiguration, - resultSender ResultSender, - metricStorage MetricStorage, -) *agent { +func CreateAgent(ctx context.Context, config *Configuration) (Agent, error) { agent := &agent{ - metricStorage: metricStorage, - resultSender: resultSender, - pollIntervalSec: config.PollInterval, - reportIntervalSec: config.ReportInterval, + metricsSource: NewRuntimeMetricsSource(), + resultSender: NewHTTPResultSender(config.ServerAddr), + gaugeStorage: storage.NewMemoryFloat64Storage(), + poolCounter: 0, } - - return agent + go agent.PoolMetrics(ctx, config.PollInterval) + go agent.ReportMetrics(ctx, config.ReportInterval) + agent.wg.Add(2) + return agent, nil } type agent struct { - metricStorage MetricStorage - resultSender ResultSender - pollIntervalSec int - reportIntervalSec int - wg sync.WaitGroup + metricsSource MetricsSource + resultSender ResultSender + gaugeStorage storage.MetricsStorage[float64] + poolCounter int64 + wg sync.WaitGroup } func (a *agent) Wait() { a.wg.Wait() } -func (a *agent) Start(ctx context.Context) { - go a.pollMetrics(ctx) - go a.reportMetrics(ctx) - a.wg.Add(2) -} - -func (a *agent) pollMetrics(ctx context.Context) { - pollInterval := time.Duration(a.pollIntervalSec) * time.Second - defer a.wg.Done() +func (a *agent) PoolMetrics(ctx context.Context, pollIntervalSec int) { + counter := 0 for { select { case <-ctx.Done(): - logrus.Info("PollMetrics DONE") + fmt.Printf("[%v] PoolMetrics DONE\n", time.Now()) + a.wg.Done() return - - case <-time.After(pollInterval): - if err := a.metricStorage.Refresh(); err != nil { - logrus.Fatalf("PollMetrics metrics error: %v", err) + default: + time.Sleep(1 * time.Second) // Будем просыпаться каждую секунду для проверки ctx + counter++ + if counter == pollIntervalSec { + counter = 0 + for k, v := range a.metricsSource.PollMetrics() { + a.gaugeStorage.Set(k, v) + } + a.poolCounter = a.metricsSource.PollCount() + fmt.Printf("[%v] PoolMetrics [SUCCESS] (poolCounter %v)\n", time.Now(), a.poolCounter) } - logrus.Info("PollMetrics SUCCESS") } } } -func (a *agent) reportMetrics(ctx context.Context) { - reportInterval := time.Duration(a.reportIntervalSec) * time.Second - +func (a *agent) ReportMetrics(ctx context.Context, reportIntervalSec int) { + counter := 0 +MAIN: for { select { case <-ctx.Done(): - log.Println("ReportMetrics DONE") + fmt.Printf("[%v] ReportMetrics DONE\n", time.Now()) a.wg.Done() return - case <-time.After(reportInterval): - metrics := a.metricStorage.GetMetrics() - err := a.resultSender.SendMetrics(metrics) - if err != nil { - log.Printf("ReportMetrics ERROR: %v\n", err) - } else { - logrus.Info("ReportMetrics SUCCESS") + default: + time.Sleep(1 * time.Second) // Будем просыпаться каждую секунду для проверки ctx + counter++ + if counter == reportIntervalSec { + counter = 0 + for _, key := range a.gaugeStorage.Keys() { + val, _ := a.gaugeStorage.Get(key) + if err := a.resultSender.SendGauge(key, val); err != nil { + fmt.Printf("[%v] ReportMetrics [ERROR] (poolCounter %v)\n", time.Now(), err) + continue MAIN + } + } + _ = a.resultSender.SendCounter("PoolCount", a.poolCounter) + fmt.Printf("[%v] ReportMetrics [SUCCESS] (poolCounter %v)\n", time.Now(), a.poolCounter) } } } diff --git a/internal/agent/api.go b/internal/agent/api.go new file mode 100644 index 0000000..d2cf8ff --- /dev/null +++ b/internal/agent/api.go @@ -0,0 +1,15 @@ +package agent + +import "errors" + +type MetricsSource interface { + PollMetrics() map[string]float64 + PollCount() int64 +} + +var ErrServerInteraction = errors.New("server interaction error") + +type ResultSender interface { + SendGauge(name string, value float64) error + SendCounter(name string, value int64) error +} diff --git a/internal/config/agent.go b/internal/agent/configuration.go similarity index 82% rename from internal/config/agent.go rename to internal/agent/configuration.go index db3f2b2..89f4560 100644 --- a/internal/config/agent.go +++ b/internal/agent/configuration.go @@ -1,4 +1,4 @@ -package config +package agent import ( "flag" @@ -8,15 +8,15 @@ import ( "github.com/caarlos0/env" ) -type AgentConfiguration struct { +type Configuration struct { ServerAddr string `env:"ADDRESS"` PollInterval int `env:"POLL_INTERVAL"` ReportInterval int `env:"REPORT_INTERVAL"` } -func LoadAgentConfig() (*AgentConfiguration, error) { +func LoadConfig() (*Configuration, error) { - agentCfg := &AgentConfiguration{} + agentCfg := &Configuration{} flag.StringVar(&agentCfg.ServerAddr, "a", "localhost:8080", "serverAddress") flag.IntVar(&agentCfg.PollInterval, "p", 2, "poolInterval in seconds") diff --git a/internal/agent/domain_model.go b/internal/agent/domain_model.go deleted file mode 100644 index 26f326c..0000000 --- a/internal/agent/domain_model.go +++ /dev/null @@ -1,15 +0,0 @@ -package agent - -type MetricType string - -type Metrics struct { - ID string `json:"id"` // имя метрики - MType MetricType `json:"type"` // параметр, принимающий значение gauge или counter - Delta *int64 `json:"delta,omitempty"` // значение метрики в случае передачи counter - Value *float64 `json:"value,omitempty"` // значение метрики в случае передачи gauge -} - -const ( - GaugeType MetricType = "gauge" - CounterType MetricType = "counter" -) diff --git a/internal/agent/http_result_sender.go b/internal/agent/http_result_sender.go index 6e7c80b..a7d52e0 100644 --- a/internal/agent/http_result_sender.go +++ b/internal/agent/http_result_sender.go @@ -1,81 +1,61 @@ package agent import ( - "bytes" - "compress/gzip" - "encoding/json" "errors" "fmt" - "net/http" "strings" + "sync" "github.com/go-resty/resty/v2" - "github.com/sirupsen/logrus" ) -func NewHTTPResultSender(serverAdd string) *httpResultSender { +func NewHTTPResultSender(serverAdd string) ResultSender { if !strings.HasPrefix(serverAdd, "http") { serverAdd = "http://" + serverAdd } serverAdd = strings.TrimSuffix(serverAdd, "/") return &httpResultSender{ serverAdd: serverAdd, - client: resty.New(). - // Иногда возникает ошибка EOF или http: server closed idle connection; добавим Retry - SetRetryCount(3), } } type httpResultSender struct { serverAdd string client *resty.Client + sm sync.Mutex } -func (h *httpResultSender) SendMetrics(metrics []Metrics) error { - for _, metric := range metrics { - err := h.store(metric) - if err != nil { - return err - } +func (h *httpResultSender) initIfNecessary() { + h.sm.Lock() + defer h.sm.Unlock() + if h.client == nil { + h.client = resty.New(). + // Иногда возникает ошибка EOF или http: server closed idle connection; добавим Retry + SetRetryCount(3) } - return nil } -func (h *httpResultSender) store(metric Metrics) error { - var buf bytes.Buffer - - w, err := gzip.NewWriterLevel(&buf, gzip.BestCompression) - if err != nil { - logrus.Errorf("gzip.NewWriterLevel error: %v", err) - return err - } - if err := json.NewEncoder(w).Encode(metric); err != nil { - logrus.Errorf("json encode error: %v", err) - return err - } - - err = w.Close() - if err != nil { - logrus.Errorf("gzip close error: %v", err) - } +func (h *httpResultSender) store(metricType string, metricName string, value string) error { + h.initIfNecessary() + _, err := h.client.R(). + SetHeader("Content-Type", "text/plain; charset=UTF-8"). + SetPathParams(map[string]string{ + "metricType": metricType, + "metricName": metricName, + "value": value, + }).Post(h.serverAdd + "/update/{metricType}/{metricName}/{value}") - resp, err := h.client.R(). - SetHeader("Content-Type", "application/json; charset=UTF-8"). - SetHeader("Content-Encoding", "gzip"). - SetBody(buf.Bytes()).Post(h.serverAdd + "/update/") if err != nil { - logrus.Errorf("server communication error: %v", err) + fmt.Printf("%v\n", errors.Unwrap(err)) } - if resp.StatusCode() != http.StatusOK { - errStr := fmt.Sprintf("unexpected server http response code: %v", resp.StatusCode()) - logrus.Errorf(errStr) - return errors.New(errStr) - } + return err +} - /*_, err := h.client.R(). - SetHeader("Content-Type", "application/json; charset=UTF-8"). - SetBody(metric).Post(h.serverAdd + "/update/") */ +func (h *httpResultSender) SendGauge(name string, value float64) error { + return h.store("gauge", name, fmt.Sprintf("%v", value)) +} - return err +func (h *httpResultSender) SendCounter(name string, value int64) error { + return h.store("counter", name, fmt.Sprintf("%v", value)) } diff --git a/internal/agent/memstats_storage_test.go b/internal/agent/memstats_storage_test.go deleted file mode 100644 index 405a746..0000000 --- a/internal/agent/memstats_storage_test.go +++ /dev/null @@ -1,70 +0,0 @@ -package agent - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestMemStatsSource(t *testing.T) { - mm := &memStatsSource{} - expectedKeys := map[string]MetricType{ - "Alloc": GaugeType, - "BuckHashSys": GaugeType, - "Frees": GaugeType, - "GCCPUFraction": GaugeType, - "GCSys": GaugeType, - "HeapAlloc": GaugeType, - "HeapIdle": GaugeType, - "HeapInuse": GaugeType, - "HeapObjects": GaugeType, - "HeapReleased": GaugeType, - "HeapSys": GaugeType, - "LastGC": GaugeType, - "Lookups": GaugeType, - "MCacheInuse": GaugeType, - "MCacheSys": GaugeType, - "MSpanInuse": GaugeType, - "MSpanSys": GaugeType, - "Mallocs": GaugeType, - "NextGC": GaugeType, - "NumForcedGC": GaugeType, - "NumGC": GaugeType, - "OtherSys": GaugeType, - "PauseTotalNs": GaugeType, - "StackInuse": GaugeType, - "StackSys": GaugeType, - "Sys": GaugeType, - "TotalAlloc": GaugeType, - "RandomValue": GaugeType, - "PollCount": CounterType, - } - err := mm.Refresh() - require.NoError(t, err) - err = mm.Refresh() - require.NoError(t, err) - err = mm.Refresh() - require.NoError(t, err) - err = mm.Refresh() - require.NoError(t, err) - - metrics := mm.GetMetrics() - - require.Equal(t, len(expectedKeys), len(metrics)) - - var pollCount int64 - - for _, metric := range metrics { - mType, ok := expectedKeys[metric.ID] - require.Truef(t, ok, "pollMetrics doesn't contain key %v", metric.ID) - require.Equalf(t, mType, metric.MType, "pollMetrics contain key %v with different type", metric.ID) - - if metric.ID == "PollCount" { - pollCount = *metric.Delta - } - - } - - assert.Equal(t, int64(4), pollCount) -} diff --git a/internal/agent/memstats_storage.go b/internal/agent/runtime_metrics.go similarity index 65% rename from internal/agent/memstats_storage.go rename to internal/agent/runtime_metrics.go index 9b05d91..ad0f3ff 100644 --- a/internal/agent/memstats_storage.go +++ b/internal/agent/runtime_metrics.go @@ -6,23 +6,22 @@ import ( "sync/atomic" ) -func NewMemStatsStorage() *memStatsSource { - return &memStatsSource{ - poolCounter: 0, - memStatStorage: nil, - } +func NewRuntimeMetricsSource() MetricsSource { + return &runtimeMetrics{} +} + +type runtimeMetrics struct { + counter int64 } -type memStatsSource struct { - poolCounter int64 - memStatStorage map[string]float64 +func (rm *runtimeMetrics) PollCount() int64 { + return rm.counter } -func (m *memStatsSource) Refresh() error { - defer atomic.AddInt64(&m.poolCounter, 1) +func (rm *runtimeMetrics) PollMetrics() map[string]float64 { + defer atomic.AddInt64(&rm.counter, 1) var memStats runtime.MemStats - runtime.ReadMemStats(&memStats) - m.memStatStorage = map[string]float64{ + return map[string]float64{ "Alloc": float64(memStats.Alloc), "BuckHashSys": float64(memStats.BuckHashSys), "Frees": float64(memStats.Frees), @@ -52,25 +51,4 @@ func (m *memStatsSource) Refresh() error { "TotalAlloc": float64(memStats.TotalAlloc), "RandomValue": rand.Float64(), } - return nil -} - -func (m *memStatsSource) GetMetrics() []Metrics { - var metrics []Metrics - for k, v := range m.memStatStorage { - value := v - metrics = append(metrics, Metrics{ - ID: k, - MType: GaugeType, - Value: &value, - }) - } - - poolCount := m.poolCounter - metrics = append(metrics, Metrics{ - ID: "PollCount", - MType: CounterType, - Delta: &poolCount, - }) - return metrics } diff --git a/internal/agent/runtime_metrics_test.go b/internal/agent/runtime_metrics_test.go new file mode 100644 index 0000000..4c983b7 --- /dev/null +++ b/internal/agent/runtime_metrics_test.go @@ -0,0 +1,53 @@ +package agent + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRuntimeMetrics(t *testing.T) { + rm := &runtimeMetrics{} + expectedKeys := []string{ + "Alloc", + "BuckHashSys", + "Frees", + "GCCPUFraction", + "GCSys", + "HeapAlloc", + "HeapIdle", + "HeapInuse", + "HeapObjects", + "HeapReleased", + "HeapSys", + "LastGC", + "Lookups", + "MCacheInuse", + "MCacheSys", + "MSpanInuse", + "MSpanSys", + "Mallocs", + "NextGC", + "NumForcedGC", + "NumGC", + "OtherSys", + "PauseTotalNs", + "StackInuse", + "StackSys", + "Sys", + "TotalAlloc", + "RandomValue", + } + result := rm.PollMetrics() + assert.Equal(t, len(expectedKeys), len(result)) + for _, expectedKey := range expectedKeys { + _, ok := result[expectedKey] + require.Truef(t, ok, "pollMetrics doesn't contain key %v", expectedKey) + } + + rm.PollMetrics() + rm.PollMetrics() + rm.PollMetrics() + assert.Equal(t, int64(4), rm.PollCount()) +} diff --git a/internal/common.go b/internal/common.go new file mode 100644 index 0000000..f33e319 --- /dev/null +++ b/internal/common.go @@ -0,0 +1,43 @@ +package internal + +import ( + "fmt" + "net/http" +) + +func BadRequestHandler(w http.ResponseWriter, req *http.Request) { + http.Error(w, "BadRequest", http.StatusBadRequest) +} + +func StatusMethodNotAllowedHandler(w http.ResponseWriter, req *http.Request) { + http.Error(w, "StatusMethodNotAllowed", http.StatusMethodNotAllowed) +} +func StatusNotImplemented(w http.ResponseWriter, req *http.Request) { + http.Error(w, "StatusMethodNotAllowed", http.StatusNotImplemented) +} + +func StatusNotFound(w http.ResponseWriter, req *http.Request) { + http.Error(w, "StatusNotFound", http.StatusNotFound) +} + +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.HandlerFunc) http.HandlerFunc + +func Conveyor(h http.HandlerFunc, middlewares ...Middleware) http.HandlerFunc { + for _, middleware := range middlewares { + h = middleware(h) + } + return h +} diff --git a/internal/config/server.go b/internal/config/server.go deleted file mode 100644 index 40b5265..0000000 --- a/internal/config/server.go +++ /dev/null @@ -1,59 +0,0 @@ -package config - -import ( - "flag" - "fmt" - "os" - "strconv" - - "github.com/caarlos0/env" -) - -type ServerConfiguration struct { - URL string `env:"ADDRESS"` - StoreInterval uint `env:"STORE_INTERVAL"` - FileStoragePath string `env:"FILE_STORAGE_PATH"` - Restore bool `env:"RESTORE"` -} - -type RestoreConfiguration struct { - Restore bool -} - -func (r *RestoreConfiguration) String() string { - return fmt.Sprintf("%v", r.Restore) -} - -func (r *RestoreConfiguration) Set(s string) (err error) { - r.Restore, err = strconv.ParseBool(s) - return -} - -var _ flag.Value = (*RestoreConfiguration)(nil) - -func LoadServerConfig() (*ServerConfiguration, error) { - srvConf := &ServerConfiguration{} - - flag.StringVar(&srvConf.URL, "a", ":8080", "server address (format \":PORT\")") - flag.UintVar(&srvConf.StoreInterval, "i", 300, "Backup store interval in seconds") - flag.StringVar(&srvConf.FileStoragePath, "f", "/tmp/metrics-db.json", "Backup file path") - - // Шаманстрва из-за того, что Go хитро обрабатывает Bool-флаги(проверяет просто наличие флага в коммандной строке) - restoreConf := &RestoreConfiguration{ - Restore: true, // Значение по-умолчанию - } - flag.Var(restoreConf, "r", "is backap restore need") - flag.Usage = func() { - fmt.Fprintf(flag.CommandLine.Output(), "Usage of %s:\n", os.Args[0]) - flag.PrintDefaults() - } - flag.Parse() - - srvConf.Restore = restoreConf.Restore - - err := env.Parse(srvConf) - if err != nil { - return nil, err - } - return srvConf, nil -} diff --git a/internal/server/adapter/fs/formatter/json.go b/internal/server/adapter/fs/formatter/json.go deleted file mode 100644 index 9f8a3b1..0000000 --- a/internal/server/adapter/fs/formatter/json.go +++ /dev/null @@ -1,90 +0,0 @@ -package formatter - -import ( - "encoding/json" - "errors" - "fmt" - "os" - - "github.com/StasMerzlyakov/go-metrics/internal/server/domain" - "go.uber.org/zap" -) - -const ( - tempFileTemplate = "metrics_backup_*.json.tmp" -) - -func NewJSON(log *zap.SugaredLogger, fileStoragePath string) *jsonFormatter { - jsonFormatter := &jsonFormatter{ - fileStoragePath: fileStoragePath, - logger: log, - } - return jsonFormatter -} - -type jsonFormatter struct { - fileStoragePath string - logger *zap.SugaredLogger -} - -func (jf *jsonFormatter) Write(metricses []domain.Metrics) error { - if jf.fileStoragePath == "" { - jf.logger.Errorw("Write", "status", "error", "msg", "fileStoragePath is not specified") - return os.ErrNotExist - } - - file, err := os.OpenFile(jf.fileStoragePath, os.O_CREATE|os.O_WRONLY, 0660) - if err != nil { - return err - } - - defer file.Close() - - // Пишем во временный файл - tmpDir := os.TempDir() - file, err = os.CreateTemp(tmpDir, tempFileTemplate) - - if err != nil { - jf.logger.Infow("Write", "status", "ok", "error", "can't create temp file") - return err - } - - defer os.Rename(file.Name(), jf.fileStoragePath) // Просто переименовываем временный файл - - err = json.NewEncoder(file).Encode(metricses) - if err != nil { - jf.logger.Errorw("Write", "status", "error", "msg", err.Error()) - return err - } - - jf.logger.Infow("Write", "status", "ok", "msg", fmt.Sprintf("metrics stored into file %v", jf.fileStoragePath)) - return nil -} -func (jf *jsonFormatter) Read() ([]domain.Metrics, error) { - - var result []domain.Metrics - - if jf.fileStoragePath == "" { - jf.logger.Errorw("Read", "status", "error", "msg", "fileStoragePath is not specified") - return result, os.ErrNotExist - } - - file, err := os.Open(jf.fileStoragePath) - if err != nil { - if errors.Is(err, os.ErrNotExist) { - jf.logger.Infow("Read", "status", "ok", "msg", fmt.Sprintf("dump file %v not exists", jf.fileStoragePath)) - return result, os.ErrNotExist - } - return nil, err - } - - defer file.Close() - - err = json.NewDecoder(file).Decode(&result) - if err != nil { - jf.logger.Infow("Read", "status", "error", "msg", fmt.Sprintf("can't restore backup from file %v", jf.fileStoragePath)) - return nil, err - } - - return result, nil -} diff --git a/internal/server/adapter/fs/formatter/json_test.go b/internal/server/adapter/fs/formatter/json_test.go deleted file mode 100644 index 4f3f646..0000000 --- a/internal/server/adapter/fs/formatter/json_test.go +++ /dev/null @@ -1,89 +0,0 @@ -package formatter_test - -import ( - "os" - "reflect" - "testing" - - "github.com/StasMerzlyakov/go-metrics/internal/server/adapter/fs/formatter" - "github.com/StasMerzlyakov/go-metrics/internal/server/domain" - "github.com/stretchr/testify/require" - "go.uber.org/zap" -) - -func getLogger() *zap.SugaredLogger { - logger, err := zap.NewDevelopment() - if err != nil { - // вызываем панику, если ошибка - panic("cannot initialize zap") - } - - return logger.Sugar() -} - -var toWrite = []domain.Metrics{ - {MType: domain.CounterType, ID: "PollCount", Delta: domain.DeltaPtr(1)}, - {MType: domain.GaugeType, ID: "RandomValue", Value: domain.ValuePtr(1.123)}, - {MType: domain.GaugeType, ID: "Alloc", Value: domain.ValuePtr(1.123)}, - {MType: domain.GaugeType, ID: "BuckHashSys", Value: domain.ValuePtr(1.123)}, - {MType: domain.GaugeType, ID: "Frees", Value: domain.ValuePtr(1.123)}, - {MType: domain.GaugeType, ID: "GCCPUFraction", Value: domain.ValuePtr(1.123)}, - {MType: domain.GaugeType, ID: "GCSys", Value: domain.ValuePtr(1.123)}, - {MType: domain.GaugeType, ID: "HeapAlloc", Value: domain.ValuePtr(1.123)}, - {MType: domain.GaugeType, ID: "HeapIdle", Value: domain.ValuePtr(1.123)}, - {MType: domain.GaugeType, ID: "HeapInuse", Value: domain.ValuePtr(1.123)}, - {MType: domain.GaugeType, ID: "HeapObjects", Value: domain.ValuePtr(1.123)}, - {MType: domain.GaugeType, ID: "HeapReleased", Value: domain.ValuePtr(1.123)}, - {MType: domain.GaugeType, ID: "HeapSys", Value: domain.ValuePtr(1.123)}, - {MType: domain.GaugeType, ID: "LastGC", Value: domain.ValuePtr(1.123)}, - {MType: domain.GaugeType, ID: "Lookups", Value: domain.ValuePtr(1.123)}, - {MType: domain.GaugeType, ID: "MCacheInuse", Value: domain.ValuePtr(1.123)}, - {MType: domain.GaugeType, ID: "MCacheSys", Value: domain.ValuePtr(1.123)}, - {MType: domain.GaugeType, ID: "MSpanInuse", Value: domain.ValuePtr(1.123)}, - {MType: domain.GaugeType, ID: "MSpanSys", Value: domain.ValuePtr(1.123)}, - {MType: domain.GaugeType, ID: "Mallocs", Value: domain.ValuePtr(1.123)}, - {MType: domain.GaugeType, ID: "NextGC", Value: domain.ValuePtr(1.123)}, - {MType: domain.GaugeType, ID: "NumForcedGC", Value: domain.ValuePtr(1.123)}, - {MType: domain.GaugeType, ID: "NumGC", Value: domain.ValuePtr(1.123)}, - {MType: domain.GaugeType, ID: "OtherSys", Value: domain.ValuePtr(1.123)}, - {MType: domain.GaugeType, ID: "PauseTotalNs", Value: domain.ValuePtr(1.123)}, - {MType: domain.GaugeType, ID: "StackInuse", Value: domain.ValuePtr(1.123)}, - {MType: domain.GaugeType, ID: "StackSys", Value: domain.ValuePtr(1.123)}, - {MType: domain.GaugeType, ID: "Sys", Value: domain.ValuePtr(1.123)}, - {MType: domain.GaugeType, ID: "TotalAlloc", Value: domain.ValuePtr(1.123)}, -} - -func TestJsonFormatter(t *testing.T) { - - logger := getLogger() - - // Файл не указан - jF := formatter.NewJSON(logger, "") - restored, err := jF.Read() - require.True(t, len(restored) == 0) - require.ErrorIs(t, os.ErrNotExist, err) - - err = jF.Write(nil) - require.ErrorIs(t, os.ErrNotExist, err) - - // Проверяем сохранение и восстановление - tmpDir := os.TempDir() - file, err := os.CreateTemp(tmpDir, "json_formatter_test*") - - require.NoError(t, err) - - defer os.Remove(file.Name()) - - fileName := file.Name() - jF = formatter.NewJSON(logger, fileName) - restored, err = jF.Read() - require.Error(t, err) // EOF - require.True(t, len(restored) == 0) - - err = jF.Write(toWrite) - require.NoError(t, err) // EOF - - restored, err = jF.Read() - require.NoError(t, err) - require.True(t, reflect.DeepEqual(toWrite, restored)) -} diff --git a/internal/server/adapter/http/handler/handler.go b/internal/server/adapter/http/handler/handler.go deleted file mode 100644 index 54d5eb8..0000000 --- a/internal/server/adapter/http/handler/handler.go +++ /dev/null @@ -1,384 +0,0 @@ -package handler - -import ( - "encoding/json" - "errors" - "fmt" - "html/template" - "io" - "net/http" - "strconv" - "strings" - - "github.com/StasMerzlyakov/go-metrics/internal/server/domain" - "github.com/go-chi/chi/v5" - "go.uber.org/zap" -) - -type MetricUseCase interface { - GetAllMetrics() ([]domain.Metrics, error) - GetCounter(name string) (*domain.Metrics, error) - GetGauge(name string) (*domain.Metrics, error) - AddCounter(m *domain.Metrics) error - SetGauge(m *domain.Metrics) error -} - -func NewHTTP(metricUseCase MetricUseCase, log *zap.SugaredLogger, middlewares ...func(http.Handler) http.Handler) http.Handler { - return createHTTPHandler(&httpAdapter{ - metricController: metricUseCase, - logger: log, - }, middlewares...) -} - -func createHTTPHandler(httpAdapter *httpAdapter, middlewares ...func(http.Handler) http.Handler) http.Handler { - r := chi.NewRouter() - - r.Use(middlewares...) - - r.Get("/", httpAdapter.AllMetrics) - - r.Route("/update", func(r chi.Router) { - r.Post("/", httpAdapter.PostMetrics) - r.Post("/gauge/{name}/{value}", httpAdapter.PostGauge) - r.Post("/gauge/{name}", StatusNotFound) - r.Post("/counter/{name}/{value}", httpAdapter.PostCounter) - r.Post("/counter/{name}", StatusNotFound) - r.Post("/{type}/{name}/{value}", StatusNotImplemented) - }) - - r.Route("/value", func(r chi.Router) { - r.Post("/", httpAdapter.ValueMetrics) - r.Get("/gauge/{name}", httpAdapter.GetGauge) - r.Get("/counter/{name}", httpAdapter.GetCounter) - }) - return r -} - -// TODO много повторяющегося кода -type httpAdapter struct { - metricController MetricUseCase - logger *zap.SugaredLogger -} - -func (h *httpAdapter) PostGauge(w http.ResponseWriter, req *http.Request) { - _, _ = io.ReadAll(req.Body) - // Проверка content-type - contentType := req.Header.Get("Content-Type") - if contentType != "" && !strings.HasPrefix(contentType, TextPlain) { - h.logger.Infow("PostGauge", "status", "error", "msg", fmt.Sprintf("unexpected content-type: %v", contentType)) - http.Error(w, "only 'text/plain' supported", http.StatusUnsupportedMediaType) - return - } - - // Извлекаем имя - name := chi.URLParam(req, "name") - if name == "" { - err := errors.New("name is not set") - h.logger.Infow("PostGauge", "status", "error", "msg", "err", err.Error()) - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - - // Проверка допустимости значения параметра - valueStr := chi.URLParam(req, "value") - value, err := ExtractFloat64(valueStr) - if err != nil { - h.logger.Infow("PostGauge", "status", "error", "msg", "err", err.Error()) - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - - metrics := &domain.Metrics{ - ID: name, - MType: domain.GaugeType, - Value: domain.ValuePtr(value), - } - - if err := h.metricController.SetGauge(metrics); err != nil { - h.handlerStorageError("PostGauge", err, w) - return - } - h.logger.Infow("PostGauge", "name", name, "status", "ok") - w.WriteHeader(http.StatusOK) -} - -func (h *httpAdapter) GetGauge(w http.ResponseWriter, req *http.Request) { - w.Header().Set("Content-Type", TextPlain) - // Извлекаем имя - name := chi.URLParam(req, "name") - if name == "" { - err := errors.New("name is not set") - h.logger.Infow("GetGauge", "status", "error", "msg", "err", err.Error()) - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - - m, err := h.metricController.GetGauge(name) - if err != nil { - h.handlerStorageError("GetGauge", err, w) - return - } - - if m == nil { - h.logger.Infow("GetGauge", "status", "error", "msg", fmt.Sprintf("can't find gague by name '%v'", name)) - w.WriteHeader(http.StatusNotFound) - } else { - h.logger.Infow("GetGauge", "name", name, "status", "ok") - w.Write([]byte(fmt.Sprintf("%v", *m.Value))) - } -} - -func (h *httpAdapter) PostCounter(w http.ResponseWriter, req *http.Request) { - _, _ = io.ReadAll(req.Body) - - // Проверка content-type - contentType := req.Header.Get("Content-Type") - if contentType != "" && !strings.HasPrefix(contentType, TextPlain) { - h.logger.Infow("PostCounter", "status", "error", "msg", fmt.Sprintf("unexpected content-type: %v", contentType)) - http.Error(w, "only 'text/plain' supported", http.StatusUnsupportedMediaType) - return - } - - // Извлекаем имя - name := chi.URLParam(req, "name") - if name == "" { - err := errors.New("name is not set") - h.logger.Infow("PostGauge", "status", "error", "msg", "err", err.Error()) - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - - // Проверка допустимости значения - valueStr := chi.URLParam(req, "value") - - value, err := ExtractInt64(valueStr) - if err != nil { - h.logger.Infow("PostCounter", "status", "error", "msg", err.Error()) - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - - metrics := &domain.Metrics{ - ID: name, - MType: domain.CounterType, - Delta: domain.DeltaPtr(value), - } - - if err := h.metricController.AddCounter(metrics); err != nil { - h.handlerStorageError("PostCounter", err, w) - return - } - - h.logger.Infow("PostCounter", "name", name, "status", "ok") -} - -func (h *httpAdapter) GetCounter(w http.ResponseWriter, req *http.Request) { - w.Header().Set("Content-Type", TextPlain) - // Извлекаем имя - name := chi.URLParam(req, "name") - if name == "" { - err := errors.New("name is not set") - h.logger.Infow("GetCounter", "status", "error", "msg", "err", err.Error()) - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - m, err := h.metricController.GetCounter(name) - if err != nil { - h.handlerStorageError("GetCounter", err, w) - return - } - - if m == nil { - h.logger.Infow("GetCounter", "status", "error", "msg", fmt.Sprintf("can't find counter by name '%v'", name)) - w.WriteHeader(http.StatusNotFound) - } else { - h.logger.Infow("GetCounter", "name", name, "status", "ok") - w.Write([]byte(fmt.Sprintf("%v", *m.Delta))) - } -} - -func (h *httpAdapter) AllMetrics(w http.ResponseWriter, request *http.Request) { - metricses, err := h.metricController.GetAllMetrics() - if err != nil { - h.handlerStorageError("AllMetrics", err, w) - return - } - - w.Header().Set("Content-Type", TextHTML) - - allMetricsViewTmpl.Execute(w, metricses) - h.logger.Infow("AllMetrics", "status", "ok") -} - -var allMetricsViewTmpl, _ = template.New("allMetrics").Parse(` - - - - - - - - - {{ range .}} - - - {{if .Delta}}{{else}}{{end}} - {{ end}} -
TypeNameValue
{{ .MType }}{{ .ID }}{{ .Delta }}{{ .Value }}
- - -`) - -func (h *httpAdapter) PostMetrics(w http.ResponseWriter, req *http.Request) { - - if metrics, ok := h.checkRequestBody("PostMetrics", w, req); ok { - var err error - if metrics.MType == "gauge" { - err = h.metricController.SetGauge(metrics) - } - if metrics.MType == "counter" { - err = h.metricController.AddCounter(metrics) - } - - if err != nil { - h.handlerStorageError("PostMetrics", err, w) - return - } - if err := h.sendMetrics("PostMetrics", w, metrics); err == nil { - h.logger.Infow("PostMetrics", "name", metrics.ID, "status", "ok") - } - } -} - -func (h *httpAdapter) ValueMetrics(w http.ResponseWriter, req *http.Request) { - if metrics, ok := h.checkRequestBody("ValueMetrics", w, req); ok { - var err error - var metricsFound *domain.Metrics - - if metrics.MType == "gauge" { - metricsFound, err = h.metricController.GetGauge(metrics.ID) - } - - if metrics.MType == "counter" { - metricsFound, err = h.metricController.GetCounter(metrics.ID) - } - - if err != nil { - h.handlerStorageError("ValueMetrics", err, w) - return - } - - if metricsFound == nil { - h.logger.Infow("ValueMetrics", "status", "error", "msg", - fmt.Sprintf("can't find metrics by ID '%v' and MType '%v'", metrics.ID, metrics.MType)) - w.WriteHeader(http.StatusNotFound) - } else { - if err := h.sendMetrics("ValueMetrics", w, metricsFound); err == nil { - h.logger.Infow("ValueMetrics", "name", metrics.ID, "status", "ok") - } - } - } -} - -func (h *httpAdapter) handlerStorageError(action string, err error, w http.ResponseWriter) { - if errors.Is(err, domain.ErrDataFormat) { - h.logger.Infow(action, "status", "error", "msg", "err", err.Error()) - http.Error(w, err.Error(), http.StatusBadRequest) - return - } else { - h.logger.Infow(action, "status", "error", "msg", "err", err.Error()) - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } -} - -func (h *httpAdapter) checkRequestBody(action string, w http.ResponseWriter, req *http.Request) (*domain.Metrics, bool) { - // Проверка content-type - contentType := req.Header.Get("Content-Type") - if contentType != "" && !strings.HasPrefix(contentType, ApplicationJSON) { - h.logger.Infow(action, "status", "error", "msg", fmt.Sprintf("unexpected content-type: %v", contentType)) - http.Error(w, "only 'text/plain' supported", http.StatusUnsupportedMediaType) - return nil, false - } - - // Декодируем входные данные - var metrics domain.Metrics - if err := json.NewDecoder(req.Body).Decode(&metrics); err != nil { - h.logger.Infow(action, "status", "error", "msg", fmt.Sprintf("json decode error: %v", err)) - http.Error(w, err.Error(), http.StatusBadRequest) - return nil, false - } - - // Проверка типа - if metrics.MType != "counter" && metrics.MType != "gauge" { - errMess := fmt.Sprintf("unexpected MType %v, expected 'counter' and 'gauge'", metrics.MType) - h.logger.Infow(action, "status", "error", "msg", errMess) - http.Error(w, errMess, http.StatusBadRequest) - return nil, false - } - - return &metrics, true -} - -func (h *httpAdapter) sendMetrics(action string, w http.ResponseWriter, metrics *domain.Metrics) error { - w.Header().Set("Content-Type", ApplicationJSON) - resp, err := json.Marshal(metrics) - if err != nil { - h.logger.Infow(action, "status", "error", "msg", err.Error()) - http.Error(w, err.Error(), http.StatusInternalServerError) - return err - } - w.Write(resp) - return nil -} - -func ExtractFloat64(valueStr string) (float64, error) { - value, err := strconv.ParseFloat(valueStr, 64) - if err != nil { - return -1, err - } - return value, nil -} - -func ExtractInt64(valueStr string) (int64, error) { - value, err := strconv.ParseInt(valueStr, 10, 64) - if err != nil { - return -1, err - } - return value, nil -} - -// TODO дублируется с internal/server/server.go -const ( - ApplicationJSON = "application/json" - TextPlain = "text/plain" - TextHTML = "text/html" -) - -func BadRequestHandler(w http.ResponseWriter, req *http.Request) { - http.Error(w, "BadRequest", http.StatusBadRequest) -} - -func StatusMethodNotAllowedHandler(w http.ResponseWriter, req *http.Request) { - http.Error(w, "StatusMethodNotAllowed", http.StatusMethodNotAllowed) -} -func StatusNotImplemented(w http.ResponseWriter, req *http.Request) { - http.Error(w, "StatusMethodNotAllowed", http.StatusNotImplemented) -} - -func StatusNotFound(w http.ResponseWriter, req *http.Request) { - http.Error(w, "StatusNotFound", http.StatusNotFound) -} - -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) -} diff --git a/internal/server/adapter/http/handler/handler_test.go b/internal/server/adapter/http/handler/handler_test.go deleted file mode 100644 index 75051ba..0000000 --- a/internal/server/adapter/http/handler/handler_test.go +++ /dev/null @@ -1,501 +0,0 @@ -package handler_test - -import ( - "encoding/json" - "fmt" - "net/http" - "net/http/httptest" - "testing" - - "github.com/StasMerzlyakov/go-metrics/internal/server/adapter/http/handler" - "github.com/StasMerzlyakov/go-metrics/internal/server/domain" - "github.com/go-resty/resty/v2" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "go.uber.org/zap" -) - -func TestExtractFloat64(t *testing.T) { - type extractFloat64Result struct { - value float64 - isSuccessExpected bool - } - tests := []struct { - name string - input string - result extractFloat64Result - }{ - { - "good value", - "123.5", - extractFloat64Result{ - 123.5, - true, - }, - }, - { - "good value 2", - "123", - extractFloat64Result{ - 123, - true, - }, - }, - { - "bad value", - "123.F", - extractFloat64Result{ - -1, - false, - }, - }, - { - "good value", - "1.8070544e+07", - extractFloat64Result{ - 18070544, - true, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - value, err := handler.ExtractFloat64(tt.input) - assert.Equal(t, tt.result.value, value) - assert.Equal(t, tt.result.isSuccessExpected, err == nil) - }) - } -} - -func TestExtractInt64(t *testing.T) { - - type extractInt64Result struct { - value int64 - isSuccessExpected bool - } - - tests := []struct { - name string - input string - result extractInt64Result - }{ - { - "good value", - "123", - extractInt64Result{ - 123, - true, - }, - }, - { - "bad value", - "123F", - extractInt64Result{ - -1, - false, - }, - }, - { - "bad value 2", - "123.5", - extractInt64Result{ - -1, - false, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - value, err := handler.ExtractInt64(tt.input) - assert.Equal(t, tt.result.value, value) - assert.Equal(t, tt.result.isSuccessExpected, err == nil) - }) - } -} - -type mockMetricUse struct { - counterVal int64 - gaugeVal float64 -} - -func (*mockMetricUse) SetAllMetrics(in []domain.Metrics) error { - return nil -} -func (*mockMetricUse) GetAllMetrics() ([]domain.Metrics, error) { - return []domain.Metrics{ - { - ID: "PoolCount", - MType: domain.CounterType, - Delta: domain.DeltaPtr(1), - }, - { - ID: "RandomValue", - MType: domain.GaugeType, - Value: domain.ValuePtr(1.1), - }, - }, nil -} -func (m *mockMetricUse) GetCounter(name string) (*domain.Metrics, error) { - return &domain.Metrics{ - ID: name, - MType: domain.CounterType, - Delta: domain.DeltaPtr(m.counterVal), - }, nil -} -func (m *mockMetricUse) GetGauge(name string) (*domain.Metrics, error) { - return &domain.Metrics{ - ID: name, - MType: domain.GaugeType, - Value: domain.ValuePtr(m.gaugeVal), - }, nil -} -func (*mockMetricUse) AddCounter(m *domain.Metrics) error { - return nil -} -func (*mockMetricUse) SetGauge(m *domain.Metrics) error { - return nil -} - -func TestPostUpdate(t *testing.T) { - logger, err := zap.NewDevelopment() - if err != nil { - // вызываем панику, если ошибка - panic("cannot initialize zap") - } - defer logger.Sync() - - log := logger.Sugar() - - hHandler := handler.NewHTTP(&mockMetricUse{}, log) - - srv := httptest.NewServer(hHandler) - defer srv.Close() - - req := resty.New().R() - req.Method = http.MethodPost - - req.URL = srv.URL + "/update/" - req.Header.Add("Content-Type", handler.ApplicationJSON) - _, err = req.Send() - require.Nil(t, err) - - req.URL = srv.URL + "/value/" - req.Header.Add("Content-Type", handler.ApplicationJSON) - _, err = req.Send() - assert.Nil(t, err) -} - -func TestCounterValueHandler(t *testing.T) { - testValue := 123 - testValueStr := fmt.Sprintf("%v", testValue) - - logger, err := zap.NewDevelopment() - if err != nil { - // вызываем панику, если ошибка - panic("cannot initialize zap") - } - defer logger.Sync() - - log := logger.Sugar() - - hHandler := handler.NewHTTP(&mockMetricUse{ - counterVal: int64(testValue), - }, log) - - srv := httptest.NewServer(hHandler) - defer srv.Close() - - req := resty.New().R() - req.Method = http.MethodPost - - req.URL = srv.URL + "/update/counter/TestCounter/" + testValueStr - req.Header.Add("Content-Type", handler.TextPlain) - _, err = req.Send() - require.Nil(t, err) - - req = resty.New().R() - req.Method = http.MethodGet - req.URL = srv.URL + "/value/counter/TestCounter" - req.Header.Add("Content-Type", handler.TextPlain) - - resp, err := req.Send() - require.Nil(t, err) - respBody := string(resp.Body()) - require.Equal(t, testValueStr, respBody) - assert.Equal(t, http.StatusOK, resp.StatusCode()) -} - -func TestGaugeValueHandler(t *testing.T) { - - logger, err := zap.NewDevelopment() - if err != nil { - // вызываем панику, если ошибка - panic("cannot initialize zap") - } - defer logger.Sync() - - log := logger.Sugar() - - testValue := 234.123 - testValueStr := fmt.Sprintf("%v", testValue) - - hHandler := handler.NewHTTP(&mockMetricUse{ - gaugeVal: float64(testValue), - }, log) - - srv := httptest.NewServer(hHandler) - defer srv.Close() - - req := resty.New().R() - req.Method = http.MethodPost - req.URL = srv.URL + "/update/gauge/TestCounter/" + testValueStr - req.Header.Add("Content-Type", handler.TextPlain) - _, err = req.Send() - require.Nil(t, err) - - req = resty.New().R() - req.Method = http.MethodGet - req.URL = srv.URL + "/value/gauge/TestCounter" - req.Header.Add("Content-Type", handler.TextPlain) - resp, err := req.Send() - require.Nil(t, err) - respBody := string(resp.Body()) - require.Equal(t, testValueStr, respBody) // Не попасть бы на потерю точности string -> float64 -> string - assert.Equal(t, http.StatusOK, resp.StatusCode()) -} - -func TestGetAll(t *testing.T) { - logger, err := zap.NewDevelopment() - if err != nil { - // вызываем панику, если ошибка - panic("cannot initialize zap") - } - defer logger.Sync() - - log := logger.Sugar() - - hHandler := handler.NewHTTP(&mockMetricUse{}, log) - - srv := httptest.NewServer(hHandler) - defer srv.Close() - - req := resty.New().R() - req.Method = http.MethodGet - - req.URL = srv.URL - - resp, err := req.Send() - require.Nil(t, err) - - assert.Equal(t, http.StatusOK, resp.StatusCode()) -} - -type mockMetricUse2 struct { - metrics *domain.Metrics -} - -func (*mockMetricUse2) SetAllMetrics(in []domain.Metrics) error { - return nil -} -func (m2 *mockMetricUse2) GetAllMetrics() ([]domain.Metrics, error) { - var out []domain.Metrics - if m2.metrics != nil { - out = append(out, *m2.metrics) - } - return out, nil - -} -func (m2 *mockMetricUse2) GetCounter(name string) (*domain.Metrics, error) { - if m2.metrics != nil { - if m2.metrics.ID == name && m2.metrics.MType == domain.CounterType { - return m2.metrics, nil - } - } - return nil, nil -} -func (m2 *mockMetricUse2) GetGauge(name string) (*domain.Metrics, error) { - if m2.metrics != nil { - if m2.metrics.ID == name && m2.metrics.MType == domain.GaugeType { - return m2.metrics, nil - } - } - return nil, nil -} -func (m2 *mockMetricUse2) AddCounter(m *domain.Metrics) error { - if m.MType == domain.CounterType { - m2.metrics = m - } else { - return fmt.Errorf("unexpected input") - } - return nil -} -func (m2 *mockMetricUse2) SetGauge(m *domain.Metrics) error { - if m.MType == domain.GaugeType { - m2.metrics = m - } else { - return fmt.Errorf("unexpected input") - } - return nil -} - -func TestCounterPostAndValue(t *testing.T) { - logger, err := zap.NewDevelopment() - if err != nil { - // вызываем панику, если ошибка - panic("cannot initialize zap") - } - defer logger.Sync() - - log := logger.Sugar() - - hHandler := handler.NewHTTP(&mockMetricUse2{}, log) - - srv := httptest.NewServer(hHandler) - defer srv.Close() - - // Метрик нет - req := resty.New().R() - req.Method = http.MethodGet - req.URL = srv.URL - - resp, err := req.Send() - require.Nil(t, err) - assert.Equal(t, http.StatusOK, resp.StatusCode()) - - req.Method = http.MethodPost - req.Header.Add("Content-Type", handler.ApplicationJSON) - req.URL = srv.URL + "/value" - metrics := domain.Metrics{ - ID: "PoolCount", - MType: domain.CounterType, - } - - req.SetBody(metrics) - - resp, err = req.Send() - require.Nil(t, err) - require.Equal(t, resp.StatusCode(), http.StatusNotFound) - - // Добавляем метрику - - req.Header.Add("Content-Type", handler.ApplicationJSON) - req.URL = srv.URL + "/update" - - metricsReq := domain.Metrics{ - ID: "PoolCount", - MType: domain.CounterType, - Delta: domain.DeltaPtr(2), - } - - req.SetBody(metricsReq) - - resp, err = req.Send() - require.Nil(t, err) - require.Equal(t, http.StatusOK, resp.StatusCode()) - - // Проверяем метрику - req.Method = http.MethodPost - req.Header.Add("Content-Type", handler.ApplicationJSON) - req.URL = srv.URL + "/value" - metrics = domain.Metrics{ - ID: metricsReq.ID, - MType: metricsReq.MType, - } - - req.SetBody(metrics) - - resp, err = req.Send() - require.Nil(t, err) - require.Equal(t, http.StatusOK, resp.StatusCode()) - - var respMetrics domain.Metrics - err = json.Unmarshal(resp.Body(), &respMetrics) - require.Nil(t, err) - - require.Equal(t, metricsReq.ID, respMetrics.ID) - require.Equal(t, metricsReq.MType, respMetrics.MType) - require.NotNil(t, respMetrics.Delta) - require.Nil(t, respMetrics.Value) - require.Equal(t, *metricsReq.Delta, *respMetrics.Delta) -} - -func TestGaguePostAndValue(t *testing.T) { - logger, err := zap.NewDevelopment() - if err != nil { - // вызываем панику, если ошибка - panic("cannot initialize zap") - } - defer logger.Sync() - - log := logger.Sugar() - - hHandler := handler.NewHTTP(&mockMetricUse2{}, log) - - srv := httptest.NewServer(hHandler) - defer srv.Close() - - // Метрик нет - req := resty.New().R() - req.Method = http.MethodGet - req.URL = srv.URL - - resp, err := req.Send() - require.Nil(t, err) - assert.Equal(t, http.StatusOK, resp.StatusCode()) - - req.Method = http.MethodPost - req.Header.Add("Content-Type", handler.ApplicationJSON) - req.URL = srv.URL + "/value" - metrics := domain.Metrics{ - ID: "RandomValue", - MType: domain.GaugeType, - } - - req.SetBody(metrics) - - resp, err = req.Send() - require.Nil(t, err) - require.Equal(t, resp.StatusCode(), http.StatusNotFound) - - // Добавляем метрику - - req.Header.Add("Content-Type", handler.ApplicationJSON) - req.URL = srv.URL + "/update" - - metricsReq := domain.Metrics{ - ID: "RandomValue", - MType: domain.GaugeType, - Value: domain.ValuePtr(2), - } - - req.SetBody(metricsReq) - - resp, err = req.Send() - require.Nil(t, err) - require.Equal(t, http.StatusOK, resp.StatusCode()) - - // Проверяем метрику - req.Method = http.MethodPost - req.Header.Add("Content-Type", handler.ApplicationJSON) - req.URL = srv.URL + "/value" - metrics = domain.Metrics{ - ID: metricsReq.ID, - MType: metricsReq.MType, - } - - req.SetBody(metrics) - - resp, err = req.Send() - require.Nil(t, err) - require.Equal(t, http.StatusOK, resp.StatusCode()) - - var respMetrics domain.Metrics - err = json.Unmarshal(resp.Body(), &respMetrics) - require.Nil(t, err) - - require.Equal(t, metricsReq.ID, respMetrics.ID) - require.Equal(t, metricsReq.MType, respMetrics.MType) - require.Nil(t, respMetrics.Delta) - require.NotNil(t, respMetrics.Value) - require.Equal(t, *metricsReq.Value, *respMetrics.Value) -} diff --git a/internal/server/adapter/http/middleware/common.go b/internal/server/adapter/http/middleware/common.go deleted file mode 100644 index 13107ca..0000000 --- a/internal/server/adapter/http/middleware/common.go +++ /dev/null @@ -1,12 +0,0 @@ -package middleware - -import "net/http" - -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/adapter/http/middleware/compress/compress_gzip_buffer_response_mw.go b/internal/server/adapter/http/middleware/compress/compress_gzip_buffer_response_mw.go deleted file mode 100644 index b780eee..0000000 --- a/internal/server/adapter/http/middleware/compress/compress_gzip_buffer_response_mw.go +++ /dev/null @@ -1,62 +0,0 @@ -package compress - -import ( - "bytes" - "compress/gzip" - "fmt" - "io" - "net/http" - "strings" - - "github.com/StasMerzlyakov/go-metrics/internal/server/adapter/http/middleware" - "go.uber.org/zap" -) - -type bufferWriter struct { - http.ResponseWriter - Writer io.Writer -} - -func (w bufferWriter) Write(b []byte) (int, error) { - return w.Writer.Write(b) -} - -// Вариант мидлы через буфер. Можно оценить ответ. -func NewCompressGZIPBufferResponseMW(log *zap.SugaredLogger) middleware.Middleware { - return func(next http.Handler) http.Handler { - cmprFn := func(w http.ResponseWriter, r *http.Request) { - acceptEncodingReqHeader := r.Header.Get("Accept-Encoding") - if !strings.Contains(acceptEncodingReqHeader, "gzip") { - next.ServeHTTP(w, r) - } else { - var buff bytes.Buffer // Пишем в буфер - next.ServeHTTP(bufferWriter{ResponseWriter: w, Writer: &buff}, r) - - contentTypeRespHeader := w.Header().Get("Content-Type") - if strings.Contains(contentTypeRespHeader, "application/json") || - strings.Contains(contentTypeRespHeader, "text/html") { - w.Header().Add("Content-Encoding", "gzip") // добавляем заголовок - gz, err := gzip.NewWriterLevel(w, gzip.BestSpeed) - if err != nil { - log.Errorln("writeGZIP", "desc", "can't initialize gzip", "err", err.Error()) - http.Error(w, "can't initialize gzip", http.StatusInternalServerError) - return - } - defer gz.Close() - _, err = gz.Write(buff.Bytes()) - if err != nil { - log.Errorln("writeGZIP", "status", "err", "msg", fmt.Sprintf("can't initialize gzip: %v", err.Error())) - http.Error(w, "can't write gzip", http.StatusInternalServerError) - return - } - - log.Infow("writeGZIP", "header", "Content-Type", "value", contentTypeRespHeader, "msg", "response will be zipped") - } else { - w.Write(buff.Bytes()) - } - - } - } - return http.HandlerFunc(cmprFn) - } -} diff --git a/internal/server/adapter/http/middleware/compress/compress_gzip_buffer_response_mw_test.go b/internal/server/adapter/http/middleware/compress/compress_gzip_buffer_response_mw_test.go deleted file mode 100644 index 26e07f2..0000000 --- a/internal/server/adapter/http/middleware/compress/compress_gzip_buffer_response_mw_test.go +++ /dev/null @@ -1,119 +0,0 @@ -package compress_test - -import ( - "net/http" - "net/http/httptest" - "strings" - "testing" - - "github.com/StasMerzlyakov/go-metrics/internal/server/adapter/http/middleware" - "github.com/StasMerzlyakov/go-metrics/internal/server/adapter/http/middleware/compress" - "github.com/go-resty/resty/v2" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "go.uber.org/zap" -) - -func TestCompressGZIPBufferResponseMW(t *testing.T) { - - logger, err := zap.NewDevelopment() - require.NoError(t, err) - defer logger.Sync() - mux := http.NewServeMux() - - suga := logger.Sugar() - - compressMW := compress.NewCompressGZIPBufferResponseMW(suga) - - mux.Handle("/json", middleware.Conveyor(defaultJSONHandle{}, compressMW)) - mux.Handle("/html", middleware.Conveyor(defaultHTMLHandle{}, compressMW)) - mux.Handle("/text", middleware.Conveyor(defaultTextHandle{}, compressMW)) - - srv := httptest.NewServer(mux) - defer srv.Close() - - testCases := []struct { - Name string - Path string - AcceptEncoding string - ContentEncoding bool - }{ - { - "json_gzip", - "/json", - "gzip", - true, - }, - { - "json", - "/json", - "", - false, - }, - { - "json_deflate", - "/json", - "deflate", - false, - }, - { - "html_gzip", - "/html", - "gzip", - true, - }, - { - "html", - "/html", - "", - false, - }, - { - "html_deflate", - "/html", - "deflate", - false, - }, - { - "text_gzip", - "/text", - "gzip", - false, - }, - { - "text", - "/text", - "", - false, - }, - { - "text_deflate", - "/text", - "deflate", - false, - }, - } - - req := resty.New().R() - req.Method = http.MethodPost - - for _, tt := range testCases { - t.Run(tt.Name, func(t *testing.T) { - req.URL = srv.URL + tt.Path - if tt.AcceptEncoding == "" { - req.Header.Del("Accept-Encoding") - } else { - req.Header.Set("Accept-Encoding", tt.AcceptEncoding) - } - resp, err := req.Send() - require.Nil(t, err) - require.Equal(t, http.StatusOK, resp.StatusCode()) - if tt.ContentEncoding { - assert.True(t, strings.Contains(resp.Header().Get("Content-Encoding"), "gzip")) - } else { - assert.False(t, strings.Contains(resp.Header().Get("Content-Encoding"), "gzip")) - } - }) - } - -} diff --git a/internal/server/adapter/http/middleware/compress/compress_gzip_response_mw.go b/internal/server/adapter/http/middleware/compress/compress_gzip_response_mw.go deleted file mode 100644 index b88e7ba..0000000 --- a/internal/server/adapter/http/middleware/compress/compress_gzip_response_mw.go +++ /dev/null @@ -1,57 +0,0 @@ -package compress - -import ( - "io" - "net/http" - "strings" - - "github.com/StasMerzlyakov/go-metrics/internal/server/adapter/http/middleware" - gPool "github.com/ungerik/go-pool" - "go.uber.org/zap" -) - -// Вариант мидлы без дополнительного буфера при обработке ответа. -func NewCompressGZIPResponseMW(log *zap.SugaredLogger) middleware.Middleware { - return func(next http.Handler) http.Handler { - cmprFn := func(w http.ResponseWriter, r *http.Request) { - acceptEncodingReqHeader := r.Header.Get("Accept-Encoding") - if !strings.Contains(acceptEncodingReqHeader, "gzip") { - next.ServeHTTP(w, r) - } else { - gz := gPool.Gzip.GetWriter(w) - defer gPool.Gzip.PutWriter(gz) - log.Infow("testAcceptEncoding", "AcceptEncoding", "exists", "value", acceptEncodingReqHeader) - next.ServeHTTP(gzipWriter{log: log, ResponseWriter: w, Writer: gz, IsGZIPHeaderRecorded: false, IsContentTypeVerified: false}, r) - } - } - return http.HandlerFunc(cmprFn) - } -} - -type gzipWriter struct { - log *zap.SugaredLogger - http.ResponseWriter - Writer io.Writer - IsGZIPHeaderRecorded bool - IsContentTypeVerified bool -} - -func (w gzipWriter) Write(b []byte) (int, error) { - // С помощью двух флагов определяем нужно ли записать заголовок и сжать - if !w.IsContentTypeVerified { - w.IsContentTypeVerified = true - contentTypeRespHeader := w.Header().Get("Content-Type") - if strings.Contains(contentTypeRespHeader, "application/json") || - strings.Contains(contentTypeRespHeader, "text/html") { - w.IsGZIPHeaderRecorded = true - w.Header().Add("Content-Encoding", "gzip") // добавляем заголовок - w.log.Infow("gzipResponse", "contentType", contentTypeRespHeader) - } - } - - if w.IsGZIPHeaderRecorded { - return w.Writer.Write(b) // пишем в gzip - } else { - return w.ResponseWriter.Write(b) // пишем в обычный поток - } -} diff --git a/internal/server/adapter/http/middleware/compress/compress_gzip_response_mw_test.go b/internal/server/adapter/http/middleware/compress/compress_gzip_response_mw_test.go deleted file mode 100644 index b4314cf..0000000 --- a/internal/server/adapter/http/middleware/compress/compress_gzip_response_mw_test.go +++ /dev/null @@ -1,119 +0,0 @@ -package compress_test - -import ( - "net/http" - "net/http/httptest" - "strings" - "testing" - - "github.com/StasMerzlyakov/go-metrics/internal/server/adapter/http/middleware" - "github.com/StasMerzlyakov/go-metrics/internal/server/adapter/http/middleware/compress" - "github.com/go-resty/resty/v2" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "go.uber.org/zap" -) - -func TestCompressGZIPResponseMW(t *testing.T) { - - logger, err := zap.NewDevelopment() - require.NoError(t, err) - defer logger.Sync() - mux := http.NewServeMux() - - suga := logger.Sugar() - - compressMW := compress.NewCompressGZIPResponseMW(suga) - - mux.Handle("/json", middleware.Conveyor(defaultJSONHandle{}, compressMW)) - mux.Handle("/html", middleware.Conveyor(defaultHTMLHandle{}, compressMW)) - mux.Handle("/text", middleware.Conveyor(defaultTextHandle{}, compressMW)) - - srv := httptest.NewServer(mux) - defer srv.Close() - - testCases := []struct { - Name string - Path string - AcceptEncoding string - ContentEncoding bool - }{ - { - "json_gzip", - "/json", - "gzip", - true, - }, - { - "json", - "/json", - "", - false, - }, - { - "json_deflate", - "/json", - "deflate", - false, - }, - { - "html_gzip", - "/html", - "gzip", - true, - }, - { - "html", - "/html", - "", - false, - }, - { - "html_deflate", - "/html", - "deflate", - false, - }, - { - "text_gzip", - "/text", - "gzip", - false, - }, - { - "text", - "/text", - "", - false, - }, - { - "text_deflate", - "/text", - "deflate", - false, - }, - } - - req := resty.New().R() - req.Method = http.MethodPost - - for _, tt := range testCases { - t.Run(tt.Name, func(t *testing.T) { - req.URL = srv.URL + tt.Path - if tt.AcceptEncoding == "" { - req.Header.Del("Accept-Encoding") - } else { - req.Header.Set("Accept-Encoding", tt.AcceptEncoding) - } - resp, err := req.Send() - require.Nil(t, err) - require.Equal(t, http.StatusOK, resp.StatusCode()) - if tt.ContentEncoding { - assert.True(t, strings.Contains(resp.Header().Get("Content-Encoding"), "gzip")) - } else { - assert.False(t, strings.Contains(resp.Header().Get("Content-Encoding"), "gzip")) - } - }) - } - -} diff --git a/internal/server/adapter/http/middleware/compress/compress_utils_test.go b/internal/server/adapter/http/middleware/compress/compress_utils_test.go deleted file mode 100644 index 3f8c46b..0000000 --- a/internal/server/adapter/http/middleware/compress/compress_utils_test.go +++ /dev/null @@ -1,46 +0,0 @@ -package compress_test - -import ( - "bytes" - "io" - "net/http" - "strings" -) - -type checkBodyHandler struct { - expected []byte -} - -func (ch *checkBodyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - body, err := io.ReadAll(r.Body) - - if err != nil && err != io.EOF { - http.Error(w, "read body err", http.StatusInternalServerError) - } - - if !bytes.Equal(ch.expected, body) { - http.Error(w, "unexpected body err", http.StatusBadRequest) - } -} - -type defaultHTMLHandle struct{} - -func (defaultHTMLHandle) ServeHTTP(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "text/html") - io.WriteString(w, ""+strings.Repeat("Hello, world
", 20)+"") -} - -type defaultTextHandle struct{} - -func (defaultTextHandle) ServeHTTP(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "text/plain") - io.WriteString(w, strings.Repeat("Hello, world\n", 20)) - -} - -type defaultJSONHandle struct{} - -func (defaultJSONHandle) ServeHTTP(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - io.WriteString(w, "{ "+strings.Repeat(`"msg":"Hello, world",`, 19)+`"msg":"Hello, world"`+"}") -} diff --git a/internal/server/adapter/http/middleware/compress/uncompress_gzip_request_mw.go b/internal/server/adapter/http/middleware/compress/uncompress_gzip_request_mw.go deleted file mode 100644 index 72f68b4..0000000 --- a/internal/server/adapter/http/middleware/compress/uncompress_gzip_request_mw.go +++ /dev/null @@ -1,41 +0,0 @@ -package compress - -import ( - "compress/gzip" - "io" - "net/http" - "strings" - - "github.com/StasMerzlyakov/go-metrics/internal/server/adapter/http/middleware" - "go.uber.org/zap" -) - -type gzipreadCloser struct { - *gzip.Reader - io.Closer -} - -func (gz gzipreadCloser) Close() error { - return gz.Closer.Close() -} - -func NewUncompressGZIPRequestMW(log *zap.SugaredLogger) middleware.Middleware { - - return func(next http.Handler) http.Handler { - uncmprFn := func(w http.ResponseWriter, r *http.Request) { - contentEncodingHeader := r.Header.Get("Content-Encoding") - if strings.Contains(contentEncodingHeader, "gzip") { - zr, err := gzip.NewReader(r.Body) - if err != nil { - log.Infow("uncompress request error:", err.Error()) - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - r.Body = gzipreadCloser{zr, r.Body} - log.Infow("readGZIP", "Content-Encoding", r.Header.Get("Content-Encoding"), "msq", "the request will be unzip") - } - next.ServeHTTP(w, r) - } - return http.HandlerFunc(uncmprFn) - } -} diff --git a/internal/server/adapter/http/middleware/compress/uncompress_gzip_request_mw_test.go b/internal/server/adapter/http/middleware/compress/uncompress_gzip_request_mw_test.go deleted file mode 100644 index 4c108d1..0000000 --- a/internal/server/adapter/http/middleware/compress/uncompress_gzip_request_mw_test.go +++ /dev/null @@ -1,73 +0,0 @@ -package compress_test - -import ( - "bytes" - "compress/gzip" - "net/http" - "net/http/httptest" - "testing" - - "github.com/StasMerzlyakov/go-metrics/internal/server/adapter/http/middleware" - "github.com/StasMerzlyakov/go-metrics/internal/server/adapter/http/middleware/compress" - "github.com/go-resty/resty/v2" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "go.uber.org/zap" -) - -func TestUncompressGZIPRequestMW(t *testing.T) { - - logger, err := zap.NewDevelopment() - require.NoError(t, err) - defer logger.Sync() - - suga := logger.Sugar() - - content := []byte("Hello World") - - handler := checkBodyHandler{ - expected: content, - } - - uncompressMW := compress.NewUncompressGZIPRequestMW(suga) - - srv := httptest.NewServer(middleware.Conveyor(&handler, uncompressMW)) - defer srv.Close() - - testCases := []struct { - Name string - Compress bool - }{ - { - Name: "compressed", - Compress: true, - }, - - { - Name: "uncompressed", - Compress: false, - }, - } - - for _, tt := range testCases { - t.Run(tt.Name, func(t *testing.T) { - req := resty.New().R(). - SetHeader("Content-Type", "text/plain; charset=UTF-8") - if tt.Compress { - req.SetHeader("Content-Encoding", "gzip") - var buff bytes.Buffer - gz, err := gzip.NewWriterLevel(&buff, gzip.BestCompression) - require.NoError(t, err) - gz.Write(content) - gz.Close() - req.SetBody(buff.Bytes()) - } else { - req.SetBody(content) - } - - resp, err := req.Post(srv.URL) - require.NoError(t, err) - assert.Equal(t, http.StatusOK, resp.StatusCode()) - }) - } -} diff --git a/internal/server/adapter/http/middleware/logging/logging_request_info_mw.go b/internal/server/adapter/http/middleware/logging/logging_request_info_mw.go deleted file mode 100644 index 8dc5b7e..0000000 --- a/internal/server/adapter/http/middleware/logging/logging_request_info_mw.go +++ /dev/null @@ -1,30 +0,0 @@ -package logging - -import ( - "net/http" - "time" - - "github.com/StasMerzlyakov/go-metrics/internal/server/adapter/http/middleware" - "go.uber.org/zap" -) - -func NewLoggingRequestMW(log *zap.SugaredLogger) middleware.Middleware { - return func(next http.Handler) http.Handler { - logReqFn := func(w http.ResponseWriter, req *http.Request) { - start := time.Now() - uri := req.RequestURI - method := req.Method - - next.ServeHTTP(w, req) - - duration := time.Since(start) - - log.Infow("requestStatus", - "uri", uri, - "method", method, - "duration", duration, - ) - } - return http.HandlerFunc(logReqFn) - } -} diff --git a/internal/server/adapter/http/middleware/logging/logging_response_info_mw.go b/internal/server/adapter/http/middleware/logging/logging_response_info_mw.go deleted file mode 100644 index d8db404..0000000 --- a/internal/server/adapter/http/middleware/logging/logging_response_info_mw.go +++ /dev/null @@ -1,53 +0,0 @@ -package logging - -import ( - "net/http" - - "github.com/StasMerzlyakov/go-metrics/internal/server/adapter/http/middleware" - "go.uber.org/zap" -) - -type responseData struct { - status int - size int -} - -type loggingResponseWriter struct { - http.ResponseWriter - responseData *responseData -} - -var _ http.ResponseWriter = (*loggingResponseWriter)(nil) - -func (lw *loggingResponseWriter) Header() http.Header { - return lw.ResponseWriter.Header() -} - -func (lw *loggingResponseWriter) Write(data []byte) (int, error) { - size, err := lw.ResponseWriter.Write(data) - lw.responseData.size += size - return size, err -} - -func (lw *loggingResponseWriter) WriteHeader(statusCode int) { - lw.ResponseWriter.WriteHeader(statusCode) - lw.responseData.status = statusCode -} - -func NewLoggingResponseMW(log *zap.SugaredLogger) middleware.Middleware { - return func(next http.Handler) http.Handler { - lrw := func(w http.ResponseWriter, r *http.Request) { - lw := &loggingResponseWriter{ - responseData: &responseData{ - status: 0, - size: 0, - }, - ResponseWriter: w, - } - - next.ServeHTTP(lw, r) - log.Infow("requestResult", "statusCode", lw.responseData.status, "size", lw.responseData.size) - } - return http.HandlerFunc(lrw) - } -} diff --git a/internal/server/configuration.go b/internal/server/configuration.go new file mode 100644 index 0000000..7d2c118 --- /dev/null +++ b/internal/server/configuration.go @@ -0,0 +1,41 @@ +package server + +import ( + "flag" + "fmt" + "os" +) + +type Configuration struct { + url string +} + +func (c *Configuration) String() string { + return c.url +} + +func (c *Configuration) Set(s string) error { + c.url = s + return nil +} + +var _ flag.Value = (*Configuration)(nil) + +func LoadConfig() (*Configuration, error) { + srvConf := &Configuration{} + srvConf.Set(":8080") // Значение по-умолчанию + + flag.Var(srvConf, "a", "serverAddress") + flag.Usage = func() { + fmt.Fprintf(flag.CommandLine.Output(), "Usage of %s:\n", os.Args[0]) + flag.PrintDefaults() + } + flag.Parse() + + addr, isExists := os.LookupEnv("ADDRESS") + if isExists { + srvConf.Set(addr) + } + + return srvConf, nil +} diff --git a/internal/server/controller.go b/internal/server/controller.go new file mode 100644 index 0000000..66ea8cf --- /dev/null +++ b/internal/server/controller.go @@ -0,0 +1,76 @@ +package server + +import ( + "fmt" + + "github.com/StasMerzlyakov/go-metrics/internal/storage" +) + +type MetricController interface { + GetAllMetrics() MetricModel + GetCounter(name string) (int64, bool) + GetGaguge(name string) (float64, bool) + AddCounter(name string, value int64) + SetGauge(name string, value float64) +} + +func NewMetricController(counterStorage storage.MetricsStorage[int64], + gaugeStorage storage.MetricsStorage[float64]) MetricController { + return &metricController{ + counterStorage: counterStorage, + gaugeStorage: gaugeStorage, + } +} + +type MetricsData struct { + Type string + Name string + Value string +} + +type MetricModel struct { + Items []MetricsData +} + +type metricController struct { + counterStorage storage.MetricsStorage[int64] + gaugeStorage storage.MetricsStorage[float64] +} + +func (mc *metricController) GetAllMetrics() MetricModel { + items := MetricModel{} + for _, k := range mc.counterStorage.Keys() { + v, _ := mc.counterStorage.Get(k) + items.Items = append(items.Items, MetricsData{ + "counter", + k, + fmt.Sprintf("%v", v), + }) + } + + for _, k := range mc.gaugeStorage.Keys() { + v, _ := mc.gaugeStorage.Get(k) + items.Items = append(items.Items, MetricsData{ + "counter", + k, + fmt.Sprintf("%v", v), + }) + } + return items +} + +func (mc *metricController) GetCounter(name string) (int64, bool) { + return mc.counterStorage.Get(name) +} + +func (mc *metricController) GetGaguge(name string) (float64, bool) { + return mc.gaugeStorage.Get(name) +} + +func (mc *metricController) AddCounter(name string, value int64) { + mc.counterStorage.Add(name, value) +} + +func (mc *metricController) SetGauge(name string, value float64) { + mc.gaugeStorage.Set(name, value) +} diff --git a/internal/server/domain/common_types.go b/internal/server/domain/common_types.go deleted file mode 100644 index c744987..0000000 --- a/internal/server/domain/common_types.go +++ /dev/null @@ -1,5 +0,0 @@ -package domain - -type ChangeListener interface { - Refresh(metrics *Metrics) error -} diff --git a/internal/server/domain/entities.go b/internal/server/domain/entities.go deleted file mode 100644 index 50621e6..0000000 --- a/internal/server/domain/entities.go +++ /dev/null @@ -1,15 +0,0 @@ -package domain - -type Metrics struct { - ID string `json:"id"` // имя метрики - MType MetricType `json:"type"` // параметр, принимающий значение gauge или counter - Delta *int64 `json:"delta,omitempty"` // значение метрики в случае передачи counter - Value *float64 `json:"value,omitempty"` // значение метрики в случае передачи gauge -} - -type MetricType string - -const ( - GaugeType MetricType = "gauge" - CounterType MetricType = "counter" -) diff --git a/internal/server/domain/errors.go b/internal/server/domain/errors.go deleted file mode 100644 index ed8de99..0000000 --- a/internal/server/domain/errors.go +++ /dev/null @@ -1,8 +0,0 @@ -package domain - -import "errors" - -var ( - ErrServerInternal = errors.New("InternalError") // Ошибка на сервере - ErrDataFormat = errors.New("DataFormatError") // Ошибка в данных -) diff --git a/internal/server/domain/usefull.go b/internal/server/domain/usefull.go deleted file mode 100644 index 8188519..0000000 --- a/internal/server/domain/usefull.go +++ /dev/null @@ -1,9 +0,0 @@ -package domain - -func DeltaPtr(v int64) *int64 { - return &v -} - -func ValuePtr(v float64) *float64 { - return &v -} diff --git a/internal/server/handlers.go b/internal/server/handlers.go new file mode 100644 index 0000000..92f354c --- /dev/null +++ b/internal/server/handlers.go @@ -0,0 +1,120 @@ +package server + +import ( + "fmt" + "html/template" + "io" + "net/http" + + "github.com/StasMerzlyakov/go-metrics/internal" + "github.com/go-chi/chi/v5" +) + +type BusinessHandler interface { + PostGauge(w http.ResponseWriter, req *http.Request) + GetGauge(w http.ResponseWriter, req *http.Request) + PostCounter(w http.ResponseWriter, req *http.Request) + GetCounter(w http.ResponseWriter, req *http.Request) + AllMetrics(w http.ResponseWriter, request *http.Request) +} + +func NewBusinessHandler(metricController MetricController) BusinessHandler { + return &handler{ + metricController: metricController, + } +} + +type handler struct { + metricController MetricController +} + +func (h *handler) PostGauge(w http.ResponseWriter, req *http.Request) { + _, _ = io.ReadAll(req.Body) + name := chi.URLParam(req, "name") + valueStr := chi.URLParam(req, "value") + value, err := ExtractFloat64(valueStr) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + h.metricController.SetGauge(name, value) +} + +func (h *handler) GetGauge(w http.ResponseWriter, req *http.Request) { + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + name := chi.URLParam(req, "name") + if v, ok := h.metricController.GetGaguge(name); !ok { + w.WriteHeader(http.StatusNotFound) + return + } else { + w.Write([]byte(fmt.Sprintf("%v", v))) + } +} + +func (h *handler) PostCounter(w http.ResponseWriter, req *http.Request) { + _, _ = io.ReadAll(req.Body) + name := chi.URLParam(req, "name") + valueStr := chi.URLParam(req, "value") + value, err := ExtractInt64(valueStr) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + h.metricController.AddCounter(name, value) +} + +func (h *handler) GetCounter(w http.ResponseWriter, req *http.Request) { + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + name := chi.URLParam(req, "name") + if v, ok := h.metricController.GetCounter(name); !ok { + w.WriteHeader(http.StatusNotFound) + } else { + w.Write([]byte(fmt.Sprintf("%v", v))) + } +} + +func (h *handler) AllMetrics(w http.ResponseWriter, request *http.Request) { + metrics := h.metricController.GetAllMetrics() + allMetricsViewTmpl.Execute(w, metrics) +} + +var allMetricsViewTmpl, _ = template.New("allMetrics").Parse(` + + + + + + + + + {{ range .Items}} + + + + + + {{ end}} +
TypeNameValue
{{ .Type }}{{ .Name }}{{ .Value }}
+ + +`) + +func CreateFullPostCounterHandler(counterHandler http.HandlerFunc) http.HandlerFunc { + return internal.Conveyor( + counterHandler, + CheckIntegerMiddleware, + CheckMetricNameMiddleware, + CheckContentTypeMiddleware, + CheckMethodPostMiddleware, + ) +} + +func CreateFullPostGaugeHandler(gaugeHandler http.HandlerFunc) http.HandlerFunc { + return internal.Conveyor( + gaugeHandler, + CheckDigitalMiddleware, + CheckMetricNameMiddleware, + CheckContentTypeMiddleware, + CheckMethodPostMiddleware, + ) +} diff --git a/internal/server/middleware.go b/internal/server/middleware.go new file mode 100644 index 0000000..0a026e2 --- /dev/null +++ b/internal/server/middleware.go @@ -0,0 +1,62 @@ +package server + +import ( + "net/http" + "strings" + + "github.com/go-chi/chi/v5" +) + +func CheckMethodPostMiddleware(next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodPost { + http.Error(w, "only post methods", http.StatusMethodNotAllowed) + return + } + next.ServeHTTP(w, req) + } +} + +func CheckContentTypeMiddleware(next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, req *http.Request) { + contentType := req.Header.Get("Content-Type") + if contentType != "" && !strings.HasPrefix(contentType, "text/plain") { + http.Error(w, "only 'text/plain' supported", http.StatusUnsupportedMediaType) + return + } + next.ServeHTTP(w, req) + } +} + +func CheckDigitalMiddleware(next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, req *http.Request) { + valueStr := chi.URLParam(req, "value") + if !CheckDecimal(valueStr) { + http.Error(w, "wrong decimal value", http.StatusBadRequest) + return + } + next.ServeHTTP(w, req) + } +} + +func CheckIntegerMiddleware(next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, req *http.Request) { + valueStr := chi.URLParam(req, "value") + if !CheckInteger(valueStr) { + http.Error(w, "wrong integer value", http.StatusBadRequest) + return + } + next.ServeHTTP(w, req) + } +} + +func CheckMetricNameMiddleware(next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, req *http.Request) { + name := chi.URLParam(req, "name") + if !CheckName(name) { + http.Error(w, "wrong name value", http.StatusBadRequest) + return + } + next.ServeHTTP(w, req) + } +} diff --git a/internal/server/middleware_test.go b/internal/server/middleware_test.go new file mode 100644 index 0000000..430fa3e --- /dev/null +++ b/internal/server/middleware_test.go @@ -0,0 +1,170 @@ +package server + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/go-resty/resty/v2" + "github.com/stretchr/testify/assert" +) + +const textPlaint = "text/plain; charset=utf-8" + +type mockBusinessHandler struct{} + +func (*mockBusinessHandler) PostGauge(w http.ResponseWriter, req *http.Request) { + w.Header().Set("Content-Type", textPlaint) + +} +func (*mockBusinessHandler) GetGauge(w http.ResponseWriter, req *http.Request) { + w.Header().Set("Content-Type", textPlaint) +} +func (*mockBusinessHandler) PostCounter(w http.ResponseWriter, req *http.Request) { + w.Header().Set("Content-Type", textPlaint) +} +func (*mockBusinessHandler) GetCounter(w http.ResponseWriter, req *http.Request) { + w.Header().Set("Content-Type", textPlaint) +} +func (*mockBusinessHandler) AllMetrics(w http.ResponseWriter, request *http.Request) { + w.Header().Set("Content-Type", textPlaint) +} + +func TestServerMiddlewareChain(t *testing.T) { + mockBusinessHandler := &mockBusinessHandler{} + serverHandler := CreateFullHTTPHandler(mockBusinessHandler) + + srv := httptest.NewServer(serverHandler) + defer srv.Close() + testCases := []struct { + name string + url string + method string + contentType string + want want + }{ + { + "unknown type", + "/update/unknown/testCounter/100", + http.MethodPost, + "text/plain", + want{ + http.StatusNotImplemented, + textPlaint, + }, + }, + { + "wrong content type", + "/update/gauge/m1/123", + http.MethodPost, + "application/json", + want{ + http.StatusUnsupportedMediaType, + textPlaint, + }, + }, + { + "wong metric value", + "/update/gauge/m1/123_", + http.MethodPost, + textPlaint, + want{ + http.StatusBadRequest, + textPlaint, + }, + }, + { + "float value", + "/update/gauge/m1/123.05", + http.MethodPost, + textPlaint, + want{ + http.StatusOK, + textPlaint, + }, + }, + { + "negative value", + "/update/gauge/m1/-123.05", + http.MethodPost, + textPlaint, + want{ + http.StatusOK, + textPlaint, + }, + }, + { + "float zero value", + "/update/gauge/m1/-0.0", + http.MethodPost, + textPlaint, + want{ + http.StatusOK, + textPlaint, + }, + }, + { + "int value", + "/update/counter/m1/123", + http.MethodPost, + textPlaint, + want{ + http.StatusOK, + textPlaint, + }, + }, + { + "negative int value", + "/update/counter/m1/-123", + http.MethodPost, + textPlaint, + want{ + http.StatusOK, + textPlaint, + }, + }, + { + "zero value", + "/update/counter/m1/0", + http.MethodPost, + textPlaint, + want{ + http.StatusOK, + textPlaint, + }, + }, + { + "request without metrics name", + "/update/counter/123", + http.MethodPost, + textPlaint, + want{ + http.StatusNotFound, + textPlaint, + }, + }, + { + "request without metrics name 2", + "/update/gauge/123", + http.MethodPost, + textPlaint, + want{ + http.StatusNotFound, + textPlaint, + }, + }, + } + + for _, test := range testCases { + t.Run(test.name, func(t *testing.T) { + req := resty.New().R() + req.Method = test.method + req.URL = srv.URL + test.url + req.Header.Add("Content-Type", test.contentType) + resp, err := req.Send() + assert.Nil(t, err) + assert.Equal(t, test.want.code, resp.StatusCode()) + assert.Equal(t, test.want.contentType, resp.Header().Get("Content-Type")) + }) + } +} diff --git a/internal/server/server.go b/internal/server/server.go index e30ab96..f1b7a38 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -1,125 +1,45 @@ package server import ( - "context" "net/http" - "sync" - "time" - "github.com/StasMerzlyakov/go-metrics/internal/config" - "github.com/StasMerzlyakov/go-metrics/internal/server/domain" - "go.uber.org/zap" + "github.com/StasMerzlyakov/go-metrics/internal" + "github.com/StasMerzlyakov/go-metrics/internal/storage" + "github.com/go-chi/chi/v5" ) -type Backuper interface { - RestoreBackUp() error - DoBackUp() error +func CreateServer(config *Configuration) error { + serverHandler := CreateServerHandler() + server := &http.Server{Addr: config.url, Handler: serverHandler, ReadTimeout: 0, IdleTimeout: 0} + return server.ListenAndServe() } -type ChangeListenerHolder interface { - AddListener(changeListener domain.ChangeListener) +func CreateServerHandler() http.Handler { + counterStorage := storage.NewMemoryInt64Storage() + gaugeStorage := storage.NewMemoryFloat64Storage() + metricController := NewMetricController(counterStorage, gaugeStorage) + businessHandler := NewBusinessHandler(metricController) + return CreateFullHTTPHandler(businessHandler) } -func NewMetricsServer( - config *config.ServerConfiguration, - sugar *zap.SugaredLogger, - httpHandler http.Handler, - holder ChangeListenerHolder, - backUper Backuper, -) *meterServer { +func CreateFullHTTPHandler(businessHandler BusinessHandler) http.Handler { - sugar.Infow("ServerConfig", "config", config) + fullGaugeHandler := CreateFullPostGaugeHandler(businessHandler.PostGauge) + fullCounterHandler := CreateFullPostCounterHandler(businessHandler.PostCounter) + r := chi.NewRouter() + r.Get("/", businessHandler.AllMetrics) + r.Post("/update/gauge/{name}/{value}", fullGaugeHandler) - // restore backup - if backUper != nil && config.Restore { - if err := backUper.RestoreBackUp(); err != nil { - panic(err) - } - } + r.Route("/update", func(r chi.Router) { + r.Post("/gauge/{name}/{value}", fullGaugeHandler) + r.Post("/gauge/{name}", internal.StatusNotFound) + r.Post("/counter/{name}/{value}", fullCounterHandler) + r.Post("/{type}/{name}/{value}", internal.StatusNotImplemented) + }) - // проверяем - нужен ли синхронный бэкап - doSyncBackup := config.StoreInterval == 0 - if doSyncBackup && backUper != nil { - sugar.Warnw("NewMetricsServer", "msg", "backup work in sync mode") - // синхронный бэкап реализован через мехинизм листенеров изменений - // (изменение данных может происходить и не только через http) - holder.AddListener(&backupSyncListener{ - backUper: backUper, - }) - } - - return &meterServer{ - srv: &http.Server{ - Addr: config.URL, - Handler: httpHandler, - ReadTimeout: 0, - IdleTimeout: 0, - }, - doSyncBackup: doSyncBackup, - backaupStoreIntervalSec: config.StoreInterval, - sugar: sugar, - backUper: backUper, - } -} - -type meterServer struct { - sugar *zap.SugaredLogger - srv *http.Server - wg sync.WaitGroup - backUper Backuper - startContext context.Context - backaupStoreIntervalSec uint - doSyncBackup bool -} - -func (s *meterServer) ServeBackup(ctx context.Context) error { - - storeInterval := time.Duration(s.backaupStoreIntervalSec) * time.Second - for { - select { - case <-ctx.Done(): - s.sugar.Infow("Run", "msg", "backup finished") - return nil - - case <-time.After(storeInterval): - if err := s.backUper.DoBackUp(); err != nil { - s.sugar.Fatalw("DoBackUp", "msg", err.Error()) - } - } - } -} - -func (s *meterServer) Start(startContext context.Context) { - s.startContext = startContext - s.wg.Add(2) - go func() { - defer s.wg.Done() - if err := s.srv.ListenAndServe(); err != http.ErrServerClosed { - s.sugar.Fatalw("srv.ListenAndServe", "msg", err.Error()) - } - }() - if s.backUper != nil && !s.doSyncBackup { - go func() { - defer s.wg.Done() - - if err := s.ServeBackup(startContext); err != nil { - s.sugar.Fatalw("ServeBackup", "msg", err.Error()) - } - }() - } - s.sugar.Infof("Server started") -} - -func (s *meterServer) WaitDone() { - s.srv.Shutdown(s.startContext) - s.wg.Wait() - s.sugar.Infof("WaitDone") -} - -type backupSyncListener struct { - backUper Backuper -} - -func (bsl *backupSyncListener) Refresh(*domain.Metrics) error { - return bsl.backUper.DoBackUp() + r.Route("/value", func(r chi.Router) { + r.Get("/gauge/{name}", businessHandler.GetGauge) + r.Get("/counter/{name}", businessHandler.GetCounter) + }) + return r } diff --git a/internal/server/server_test.go b/internal/server/server_test.go index abb4e43..9791c11 100644 --- a/internal/server/server_test.go +++ b/internal/server/server_test.go @@ -1 +1,113 @@ package server + +import ( + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "github.com/go-resty/resty/v2" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCounterValueHandler(t *testing.T) { + // TODO использовать mock-storage + serverHandler := CreateServerHandler() + srv := httptest.NewServer(serverHandler) + defer srv.Close() + + req := resty.New().R() + req.Method = http.MethodPost + value1 := 123 + value1Str := fmt.Sprintf("%v", value1) + req.URL = srv.URL + "/update/counter/TestCounter/" + value1Str + req.Header.Add("Content-Type", textPlaint) + _, err := req.Send() + require.Nil(t, err) + + req = resty.New().R() + req.Method = http.MethodGet + req.URL = srv.URL + "/value/counter/TestCounter" + req.Header.Add("Content-Type", textPlaint) + + resp, err := req.Send() + require.Nil(t, err) + respBody := string(resp.Body()) + require.Equal(t, value1Str, respBody) + + req = resty.New().R() + req.Method = http.MethodPost + value2 := 234 + req.URL = srv.URL + "/update/counter/TestCounter/" + fmt.Sprintf("%v", value2) + req.Header.Add("Content-Type", textPlaint) + _, err = req.Send() + require.Nil(t, err) + + req = resty.New().R() + req.Method = http.MethodGet + req.URL = srv.URL + "/value/counter/TestCounter" + req.Header.Add("Content-Type", textPlaint) + + resp, err = req.Send() + require.Nil(t, err) + respBody = string(resp.Body()) + value3 := fmt.Sprintf("%v", value1+value2) + assert.Equal(t, value3, respBody) + +} + +func TestGaugeValueHandler(t *testing.T) { + // TODO использовать mock-storage + serverHandler := CreateServerHandler() + + srv := httptest.NewServer(serverHandler) + defer srv.Close() + + req := resty.New().R() + req.Method = http.MethodPost + value1 := 234.123 + value1Str := fmt.Sprintf("%v", value1) + req.URL = srv.URL + "/update/gauge/TestCounter/" + value1Str + req.Header.Add("Content-Type", textPlaint) + _, err := req.Send() + require.Nil(t, err) + + req = resty.New().R() + req.Method = http.MethodGet + req.URL = srv.URL + "/value/gauge/TestCounter" + req.Header.Add("Content-Type", textPlaint) + resp, err := req.Send() + require.Nil(t, err) + respBody := string(resp.Body()) + require.Equal(t, value1Str, respBody) // Не попасть бы на потерю точности string -> float64 -> string + + req = resty.New().R() + req.Method = http.MethodPost + value2 := 534.123 + value2Str := fmt.Sprintf("%v", value2) + req.URL = srv.URL + "/update/gauge/TestCounter/" + value2Str + req.Header.Add("Content-Type", textPlaint) + _, err = req.Send() + require.Nil(t, err) + + req = resty.New().R() + req.Method = http.MethodGet + req.URL = srv.URL + "/value/gauge/TestCounter" + req.Header.Add("Content-Type", textPlaint) + resp, err = req.Send() + require.Nil(t, err) + respBody = string(resp.Body()) + require.Equal(t, value2Str, respBody) // Не попасть бы на потерю точности string -> float64 -> string + +} + +var mockSuccessHandler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + w.Header().Add("Content-Type", "text/plain; charset=utf-8") + w.WriteHeader(http.StatusOK) +}) + +type want struct { + code int + contentType string +} diff --git a/internal/server/storage/memory/storage.go b/internal/server/storage/memory/storage.go deleted file mode 100644 index de829d4..0000000 --- a/internal/server/storage/memory/storage.go +++ /dev/null @@ -1,141 +0,0 @@ -package memory - -import ( - "fmt" - - "github.com/StasMerzlyakov/go-metrics/internal/server/domain" -) - -func NewStorage() *storage { - return &storage{ - counterStorage: make(map[string]int64), - gaugeStorage: make(map[string]float64), - } -} - -type storage struct { - counterStorage map[string]int64 - gaugeStorage map[string]float64 -} - -func (st *storage) SetAllMetrics(in []domain.Metrics) error { - newCounterStorage := make(map[string]int64) - newGaugeStorage := make(map[string]float64) - - for _, m := range in { - switch m.MType { - case domain.CounterType: - delta := *m.Delta - newCounterStorage[m.ID] = delta - case domain.GaugeType: - value := *m.Value - newGaugeStorage[m.ID] = value - default: - return fmt.Errorf("unknown MType %v", m.MType) - } - } - - st.counterStorage = newCounterStorage - st.gaugeStorage = newGaugeStorage - return nil -} - -func (st *storage) GetAllMetrics() ([]domain.Metrics, error) { - - var out []domain.Metrics - - for k, v := range st.counterStorage { - delta := v - out = append(out, domain.Metrics{ - ID: k, - MType: domain.CounterType, - Delta: &delta, - }) - } - - for k, v := range st.gaugeStorage { - value := v - out = append(out, domain.Metrics{ - ID: k, - MType: domain.GaugeType, - Value: &value, - }) - } - - return out, nil -} - -func (st *storage) Set(m *domain.Metrics) error { - switch m.MType { - case domain.CounterType: - delta := *m.Delta - st.counterStorage[m.ID] = delta - case domain.GaugeType: - value := *m.Value - st.gaugeStorage[m.ID] = value - default: - return fmt.Errorf("unknown MType %v", m.MType) - } - return nil -} - -func (st *storage) Add(m *domain.Metrics) error { - switch m.MType { - case domain.CounterType: - delta := *m.Delta - curValue, ok := st.counterStorage[m.ID] - if ok { - delta += curValue - st.counterStorage[m.ID] = delta - // обновляем значение для входной переменной - m.Delta = &delta - } else { - st.counterStorage[m.ID] = delta - } - case domain.GaugeType: - value := *m.Value - st.gaugeStorage[m.ID] = value - curValue, ok := st.gaugeStorage[m.ID] - if ok { - curValue += value - st.gaugeStorage[m.ID] = curValue - // обновляем значение для входной переменной - m.Value = &curValue - } else { - st.gaugeStorage[m.ID] = value - } - default: - return fmt.Errorf("unknown MType %v", m.MType) - } - return nil -} -func (st *storage) Get(id string, mType domain.MetricType) (*domain.Metrics, error) { - switch mType { - case domain.CounterType: - curValue, ok := st.counterStorage[id] - delta := curValue - if ok { - return &domain.Metrics{ - ID: id, - MType: mType, - Delta: &delta, - }, nil - } else { - return nil, nil - } - case domain.GaugeType: - curValue, ok := st.gaugeStorage[id] - value := curValue - if ok { - return &domain.Metrics{ - ID: id, - MType: mType, - Value: &value, - }, nil - } else { - return nil, nil - } - default: - return nil, fmt.Errorf("unknown MType %v", mType) - } -} diff --git a/internal/server/storage/memory/storage_test.go b/internal/server/storage/memory/storage_test.go deleted file mode 100644 index 0f87351..0000000 --- a/internal/server/storage/memory/storage_test.go +++ /dev/null @@ -1,174 +0,0 @@ -package memory_test - -import ( - "testing" - - "github.com/StasMerzlyakov/go-metrics/internal/server/domain" - "github.com/StasMerzlyakov/go-metrics/internal/server/storage/memory" - "github.com/stretchr/testify/require" -) - -func TestMemoryStorageStoreAndLoad(t *testing.T) { - - toLoad := []domain.Metrics{ - {MType: domain.CounterType, ID: "PollCount", Delta: domain.DeltaPtr(1)}, - {MType: domain.GaugeType, ID: "RandomValue", Value: domain.ValuePtr(1.123)}, - {MType: domain.GaugeType, ID: "Alloc", Value: domain.ValuePtr(1.123)}, - {MType: domain.GaugeType, ID: "BuckHashSys", Value: domain.ValuePtr(1.123)}, - {MType: domain.GaugeType, ID: "Frees", Value: domain.ValuePtr(1.123)}, - {MType: domain.GaugeType, ID: "GCCPUFraction", Value: domain.ValuePtr(1.123)}, - {MType: domain.GaugeType, ID: "GCSys", Value: domain.ValuePtr(1.123)}, - {MType: domain.GaugeType, ID: "HeapAlloc", Value: domain.ValuePtr(1.123)}, - {MType: domain.GaugeType, ID: "HeapIdle", Value: domain.ValuePtr(1.123)}, - {MType: domain.GaugeType, ID: "HeapInuse", Value: domain.ValuePtr(1.123)}, - {MType: domain.GaugeType, ID: "HeapObjects", Value: domain.ValuePtr(1.123)}, - {MType: domain.GaugeType, ID: "HeapReleased", Value: domain.ValuePtr(1.123)}, - {MType: domain.GaugeType, ID: "HeapSys", Value: domain.ValuePtr(1.123)}, - {MType: domain.GaugeType, ID: "LastGC", Value: domain.ValuePtr(1.123)}, - {MType: domain.GaugeType, ID: "Lookups", Value: domain.ValuePtr(1.123)}, - {MType: domain.GaugeType, ID: "MCacheInuse", Value: domain.ValuePtr(1.123)}, - {MType: domain.GaugeType, ID: "MCacheSys", Value: domain.ValuePtr(1.123)}, - {MType: domain.GaugeType, ID: "MSpanInuse", Value: domain.ValuePtr(1.123)}, - {MType: domain.GaugeType, ID: "MSpanSys", Value: domain.ValuePtr(1.123)}, - {MType: domain.GaugeType, ID: "Mallocs", Value: domain.ValuePtr(1.123)}, - {MType: domain.GaugeType, ID: "NextGC", Value: domain.ValuePtr(1.123)}, - {MType: domain.GaugeType, ID: "NumForcedGC", Value: domain.ValuePtr(1.123)}, - {MType: domain.GaugeType, ID: "NumGC", Value: domain.ValuePtr(1.123)}, - {MType: domain.GaugeType, ID: "OtherSys", Value: domain.ValuePtr(1.123)}, - {MType: domain.GaugeType, ID: "PauseTotalNs", Value: domain.ValuePtr(1.123)}, - {MType: domain.GaugeType, ID: "StackInuse", Value: domain.ValuePtr(1.123)}, - {MType: domain.GaugeType, ID: "StackSys", Value: domain.ValuePtr(1.123)}, - {MType: domain.GaugeType, ID: "Sys", Value: domain.ValuePtr(1.123)}, - {MType: domain.GaugeType, ID: "TotalAlloc", Value: domain.ValuePtr(1.123)}, - } - - storage := memory.NewStorage() - out, err := storage.GetAllMetrics() - require.NoError(t, err) - require.True(t, len(out) == 0) - - err = storage.SetAllMetrics(toLoad) - require.NoError(t, err) - - out, err = storage.GetAllMetrics() - require.NoError(t, err) - require.Equal(t, len(toLoad), len(out)) -} - -func TestMemoryStorageGaugeOperations(t *testing.T) { - - storage := memory.NewStorage() - - GagueID := "NumGC" - - mConst := &domain.Metrics{ - ID: GagueID, - MType: domain.GaugeType, - Value: domain.ValuePtr(2), - } - - ms, err := storage.Get(GagueID, domain.CounterType) - require.NoError(t, err) - require.Nil(t, ms) - - ms, err = storage.Get(GagueID, domain.GaugeType) - require.NoError(t, err) - require.Nil(t, ms) - - err = storage.Set(mConst) - require.NoError(t, err) - - ms, err = storage.Get(GagueID, domain.GaugeType) - require.NoError(t, err) - require.NotNil(t, ms) - require.Equal(t, ms.ID, GagueID) - require.Equal(t, ms.MType, domain.GaugeType) - require.NotNil(t, ms.Value) - require.Equal(t, float64(2), *ms.Value) - require.Nil(t, ms.Delta) - - ms, err = storage.Get(GagueID, domain.CounterType) - require.NoError(t, err) - require.Nil(t, ms) - - err = storage.Set(mConst) - require.NoError(t, err) - ms, err = storage.Get(GagueID, domain.GaugeType) - require.NoError(t, err) - require.NotNil(t, ms) - require.Equal(t, ms.ID, GagueID) - require.Equal(t, ms.MType, domain.GaugeType) - require.NotNil(t, ms.Value) - require.Equal(t, float64(2), *ms.Value) - require.Nil(t, ms.Delta) - - err = storage.Add(mConst) - require.NoError(t, err) - ms, err = storage.Get(GagueID, domain.GaugeType) - require.NoError(t, err) - require.NotNil(t, ms) - require.Equal(t, ms.ID, GagueID) - require.Equal(t, ms.MType, domain.GaugeType) - require.NotNil(t, ms.Value) - require.Equal(t, float64(4), *ms.Value) - require.Nil(t, ms.Delta) -} - -func TestMemoryStorageCounterOperations(t *testing.T) { - - storage := memory.NewStorage() - - CounterID := "PollCount" - - mConst := &domain.Metrics{ - ID: CounterID, - MType: domain.CounterType, - Delta: domain.DeltaPtr(2), - } - - ms, err := storage.Get(CounterID, domain.CounterType) - require.NoError(t, err) - require.Nil(t, ms) - - ms, err = storage.Get(CounterID, domain.GaugeType) - require.NoError(t, err) - require.Nil(t, ms) - - err = storage.Set(mConst) - require.NoError(t, err) - - ms, err = storage.Get(CounterID, domain.CounterType) - require.NoError(t, err) - require.NotNil(t, ms) - require.Equal(t, ms.ID, CounterID) - require.Equal(t, ms.MType, domain.CounterType) - require.NotNil(t, ms.Delta) - require.Equal(t, int64(2), *ms.Delta) - require.Nil(t, ms.Value) - - ms, err = storage.Get(CounterID, domain.GaugeType) - require.NoError(t, err) - require.Nil(t, ms) - - err = storage.Set(mConst) - require.NoError(t, err) - ms, err = storage.Get(CounterID, domain.CounterType) - require.NoError(t, err) - require.NotNil(t, ms) - require.Equal(t, ms.ID, CounterID) - require.Equal(t, ms.MType, domain.CounterType) - require.NotNil(t, ms.Delta) - require.Equal(t, int64(2), *ms.Delta) - require.Nil(t, ms.Value) - - err = storage.Add(mConst) - require.NoError(t, err) - ms, err = storage.Get(CounterID, domain.CounterType) - require.NoError(t, err) - require.NotNil(t, ms) - require.Equal(t, ms.ID, CounterID) - require.Equal(t, ms.MType, domain.CounterType) - require.NotNil(t, ms.Delta) - require.Equal(t, int64(4), *ms.Delta) - require.Nil(t, ms.Value) -} diff --git a/internal/server/usecase/backup.go b/internal/server/usecase/backup.go deleted file mode 100644 index 757e082..0000000 --- a/internal/server/usecase/backup.go +++ /dev/null @@ -1,56 +0,0 @@ -package usecase - -import ( - "errors" - "os" - - "github.com/StasMerzlyakov/go-metrics/internal/server/domain" - "go.uber.org/zap" -) - -type AllMetricsStorage interface { - SetAllMetrics(in []domain.Metrics) error - GetAllMetrics() ([]domain.Metrics, error) -} - -type BackupFormatter interface { - Write([]domain.Metrics) error - Read() ([]domain.Metrics, error) -} - -func NewBackup(suga *zap.SugaredLogger, storage AllMetricsStorage, formatter BackupFormatter) *backUper { - return &backUper{ - suga: suga, - storage: storage, - formatter: formatter, - } -} - -type backUper struct { - suga *zap.SugaredLogger - storage AllMetricsStorage - formatter BackupFormatter -} - -func (bU *backUper) RestoreBackUp() error { - metrics, err := bU.formatter.Read() - if err != nil && !errors.Is(err, os.ErrNotExist) { - panic(err) - } - - return bU.storage.SetAllMetrics(metrics) -} - -func (bU *backUper) DoBackUp() error { - - metrics, err := bU.storage.GetAllMetrics() - if err != nil { - return err - } - err = bU.formatter.Write(metrics) - if err != nil { - return err - } - bU.suga.Infow("DoBackUp", "status", "ok", "msg", "backup is done") - return nil -} diff --git a/internal/server/usecase/metrics.go b/internal/server/usecase/metrics.go deleted file mode 100644 index 58bee36..0000000 --- a/internal/server/usecase/metrics.go +++ /dev/null @@ -1,145 +0,0 @@ -package usecase - -import ( - "fmt" - "regexp" - - "github.com/pkg/errors" - - "github.com/StasMerzlyakov/go-metrics/internal/server/domain" -) - -type Storage interface { - SetAllMetrics(in []domain.Metrics) error - GetAllMetrics() ([]domain.Metrics, error) - Set(m *domain.Metrics) error - Add(m *domain.Metrics) error - Get(id string, mType domain.MetricType) (*domain.Metrics, error) -} - -type metricsUseCase struct { - storage Storage - changeListeners []domain.ChangeListener -} - -func NewMetrics(storage Storage) *metricsUseCase { - return &metricsUseCase{ - storage: storage, - } -} - -var nameRegexp = regexp.MustCompile("^[a-zA-Z][a-zA-Z0-9_]*$") - -func (mc *metricsUseCase) AddListener(changeListener domain.ChangeListener) { - mc.changeListeners = append(mc.changeListeners, changeListener) -} - -func (mc *metricsUseCase) CheckName(name string) bool { - return nameRegexp.MatchString(name) -} - -func (mc *metricsUseCase) CheckMetrics(m *domain.Metrics) error { - if !mc.CheckName(m.ID) { - return errors.Wrap(domain.ErrDataFormat, fmt.Sprintf("wrong metric ID %v", m.ID)) - } - - if m.MType != domain.CounterType && m.MType != domain.GaugeType { - return errors.Wrap(domain.ErrDataFormat, fmt.Sprintf("metric ID: %v have type %v", m.ID, m.MType)) - } - - if m.MType == domain.CounterType { - if m.Delta == nil { - return errors.Wrap(domain.ErrDataFormat, fmt.Sprintf("metric ID: %v have MType %v, but delta is null", m.ID, m.MType)) - } - - if m.Value != nil { - return errors.Wrap(domain.ErrDataFormat, fmt.Sprintf("metric ID: %v have MType %v, but value is not null", m.ID, m.MType)) - } - } - - if m.MType == domain.GaugeType { - if m.Value == nil { - return errors.Wrap(domain.ErrDataFormat, fmt.Sprintf("metric ID: %v have MType %v, but value is null", m.ID, m.MType)) - } - - if m.Delta != nil { - return errors.Wrap(domain.ErrDataFormat, fmt.Sprintf("metric ID: %v have MType %v, but delta is not null", m.ID, m.MType)) - } - } - return nil -} - -func (mc *metricsUseCase) SetAllMetrics(in []domain.Metrics) error { - // Проверка данных - for _, m := range in { - err := mc.CheckMetrics(&m) - if err != nil { - return err - } - } - - return mc.storage.SetAllMetrics(in) -} - -func (mc *metricsUseCase) GetAllMetrics() ([]domain.Metrics, error) { - return mc.storage.GetAllMetrics() -} - -func (mc *metricsUseCase) GetCounter(name string) (*domain.Metrics, error) { - if !mc.CheckName(name) { - return nil, errors.Wrap(domain.ErrDataFormat, fmt.Sprintf("wrong metric ID %v", name)) - } - return mc.storage.Get(name, domain.CounterType) -} - -func (mc *metricsUseCase) GetGauge(name string) (*domain.Metrics, error) { - if !mc.CheckName(name) { - return nil, errors.Wrap(domain.ErrDataFormat, fmt.Sprintf("wrong metric ID %v", name)) - } - return mc.storage.Get(name, domain.GaugeType) -} - -func (mc *metricsUseCase) AddCounter(m *domain.Metrics) error { - if err := mc.CheckMetrics(m); err != nil { - return err - } - - if m.MType != domain.CounterType { - return fmt.Errorf("unexpected MType %v, expected %v", m.MType, domain.CounterType) - } - - if err := mc.storage.Add(m); err != nil { - return err - } - - if newValue, err := mc.storage.Get(m.ID, m.MType); err != nil { - return err - } else { - delta := *newValue.Delta - m.Delta = &delta - } - - for _, lst := range mc.changeListeners { - lst.Refresh(m) - } - - return nil -} - -func (mc *metricsUseCase) SetGauge(m *domain.Metrics) error { - if err := mc.CheckMetrics(m); err != nil { - return err - } - if m.MType != domain.GaugeType { - return fmt.Errorf("unexpected MType %v, expected %v", m.MType, domain.GaugeType) - } - if err := mc.storage.Set(m); err != nil { - return err - } - - for _, lst := range mc.changeListeners { - lst.Refresh(m) - } - - return nil -} diff --git a/internal/server/usecase/metrics_test.go b/internal/server/usecase/metrics_test.go deleted file mode 100644 index 3580567..0000000 --- a/internal/server/usecase/metrics_test.go +++ /dev/null @@ -1,145 +0,0 @@ -package usecase_test - -import ( - "errors" - "testing" - - "github.com/StasMerzlyakov/go-metrics/internal/server/domain" - "github.com/StasMerzlyakov/go-metrics/internal/server/usecase" - "github.com/stretchr/testify/assert" -) - -type mockStorage struct { -} - -func (*mockStorage) SetAllMetrics(in []domain.Metrics) error { - return nil -} -func (*mockStorage) GetAllMetrics() ([]domain.Metrics, error) { - return nil, nil -} -func (*mockStorage) Set(m *domain.Metrics) error { - return nil -} -func (*mockStorage) Add(m *domain.Metrics) error { - return nil -} -func (*mockStorage) Get(id string, mType domain.MetricType) (*domain.Metrics, error) { - return nil, nil -} - -func TestCheckName(t *testing.T) { - - mc := usecase.NewMetrics(&mockStorage{}) - - testCases := []struct { - name string - input string - result bool - }{ - { - "TestCheckName_1", - "a1s_asd1_1", - true, - }, - { - "TestCheckName_2", - "00.123", - false, - }, - { - "TestCheckName_3", - "0asd", - false, - }, - { - "TestCheckName_4", - "-A", - false, - }, - { - "TestCheckName_5", - "_asd", - false, - }, - { - "TestCheckName_6", - "A", - true, - }, - { - "TestCheckName_7", - "A0_123", - true, - }, - { - "TestCheckName_8", - "B123.1", - false, - }, - } - - for _, test := range testCases { - t.Run(test.name, func(t *testing.T) { - assert.Equal(t, test.result, mc.CheckName(test.input)) - }) - } -} - -func TestCheckMetrics(t *testing.T) { - mc := usecase.NewMetrics(&mockStorage{}) - - testCases := []struct { - name string - input *domain.Metrics - isOk bool - }{ - { - "CheckMetrics_1", - &domain.Metrics{ - ID: "a1s_asd1_1", - MType: domain.CounterType, - Delta: domain.DeltaPtr(1), - }, - true, - }, - { - "CheckMetrics_2", - &domain.Metrics{ - ID: "a1s_asd1_1", - MType: domain.GaugeType, - Delta: domain.DeltaPtr(1), - }, - false, - }, - { - "CheckMetrics_3", - &domain.Metrics{ - ID: "0asd", - MType: domain.CounterType, - Delta: domain.DeltaPtr(1), - }, - false, - }, - { - "CheckMetrics_4", - &domain.Metrics{ - ID: "OK", - MType: domain.GaugeType, - Value: domain.ValuePtr(1), - }, - true, - }, - } - - for _, test := range testCases { - t.Run(test.name, func(t *testing.T) { - err := mc.CheckMetrics(test.input) - if test.isOk { - assert.NoError(t, err) - } else { - assert.True(t, errors.Is(err, domain.ErrDataFormat)) - } - }) - } -} diff --git a/internal/server/utils.go b/internal/server/utils.go new file mode 100644 index 0000000..b7038d4 --- /dev/null +++ b/internal/server/utils.go @@ -0,0 +1,40 @@ +package server + +import ( + "regexp" + "strconv" +) + +var decimalRegexp = regexp.MustCompile("^[-+]?([1-9][0-9]*|0?)([.][0-9]*)?$") + +func CheckDecimal(value string) bool { + return decimalRegexp.MatchString(value) +} + +var integerRegexp = regexp.MustCompile("^[-+]?([1-9][0-9]*|0?)$") + +func CheckInteger(value string) bool { + return integerRegexp.MatchString(value) +} + +var nameRegexp = regexp.MustCompile("^[a-zA-Z][a-zA-Z0-9_]*$") + +func CheckName(value string) bool { + return nameRegexp.MatchString(value) +} + +func ExtractFloat64(valueStr string) (float64, error) { + value, err := strconv.ParseFloat(valueStr, 64) + if err != nil { + return -1, err + } + return value, nil +} + +func ExtractInt64(valueStr string) (int64, error) { + value, err := strconv.ParseInt(valueStr, 10, 64) + if err != nil { + return -1, err + } + return value, nil +} diff --git a/internal/server/utils_test.go b/internal/server/utils_test.go new file mode 100644 index 0000000..dc1531e --- /dev/null +++ b/internal/server/utils_test.go @@ -0,0 +1,292 @@ +package server + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestExtractFloat64(t *testing.T) { + type extractFloat64Result struct { + value float64 + isSuccessExpected bool + } + tests := []struct { + name string + input string + result extractFloat64Result + }{ + { + "good value", + "123.5", + extractFloat64Result{ + 123.5, + true, + }, + }, + { + "good value 2", + "123", + extractFloat64Result{ + 123, + true, + }, + }, + { + "bad value", + "123.F", + extractFloat64Result{ + -1, + false, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + value, err := ExtractFloat64(tt.input) + assert.Equal(t, tt.result.value, value) + assert.Equal(t, tt.result.isSuccessExpected, err == nil) + }) + } +} + +func TestExtractInt64(t *testing.T) { + + type extractInt64Result struct { + value int64 + isSuccessExpected bool + } + + tests := []struct { + name string + input string + result extractInt64Result + }{ + { + "good value", + "123", + extractInt64Result{ + 123, + true, + }, + }, + { + "bad value", + "123F", + extractInt64Result{ + -1, + false, + }, + }, + { + "bad value 2", + "123.5", + extractInt64Result{ + -1, + false, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + value, err := ExtractInt64(tt.input) + assert.Equal(t, tt.result.value, value) + assert.Equal(t, tt.result.isSuccessExpected, err == nil) + }) + } +} + +func TestCheckDecimal(t *testing.T) { + testCases := []struct { + name string + input string + result bool + }{ + { + "TestCheckDecimal_1", + "a1s_asd1_1", + false, + }, + { + "TestCheckDecimal_2", + "00.123", + false, + }, + { + "TestCheckDecimal_3", + "0.123", + true, + }, + { + "TestCheckDecimal_4", + "-0.123", + true, + }, + { + "TestCheckDecimal_5", + "0.123", + true, + }, + { + "TestCheckDecimal_6", + "0.123", + true, + }, + { + "TestCheckDecimal_7", + "0.123", + true, + }, + { + "TestCheckDecimal_8", + "123", + true, + }, + { + "TestCheckDecimal_9", + "-123", + true, + }, + { + "TestCheckDecimal_10", + "123.", + true, + }, + { + "TestCheckDecimal_11", + "123.123.1", + false, + }, + { + "TestCheckDecimal_12", + "123.123.", + false, + }, + } + + for _, test := range testCases { + t.Run(test.name, func(t *testing.T) { + assert.Equal(t, test.result, CheckDecimal(test.input)) + }) + } +} + +func TestCheckInteger(t *testing.T) { + testCases := []struct { + name string + input string + result bool + }{ + { + "TestCheckInteger_1", + "a1s_asd1_1", + false, + }, + { + "TestCheckInteger_2", + "00.123", + false, + }, + { + "TestCheckInteger_3", + "0.123", + false, + }, + { + "TestCheckInteger_4", + "-0.123", + false, + }, + { + "TestCheckInteger_5", + "0.123", + false, + }, + { + "TestCheckInteger_6", + "123", + true, + }, + { + "TestCheckInteger_7", + "-123", + true, + }, + { + "TestCheckInteger_8", + "123.", + false, + }, + { + "TestCheckInteger_9", + "123.123.1", + false, + }, + { + "TestCheckInteger_10", + "123.123.", + false, + }, + } + + for _, test := range testCases { + t.Run(test.name, func(t *testing.T) { + assert.Equal(t, test.result, CheckInteger(test.input)) + }) + } +} + +func TestCheckName(t *testing.T) { + testCases := []struct { + name string + input string + result bool + }{ + { + "TestCheckName_1", + "a1s_asd1_1", + true, + }, + { + "TestCheckName_2", + "00.123", + false, + }, + { + "TestCheckName_3", + "0asd", + false, + }, + { + "TestCheckName_4", + "-A", + false, + }, + { + "TestCheckName_5", + "_asd", + false, + }, + { + "TestCheckName_6", + "A", + true, + }, + { + "TestCheckName_7", + "A0_123", + true, + }, + { + "TestCheckName_8", + "B123.1", + false, + }, + } + + for _, test := range testCases { + t.Run(test.name, func(t *testing.T) { + assert.Equal(t, test.result, CheckName(test.input)) + }) + } +} diff --git a/internal/storage/api.go b/internal/storage/api.go new file mode 100644 index 0000000..5cf107e --- /dev/null +++ b/internal/storage/api.go @@ -0,0 +1,12 @@ +package storage + +type MemValue interface { + int64 | float64 +} + +type MetricsStorage[T MemValue] interface { + Set(key string, value T) + Add(key string, value T) + Get(key string) (T, bool) + Keys() []string +} diff --git a/internal/storage/mem_storage.go b/internal/storage/mem_storage.go new file mode 100644 index 0000000..2075d20 --- /dev/null +++ b/internal/storage/mem_storage.go @@ -0,0 +1,53 @@ +package storage + +import "sync" + +func NewMemoryFloat64Storage() MetricsStorage[float64] { + return &memStorage[float64]{} +} + +func NewMemoryInt64Storage() MetricsStorage[int64] { + return &memStorage[int64]{} +} + +type memStorage[T MemValue] struct { + mtx sync.Mutex + storage map[string]T +} + +func (ms *memStorage[T]) checkInit() { + ms.mtx.Lock() + defer ms.mtx.Unlock() + if ms.storage == nil { + ms.storage = make(map[string]T) + } +} + +func (ms *memStorage[T]) Keys() []string { + ms.checkInit() + keys := make([]string, 0, len(ms.storage)) + for k := range ms.storage { + keys = append(keys, k) + } + return keys +} + +func (ms *memStorage[T]) Set(key string, value T) { + ms.checkInit() + ms.storage[key] = value +} + +func (ms *memStorage[T]) Add(key string, value T) { + ms.checkInit() + 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.checkInit() + curVal, ok := ms.storage[key] + return curVal, ok +} diff --git a/internal/storage/mem_storage_test.go b/internal/storage/mem_storage_test.go new file mode 100644 index 0000000..13d4654 --- /dev/null +++ b/internal/storage/mem_storage_test.go @@ -0,0 +1,46 @@ +package storage + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestInt64MemStorage(t *testing.T) { + ms := NewMemoryInt64Storage() + 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 := NewMemoryFloat64Storage() + 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) +}