diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..b98047c --- /dev/null +++ b/.dockerignore @@ -0,0 +1,34 @@ +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/azds.yaml +**/bin +**/charts +**/docker-compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md +out/ + +**/.vagrant/ +.idea/ +local/data/ +.env + +.terraform +*tfstate* \ No newline at end of file diff --git a/.env b/.env new file mode 100644 index 0000000..f40d1f1 --- /dev/null +++ b/.env @@ -0,0 +1,6 @@ +APP_ADDRESS = "localhost" +APP_PORT = "3000" + +REDIS_ADDRESS="localhost" +REDIS_PORT="6379" +REDIS_PASSWORD = "110502" \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6ffcb61 --- /dev/null +++ b/.gitignore @@ -0,0 +1,32 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +github.com/ +.vscode +.vs/ +/vendor +**/*.log +**/.project +**/.factorypath +google + +test_report* + +# Go Workspaces (introduced in Go 1.18+) +go.work + +*.env + +.terraform +*tfstate* \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..714d20d --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,9 @@ +version: '3' +services: + redis: + image: redis:latest + command: ["redis-server"] + environment: + - REDIS_PASSWORD=${REDIS_PASSWORD} + ports: + - "6379:6379" \ No newline at end of file diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..539394f --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,4 @@ +FROM ubuntu:latest +LABEL authors="nqkhanh2003" + +ENTRYPOINT ["top", "-b"] \ No newline at end of file diff --git a/go.mod b/go.mod index 7629614..7925dda 100644 --- a/go.mod +++ b/go.mod @@ -1 +1,22 @@ module url-shortener + +go 1.23.1 + +require ( + github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 + github.com/joho/godotenv v1.5.1 + github.com/sirupsen/logrus v1.9.3 + golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 + google.golang.org/protobuf v1.34.2 +) + +require ( + github.com/kr/text v0.2.0 // indirect + github.com/rogpeppe/go-internal v1.13.1 // indirect + golang.org/x/sys v0.21.0 // indirect + golang.org/x/text v0.17.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 // indirect + google.golang.org/grpc v1.64.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..59f56ae --- /dev/null +++ b/go.sum @@ -0,0 +1,46 @@ +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/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +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/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +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 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= +golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= +golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 h1:wKguEg1hsxI2/L3hUYrpo1RVi48K+uTyzKqprwLXsb8= +google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142/go.mod h1:d6be+8HhtEtucleCbxpPW9PA9XwISACu8nvpPqF0BVo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 h1:e7S5W7MGGLaSu8j3YjdezkZ+m1/Nm0uRVRMEMGk26Xs= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= +google.golang.org/grpc v1.64.1 h1:LKtvyfbX3UGVPFcGqJ9ItpVWW6oN/2XqTxfAnwRRXiA= +google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +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/main.go b/main.go index 2486a81..a92121c 100644 --- a/main.go +++ b/main.go @@ -1,25 +1,45 @@ package main import ( + "encoding/json" "fmt" + "github.com/joho/godotenv" + "net/http" ) -//TIP To run your code, right-click the code and select Run. Alternatively, click -// the icon in the gutter and select the Run menu item from here. - -func main() { - //TIP Press when your caret is at the underlined or highlighted text - // to see how GoLand suggests fixing it. - s := "gopher" - fmt.Println("Hello and welcome, %s!", s) - - for i := 1; i <= 5; i++ { - //TIP You can try debugging your code. We have set one breakpoint - // for you, but you can always add more by pressing . To start your debugging session, - // right-click your code in the editor and select the Debug option. - fmt.Println("i =", 100/i) - } +type ShorterRequest struct { + Url string } -//TIP See GoLand help at jetbrains.com/help/go/. -// Also, you can try interactive lessons for GoLand by selecting 'Help | Learn IDE Features' from the main menu. +func main() { + envError := godotenv.Load() + if envError != nil { + panic("Error loading .env file") + } + + router := http.NewServeMux() + + router.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("Hello World")) + }) + + router.HandleFunc("POST /shorten", func(w http.ResponseWriter, r *http.Request) { + requestData := ShorterRequest{} + err := json.NewDecoder(r.Body).Decode(&requestData) + if err != nil { + fmt.Println(err) + panic(err) + } + + w.Write([]byte(requestData.Url)) + }) + + server := http.Server{Addr: ":3000", Handler: router} + + fmt.Println("URL Shortener is running on :3000") + + err := server.ListenAndServe() + if err != nil { + return + } +} diff --git a/pkg/config/configs.go b/pkg/config/configs.go new file mode 100644 index 0000000..213b867 --- /dev/null +++ b/pkg/config/configs.go @@ -0,0 +1,17 @@ +package configs + +type ( + App struct { + Name string `env-required:"true" yaml:"name" env:"APP_NAME"` + Version string `env-required:"true" yaml:"version" env:"APP_VERSION"` + } + + HTTP struct { + Host string `env-required:"true" yaml:"host" env:"HTTP_HOST"` + Port int `env-required:"true" yaml:"port" env:"HTTP_PORT"` + } + + Log struct { + Level string `env-required:"true" yaml:"log_level" env:"LOG_LEVEL"` + } +) diff --git a/pkg/kafka/consumer/consumer.go b/pkg/kafka/consumer/consumer.go new file mode 100644 index 0000000..b78b46c --- /dev/null +++ b/pkg/kafka/consumer/consumer.go @@ -0,0 +1 @@ +package consumer diff --git a/pkg/kafka/consumer/interfaces.go b/pkg/kafka/consumer/interfaces.go new file mode 100644 index 0000000..b78b46c --- /dev/null +++ b/pkg/kafka/consumer/interfaces.go @@ -0,0 +1 @@ +package consumer diff --git a/pkg/kafka/consumer/option.go b/pkg/kafka/consumer/option.go new file mode 100644 index 0000000..b78b46c --- /dev/null +++ b/pkg/kafka/consumer/option.go @@ -0,0 +1 @@ +package consumer diff --git a/pkg/kafka/kafka.go b/pkg/kafka/kafka.go new file mode 100644 index 0000000..82b3441 --- /dev/null +++ b/pkg/kafka/kafka.go @@ -0,0 +1 @@ +package kafka diff --git a/pkg/kafka/producer/interfaces.go b/pkg/kafka/producer/interfaces.go new file mode 100644 index 0000000..30f1d3d --- /dev/null +++ b/pkg/kafka/producer/interfaces.go @@ -0,0 +1 @@ +package producer diff --git a/pkg/kafka/producer/option.go b/pkg/kafka/producer/option.go new file mode 100644 index 0000000..30f1d3d --- /dev/null +++ b/pkg/kafka/producer/option.go @@ -0,0 +1 @@ +package producer diff --git a/pkg/kafka/producer/producer.go b/pkg/kafka/producer/producer.go new file mode 100644 index 0000000..30f1d3d --- /dev/null +++ b/pkg/kafka/producer/producer.go @@ -0,0 +1 @@ +package producer diff --git a/pkg/logger/logrus_adaptor.go b/pkg/logger/logrus_adaptor.go new file mode 100644 index 0000000..f11d225 --- /dev/null +++ b/pkg/logger/logrus_adaptor.go @@ -0,0 +1,79 @@ +package logger + +// refs: +// https://josephwoodward.co.uk/2022/11/slog-structured-logging-proposal +// https://thedevelopercafe.com/articles/logging-in-go-with-slog-a7bb489755c2 + +import ( + "strings" + + "github.com/sirupsen/logrus" + "golang.org/x/exp/slog" +) + +type LogrusHandler struct { + logger *logrus.Logger +} + +func NewLogrusHandler(logger *logrus.Logger) *LogrusHandler { + return &LogrusHandler{ + logger: logger, + } +} + +func ConvertLogLevel(level string) logrus.Level { + var l logrus.Level + + switch strings.ToLower(level) { + case "error": + l = logrus.ErrorLevel + case "warm": + l = logrus.WarnLevel + case "info": + l = logrus.InfoLevel + case "debug": + l = logrus.DebugLevel + default: + l = logrus.InfoLevel + } + + return l +} + +func (h *LogrusHandler) Enabled(_ slog.Level) bool { + // support all logging levels + return true +} + +func (h *LogrusHandler) Handle(rec slog.Record) error { + fields := make(map[string]interface{}, rec.NumAttrs()) + + rec.Attrs(func(a slog.Attr) { + fields[a.Key] = a.Value.Any() + }) + + entry := h.logger.WithFields(fields) + + switch rec.Level { + case slog.DebugLevel: + entry.Debug(rec.Message) + case slog.InfoLevel.Level(): + entry.Info(rec.Message) + case slog.WarnLevel: + entry.Warn(rec.Message) + case slog.ErrorLevel: + entry.Error(rec.Message) + } + + return nil +} + +func (h *LogrusHandler) WithAttrs(attrs []slog.Attr) slog.Handler { + // not implemented for brevity + return h +} + +func (h *LogrusHandler) WithGroup(name string) slog.Handler { + // not implemented for brevity + return h +} diff --git a/pkg/postgres/interfaces.go b/pkg/postgres/interfaces.go new file mode 100644 index 0000000..3d7fd8f --- /dev/null +++ b/pkg/postgres/interfaces.go @@ -0,0 +1,9 @@ +package postgres + +import "database/sql" + +type DBEngine interface { + GetDB() *sql.DB + Configure(...Option) DBEngine + Close() +} diff --git a/pkg/postgres/option.go b/pkg/postgres/option.go new file mode 100644 index 0000000..0907da4 --- /dev/null +++ b/pkg/postgres/option.go @@ -0,0 +1,17 @@ +package postgres + +import "time" + +type Option func(*postgres) + +func ConnAttempts(attempts int) Option { + return func(p *postgres) { + p.connAttempts = attempts + } +} + +func ConnTimeout(timeout time.Duration) Option { + return func(p *postgres) { + p.connTimeout = timeout + } +} diff --git a/pkg/postgres/postgres.go b/pkg/postgres/postgres.go new file mode 100644 index 0000000..ee2f8b3 --- /dev/null +++ b/pkg/postgres/postgres.go @@ -0,0 +1,70 @@ +package postgres + +import ( + "database/sql" + "log" + "time" + + "golang.org/x/exp/slog" +) + +const ( + _defaultConnAttempts = 3 + _defaultConnTimeout = time.Second +) + +type DBConnString string + +type postgres struct { + connAttempts int + connTimeout time.Duration + + db *sql.DB +} + +var _ DBEngine = (*postgres)(nil) + +func NewPostgresDB(url DBConnString) (DBEngine, error) { + slog.Info("CONN", "connect string", url) + + pg := &postgres{ + connAttempts: _defaultConnAttempts, + connTimeout: _defaultConnTimeout, + } + + var err error + for pg.connAttempts > 0 { + pg.db, err = sql.Open("postgres", string(url)) + if err != nil { + break + } + + log.Printf("Postgres is trying to connect, attempts left: %d", pg.connAttempts) + + time.Sleep(pg.connTimeout) + + pg.connAttempts-- + } + + slog.Info("📰 connected to postgresdb 🎉") + + return pg, nil +} + +func (p *postgres) Configure(opts ...Option) DBEngine { + for _, opt := range opts { + opt(p) + } + + return p +} + +func (p *postgres) GetDB() *sql.DB { + return p.db +} + +func (p *postgres) Close() { + if p.db != nil { + p.db.Close() + } +} diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go new file mode 100644 index 0000000..4b99bc3 --- /dev/null +++ b/pkg/utils/utils.go @@ -0,0 +1,11 @@ +package utils + +import "os" + +func IsRunningInContainer() bool { + if _, err := os.Stat("/.dockerenv"); err != nil { + return false + } + + return true +} diff --git a/tools/tools.go b/tools/tools.go new file mode 100644 index 0000000..4bb0531 --- /dev/null +++ b/tools/tools.go @@ -0,0 +1,7 @@ +package tools + +import ( + _ "github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway" + _ "github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2" + _ "google.golang.org/protobuf/cmd/protoc-gen-go" +)