From 4d8b9119d8f0ae4c51a18ed3452ac4c6e90097fe Mon Sep 17 00:00:00 2001 From: Elchin Gasimov Date: Sat, 8 Jul 2023 00:49:38 +0400 Subject: [PATCH] Web-APP: Add gracefully shutdown, JSON structured logging --- cmd/aznum2words-webapp/.env.local | 0 .../api/converter-server.cfg.yaml | 0 .../api/converterapi/converter-api.go | 0 .../converterapi/converterapi-server.gen.go | 0 cmd/aznum2words-webapp/api/doc.go | 0 .../api/health-server.cfg.yaml | 0 .../api/healthapi/health-api.go | 0 .../api/healthapi/healthapi-server.gen.go | 0 cmd/aznum2words-webapp/api/models.cfg.yaml | 0 .../api/models/models.gen.go | 0 cmd/aznum2words-webapp/api/open-api-spec.yaml | 0 cmd/aznum2words-webapp/aznum2words-webapp.go | 57 ++++++++++++++++--- cmd/aznum2words-webapp/config/config.go | 6 +- cmd/aznum2words-webapp/constant/config.go | 4 ++ cmd/aznum2words-webapp/constant/error.go | 0 cmd/aznum2words-webapp/handler/handler.go | 0 .../aznum2words-client.gen.go | 0 .../it/aznum2wordsclient/client.cfg.yaml | 0 .../it/aznum2wordsclient/doc.go | 0 cmd/aznum2words-webapp/it/e2e_test.go | 0 cmd/aznum2words-webapp/logger/logging.go | 47 +++++++++++++++ cmd/aznum2words-webapp/router/router.go | 46 +++++++++++---- 22 files changed, 140 insertions(+), 20 deletions(-) mode change 100644 => 100755 cmd/aznum2words-webapp/.env.local mode change 100644 => 100755 cmd/aznum2words-webapp/api/converter-server.cfg.yaml mode change 100644 => 100755 cmd/aznum2words-webapp/api/converterapi/converter-api.go mode change 100644 => 100755 cmd/aznum2words-webapp/api/converterapi/converterapi-server.gen.go mode change 100644 => 100755 cmd/aznum2words-webapp/api/doc.go mode change 100644 => 100755 cmd/aznum2words-webapp/api/health-server.cfg.yaml mode change 100644 => 100755 cmd/aznum2words-webapp/api/healthapi/health-api.go mode change 100644 => 100755 cmd/aznum2words-webapp/api/healthapi/healthapi-server.gen.go mode change 100644 => 100755 cmd/aznum2words-webapp/api/models.cfg.yaml mode change 100644 => 100755 cmd/aznum2words-webapp/api/models/models.gen.go mode change 100644 => 100755 cmd/aznum2words-webapp/api/open-api-spec.yaml mode change 100644 => 100755 cmd/aznum2words-webapp/aznum2words-webapp.go mode change 100644 => 100755 cmd/aznum2words-webapp/config/config.go mode change 100644 => 100755 cmd/aznum2words-webapp/constant/config.go mode change 100644 => 100755 cmd/aznum2words-webapp/constant/error.go mode change 100644 => 100755 cmd/aznum2words-webapp/handler/handler.go mode change 100644 => 100755 cmd/aznum2words-webapp/it/aznum2wordsclient/aznum2words-client.gen.go mode change 100644 => 100755 cmd/aznum2words-webapp/it/aznum2wordsclient/client.cfg.yaml mode change 100644 => 100755 cmd/aznum2words-webapp/it/aznum2wordsclient/doc.go mode change 100644 => 100755 cmd/aznum2words-webapp/it/e2e_test.go create mode 100644 cmd/aznum2words-webapp/logger/logging.go mode change 100644 => 100755 cmd/aznum2words-webapp/router/router.go diff --git a/cmd/aznum2words-webapp/.env.local b/cmd/aznum2words-webapp/.env.local old mode 100644 new mode 100755 diff --git a/cmd/aznum2words-webapp/api/converter-server.cfg.yaml b/cmd/aznum2words-webapp/api/converter-server.cfg.yaml old mode 100644 new mode 100755 diff --git a/cmd/aznum2words-webapp/api/converterapi/converter-api.go b/cmd/aznum2words-webapp/api/converterapi/converter-api.go old mode 100644 new mode 100755 diff --git a/cmd/aznum2words-webapp/api/converterapi/converterapi-server.gen.go b/cmd/aznum2words-webapp/api/converterapi/converterapi-server.gen.go old mode 100644 new mode 100755 diff --git a/cmd/aznum2words-webapp/api/doc.go b/cmd/aznum2words-webapp/api/doc.go old mode 100644 new mode 100755 diff --git a/cmd/aznum2words-webapp/api/health-server.cfg.yaml b/cmd/aznum2words-webapp/api/health-server.cfg.yaml old mode 100644 new mode 100755 diff --git a/cmd/aznum2words-webapp/api/healthapi/health-api.go b/cmd/aznum2words-webapp/api/healthapi/health-api.go old mode 100644 new mode 100755 diff --git a/cmd/aznum2words-webapp/api/healthapi/healthapi-server.gen.go b/cmd/aznum2words-webapp/api/healthapi/healthapi-server.gen.go old mode 100644 new mode 100755 diff --git a/cmd/aznum2words-webapp/api/models.cfg.yaml b/cmd/aznum2words-webapp/api/models.cfg.yaml old mode 100644 new mode 100755 diff --git a/cmd/aznum2words-webapp/api/models/models.gen.go b/cmd/aznum2words-webapp/api/models/models.gen.go old mode 100644 new mode 100755 diff --git a/cmd/aznum2words-webapp/api/open-api-spec.yaml b/cmd/aznum2words-webapp/api/open-api-spec.yaml old mode 100644 new mode 100755 diff --git a/cmd/aznum2words-webapp/aznum2words-webapp.go b/cmd/aznum2words-webapp/aznum2words-webapp.go old mode 100644 new mode 100755 index 9f72a6b..f0db86d --- a/cmd/aznum2words-webapp/aznum2words-webapp.go +++ b/cmd/aznum2words-webapp/aznum2words-webapp.go @@ -1,15 +1,24 @@ package main import ( + "context" + "fmt" "github.com/egasimov/aznum2words/cmd/aznum2words-webapp/api/converterapi" "github.com/egasimov/aznum2words/cmd/aznum2words-webapp/api/healthapi" "github.com/egasimov/aznum2words/cmd/aznum2words-webapp/config" "github.com/egasimov/aznum2words/cmd/aznum2words-webapp/handler" + "github.com/egasimov/aznum2words/cmd/aznum2words-webapp/logger" "github.com/egasimov/aznum2words/cmd/aznum2words-webapp/router" + "net/http" + "os" + "os/signal" + "syscall" + "time" ) func main() { - config.LoadConfig() + logger := logger.Logger() + defer logger.Sync() // Create an instance of our handler which satisfies the generated interface var converterApi = new(converterapi.Api) @@ -24,13 +33,45 @@ func main() { routerPrometheus := router.NewPrometheusServer(routerApp) go func() { - routerPrometheus.Logger.Fatal(routerPrometheus.Start( - config.GetConfig().GetMetricNetListenAddr()), - ) + logger. + Info(fmt.Sprintf("prometheus server started at %s", config.GetConfig().GetAppNetListenAddr())) + + if err := routerPrometheus.Start(config.GetConfig().GetMetricNetListenAddr()); err != nil && err != http.ErrServerClosed { + routerPrometheus.Logger.Fatal("shutting down the prometheus server") + } + }() + + go func() { + logger. + Info(fmt.Sprintf("app server started at %s", config.GetConfig().GetAppNetListenAddr())) + + if err := routerApp.Start(config.GetConfig().GetAppNetListenAddr()); err != nil && err != http.ErrServerClosed { + routerApp.Logger.Fatal("shutting down the app server") + } }() - // And we serve HTTP until the world ends. - routerApp.Logger.Fatal(routerApp.Start( - config.GetConfig().GetAppNetListenAddr(), - )) + // Wait for interrupt signal to gracefully shut down the server with a timeout of 10 seconds. + // Use a buffered channel to avoid missing signals as recommended for signal.Notify + quit := make(chan os.Signal, 1) + signal.Notify(quit, os.Interrupt, syscall.SIGTERM, syscall.SIGKILL) + <-quit + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + if err := routerApp.Shutdown(ctx); err != nil { + logger. + Error(fmt.Sprintf("an error occurred when shutdown app server, err: %s", err)) + } else { + logger. + Info("gracefully shut downed app server") + } + + if err := routerPrometheus.Shutdown(ctx); err != nil { + logger. + Error(fmt.Sprintf("an error occurred when shutdown prometheus server, err: %s", err)) + } else { + logger. + Info("gracefully shut downed prometheus server") + } } diff --git a/cmd/aznum2words-webapp/config/config.go b/cmd/aznum2words-webapp/config/config.go old mode 100644 new mode 100755 index c331011..f2dd1e7 --- a/cmd/aznum2words-webapp/config/config.go +++ b/cmd/aznum2words-webapp/config/config.go @@ -11,9 +11,11 @@ import ( // Config holds a configuration variables. type Config struct { + AppName string `arg:"-n, env:APP_NAME" default:"aznum2words-webapp"` AppHost string `arg:"-h, env:APP_HOST" default:"0.0.0.0"` AppPort string `arg:"-p, env:APP_PORT" default:"8080"` MetricPort string `arg:"-m, env:METRIC_PORT" default:"9090"` + DeployEnv string `arg:"-d, env:DEPLOY_ENV" default:"local"` } func (this *Config) GetAppNetListenAddr() string { @@ -35,9 +37,9 @@ func (this *Config) GetMetricNetListenAddr() string { var conf *Config = &Config{} // LoadConfig ... -func LoadConfig() { +func init() { // Local development purpose - if env := os.Getenv(constant.DEPLOY_ENV_KEY); env == "" { + if env := os.Getenv(constant.DEPLOY_ENV_KEY); env == constant.LOCAL_ENVIRONMENT { errLoad := godotenv.Load(".env.local") if errLoad != nil { diff --git a/cmd/aznum2words-webapp/constant/config.go b/cmd/aznum2words-webapp/constant/config.go old mode 100644 new mode 100755 index 03fe334..5995fc6 --- a/cmd/aznum2words-webapp/constant/config.go +++ b/cmd/aznum2words-webapp/constant/config.go @@ -3,3 +3,7 @@ package constant const ( DEPLOY_ENV_KEY = "DEPLOY_ENV" ) + +const ( + LOCAL_ENVIRONMENT = "local" +) diff --git a/cmd/aznum2words-webapp/constant/error.go b/cmd/aznum2words-webapp/constant/error.go old mode 100644 new mode 100755 diff --git a/cmd/aznum2words-webapp/handler/handler.go b/cmd/aznum2words-webapp/handler/handler.go old mode 100644 new mode 100755 diff --git a/cmd/aznum2words-webapp/it/aznum2wordsclient/aznum2words-client.gen.go b/cmd/aznum2words-webapp/it/aznum2wordsclient/aznum2words-client.gen.go old mode 100644 new mode 100755 diff --git a/cmd/aznum2words-webapp/it/aznum2wordsclient/client.cfg.yaml b/cmd/aznum2words-webapp/it/aznum2wordsclient/client.cfg.yaml old mode 100644 new mode 100755 diff --git a/cmd/aznum2words-webapp/it/aznum2wordsclient/doc.go b/cmd/aznum2words-webapp/it/aznum2wordsclient/doc.go old mode 100644 new mode 100755 diff --git a/cmd/aznum2words-webapp/it/e2e_test.go b/cmd/aznum2words-webapp/it/e2e_test.go old mode 100644 new mode 100755 diff --git a/cmd/aznum2words-webapp/logger/logging.go b/cmd/aznum2words-webapp/logger/logging.go new file mode 100644 index 0000000..90fc68d --- /dev/null +++ b/cmd/aznum2words-webapp/logger/logging.go @@ -0,0 +1,47 @@ +package logger + +import ( + "github.com/egasimov/aznum2words/cmd/aznum2words-webapp/config" + "github.com/egasimov/aznum2words/cmd/aznum2words-webapp/constant" + "log" + "time" + + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +var logger *zap.Logger + +func init() { + encoderConfig := zap.NewProductionConfig() + encoderConfig.Level.SetLevel(zapcore.InfoLevel) + if config.GetConfig().DeployEnv == constant.LOCAL_ENVIRONMENT { + encoderConfig.Level.SetLevel(zapcore.DebugLevel) + } + encoderConfig.EncoderConfig.TimeKey = "timestamp" + encoderConfig.EncoderConfig.MessageKey = "message" + encoderConfig.EncoderConfig.EncodeTime = zapcore.TimeEncoderOfLayout(time.RFC3339Nano) + + zapLogger, err := encoderConfig.Build() + if err != nil { + log.Fatalf("fail to build log. err: %s", err) + } + + for k, v := range GetDefaultLogEntries() { + zapLogger = zapLogger. + With(zap.Any(k, v)) + } + + logger = zapLogger +} + +func Logger() *zap.Logger { + return logger +} + +func GetDefaultLogEntries() map[string]string { + return map[string]string{ + "app": config.GetConfig().AppName, + "env": config.GetConfig().DeployEnv, + } +} diff --git a/cmd/aznum2words-webapp/router/router.go b/cmd/aznum2words-webapp/router/router.go old mode 100644 new mode 100755 index e740c7a..f913573 --- a/cmd/aznum2words-webapp/router/router.go +++ b/cmd/aznum2words-webapp/router/router.go @@ -1,21 +1,18 @@ package router import ( + "fmt" + "github.com/egasimov/aznum2words/cmd/aznum2words-webapp/logger" "github.com/labstack/echo-contrib/prometheus" "github.com/labstack/echo/v4" echomiddleware "github.com/labstack/echo/v4/middleware" + "strings" ) -var loggerConfig = echomiddleware.LoggerConfig{ - Format: `{"time":"${time_rfc3339_nano}","x-correlation-id":"${header:x-correlation-id}","remote_ip":"${remote_ip}",` + - `"host":"${host}","method":"${method}","uri":"${uri}","user_agent":"${user_agent}",` + - `"status":${status},"error":"${error}","latency":${latency},"latency_human":"${latency_human}"}` + "\n", - CustomTimeFormat: "2006-01-02 15:04:05.00000", -} - func NewMainServer() *echo.Echo { echoMainServer := echo.New() echoMainServer.HideBanner = true + echoMainServer.HidePort = true echoMainServer.Use(echomiddleware.CORS()) echoMainServer.Use(echomiddleware.RequestIDWithConfig( @@ -23,7 +20,7 @@ func NewMainServer() *echo.Echo { TargetHeader: echo.HeaderXCorrelationID, })) // Log all requests - echoMainServer.Use(echomiddleware.LoggerWithConfig(loggerConfig)) + echoMainServer.Use(echomiddleware.LoggerWithConfig(getLoggerConfig())) return echoMainServer } @@ -32,14 +29,14 @@ func NewPrometheusServer(echoMainServer *echo.Echo) *echo.Echo { // Create Prometheus server and Middleware echoPrometheus := echo.New() echoPrometheus.HideBanner = true - + echoPrometheus.HidePort = true echoPrometheus.Use(echomiddleware.CORS()) echoPrometheus.Use(echomiddleware.RequestIDWithConfig( echomiddleware.RequestIDConfig{ TargetHeader: echo.HeaderXCorrelationID, })) // Log all requests - echoPrometheus.Use(echomiddleware.LoggerWithConfig(loggerConfig)) + echoPrometheus.Use(echomiddleware.LoggerWithConfig(getLoggerConfig())) prom := prometheus.NewPrometheus("echo", nil) @@ -51,3 +48,32 @@ func NewPrometheusServer(echoMainServer *echo.Echo) *echo.Echo { return echoPrometheus } + +func getLoggerConfig() echomiddleware.LoggerConfig { + return echomiddleware.LoggerConfig{ + Format: `{"time":"${time_rfc3339_nano}","x-correlation-id":"${header:x-correlation-id}","remote_ip":"${remote_ip}",` + + `"host":"${host}","method":"${method}","uri":"${uri}","user_agent":"${user_agent}",` + + addDefaultLogEntries() + + `"status":${status},"error":"${error}","latency":${latency},"latency_human":"${latency_human}"}` + "\n", + CustomTimeFormat: "2006-01-02 15:04:05.00000", + } +} + +func addDefaultLogEntries() string { + var logEntries []string + for k, v := range logger.GetDefaultLogEntries() { + logEntries = append(logEntries, addKeyValPair(k, v, true)) + } + + return strings.Join(logEntries, "") +} + +func addKeyValPair(keyName string, valName string, appendComma bool) string { + var result string + if appendComma { + result = fmt.Sprintf(`"%s":"%s",`, keyName, valName) + } else { + result = fmt.Sprintf(`"%s":"%s"`, keyName, valName) + } + return result +}