diff --git a/.img/example_get.png b/.img/example_get.png index ea41ecc..f0a78e0 100644 Binary files a/.img/example_get.png and b/.img/example_get.png differ diff --git a/.img/example_get_2_not_ready.png b/.img/example_get_2_not_ready.png new file mode 100644 index 0000000..bd2b81f Binary files /dev/null and b/.img/example_get_2_not_ready.png differ diff --git a/.img/example_get_3.png b/.img/example_get_3.png new file mode 100644 index 0000000..81a04f9 Binary files /dev/null and b/.img/example_get_3.png differ diff --git a/.img/example_post.png b/.img/example_post.png index bf27583..312a2a8 100644 Binary files a/.img/example_post.png and b/.img/example_post.png differ diff --git a/.img/example_post_2.png b/.img/example_post_2.png new file mode 100644 index 0000000..a051e9b Binary files /dev/null and b/.img/example_post_2.png differ diff --git a/README.md b/README.md index 62e8032..82a2d49 100644 --- a/README.md +++ b/README.md @@ -53,20 +53,28 @@ From root of the repository: 2) Start monitoring ```curl --request POST --url http://localhost:4000/api/v1/monitoring?cur=btcusd&period=1m&freq=10s``` + ![img](./.img/example_post.png) +3) ![img](./.img/example_post_2.png) -3) Get results of monitoring +4) Get results of monitoring ```curl --request GET --url http://localhost:4000/api/v1/monitoring/1``` +![img](./.img/example_get.png) +![img](./.img/example_get_2_not_ready.png) +![img](./.img/example_get_3.png) -#### Insomnia examples: +Optional: -see in folder -> `.insomnia` +4) Get result monitoring and delete monitoring record from db -![img](./.img/example_post.png) -![img](./.img/example_get.png) + ```curl --request GET --url http://localhost:4000/api/v1/monitoring/1?delete=true``` +#### Insomnia examples: + +see in folder -> `.insomnia` + #### Docker Compose run ![img](./.img/result_running_service_in_docker_compose.png) @@ -88,7 +96,7 @@ see in folder -> `.insomnia` #### Env up - make dev_env_up + make dev_env_up After that can run go app (in JetBrains IDE or VSCode) (do not forget set up .env file and add ENVs) @@ -97,11 +105,11 @@ After that can run go app (in JetBrains IDE or VSCode) (do not forget set up .en #### Remove old container or volumes - make docker_clean_all + make docker_clean_all #### Rebuild prod app container - make rebuild + make rebuild Made by Arseny Sazanov 2022 diff --git a/configs/default.yml b/configs/default.yml index 4022c0f..e9b3b04 100644 --- a/configs/default.yml +++ b/configs/default.yml @@ -1,7 +1,7 @@ logger: level: info - encoding: json - color: false + encoding: console + color: true outputs: - stdout tags: @@ -32,7 +32,7 @@ services: port: 5432 database: pm maxTryConnect: 3 - timeoutTryConnect: 5s + timeoutTryConnect: "5s" options: maxLifeTime: 600 maxIdleConn: 10 @@ -41,10 +41,10 @@ services: controllers: general: monitor: - timeoutConsulLeaderCheck: "1s" # TODO define lose 1 second + timeoutConsulLeaderCheck: "1s" # TODO define that we can lose 1 second (need discuss!) master: scanner: - timeoutOneTaskProcess: "2s" # TODO define max timeout for one task - intervalPeriodicScan: "1s" # TODO define max frequency for price scanner + timeoutOneTaskProcess: "2s" # TODO define max timeout for one task (need discuss!) + intervalPeriodicScan: "1s" # TODO define max frequency for price scanner (need discuss!) cntWorkers: 1 diff --git a/internal/config/config.go b/internal/config/config.go index 02521dc..103ade7 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -51,6 +51,7 @@ func New(appName string, envName env.Var, configPath string, nodeName string) (C applyEnvOnConfig(&cfg, appName) cfg.Servers.HTTP.Name += "_" + nodeName + cfg.Servers.HTTP.NodeID = nodeName // show config for debug purposes // nolint forbidigo // exception of rule ) diff --git a/internal/servers/http/handlers.go b/internal/servers/http/handlers.go index d10d5cf..523a23a 100644 --- a/internal/servers/http/handlers.go +++ b/internal/servers/http/handlers.go @@ -13,7 +13,6 @@ import ( "github.com/imperiuse/price_monitor/internal/helper" "github.com/imperiuse/price_monitor/internal/logger/field" mw "github.com/imperiuse/price_monitor/internal/servers/http/middlerware" - "github.com/imperiuse/price_monitor/internal/servers/http/util" "github.com/imperiuse/price_monitor/internal/services/storage" "github.com/imperiuse/price_monitor/internal/services/storage/model" ) @@ -23,12 +22,13 @@ const defaultLimit = 10000 type ( FormGetMonitoring struct { ID int64 `uri:"id" binding:"required,min=1,max=9223372036854775807"` + Delete bool `form:"delete" binding:"omitempty"` Cursor uint64 `form:"cursor" binding:"omitempty,min=0,max=18446744073709551615"` Limit uint64 `form:"limit" binding:"omitempty,min=1,max=10000"` } FormPostMonitoring struct { - // TODO ALSO CAN BE LIKE HERE + // TODO ALSO CAN BE LIKE HERE (I decide to go straightforward, simple and utility) //FromTime time.Time `form:"from" binding:"required" time_format:"2006-01-02T15:04:05" time_utc:"0"` // time.RFC3339 //ToTime time.Time `form:"to" binding:"required" time_format:"2006-01-02T15:04:05" time_utc:"0"` // time.RFC3339 @@ -52,8 +52,8 @@ type ( // @Produce json // @Success 200 // @Router /health [get] -func Health(c *gin.Context) { - c.String(http.StatusOK, "") +func (s *Server) Health(c *gin.Context) { + c.String(http.StatusOK, `{"NodeID": %s}`, s.config.NodeID) } // Readiness godoc @@ -65,8 +65,8 @@ func Health(c *gin.Context) { // @Produce json // @Success 200 // @Router /ready [get] -func Readiness(c *gin.Context) { - c.String(http.StatusOK, "ready") +func (s *Server) Readiness(c *gin.Context) { + c.String(http.StatusOK, `{"NodeID": %s}`, s.config.NodeID) } // GetMonitoring godoc @@ -75,6 +75,7 @@ func Readiness(c *gin.Context) { // @Id GetMonitoring // @Tags Server API // @Param id path int true "id of road controller" +// @Param delete query bool false "delete monitoring after" // @Param cursor query int false "cursor for cursor pagination" // @Param limit query int false "limit for limit pagination"// todo https://uxdesign.cc/why-facebook-says-cursor-pagination-is-the-greatest-d6b98d86b6c0 // @Accept json @@ -93,14 +94,14 @@ func (s *Server) GetMonitoring(c *gin.Context) { // TODO toooo-looong func need f := FormGetMonitoring{} if err := c.ShouldBindUri(&f); err != nil { s.log.Error("can not parse params (uri)", field.ID(f.ID), field.Any("form", f), field.Error(err)) - util.SendErrorJSON(c, http.StatusBadRequest, "can not parse params (uri)", err) + s.SendErrorJSON(c, http.StatusBadRequest, "can not parse params (uri)", err) return } if err := c.ShouldBindQuery(&f); err != nil { s.log.Error("can not parse params (query)", field.ID(f.ID), field.Any("form", f), field.Error(err)) - util.SendErrorJSON(c, http.StatusBadRequest, "can not parse params (query)", err) + s.SendErrorJSON(c, http.StatusBadRequest, "can not parse params (query)", err) return } @@ -114,20 +115,20 @@ func (s *Server) GetMonitoring(c *gin.Context) { // TODO toooo-looong func need if err != nil { if errors.Is(err, sql.ErrNoRows) { s.log.Debug("not found monitoring obj with id", field.ID(f.ID)) - util.SendErrorJSON(c, http.StatusNotFound, "no monitoring obj with that id", nil) + s.SendErrorJSON(c, http.StatusNotFound, "no monitoring obj with that id", nil) return } s.log.Error("can not get data for monitoring from db", field.ID(f.ID), field.Any("form", f), field.Error(err)) - util.SendErrorJSON(c, http.StatusInternalServerError, "can not get data for monitoring from db", err) + s.SendErrorJSON(c, http.StatusInternalServerError, "can not get data for monitoring from db", err) return } if time.Now().UTC().Before(m.ExpiredAt) { s.log.Debug("monitoring has not finished yet", field.Any("form", f), field.ID(f.ID)) - util.SendErrorJSON(c, http.StatusAccepted, "monitoring has not finished yet", nil) + s.SendErrorJSON(c, http.StatusAccepted, "monitoring has not finished yet", nil) return } @@ -135,7 +136,7 @@ func (s *Server) GetMonitoring(c *gin.Context) { // TODO toooo-looong func need curCode, err := s.getCurrencyCodeById(ctx, m.CurrencyID) if err != nil { s.log.Debug("not found currency code by code id", field.Any("m", m), field.ID(f.ID)) - util.SendErrorJSON(c, http.StatusInternalServerError, "not found currency code by code id", nil) + s.SendErrorJSON(c, http.StatusInternalServerError, "not found currency code by code id", nil) return } @@ -143,7 +144,7 @@ func (s *Server) GetMonitoring(c *gin.Context) { // TODO toooo-looong func need freq, err := time.ParseDuration(m.Frequency) if err != nil { s.log.Error("bad value for frequency from db", field.ID(f.ID), field.Any("m", m), field.Error(err)) - util.SendErrorJSON(c, http.StatusInternalServerError, "bad value for frequency from db ", err) + s.SendErrorJSON(c, http.StatusInternalServerError, "bad value for frequency from db ", err) return } @@ -162,7 +163,7 @@ func (s *Server) GetMonitoring(c *gin.Context) { // TODO toooo-looong func need if err != nil { s.log.Error("can not get prices data for monitoring", field.ID(f.ID), field.Any("form", f), field.Error(err)) - util.SendErrorJSON(c, http.StatusInternalServerError, "can not get prices data for monitoring", err) + s.SendErrorJSON(c, http.StatusInternalServerError, "can not get prices data for monitoring", err) return } @@ -174,21 +175,20 @@ func (s *Server) GetMonitoring(c *gin.Context) { // TODO toooo-looong func need prices = applyFreqFilter(prices, freq) // TODO optional we can delete monitoring with that ID (auto clean table, good idea imho) - //_, err = s.storage.Connector().Repo(m).Delete(ctx, f.ID) - //if err != nil { - // s.log.Error("can not deleter monitoring", field.ID(f.ID), field.Any("form", f), field.Error(err)) - //} - - util.SendJSON(c, http.StatusOK, util.HTTPGoodResponse{ - Time: time.Now().UTC().Unix(), - UUID: helper.FromContextGetUUID(ctx), - Status: "Ok", - Description: "Result of monitoring (time in UTC)", - H: gin.H{ + if f.Delete { + _, err = s.storage.Connector().Repo(m).Delete(ctx, f.ID) + if err != nil { + s.log.Error("can not deleter monitoring", field.ID(f.ID), field.Any("form", f), field.Error(err)) + } + } + + s.SendJSON(c, http.StatusOK, "Result of monitoring (time in UTC)", + gin.H{ "MonitoringID": f.ID, + "StartAt": m.StartedAt, + "FinishedAt": m.ExpiredAt, "Prices": convertToResponsePrices(prices), - }, - }) + }) } func (s *Server) getCurrencyCodeById(ctx context.Context, id model.Identity) (string, error) { @@ -263,7 +263,7 @@ func (s *Server) PostMonitoring(c *gin.Context) { // TODO toooo-looong func nee periodDuration, err := time.ParseDuration(f.Period) if err != nil { s.log.Error("bad value for period", field.Any("form", f), field.Error(err)) - util.SendErrorJSON(c, http.StatusBadRequest, "bad value for period", err) + s.SendErrorJSON(c, http.StatusBadRequest, "bad value for period", err) return } @@ -271,14 +271,14 @@ func (s *Server) PostMonitoring(c *gin.Context) { // TODO toooo-looong func nee freqDur, err := time.ParseDuration(f.Frequency) if err != nil { s.log.Error("bad value for frequncy", field.Any("form", f), field.Error(err)) - util.SendErrorJSON(c, http.StatusBadRequest, "bad value for frequency", err) + s.SendErrorJSON(c, http.StatusBadRequest, "bad value for frequency", err) return } if f.Period == "" || f.Frequency == "" || f.Currency == "" { s.log.Error("bad value fin form", field.Any("form", f), field.Error(err)) - util.SendErrorJSON(c, http.StatusBadRequest, "bad values in form", err) + s.SendErrorJSON(c, http.StatusBadRequest, "bad values in form", err) return } @@ -286,7 +286,7 @@ func (s *Server) PostMonitoring(c *gin.Context) { // TODO toooo-looong func nee // TODO need clarify this, I add my constraints instead if periodDuration > time.Hour*24 || freqDur < time.Second { s.log.Error("period to much or freq too low", field.Any("form", f), field.Error(err)) - util.SendErrorJSON(c, http.StatusBadRequest, "freqDur", err) + s.SendErrorJSON(c, http.StatusBadRequest, "freqDur", err) return } @@ -299,7 +299,8 @@ func (s *Server) PostMonitoring(c *gin.Context) { // TODO toooo-looong func nee m.CurrencyID, err = s.getCurrencyIdByCurrencyCode(ctx, f.Currency) if err != nil { s.log.Error("can not get data currency data from db", field.Any("form", f), field.Error(err)) - util.SendErrorJSON(c, http.StatusInternalServerError, "can not get data currency data from db", err) + s.SendErrorJSON(c, http.StatusBadRequest, "can not get data currency data from db."+ + " probably you try to start monitoring unsupported currency", err) return } @@ -307,20 +308,15 @@ func (s *Server) PostMonitoring(c *gin.Context) { // TODO toooo-looong func nee id, err := s.storage.Connector().Repo(m).Create(ctx, m) if err != nil { s.log.Error("can not create new monitor obj", field.Any("m", m), field.Error(err)) - util.SendErrorJSON(c, http.StatusInternalServerError, "can not create new monitor obj", err) + s.SendErrorJSON(c, http.StatusInternalServerError, "can not create new monitor obj", err) return } - util.SendJSON(c, http.StatusOK, util.HTTPGoodResponse{ - Time: time.Now().UTC().Unix(), - UUID: helper.FromContextGetUUID(ctx), - Status: "Ok", - Description: "Successfully created new monitoring", - H: gin.H{ + s.SendJSON(c, http.StatusOK, "Successfully created new monitoring", + gin.H{ "MonitoringID": id, - }, - }) + }) } diff --git a/internal/servers/http/server.go b/internal/servers/http/server.go index 76e660a..aa2150e 100644 --- a/internal/servers/http/server.go +++ b/internal/servers/http/server.go @@ -26,6 +26,7 @@ type ( // Config - config struct. Config struct { Name string + NodeID string Address string DomainName string `yaml:"domainName"` AllowOrigin string `yaml:"allowOrigin"` @@ -117,8 +118,8 @@ func New( // Server's check handlers // todo metrics middleware -> mw.MetricsRPC("health", Health)) - e.GET("/health", Health) - e.GET("/ready", Readiness) + e.GET("/health", s.Health) + e.GET("/ready", s.Readiness) // Add a ginzap middleware, which: // - Logs all requests, like a combined access and error log. diff --git a/internal/servers/http/util/util.go b/internal/servers/http/util.go similarity index 64% rename from internal/servers/http/util/util.go rename to internal/servers/http/util.go index 6655e37..7da083a 100644 --- a/internal/servers/http/util/util.go +++ b/internal/servers/http/util.go @@ -1,4 +1,4 @@ -package util +package http import ( "time" @@ -8,14 +8,13 @@ import ( "github.com/imperiuse/price_monitor/internal/helper" ) -const HTTPStatusOk = "OK" - // HTTPGoodResponse - http good api response. type HTTPGoodResponse struct { xxxNoUnKeyLiteral [0]int // nolint Time int64 `json:"time,omitempty"` UUID string `json:"uuid,omitempty"` - Status string `json:"status,omitempty"` + NodeID string `json:"node_id,omitempty"` + Status int `json:"status,omitempty"` Description string `json:"description,omitempty"` gin.H } @@ -25,18 +24,26 @@ type HTTPErrorResponse struct { xxxNoUnKeyLiteral [0]int // nolint Time int64 `json:"time,omitempty"` UUID string `json:"uuid,omitempty"` + NodeID string `json:"node_id,omitempty"` Status int `json:"status,omitempty"` Desc string `json:"desc" example:"some human readable desc"` Err string `json:"err" example:"internal server error"` } -// SendJSON - send error data in json format with status AND Abort gin ctx. -func SendJSON(ctx *gin.Context, status int, response HTTPGoodResponse) { - ctx.JSON(status, response) +// SendJSON - send json (good response) +func (s *Server) SendJSON(ctx *gin.Context, status int, desc string, h gin.H) { + ctx.JSON(status, HTTPGoodResponse{ + Time: time.Now().UTC().Unix(), + UUID: helper.FromContextGetUUID(ctx), + NodeID: s.config.NodeID, + Status: status, + Description: desc, + H: h, + }) } // SendErrorJSON - send error data in json format with status AND Abort gin ctx. -func SendErrorJSON(ctx *gin.Context, status int, desc string, err error) { +func (s *Server) SendErrorJSON(ctx *gin.Context, status int, desc string, err error) { var errStr = "" if err != nil { errStr = err.Error() @@ -45,6 +52,7 @@ func SendErrorJSON(ctx *gin.Context, status int, desc string, err error) { ctx.JSON(status, HTTPErrorResponse{ Time: time.Now().UTC().Unix(), UUID: helper.FromContextGetUUID(ctx), + NodeID: s.config.NodeID, Status: status, Desc: desc, Err: errStr,