From 3e7251ef9d6720ad9c78ede980f1e0f5a51b1978 Mon Sep 17 00:00:00 2001 From: Tim Wundenberg Date: Sat, 7 Jun 2025 12:18:41 +0200 Subject: [PATCH] feat(observabillity): #115 integrate otel for metrics and traces --- go.mod | 29 +++-- go.sum | 71 ++++++---- internal/db/auth.go | 56 ++++---- internal/db/error.go | 6 +- internal/db/migration.go | 10 +- internal/default.go | 55 +++++--- internal/handler/account.go | 8 ++ internal/handler/auth.go | 42 +++++- internal/handler/{error.go => default.go} | 11 ++ internal/handler/middleware/cache_control.go | 5 + .../middleware/cross_site_request_forgery.go | 2 +- internal/handler/middleware/gzip.go | 2 +- internal/handler/middleware/logger.go | 27 ++-- internal/handler/render.go | 5 +- internal/handler/root_and_404.go | 4 + internal/handler/transaction.go | 17 +++ internal/handler/transaction_recurring.go | 6 + internal/handler/treasure_chest.go | 8 ++ internal/log/default.go | 50 +------ internal/otel.go | 123 ++++++++++++++++++ internal/service/account.go | 27 +--- internal/service/auth.go | 10 +- internal/service/mail.go | 4 +- internal/service/random_generator.go | 6 +- internal/service/transaction.go | 28 +--- internal/service/transaction_recurring.go | 59 ++++----- internal/service/treasure_chest.go | 25 +--- internal/types/settings.go | 68 ++++++---- internal/utils/http.go | 2 +- main.go | 16 ++- test/auth_test.go | 10 +- test/it_test.go | 2 +- 32 files changed, 480 insertions(+), 314 deletions(-) rename internal/handler/{error.go => default.go} (73%) create mode 100644 internal/otel.go diff --git a/go.mod b/go.mod index f749d39..1a4af39 100644 --- a/go.mod +++ b/go.mod @@ -11,26 +11,39 @@ require ( github.com/jmoiron/sqlx v1.4.0 github.com/joho/godotenv v1.5.1 github.com/mattn/go-sqlite3 v1.14.28 - github.com/prometheus/client_golang v1.22.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/net v0.41.0 ) require ( - github.com/beorn7/perks v1.0.1 // indirect - github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/cenkalti/backoff/v5 v5.0.2 // 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/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/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 + 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 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 ) diff --git a/go.sum b/go.sum index b7db792..cf78179 100644 --- a/go.sum +++ b/go.sum @@ -2,20 +2,29 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= 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/go.mod h1:oLBbZVQ6//Q6zpvSMPTuBK0F3qOtBdFBcGRspcT+VNQ= -github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= -github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -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/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8= +github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= 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/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/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/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/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 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/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.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= 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/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= 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/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/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/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.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A= 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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= -github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= -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/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/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/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 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/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/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= 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/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= -google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= +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 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/internal/db/auth.go b/internal/db/auth.go index 049003e..c31e9dc 100644 --- a/internal/db/auth.go +++ b/internal/db/auth.go @@ -52,7 +52,7 @@ func (db AuthSqlite) InsertUser(user *types.User) error { return ErrAlreadyExists } - log.Error("SQL error InsertUser: %v", err) + log.L.Error("SQL error InsertUser", "err", err) return types.ErrInternal } @@ -67,7 +67,7 @@ func (db AuthSqlite) UpdateUser(user *types.User) error { user.EmailVerified, user.EmailVerifiedAt, user.Password, user.Id) if err != nil { - log.Error("SQL error UpdateUser: %v", err) + log.L.Error("SQL error UpdateUser", "err", err) return types.ErrInternal } @@ -93,7 +93,7 @@ func (db AuthSqlite) GetUserByEmail(email string) (*types.User, error) { if errors.Is(err, sql.ErrNoRows) { return nil, ErrNotFound } else { - log.Error("SQL error GetUser: %v", err) + log.L.Error("SQL error GetUser", "err", err) return nil, types.ErrInternal } } @@ -120,7 +120,7 @@ func (db AuthSqlite) GetUser(userId uuid.UUID) (*types.User, error) { if errors.Is(err, sql.ErrNoRows) { return nil, ErrNotFound } else { - log.Error("SQL error GetUser %v", err) + log.L.Error("SQL error GetUser", "err", err) 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 { tx, err := db.db.Begin() if err != nil { - log.Error("Could not start transaction: %v", err) + log.L.Error("Could not start transaction", "err", err) return types.ErrInternal } _, err = tx.Exec("DELETE FROM account WHERE user_id = ?", userId) if err != nil { _ = tx.Rollback() - log.Error("Could not delete accounts: %v", err) + log.L.Error("Could not delete accounts", "err", err) return types.ErrInternal } _, err = tx.Exec("DELETE FROM token WHERE user_id = ?", userId) if err != nil { _ = tx.Rollback() - log.Error("Could not delete user tokens: %v", err) + log.L.Error("Could not delete user tokens", "err", err) return types.ErrInternal } _, err = tx.Exec("DELETE FROM session WHERE user_id = ?", userId) if err != nil { _ = tx.Rollback() - log.Error("Could not delete sessions: %v", err) + log.L.Error("Could not delete sessions", "err", err) return types.ErrInternal } _, err = tx.Exec("DELETE FROM user WHERE user_id = ?", userId) if err != nil { _ = tx.Rollback() - log.Error("Could not delete user: %v", err) + log.L.Error("Could not delete user", "err", err) return types.ErrInternal } _, err = tx.Exec("DELETE FROM treasure_chest WHERE user_id = ?", userId) if err != nil { _ = tx.Rollback() - log.Error("Could not delete user: %v", err) + log.L.Error("Could not delete user", "err", err) return types.ErrInternal } _, err = tx.Exec("DELETE FROM \"transaction\" WHERE user_id = ?", userId) if err != nil { _ = tx.Rollback() - log.Error("Could not delete user: %v", err) + log.L.Error("Could not delete user", "err", err) return types.ErrInternal } err = tx.Commit() if err != nil { - log.Error("Could not commit transaction: %v", err) + log.L.Error("Could not commit transaction", "err", err) 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) if err != nil { - log.Error("Could not insert token: %v", err) + log.L.Error("Could not insert token", "err", err) return types.ErrInternal } @@ -217,23 +217,23 @@ func (db AuthSqlite) GetToken(token string) (*types.Token, error) { if err != nil { if errors.Is(err, sql.ErrNoRows) { - log.Info("Token '%v' not found", token) + log.L.Info("Token not found", "token", token) return nil, ErrNotFound } else { - log.Error("Could not get token: %v", err) + log.L.Error("Could not get token", "err", err) return nil, types.ErrInternal } } createdAt, err = time.Parse(time.RFC3339, createdAtStr) 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 } expiresAt, err = time.Parse(time.RFC3339, expiresAtStr) 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 } @@ -248,7 +248,7 @@ func (db AuthSqlite) GetTokensByUserIdAndType(userId uuid.UUID, tokenType types. AND type = ?`, userId, tokenType) if err != nil { - log.Error("Could not get token: %v", err) + log.L.Error("Could not get token", "err", err) return nil, types.ErrInternal } @@ -263,7 +263,7 @@ func (db AuthSqlite) GetTokensBySessionIdAndType(sessionId string, tokenType typ AND type = ?`, sessionId, tokenType) if err != nil { - log.Error("Could not get token: %v", err) + log.L.Error("Could not get token", "err", err) 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) if err != nil { - log.Error("Could not scan token: %v", err) + log.L.Error("Could not scan token", "err", err) return nil, types.ErrInternal } createdAt, err = time.Parse(time.RFC3339, createdAtStr) 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 } expiresAt, err = time.Parse(time.RFC3339, expiresAtStr) 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 } @@ -316,7 +316,7 @@ func getTokensFromQuery(query *sql.Rows, userId uuid.UUID, sessionId string, tok func (db AuthSqlite) DeleteToken(token string) error { _, err := db.db.Exec("DELETE FROM token WHERE token = ?", token) if err != nil { - log.Error("Could not delete token: %v", err) + log.L.Error("Could not delete token", "err", err) return types.ErrInternal } return nil @@ -328,7 +328,7 @@ func (db AuthSqlite) InsertSession(session *types.Session) error { VALUES (?, ?, ?, ?)`, session.Id, session.UserId, session.CreatedAt, session.ExpiresAt) 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 } @@ -348,7 +348,7 @@ func (db AuthSqlite) GetSession(sessionId string) (*types.Session, error) { WHERE session_id = ?`, sessionId).Scan(&userId, &createdAt, &expiresAt) 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 } @@ -362,7 +362,7 @@ func (db AuthSqlite) GetSessions(userId uuid.UUID) ([]*types.Session, error) { FROM session WHERE user_id = ?`, userId) if err != nil { - log.Error("Could not get sessions: %v", err) + log.L.Error("Could not get sessions", "err", err) return nil, types.ErrInternal } @@ -375,7 +375,7 @@ func (db AuthSqlite) DeleteOldSessions(userId uuid.UUID) error { WHERE expires_at < datetime('now') AND user_id = ?`, userId) 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 nil @@ -385,7 +385,7 @@ func (db AuthSqlite) DeleteSession(sessionId string) error { if sessionId != "" { _, err := db.db.Exec("DELETE FROM session WHERE session_id = ?", sessionId) if err != nil { - log.Error("Could not delete session: %v", err) + log.L.Error("Could not delete session", "err", err) return types.ErrInternal } } diff --git a/internal/db/error.go b/internal/db/error.go index 11091ae..ef9c3d0 100644 --- a/internal/db/error.go +++ b/internal/db/error.go @@ -17,19 +17,19 @@ func TransformAndLogDbError(module string, r sql.Result, err error) error { if errors.Is(err, sql.ErrNoRows) { return ErrNotFound } - log.Error("%v: %v", module, err) + log.L.Error("database sql", "module", module, "err", err) return types.ErrInternal } if r != nil { rows, err := r.RowsAffected() if err != nil { - log.Error("%v: %v", module, err) + log.L.Error("database rows affected", "module", module, "err", err) return types.ErrInternal } if rows == 0 { - log.Info("%v: not found", module) + log.L.Info("row not found", "module", module) return ErrNotFound } } diff --git a/internal/db/migration.go b/internal/db/migration.go index 17f75c7..f6e2df8 100644 --- a/internal/db/migration.go +++ b/internal/db/migration.go @@ -14,8 +14,8 @@ import ( type migrationLogger struct{} -func (l migrationLogger) Printf(format string, v ...interface{}) { - log.Info(format, v...) +func (l migrationLogger) Printf(format string, v ...any) { + log.L.Info(format, v...) } func (l migrationLogger) Verbose() bool { return false @@ -24,7 +24,7 @@ func (l migrationLogger) Verbose() bool { func RunMigrations(db *sqlx.DB, pathPrefix string) error { driver, err := sqlite3.WithInstance(db.DB, &sqlite3.Config{}) 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 } @@ -33,14 +33,14 @@ func RunMigrations(db *sqlx.DB, pathPrefix string) error { "", driver) 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 } m.Log = migrationLogger{} 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 } diff --git a/internal/default.go b/internal/default.go index 54203c9..cb91b7a 100644 --- a/internal/default.go +++ b/internal/default.go @@ -19,56 +19,67 @@ import ( "github.com/jmoiron/sqlx" _ "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 { ctx, cancel := signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGTERM) defer cancel() - log.Info("Starting server...") + log.L.Info("Starting server...") // init server settings - serverSettings := types.NewSettingsFromEnv(env) + serverSettings, err := types.NewSettingsFromEnv(env) + if err != nil { + return err + } // init db - err := db.RunMigrations(database, migrationsPrefix) + err = db.RunMigrations(database, migrationsPrefix) if err != nil { return fmt.Errorf("could not run migrations: %w", err) } - // init servers - var prometheusServer *http.Server - if serverSettings.PrometheusEnabled { - prometheusServer := &http.Server{ - Addr: ":8081", - Handler: promhttp.Handler(), - ReadHeaderTimeout: 10 * time.Second, + if serverSettings.OtelEnabled { + // use context.Background(), otherwise the shutdown can't be called, as the context is already cancelled + otelShutdown, err := setupOTelSDK(context.Background()) + if err != nil { + return fmt.Errorf("could not setup OpenTelemetry SDK: %w", err) } - 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{ Addr: ":" + serverSettings.Port, Handler: createHandler(database, serverSettings), - ReadHeaderTimeout: 10 * time.Second, + ReadHeaderTimeout: 2 * time.Second, } go startServer(httpServer) // graceful shutdown var wg sync.WaitGroup - wg.Add(2) + wg.Add(1) go shutdownServer(httpServer, ctx, &wg) - go shutdownServer(prometheusServer, ctx, &wg) wg.Wait() return nil } 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) { - 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) defer cancel() 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 { - 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) router.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./static/")))) - return middleware.Wrapper( + wrapper := middleware.Wrapper( router, middleware.GenerateRecurringTransactions(transactionRecurringService), middleware.SecurityHeaders(serverSettings), @@ -132,4 +143,8 @@ func createHandler(d *sqlx.DB, serverSettings *types.Settings) http.Handler { middleware.Gzip, middleware.Log, ) + + wrapper = otelhttp.NewHandler(wrapper, "http.request") + + return wrapper } diff --git a/internal/handler/account.go b/internal/handler/account.go index 85765c5..3ef2b7f 100644 --- a/internal/handler/account.go +++ b/internal/handler/account.go @@ -36,6 +36,8 @@ func (h AccountImpl) Handle(r *http.ServeMux) { func (h AccountImpl) handleAccountPage() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { + updateSpan(r) + user := middleware.GetUser(r) if user == nil { utils.DoRedirect(w, r, "/auth/signin") @@ -55,6 +57,8 @@ func (h AccountImpl) handleAccountPage() http.HandlerFunc { func (h AccountImpl) handleAccountItemComp() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { + updateSpan(r) + user := middleware.GetUser(r) if user == nil { utils.DoRedirect(w, r, "/auth/signin") @@ -86,6 +90,8 @@ func (h AccountImpl) handleAccountItemComp() http.HandlerFunc { func (h AccountImpl) handleUpdateAccount() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { + updateSpan(r) + user := middleware.GetUser(r) if user == nil { utils.DoRedirect(w, r, "/auth/signin") @@ -119,6 +125,8 @@ func (h AccountImpl) handleUpdateAccount() http.HandlerFunc { func (h AccountImpl) handleDeleteAccount() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { + updateSpan(r) + user := middleware.GetUser(r) if user == nil { utils.DoRedirect(w, r, "/auth/signin") diff --git a/internal/handler/auth.go b/internal/handler/auth.go index 75bcb9c..5dd328b 100644 --- a/internal/handler/auth.go +++ b/internal/handler/auth.go @@ -59,6 +59,8 @@ var ( func (handler AuthImpl) handleSignInPage() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { + updateSpan(r) + user := middleware.GetUser(r) if user != nil { if !user.EmailVerified { @@ -77,6 +79,8 @@ func (handler AuthImpl) handleSignInPage() http.HandlerFunc { func (handler AuthImpl) handleSignIn() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { + updateSpan(r) + user, err := utils.WaitMinimumTime(securityWaitDuration, func() (*types.User, error) { session := middleware.GetSession(r) email := r.FormValue("email") @@ -112,6 +116,8 @@ func (handler AuthImpl) handleSignIn() http.HandlerFunc { func (handler AuthImpl) handleSignUpPage() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { + updateSpan(r) + user := middleware.GetUser(r) if user != nil { @@ -130,6 +136,8 @@ func (handler AuthImpl) handleSignUpPage() http.HandlerFunc { func (handler AuthImpl) handleSignUpVerifyPage() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { + updateSpan(r) + user := middleware.GetUser(r) if user == nil { utils.DoRedirect(w, r, "/auth/signin") @@ -148,6 +156,8 @@ func (handler AuthImpl) handleSignUpVerifyPage() http.HandlerFunc { func (handler AuthImpl) handleVerifyResendComp() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { + updateSpan(r) + user := middleware.GetUser(r) if user == nil { utils.DoRedirect(w, r, "/auth/signin") @@ -158,13 +168,15 @@ func (handler AuthImpl) handleVerifyResendComp() http.HandlerFunc { _, err := w.Write([]byte("

