diff --git a/go.mod b/go.mod index 75a1b56fe1..6afa681736 100644 --- a/go.mod +++ b/go.mod @@ -46,7 +46,7 @@ require ( github.com/spf13/viper v1.17.0 github.com/stellar/go-xdr v0.0.0-20231122183749-b53fb00bcac2 github.com/stellar/throttled v2.2.3-0.20190823235211-89d75816f59d+incompatible - github.com/stretchr/testify v1.9.0 + github.com/stretchr/testify v1.10.0 github.com/tyler-smith/go-bip39 v0.0.0-20180618194314-52158e4697b8 github.com/xdrpp/goxdr v0.1.1 google.golang.org/api v0.183.0 @@ -68,7 +68,14 @@ require ( github.com/docker/docker v27.3.1+incompatible github.com/docker/go-connections v0.5.0 github.com/fsouza/fake-gcs-server v1.49.2 + github.com/go-chi/chi/v5 v5.1.0 + github.com/riandyrn/otelchi v0.12.1 github.com/stellar/stellar-rpc v0.9.6-0.20250130160539-be7702aa01ba + go.opentelemetry.io/otel v1.34.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0 + go.opentelemetry.io/otel/sdk v1.34.0 + go.opentelemetry.io/otel/trace v1.34.0 ) require ( @@ -111,6 +118,7 @@ require ( github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect github.com/gorilla/handlers v1.5.2 // indirect github.com/gorilla/mux v1.8.1 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect @@ -121,23 +129,21 @@ require ( github.com/opencontainers/image-spec v1.1.0 // indirect github.com/pelletier/go-toml/v2 v2.1.0 // indirect github.com/pkg/xattr v0.4.9 // indirect - github.com/rogpeppe/go-internal v1.11.0 // indirect + github.com/rogpeppe/go-internal v1.13.1 // indirect github.com/sagikazarmark/locafero v0.3.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.10.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect - go.opentelemetry.io/otel v1.28.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0 // indirect - go.opentelemetry.io/otel/metric v1.28.0 // indirect - go.opentelemetry.io/otel/sdk v1.28.0 // indirect - go.opentelemetry.io/otel/trace v1.28.0 // indirect + go.opentelemetry.io/otel/metric v1.34.0 // indirect + go.opentelemetry.io/proto/otlp v1.3.1 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/mod v0.17.0 // indirect + golang.org/x/mod v0.18.0 // indirect golang.org/x/sync v0.10.0 // indirect - golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect + golang.org/x/tools v0.22.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect gopkg.in/djherbis/atime.v1 v1.0.0 // indirect @@ -191,7 +197,7 @@ require ( golang.org/x/exp v0.0.0-20231006140011-7918f672742d golang.org/x/net v0.26.0 // indirect golang.org/x/oauth2 v0.21.0 // indirect - golang.org/x/sys v0.28.0 // indirect + golang.org/x/sys v0.29.0 // indirect golang.org/x/term v0.27.0 // indirect golang.org/x/text v0.21.0 // indirect golang.org/x/time v0.5.0 diff --git a/go.sum b/go.sum index cedc199994..3e5efc72b0 100644 --- a/go.sum +++ b/go.sum @@ -200,6 +200,8 @@ github.com/getsentry/raven-go v0.2.0 h1:no+xWJRb5ZI7eE8TWgIq1jLulQiIoLG0IfYxv5JY github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= github.com/go-chi/chi v4.1.2+incompatible h1:fGFk2Gmi/YKXk0OmGfBh0WgmN3XB8lVnEyNz34tQRec= github.com/go-chi/chi v4.1.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= +github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw= +github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk= github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= @@ -468,10 +470,12 @@ github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= github.com/rcrowley/go-metrics v0.0.0-20190826022208-cac0b30c2563/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/riandyrn/otelchi v0.12.1 h1:FdRKK3/RgZ/T+d+qTH5Uw3MFx0KwRF38SkdfTMMq/m8= +github.com/riandyrn/otelchi v0.12.1/go.mod h1:weZZeUJURvtCcbWsdb7Y6F8KFZGedJlSrgUjq9VirV8= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= -github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/rs/cors v1.11.0 h1:0B9GE/r9Bc2UxRMMtymBkHTenPkHDv0CW4Y98GBY+po= github.com/rs/cors v1.11.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= @@ -527,8 +531,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/tyler-smith/go-bip39 v0.0.0-20180618194314-52158e4697b8 h1:g3yQGZK+G6dfF/mw/SOwsTMzUVkpT4hB8pHxpbTXkKw= @@ -574,22 +578,24 @@ go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 h1:4Pp6oUg3+e/6M4C0A/3kJ2VYa++dsWVTtGgLVj5xtHg= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= -go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo= -go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4= +go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= +go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 h1:3Q/xZUyC1BBkualc9ROb4G8qkH90LXEIICcs5zv1OYY= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0 h1:j9+03ymgYhPKmeXGk5Zu+cIZOlVzd9Zv7QIiyItjFBU= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0/go.mod h1:Y5+XiUG4Emn1hTfciPzGPJaSI+RpDts6BnCIir0SLqk= -go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q= -go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s= -go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE= -go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg= -go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g= -go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI= +go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= +go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= +go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= +go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= +go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= +go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= @@ -642,8 +648,8 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= -golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= +golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -758,8 +764,8 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= -golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -834,8 +840,8 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= +golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/services/friendbot/docker/Dockerfile b/services/friendbot/docker/Dockerfile index 5dab285e83..8b97d35c83 100644 --- a/services/friendbot/docker/Dockerfile +++ b/services/friendbot/docker/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.23-bullseye as build +FROM golang:1.23-bullseye AS build ADD . /src/friendbot WORKDIR /src/friendbot diff --git a/services/friendbot/friendbot.cfg b/services/friendbot/friendbot.cfg index da8f7aefcf..817f82a40c 100644 --- a/services/friendbot/friendbot.cfg +++ b/services/friendbot/friendbot.cfg @@ -7,4 +7,3 @@ num_minions = 1000 base_fee = 100000 minion_batch_size = 50 submit_tx_retries_allowed = 5 - diff --git a/services/friendbot/internal/friendbot.go b/services/friendbot/internal/friendbot.go index 1ad9af63d2..bb09580fe0 100644 --- a/services/friendbot/internal/friendbot.go +++ b/services/friendbot/internal/friendbot.go @@ -1,6 +1,7 @@ package internal import ( + "context" "log" "sync" @@ -22,14 +23,14 @@ type SubmitResult struct { } // Pay funds the account at `destAddress`. -func (bot *Bot) Pay(destAddress string) (*hProtocol.Transaction, error) { +func (bot *Bot) Pay(ctx context.Context, destAddress string) (*hProtocol.Transaction, error) { bot.indexMux.Lock() log.Printf("Selecting minion at index %d of max length %d", bot.nextMinionIndex, len(bot.Minions)) minion := bot.Minions[bot.nextMinionIndex] bot.nextMinionIndex = (bot.nextMinionIndex + 1) % len(bot.Minions) bot.indexMux.Unlock() resultChan := make(chan SubmitResult) - go minion.Run(destAddress, resultChan) + go minion.Run(ctx, destAddress, resultChan) maybeSubmitResult := <-resultChan close(resultChan) return maybeSubmitResult.maybeTransactionSuccess, maybeSubmitResult.maybeErr diff --git a/services/friendbot/internal/friendbot_handler.go b/services/friendbot/internal/friendbot_handler.go index 9e6f3f8d74..aa85f5d026 100644 --- a/services/friendbot/internal/friendbot_handler.go +++ b/services/friendbot/internal/friendbot_handler.go @@ -1,6 +1,7 @@ package internal import ( + "context" "net/http" "net/url" @@ -8,47 +9,95 @@ import ( "github.com/stellar/go/strkey" "github.com/stellar/go/support/render/hal" "github.com/stellar/go/support/render/problem" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/trace" +) + +const ( + // Tracer name for friendbot service + tracerName = "stellar-friendbot" ) // FriendbotHandler causes an account at `Address` to be created. type FriendbotHandler struct { Friendbot *Bot + tracer trace.Tracer +} + +// NewFriendbotHandler returns friendbot handler based on the tracing enabled +func NewFriendbotHandler(fb *Bot) *FriendbotHandler { + tracer := otel.Tracer(tracerName) + return &FriendbotHandler{ + Friendbot: fb, + tracer: tracer, + } + } // Handle is a method that implements http.HandlerFunc func (handler *FriendbotHandler) Handle(w http.ResponseWriter, r *http.Request) { - result, err := handler.doHandle(r) + ctx, span := handler.tracer.Start(r.Context(), "friendbot.init_http_request") + defer span.End() + + // Add request attributes to span + span.SetAttributes( + attribute.String("http.method", r.Method), + attribute.String("http.url", r.URL.String()), + attribute.String("http.user_agent", r.UserAgent()), + ) + + result, err := handler.doHandle(ctx, r) if err != nil { problem.Render(r.Context(), w, err) + span.SetStatus(codes.Error, err.Error()) return } + span.SetStatus(codes.Ok, codes.Ok.String()) hal.Render(w, *result) } // doHandle is just a convenience method that returns the object to be rendered -func (handler *FriendbotHandler) doHandle(r *http.Request) (*horizon.Transaction, error) { +func (handler *FriendbotHandler) doHandle(ctx context.Context, r *http.Request) (*horizon.Transaction, error) { + ctx, span := handler.tracer.Start(ctx, "friendbot.parse_http_request") + defer span.End() err := r.ParseForm() if err != nil { p := problem.BadRequest p.Detail = "Request parameters are not escaped or incorrectly formatted." + span.SetStatus(codes.Error, err.Error()) return nil, &p } - address, err := handler.loadAddress(r) + address, err := handler.loadAddress(ctx, r) if err != nil { + span.SetStatus(codes.Error, err.Error()) return nil, problem.MakeInvalidFieldProblem("addr", err) } - return handler.Friendbot.Pay(address) + span.SetStatus(codes.Ok, codes.Ok.String()) + return handler.Friendbot.Pay(ctx, address) } -func (handler *FriendbotHandler) loadAddress(r *http.Request) (string, error) { +func (handler *FriendbotHandler) loadAddress(ctx context.Context, r *http.Request) (string, error) { + _, span := handler.tracer.Start(ctx, "minion.destination_address") + defer span.End() + address := r.Form.Get("addr") + if address == "" { + span.SetStatus(codes.Error, "missing destination account address") + span.SetAttributes(attribute.String("error.type", "missing_parameter")) + } + unescaped, err := url.QueryUnescape(address) if err != nil { + span.SetStatus(codes.Error, err.Error()) return unescaped, err } _, err = strkey.Decode(strkey.VersionByteAccountID, unescaped) + span.SetAttributes(attribute.String("destination.account", address)) + span.SetStatus(codes.Ok, codes.Ok.String()) return unescaped, err } diff --git a/services/friendbot/internal/friendbot_test.go b/services/friendbot/internal/friendbot_test.go index 6313dd52dc..172aa4f81e 100644 --- a/services/friendbot/internal/friendbot_test.go +++ b/services/friendbot/internal/friendbot_test.go @@ -1,6 +1,7 @@ package internal import ( + "context" "sync" "testing" @@ -13,7 +14,9 @@ import ( ) func TestFriendbot_Pay_accountDoesNotExist(t *testing.T) { - mockSubmitTransaction := func(minion *Minion, hclient horizonclient.ClientInterface, tx string) (*hProtocol.Transaction, error) { + ctx := context.Background() + + mockSubmitTransaction := func(ctx context.Context, minion *Minion, hclient horizonclient.ClientInterface, tx string) (*hProtocol.Transaction, error) { // Instead of submitting the tx, we emulate a success. txSuccess := hProtocol.Transaction{EnvelopeXdr: tx, Successful: true} return &txSuccess, nil @@ -56,7 +59,7 @@ func TestFriendbot_Pay_accountDoesNotExist(t *testing.T) { fb := &Bot{Minions: []Minion{minion}} recipientAddress := "GDJIN6W6PLTPKLLM57UW65ZH4BITUXUMYQHIMAZFYXF45PZVAWDBI77Z" - txSuccess, err := fb.Pay(recipientAddress) + txSuccess, err := fb.Pay(ctx, recipientAddress) if !assert.NoError(t, err) { return } @@ -67,12 +70,12 @@ func TestFriendbot_Pay_accountDoesNotExist(t *testing.T) { var wg sync.WaitGroup wg.Add(2) go func() { - _, err := fb.Pay(recipientAddress) + _, err := fb.Pay(ctx, recipientAddress) assert.NoError(t, err) wg.Done() }() go func() { - _, err := fb.Pay(recipientAddress) + _, err := fb.Pay(ctx, recipientAddress) assert.NoError(t, err) wg.Done() }() @@ -80,7 +83,8 @@ func TestFriendbot_Pay_accountDoesNotExist(t *testing.T) { } func TestFriendbot_Pay_accountExists(t *testing.T) { - mockSubmitTransaction := func(minion *Minion, hclient horizonclient.ClientInterface, tx string) (*hProtocol.Transaction, error) { + ctx := context.Background() + mockSubmitTransaction := func(ctx context.Context, minion *Minion, hclient horizonclient.ClientInterface, tx string) (*hProtocol.Transaction, error) { // Instead of submitting the tx, we emulate a success. txSuccess := hProtocol.Transaction{EnvelopeXdr: tx, Successful: true} return &txSuccess, nil @@ -123,7 +127,7 @@ func TestFriendbot_Pay_accountExists(t *testing.T) { fb := &Bot{Minions: []Minion{minion}} recipientAddress := "GDJIN6W6PLTPKLLM57UW65ZH4BITUXUMYQHIMAZFYXF45PZVAWDBI77Z" - txSuccess, err := fb.Pay(recipientAddress) + txSuccess, err := fb.Pay(ctx, recipientAddress) if !assert.NoError(t, err) { return } @@ -134,12 +138,12 @@ func TestFriendbot_Pay_accountExists(t *testing.T) { var wg sync.WaitGroup wg.Add(2) go func() { - _, err := fb.Pay(recipientAddress) + _, err := fb.Pay(ctx, recipientAddress) assert.NoError(t, err) wg.Done() }() go func() { - _, err := fb.Pay(recipientAddress) + _, err := fb.Pay(ctx, recipientAddress) assert.NoError(t, err) wg.Done() }() @@ -147,7 +151,8 @@ func TestFriendbot_Pay_accountExists(t *testing.T) { } func TestFriendbot_Pay_accountExistsAlreadyFunded(t *testing.T) { - mockSubmitTransaction := func(minion *Minion, hclient horizonclient.ClientInterface, tx string) (*hProtocol.Transaction, error) { + ctx := context.Background() + mockSubmitTransaction := func(ctx context.Context, minion *Minion, hclient horizonclient.ClientInterface, tx string) (*hProtocol.Transaction, error) { // Instead of submitting the tx, we emulate a success. txSuccess := hProtocol.Transaction{EnvelopeXdr: tx, Successful: true} return &txSuccess, nil @@ -190,6 +195,6 @@ func TestFriendbot_Pay_accountExistsAlreadyFunded(t *testing.T) { fb := &Bot{Minions: []Minion{minion}} recipientAddress := "GDJIN6W6PLTPKLLM57UW65ZH4BITUXUMYQHIMAZFYXF45PZVAWDBI77Z" - _, err = fb.Pay(recipientAddress) + _, err = fb.Pay(ctx, recipientAddress) assert.ErrorIs(t, err, ErrAccountFunded) } diff --git a/services/friendbot/internal/minion.go b/services/friendbot/internal/minion.go index 697806a1f9..58e80c259a 100644 --- a/services/friendbot/internal/minion.go +++ b/services/friendbot/internal/minion.go @@ -1,6 +1,7 @@ package internal import ( + "context" "fmt" "github.com/stellar/go/amount" @@ -9,6 +10,9 @@ import ( hProtocol "github.com/stellar/go/protocols/horizon" "github.com/stellar/go/support/errors" "github.com/stellar/go/txnbuild" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" ) const createAccountAlreadyExistXDR = "AAAAAAAAAGT/////AAAAAQAAAAAAAAAA/////AAAAAA=" @@ -17,6 +21,8 @@ var ErrAccountExists error = errors.New(fmt.Sprintf("createAccountAlreadyExist ( var ErrAccountFunded error = errors.New("account already funded to starting balance") +var botTracer = otel.Tracer("stellar_friendbot_minion") + // Minion contains a Stellar channel account and Go channels to communicate with friendbot. type Minion struct { Account Account @@ -29,7 +35,7 @@ type Minion struct { BaseFee int64 // Mockable functions - SubmitTransaction func(minion *Minion, hclient horizonclient.ClientInterface, tx string) (*hProtocol.Transaction, error) + SubmitTransaction func(ctx context.Context, minion *Minion, hclient horizonclient.ClientInterface, tx string) (*hProtocol.Transaction, error) CheckSequenceRefresh func(minion *Minion, hclient horizonclient.ClientInterface) error CheckAccountExists func(minion *Minion, hclient horizonclient.ClientInterface, destAddress string) (bool, string, error) @@ -39,7 +45,10 @@ type Minion struct { // Run reads a payment destination address and an output channel. It attempts // to pay that address and submits the result to the channel. -func (minion *Minion) Run(destAddress string, resultChan chan SubmitResult) { +func (minion *Minion) Run(ctx context.Context, destAddress string, resultChan chan SubmitResult) { + ctx, span := botTracer.Start(ctx, "minion.run.pay_minion") + defer span.End() + span.SetAttributes(attribute.String("minion.account_id", minion.Account.AccountID)) err := minion.CheckSequenceRefresh(minion, minion.Horizon) if err != nil { resultChan <- SubmitResult{ @@ -56,6 +65,11 @@ func (minion *Minion) Run(destAddress string, resultChan chan SubmitResult) { } return } + if exists { + span.AddEvent("Destination account exists") + span.SetAttributes(attribute.String("destination.account_address", destAddress), + attribute.String("destination.account_balance", balance)) + } err = minion.checkBalance(balance) if err != nil { resultChan <- SubmitResult{ @@ -80,15 +94,22 @@ func (minion *Minion) Run(destAddress string, resultChan chan SubmitResult) { } return } - succ, err := minion.SubmitTransaction(minion, minion.Horizon, txStr) + succ, err := minion.SubmitTransaction(ctx, minion, minion.Horizon, txStr) resultChan <- SubmitResult{ maybeTransactionSuccess: succ, maybeErr: errors.Wrapf(err, "submitting tx to minion %x", txHash), } + if succ != nil { + span.SetAttributes(attribute.Bool("minion.tx_success_status", succ.Successful)) + span.SetStatus(codes.Ok, codes.Ok.String()) + } } // SubmitTransaction should be passed to the Minion. -func SubmitTransaction(minion *Minion, hclient horizonclient.ClientInterface, tx string) (*hProtocol.Transaction, error) { +func SubmitTransaction(ctx context.Context, minion *Minion, hclient horizonclient.ClientInterface, tx string) (*hProtocol.Transaction, error) { + _, span := botTracer.Start(ctx, "minion.submit_transaction") + defer span.End() + result, err := hclient.SubmitTransactionXDR(tx) if err != nil { errStr := "submitting tx to horizon" @@ -99,14 +120,23 @@ func SubmitTransaction(minion *Minion, hclient horizonclient.ClientInterface, tx if resErr != nil { errStr += ": error getting horizon error code: " + resErr.Error() } else if resStr == createAccountAlreadyExistXDR { + span.SetStatus(codes.Error, errStr) + span.AddEvent("transaction submission failed") return nil, errors.Wrap(ErrAccountExists, errStr) } else { errStr += ": horizon error string: " + resStr } + span.SetStatus(codes.Error, errStr) + span.AddEvent("transaction submission failed") return nil, errors.New(errStr) } + span.SetStatus(codes.Error, err.Error()) + span.AddEvent("transaction submission failed") return nil, errors.Wrap(err, errStr) } + span.SetAttributes(attribute.String("minion.tx_hash", result.Hash)) + span.AddEvent("transaction submission success") + span.SetStatus(codes.Ok, codes.Ok.String()) return &result, nil } diff --git a/services/friendbot/internal/minion_test.go b/services/friendbot/internal/minion_test.go index 272bb64afb..9786c735bb 100644 --- a/services/friendbot/internal/minion_test.go +++ b/services/friendbot/internal/minion_test.go @@ -1,6 +1,7 @@ package internal import ( + "context" "sync" "testing" @@ -16,7 +17,9 @@ import ( // in which Minion.Run() will try to send multiple messages to a channel that gets closed // immediately after receiving one message. func TestMinion_NoChannelErrors(t *testing.T) { - mockSubmitTransaction := func(minion *Minion, hclient horizonclient.ClientInterface, tx string) (txn *hProtocol.Transaction, err error) { + ctx := context.Background() + + mockSubmitTransaction := func(ctx context.Context, minion *Minion, hclient horizonclient.ClientInterface, tx string) (txn *hProtocol.Transaction, err error) { return txn, nil } @@ -70,7 +73,7 @@ func TestMinion_NoChannelErrors(t *testing.T) { for i := 0; i < numTests; i++ { go func() { - fb.Pay(recipientAddress) + fb.Pay(ctx, recipientAddress) wg.Done() }() } @@ -78,12 +81,14 @@ func TestMinion_NoChannelErrors(t *testing.T) { } func TestMinion_CorrectNumberOfTxSubmissions(t *testing.T) { + ctx := context.Background() + var ( numTxSubmits int mux sync.Mutex ) - mockSubmitTransaction := func(minion *Minion, hclient horizonclient.ClientInterface, tx string) (txn *hProtocol.Transaction, err error) { + mockSubmitTransaction := func(ctx context.Context, minion *Minion, hclient horizonclient.ClientInterface, tx string) (txn *hProtocol.Transaction, err error) { mux.Lock() numTxSubmits++ mux.Unlock() @@ -138,7 +143,7 @@ func TestMinion_CorrectNumberOfTxSubmissions(t *testing.T) { for i := 0; i < numTests; i++ { go func() { - fb.Pay(recipientAddress) + fb.Pay(ctx, recipientAddress) wg.Done() }() } diff --git a/services/friendbot/main.go b/services/friendbot/main.go index c5933c2fc3..468a922f33 100644 --- a/services/friendbot/main.go +++ b/services/friendbot/main.go @@ -6,7 +6,8 @@ import ( stdhttp "net/http" "os" - "github.com/go-chi/chi" + "github.com/go-chi/chi/v5" + "github.com/riandyrn/otelchi" "github.com/spf13/cobra" "github.com/stellar/go/services/friendbot/internal" @@ -16,6 +17,12 @@ import ( "github.com/stellar/go/support/http" "github.com/stellar/go/support/log" "github.com/stellar/go/support/render/problem" + "github.com/stellar/go/utils/tracer" +) + +const ( + serviceName = "stellar-friendbot" + serviceVersion = "1.0.0" ) // Config represents the configuration of a friendbot server @@ -31,6 +38,8 @@ type Config struct { MinionBatchSize int `toml:"minion_batch_size" valid:"optional"` SubmitTxRetriesAllowed int `toml:"submit_tx_retries_allowed" valid:"optional"` UseCloudflareIP bool `toml:"use_cloudflare_ip" valid:"optional"` + OtelEndpoint string `toml:"otel_endpoint" valid:"optional"` + OtelEnabled bool `toml:"otel_enabled" valid:"optional"` } func main() { @@ -63,6 +72,14 @@ func run(cmd *cobra.Command, args []string) { os.Exit(1) } + //Setup and initialize tracer + tracer, err := tracer.InitializeTracer(cfg.OtelEnabled, cfg.OtelEndpoint, serviceName, serviceVersion) + if err != nil { + log.Error("Failed to initialize tracer:", err) + } + log.Infof("Tracer initialized") + defer tracer() + fb, err := initFriendbot(cfg.FriendbotSecret, cfg.NetworkPassphrase, cfg.HorizonURL, cfg.StartingBalance, cfg.NumMinions, cfg.BaseFee, cfg.MinionBatchSize, cfg.SubmitTxRetriesAllowed) if err != nil { @@ -87,8 +104,7 @@ func run(cmd *cobra.Command, args []string) { func initRouter(cfg Config, fb *internal.Bot) *chi.Mux { mux := newMux(cfg) - - handler := &internal.FriendbotHandler{Friendbot: fb} + handler := internal.NewFriendbotHandler(fb) mux.Get("/", handler.Handle) mux.Post("/", handler.Handle) mux.NotFound(stdhttp.HandlerFunc(func(w stdhttp.ResponseWriter, r *stdhttp.Request) { @@ -104,6 +120,8 @@ func newMux(cfg Config) *chi.Mux { // middlewares mux.Use(http.XFFMiddleware(http.XFFMiddlewareConfig{BehindCloudflare: cfg.UseCloudflareIP})) mux.Use(http.NewAPIMux(log.DefaultLogger).Middlewares()...) + mux.Use(otelchi.Middleware(serviceName, otelchi.WithChiRoutes(mux))) + return mux } diff --git a/utils/tracer/tracer.go b/utils/tracer/tracer.go new file mode 100644 index 0000000000..78e2450978 --- /dev/null +++ b/utils/tracer/tracer.go @@ -0,0 +1,77 @@ +package tracer + +import ( + "context" + "time" + + "github.com/stellar/go/support/errors" + "github.com/stellar/go/support/log" + + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" + "go.opentelemetry.io/otel/propagation" + "go.opentelemetry.io/otel/sdk/resource" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + semconv "go.opentelemetry.io/otel/semconv/v1.17.0" + "go.opentelemetry.io/otel/trace/noop" +) + +// InitializeTracer sets up traceProvider and returns a function to shutdown traceprovider +func InitializeTracer(enabled bool, OtelEndpoint, ServiceName, ServiceVersion string) (func(), error) { + if !enabled { + log.Info("Tracing disabled - using no-op tracer") + // Set no-op tracer provider + otel.SetTracerProvider(noop.NewTracerProvider()) + otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator()) + + // Return a no-op shutdown function + return func() {}, nil + } + + log.Infof("Initializing tracer") + headers := map[string]string{ + "content-type": "application/json", + } + + exporter, err := otlptrace.New( + context.Background(), + otlptracehttp.NewClient( + otlptracehttp.WithEndpoint(OtelEndpoint), + otlptracehttp.WithHeaders(headers), + otlptracehttp.WithInsecure(), + ), + ) + if err != nil { + return nil, errors.Wrap(err, "Error while creating exporter") + } + + res, err := resource.New( + context.Background(), + resource.WithAttributes( + semconv.ServiceName(ServiceName), + semconv.ServiceVersion(ServiceVersion), + ), + ) + + if err != nil { + return nil, errors.Wrap(err, "Error while creating resource") + } + + traceProvider := sdktrace.NewTracerProvider( + sdktrace.WithBatcher(exporter), + sdktrace.WithResource(res), + ) + + // Set traceprovider for the otel. + otel.SetTracerProvider(traceProvider) + otel.SetTextMapPropagator(propagation.TraceContext{}) + + return func() { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + if err := traceProvider.Shutdown(ctx); err != nil { + log.Error("Error shutting down tracer provider", err) + } + }, nil +}