From 2a3ae6d45da2404da5823078fa9cf005e77fd8f3 Mon Sep 17 00:00:00 2001 From: bayusamudra5502 <bayusamudra.55.02.com@gmail.com> Date: Sat, 4 Feb 2023 15:27:14 +0700 Subject: [PATCH] feat: adding project structure --- .env | 5 + .gitignore | 2 +- Makefile | 6 +- di.go | 28 +++++ docs/base.go | 13 +++ docs/docs.go | 38 ++++++- docs/swagger.json | 38 ++++++- docs/swagger.yaml | 25 ++++- go.mod | 3 + go.sum | 6 ++ handler/common/handler.go | 13 +++ handler/common/home.go | 17 ++++ handler/common/types.go | 7 ++ handler/di.go | 17 ++++ handler/swagger/route.go | 3 + handler/swagger/swag.go | 20 ++++ handler/swagger/types.go | 8 ++ main.go | 12 ++- middleware/cleanpath/cleanpath.go | 13 +++ middleware/cors/cors.go | 22 ++++ middleware/di.go | 34 +++++++ middleware/log/log.go | 81 +++++++++++++++ middleware/middlewares.go | 22 ++++ middleware/recoverer/recover.go | 51 ++++++++++ middleware/register.go | 41 ++++++++ middleware/trailslash/trailslash.go | 13 +++ middleware/type.go | 13 +++ model/web/base_response.go | 7 ++ model/web/status.go | 35 +++++++ repository/di.go | 3 + routes/common/route.go | 22 ++++ routes/di.go | 19 ++++ routes/register.go | 27 +++++ routes/routes.go | 15 +++ routes/swagger/route.go | 15 +++ routes/type.go | 11 ++ service/common/home.go | 5 + service/common/service.go | 3 + service/common/type.go | 5 + service/di.go | 32 ++++++ service/logger/formatter.go | 28 +++++ service/logger/hooks/hooks.go | 21 ++++ service/logger/hooks/reporter.go | 33 ++++++ service/logger/logger.go | 50 +++++++++ service/logger/method.go | 17 ++++ service/logger/type.go | 8 ++ service/reporter/logtail.go | 153 ++++++++++++++++++++++++++++ service/reporter/type.go | 16 +++ utils/app/app.go | 50 +++++++++ utils/app/list.go | 71 +++++++++++++ utils/app/start.go | 78 ++++++++++++++ utils/app/type.go | 7 ++ utils/app/version.go | 33 ++++++ utils/di.go | 36 +++++++ utils/env/env.go | 46 +++++++++ utils/httputil/impl.go | 3 + utils/httputil/parse.go | 11 ++ utils/httputil/type.go | 9 ++ utils/httputil/write.go | 16 +++ utils/log/color.go | 27 +++++ utils/log/output.go | 31 ++++++ utils/log/type.go | 7 ++ utils/res/data/ascii.art | 8 ++ utils/res/data/version | 1 + utils/res/embed.go | 25 +++++ utils/res/res.go | 6 ++ utils/wrapper/type.go | 8 ++ utils/wrapper/wrapper.go | 21 ++++ 68 files changed, 1561 insertions(+), 9 deletions(-) create mode 100644 .env create mode 100644 di.go create mode 100644 docs/base.go create mode 100644 handler/common/handler.go create mode 100644 handler/common/home.go create mode 100644 handler/common/types.go create mode 100644 handler/di.go create mode 100644 handler/swagger/route.go create mode 100644 handler/swagger/swag.go create mode 100644 handler/swagger/types.go create mode 100644 middleware/cleanpath/cleanpath.go create mode 100644 middleware/cors/cors.go create mode 100644 middleware/di.go create mode 100644 middleware/log/log.go create mode 100644 middleware/middlewares.go create mode 100644 middleware/recoverer/recover.go create mode 100644 middleware/register.go create mode 100644 middleware/trailslash/trailslash.go create mode 100644 middleware/type.go create mode 100644 model/web/base_response.go create mode 100644 model/web/status.go create mode 100644 repository/di.go create mode 100644 routes/common/route.go create mode 100644 routes/di.go create mode 100644 routes/register.go create mode 100644 routes/routes.go create mode 100644 routes/swagger/route.go create mode 100644 routes/type.go create mode 100644 service/common/home.go create mode 100644 service/common/service.go create mode 100644 service/common/type.go create mode 100644 service/di.go create mode 100644 service/logger/formatter.go create mode 100644 service/logger/hooks/hooks.go create mode 100644 service/logger/hooks/reporter.go create mode 100644 service/logger/logger.go create mode 100644 service/logger/method.go create mode 100644 service/logger/type.go create mode 100644 service/reporter/logtail.go create mode 100644 service/reporter/type.go create mode 100644 utils/app/app.go create mode 100644 utils/app/list.go create mode 100644 utils/app/start.go create mode 100644 utils/app/type.go create mode 100644 utils/app/version.go create mode 100644 utils/di.go create mode 100644 utils/env/env.go create mode 100644 utils/httputil/impl.go create mode 100644 utils/httputil/parse.go create mode 100644 utils/httputil/type.go create mode 100644 utils/httputil/write.go create mode 100644 utils/log/color.go create mode 100644 utils/log/output.go create mode 100644 utils/log/type.go create mode 100644 utils/res/data/ascii.art create mode 100644 utils/res/data/version create mode 100644 utils/res/embed.go create mode 100644 utils/res/res.go create mode 100644 utils/wrapper/type.go create mode 100644 utils/wrapper/wrapper.go diff --git a/.env b/.env new file mode 100644 index 0000000..df1ee32 --- /dev/null +++ b/.env @@ -0,0 +1,5 @@ +ENV=DEVELOPMENT +LISTEN_ADDR=0.0.0.0 +PORT=8080 +LOGTAIL_TOKEN= +HTTP_SEC_TIMEOUT=2 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 5a10374..7e2965e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ bin/ tmp/ -app/wire_gen.go +wire_gen.go diff --git a/Makefile b/Makefile index 88ebcf6..6b33651 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,8 @@ all: build dependency: - # @swag init - # @wire ./app + @swag init + @wire run: dependency @go run . @@ -11,7 +11,7 @@ build: dependency @go build -o=bin/server.app . watch: - @air --build.cmd="make build" --build.bin="./bin/server.app" --build.exclude_dir="bin,tmp,docs" --build.exclude_file="app/wire_gen.go" + @air --build.cmd="make build" --build.bin="./bin/server.app" --build.exclude_dir="bin,tmp,docs" --build.exclude_file="wire_gen.go" test: dependency @go test ./test/... -v diff --git a/di.go b/di.go new file mode 100644 index 0000000..25590b9 --- /dev/null +++ b/di.go @@ -0,0 +1,28 @@ +//go:build wireinject +// +build wireinject + +package main + +import ( + "github.com/google/wire" + + "gitlab.informatika.org/ocw/ocw-backend/handler" + "gitlab.informatika.org/ocw/ocw-backend/middleware" + "gitlab.informatika.org/ocw/ocw-backend/routes" + "gitlab.informatika.org/ocw/ocw-backend/service" + "gitlab.informatika.org/ocw/ocw-backend/utils" + + "gitlab.informatika.org/ocw/ocw-backend/utils/app" +) + +func CreateServer() (app.Server, error) { + wire.Build( + utils.UtilSet, + handler.HandlerSet, + middleware.MiddlewareSet, + routes.RoutesSet, + service.ServiceSet, + ) + + return nil, nil +} diff --git a/docs/base.go b/docs/base.go new file mode 100644 index 0000000..88b5e59 --- /dev/null +++ b/docs/base.go @@ -0,0 +1,13 @@ +package docs + +import ( + "bytes" + _ "embed" +) + +//go:embed swagger.json +var jsonDefinition []byte + +func GetJsonSwagger() *bytes.Reader { + return bytes.NewReader(jsonDefinition) +} diff --git a/docs/docs.go b/docs/docs.go index 6952370..9ac0165 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -14,7 +14,43 @@ const docTemplate = `{ }, "host": "{{.Host}}", "basePath": "{{.BasePath}}", - "paths": {} + "paths": { + "/": { + "get": { + "description": "Give server index page response", + "produces": [ + "application/json" + ], + "summary": "Index page", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + } + } + } + } + }, + "definitions": { + "web.BaseResponse": { + "type": "object", + "properties": { + "data": {}, + "message": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "success", + "failed" + ] + } + } + } + } }` // SwaggerInfo holds exported Swagger Info so clients can modify it diff --git a/docs/swagger.json b/docs/swagger.json index ec416cd..c853049 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -3,5 +3,41 @@ "info": { "contact": {} }, - "paths": {} + "paths": { + "/": { + "get": { + "description": "Give server index page response", + "produces": [ + "application/json" + ], + "summary": "Index page", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/web.BaseResponse" + } + } + } + } + } + }, + "definitions": { + "web.BaseResponse": { + "type": "object", + "properties": { + "data": {}, + "message": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "success", + "failed" + ] + } + } + } + } } \ No newline at end of file diff --git a/docs/swagger.yaml b/docs/swagger.yaml index b64379c..59dad6b 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1,4 +1,27 @@ +definitions: + web.BaseResponse: + properties: + data: {} + message: + type: string + status: + enum: + - success + - failed + type: string + type: object info: contact: {} -paths: {} +paths: + /: + get: + description: Give server index page response + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/web.BaseResponse' + summary: Index page swagger: "2.0" diff --git a/go.mod b/go.mod index bc84614..dc2309a 100644 --- a/go.mod +++ b/go.mod @@ -6,13 +6,16 @@ require ( github.com/KyleBanks/depth v1.2.1 // indirect github.com/PuerkitoBio/purell v1.1.1 // indirect github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect + github.com/caarlos0/env/v6 v6.10.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/go-chi/chi/v5 v5.0.8 // indirect + github.com/go-chi/cors v1.2.1 // indirect github.com/go-openapi/jsonpointer v0.19.6 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/spec v0.20.8 // indirect github.com/go-openapi/swag v0.22.3 // indirect github.com/google/wire v0.5.0 // indirect + github.com/joho/godotenv v1.4.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect diff --git a/go.sum b/go.sum index c58c519..2814bd2 100644 --- a/go.sum +++ b/go.sum @@ -4,12 +4,16 @@ github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tN github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/caarlos0/env/v6 v6.10.1 h1:t1mPSxNpei6M5yAeu1qtRdPAK29Nbcf/n3G7x+b3/II= +github.com/caarlos0/env/v6 v6.10.1/go.mod h1:hvp/ryKXKipEkcuYjs9mI4bBCg+UI0Yhgm5Zu0ddvwc= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 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.8 h1:lD+NLqFcAi1ovnVZpsnObHGW4xb4J8lNmoYVfECH1Y0= github.com/go-chi/chi/v5 v5.0.8/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= +github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= @@ -33,6 +37,8 @@ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5a github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= github.com/google/wire v0.5.0 h1:I7ELFeVBr3yfPIcc8+MWvrjk+3VjbcSzoXm3JVa+jD8= github.com/google/wire v0.5.0/go.mod h1:ngWDr9Qvq3yZA10YrxfyGELY/AFWGVpy9c1LTRi1EoU= +github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= +github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= diff --git a/handler/common/handler.go b/handler/common/handler.go new file mode 100644 index 0000000..4b44093 --- /dev/null +++ b/handler/common/handler.go @@ -0,0 +1,13 @@ +package common + +import ( + "gitlab.informatika.org/ocw/ocw-backend/service/common" + "gitlab.informatika.org/ocw/ocw-backend/utils/httputil" + "gitlab.informatika.org/ocw/ocw-backend/utils/wrapper" +) + +type CommonHandlerImpl struct { + common.CommonService + httputil.HttpUtil + wrapper.WrapperUtil +} diff --git a/handler/common/home.go b/handler/common/home.go new file mode 100644 index 0000000..81fd72f --- /dev/null +++ b/handler/common/home.go @@ -0,0 +1,17 @@ +package common + +import ( + "net/http" +) + +// Index godoc +// +// @Summary Index page +// @Description Give server index page response +// @Produce json +// @Success 200 {object} web.BaseResponse +// @Router / [get] +func (route CommonHandlerImpl) Home(w http.ResponseWriter, r *http.Request) { + payload := route.WrapperUtil.SuccessResponseWrap(route.CommonService.Home()) + route.HttpUtil.WriteSuccessJson(w, payload) +} diff --git a/handler/common/types.go b/handler/common/types.go new file mode 100644 index 0000000..f799be2 --- /dev/null +++ b/handler/common/types.go @@ -0,0 +1,7 @@ +package common + +import "net/http" + +type CommonHandler interface { + Home(w http.ResponseWriter, r *http.Request) +} diff --git a/handler/di.go b/handler/di.go new file mode 100644 index 0000000..7c3cb64 --- /dev/null +++ b/handler/di.go @@ -0,0 +1,17 @@ +package handler + +import ( + "github.com/google/wire" + "gitlab.informatika.org/ocw/ocw-backend/handler/common" + "gitlab.informatika.org/ocw/ocw-backend/handler/swagger" +) + +var HandlerSet = wire.NewSet( + // Common + wire.Struct(new(common.CommonHandlerImpl), "*"), + wire.Bind(new(common.CommonHandler), new(*common.CommonHandlerImpl)), + + // Swagger + wire.Struct(new(swagger.SwaggerHandlerImpl), "*"), + wire.Bind(new(swagger.SwaggerHandler), new(*swagger.SwaggerHandlerImpl)), +) diff --git a/handler/swagger/route.go b/handler/swagger/route.go new file mode 100644 index 0000000..f5afd4f --- /dev/null +++ b/handler/swagger/route.go @@ -0,0 +1,3 @@ +package swagger + +type SwaggerHandlerImpl struct{} diff --git a/handler/swagger/swag.go b/handler/swagger/swag.go new file mode 100644 index 0000000..a5fa451 --- /dev/null +++ b/handler/swagger/swag.go @@ -0,0 +1,20 @@ +package swagger + +import ( + "net/http" + + httpSwagger "github.com/swaggo/http-swagger" + "gitlab.informatika.org/ocw/ocw-backend/docs" +) + +func (SwaggerHandlerImpl) Swagger(w http.ResponseWriter, r *http.Request) { + handler := httpSwagger.Handler() + handler(w, r) +} + +func (SwaggerHandlerImpl) SwaggerFile(w http.ResponseWriter, r *http.Request) { + stream := docs.GetJsonSwagger() + + w.WriteHeader(http.StatusOK) + stream.WriteTo(w) +} diff --git a/handler/swagger/types.go b/handler/swagger/types.go new file mode 100644 index 0000000..dc08889 --- /dev/null +++ b/handler/swagger/types.go @@ -0,0 +1,8 @@ +package swagger + +import "net/http" + +type SwaggerHandler interface { + Swagger(w http.ResponseWriter, r *http.Request) + SwaggerFile(w http.ResponseWriter, r *http.Request) +} diff --git a/main.go b/main.go index 54e1d9b..17e62fd 100644 --- a/main.go +++ b/main.go @@ -1,5 +1,13 @@ package main func main() { - println("Hello, World") -} \ No newline at end of file + server, err := CreateServer() + + if err != nil { + panic(err) + } + + server.Version() + server.ListRoute() + server.Start() +} diff --git a/middleware/cleanpath/cleanpath.go b/middleware/cleanpath/cleanpath.go new file mode 100644 index 0000000..7701e65 --- /dev/null +++ b/middleware/cleanpath/cleanpath.go @@ -0,0 +1,13 @@ +package cleanpath + +import ( + "net/http" + + "github.com/go-chi/chi/v5/middleware" +) + +type CleanPathMiddleware struct{} + +func (CleanPathMiddleware) Handle(next http.Handler) http.Handler { + return middleware.CleanPath(next) +} diff --git a/middleware/cors/cors.go b/middleware/cors/cors.go new file mode 100644 index 0000000..7be187f --- /dev/null +++ b/middleware/cors/cors.go @@ -0,0 +1,22 @@ +package cors + +import ( + "net/http" + + "github.com/go-chi/cors" +) + +type CorsMiddleware struct{} + +var corsHandler = cors.Handler(cors.Options{ + AllowedOrigins: []string{"http://*", "https://*"}, + AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, + AllowedHeaders: []string{"*"}, + ExposedHeaders: []string{"Link"}, + AllowCredentials: false, + MaxAge: 300, +}) + +func (CorsMiddleware) Handle(next http.Handler) http.Handler { + return corsHandler(next) +} diff --git a/middleware/di.go b/middleware/di.go new file mode 100644 index 0000000..3e73df4 --- /dev/null +++ b/middleware/di.go @@ -0,0 +1,34 @@ +package middleware + +import ( + "github.com/google/wire" + "gitlab.informatika.org/ocw/ocw-backend/middleware/cleanpath" + "gitlab.informatika.org/ocw/ocw-backend/middleware/cors" + "gitlab.informatika.org/ocw/ocw-backend/middleware/log" + "gitlab.informatika.org/ocw/ocw-backend/middleware/recoverer" + "gitlab.informatika.org/ocw/ocw-backend/middleware/trailslash" +) + +var middlewareCollectionSet = wire.NewSet( + // Cleanpath + wire.Struct(new(cleanpath.CleanPathMiddleware), "*"), + + // Cors + wire.Struct(new(cors.CorsMiddleware), "*"), + + // Log + wire.Struct(new(log.RequestLogMiddleware), "*"), + + // Recoverer + wire.Struct(new(recoverer.RecovererMiddleware), "*"), + + // Trailslash + wire.Struct(new(trailslash.TrailSlashMiddleware), "*"), +) + +var MiddlewareSet = wire.NewSet( + middlewareCollectionSet, + + wire.Struct(new(AppMiddlewares), "*"), + wire.Bind(new(MiddlewareCollection), new(*AppMiddlewares)), +) diff --git a/middleware/log/log.go b/middleware/log/log.go new file mode 100644 index 0000000..279d085 --- /dev/null +++ b/middleware/log/log.go @@ -0,0 +1,81 @@ +package log + +import ( + "fmt" + "net/http" + "time" + + chiMiddleware "github.com/go-chi/chi/v5/middleware" + "gitlab.informatika.org/ocw/ocw-backend/service/logger" + "gitlab.informatika.org/ocw/ocw-backend/utils/log" +) + +type RequestLogMiddleware struct { + LogUtil log.LogUtils + Logger logger.Logger +} + +var methodColor = map[string]log.Color{ + "GET": log.ForeGreen, + "POST": log.ForeBlue, + "PUT": log.ForeCyan, + "PATCH": log.ForeMagenta, + "DELETE": log.ForeRed, +} + +var statusColor = map[int]log.Color{ + 0: log.ForeCyan, + 200: log.ForeGreen, + 300: log.ForeBlue, + 400: log.ForeYellow, + 500: log.ForeRed, +} + +func (rl RequestLogMiddleware) colorizeMethod(method string) string { + val, ok := methodColor[method] + + if ok { + return rl.LogUtil.ColoredOutput(method, val) + } else { + return rl.LogUtil.ColoredOutput(method, log.ForeWhite) + } +} + +func (rl RequestLogMiddleware) colorizeCode(code int) string { + if code < 200 { + return rl.LogUtil.ColoredOutput(fmt.Sprint(code), statusColor[0]) + } else if code < 300 { + return rl.LogUtil.ColoredOutput(fmt.Sprint(code), statusColor[200]) + } else if code < 400 { + return rl.LogUtil.ColoredOutput(fmt.Sprint(code), statusColor[300]) + } else if code < 500 { + return rl.LogUtil.ColoredOutput(fmt.Sprint(code), statusColor[400]) + } else { + return rl.LogUtil.ColoredOutput(fmt.Sprint(code), statusColor[500]) + } +} + +func (rl RequestLogMiddleware) Handle(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + startTime := time.Now() + ww := chiMiddleware.NewWrapResponseWriter(w, r.ProtoMajor) + + defer func() { + delta := time.Since(startTime) + status := ww.Status() + path := r.URL.Path + method := r.Method + + rl.Logger.Info( + fmt.Sprintf("Request %s %s %s (%dms)", + rl.colorizeCode(status), + rl.colorizeMethod(method), + path, + delta.Milliseconds(), + ), + ) + }() + + next.ServeHTTP(ww, r) + }) +} diff --git a/middleware/middlewares.go b/middleware/middlewares.go new file mode 100644 index 0000000..b41cf45 --- /dev/null +++ b/middleware/middlewares.go @@ -0,0 +1,22 @@ +package middleware + +import ( + "gitlab.informatika.org/ocw/ocw-backend/middleware/cleanpath" + "gitlab.informatika.org/ocw/ocw-backend/middleware/cors" + "gitlab.informatika.org/ocw/ocw-backend/middleware/log" + "gitlab.informatika.org/ocw/ocw-backend/middleware/recoverer" + "gitlab.informatika.org/ocw/ocw-backend/middleware/trailslash" + "gitlab.informatika.org/ocw/ocw-backend/service/logger" +) + +type AppMiddlewares struct { + // Registered Middleware + recoverer.RecovererMiddleware + cors.CorsMiddleware + log.RequestLogMiddleware + trailslash.TrailSlashMiddleware + cleanpath.CleanPathMiddleware + + // Utility + Logger logger.Logger +} diff --git a/middleware/recoverer/recover.go b/middleware/recoverer/recover.go new file mode 100644 index 0000000..7c7b5b1 --- /dev/null +++ b/middleware/recoverer/recover.go @@ -0,0 +1,51 @@ +package recoverer + +import ( + "encoding/json" + "fmt" + "net/http" + "runtime/debug" + "strings" + + "gitlab.informatika.org/ocw/ocw-backend/service/logger" + "gitlab.informatika.org/ocw/ocw-backend/utils/wrapper" +) + +type RecovererMiddleware struct { + Logger logger.Logger + wrapper.WrapperUtil +} + +func (rm RecovererMiddleware) Handle(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + defer func() { + if rec := recover(); rec != nil { + parser := json.NewEncoder(w) + + w.WriteHeader(http.StatusInternalServerError) + payload := rm.WrapperUtil.ErrorResponseWrap("internal server error", nil) + + err := parser.Encode(payload) + + if err != nil { + rm.Logger.Error("Failed to parse error:" + err.Error()) + rm.Logger.Error("") + + return + } + + stacks := strings.Split(string(debug.Stack()), "\n") + rm.Logger.Error("Some panic occured when processing request:") + rm.Logger.Error(fmt.Sprint(rec)) + rm.Logger.Error("") + + rm.Logger.Error("Stack Trace:") + for _, val := range stacks { + rm.Logger.Error(val) + } + } + }() + + next.ServeHTTP(w, r) + }) +} diff --git a/middleware/register.go b/middleware/register.go new file mode 100644 index 0000000..998ed3e --- /dev/null +++ b/middleware/register.go @@ -0,0 +1,41 @@ +package middleware + +import ( + "reflect" +) + +func (app *AppMiddlewares) Register() []Middleware { + reflectValue := reflect.ValueOf(app) + + if reflectValue.Kind() == reflect.Ptr { + reflectValue = reflectValue.Elem() + } + + var reflectType = reflectValue.Type() + collections := []Middleware{} + + middlewareName := []string{} + + for i := 0; i < reflectValue.NumField(); i++ { + field := reflectValue.Field(i) + handler, ok := field.Interface().(Middleware) + + if !ok { + continue + } + + middlewareName = append(middlewareName, reflectType.Field(i).Name) + collections = append(collections, handler) + } + + if len(middlewareName) > 0 { + app.Logger.Info("Registered Middlewares:") + for _, middleware := range middlewareName { + app.Logger.Info("- " + middleware) + } + } else { + app.Logger.Info("No middleware registered") + } + + return collections +} diff --git a/middleware/trailslash/trailslash.go b/middleware/trailslash/trailslash.go new file mode 100644 index 0000000..7bcb14c --- /dev/null +++ b/middleware/trailslash/trailslash.go @@ -0,0 +1,13 @@ +package trailslash + +import ( + "net/http" + + "github.com/go-chi/chi/v5/middleware" +) + +type TrailSlashMiddleware struct{} + +func (TrailSlashMiddleware) Handle(next http.Handler) http.Handler { + return middleware.RedirectSlashes(next) +} diff --git a/middleware/type.go b/middleware/type.go new file mode 100644 index 0000000..3c4b474 --- /dev/null +++ b/middleware/type.go @@ -0,0 +1,13 @@ +package middleware + +import ( + "net/http" +) + +type MiddlewareCollection interface { + Register() []Middleware +} + +type Middleware interface { + Handle(next http.Handler) http.Handler +} diff --git a/model/web/base_response.go b/model/web/base_response.go new file mode 100644 index 0000000..c752720 --- /dev/null +++ b/model/web/base_response.go @@ -0,0 +1,7 @@ +package web + +type BaseResponse struct { + Status Status `json:"status" swaggertype:"primitive,string" enums:"success,failed"` + Message string `json:"message"` + Data interface{} `json:"data"` +} diff --git a/model/web/status.go b/model/web/status.go new file mode 100644 index 0000000..128aea5 --- /dev/null +++ b/model/web/status.go @@ -0,0 +1,35 @@ +package web + +import ( + "errors" +) + +type Status uint + +const ( + Success Status = iota + Failed +) + +func (s Status) MarshalJSON() ([]byte, error) { + switch s { + case Success: + return []byte("\"success\""), nil + case Failed: + return []byte("\"failed\""), nil + } + + return nil, errors.New("unkown value") +} + +func (s *Status) UnmarshalJSON(data []byte) error { + if string(data) == "\"success\"" { + *s = Success + return nil + } else if string(data) == "\"failed\"" { + *s = Failed + return nil + } + + return errors.New("Unkown type of " + string(data)) +} diff --git a/repository/di.go b/repository/di.go new file mode 100644 index 0000000..f9a484e --- /dev/null +++ b/repository/di.go @@ -0,0 +1,3 @@ +package repository + +// var RepositorySet = wire.Value() diff --git a/routes/common/route.go b/routes/common/route.go new file mode 100644 index 0000000..d57f0c1 --- /dev/null +++ b/routes/common/route.go @@ -0,0 +1,22 @@ +package common + +import ( + "github.com/go-chi/chi/v5" + "gitlab.informatika.org/ocw/ocw-backend/service/common" + "gitlab.informatika.org/ocw/ocw-backend/utils/httputil" + "gitlab.informatika.org/ocw/ocw-backend/utils/wrapper" + + commonHandler "gitlab.informatika.org/ocw/ocw-backend/handler/common" +) + +type CommonRoutes struct { + common.CommonService + httputil.HttpUtil + wrapper.WrapperUtil + commonHandler.CommonHandler +} + +func (cr CommonRoutes) Register(r chi.Router) { + r.Get("/", cr.CommonHandler.Home) + r.Get("/test", cr.CommonHandler.Home) +} diff --git a/routes/di.go b/routes/di.go new file mode 100644 index 0000000..93a65f0 --- /dev/null +++ b/routes/di.go @@ -0,0 +1,19 @@ +package routes + +import ( + "github.com/google/wire" + "gitlab.informatika.org/ocw/ocw-backend/routes/common" + "gitlab.informatika.org/ocw/ocw-backend/routes/swagger" +) + +var routesCollectionSet = wire.NewSet( + wire.Struct(new(common.CommonRoutes), "*"), + wire.Struct(new(swagger.SwaggerRoutes), "*"), +) + +var RoutesSet = wire.NewSet( + routesCollectionSet, + + wire.Struct(new(AppRouter), "*"), + wire.Bind(new(RouteCollection), new(*AppRouter)), +) diff --git a/routes/register.go b/routes/register.go new file mode 100644 index 0000000..e1dd4a4 --- /dev/null +++ b/routes/register.go @@ -0,0 +1,27 @@ +package routes + +import ( + "reflect" +) + +func (app *AppRouter) Register() []RouteGroup { + reflectValue := reflect.ValueOf(app) + + if reflectValue.Kind() == reflect.Ptr { + reflectValue = reflectValue.Elem() + } + + collections := []RouteGroup{} + + for i := 0; i < reflectValue.NumField(); i++ { + handler, ok := reflectValue.Field(i).Interface().(RouteGroup) + + if !ok { + continue + } + + collections = append(collections, handler) + } + + return collections +} diff --git a/routes/routes.go b/routes/routes.go new file mode 100644 index 0000000..636fc01 --- /dev/null +++ b/routes/routes.go @@ -0,0 +1,15 @@ +package routes + +import ( + "gitlab.informatika.org/ocw/ocw-backend/routes/common" + "gitlab.informatika.org/ocw/ocw-backend/routes/swagger" + + "gitlab.informatika.org/ocw/ocw-backend/service/logger" +) + +type AppRouter struct { + common.CommonRoutes + swagger.SwaggerRoutes + + Logger logger.Logger +} diff --git a/routes/swagger/route.go b/routes/swagger/route.go new file mode 100644 index 0000000..345aa7e --- /dev/null +++ b/routes/swagger/route.go @@ -0,0 +1,15 @@ +package swagger + +import ( + "github.com/go-chi/chi/v5" + "gitlab.informatika.org/ocw/ocw-backend/handler/swagger" +) + +type SwaggerRoutes struct { + swagger.SwaggerHandler +} + +func (sr SwaggerRoutes) Register(r chi.Router) { + r.Get("/docs", sr.SwaggerHandler.SwaggerFile) + r.Get("/docs/*", sr.SwaggerHandler.Swagger) +} diff --git a/routes/type.go b/routes/type.go new file mode 100644 index 0000000..ee6a9db --- /dev/null +++ b/routes/type.go @@ -0,0 +1,11 @@ +package routes + +import "github.com/go-chi/chi/v5" + +type RouteCollection interface { + Register() []RouteGroup +} + +type RouteGroup interface { + Register(chi.Router) +} diff --git a/service/common/home.go b/service/common/home.go new file mode 100644 index 0000000..d78a894 --- /dev/null +++ b/service/common/home.go @@ -0,0 +1,5 @@ +package common + +func (CommonServiceImpl) Home() string { + return "Server is running 🙂" +} diff --git a/service/common/service.go b/service/common/service.go new file mode 100644 index 0000000..dd8d182 --- /dev/null +++ b/service/common/service.go @@ -0,0 +1,3 @@ +package common + +type CommonServiceImpl struct{} diff --git a/service/common/type.go b/service/common/type.go new file mode 100644 index 0000000..bb73f87 --- /dev/null +++ b/service/common/type.go @@ -0,0 +1,5 @@ +package common + +type CommonService interface { + Home() string +} diff --git a/service/di.go b/service/di.go new file mode 100644 index 0000000..349e0a4 --- /dev/null +++ b/service/di.go @@ -0,0 +1,32 @@ +package service + +import ( + "github.com/google/wire" + "gitlab.informatika.org/ocw/ocw-backend/service/common" + "gitlab.informatika.org/ocw/ocw-backend/service/logger" + "gitlab.informatika.org/ocw/ocw-backend/service/logger/hooks" + "gitlab.informatika.org/ocw/ocw-backend/service/reporter" +) + +var ServiceSet = wire.NewSet( + // Common service + wire.NewSet( + wire.Struct(new(common.CommonServiceImpl), "*"), + wire.Bind(new(common.CommonService), new(*common.CommonServiceImpl)), + ), + + // Logger service + wire.NewSet( + logger.New, + hooks.NewHookCollection, + wire.Struct(new(hooks.LogrusReporter), "*"), + wire.Struct(new(logger.LogrusFormatter), "*"), + wire.Bind(new(logger.Logger), new(*logger.LogrusLogger)), + ), + + // Reporter service + wire.NewSet( + reporter.New, + wire.Bind(new(reporter.Reporter), new(*reporter.LogtailReporter)), + ), +) diff --git a/service/logger/formatter.go b/service/logger/formatter.go new file mode 100644 index 0000000..bdaa27d --- /dev/null +++ b/service/logger/formatter.go @@ -0,0 +1,28 @@ +package logger + +import ( + "github.com/sirupsen/logrus" + "gitlab.informatika.org/ocw/ocw-backend/utils/log" +) + +type LogrusFormatter struct { + Util log.LogUtils +} + +var colorMap = map[logrus.Level]log.Color{ + logrus.TraceLevel: log.ForeWhite, + logrus.DebugLevel: log.ForeWhite, + logrus.InfoLevel: log.ForeGreen, + logrus.WarnLevel: log.ForeYellow, + logrus.ErrorLevel: log.ForeRed, + logrus.PanicLevel: log.ForeRed, +} + +func (l *LogrusFormatter) Format(entry *logrus.Entry) ([]byte, error) { + return []byte(l.Util.FormattedOutput( + entry.Message, + "App", + entry.Level.String(), + colorMap[entry.Level], + )), nil +} diff --git a/service/logger/hooks/hooks.go b/service/logger/hooks/hooks.go new file mode 100644 index 0000000..3fe1856 --- /dev/null +++ b/service/logger/hooks/hooks.go @@ -0,0 +1,21 @@ +package hooks + +import "github.com/sirupsen/logrus" + +type LogrusHookCollection []LogrusLogHook + +type LogrusLogHook struct { + Hook logrus.Hook + IsProductionOnly bool +} + +func NewHookCollection( + reporter LogrusReporter, +) LogrusHookCollection { + return []LogrusLogHook{ + { + IsProductionOnly: true, + Hook: reporter, + }, + } +} diff --git a/service/logger/hooks/reporter.go b/service/logger/hooks/reporter.go new file mode 100644 index 0000000..cb08c25 --- /dev/null +++ b/service/logger/hooks/reporter.go @@ -0,0 +1,33 @@ +package hooks + +import ( + "time" + + "github.com/sirupsen/logrus" + "gitlab.informatika.org/ocw/ocw-backend/service/reporter" +) + +type LogrusReporter struct { + Reporter reporter.Reporter +} + +func (LogrusReporter) Levels() []logrus.Level { + return []logrus.Level{ + logrus.PanicLevel, + logrus.FatalLevel, + logrus.ErrorLevel, + logrus.WarnLevel, + logrus.InfoLevel, + } +} + +func (l LogrusReporter) Fire(entry *logrus.Entry) error { + payload := reporter.ReporterPayload{ + Level: entry.Level.String(), + Timestamp: entry.Time.Format(time.RFC3339), + Message: entry.Message, + } + + l.Reporter.Send(payload) + return nil +} diff --git a/service/logger/logger.go b/service/logger/logger.go new file mode 100644 index 0000000..96686e9 --- /dev/null +++ b/service/logger/logger.go @@ -0,0 +1,50 @@ +package logger + +import ( + "github.com/sirupsen/logrus" + "gitlab.informatika.org/ocw/ocw-backend/service/logger/hooks" + "gitlab.informatika.org/ocw/ocw-backend/utils/env" + "gitlab.informatika.org/ocw/ocw-backend/utils/log" +) + +type LogrusLogger struct { + logger *logrus.Logger +} + +// Object Builder +var createdLogger *LogrusLogger = nil + +func New( + env *env.Environment, + logUtil log.LogUtils, + hooks hooks.LogrusHookCollection, +) *LogrusLogger { + if createdLogger != nil { + return createdLogger + } + + log := logrus.New() + + log.SetFormatter(&LogrusFormatter{ + Util: logUtil, + }) + + for _, hook := range hooks { + if hook.IsProductionOnly && env.AppEnvironment == "PRODUCTION" || + !hook.IsProductionOnly { + log.AddHook(hook.Hook) + } + } + + if env.AppEnvironment == "PRODUCTION" { + log.SetLevel(logrus.InfoLevel) + } else { + log.SetLevel(logrus.DebugLevel) + } + + createdLogger = &LogrusLogger{ + logger: log, + } + + return createdLogger +} diff --git a/service/logger/method.go b/service/logger/method.go new file mode 100644 index 0000000..439b406 --- /dev/null +++ b/service/logger/method.go @@ -0,0 +1,17 @@ +package logger + +func (l *LogrusLogger) Debug(message string) { + l.logger.Debug(message) +} + +func (l *LogrusLogger) Info(message string) { + l.logger.Info(message) +} + +func (l *LogrusLogger) Warning(message string) { + l.logger.Warn(message) +} + +func (l *LogrusLogger) Error(message string) { + l.logger.Error(message) +} diff --git a/service/logger/type.go b/service/logger/type.go new file mode 100644 index 0000000..b53578c --- /dev/null +++ b/service/logger/type.go @@ -0,0 +1,8 @@ +package logger + +type Logger interface { + Debug(message string) + Info(message string) + Warning(message string) + Error(message string) +} diff --git a/service/reporter/logtail.go b/service/reporter/logtail.go new file mode 100644 index 0000000..f156450 --- /dev/null +++ b/service/reporter/logtail.go @@ -0,0 +1,153 @@ +package reporter + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + "sync" + "time" + + "gitlab.informatika.org/ocw/ocw-backend/utils/env" + "gitlab.informatika.org/ocw/ocw-backend/utils/log" +) + +type LogtailReporter struct { + env *env.Environment + logUtil log.LogUtils + logQueue []ReporterPayload + mutex sync.Mutex + httpClient *http.Client + isStarted bool +} + +func New( + env *env.Environment, + logUtil log.LogUtils, +) *LogtailReporter { + return &LogtailReporter{ + env, + logUtil, + []ReporterPayload{}, + sync.Mutex{}, + &http.Client{ + Transport: &http.Transport{ + IdleConnTimeout: time.Duration(env.HttpReqTimeout) * time.Second, + }, + }, + false, + } +} + +func (l *LogtailReporter) Send(payload ReporterPayload) { + if !l.isStarted { + return + } + + l.mutex.Lock() + defer l.mutex.Unlock() + + l.logQueue = append(l.logQueue, payload) +} + +func (l *LogtailReporter) Flush() { + l.mutex.Lock() + defer l.mutex.Unlock() + + payloadBytes, err := json.Marshal(l.logQueue) + + if err != nil { + l.logUtil.PrintFormattedOutput( + fmt.Sprintf("Some error happened when parse json: %s", err), + "REPORT", + "ERROR", + log.ForeRed, + ) + } else { + l.logQueue = []ReporterPayload{} + + go func() { + reader := bytes.NewReader(payloadBytes) + req, err := http.NewRequest("POST", "https://in.logtail.com", reader) + + if err != nil { + l.logUtil.PrintFormattedOutput( + fmt.Sprintf("Some error happened when creating request: %s", err), + "REPORT", + "ERROR", + log.ForeRed, + ) + return + } + + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", l.env.LogtailToken)) + req.Header.Add("Content-Type", "application/json") + + res, err := l.httpClient.Do(req) + + if err != nil { + l.logUtil.PrintFormattedOutput( + fmt.Sprintf("Some error happened when sending request: %s", err), + "REPORT", + "ERROR", + log.ForeRed, + ) + return + } + + if res.StatusCode != http.StatusOK { + l.logUtil.PrintFormattedOutput( + fmt.Sprintf("Request respose is not 200 OK: got %d", res.StatusCode), + "REPORT", + "ERROR", + log.ForeRed, + ) + } + }() + } + +} + +func (l *LogtailReporter) Start(ctx context.Context) { + if l.env.AppEnvironment != "PRODUCTION" { + l.logUtil.PrintFormattedOutput( + "Reporter is not started due to non-production environment", + "REPORT", + "WARNING", + log.ForeYellow) + return + } + + go func() { + l.isStarted = true + defer func() { l.isStarted = false }() + defer l.Flush() + + timer := time.NewTicker(time.Second) + defer timer.Stop() + + l.logUtil.PrintFormattedOutput( + "Reporter started to listen...", + "REPORT", + "INFO", + log.ForeBlue, + ) + + for { + select { + case <-ctx.Done(): + break + case <-timer.C: + l.Flush() + } + } + }() +} + +func (l *LogtailReporter) Clear() { + l.mutex.Lock() + defer l.mutex.Unlock() + + l.logQueue = []ReporterPayload{} +} diff --git a/service/reporter/type.go b/service/reporter/type.go new file mode 100644 index 0000000..a910ed0 --- /dev/null +++ b/service/reporter/type.go @@ -0,0 +1,16 @@ +package reporter + +import "context" + +type ReporterPayload struct { + Timestamp string `json:"dt"` + Level string `json:"level"` + Message string `json:"message"` +} + +type Reporter interface { + Send(payload ReporterPayload) + Flush() + Start(ctx context.Context) + Clear() +} diff --git a/utils/app/app.go b/utils/app/app.go new file mode 100644 index 0000000..dbde6d4 --- /dev/null +++ b/utils/app/app.go @@ -0,0 +1,50 @@ +package app + +import ( + "github.com/go-chi/chi/v5" + "gitlab.informatika.org/ocw/ocw-backend/middleware" + "gitlab.informatika.org/ocw/ocw-backend/routes" + "gitlab.informatika.org/ocw/ocw-backend/service/logger" + "gitlab.informatika.org/ocw/ocw-backend/service/reporter" + "gitlab.informatika.org/ocw/ocw-backend/utils/env" + "gitlab.informatika.org/ocw/ocw-backend/utils/log" + "gitlab.informatika.org/ocw/ocw-backend/utils/res" +) + +type HttpServer struct { + server *chi.Mux + log logger.Logger + logUtil log.LogUtils + res res.Resource + env *env.Environment + reporter reporter.Reporter +} + +func New( + middlewares middleware.MiddlewareCollection, + routes routes.RouteCollection, + env *env.Environment, + log logger.Logger, + logUtil log.LogUtils, + res res.Resource, + reporter reporter.Reporter, +) *HttpServer { + r := chi.NewRouter() + + for _, handler := range middlewares.Register() { + r.Use(handler.Handle) + } + + for _, group := range routes.Register() { + r.Group(group.Register) + } + + return &HttpServer{ + server: r, + log: log, + res: res, + logUtil: logUtil, + env: env, + reporter: reporter, + } +} diff --git a/utils/app/list.go b/utils/app/list.go new file mode 100644 index 0000000..481a1ad --- /dev/null +++ b/utils/app/list.go @@ -0,0 +1,71 @@ +package app + +import ( + "fmt" + + "gitlab.informatika.org/ocw/ocw-backend/utils/log" +) + +var colorMap = map[string][]log.Color{ + "GET": {log.BackGreen, log.ForeWhite}, + "POST": {log.BackBlue, log.ForeWhite}, + "PUT": {log.BackCyan, log.ForeWhite}, + "PATCH": {log.BackMagenta, log.ForeWhite}, + "DELETE": {log.BackRed, log.ForeWhite}, +} + +func colorizeMethod(name string) string { + res := "" + val, ok := colorMap[name] + + if ok { + res = string(val[0]) + string(val[1]) + } else { + res = string(log.ForeBlack) + string(log.BackWhite) + } + + res = res + " " + + res = res + name + + for i := 0; i < 8-(len(name)+1); i++ { + res = res + " " + } + + return res + string(log.Reset) +} + +func (l HttpServer) ListRoute() { + routeData := map[string][]string{} + + for _, route := range l.server.Routes() { + for method := range route.Handlers { + name := route.Pattern + + if routeData[method] == nil { + routeData[method] = []string{name} + } else { + routeData[method] = append(routeData[method], name) + } + } + } + + l.log.Info("Routes Information:") + l.log.Info("") + + loggedMethod := []string{ + "GET", "POST", "PUT", "PATCH", "DELETE", + } + + for _, method := range loggedMethod { + for _, pattern := range routeData[method] { + l.log.Info( + fmt.Sprintf("%s %s", + colorizeMethod(method), + pattern), + ) + } + } + + l.log.Info("") +} diff --git a/utils/app/start.go b/utils/app/start.go new file mode 100644 index 0000000..1ea8ecd --- /dev/null +++ b/utils/app/start.go @@ -0,0 +1,78 @@ +package app + +import ( + "context" + "fmt" + "net/http" + "os" + "os/signal" + "sync" + "syscall" + "time" + + "gitlab.informatika.org/ocw/ocw-backend/utils/log" +) + +func (l *HttpServer) Start() { + listenAddr := fmt.Sprintf("%s:%d", l.env.ListenAddress, l.env.ListenPort) + + server := &http.Server{ + Addr: listenAddr, + Handler: l.server, + } + + serverCtx, cancelServer := context.WithCancel(context.Background()) + l.reporter.Start(serverCtx) + + sig := make(chan os.Signal, 3) + signal.Notify(sig, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) + + go func() { + defer cancelServer() + defer l.log.Info("🛑 Server is successfully shut down") + + <-sig + + forceQuit, cancelForceQuit := context.WithTimeout(context.Background(), 30*time.Second) + defer cancelForceQuit() + + group := sync.WaitGroup{} + group.Add(1) + + go func() { + defer group.Done() + + l.log.Info("â±ï¸ Gracefully shutdown....") + <-forceQuit.Done() + + if forceQuit.Err() == context.DeadlineExceeded { + l.log.Error("â±ï¸ Waiting timeout, force shutdown...") + } + }() + + err := server.Shutdown(forceQuit) + if err != nil { + l.log.Error(err.Error()) + } + + cancelForceQuit() + group.Wait() + }() + + l.log.Info(fmt.Sprintf("🌎 Server Listen at %s", + l.logUtil.ColoredOutput( + "http://"+listenAddr, + log.ForeGreen, + ), + )) + + err := server.ListenAndServe() + + if err != nil && err != http.ErrServerClosed { + l.log.Error("🔥 Failed to start server") + l.log.Error(err.Error()) + os.Exit(1) + } + + <-serverCtx.Done() +} diff --git a/utils/app/type.go b/utils/app/type.go new file mode 100644 index 0000000..92bc9d5 --- /dev/null +++ b/utils/app/type.go @@ -0,0 +1,7 @@ +package app + +type Server interface { + Start() + ListRoute() + Version() +} diff --git a/utils/app/version.go b/utils/app/version.go new file mode 100644 index 0000000..6b3cac8 --- /dev/null +++ b/utils/app/version.go @@ -0,0 +1,33 @@ +package app + +import ( + "fmt" + + "gitlab.informatika.org/ocw/ocw-backend/utils/log" +) + +func (l HttpServer) Version() { + data, err := l.res.GetStringResource("ascii.art") + + if err == nil { + fmt.Println( + l.logUtil.ColoredOutput( + data, + log.ForeGreen, + ), + ) + println() + } + + data, err = l.res.GetStringResource("version") + + if err == nil { + fmt.Println( + l.logUtil.ColoredOutput( + data, + log.ForeCyan, + ), + ) + println() + } +} diff --git a/utils/di.go b/utils/di.go new file mode 100644 index 0000000..ac8adc5 --- /dev/null +++ b/utils/di.go @@ -0,0 +1,36 @@ +package utils + +import ( + "github.com/google/wire" + "gitlab.informatika.org/ocw/ocw-backend/utils/app" + "gitlab.informatika.org/ocw/ocw-backend/utils/env" + "gitlab.informatika.org/ocw/ocw-backend/utils/httputil" + "gitlab.informatika.org/ocw/ocw-backend/utils/log" + "gitlab.informatika.org/ocw/ocw-backend/utils/res" + "gitlab.informatika.org/ocw/ocw-backend/utils/wrapper" +) + +var UtilSet = wire.NewSet( + // env + env.New, + + // httputil utility + wire.Struct(new(httputil.HttpUtilImpl), "*"), + wire.Bind(new(httputil.HttpUtil), new(*httputil.HttpUtilImpl)), + + // log utility + wire.Struct(new(log.LogUtilsImpl), "*"), + wire.Bind(new(log.LogUtils), new(*log.LogUtilsImpl)), + + // res utility + wire.Struct(new(res.EmbedResources), "*"), + wire.Bind(new(res.Resource), new(*res.EmbedResources)), + + // wrapper utility + wire.Struct(new(wrapper.WrapperUtilImpl), "*"), + wire.Bind(new(wrapper.WrapperUtil), new(*wrapper.WrapperUtilImpl)), + + // app + app.New, + wire.Bind(new(app.Server), new(*app.HttpServer)), +) diff --git a/utils/env/env.go b/utils/env/env.go new file mode 100644 index 0000000..a249e0f --- /dev/null +++ b/utils/env/env.go @@ -0,0 +1,46 @@ +package env + +import ( + "os" + + "github.com/caarlos0/env/v6" + "github.com/joho/godotenv" +) + +type Environment struct { + AppEnvironment string `env:"ENV"` + ListenAddress string `env:"LISTEN_ADDR" envDefault:"0.0.0.0"` + + ListenPort int `env:"PORT" envDefault:"8080"` + LogtailToken string `env:"LOGTAIL_TOKEN"` + + HttpReqTimeout int64 `env:"HTTP_SEC_TIMEOUT" envDefault:"1"` +} + +func New() (*Environment, error) { + if os.Getenv("ENV") == "PRODUCTION" { + return NewEnv() + } + + return NewDotEnv() +} + +func NewEnv() (*Environment, error) { + cfg := &Environment{} + + if err := env.Parse(cfg); err != nil { + return nil, err + } + + return cfg, nil +} + +func NewDotEnv() (*Environment, error) { + err := godotenv.Load() + + if err != nil { + return nil, err + } + + return NewEnv() +} diff --git a/utils/httputil/impl.go b/utils/httputil/impl.go new file mode 100644 index 0000000..41cc17f --- /dev/null +++ b/utils/httputil/impl.go @@ -0,0 +1,3 @@ +package httputil + +type HttpUtilImpl struct{} diff --git a/utils/httputil/parse.go b/utils/httputil/parse.go new file mode 100644 index 0000000..d438e56 --- /dev/null +++ b/utils/httputil/parse.go @@ -0,0 +1,11 @@ +package httputil + +import ( + "encoding/json" + "net/http" +) + +func (HttpUtilImpl) ParseJson(r *http.Request, output interface{}) error { + decoder := json.NewDecoder(r.Body) + return decoder.Decode(output) +} diff --git a/utils/httputil/type.go b/utils/httputil/type.go new file mode 100644 index 0000000..5eec02c --- /dev/null +++ b/utils/httputil/type.go @@ -0,0 +1,9 @@ +package httputil + +import "net/http" + +type HttpUtil interface { + WriteSuccessJson(w http.ResponseWriter, payload interface{}) error + WriteJson(w http.ResponseWriter, httpCode int, payload interface{}) error + ParseJson(r *http.Request, output interface{}) error +} diff --git a/utils/httputil/write.go b/utils/httputil/write.go new file mode 100644 index 0000000..effce03 --- /dev/null +++ b/utils/httputil/write.go @@ -0,0 +1,16 @@ +package httputil + +import ( + "encoding/json" + "net/http" +) + +func (HttpUtilImpl) WriteJson(w http.ResponseWriter, httpCode int, payload interface{}) error { + encoder := json.NewEncoder(w) + w.WriteHeader(httpCode) + return encoder.Encode(payload) +} + +func (h HttpUtilImpl) WriteSuccessJson(w http.ResponseWriter, payload interface{}) error { + return h.WriteJson(w, http.StatusOK, payload) +} diff --git a/utils/log/color.go b/utils/log/color.go new file mode 100644 index 0000000..c339153 --- /dev/null +++ b/utils/log/color.go @@ -0,0 +1,27 @@ +package log + +type Color string + +const Reset Color = "\u001b[0m" + +const ( + ForeBlack Color = "\u001b[30m" + ForeRed Color = "\u001b[31m" + ForeGreen Color = "\u001b[32m" + ForeYellow Color = "\u001b[33m" + ForeBlue Color = "\u001b[34m" + ForeMagenta Color = "\u001b[35m" + ForeCyan Color = "\u001b[36m" + ForeWhite Color = "\u001b[37m" +) + +const ( + BackBlack Color = "\u001b[40m" + BackRed Color = "\u001b[41m" + BackGreen Color = "\u001b[42m" + BackYellow Color = "\u001b[43m" + BackBlue Color = "\u001b[44m" + BackMagenta Color = "\u001b[45m" + BackCyan Color = "\u001b[46m" + BackWhite Color = "\u001b[47m" +) \ No newline at end of file diff --git a/utils/log/output.go b/utils/log/output.go new file mode 100644 index 0000000..773bece --- /dev/null +++ b/utils/log/output.go @@ -0,0 +1,31 @@ +package log + +import ( + "fmt" + "time" +) + +type LogUtilsImpl struct{} + +func (l LogUtilsImpl) FormattedOutput(text string, process string, logType string, color Color) string { + return fmt.Sprintf("%s %s: [%s] %s\n", + time.Now().Format("2006-01-02 15:04:05 MST"), + l.ColoredOutput(logType, color), + process, + text, + ) +} + +func (LogUtilsImpl) ColoredOutput(text string, color Color) string { + return fmt.Sprintf("%s%s%s", + color, + text, + Reset, + ) +} + +func (l LogUtilsImpl) PrintFormattedOutput(text string, process string, logType string, color Color) { + print( + l.FormattedOutput(text, process, logType, color), + ) +} diff --git a/utils/log/type.go b/utils/log/type.go new file mode 100644 index 0000000..6bd07ca --- /dev/null +++ b/utils/log/type.go @@ -0,0 +1,7 @@ +package log + +type LogUtils interface { + PrintFormattedOutput(text string, process string, logType string, color Color) + FormattedOutput(text string, process string, logType string, color Color) string + ColoredOutput(text string, color Color) string +} diff --git a/utils/res/data/ascii.art b/utils/res/data/ascii.art new file mode 100644 index 0000000..269bead --- /dev/null +++ b/utils/res/data/ascii.art @@ -0,0 +1,8 @@ + ____ _____ __ __ + / __ \ / ____| \ \ / / + | | | |_ __ ___ _ __ | | ___ _ _ _ __ ___ __\ \ /\ / /_ _ _ __ ___ + | | | | '_ \ / _ \ '_ \| | / _ \| | | | '__/ __|/ _ \ \/ \/ / _` | '__/ _ \ + | |__| | |_) | __/ | | | |___| (_) | |_| | | \__ \ __/\ /\ / (_| | | | __/ + \____/| .__/ \___|_| |_|\_____\___/ \__,_|_| |___/\___| \/ \/ \__,_|_| \___| + | | + |_| diff --git a/utils/res/data/version b/utils/res/data/version new file mode 100644 index 0000000..cfbbe7b --- /dev/null +++ b/utils/res/data/version @@ -0,0 +1 @@ +ALPHA-1.0.0 \ No newline at end of file diff --git a/utils/res/embed.go b/utils/res/embed.go new file mode 100644 index 0000000..598ad06 --- /dev/null +++ b/utils/res/embed.go @@ -0,0 +1,25 @@ +package res + +import ( + "embed" +) + +//go:embed data/* +var data embed.FS + +type EmbedResources struct{} + +func (EmbedResources) GetBytesResource(path string) ([]byte, error) { + return data.ReadFile("data/" + path) +} + + +func (EmbedResources) GetStringResource(path string) (string, error) { + content, err := data.ReadFile("data/" + path) + + if err != nil { + return "", err + } + + return string(content), nil +} \ No newline at end of file diff --git a/utils/res/res.go b/utils/res/res.go new file mode 100644 index 0000000..387823d --- /dev/null +++ b/utils/res/res.go @@ -0,0 +1,6 @@ +package res + +type Resource interface { + GetBytesResource(path string) ([]byte, error) + GetStringResource(path string) (string, error) +} diff --git a/utils/wrapper/type.go b/utils/wrapper/type.go new file mode 100644 index 0000000..0270714 --- /dev/null +++ b/utils/wrapper/type.go @@ -0,0 +1,8 @@ +package wrapper + +import "gitlab.informatika.org/ocw/ocw-backend/model/web" + +type WrapperUtil interface { + SuccessResponseWrap(data interface{}) *web.BaseResponse + ErrorResponseWrap(message string, payload interface{}) *web.BaseResponse +} diff --git a/utils/wrapper/wrapper.go b/utils/wrapper/wrapper.go new file mode 100644 index 0000000..d35e605 --- /dev/null +++ b/utils/wrapper/wrapper.go @@ -0,0 +1,21 @@ +package wrapper + +import "gitlab.informatika.org/ocw/ocw-backend/model/web" + +type WrapperUtilImpl struct{} + +func (WrapperUtilImpl) SuccessResponseWrap(data interface{}) *web.BaseResponse { + return &web.BaseResponse{ + Status: web.Success, + Message: "success", + Data: data, + } +} + +func (WrapperUtilImpl) ErrorResponseWrap(message string, payload interface{}) *web.BaseResponse { + return &web.BaseResponse{ + Status: web.Failed, + Message: message, + Data: payload, + } +} -- GitLab