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"
+)