115-enable-tracing #152

Merged
tim merged 1 commits from 115-enable-tracing into prod 2025-06-07 10:35:50 +00:00
32 changed files with 480 additions and 314 deletions

29
go.mod
View File

@@ -11,26 +11,39 @@ require (
github.com/jmoiron/sqlx v1.4.0 github.com/jmoiron/sqlx v1.4.0
github.com/joho/godotenv v1.5.1 github.com/joho/godotenv v1.5.1
github.com/mattn/go-sqlite3 v1.14.28 github.com/mattn/go-sqlite3 v1.14.28
github.com/prometheus/client_golang v1.22.0
github.com/stretchr/testify v1.10.0 github.com/stretchr/testify v1.10.0
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0
go.opentelemetry.io/otel v1.36.0
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.36.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0
go.opentelemetry.io/otel/sdk v1.36.0
go.opentelemetry.io/otel/sdk/metric v1.36.0
go.opentelemetry.io/otel/trace v1.36.0
golang.org/x/crypto v0.39.0 golang.org/x/crypto v0.39.0
golang.org/x/net v0.41.0 golang.org/x/net v0.41.0
) )
require ( require (
github.com/beorn7/perks v1.0.1 // indirect github.com/cenkalti/backoff/v5 v5.0.2 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.62.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/stretchr/objx v0.5.2 // indirect github.com/stretchr/objx v0.5.2 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 // indirect
go.opentelemetry.io/otel/metric v1.36.0 // indirect
go.opentelemetry.io/proto/otlp v1.6.0 // indirect
go.uber.org/atomic v1.11.0 // indirect go.uber.org/atomic v1.11.0 // indirect
golang.org/x/sys v0.33.0 // indirect golang.org/x/sys v0.33.0 // indirect
google.golang.org/protobuf v1.36.5 // indirect golang.org/x/text v0.26.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 // indirect
google.golang.org/grpc v1.72.1 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

71
go.sum
View File

@@ -2,20 +2,29 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/a-h/templ v0.3.898 h1:g9oxL/dmM6tvwRe2egJS8hBDQTncokbMoOFk1oJMX7s= github.com/a-h/templ v0.3.898 h1:g9oxL/dmM6tvwRe2egJS8hBDQTncokbMoOFk1oJMX7s=
github.com/a-h/templ v0.3.898/go.mod h1:oLBbZVQ6//Q6zpvSMPTuBK0F3qOtBdFBcGRspcT+VNQ= github.com/a-h/templ v0.3.898/go.mod h1:oLBbZVQ6//Q6zpvSMPTuBK0F3qOtBdFBcGRspcT+VNQ=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
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/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/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
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-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/golang-migrate/migrate/v4 v4.18.3 h1:EYGkoOsvgHHfm5U/naS1RP/6PL/Xv3S4B/swMiAmDLs= github.com/golang-migrate/migrate/v4 v4.18.3 h1:EYGkoOsvgHHfm5U/naS1RP/6PL/Xv3S4B/swMiAmDLs=
github.com/golang-migrate/migrate/v4 v4.18.3/go.mod h1:99BKpIi6ruaaXRM1A77eqZ+FWPQ3cfRa+ZVy5bmWMaY= github.com/golang-migrate/migrate/v4 v4.18.3/go.mod h1:99BKpIi6ruaaXRM1A77eqZ+FWPQ3cfRa+ZVy5bmWMaY=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@@ -25,47 +34,65 @@ github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 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 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A= github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A=
github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
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/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8=
go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg=
go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.36.0 h1:zwdo1gS2eH26Rg+CoqVQpEK1h8gvt5qyU5Kk5Bixvow=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.36.0/go.mod h1:rUKCPscaRWWcqGT6HnEmYrK+YNe5+Sw64xgQTOJ5b30=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 h1:dNzwXjZKpMpE2JhmO+9HsPl42NIXFIFSUSSs0fiqra0=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0/go.mod h1:90PoxvaEB5n6AOdZvi+yWJQoE95U8Dhhw2bSyRqnTD0=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0 h1:JgtbA0xkWHnTmYk7YusopJFX6uleBmAuZ8n05NEh8nQ=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0/go.mod h1:179AK5aar5R3eS9FucPy6rggvU0g52cvKId8pv4+v0c=
go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE=
go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs=
go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs=
go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY=
go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis=
go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4=
go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w=
go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=
go.opentelemetry.io/proto/otlp v1.6.0 h1:jQjP+AQyTf+Fe7OKj/MfkDrmK4MNVtw2NpXsf9fefDI=
go.opentelemetry.io/proto/otlp v1.6.0/go.mod h1:cicgGehlFuNdgZkcALOCh3VE6K/u2tAjzlRhDwmVpZc=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 h1:Kog3KlB4xevJlAcbbbzPfRG0+X9fdoGM+UBRKVz6Wr0=
google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237/go.mod h1:ezi0AVyMKDWy5xAncvjLWH7UcLBB5n7y2fQ8MzjJcto=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 h1:cJfm9zPbe1e873mHJzmQ1nwVEeRDU/T1wXDK2kUSU34=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA=
google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 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 h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=

View File

@@ -52,7 +52,7 @@ func (db AuthSqlite) InsertUser(user *types.User) error {
return ErrAlreadyExists return ErrAlreadyExists
} }
log.Error("SQL error InsertUser: %v", err) log.L.Error("SQL error InsertUser", "err", err)
return types.ErrInternal return types.ErrInternal
} }
@@ -67,7 +67,7 @@ func (db AuthSqlite) UpdateUser(user *types.User) error {
user.EmailVerified, user.EmailVerifiedAt, user.Password, user.Id) user.EmailVerified, user.EmailVerifiedAt, user.Password, user.Id)
if err != nil { if err != nil {
log.Error("SQL error UpdateUser: %v", err) log.L.Error("SQL error UpdateUser", "err", err)
return types.ErrInternal return types.ErrInternal
} }
@@ -93,7 +93,7 @@ func (db AuthSqlite) GetUserByEmail(email string) (*types.User, error) {
if errors.Is(err, sql.ErrNoRows) { if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNotFound return nil, ErrNotFound
} else { } else {
log.Error("SQL error GetUser: %v", err) log.L.Error("SQL error GetUser", "err", err)
return nil, types.ErrInternal return nil, types.ErrInternal
} }
} }
@@ -120,7 +120,7 @@ func (db AuthSqlite) GetUser(userId uuid.UUID) (*types.User, error) {
if errors.Is(err, sql.ErrNoRows) { if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNotFound return nil, ErrNotFound
} else { } else {
log.Error("SQL error GetUser %v", err) log.L.Error("SQL error GetUser", "err", err)
return nil, types.ErrInternal return nil, types.ErrInternal
} }
} }
@@ -131,55 +131,55 @@ func (db AuthSqlite) GetUser(userId uuid.UUID) (*types.User, error) {
func (db AuthSqlite) DeleteUser(userId uuid.UUID) error { func (db AuthSqlite) DeleteUser(userId uuid.UUID) error {
tx, err := db.db.Begin() tx, err := db.db.Begin()
if err != nil { if err != nil {
log.Error("Could not start transaction: %v", err) log.L.Error("Could not start transaction", "err", err)
return types.ErrInternal return types.ErrInternal
} }
_, err = tx.Exec("DELETE FROM account WHERE user_id = ?", userId) _, err = tx.Exec("DELETE FROM account WHERE user_id = ?", userId)
if err != nil { if err != nil {
_ = tx.Rollback() _ = tx.Rollback()
log.Error("Could not delete accounts: %v", err) log.L.Error("Could not delete accounts", "err", err)
return types.ErrInternal return types.ErrInternal
} }
_, err = tx.Exec("DELETE FROM token WHERE user_id = ?", userId) _, err = tx.Exec("DELETE FROM token WHERE user_id = ?", userId)
if err != nil { if err != nil {
_ = tx.Rollback() _ = tx.Rollback()
log.Error("Could not delete user tokens: %v", err) log.L.Error("Could not delete user tokens", "err", err)
return types.ErrInternal return types.ErrInternal
} }
_, err = tx.Exec("DELETE FROM session WHERE user_id = ?", userId) _, err = tx.Exec("DELETE FROM session WHERE user_id = ?", userId)
if err != nil { if err != nil {
_ = tx.Rollback() _ = tx.Rollback()
log.Error("Could not delete sessions: %v", err) log.L.Error("Could not delete sessions", "err", err)
return types.ErrInternal return types.ErrInternal
} }
_, err = tx.Exec("DELETE FROM user WHERE user_id = ?", userId) _, err = tx.Exec("DELETE FROM user WHERE user_id = ?", userId)
if err != nil { if err != nil {
_ = tx.Rollback() _ = tx.Rollback()
log.Error("Could not delete user: %v", err) log.L.Error("Could not delete user", "err", err)
return types.ErrInternal return types.ErrInternal
} }
_, err = tx.Exec("DELETE FROM treasure_chest WHERE user_id = ?", userId) _, err = tx.Exec("DELETE FROM treasure_chest WHERE user_id = ?", userId)
if err != nil { if err != nil {
_ = tx.Rollback() _ = tx.Rollback()
log.Error("Could not delete user: %v", err) log.L.Error("Could not delete user", "err", err)
return types.ErrInternal return types.ErrInternal
} }
_, err = tx.Exec("DELETE FROM \"transaction\" WHERE user_id = ?", userId) _, err = tx.Exec("DELETE FROM \"transaction\" WHERE user_id = ?", userId)
if err != nil { if err != nil {
_ = tx.Rollback() _ = tx.Rollback()
log.Error("Could not delete user: %v", err) log.L.Error("Could not delete user", "err", err)
return types.ErrInternal return types.ErrInternal
} }
err = tx.Commit() err = tx.Commit()
if err != nil { if err != nil {
log.Error("Could not commit transaction: %v", err) log.L.Error("Could not commit transaction", "err", err)
return types.ErrInternal return types.ErrInternal
} }
@@ -192,7 +192,7 @@ func (db AuthSqlite) InsertToken(token *types.Token) error {
VALUES (?, ?, ?, ?, ?, ?)`, token.UserId, token.SessionId, token.Type, token.Token, token.CreatedAt, token.ExpiresAt) VALUES (?, ?, ?, ?, ?, ?)`, token.UserId, token.SessionId, token.Type, token.Token, token.CreatedAt, token.ExpiresAt)
if err != nil { if err != nil {
log.Error("Could not insert token: %v", err) log.L.Error("Could not insert token", "err", err)
return types.ErrInternal return types.ErrInternal
} }
@@ -217,23 +217,23 @@ func (db AuthSqlite) GetToken(token string) (*types.Token, error) {
if err != nil { if err != nil {
if errors.Is(err, sql.ErrNoRows) { if errors.Is(err, sql.ErrNoRows) {
log.Info("Token '%v' not found", token) log.L.Info("Token not found", "token", token)
return nil, ErrNotFound return nil, ErrNotFound
} else { } else {
log.Error("Could not get token: %v", err) log.L.Error("Could not get token", "err", err)
return nil, types.ErrInternal return nil, types.ErrInternal
} }
} }
createdAt, err = time.Parse(time.RFC3339, createdAtStr) createdAt, err = time.Parse(time.RFC3339, createdAtStr)
if err != nil { if err != nil {
log.Error("Could not parse token.created_at: %v", err) log.L.Error("Could not parse token.created_at", "err", err)
return nil, types.ErrInternal return nil, types.ErrInternal
} }
expiresAt, err = time.Parse(time.RFC3339, expiresAtStr) expiresAt, err = time.Parse(time.RFC3339, expiresAtStr)
if err != nil { if err != nil {
log.Error("Could not parse token.expires_at: %v", err) log.L.Error("Could not parse token.expires_at", "err", err)
return nil, types.ErrInternal return nil, types.ErrInternal
} }
@@ -248,7 +248,7 @@ func (db AuthSqlite) GetTokensByUserIdAndType(userId uuid.UUID, tokenType types.
AND type = ?`, userId, tokenType) AND type = ?`, userId, tokenType)
if err != nil { if err != nil {
log.Error("Could not get token: %v", err) log.L.Error("Could not get token", "err", err)
return nil, types.ErrInternal return nil, types.ErrInternal
} }
@@ -263,7 +263,7 @@ func (db AuthSqlite) GetTokensBySessionIdAndType(sessionId string, tokenType typ
AND type = ?`, sessionId, tokenType) AND type = ?`, sessionId, tokenType)
if err != nil { if err != nil {
log.Error("Could not get token: %v", err) log.L.Error("Could not get token", "err", err)
return nil, types.ErrInternal return nil, types.ErrInternal
} }
@@ -287,19 +287,19 @@ func getTokensFromQuery(query *sql.Rows, userId uuid.UUID, sessionId string, tok
err := query.Scan(&token, &createdAtStr, &expiresAtStr) err := query.Scan(&token, &createdAtStr, &expiresAtStr)
if err != nil { if err != nil {
log.Error("Could not scan token: %v", err) log.L.Error("Could not scan token", "err", err)
return nil, types.ErrInternal return nil, types.ErrInternal
} }
createdAt, err = time.Parse(time.RFC3339, createdAtStr) createdAt, err = time.Parse(time.RFC3339, createdAtStr)
if err != nil { if err != nil {
log.Error("Could not parse token.created_at: %v", err) log.L.Error("Could not parse token.created_at", "err", err)
return nil, types.ErrInternal return nil, types.ErrInternal
} }
expiresAt, err = time.Parse(time.RFC3339, expiresAtStr) expiresAt, err = time.Parse(time.RFC3339, expiresAtStr)
if err != nil { if err != nil {
log.Error("Could not parse token.expires_at: %v", err) log.L.Error("Could not parse token.expires_at", "err", err)
return nil, types.ErrInternal return nil, types.ErrInternal
} }
@@ -316,7 +316,7 @@ func getTokensFromQuery(query *sql.Rows, userId uuid.UUID, sessionId string, tok
func (db AuthSqlite) DeleteToken(token string) error { func (db AuthSqlite) DeleteToken(token string) error {
_, err := db.db.Exec("DELETE FROM token WHERE token = ?", token) _, err := db.db.Exec("DELETE FROM token WHERE token = ?", token)
if err != nil { if err != nil {
log.Error("Could not delete token: %v", err) log.L.Error("Could not delete token", "err", err)
return types.ErrInternal return types.ErrInternal
} }
return nil return nil
@@ -328,7 +328,7 @@ func (db AuthSqlite) InsertSession(session *types.Session) error {
VALUES (?, ?, ?, ?)`, session.Id, session.UserId, session.CreatedAt, session.ExpiresAt) VALUES (?, ?, ?, ?)`, session.Id, session.UserId, session.CreatedAt, session.ExpiresAt)
if err != nil { if err != nil {
log.Error("Could not insert new session %v", err) log.L.Error("Could not insert new session", "err", err)
return types.ErrInternal return types.ErrInternal
} }
@@ -348,7 +348,7 @@ func (db AuthSqlite) GetSession(sessionId string) (*types.Session, error) {
WHERE session_id = ?`, sessionId).Scan(&userId, &createdAt, &expiresAt) WHERE session_id = ?`, sessionId).Scan(&userId, &createdAt, &expiresAt)
if err != nil { if err != nil {
log.Warn("Session \"%s\" not found: %v", sessionId, err) log.L.Warn("Session not found", "session-id", sessionId, "err", err)
return nil, ErrNotFound return nil, ErrNotFound
} }
@@ -362,7 +362,7 @@ func (db AuthSqlite) GetSessions(userId uuid.UUID) ([]*types.Session, error) {
FROM session FROM session
WHERE user_id = ?`, userId) WHERE user_id = ?`, userId)
if err != nil { if err != nil {
log.Error("Could not get sessions: %v", err) log.L.Error("Could not get sessions", "err", err)
return nil, types.ErrInternal return nil, types.ErrInternal
} }
@@ -375,7 +375,7 @@ func (db AuthSqlite) DeleteOldSessions(userId uuid.UUID) error {
WHERE expires_at < datetime('now') WHERE expires_at < datetime('now')
AND user_id = ?`, userId) AND user_id = ?`, userId)
if err != nil { if err != nil {
log.Error("Could not delete old sessions: %v", err) log.L.Error("Could not delete old sessions", "err", err)
return types.ErrInternal return types.ErrInternal
} }
return nil return nil
@@ -385,7 +385,7 @@ func (db AuthSqlite) DeleteSession(sessionId string) error {
if sessionId != "" { if sessionId != "" {
_, err := db.db.Exec("DELETE FROM session WHERE session_id = ?", sessionId) _, err := db.db.Exec("DELETE FROM session WHERE session_id = ?", sessionId)
if err != nil { if err != nil {
log.Error("Could not delete session: %v", err) log.L.Error("Could not delete session", "err", err)
return types.ErrInternal return types.ErrInternal
} }
} }

View File

@@ -17,19 +17,19 @@ func TransformAndLogDbError(module string, r sql.Result, err error) error {
if errors.Is(err, sql.ErrNoRows) { if errors.Is(err, sql.ErrNoRows) {
return ErrNotFound return ErrNotFound
} }
log.Error("%v: %v", module, err) log.L.Error("database sql", "module", module, "err", err)
return types.ErrInternal return types.ErrInternal
} }
if r != nil { if r != nil {
rows, err := r.RowsAffected() rows, err := r.RowsAffected()
if err != nil { if err != nil {
log.Error("%v: %v", module, err) log.L.Error("database rows affected", "module", module, "err", err)
return types.ErrInternal return types.ErrInternal
} }
if rows == 0 { if rows == 0 {
log.Info("%v: not found", module) log.L.Info("row not found", "module", module)
return ErrNotFound return ErrNotFound
} }
} }

View File

@@ -14,8 +14,8 @@ import (
type migrationLogger struct{} type migrationLogger struct{}
func (l migrationLogger) Printf(format string, v ...interface{}) { func (l migrationLogger) Printf(format string, v ...any) {
log.Info(format, v...) log.L.Info(format, v...)
} }
func (l migrationLogger) Verbose() bool { func (l migrationLogger) Verbose() bool {
return false return false
@@ -24,7 +24,7 @@ func (l migrationLogger) Verbose() bool {
func RunMigrations(db *sqlx.DB, pathPrefix string) error { func RunMigrations(db *sqlx.DB, pathPrefix string) error {
driver, err := sqlite3.WithInstance(db.DB, &sqlite3.Config{}) driver, err := sqlite3.WithInstance(db.DB, &sqlite3.Config{})
if err != nil { if err != nil {
log.Error("Could not create Migration instance: %v", err) log.L.Error("Could not create Migration instance", "err", err)
return types.ErrInternal return types.ErrInternal
} }
@@ -33,14 +33,14 @@ func RunMigrations(db *sqlx.DB, pathPrefix string) error {
"", "",
driver) driver)
if err != nil { if err != nil {
log.Error("Could not create migrations instance: %v", err) log.L.Error("Could not create migrations instance", "err", err)
return types.ErrInternal return types.ErrInternal
} }
m.Log = migrationLogger{} m.Log = migrationLogger{}
if err = m.Up(); err != nil && !errors.Is(err, migrate.ErrNoChange) { if err = m.Up(); err != nil && !errors.Is(err, migrate.ErrNoChange) {
log.Error("Could not run migrations: %v", err) log.L.Error("Could not run migrations", "err", err)
return types.ErrInternal return types.ErrInternal
} }

View File

@@ -19,56 +19,67 @@ import (
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
_ "github.com/mattn/go-sqlite3" _ "github.com/mattn/go-sqlite3"
"github.com/prometheus/client_golang/prometheus/promhttp" "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
) )
func Run(ctx context.Context, database *sqlx.DB, migrationsPrefix string, env func(string) string) error { func Run(ctx context.Context, database *sqlx.DB, migrationsPrefix string, env func(string) string) error {
ctx, cancel := signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGTERM) ctx, cancel := signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGTERM)
defer cancel() defer cancel()
log.Info("Starting server...") log.L.Info("Starting server...")
// init server settings // init server settings
serverSettings := types.NewSettingsFromEnv(env) serverSettings, err := types.NewSettingsFromEnv(env)
if err != nil {
return err
}
// init db // init db
err := db.RunMigrations(database, migrationsPrefix) err = db.RunMigrations(database, migrationsPrefix)
if err != nil { if err != nil {
return fmt.Errorf("could not run migrations: %w", err) return fmt.Errorf("could not run migrations: %w", err)
} }
// init servers if serverSettings.OtelEnabled {
var prometheusServer *http.Server // use context.Background(), otherwise the shutdown can't be called, as the context is already cancelled
if serverSettings.PrometheusEnabled { otelShutdown, err := setupOTelSDK(context.Background())
prometheusServer := &http.Server{ if err != nil {
Addr: ":8081", return fmt.Errorf("could not setup OpenTelemetry SDK: %w", err)
Handler: promhttp.Handler(),
ReadHeaderTimeout: 10 * time.Second,
} }
go startServer(prometheusServer) defer func() {
// User context.Background(), as the main context is already cancelled
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
err = otelShutdown(ctx)
if err != nil {
log.L.Error("error shutting down OpenTelemetry SDK", "err", err)
}
cancel()
}()
log.InitOtelLogger()
} }
// init server
httpServer := &http.Server{ httpServer := &http.Server{
Addr: ":" + serverSettings.Port, Addr: ":" + serverSettings.Port,
Handler: createHandler(database, serverSettings), Handler: createHandler(database, serverSettings),
ReadHeaderTimeout: 10 * time.Second, ReadHeaderTimeout: 2 * time.Second,
} }
go startServer(httpServer) go startServer(httpServer)
// graceful shutdown // graceful shutdown
var wg sync.WaitGroup var wg sync.WaitGroup
wg.Add(2) wg.Add(1)
go shutdownServer(httpServer, ctx, &wg) go shutdownServer(httpServer, ctx, &wg)
go shutdownServer(prometheusServer, ctx, &wg)
wg.Wait() wg.Wait()
return nil return nil
} }
func startServer(s *http.Server) { func startServer(s *http.Server) {
log.Info("Starting server on %q", s.Addr) log.L.Info("Starting server", "addr", s.Addr)
if err := s.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { if err := s.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Error("error listening and serving: %v", err) log.L.Error("error listening and serving", "err", err)
} }
} }
@@ -83,9 +94,9 @@ func shutdownServer(s *http.Server, ctx context.Context, wg *sync.WaitGroup) {
shutdownCtx, cancel := context.WithTimeout(shutdownCtx, 10*time.Second) shutdownCtx, cancel := context.WithTimeout(shutdownCtx, 10*time.Second)
defer cancel() defer cancel()
if err := s.Shutdown(shutdownCtx); err != nil { if err := s.Shutdown(shutdownCtx); err != nil {
log.Error("error shutting down http server: %v", err) log.L.Error("error shutting down http server", "err", err)
} else { } else {
log.Info("Gracefully stopped http server on %v", s.Addr) log.L.Info("Gracefully stopped http server", "addr", s.Addr)
} }
} }
@@ -122,7 +133,7 @@ func createHandler(d *sqlx.DB, serverSettings *types.Settings) http.Handler {
// Serve static files (CSS, JS and images) // Serve static files (CSS, JS and images)
router.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./static/")))) router.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./static/"))))
return middleware.Wrapper( wrapper := middleware.Wrapper(
router, router,
middleware.GenerateRecurringTransactions(transactionRecurringService), middleware.GenerateRecurringTransactions(transactionRecurringService),
middleware.SecurityHeaders(serverSettings), middleware.SecurityHeaders(serverSettings),
@@ -132,4 +143,8 @@ func createHandler(d *sqlx.DB, serverSettings *types.Settings) http.Handler {
middleware.Gzip, middleware.Gzip,
middleware.Log, middleware.Log,
) )
wrapper = otelhttp.NewHandler(wrapper, "http.request")
return wrapper
} }

View File

@@ -36,6 +36,8 @@ func (h AccountImpl) Handle(r *http.ServeMux) {
func (h AccountImpl) handleAccountPage() http.HandlerFunc { func (h AccountImpl) handleAccountPage() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
updateSpan(r)
user := middleware.GetUser(r) user := middleware.GetUser(r)
if user == nil { if user == nil {
utils.DoRedirect(w, r, "/auth/signin") utils.DoRedirect(w, r, "/auth/signin")
@@ -55,6 +57,8 @@ func (h AccountImpl) handleAccountPage() http.HandlerFunc {
func (h AccountImpl) handleAccountItemComp() http.HandlerFunc { func (h AccountImpl) handleAccountItemComp() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
updateSpan(r)
user := middleware.GetUser(r) user := middleware.GetUser(r)
if user == nil { if user == nil {
utils.DoRedirect(w, r, "/auth/signin") utils.DoRedirect(w, r, "/auth/signin")
@@ -86,6 +90,8 @@ func (h AccountImpl) handleAccountItemComp() http.HandlerFunc {
func (h AccountImpl) handleUpdateAccount() http.HandlerFunc { func (h AccountImpl) handleUpdateAccount() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
updateSpan(r)
user := middleware.GetUser(r) user := middleware.GetUser(r)
if user == nil { if user == nil {
utils.DoRedirect(w, r, "/auth/signin") utils.DoRedirect(w, r, "/auth/signin")
@@ -119,6 +125,8 @@ func (h AccountImpl) handleUpdateAccount() http.HandlerFunc {
func (h AccountImpl) handleDeleteAccount() http.HandlerFunc { func (h AccountImpl) handleDeleteAccount() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
updateSpan(r)
user := middleware.GetUser(r) user := middleware.GetUser(r)
if user == nil { if user == nil {
utils.DoRedirect(w, r, "/auth/signin") utils.DoRedirect(w, r, "/auth/signin")

View File

@@ -59,6 +59,8 @@ var (
func (handler AuthImpl) handleSignInPage() http.HandlerFunc { func (handler AuthImpl) handleSignInPage() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
updateSpan(r)
user := middleware.GetUser(r) user := middleware.GetUser(r)
if user != nil { if user != nil {
if !user.EmailVerified { if !user.EmailVerified {
@@ -77,6 +79,8 @@ func (handler AuthImpl) handleSignInPage() http.HandlerFunc {
func (handler AuthImpl) handleSignIn() http.HandlerFunc { func (handler AuthImpl) handleSignIn() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
updateSpan(r)
user, err := utils.WaitMinimumTime(securityWaitDuration, func() (*types.User, error) { user, err := utils.WaitMinimumTime(securityWaitDuration, func() (*types.User, error) {
session := middleware.GetSession(r) session := middleware.GetSession(r)
email := r.FormValue("email") email := r.FormValue("email")
@@ -112,6 +116,8 @@ func (handler AuthImpl) handleSignIn() http.HandlerFunc {
func (handler AuthImpl) handleSignUpPage() http.HandlerFunc { func (handler AuthImpl) handleSignUpPage() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
updateSpan(r)
user := middleware.GetUser(r) user := middleware.GetUser(r)
if user != nil { if user != nil {
@@ -130,6 +136,8 @@ func (handler AuthImpl) handleSignUpPage() http.HandlerFunc {
func (handler AuthImpl) handleSignUpVerifyPage() http.HandlerFunc { func (handler AuthImpl) handleSignUpVerifyPage() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
updateSpan(r)
user := middleware.GetUser(r) user := middleware.GetUser(r)
if user == nil { if user == nil {
utils.DoRedirect(w, r, "/auth/signin") utils.DoRedirect(w, r, "/auth/signin")
@@ -148,6 +156,8 @@ func (handler AuthImpl) handleSignUpVerifyPage() http.HandlerFunc {
func (handler AuthImpl) handleVerifyResendComp() http.HandlerFunc { func (handler AuthImpl) handleVerifyResendComp() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
updateSpan(r)
user := middleware.GetUser(r) user := middleware.GetUser(r)
if user == nil { if user == nil {
utils.DoRedirect(w, r, "/auth/signin") utils.DoRedirect(w, r, "/auth/signin")
@@ -158,13 +168,15 @@ func (handler AuthImpl) handleVerifyResendComp() http.HandlerFunc {
_, err := w.Write([]byte("<p class=\"mt-8\">Verification email sent</p>")) _, err := w.Write([]byte("<p class=\"mt-8\">Verification email sent</p>"))
if err != nil { if err != nil {
log.Error("Could not write response: %v", err) log.L.Error("Could not write response", "err", err)
} }
} }
} }
func (handler AuthImpl) handleSignUpVerifyResponsePage() http.HandlerFunc { func (handler AuthImpl) handleSignUpVerifyResponsePage() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
updateSpan(r)
token := r.URL.Query().Get("token") token := r.URL.Query().Get("token")
err := handler.service.VerifyUserEmail(token) err := handler.service.VerifyUserEmail(token)
@@ -185,17 +197,19 @@ func (handler AuthImpl) handleSignUpVerifyResponsePage() http.HandlerFunc {
func (handler AuthImpl) handleSignUp() http.HandlerFunc { func (handler AuthImpl) handleSignUp() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
updateSpan(r)
var email = r.FormValue("email") var email = r.FormValue("email")
var password = r.FormValue("password") var password = r.FormValue("password")
_, err := utils.WaitMinimumTime(securityWaitDuration, func() (interface{}, error) { _, err := utils.WaitMinimumTime(securityWaitDuration, func() (any, error) {
log.Info("Signing up %v", email) log.L.Info("signing up", "email", email)
user, err := handler.service.SignUp(email, password) user, err := handler.service.SignUp(email, password)
if err != nil { if err != nil {
return nil, err return nil, err
} }
log.Info("Sending verification email to %v", user.Email) log.L.Info("Sending verification email", "to", user.Email)
go handler.service.SendVerificationMail(user.Id, user.Email) go handler.service.SendVerificationMail(user.Id, user.Email)
return nil, nil return nil, nil
}) })
@@ -221,6 +235,8 @@ func (handler AuthImpl) handleSignUp() http.HandlerFunc {
func (handler AuthImpl) handleSignOut() http.HandlerFunc { func (handler AuthImpl) handleSignOut() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
updateSpan(r)
session := middleware.GetSession(r) session := middleware.GetSession(r)
if session != nil { if session != nil {
@@ -248,6 +264,8 @@ func (handler AuthImpl) handleSignOut() http.HandlerFunc {
func (handler AuthImpl) handleDeleteAccountPage() http.HandlerFunc { func (handler AuthImpl) handleDeleteAccountPage() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
updateSpan(r)
user := middleware.GetUser(r) user := middleware.GetUser(r)
if user == nil { if user == nil {
utils.DoRedirect(w, r, "/auth/signin") utils.DoRedirect(w, r, "/auth/signin")
@@ -261,6 +279,8 @@ func (handler AuthImpl) handleDeleteAccountPage() http.HandlerFunc {
func (handler AuthImpl) handleDeleteAccountComp() http.HandlerFunc { func (handler AuthImpl) handleDeleteAccountComp() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
updateSpan(r)
user := middleware.GetUser(r) user := middleware.GetUser(r)
if user == nil { if user == nil {
utils.DoRedirect(w, r, "/auth/signin") utils.DoRedirect(w, r, "/auth/signin")
@@ -285,6 +305,8 @@ func (handler AuthImpl) handleDeleteAccountComp() http.HandlerFunc {
func (handler AuthImpl) handleChangePasswordPage() http.HandlerFunc { func (handler AuthImpl) handleChangePasswordPage() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
updateSpan(r)
isPasswordReset := r.URL.Query().Has("token") isPasswordReset := r.URL.Query().Has("token")
user := middleware.GetUser(r) user := middleware.GetUser(r)
@@ -301,6 +323,8 @@ func (handler AuthImpl) handleChangePasswordPage() http.HandlerFunc {
func (handler AuthImpl) handleChangePasswordComp() http.HandlerFunc { func (handler AuthImpl) handleChangePasswordComp() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
updateSpan(r)
session := middleware.GetSession(r) session := middleware.GetSession(r)
user := middleware.GetUser(r) user := middleware.GetUser(r)
if session == nil || user == nil { if session == nil || user == nil {
@@ -323,6 +347,8 @@ func (handler AuthImpl) handleChangePasswordComp() http.HandlerFunc {
func (handler AuthImpl) handleForgotPasswordPage() http.HandlerFunc { func (handler AuthImpl) handleForgotPasswordPage() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
updateSpan(r)
user := middleware.GetUser(r) user := middleware.GetUser(r)
if user != nil { if user != nil {
utils.DoRedirect(w, r, "/") utils.DoRedirect(w, r, "/")
@@ -336,13 +362,15 @@ func (handler AuthImpl) handleForgotPasswordPage() http.HandlerFunc {
func (handler AuthImpl) handleForgotPasswordComp() http.HandlerFunc { func (handler AuthImpl) handleForgotPasswordComp() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
updateSpan(r)
email := r.FormValue("email") email := r.FormValue("email")
if email == "" { if email == "" {
utils.TriggerToastWithStatus(w, r, "error", "Please enter an email", http.StatusBadRequest) utils.TriggerToastWithStatus(w, r, "error", "Please enter an email", http.StatusBadRequest)
return return
} }
_, err := utils.WaitMinimumTime(securityWaitDuration, func() (interface{}, error) { _, err := utils.WaitMinimumTime(securityWaitDuration, func() (any, error) {
err := handler.service.SendForgotPasswordMail(email) err := handler.service.SendForgotPasswordMail(email)
return nil, err return nil, err
}) })
@@ -357,9 +385,11 @@ func (handler AuthImpl) handleForgotPasswordComp() http.HandlerFunc {
func (handler AuthImpl) handleForgotPasswordResponseComp() http.HandlerFunc { func (handler AuthImpl) handleForgotPasswordResponseComp() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
updateSpan(r)
pageUrl, err := url.Parse(r.Header.Get("Hx-Current-Url")) pageUrl, err := url.Parse(r.Header.Get("Hx-Current-Url"))
if err != nil { if err != nil {
log.Error("Could not get current URL: %v", err) log.L.Error("Could not get current URL", "err", err)
utils.TriggerToastWithStatus(w, r, "error", "Internal Server Error", http.StatusInternalServerError) utils.TriggerToastWithStatus(w, r, "error", "Internal Server Error", http.StatusInternalServerError)
return return
} }

View File

@@ -7,6 +7,9 @@ import (
"spend-sparrow/internal/service" "spend-sparrow/internal/service"
"spend-sparrow/internal/utils" "spend-sparrow/internal/utils"
"strings" "strings"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
) )
func handleError(w http.ResponseWriter, r *http.Request, err error) { func handleError(w http.ResponseWriter, r *http.Request, err error) {
@@ -33,3 +36,11 @@ func extractErrorMessage(err error) string {
return strings.SplitN(errMsg, ":", 2)[0] return strings.SplitN(errMsg, ":", 2)[0]
} }
func updateSpan(r *http.Request) {
currentSpan := trace.SpanFromContext(r.Context())
if currentSpan != nil {
currentSpan.SetAttributes(attribute.String("http.pattern", r.Pattern))
currentSpan.SetAttributes(attribute.String("http.pattern.id", r.PathValue("id")))
}
}

View File

@@ -3,10 +3,15 @@ package middleware
import ( import (
"net/http" "net/http"
"strings" "strings"
"go.opentelemetry.io/otel"
) )
func CacheControl(next http.Handler) http.Handler { func CacheControl(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
counter, _ := otel.Meter("").Int64Counter("spend.sparrow.test")
counter.Add(r.Context(), 1)
shouldCache := strings.HasPrefix(r.URL.Path, "/static") shouldCache := strings.HasPrefix(r.URL.Path, "/static")
if !shouldCache { if !shouldCache {

View File

@@ -46,7 +46,7 @@ func CrossSiteRequestForgery(auth service.Auth) func(http.Handler) http.Handler
csrfToken := r.Header.Get("Csrf-Token") csrfToken := r.Header.Get("Csrf-Token")
if session == nil || csrfToken == "" || !auth.IsCsrfTokenValid(csrfToken, session.Id) { if session == nil || csrfToken == "" || !auth.IsCsrfTokenValid(csrfToken, session.Id) {
log.Info("CSRF-Token \"%s\" not correct", csrfToken) log.L.Info("CSRF-Token not correct", "token", csrfToken)
if r.Header.Get("Hx-Request") == "true" { if r.Header.Get("Hx-Request") == "true" {
utils.TriggerToastWithStatus(w, r, "error", "CSRF-Token not correct", http.StatusBadRequest) utils.TriggerToastWithStatus(w, r, "error", "CSRF-Token not correct", http.StatusBadRequest)
} else { } else {

View File

@@ -34,7 +34,7 @@ func Gzip(next http.Handler) http.Handler {
err := gz.Close() err := gz.Close()
if err != nil && !errors.Is(err, http.ErrBodyNotAllowed) { if err != nil && !errors.Is(err, http.ErrBodyNotAllowed) {
log.Error("Gzip: could not close Writer: %v", err) log.L.Error("Gzip: could not close Writer", "err", err)
} }
}) })
} }

View File

@@ -2,23 +2,8 @@ package middleware
import ( import (
"net/http" "net/http"
"strconv"
"time"
"spend-sparrow/internal/log" "spend-sparrow/internal/log"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)
var (
metrics = promauto.NewCounterVec(
prometheus.CounterOpts{
Name: "mefit_request_total",
Help: "The total number of requests processed",
},
[]string{"path", "method", "status"},
)
) )
type WrappedWriter struct { type WrappedWriter struct {
@@ -35,13 +20,19 @@ func Log(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now() start := time.Now()
log.L.Info("request pattern", "pattern", r.Pattern)
wrapped := &WrappedWriter{ wrapped := &WrappedWriter{
ResponseWriter: w, ResponseWriter: w,
StatusCode: http.StatusOK, StatusCode: http.StatusOK,
} }
next.ServeHTTP(wrapped, r) next.ServeHTTP(wrapped, r)
log.Info(r.RemoteAddr + " " + strconv.Itoa(wrapped.StatusCode) + " " + r.Method + " " + r.URL.Path + " " + time.Since(start).String()) log.L.Info("request",
metrics.WithLabelValues(r.URL.Path, r.Method, http.StatusText(wrapped.StatusCode)).Inc() "remoteAddr", r.RemoteAddr,
"status", wrapped.StatusCode,
"method", r.Method,
"path", r.URL.Path,
"duration", time.Since(start).String())
}) })
} }

View File

@@ -1,13 +1,12 @@
package handler package handler
import ( import (
"net/http"
"spend-sparrow/internal/log" "spend-sparrow/internal/log"
"spend-sparrow/internal/template" "spend-sparrow/internal/template"
"spend-sparrow/internal/template/auth" "spend-sparrow/internal/template/auth"
"spend-sparrow/internal/types" "spend-sparrow/internal/types"
"net/http"
"github.com/a-h/templ" "github.com/a-h/templ"
) )
@@ -23,7 +22,7 @@ func (render *Render) RenderWithStatus(r *http.Request, w http.ResponseWriter, c
w.WriteHeader(status) w.WriteHeader(status)
err := comp.Render(r.Context(), w) err := comp.Render(r.Context(), w)
if err != nil { if err != nil {
log.Error("Failed to render layout: %v", err) log.L.Error("Failed to render layout", "err", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError) http.Error(w, "Internal Server Error", http.StatusInternalServerError)
} }
} }

View File

@@ -29,6 +29,8 @@ func (handler IndexImpl) Handle(router *http.ServeMux) {
func (handler IndexImpl) handleRootAnd404() http.HandlerFunc { func (handler IndexImpl) handleRootAnd404() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
updateSpan(r)
user := middleware.GetUser(r) user := middleware.GetUser(r)
var comp templ.Component var comp templ.Component
@@ -52,6 +54,8 @@ func (handler IndexImpl) handleRootAnd404() http.HandlerFunc {
func (handler IndexImpl) handleEmpty() http.HandlerFunc { func (handler IndexImpl) handleEmpty() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
updateSpan(r)
// Return nothing // Return nothing
} }
} }

View File

@@ -13,6 +13,8 @@ import (
"github.com/a-h/templ" "github.com/a-h/templ"
"github.com/google/uuid" "github.com/google/uuid"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
) )
type Transaction interface { type Transaction interface {
@@ -45,12 +47,17 @@ func (h TransactionImpl) Handle(r *http.ServeMux) {
func (h TransactionImpl) handleTransactionPage() http.HandlerFunc { func (h TransactionImpl) handleTransactionPage() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
updateSpan(r)
user := middleware.GetUser(r) user := middleware.GetUser(r)
if user == nil { if user == nil {
utils.DoRedirect(w, r, "/auth/signin") utils.DoRedirect(w, r, "/auth/signin")
return return
} }
currentSpan := trace.SpanFromContext(r.Context())
currentSpan.SetAttributes(attribute.String("", "test"))
filter := types.TransactionItemsFilter{ filter := types.TransactionItemsFilter{
AccountId: r.URL.Query().Get("account-id"), AccountId: r.URL.Query().Get("account-id"),
TreasureChestId: r.URL.Query().Get("treasure-chest-id"), TreasureChestId: r.URL.Query().Get("treasure-chest-id"),
@@ -89,12 +96,16 @@ func (h TransactionImpl) handleTransactionPage() http.HandlerFunc {
func (h TransactionImpl) handleTransactionItemComp() http.HandlerFunc { func (h TransactionImpl) handleTransactionItemComp() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
updateSpan(r)
user := middleware.GetUser(r) user := middleware.GetUser(r)
if user == nil { if user == nil {
utils.DoRedirect(w, r, "/auth/signin") utils.DoRedirect(w, r, "/auth/signin")
return return
} }
// log.L.Info("request", "pattern", r.Pattern, "path", r.URL.Path, "method", r.Method, "path", r.URL.Path)
accounts, err := h.account.GetAll(user) accounts, err := h.account.GetAll(user)
if err != nil { if err != nil {
handleError(w, r, err) handleError(w, r, err)
@@ -133,6 +144,8 @@ func (h TransactionImpl) handleTransactionItemComp() http.HandlerFunc {
func (h TransactionImpl) handleUpdateTransaction() http.HandlerFunc { func (h TransactionImpl) handleUpdateTransaction() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
updateSpan(r)
user := middleware.GetUser(r) user := middleware.GetUser(r)
if user == nil { if user == nil {
utils.DoRedirect(w, r, "/auth/signin") utils.DoRedirect(w, r, "/auth/signin")
@@ -233,6 +246,8 @@ func (h TransactionImpl) handleUpdateTransaction() http.HandlerFunc {
func (h TransactionImpl) handleRecalculate() http.HandlerFunc { func (h TransactionImpl) handleRecalculate() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
updateSpan(r)
user := middleware.GetUser(r) user := middleware.GetUser(r)
if user == nil { if user == nil {
utils.DoRedirect(w, r, "/auth/signin") utils.DoRedirect(w, r, "/auth/signin")
@@ -251,6 +266,8 @@ func (h TransactionImpl) handleRecalculate() http.HandlerFunc {
func (h TransactionImpl) handleDeleteTransaction() http.HandlerFunc { func (h TransactionImpl) handleDeleteTransaction() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
updateSpan(r)
user := middleware.GetUser(r) user := middleware.GetUser(r)
if user == nil { if user == nil {
utils.DoRedirect(w, r, "/auth/signin") utils.DoRedirect(w, r, "/auth/signin")

View File

@@ -33,6 +33,8 @@ func (h TransactionRecurringImpl) Handle(r *http.ServeMux) {
func (h TransactionRecurringImpl) handleTransactionRecurringItemComp() http.HandlerFunc { func (h TransactionRecurringImpl) handleTransactionRecurringItemComp() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
updateSpan(r)
user := middleware.GetUser(r) user := middleware.GetUser(r)
if user == nil { if user == nil {
utils.DoRedirect(w, r, "/auth/signin") utils.DoRedirect(w, r, "/auth/signin")
@@ -48,6 +50,8 @@ func (h TransactionRecurringImpl) handleTransactionRecurringItemComp() http.Hand
func (h TransactionRecurringImpl) handleUpdateTransactionRecurring() http.HandlerFunc { func (h TransactionRecurringImpl) handleUpdateTransactionRecurring() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
updateSpan(r)
user := middleware.GetUser(r) user := middleware.GetUser(r)
if user == nil { if user == nil {
utils.DoRedirect(w, r, "/auth/signin") utils.DoRedirect(w, r, "/auth/signin")
@@ -85,6 +89,8 @@ func (h TransactionRecurringImpl) handleUpdateTransactionRecurring() http.Handle
func (h TransactionRecurringImpl) handleDeleteTransactionRecurring() http.HandlerFunc { func (h TransactionRecurringImpl) handleDeleteTransactionRecurring() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
updateSpan(r)
user := middleware.GetUser(r) user := middleware.GetUser(r)
if user == nil { if user == nil {
utils.DoRedirect(w, r, "/auth/signin") utils.DoRedirect(w, r, "/auth/signin")

View File

@@ -40,6 +40,8 @@ func (h TreasureChestImpl) Handle(r *http.ServeMux) {
func (h TreasureChestImpl) handleTreasureChestPage() http.HandlerFunc { func (h TreasureChestImpl) handleTreasureChestPage() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
updateSpan(r)
user := middleware.GetUser(r) user := middleware.GetUser(r)
if user == nil { if user == nil {
utils.DoRedirect(w, r, "/auth/signin") utils.DoRedirect(w, r, "/auth/signin")
@@ -67,6 +69,8 @@ func (h TreasureChestImpl) handleTreasureChestPage() http.HandlerFunc {
func (h TreasureChestImpl) handleTreasureChestItemComp() http.HandlerFunc { func (h TreasureChestImpl) handleTreasureChestItemComp() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
updateSpan(r)
user := middleware.GetUser(r) user := middleware.GetUser(r)
if user == nil { if user == nil {
utils.DoRedirect(w, r, "/auth/signin") utils.DoRedirect(w, r, "/auth/signin")
@@ -112,6 +116,8 @@ func (h TreasureChestImpl) handleTreasureChestItemComp() http.HandlerFunc {
func (h TreasureChestImpl) handleUpdateTreasureChest() http.HandlerFunc { func (h TreasureChestImpl) handleUpdateTreasureChest() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
updateSpan(r)
user := middleware.GetUser(r) user := middleware.GetUser(r)
if user == nil { if user == nil {
utils.DoRedirect(w, r, "/auth/signin") utils.DoRedirect(w, r, "/auth/signin")
@@ -155,6 +161,8 @@ func (h TreasureChestImpl) handleUpdateTreasureChest() http.HandlerFunc {
func (h TreasureChestImpl) handleDeleteTreasureChest() http.HandlerFunc { func (h TreasureChestImpl) handleDeleteTreasureChest() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
updateSpan(r)
user := middleware.GetUser(r) user := middleware.GetUser(r)
if user == nil { if user == nil {
utils.DoRedirect(w, r, "/auth/signin") utils.DoRedirect(w, r, "/auth/signin")

View File

@@ -1,56 +1,14 @@
package log package log
import ( import (
"fmt"
"log"
"log/slog" "log/slog"
"strings" // "go.opentelemetry.io/contrib/bridges/otelslog".
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
) )
var ( var (
errorMetric = promauto.NewCounter( L = slog.New(slog.Default().Handler())
prometheus.CounterOpts{
Name: "spendsparrow_error_total",
Help: "The total number of errors during processing",
},
)
) )
func Fatal(message string, args ...interface{}) { func InitOtelLogger() {
errorMetric.Inc() // L = otelslog.NewLogger("spend-sparrow")
s := format(message, args)
log.Fatal(s)
}
func Error(message string, args ...interface{}) {
errorMetric.Inc()
s := format(message, args)
slog.Error(s)
}
func Warn(message string, args ...interface{}) {
s := format(message, args)
slog.Warn(s)
}
func Info(message string, args ...interface{}) {
s := format(message, args)
slog.Info(s)
}
func format(message string, args []interface{}) string {
var w strings.Builder
if len(args) > 0 {
fmt.Fprintf(&w, message, args...)
} else {
w.WriteString(message)
}
return w.String()
} }

123
internal/otel.go Normal file
View File

@@ -0,0 +1,123 @@
package internal
import (
"context"
"errors"
"time"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc"
// "go.opentelemetry.io/otel/exporters/stdout/stdoutlog".
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
// "go.opentelemetry.io/otel/log/global".
"go.opentelemetry.io/otel/propagation"
// "go.opentelemetry.io/otel/sdk/log".
"go.opentelemetry.io/otel/sdk/metric"
"go.opentelemetry.io/otel/sdk/trace"
)
var (
otelEndpoint = "192.168.188.155:4317"
)
// setupOTelSDK bootstraps the OpenTelemetry pipeline.
// If it does not return an error, make sure to call shutdown for proper cleanup.
func setupOTelSDK(ctx context.Context) (func(context.Context) error, error) {
var shutdownFuncs []func(context.Context) error
// shutdown calls cleanup functions registered via shutdownFuncs.
// The errors from the calls are joined.
// Each registered cleanup will be invoked once.
shutdown := func(ctxInternal context.Context) error {
var err error
for _, fn := range shutdownFuncs {
err = errors.Join(err, fn(ctxInternal))
}
shutdownFuncs = nil
return err
}
var err error
// handleErr calls shutdown for cleanup and makes sure that all errors are returned.
handleErr := func(ctxInternal context.Context, inErr error) {
err = errors.Join(inErr, shutdown(ctxInternal))
}
// Set up propagator.
prop := newPropagator()
otel.SetTextMapPropagator(prop)
// Set up trace provider.
tracerProvider, err := newTracerProvider(ctx)
if err != nil {
handleErr(ctx, err)
return nil, err
}
shutdownFuncs = append(shutdownFuncs, tracerProvider.Shutdown)
otel.SetTracerProvider(tracerProvider)
// Set up meter provider.
meterProvider, err := newMeterProvider(ctx)
if err != nil {
handleErr(ctx, err)
return nil, err
}
shutdownFuncs = append(shutdownFuncs, meterProvider.Shutdown)
otel.SetMeterProvider(meterProvider)
// Set up logger provider.
// loggerProvider, err := newLoggerProvider()
// if err != nil {
// handleErr(err)
// return
// }
// shutdownFuncs = append(shutdownFuncs, loggerProvider.Shutdown)
// global.SetLoggerProvider(loggerProvider)
return shutdown, nil
}
func newPropagator() propagation.TextMapPropagator {
return propagation.NewCompositeTextMapPropagator(
propagation.TraceContext{},
propagation.Baggage{},
)
}
func newTracerProvider(ctx context.Context) (*trace.TracerProvider, error) {
exp, err := otlptracegrpc.New(ctx,
otlptracegrpc.WithEndpoint(otelEndpoint),
otlptracegrpc.WithInsecure(),
)
if err != nil {
return nil, err
}
return trace.NewTracerProvider(trace.WithBatcher(exp)), nil
}
func newMeterProvider(ctx context.Context) (*metric.MeterProvider, error) {
exp, err := otlpmetricgrpc.New(ctx,
otlpmetricgrpc.WithInsecure(),
otlpmetricgrpc.WithEndpoint(otelEndpoint))
if err != nil {
return nil, err
}
return metric.NewMeterProvider(
metric.WithReader(
metric.NewPeriodicReader(
exp, metric.WithInterval(15*time.Second)))), nil
}
// func newLoggerProvider() (*log.LoggerProvider, error) {
// logExporter, err := stdoutlog.New()
// if err != nil {
// return nil, err
// }
//
// loggerProvider := log.NewLoggerProvider(
// log.WithProcessor(log.NewBatchProcessor(logExporter)),
// )
// return loggerProvider, nil
// }

View File

@@ -9,18 +9,6 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)
var (
accountMetric = promauto.NewCounterVec(
prometheus.CounterOpts{
Name: "spendsparrow_account_total",
Help: "The total of account operations",
},
[]string{"operation"},
)
) )
type Account interface { type Account interface {
@@ -46,8 +34,6 @@ func NewAccount(db *sqlx.DB, random Random, clock Clock) Account {
} }
func (s AccountImpl) Add(user *types.User, name string) (*types.Account, error) { func (s AccountImpl) Add(user *types.User, name string) (*types.Account, error) {
accountMetric.WithLabelValues("add").Inc()
if user == nil { if user == nil {
return nil, ErrUnauthorized return nil, ErrUnauthorized
} }
@@ -90,7 +76,6 @@ func (s AccountImpl) Add(user *types.User, name string) (*types.Account, error)
} }
func (s AccountImpl) UpdateName(user *types.User, id string, name string) (*types.Account, error) { func (s AccountImpl) UpdateName(user *types.User, id string, name string) (*types.Account, error) {
accountMetric.WithLabelValues("update").Inc()
if user == nil { if user == nil {
return nil, ErrUnauthorized return nil, ErrUnauthorized
} }
@@ -100,7 +85,7 @@ func (s AccountImpl) UpdateName(user *types.User, id string, name string) (*type
} }
uuid, err := uuid.Parse(id) uuid, err := uuid.Parse(id)
if err != nil { if err != nil {
log.Error("account update: %v", err) log.L.Error("account update", "err", err)
return nil, fmt.Errorf("could not parse Id: %w", ErrBadRequest) return nil, fmt.Errorf("could not parse Id: %w", ErrBadRequest)
} }
@@ -151,14 +136,12 @@ func (s AccountImpl) UpdateName(user *types.User, id string, name string) (*type
} }
func (s AccountImpl) Get(user *types.User, id string) (*types.Account, error) { func (s AccountImpl) Get(user *types.User, id string) (*types.Account, error) {
accountMetric.WithLabelValues("get").Inc()
if user == nil { if user == nil {
return nil, ErrUnauthorized return nil, ErrUnauthorized
} }
uuid, err := uuid.Parse(id) uuid, err := uuid.Parse(id)
if err != nil { if err != nil {
log.Error("account get: %v", err) log.L.Error("account get", "err", err)
return nil, fmt.Errorf("could not parse Id: %w", ErrBadRequest) return nil, fmt.Errorf("could not parse Id: %w", ErrBadRequest)
} }
@@ -167,7 +150,7 @@ func (s AccountImpl) Get(user *types.User, id string) (*types.Account, error) {
SELECT * FROM account WHERE user_id = ? AND id = ?`, user.Id, uuid) SELECT * FROM account WHERE user_id = ? AND id = ?`, user.Id, uuid)
err = db.TransformAndLogDbError("account Get", nil, err) err = db.TransformAndLogDbError("account Get", nil, err)
if err != nil { if err != nil {
log.Error("account get: %v", err) log.L.Error("account get", "err", err)
return nil, err return nil, err
} }
@@ -175,7 +158,6 @@ func (s AccountImpl) Get(user *types.User, id string) (*types.Account, error) {
} }
func (s AccountImpl) GetAll(user *types.User) ([]*types.Account, error) { func (s AccountImpl) GetAll(user *types.User) ([]*types.Account, error) {
accountMetric.WithLabelValues("get_all").Inc()
if user == nil { if user == nil {
return nil, ErrUnauthorized return nil, ErrUnauthorized
} }
@@ -192,13 +174,12 @@ func (s AccountImpl) GetAll(user *types.User) ([]*types.Account, error) {
} }
func (s AccountImpl) Delete(user *types.User, id string) error { func (s AccountImpl) Delete(user *types.User, id string) error {
accountMetric.WithLabelValues("delete").Inc()
if user == nil { if user == nil {
return ErrUnauthorized return ErrUnauthorized
} }
uuid, err := uuid.Parse(id) uuid, err := uuid.Parse(id)
if err != nil { if err != nil {
log.Error("account delete: %v", err) log.L.Error("account delete", "err", err)
return fmt.Errorf("could not parse Id: %w", ErrBadRequest) return fmt.Errorf("could not parse Id: %w", ErrBadRequest)
} }

View File

@@ -125,7 +125,7 @@ func (service AuthImpl) SignInAnonymous() (*types.Session, error) {
return nil, types.ErrInternal return nil, types.ErrInternal
} }
log.Info("Anonymous session created: %v", session.Id) log.L.Info("anonymous session created", "session-id", session.Id)
return session, nil return session, nil
} }
@@ -201,7 +201,7 @@ func (service AuthImpl) SendVerificationMail(userId uuid.UUID, email string) {
var w strings.Builder var w strings.Builder
err = mailTemplate.Register(service.serverSettings.BaseUrl, token.Token).Render(context.Background(), &w) err = mailTemplate.Register(service.serverSettings.BaseUrl, token.Token).Render(context.Background(), &w)
if err != nil { if err != nil {
log.Error("Could not render welcome email: %v", err) log.L.Error("Could not render welcome email", "err", err)
return return
} }
@@ -340,7 +340,7 @@ func (service AuthImpl) SendForgotPasswordMail(email string) error {
var mail strings.Builder var mail strings.Builder
err = mailTemplate.ResetPassword(service.serverSettings.BaseUrl, token.Token).Render(context.Background(), &mail) err = mailTemplate.ResetPassword(service.serverSettings.BaseUrl, token.Token).Render(context.Background(), &mail)
if err != nil { if err != nil {
log.Error("Could not render reset password email: %v", err) log.L.Error("Could not render reset password email", "err", err)
return types.ErrInternal return types.ErrInternal
} }
service.mail.SendMail(email, "Reset Password", mail.String()) service.mail.SendMail(email, "Reset Password", mail.String())
@@ -370,7 +370,7 @@ func (service AuthImpl) ForgotPassword(tokenStr string, newPass string) error {
user, err := service.db.GetUser(token.UserId) user, err := service.db.GetUser(token.UserId)
if err != nil { if err != nil {
log.Error("Could not get user from token: %v", err) log.L.Error("Could not get user from token", "err", err)
return types.ErrInternal return types.ErrInternal
} }
@@ -440,7 +440,7 @@ func (service AuthImpl) GetCsrfToken(session *types.Session) (string, error) {
return "", types.ErrInternal return "", types.ErrInternal
} }
log.Info("CSRF-Token created: %v", tokenStr) log.L.Info("CSRF-Token created", "token", tokenStr)
return tokenStr, nil return tokenStr, nil
} }

View File

@@ -47,9 +47,9 @@ func (m MailImpl) internalSendMail(to string, subject string, message string) {
subject, subject,
message) message)
log.Info("Sending mail to %v", to) log.L.Info("sending mail", "to", to)
err := smtp.SendMail(s.Host+":"+s.Port, auth, s.FromMail, []string{to}, []byte(msg)) err := smtp.SendMail(s.Host+":"+s.Port, auth, s.FromMail, []string{to}, []byte(msg))
if err != nil { if err != nil {
log.Error("Error sending mail: %v", err) log.L.Error("Error sending mail", "err", err)
} }
} }

View File

@@ -26,7 +26,7 @@ func (r *RandomImpl) Bytes(size int) ([]byte, error) {
b := make([]byte, 32) b := make([]byte, 32)
_, err := rand.Read(b) _, err := rand.Read(b)
if err != nil { if err != nil {
log.Error("Error generating random bytes: %v", err) log.L.Error("Error generating random bytes", "err", err)
return []byte{}, types.ErrInternal return []byte{}, types.ErrInternal
} }
@@ -36,7 +36,7 @@ func (r *RandomImpl) Bytes(size int) ([]byte, error) {
func (r *RandomImpl) String(size int) (string, error) { func (r *RandomImpl) String(size int) (string, error) {
bytes, err := r.Bytes(size) bytes, err := r.Bytes(size)
if err != nil { if err != nil {
log.Error("Error generating random string: %v", err) log.L.Error("Error generating random string", "err", err)
return "", types.ErrInternal return "", types.ErrInternal
} }
@@ -46,7 +46,7 @@ func (r *RandomImpl) String(size int) (string, error) {
func (r *RandomImpl) UUID() (uuid.UUID, error) { func (r *RandomImpl) UUID() (uuid.UUID, error) {
id, err := uuid.NewRandom() id, err := uuid.NewRandom()
if err != nil { if err != nil {
log.Error("Error generating random UUID: %v", err) log.L.Error("Error generating random UUID", "err", err)
return uuid.Nil, types.ErrInternal return uuid.Nil, types.ErrInternal
} }

View File

@@ -10,18 +10,6 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)
var (
transactionMetric = promauto.NewCounterVec(
prometheus.CounterOpts{
Name: "spendsparrow_transaction_total",
Help: "The total of transaction operations",
},
[]string{"operation"},
)
) )
type Transaction interface { type Transaction interface {
@@ -49,8 +37,6 @@ func NewTransaction(db *sqlx.DB, random Random, clock Clock) Transaction {
} }
func (s TransactionImpl) Add(tx *sqlx.Tx, user *types.User, transactionInput types.Transaction) (*types.Transaction, error) { func (s TransactionImpl) Add(tx *sqlx.Tx, user *types.User, transactionInput types.Transaction) (*types.Transaction, error) {
transactionMetric.WithLabelValues("add").Inc()
if user == nil { if user == nil {
return nil, ErrUnauthorized return nil, ErrUnauthorized
} }
@@ -118,7 +104,6 @@ func (s TransactionImpl) Add(tx *sqlx.Tx, user *types.User, transactionInput typ
} }
func (s TransactionImpl) Update(user *types.User, input types.Transaction) (*types.Transaction, error) { func (s TransactionImpl) Update(user *types.User, input types.Transaction) (*types.Transaction, error) {
transactionMetric.WithLabelValues("update").Inc()
if user == nil { if user == nil {
return nil, ErrUnauthorized return nil, ErrUnauthorized
} }
@@ -218,14 +203,12 @@ func (s TransactionImpl) Update(user *types.User, input types.Transaction) (*typ
} }
func (s TransactionImpl) Get(user *types.User, id string) (*types.Transaction, error) { func (s TransactionImpl) Get(user *types.User, id string) (*types.Transaction, error) {
transactionMetric.WithLabelValues("get").Inc()
if user == nil { if user == nil {
return nil, ErrUnauthorized return nil, ErrUnauthorized
} }
uuid, err := uuid.Parse(id) uuid, err := uuid.Parse(id)
if err != nil { if err != nil {
log.Error("transaction get: %v", err) log.L.Error("transaction get", "err", err)
return nil, fmt.Errorf("could not parse Id: %w", ErrBadRequest) return nil, fmt.Errorf("could not parse Id: %w", ErrBadRequest)
} }
@@ -243,7 +226,6 @@ func (s TransactionImpl) Get(user *types.User, id string) (*types.Transaction, e
} }
func (s TransactionImpl) GetAll(user *types.User, filter types.TransactionItemsFilter) ([]*types.Transaction, error) { func (s TransactionImpl) GetAll(user *types.User, filter types.TransactionItemsFilter) ([]*types.Transaction, error) {
transactionMetric.WithLabelValues("get_all").Inc()
if user == nil { if user == nil {
return nil, ErrUnauthorized return nil, ErrUnauthorized
} }
@@ -273,13 +255,12 @@ func (s TransactionImpl) GetAll(user *types.User, filter types.TransactionItemsF
} }
func (s TransactionImpl) Delete(user *types.User, id string) error { func (s TransactionImpl) Delete(user *types.User, id string) error {
transactionMetric.WithLabelValues("delete").Inc()
if user == nil { if user == nil {
return ErrUnauthorized return ErrUnauthorized
} }
uuid, err := uuid.Parse(id) uuid, err := uuid.Parse(id)
if err != nil { if err != nil {
log.Error("transaction delete: %v", err) log.L.Error("transaction delete", "err", err)
return fmt.Errorf("could not parse Id: %w", ErrBadRequest) return fmt.Errorf("could not parse Id: %w", ErrBadRequest)
} }
@@ -339,7 +320,6 @@ func (s TransactionImpl) Delete(user *types.User, id string) error {
} }
func (s TransactionImpl) RecalculateBalances(user *types.User) error { func (s TransactionImpl) RecalculateBalances(user *types.User) error {
transactionMetric.WithLabelValues("recalculate").Inc()
if user == nil { if user == nil {
return ErrUnauthorized return ErrUnauthorized
} }
@@ -382,7 +362,7 @@ func (s TransactionImpl) RecalculateBalances(user *types.User) error {
defer func() { defer func() {
err := rows.Close() err := rows.Close()
if err != nil { if err != nil {
log.Error("transaction RecalculateBalances: %v", err) log.L.Error("transaction RecalculateBalances", "err", err)
} }
}() }()
@@ -475,7 +455,7 @@ func (s TransactionImpl) validateAndEnrichTransaction(tx *sqlx.Tx, oldTransactio
return nil, err return nil, err
} }
if rowCount == 0 { if rowCount == 0 {
log.Error("transaction validate: %v", err) log.L.Error("transaction validate", "err", err)
return nil, fmt.Errorf("account not found: %w", ErrBadRequest) return nil, fmt.Errorf("account not found: %w", ErrBadRequest)
} }
} }

View File

@@ -11,18 +11,6 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)
var (
transactionRecurringMetric = promauto.NewCounterVec(
prometheus.CounterOpts{
Name: "spendsparrow_transaction_recurring_total",
Help: "The total of transactionRecurring operations",
},
[]string{"operation"},
)
) )
type TransactionRecurring interface { type TransactionRecurring interface {
@@ -54,9 +42,8 @@ func NewTransactionRecurring(db *sqlx.DB, random Random, clock Clock, transactio
func (s TransactionRecurringImpl) Add( func (s TransactionRecurringImpl) Add(
user *types.User, user *types.User,
transactionRecurringInput types.TransactionRecurringInput) (*types.TransactionRecurring, error) { transactionRecurringInput types.TransactionRecurringInput,
transactionRecurringMetric.WithLabelValues("add").Inc() ) (*types.TransactionRecurring, error) {
if user == nil { if user == nil {
return nil, ErrUnauthorized return nil, ErrUnauthorized
} }
@@ -97,14 +84,14 @@ func (s TransactionRecurringImpl) Add(
func (s TransactionRecurringImpl) Update( func (s TransactionRecurringImpl) Update(
user *types.User, user *types.User,
input types.TransactionRecurringInput) (*types.TransactionRecurring, error) { input types.TransactionRecurringInput,
transactionRecurringMetric.WithLabelValues("update").Inc() ) (*types.TransactionRecurring, error) {
if user == nil { if user == nil {
return nil, ErrUnauthorized return nil, ErrUnauthorized
} }
uuid, err := uuid.Parse(input.Id) uuid, err := uuid.Parse(input.Id)
if err != nil { if err != nil {
log.Error("transactionRecurring update: %v", err) log.L.Error("transactionRecurring update", "err", err)
return nil, fmt.Errorf("could not parse Id: %w", ErrBadRequest) return nil, fmt.Errorf("could not parse Id: %w", ErrBadRequest)
} }
@@ -161,7 +148,6 @@ func (s TransactionRecurringImpl) Update(
} }
func (s TransactionRecurringImpl) GetAll(user *types.User) ([]*types.TransactionRecurring, error) { func (s TransactionRecurringImpl) GetAll(user *types.User) ([]*types.TransactionRecurring, error) {
transactionRecurringMetric.WithLabelValues("get_all_by_account").Inc()
if user == nil { if user == nil {
return nil, ErrUnauthorized return nil, ErrUnauthorized
} }
@@ -182,14 +168,13 @@ func (s TransactionRecurringImpl) GetAll(user *types.User) ([]*types.Transaction
} }
func (s TransactionRecurringImpl) GetAllByAccount(user *types.User, accountId string) ([]*types.TransactionRecurring, error) { func (s TransactionRecurringImpl) GetAllByAccount(user *types.User, accountId string) ([]*types.TransactionRecurring, error) {
transactionRecurringMetric.WithLabelValues("get_all_by_account").Inc()
if user == nil { if user == nil {
return nil, ErrUnauthorized return nil, ErrUnauthorized
} }
accountUuid, err := uuid.Parse(accountId) accountUuid, err := uuid.Parse(accountId)
if err != nil { if err != nil {
log.Error("transactionRecurring GetAllByAccount: %v", err) log.L.Error("transactionRecurring GetAllByAccount", "err", err)
return nil, fmt.Errorf("could not parse accountId: %w", ErrBadRequest) return nil, fmt.Errorf("could not parse accountId: %w", ErrBadRequest)
} }
@@ -234,15 +219,17 @@ func (s TransactionRecurringImpl) GetAllByAccount(user *types.User, accountId st
return transactionRecurrings, nil return transactionRecurrings, nil
} }
func (s TransactionRecurringImpl) GetAllByTreasureChest(user *types.User, treasureChestId string) ([]*types.TransactionRecurring, error) { func (s TransactionRecurringImpl) GetAllByTreasureChest(
transactionRecurringMetric.WithLabelValues("get_all_by_treasurechest").Inc() user *types.User,
treasureChestId string,
) ([]*types.TransactionRecurring, error) {
if user == nil { if user == nil {
return nil, ErrUnauthorized return nil, ErrUnauthorized
} }
treasureChestUuid, err := uuid.Parse(treasureChestId) treasureChestUuid, err := uuid.Parse(treasureChestId)
if err != nil { if err != nil {
log.Error("transactionRecurring GetAllByTreasureChest: %v", err) log.L.Error("transactionRecurring GetAllByTreasureChest", "err", err)
return nil, fmt.Errorf("could not parse treasureChestId: %w", ErrBadRequest) return nil, fmt.Errorf("could not parse treasureChestId: %w", ErrBadRequest)
} }
@@ -288,13 +275,12 @@ func (s TransactionRecurringImpl) GetAllByTreasureChest(user *types.User, treasu
} }
func (s TransactionRecurringImpl) Delete(user *types.User, id string) error { func (s TransactionRecurringImpl) Delete(user *types.User, id string) error {
transactionRecurringMetric.WithLabelValues("delete").Inc()
if user == nil { if user == nil {
return ErrUnauthorized return ErrUnauthorized
} }
uuid, err := uuid.Parse(id) uuid, err := uuid.Parse(id)
if err != nil { if err != nil {
log.Error("transactionRecurring delete: %v", err) log.L.Error("transactionRecurring delete", "err", err)
return fmt.Errorf("could not parse Id: %w", ErrBadRequest) return fmt.Errorf("could not parse Id: %w", ErrBadRequest)
} }
@@ -389,7 +375,8 @@ func (s TransactionRecurringImpl) validateAndEnrichTransactionRecurring(
tx *sqlx.Tx, tx *sqlx.Tx,
oldTransactionRecurring *types.TransactionRecurring, oldTransactionRecurring *types.TransactionRecurring,
userId uuid.UUID, userId uuid.UUID,
input types.TransactionRecurringInput) (*types.TransactionRecurring, error) { input types.TransactionRecurringInput,
) (*types.TransactionRecurring, error) {
var ( var (
id uuid.UUID id uuid.UUID
accountUuid *uuid.UUID accountUuid *uuid.UUID
@@ -425,7 +412,7 @@ func (s TransactionRecurringImpl) validateAndEnrichTransactionRecurring(
if input.AccountId != "" { if input.AccountId != "" {
temp, err := uuid.Parse(input.AccountId) temp, err := uuid.Parse(input.AccountId)
if err != nil { if err != nil {
log.Error("transactionRecurring validate: %v", err) log.L.Error("transactionRecurring validate", "err", err)
return nil, fmt.Errorf("could not parse accountId: %w", ErrBadRequest) return nil, fmt.Errorf("could not parse accountId: %w", ErrBadRequest)
} }
accountUuid = &temp accountUuid = &temp
@@ -435,7 +422,7 @@ func (s TransactionRecurringImpl) validateAndEnrichTransactionRecurring(
return nil, err return nil, err
} }
if rowCount == 0 { if rowCount == 0 {
log.Error("transactionRecurring validate: %v", err) log.L.Error("transactionRecurring validate", "err", err)
return nil, fmt.Errorf("account not found: %w", ErrBadRequest) return nil, fmt.Errorf("account not found: %w", ErrBadRequest)
} }
@@ -445,7 +432,7 @@ func (s TransactionRecurringImpl) validateAndEnrichTransactionRecurring(
if input.TreasureChestId != "" { if input.TreasureChestId != "" {
temp, err := uuid.Parse(input.TreasureChestId) temp, err := uuid.Parse(input.TreasureChestId)
if err != nil { if err != nil {
log.Error("transactionRecurring validate: %v", err) log.L.Error("transactionRecurring validate", "err", err)
return nil, fmt.Errorf("could not parse treasureChestId: %w", ErrBadRequest) return nil, fmt.Errorf("could not parse treasureChestId: %w", ErrBadRequest)
} }
treasureChestUuid = &temp treasureChestUuid = &temp
@@ -465,17 +452,17 @@ func (s TransactionRecurringImpl) validateAndEnrichTransactionRecurring(
} }
if !hasAccount && !hasTreasureChest { if !hasAccount && !hasTreasureChest {
log.Error("transactionRecurring validate: %v", err) log.L.Error("transactionRecurring validate", "err", err)
return nil, fmt.Errorf("either account or treasure chest is required: %w", ErrBadRequest) return nil, fmt.Errorf("either account or treasure chest is required: %w", ErrBadRequest)
} }
if hasAccount && hasTreasureChest { if hasAccount && hasTreasureChest {
log.Error("transactionRecurring validate: %v", err) log.L.Error("transactionRecurring validate", "err", err)
return nil, fmt.Errorf("either account or treasure chest is required, not both: %w", ErrBadRequest) return nil, fmt.Errorf("either account or treasure chest is required, not both: %w", ErrBadRequest)
} }
valueFloat, err := strconv.ParseFloat(input.Value, 64) valueFloat, err := strconv.ParseFloat(input.Value, 64)
if err != nil { if err != nil {
log.Error("transactionRecurring validate: %v", err) log.L.Error("transactionRecurring validate", "err", err)
return nil, fmt.Errorf("could not parse value: %w", ErrBadRequest) return nil, fmt.Errorf("could not parse value: %w", ErrBadRequest)
} }
valueInt := int64(valueFloat * DECIMALS_MULTIPLIER) valueInt := int64(valueFloat * DECIMALS_MULTIPLIER)
@@ -494,18 +481,18 @@ func (s TransactionRecurringImpl) validateAndEnrichTransactionRecurring(
} }
intervalMonths, err = strconv.ParseInt(input.IntervalMonths, 10, 0) intervalMonths, err = strconv.ParseInt(input.IntervalMonths, 10, 0)
if err != nil { if err != nil {
log.Error("transactionRecurring validate: %v", err) log.L.Error("transactionRecurring validate", "err", err)
return nil, fmt.Errorf("could not parse intervalMonths: %w", ErrBadRequest) return nil, fmt.Errorf("could not parse intervalMonths: %w", ErrBadRequest)
} }
if intervalMonths < 1 { if intervalMonths < 1 {
log.Error("transactionRecurring validate: %v", err) log.L.Error("transactionRecurring validate", "err", err)
return nil, fmt.Errorf("intervalMonths needs to be greater than 0: %w", ErrBadRequest) return nil, fmt.Errorf("intervalMonths needs to be greater than 0: %w", ErrBadRequest)
} }
var nextExecution *time.Time = nil var nextExecution *time.Time = nil
if input.NextExecution != "" { if input.NextExecution != "" {
t, err := time.Parse("2006-01-02", input.NextExecution) t, err := time.Parse("2006-01-02", input.NextExecution)
if err != nil { if err != nil {
log.Error("transaction validate: %v", err) log.L.Error("transaction validate", "err", err)
return nil, fmt.Errorf("could not parse timestamp: %w", ErrBadRequest) return nil, fmt.Errorf("could not parse timestamp: %w", ErrBadRequest)
} }

View File

@@ -10,18 +10,6 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)
var (
treasureChestMetric = promauto.NewCounterVec(
prometheus.CounterOpts{
Name: "spendsparrow_treasurechest_total",
Help: "The total of treasurechest operations",
},
[]string{"operation"},
)
) )
type TreasureChest interface { type TreasureChest interface {
@@ -47,8 +35,6 @@ func NewTreasureChest(db *sqlx.DB, random Random, clock Clock) TreasureChest {
} }
func (s TreasureChestImpl) Add(user *types.User, parentId, name string) (*types.TreasureChest, error) { func (s TreasureChestImpl) Add(user *types.User, parentId, name string) (*types.TreasureChest, error) {
treasureChestMetric.WithLabelValues("add").Inc()
if user == nil { if user == nil {
return nil, ErrUnauthorized return nil, ErrUnauthorized
} }
@@ -102,7 +88,6 @@ func (s TreasureChestImpl) Add(user *types.User, parentId, name string) (*types.
} }
func (s TreasureChestImpl) Update(user *types.User, idStr, parentId, name string) (*types.TreasureChest, error) { func (s TreasureChestImpl) Update(user *types.User, idStr, parentId, name string) (*types.TreasureChest, error) {
treasureChestMetric.WithLabelValues("update").Inc()
if user == nil { if user == nil {
return nil, ErrUnauthorized return nil, ErrUnauthorized
} }
@@ -112,7 +97,7 @@ func (s TreasureChestImpl) Update(user *types.User, idStr, parentId, name string
} }
id, err := uuid.Parse(idStr) id, err := uuid.Parse(idStr)
if err != nil { if err != nil {
log.Error("treasureChest update: %v", err) log.L.Error("treasureChest update", "err", err)
return nil, fmt.Errorf("could not parse Id: %w", ErrBadRequest) return nil, fmt.Errorf("could not parse Id: %w", ErrBadRequest)
} }
@@ -185,14 +170,12 @@ func (s TreasureChestImpl) Update(user *types.User, idStr, parentId, name string
} }
func (s TreasureChestImpl) Get(user *types.User, id string) (*types.TreasureChest, error) { func (s TreasureChestImpl) Get(user *types.User, id string) (*types.TreasureChest, error) {
treasureChestMetric.WithLabelValues("get").Inc()
if user == nil { if user == nil {
return nil, ErrUnauthorized return nil, ErrUnauthorized
} }
uuid, err := uuid.Parse(id) uuid, err := uuid.Parse(id)
if err != nil { if err != nil {
log.Error("treasureChest get: %v", err) log.L.Error("treasureChest get", "err", err)
return nil, fmt.Errorf("could not parse Id: %w", ErrBadRequest) return nil, fmt.Errorf("could not parse Id: %w", ErrBadRequest)
} }
@@ -210,7 +193,6 @@ func (s TreasureChestImpl) Get(user *types.User, id string) (*types.TreasureChes
} }
func (s TreasureChestImpl) GetAll(user *types.User) ([]*types.TreasureChest, error) { func (s TreasureChestImpl) GetAll(user *types.User) ([]*types.TreasureChest, error) {
treasureChestMetric.WithLabelValues("get_all").Inc()
if user == nil { if user == nil {
return nil, ErrUnauthorized return nil, ErrUnauthorized
} }
@@ -226,13 +208,12 @@ func (s TreasureChestImpl) GetAll(user *types.User) ([]*types.TreasureChest, err
} }
func (s TreasureChestImpl) Delete(user *types.User, idStr string) error { func (s TreasureChestImpl) Delete(user *types.User, idStr string) error {
treasureChestMetric.WithLabelValues("delete").Inc()
if user == nil { if user == nil {
return ErrUnauthorized return ErrUnauthorized
} }
id, err := uuid.Parse(idStr) id, err := uuid.Parse(idStr)
if err != nil { if err != nil {
log.Error("treasureChest delete: %v", err) log.L.Error("treasureChest delete", "err", err)
return fmt.Errorf("could not parse Id: %w", ErrBadRequest) return fmt.Errorf("could not parse Id: %w", ErrBadRequest)
} }

View File

@@ -1,12 +1,17 @@
package types package types
import ( import (
"errors"
"spend-sparrow/internal/log" "spend-sparrow/internal/log"
) )
var (
ErrMissingConfig = errors.New("missing config")
)
type Settings struct { type Settings struct {
Port string Port string
PrometheusEnabled bool OtelEnabled bool
BaseUrl string BaseUrl string
Environment string Environment string
@@ -22,37 +27,46 @@ type SmtpSettings struct {
FromName string FromName string
} }
func NewSettingsFromEnv(env func(string) string) *Settings { func NewSettingsFromEnv(env func(string) string) (*Settings, error) {
var smtp *SmtpSettings var (
smtp *SmtpSettings
err error
)
if env("SMTP_ENABLED") == "true" { if env("SMTP_ENABLED") == "true" {
smtp = getSmtpSettings(env) smtp, err = getSmtpSettings(env)
if err != nil {
return nil, err
}
} }
settings := &Settings{ settings := &Settings{
Port: env("PORT"), Port: env("PORT"),
PrometheusEnabled: env("PROMETHEUS_ENABLED") == "true", OtelEnabled: env("OTEL_ENABLED") == "true",
BaseUrl: env("BASE_URL"), BaseUrl: env("BASE_URL"),
Environment: env("ENVIRONMENT"), Environment: env("ENVIRONMENT"),
Smtp: smtp, Smtp: smtp,
} }
if settings.BaseUrl == "" { if settings.BaseUrl == "" {
log.Fatal("BASE_URL must be set") log.L.Error("BASE_URL must be set")
return nil, ErrMissingConfig
} }
if settings.Port == "" { if settings.Port == "" {
log.Fatal("PORT must be set") log.L.Error("PORT must be set")
return nil, ErrMissingConfig
} }
if settings.Environment == "" { if settings.Environment == "" {
log.Fatal("ENVIRONMENT must be set") log.L.Error("ENVIRONMENT must be set")
return nil, ErrMissingConfig
} }
log.Info("BASE_URL is %q", settings.BaseUrl) log.L.Info("settings read", "BASE_URL", settings.BaseUrl)
log.Info("ENVIRONMENT is %q", settings.Environment) log.L.Info("settings read", "ENVIRONMENT", settings.Environment)
return settings return settings, nil
} }
func getSmtpSettings(env func(string) string) *SmtpSettings { func getSmtpSettings(env func(string) string) (*SmtpSettings, error) {
smtp := SmtpSettings{ smtp := SmtpSettings{
Host: env("SMTP_HOST"), Host: env("SMTP_HOST"),
Port: env("SMTP_PORT"), Port: env("SMTP_PORT"),
@@ -63,23 +77,29 @@ func getSmtpSettings(env func(string) string) *SmtpSettings {
} }
if smtp.Host == "" { if smtp.Host == "" {
log.Fatal("SMTP_HOST must be set") log.L.Error("SMTP_HOST must be set")
return nil, ErrMissingConfig
} }
if smtp.Port == "" { if smtp.Port == "" {
log.Fatal("SMTP_PORT must be set") log.L.Error("SMTP_PORT must be set")
return nil, ErrMissingConfig
} }
if smtp.User == "" { if smtp.User == "" {
log.Fatal("SMTP_USER must be set") log.L.Error("SMTP_USER must be set")
return nil, ErrMissingConfig
} }
if smtp.Pass == "" { if smtp.Pass == "" {
log.Fatal("SMTP_PASS must be set") log.L.Error("SMTP_PASS must be set")
return nil, ErrMissingConfig
} }
if smtp.FromMail == "" { if smtp.FromMail == "" {
log.Fatal("SMTP_FROM_MAIL must be set") log.L.Error("SMTP_FROM_MAIL must be set")
return nil, ErrMissingConfig
} }
if smtp.FromName == "" { if smtp.FromName == "" {
log.Fatal("SMTP_FROM_NAME must be set") log.L.Error("SMTP_FROM_NAME must be set")
return nil, ErrMissingConfig
} }
return &smtp return &smtp, nil
} }

View File

@@ -13,7 +13,7 @@ func TriggerToast(w http.ResponseWriter, r *http.Request, class string, message
if IsHtmx(r) { if IsHtmx(r) {
w.Header().Set("Hx-Trigger", fmt.Sprintf(`{"toast": "%v|%v"}`, class, strings.ReplaceAll(message, `"`, `\"`))) w.Header().Set("Hx-Trigger", fmt.Sprintf(`{"toast": "%v|%v"}`, class, strings.ReplaceAll(message, `"`, `\"`)))
} else { } else {
log.Error("Trying to trigger toast in non-HTMX request") log.L.Error("Trying to trigger toast in non-HTMX request")
} }
} }

16
main.go
View File

@@ -14,21 +14,23 @@ import (
func main() { func main() {
err := godotenv.Load() err := godotenv.Load()
if err != nil { if err != nil {
log.Fatal("Error loading .env file") log.L.Error("Error loading .env file")
return
} }
db, err := sqlx.Open("sqlite3", "./data/spend-sparrow.db") db, err := sqlx.Open("sqlite3", "./data/spend-sparrow.db")
if err != nil { if err != nil {
log.Fatal("Could not open Database data.db: %v", err) log.L.Error("Could not open Database data.db", "err", err)
return
} }
defer func() { defer func() {
err := db.Close() if err = db.Close(); err != nil {
log.Fatal("Could not close Database data.db: %v", err) log.L.Error("Database close failed", "err", err)
}
}() }()
err = internal.Run(context.Background(), db, "", os.Getenv) if err = internal.Run(context.Background(), db, "", os.Getenv); err != nil {
if err != nil { log.L.Error("Error running server", "err", err)
log.Error("Error running server: %v", err)
return return
} }
} }

View File

@@ -18,7 +18,7 @@ import (
var ( var (
settings = types.Settings{ settings = types.Settings{
Port: "", Port: "",
PrometheusEnabled: false, OtelEnabled: false,
BaseUrl: "", BaseUrl: "",
Environment: "test", Environment: "test",
Smtp: nil, Smtp: nil,

View File

@@ -72,7 +72,7 @@ func getEnv(port int64) func(string) string {
return strconv.Itoa(int(port)) return strconv.Itoa(int(port))
case "SMTP_ENABLED": case "SMTP_ENABLED":
return "false" return "false"
case "PROMETHEUS_ENABLED": case "OLTP_ENABLED":
return "false" return "false"
case "BASE_URL": case "BASE_URL":
return "http://localhost:" + strconv.Itoa(int(port)) return "http://localhost:" + strconv.Itoa(int(port))