Verification email sent

")) 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 { return func(w http.ResponseWriter, r *http.Request) { + updateSpan(r) + token := r.URL.Query().Get("token") err := handler.service.VerifyUserEmail(token) @@ -185,17 +197,19 @@ func (handler AuthImpl) handleSignUpVerifyResponsePage() http.HandlerFunc { func (handler AuthImpl) handleSignUp() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { + updateSpan(r) + var email = r.FormValue("email") var password = r.FormValue("password") - _, err := utils.WaitMinimumTime(securityWaitDuration, func() (interface{}, error) { - log.Info("Signing up %v", email) + _, err := utils.WaitMinimumTime(securityWaitDuration, func() (any, error) { + log.L.Info("signing up", "email", email) user, err := handler.service.SignUp(email, password) if err != nil { 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) return nil, nil }) @@ -221,6 +235,8 @@ func (handler AuthImpl) handleSignUp() http.HandlerFunc { func (handler AuthImpl) handleSignOut() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { + updateSpan(r) + session := middleware.GetSession(r) if session != nil { @@ -248,6 +264,8 @@ func (handler AuthImpl) handleSignOut() http.HandlerFunc { func (handler AuthImpl) handleDeleteAccountPage() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { + updateSpan(r) + user := middleware.GetUser(r) if user == nil { utils.DoRedirect(w, r, "/auth/signin") @@ -261,6 +279,8 @@ func (handler AuthImpl) handleDeleteAccountPage() http.HandlerFunc { func (handler AuthImpl) handleDeleteAccountComp() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { + updateSpan(r) + user := middleware.GetUser(r) if user == nil { utils.DoRedirect(w, r, "/auth/signin") @@ -285,6 +305,8 @@ func (handler AuthImpl) handleDeleteAccountComp() http.HandlerFunc { func (handler AuthImpl) handleChangePasswordPage() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { + updateSpan(r) + isPasswordReset := r.URL.Query().Has("token") user := middleware.GetUser(r) @@ -301,6 +323,8 @@ func (handler AuthImpl) handleChangePasswordPage() http.HandlerFunc { func (handler AuthImpl) handleChangePasswordComp() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { + updateSpan(r) + session := middleware.GetSession(r) user := middleware.GetUser(r) if session == nil || user == nil { @@ -323,6 +347,8 @@ func (handler AuthImpl) handleChangePasswordComp() http.HandlerFunc { func (handler AuthImpl) handleForgotPasswordPage() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { + updateSpan(r) + user := middleware.GetUser(r) if user != nil { utils.DoRedirect(w, r, "/") @@ -336,13 +362,15 @@ func (handler AuthImpl) handleForgotPasswordPage() http.HandlerFunc { func (handler AuthImpl) handleForgotPasswordComp() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { + updateSpan(r) + email := r.FormValue("email") if email == "" { utils.TriggerToastWithStatus(w, r, "error", "Please enter an email", http.StatusBadRequest) return } - _, err := utils.WaitMinimumTime(securityWaitDuration, func() (interface{}, error) { + _, err := utils.WaitMinimumTime(securityWaitDuration, func() (any, error) { err := handler.service.SendForgotPasswordMail(email) return nil, err }) @@ -357,9 +385,11 @@ func (handler AuthImpl) handleForgotPasswordComp() http.HandlerFunc { func (handler AuthImpl) handleForgotPasswordResponseComp() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { + updateSpan(r) + pageUrl, err := url.Parse(r.Header.Get("Hx-Current-Url")) 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) return } diff --git a/internal/handler/error.go b/internal/handler/default.go similarity index 73% rename from internal/handler/error.go rename to internal/handler/default.go index cbf7a77..4b8bb39 100644 --- a/internal/handler/error.go +++ b/internal/handler/default.go @@ -7,6 +7,9 @@ import ( "spend-sparrow/internal/service" "spend-sparrow/internal/utils" "strings" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" ) 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] } + +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"))) + } +} diff --git a/internal/handler/middleware/cache_control.go b/internal/handler/middleware/cache_control.go index f6bc7a0..eb585e8 100644 --- a/internal/handler/middleware/cache_control.go +++ b/internal/handler/middleware/cache_control.go @@ -3,10 +3,15 @@ package middleware import ( "net/http" "strings" + + "go.opentelemetry.io/otel" ) func CacheControl(next http.Handler) http.Handler { 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") if !shouldCache { diff --git a/internal/handler/middleware/cross_site_request_forgery.go b/internal/handler/middleware/cross_site_request_forgery.go index 307b886..1527b38 100644 --- a/internal/handler/middleware/cross_site_request_forgery.go +++ b/internal/handler/middleware/cross_site_request_forgery.go @@ -46,7 +46,7 @@ func CrossSiteRequestForgery(auth service.Auth) func(http.Handler) http.Handler csrfToken := r.Header.Get("Csrf-Token") 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" { utils.TriggerToastWithStatus(w, r, "error", "CSRF-Token not correct", http.StatusBadRequest) } else { diff --git a/internal/handler/middleware/gzip.go b/internal/handler/middleware/gzip.go index 626bba1..7676640 100644 --- a/internal/handler/middleware/gzip.go +++ b/internal/handler/middleware/gzip.go @@ -34,7 +34,7 @@ func Gzip(next http.Handler) http.Handler { err := gz.Close() 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) } }) } diff --git a/internal/handler/middleware/logger.go b/internal/handler/middleware/logger.go index c374929..ba002cb 100644 --- a/internal/handler/middleware/logger.go +++ b/internal/handler/middleware/logger.go @@ -2,23 +2,8 @@ package middleware import ( "net/http" - "strconv" - "time" - "spend-sparrow/internal/log" - - "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"}, - ) + "time" ) type WrappedWriter struct { @@ -35,13 +20,19 @@ func Log(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { start := time.Now() + log.L.Info("request pattern", "pattern", r.Pattern) + wrapped := &WrappedWriter{ ResponseWriter: w, StatusCode: http.StatusOK, } next.ServeHTTP(wrapped, r) - log.Info(r.RemoteAddr + " " + strconv.Itoa(wrapped.StatusCode) + " " + r.Method + " " + r.URL.Path + " " + time.Since(start).String()) - metrics.WithLabelValues(r.URL.Path, r.Method, http.StatusText(wrapped.StatusCode)).Inc() + log.L.Info("request", + "remoteAddr", r.RemoteAddr, + "status", wrapped.StatusCode, + "method", r.Method, + "path", r.URL.Path, + "duration", time.Since(start).String()) }) } diff --git a/internal/handler/render.go b/internal/handler/render.go index fd2ac0e..f3392f4 100644 --- a/internal/handler/render.go +++ b/internal/handler/render.go @@ -1,13 +1,12 @@ package handler import ( + "net/http" "spend-sparrow/internal/log" "spend-sparrow/internal/template" "spend-sparrow/internal/template/auth" "spend-sparrow/internal/types" - "net/http" - "github.com/a-h/templ" ) @@ -23,7 +22,7 @@ func (render *Render) RenderWithStatus(r *http.Request, w http.ResponseWriter, c w.WriteHeader(status) err := comp.Render(r.Context(), w) 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) } } diff --git a/internal/handler/root_and_404.go b/internal/handler/root_and_404.go index eab58e6..d346101 100644 --- a/internal/handler/root_and_404.go +++ b/internal/handler/root_and_404.go @@ -29,6 +29,8 @@ func (handler IndexImpl) Handle(router *http.ServeMux) { func (handler IndexImpl) handleRootAnd404() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { + updateSpan(r) + user := middleware.GetUser(r) var comp templ.Component @@ -52,6 +54,8 @@ func (handler IndexImpl) handleRootAnd404() http.HandlerFunc { func (handler IndexImpl) handleEmpty() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { + updateSpan(r) + // Return nothing } } diff --git a/internal/handler/transaction.go b/internal/handler/transaction.go index d335bbf..5dfeff3 100644 --- a/internal/handler/transaction.go +++ b/internal/handler/transaction.go @@ -13,6 +13,8 @@ import ( "github.com/a-h/templ" "github.com/google/uuid" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" ) type Transaction interface { @@ -45,12 +47,17 @@ func (h TransactionImpl) Handle(r *http.ServeMux) { func (h TransactionImpl) handleTransactionPage() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { + updateSpan(r) + user := middleware.GetUser(r) if user == nil { utils.DoRedirect(w, r, "/auth/signin") return } + currentSpan := trace.SpanFromContext(r.Context()) + currentSpan.SetAttributes(attribute.String("", "test")) + filter := types.TransactionItemsFilter{ AccountId: r.URL.Query().Get("account-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 { return func(w http.ResponseWriter, r *http.Request) { + updateSpan(r) + user := middleware.GetUser(r) if user == nil { utils.DoRedirect(w, r, "/auth/signin") 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) if err != nil { handleError(w, r, err) @@ -133,6 +144,8 @@ func (h TransactionImpl) handleTransactionItemComp() http.HandlerFunc { func (h TransactionImpl) handleUpdateTransaction() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { + updateSpan(r) + user := middleware.GetUser(r) if user == nil { utils.DoRedirect(w, r, "/auth/signin") @@ -233,6 +246,8 @@ func (h TransactionImpl) handleUpdateTransaction() http.HandlerFunc { func (h TransactionImpl) handleRecalculate() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { + updateSpan(r) + user := middleware.GetUser(r) if user == nil { utils.DoRedirect(w, r, "/auth/signin") @@ -251,6 +266,8 @@ func (h TransactionImpl) handleRecalculate() http.HandlerFunc { func (h TransactionImpl) handleDeleteTransaction() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { + updateSpan(r) + user := middleware.GetUser(r) if user == nil { utils.DoRedirect(w, r, "/auth/signin") diff --git a/internal/handler/transaction_recurring.go b/internal/handler/transaction_recurring.go index 4f8392e..6ca659f 100644 --- a/internal/handler/transaction_recurring.go +++ b/internal/handler/transaction_recurring.go @@ -33,6 +33,8 @@ func (h TransactionRecurringImpl) Handle(r *http.ServeMux) { func (h TransactionRecurringImpl) handleTransactionRecurringItemComp() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { + updateSpan(r) + user := middleware.GetUser(r) if user == nil { utils.DoRedirect(w, r, "/auth/signin") @@ -48,6 +50,8 @@ func (h TransactionRecurringImpl) handleTransactionRecurringItemComp() http.Hand func (h TransactionRecurringImpl) handleUpdateTransactionRecurring() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { + updateSpan(r) + user := middleware.GetUser(r) if user == nil { utils.DoRedirect(w, r, "/auth/signin") @@ -85,6 +89,8 @@ func (h TransactionRecurringImpl) handleUpdateTransactionRecurring() http.Handle func (h TransactionRecurringImpl) handleDeleteTransactionRecurring() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { + updateSpan(r) + user := middleware.GetUser(r) if user == nil { utils.DoRedirect(w, r, "/auth/signin") diff --git a/internal/handler/treasure_chest.go b/internal/handler/treasure_chest.go index 438fd06..9423eff 100644 --- a/internal/handler/treasure_chest.go +++ b/internal/handler/treasure_chest.go @@ -40,6 +40,8 @@ func (h TreasureChestImpl) Handle(r *http.ServeMux) { func (h TreasureChestImpl) handleTreasureChestPage() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { + updateSpan(r) + user := middleware.GetUser(r) if user == nil { utils.DoRedirect(w, r, "/auth/signin") @@ -67,6 +69,8 @@ func (h TreasureChestImpl) handleTreasureChestPage() http.HandlerFunc { func (h TreasureChestImpl) handleTreasureChestItemComp() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { + updateSpan(r) + user := middleware.GetUser(r) if user == nil { utils.DoRedirect(w, r, "/auth/signin") @@ -112,6 +116,8 @@ func (h TreasureChestImpl) handleTreasureChestItemComp() http.HandlerFunc { func (h TreasureChestImpl) handleUpdateTreasureChest() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { + updateSpan(r) + user := middleware.GetUser(r) if user == nil { utils.DoRedirect(w, r, "/auth/signin") @@ -155,6 +161,8 @@ func (h TreasureChestImpl) handleUpdateTreasureChest() http.HandlerFunc { func (h TreasureChestImpl) handleDeleteTreasureChest() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { + updateSpan(r) + user := middleware.GetUser(r) if user == nil { utils.DoRedirect(w, r, "/auth/signin") diff --git a/internal/log/default.go b/internal/log/default.go index 7ab6213..725f507 100644 --- a/internal/log/default.go +++ b/internal/log/default.go @@ -1,56 +1,14 @@ package log import ( - "fmt" - "log" "log/slog" - "strings" - - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/promauto" + // "go.opentelemetry.io/contrib/bridges/otelslog". ) var ( - errorMetric = promauto.NewCounter( - prometheus.CounterOpts{ - Name: "spendsparrow_error_total", - Help: "The total number of errors during processing", - }, - ) + L = slog.New(slog.Default().Handler()) ) -func Fatal(message string, args ...interface{}) { - errorMetric.Inc() - - 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() +func InitOtelLogger() { + // L = otelslog.NewLogger("spend-sparrow") } diff --git a/internal/otel.go b/internal/otel.go new file mode 100644 index 0000000..2ab8815 --- /dev/null +++ b/internal/otel.go @@ -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 +// } diff --git a/internal/service/account.go b/internal/service/account.go index 0749fc3..bced1db 100644 --- a/internal/service/account.go +++ b/internal/service/account.go @@ -9,18 +9,6 @@ import ( "github.com/google/uuid" "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 { @@ -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) { - accountMetric.WithLabelValues("add").Inc() - if user == nil { 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) { - accountMetric.WithLabelValues("update").Inc() if user == nil { return nil, ErrUnauthorized } @@ -100,7 +85,7 @@ func (s AccountImpl) UpdateName(user *types.User, id string, name string) (*type } uuid, err := uuid.Parse(id) 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) } @@ -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) { - accountMetric.WithLabelValues("get").Inc() - if user == nil { return nil, ErrUnauthorized } uuid, err := uuid.Parse(id) 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) } @@ -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) err = db.TransformAndLogDbError("account Get", nil, err) if err != nil { - log.Error("account get: %v", err) + log.L.Error("account get", "err", 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) { - accountMetric.WithLabelValues("get_all").Inc() if user == nil { 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 { - accountMetric.WithLabelValues("delete").Inc() if user == nil { return ErrUnauthorized } uuid, err := uuid.Parse(id) 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) } diff --git a/internal/service/auth.go b/internal/service/auth.go index f9d80c4..0e347c7 100644 --- a/internal/service/auth.go +++ b/internal/service/auth.go @@ -125,7 +125,7 @@ func (service AuthImpl) SignInAnonymous() (*types.Session, error) { 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 } @@ -201,7 +201,7 @@ func (service AuthImpl) SendVerificationMail(userId uuid.UUID, email string) { var w strings.Builder err = mailTemplate.Register(service.serverSettings.BaseUrl, token.Token).Render(context.Background(), &w) if err != nil { - log.Error("Could not render welcome email: %v", err) + log.L.Error("Could not render welcome email", "err", err) return } @@ -340,7 +340,7 @@ func (service AuthImpl) SendForgotPasswordMail(email string) error { var mail strings.Builder err = mailTemplate.ResetPassword(service.serverSettings.BaseUrl, token.Token).Render(context.Background(), &mail) 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 } 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) 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 } @@ -440,7 +440,7 @@ func (service AuthImpl) GetCsrfToken(session *types.Session) (string, error) { return "", types.ErrInternal } - log.Info("CSRF-Token created: %v", tokenStr) + log.L.Info("CSRF-Token created", "token", tokenStr) return tokenStr, nil } diff --git a/internal/service/mail.go b/internal/service/mail.go index 7bca066..1801dc2 100644 --- a/internal/service/mail.go +++ b/internal/service/mail.go @@ -47,9 +47,9 @@ func (m MailImpl) internalSendMail(to string, subject string, message string) { subject, 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)) if err != nil { - log.Error("Error sending mail: %v", err) + log.L.Error("Error sending mail", "err", err) } } diff --git a/internal/service/random_generator.go b/internal/service/random_generator.go index bce038a..33dd90f 100644 --- a/internal/service/random_generator.go +++ b/internal/service/random_generator.go @@ -26,7 +26,7 @@ func (r *RandomImpl) Bytes(size int) ([]byte, error) { b := make([]byte, 32) _, err := rand.Read(b) if err != nil { - log.Error("Error generating random bytes: %v", err) + log.L.Error("Error generating random bytes", "err", err) return []byte{}, types.ErrInternal } @@ -36,7 +36,7 @@ func (r *RandomImpl) Bytes(size int) ([]byte, error) { func (r *RandomImpl) String(size int) (string, error) { bytes, err := r.Bytes(size) if err != nil { - log.Error("Error generating random string: %v", err) + log.L.Error("Error generating random string", "err", err) return "", types.ErrInternal } @@ -46,7 +46,7 @@ func (r *RandomImpl) String(size int) (string, error) { func (r *RandomImpl) UUID() (uuid.UUID, error) { id, err := uuid.NewRandom() 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 } diff --git a/internal/service/transaction.go b/internal/service/transaction.go index cf63405..01dc2ee 100644 --- a/internal/service/transaction.go +++ b/internal/service/transaction.go @@ -10,18 +10,6 @@ import ( "github.com/google/uuid" "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 { @@ -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) { - transactionMetric.WithLabelValues("add").Inc() - if user == nil { 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) { - transactionMetric.WithLabelValues("update").Inc() if user == nil { 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) { - transactionMetric.WithLabelValues("get").Inc() - if user == nil { return nil, ErrUnauthorized } uuid, err := uuid.Parse(id) 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) } @@ -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) { - transactionMetric.WithLabelValues("get_all").Inc() if user == nil { 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 { - transactionMetric.WithLabelValues("delete").Inc() if user == nil { return ErrUnauthorized } uuid, err := uuid.Parse(id) 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) } @@ -339,7 +320,6 @@ func (s TransactionImpl) Delete(user *types.User, id string) error { } func (s TransactionImpl) RecalculateBalances(user *types.User) error { - transactionMetric.WithLabelValues("recalculate").Inc() if user == nil { return ErrUnauthorized } @@ -382,7 +362,7 @@ func (s TransactionImpl) RecalculateBalances(user *types.User) error { defer func() { err := rows.Close() 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 } 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) } } diff --git a/internal/service/transaction_recurring.go b/internal/service/transaction_recurring.go index f010e4d..30d0f6b 100644 --- a/internal/service/transaction_recurring.go +++ b/internal/service/transaction_recurring.go @@ -11,18 +11,6 @@ import ( "github.com/google/uuid" "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 { @@ -54,9 +42,8 @@ func NewTransactionRecurring(db *sqlx.DB, random Random, clock Clock, transactio func (s TransactionRecurringImpl) Add( user *types.User, - transactionRecurringInput types.TransactionRecurringInput) (*types.TransactionRecurring, error) { - transactionRecurringMetric.WithLabelValues("add").Inc() - + transactionRecurringInput types.TransactionRecurringInput, +) (*types.TransactionRecurring, error) { if user == nil { return nil, ErrUnauthorized } @@ -97,14 +84,14 @@ func (s TransactionRecurringImpl) Add( func (s TransactionRecurringImpl) Update( user *types.User, - input types.TransactionRecurringInput) (*types.TransactionRecurring, error) { - transactionRecurringMetric.WithLabelValues("update").Inc() + input types.TransactionRecurringInput, +) (*types.TransactionRecurring, error) { if user == nil { return nil, ErrUnauthorized } uuid, err := uuid.Parse(input.Id) 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) } @@ -161,7 +148,6 @@ func (s TransactionRecurringImpl) Update( } func (s TransactionRecurringImpl) GetAll(user *types.User) ([]*types.TransactionRecurring, error) { - transactionRecurringMetric.WithLabelValues("get_all_by_account").Inc() if user == nil { 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) { - transactionRecurringMetric.WithLabelValues("get_all_by_account").Inc() if user == nil { return nil, ErrUnauthorized } accountUuid, err := uuid.Parse(accountId) 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) } @@ -234,15 +219,17 @@ func (s TransactionRecurringImpl) GetAllByAccount(user *types.User, accountId st return transactionRecurrings, nil } -func (s TransactionRecurringImpl) GetAllByTreasureChest(user *types.User, treasureChestId string) ([]*types.TransactionRecurring, error) { - transactionRecurringMetric.WithLabelValues("get_all_by_treasurechest").Inc() +func (s TransactionRecurringImpl) GetAllByTreasureChest( + user *types.User, + treasureChestId string, +) ([]*types.TransactionRecurring, error) { if user == nil { return nil, ErrUnauthorized } treasureChestUuid, err := uuid.Parse(treasureChestId) 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) } @@ -288,13 +275,12 @@ func (s TransactionRecurringImpl) GetAllByTreasureChest(user *types.User, treasu } func (s TransactionRecurringImpl) Delete(user *types.User, id string) error { - transactionRecurringMetric.WithLabelValues("delete").Inc() if user == nil { return ErrUnauthorized } uuid, err := uuid.Parse(id) 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) } @@ -389,7 +375,8 @@ func (s TransactionRecurringImpl) validateAndEnrichTransactionRecurring( tx *sqlx.Tx, oldTransactionRecurring *types.TransactionRecurring, userId uuid.UUID, - input types.TransactionRecurringInput) (*types.TransactionRecurring, error) { + input types.TransactionRecurringInput, +) (*types.TransactionRecurring, error) { var ( id uuid.UUID accountUuid *uuid.UUID @@ -425,7 +412,7 @@ func (s TransactionRecurringImpl) validateAndEnrichTransactionRecurring( if input.AccountId != "" { temp, err := uuid.Parse(input.AccountId) 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) } accountUuid = &temp @@ -435,7 +422,7 @@ func (s TransactionRecurringImpl) validateAndEnrichTransactionRecurring( return nil, err } 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) } @@ -445,7 +432,7 @@ func (s TransactionRecurringImpl) validateAndEnrichTransactionRecurring( if input.TreasureChestId != "" { temp, err := uuid.Parse(input.TreasureChestId) 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) } treasureChestUuid = &temp @@ -465,17 +452,17 @@ func (s TransactionRecurringImpl) validateAndEnrichTransactionRecurring( } 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) } 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) } valueFloat, err := strconv.ParseFloat(input.Value, 64) 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) } valueInt := int64(valueFloat * DECIMALS_MULTIPLIER) @@ -494,18 +481,18 @@ func (s TransactionRecurringImpl) validateAndEnrichTransactionRecurring( } intervalMonths, err = strconv.ParseInt(input.IntervalMonths, 10, 0) 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) } 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) } var nextExecution *time.Time = nil if input.NextExecution != "" { t, err := time.Parse("2006-01-02", input.NextExecution) 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) } diff --git a/internal/service/treasure_chest.go b/internal/service/treasure_chest.go index fb32256..c606e14 100644 --- a/internal/service/treasure_chest.go +++ b/internal/service/treasure_chest.go @@ -10,18 +10,6 @@ import ( "github.com/google/uuid" "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 { @@ -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) { - treasureChestMetric.WithLabelValues("add").Inc() - if user == nil { 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) { - treasureChestMetric.WithLabelValues("update").Inc() if user == nil { return nil, ErrUnauthorized } @@ -112,7 +97,7 @@ func (s TreasureChestImpl) Update(user *types.User, idStr, parentId, name string } id, err := uuid.Parse(idStr) 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) } @@ -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) { - treasureChestMetric.WithLabelValues("get").Inc() - if user == nil { return nil, ErrUnauthorized } uuid, err := uuid.Parse(id) 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) } @@ -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) { - treasureChestMetric.WithLabelValues("get_all").Inc() if user == nil { 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 { - treasureChestMetric.WithLabelValues("delete").Inc() if user == nil { return ErrUnauthorized } id, err := uuid.Parse(idStr) 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) } diff --git a/internal/types/settings.go b/internal/types/settings.go index 3083652..cb291b1 100644 --- a/internal/types/settings.go +++ b/internal/types/settings.go @@ -1,12 +1,17 @@ package types import ( + "errors" "spend-sparrow/internal/log" ) +var ( + ErrMissingConfig = errors.New("missing config") +) + type Settings struct { - Port string - PrometheusEnabled bool + Port string + OtelEnabled bool BaseUrl string Environment string @@ -22,37 +27,46 @@ type SmtpSettings struct { FromName string } -func NewSettingsFromEnv(env func(string) string) *Settings { - var smtp *SmtpSettings +func NewSettingsFromEnv(env func(string) string) (*Settings, error) { + var ( + smtp *SmtpSettings + err error + ) if env("SMTP_ENABLED") == "true" { - smtp = getSmtpSettings(env) + smtp, err = getSmtpSettings(env) + if err != nil { + return nil, err + } } settings := &Settings{ - Port: env("PORT"), - PrometheusEnabled: env("PROMETHEUS_ENABLED") == "true", - BaseUrl: env("BASE_URL"), - Environment: env("ENVIRONMENT"), - Smtp: smtp, + Port: env("PORT"), + OtelEnabled: env("OTEL_ENABLED") == "true", + BaseUrl: env("BASE_URL"), + Environment: env("ENVIRONMENT"), + Smtp: smtp, } if settings.BaseUrl == "" { - log.Fatal("BASE_URL must be set") + log.L.Error("BASE_URL must be set") + return nil, ErrMissingConfig } if settings.Port == "" { - log.Fatal("PORT must be set") + log.L.Error("PORT must be set") + return nil, ErrMissingConfig } 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.Info("ENVIRONMENT is %q", settings.Environment) + log.L.Info("settings read", "BASE_URL", settings.BaseUrl) + 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{ Host: env("SMTP_HOST"), Port: env("SMTP_PORT"), @@ -63,23 +77,29 @@ func getSmtpSettings(env func(string) string) *SmtpSettings { } if smtp.Host == "" { - log.Fatal("SMTP_HOST must be set") + log.L.Error("SMTP_HOST must be set") + return nil, ErrMissingConfig } if smtp.Port == "" { - log.Fatal("SMTP_PORT must be set") + log.L.Error("SMTP_PORT must be set") + return nil, ErrMissingConfig } if smtp.User == "" { - log.Fatal("SMTP_USER must be set") + log.L.Error("SMTP_USER must be set") + return nil, ErrMissingConfig } if smtp.Pass == "" { - log.Fatal("SMTP_PASS must be set") + log.L.Error("SMTP_PASS must be set") + return nil, ErrMissingConfig } 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 == "" { - 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 } diff --git a/internal/utils/http.go b/internal/utils/http.go index 233edee..d741c87 100644 --- a/internal/utils/http.go +++ b/internal/utils/http.go @@ -13,7 +13,7 @@ func TriggerToast(w http.ResponseWriter, r *http.Request, class string, message if IsHtmx(r) { w.Header().Set("Hx-Trigger", fmt.Sprintf(`{"toast": "%v|%v"}`, class, strings.ReplaceAll(message, `"`, `\"`))) } else { - log.Error("Trying to trigger toast in non-HTMX request") + log.L.Error("Trying to trigger toast in non-HTMX request") } } diff --git a/main.go b/main.go index 1654383..c42b9f5 100644 --- a/main.go +++ b/main.go @@ -14,21 +14,23 @@ import ( func main() { err := godotenv.Load() 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") 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() { - err := db.Close() - log.Fatal("Could not close Database data.db: %v", err) + if err = db.Close(); err != nil { + log.L.Error("Database close failed", "err", err) + } }() - err = internal.Run(context.Background(), db, "", os.Getenv) - if err != nil { - log.Error("Error running server: %v", err) + if err = internal.Run(context.Background(), db, "", os.Getenv); err != nil { + log.L.Error("Error running server", "err", err) return } } diff --git a/test/auth_test.go b/test/auth_test.go index eaee619..903e517 100644 --- a/test/auth_test.go +++ b/test/auth_test.go @@ -17,11 +17,11 @@ import ( var ( settings = types.Settings{ - Port: "", - PrometheusEnabled: false, - BaseUrl: "", - Environment: "test", - Smtp: nil, + Port: "", + OtelEnabled: false, + BaseUrl: "", + Environment: "test", + Smtp: nil, } ) diff --git a/test/it_test.go b/test/it_test.go index 19ffb69..db1ed22 100644 --- a/test/it_test.go +++ b/test/it_test.go @@ -72,7 +72,7 @@ func getEnv(port int64) func(string) string { return strconv.Itoa(int(port)) case "SMTP_ENABLED": return "false" - case "PROMETHEUS_ENABLED": + case "OLTP_ENABLED": return "false" case "BASE_URL": return "http://localhost:" + strconv.Itoa(int(port))