Compare commits

...

8 Commits

Author SHA1 Message Date
96610e0d2f Migration: Depend on Service Base
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m22s
2026-06-04 22:22:40 +02:00
ce93c5f423 Add CORS Support to allow frontend requests
All checks were successful
Build and Push Docker Image / build (push) Successful in 7m28s
2026-06-03 13:22:14 +02:00
d348d0bcbe Script go generate stubs for protobuf implementation
All checks were successful
Build and Push Docker Image / build (push) Successful in 9m8s
2026-06-03 10:03:25 +02:00
6d693db605 Move all tables into a schema
All checks were successful
Build and Push Docker Image / build (push) Successful in 7m19s
2026-06-03 00:02:43 +02:00
ec4159e84c New: Add goose to run migrations as an init container
All checks were successful
Build and Push Docker Image / build (push) Successful in 7m56s
2026-06-02 22:22:21 +02:00
8f1e292995 Get the port from the json config
All checks were successful
Build and Push Docker Image / build (push) Successful in 5m53s
2026-06-02 22:08:47 +02:00
a2d8c5c156 Remove settings.json from docker and git repository
All checks were successful
Build and Push Docker Image / build (push) Successful in 5m54s
2026-06-02 22:02:24 +02:00
ad9223d2d9 Update: config json path
Some checks failed
Build and Push Docker Image / build (push) Has been cancelled
2026-06-02 21:59:47 +02:00
16 changed files with 100 additions and 204 deletions

1
.dockerignore Normal file
View File

@@ -0,0 +1 @@
settings.json

1
.gitignore vendored
View File

@@ -0,0 +1 @@
settings.json

View File

@@ -15,12 +15,18 @@ COPY . .
# Build the application # Build the application
RUN go build -o shortener RUN go build -o shortener
# Install goose migration tool
RUN GOBIN=/app go install github.com/pressly/goose/v3/cmd/goose@latest
FROM golang:1.26-bookworm AS runner FROM golang:1.26-bookworm AS runner
WORKDIR /app
COPY --from=base /app/shortener /app/shortener COPY --from=base /app/shortener /app/shortener
COPY --from=base /app/goose /app/goose
COPY --from=base /app/sql/schema /app/sql/schema
# Document the port that may need to be published # Document the port that may need to be published
EXPOSE 8000 EXPOSE 8080
# Start the application # Start the application
CMD ["/app/shortener"] CMD ["/app/shortener"]

2
gen-proto.sh Executable file
View File

@@ -0,0 +1,2 @@
#!/bin/bash
buf generate --path proto/shorten

39
go.mod
View File

