diff --git a/go.mod b/go.mod index 9041914..a4ce770 100644 --- a/go.mod +++ b/go.mod @@ -5,12 +5,15 @@ go 1.23.2 require ( github.com/alitto/pond v1.9.2 github.com/aws/aws-sdk-go v1.55.5 + github.com/dlmiddlecote/sqlstats v1.0.2 github.com/getsentry/sentry-go v0.29.1 github.com/go-chi/chi v4.1.2+incompatible github.com/go-playground/validator/v10 v10.22.1 github.com/google/uuid v1.6.0 github.com/jmoiron/sqlx v1.4.0 github.com/lib/pq v1.10.9 + github.com/mattn/go-sqlite3 v1.14.22 + github.com/prometheus/client_golang v1.17.0 github.com/rubenv/sql-migrate v1.7.0 github.com/sirupsen/logrus v1.9.3 github.com/spf13/cobra v1.8.1 @@ -49,7 +52,6 @@ require ( github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/prometheus/client_golang v1.17.0 // indirect github.com/prometheus/client_model v0.5.0 // indirect github.com/prometheus/common v0.44.0 // indirect github.com/prometheus/procfs v0.12.0 // indirect diff --git a/go.sum b/go.sum index 9c25509..268699b 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,10 @@ github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8 github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alitto/pond v1.9.2 h1:9Qb75z/scEZVCoSU+osVmQ0I0JOeLfdTDafrbcJ8CLs= github.com/alitto/pond v1.9.2/go.mod h1:xQn3P/sHTYcU/1BR3i86IGIrilcrGC2LiS+E2+CJWsI= github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= @@ -14,8 +18,11 @@ github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3d github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/aws/aws-sdk-go v1.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU= github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 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.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= @@ -23,6 +30,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dlmiddlecote/sqlstats v1.0.2 h1:gSU11YN23D/iY50A2zVYwgXgy072khatTsIW6UPjUtI= +github.com/dlmiddlecote/sqlstats v1.0.2/go.mod h1:0CWaIh/Th+z2aI6Q9Jpfg/o21zmGxWhbByHgQSCUQvY= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= @@ -41,6 +50,10 @@ github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8b github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs= github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= @@ -51,13 +64,19 @@ github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27 github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= 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-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 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.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 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/gorilla/schema v1.4.1 h1:jUg5hUjCSDZpNGLuXQOgIWGdlgrIdYvgQ0wZtdK1M3E= @@ -78,8 +97,13 @@ github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGw github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg= github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 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= @@ -96,14 +120,21 @@ github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0V github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/manucorporat/sse v0.0.0-20160126180136-ee05b128a739 h1:ykXz+pRRTibcSjG1yRhpdSHInF8yZY/mfn+Rz2Nd1rE= github.com/manucorporat/sse v0.0.0-20160126180136-ee05b128a739/go.mod h1:zUx1mhth20V3VKgL5jbd1BSQcW4Fy6Qs4PZvQwRFwzM= +github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/moul/http2curl v0.0.0-20161031194548-4e24498b31db h1:eZgFHVkk9uOTaOQLC6tgjkzdp7Ays8eEVecBcfHZlJQ= github.com/moul/http2curl v0.0.0-20161031194548-4e24498b31db/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY= github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= @@ -114,6 +145,8 @@ github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6 github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -121,12 +154,23 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/poy/onpar v1.1.2 h1:QaNrNiZx0+Nar5dLgTVp5mXkyoVFIbepjyEoGSnhbAY= github.com/poy/onpar v1.1.2/go.mod h1:6X8FLNoxyr9kkmnlqpK6LSoiOtrO6MICtWwEuWkLjzg= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og= github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q= github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY= github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= @@ -144,6 +188,8 @@ github.com/segmentio/go-loggly v0.5.1-0.20171222203950-eb91657e62b2 h1:S4OC0+OBK github.com/segmentio/go-loggly v0.5.1-0.20171222203950-eb91657e62b2/go.mod h1:8zLRYR5npGjaOXgPSKat5+oOh+UHd8OdbS18iqX9F6Y= github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= @@ -163,11 +209,13 @@ github.com/stellar/go v0.0.0-20241113194904-713725358e05/go.mod h1:rrFK7a8i2h9xa github.com/stellar/go-xdr v0.0.0-20231122183749-b53fb00bcac2 h1:OzCVd0SV5qE3ZcDeSFCmOWLZfEWZ3Oe8KtmSOYKEVWE= github.com/stellar/go-xdr v0.0.0-20231122183749-b53fb00bcac2/go.mod h1:yoxyU/M8nl9LKeWIoBrbDPQ7Cy+4jxRcWcOayZ4BMps= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 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.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= @@ -196,22 +244,35 @@ github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3Ifn github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 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= @@ -223,6 +284,8 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkep gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/tylerb/graceful.v1 v1.2.15 h1:1JmOyhKqAyX3BgTXMI84LwT6FOJ4tP2N9e2kwTCM0nQ= gopkg.in/tylerb/graceful.v1 v1.2.15/go.mod h1:yBhekWvR20ACXVObSSdD3u6S9DeSylanL2PAbAC/uJ8= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/internal/data/accounts.go b/internal/data/accounts.go index 3fd8d1b..115278d 100644 --- a/internal/data/accounts.go +++ b/internal/data/accounts.go @@ -3,31 +3,41 @@ package data import ( "context" "fmt" + "time" "github.com/stellar/wallet-backend/internal/db" + "github.com/stellar/wallet-backend/internal/metrics" ) type AccountModel struct { - DB db.ConnectionPool + DB db.ConnectionPool + MetricsService *metrics.MetricsService } func (m *AccountModel) Insert(ctx context.Context, address string) error { const query = `INSERT INTO accounts (stellar_address) VALUES ($1) ON CONFLICT DO NOTHING` + start := time.Now() _, err := m.DB.ExecContext(ctx, query, address) + duration := time.Since(start).Seconds() + m.MetricsService.ObserveDBQueryDuration("INSERT", "accounts", duration) if err != nil { return fmt.Errorf("inserting address %s: %w", address, err) } + m.MetricsService.IncDBQuery("INSERT", "accounts") return nil } func (m *AccountModel) Delete(ctx context.Context, address string) error { const query = `DELETE FROM accounts WHERE stellar_address = $1` + start := time.Now() _, err := m.DB.ExecContext(ctx, query, address) + duration := time.Since(start).Seconds() + m.MetricsService.ObserveDBQueryDuration("DELETE", "accounts", duration) if err != nil { return fmt.Errorf("deleting address %s: %w", address, err) } - + m.MetricsService.IncDBQuery("DELETE", "accounts") return nil } @@ -43,10 +53,13 @@ func (m *AccountModel) IsAccountFeeBumpEligible(ctx context.Context, address str ) ` var exists bool + start := time.Now() err := m.DB.GetContext(ctx, &exists, query, address) + duration := time.Since(start).Seconds() + m.MetricsService.ObserveDBQueryDuration("SELECT", "accounts", duration) if err != nil { return false, fmt.Errorf("checking if account %s is fee bump eligible: %w", address, err) } - + m.MetricsService.IncDBQuery("SELECT", "accounts") return exists, nil } diff --git a/internal/data/accounts_test.go b/internal/data/accounts_test.go index 18b5e88..dc79638 100644 --- a/internal/data/accounts_test.go +++ b/internal/data/accounts_test.go @@ -8,6 +8,7 @@ import ( "github.com/stellar/go/keypair" "github.com/stellar/wallet-backend/internal/db" "github.com/stellar/wallet-backend/internal/db/dbtest" + "github.com/stellar/wallet-backend/internal/metrics" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -19,9 +20,13 @@ func TestAccountModelInsert(t *testing.T) { dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) require.NoError(t, err) defer dbConnectionPool.Close() + sqlxDB, err := dbConnectionPool.SqlxDB(context.Background()) + require.NoError(t, err) + metricsService := metrics.NewMetricsService(sqlxDB) m := &AccountModel{ - DB: dbConnectionPool, + DB: dbConnectionPool, + MetricsService: metricsService, } ctx := context.Background() @@ -44,9 +49,13 @@ func TestAccountModelDelete(t *testing.T) { dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) require.NoError(t, err) defer dbConnectionPool.Close() + sqlxDB, err := dbConnectionPool.SqlxDB(context.Background()) + require.NoError(t, err) + metricsService := metrics.NewMetricsService(sqlxDB) m := &AccountModel{ - DB: dbConnectionPool, + DB: dbConnectionPool, + MetricsService: metricsService, } ctx := context.Background() @@ -72,9 +81,13 @@ func TestAccountModelIsAccountFeeBumpEligible(t *testing.T) { dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) require.NoError(t, err) defer dbConnectionPool.Close() + sqlxDB, err := dbConnectionPool.SqlxDB(context.Background()) + require.NoError(t, err) + metricsService := metrics.NewMetricsService(sqlxDB) m := &AccountModel{ - DB: dbConnectionPool, + DB: dbConnectionPool, + MetricsService: metricsService, } ctx := context.Background() diff --git a/internal/data/models.go b/internal/data/models.go index 06a779e..cc46c76 100644 --- a/internal/data/models.go +++ b/internal/data/models.go @@ -4,6 +4,7 @@ import ( "errors" "github.com/stellar/wallet-backend/internal/db" + "github.com/stellar/wallet-backend/internal/metrics" ) type Models struct { @@ -11,13 +12,13 @@ type Models struct { Account *AccountModel } -func NewModels(db db.ConnectionPool) (*Models, error) { +func NewModels(db db.ConnectionPool, metricsService *metrics.MetricsService) (*Models, error) { if db == nil { return nil, errors.New("ConnectionPool must be initialized") } return &Models{ - Payments: &PaymentModel{DB: db}, - Account: &AccountModel{DB: db}, + Payments: &PaymentModel{DB: db, MetricsService: metricsService}, + Account: &AccountModel{DB: db, MetricsService: metricsService}, }, nil } diff --git a/internal/data/payments.go b/internal/data/payments.go index ded436d..703935a 100644 --- a/internal/data/payments.go +++ b/internal/data/payments.go @@ -8,10 +8,12 @@ import ( "time" "github.com/stellar/wallet-backend/internal/db" + "github.com/stellar/wallet-backend/internal/metrics" ) type PaymentModel struct { - DB db.ConnectionPool + DB db.ConnectionPool + MetricsService *metrics.MetricsService } type Payment struct { @@ -36,7 +38,10 @@ type Payment struct { func (m *PaymentModel) GetLatestLedgerSynced(ctx context.Context, cursorName string) (uint32, error) { var lastSyncedLedger uint32 + start := time.Now() err := m.DB.GetContext(ctx, &lastSyncedLedger, `SELECT value FROM ingest_store WHERE key = $1`, cursorName) + duration := time.Since(start).Seconds() + m.MetricsService.ObserveDBQueryDuration("SELECT", "ingest_store", duration) // First run, key does not exist yet if err == sql.ErrNoRows { return 0, nil @@ -53,10 +58,14 @@ func (m *PaymentModel) UpdateLatestLedgerSynced(ctx context.Context, cursorName INSERT INTO ingest_store (key, value) VALUES ($1, $2) ON CONFLICT (key) DO UPDATE SET value = excluded.value ` + start := time.Now() _, err := m.DB.ExecContext(ctx, query, cursorName, ledger) + duration := time.Since(start).Seconds() + m.MetricsService.ObserveDBQueryDuration("INSERT", "ingest_store", duration) if err != nil { return fmt.Errorf("updating last synced ledger to %d: %w", ledger, err) } + m.MetricsService.IncDBQuery("INSERT", "ingest_store") return nil } @@ -90,12 +99,15 @@ func (m *PaymentModel) AddPayment(ctx context.Context, tx db.Transaction, paymen memo_type = EXCLUDED.memo_type ; ` + start := time.Now() _, err := tx.ExecContext(ctx, query, payment.OperationID, payment.OperationType, payment.TransactionID, payment.TransactionHash, payment.FromAddress, payment.ToAddress, payment.SrcAssetCode, payment.SrcAssetIssuer, payment.SrcAssetType, payment.SrcAmount, payment.DestAssetCode, payment.DestAssetIssuer, payment.DestAssetType, payment.DestAmount, payment.CreatedAt, payment.Memo, payment.MemoType) + duration := time.Since(start).Seconds() + m.MetricsService.ObserveDBQueryDuration("INSERT", "ingest_payments", duration) if err != nil { return fmt.Errorf("inserting payment: %w", err) } - + m.MetricsService.IncDBQuery("INSERT", "ingest_payments") return nil } @@ -142,7 +154,10 @@ func (m *PaymentModel) GetPaymentsPaginated(ctx context.Context, address string, if err != nil { return nil, false, false, fmt.Errorf("preparing named query: %w", err) } + start := time.Now() err = m.DB.SelectContext(ctx, &payments, query, args...) + duration := time.Since(start).Seconds() + m.MetricsService.ObserveDBQueryDuration("SELECT", "ingest_payments", duration) if err != nil { return nil, false, false, fmt.Errorf("fetching payments: %w", err) } @@ -151,7 +166,7 @@ func (m *PaymentModel) GetPaymentsPaginated(ctx context.Context, address string, if err != nil { return nil, false, false, fmt.Errorf("checking prev and next pages: %w", err) } - + m.MetricsService.IncDBQuery("SELECT", "ingest_payments") return payments, prevExists, nextExists, nil } @@ -187,11 +202,14 @@ func (m *PaymentModel) existsPrevNext(ctx context.Context, filteredSetCTE string } var prevExists, nextExists bool + start := time.Now() err = m.DB.QueryRowxContext(ctx, query, args...).Scan(&prevExists, &nextExists) + duration := time.Since(start).Seconds() + m.MetricsService.ObserveDBQueryDuration("SELECT", "ingest_payments", duration) if err != nil { return false, false, fmt.Errorf("fetching prev and next exists: %w", err) } - + m.MetricsService.IncDBQuery("SELECT", "ingest_payments") return prevExists, nextExists, nil } diff --git a/internal/data/payments_test.go b/internal/data/payments_test.go index 564ab0a..01c145a 100644 --- a/internal/data/payments_test.go +++ b/internal/data/payments_test.go @@ -9,6 +9,7 @@ import ( "github.com/stellar/go/xdr" "github.com/stellar/wallet-backend/internal/db" "github.com/stellar/wallet-backend/internal/db/dbtest" + "github.com/stellar/wallet-backend/internal/metrics" "github.com/stellar/wallet-backend/internal/utils" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -21,9 +22,13 @@ func TestPaymentModelAddPayment(t *testing.T) { dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) require.NoError(t, err) defer dbConnectionPool.Close() + sqlxDB, err := dbConnectionPool.SqlxDB(context.Background()) + require.NoError(t, err) + metricsService := metrics.NewMetricsService(sqlxDB) m := &PaymentModel{ - DB: dbConnectionPool, + DB: dbConnectionPool, + MetricsService: metricsService, } ctx := context.Background() @@ -145,10 +150,14 @@ func TestPaymentModelGetLatestLedgerSynced(t *testing.T) { dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) require.NoError(t, err) defer dbConnectionPool.Close() + sqlxDB, err := dbConnectionPool.SqlxDB(context.Background()) + require.NoError(t, err) + metricsService := metrics.NewMetricsService(sqlxDB) ctx := context.Background() m := &PaymentModel{ - DB: dbConnectionPool, + DB: dbConnectionPool, + MetricsService: metricsService, } const key = "ingest_store_key" @@ -170,10 +179,14 @@ func TestPaymentModelUpdateLatestLedgerSynced(t *testing.T) { dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) require.NoError(t, err) defer dbConnectionPool.Close() + sqlxDB, err := dbConnectionPool.SqlxDB(context.Background()) + require.NoError(t, err) + metricsService := metrics.NewMetricsService(sqlxDB) ctx := context.Background() m := &PaymentModel{ - DB: dbConnectionPool, + DB: dbConnectionPool, + MetricsService: metricsService, } const key = "ingest_store_key" @@ -192,10 +205,14 @@ func TestPaymentModelGetPaymentsPaginated(t *testing.T) { dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) require.NoError(t, err) defer dbConnectionPool.Close() + sqlxDB, err := dbConnectionPool.SqlxDB(context.Background()) + require.NoError(t, err) + metricsService := metrics.NewMetricsService(sqlxDB) ctx := context.Background() m := &PaymentModel{ - DB: dbConnectionPool, + DB: dbConnectionPool, + MetricsService: metricsService, } dbPayments := []Payment{ diff --git a/internal/ingest/ingest.go b/internal/ingest/ingest.go index fc5357d..726851d 100644 --- a/internal/ingest/ingest.go +++ b/internal/ingest/ingest.go @@ -6,12 +6,14 @@ import ( "net/http" "time" + "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/sirupsen/logrus" "github.com/stellar/go/support/log" "github.com/stellar/wallet-backend/internal/apptracker" "github.com/stellar/wallet-backend/internal/data" "github.com/stellar/wallet-backend/internal/db" + "github.com/stellar/wallet-backend/internal/metrics" "github.com/stellar/wallet-backend/internal/services" "github.com/stellar/wallet-backend/internal/tss" tssrouter "github.com/stellar/wallet-backend/internal/tss/router" @@ -49,22 +51,26 @@ func Ingest(cfg Configs) error { } func setupDeps(cfg Configs) (services.IngestService, error) { - // Open DB connection pool dbConnectionPool, err := db.OpenDBConnectionPool(cfg.DatabaseURL) if err != nil { return nil, fmt.Errorf("connecting to the database: %w", err) } - models, err := data.NewModels(dbConnectionPool) + db, err := dbConnectionPool.SqlxDB(context.Background()) + if err != nil { + return nil, fmt.Errorf("getting sqlx db: %w", err) + } + metricsService := metrics.NewMetricsService(db) + models, err := data.NewModels(dbConnectionPool, metricsService) if err != nil { return nil, fmt.Errorf("creating models: %w", err) } httpClient := &http.Client{Timeout: 30 * time.Second} - rpcService, err := services.NewRPCService(cfg.RPCURL, httpClient) + rpcService, err := services.NewRPCService(cfg.RPCURL, httpClient, metricsService) if err != nil { return nil, fmt.Errorf("instantiating rpc service: %w", err) } go rpcService.TrackRPCServiceHealth(context.Background()) - tssStore, err := tssstore.NewStore(dbConnectionPool) + tssStore, err := tssstore.NewStore(dbConnectionPool, metricsService) if err != nil { return nil, fmt.Errorf("instantiating tss store: %w", err) } @@ -75,10 +81,18 @@ func setupDeps(cfg Configs) (services.IngestService, error) { router := tssrouter.NewRouter(tssRouterConfig) ingestService, err := services.NewIngestService( - models, cfg.LedgerCursorName, cfg.AppTracker, rpcService, router, tssStore) + models, cfg.LedgerCursorName, cfg.AppTracker, rpcService, router, tssStore, metricsService) if err != nil { return nil, fmt.Errorf("instantiating ingest service: %w", err) } + http.Handle("/ingest-metrics", promhttp.HandlerFor(metricsService.GetRegistry(), promhttp.HandlerOpts{})) + go func() { + err := http.ListenAndServe(":8002", nil) + if err != nil { + log.Ctx(context.Background()).Fatalf("starting ingest metrics server: %v", err) + } + }() + return ingestService, nil } diff --git a/internal/metrics/metrics.go b/internal/metrics/metrics.go new file mode 100644 index 0000000..368f5ff --- /dev/null +++ b/internal/metrics/metrics.go @@ -0,0 +1,404 @@ +package metrics + +import ( + "fmt" + "strconv" + + "github.com/alitto/pond" + "github.com/dlmiddlecote/sqlstats" + "github.com/jmoiron/sqlx" + "github.com/prometheus/client_golang/prometheus" +) + +// MetricsService handles all metrics for the wallet-backend +type MetricsService struct { + registry *prometheus.Registry + db *sqlx.DB + + // Ingest Service Metrics + numPaymentOpsIngestedPerLedger *prometheus.GaugeVec + numTssTransactionsIngestedPerLedger *prometheus.GaugeVec + latestLedgerIngested prometheus.Gauge + ingestionDuration *prometheus.SummaryVec + + // TSS Service Metrics + numTSSTransactionsSubmitted prometheus.Counter + timeUntilTSSTransactionInclusion *prometheus.SummaryVec + + // Account Metrics + activeAccounts prometheus.Gauge + + // RPC Service Metrics + rpcRequestsTotal *prometheus.CounterVec + rpcRequestsDuration *prometheus.SummaryVec + rpcEndpointFailures *prometheus.CounterVec + rpcEndpointSuccesses *prometheus.CounterVec + rpcServiceHealth prometheus.Gauge + rpcLatestLedger prometheus.Gauge + + // TSS Transaction Status Metrics + tssTransactionCurrentStates *prometheus.GaugeVec + + // HTTP Request Metrics + numRequestsTotal *prometheus.CounterVec + requestsDuration *prometheus.SummaryVec + + // DB Query Metrics + dbQueryDuration *prometheus.SummaryVec + dbQueriesTotal *prometheus.CounterVec + + pools map[string]*pond.WorkerPool +} + +// NewMetricsService creates a new metrics service with all metrics registered +func NewMetricsService(db *sqlx.DB) *MetricsService { + m := &MetricsService{ + registry: prometheus.NewRegistry(), + db: db, + pools: make(map[string]*pond.WorkerPool), + } + + // Ingest Service Metrics + m.numPaymentOpsIngestedPerLedger = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "num_payment_ops_ingested_per_ledger", + Help: "Number of payment operations ingested per ledger", + }, + []string{"operation_type"}, + ) + m.numTssTransactionsIngestedPerLedger = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "num_tss_transactions_ingested_per_ledger", + Help: "Number of tss transactions ingested per ledger", + }, + []string{"status"}, + ) + m.latestLedgerIngested = prometheus.NewGauge( + prometheus.GaugeOpts{ + Name: "latest_ledger_ingested", + Help: "Latest ledger ingested", + }, + ) + m.ingestionDuration = prometheus.NewSummaryVec( + prometheus.SummaryOpts{ + Name: "ingestion_duration_seconds", + Help: "Duration of ledger ingestion", + Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001}, + }, + []string{"type"}, + ) + + // TSS Service Metrics + m.numTSSTransactionsSubmitted = prometheus.NewCounter( + prometheus.CounterOpts{ + Name: "num_tss_transactions_submitted", + Help: "Total number of transactions submitted to TSS", + }, + ) + m.timeUntilTSSTransactionInclusion = prometheus.NewSummaryVec( + prometheus.SummaryOpts{ + Name: "tss_transaction_inclusion_time_seconds", + Help: "Time from transaction submission to ledger inclusion (success or failure)", + Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001}, + }, + []string{"status"}, + ) + + // Account Metrics + m.activeAccounts = prometheus.NewGauge( + prometheus.GaugeOpts{ + Name: "active_accounts", + Help: "Number of currently registered active accounts", + }, + ) + + // RPC Service Metrics + m.rpcRequestsTotal = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "rpc_requests_total", + Help: "Total number of RPC requests", + }, + []string{"endpoint"}, + ) + m.rpcRequestsDuration = prometheus.NewSummaryVec( + prometheus.SummaryOpts{ + Name: "rpc_requests_duration_seconds", + Help: "Duration of RPC requests in seconds", + Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001}, + }, + []string{"endpoint"}, + ) + m.rpcEndpointFailures = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "rpc_endpoint_failures_total", + Help: "Total number of RPC endpoint failures", + }, + []string{"endpoint"}, + ) + m.rpcEndpointSuccesses = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "rpc_endpoint_successes_total", + Help: "Total number of successful RPC requests", + }, + []string{"endpoint"}, + ) + m.rpcServiceHealth = prometheus.NewGauge( + prometheus.GaugeOpts{ + Name: "rpc_service_health", + Help: "RPC service health status (1 for healthy, 0 for unhealthy)", + }, + ) + m.rpcLatestLedger = prometheus.NewGauge( + prometheus.GaugeOpts{ + Name: "rpc_latest_ledger", + Help: "Latest ledger number reported by the RPC service", + }, + ) + + m.tssTransactionCurrentStates = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "tss_transaction_current_states", + Help: "Current number of TSS transactions in each state", + }, + []string{"channel", "status"}, + ) + + // HTTP Request Metrics + m.numRequestsTotal = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "http_requests_total", + Help: "Total number of HTTP requests", + }, + []string{"endpoint", "method", "status_code"}, + ) + m.requestsDuration = prometheus.NewSummaryVec( + prometheus.SummaryOpts{ + Name: "http_request_duration_seconds", + Help: "Duration of HTTP requests in seconds", + Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001}, + }, + []string{"endpoint", "method"}, + ) + + // DB Query Metrics + m.dbQueryDuration = prometheus.NewSummaryVec( + prometheus.SummaryOpts{ + Name: "db_query_duration_seconds", + Help: "Duration of database queries", + Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001}, + }, + []string{"query_type", "table"}, + ) + m.dbQueriesTotal = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "db_queries_total", + Help: "Total number of database queries", + }, + []string{"query_type", "table"}, + ) + m.registerMetrics() + return m +} + +func (m *MetricsService) registerMetrics() { + collector := sqlstats.NewStatsCollector("wallet-backend-db", m.db) + m.registry.MustRegister( + collector, + m.numPaymentOpsIngestedPerLedger, + m.numTssTransactionsIngestedPerLedger, + m.latestLedgerIngested, + m.ingestionDuration, + m.numTSSTransactionsSubmitted, + m.timeUntilTSSTransactionInclusion, + m.activeAccounts, + m.rpcRequestsTotal, + m.rpcRequestsDuration, + m.rpcEndpointFailures, + m.rpcEndpointSuccesses, + m.rpcServiceHealth, + m.rpcLatestLedger, + m.tssTransactionCurrentStates, + m.numRequestsTotal, + m.requestsDuration, + m.dbQueryDuration, + m.dbQueriesTotal, + ) +} + +// RegisterPool registers a worker pool for metrics collection +func (m *MetricsService) RegisterPoolMetrics(channel string, pool *pond.WorkerPool) { + m.pools[channel] = pool + + m.registry.MustRegister(prometheus.NewGaugeFunc( + prometheus.GaugeOpts{ + Name: fmt.Sprintf("pool_workers_running_%s", channel), + Help: "Number of running worker goroutines", + ConstLabels: prometheus.Labels{"channel": channel}, + }, + func() float64 { + return float64(pool.RunningWorkers()) + }, + )) + + m.registry.MustRegister(prometheus.NewGaugeFunc( + prometheus.GaugeOpts{ + Name: fmt.Sprintf("pool_workers_idle_%s", channel), + Help: "Number of idle worker goroutines", + ConstLabels: prometheus.Labels{"channel": channel}, + }, + func() float64 { + return float64(pool.IdleWorkers()) + }, + )) + + m.registry.MustRegister(prometheus.NewCounterFunc( + prometheus.CounterOpts{ + Name: fmt.Sprintf("pool_tasks_submitted_total_%s", channel), + Help: "Number of tasks submitted", + ConstLabels: prometheus.Labels{"channel": channel}, + }, + func() float64 { + return float64(pool.SubmittedTasks()) + }, + )) + + m.registry.MustRegister(prometheus.NewGaugeFunc( + prometheus.GaugeOpts{ + Name: fmt.Sprintf("pool_tasks_waiting_%s", channel), + Help: "Number of tasks currently waiting in the queue", + ConstLabels: prometheus.Labels{"channel": channel}, + }, + func() float64 { + return float64(pool.WaitingTasks()) + }, + )) + + m.registry.MustRegister(prometheus.NewCounterFunc( + prometheus.CounterOpts{ + Name: fmt.Sprintf("pool_tasks_successful_total_%s", channel), + Help: "Number of tasks that completed successfully", + ConstLabels: prometheus.Labels{"channel": channel}, + }, + func() float64 { + return float64(pool.SuccessfulTasks()) + }, + )) + + m.registry.MustRegister(prometheus.NewCounterFunc( + prometheus.CounterOpts{ + Name: fmt.Sprintf("pool_tasks_failed_total_%s", channel), + Help: "Number of tasks that completed with panic", + ConstLabels: prometheus.Labels{"channel": channel}, + }, + func() float64 { + return float64(pool.FailedTasks()) + }, + )) + + m.registry.MustRegister(prometheus.NewCounterFunc( + prometheus.CounterOpts{ + Name: fmt.Sprintf("pool_tasks_completed_total_%s", channel), + Help: "Number of tasks that completed either successfully or with panic", + ConstLabels: prometheus.Labels{"channel": channel}, + }, + func() float64 { + return float64(pool.CompletedTasks()) + }, + )) +} + +// GetRegistry returns the prometheus registry +func (m *MetricsService) GetRegistry() *prometheus.Registry { + return m.registry +} + +// Ingest Service Metrics +func (m *MetricsService) SetNumPaymentOpsIngestedPerLedger(operationType string, value int) { + m.numPaymentOpsIngestedPerLedger.WithLabelValues(operationType).Set(float64(value)) +} + +func (m *MetricsService) SetNumTssTransactionsIngestedPerLedger(status string, value float64) { + m.numTssTransactionsIngestedPerLedger.WithLabelValues(status).Set(value) +} + +func (m *MetricsService) SetLatestLedgerIngested(value float64) { + m.latestLedgerIngested.Set(value) +} + +func (m *MetricsService) ObserveIngestionDuration(ingestionType string, duration float64) { + m.ingestionDuration.WithLabelValues(ingestionType).Observe(duration) +} + +// TSS Service Metrics +func (m *MetricsService) IncNumTSSTransactionsSubmitted() { + m.numTSSTransactionsSubmitted.Inc() +} + +// ObserveTSSTransactionInclusionTime records the time taken for a transaction to be included in the ledger +func (m *MetricsService) ObserveTSSTransactionInclusionTime(status string, durationSeconds float64) { + m.timeUntilTSSTransactionInclusion.WithLabelValues(status).Observe(durationSeconds) +} + +// Account Service Metrics +func (m *MetricsService) IncActiveAccount() { + m.activeAccounts.Inc() +} + +func (m *MetricsService) DecActiveAccount() { + m.activeAccounts.Dec() +} + +// RPC Service Metrics +func (m *MetricsService) IncRPCRequests(endpoint string) { + m.rpcRequestsTotal.WithLabelValues(endpoint).Inc() +} + +func (m *MetricsService) ObserveRPCRequestDuration(endpoint string, duration float64) { + m.rpcRequestsDuration.WithLabelValues(endpoint).Observe(duration) +} + +func (m *MetricsService) IncRPCEndpointFailure(endpoint string) { + m.rpcEndpointFailures.WithLabelValues(endpoint).Inc() +} + +func (m *MetricsService) IncRPCEndpointSuccess(endpoint string) { + m.rpcEndpointSuccesses.WithLabelValues(endpoint).Inc() +} + +func (m *MetricsService) SetRPCServiceHealth(healthy bool) { + if healthy { + m.rpcServiceHealth.Set(1) + } else { + m.rpcServiceHealth.Set(0) + } +} + +func (m *MetricsService) SetRPCLatestLedger(ledger int64) { + m.rpcLatestLedger.Set(float64(ledger)) +} + +// HTTP Request Metrics +func (m *MetricsService) IncNumRequests(endpoint, method string, statusCode int) { + m.numRequestsTotal.WithLabelValues(endpoint, method, strconv.Itoa(statusCode)).Inc() +} + +func (m *MetricsService) ObserveRequestDuration(endpoint, method string, duration float64) { + m.requestsDuration.WithLabelValues(endpoint, method).Observe(duration) +} + +// DB Query Metrics +func (m *MetricsService) ObserveDBQueryDuration(queryType, table string, duration float64) { + m.dbQueryDuration.WithLabelValues(queryType, table).Observe(duration) +} + +func (m *MetricsService) IncDBQuery(queryType, table string) { + m.dbQueriesTotal.WithLabelValues(queryType, table).Inc() +} + +// TSS Transaction Status Metrics +func (m *MetricsService) RecordTSSTransactionStatusTransition(channel, oldStatus, newStatus string) { + if oldStatus != "" { + m.tssTransactionCurrentStates.WithLabelValues(channel, oldStatus).Dec() + } + m.tssTransactionCurrentStates.WithLabelValues(channel, newStatus).Inc() +} diff --git a/internal/metrics/metrics_test.go b/internal/metrics/metrics_test.go new file mode 100644 index 0000000..3ecba09 --- /dev/null +++ b/internal/metrics/metrics_test.go @@ -0,0 +1,495 @@ +package metrics + +import ( + "testing" + "time" + + "github.com/alitto/pond" + "github.com/jmoiron/sqlx" + _ "github.com/mattn/go-sqlite3" // SQLite driver + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func setupTestDB(t *testing.T) *sqlx.DB { + // For testing, we can use a mock DB or sqlite in-memory + db, err := sqlx.Connect("sqlite3", ":memory:") + require.NoError(t, err) + return db +} + +func TestNewMetricsService(t *testing.T) { + db := setupTestDB(t) + defer db.Close() + + ms := NewMetricsService(db) + assert.NotNil(t, ms) + assert.NotNil(t, ms.registry) + assert.NotNil(t, ms.pools) + + // Test that all metric vectors are initialized + assert.NotNil(t, ms.numPaymentOpsIngestedPerLedger) + assert.NotNil(t, ms.numTssTransactionsIngestedPerLedger) + assert.NotNil(t, ms.latestLedgerIngested) + assert.NotNil(t, ms.ingestionDuration) + assert.NotNil(t, ms.activeAccounts) + assert.NotNil(t, ms.rpcRequestsTotal) + assert.NotNil(t, ms.rpcRequestsDuration) + assert.NotNil(t, ms.rpcEndpointFailures) + assert.NotNil(t, ms.rpcEndpointSuccesses) + assert.NotNil(t, ms.rpcServiceHealth) + assert.NotNil(t, ms.numRequestsTotal) + assert.NotNil(t, ms.requestsDuration) + assert.NotNil(t, ms.dbQueryDuration) + assert.NotNil(t, ms.dbQueriesTotal) +} + +func TestIngestMetrics(t *testing.T) { + db := setupTestDB(t) + defer db.Close() + + ms := NewMetricsService(db) + + t.Run("payment ops metrics", func(t *testing.T) { + ms.SetNumPaymentOpsIngestedPerLedger("create_account", 5) + ms.SetNumPaymentOpsIngestedPerLedger("payment", 10) + + // We can't directly access the metric values, but we can verify they're collected + metricFamilies, err := ms.registry.Gather() + require.NoError(t, err) + + found := false + for _, mf := range metricFamilies { + if mf.GetName() == "num_payment_ops_ingested_per_ledger" { + found = true + assert.Equal(t, 2, len(mf.GetMetric())) + } + } + assert.True(t, found) + }) + + t.Run("latest ledger metrics", func(t *testing.T) { + ms.SetLatestLedgerIngested(1234) + + metricFamilies, err := ms.registry.Gather() + require.NoError(t, err) + + found := false + for _, mf := range metricFamilies { + if mf.GetName() == "latest_ledger_ingested" { + found = true + assert.Equal(t, 1, len(mf.GetMetric())) + } + } + assert.True(t, found) + }) + + t.Run("ingestion duration metrics", func(t *testing.T) { + ms.ObserveIngestionDuration("payment", 0.5) + ms.ObserveIngestionDuration("transaction", 1.0) + + metricFamilies, err := ms.registry.Gather() + require.NoError(t, err) + + found := false + for _, mf := range metricFamilies { + if mf.GetName() == "ingestion_duration_seconds" { + found = true + assert.Equal(t, 2, len(mf.GetMetric())) + } + } + assert.True(t, found) + }) +} + +func TestTSSMetrics(t *testing.T) { + db := setupTestDB(t) + defer db.Close() + + ms := NewMetricsService(db) + + t.Run("TSS transactions submitted counter", func(t *testing.T) { + // Increment the counter multiple times + ms.IncNumTSSTransactionsSubmitted() + ms.IncNumTSSTransactionsSubmitted() + ms.IncNumTSSTransactionsSubmitted() + + metricFamilies, err := ms.registry.Gather() + require.NoError(t, err) + + found := false + for _, mf := range metricFamilies { + if mf.GetName() == "num_tss_transactions_submitted" { + found = true + metric := mf.GetMetric()[0] + assert.Equal(t, float64(3), metric.GetCounter().GetValue(), "Expected counter value to be 3") + } + } + assert.True(t, found, "TSS transactions submitted metric not found") + }) + + t.Run("TSS transaction inclusion time", func(t *testing.T) { + // Test successful transaction + ms.ObserveTSSTransactionInclusionTime("success", 5.5) + // Test failed transaction + ms.ObserveTSSTransactionInclusionTime("failed", 2.3) + + metricFamilies, err := ms.registry.Gather() + require.NoError(t, err) + + found := false + for _, mf := range metricFamilies { + if mf.GetName() == "tss_transaction_inclusion_time_seconds" { + found = true + metric := mf.GetMetric()[0] + assert.Equal(t, uint64(1), metric.GetSummary().GetSampleCount()) + assert.Equal(t, 2.3, metric.GetSummary().GetSampleSum()) + assert.Equal(t, "failed", metric.GetLabel()[0].GetValue()) + + metric = mf.GetMetric()[1] + assert.Equal(t, uint64(1), metric.GetSummary().GetSampleCount()) + assert.Equal(t, 5.5, metric.GetSummary().GetSampleSum()) + assert.Equal(t, "success", metric.GetLabel()[0].GetValue()) + } + } + assert.True(t, found, "Transaction inclusion time metric not found") + }) +} + +func TestAccountMetrics(t *testing.T) { + db := setupTestDB(t) + defer db.Close() + + ms := NewMetricsService(db) + + t.Run("active accounts counter", func(t *testing.T) { + // Initial state should be 0 + _, err := ms.registry.Gather() + require.NoError(t, err) + + // Increment and verify + ms.IncActiveAccount() + ms.IncActiveAccount() + + // Decrement and verify + ms.DecActiveAccount() + + metricFamilies, err := ms.registry.Gather() + require.NoError(t, err) + + found := false + for _, mf := range metricFamilies { + if mf.GetName() == "active_accounts" { + found = true + assert.Equal(t, 1, len(mf.GetMetric())) + } + } + assert.True(t, found) + }) +} + +func TestRPCMetrics(t *testing.T) { + db := setupTestDB(t) + defer db.Close() + + ms := NewMetricsService(db) + + t.Run("RPC request metrics", func(t *testing.T) { + endpoint := "test_endpoint" + + ms.IncRPCRequests(endpoint) + ms.ObserveRPCRequestDuration(endpoint, 0.1) + ms.IncRPCEndpointSuccess(endpoint) + ms.IncRPCEndpointFailure(endpoint) + metricFamilies, err := ms.registry.Gather() + require.NoError(t, err) + + foundRequests := false + foundDuration := false + foundSuccess := false + foundFailures := false + for _, mf := range metricFamilies { + switch mf.GetName() { + case "rpc_requests_total": + foundRequests = true + metric := mf.GetMetric()[0] + assert.Equal(t, float64(1), metric.GetCounter().GetValue()) + assert.Equal(t, "test_endpoint", metric.GetLabel()[0].GetValue()) + case "rpc_requests_duration_seconds": + foundDuration = true + metric := mf.GetMetric()[0] + assert.Equal(t, uint64(1), metric.GetSummary().GetSampleCount()) + assert.Equal(t, 0.1, metric.GetSummary().GetSampleSum()) + assert.Equal(t, "test_endpoint", metric.GetLabel()[0].GetValue()) + case "rpc_endpoint_successes_total": + foundSuccess = true + metric := mf.GetMetric()[0] + assert.Equal(t, float64(1), metric.GetCounter().GetValue()) + assert.Equal(t, "test_endpoint", metric.GetLabel()[0].GetValue()) + case "rpc_endpoint_failures_total": + foundFailures = true + metric := mf.GetMetric()[0] + assert.Equal(t, float64(1), metric.GetCounter().GetValue()) + assert.Equal(t, "test_endpoint", metric.GetLabel()[0].GetValue()) + } + } + + assert.True(t, foundRequests) + assert.True(t, foundDuration) + assert.True(t, foundSuccess) + assert.True(t, foundFailures) + }) + + t.Run("RPC health metrics", func(t *testing.T) { + ms.SetRPCServiceHealth(true) + + metricFamilies, err := ms.registry.Gather() + require.NoError(t, err) + + found := false + for _, mf := range metricFamilies { + if mf.GetName() == "rpc_service_health" { + found = true + metric := mf.GetMetric()[0] + assert.Equal(t, float64(1), metric.GetGauge().GetValue()) + } + } + assert.True(t, found) + + ms.SetRPCServiceHealth(false) + metricFamilies, err = ms.registry.Gather() + require.NoError(t, err) + + for _, mf := range metricFamilies { + if mf.GetName() == "rpc_service_health" { + metric := mf.GetMetric()[0] + assert.Equal(t, float64(0), metric.GetGauge().GetValue()) + } + } + }) +} + +func TestHTTPMetrics(t *testing.T) { + db := setupTestDB(t) + defer db.Close() + + ms := NewMetricsService(db) + endpoint := "/api/v1/test" + method := "POST" + statusCode := 200 + + ms.IncNumRequests(endpoint, method, statusCode) + ms.ObserveRequestDuration(endpoint, method, 0.05) + + metricFamilies, err := ms.registry.Gather() + require.NoError(t, err) + + foundRequests := false + foundDuration := false + + for _, mf := range metricFamilies { + switch mf.GetName() { + case "http_requests_total": + foundRequests = true + metric := mf.GetMetric()[0] + assert.Equal(t, float64(1), metric.GetCounter().GetValue()) + // Verify all labels + labels := make(map[string]string) + for _, label := range metric.GetLabel() { + labels[label.GetName()] = label.GetValue() + } + assert.Equal(t, endpoint, labels["endpoint"]) + assert.Equal(t, method, labels["method"]) + assert.Equal(t, "200", labels["status_code"]) + case "http_request_duration_seconds": + foundDuration = true + metric := mf.GetMetric()[0] + assert.Equal(t, uint64(1), metric.GetSummary().GetSampleCount()) + assert.Equal(t, 0.05, metric.GetSummary().GetSampleSum()) + // Verify all labels + labels := make(map[string]string) + for _, label := range metric.GetLabel() { + labels[label.GetName()] = label.GetValue() + } + assert.Equal(t, endpoint, labels["endpoint"]) + assert.Equal(t, method, labels["method"]) + } + } + + assert.True(t, foundRequests) + assert.True(t, foundDuration) +} + +func TestDBMetrics(t *testing.T) { + db := setupTestDB(t) + defer db.Close() + + ms := NewMetricsService(db) + queryType := "SELECT" + table := "accounts" + + ms.IncDBQuery(queryType, table) + ms.ObserveDBQueryDuration(queryType, table, 0.01) + + metricFamilies, err := ms.registry.Gather() + require.NoError(t, err) + + foundQueries := false + foundDuration := false + + for _, mf := range metricFamilies { + switch mf.GetName() { + case "db_queries_total": + foundQueries = true + metric := mf.GetMetric()[0] + assert.Equal(t, float64(1), metric.GetCounter().GetValue()) + // Verify labels + labels := make(map[string]string) + for _, label := range metric.GetLabel() { + labels[label.GetName()] = label.GetValue() + } + assert.Equal(t, queryType, labels["query_type"]) + assert.Equal(t, table, labels["table"]) + case "db_query_duration_seconds": + foundDuration = true + metric := mf.GetMetric()[0] + assert.Equal(t, uint64(1), metric.GetSummary().GetSampleCount()) + assert.Equal(t, 0.01, metric.GetSummary().GetSampleSum()) + // Verify labels + labels := make(map[string]string) + for _, label := range metric.GetLabel() { + labels[label.GetName()] = label.GetValue() + } + assert.Equal(t, queryType, labels["query_type"]) + assert.Equal(t, table, labels["table"]) + } + } + + assert.True(t, foundQueries, "Query counter metric not found") + assert.True(t, foundDuration, "Query duration metric not found") +} + +func TestPoolMetrics(t *testing.T) { + db := setupTestDB(t) + defer db.Close() + + ms := NewMetricsService(db) + + t.Run("worker pool metrics - success case", func(t *testing.T) { + channel := "test_channel" + pool := pond.New(5, 10) + + ms.RegisterPoolMetrics(channel, pool) + + // Submit some tasks to the pool + for i := 0; i < 3; i++ { + pool.Submit(func() { + time.Sleep(10 * time.Millisecond) + }) + } + + // Wait for tasks to complete + time.Sleep(20 * time.Millisecond) + + metricFamilies, err := ms.registry.Gather() + require.NoError(t, err) + + // Map to store metric values + metricValues := make(map[string]float64) + + for _, mf := range metricFamilies { + metric := mf.GetMetric()[0] + switch mf.GetName() { + case "pool_workers_running_" + channel: + metricValues["workers_running"] = metric.GetGauge().GetValue() + assert.Equal(t, channel, metric.GetLabel()[0].GetValue(), "Unexpected channel label for workers_running") + + case "pool_workers_idle_" + channel: + metricValues["workers_idle"] = metric.GetGauge().GetValue() + assert.Equal(t, channel, metric.GetLabel()[0].GetValue(), "Unexpected channel label for workers_idle") + + case "pool_tasks_submitted_total_" + channel: + metricValues["tasks_submitted"] = metric.GetCounter().GetValue() + assert.Equal(t, channel, metric.GetLabel()[0].GetValue(), "Unexpected channel label for tasks_submitted") + + case "pool_tasks_completed_total_" + channel: + metricValues["tasks_completed"] = metric.GetCounter().GetValue() + assert.Equal(t, channel, metric.GetLabel()[0].GetValue(), "Unexpected channel label for tasks_completed") + + case "pool_tasks_successful_total_" + channel: + metricValues["tasks_successful"] = metric.GetCounter().GetValue() + assert.Equal(t, channel, metric.GetLabel()[0].GetValue(), "Unexpected channel label for tasks_successful") + + case "pool_tasks_failed_total_" + channel: + metricValues["tasks_failed"] = metric.GetCounter().GetValue() + assert.Equal(t, channel, metric.GetLabel()[0].GetValue(), "Unexpected channel label for tasks_failed") + + case "pool_tasks_waiting_" + channel: + metricValues["tasks_waiting"] = metric.GetGauge().GetValue() + assert.Equal(t, channel, metric.GetLabel()[0].GetValue(), "Unexpected channel label for tasks_waiting") + } + } + + assert.Equal(t, float64(3), metricValues["tasks_submitted"], "Expected 3 tasks submitted") + assert.Equal(t, float64(3), metricValues["tasks_completed"], "Expected 3 tasks completed") + assert.Equal(t, float64(3), metricValues["tasks_successful"], "Expected 3 successful tasks") + assert.Equal(t, float64(0), metricValues["tasks_failed"], "Expected 0 failed tasks") + assert.Equal(t, float64(0), metricValues["tasks_waiting"], "Expected 0 waiting tasks") + + // Workers should be idle after tasks complete + assert.Equal(t, float64(3), metricValues["workers_idle"], "Should have idle workers") + pool.StopAndWait() + }) + + t.Run("worker pool metrics - with failures", func(t *testing.T) { + channel := "test_channel_failures" + pool := pond.New(2, 5) + + ms.RegisterPoolMetrics(channel, pool) + + // Submit tasks that will panic + for i := 0; i < 2; i++ { + pool.Submit(func() { + panic("test panic") + }) + } + + // Submit successful task + pool.Submit(func() { + time.Sleep(5 * time.Millisecond) + }) + + // Wait for tasks to complete + time.Sleep(10 * time.Millisecond) + + metricFamilies, err := ms.registry.Gather() + require.NoError(t, err) + + metricValues := make(map[string]float64) + + for _, mf := range metricFamilies { + metric := mf.GetMetric()[0] + switch mf.GetName() { + case "pool_tasks_failed_total_" + channel: + metricValues["tasks_failed"] = metric.GetCounter().GetValue() + assert.Equal(t, channel, metric.GetLabel()[0].GetValue(), "Unexpected channel label for failed tasks") + case "pool_tasks_successful_total_" + channel: + metricValues["tasks_successful"] = metric.GetCounter().GetValue() + assert.Equal(t, channel, metric.GetLabel()[0].GetValue(), "Unexpected channel label for successful tasks") + case "pool_tasks_submitted_total_" + channel: + metricValues["tasks_submitted"] = metric.GetCounter().GetValue() + assert.Equal(t, channel, metric.GetLabel()[0].GetValue(), "Unexpected channel label for submitted tasks") + case "pool_tasks_completed_total_" + channel: + metricValues["tasks_completed"] = metric.GetCounter().GetValue() + assert.Equal(t, channel, metric.GetLabel()[0].GetValue(), "Unexpected channel label for completed tasks") + } + } + + assert.Equal(t, float64(3), metricValues["tasks_submitted"], "Expected 3 total tasks") + assert.Equal(t, float64(2), metricValues["tasks_failed"], "Expected 2 failed tasks") + assert.Equal(t, float64(1), metricValues["tasks_successful"], "Expected 1 successful task") + assert.Equal(t, float64(3), metricValues["tasks_completed"], "Expected 3 completed tasks (both successful and failed)") + + pool.StopAndWait() + }) +} diff --git a/internal/serve/httphandler/account_handler_test.go b/internal/serve/httphandler/account_handler_test.go index 2d1cb89..77bac44 100644 --- a/internal/serve/httphandler/account_handler_test.go +++ b/internal/serve/httphandler/account_handler_test.go @@ -21,6 +21,7 @@ import ( "github.com/stellar/wallet-backend/internal/db" "github.com/stellar/wallet-backend/internal/db/dbtest" "github.com/stellar/wallet-backend/internal/entities" + "github.com/stellar/wallet-backend/internal/metrics" "github.com/stellar/wallet-backend/internal/services" "github.com/stellar/wallet-backend/internal/services/servicesmocks" "github.com/stretchr/testify/assert" @@ -34,10 +35,13 @@ func TestAccountHandlerRegisterAccount(t *testing.T) { dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) require.NoError(t, err) defer dbConnectionPool.Close() + sqlxDB, err := dbConnectionPool.SqlxDB(context.Background()) + require.NoError(t, err) + metricsService := metrics.NewMetricsService(sqlxDB) - models, err := data.NewModels(dbConnectionPool) + models, err := data.NewModels(dbConnectionPool, metricsService) require.NoError(t, err) - accountService, err := services.NewAccountService(models) + accountService, err := services.NewAccountService(models, metricsService) require.NoError(t, err) handler := &AccountHandler{ AccountService: accountService, @@ -133,10 +137,13 @@ func TestAccountHandlerDeregisterAccount(t *testing.T) { dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) require.NoError(t, err) defer dbConnectionPool.Close() + sqlxDB, err := dbConnectionPool.SqlxDB(context.Background()) + require.NoError(t, err) + metricsService := metrics.NewMetricsService(sqlxDB) - models, err := data.NewModels(dbConnectionPool) + models, err := data.NewModels(dbConnectionPool, metricsService) require.NoError(t, err) - accountService, err := services.NewAccountService(models) + accountService, err := services.NewAccountService(models, metricsService) require.NoError(t, err) handler := &AccountHandler{ AccountService: accountService, diff --git a/internal/serve/httphandler/payment_handler_test.go b/internal/serve/httphandler/payment_handler_test.go index 1409569..b981190 100644 --- a/internal/serve/httphandler/payment_handler_test.go +++ b/internal/serve/httphandler/payment_handler_test.go @@ -13,6 +13,7 @@ import ( "github.com/stellar/wallet-backend/internal/data" "github.com/stellar/wallet-backend/internal/db" "github.com/stellar/wallet-backend/internal/db/dbtest" + "github.com/stellar/wallet-backend/internal/metrics" "github.com/stellar/wallet-backend/internal/services" "github.com/stellar/wallet-backend/internal/utils" "github.com/stretchr/testify/assert" @@ -26,8 +27,11 @@ func TestPaymentHandlerGetPayments(t *testing.T) { dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) require.NoError(t, err) defer dbConnectionPool.Close() + sqlxDB, err := dbConnectionPool.SqlxDB(context.Background()) + require.NoError(t, err) + metricsService := metrics.NewMetricsService(sqlxDB) - models, err := data.NewModels(dbConnectionPool) + models, err := data.NewModels(dbConnectionPool, metricsService) require.NoError(t, err) paymentService, err := services.NewPaymentService(models, "http://testing.com") require.NoError(t, err) diff --git a/internal/serve/httphandler/tss_handler.go b/internal/serve/httphandler/tss_handler.go index 7d4d234..3f0f7c7 100644 --- a/internal/serve/httphandler/tss_handler.go +++ b/internal/serve/httphandler/tss_handler.go @@ -8,6 +8,7 @@ import ( "github.com/stellar/go/support/render/httpjson" "github.com/stellar/go/txnbuild" "github.com/stellar/wallet-backend/internal/apptracker" + "github.com/stellar/wallet-backend/internal/metrics" "github.com/stellar/wallet-backend/internal/serve/httperror" "github.com/stellar/wallet-backend/internal/tss" "github.com/stellar/wallet-backend/internal/tss/router" @@ -22,6 +23,7 @@ type TSSHandler struct { AppTracker apptracker.AppTracker NetworkPassphrase string TransactionService tssservices.TransactionService + MetricsService *metrics.MetricsService } type Transaction struct { @@ -114,6 +116,9 @@ func (t *TSSHandler) SubmitTransactions(w http.ResponseWriter, r *http.Request) payloads = append(payloads, payload) transactionHashes = append(transactionHashes, txHash) + if t.MetricsService != nil { + t.MetricsService.IncNumTSSTransactionsSubmitted() + } } httpjson.Render(w, TransactionSubmissionResponse{ TransactionHashes: transactionHashes, diff --git a/internal/serve/httphandler/tss_handler_test.go b/internal/serve/httphandler/tss_handler_test.go index 5453871..eb40e73 100644 --- a/internal/serve/httphandler/tss_handler_test.go +++ b/internal/serve/httphandler/tss_handler_test.go @@ -20,6 +20,7 @@ import ( "github.com/stellar/wallet-backend/internal/apptracker" "github.com/stellar/wallet-backend/internal/db" "github.com/stellar/wallet-backend/internal/db/dbtest" + "github.com/stellar/wallet-backend/internal/metrics" "github.com/stellar/wallet-backend/internal/tss" "github.com/stellar/wallet-backend/internal/tss/router" tssservices "github.com/stellar/wallet-backend/internal/tss/services" @@ -37,7 +38,10 @@ func TestBuildTransactions(t *testing.T) { dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) require.NoError(t, err) defer dbConnectionPool.Close() - store, _ := store.NewStore(dbConnectionPool) + sqlxDB, err := dbConnectionPool.SqlxDB(context.Background()) + require.NoError(t, err) + metricsService := metrics.NewMetricsService(sqlxDB) + store, _ := store.NewStore(dbConnectionPool, metricsService) mockRouter := router.MockRouter{} mockAppTracker := apptracker.MockAppTracker{} mockTxService := tssservices.TransactionServiceMock{} @@ -135,7 +139,10 @@ func TestSubmitTransactions(t *testing.T) { dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) require.NoError(t, err) defer dbConnectionPool.Close() - store, _ := store.NewStore(dbConnectionPool) + sqlxDB, err := dbConnectionPool.SqlxDB(context.Background()) + require.NoError(t, err) + metricsService := metrics.NewMetricsService(sqlxDB) + store, _ := store.NewStore(dbConnectionPool, metricsService) mockRouter := router.MockRouter{} mockAppTracker := apptracker.MockAppTracker{} txServiceMock := tssservices.TransactionServiceMock{} @@ -230,7 +237,10 @@ func TestGetTransaction(t *testing.T) { dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) require.NoError(t, err) defer dbConnectionPool.Close() - store, _ := store.NewStore(dbConnectionPool) + sqlxDB, err := dbConnectionPool.SqlxDB(context.Background()) + require.NoError(t, err) + metricsService := metrics.NewMetricsService(sqlxDB) + store, _ := store.NewStore(dbConnectionPool, metricsService) mockRouter := router.MockRouter{} mockAppTracker := apptracker.MockAppTracker{} txServiceMock := tssservices.TransactionServiceMock{} diff --git a/internal/serve/middleware/metrics_middleware.go b/internal/serve/middleware/metrics_middleware.go new file mode 100644 index 0000000..8e39968 --- /dev/null +++ b/internal/serve/middleware/metrics_middleware.go @@ -0,0 +1,52 @@ +package middleware + +import ( + "net/http" + "time" + + "github.com/stellar/wallet-backend/internal/metrics" +) + +// MetricsMiddleware creates a middleware that tracks HTTP request metrics +func MetricsMiddleware(metricsService *metrics.MetricsService) func(next http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + startTime := time.Now() + + // Get the endpoint from the request path + endpoint := r.URL.Path + if endpoint == "" { + endpoint = "/" + } + + // Create a response wrapper to capture the status code + rw := &responseWriter{ResponseWriter: w} + + // Call the next handler + next.ServeHTTP(rw, r) + + duration := time.Since(startTime).Seconds() + metricsService.ObserveRequestDuration(endpoint, r.Method, duration) + metricsService.IncNumRequests(endpoint, r.Method, rw.statusCode) + }) + } +} + +// responseWriter wraps http.ResponseWriter to capture the status code +type responseWriter struct { + http.ResponseWriter + statusCode int +} + +func (rw *responseWriter) WriteHeader(code int) { + rw.statusCode = code + rw.ResponseWriter.WriteHeader(code) +} + +func (rw *responseWriter) Write(b []byte) (int, error) { + // If WriteHeader hasn't been called yet, we assume it's a 200 + if rw.statusCode == 0 { + rw.statusCode = http.StatusOK + } + return rw.ResponseWriter.Write(b) +} diff --git a/internal/serve/serve.go b/internal/serve/serve.go index 0574b5b..8f6ca9c 100644 --- a/internal/serve/serve.go +++ b/internal/serve/serve.go @@ -7,6 +7,7 @@ import ( "time" "github.com/go-chi/chi" + "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/sirupsen/logrus" supporthttp "github.com/stellar/go/support/http" "github.com/stellar/go/support/log" @@ -17,6 +18,7 @@ import ( "github.com/stellar/wallet-backend/internal/data" "github.com/stellar/wallet-backend/internal/db" "github.com/stellar/wallet-backend/internal/entities" + "github.com/stellar/wallet-backend/internal/metrics" "github.com/stellar/wallet-backend/internal/serve/auth" "github.com/stellar/wallet-backend/internal/serve/httperror" "github.com/stellar/wallet-backend/internal/serve/httphandler" @@ -95,6 +97,7 @@ type handlerDeps struct { AccountService services.AccountService AccountSponsorshipService services.AccountSponsorshipService PaymentService services.PaymentService + MetricsService *metrics.MetricsService // TSS RPCCallerChannel tss.Channel ErrorJitterChannel tss.Channel @@ -139,7 +142,12 @@ func initHandlerDeps(cfg Configs) (handlerDeps, error) { if err != nil { return handlerDeps{}, fmt.Errorf("connecting to the database: %w", err) } - models, err := data.NewModels(dbConnectionPool) + db, err := dbConnectionPool.SqlxDB(context.Background()) + if err != nil { + return handlerDeps{}, fmt.Errorf("getting sqlx db: %w", err) + } + metricsService := metrics.NewMetricsService(db) + models, err := data.NewModels(dbConnectionPool, metricsService) if err != nil { return handlerDeps{}, fmt.Errorf("creating models for Serve: %w", err) } @@ -150,7 +158,7 @@ func initHandlerDeps(cfg Configs) (handlerDeps, error) { } httpClient := http.Client{Timeout: time.Duration(30 * time.Second)} - rpcService, err := services.NewRPCService(cfg.RPCURL, &httpClient) + rpcService, err := services.NewRPCService(cfg.RPCURL, &httpClient, metricsService) if err != nil { return handlerDeps{}, fmt.Errorf("instantiating rpc service: %w", err) } @@ -158,7 +166,7 @@ func initHandlerDeps(cfg Configs) (handlerDeps, error) { channelAccountStore := store.NewChannelAccountModel(dbConnectionPool) - accountService, err := services.NewAccountService(models) + accountService, err := services.NewAccountService(models, metricsService) if err != nil { return handlerDeps{}, fmt.Errorf("instantiating account service: %w", err) } @@ -195,7 +203,7 @@ func initHandlerDeps(cfg Configs) (handlerDeps, error) { return handlerDeps{}, fmt.Errorf("instantiating tss transaction service: %w", err) } - tssStore, err := tssstore.NewStore(dbConnectionPool) + tssStore, err := tssstore.NewStore(dbConnectionPool, metricsService) if err != nil { return handlerDeps{}, fmt.Errorf("instantiating tss store: %w", err) } @@ -210,6 +218,7 @@ func initHandlerDeps(cfg Configs) (handlerDeps, error) { Store: tssStore, MaxBufferSize: cfg.RPCCallerServiceChannelBufferSize, MaxWorkers: cfg.RPCCallerServiceChannelMaxWorkers, + MetricsService: metricsService, }) errorJitterChannel := tsschannel.NewErrorJitterChannel(tsschannel.ErrorJitterChannelConfigs{ @@ -218,6 +227,7 @@ func initHandlerDeps(cfg Configs) (handlerDeps, error) { MaxWorkers: cfg.ErrorHandlerServiceJitterChannelMaxWorkers, MaxRetries: cfg.ErrorHandlerServiceJitterChannelMaxRetries, MinWaitBtwnRetriesMS: cfg.ErrorHandlerServiceJitterChannelMinWaitBtwnRetriesMS, + MetricsService: metricsService, }) errorNonJitterChannel := tsschannel.NewErrorNonJitterChannel(tsschannel.ErrorNonJitterChannelConfigs{ @@ -226,6 +236,7 @@ func initHandlerDeps(cfg Configs) (handlerDeps, error) { MaxWorkers: cfg.ErrorHandlerServiceJitterChannelMaxWorkers, MaxRetries: cfg.ErrorHandlerServiceJitterChannelMaxRetries, WaitBtwnRetriesMS: cfg.ErrorHandlerServiceJitterChannelMinWaitBtwnRetriesMS, + MetricsService: metricsService, }) httpClient = http.Client{Timeout: time.Duration(30 * time.Second)} @@ -238,6 +249,7 @@ func initHandlerDeps(cfg Configs) (handlerDeps, error) { MaxRetries: cfg.WebhookHandlerServiceChannelMaxRetries, MinWaitBtwnRetriesMS: cfg.WebhookHandlerServiceChannelMinWaitBtwnRetriesMS, NetworkPassphrase: cfg.NetworkPassphrase, + MetricsService: metricsService, }) router := tssrouter.NewRouter(tssrouter.RouterConfigs{ @@ -277,6 +289,7 @@ func initHandlerDeps(cfg Configs) (handlerDeps, error) { AccountService: accountService, AccountSponsorshipService: accountSponsorshipService, PaymentService: paymentService, + MetricsService: metricsService, AppTracker: cfg.AppTracker, NetworkPassphrase: cfg.NetworkPassphrase, // TSS @@ -316,9 +329,13 @@ func handler(deps handlerDeps) http.Handler { mux := supporthttp.NewAPIMux(log.DefaultLogger) mux.NotFound(httperror.ErrorHandler{Error: httperror.NotFound}.ServeHTTP) mux.MethodNotAllowed(httperror.ErrorHandler{Error: httperror.MethodNotAllowed}.ServeHTTP) + + // Add metrics middleware first to capture all requests + mux.Use(middleware.MetricsMiddleware(deps.MetricsService)) mux.Use(middleware.RecoverHandler(deps.AppTracker)) mux.Get("/health", health.PassHandler{}.ServeHTTP) + mux.Get("/api-metrics", promhttp.HandlerFor(deps.MetricsService.GetRegistry(), promhttp.HandlerOpts{}).ServeHTTP) // Authenticated routes mux.Group(func(r chi.Router) { @@ -363,6 +380,7 @@ func handler(deps handlerDeps) http.Handler { Store: deps.TSSStore, AppTracker: deps.AppTracker, NetworkPassphrase: deps.NetworkPassphrase, + MetricsService: deps.MetricsService, } r.Get("/transactions/{transactionhash}", handler.GetTransaction) diff --git a/internal/services/account_service.go b/internal/services/account_service.go index bc21e9b..8a3b4a4 100644 --- a/internal/services/account_service.go +++ b/internal/services/account_service.go @@ -7,6 +7,7 @@ import ( "errors" "github.com/stellar/wallet-backend/internal/data" + "github.com/stellar/wallet-backend/internal/metrics" ) type AccountService interface { @@ -19,16 +20,18 @@ type AccountService interface { var _ AccountService = (*accountService)(nil) type accountService struct { - models *data.Models + models *data.Models + metricsService *metrics.MetricsService } -func NewAccountService(models *data.Models) (*accountService, error) { +func NewAccountService(models *data.Models, metricsService *metrics.MetricsService) (*accountService, error) { if models == nil { return nil, errors.New("models cannot be nil") } return &accountService{ - models: models, + models: models, + metricsService: metricsService, }, nil } @@ -37,7 +40,7 @@ func (s *accountService) RegisterAccount(ctx context.Context, address string) er if err != nil { return fmt.Errorf("registering account %s: %w", address, err) } - + s.metricsService.IncActiveAccount() return nil } @@ -46,6 +49,6 @@ func (s *accountService) DeregisterAccount(ctx context.Context, address string) if err != nil { return fmt.Errorf("deregistering account %s: %w", address, err) } - + s.metricsService.DecActiveAccount() return nil } diff --git a/internal/services/account_service_test.go b/internal/services/account_service_test.go index 7cd3a5a..6aba584 100644 --- a/internal/services/account_service_test.go +++ b/internal/services/account_service_test.go @@ -9,6 +9,7 @@ import ( "github.com/stellar/wallet-backend/internal/data" "github.com/stellar/wallet-backend/internal/db" "github.com/stellar/wallet-backend/internal/db/dbtest" + "github.com/stellar/wallet-backend/internal/metrics" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -20,10 +21,13 @@ func TestAccountRegister(t *testing.T) { dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) require.NoError(t, err) defer dbConnectionPool.Close() + sqlxDB, err := dbConnectionPool.SqlxDB(context.Background()) + require.NoError(t, err) + metricsService := metrics.NewMetricsService(sqlxDB) - models, err := data.NewModels(dbConnectionPool) + models, err := data.NewModels(dbConnectionPool, metricsService) require.NoError(t, err) - accountService, err := NewAccountService(models) + accountService, err := NewAccountService(models, metricsService) require.NoError(t, err) ctx := context.Background() @@ -46,10 +50,13 @@ func TestAccountDeregister(t *testing.T) { dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) require.NoError(t, err) defer dbConnectionPool.Close() + sqlxDB, err := dbConnectionPool.SqlxDB(context.Background()) + require.NoError(t, err) + metricsService := metrics.NewMetricsService(sqlxDB) - models, err := data.NewModels(dbConnectionPool) + models, err := data.NewModels(dbConnectionPool, metricsService) require.NoError(t, err) - accountService, err := NewAccountService(models) + accountService, err := NewAccountService(models, metricsService) require.NoError(t, err) ctx := context.Background() diff --git a/internal/services/account_sponsorship_service_test.go b/internal/services/account_sponsorship_service_test.go index 12ea1f7..9bad9de 100644 --- a/internal/services/account_sponsorship_service_test.go +++ b/internal/services/account_sponsorship_service_test.go @@ -16,6 +16,7 @@ import ( "github.com/stellar/wallet-backend/internal/db" "github.com/stellar/wallet-backend/internal/db/dbtest" "github.com/stellar/wallet-backend/internal/entities" + "github.com/stellar/wallet-backend/internal/metrics" "github.com/stellar/wallet-backend/internal/signing" ) @@ -26,7 +27,11 @@ func TestAccountSponsorshipServiceSponsorAccountCreationTransaction(t *testing.T require.NoError(t, err) defer dbConnectionPool.Close() - models, err := data.NewModels(dbConnectionPool) + sqlxDB, err := dbConnectionPool.SqlxDB(context.Background()) + require.NoError(t, err) + metricsService := metrics.NewMetricsService(sqlxDB) + + models, err := data.NewModels(dbConnectionPool, metricsService) require.NoError(t, err) signatureClient := signing.SignatureClientMock{} @@ -303,7 +308,11 @@ func TestAccountSponsorshipServiceWrapTransaction(t *testing.T) { require.NoError(t, err) defer dbConnectionPool.Close() - models, err := data.NewModels(dbConnectionPool) + sqlxDB, err := dbConnectionPool.SqlxDB(context.Background()) + require.NoError(t, err) + metricsService := metrics.NewMetricsService(sqlxDB) + + models, err := data.NewModels(dbConnectionPool, metricsService) require.NoError(t, err) signatureClient := signing.SignatureClientMock{} diff --git a/internal/services/ingest.go b/internal/services/ingest.go index 0351260..698899e 100644 --- a/internal/services/ingest.go +++ b/internal/services/ingest.go @@ -14,6 +14,7 @@ import ( "github.com/stellar/wallet-backend/internal/data" "github.com/stellar/wallet-backend/internal/db" "github.com/stellar/wallet-backend/internal/entities" + "github.com/stellar/wallet-backend/internal/metrics" "github.com/stellar/wallet-backend/internal/tss" tssrouter "github.com/stellar/wallet-backend/internal/tss/router" tssstore "github.com/stellar/wallet-backend/internal/tss/store" @@ -21,7 +22,12 @@ import ( ) const ( - ingestHealthCheckMaxWaitTime = 90 * time.Second + ingestHealthCheckMaxWaitTime = 90 * time.Second + paymentPrometheusLabel = "payment" + tssPrometheusLabel = "tss" + pathPaymentStrictSendPrometheusLabel = "path_payment_strict_send" + pathPaymentStrictReceivePrometheusLabel = "path_payment_strict_receive" + totalIngestionPrometheusLabel = "total" ) type IngestService interface { @@ -37,6 +43,7 @@ type ingestService struct { rpcService RPCService tssRouter tssrouter.Router tssStore tssstore.Store + metricsService *metrics.MetricsService } func NewIngestService( @@ -46,6 +53,7 @@ func NewIngestService( rpcService RPCService, tssRouter tssrouter.Router, tssStore tssstore.Store, + metricsService *metrics.MetricsService, ) (*ingestService, error) { if models == nil { return nil, errors.New("models cannot be nil") @@ -65,6 +73,9 @@ func NewIngestService( if tssStore == nil { return nil, errors.New("tssStore cannot be nil") } + if metricsService == nil { + return nil, errors.New("metricsService cannot be nil") + } return &ingestService{ models: models, @@ -73,6 +84,7 @@ func NewIngestService( rpcService: rpcService, tssRouter: tssRouter, tssStore: tssStore, + metricsService: metricsService, }, nil } @@ -110,26 +122,33 @@ func (m *ingestService) Run(ctx context.Context, startLedger uint32, endLedger u } log.Ctx(ctx).Infof("ingesting ledger: %d", ingestLedger) + start := time.Now() ledgerTransactions, err := m.GetLedgerTransactions(int64(ingestLedger)) if err != nil { log.Error("getTransactions: %w", err) continue } ingestHeartbeatChannel <- true + startTime := time.Now() err = m.ingestPayments(ctx, ledgerTransactions) if err != nil { return fmt.Errorf("error ingesting payments: %w", err) } - + m.metricsService.ObserveIngestionDuration(paymentPrometheusLabel, time.Since(startTime).Seconds()) + + startTime = time.Now() err = m.processTSSTransactions(ctx, ledgerTransactions) if err != nil { return fmt.Errorf("error processing tss transactions: %w", err) } + m.metricsService.ObserveIngestionDuration(tssPrometheusLabel, time.Since(startTime).Seconds()) err = m.models.Payments.UpdateLatestLedgerSynced(ctx, m.ledgerCursorName, ingestLedger) if err != nil { return fmt.Errorf("error updating latest synced ledger: %w", err) } + m.metricsService.SetLatestLedgerIngested(float64(ingestLedger)) + m.metricsService.ObserveIngestionDuration(totalIngestionPrometheusLabel, time.Since(start).Seconds()) ingestLedger++ } @@ -163,6 +182,9 @@ func (m *ingestService) GetLedgerTransactions(ledger int64) ([]entities.Transact func (m *ingestService) ingestPayments(ctx context.Context, ledgerTransactions []entities.Transaction) error { return db.RunInTransaction(ctx, m.models.Payments.DB, nil, func(dbTx db.Transaction) error { + paymentOpsIngested := 0 + pathPaymentStrictSendOpsIngested := 0 + pathPaymentStrictReceiveOpsIngested := 0 for _, tx := range ledgerTransactions { if tx.Status != entities.SuccessStatus { continue @@ -199,10 +221,13 @@ func (m *ingestService) ingestPayments(ctx context.Context, ledgerTransactions [ switch op.Body.Type { case xdr.OperationTypePayment: + paymentOpsIngested++ fillPayment(&payment, op.Body) case xdr.OperationTypePathPaymentStrictSend: + pathPaymentStrictSendOpsIngested++ fillPathSend(&payment, op.Body, txResultXDR, opIdx) case xdr.OperationTypePathPaymentStrictReceive: + pathPaymentStrictReceiveOpsIngested++ fillPathReceive(&payment, op.Body, txResultXDR, opIdx) default: continue @@ -214,11 +239,17 @@ func (m *ingestService) ingestPayments(ctx context.Context, ledgerTransactions [ } } } + m.metricsService.SetNumPaymentOpsIngestedPerLedger(paymentPrometheusLabel, paymentOpsIngested) + m.metricsService.SetNumPaymentOpsIngestedPerLedger(pathPaymentStrictSendPrometheusLabel, pathPaymentStrictSendOpsIngested) + m.metricsService.SetNumPaymentOpsIngestedPerLedger(pathPaymentStrictReceivePrometheusLabel, pathPaymentStrictReceiveOpsIngested) return nil }) } func (m *ingestService) processTSSTransactions(ctx context.Context, ledgerTransactions []entities.Transaction) error { + // Initialize a map to track counts by status + statusCounts := make(map[string]float64) + for _, tx := range ledgerTransactions { if !tx.FeeBump { // because all transactions submitted by TSS are fee bump transactions @@ -269,6 +300,18 @@ func (m *ingestService) processTSSTransactions(ctx context.Context, ledgerTransa if err != nil { return fmt.Errorf("unable to route payload: %w", err) } + + // Calculate and record the transaction inclusion time + inclusionTime := time.Since(time.Unix(int64(tx.CreatedAt), 0)).Seconds() + m.metricsService.ObserveTSSTransactionInclusionTime(string(tx.Status), inclusionTime) + + // Increment the count for this status + statusCounts[string(tx.Status)]++ + } + + // Set the final counts for each status + for status, count := range statusCounts { + m.metricsService.SetNumTssTransactionsIngestedPerLedger(status, count) } return nil } diff --git a/internal/services/ingest_test.go b/internal/services/ingest_test.go index 33d2ebe..edc09dc 100644 --- a/internal/services/ingest_test.go +++ b/internal/services/ingest_test.go @@ -21,6 +21,7 @@ import ( "github.com/stellar/wallet-backend/internal/db" "github.com/stellar/wallet-backend/internal/db/dbtest" "github.com/stellar/wallet-backend/internal/entities" + "github.com/stellar/wallet-backend/internal/metrics" "github.com/stellar/wallet-backend/internal/tss" tssrouter "github.com/stellar/wallet-backend/internal/tss/router" tssstore "github.com/stellar/wallet-backend/internal/tss/store" @@ -33,12 +34,16 @@ func TestGetLedgerTransactions(t *testing.T) { dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) require.NoError(t, err) defer dbConnectionPool.Close() - models, _ := data.NewModels(dbConnectionPool) + sqlxDB, err := dbConnectionPool.SqlxDB(context.Background()) + require.NoError(t, err) + metricsService := metrics.NewMetricsService(sqlxDB) + models, err := data.NewModels(dbConnectionPool, metricsService) + require.NoError(t, err) mockAppTracker := apptracker.MockAppTracker{} mockRPCService := RPCServiceMock{} mockRouter := tssrouter.MockRouter{} - tssStore, _ := tssstore.NewStore(dbConnectionPool) - ingestService, _ := NewIngestService(models, "ingestionLedger", &mockAppTracker, &mockRPCService, &mockRouter, tssStore) + tssStore, _ := tssstore.NewStore(dbConnectionPool, metricsService) + ingestService, _ := NewIngestService(models, "ingestionLedger", &mockAppTracker, &mockRPCService, &mockRouter, tssStore, metricsService) t.Run("all_ledger_transactions_in_single_gettransactions_call", func(t *testing.T) { rpcGetTransactionsResult := entities.RPCGetTransactionsResult{ Cursor: "51", @@ -125,12 +130,16 @@ func TestProcessTSSTransactions(t *testing.T) { dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) require.NoError(t, err) defer dbConnectionPool.Close() - models, _ := data.NewModels(dbConnectionPool) + sqlxDB, err := dbConnectionPool.SqlxDB(context.Background()) + require.NoError(t, err) + metricsService := metrics.NewMetricsService(sqlxDB) + models, err := data.NewModels(dbConnectionPool, metricsService) + require.NoError(t, err) mockAppTracker := apptracker.MockAppTracker{} mockRPCService := RPCServiceMock{} mockRouter := tssrouter.MockRouter{} - tssStore, _ := tssstore.NewStore(dbConnectionPool) - ingestService, _ := NewIngestService(models, "ingestionLedger", &mockAppTracker, &mockRPCService, &mockRouter, tssStore) + tssStore, _ := tssstore.NewStore(dbConnectionPool, metricsService) + ingestService, _ := NewIngestService(models, "ingestionLedger", &mockAppTracker, &mockRPCService, &mockRouter, tssStore, metricsService) t.Run("routes_to_tss_router", func(t *testing.T) { @@ -175,13 +184,17 @@ func TestIngestPayments(t *testing.T) { dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) require.NoError(t, err) defer dbConnectionPool.Close() + sqlxDB, err := dbConnectionPool.SqlxDB(context.Background()) + require.NoError(t, err) + metricsService := metrics.NewMetricsService(sqlxDB) - models, _ := data.NewModels(dbConnectionPool) + models, err := data.NewModels(dbConnectionPool, metricsService) + require.NoError(t, err) mockAppTracker := apptracker.MockAppTracker{} mockRPCService := RPCServiceMock{} mockRouter := tssrouter.MockRouter{} - tssStore, _ := tssstore.NewStore(dbConnectionPool) - ingestService, _ := NewIngestService(models, "ingestionLedger", &mockAppTracker, &mockRPCService, &mockRouter, tssStore) + tssStore, _ := tssstore.NewStore(dbConnectionPool, metricsService) + ingestService, _ := NewIngestService(models, "ingestionLedger", &mockAppTracker, &mockRPCService, &mockRouter, tssStore, metricsService) srcAccount := keypair.MustRandom().Address() destAccount := keypair.MustRandom().Address() usdIssuer := keypair.MustRandom().Address() @@ -355,15 +368,19 @@ func TestIngest_LatestSyncedLedgerBehindRPC(t *testing.T) { dbt.Close() }() - models, _ := data.NewModels(dbConnectionPool) + sqlxDB, err := dbConnectionPool.SqlxDB(context.Background()) + require.NoError(t, err) + metricsService := metrics.NewMetricsService(sqlxDB) + models, err := data.NewModels(dbConnectionPool, metricsService) + require.NoError(t, err) mockAppTracker := apptracker.MockAppTracker{} mockRPCService := RPCServiceMock{} mockRouter := tssrouter.MockRouter{} - tssStore, err := tssstore.NewStore(dbConnectionPool) + tssStore, err := tssstore.NewStore(dbConnectionPool, metricsService) require.NoError(t, err) - ingestService, err := NewIngestService(models, "ingestionLedger", &mockAppTracker, &mockRPCService, &mockRouter, tssStore) + ingestService, err := NewIngestService(models, "ingestionLedger", &mockAppTracker, &mockRPCService, &mockRouter, tssStore, metricsService) require.NoError(t, err) srcAccount := keypair.MustRandom().Address() @@ -439,15 +456,19 @@ func TestIngest_LatestSyncedLedgerAheadOfRPC(t *testing.T) { log.DefaultLogger.SetOutput(os.Stderr) }() - models, _ := data.NewModels(dbConnectionPool) + sqlxDB, err := dbConnectionPool.SqlxDB(context.Background()) + require.NoError(t, err) + metricsService := metrics.NewMetricsService(sqlxDB) + models, err := data.NewModels(dbConnectionPool, metricsService) + require.NoError(t, err) mockAppTracker := apptracker.MockAppTracker{} mockRPCService := RPCServiceMock{} mockRouter := tssrouter.MockRouter{} - tssStore, err := tssstore.NewStore(dbConnectionPool) + tssStore, err := tssstore.NewStore(dbConnectionPool, metricsService) require.NoError(t, err) - ingestService, err := NewIngestService(models, "ingestionLedger", &mockAppTracker, &mockRPCService, &mockRouter, tssStore) + ingestService, err := NewIngestService(models, "ingestionLedger", &mockAppTracker, &mockRPCService, &mockRouter, tssStore, metricsService) require.NoError(t, err) // Create and set up the heartbeat channel diff --git a/internal/services/payment_service_test.go b/internal/services/payment_service_test.go index 6eeaf41..63fff76 100644 --- a/internal/services/payment_service_test.go +++ b/internal/services/payment_service_test.go @@ -11,6 +11,7 @@ import ( "github.com/stellar/wallet-backend/internal/db" "github.com/stellar/wallet-backend/internal/db/dbtest" "github.com/stellar/wallet-backend/internal/entities" + "github.com/stellar/wallet-backend/internal/metrics" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -23,7 +24,11 @@ func TestPaymentServiceGetPaymentsPaginated(t *testing.T) { require.NoError(t, err) defer dbConnectionPool.Close() - models, err := data.NewModels(dbConnectionPool) + sqlxDB, err := dbConnectionPool.SqlxDB(context.Background()) + require.NoError(t, err) + metricsService := metrics.NewMetricsService(sqlxDB) + + models, err := data.NewModels(dbConnectionPool, metricsService) require.NoError(t, err) service, err := NewPaymentService(models, "http://testing.com") require.NoError(t, err) diff --git a/internal/services/rpc_service.go b/internal/services/rpc_service.go index 160cf6e..5a2a352 100644 --- a/internal/services/rpc_service.go +++ b/internal/services/rpc_service.go @@ -11,6 +11,7 @@ import ( "github.com/stellar/go/support/log" "github.com/stellar/wallet-backend/internal/entities" + "github.com/stellar/wallet-backend/internal/metrics" "github.com/stellar/wallet-backend/internal/utils" ) @@ -35,25 +36,30 @@ type rpcService struct { rpcURL string httpClient utils.HTTPClient heartbeatChannel chan entities.RPCGetHealthResult + metricsService *metrics.MetricsService } var PageLimit = 200 var _ RPCService = (*rpcService)(nil) -func NewRPCService(rpcURL string, httpClient utils.HTTPClient) (*rpcService, error) { +func NewRPCService(rpcURL string, httpClient utils.HTTPClient, metricsService *metrics.MetricsService) (*rpcService, error) { if rpcURL == "" { return nil, errors.New("rpcURL cannot be nil") } if httpClient == nil { return nil, errors.New("httpClient cannot be nil") } + if metricsService == nil { + return nil, errors.New("metricsService cannot be nil") + } heartbeatChannel := make(chan entities.RPCGetHealthResult, 1) return &rpcService{ rpcURL: rpcURL, httpClient: httpClient, heartbeatChannel: heartbeatChannel, + metricsService: metricsService, }, nil } @@ -182,20 +188,30 @@ func (r *rpcService) TrackRPCServiceHealth(ctx context.Context) { return case <-warningTicker.C: log.Warn(fmt.Sprintf("rpc service unhealthy for over %s", rpcHealthCheckMaxWaitTime)) + r.metricsService.SetRPCServiceHealth(false) warningTicker.Reset(rpcHealthCheckMaxWaitTime) case <-healthCheckTicker.C: result, err := r.GetHealth() if err != nil { log.Warnf("rpc health check failed: %v", err) + r.metricsService.SetRPCServiceHealth(false) continue } r.heartbeatChannel <- result + r.metricsService.SetRPCServiceHealth(true) + r.metricsService.SetRPCLatestLedger(int64(result.LatestLedger)) warningTicker.Reset(rpcHealthCheckMaxWaitTime) } } } func (r *rpcService) sendRPCRequest(method string, params entities.RPCParams) (json.RawMessage, error) { + startTime := time.Now() + r.metricsService.IncRPCRequests(method) + defer func() { + duration := time.Since(startTime).Seconds() + r.metricsService.ObserveRPCRequestDuration(method, duration) + }() payload := map[string]interface{}{ "jsonrpc": "2.0", @@ -210,28 +226,34 @@ func (r *rpcService) sendRPCRequest(method string, params entities.RPCParams) (j jsonData, err := json.Marshal(payload) if err != nil { + r.metricsService.IncRPCEndpointFailure(method) return nil, fmt.Errorf("marshaling payload") } resp, err := r.httpClient.Post(r.rpcURL, "application/json", bytes.NewBuffer(jsonData)) if err != nil { + r.metricsService.IncRPCEndpointFailure(method) return nil, fmt.Errorf("sending POST request to RPC: %w", err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { + r.metricsService.IncRPCEndpointFailure(method) return nil, fmt.Errorf("unmarshaling RPC response: %w", err) } var res entities.RPCResponse err = json.Unmarshal(body, &res) if err != nil { + r.metricsService.IncRPCEndpointFailure(method) return nil, fmt.Errorf("parsing RPC response JSON: %w", err) } if res.Result == nil { + r.metricsService.IncRPCEndpointFailure(method) return nil, fmt.Errorf("response %s missing result field", string(body)) } + r.metricsService.IncRPCEndpointSuccess(method) return res.Result, nil } diff --git a/internal/services/rpc_service_test.go b/internal/services/rpc_service_test.go index ff9d3ef..94d6e8e 100644 --- a/internal/services/rpc_service_test.go +++ b/internal/services/rpc_service_test.go @@ -17,7 +17,10 @@ import ( "github.com/stretchr/testify/require" "github.com/stellar/go/support/log" + "github.com/stellar/wallet-backend/internal/db" + "github.com/stellar/wallet-backend/internal/db/dbtest" "github.com/stellar/wallet-backend/internal/entities" + "github.com/stellar/wallet-backend/internal/metrics" "github.com/stellar/wallet-backend/internal/utils" ) @@ -32,9 +35,18 @@ func (e *errorReader) Close() error { } func TestSendRPCRequest(t *testing.T) { + dbt := dbtest.Open(t) + defer dbt.Close() + + dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) + require.NoError(t, err) + defer dbConnectionPool.Close() + sqlxDB, err := dbConnectionPool.SqlxDB(context.Background()) + require.NoError(t, err) + metricsService := metrics.NewMetricsService(sqlxDB) mockHTTPClient := utils.MockHTTPClient{} rpcURL := "http://api.vibrantapp.com/soroban/rpc" - rpcService, _ := NewRPCService(rpcURL, &mockHTTPClient) + rpcService, _ := NewRPCService(rpcURL, &mockHTTPClient, metricsService) t.Run("successful", func(t *testing.T) { httpResponse := http.Response{ @@ -127,9 +139,18 @@ func TestSendRPCRequest(t *testing.T) { } func TestSendTransaction(t *testing.T) { + dbt := dbtest.Open(t) + defer dbt.Close() + + dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) + require.NoError(t, err) + defer dbConnectionPool.Close() + sqlxDB, err := dbConnectionPool.SqlxDB(context.Background()) + require.NoError(t, err) + metricsService := metrics.NewMetricsService(sqlxDB) mockHTTPClient := utils.MockHTTPClient{} rpcURL := "http://api.vibrantapp.com/soroban/rpc" - rpcService, _ := NewRPCService(rpcURL, &mockHTTPClient) + rpcService, _ := NewRPCService(rpcURL, &mockHTTPClient, metricsService) t.Run("successful", func(t *testing.T) { transactionXDR := "AAAAAgAAAABYJgX6SmA2tGVDv3GXfOWbkeL869ahE0e5DG9HnXQw/QAAAGQAAjpnAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAQAAAACxaDFEbbssZfrbRgFxTYIygITSQxsUpDmneN2gAZBEFQAAAAAAAAAABfXhAAAAAAAAAAAA" @@ -188,9 +209,18 @@ func TestSendTransaction(t *testing.T) { } func TestGetTransaction(t *testing.T) { + dbt := dbtest.Open(t) + defer dbt.Close() + + dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) + require.NoError(t, err) + defer dbConnectionPool.Close() + sqlxDB, err := dbConnectionPool.SqlxDB(context.Background()) + require.NoError(t, err) + metricsService := metrics.NewMetricsService(sqlxDB) mockHTTPClient := utils.MockHTTPClient{} rpcURL := "http://api.vibrantapp.com/soroban/rpc" - rpcService, _ := NewRPCService(rpcURL, &mockHTTPClient) + rpcService, _ := NewRPCService(rpcURL, &mockHTTPClient, metricsService) t.Run("successful", func(t *testing.T) { transactionHash := "6bc97bddc21811c626839baf4ab574f4f9f7ddbebb44d286ae504396d4e752da" @@ -264,9 +294,18 @@ func TestGetTransaction(t *testing.T) { } func TestGetTransactions(t *testing.T) { + dbt := dbtest.Open(t) + defer dbt.Close() + + dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) + require.NoError(t, err) + defer dbConnectionPool.Close() + sqlxDB, err := dbConnectionPool.SqlxDB(context.Background()) + require.NoError(t, err) + metricsService := metrics.NewMetricsService(sqlxDB) mockHTTPClient := utils.MockHTTPClient{} rpcURL := "http://api.vibrantapp.com/soroban/rpc" - rpcService, _ := NewRPCService(rpcURL, &mockHTTPClient) + rpcService, _ := NewRPCService(rpcURL, &mockHTTPClient, metricsService) t.Run("rpc_request_fails", func(t *testing.T) { mockHTTPClient. @@ -327,9 +366,18 @@ func TestGetTransactions(t *testing.T) { } func TestSendGetHealth(t *testing.T) { + dbt := dbtest.Open(t) + defer dbt.Close() + + dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) + require.NoError(t, err) + defer dbConnectionPool.Close() + sqlxDB, err := dbConnectionPool.SqlxDB(context.Background()) + require.NoError(t, err) + metricsService := metrics.NewMetricsService(sqlxDB) mockHTTPClient := utils.MockHTTPClient{} rpcURL := "http://api.vibrantapp.com/soroban/rpc" - rpcService, _ := NewRPCService(rpcURL, &mockHTTPClient) + rpcService, _ := NewRPCService(rpcURL, &mockHTTPClient, metricsService) t.Run("successful", func(t *testing.T) { payload := map[string]interface{}{ @@ -374,12 +422,19 @@ func TestSendGetHealth(t *testing.T) { } func TestTrackRPCServiceHealth_HealthyService(t *testing.T) { - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() + dbt := dbtest.Open(t) + defer dbt.Close() + + dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) + require.NoError(t, err) + defer dbConnectionPool.Close() + sqlxDB, err := dbConnectionPool.SqlxDB(context.Background()) + require.NoError(t, err) + metricsService := metrics.NewMetricsService(sqlxDB) mockHTTPClient := &utils.MockHTTPClient{} rpcURL := "http://test-url-track-rpc-service-health" - rpcService, err := NewRPCService(rpcURL, mockHTTPClient) + rpcService, err := NewRPCService(rpcURL, mockHTTPClient, metricsService) require.NoError(t, err) healthResult := entities.RPCGetHealthResult{ @@ -390,6 +445,8 @@ func TestTrackRPCServiceHealth_HealthyService(t *testing.T) { } // Mock the HTTP response for GetHealth + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() mockResponse := &http.Response{ Body: io.NopCloser(bytes.NewBuffer([]byte(`{ "jsonrpc": "2.0", @@ -419,14 +476,24 @@ func TestTrackRPCServiceHealth_HealthyService(t *testing.T) { } func TestTrackRPCServiceHealth_UnhealthyService(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 70*time.Second) + defer cancel() + + dbt := dbtest.Open(t) + defer dbt.Close() + + dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) + require.NoError(t, err) + defer dbConnectionPool.Close() + sqlxDB, err := dbConnectionPool.SqlxDB(ctx) + require.NoError(t, err) + metricsService := metrics.NewMetricsService(sqlxDB) getLogs := log.DefaultLogger.StartTest(log.WarnLevel) mockHTTPClient := &utils.MockHTTPClient{} rpcURL := "http://test-url-track-rpc-service-health" - rpcService, err := NewRPCService(rpcURL, mockHTTPClient) + rpcService, err := NewRPCService(rpcURL, mockHTTPClient, metricsService) require.NoError(t, err) - ctx, cancel := context.WithTimeout(context.Background(), 70*time.Second) - defer cancel() // Mock error response for GetHealth with a valid http.Response mockResponse := &http.Response{ @@ -457,12 +524,22 @@ func TestTrackRPCServiceHealth_UnhealthyService(t *testing.T) { } func TestTrackRPCService_ContextCancelled(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + + dbt := dbtest.Open(t) + defer dbt.Close() + + dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) + require.NoError(t, err) + defer dbConnectionPool.Close() + sqlxDB, err := dbConnectionPool.SqlxDB(ctx) + require.NoError(t, err) + metricsService := metrics.NewMetricsService(sqlxDB) mockHTTPClient := &utils.MockHTTPClient{} rpcURL := "http://test-url-track-rpc-service-health" - rpcService, err := NewRPCService(rpcURL, mockHTTPClient) + rpcService, err := NewRPCService(rpcURL, mockHTTPClient, metricsService) require.NoError(t, err) - ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) - defer cancel() rpcService.TrackRPCServiceHealth(ctx) diff --git a/internal/tss/channels/error_jitter_channel.go b/internal/tss/channels/error_jitter_channel.go index 41a2795..f250e1f 100644 --- a/internal/tss/channels/error_jitter_channel.go +++ b/internal/tss/channels/error_jitter_channel.go @@ -7,6 +7,7 @@ import ( "github.com/alitto/pond" "github.com/stellar/go/support/log" + "github.com/stellar/wallet-backend/internal/metrics" "github.com/stellar/wallet-backend/internal/tss" "github.com/stellar/wallet-backend/internal/tss/router" "github.com/stellar/wallet-backend/internal/tss/services" @@ -20,6 +21,7 @@ type ErrorJitterChannelConfigs struct { MaxWorkers int MaxRetries int MinWaitBtwnRetriesMS int + MetricsService *metrics.MetricsService } type errorJitterPool struct { @@ -28,6 +30,7 @@ type errorJitterPool struct { Router router.Router MaxRetries int MinWaitBtwnRetriesMS int + MetricsService *metrics.MetricsService } var ErrorJitterChannelName = "ErrorJitterChannel" @@ -42,13 +45,18 @@ func jitter(dur time.Duration) time.Duration { func NewErrorJitterChannel(cfg ErrorJitterChannelConfigs) *errorJitterPool { pool := pond.New(cfg.MaxBufferSize, cfg.MaxWorkers, pond.Strategy(pond.Balanced())) - return &errorJitterPool{ + jitterPool := &errorJitterPool{ Pool: pool, TxManager: cfg.TxManager, Router: cfg.Router, MaxRetries: cfg.MaxRetries, MinWaitBtwnRetriesMS: cfg.MinWaitBtwnRetriesMS, + MetricsService: cfg.MetricsService, } + if cfg.MetricsService != nil { + cfg.MetricsService.RegisterPoolMetrics(ErrorJitterChannelName, pool) + } + return jitterPool } func (p *errorJitterPool) Send(payload tss.Payload) { @@ -63,28 +71,41 @@ func (p *errorJitterPool) Receive(payload tss.Payload) { for i = 0; i < p.MaxRetries; i++ { currentBackoff := p.MinWaitBtwnRetriesMS * (1 << i) time.Sleep(jitter(time.Duration(currentBackoff)) * time.Millisecond) + + oldStatus := payload.RpcSubmitTxResponse.Status.Status() rpcSendResp, err := p.TxManager.BuildAndSubmitTransaction(ctx, ErrorJitterChannelName, payload) if err != nil { - log.Errorf("%s: unable to sign and submit transaction: %v", ErrorJitterChannelName, err) + log.Errorf("%s: unable to sign and submit transaction: %e", ErrorJitterChannelName, err) return } + payload.RpcSubmitTxResponse = rpcSendResp + p.MetricsService.RecordTSSTransactionStatusTransition(ErrorJitterChannelName, oldStatus, rpcSendResp.Status.Status()) + if !slices.Contains(tss.JitterErrorCodes, rpcSendResp.Code.TxResultCode) { - err = p.Router.Route(payload) + err := p.Router.Route(payload) if err != nil { - log.Errorf("%s: unable to route payload: %v", ErrorJitterChannelName, err) - + log.Errorf("%s: unable to route payload: %e", ErrorJitterChannelName, err) return } return } } - // Retry limit reached, route the payload to the router so it can re-route it to this pool and keep re-trying log.Infof("%s: max retry limit reached", ErrorJitterChannelName) + + if p.MetricsService != nil && payload.RpcSubmitTxResponse.Code.TxResultCode != 0 { + p.MetricsService.RecordTSSTransactionStatusTransition( + ErrorJitterChannelName, + payload.RpcSubmitTxResponse.Code.TxResultCode.String(), + "max_retries_reached", + ) + } + err := p.Router.Route(payload) if err != nil { - log.Errorf("%s: unable to route payload: %v", ErrorJitterChannelName, err) + log.Errorf("%s: unable to route payload: %e", ErrorJitterChannelName, err) + return } } diff --git a/internal/tss/channels/error_jitter_channel_test.go b/internal/tss/channels/error_jitter_channel_test.go index 8dd746d..b50adca 100644 --- a/internal/tss/channels/error_jitter_channel_test.go +++ b/internal/tss/channels/error_jitter_channel_test.go @@ -8,6 +8,7 @@ import ( "github.com/stellar/wallet-backend/internal/db" "github.com/stellar/wallet-backend/internal/db/dbtest" "github.com/stellar/wallet-backend/internal/entities" + "github.com/stellar/wallet-backend/internal/metrics" "github.com/stellar/wallet-backend/internal/tss" "github.com/stellar/wallet-backend/internal/tss/router" "github.com/stellar/wallet-backend/internal/tss/services" @@ -21,6 +22,8 @@ func TestJitterSend(t *testing.T) { dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) require.NoError(t, err) defer dbConnectionPool.Close() + sqlxDB, err := dbConnectionPool.SqlxDB(context.Background()) + require.NoError(t, err) txManagerMock := services.TransactionManagerMock{} routerMock := router.MockRouter{} cfg := ErrorJitterChannelConfigs{ @@ -30,6 +33,7 @@ func TestJitterSend(t *testing.T) { MaxWorkers: 1, MaxRetries: 3, MinWaitBtwnRetriesMS: 10, + MetricsService: metrics.NewMetricsService(sqlxDB), } channel := NewErrorJitterChannel(cfg) @@ -67,6 +71,8 @@ func TestJitterReceive(t *testing.T) { dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) require.NoError(t, err) defer dbConnectionPool.Close() + sqlxDB, err := dbConnectionPool.SqlxDB(context.Background()) + require.NoError(t, err) txManagerMock := services.TransactionManagerMock{} routerMock := router.MockRouter{} cfg := ErrorJitterChannelConfigs{ @@ -76,6 +82,7 @@ func TestJitterReceive(t *testing.T) { MaxWorkers: 1, MaxRetries: 3, MinWaitBtwnRetriesMS: 10, + MetricsService: metrics.NewMetricsService(sqlxDB), } channel := NewErrorJitterChannel(cfg) diff --git a/internal/tss/channels/error_non_jitter_channel.go b/internal/tss/channels/error_non_jitter_channel.go index 1e23b27..471289f 100644 --- a/internal/tss/channels/error_non_jitter_channel.go +++ b/internal/tss/channels/error_non_jitter_channel.go @@ -7,6 +7,7 @@ import ( "github.com/alitto/pond" "github.com/stellar/go/support/log" + "github.com/stellar/wallet-backend/internal/metrics" "github.com/stellar/wallet-backend/internal/tss" "github.com/stellar/wallet-backend/internal/tss/router" "github.com/stellar/wallet-backend/internal/tss/services" @@ -20,6 +21,7 @@ type ErrorNonJitterChannelConfigs struct { MaxWorkers int MaxRetries int WaitBtwnRetriesMS int + MetricsService *metrics.MetricsService } type errorNonJitterPool struct { @@ -29,6 +31,7 @@ type errorNonJitterPool struct { Router router.Router MaxRetries int WaitBtwnRetriesMS int + MetricsService *metrics.MetricsService } var ErrorNonJitterChannelName = "ErrorNonJitterChannel" @@ -37,13 +40,18 @@ var _ tss.Channel = (*errorNonJitterPool)(nil) func NewErrorNonJitterChannel(cfg ErrorNonJitterChannelConfigs) *errorNonJitterPool { pool := pond.New(cfg.MaxBufferSize, cfg.MaxWorkers, pond.Strategy(pond.Balanced())) - return &errorNonJitterPool{ + nonJitterPool := &errorNonJitterPool{ Pool: pool, TxManager: cfg.TxManager, Router: cfg.Router, MaxRetries: cfg.MaxRetries, WaitBtwnRetriesMS: cfg.WaitBtwnRetriesMS, + MetricsService: cfg.MetricsService, } + if cfg.MetricsService != nil { + cfg.MetricsService.RegisterPoolMetrics(ErrorNonJitterChannelName, pool) + } + return nonJitterPool } func (p *errorNonJitterPool) Send(payload tss.Payload) { @@ -57,12 +65,17 @@ func (p *errorNonJitterPool) Receive(payload tss.Payload) { var i int for i = 0; i < p.MaxRetries; i++ { time.Sleep(time.Duration(p.WaitBtwnRetriesMS) * time.Millisecond) + + oldStatus := payload.RpcSubmitTxResponse.Status.Status() rpcSendResp, err := p.TxManager.BuildAndSubmitTransaction(ctx, ErrorNonJitterChannelName, payload) if err != nil { log.Errorf("%s: unable to sign and submit transaction: %v", ErrorNonJitterChannelName, err) return } + payload.RpcSubmitTxResponse = rpcSendResp + p.MetricsService.RecordTSSTransactionStatusTransition(ErrorNonJitterChannelName, oldStatus, rpcSendResp.Status.Status()) + if !slices.Contains(tss.NonJitterErrorCodes, rpcSendResp.Code.TxResultCode) { err := p.Router.Route(payload) if err != nil { @@ -74,6 +87,15 @@ func (p *errorNonJitterPool) Receive(payload tss.Payload) { } // Retry limit reached, route the payload to the router so it can re-route it to this pool and keep re-trying log.Infof("%s: max retry limit reached", ErrorNonJitterChannelName) + + if p.MetricsService != nil && payload.RpcSubmitTxResponse.Code.TxResultCode != 0 { + p.MetricsService.RecordTSSTransactionStatusTransition( + ErrorNonJitterChannelName, + payload.RpcSubmitTxResponse.Code.TxResultCode.String(), + "max_retries_reached", + ) + } + err := p.Router.Route(payload) if err != nil { log.Errorf("%s: unable to route payload: %v", ErrorNonJitterChannelName, err) diff --git a/internal/tss/channels/rpc_caller_channel.go b/internal/tss/channels/rpc_caller_channel.go index d00af29..abacf4f 100644 --- a/internal/tss/channels/rpc_caller_channel.go +++ b/internal/tss/channels/rpc_caller_channel.go @@ -6,6 +6,7 @@ import ( "github.com/alitto/pond" "github.com/stellar/go/support/log" + "github.com/stellar/wallet-backend/internal/metrics" "github.com/stellar/wallet-backend/internal/tss" "github.com/stellar/wallet-backend/internal/tss/router" "github.com/stellar/wallet-backend/internal/tss/services" @@ -13,18 +14,20 @@ import ( ) type RPCCallerChannelConfigs struct { - TxManager services.TransactionManager - Router router.Router - Store store.Store - MaxBufferSize int - MaxWorkers int + TxManager services.TransactionManager + Router router.Router + Store store.Store + MaxBufferSize int + MaxWorkers int + MetricsService *metrics.MetricsService } type rpcCallerPool struct { - Pool *pond.WorkerPool - TxManager services.TransactionManager - Router router.Router - Store store.Store + Pool *pond.WorkerPool + TxManager services.TransactionManager + Router router.Router + Store store.Store + MetricsService *metrics.MetricsService } var RPCCallerChannelName = "RPCCallerChannel" @@ -33,13 +36,17 @@ var _ tss.Channel = (*rpcCallerPool)(nil) func NewRPCCallerChannel(cfg RPCCallerChannelConfigs) *rpcCallerPool { pool := pond.New(cfg.MaxBufferSize, cfg.MaxWorkers, pond.Strategy(pond.Balanced())) - return &rpcCallerPool{ - Pool: pool, - TxManager: cfg.TxManager, - Store: cfg.Store, - Router: cfg.Router, + rpcPool := &rpcCallerPool{ + Pool: pool, + TxManager: cfg.TxManager, + Store: cfg.Store, + Router: cfg.Router, + MetricsService: cfg.MetricsService, } - + if cfg.MetricsService != nil { + cfg.MetricsService.RegisterPoolMetrics(RPCCallerChannelName, pool) + } + return rpcPool } func (p *rpcCallerPool) Send(payload tss.Payload) { @@ -49,7 +56,6 @@ func (p *rpcCallerPool) Send(payload tss.Payload) { } func (p *rpcCallerPool) Receive(payload tss.Payload) { - ctx := context.Background() // Create a new transaction record in the transactions table. err := p.Store.UpsertTransaction(ctx, payload.WebhookURL, payload.TransactionHash, payload.TransactionXDR, tss.RPCTXStatus{OtherStatus: tss.NewStatus}) @@ -58,13 +64,17 @@ func (p *rpcCallerPool) Receive(payload tss.Payload) { log.Errorf("%s: unable to upsert transaction into transactions table: %e", RPCCallerChannelName, err) return } + rpcSendResp, err := p.TxManager.BuildAndSubmitTransaction(ctx, RPCCallerChannelName, payload) if err != nil { log.Errorf("%s: unable to sign and submit transaction: %e", RPCCallerChannelName, err) return } + payload.RpcSubmitTxResponse = rpcSendResp + p.MetricsService.RecordTSSTransactionStatusTransition(RPCCallerChannelName, "", rpcSendResp.Status.Status()) + err = p.Router.Route(payload) if err != nil { log.Errorf("%s: unable to route payload: %e", RPCCallerChannelName, err) diff --git a/internal/tss/channels/rpc_caller_channel_test.go b/internal/tss/channels/rpc_caller_channel_test.go index 04bcc4f..2c63f21 100644 --- a/internal/tss/channels/rpc_caller_channel_test.go +++ b/internal/tss/channels/rpc_caller_channel_test.go @@ -8,6 +8,7 @@ import ( "github.com/stellar/wallet-backend/internal/db" "github.com/stellar/wallet-backend/internal/db/dbtest" "github.com/stellar/wallet-backend/internal/entities" + "github.com/stellar/wallet-backend/internal/metrics" "github.com/stellar/wallet-backend/internal/tss" "github.com/stellar/wallet-backend/internal/tss/router" "github.com/stellar/wallet-backend/internal/tss/services" @@ -21,7 +22,10 @@ func TestSend(t *testing.T) { dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) require.NoError(t, err) defer dbConnectionPool.Close() - store, _ := store.NewStore(dbConnectionPool) + sqlxDB, err := dbConnectionPool.SqlxDB(context.Background()) + require.NoError(t, err) + metricsService := metrics.NewMetricsService(sqlxDB) + store, _ := store.NewStore(dbConnectionPool, metricsService) txManagerMock := services.TransactionManagerMock{} routerMock := router.MockRouter{} cfgs := RPCCallerChannelConfigs{ @@ -64,7 +68,10 @@ func TestReceivee(t *testing.T) { dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) require.NoError(t, err) defer dbConnectionPool.Close() - store, _ := store.NewStore(dbConnectionPool) + sqlxDB, err := dbConnectionPool.SqlxDB(context.Background()) + require.NoError(t, err) + metricsService := metrics.NewMetricsService(sqlxDB) + store, _ := store.NewStore(dbConnectionPool, metricsService) txManagerMock := services.TransactionManagerMock{} routerMock := router.MockRouter{} cfgs := RPCCallerChannelConfigs{ diff --git a/internal/tss/channels/webhook_channel.go b/internal/tss/channels/webhook_channel.go index b1f2bf8..f36e9fb 100644 --- a/internal/tss/channels/webhook_channel.go +++ b/internal/tss/channels/webhook_channel.go @@ -12,6 +12,7 @@ import ( "github.com/stellar/go/support/log" "github.com/stellar/go/txnbuild" channelAccountStore "github.com/stellar/wallet-backend/internal/signing/store" + "github.com/stellar/wallet-backend/internal/metrics" "github.com/stellar/wallet-backend/internal/tss" "github.com/stellar/wallet-backend/internal/tss/store" tssutils "github.com/stellar/wallet-backend/internal/tss/utils" @@ -27,6 +28,7 @@ type WebhookChannelConfigs struct { NetworkPassphrase string MaxBufferSize int MaxWorkers int + MetricsService *metrics.MetricsService } type webhookPool struct { @@ -37,6 +39,7 @@ type webhookPool struct { MaxRetries int MinWaitBtwnRetriesMS int NetworkPassphrase string + MetricsService *metrics.MetricsService } var WebhookChannelName = "WebhookChannel" @@ -45,7 +48,7 @@ var _ tss.Channel = (*webhookPool)(nil) func NewWebhookChannel(cfg WebhookChannelConfigs) *webhookPool { pool := pond.New(cfg.MaxBufferSize, cfg.MaxWorkers, pond.Strategy(pond.Balanced())) - return &webhookPool{ + webhookPool := &webhookPool{ Pool: pool, Store: cfg.Store, ChannelAccountStore: cfg.ChannelAccountStore, @@ -53,8 +56,12 @@ func NewWebhookChannel(cfg WebhookChannelConfigs) *webhookPool { MaxRetries: cfg.MaxRetries, MinWaitBtwnRetriesMS: cfg.MinWaitBtwnRetriesMS, NetworkPassphrase: cfg.NetworkPassphrase, + MetricsService: cfg.MetricsService, } - + if cfg.MetricsService != nil { + cfg.MetricsService.RegisterPoolMetrics(WebhookChannelName, pool) + } + return webhookPool } func (p *webhookPool) Send(payload tss.Payload) { diff --git a/internal/tss/channels/webhook_channel_test.go b/internal/tss/channels/webhook_channel_test.go index f8eabae..c47ad6a 100644 --- a/internal/tss/channels/webhook_channel_test.go +++ b/internal/tss/channels/webhook_channel_test.go @@ -15,6 +15,7 @@ import ( "github.com/stellar/wallet-backend/internal/db" "github.com/stellar/wallet-backend/internal/db/dbtest" channelAccountStore "github.com/stellar/wallet-backend/internal/signing/store" + "github.com/stellar/wallet-backend/internal/metrics" "github.com/stellar/wallet-backend/internal/tss" "github.com/stellar/wallet-backend/internal/tss/store" tssutils "github.com/stellar/wallet-backend/internal/tss/utils" @@ -29,7 +30,10 @@ func TestWebhookHandlerServiceChannel(t *testing.T) { dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) require.NoError(t, err) defer dbConnectionPool.Close() - store, _ := store.NewStore(dbConnectionPool) + sqlxDB, err := dbConnectionPool.SqlxDB(context.Background()) + require.NoError(t, err) + metricsService := metrics.NewMetricsService(sqlxDB) + store, _ := store.NewStore(dbConnectionPool, metricsService) channelAccountStore := channelAccountStore.ChannelAccountStoreMock{} mockHTTPClient := utils.MockHTTPClient{} cfg := WebhookChannelConfigs{ @@ -41,6 +45,7 @@ func TestWebhookHandlerServiceChannel(t *testing.T) { MaxRetries: 3, MinWaitBtwnRetriesMS: 5, NetworkPassphrase: "networkpassphrase", + MetricsService: metricsService, } channel := NewWebhookChannel(cfg) @@ -86,7 +91,10 @@ func TestUnlockChannelAccount(t *testing.T) { dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) require.NoError(t, err) defer dbConnectionPool.Close() - store, _ := store.NewStore(dbConnectionPool) + sqlxDB, err := dbConnectionPool.SqlxDB(context.Background()) + require.NoError(t, err) + metricsService := metrics.NewMetricsService(sqlxDB) + store, _ := store.NewStore(dbConnectionPool, metricsService) channelAccountStore := channelAccountStore.ChannelAccountStoreMock{} mockHTTPClient := utils.MockHTTPClient{} cfg := WebhookChannelConfigs{ @@ -98,6 +106,7 @@ func TestUnlockChannelAccount(t *testing.T) { MaxRetries: 3, MinWaitBtwnRetriesMS: 5, NetworkPassphrase: "networkpassphrase", + MetricsService: metricsService, } channel := NewWebhookChannel(cfg) account := keypair.MustRandom() diff --git a/internal/tss/services/pool_populator_test.go b/internal/tss/services/pool_populator_test.go index 6ba2c2f..dee2a67 100644 --- a/internal/tss/services/pool_populator_test.go +++ b/internal/tss/services/pool_populator_test.go @@ -8,6 +8,7 @@ import ( "github.com/stellar/wallet-backend/internal/db" "github.com/stellar/wallet-backend/internal/db/dbtest" "github.com/stellar/wallet-backend/internal/entities" + "github.com/stellar/wallet-backend/internal/metrics" "github.com/stellar/wallet-backend/internal/services" "github.com/stellar/wallet-backend/internal/tss" "github.com/stellar/wallet-backend/internal/tss/router" @@ -23,7 +24,10 @@ func TestRouteNewTransactions(t *testing.T) { dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) require.NoError(t, err) defer dbConnectionPool.Close() - store, _ := store.NewStore(dbConnectionPool) + sqlxDB, err := dbConnectionPool.SqlxDB(context.Background()) + require.NoError(t, err) + metricsService := metrics.NewMetricsService(sqlxDB) + store, _ := store.NewStore(dbConnectionPool, metricsService) mockRouter := router.MockRouter{} mockRPCSerive := services.RPCServiceMock{} populator, _ := NewPoolPopulator(&mockRouter, store, &mockRPCSerive) @@ -73,7 +77,10 @@ func TestRouteErrorTransactions(t *testing.T) { dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) require.NoError(t, err) defer dbConnectionPool.Close() - store, _ := store.NewStore(dbConnectionPool) + sqlxDB, err := dbConnectionPool.SqlxDB(context.Background()) + require.NoError(t, err) + metricsService := metrics.NewMetricsService(sqlxDB) + store, _ := store.NewStore(dbConnectionPool, metricsService) mockRouter := router.MockRouter{} mockRPCSerive := services.RPCServiceMock{} populator, _ := NewPoolPopulator(&mockRouter, store, &mockRPCSerive) @@ -137,7 +144,10 @@ func TestRouteFinalTransactions(t *testing.T) { dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) require.NoError(t, err) defer dbConnectionPool.Close() - store, _ := store.NewStore(dbConnectionPool) + sqlxDB, err := dbConnectionPool.SqlxDB(context.Background()) + require.NoError(t, err) + metricsService := metrics.NewMetricsService(sqlxDB) + store, _ := store.NewStore(dbConnectionPool, metricsService) mockRouter := router.MockRouter{} mockRPCSerive := services.RPCServiceMock{} populator, _ := NewPoolPopulator(&mockRouter, store, &mockRPCSerive) @@ -175,7 +185,10 @@ func TestNotSentTransactions(t *testing.T) { dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) require.NoError(t, err) defer dbConnectionPool.Close() - store, _ := store.NewStore(dbConnectionPool) + sqlxDB, err := dbConnectionPool.SqlxDB(context.Background()) + require.NoError(t, err) + metricsService := metrics.NewMetricsService(sqlxDB) + store, _ := store.NewStore(dbConnectionPool, metricsService) mockRouter := router.MockRouter{} mockRPCSerive := services.RPCServiceMock{} populator, _ := NewPoolPopulator(&mockRouter, store, &mockRPCSerive) diff --git a/internal/tss/services/transaction_manager_test.go b/internal/tss/services/transaction_manager_test.go index c8771a8..a9876f8 100644 --- a/internal/tss/services/transaction_manager_test.go +++ b/internal/tss/services/transaction_manager_test.go @@ -9,6 +9,7 @@ import ( "github.com/stellar/wallet-backend/internal/db" "github.com/stellar/wallet-backend/internal/db/dbtest" "github.com/stellar/wallet-backend/internal/entities" + "github.com/stellar/wallet-backend/internal/metrics" "github.com/stellar/wallet-backend/internal/services" "github.com/stellar/wallet-backend/internal/tss" "github.com/stellar/wallet-backend/internal/tss/store" @@ -24,7 +25,10 @@ func TestBuildAndSubmitTransaction(t *testing.T) { dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) require.NoError(t, err) defer dbConnectionPool.Close() - store, _ := store.NewStore(dbConnectionPool) + sqlxDB, err := dbConnectionPool.SqlxDB(context.Background()) + require.NoError(t, err) + metricsService := metrics.NewMetricsService(sqlxDB) + store, _ := store.NewStore(dbConnectionPool, metricsService) txServiceMock := TransactionServiceMock{} rpcServiceMock := services.RPCServiceMock{} txManager := NewTransactionManager(TransactionManagerConfigs{ diff --git a/internal/tss/store/store.go b/internal/tss/store/store.go index 2ef0661..436e065 100644 --- a/internal/tss/store/store.go +++ b/internal/tss/store/store.go @@ -8,6 +8,7 @@ import ( "time" "github.com/stellar/wallet-backend/internal/db" + "github.com/stellar/wallet-backend/internal/metrics" "github.com/stellar/wallet-backend/internal/tss" ) @@ -24,7 +25,8 @@ type Store interface { var _ Store = (*store)(nil) type store struct { - DB db.ConnectionPool + DB db.ConnectionPool + MetricsService *metrics.MetricsService } type Transaction struct { @@ -47,12 +49,16 @@ type Try struct { CreatedAt time.Time `db:"updated_at"` } -func NewStore(db db.ConnectionPool) (Store, error) { +func NewStore(db db.ConnectionPool, metricsService *metrics.MetricsService) (Store, error) { if db == nil { return nil, fmt.Errorf("db cannot be nil") } + if metricsService == nil { + return nil, fmt.Errorf("metricsService cannot be nil") + } return &store{ - DB: db, + DB: db, + MetricsService: metricsService, }, nil } @@ -69,10 +75,14 @@ func (s *store) UpsertTransaction(ctx context.Context, webhookURL string, txHash current_status = EXCLUDED.current_status, updated_at = NOW(); ` + start := time.Now() _, err := s.DB.ExecContext(ctx, q, txHash, txXDR, webhookURL, status.Status()) + duration := time.Since(start).Seconds() + s.MetricsService.ObserveDBQueryDuration("INSERT", "tss_transactions", duration) if err != nil { return fmt.Errorf("inserting/updatig tss transaction: %w", err) } + s.MetricsService.IncDBQuery("INSERT", "tss_transactions") return nil } @@ -91,74 +101,98 @@ func (s *store) UpsertTry(ctx context.Context, txHash string, feeBumpTxHash stri result_xdr = EXCLUDED.result_xdr, updated_at = NOW(); ` + start := time.Now() _, err := s.DB.ExecContext(ctx, q, txHash, feeBumpTxHash, feeBumpTxXDR, status.Status(), code.Code(), resultXDR) + duration := time.Since(start).Seconds() + s.MetricsService.ObserveDBQueryDuration("INSERT", "tss_transaction_submission_tries", duration) if err != nil { return fmt.Errorf("inserting/updating tss try: %w", err) } + s.MetricsService.IncDBQuery("INSERT", "tss_transaction_submission_tries") return nil } func (s *store) GetTransaction(ctx context.Context, hash string) (Transaction, error) { q := `SELECT * FROM tss_transactions WHERE transaction_hash = $1` var transaction Transaction + start := time.Now() err := s.DB.GetContext(ctx, &transaction, q, hash) + duration := time.Since(start).Seconds() + s.MetricsService.ObserveDBQueryDuration("SELECT", "tss_transactions", duration) if err != nil { if errors.Is(err, sql.ErrNoRows) { return Transaction{}, nil } return Transaction{}, fmt.Errorf("getting transaction: %w", err) } + s.MetricsService.IncDBQuery("SELECT", "tss_transactions") return transaction, nil } func (s *store) GetTry(ctx context.Context, hash string) (Try, error) { q := `SELECT * FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1` var try Try + start := time.Now() err := s.DB.GetContext(ctx, &try, q, hash) + duration := time.Since(start).Seconds() + s.MetricsService.ObserveDBQueryDuration("SELECT", "tss_transaction_submission_tries", duration) if err != nil { if errors.Is(err, sql.ErrNoRows) { return Try{}, nil } return Try{}, fmt.Errorf("getting try: %w", err) } + s.MetricsService.IncDBQuery("SELECT", "tss_transaction_submission_tries") return try, nil } func (s *store) GetTryByXDR(ctx context.Context, xdr string) (Try, error) { q := `SELECT * FROM tss_transaction_submission_tries WHERE try_transaction_xdr = $1` var try Try + start := time.Now() err := s.DB.GetContext(ctx, &try, q, xdr) + duration := time.Since(start).Seconds() + s.MetricsService.ObserveDBQueryDuration("SELECT", "tss_transaction_submission_tries", duration) if err != nil { if errors.Is(err, sql.ErrNoRows) { return Try{}, nil } return Try{}, fmt.Errorf("getting try: %w", err) } + s.MetricsService.IncDBQuery("SELECT", "tss_transaction_submission_tries") return try, nil } func (s *store) GetTransactionsWithStatus(ctx context.Context, status tss.RPCTXStatus) ([]Transaction, error) { q := `SELECT * FROM tss_transactions WHERE current_status = $1` var transactions []Transaction + start := time.Now() err := s.DB.SelectContext(ctx, &transactions, q, status.Status()) + duration := time.Since(start).Seconds() + s.MetricsService.ObserveDBQueryDuration("SELECT", "tss_transactions", duration) if err != nil { if errors.Is(err, sql.ErrNoRows) { return []Transaction{}, nil } return []Transaction{}, fmt.Errorf("getting transactions: %w", err) } + s.MetricsService.IncDBQuery("SELECT", "tss_transactions") return transactions, nil } func (s *store) GetLatestTry(ctx context.Context, txHash string) (Try, error) { q := `SELECT * FROM tss_transaction_submission_tries WHERE original_transaction_hash = $1 ORDER BY updated_at DESC LIMIT 1` var try Try + start := time.Now() err := s.DB.GetContext(ctx, &try, q, txHash) + duration := time.Since(start).Seconds() + s.MetricsService.ObserveDBQueryDuration("SELECT", "tss_transaction_submission_tries", duration) if err != nil { if errors.Is(err, sql.ErrNoRows) { return Try{}, nil } return Try{}, fmt.Errorf("getting latest trt: %w", err) } + s.MetricsService.IncDBQuery("SELECT", "tss_transaction_submission_tries") return try, nil } diff --git a/internal/tss/store/store_test.go b/internal/tss/store/store_test.go index 2a2a3e1..75d6c4e 100644 --- a/internal/tss/store/store_test.go +++ b/internal/tss/store/store_test.go @@ -8,6 +8,7 @@ import ( "github.com/stellar/wallet-backend/internal/db" "github.com/stellar/wallet-backend/internal/db/dbtest" "github.com/stellar/wallet-backend/internal/entities" + "github.com/stellar/wallet-backend/internal/metrics" "github.com/stellar/wallet-backend/internal/tss" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -19,7 +20,10 @@ func TestUpsertTransaction(t *testing.T) { dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) require.NoError(t, err) defer dbConnectionPool.Close() - store, _ := NewStore(dbConnectionPool) + sqlxDB, err := dbConnectionPool.SqlxDB(context.Background()) + require.NoError(t, err) + metricsService := metrics.NewMetricsService(sqlxDB) + store, _ := NewStore(dbConnectionPool, metricsService) t.Run("insert", func(t *testing.T) { _ = store.UpsertTransaction(context.Background(), "www.stellar.org", "hash", "xdr", tss.RPCTXStatus{OtherStatus: tss.NewStatus}) @@ -50,7 +54,10 @@ func TestUpsertTry(t *testing.T) { dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) require.NoError(t, err) defer dbConnectionPool.Close() - store, _ := NewStore(dbConnectionPool) + sqlxDB, err := dbConnectionPool.SqlxDB(context.Background()) + require.NoError(t, err) + metricsService := metrics.NewMetricsService(sqlxDB) + store, _ := NewStore(dbConnectionPool, metricsService) t.Run("insert", func(t *testing.T) { status := tss.RPCTXStatus{OtherStatus: tss.NewStatus} code := tss.RPCTXCode{OtherCodes: tss.NewCode} @@ -112,7 +119,10 @@ func TestGetTransaction(t *testing.T) { dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) require.NoError(t, err) defer dbConnectionPool.Close() - store, _ := NewStore(dbConnectionPool) + sqlxDB, err := dbConnectionPool.SqlxDB(context.Background()) + require.NoError(t, err) + metricsService := metrics.NewMetricsService(sqlxDB) + store, _ := NewStore(dbConnectionPool, metricsService) t.Run("transaction_exists", func(t *testing.T) { status := tss.RPCTXStatus{OtherStatus: tss.NewStatus} _ = store.UpsertTransaction(context.Background(), "localhost:8000", "hash", "xdr", status) @@ -136,7 +146,10 @@ func TestGetTry(t *testing.T) { dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) require.NoError(t, err) defer dbConnectionPool.Close() - store, _ := NewStore(dbConnectionPool) + sqlxDB, err := dbConnectionPool.SqlxDB(context.Background()) + require.NoError(t, err) + metricsService := metrics.NewMetricsService(sqlxDB) + store, _ := NewStore(dbConnectionPool, metricsService) t.Run("try_exists", func(t *testing.T) { status := tss.RPCTXStatus{OtherStatus: tss.NewStatus} code := tss.RPCTXCode{OtherCodes: tss.NewCode} @@ -165,7 +178,10 @@ func TestGetTryByXDR(t *testing.T) { dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) require.NoError(t, err) defer dbConnectionPool.Close() - store, _ := NewStore(dbConnectionPool) + sqlxDB, err := dbConnectionPool.SqlxDB(context.Background()) + require.NoError(t, err) + metricsService := metrics.NewMetricsService(sqlxDB) + store, _ := NewStore(dbConnectionPool, metricsService) t.Run("try_exists", func(t *testing.T) { status := tss.RPCTXStatus{OtherStatus: tss.NewStatus} code := tss.RPCTXCode{OtherCodes: tss.NewCode} @@ -194,7 +210,10 @@ func TestGetTransactionsWithStatus(t *testing.T) { dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) require.NoError(t, err) defer dbConnectionPool.Close() - store, _ := NewStore(dbConnectionPool) + sqlxDB, err := dbConnectionPool.SqlxDB(context.Background()) + require.NoError(t, err) + metricsService := metrics.NewMetricsService(sqlxDB) + store, _ := NewStore(dbConnectionPool, metricsService) t.Run("transactions_do_not_exist", func(t *testing.T) { status := tss.RPCTXStatus{OtherStatus: tss.NewStatus} @@ -223,7 +242,10 @@ func TestGetLatestTry(t *testing.T) { dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) require.NoError(t, err) defer dbConnectionPool.Close() - store, _ := NewStore(dbConnectionPool) + sqlxDB, err := dbConnectionPool.SqlxDB(context.Background()) + require.NoError(t, err) + metricsService := metrics.NewMetricsService(sqlxDB) + store, _ := NewStore(dbConnectionPool, metricsService) t.Run("tries_do_not_exist", func(t *testing.T) { try, err := store.GetLatestTry(context.Background(), "hash")