diff --git a/.github/workflows/golangci_lint.yml b/.github/workflows/golangci_lint.yml index f835e64..a03a451 100644 --- a/.github/workflows/golangci_lint.yml +++ b/.github/workflows/golangci_lint.yml @@ -38,7 +38,7 @@ jobs: with: args: --config=${{ github.workspace }}/.golangci.yml only-new-issues: true - version: v1.62.2 + version: v1.64.2 working-directory: ${{ matrix.module }} # Per https://github.com/orgs/community/discussions/4324, we need to capture a matrix-job result diff --git a/.golangci.yml b/.golangci.yml index 038ccb3..1d3ad14 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -7,7 +7,6 @@ linters: - errname - errorlint - exhaustive - - exportloopref - fatcontext - ginkgolinter - gocritic diff --git a/capabilities/go.mod b/capabilities/go.mod new file mode 100644 index 0000000..0f5e3ce --- /dev/null +++ b/capabilities/go.mod @@ -0,0 +1,56 @@ +module github.com/smartcontractkit/chainlink-framework/capabilities + +go 1.24.1 + +require ( + github.com/google/uuid v1.6.0 + github.com/jpillora/backoff v1.0.0 + github.com/smartcontractkit/chainlink-common v0.6.1-0.20250407100046-dfdf9600557b + github.com/stretchr/testify v1.10.0 + go.opentelemetry.io/otel v1.35.0 + go.opentelemetry.io/otel/metric v1.35.0 +) + +require ( + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/gabriel-vasile/mimetype v1.4.8 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.25.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mr-tron/base58 v1.2.0 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/smartcontractkit/libocr v0.0.0-20250328171017-609ec10a5510 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.10.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.10.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.34.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.10.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.34.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.34.0 // indirect + go.opentelemetry.io/otel/log v0.10.0 // indirect + go.opentelemetry.io/otel/sdk v1.35.0 // indirect + go.opentelemetry.io/otel/sdk/log v0.10.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.35.0 // indirect + go.opentelemetry.io/otel/trace v1.35.0 // indirect + go.opentelemetry.io/proto/otlp v1.5.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.0 // indirect + golang.org/x/crypto v0.36.0 // indirect + golang.org/x/net v0.38.0 // indirect + golang.org/x/sys v0.31.0 // indirect + golang.org/x/text v0.23.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250219182151-9fdb1cabc7b2 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 // indirect + google.golang.org/grpc v1.71.0 // indirect + google.golang.org/protobuf v1.36.6 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/capabilities/go.sum b/capabilities/go.sum new file mode 100644 index 0000000..055feed --- /dev/null +++ b/capabilities/go.sum @@ -0,0 +1,114 @@ +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +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/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= +github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-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= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.25.0 h1:5Dh7cjvzR7BRZadnsVOzPhWsrwUr0nmsZJxEAnFLNO8= +github.com/go-playground/validator/v10 v10.25.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 h1:e9Rjr40Z98/clHv5Yg79Is0NtosR5LXRvdr7o/6NwbA= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1/go.mod h1:tIxuGz/9mpox++sgp9fJjHO0+q1X9/UOWd798aAm22M= +github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= +github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +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/smartcontractkit/chainlink-common v0.6.1-0.20250407100046-dfdf9600557b h1:a89rWexN5nUjJwH3X2ocEWTWEzz4/BDykPSFq2pYHMU= +github.com/smartcontractkit/chainlink-common v0.6.1-0.20250407100046-dfdf9600557b/go.mod h1:ASXpANdCfcKd+LF3Vhz37q4rmJ/XYQKEQ3La1k7idp0= +github.com/smartcontractkit/libocr v0.0.0-20250328171017-609ec10a5510 h1:gm8Jli0sdkrZYnrWBngAkPSDzFDkdNCy1/Dj86kVtYk= +github.com/smartcontractkit/libocr v0.0.0-20250328171017-609ec10a5510/go.mod h1:Mb7+/LC4edz7HyHxX4QkE42pSuov4AV68+AxBXAap0o= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.10.0 h1:5dTKu4I5Dn4P2hxyW3l3jTaZx9ACgg0ECos1eAVrheY= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.10.0/go.mod h1:P5HcUI8obLrCCmM3sbVBohZFH34iszk/+CPWuakZWL8= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.10.0 h1:q/heq5Zh8xV1+7GoMGJpTxM2Lhq5+bFxB29tshuRuw0= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.10.0/go.mod h1:leO2CSTg0Y+LyvmR7Wm4pUxE8KAmaM2GCVx7O+RATLA= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0 h1:QcFwRrZLc82r8wODjvyCbP7Ifp3UANaBSmhDSFjnqSc= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0/go.mod h1:CXIWhUomyWBG/oY2/r/kLp6K/cmx9e/7DLpBuuGdLCA= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.34.0 h1:opwv08VbCZ8iecIWs+McMdHRcAXzjAeda3uG2kI/hcA= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.34.0/go.mod h1:oOP3ABpW7vFHulLpE8aYtNBodrHhMTrvfxUXGvqm7Ac= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 h1:tgJ0uaNS4c98WRNUEx5U3aDlrDOI5Rs+1Vifcw4DJ8U= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0/go.mod h1:U7HYyW0zt/a9x5J1Kjs+r1f/d4ZHnYFclhYY2+YbeoE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 h1:xJ2qHD0C1BeYVTLLR9sX12+Qb95kfeD/byKj6Ky1pXg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0/go.mod h1:u5BF1xyjstDowA1R5QAO9JHzqK+ublenEW/dyqTjBVk= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.10.0 h1:GKCEAZLEpEf78cUvudQdTg0aET2ObOZRB2HtXA0qPAI= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.10.0/go.mod h1:9/zqSWLCmHT/9Jo6fYeUDRRogOLL60ABLsHWS99lF8s= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.34.0 h1:czJDQwFrMbOr9Kk+BPo1y8WZIIFIK58SA1kykuVeiOU= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.34.0/go.mod h1:lT7bmsxOe58Tq+JIOkTQMCGXdu47oA+VJKLZHbaBKbs= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.34.0 h1:jBpDk4HAUsrnVO1FsfCfCOTEc/MkInJmvfCHYLFiT80= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.34.0/go.mod h1:H9LUIM1daaeZaz91vZcfeM0fejXPmgCYE8ZhzqfJuiU= +go.opentelemetry.io/otel/log v0.10.0 h1:1CXmspaRITvFcjA4kyVszuG4HjA61fPDxMb7q3BuyF0= +go.opentelemetry.io/otel/log v0.10.0/go.mod h1:PbVdm9bXKku/gL0oFfUF4wwsQsOPlpo4VEqjvxih+FM= +go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= +go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= +go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= +go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= +go.opentelemetry.io/otel/sdk/log v0.10.0 h1:lR4teQGWfeDVGoute6l0Ou+RpFqQ9vaPdrNJlST0bvw= +go.opentelemetry.io/otel/sdk/log v0.10.0/go.mod h1:A+V1UTWREhWAittaQEG4bYm4gAZa6xnvVu+xKrIRkzo= +go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o= +go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= +go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= +go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= +golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +google.golang.org/genproto/googleapis/api v0.0.0-20250219182151-9fdb1cabc7b2 h1:35ZFtrCgaAjF7AFAK0+lRSf+4AyYnWRbH7og13p7rZ4= +google.golang.org/genproto/googleapis/api v0.0.0-20250219182151-9fdb1cabc7b2/go.mod h1:W9ynFDP/shebLB1Hl/ESTOap2jHd6pmLXPNZC7SVDbA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 h1:e0AIkUUhxyBKh6ssZNrAMeqhA7RKUj42346d1y02i2g= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= +google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/capabilities/writetarget/beholder/common.go b/capabilities/writetarget/beholder/common.go new file mode 100644 index 0000000..7ab46e2 --- /dev/null +++ b/capabilities/writetarget/beholder/common.go @@ -0,0 +1,99 @@ +//nolint:gosec // disable G115 +package beholder + +import ( + "context" + "fmt" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/metric" + + "github.com/smartcontractkit/chainlink-common/pkg/beholder" +) + +// MetricInfo is a struct for metrics information +type MetricsInfoCapBasic struct { + // common + count MetricInfo + capTimestampStart MetricInfo + capTimestampEmit MetricInfo + capDuration MetricInfo // ts.emit - ts.start +} + +// NewMetricsInfoCapBasic creates a new MetricsInfoCapBasic using the provided event/metric information +func NewMetricsInfoCapBasic(metricPrefix, eventRef string) MetricsInfoCapBasic { + return MetricsInfoCapBasic{ + count: MetricInfo{ + Name: fmt.Sprintf("%s_count", metricPrefix), + Unit: "", + Description: fmt.Sprintf("The count of message: '%s' emitted", eventRef), + }, + capTimestampStart: MetricInfo{ + Name: fmt.Sprintf("%s_cap_timestamp_start", metricPrefix), + Unit: "ms", + Description: fmt.Sprintf("The timestamp (local) at capability exec start that resulted in message: '%s' emit", eventRef), + }, + capTimestampEmit: MetricInfo{ + Name: fmt.Sprintf("%s_cap_timestamp_emit", metricPrefix), + Unit: "ms", + Description: fmt.Sprintf("The timestamp (local) at message: '%s' emit", eventRef), + }, + capDuration: MetricInfo{ + Name: fmt.Sprintf("%s_cap_duration", metricPrefix), + Unit: "ms", + Description: fmt.Sprintf("The duration (local) since capability exec start to message: '%s' emit", eventRef), + }, + } +} + +// MetricsCapBasic is a base struct for metrics related to a capability +type MetricsCapBasic struct { + count metric.Int64Counter + capTimestampStart metric.Int64Gauge + capTimestampEmit metric.Int64Gauge + capDuration metric.Int64Gauge // ts.emit - ts.start +} + +// NewMetricsCapBasic creates a new MetricsCapBasic using the provided MetricsInfoCapBasic +func NewMetricsCapBasic(info MetricsInfoCapBasic) (MetricsCapBasic, error) { + meter := beholder.GetMeter() + set := MetricsCapBasic{} + + // Create new metrics + var err error + + set.count, err = info.count.NewInt64Counter(meter) + if err != nil { + return set, fmt.Errorf("failed to create new counter: %w", err) + } + + set.capTimestampStart, err = info.capTimestampStart.NewInt64Gauge(meter) + if err != nil { + return set, fmt.Errorf("failed to create new gauge: %w", err) + } + + set.capTimestampEmit, err = info.capTimestampEmit.NewInt64Gauge(meter) + if err != nil { + return set, fmt.Errorf("failed to create new gauge: %w", err) + } + + set.capDuration, err = info.capDuration.NewInt64Gauge(meter) + if err != nil { + return set, fmt.Errorf("failed to create new gauge: %w", err) + } + + return set, nil +} + +func (m *MetricsCapBasic) RecordEmit(ctx context.Context, start, emit uint64, attrKVs ...attribute.KeyValue) { + // Define attributes + attrs := metric.WithAttributes(attrKVs...) + + // Count events + m.count.Add(ctx, 1, attrs) + + // Timestamp events + m.capTimestampStart.Record(ctx, int64(start), attrs) + m.capTimestampEmit.Record(ctx, int64(emit), attrs) + m.capDuration.Record(ctx, int64(emit-start), attrs) +} diff --git a/capabilities/writetarget/beholder/info.go b/capabilities/writetarget/beholder/info.go new file mode 100644 index 0000000..264b6d7 --- /dev/null +++ b/capabilities/writetarget/beholder/info.go @@ -0,0 +1,39 @@ +package beholder + +import ( + "go.opentelemetry.io/otel/metric" +) + +// Define a new struct for metrics information +type MetricInfo struct { + Name string + Unit string + Description string +} + +// NewInt64Counter creates a new Int64Counter metric +func (m MetricInfo) NewInt64Counter(meter metric.Meter) (metric.Int64Counter, error) { + return meter.Int64Counter( + m.Name, + metric.WithUnit(m.Unit), + metric.WithDescription(m.Description), + ) +} + +// NewInt64Gauge creates a new Int64Gauge metric +func (m MetricInfo) NewInt64Gauge(meter metric.Meter) (metric.Int64Gauge, error) { + return meter.Int64Gauge( + m.Name, + metric.WithUnit(m.Unit), + metric.WithDescription(m.Description), + ) +} + +// NewFloat64Gauge creates a new Float64Gauge metric +func (m MetricInfo) NewFloat64Gauge(meter metric.Meter) (metric.Float64Gauge, error) { + return meter.Float64Gauge( + m.Name, + metric.WithUnit(m.Unit), + metric.WithDescription(m.Description), + ) +} diff --git a/capabilities/writetarget/beholder/metadata.go b/capabilities/writetarget/beholder/metadata.go new file mode 100644 index 0000000..f26846f --- /dev/null +++ b/capabilities/writetarget/beholder/metadata.go @@ -0,0 +1,98 @@ +package beholder + +import ( + "encoding/hex" + + "go.opentelemetry.io/otel/attribute" +) + +const ( + // WorkflowExecutionIDShortLen is the length of the short version of the WorkflowExecutionID (label) + WorkflowExecutionIDShortLen = 3 // first 3 characters, 16^3 = 4.096 possibilities (mid-high cardinality) +) + +// TODO: Refactor as a proto referenced from the other proto files (telemetry messages) +type ExecutionMetadata struct { + // Execution Context - Source + SourceID string + // Execution Context - Chain + ChainFamilyName string + ChainID string + NetworkName string + NetworkNameFull string + // Execution Context - Workflow (capabilities.RequestMetadata) + WorkflowID string + WorkflowOwner string + WorkflowExecutionID string + WorkflowName string + WorkflowDonID uint32 + WorkflowDonConfigVersion uint32 + ReferenceID string + // Execution Context - Capability + CapabilityType string + CapabilityID string + CapabilityTimestampStart uint32 + CapabilityTimestampEmit uint32 +} + +// Attributes returns common attributes used for metrics +func (m ExecutionMetadata) Attributes() []attribute.KeyValue { + // Decode workflow name attribute for output + workflowName := m.decodeWorkflowName() + + return []attribute.KeyValue{ + // Execution Context - Source + attribute.String("source_id", ValOrUnknown(m.SourceID)), + // Execution Context - Chain + attribute.String("chain_family_name", ValOrUnknown(m.ChainFamilyName)), + attribute.String("chain_id", ValOrUnknown(m.ChainID)), + attribute.String("network_name", ValOrUnknown(m.NetworkName)), + attribute.String("network_name_full", ValOrUnknown(m.NetworkNameFull)), + // Execution Context - Workflow (capabilities.RequestMetadata) + attribute.String("workflow_id", ValOrUnknown(m.WorkflowID)), + attribute.String("workflow_owner", ValOrUnknown(m.WorkflowOwner)), + // Notice: We lower the cardinality on the WorkflowExecutionID so it can be used by metrics + // This label has good chances to be unique per workflow, in a reasonable bounded time window + // TODO: enable this when sufficiently tested (PromQL queries like alerts might need to change if this is used) + // attribute.String("workflow_execution_id_short", ValShortOrUnknown(m.WorkflowExecutionID, WorkflowExecutionIDShortLen)), + attribute.String("workflow_name", ValOrUnknown(workflowName)), + attribute.Int64("workflow_don_id", int64(m.WorkflowDonID)), + attribute.Int64("workflow_don_config_version", int64(m.WorkflowDonConfigVersion)), + attribute.String("reference_id", ValOrUnknown(m.ReferenceID)), + // Execution Context - Capability + attribute.String("capability_type", ValOrUnknown(m.CapabilityType)), + attribute.String("capability_id", ValOrUnknown(m.CapabilityID)), + // Notice: we don't include the timestamps here (high cardinality) + } +} + +// decodeWorkflowName decodes the workflow name from hex string to raw string (underlying, output) +func (m ExecutionMetadata) decodeWorkflowName() string { + bytes, err := hex.DecodeString(m.WorkflowName) + if err != nil { + // This should never happen + bytes = []byte("unknown-decode-error") + } + return string(bytes) +} + +// This is needed to avoid issues during exporting OTel metrics to Prometheus +// For more details see https://smartcontract-it.atlassian.net/browse/INFOPLAT-1349 +// ValOrUnknown returns the value if it is not empty, otherwise it returns "unknown" +func ValOrUnknown(val string) string { + if val == "" { + return "unknown" + } + return val +} + +// ValShortOrUnknown returns the short len value if not empty or available, otherwise it returns "unknown" +func ValShortOrUnknown(val string, _len int) string { + if val == "" || _len <= 0 { + return "unknown" + } + if _len > len(val) { + return val + } + return val[:_len] +} diff --git a/capabilities/writetarget/retry/retry.go b/capabilities/writetarget/retry/retry.go new file mode 100644 index 0000000..3aab611 --- /dev/null +++ b/capabilities/writetarget/retry/retry.go @@ -0,0 +1,65 @@ +package retry + +import ( + "context" + "fmt" + "time" + + "github.com/google/uuid" + "github.com/jpillora/backoff" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" +) + +type ctxKey string + +// ctxKeyID is the context key for tracing ID +const ctxKeyID ctxKey = "retryID" + +func CtxWithID(ctx context.Context, retryID string) context.Context { + return context.WithValue(ctx, ctxKeyID, retryID) +} + +// Exponential backoff (default) is used to handle retries with increasing wait times in case of errors +var BackoffStrategyDefault = backoff.Backoff{ + Min: 100 * time.Millisecond, + Max: 3 * time.Second, + Factor: 2, +} + +// WithStrategy applies a retry strategy to a given function. +func WithStrategy[R any](ctx context.Context, lggr logger.Logger, strategy backoff.Backoff, fn func(ctx context.Context) (R, error)) (R, error) { + // Generate a new tracing ID if not present, used to track retries + retryID := ctx.Value(ctxKeyID) + if retryID == nil { + retryID = uuid.New().String() + // Add the generated tracing ID to the context (as it was not already present) + ctx = context.WithValue(ctx, ctxKeyID, retryID) + } + + // Track the number of retries + numRetries := int(strategy.Attempt()) + for { + result, err := fn(ctx) + if err == nil { + return result, nil + } + + wait := strategy.Duration() + message := fmt.Sprintf("Failed to execute function, retrying in %s ...", wait) + lggr.Warnw(message, "wait", wait, "numRetries", numRetries, "retryID", retryID, "err", err) + + select { + case <-ctx.Done(): + return result, fmt.Errorf("context done while executing function {retryID=%s, numRetries=%d}: %w", retryID, numRetries, ctx.Err()) + case <-time.After(wait): + numRetries++ + // Continue with the next retry + } + } +} + +// With applies a default retry strategy to a given function. +func With[R any](ctx context.Context, lggr logger.Logger, fn func(ctx context.Context) (R, error)) (R, error) { + return WithStrategy(ctx, lggr, BackoffStrategyDefault, fn) +} diff --git a/capabilities/writetarget/retry/retry_test.go b/capabilities/writetarget/retry/retry_test.go new file mode 100644 index 0000000..20143f2 --- /dev/null +++ b/capabilities/writetarget/retry/retry_test.go @@ -0,0 +1,123 @@ +package retry + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" +) + +// exampleFunc is a function type used for testing retry strategies +type exampleFunc func(ctx context.Context) (string, error) + +func TestWithRetry(t *testing.T) { + lggr := logger.Test(t) + + tests := []struct { + name string + fn exampleFunc + expected string + errMsg string + timeout time.Duration + }{ + { + name: "successful function", + fn: func(ctx context.Context) (string, error) { + return "success", nil + }, + expected: "success", + timeout: 100 * time.Millisecond, + }, + { + name: "always failing function", + fn: func(ctx context.Context) (string, error) { + return "", errors.New("permanent error") + }, + errMsg: "context done while executing function", + timeout: 100 * time.Millisecond, + }, + { + name: "eventually successful function", + fn: func() exampleFunc { + attempts := 0 + return func(ctx context.Context) (string, error) { + attempts++ + if attempts < 3 { + return "", errors.New("temporary error") + } + return "eventual success", nil + } + }(), + expected: "eventual success", + timeout: 500 * time.Millisecond, + }, + { + name: "eventually successful function (fail - exceeding context timeout)", + fn: func() exampleFunc { + attempts := 0 + return func(ctx context.Context) (string, error) { + attempts++ + if attempts < 3 { + return "", errors.New("temporary error") + } + return "eventual success", nil + } + }(), + errMsg: "context done while executing function", + timeout: 100 * time.Millisecond, + }, + { + name: "eventually (in time) successful function", + fn: func() exampleFunc { + // Start timer, successful after 400ms + timeout := 400 * time.Millisecond + start := time.Now() + return func(ctx context.Context) (string, error) { + if time.Since(start) < timeout { + return "", errors.New("temporary error") + } + return "eventual success", nil + } + }(), + expected: "eventual success", + timeout: 1 * time.Second, + }, + { + name: "eventually (in time) successful function (fail - exceeding context timeout)", + fn: func() exampleFunc { + // Start timer, successful after 4s + timeout := 4 * time.Second + start := time.Now() + return func(ctx context.Context) (string, error) { + if time.Since(start) < timeout { + return "", errors.New("temporary error") + } + return "eventual success", nil + } + }(), + errMsg: "context done while executing function", + timeout: 1 * time.Second, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + ctx, cancel := context.WithTimeout(ctx, tt.timeout) + defer cancel() + + result, err := With(ctx, lggr, tt.fn) + if tt.errMsg != "" { + require.Error(t, err) + require.Contains(t, err.Error(), tt.errMsg) + } else { + require.NoError(t, err) + require.Equal(t, tt.expected, result) + } + }) + } +}