@@ -8,27 +8,33 @@ tool (
) )
require ( require (
git.kocoder.xyz/vt/service-base v1.0.0
github.com/lib/pq v1.12.3 github.com/lib/pq v1.12.3
github.com/uptrace/opentelemetry-go-extra/otelsql v0.3.2
go.opentelemetry.io/contrib/bridges/otelslog v0.19.0
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0
go.opentelemetry.io/otel v1.44.0
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.20.0
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.44.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.44.0
go.opentelemetry.io/otel/log v0.20.0
go.opentelemetry.io/otel/sdk v1.44.0
go.opentelemetry.io/otel/sdk/log v0.20.0
go.opentelemetry.io/otel/sdk/metric v1.44.0
) )
require ( require (
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260415201107-50325440f8f2.1 // indirect connectrpc.com/cors v0.1.0 // indirect
github.com/rs/cors v1.11.1 // indirect
github.com/uptrace/opentelemetry-go-extra/otelsql v0.3.2 // indirect
go.opentelemetry.io/contrib/bridges/otelslog v0.19.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 // indirect
go.opentelemetry.io/otel v1.44.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.20.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.44.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.44.0 // indirect
go.opentelemetry.io/otel/log v0.20.0 // indirect
go.opentelemetry.io/otel/sdk v1.44.0 // indirect
go.opentelemetry.io/otel/sdk/log v0.20.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.44.0 // indirect
)
require (
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260415201107-50325440f8f2.1
buf.build/go/protovalidate v1.2.0 // indirect buf.build/go/protovalidate v1.2.0 // indirect
cel.dev/expr v0.25.1 // indirect cel.dev/expr v0.25.1 // indirect
connectrpc.com/connect v1.20.0 // indirect connectrpc.com/connect v1.20.0
connectrpc.com/grpcreflect v1.3.0 connectrpc.com/grpcreflect v1.3.0 // indirect
connectrpc.com/validate v0.6.0 // indirect connectrpc.com/validate v0.6.0
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
@@ -38,7 +44,6 @@ require (
github.com/google/cel-go v0.28.0 // indirect github.com/google/cel-go v0.28.0 // indirect
github.com/google/uuid v1.6.0 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect
github.com/stoewer/go-strcase v1.3.1 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0 // indirect
go.opentelemetry.io/otel/metric v1.44.0 // indirect go.opentelemetry.io/otel/metric v1.44.0 // indirect
@@ -51,5 +56,5 @@ require (
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect
google.golang.org/grpc v1.81.1 // indirect google.golang.org/grpc v1.81.1 // indirect
google.golang.org/protobuf v1.36.11 // indirect google.golang.org/protobuf v1.36.11
) )

29
go.sum
View File

@@ -1,26 +1,27 @@
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.9-20250912141014-52f32327d4b0.1 h1:DQLS/rRxLHuugVzjJU5AvOwD57pdFl9he/0O7e5P294=
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.9-20250912141014-52f32327d4b0.1/go.mod h1:aY3zbkNan5F+cGm9lITDP6oxJIwu0dn9KjJuJjWaHkg=
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260415201107-50325440f8f2.1 h1:s6hzCXtND/ICdGPTMGk7C+/BFlr2Jg5GyH0NKf4XGXg= buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260415201107-50325440f8f2.1 h1:s6hzCXtND/ICdGPTMGk7C+/BFlr2Jg5GyH0NKf4XGXg=
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260415201107-50325440f8f2.1/go.mod h1:tvtbpgaVXZX4g6Pn+AnzFycuRK3MOz5HJfEGeEllXYM= buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260415201107-50325440f8f2.1/go.mod h1:tvtbpgaVXZX4g6Pn+AnzFycuRK3MOz5HJfEGeEllXYM=
buf.build/go/protovalidate v1.0.0 h1:IAG1etULddAy93fiBsFVhpj7es5zL53AfB/79CVGtyY=
buf.build/go/protovalidate v1.0.0/go.mod h1:KQmEUrcQuC99hAw+juzOEAmILScQiKBP1Oc36vvCLW8=
buf.build/go/protovalidate v1.2.0 h1:DQVrUWkmGTBij+kOYv/x2LLxwcLaGKMdzShj1/6/3H0= buf.build/go/protovalidate v1.2.0 h1:DQVrUWkmGTBij+kOYv/x2LLxwcLaGKMdzShj1/6/3H0=
buf.build/go/protovalidate v1.2.0/go.mod h1:7rYiQEhqvAipoazpVNBBH2S2f8bjG4huMVy1V2Yofn4= buf.build/go/protovalidate v1.2.0/go.mod h1:7rYiQEhqvAipoazpVNBBH2S2f8bjG4huMVy1V2Yofn4=
cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4= cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=
cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
connectrpc.com/connect v1.20.0 h1:6TNDAB+WeNd2uolWNlYczB5E0KNNaVMNUEx8JEUsPmQ= connectrpc.com/connect v1.20.0 h1:6TNDAB+WeNd2uolWNlYczB5E0KNNaVMNUEx8JEUsPmQ=
connectrpc.com/connect v1.20.0/go.mod h1:A2ygJrukXwWy32vkCAAHNVguZrqZ+jeZ9rGRnGR4dN4= connectrpc.com/connect v1.20.0/go.mod h1:A2ygJrukXwWy32vkCAAHNVguZrqZ+jeZ9rGRnGR4dN4=
connectrpc.com/cors v0.1.0 h1:f3gTXJyDZPrDIZCQ567jxfD9PAIpopHiRDnJRt3QuOQ=
connectrpc.com/cors v0.1.0/go.mod h1:v8SJZCPfHtGH1zsm+Ttajpozd4cYIUryl4dFB6QEpfg=
connectrpc.com/grpcreflect v1.3.0 h1:Y4V+ACf8/vOb1XOc251Qun7jMB75gCUNw6llvB9csXc= connectrpc.com/grpcreflect v1.3.0 h1:Y4V+ACf8/vOb1XOc251Qun7jMB75gCUNw6llvB9csXc=
connectrpc.com/grpcreflect v1.3.0/go.mod h1:nfloOtCS8VUQOQ1+GTdFzVg2CJo4ZGaat8JIovCtDYs= connectrpc.com/grpcreflect v1.3.0/go.mod h1:nfloOtCS8VUQOQ1+GTdFzVg2CJo4ZGaat8JIovCtDYs=
connectrpc.com/validate v0.6.0 h1:DcrgDKt2ZScrUs/d/mh9itD2yeEa0UbBBa+i0mwzx+4= connectrpc.com/validate v0.6.0 h1:DcrgDKt2ZScrUs/d/mh9itD2yeEa0UbBBa+i0mwzx+4=
connectrpc.com/validate v0.6.0/go.mod h1:ihrpI+8gVbLH1fvVWJL1I3j0CfWnF8P/90LsmluRiZs= connectrpc.com/validate v0.6.0/go.mod h1:ihrpI+8gVbLH1fvVWJL1I3j0CfWnF8P/90LsmluRiZs=
git.kocoder.xyz/vt/service-base v1.0.0 h1:WeakTvsz9389dZm3BXEbnWt2SVkLRc/9RfXjesFUqEo=
git.kocoder.xyz/vt/service-base v1.0.0/go.mod h1:FBXO015/GkyPP6CBan4Rku76Y/vrZgIL7zQyz39dTgs=
github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ=
github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=
github.com/brianvoe/gofakeit/v6 v6.28.0 h1:Xib46XXuQfmlLS2EXRuJpqcw8St6qSZz75OUo0tgAW4=
github.com/brianvoe/gofakeit/v6 v6.28.0/go.mod h1:Xj58BMSnFqcn/fAQeSK+/PLtC5kSb7FJIq4JyGa8vEs=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
@@ -32,8 +33,6 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/cel-go v0.26.1 h1:iPbVVEdkhTX++hpe3lzSk7D3G3QSYqLGoHOcEio+UXQ=
github.com/google/cel-go v0.26.1/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM=
github.com/google/cel-go v0.28.0 h1:KjSWstCpz/MN5t4a8gnGJNIYUsJRpdi/r97xWDphIQc= github.com/google/cel-go v0.28.0 h1:KjSWstCpz/MN5t4a8gnGJNIYUsJRpdi/r97xWDphIQc=
github.com/google/cel-go v0.28.0/go.mod h1:X0bD6iVNR8pkROSOoHVdgTkzmRcosof7WQqCD6wcMc8= github.com/google/cel-go v0.28.0/go.mod h1:X0bD6iVNR8pkROSOoHVdgTkzmRcosof7WQqCD6wcMc8=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
@@ -46,14 +45,10 @@ github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ=
github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 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/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stoewer/go-strcase v1.3.1 h1:iS0MdW+kVTxgMoE1LAZyMiYJFKlOzLooE4MxjirtkAs= github.com/rodaine/protogofakeit v0.1.1 h1:ZKouljuRM3A+TArppfBqnH8tGZHOwM/pjvtXe9DaXH8=
github.com/stoewer/go-strcase v1.3.1/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= github.com/rodaine/protogofakeit v0.1.1/go.mod h1:pXn/AstBYMaSfc1/RqH3N82pBuxtWgejz1AlYpY1mI0=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/uptrace/opentelemetry-go-extra/otelsql v0.3.2 h1:ZjUj9BLYf9PEqBn8W/OapxhPjVRdC6CsXTdULHsyk5c= github.com/uptrace/opentelemetry-go-extra/otelsql v0.3.2 h1:ZjUj9BLYf9PEqBn8W/OapxhPjVRdC6CsXTdULHsyk5c=
@@ -94,6 +89,8 @@ go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpu
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/exp v0.0.0-20250911091902-df9299821621 h1:2id6c1/gto0kaHYyrixvknJ8tUK/Qs5IsmBtrc+FtgU= golang.org/x/exp v0.0.0-20250911091902-df9299821621 h1:2id6c1/gto0kaHYyrixvknJ8tUK/Qs5IsmBtrc+FtgU=
golang.org/x/exp v0.0.0-20250911091902-df9299821621/go.mod h1:TwQYMMnGpvZyc+JpB/UAuTNIsVJifOlSkrZkhcvpVUk= golang.org/x/exp v0.0.0-20250911091902-df9299821621/go.mod h1:TwQYMMnGpvZyc+JpB/UAuTNIsVJifOlSkrZkhcvpVUk=
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
@@ -112,7 +109,5 @@ google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ=
google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -7,12 +7,14 @@ import (
type Config struct { type Config struct {
DB_URL string `json:"db_url"` DB_URL string `json:"db_url"`
LISTEN_ON string `json:"listen_on"`
TRUSTED_PROXIES []string `json:"trusted_proxies"` TRUSTED_PROXIES []string `json:"trusted_proxies"`
OTLP_ENDPOINT string `json:"otlp_endpoint"` OTLP_ENDPOINT string `json:"otlp_endpoint"`
ALLOWED_ORIGINS []string `json:"allowed_origins"`
} }
func Read() *Config { func Read() *Config {
file, err := os.ReadFile("/home/kocoder/src/go/shortener/settings.json") file, err := os.ReadFile("./settings.json")
if err != nil { if err != nil {
panic(err) panic(err)
} }

View File

@@ -11,18 +11,18 @@ import (
) )
const getClicks = `-- name: GetClicks :many const getClicks = `-- name: GetClicks :many
SELECT click_id, url_id, clicked_at, referrer, user_agent, ip_address FROM click_logs SELECT click_id, url_id, clicked_at, referrer, user_agent, ip_address FROM lss.click_logs
` `
func (q *Queries) GetClicks(ctx context.Context) ([]ClickLog, error) { func (q *Queries) GetClicks(ctx context.Context) ([]LssClickLog, error) {
rows, err := q.db.QueryContext(ctx, getClicks) rows, err := q.db.QueryContext(ctx, getClicks)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer rows.Close() defer rows.Close()
var items []ClickLog var items []LssClickLog
for rows.Next() { for rows.Next() {
var i ClickLog var i LssClickLog
if err := rows.Scan( if err := rows.Scan(
&i.ClickID, &i.ClickID,
&i.UrlID, &i.UrlID,
@@ -45,7 +45,7 @@ func (q *Queries) GetClicks(ctx context.Context) ([]ClickLog, error) {
} }
const trackClick = `-- name: TrackClick :one const trackClick = `-- name: TrackClick :one
INSERT INTO click_logs (url_id, referrer, user_agent, ip_address) INSERT INTO lss.click_logs (url_id, referrer, user_agent, ip_address)
VALUES ( VALUES (
$1, $1,
$2, $2,
@@ -62,14 +62,14 @@ type TrackClickParams struct {
IpAddress sql.NullString IpAddress sql.NullString
} }
func (q *Queries) TrackClick(ctx context.Context, arg TrackClickParams) (ClickLog, error) { func (q *Queries) TrackClick(ctx context.Context, arg TrackClickParams) (LssClickLog, error) {
row := q.db.QueryRowContext(ctx, trackClick, row := q.db.QueryRowContext(ctx, trackClick,
arg.UrlID, arg.UrlID,
arg.Referrer, arg.Referrer,
arg.UserAgent, arg.UserAgent,
arg.IpAddress, arg.IpAddress,
) )
var i ClickLog var i LssClickLog
err := row.Scan( err := row.Scan(
&i.ClickID, &i.ClickID,
&i.UrlID, &i.UrlID,

View File

@@ -9,7 +9,7 @@ import (
"time" "time"
) )
type ClickLog struct { type LssClickLog struct {
ClickID int64 ClickID int64
UrlID int32 UrlID int32
ClickedAt time.Time ClickedAt time.Time
@@ -18,7 +18,7 @@ type ClickLog struct {
IpAddress sql.NullString IpAddress sql.NullString
} }
type Url struct { type LssUrl struct {
UrlID int32 UrlID int32
LongUrl string LongUrl string
ShortCode string ShortCode string

View File

@@ -12,7 +12,7 @@ import (
) )
const createURL = `-- name: CreateURL :one const createURL = `-- name: CreateURL :one
INSERT INTO urls (created_at, expires_at, long_url, short_code, is_active) INSERT INTO lss.urls (created_at, expires_at, long_url, short_code, is_active)
VALUES ( VALUES (
$1, $1,
$2, $2,
@@ -31,7 +31,7 @@ type CreateURLParams struct {
IsActive bool IsActive bool
} }
func (q *Queries) CreateURL(ctx context.Context, arg CreateURLParams) (Url, error) { func (q *Queries) CreateURL(ctx context.Context, arg CreateURLParams) (LssUrl, error) {
row := q.db.QueryRowContext(ctx, createURL, row := q.db.QueryRowContext(ctx, createURL,
arg.CreatedAt, arg.CreatedAt,
arg.ExpiresAt, arg.ExpiresAt,
@@ -39,7 +39,7 @@ func (q *Queries) CreateURL(ctx context.Context, arg CreateURLParams) (Url, erro
arg.ShortCode, arg.ShortCode,
arg.IsActive, arg.IsActive,
) )
var i Url var i LssUrl
err := row.Scan( err := row.Scan(
&i.UrlID, &i.UrlID,
&i.LongUrl, &i.LongUrl,
@@ -52,12 +52,12 @@ func (q *Queries) CreateURL(ctx context.Context, arg CreateURLParams) (Url, erro
} }
const deleteURL = `-- name: DeleteURL :one const deleteURL = `-- name: DeleteURL :one
DELETE FROM urls WHERE url_id = $1 RETURNING url_id, long_url, short_code, created_at, expires_at, is_active DELETE FROM lss.urls WHERE url_id = $1 RETURNING url_id, long_url, short_code, created_at, expires_at, is_active
` `
func (q *Queries) DeleteURL(ctx context.Context, urlID int32) (Url, error) { func (q *Queries) DeleteURL(ctx context.Context, urlID int32) (LssUrl, error) {
row := q.db.QueryRowContext(ctx, deleteURL, urlID) row := q.db.QueryRowContext(ctx, deleteURL, urlID)
var i Url var i LssUrl
err := row.Scan( err := row.Scan(
&i.UrlID, &i.UrlID,
&i.LongUrl, &i.LongUrl,
@@ -70,7 +70,7 @@ func (q *Queries) DeleteURL(ctx context.Context, urlID int32) (Url, error) {
} }
const getURLByShortCode = `-- name: GetURLByShortCode :one const getURLByShortCode = `-- name: GetURLByShortCode :one
SELECT url_id, long_url FROM urls WHERE is_active = true AND short_code = $1 SELECT url_id, long_url FROM lss.urls WHERE is_active = true AND short_code = $1
` `
type GetURLByShortCodeRow struct { type GetURLByShortCodeRow struct {
@@ -86,18 +86,18 @@ func (q *Queries) GetURLByShortCode(ctx context.Context, shortCode string) (GetU
} }
const getURLs = `-- name: GetURLs :many const getURLs = `-- name: GetURLs :many
SELECT url_id, long_url, short_code, created_at, expires_at, is_active FROM urls SELECT url_id, long_url, short_code, created_at, expires_at, is_active FROM lss.urls
` `
func (q *Queries) GetURLs(ctx context.Context) ([]Url, error) { func (q *Queries) GetURLs(ctx context.Context) ([]LssUrl, error) {
rows, err := q.db.QueryContext(ctx, getURLs) rows, err := q.db.QueryContext(ctx, getURLs)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer rows.Close() defer rows.Close()
var items []Url var items []LssUrl
for rows.Next() { for rows.Next() {
var i Url var i LssUrl
if err := rows.Scan( if err := rows.Scan(
&i.UrlID, &i.UrlID,
&i.LongUrl, &i.LongUrl,
@@ -120,7 +120,7 @@ func (q *Queries) GetURLs(ctx context.Context) ([]Url, error) {
} }
const setActive = `-- name: SetActive :one const setActive = `-- name: SetActive :one
UPDATE urls SET is_active = $1 WHERE url_id = $2 RETURNING url_id, long_url, short_code, created_at, expires_at, is_active UPDATE lss.urls SET is_active = $1 WHERE url_id = $2 RETURNING url_id, long_url, short_code, created_at, expires_at, is_active
` `
type SetActiveParams struct { type SetActiveParams struct {
@@ -128,9 +128,9 @@ type SetActiveParams struct {
UrlID int32 UrlID int32
} }
func (q *Queries) SetActive(ctx context.Context, arg SetActiveParams) (Url, error) { func (q *Queries) SetActive(ctx context.Context, arg SetActiveParams) (LssUrl, error) {
row := q.db.QueryRowContext(ctx, setActive, arg.IsActive, arg.UrlID) row := q.db.QueryRowContext(ctx, setActive, arg.IsActive, arg.UrlID)
var i Url var i LssUrl
err := row.Scan( err := row.Scan(
&i.UrlID, &i.UrlID,
&i.LongUrl, &i.LongUrl,

View File

@@ -1,101 +0,0 @@
package telemetry
import (
"context"
"errors"
"log/slog"
"strings"
"go.opentelemetry.io/contrib/bridges/otelslog"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp"
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
"go.opentelemetry.io/otel/log/global"
sdklog "go.opentelemetry.io/otel/sdk/log"
"go.opentelemetry.io/otel/sdk/metric"
"go.opentelemetry.io/otel/sdk/resource"
"go.opentelemetry.io/otel/sdk/trace"
)
func newResource(serviceName string) *resource.Resource {
return resource.NewWithAttributes(
resource.Default().SchemaURL(),
attribute.String("service.name", serviceName),
)
}
// Init sets up OpenTelemetry tracing, metrics, and logs via OTLP Push.
// It returns a shutdown function that should be deferred in main.
func Init(ctx context.Context, endpoint string, serviceName string) (func(context.Context) error, error) {
res := newResource(serviceName)
var shutdownFuncs []func(context.Context) error
shutdown := func(ctx context.Context) error {
var err error
for _, fn := range shutdownFuncs {
if fnErr := fn(ctx); fnErr != nil {
err = errors.Join(err, fnErr)
}
}
return err
}
var traceOpts []otlptracehttp.Option
var metricOpts []otlpmetrichttp.Option
var logOpts []otlploghttp.Option
if endpoint != "" {
endpoint = strings.TrimSuffix(endpoint, "/")
traceOpts = append(traceOpts, otlptracehttp.WithEndpointURL(endpoint+"/v1/traces"))
metricOpts = append(metricOpts, otlpmetrichttp.WithEndpointURL(endpoint+"/v1/metrics"))
logOpts = append(logOpts, otlploghttp.WithEndpointURL(endpoint+"/v1/logs"))
} else {
// Default to insecure localhost if no endpoint is specified
traceOpts = append(traceOpts, otlptracehttp.WithInsecure())
metricOpts = append(metricOpts, otlpmetrichttp.WithInsecure())
logOpts = append(logOpts, otlploghttp.WithInsecure())
}
// Trace setup
traceExporter, err := otlptracehttp.New(ctx, traceOpts...)
if err != nil {
return shutdown, err
}
tp := trace.NewTracerProvider(
trace.WithBatcher(traceExporter),
trace.WithResource(res),
)
otel.SetTracerProvider(tp)
shutdownFuncs = append(shutdownFuncs, tp.Shutdown)
// Metrics setup
metricExporter, err := otlpmetrichttp.New(ctx, metricOpts...)
if err != nil {
return shutdown, err
}
mp := metric.NewMeterProvider(
metric.WithReader(metric.NewPeriodicReader(metricExporter)),
metric.WithResource(res),
)
otel.SetMeterProvider(mp)
shutdownFuncs = append(shutdownFuncs, mp.Shutdown)
// Logs setup
logExporter, err := otlploghttp.New(ctx, logOpts...)
if err != nil {
return shutdown, err
}
lp := sdklog.NewLoggerProvider(
sdklog.WithProcessor(sdklog.NewBatchProcessor(logExporter)),
sdklog.WithResource(res),
)
global.SetLoggerProvider(lp)
shutdownFuncs = append(shutdownFuncs, lp.Shutdown)
// Route slog to OpenTelemetry
slog.SetDefault(otelslog.NewLogger(serviceName))
return shutdown, nil
}

45
main.go
View File

@@ -3,26 +3,25 @@ package main
import ( import (
"context" "context"
"log/slog" "log/slog"
"net/http"
"os" "os"
"connectrpc.com/connect" "connectrpc.com/connect"
"connectrpc.com/grpcreflect"
"connectrpc.com/validate" "connectrpc.com/validate"
sdb "git.kocoder.xyz/vt/service-base/database"
"git.kocoder.xyz/vt/service-base/middleware"
"git.kocoder.xyz/vt/service-base/rpc"
"git.kocoder.xyz/vt/service-base/server"
"git.kocoder.xyz/vt/service-base/telemetry"
"git.kocoder.xyz/vt/shortener/internal/config" "git.kocoder.xyz/vt/shortener/internal/config"
"git.kocoder.xyz/vt/shortener/internal/database" "git.kocoder.xyz/vt/shortener/internal/database"
"git.kocoder.xyz/vt/shortener/internal/proto/shorten/v1/shortenv1connect" "git.kocoder.xyz/vt/shortener/internal/proto/shorten/v1/shortenv1connect"
"git.kocoder.xyz/vt/shortener/internal/service" "git.kocoder.xyz/vt/shortener/internal/service"
"git.kocoder.xyz/vt/shortener/internal/telemetry"
_ "github.com/lib/pq" _ "github.com/lib/pq"
"github.com/uptrace/opentelemetry-go-extra/otelsql"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
"go.opentelemetry.io/otel/attribute"
) )
func main() { func main() {
// Fallback logger for early startup errors // Fallback logger for early startup errors
slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stdout, nil))) telemetry.SetupFallbackLogger()
conf := config.Read() conf := config.Read()
@@ -38,49 +37,33 @@ func main() {
}() }()
// Connect to DB and wrap with otelsql for database tracing // Connect to DB and wrap with otelsql for database tracing
db, err := otelsql.Open("postgres", conf.DB_URL) db, err := sdb.OpenPostgres(conf.DB_URL)
if err != nil { if err != nil {
slog.Error("failed to connect to database", slog.String("err", err.Error())) slog.Error("failed to connect to database", slog.String("err", err.Error()))
os.Exit(1) os.Exit(1)
} }
defer db.Close() defer db.Close()
otelsql.ReportDBStatsMetrics(db, otelsql.WithAttributes(
attribute.String("db.system", "postgresql"),
))
dbQueries := database.New(db) dbQueries := database.New(db)
srv := service.NewServer(conf, dbQueries) srv := service.NewServer(conf, dbQueries)
mux := http.NewServeMux() mux := rpc.NewServeMux()
path, handler := shortenv1connect.NewShortenServiceHandler( path, handler := shortenv1connect.NewShortenServiceHandler(
service.NewShortenerService(conf, dbQueries), service.NewShortenerService(conf, dbQueries),
connect.WithInterceptors(validate.NewInterceptor()), connect.WithInterceptors(validate.NewInterceptor()),
) )
mux.Handle(path, handler) mux.RegisterConnect(shortenv1connect.ShortenServiceName, path, handler)
reflector := grpcreflect.NewStaticReflector(shortenv1connect.ShortenServiceName)
mux.Handle(grpcreflect.NewHandlerV1(reflector))
mux.Handle(grpcreflect.NewHandlerV1Alpha(reflector))
mux.Handle("/", srv) mux.Handle("/", srv)
// Wrap the server with OpenTelemetry HTTP handler for tracing & metrics mux.MountReflection()
instrumentedHandler := otelhttp.NewHandler(mux, "shortener-server2")
p := new(http.Protocols) c := middleware.NewCORS(conf.ALLOWED_ORIGINS)
p.SetHTTP1(true) corsMux := c.Handler(mux)
// Use h2c so we can serve HTTP/2 without TLS.
p.SetUnencryptedHTTP2(true)
s := http.Server{
Addr: ":3001",
Handler: instrumentedHandler,
Protocols: p,
}
slog.Info("Starting server on :3001") // Start server with OpenTelemetry tracing, HTTP/1.1 & HTTP/2 (h2c) support
if err := s.ListenAndServe(); err != nil { if err := server.ListenAndServeH2C(conf.LISTEN_ON, corsMux, "shortener-server"); err != nil {
slog.Error("Server failed", slog.String("err", err.Error())) slog.Error("Server failed", slog.String("err", err.Error()))
} }
} }

View File

@@ -1,5 +1,5 @@
-- name: TrackClick :one -- name: TrackClick :one
INSERT INTO click_logs (url_id, referrer, user_agent, ip_address) INSERT INTO lss.click_logs (url_id, referrer, user_agent, ip_address)
VALUES ( VALUES (
$1, $1,
$2, $2,
@@ -9,4 +9,4 @@ VALUES (
RETURNING *; RETURNING *;
-- name: GetClicks :many -- name: GetClicks :many
SELECT * FROM click_logs; SELECT * FROM lss.click_logs;

View File

@@ -1,5 +1,5 @@
-- name: CreateURL :one -- name: CreateURL :one
INSERT INTO urls (created_at, expires_at, long_url, short_code, is_active) INSERT INTO lss.urls (created_at, expires_at, long_url, short_code, is_active)
VALUES ( VALUES (
$1, $1,
$2, $2,
@@ -10,13 +10,13 @@ VALUES (
RETURNING *; RETURNING *;
-- name: GetURLs :many -- name: GetURLs :many
SELECT * FROM urls; SELECT * FROM lss.urls;
-- name: GetURLByShortCode :one -- name: GetURLByShortCode :one
SELECT url_id, long_url FROM urls WHERE is_active = true AND short_code = $1; SELECT url_id, long_url FROM lss.urls WHERE is_active = true AND short_code = $1;
-- name: SetActive :one -- name: SetActive :one
UPDATE urls SET is_active = $1 WHERE url_id = $2 RETURNING *; UPDATE lss.urls SET is_active = $1 WHERE url_id = $2 RETURNING *;
-- name: DeleteURL :one -- name: DeleteURL :one
DELETE FROM urls WHERE url_id = $1 RETURNING *; DELETE FROM lss.urls WHERE url_id = $1 RETURNING *;

View File

@@ -1,5 +1,7 @@
-- +goose Up -- +goose Up
CREATE TABLE urls ( CREATE SCHEMA IF NOT EXISTS lss;
CREATE TABLE lss.urls (
url_id SERIAL PRIMARY KEY, url_id SERIAL PRIMARY KEY,
long_url TEXT NOT NULL, long_url TEXT NOT NULL,
short_code VARCHAR(10) UNIQUE NOT NULL, short_code VARCHAR(10) UNIQUE NOT NULL,
@@ -9,8 +11,8 @@ CREATE TABLE urls (
); );
-- Crucial index for lightning-fast lookups when redirecting -- Crucial index for lightning-fast lookups when redirecting
CREATE INDEX idx_urls_short_code ON urls(short_code); CREATE INDEX idx_urls_short_code ON lss.urls(short_code);
-- +goose Down -- +goose Down
DROP INDEX idx_urls_short_code; DROP INDEX idx_urls_short_code;
DROP TABLE urls; DROP TABLE lss.urls;

View File

@@ -1,5 +1,5 @@
-- +goose Up -- +goose Up
CREATE TABLE click_logs ( CREATE TABLE lss.click_logs (
click_id BIGSERIAL PRIMARY KEY, click_id BIGSERIAL PRIMARY KEY,
url_id INT NOT NULL REFERENCES urls(url_id) ON DELETE CASCADE, url_id INT NOT NULL REFERENCES urls(url_id) ON DELETE CASCADE,
clicked_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, clicked_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
@@ -9,8 +9,8 @@ CREATE TABLE click_logs (
); );
-- Index to optimize analytics queries for a specific link -- Index to optimize analytics queries for a specific link
CREATE INDEX idx_click_logs_url_id ON click_logs(url_id); CREATE INDEX idx_click_logs_url_id ON lss.click_logs(url_id);
-- +goose Down -- +goose Down
DROP INDEX idx_click_logs_url_id; DROP INDEX idx_click_logs_url_id;
DROP TABLE click_logs; DROP TABLE lss.click_logs;