Skip to content

Commit 4bd6db1

Browse files
authored
feat: Redirection analytics (#19)
* Set log level based on env * Add memory based queue to record click analytics for every redirection * Add workers to prod compose * Delete unused file * Remove unused files
1 parent 890e564 commit 4bd6db1

File tree

15 files changed

+297
-215
lines changed

15 files changed

+297
-215
lines changed

Dockerfile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ COPY go.mod go.sum ./
1212
RUN go mod download
1313
COPY . .
1414
RUN make build
15+
RUN make build-workers
1516
# Install CompileDaemon
1617
RUN go install github.com/githubnemo/CompileDaemon@latest
1718

@@ -27,6 +28,8 @@ RUN apk --no-cache add make
2728

2829
WORKDIR /app
2930
COPY --from=development /app/build/url-shortener .
31+
COPY --from=development /app/build/url-shortener-workers .
3032
COPY --from=development /app/makefile .
3133

34+
# This entrypoint will be overriden by docker-compose to support running both workers and app
3235
ENTRYPOINT ["./url-shortener"]

cmd/server/main.go

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,32 @@ package main
22

33
import (
44
"context"
5+
"net/http"
6+
"os"
7+
"time"
8+
59
"github.com/AjayPoshak/url-shortener/internal/handlers"
10+
"github.com/hibiken/asynq"
611
"github.com/redis/go-redis/v9"
712
"github.com/rs/zerolog"
813
"github.com/rs/zerolog/log"
914
"go.mongodb.org/mongo-driver/bson"
1015
"go.mongodb.org/mongo-driver/mongo"
1116
"go.mongodb.org/mongo-driver/mongo/options"
12-
"net/http"
13-
"os"
14-
"time"
1517
)
1618

1719
func main() {
18-
zerolog.SetGlobalLevel(zerolog.WarnLevel) // Setting log level
20+
env := os.Getenv("GO_ENV")
21+
if env == "" {
22+
log.Fatal().Msg("GO_ENV is not set")
23+
}
24+
if env == "production" {
25+
26+
zerolog.SetGlobalLevel(zerolog.WarnLevel)
27+
} else {
28+
29+
zerolog.SetGlobalLevel(zerolog.InfoLevel)
30+
}
1931
mongoURI := os.Getenv("MONGODB_URI")
2032
if mongoURI == "" {
2133
log.Fatal().Msg("MONGO_URI is not set")
@@ -34,6 +46,9 @@ func main() {
3446
DB: 0,
3547
})
3648

49+
queueClient := asynq.NewClient(asynq.RedisClientOpt{Addr: redisURI})
50+
defer queueClient.Close()
51+
3752
serverAPI := options.ServerAPI(options.ServerAPIVersion1)
3853
opts := options.Client().ApplyURI(mongoURI).SetServerAPIOptions(serverAPI)
3954
opts.SetDirect(true)
@@ -63,7 +78,7 @@ func main() {
6378
router := http.NewServeMux()
6479

6580
// Initialize the handlers
66-
handlers := handlers.NewHandlers(client, databaseName, redis)
81+
handlers := handlers.NewHandlers(client, databaseName, redis, queueClient)
6782

6883
// Register routes with middleware
6984
router.HandleFunc("GET /urls", handlers.GetUrls)

cmd/workers/workers.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"os"
6+
7+
"github.com/AjayPoshak/url-shortener/internal/tasks"
8+
"github.com/hibiken/asynq"
9+
"github.com/rs/zerolog/log"
10+
"go.mongodb.org/mongo-driver/bson"
11+
"go.mongodb.org/mongo-driver/mongo"
12+
"go.mongodb.org/mongo-driver/mongo/options"
13+
)
14+
15+
func NewAnalyticsHandler(db *mongo.Client) asynq.HandlerFunc {
16+
return func(ctx context.Context, t *asynq.Task) error {
17+
return tasks.HandleAnalyticsDeliveryTask(ctx, t, db)
18+
}
19+
}
20+
func main() {
21+
mongoURI := os.Getenv("MONGODB_URI")
22+
if mongoURI == "" {
23+
log.Fatal().Msg("MONGO_URI is not set")
24+
}
25+
databaseName := os.Getenv("MONGODB_DATABASE")
26+
if databaseName == "" {
27+
log.Fatal().Msg("MONGODB_DATABASE is not set")
28+
}
29+
30+
serverAPI := options.ServerAPI(options.ServerAPIVersion1)
31+
opts := options.Client().ApplyURI(mongoURI).SetServerAPIOptions(serverAPI)
32+
opts.SetDirect(true)
33+
34+
// Create a new client and connect to mongo
35+
client, err := mongo.Connect(context.TODO(), opts)
36+
if err != nil {
37+
log.Fatal().Msgf("Failed to connect to MongoDB: %v", err)
38+
panic(err)
39+
}
40+
defer func() {
41+
if err := client.Disconnect(context.TODO()); err != nil {
42+
log.Fatal().Msgf("Failed to disconnect from MongoDB: %v", err)
43+
panic(err)
44+
}
45+
}()
46+
47+
// Send a ping to confirm successful connection
48+
var result bson.M
49+
50+
if err := client.Database("admin").RunCommand(context.TODO(), bson.D{{"ping", 1}}).Decode(&result); err != nil {
51+
log.Fatal().Msgf("Failed to ping MongoDB: %v", err)
52+
panic(err)
53+
}
54+
log.Info().Msg("Succesfully connected to MongoDB")
55+
56+
redisURI := os.Getenv("REDIS_URI")
57+
if redisURI == "" {
58+
log.Fatal().Msg("REDIS_URI is not set")
59+
}
60+
server := asynq.NewServer(
61+
asynq.RedisClientOpt{Addr: redisURI},
62+
asynq.Config{
63+
Concurrency: 2,
64+
},
65+
)
66+
mux := asynq.NewServeMux()
67+
68+
mux.HandleFunc(tasks.TypeRedirectionAnalytics, NewAnalyticsHandler(client))
69+
70+
serverErr := server.Run(mux)
71+
if serverErr != nil {
72+
log.Fatal().Msgf("Could not run server %v", serverErr)
73+
}
74+
}

docker-compose.dev.yml

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ services:
1515
- "8095:8095"
1616
depends_on:
1717
- mongodb-dev
18-
# condition: service_healthy
1918
networks:
2019
- app-network-dev
2120

@@ -32,6 +31,38 @@ services:
3231
- vendor/
3332
- "*.md"
3433

34+
app-workers:
35+
build:
36+
context: .
37+
dockerfile: Dockerfile
38+
target: development
39+
volumes:
40+
- go-modules:/go/pkg/mod
41+
environment:
42+
- GO_ENV=development
43+
- MONGODB_URI=mongodb://${MONGODB_USER}:${MONGODB_PASSWORD}@mongodb-dev:27017/${MONGODB_DATABASE}?authSource=${MONGODB_DATABASE}
44+
- MONGODB_DATABASE=${MONGODB_DATABASE}
45+
- REDIS_URI=${REDIS_HOST}:${REDIS_PORT}
46+
ports:
47+
- "8096:8096"
48+
depends_on:
49+
- redis-dev
50+
networks:
51+
- app-network-dev
52+
53+
command: sh -c "while [ ! -f /app/go.mod ]; do sleep 1; done && CompileDaemon --build='go build -o /app/build/url-shortener-workers /app/cmd/workers/workers.go' --command='/app/build/url-shortener-workers' --color=true -pattern='(.+\\.go|.+\\.env)$$' --directory='/app' --verbose"
54+
develop:
55+
watch:
56+
- action: sync
57+
path: .
58+
target: /app
59+
ignore:
60+
- /app/.git/
61+
- .gitignore
62+
- build/
63+
- vendor/
64+
- "*.md"
65+
3566
mongodb-dev:
3667
user: mongodb
3768
image: mongo:6

docker-compose.yml

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@ services:
4747
- "traefik.http.routers.app-prod.entrypoints=websecure" # Allow requests from predefined entrypoint named "websecure"
4848
- "traefik.http.routers.app-prod.rule=Host(`urlly.app`)"
4949
- "traefik.http.routers.app-prod.tls.certresolver=myresolver"
50-
5150
depends_on:
5251
- mongodb-prod
5352
networks:
@@ -61,6 +60,36 @@ services:
6160
options:
6261
loki-url: "http://172.31.16.214:3100/loki/api/v1/push"
6362
loki-external-labels: service=url-shortener
63+
command: ["./url-shortener"]
64+
65+
app-workers:
66+
image: ghcr.io/ajayposhak/url-shortener-workers:latest
67+
build:
68+
context: .
69+
dockerfile: Dockerfile
70+
target: production
71+
environment:
72+
- GO_ENV=production
73+
- MONGODB_URI=mongodb://${MONGODB_USER}:${MONGODB_PASSWORD}@mongodb-prod:27017/${MONGODB_DATABASE}?authSource=${MONGODB_DATABASE}
74+
- MONGODB_DATABASE=${MONGODB_DATABASE}
75+
- REDIS_URI=${REDIS_HOST}:${REDIS_PORT}
76+
ports:
77+
- "8096:8096"
78+
depends_on:
79+
- redis-prod
80+
- mongodb-prod
81+
networks:
82+
- app-network-prod
83+
deploy:
84+
restart_policy:
85+
condition: on-failure
86+
max_attempts: 3
87+
logging:
88+
driver: loki
89+
options:
90+
loki-url: "http://172.31.16.214:3100/loki/api/v1/push"
91+
loki-external-labels: service=url-shortener-workers
92+
command: ["./url-shortener-workers"]
6493

6594
mongodb-prod:
6695
image: mongo:6

generation-test.js

Lines changed: 0 additions & 63 deletions
This file was deleted.

go.mod

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,27 @@ go 1.23.3
55
require go.mongodb.org/mongo-driver v1.17.1
66

77
require (
8-
github.com/cespare/xxhash/v2 v2.2.0 // indirect
8+
github.com/cespare/xxhash/v2 v2.3.0 // indirect
99
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
1010
github.com/golang/snappy v0.0.4 // indirect
11+
github.com/google/uuid v1.6.0 // indirect
12+
github.com/hibiken/asynq v0.25.1 // indirect
1113
github.com/klauspost/compress v1.13.6 // indirect
1214
github.com/mattn/go-colorable v0.1.13 // indirect
1315
github.com/mattn/go-isatty v0.0.20 // indirect
1416
github.com/montanaflynn/stats v0.7.1 // indirect
1517
github.com/redis/go-redis/v9 v9.7.0 // indirect
18+
github.com/robfig/cron/v3 v3.0.1 // indirect
1619
github.com/rs/zerolog v1.33.0 // indirect
20+
github.com/spf13/cast v1.7.1 // indirect
1721
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
1822
github.com/xdg-go/scram v1.1.2 // indirect
1923
github.com/xdg-go/stringprep v1.0.4 // indirect
2024
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
2125
golang.org/x/crypto v0.26.0 // indirect
2226
golang.org/x/sync v0.8.0 // indirect
23-
golang.org/x/sys v0.28.0 // indirect
27+
golang.org/x/sys v0.29.0 // indirect
2428
golang.org/x/text v0.17.0 // indirect
29+
golang.org/x/time v0.9.0 // indirect
30+
google.golang.org/protobuf v1.36.4 // indirect
2531
)

go.sum

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
22
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
3+
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
4+
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
35
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
46
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
57
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -10,6 +12,10 @@ github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
1012
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
1113
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
1214
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
15+
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
16+
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
17+
github.com/hibiken/asynq v0.25.1 h1:phj028N0nm15n8O2ims+IvJ2gz4k2auvermngh9JhTw=
18+
github.com/hibiken/asynq v0.25.1/go.mod h1:pazWNOLBu0FEynQRBvHA26qdIKRSmfdIfUm4HdsLmXg=
1319
github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc=
1420
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
1521
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
@@ -23,9 +29,13 @@ github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt
2329
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
2430
github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E=
2531
github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw=
32+
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
33+
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
2634
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
2735
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
2836
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
37+
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
38+
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
2939
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
3040
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
3141
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
@@ -59,6 +69,8 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
5969
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
6070
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
6171
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
72+
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
73+
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
6274
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
6375
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
6476
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -67,7 +79,11 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
6779
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
6880
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
6981
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
82+
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
83+
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
7084
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
7185
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
7286
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
7387
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
88+
google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM=
89+
google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=

0 commit comments

Comments
 (0)