From 53d9e40d35ac2ebd441831a512f4d79a3f1694c3 Mon Sep 17 00:00:00 2001 From: Geert G <117188496+cll-gg@users.noreply.github.com> Date: Fri, 2 Aug 2024 11:38:08 +0200 Subject: [PATCH 01/49] Add Beholder sdk --- .github/CODEOWNERS | 1 + .mockery.yaml | 3 + go.mod | 25 +- go.sum | 40 ++- pkg/beholder/client.go | 332 +++++++++++++++++++ pkg/beholder/client_test.go | 273 +++++++++++++++ pkg/beholder/config.go | 58 ++++ pkg/beholder/config_test.go | 38 +++ pkg/beholder/example_test.go | 157 +++++++++ pkg/beholder/global/global.go | 78 +++++ pkg/beholder/global/global_test.go | 58 ++++ pkg/beholder/internal/exporter.go | 18 + pkg/beholder/internal/mocks/otlp_exporter.go | 177 ++++++++++ pkg/beholder/message.go | 256 ++++++++++++++ pkg/beholder/message_test.go | 131 ++++++++ pkg/beholder/noop.go | 91 +++++ pkg/beholder/pb/example.pb.go | 185 +++++++++++ pkg/beholder/pb/example.proto | 14 + pkg/beholder/pb/generate.go | 3 + 19 files changed, 1927 insertions(+), 11 deletions(-) create mode 100644 pkg/beholder/client.go create mode 100644 pkg/beholder/client_test.go create mode 100644 pkg/beholder/config.go create mode 100644 pkg/beholder/config_test.go create mode 100644 pkg/beholder/example_test.go create mode 100644 pkg/beholder/global/global.go create mode 100644 pkg/beholder/global/global_test.go create mode 100644 pkg/beholder/internal/exporter.go create mode 100644 pkg/beholder/internal/mocks/otlp_exporter.go create mode 100644 pkg/beholder/message.go create mode 100644 pkg/beholder/message_test.go create mode 100644 pkg/beholder/noop.go create mode 100644 pkg/beholder/pb/example.pb.go create mode 100644 pkg/beholder/pb/example.proto create mode 100644 pkg/beholder/pb/generate.go diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index b34b9fb93..d2e72b6eb 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -15,3 +15,4 @@ /pkg/timeutil/ @jmank88 /pkg/values/ @smartcontractkit/keystone /pkg/workflows/ @smartcontractkit/keystone +/pkg/beholder/ @smartcontractkit/realtime diff --git a/.mockery.yaml b/.mockery.yaml index 01391f6bd..95cf7acdf 100644 --- a/.mockery.yaml +++ b/.mockery.yaml @@ -32,3 +32,6 @@ packages: interfaces: CapabilitiesRegistry: Relayer: + github.com/smartcontractkit/chainlink-common/pkg/beholder/internal: + interfaces: + OTLPExporter: diff --git a/go.mod b/go.mod index 94065c9cb..9c210662f 100644 --- a/go.mod +++ b/go.mod @@ -1,12 +1,15 @@ module github.com/smartcontractkit/chainlink-common -go 1.21 +go 1.22 + +toolchain go1.22.6 require ( github.com/confluentinc/confluent-kafka-go/v2 v2.3.0 github.com/dominikbraun/graph v0.23.0 github.com/fxamacker/cbor/v2 v2.5.0 github.com/go-json-experiment/json v0.0.0-20231102232822-2e55bd4e08b0 + github.com/go-playground/validator/v10 v10.22.0 github.com/google/go-cmp v0.6.0 github.com/google/uuid v1.6.0 github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 @@ -29,8 +32,16 @@ require ( github.com/stretchr/testify v1.9.0 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.52.0 go.opentelemetry.io/otel v1.28.0 + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.0.0-20240801233905-f7977e064c9c + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.28.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0 + go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.4.0 + go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.28.0 + go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.28.0 + go.opentelemetry.io/otel/log v0.4.0 go.opentelemetry.io/otel/sdk v1.28.0 + go.opentelemetry.io/otel/sdk/log v0.4.0 + go.opentelemetry.io/otel/sdk/metric v1.28.0 go.opentelemetry.io/otel/trace v1.28.0 go.uber.org/goleak v1.3.0 go.uber.org/zap v1.27.0 @@ -50,13 +61,17 @@ require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/fatih/color v1.14.1 // indirect + github.com/gabriel-vasile/mimetype v1.4.3 // 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/golang/protobuf v1.5.4 // indirect github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb // indirect; indirec github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.21.0 // indirect github.com/hashicorp/yamux v0.1.1 // indirect + github.com/leodido/go-urn v1.4.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.17 // indirect @@ -74,15 +89,15 @@ require ( github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect github.com/x448/float16 v0.8.4 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 // indirect - go.opentelemetry.io/otel/metric v1.28.0 // indirect + go.opentelemetry.io/otel/metric v1.28.0 go.opentelemetry.io/proto/otlp v1.3.1 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/net v0.27.0 // indirect golang.org/x/sync v0.7.0 // indirect golang.org/x/sys v0.22.0 // indirect golang.org/x/text v0.16.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240711142825-46eb208f015d // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240730163845-b1a4ccb954bf // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240730163845-b1a4ccb954bf // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 3b3797a39..5f25f0974 100644 --- a/go.sum +++ b/go.sum @@ -52,6 +52,8 @@ github.com/fatih/color v1.14.1 h1:qfhVLaG5s+nCROl1zJsZRxFeYrHLqWroPOQ8BWiNb4w= github.com/fatih/color v1.14.1/go.mod h1:2oHN61fhTpgcxD3TSWCgKDiH1+x4OiDVVGH8WlgGZGg= github.com/fxamacker/cbor/v2 v2.5.0 h1:oHsG0V/Q6E/wqTS2O1Cozzsy69nqCiguo5Q1a1ADivE= github.com/fxamacker/cbor/v2 v2.5.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo= +github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= github.com/go-json-experiment/json v0.0.0-20231102232822-2e55bd4e08b0 h1:ymLjT4f35nQbASLnvxEde4XOBL+Sn7rFuV+FOJqkljg= github.com/go-json-experiment/json v0.0.0-20231102232822-2e55bd4e08b0/go.mod h1:6daplAwHHGbUGib4990V3Il26O0OC4aRyvewaaAihaA= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -59,6 +61,14 @@ 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.22.0 h1:k6HsTZ0sTnROkhS//R0O+55JgM8C4Bx7ia+JlgcnOao= +github.com/go-playground/validator/v10 v10.22.0/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/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= @@ -100,8 +110,8 @@ github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 h1:qnpS github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1/go.mod h1:lXGCsh6c22WGtjr+qGHj1otzZpV/1kwTMAqkwZsnWRU= github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0 h1:pRhl55Yx1eC7BZ1N+BBWwnKaMyD8uC+34TLdndZMAKk= github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0/go.mod h1:XKMd7iuf/RGPSMJ/U4HP0zS2Z9Fh8Ps9a+6X26m/tmI= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.21.0 h1:CWyXh/jylQWp2dtiV33mY4iSSp6yf4lmn+c7/tN+ObI= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.21.0/go.mod h1:nCLIt0w3Ept2NwF8ThLmrppXsfT07oC8k0XNDxd8sVU= github.com/hashicorp/consul/sdk v0.16.0 h1:SE9m0W6DEfgIVCJX7xU+iv/hUl4m/nxqMTnCdMxDpJ8= github.com/hashicorp/consul/sdk v0.16.0/go.mod h1:7pxqqhqoaPqnBnzXD1StKed62LqJeClzVsUEy85Zr0A= github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c= @@ -124,6 +134,8 @@ 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/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/linkedin/goavro/v2 v2.9.7/go.mod h1:UgQUb2N/pmueQYH9bfqFioWxzYCZXSfF8Jw03O5sjqA= @@ -229,14 +241,30 @@ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.5 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.52.0/go.mod h1:BMsdeOxN04K0L5FNUBfjFdvwWGNe/rkmSwH4Aelu/X0= 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/exporters/otlp/otlplog/otlploggrpc v0.0.0-20240801233905-f7977e064c9c h1:cj2Wk3ZNyGRDmQqjK0QJdS+teMQnvv7+uTiKNqnSjTo= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.0.0-20240801233905-f7977e064c9c/go.mod h1:I011FHqumHsou3RhN8DH/ZnkwFRS1rNyq4EAnu+2Twc= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.28.0 h1:U2guen0GhqH8o/G2un8f/aG/y++OuW6MyCo6hT9prXk= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.28.0/go.mod h1:yeGZANgEcpdx/WK0IvvRFC+2oLiMS2u4L/0Rj2M2Qr0= 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/otlptracegrpc v1.28.0 h1:R3X6ZXmNPRR8ul6i3WgFURCHzaXjHdm0karRG/+dj3s= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0/go.mod h1:QWFXnDavXWwMx2EEcZsf3yxgEKAqsxQ+Syjp+seyInw= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.4.0 h1:0MH3f8lZrflbUWXVxyBg/zviDFdGE062uKh5+fu8Vv0= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.4.0/go.mod h1:Vh68vYiHY5mPdekTr0ox0sALsqjoVy0w3Os278yX5SQ= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.28.0 h1:BJee2iLkfRfl9lc7aFmBwkWxY/RI1RDdXepSF6y8TPE= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.28.0/go.mod h1:DIzlHs3DRscCIBU3Y9YSzPfScwnYnzfnCd4g8zA7bZc= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.28.0 h1:EVSnY9JbEEW92bEkIYOVMw4q1WJxIAGoFTrtYOzWuRQ= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.28.0/go.mod h1:Ea1N1QQryNXpCD0I1fdLibBAIpQuBkznMmkdKrapk1Y= +go.opentelemetry.io/otel/log v0.4.0 h1:/vZ+3Utqh18e8TPjuc3ecg284078KWrR8BRz+PQAj3o= +go.opentelemetry.io/otel/log v0.4.0/go.mod h1:DhGnQvky7pHy82MIRV43iXh3FlKN8UUKftn0KbLOq6I= 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/sdk/log v0.4.0 h1:1mMI22L82zLqf6KtkjrRy5BbagOTWdJsqMY/HSqILAA= +go.opentelemetry.io/otel/sdk/log v0.4.0/go.mod h1:AYJ9FVF0hNOgAVzUG/ybg/QttnXhUePWAupmCqtdESo= +go.opentelemetry.io/otel/sdk/metric v1.28.0 h1:OkuaKgKrgAbYrrY0t92c+cC+2F6hsFNnCQArXCKlg08= +go.opentelemetry.io/otel/sdk/metric v1.28.0/go.mod h1:cWPjykihLAPvXKi4iZc1dpER3Jdq2Z0YLse3moQUCpg= 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/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= @@ -330,10 +358,10 @@ google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoA google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20210401141331-865547bb08e2/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= -google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d h1:kHjw/5UfflP/L5EbledDrcG4C2597RtymmGRZvHiCuY= -google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d/go.mod h1:mw8MG/Qz5wfgYr6VqVCiZcHe/GJEfI+oGGDCohaVgB0= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240711142825-46eb208f015d h1:JU0iKnSg02Gmb5ZdV8nYsKEKsP6o/FGVWTrw4i1DA9A= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240711142825-46eb208f015d/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= +google.golang.org/genproto/googleapis/api v0.0.0-20240730163845-b1a4ccb954bf h1:GillM0Ef0pkZPIB+5iO6SDK+4T9pf6TpaYR6ICD5rVE= +google.golang.org/genproto/googleapis/api v0.0.0-20240730163845-b1a4ccb954bf/go.mod h1:OFMYQFHJ4TM3JRlWDZhJbZfra2uqc3WLBZiaaqP4DtU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240730163845-b1a4ccb954bf h1:liao9UHurZLtiEwBgT9LMOnKYsHze6eA6w1KQCMVN2Q= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240730163845-b1a4ccb954bf/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= diff --git a/pkg/beholder/client.go b/pkg/beholder/client.go new file mode 100644 index 000000000..cdebed97a --- /dev/null +++ b/pkg/beholder/client.go @@ -0,0 +1,332 @@ +package beholder + +import ( + "context" + "errors" + + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc" + "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" + otellog "go.opentelemetry.io/otel/log" + otelglobal "go.opentelemetry.io/otel/log/global" + otelmetric "go.opentelemetry.io/otel/metric" + "go.opentelemetry.io/otel/propagation" + sdklog "go.opentelemetry.io/otel/sdk/log" + sdkmetric "go.opentelemetry.io/otel/sdk/metric" + sdkresource "go.opentelemetry.io/otel/sdk/resource" + "go.opentelemetry.io/otel/sdk/trace" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + oteltrace "go.opentelemetry.io/otel/trace" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/credentials/insecure" +) + +type Emitter interface { + // Sends message with bytes and attributes to OTel Collector + Emit(ctx context.Context, body []byte, attrs map[string]any) error + // Sends message to OTel Collector + EmitMessage(ctx context.Context, m Message) error +} +type Client interface { + Logger() otellog.Logger + Tracer() oteltrace.Tracer + Meter() otelmetric.Meter + Emitter() Emitter + Close() error +} + +var _ Client = (*otelClient)(nil) + +type messageEmitter struct { + exporter sdklog.Exporter + messageLogger otellog.Logger +} + +type otelClient struct { + config Config + // Logger + logger otellog.Logger + // Tracer + tracer oteltrace.Tracer + // Meter + meter otelmetric.Meter + // Message Emitter + emitter Emitter + // Graceful shutdown for tracer, meter, logger providers + closeFunc func() error +} + +func NewClient( + config Config, + logger otellog.Logger, + tracer oteltrace.Tracer, + meter otelmetric.Meter, + emitter Emitter, + onClose func() error, +) Client { + return &otelClient{ + config: config, + logger: logger, + tracer: tracer, + meter: meter, + emitter: emitter, + closeFunc: onClose, + } +} + +// NewOtelClient creates a new BeholderClient with OTel exporter +func NewOtelClient(cfg Config, errorHandler errorHandlerFunc) (Client, error) { + factory := func(ctx context.Context, options ...otlploggrpc.Option) (sdklog.Exporter, error) { + return otlploggrpc.New(ctx, options...) + } + return newOtelClient(cfg, errorHandler, factory) +} + +// Used for testing to override the default exporter +type otlploggrpcFactory func(ctx context.Context, options ...otlploggrpc.Option) (sdklog.Exporter, error) + +func newOtelClient(cfg Config, errorHandler errorHandlerFunc, otlploggrpcNew otlploggrpcFactory) (Client, error) { + ctx := context.Background() + baseResource, err := newOtelResource(cfg) + if err != nil { + return nil, err + } + creds := insecure.NewCredentials() + if !cfg.InsecureConnection && cfg.CACertFile != "" { + creds, err = credentials.NewClientTLSFromFile(cfg.CACertFile, "") + if err != nil { + return nil, err + } + } + sharedLogExporter, err := otlploggrpcNew( + ctx, + otlploggrpc.WithTLSCredentials(creds), + otlploggrpc.WithEndpoint(cfg.OtelExporterGRPCEndpoint), + ) + if err != nil { + return nil, err + } + + // Logger + loggerProcessor := sdklog.NewBatchProcessor( + sharedLogExporter, + sdklog.WithExportTimeout(cfg.LogExportTimeout), // Default is 30s + ) + loggerAttributes := []attribute.KeyValue{ + attribute.String("beholder_data_type", "zap_log_message"), + } + loggerResource, err := sdkresource.Merge( + sdkresource.NewSchemaless(loggerAttributes...), + baseResource, + ) + if err != nil { + return nil, err + } + loggerProvider := sdklog.NewLoggerProvider( + sdklog.WithResource(loggerResource), + sdklog.WithProcessor(loggerProcessor), + ) + logger := loggerProvider.Logger(cfg.PackageName) + + // Set global logger provider + otelglobal.SetLoggerProvider(loggerProvider) + + // Tracer + tracerProvider, err := newTracerProvider(cfg, baseResource, creds) + if err != nil { + return nil, err + } + tracer := tracerProvider.Tracer(cfg.PackageName) + otel.SetTracerProvider(tracerProvider) + otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{})) + + // Meter + meterProvider, err := newMeterProvider(cfg, baseResource, creds) + if err != nil { + return nil, err + } + meter := meterProvider.Meter(cfg.PackageName) + otel.SetMeterProvider(meterProvider) + + // Message Emitter + messageLogProcessor := sdklog.NewBatchProcessor( + sharedLogExporter, + sdklog.WithExportTimeout(cfg.EmitterExportTimeout), // Default is 30s + ) + messageAttributes := []attribute.KeyValue{ + attribute.String("beholder_data_type", "custom_message"), + } + messageLoggerResource, err := sdkresource.Merge( + sdkresource.NewSchemaless(messageAttributes...), + baseResource, + ) + if err != nil { + return nil, err + } + messageLoggerProvider := sdklog.NewLoggerProvider( + sdklog.WithResource(messageLoggerResource), + sdklog.WithProcessor(messageLogProcessor), + ) + messageLogger := messageLoggerProvider.Logger(cfg.PackageName) + messageEmitter := newMessageEmitter(sharedLogExporter, messageLogger) + + setOtelErrorHandler(errorHandler) + + onClose := closeFunc(ctx, loggerProvider, messageLoggerProvider, tracerProvider, meterProvider) + + client := NewClient(cfg, logger, tracer, meter, messageEmitter, onClose) + + return client, nil +} + +type errorHandlerFunc func(err error) + +// Sets the global error handler for OpenTelemetry +func setOtelErrorHandler(h errorHandlerFunc) { + otel.SetErrorHandler(otel.ErrorHandlerFunc(h)) +} + +func newOtelResource(cfg Config) (resource *sdkresource.Resource, err error) { + extraResources, err := sdkresource.New( + context.Background(), + sdkresource.WithOS(), + sdkresource.WithContainer(), + sdkresource.WithHost(), + ) + if err != nil { + return nil, err + } + resource, err = sdkresource.Merge( + sdkresource.Default(), + extraResources, + ) + if err != nil { + return nil, err + } + // Add custom resource attributes + attrs := make([]attribute.KeyValue, 0, len(cfg.ResourceAttributes)) + for k, v := range cfg.ResourceAttributes { + attrs = append(attrs, attribute.String(k, v)) + } + resource, err = sdkresource.Merge( + sdkresource.NewSchemaless(attrs...), + resource, + ) + if err != nil { + return nil, err + } + return +} + +func newMessageEmitter( + exporter sdklog.Exporter, + messageLogger otellog.Logger, +) Emitter { + return messageEmitter{ + exporter: exporter, + messageLogger: messageLogger, + } +} + +// Emits logs the message, but does not wait for the message to be processed. +// Open question: what are pros/cons for using use map[]any vs use otellog.KeyValue +func (e messageEmitter) Emit(ctx context.Context, body []byte, attrs map[string]any) error { + message := NewMessage(body, attrs) + if err := message.Validate(); err != nil { + return err + } + e.messageLogger.Emit(ctx, message.OtelRecord()) + return nil +} + +func (e messageEmitter) EmitMessage(ctx context.Context, message Message) error { + if err := message.Validate(); err != nil { + return err + } + e.messageLogger.Emit(ctx, message.OtelRecord()) + return nil +} + +func (b *otelClient) Logger() otellog.Logger { + return b.logger +} + +func (b *otelClient) Tracer() oteltrace.Tracer { + return b.tracer +} + +func (b *otelClient) Meter() otelmetric.Meter { + return b.meter +} +func (b *otelClient) Emitter() Emitter { + return b.emitter +} + +func (b *otelClient) Close() error { + if b.closeFunc != nil { + return b.closeFunc() + } + return nil +} + +type otelProvider interface { + Shutdown(ctx context.Context) error +} + +// Returns function that finalizes all providers +func closeFunc(ctx context.Context, providers ...otelProvider) func() error { + return func() (err error) { + for _, provider := range providers { + err = errors.Join(err, provider.Shutdown(ctx)) + } + return + } +} + +func newTracerProvider(config Config, resource *sdkresource.Resource, creds credentials.TransportCredentials) (*sdktrace.TracerProvider, error) { + ctx := context.Background() + + exporter, err := otlptracegrpc.New(ctx, + otlptracegrpc.WithTLSCredentials(creds), + otlptracegrpc.WithEndpoint(config.OtelExporterGRPCEndpoint), + ) + if err != nil { + return nil, err + } + tp := sdktrace.NewTracerProvider( + sdktrace.WithBatcher(exporter, + trace.WithBatchTimeout(config.TraceBatchTimeout)), // Default is 5s + sdktrace.WithResource(resource), + sdktrace.WithSampler( + sdktrace.ParentBased( + sdktrace.TraceIDRatioBased(config.TraceSampleRate), + ), + ), + ) + return tp, nil +} + +func newMeterProvider(config Config, resource *sdkresource.Resource, creds credentials.TransportCredentials) (*sdkmetric.MeterProvider, error) { + ctx := context.Background() + + exporter, err := otlpmetricgrpc.New( + ctx, + otlpmetricgrpc.WithTLSCredentials(creds), + otlpmetricgrpc.WithEndpoint(config.OtelExporterGRPCEndpoint), + ) + if err != nil { + return nil, err + } + + mp := sdkmetric.NewMeterProvider( + sdkmetric.WithReader( + sdkmetric.NewPeriodicReader( + exporter, + sdkmetric.WithInterval(config.MetricReaderInterval), // Default is 10s + )), + sdkmetric.WithResource(resource), + ) + return mp, nil +} diff --git a/pkg/beholder/client_test.go b/pkg/beholder/client_test.go new file mode 100644 index 000000000..02f8e56fd --- /dev/null +++ b/pkg/beholder/client_test.go @@ -0,0 +1,273 @@ +package beholder + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc" + otellog "go.opentelemetry.io/otel/log" + sdklog "go.opentelemetry.io/otel/sdk/log" + + "github.com/smartcontractkit/chainlink-common/pkg/beholder/internal/mocks" +) + +type MockExporter struct { + mock.Mock + sdklog.Exporter +} + +func (m *MockExporter) Export(ctx context.Context, records []sdklog.Record) error { + args := m.Called(ctx, records) + return args.Error(0) +} + +func (m *MockExporter) Shutdown(ctx context.Context) error { + args := m.Called(ctx) + return args.Error(0) +} + +func (m *MockExporter) ForceFlush(ctx context.Context) error { + args := m.Called(ctx) + return args.Error(0) +} + +func TestClient(t *testing.T) { + defaultCustomAttributes := func() map[string]any { + return map[string]any{ + "int_key_1": 123, + "int64_key_1": int64(123), + "int32_key_1": int32(123), + "str_key_1": "str_val_1", + "bool_key_1": true, + "float_key_1": 123.456, + "byte_key_1": []byte("byte_val_1"), + "str_slice_key_1": []string{"str_val_1", "str_val_2"}, + "nil_key_1": nil, + "beholder_data_schema": "/schemas/ids/1001", // Required field, URI + } + } + defaultMessageBody := []byte("body bytes") + + tests := []struct { + name string + makeCustomAttributes func() map[string]any + messageBody []byte + messageCount int + exporterMockErrorCount int + exporterOutputExpected bool + messageGenerator func(client Client, messageBody []byte, customAttributes map[string]any) + }{ + { + name: "Test Emit", + makeCustomAttributes: defaultCustomAttributes, + messageBody: defaultMessageBody, + messageCount: 10, + exporterMockErrorCount: 0, + exporterOutputExpected: true, + messageGenerator: func(client Client, messageBody []byte, customAttributes map[string]any) { + err := client.Emitter().Emit(context.Background(), messageBody, customAttributes) + assert.NoError(t, err) + }, + }, { + name: "Test EmitMessage", + makeCustomAttributes: defaultCustomAttributes, + messageBody: defaultMessageBody, + messageCount: 10, + exporterMockErrorCount: 0, + exporterOutputExpected: true, + messageGenerator: func(client Client, messageBody []byte, customAttributes map[string]any) { + message := NewMessage(messageBody, customAttributes) + err := client.Emitter().EmitMessage(context.Background(), message) + assert.NoError(t, err) + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + exporterMock := mocks.NewOTLPExporter(t) + defer exporterMock.AssertExpectations(t) + + otelErrorHandler := func(err error) { + t.Fatalf("otel error: %v", err) + } + // Override exporter factory which is used by BeholderClient + exporterFactory := func(context.Context, ...otlploggrpc.Option) (sdklog.Exporter, error) { + return exporterMock, nil + } + client, err := newOtelClient(DefaultConfig(), otelErrorHandler, exporterFactory) + if err != nil { + t.Fatalf("Error creating beholder client: %v", err) + } + // Number of messages to emit + done := make(chan struct{}, 1) + + // Simulate exporter error if configured + if tc.exporterMockErrorCount > 0 { + exporterMock.On("Export", mock.Anything, mock.Anything).Return(fmt.Errorf("an error occurred")).Times(tc.exporterMockErrorCount) + } + + customAttributes := tc.makeCustomAttributes() + + if tc.exporterOutputExpected { + exporterMock.On("Export", mock.Anything, mock.Anything).Return(nil).Once(). + Run(func(args mock.Arguments) { + assert.IsType(t, args.Get(1), []sdklog.Record{}, "Record type mismatch") + records := args.Get(1).([]sdklog.Record) + assert.Equal(t, tc.messageCount, len(records), "Record count mismatch") + record := records[0] + assert.Equal(t, tc.messageBody, record.Body().AsBytes(), "Record body mismatch") + actualAttributeKeys := map[string]struct{}{} + record.WalkAttributes(func(kv otellog.KeyValue) bool { + key := kv.Key + actualAttributeKeys[key] = struct{}{} + expectedValue, ok := customAttributes[key] + if !ok { + t.Fatalf("Record attribute key not found: %s", key) + } + expectedKv := OtelAttr(key, expectedValue) + equal := kv.Value.Equal(expectedKv.Value) + assert.True(t, equal, fmt.Sprintf("Record attributes mismatch for key %v", key)) + return true + }) + for key := range customAttributes { + if _, ok := actualAttributeKeys[key]; !ok { + t.Fatalf("Record attribute key not found: %s", key) + } + } + done <- struct{}{} + }) + } + for i := 0; i < tc.messageCount; i++ { + go tc.messageGenerator(client, tc.messageBody, customAttributes) + } + + select { + case <-done: + case <-time.After(10 * time.Second): + t.Fatalf("Timed out waiting for messages to be emitted") + } + }) + } +} + +func TestEmitterMessageValidation(t *testing.T) { + getEmitter := func(exporterMock *mocks.OTLPExporter) Emitter { + client, err := newOtelClient( + DefaultConfig(), + func(err error) { t.Fatalf("otel error: %v", err) }, + // Override exporter factory which is used by BeholderClient + func(context.Context, ...otlploggrpc.Option) (sdklog.Exporter, error) { + return exporterMock, nil + }, + ) + assert.NoError(t, err) + return client.Emitter() + } + + for _, tc := range []struct { + name string + attrs Attributes + exporterCalledTimes int + expectedError string + }{ + { + name: "Missing required attribute", + attrs: Attributes{ + "key": "value", + }, + exporterCalledTimes: 0, + expectedError: "'Metadata.BeholderDataSchema' Error:Field validation for 'BeholderDataSchema' failed on the 'required' tag", + }, + { + name: "Invalid URI", + attrs: Attributes{ + "beholder_data_schema": "example-schema", + }, + exporterCalledTimes: 0, + expectedError: "'Metadata.BeholderDataSchema' Error:Field validation for 'BeholderDataSchema' failed on the 'uri' tag", + }, + { + name: "Valid URI", + exporterCalledTimes: 1, + attrs: Attributes{ + "beholder_data_schema": "/example-schema/versions/1", + }, + expectedError: "", + }, + } { + t.Run(tc.name, func(t *testing.T) { + setupMock := func(exporterMock *mocks.OTLPExporter) (*mocks.OTLPExporter, <-chan struct{}) { + done := make(chan struct{}, tc.exporterCalledTimes) + if tc.exporterCalledTimes > 0 { + exporterMock.On("Export", mock.Anything, mock.Anything).Return(nil).Times(tc.exporterCalledTimes). + Run(func(args mock.Arguments) { + done <- struct{}{} + }) + } + return exporterMock, done + } + + assertError := func(err error, expected string) { + if tc.expectedError != "" { + assert.ErrorContains(t, err, expected) + } else { + assert.NoError(t, err) + } + } + + assertMock := func(exporterMock *mocks.OTLPExporter) { + if tc.exporterCalledTimes > 0 { + exporterMock.AssertExpectations(t) + } else { + exporterMock.AssertNotCalled(t, "Export") + } + } + + waitUntilSent := func(done <-chan struct{}) { + for range tc.exporterCalledTimes { + select { + case <-done: + case <-time.After(10 * time.Second): + t.Fatalf("Timed out waiting for messages to be emitted") + } + } + } + + setupTest := func() (emitter Emitter, message Message, assertExpectations func(err error)) { + exporterMock, done := setupMock(mocks.NewOTLPExporter(t)) + emitter = getEmitter(exporterMock) + message = NewMessage([]byte("test"), tc.attrs) + + assertExpectations = func(err error) { + assertError(err, tc.expectedError) + if err == nil { + waitUntilSent(done) + } + assertMock(exporterMock) + } + return + } + + t.Run("Emitter.EmitMessage", func(t *testing.T) { + emitter, message, assertExpectations := setupTest() + + err := emitter.EmitMessage(context.Background(), message) + + assertExpectations(err) + }) + + t.Run("Emitter.Emit", func(t *testing.T) { + emitter, message, assertExpectations := setupTest() + + err := emitter.Emit(context.Background(), message.Body, tc.attrs) + + assertExpectations(err) + }) + }) + } +} diff --git a/pkg/beholder/config.go b/pkg/beholder/config.go new file mode 100644 index 000000000..a7f7dd0b9 --- /dev/null +++ b/pkg/beholder/config.go @@ -0,0 +1,58 @@ +package beholder + +import ( + "time" + + otelattr "go.opentelemetry.io/otel/attribute" +) + +type Config struct { + InsecureConnection bool + CACertFile string + OtelExporterGRPCEndpoint string + + PackageName string + // OTel Resource + ResourceAttributes map[string]string + // EventEmitter + EmitterExportTimeout time.Duration + // OTel Trace + TraceSampleRate float64 + TraceBatchTimeout time.Duration + // OTel Metric + MetricReaderInterval time.Duration + // OTel Log + LogExportTimeout time.Duration +} + +var defaultOtelAttributes = map[string]string{ + "package_name": "beholder", +} + +func DefaultConfig() Config { + return Config{ + InsecureConnection: true, + CACertFile: "", + OtelExporterGRPCEndpoint: "localhost:4317", + PackageName: "beholder", + // Resource + ResourceAttributes: defaultOtelAttributes, + // EventEmitter + EmitterExportTimeout: 1 * time.Second, + // Trace + TraceSampleRate: 1, + TraceBatchTimeout: 1 * time.Second, + // Metric + MetricReaderInterval: 1 * time.Second, + // Log + LogExportTimeout: 1 * time.Second, + } +} + +func (c Config) Attributes() []otelattr.KeyValue { + attrs := make([]otelattr.KeyValue, 0, len(c.ResourceAttributes)) + for k, v := range c.ResourceAttributes { + attrs = append(attrs, otelattr.String(k, v)) + } + return attrs +} diff --git a/pkg/beholder/config_test.go b/pkg/beholder/config_test.go new file mode 100644 index 000000000..523376f60 --- /dev/null +++ b/pkg/beholder/config_test.go @@ -0,0 +1,38 @@ +package beholder_test + +import ( + "fmt" + "time" + + beholder "github.com/smartcontractkit/chainlink-common/pkg/beholder" +) + +const ( + packageName = "beholder" +) + +func ExampleConfig() { + config := beholder.Config{ + InsecureConnection: true, + CACertFile: "", + OtelExporterGRPCEndpoint: "localhost:4317", + PackageName: packageName, + // Resource + ResourceAttributes: map[string]string{ + "package_name": packageName, + "sender": "beholdeclient", + }, + // EventEmitter + EmitterExportTimeout: 1 * time.Second, + // Trace + TraceSampleRate: 1, + TraceBatchTimeout: 1 * time.Second, + // Metric + MetricReaderInterval: 1 * time.Second, + // Log + LogExportTimeout: 1 * time.Second, + } + fmt.Printf("%+v", config) + // Output: + // {InsecureConnection:true CACertFile: OtelExporterGRPCEndpoint:localhost:4317 PackageName:beholder ResourceAttributes:map[package_name:beholder sender:beholdeclient] EmitterExportTimeout:1s TraceSampleRate:1 TraceBatchTimeout:1s MetricReaderInterval:1s LogExportTimeout:1s} +} diff --git a/pkg/beholder/example_test.go b/pkg/beholder/example_test.go new file mode 100644 index 000000000..26043c93c --- /dev/null +++ b/pkg/beholder/example_test.go @@ -0,0 +1,157 @@ +package beholder_test + +import ( + "context" + "fmt" + "log" + "sync" + "time" + + otelattribute "go.opentelemetry.io/otel/attribute" + otellog "go.opentelemetry.io/otel/log" + oteltrace "go.opentelemetry.io/otel/trace" + "google.golang.org/protobuf/proto" + + "github.com/smartcontractkit/chainlink-common/pkg/beholder" + "github.com/smartcontractkit/chainlink-common/pkg/beholder/global" + "github.com/smartcontractkit/chainlink-common/pkg/beholder/pb" +) + +func ExampleClient() { + ctx := context.Background() + + client, err := beholder.NewOtelClient(beholder.DefaultConfig(), errorHandler) + if err != nil { + log.Fatalf("Error creating beholder client: %v", err) + } + var wg sync.WaitGroup + for i := range 3 { + wg.Add(1) + fmt.Printf("Emitting message %d\n", i) + go func(i int) { + // Create message metadata + metadata := beholder.Metadata{ + DonID: "test_don_id", + NetworkName: []string{"test_network"}, + NetworkChainID: "test_chain_id", + BeholderDataSchema: "/custom-message/versions/1", + } + // Create custom message + customMessage := beholder.Message{ + // Set protobuf message bytes as body + Body: newMessageBytes(i), + // Set metadata attributes + Attrs: metadata.Attributes().Add( + // Add custom attributes + "timestamp", time.Now().Unix(), + "sender", "example-client", + ), + } + // Get message emitter + em := client.Emitter() + // Emit custom message + err := em.EmitMessage(ctx, customMessage) + if err != nil { + log.Fatalf("Error emitting message: %v", err) + } + wg.Done() + }(i) + } + wg.Wait() + // Output: + // Emitting message 0 + // Emitting message 1 + // Emitting message 2 +} + +func newMessageBytes(i int) []byte { + // Create protobuf message + customMessagePb := &pb.CustomMessage{} + customMessagePb.BoolVal = true + customMessagePb.IntVal = int64(i) + customMessagePb.FloatVal = float32(i) + customMessagePb.StringVal = fmt.Sprintf("string-value-%d", i) + customMessagePb.BytesVal = []byte{byte(i)} + // Encode protobuf message + customMessageBytes, err := proto.Marshal(customMessagePb) + if err != nil { + log.Fatalf("Error encoding message: %v", err) + } + return customMessageBytes +} + +func errorHandler(e error) { + if e != nil { + log.Fatalf("otel error: %v", e) + } +} + +func asseetNoError(err error) { + if err != nil { + panic(err) + } +} + +func ExampleEmitter() { + ctx := context.Background() + // Initialize beholder client + c, err := beholder.NewOtelClient(beholder.DefaultConfig(), asseetNoError) + if err != nil { + log.Fatalf("Error creating beholder client: %v", err) + } + var client beholder.Client = c + + // Set global client so it will be accessible from anywhere through beholder/global functions + global.SetClient(&client) + // After that you can use global functions to get logger, tracer, meter, messageEmitter + logger, tracer, meter, messageEmitter := global.Logger(), global.Tracer(), global.Meter(), global.Emitter() + + fmt.Println("Emit otel log record") + logger.Emit(ctx, otellog.Record{}) + + fmt.Println("Create trace span") + ctx, span := tracer.Start(ctx, "ExampleGlobalClient", oteltrace.WithAttributes(otelattribute.String("key", "value"))) + defer span.End() + + fmt.Println("Create metric counter") + counter, _ := meter.Int64Counter("global_counter") + counter.Add(ctx, 1) + + fmt.Println("Emit custom message") + err = messageEmitter.Emit(ctx, []byte("test"), beholder.Attributes{ + "key": "value", + "beholder_data_schema": "/test/versions/1", + }) + if err != nil { + log.Fatalf("Error emitting message: %v", err) + } + // Output: + // Emit otel log record + // Create trace span + // Create metric counter + // Emit custom message +} + +func ExampleBootstrap() { + beholderConfig := beholder.DefaultConfig() + + // Bootstrap Beholder Client + err := global.Bootstrap(beholderConfig, errorHandler) + if err != nil { + log.Fatalf("Error bootstrapping Beholder: %v", err) + } + + payloadBytes := newMessageBytes(0) + + // Emit custom message + for range 3 { + err := global.Emit(context.Background(), payloadBytes, beholder.Attributes{ + "beholder_data_type": "custom_message", + "foo": "bar", + }) + if err != nil { + log.Printf("Error emitting message: %v", err) + } + } + // Output: +} diff --git a/pkg/beholder/global/global.go b/pkg/beholder/global/global.go new file mode 100644 index 000000000..350d3353e --- /dev/null +++ b/pkg/beholder/global/global.go @@ -0,0 +1,78 @@ +package global + +import ( + "context" + "sync/atomic" + + otellog "go.opentelemetry.io/otel/log" + otelmetric "go.opentelemetry.io/otel/metric" + oteltrace "go.opentelemetry.io/otel/trace" + + "github.com/smartcontractkit/chainlink-common/pkg/beholder" +) + +// Pointer to the global Beholder Client +var globalBeholderClient = defaultBeholderClient() + +// SetClient sets the global Beholder Client +func SetClient(client *beholder.Client) { + globalBeholderClient.Store(client) +} + +// Returns the global Beholder Client +// Its thread-safe and can be used concurrently +func GetClient() beholder.Client { + ptr := globalBeholderClient.Load() + return *ptr +} + +func Logger() otellog.Logger { + return GetClient().Logger() +} + +func Tracer() oteltrace.Tracer { + return GetClient().Tracer() +} + +func Meter() otelmetric.Meter { + return GetClient().Meter() +} + +func Emitter() beholder.Emitter { + return GetClient().Emitter() +} + +func SpanFromContext(ctx context.Context) oteltrace.Span { + return oteltrace.SpanFromContext(ctx) +} + +func defaultBeholderClient() *atomic.Pointer[beholder.Client] { + ptr := &atomic.Pointer[beholder.Client]{} + client := beholder.NewNoopClient() + ptr.Store(&client) + return ptr +} + +func EmitMessage(ctx context.Context, message beholder.Message) error { + return Emitter().EmitMessage(ctx, message) +} + +func Emit(ctx context.Context, body []byte, attrs beholder.Attributes) error { + return Emitter().Emit(ctx, body, attrs) +} + +func Bootstrap(cfg beholder.Config, errorHandler func(error)) error { + // Initialize beholder client + c, err := beholder.NewOtelClient(cfg, errorHandler) + if err != nil { + return err + } + var client beholder.Client = c + // Set global client so it will be accessible from anywhere through beholder/global functions + SetClient(&client) + return nil +} + +func NewConfig() beholder.Config { + return beholder.DefaultConfig() +} diff --git a/pkg/beholder/global/global_test.go b/pkg/beholder/global/global_test.go new file mode 100644 index 000000000..e5c3cefbe --- /dev/null +++ b/pkg/beholder/global/global_test.go @@ -0,0 +1,58 @@ +package global_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + + otelattribute "go.opentelemetry.io/otel/attribute" + otellog "go.opentelemetry.io/otel/log" + otellognoop "go.opentelemetry.io/otel/log/noop" + otelmetricnoop "go.opentelemetry.io/otel/metric/noop" + oteltrace "go.opentelemetry.io/otel/trace" + oteltracenoop "go.opentelemetry.io/otel/trace/noop" + + "github.com/smartcontractkit/chainlink-common/pkg/beholder" + "github.com/smartcontractkit/chainlink-common/pkg/beholder/global" +) + +func TestGlobal(t *testing.T) { + // Get global logger, tracer, meter, messageEmitter + // If not initialized with global.SetClient will return noop client + logger, tracer, meter, messageEmitter := global.Logger(), global.Tracer(), global.Meter(), global.Emitter() + noopClient := beholder.NewNoopClient() + assert.IsType(t, otellognoop.Logger{}, logger) + assert.IsType(t, oteltracenoop.Tracer{}, tracer) + assert.IsType(t, otelmetricnoop.Meter{}, meter) + expectedMessageEmitter := beholder.NewNoopClient().Emitter() + assert.IsType(t, expectedMessageEmitter, messageEmitter) + + assert.IsType(t, noopClient, global.GetClient()) + assert.NotSame(t, noopClient, global.GetClient()) + + // Set global client so it will be accessible from anywhere through beholder/global functions + var client beholder.Client = noopClient + global.SetClient(&client) + assert.Same(t, noopClient, global.GetClient()) + + // After that use global functions to get logger, tracer, meter, messageEmitter + logger, tracer, meter, messageEmitter = global.Logger(), global.Tracer(), global.Meter(), global.Emitter() + + // Emit otel log record + logger.Emit(context.Background(), otellog.Record{}) + + // Create trace span + ctx, span := tracer.Start(context.Background(), "ExampleGlobalClient", oteltrace.WithAttributes(otelattribute.String("key", "value"))) + defer span.End() + + // Create metric counter + counter, _ := meter.Int64Counter("global_counter") + counter.Add(context.Background(), 1) + + // Emit custom message + err := messageEmitter.Emit(ctx, []byte("test"), beholder.Attributes{"key": "value"}) + if err != nil { + t.Fatalf("Error emitting message: %v", err) + } +} diff --git a/pkg/beholder/internal/exporter.go b/pkg/beholder/internal/exporter.go new file mode 100644 index 000000000..271077a5c --- /dev/null +++ b/pkg/beholder/internal/exporter.go @@ -0,0 +1,18 @@ +package internal + +import ( + "context" + + "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc" + sdklog "go.opentelemetry.io/otel/sdk/log" +) + +var _ sdklog.Exporter = (*otlploggrpc.Exporter)(nil) +var _ OTLPExporter = (*otlploggrpc.Exporter)(nil) + +// Copy of sdklog.Exporter interface, used for mocking +type OTLPExporter interface { + Export(ctx context.Context, records []sdklog.Record) error + Shutdown(ctx context.Context) error + ForceFlush(ctx context.Context) error +} diff --git a/pkg/beholder/internal/mocks/otlp_exporter.go b/pkg/beholder/internal/mocks/otlp_exporter.go new file mode 100644 index 000000000..c5feb8aa2 --- /dev/null +++ b/pkg/beholder/internal/mocks/otlp_exporter.go @@ -0,0 +1,177 @@ +// Code generated by mockery v2.43.2. DO NOT EDIT. + +package mocks + +import ( + context "context" + + log "go.opentelemetry.io/otel/sdk/log" + + mock "github.com/stretchr/testify/mock" +) + +// OTLPExporter is an autogenerated mock type for the OTLPExporter type +type OTLPExporter struct { + mock.Mock +} + +type OTLPExporter_Expecter struct { + mock *mock.Mock +} + +func (_m *OTLPExporter) EXPECT() *OTLPExporter_Expecter { + return &OTLPExporter_Expecter{mock: &_m.Mock} +} + +// Export provides a mock function with given fields: ctx, records +func (_m *OTLPExporter) Export(ctx context.Context, records []log.Record) error { + ret := _m.Called(ctx, records) + + if len(ret) == 0 { + panic("no return value specified for Export") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, []log.Record) error); ok { + r0 = rf(ctx, records) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// OTLPExporter_Export_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Export' +type OTLPExporter_Export_Call struct { + *mock.Call +} + +// Export is a helper method to define mock.On call +// - ctx context.Context +// - records []log.Record +func (_e *OTLPExporter_Expecter) Export(ctx interface{}, records interface{}) *OTLPExporter_Export_Call { + return &OTLPExporter_Export_Call{Call: _e.mock.On("Export", ctx, records)} +} + +func (_c *OTLPExporter_Export_Call) Run(run func(ctx context.Context, records []log.Record)) *OTLPExporter_Export_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].([]log.Record)) + }) + return _c +} + +func (_c *OTLPExporter_Export_Call) Return(_a0 error) *OTLPExporter_Export_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *OTLPExporter_Export_Call) RunAndReturn(run func(context.Context, []log.Record) error) *OTLPExporter_Export_Call { + _c.Call.Return(run) + return _c +} + +// ForceFlush provides a mock function with given fields: ctx +func (_m *OTLPExporter) ForceFlush(ctx context.Context) error { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for ForceFlush") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context) error); ok { + r0 = rf(ctx) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// OTLPExporter_ForceFlush_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ForceFlush' +type OTLPExporter_ForceFlush_Call struct { + *mock.Call +} + +// ForceFlush is a helper method to define mock.On call +// - ctx context.Context +func (_e *OTLPExporter_Expecter) ForceFlush(ctx interface{}) *OTLPExporter_ForceFlush_Call { + return &OTLPExporter_ForceFlush_Call{Call: _e.mock.On("ForceFlush", ctx)} +} + +func (_c *OTLPExporter_ForceFlush_Call) Run(run func(ctx context.Context)) *OTLPExporter_ForceFlush_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *OTLPExporter_ForceFlush_Call) Return(_a0 error) *OTLPExporter_ForceFlush_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *OTLPExporter_ForceFlush_Call) RunAndReturn(run func(context.Context) error) *OTLPExporter_ForceFlush_Call { + _c.Call.Return(run) + return _c +} + +// Shutdown provides a mock function with given fields: ctx +func (_m *OTLPExporter) Shutdown(ctx context.Context) error { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for Shutdown") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context) error); ok { + r0 = rf(ctx) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// OTLPExporter_Shutdown_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Shutdown' +type OTLPExporter_Shutdown_Call struct { + *mock.Call +} + +// Shutdown is a helper method to define mock.On call +// - ctx context.Context +func (_e *OTLPExporter_Expecter) Shutdown(ctx interface{}) *OTLPExporter_Shutdown_Call { + return &OTLPExporter_Shutdown_Call{Call: _e.mock.On("Shutdown", ctx)} +} + +func (_c *OTLPExporter_Shutdown_Call) Run(run func(ctx context.Context)) *OTLPExporter_Shutdown_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *OTLPExporter_Shutdown_Call) Return(_a0 error) *OTLPExporter_Shutdown_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *OTLPExporter_Shutdown_Call) RunAndReturn(run func(context.Context) error) *OTLPExporter_Shutdown_Call { + _c.Call.Return(run) + return _c +} + +// NewOTLPExporter creates a new instance of OTLPExporter. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewOTLPExporter(t interface { + mock.TestingT + Cleanup(func()) +}) *OTLPExporter { + mock := &OTLPExporter{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/beholder/message.go b/pkg/beholder/message.go new file mode 100644 index 000000000..d6e7964af --- /dev/null +++ b/pkg/beholder/message.go @@ -0,0 +1,256 @@ +package beholder + +import ( + "fmt" + "reflect" + + "github.com/go-playground/validator/v10" + "go.opentelemetry.io/otel/attribute" + otellog "go.opentelemetry.io/otel/log" + otelsdklog "go.opentelemetry.io/otel/sdk/log" +) + +type Message struct { + Attrs map[string]any + Body []byte +} + +type Metadata struct { + // REQUIRED FIELDS + // Schema Registry URI to fetch schema + BeholderDataSchema string `validate:"required,uri"` + + // OPTIONAL FIELDS + // The version of the CL node. + NodeVersion string + // mTLS public key for the node operator. This is used as an identity key but with the added benefit of being able to provide signatures. + NodeCsaKey string + // Signature from CSA private key. + NodeCsaSignature string + DonID string + // The RDD network name the CL node is operating with. + NetworkName []string + WorkflowID string + WorkflowName string + WorkflowOwnerAddress string + // Hash of the workflow spec. + WorkflowSpecID string + // The unique execution of a workflow. + WorkflowExecutionID string + // The address for the contract. + CapabilityContractAddress string + CapabilityID string + CapabilityVersion string + CapabilityName string + NetworkChainID string +} + +func (m Metadata) Attributes() Attributes { + attrs := make(Attributes, reflect.ValueOf(m).NumField()) + attrs["node_version"] = m.NodeVersion + attrs["node_csa_key"] = m.NodeCsaKey + attrs["node_csa_signature"] = m.NodeCsaSignature + attrs["don_id"] = m.DonID + attrs["network_name"] = m.NetworkName + attrs["workflow_id"] = m.WorkflowID + attrs["workflow_name"] = m.WorkflowName + attrs["workflow_owner_address"] = m.WorkflowOwnerAddress + attrs["workflow_spec_id"] = m.WorkflowSpecID + attrs["workflow_execution_id"] = m.WorkflowExecutionID + attrs["beholder_data_schema"] = m.BeholderDataSchema + attrs["capability_contract_address"] = m.CapabilityContractAddress + attrs["capability_id"] = m.CapabilityID + attrs["capability_version"] = m.CapabilityVersion + attrs["capability_name"] = m.CapabilityName + attrs["network_chain_id"] = m.NetworkChainID + return attrs +} + +type Attributes map[string]any + +func NewAttributes(args ...any) Attributes { + attrs := make(Attributes, len(args)/2) + attrs.Add(args...) + return attrs +} + +func (a Attributes) Add(args ...any) Attributes { + for i := 1; i < len(args); i += 2 { + if key, ok := args[i-1].(string); ok { + val := args[i] + a[key] = val + } + } + return a +} + +func NewMessage(body []byte, attrs Attributes) Message { + return Message{ + Body: body, + Attrs: attrs, + } +} + +func (e *Message) AddAttributes(attrs Attributes) { + if e.Attrs == nil { + e.Attrs = make(map[string]any, len(attrs)) + } + for k, v := range attrs { + e.Attrs[k] = v + } +} + +func (e *Message) AddOtelAttributes(attrs ...attribute.KeyValue) { + if e.Attrs == nil { + e.Attrs = make(map[string]any, len(attrs)) + } + for _, v := range attrs { + e.Attrs[string(v.Key)] = v.Value + } +} + +func (e *Message) OtelRecord() otellog.Record { + return newRecord(e.Body, e.Attrs) +} + +func (e *Message) SdkOtelRecord() otelsdklog.Record { + return newSdkRecord(e.Body, e.Attrs) +} + +func (e *Message) Copy() Message { + attrs := make(Attributes, len(e.Attrs)) + for k, v := range e.Attrs { + attrs[k] = v + } + c := Message{ + Attrs: attrs, + } + if e.Body != nil { + c.Body = make([]byte, len(e.Body)) + copy(c.Body, e.Body) + } + return c +} + +// Creates otellog.Record from body and attributes +func newRecord(body []byte, attrs map[string]any) otellog.Record { + otelRecord := otellog.Record{} + if body != nil { + otelRecord.SetBody(otellog.BytesValue(body)) + } + for k, v := range attrs { + otelRecord.AddAttributes(OtelAttr(k, v)) + } + return otelRecord +} + +// Creates otelsdklog.Record from body and attributes +// NOTE: internal function otelsdklog.newRecord returns value not pointer +func newSdkRecord(body []byte, attrs map[string]any) otelsdklog.Record { + sdkRecord := otelsdklog.Record{} + if body != nil { + sdkRecord.SetBody(otellog.BytesValue(body)) + } + for k, v := range attrs { + sdkRecord.AddAttributes(OtelAttr(k, v)) + } + return sdkRecord +} + +func OtelAttr(key string, value any) otellog.KeyValue { + switch v := value.(type) { + case string: + return otellog.String(key, v) + case []string: + vals := make([]otellog.Value, 0, len(v)) + for _, s := range v { + vals = append(vals, otellog.StringValue(s)) + } + return otellog.Slice(key, vals...) + case int64: + return otellog.Int64(key, v) + case int: + return otellog.Int(key, v) + case float64: + return otellog.Float64(key, v) + case bool: + return otellog.Bool(key, v) + case []byte: + return otellog.Bytes(key, v) + case nil: + return otellog.Empty(key) + case otellog.Value: + return otellog.KeyValue{Key: key, Value: v} + case attribute.Value: + return OtelAttr(key, v.AsInterface()) + default: + return otellog.String(key, fmt.Sprintf("", v)) + } +} + +func (e Message) String() string { + return fmt.Sprintf("Message{Attrs: %v, Body: %v}", e.Attrs, e.Body) +} + +// Sets metadata fields from attributes +func (m *Metadata) FromAttributes(attrs Attributes) *Metadata { + for k, v := range attrs { + switch k { + case "node_version": + m.NodeVersion = v.(string) + case "node_csa_key": + m.NodeCsaKey = v.(string) + case "node_csa_signature": + m.NodeCsaSignature = v.(string) + case "don_id": + m.DonID = v.(string) + case "network_name": + m.NetworkName = v.([]string) + case "workflow_id": + m.WorkflowID = v.(string) + case "workflow_name": + m.WorkflowName = v.(string) + case "workflow_owner_address": + m.WorkflowOwnerAddress = v.(string) + case "workflow_spec_id": + m.WorkflowSpecID = v.(string) + case "workflow_execution_id": + m.WorkflowExecutionID = v.(string) + case "beholder_data_schema": + m.BeholderDataSchema = v.(string) + case "capability_contract_address": + m.CapabilityContractAddress = v.(string) + case "capability_id": + m.CapabilityID = v.(string) + case "capability_version": + m.CapabilityVersion = v.(string) + case "capability_name": + m.CapabilityName = v.(string) + case "network_chain_id": + m.NetworkChainID = v.(string) + } + } + return m +} + +func NewMetadata(attrs Attributes) *Metadata { + m := &Metadata{} + m.FromAttributes(attrs) + return m +} + +func (m *Metadata) Validate() error { + validate := validator.New() + return validate.Struct(m) +} + +func (e Message) Validate() error { + if e.Body == nil { + return fmt.Errorf("message body is required") + } + if len(e.Attrs) == 0 { + return fmt.Errorf("message attributes are required") + } + metadata := NewMetadata(e.Attrs) + return metadata.Validate() +} diff --git a/pkg/beholder/message_test.go b/pkg/beholder/message_test.go new file mode 100644 index 000000000..a547af5b0 --- /dev/null +++ b/pkg/beholder/message_test.go @@ -0,0 +1,131 @@ +package beholder_test + +import ( + "fmt" + "testing" + + "github.com/go-playground/validator/v10" + "github.com/stretchr/testify/assert" + + "github.com/smartcontractkit/chainlink-common/pkg/beholder" +) + +func ExampleMessage() { + // Create message with body and attributes + e1 := beholder.NewMessage([]byte{1}, beholder.Attributes{"key_string": "value"}) + fmt.Println("#1", e1) + // Create attributes + additionalAttributes := beholder.Attributes{ + "key_string": "new value", + "key_int32": int32(1), + } + // Add more attributes + additionalAttributes.Add( + "key_string", "updated value", // this will overrider previous value + "key_int32", int32(2), + "key3", true, + ) + // Add attributes to message + e1.AddAttributes(additionalAttributes) + fmt.Println("#2", e1) + // Create empty message struct + e2 := beholder.Message{} + fmt.Println("#3", e2) + // Add attributes to message + e2.AddAttributes(beholder.Attributes{"key_int": 1}) + fmt.Println("#4", e2) + // Update attribute key_int + e2.AddAttributes(beholder.Attributes{"key_int": 2}) + fmt.Println("#5", e2) + // Set message body + e2.Body = []byte("0123") + fmt.Println("#6", e2) + // Reset attributes + e2.Attrs = beholder.Attributes{} + fmt.Println("#7", e2) + // Reset body + e2.Body = nil + fmt.Println("#8", e2) + // Shalow copy of message + e3 := beholder.NewMessage(e1.Body, e1.Attrs) + fmt.Println("#9", e3) + e1.Body[0] = byte(2) // Wil mutate e3 + fmt.Println("#10", e3) + // Deep copy + e4 := e1.Copy() + fmt.Println("#11", e4) + e1.Body[0] = byte(3) // Should not mutate e4 + fmt.Println("#12", e4) + // Output: + // #1 Message{Attrs: map[key_string:value], Body: [1]} + // #2 Message{Attrs: map[key3:true key_int32:2 key_string:updated value], Body: [1]} + // #3 Message{Attrs: map[], Body: []} + // #4 Message{Attrs: map[key_int:1], Body: []} + // #5 Message{Attrs: map[key_int:2], Body: []} + // #6 Message{Attrs: map[key_int:2], Body: [48 49 50 51]} + // #7 Message{Attrs: map[], Body: [48 49 50 51]} + // #8 Message{Attrs: map[], Body: []} + // #9 Message{Attrs: map[key3:true key_int32:2 key_string:updated value], Body: [1]} + // #10 Message{Attrs: map[key3:true key_int32:2 key_string:updated value], Body: [2]} + // #11 Message{Attrs: map[key3:true key_int32:2 key_string:updated value], Body: [2]} + // #12 Message{Attrs: map[key3:true key_int32:2 key_string:updated value], Body: [2]} +} + +func testMetadata() beholder.Metadata { + return beholder.Metadata{ + NodeVersion: "v1.0.0", + NodeCsaKey: "test_key", + NodeCsaSignature: "test_signature", + DonID: "test_don_id", + NetworkName: []string{"test_network"}, + WorkflowID: "test_workflow_id", + WorkflowName: "test_workflow_name", + WorkflowOwnerAddress: "test_owner_address", + WorkflowSpecID: "test_spec_id", + WorkflowExecutionID: "test_execution_id", + BeholderDataSchema: "/schemas/ids/test_schema", // required field, URI + CapabilityContractAddress: "test_contract_address", + CapabilityID: "test_capability_id", + CapabilityVersion: "test_capability_version", + CapabilityName: "test_capability_name", + NetworkChainID: "test_chain_id", + } +} +func ExampleMetadata() { + m := testMetadata() + fmt.Println(m) + fmt.Println(m.Attributes()) + // Output: + // {/schemas/ids/test_schema v1.0.0 test_key test_signature test_don_id [test_network] test_workflow_id test_workflow_name test_owner_address test_spec_id test_execution_id test_contract_address test_capability_id test_capability_version test_capability_name test_chain_id} + // map[beholder_data_schema:/schemas/ids/test_schema capability_contract_address:test_contract_address capability_id:test_capability_id capability_name:test_capability_name capability_version:test_capability_version don_id:test_don_id network_chain_id:test_chain_id network_name:[test_network] node_csa_key:test_key node_csa_signature:test_signature node_version:v1.0.0 workflow_execution_id:test_execution_id workflow_id:test_workflow_id workflow_name:test_workflow_name workflow_owner_address:test_owner_address workflow_spec_id:test_spec_id] +} + +func ExampleValidate() { + validate := validator.New() + + metadata := beholder.Metadata{} + if err := validate.Struct(metadata); err != nil { + fmt.Println(err) + } + metadata.BeholderDataSchema = "example.proto" + if err := validate.Struct(metadata); err != nil { + fmt.Println(err) + } + metadata.BeholderDataSchema = "/schemas/ids/test_schema" + if err := validate.Struct(metadata); err != nil { + fmt.Println(err) + } else { + fmt.Println("Metadata is valid") + } + // Output: + // Key: 'Metadata.BeholderDataSchema' Error:Field validation for 'BeholderDataSchema' failed on the 'required' tag + // Key: 'Metadata.BeholderDataSchema' Error:Field validation for 'BeholderDataSchema' failed on the 'uri' tag + // Metadata is valid +} + +func TestAttributesConversion(t *testing.T) { + expected := testMetadata() + attrs := expected.Attributes() + actual := beholder.NewMetadata(attrs) + assert.Equal(t, expected, *actual) +} diff --git a/pkg/beholder/noop.go b/pkg/beholder/noop.go new file mode 100644 index 000000000..3df416934 --- /dev/null +++ b/pkg/beholder/noop.go @@ -0,0 +1,91 @@ +package beholder + +import ( + "context" + "fmt" + "time" + + "go.opentelemetry.io/otel/exporters/stdout/stdoutlog" + "go.opentelemetry.io/otel/exporters/stdout/stdoutmetric" + "go.opentelemetry.io/otel/exporters/stdout/stdouttrace" + otellognoop "go.opentelemetry.io/otel/log/noop" + otelmetricnoop "go.opentelemetry.io/otel/metric/noop" + sdklog "go.opentelemetry.io/otel/sdk/log" + sdkmetric "go.opentelemetry.io/otel/sdk/metric" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + oteltracenoop "go.opentelemetry.io/otel/trace/noop" +) + +// Default client to fallback when is is not initialized properly +func NewNoopClient() Client { + cfg := DefaultConfig() + // Logger + loggerProvider := otellognoop.NewLoggerProvider() + logger := loggerProvider.Logger(cfg.PackageName) + // Tracer + tracerProvider := oteltracenoop.NewTracerProvider() + tracer := tracerProvider.Tracer(cfg.PackageName) + + // Meter + meterProvider := otelmetricnoop.NewMeterProvider() + meter := meterProvider.Meter(cfg.PackageName) + + // MessageEmitter + messageEmitter := noopMessageEmitter{} + + onClose := func() error { return nil } + + client := NewClient(cfg, logger, tracer, meter, messageEmitter, onClose) + + return client +} + +// NewStdoutClient creates a new BeholderClient with stdout exporters +// Use for testing and debugging +// Also this client is used as a noop client when otel exporter is not initialized properly +func NewStdoutClient() Client { + cfg := DefaultConfig() + // Logger + loggerExporter, _ := stdoutlog.New(stdoutlog.WithoutTimestamps()) // stdoutlog.New() never returns an error + loggerProvider := sdklog.NewLoggerProvider(sdklog.WithProcessor(sdklog.NewSimpleProcessor(loggerExporter))) + logger := loggerProvider.Logger(cfg.PackageName) + setOtelErrorHandler(func(err error) { + fmt.Printf("OTel error %s", err) + }) + + // Tracer + traceExporter, _ := stdouttrace.New() // stdouttrace.New() never returns an error + tracerProvider := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor( + sdktrace.NewSimpleSpanProcessor(traceExporter), + )) + tracer := tracerProvider.Tracer(cfg.PackageName) + + // Meter + metricExporter, _ := stdoutmetric.New() // stdoutmetric.New() never returns an error + meterProvider := sdkmetric.NewMeterProvider( + sdkmetric.WithReader( + sdkmetric.NewPeriodicReader( + metricExporter, + sdkmetric.WithInterval(time.Second), // Default is 10s + )), + ) + meter := meterProvider.Meter(cfg.PackageName) + + // MessageEmitter + messageEmitter := newMessageEmitter(loggerExporter, logger) + + onClose := closeFunc(context.Background(), loggerProvider, tracerProvider, meterProvider) + + client := NewClient(cfg, logger, tracer, meter, messageEmitter, onClose) + + return client +} + +type noopMessageEmitter struct{} + +func (noopMessageEmitter) Emit(ctx context.Context, body []byte, attrs map[string]any) error { + return nil +} +func (noopMessageEmitter) EmitMessage(ctx context.Context, message Message) error { + return nil +} diff --git a/pkg/beholder/pb/example.pb.go b/pkg/beholder/pb/example.pb.go new file mode 100644 index 000000000..9d8914468 --- /dev/null +++ b/pkg/beholder/pb/example.pb.go @@ -0,0 +1,185 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.31.0 +// protoc v4.25.1 +// source: example.proto + +package pb + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// Used for testing +type CustomMessage struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + BoolVal bool `protobuf:"varint,1,opt,name=bool_val,json=boolVal,proto3" json:"bool_val,omitempty"` + IntVal int64 `protobuf:"varint,2,opt,name=int_val,json=intVal,proto3" json:"int_val,omitempty"` + FloatVal float32 `protobuf:"fixed32,3,opt,name=float_val,json=floatVal,proto3" json:"float_val,omitempty"` + StringVal string `protobuf:"bytes,4,opt,name=string_val,json=stringVal,proto3" json:"string_val,omitempty"` + BytesVal []byte `protobuf:"bytes,5,opt,name=bytes_val,json=bytesVal,proto3" json:"bytes_val,omitempty"` +} + +func (x *CustomMessage) Reset() { + *x = CustomMessage{} + if protoimpl.UnsafeEnabled { + mi := &file_example_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CustomMessage) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CustomMessage) ProtoMessage() {} + +func (x *CustomMessage) ProtoReflect() protoreflect.Message { + mi := &file_example_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CustomMessage.ProtoReflect.Descriptor instead. +func (*CustomMessage) Descriptor() ([]byte, []int) { + return file_example_proto_rawDescGZIP(), []int{0} +} + +func (x *CustomMessage) GetBoolVal() bool { + if x != nil { + return x.BoolVal + } + return false +} + +func (x *CustomMessage) GetIntVal() int64 { + if x != nil { + return x.IntVal + } + return 0 +} + +func (x *CustomMessage) GetFloatVal() float32 { + if x != nil { + return x.FloatVal + } + return 0 +} + +func (x *CustomMessage) GetStringVal() string { + if x != nil { + return x.StringVal + } + return "" +} + +func (x *CustomMessage) GetBytesVal() []byte { + if x != nil { + return x.BytesVal + } + return nil +} + +var File_example_proto protoreflect.FileDescriptor + +var file_example_proto_rawDesc = []byte{ + 0x0a, 0x0d, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, + 0x02, 0x70, 0x62, 0x22, 0x9c, 0x01, 0x0a, 0x0d, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x4d, 0x65, + 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x62, 0x6f, 0x6f, 0x6c, 0x5f, 0x76, 0x61, + 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x62, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, + 0x12, 0x17, 0x0a, 0x07, 0x69, 0x6e, 0x74, 0x5f, 0x76, 0x61, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x03, 0x52, 0x06, 0x69, 0x6e, 0x74, 0x56, 0x61, 0x6c, 0x12, 0x1b, 0x0a, 0x09, 0x66, 0x6c, 0x6f, + 0x61, 0x74, 0x5f, 0x76, 0x61, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x02, 0x52, 0x08, 0x66, 0x6c, + 0x6f, 0x61, 0x74, 0x56, 0x61, 0x6c, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, + 0x5f, 0x76, 0x61, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x74, 0x72, 0x69, + 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x12, 0x1b, 0x0a, 0x09, 0x62, 0x79, 0x74, 0x65, 0x73, 0x5f, 0x76, + 0x61, 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x62, 0x79, 0x74, 0x65, 0x73, 0x56, + 0x61, 0x6c, 0x42, 0x3e, 0x5a, 0x3c, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, + 0x2f, 0x73, 0x6d, 0x61, 0x72, 0x74, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x61, 0x63, 0x74, 0x6b, 0x69, + 0x74, 0x2f, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x6c, 0x69, 0x6e, 0x6b, 0x2d, 0x63, 0x6f, 0x6d, 0x6d, + 0x6f, 0x6e, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x62, 0x65, 0x68, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x2f, + 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_example_proto_rawDescOnce sync.Once + file_example_proto_rawDescData = file_example_proto_rawDesc +) + +func file_example_proto_rawDescGZIP() []byte { + file_example_proto_rawDescOnce.Do(func() { + file_example_proto_rawDescData = protoimpl.X.CompressGZIP(file_example_proto_rawDescData) + }) + return file_example_proto_rawDescData +} + +var file_example_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_example_proto_goTypes = []interface{}{ + (*CustomMessage)(nil), // 0: pb.CustomMessage +} +var file_example_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_example_proto_init() } +func file_example_proto_init() { + if File_example_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_example_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CustomMessage); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_example_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_example_proto_goTypes, + DependencyIndexes: file_example_proto_depIdxs, + MessageInfos: file_example_proto_msgTypes, + }.Build() + File_example_proto = out.File + file_example_proto_rawDesc = nil + file_example_proto_goTypes = nil + file_example_proto_depIdxs = nil +} diff --git a/pkg/beholder/pb/example.proto b/pkg/beholder/pb/example.proto new file mode 100644 index 000000000..55d1a9b7d --- /dev/null +++ b/pkg/beholder/pb/example.proto @@ -0,0 +1,14 @@ +syntax = "proto3"; + +option go_package = "github.com/smartcontractkit/chainlink-common/pkg/beholder/pb"; + +package pb; + +// Used for testing +message CustomMessage { + bool bool_val=1; + int64 int_val=2; + float float_val=3; + string string_val=4; + bytes bytes_val=5; +} \ No newline at end of file diff --git a/pkg/beholder/pb/generate.go b/pkg/beholder/pb/generate.go new file mode 100644 index 000000000..697c33637 --- /dev/null +++ b/pkg/beholder/pb/generate.go @@ -0,0 +1,3 @@ +//go:generate protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative example.proto + +package pb From d2b8e226ea54371e185e787f6a35b5608bfea63f Mon Sep 17 00:00:00 2001 From: Pavel <177363085+pkcll@users.noreply.github.com> Date: Mon, 12 Aug 2024 16:21:44 -0400 Subject: [PATCH 02/49] Beholder: update comment --- pkg/beholder/config.go | 4 ++-- pkg/beholder/config_test.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/beholder/config.go b/pkg/beholder/config.go index a7f7dd0b9..4c1c24f61 100644 --- a/pkg/beholder/config.go +++ b/pkg/beholder/config.go @@ -14,7 +14,7 @@ type Config struct { PackageName string // OTel Resource ResourceAttributes map[string]string - // EventEmitter + // Message Emitter EmitterExportTimeout time.Duration // OTel Trace TraceSampleRate float64 @@ -37,7 +37,7 @@ func DefaultConfig() Config { PackageName: "beholder", // Resource ResourceAttributes: defaultOtelAttributes, - // EventEmitter + // Message Emitter EmitterExportTimeout: 1 * time.Second, // Trace TraceSampleRate: 1, diff --git a/pkg/beholder/config_test.go b/pkg/beholder/config_test.go index 523376f60..47b5aeeb1 100644 --- a/pkg/beholder/config_test.go +++ b/pkg/beholder/config_test.go @@ -22,7 +22,7 @@ func ExampleConfig() { "package_name": packageName, "sender": "beholdeclient", }, - // EventEmitter + // Message Emitter EmitterExportTimeout: 1 * time.Second, // Trace TraceSampleRate: 1, From 5450f88144fde2ba7dc195a12aca42a7bef75153 Mon Sep 17 00:00:00 2001 From: Pavel <177363085+pkcll@users.noreply.github.com> Date: Mon, 12 Aug 2024 16:46:43 -0400 Subject: [PATCH 03/49] Beholder: rename variable --- pkg/beholder/client.go | 2 +- pkg/beholder/client_test.go | 4 ++-- pkg/beholder/global/global.go | 8 ++++---- pkg/beholder/noop.go | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pkg/beholder/client.go b/pkg/beholder/client.go index cdebed97a..dc4e3f150 100644 --- a/pkg/beholder/client.go +++ b/pkg/beholder/client.go @@ -76,7 +76,7 @@ func NewClient( } } -// NewOtelClient creates a new BeholderClient with OTel exporter +// NewOtelClient creates a new Client with OTel exporter func NewOtelClient(cfg Config, errorHandler errorHandlerFunc) (Client, error) { factory := func(ctx context.Context, options ...otlploggrpc.Option) (sdklog.Exporter, error) { return otlploggrpc.New(ctx, options...) diff --git a/pkg/beholder/client_test.go b/pkg/beholder/client_test.go index 02f8e56fd..2fc906932 100644 --- a/pkg/beholder/client_test.go +++ b/pkg/beholder/client_test.go @@ -95,7 +95,7 @@ func TestClient(t *testing.T) { otelErrorHandler := func(err error) { t.Fatalf("otel error: %v", err) } - // Override exporter factory which is used by BeholderClient + // Override exporter factory which is used by Client exporterFactory := func(context.Context, ...otlploggrpc.Option) (sdklog.Exporter, error) { return exporterMock, nil } @@ -160,7 +160,7 @@ func TestEmitterMessageValidation(t *testing.T) { client, err := newOtelClient( DefaultConfig(), func(err error) { t.Fatalf("otel error: %v", err) }, - // Override exporter factory which is used by BeholderClient + // Override exporter factory which is used by Client func(context.Context, ...otlploggrpc.Option) (sdklog.Exporter, error) { return exporterMock, nil }, diff --git a/pkg/beholder/global/global.go b/pkg/beholder/global/global.go index 350d3353e..839c81c25 100644 --- a/pkg/beholder/global/global.go +++ b/pkg/beholder/global/global.go @@ -12,17 +12,17 @@ import ( ) // Pointer to the global Beholder Client -var globalBeholderClient = defaultBeholderClient() +var globalClient = defaultClient() // SetClient sets the global Beholder Client func SetClient(client *beholder.Client) { - globalBeholderClient.Store(client) + globalClient.Store(client) } // Returns the global Beholder Client // Its thread-safe and can be used concurrently func GetClient() beholder.Client { - ptr := globalBeholderClient.Load() + ptr := globalClient.Load() return *ptr } @@ -46,7 +46,7 @@ func SpanFromContext(ctx context.Context) oteltrace.Span { return oteltrace.SpanFromContext(ctx) } -func defaultBeholderClient() *atomic.Pointer[beholder.Client] { +func defaultClient() *atomic.Pointer[beholder.Client] { ptr := &atomic.Pointer[beholder.Client]{} client := beholder.NewNoopClient() ptr.Store(&client) diff --git a/pkg/beholder/noop.go b/pkg/beholder/noop.go index 3df416934..f2e0d9679 100644 --- a/pkg/beholder/noop.go +++ b/pkg/beholder/noop.go @@ -40,7 +40,7 @@ func NewNoopClient() Client { return client } -// NewStdoutClient creates a new BeholderClient with stdout exporters +// NewStdoutClient creates a new Client with stdout exporters // Use for testing and debugging // Also this client is used as a noop client when otel exporter is not initialized properly func NewStdoutClient() Client { From 10707bdb834a8eeff624bedd7e04c7d34e5a435d Mon Sep 17 00:00:00 2001 From: Pavel <177363085+pkcll@users.noreply.github.com> Date: Mon, 12 Aug 2024 18:11:01 -0400 Subject: [PATCH 04/49] Beholder: add Close function to global --- pkg/beholder/global/global.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/beholder/global/global.go b/pkg/beholder/global/global.go index 839c81c25..f150bffae 100644 --- a/pkg/beholder/global/global.go +++ b/pkg/beholder/global/global.go @@ -42,6 +42,10 @@ func Emitter() beholder.Emitter { return GetClient().Emitter() } +func Close() error { + return GetClient().Close() +} + func SpanFromContext(ctx context.Context) oteltrace.Span { return oteltrace.SpanFromContext(ctx) } From 656550bf232be5293e161fed201c3059a9e8192f Mon Sep 17 00:00:00 2001 From: Pavel <177363085+pkcll@users.noreply.github.com> Date: Mon, 12 Aug 2024 18:53:14 -0400 Subject: [PATCH 05/49] Beholder: export OtelClient struct - make otelClient struct exportable - export fields, remove getters - remove Client interface --- pkg/beholder/client.go | 80 ++++++++---------------------- pkg/beholder/client_test.go | 12 ++--- pkg/beholder/example_test.go | 8 +-- pkg/beholder/global/global.go | 22 ++++---- pkg/beholder/global/global_test.go | 12 ++--- pkg/beholder/noop.go | 8 +-- 6 files changed, 49 insertions(+), 93 deletions(-) diff --git a/pkg/beholder/client.go b/pkg/beholder/client.go index dc4e3f150..9708258f1 100644 --- a/pkg/beholder/client.go +++ b/pkg/beholder/client.go @@ -30,54 +30,30 @@ type Emitter interface { EmitMessage(ctx context.Context, m Message) error } type Client interface { - Logger() otellog.Logger - Tracer() oteltrace.Tracer - Meter() otelmetric.Meter - Emitter() Emitter Close() error } -var _ Client = (*otelClient)(nil) - type messageEmitter struct { exporter sdklog.Exporter messageLogger otellog.Logger } -type otelClient struct { - config Config +type OtelClient struct { + Config Config // Logger - logger otellog.Logger + Logger otellog.Logger // Tracer - tracer oteltrace.Tracer + Tracer oteltrace.Tracer // Meter - meter otelmetric.Meter + Meter otelmetric.Meter // Message Emitter - emitter Emitter + Emitter Emitter // Graceful shutdown for tracer, meter, logger providers - closeFunc func() error -} - -func NewClient( - config Config, - logger otellog.Logger, - tracer oteltrace.Tracer, - meter otelmetric.Meter, - emitter Emitter, - onClose func() error, -) Client { - return &otelClient{ - config: config, - logger: logger, - tracer: tracer, - meter: meter, - emitter: emitter, - closeFunc: onClose, - } + CloseFunc func() error } // NewOtelClient creates a new Client with OTel exporter -func NewOtelClient(cfg Config, errorHandler errorHandlerFunc) (Client, error) { +func NewOtelClient(cfg Config, errorHandler errorHandlerFunc) (OtelClient, error) { factory := func(ctx context.Context, options ...otlploggrpc.Option) (sdklog.Exporter, error) { return otlploggrpc.New(ctx, options...) } @@ -87,17 +63,18 @@ func NewOtelClient(cfg Config, errorHandler errorHandlerFunc) (Client, error) { // Used for testing to override the default exporter type otlploggrpcFactory func(ctx context.Context, options ...otlploggrpc.Option) (sdklog.Exporter, error) -func newOtelClient(cfg Config, errorHandler errorHandlerFunc, otlploggrpcNew otlploggrpcFactory) (Client, error) { +func newOtelClient(cfg Config, errorHandler errorHandlerFunc, otlploggrpcNew otlploggrpcFactory) (OtelClient, error) { ctx := context.Background() baseResource, err := newOtelResource(cfg) + noop := NewNoopClient() if err != nil { - return nil, err + return noop, err } creds := insecure.NewCredentials() if !cfg.InsecureConnection && cfg.CACertFile != "" { creds, err = credentials.NewClientTLSFromFile(cfg.CACertFile, "") if err != nil { - return nil, err + return noop, err } } sharedLogExporter, err := otlploggrpcNew( @@ -106,7 +83,7 @@ func newOtelClient(cfg Config, errorHandler errorHandlerFunc, otlploggrpcNew otl otlploggrpc.WithEndpoint(cfg.OtelExporterGRPCEndpoint), ) if err != nil { - return nil, err + return noop, err } // Logger @@ -122,7 +99,7 @@ func newOtelClient(cfg Config, errorHandler errorHandlerFunc, otlploggrpcNew otl baseResource, ) if err != nil { - return nil, err + return noop, err } loggerProvider := sdklog.NewLoggerProvider( sdklog.WithResource(loggerResource), @@ -136,7 +113,7 @@ func newOtelClient(cfg Config, errorHandler errorHandlerFunc, otlploggrpcNew otl // Tracer tracerProvider, err := newTracerProvider(cfg, baseResource, creds) if err != nil { - return nil, err + return noop, err } tracer := tracerProvider.Tracer(cfg.PackageName) otel.SetTracerProvider(tracerProvider) @@ -145,7 +122,7 @@ func newOtelClient(cfg Config, errorHandler errorHandlerFunc, otlploggrpcNew otl // Meter meterProvider, err := newMeterProvider(cfg, baseResource, creds) if err != nil { - return nil, err + return noop, err } meter := meterProvider.Meter(cfg.PackageName) otel.SetMeterProvider(meterProvider) @@ -163,7 +140,7 @@ func newOtelClient(cfg Config, errorHandler errorHandlerFunc, otlploggrpcNew otl baseResource, ) if err != nil { - return nil, err + return noop, err } messageLoggerProvider := sdklog.NewLoggerProvider( sdklog.WithResource(messageLoggerResource), @@ -176,7 +153,7 @@ func newOtelClient(cfg Config, errorHandler errorHandlerFunc, otlploggrpcNew otl onClose := closeFunc(ctx, loggerProvider, messageLoggerProvider, tracerProvider, meterProvider) - client := NewClient(cfg, logger, tracer, meter, messageEmitter, onClose) + client := OtelClient{cfg, logger, tracer, meter, messageEmitter, onClose} return client, nil } @@ -249,24 +226,9 @@ func (e messageEmitter) EmitMessage(ctx context.Context, message Message) error return nil } -func (b *otelClient) Logger() otellog.Logger { - return b.logger -} - -func (b *otelClient) Tracer() oteltrace.Tracer { - return b.tracer -} - -func (b *otelClient) Meter() otelmetric.Meter { - return b.meter -} -func (b *otelClient) Emitter() Emitter { - return b.emitter -} - -func (b *otelClient) Close() error { - if b.closeFunc != nil { - return b.closeFunc() +func (b OtelClient) Close() error { + if b.CloseFunc != nil { + return b.CloseFunc() } return nil } diff --git a/pkg/beholder/client_test.go b/pkg/beholder/client_test.go index 2fc906932..ed2dbcc1d 100644 --- a/pkg/beholder/client_test.go +++ b/pkg/beholder/client_test.go @@ -59,7 +59,7 @@ func TestClient(t *testing.T) { messageCount int exporterMockErrorCount int exporterOutputExpected bool - messageGenerator func(client Client, messageBody []byte, customAttributes map[string]any) + messageGenerator func(client OtelClient, messageBody []byte, customAttributes map[string]any) }{ { name: "Test Emit", @@ -68,8 +68,8 @@ func TestClient(t *testing.T) { messageCount: 10, exporterMockErrorCount: 0, exporterOutputExpected: true, - messageGenerator: func(client Client, messageBody []byte, customAttributes map[string]any) { - err := client.Emitter().Emit(context.Background(), messageBody, customAttributes) + messageGenerator: func(client OtelClient, messageBody []byte, customAttributes map[string]any) { + err := client.Emitter.Emit(context.Background(), messageBody, customAttributes) assert.NoError(t, err) }, }, { @@ -79,9 +79,9 @@ func TestClient(t *testing.T) { messageCount: 10, exporterMockErrorCount: 0, exporterOutputExpected: true, - messageGenerator: func(client Client, messageBody []byte, customAttributes map[string]any) { + messageGenerator: func(client OtelClient, messageBody []byte, customAttributes map[string]any) { message := NewMessage(messageBody, customAttributes) - err := client.Emitter().EmitMessage(context.Background(), message) + err := client.Emitter.EmitMessage(context.Background(), message) assert.NoError(t, err) }, }, @@ -166,7 +166,7 @@ func TestEmitterMessageValidation(t *testing.T) { }, ) assert.NoError(t, err) - return client.Emitter() + return client.Emitter } for _, tc := range []struct { diff --git a/pkg/beholder/example_test.go b/pkg/beholder/example_test.go index 26043c93c..7856c578e 100644 --- a/pkg/beholder/example_test.go +++ b/pkg/beholder/example_test.go @@ -47,10 +47,8 @@ func ExampleClient() { "sender", "example-client", ), } - // Get message emitter - em := client.Emitter() // Emit custom message - err := em.EmitMessage(ctx, customMessage) + err := client.Emitter.EmitMessage(ctx, customMessage) if err != nil { log.Fatalf("Error emitting message: %v", err) } @@ -95,12 +93,10 @@ func asseetNoError(err error) { func ExampleEmitter() { ctx := context.Background() // Initialize beholder client - c, err := beholder.NewOtelClient(beholder.DefaultConfig(), asseetNoError) + client, err := beholder.NewOtelClient(beholder.DefaultConfig(), asseetNoError) if err != nil { log.Fatalf("Error creating beholder client: %v", err) } - var client beholder.Client = c - // Set global client so it will be accessible from anywhere through beholder/global functions global.SetClient(&client) // After that you can use global functions to get logger, tracer, meter, messageEmitter diff --git a/pkg/beholder/global/global.go b/pkg/beholder/global/global.go index f150bffae..d4a1c1343 100644 --- a/pkg/beholder/global/global.go +++ b/pkg/beholder/global/global.go @@ -15,31 +15,30 @@ import ( var globalClient = defaultClient() // SetClient sets the global Beholder Client -func SetClient(client *beholder.Client) { +func SetClient(client *beholder.OtelClient) { globalClient.Store(client) } // Returns the global Beholder Client // Its thread-safe and can be used concurrently -func GetClient() beholder.Client { - ptr := globalClient.Load() - return *ptr +func GetClient() *beholder.OtelClient { + return globalClient.Load() } func Logger() otellog.Logger { - return GetClient().Logger() + return GetClient().Logger } func Tracer() oteltrace.Tracer { - return GetClient().Tracer() + return GetClient().Tracer } func Meter() otelmetric.Meter { - return GetClient().Meter() + return GetClient().Meter } func Emitter() beholder.Emitter { - return GetClient().Emitter() + return GetClient().Emitter } func Close() error { @@ -50,8 +49,8 @@ func SpanFromContext(ctx context.Context) oteltrace.Span { return oteltrace.SpanFromContext(ctx) } -func defaultClient() *atomic.Pointer[beholder.Client] { - ptr := &atomic.Pointer[beholder.Client]{} +func defaultClient() *atomic.Pointer[beholder.OtelClient] { + ptr := &atomic.Pointer[beholder.OtelClient]{} client := beholder.NewNoopClient() ptr.Store(&client) return ptr @@ -71,9 +70,8 @@ func Bootstrap(cfg beholder.Config, errorHandler func(error)) error { if err != nil { return err } - var client beholder.Client = c // Set global client so it will be accessible from anywhere through beholder/global functions - SetClient(&client) + SetClient(&c) return nil } diff --git a/pkg/beholder/global/global_test.go b/pkg/beholder/global/global_test.go index e5c3cefbe..dda577ab8 100644 --- a/pkg/beholder/global/global_test.go +++ b/pkg/beholder/global/global_test.go @@ -25,16 +25,16 @@ func TestGlobal(t *testing.T) { assert.IsType(t, otellognoop.Logger{}, logger) assert.IsType(t, oteltracenoop.Tracer{}, tracer) assert.IsType(t, otelmetricnoop.Meter{}, meter) - expectedMessageEmitter := beholder.NewNoopClient().Emitter() + expectedMessageEmitter := beholder.NewNoopClient().Emitter assert.IsType(t, expectedMessageEmitter, messageEmitter) - assert.IsType(t, noopClient, global.GetClient()) - assert.NotSame(t, noopClient, global.GetClient()) + var noopClientPtr *beholder.OtelClient = &noopClient + assert.IsType(t, noopClientPtr, global.GetClient()) + assert.NotSame(t, noopClientPtr, global.GetClient()) // Set global client so it will be accessible from anywhere through beholder/global functions - var client beholder.Client = noopClient - global.SetClient(&client) - assert.Same(t, noopClient, global.GetClient()) + global.SetClient(noopClientPtr) + assert.Same(t, noopClientPtr, global.GetClient()) // After that use global functions to get logger, tracer, meter, messageEmitter logger, tracer, meter, messageEmitter = global.Logger(), global.Tracer(), global.Meter(), global.Emitter() diff --git a/pkg/beholder/noop.go b/pkg/beholder/noop.go index f2e0d9679..5fdc96f53 100644 --- a/pkg/beholder/noop.go +++ b/pkg/beholder/noop.go @@ -17,7 +17,7 @@ import ( ) // Default client to fallback when is is not initialized properly -func NewNoopClient() Client { +func NewNoopClient() OtelClient { cfg := DefaultConfig() // Logger loggerProvider := otellognoop.NewLoggerProvider() @@ -35,7 +35,7 @@ func NewNoopClient() Client { onClose := func() error { return nil } - client := NewClient(cfg, logger, tracer, meter, messageEmitter, onClose) + client := OtelClient{cfg, logger, tracer, meter, messageEmitter, onClose} return client } @@ -43,7 +43,7 @@ func NewNoopClient() Client { // NewStdoutClient creates a new Client with stdout exporters // Use for testing and debugging // Also this client is used as a noop client when otel exporter is not initialized properly -func NewStdoutClient() Client { +func NewStdoutClient() OtelClient { cfg := DefaultConfig() // Logger loggerExporter, _ := stdoutlog.New(stdoutlog.WithoutTimestamps()) // stdoutlog.New() never returns an error @@ -76,7 +76,7 @@ func NewStdoutClient() Client { onClose := closeFunc(context.Background(), loggerProvider, tracerProvider, meterProvider) - client := NewClient(cfg, logger, tracer, meter, messageEmitter, onClose) + client := OtelClient{cfg, logger, tracer, meter, messageEmitter, onClose} return client } From 43b59453daa04efffbbecd830856e921dd50b066 Mon Sep 17 00:00:00 2001 From: Pavel <177363085+pkcll@users.noreply.github.com> Date: Mon, 12 Aug 2024 19:02:12 -0400 Subject: [PATCH 06/49] Beholder: rename pb test message --- pkg/beholder/example_test.go | 8 ++--- pkg/beholder/pb/example.pb.go | 62 +++++++++++++++++------------------ pkg/beholder/pb/example.proto | 2 +- 3 files changed, 36 insertions(+), 36 deletions(-) diff --git a/pkg/beholder/example_test.go b/pkg/beholder/example_test.go index 7856c578e..97eba2426 100644 --- a/pkg/beholder/example_test.go +++ b/pkg/beholder/example_test.go @@ -39,7 +39,7 @@ func ExampleClient() { // Create custom message customMessage := beholder.Message{ // Set protobuf message bytes as body - Body: newMessageBytes(i), + Body: newTestMessageBytes(i), // Set metadata attributes Attrs: metadata.Attributes().Add( // Add custom attributes @@ -62,9 +62,9 @@ func ExampleClient() { // Emitting message 2 } -func newMessageBytes(i int) []byte { +func newTestMessageBytes(i int) []byte { // Create protobuf message - customMessagePb := &pb.CustomMessage{} + customMessagePb := &pb.TestCustomMessage{} customMessagePb.BoolVal = true customMessagePb.IntVal = int64(i) customMessagePb.FloatVal = float32(i) @@ -137,7 +137,7 @@ func ExampleBootstrap() { log.Fatalf("Error bootstrapping Beholder: %v", err) } - payloadBytes := newMessageBytes(0) + payloadBytes := newTestMessageBytes(0) // Emit custom message for range 3 { diff --git a/pkg/beholder/pb/example.pb.go b/pkg/beholder/pb/example.pb.go index 9d8914468..5a5b47ea3 100644 --- a/pkg/beholder/pb/example.pb.go +++ b/pkg/beholder/pb/example.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.31.0 -// protoc v4.25.1 +// protoc v5.27.3 // source: example.proto package pb @@ -21,7 +21,7 @@ const ( ) // Used for testing -type CustomMessage struct { +type TestCustomMessage struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields @@ -33,8 +33,8 @@ type CustomMessage struct { BytesVal []byte `protobuf:"bytes,5,opt,name=bytes_val,json=bytesVal,proto3" json:"bytes_val,omitempty"` } -func (x *CustomMessage) Reset() { - *x = CustomMessage{} +func (x *TestCustomMessage) Reset() { + *x = TestCustomMessage{} if protoimpl.UnsafeEnabled { mi := &file_example_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -42,13 +42,13 @@ func (x *CustomMessage) Reset() { } } -func (x *CustomMessage) String() string { +func (x *TestCustomMessage) String() string { return protoimpl.X.MessageStringOf(x) } -func (*CustomMessage) ProtoMessage() {} +func (*TestCustomMessage) ProtoMessage() {} -func (x *CustomMessage) ProtoReflect() protoreflect.Message { +func (x *TestCustomMessage) ProtoReflect() protoreflect.Message { mi := &file_example_proto_msgTypes[0] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -60,40 +60,40 @@ func (x *CustomMessage) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use CustomMessage.ProtoReflect.Descriptor instead. -func (*CustomMessage) Descriptor() ([]byte, []int) { +// Deprecated: Use TestCustomMessage.ProtoReflect.Descriptor instead. +func (*TestCustomMessage) Descriptor() ([]byte, []int) { return file_example_proto_rawDescGZIP(), []int{0} } -func (x *CustomMessage) GetBoolVal() bool { +func (x *TestCustomMessage) GetBoolVal() bool { if x != nil { return x.BoolVal } return false } -func (x *CustomMessage) GetIntVal() int64 { +func (x *TestCustomMessage) GetIntVal() int64 { if x != nil { return x.IntVal } return 0 } -func (x *CustomMessage) GetFloatVal() float32 { +func (x *TestCustomMessage) GetFloatVal() float32 { if x != nil { return x.FloatVal } return 0 } -func (x *CustomMessage) GetStringVal() string { +func (x *TestCustomMessage) GetStringVal() string { if x != nil { return x.StringVal } return "" } -func (x *CustomMessage) GetBytesVal() []byte { +func (x *TestCustomMessage) GetBytesVal() []byte { if x != nil { return x.BytesVal } @@ -104,21 +104,21 @@ var File_example_proto protoreflect.FileDescriptor var file_example_proto_rawDesc = []byte{ 0x0a, 0x0d, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, - 0x02, 0x70, 0x62, 0x22, 0x9c, 0x01, 0x0a, 0x0d, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x4d, 0x65, - 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x62, 0x6f, 0x6f, 0x6c, 0x5f, 0x76, 0x61, - 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x62, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, - 0x12, 0x17, 0x0a, 0x07, 0x69, 0x6e, 0x74, 0x5f, 0x76, 0x61, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x03, 0x52, 0x06, 0x69, 0x6e, 0x74, 0x56, 0x61, 0x6c, 0x12, 0x1b, 0x0a, 0x09, 0x66, 0x6c, 0x6f, - 0x61, 0x74, 0x5f, 0x76, 0x61, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x02, 0x52, 0x08, 0x66, 0x6c, - 0x6f, 0x61, 0x74, 0x56, 0x61, 0x6c, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, - 0x5f, 0x76, 0x61, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x74, 0x72, 0x69, - 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x12, 0x1b, 0x0a, 0x09, 0x62, 0x79, 0x74, 0x65, 0x73, 0x5f, 0x76, - 0x61, 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x62, 0x79, 0x74, 0x65, 0x73, 0x56, - 0x61, 0x6c, 0x42, 0x3e, 0x5a, 0x3c, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, - 0x2f, 0x73, 0x6d, 0x61, 0x72, 0x74, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x61, 0x63, 0x74, 0x6b, 0x69, - 0x74, 0x2f, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x6c, 0x69, 0x6e, 0x6b, 0x2d, 0x63, 0x6f, 0x6d, 0x6d, - 0x6f, 0x6e, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x62, 0x65, 0x68, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x2f, - 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x02, 0x70, 0x62, 0x22, 0xa0, 0x01, 0x0a, 0x11, 0x54, 0x65, 0x73, 0x74, 0x43, 0x75, 0x73, 0x74, + 0x6f, 0x6d, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x62, 0x6f, 0x6f, + 0x6c, 0x5f, 0x76, 0x61, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x62, 0x6f, 0x6f, + 0x6c, 0x56, 0x61, 0x6c, 0x12, 0x17, 0x0a, 0x07, 0x69, 0x6e, 0x74, 0x5f, 0x76, 0x61, 0x6c, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x69, 0x6e, 0x74, 0x56, 0x61, 0x6c, 0x12, 0x1b, 0x0a, + 0x09, 0x66, 0x6c, 0x6f, 0x61, 0x74, 0x5f, 0x76, 0x61, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x02, + 0x52, 0x08, 0x66, 0x6c, 0x6f, 0x61, 0x74, 0x56, 0x61, 0x6c, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x74, + 0x72, 0x69, 0x6e, 0x67, 0x5f, 0x76, 0x61, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, + 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x12, 0x1b, 0x0a, 0x09, 0x62, 0x79, 0x74, + 0x65, 0x73, 0x5f, 0x76, 0x61, 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x62, 0x79, + 0x74, 0x65, 0x73, 0x56, 0x61, 0x6c, 0x42, 0x3e, 0x5a, 0x3c, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, + 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x73, 0x6d, 0x61, 0x72, 0x74, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x61, + 0x63, 0x74, 0x6b, 0x69, 0x74, 0x2f, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x6c, 0x69, 0x6e, 0x6b, 0x2d, + 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x62, 0x65, 0x68, 0x6f, 0x6c, + 0x64, 0x65, 0x72, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -135,7 +135,7 @@ func file_example_proto_rawDescGZIP() []byte { var file_example_proto_msgTypes = make([]protoimpl.MessageInfo, 1) var file_example_proto_goTypes = []interface{}{ - (*CustomMessage)(nil), // 0: pb.CustomMessage + (*TestCustomMessage)(nil), // 0: pb.TestCustomMessage } var file_example_proto_depIdxs = []int32{ 0, // [0:0] is the sub-list for method output_type @@ -152,7 +152,7 @@ func file_example_proto_init() { } if !protoimpl.UnsafeEnabled { file_example_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*CustomMessage); i { + switch v := v.(*TestCustomMessage); i { case 0: return &v.state case 1: diff --git a/pkg/beholder/pb/example.proto b/pkg/beholder/pb/example.proto index 55d1a9b7d..223770ff8 100644 --- a/pkg/beholder/pb/example.proto +++ b/pkg/beholder/pb/example.proto @@ -5,7 +5,7 @@ option go_package = "github.com/smartcontractkit/chainlink-common/pkg/beholder/p package pb; // Used for testing -message CustomMessage { +message TestCustomMessage { bool bool_val=1; int64 int_val=2; float float_val=3; From 14b67f1738ef1a7c193fcb1367f3d041e61ec336 Mon Sep 17 00:00:00 2001 From: Pavel <177363085+pkcll@users.noreply.github.com> Date: Mon, 12 Aug 2024 21:39:18 -0400 Subject: [PATCH 07/49] Beholder: extract logic to set OTel globals from OtelClient constructor - add new SetGlobals method - refactor Close method - add provider fields --- pkg/beholder/client.go | 55 ++++++++++++------------ pkg/beholder/client_test.go | 84 +++++++++++++++++++++++++++++++++++++ pkg/beholder/noop.go | 10 ++--- 3 files changed, 114 insertions(+), 35 deletions(-) diff --git a/pkg/beholder/client.go b/pkg/beholder/client.go index 9708258f1..821d670c6 100644 --- a/pkg/beholder/client.go +++ b/pkg/beholder/client.go @@ -48,8 +48,11 @@ type OtelClient struct { Meter otelmetric.Meter // Message Emitter Emitter Emitter - // Graceful shutdown for tracer, meter, logger providers - CloseFunc func() error + + // Providers + LoggerProvider otellog.LoggerProvider + TracerProvider oteltrace.TracerProvider + MeterProvider otelmetric.MeterProvider } // NewOtelClient creates a new Client with OTel exporter @@ -107,17 +110,12 @@ func newOtelClient(cfg Config, errorHandler errorHandlerFunc, otlploggrpcNew otl ) logger := loggerProvider.Logger(cfg.PackageName) - // Set global logger provider - otelglobal.SetLoggerProvider(loggerProvider) - // Tracer tracerProvider, err := newTracerProvider(cfg, baseResource, creds) if err != nil { return noop, err } tracer := tracerProvider.Tracer(cfg.PackageName) - otel.SetTracerProvider(tracerProvider) - otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{})) // Meter meterProvider, err := newMeterProvider(cfg, baseResource, creds) @@ -125,7 +123,6 @@ func newOtelClient(cfg Config, errorHandler errorHandlerFunc, otlploggrpcNew otl return noop, err } meter := meterProvider.Meter(cfg.PackageName) - otel.SetMeterProvider(meterProvider) // Message Emitter messageLogProcessor := sdklog.NewBatchProcessor( @@ -151,13 +148,32 @@ func newOtelClient(cfg Config, errorHandler errorHandlerFunc, otlploggrpcNew otl setOtelErrorHandler(errorHandler) - onClose := closeFunc(ctx, loggerProvider, messageLoggerProvider, tracerProvider, meterProvider) - - client := OtelClient{cfg, logger, tracer, meter, messageEmitter, onClose} + client := OtelClient{cfg, logger, tracer, meter, messageEmitter, loggerProvider, tracerProvider, meterProvider} return client, nil } +func (c OtelClient) SetGlobals() { + // Logger + otelglobal.SetLoggerProvider(c.LoggerProvider) + // Tracer + otel.SetTracerProvider(c.TracerProvider) + otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{})) + // Meter + otel.SetMeterProvider(c.MeterProvider) +} + +func (c OtelClient) Close() (err error) { + for _, provider := range []any{c.LoggerProvider, c.TracerProvider, c.MeterProvider} { + if p, ok := provider.(otelProvider); ok { + err = errors.Join(err, p.Shutdown(context.Background())) + } else { + err = errors.Join(err, errors.New("provider does not implement Shutdown method")) + } + } + return +} + type errorHandlerFunc func(err error) // Sets the global error handler for OpenTelemetry @@ -226,27 +242,10 @@ func (e messageEmitter) EmitMessage(ctx context.Context, message Message) error return nil } -func (b OtelClient) Close() error { - if b.CloseFunc != nil { - return b.CloseFunc() - } - return nil -} - type otelProvider interface { Shutdown(ctx context.Context) error } -// Returns function that finalizes all providers -func closeFunc(ctx context.Context, providers ...otelProvider) func() error { - return func() (err error) { - for _, provider := range providers { - err = errors.Join(err, provider.Shutdown(ctx)) - } - return - } -} - func newTracerProvider(config Config, resource *sdkresource.Resource, creds credentials.TransportCredentials) (*sdktrace.TracerProvider, error) { ctx := context.Background() diff --git a/pkg/beholder/client_test.go b/pkg/beholder/client_test.go index ed2dbcc1d..0f43f16ae 100644 --- a/pkg/beholder/client_test.go +++ b/pkg/beholder/client_test.go @@ -8,9 +8,14 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" + "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc" otellog "go.opentelemetry.io/otel/log" + otelglobal "go.opentelemetry.io/otel/log/global" + otelmetric "go.opentelemetry.io/otel/metric" + "go.opentelemetry.io/otel/propagation" sdklog "go.opentelemetry.io/otel/sdk/log" + oteltrace "go.opentelemetry.io/otel/trace" "github.com/smartcontractkit/chainlink-common/pkg/beholder/internal/mocks" ) @@ -271,3 +276,82 @@ func TestEmitterMessageValidation(t *testing.T) { }) } } + +func TestClient_Close(t *testing.T) { + exporterMock := mocks.NewOTLPExporter(t) + defer exporterMock.AssertExpectations(t) + + otelErrorHandler := func(err error) { + t.Fatalf("otel error: %v", err) + } + // Override exporter factory which is used by Client + exporterFactory := func(context.Context, ...otlploggrpc.Option) (sdklog.Exporter, error) { + return exporterMock, nil + } + client, err := newOtelClient(DefaultConfig(), otelErrorHandler, exporterFactory) + if err != nil { + t.Fatalf("Error creating beholder client: %v", err) + } + + exporterMock.On("Shutdown", mock.Anything).Return(nil).Once() + + err = client.Close() + assert.NoError(t, err) + + exporterMock.AssertExpectations(t) +} + +func TestClient_SetGlobas(t *testing.T) { + exporterMock := mocks.NewOTLPExporter(t) + defer exporterMock.AssertExpectations(t) + + otelErrorHandler := func(err error) { + t.Fatalf("otel error: %v", err) + } + // Override exporter factory which is used by Client + exporterFactory := func(context.Context, ...otlploggrpc.Option) (sdklog.Exporter, error) { + return exporterMock, nil + } + client, err := newOtelClient(DefaultConfig(), otelErrorHandler, exporterFactory) + if err != nil { + t.Fatalf("Error creating beholder client: %v", err) + } + assert.NoError(t, err) + + globals := getGlobals() + defer restoreGlobas(t, globals) + + client.SetGlobals() + + assert.Equal(t, client.LoggerProvider, otelglobal.GetLoggerProvider()) + assert.Equal(t, client.TracerProvider, otel.GetTracerProvider()) + assert.Equal(t, client.MeterProvider, otel.GetMeterProvider()) +} + +type globals struct { + loggerProvider otellog.LoggerProvider + tracerProvider oteltrace.TracerProvider + textMapPropagator propagation.TextMapPropagator + meterProvider otelmetric.MeterProvider +} + +func getGlobals() globals { + return globals{ + otelglobal.GetLoggerProvider(), + otel.GetTracerProvider(), + otel.GetTextMapPropagator(), + otel.GetMeterProvider(), + } +} + +func restoreGlobas(t *testing.T, g globals) { + otelglobal.SetLoggerProvider(g.loggerProvider) + otel.SetTracerProvider(g.tracerProvider) + otel.SetTextMapPropagator(g.textMapPropagator) + otel.SetMeterProvider(g.meterProvider) + + assert.Equal(t, g.loggerProvider, otelglobal.GetLoggerProvider()) + assert.Equal(t, g.tracerProvider, otel.GetTracerProvider()) + assert.Equal(t, g.textMapPropagator, otel.GetTextMapPropagator()) + assert.Equal(t, g.meterProvider, otel.GetMeterProvider()) +} diff --git a/pkg/beholder/noop.go b/pkg/beholder/noop.go index 5fdc96f53..094856d5f 100644 --- a/pkg/beholder/noop.go +++ b/pkg/beholder/noop.go @@ -33,9 +33,7 @@ func NewNoopClient() OtelClient { // MessageEmitter messageEmitter := noopMessageEmitter{} - onClose := func() error { return nil } - - client := OtelClient{cfg, logger, tracer, meter, messageEmitter, onClose} + client := OtelClient{cfg, logger, tracer, meter, messageEmitter, loggerProvider, tracerProvider, meterProvider} return client } @@ -72,11 +70,9 @@ func NewStdoutClient() OtelClient { meter := meterProvider.Meter(cfg.PackageName) // MessageEmitter - messageEmitter := newMessageEmitter(loggerExporter, logger) - - onClose := closeFunc(context.Background(), loggerProvider, tracerProvider, meterProvider) + emitter := messageEmitter{loggerExporter, logger} - client := OtelClient{cfg, logger, tracer, meter, messageEmitter, onClose} + client := OtelClient{cfg, logger, tracer, meter, emitter, loggerProvider, tracerProvider, meterProvider} return client } From ec50c05c7b341745d81426767ff0a6e0f339a3d8 Mon Sep 17 00:00:00 2001 From: Pavel <177363085+pkcll@users.noreply.github.com> Date: Tue, 13 Aug 2024 00:48:39 -0400 Subject: [PATCH 08/49] Beholder: make OtelClient.Emit variadic --- pkg/beholder/client.go | 8 ++-- pkg/beholder/message.go | 41 +++++++++++++++------ pkg/beholder/message_test.go | 71 +++++++++++++++++++++++------------- pkg/beholder/noop.go | 2 +- 4 files changed, 81 insertions(+), 41 deletions(-) diff --git a/pkg/beholder/client.go b/pkg/beholder/client.go index 821d670c6..80abc11b2 100644 --- a/pkg/beholder/client.go +++ b/pkg/beholder/client.go @@ -24,10 +24,10 @@ import ( ) type Emitter interface { - // Sends message with bytes and attributes to OTel Collector - Emit(ctx context.Context, body []byte, attrs map[string]any) error // Sends message to OTel Collector EmitMessage(ctx context.Context, m Message) error + // Sends message with bytes and attributes to OTel Collector + Emit(ctx context.Context, body []byte, attrKVs ...any) error } type Client interface { Close() error @@ -225,8 +225,8 @@ func newMessageEmitter( // Emits logs the message, but does not wait for the message to be processed. // Open question: what are pros/cons for using use map[]any vs use otellog.KeyValue -func (e messageEmitter) Emit(ctx context.Context, body []byte, attrs map[string]any) error { - message := NewMessage(body, attrs) +func (e messageEmitter) Emit(ctx context.Context, body []byte, attrKVs ...any) error { + message := NewMessage(body, attrKVs...) if err := message.Validate(); err != nil { return err } diff --git a/pkg/beholder/message.go b/pkg/beholder/message.go index d6e7964af..25a57c84e 100644 --- a/pkg/beholder/message.go +++ b/pkg/beholder/message.go @@ -11,7 +11,7 @@ import ( ) type Message struct { - Attrs map[string]any + Attrs Attributes Body []byte } @@ -68,26 +68,45 @@ func (m Metadata) Attributes() Attributes { type Attributes map[string]any -func NewAttributes(args ...any) Attributes { - attrs := make(Attributes, len(args)/2) - attrs.Add(args...) +func NewAttributes(attrKVs ...any) Attributes { + attrs := make(Attributes, len(attrKVs)/2) + attrs.Add(attrKVs...) return attrs } -func (a Attributes) Add(args ...any) Attributes { - for i := 1; i < len(args); i += 2 { - if key, ok := args[i-1].(string); ok { - val := args[i] - a[key] = val +func (a Attributes) Add(attrKVs ...any) Attributes { + l := len(attrKVs) + for i := 0; i < l; { + switch t := attrKVs[i].(type) { + case map[string]any: + for k, v := range t { + a[k] = v + } + i++ + case Attributes: + for k, v := range t { + a[k] = v + } + i++ + case string: + if i+1 >= l { + break + } + val := attrKVs[i+1] + a[t] = val + i += 2 + default: + // Unexpected type + return a } } return a } -func NewMessage(body []byte, attrs Attributes) Message { +func NewMessage(body []byte, attrKVs ...any) Message { return Message{ Body: body, - Attrs: attrs, + Attrs: NewAttributes(attrKVs...), } } diff --git a/pkg/beholder/message_test.go b/pkg/beholder/message_test.go index a547af5b0..5c6762b45 100644 --- a/pkg/beholder/message_test.go +++ b/pkg/beholder/message_test.go @@ -12,8 +12,8 @@ import ( func ExampleMessage() { // Create message with body and attributes - e1 := beholder.NewMessage([]byte{1}, beholder.Attributes{"key_string": "value"}) - fmt.Println("#1", e1) + m1 := beholder.NewMessage([]byte{1}, beholder.Attributes{"key_string": "value"}) + fmt.Println("#1", m1) // Create attributes additionalAttributes := beholder.Attributes{ "key_string": "new value", @@ -26,36 +26,56 @@ func ExampleMessage() { "key3", true, ) // Add attributes to message - e1.AddAttributes(additionalAttributes) - fmt.Println("#2", e1) - // Create empty message struct - e2 := beholder.Message{} - fmt.Println("#3", e2) + m1.AddAttributes(additionalAttributes) + fmt.Println("#2", m1) + // Create mmpty message struct + m2 := beholder.Message{} + fmt.Println("#3", m2) // Add attributes to message - e2.AddAttributes(beholder.Attributes{"key_int": 1}) - fmt.Println("#4", e2) + m2.AddAttributes(beholder.Attributes{"key_int": 1}) + fmt.Println("#4", m2) // Update attribute key_int - e2.AddAttributes(beholder.Attributes{"key_int": 2}) - fmt.Println("#5", e2) + m2.AddAttributes(beholder.Attributes{"key_int": 2}) + fmt.Println("#5", m2) // Set message body - e2.Body = []byte("0123") - fmt.Println("#6", e2) + m2.Body = []byte("0123") + fmt.Println("#6", m2) // Reset attributes - e2.Attrs = beholder.Attributes{} - fmt.Println("#7", e2) + m2.Attrs = beholder.Attributes{} + fmt.Println("#7", m2) // Reset body - e2.Body = nil - fmt.Println("#8", e2) + m2.Body = nil + fmt.Println("#8", m2) // Shalow copy of message - e3 := beholder.NewMessage(e1.Body, e1.Attrs) - fmt.Println("#9", e3) - e1.Body[0] = byte(2) // Wil mutate e3 - fmt.Println("#10", e3) + m3 := beholder.NewMessage(m1.Body, m1.Attrs) + fmt.Println("#9", m3) + m1.Body[0] = byte(2) // Wil mutate m3 + fmt.Println("#10", m3) // Deep copy - e4 := e1.Copy() - fmt.Println("#11", e4) - e1.Body[0] = byte(3) // Should not mutate e4 - fmt.Println("#12", e4) + m4 := m1.Copy() + fmt.Println("#11", m4) + m1.Body[0] = byte(3) // Should not mutate m4 + fmt.Println("#12", m4) + // Create message with mixed attributes: kv pairs and maps + m5 := beholder.NewMessage([]byte{1}, + // Add attributes from the map + map[string]any{ + "key1": "value1", + }, + // Add attributes from KV pair + "key2", "value2", + // Add attributes from Attributes map + beholder.Attributes{"key3": "value3"}, + // Add attributes from KV pair + "key4", "value4", + // Modify key1 + "key1", "value5", + // Modify key2 + map[string]any{ + "key2": "value6", + }, + ) + fmt.Println("#13", m5) // Output: // #1 Message{Attrs: map[key_string:value], Body: [1]} // #2 Message{Attrs: map[key3:true key_int32:2 key_string:updated value], Body: [1]} @@ -69,6 +89,7 @@ func ExampleMessage() { // #10 Message{Attrs: map[key3:true key_int32:2 key_string:updated value], Body: [2]} // #11 Message{Attrs: map[key3:true key_int32:2 key_string:updated value], Body: [2]} // #12 Message{Attrs: map[key3:true key_int32:2 key_string:updated value], Body: [2]} + // #13 Message{Attrs: map[key1:value5 key2:value6 key3:value3 key4:value4], Body: [1]} } func testMetadata() beholder.Metadata { diff --git a/pkg/beholder/noop.go b/pkg/beholder/noop.go index 094856d5f..c06abb452 100644 --- a/pkg/beholder/noop.go +++ b/pkg/beholder/noop.go @@ -79,7 +79,7 @@ func NewStdoutClient() OtelClient { type noopMessageEmitter struct{} -func (noopMessageEmitter) Emit(ctx context.Context, body []byte, attrs map[string]any) error { +func (noopMessageEmitter) Emit(ctx context.Context, body []byte, attrKVs ...any) error { return nil } func (noopMessageEmitter) EmitMessage(ctx context.Context, message Message) error { From c29bb1a1a6af35455862b8c1a347d3d53250399c Mon Sep 17 00:00:00 2001 From: Pavel <177363085+pkcll@users.noreply.github.com> Date: Tue, 13 Aug 2024 01:00:57 -0400 Subject: [PATCH 09/49] Beholder: fix test --- pkg/beholder/client_test.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pkg/beholder/client_test.go b/pkg/beholder/client_test.go index 0f43f16ae..9dfde14ad 100644 --- a/pkg/beholder/client_test.go +++ b/pkg/beholder/client_test.go @@ -281,9 +281,7 @@ func TestClient_Close(t *testing.T) { exporterMock := mocks.NewOTLPExporter(t) defer exporterMock.AssertExpectations(t) - otelErrorHandler := func(err error) { - t.Fatalf("otel error: %v", err) - } + otelErrorHandler := func(err error) {} // Override exporter factory which is used by Client exporterFactory := func(context.Context, ...otlploggrpc.Option) (sdklog.Exporter, error) { return exporterMock, nil From de7148038e71eabc589fd59dd126c6c1e441da14 Mon Sep 17 00:00:00 2001 From: Pavel <177363085+pkcll@users.noreply.github.com> Date: Tue, 13 Aug 2024 01:52:21 -0400 Subject: [PATCH 10/49] Fix test --- pkg/beholder/client_test.go | 14 ++------------ pkg/beholder/pb/example.pb.go | 2 +- 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/pkg/beholder/client_test.go b/pkg/beholder/client_test.go index 9dfde14ad..5db36d059 100644 --- a/pkg/beholder/client_test.go +++ b/pkg/beholder/client_test.go @@ -281,19 +281,9 @@ func TestClient_Close(t *testing.T) { exporterMock := mocks.NewOTLPExporter(t) defer exporterMock.AssertExpectations(t) - otelErrorHandler := func(err error) {} - // Override exporter factory which is used by Client - exporterFactory := func(context.Context, ...otlploggrpc.Option) (sdklog.Exporter, error) { - return exporterMock, nil - } - client, err := newOtelClient(DefaultConfig(), otelErrorHandler, exporterFactory) - if err != nil { - t.Fatalf("Error creating beholder client: %v", err) - } - - exporterMock.On("Shutdown", mock.Anything).Return(nil).Once() + client := NewStdoutClient() - err = client.Close() + err := client.Close() assert.NoError(t, err) exporterMock.AssertExpectations(t) diff --git a/pkg/beholder/pb/example.pb.go b/pkg/beholder/pb/example.pb.go index 5a5b47ea3..074b49f27 100644 --- a/pkg/beholder/pb/example.pb.go +++ b/pkg/beholder/pb/example.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.31.0 -// protoc v5.27.3 +// protoc v4.25.1 // source: example.proto package pb From b8f6c6f310051c056d18f9ba9dd20b98812996ac Mon Sep 17 00:00:00 2001 From: Pavel <177363085+pkcll@users.noreply.github.com> Date: Tue, 13 Aug 2024 12:31:34 -0400 Subject: [PATCH 11/49] Beholder: change config.ResourceAttributes type --- pkg/beholder/client.go | 6 +----- pkg/beholder/config.go | 14 +++----------- pkg/beholder/config_test.go | 10 ++++++---- 3 files changed, 10 insertions(+), 20 deletions(-) diff --git a/pkg/beholder/client.go b/pkg/beholder/client.go index 80abc11b2..7ffb57d0f 100644 --- a/pkg/beholder/client.go +++ b/pkg/beholder/client.go @@ -199,12 +199,8 @@ func newOtelResource(cfg Config) (resource *sdkresource.Resource, err error) { return nil, err } // Add custom resource attributes - attrs := make([]attribute.KeyValue, 0, len(cfg.ResourceAttributes)) - for k, v := range cfg.ResourceAttributes { - attrs = append(attrs, attribute.String(k, v)) - } resource, err = sdkresource.Merge( - sdkresource.NewSchemaless(attrs...), + sdkresource.NewSchemaless(cfg.ResourceAttributes...), resource, ) if err != nil { diff --git a/pkg/beholder/config.go b/pkg/beholder/config.go index 4c1c24f61..70d9af010 100644 --- a/pkg/beholder/config.go +++ b/pkg/beholder/config.go @@ -13,7 +13,7 @@ type Config struct { PackageName string // OTel Resource - ResourceAttributes map[string]string + ResourceAttributes []otelattr.KeyValue // Message Emitter EmitterExportTimeout time.Duration // OTel Trace @@ -25,8 +25,8 @@ type Config struct { LogExportTimeout time.Duration } -var defaultOtelAttributes = map[string]string{ - "package_name": "beholder", +var defaultOtelAttributes = []otelattr.KeyValue{ + otelattr.String("package_name", "beholder"), } func DefaultConfig() Config { @@ -48,11 +48,3 @@ func DefaultConfig() Config { LogExportTimeout: 1 * time.Second, } } - -func (c Config) Attributes() []otelattr.KeyValue { - attrs := make([]otelattr.KeyValue, 0, len(c.ResourceAttributes)) - for k, v := range c.ResourceAttributes { - attrs = append(attrs, otelattr.String(k, v)) - } - return attrs -} diff --git a/pkg/beholder/config_test.go b/pkg/beholder/config_test.go index 47b5aeeb1..02938eee4 100644 --- a/pkg/beholder/config_test.go +++ b/pkg/beholder/config_test.go @@ -4,6 +4,8 @@ import ( "fmt" "time" + otelattr "go.opentelemetry.io/otel/attribute" + beholder "github.com/smartcontractkit/chainlink-common/pkg/beholder" ) @@ -18,9 +20,9 @@ func ExampleConfig() { OtelExporterGRPCEndpoint: "localhost:4317", PackageName: packageName, // Resource - ResourceAttributes: map[string]string{ - "package_name": packageName, - "sender": "beholdeclient", + ResourceAttributes: []otelattr.KeyValue{ + otelattr.String("package_name", packageName), + otelattr.String("sender", "beholdeclient"), }, // Message Emitter EmitterExportTimeout: 1 * time.Second, @@ -34,5 +36,5 @@ func ExampleConfig() { } fmt.Printf("%+v", config) // Output: - // {InsecureConnection:true CACertFile: OtelExporterGRPCEndpoint:localhost:4317 PackageName:beholder ResourceAttributes:map[package_name:beholder sender:beholdeclient] EmitterExportTimeout:1s TraceSampleRate:1 TraceBatchTimeout:1s MetricReaderInterval:1s LogExportTimeout:1s} + // {InsecureConnection:true CACertFile: OtelExporterGRPCEndpoint:localhost:4317 PackageName:beholder ResourceAttributes:[{Key:package_name Value:{vtype:4 numeric:0 stringly:beholder slice:}} {Key:sender Value:{vtype:4 numeric:0 stringly:beholdeclient slice:}}] EmitterExportTimeout:1s TraceSampleRate:1 TraceBatchTimeout:1s MetricReaderInterval:1s LogExportTimeout:1s} } From 15d4bebb34706b81fa7b69157cce812157e6a20d Mon Sep 17 00:00:00 2001 From: Pavel <177363085+pkcll@users.noreply.github.com> Date: Tue, 13 Aug 2024 12:42:33 -0400 Subject: [PATCH 12/49] Beholder: improve test for Message attributes --- pkg/beholder/message.go | 37 ++++++++++++++++++------------------ pkg/beholder/message_test.go | 4 ++-- 2 files changed, 20 insertions(+), 21 deletions(-) diff --git a/pkg/beholder/message.go b/pkg/beholder/message.go index 25a57c84e..e6dbcb438 100644 --- a/pkg/beholder/message.go +++ b/pkg/beholder/message.go @@ -2,7 +2,6 @@ package beholder import ( "fmt" - "reflect" "github.com/go-playground/validator/v10" "go.opentelemetry.io/otel/attribute" @@ -46,24 +45,24 @@ type Metadata struct { } func (m Metadata) Attributes() Attributes { - attrs := make(Attributes, reflect.ValueOf(m).NumField()) - attrs["node_version"] = m.NodeVersion - attrs["node_csa_key"] = m.NodeCsaKey - attrs["node_csa_signature"] = m.NodeCsaSignature - attrs["don_id"] = m.DonID - attrs["network_name"] = m.NetworkName - attrs["workflow_id"] = m.WorkflowID - attrs["workflow_name"] = m.WorkflowName - attrs["workflow_owner_address"] = m.WorkflowOwnerAddress - attrs["workflow_spec_id"] = m.WorkflowSpecID - attrs["workflow_execution_id"] = m.WorkflowExecutionID - attrs["beholder_data_schema"] = m.BeholderDataSchema - attrs["capability_contract_address"] = m.CapabilityContractAddress - attrs["capability_id"] = m.CapabilityID - attrs["capability_version"] = m.CapabilityVersion - attrs["capability_name"] = m.CapabilityName - attrs["network_chain_id"] = m.NetworkChainID - return attrs + return Attributes{ + "node_version": m.NodeVersion, + "node_csa_key": m.NodeCsaKey, + "node_csa_signature": m.NodeCsaSignature, + "don_id": m.DonID, + "network_name": m.NetworkName, + "workflow_id": m.WorkflowID, + "workflow_name": m.WorkflowName, + "workflow_owner_address": m.WorkflowOwnerAddress, + "workflow_spec_id": m.WorkflowSpecID, + "workflow_execution_id": m.WorkflowExecutionID, + "beholder_data_schema": m.BeholderDataSchema, + "capability_contract_address": m.CapabilityContractAddress, + "capability_id": m.CapabilityID, + "capability_version": m.CapabilityVersion, + "capability_name": m.CapabilityName, + "network_chain_id": m.NetworkChainID, + } } type Attributes map[string]any diff --git a/pkg/beholder/message_test.go b/pkg/beholder/message_test.go index 5c6762b45..3b4df1d5f 100644 --- a/pkg/beholder/message_test.go +++ b/pkg/beholder/message_test.go @@ -114,10 +114,10 @@ func testMetadata() beholder.Metadata { } func ExampleMetadata() { m := testMetadata() - fmt.Println(m) + fmt.Printf("%#v\n", m) fmt.Println(m.Attributes()) // Output: - // {/schemas/ids/test_schema v1.0.0 test_key test_signature test_don_id [test_network] test_workflow_id test_workflow_name test_owner_address test_spec_id test_execution_id test_contract_address test_capability_id test_capability_version test_capability_name test_chain_id} + // beholder.Metadata{BeholderDataSchema:"/schemas/ids/test_schema", NodeVersion:"v1.0.0", NodeCsaKey:"test_key", NodeCsaSignature:"test_signature", DonID:"test_don_id", NetworkName:[]string{"test_network"}, WorkflowID:"test_workflow_id", WorkflowName:"test_workflow_name", WorkflowOwnerAddress:"test_owner_address", WorkflowSpecID:"test_spec_id", WorkflowExecutionID:"test_execution_id", CapabilityContractAddress:"test_contract_address", CapabilityID:"test_capability_id", CapabilityVersion:"test_capability_version", CapabilityName:"test_capability_name", NetworkChainID:"test_chain_id"} // map[beholder_data_schema:/schemas/ids/test_schema capability_contract_address:test_contract_address capability_id:test_capability_id capability_name:test_capability_name capability_version:test_capability_version don_id:test_don_id network_chain_id:test_chain_id network_name:[test_network] node_csa_key:test_key node_csa_signature:test_signature node_version:v1.0.0 workflow_execution_id:test_execution_id workflow_id:test_workflow_id workflow_name:test_workflow_name workflow_owner_address:test_owner_address workflow_spec_id:test_spec_id] } From b99d4393e83c1fafd899be0318746893e56b778c Mon Sep 17 00:00:00 2001 From: Pavel <177363085+pkcll@users.noreply.github.com> Date: Tue, 13 Aug 2024 13:07:22 -0400 Subject: [PATCH 13/49] Beholder: remove Exporter field from messageEmitter --- pkg/beholder/client.go | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/pkg/beholder/client.go b/pkg/beholder/client.go index 7ffb57d0f..1e5ad9d2f 100644 --- a/pkg/beholder/client.go +++ b/pkg/beholder/client.go @@ -144,7 +144,10 @@ func newOtelClient(cfg Config, errorHandler errorHandlerFunc, otlploggrpcNew otl sdklog.WithProcessor(messageLogProcessor), ) messageLogger := messageLoggerProvider.Logger(cfg.PackageName) - messageEmitter := newMessageEmitter(sharedLogExporter, messageLogger) + + messageEmitter := &messageEmitter{ + messageLogger: messageLogger, + } setOtelErrorHandler(errorHandler) @@ -209,16 +212,6 @@ func newOtelResource(cfg Config) (resource *sdkresource.Resource, err error) { return } -func newMessageEmitter( - exporter sdklog.Exporter, - messageLogger otellog.Logger, -) Emitter { - return messageEmitter{ - exporter: exporter, - messageLogger: messageLogger, - } -} - // Emits logs the message, but does not wait for the message to be processed. // Open question: what are pros/cons for using use map[]any vs use otellog.KeyValue func (e messageEmitter) Emit(ctx context.Context, body []byte, attrKVs ...any) error { From b80e230cd57a43ccf512f67c615aa5913148270b Mon Sep 17 00:00:00 2001 From: Pavel <177363085+pkcll@users.noreply.github.com> Date: Tue, 13 Aug 2024 15:02:54 -0400 Subject: [PATCH 14/49] Beholder: add onClose callback to shutdown providers --- pkg/beholder/client.go | 30 ++++++++++++++++++++++-------- pkg/beholder/noop.go | 8 ++++++-- 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/pkg/beholder/client.go b/pkg/beholder/client.go index 1e5ad9d2f..7fc8bd898 100644 --- a/pkg/beholder/client.go +++ b/pkg/beholder/client.go @@ -53,6 +53,9 @@ type OtelClient struct { LoggerProvider otellog.LoggerProvider TracerProvider oteltrace.TracerProvider MeterProvider otelmetric.MeterProvider + + // OnClose + onClose func() error } // NewOtelClient creates a new Client with OTel exporter @@ -151,11 +154,25 @@ func newOtelClient(cfg Config, errorHandler errorHandlerFunc, otlploggrpcNew otl setOtelErrorHandler(errorHandler) - client := OtelClient{cfg, logger, tracer, meter, messageEmitter, loggerProvider, tracerProvider, meterProvider} + onClose := func() (err error) { + for _, provider := range []shutdowner{messageLoggerProvider, loggerProvider, tracerProvider, meterProvider} { + err = errors.Join(err, provider.Shutdown(context.Background())) + } + return + } + client := OtelClient{cfg, logger, tracer, meter, messageEmitter, loggerProvider, tracerProvider, meterProvider, onClose} return client, nil } +// Sets the global OTel logger, tracer, meter providers. +// Makes them accessible from anywhere in the code via global otel getters: +// - otelglobal.GetLoggerProvider() +// - otel.GetTracerProvider() +// - otel.GetTextMapPropagator() +// - otel.GetMeterProvider() +// Any package that relies on go.opentelemetry.io will be able to pick up configured global providers +// e.g [otelgrpc](https://pkg.go.dev/go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc#example-NewServerHandler) func (c OtelClient) SetGlobals() { // Logger otelglobal.SetLoggerProvider(c.LoggerProvider) @@ -166,13 +183,10 @@ func (c OtelClient) SetGlobals() { otel.SetMeterProvider(c.MeterProvider) } +// Closes all providers, flushes all data and stops all background processes func (c OtelClient) Close() (err error) { - for _, provider := range []any{c.LoggerProvider, c.TracerProvider, c.MeterProvider} { - if p, ok := provider.(otelProvider); ok { - err = errors.Join(err, p.Shutdown(context.Background())) - } else { - err = errors.Join(err, errors.New("provider does not implement Shutdown method")) - } + if c.onClose != nil { + return c.onClose() } return } @@ -231,7 +245,7 @@ func (e messageEmitter) EmitMessage(ctx context.Context, message Message) error return nil } -type otelProvider interface { +type shutdowner interface { Shutdown(ctx context.Context) error } diff --git a/pkg/beholder/noop.go b/pkg/beholder/noop.go index c06abb452..04b559da7 100644 --- a/pkg/beholder/noop.go +++ b/pkg/beholder/noop.go @@ -33,7 +33,7 @@ func NewNoopClient() OtelClient { // MessageEmitter messageEmitter := noopMessageEmitter{} - client := OtelClient{cfg, logger, tracer, meter, messageEmitter, loggerProvider, tracerProvider, meterProvider} + client := OtelClient{cfg, logger, tracer, meter, messageEmitter, loggerProvider, tracerProvider, meterProvider, noopOnClose} return client } @@ -72,7 +72,7 @@ func NewStdoutClient() OtelClient { // MessageEmitter emitter := messageEmitter{loggerExporter, logger} - client := OtelClient{cfg, logger, tracer, meter, emitter, loggerProvider, tracerProvider, meterProvider} + client := OtelClient{cfg, logger, tracer, meter, emitter, loggerProvider, tracerProvider, meterProvider, noopOnClose} return client } @@ -85,3 +85,7 @@ func (noopMessageEmitter) Emit(ctx context.Context, body []byte, attrKVs ...any) func (noopMessageEmitter) EmitMessage(ctx context.Context, message Message) error { return nil } + +func noopOnClose() error { + return nil +} From 96027a3e2b2379bb38f5302876c0afc08a95bc97 Mon Sep 17 00:00:00 2001 From: Pavel <177363085+pkcll@users.noreply.github.com> Date: Tue, 13 Aug 2024 15:19:06 -0400 Subject: [PATCH 15/49] Beholder: use tests.Context(t) --- pkg/beholder/client_test.go | 9 +++++---- pkg/beholder/global/global_test.go | 8 ++++---- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/pkg/beholder/client_test.go b/pkg/beholder/client_test.go index 5db36d059..1808c573c 100644 --- a/pkg/beholder/client_test.go +++ b/pkg/beholder/client_test.go @@ -18,6 +18,7 @@ import ( oteltrace "go.opentelemetry.io/otel/trace" "github.com/smartcontractkit/chainlink-common/pkg/beholder/internal/mocks" + "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" ) type MockExporter struct { @@ -74,7 +75,7 @@ func TestClient(t *testing.T) { exporterMockErrorCount: 0, exporterOutputExpected: true, messageGenerator: func(client OtelClient, messageBody []byte, customAttributes map[string]any) { - err := client.Emitter.Emit(context.Background(), messageBody, customAttributes) + err := client.Emitter.Emit(tests.Context(t), messageBody, customAttributes) assert.NoError(t, err) }, }, { @@ -86,7 +87,7 @@ func TestClient(t *testing.T) { exporterOutputExpected: true, messageGenerator: func(client OtelClient, messageBody []byte, customAttributes map[string]any) { message := NewMessage(messageBody, customAttributes) - err := client.Emitter.EmitMessage(context.Background(), message) + err := client.Emitter.EmitMessage(tests.Context(t), message) assert.NoError(t, err) }, }, @@ -261,7 +262,7 @@ func TestEmitterMessageValidation(t *testing.T) { t.Run("Emitter.EmitMessage", func(t *testing.T) { emitter, message, assertExpectations := setupTest() - err := emitter.EmitMessage(context.Background(), message) + err := emitter.EmitMessage(tests.Context(t), message) assertExpectations(err) }) @@ -269,7 +270,7 @@ func TestEmitterMessageValidation(t *testing.T) { t.Run("Emitter.Emit", func(t *testing.T) { emitter, message, assertExpectations := setupTest() - err := emitter.Emit(context.Background(), message.Body, tc.attrs) + err := emitter.Emit(tests.Context(t), message.Body, tc.attrs) assertExpectations(err) }) diff --git a/pkg/beholder/global/global_test.go b/pkg/beholder/global/global_test.go index dda577ab8..081d4dad1 100644 --- a/pkg/beholder/global/global_test.go +++ b/pkg/beholder/global/global_test.go @@ -1,7 +1,6 @@ package global_test import ( - "context" "testing" "github.com/stretchr/testify/assert" @@ -15,6 +14,7 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/beholder" "github.com/smartcontractkit/chainlink-common/pkg/beholder/global" + "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" ) func TestGlobal(t *testing.T) { @@ -40,15 +40,15 @@ func TestGlobal(t *testing.T) { logger, tracer, meter, messageEmitter = global.Logger(), global.Tracer(), global.Meter(), global.Emitter() // Emit otel log record - logger.Emit(context.Background(), otellog.Record{}) + logger.Emit(tests.Context(t), otellog.Record{}) // Create trace span - ctx, span := tracer.Start(context.Background(), "ExampleGlobalClient", oteltrace.WithAttributes(otelattribute.String("key", "value"))) + ctx, span := tracer.Start(tests.Context(t), "ExampleGlobalClient", oteltrace.WithAttributes(otelattribute.String("key", "value"))) defer span.End() // Create metric counter counter, _ := meter.Int64Counter("global_counter") - counter.Add(context.Background(), 1) + counter.Add(tests.Context(t), 1) // Emit custom message err := messageEmitter.Emit(ctx, []byte("test"), beholder.Attributes{"key": "value"}) From 84357e5185442a17dd870226c45b83cd983dbd7b Mon Sep 17 00:00:00 2001 From: Pavel <177363085+pkcll@users.noreply.github.com> Date: Tue, 13 Aug 2024 18:04:17 -0400 Subject: [PATCH 16/49] Beholder: OtelAttr print unhandled value --- pkg/beholder/message.go | 2 +- pkg/beholder/message_test.go | 29 +++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/pkg/beholder/message.go b/pkg/beholder/message.go index e6dbcb438..f6f58c57f 100644 --- a/pkg/beholder/message.go +++ b/pkg/beholder/message.go @@ -202,7 +202,7 @@ func OtelAttr(key string, value any) otellog.KeyValue { case attribute.Value: return OtelAttr(key, v.AsInterface()) default: - return otellog.String(key, fmt.Sprintf("", v)) + return otellog.String(key, fmt.Sprintf("", v, v)) } } diff --git a/pkg/beholder/message_test.go b/pkg/beholder/message_test.go index 3b4df1d5f..e6cd7ecd3 100644 --- a/pkg/beholder/message_test.go +++ b/pkg/beholder/message_test.go @@ -2,10 +2,13 @@ package beholder_test import ( "fmt" + "slices" + "strings" "testing" "github.com/go-playground/validator/v10" "github.com/stretchr/testify/assert" + otellog "go.opentelemetry.io/otel/log" "github.com/smartcontractkit/chainlink-common/pkg/beholder" ) @@ -150,3 +153,29 @@ func TestAttributesConversion(t *testing.T) { actual := beholder.NewMetadata(attrs) assert.Equal(t, expected, *actual) } + +func TestMessage_OtelAttributes(t *testing.T) { + type notSupportedType struct { + value string + } + m := beholder.NewMessage([]byte{1}, beholder.Attributes{ + "key_string": "value", + "key_int": 1, + "not_supported_type": notSupportedType{"not supported type"}, + }) + otelAttrs := make([]otellog.KeyValue, 0, len(m.Attrs)) + for k, v := range m.Attrs { + otelAttrs = append(otelAttrs, beholder.OtelAttr(k, v)) + } + slices.SortFunc(otelAttrs, func(a, b otellog.KeyValue) int { + return strings.Compare(a.Key, b.Key) + }) + + assert.Equal(t, 3, len(otelAttrs)) + assert.Equal(t, "key_int", otelAttrs[0].Key) + assert.Equal(t, int64(1), otelAttrs[0].Value.AsInt64()) + assert.Equal(t, "key_string", otelAttrs[1].Key) + assert.Equal(t, "value", otelAttrs[1].Value.AsString()) + assert.Equal(t, "not_supported_type", otelAttrs[2].Key) + assert.Equal(t, "", otelAttrs[2].Value.AsString()) +} From 160a7d8ef4ae3bf4f994100bcabbfbbec1c24998 Mon Sep 17 00:00:00 2001 From: Pavel <177363085+pkcll@users.noreply.github.com> Date: Wed, 14 Aug 2024 13:20:24 -0400 Subject: [PATCH 17/49] Beholder: update global.Emit --- pkg/beholder/global/global.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/beholder/global/global.go b/pkg/beholder/global/global.go index d4a1c1343..79e9c0dae 100644 --- a/pkg/beholder/global/global.go +++ b/pkg/beholder/global/global.go @@ -60,8 +60,8 @@ func EmitMessage(ctx context.Context, message beholder.Message) error { return Emitter().EmitMessage(ctx, message) } -func Emit(ctx context.Context, body []byte, attrs beholder.Attributes) error { - return Emitter().Emit(ctx, body, attrs) +func Emit(ctx context.Context, body []byte, attrKVs ...any) error { + return Emitter().Emit(ctx, body, attrKVs...) } func Bootstrap(cfg beholder.Config, errorHandler func(error)) error { From e48eaedf0264f57f68c8a1a74080c5ccbf96de48 Mon Sep 17 00:00:00 2001 From: Pavel <177363085+pkcll@users.noreply.github.com> Date: Wed, 14 Aug 2024 13:41:16 -0400 Subject: [PATCH 18/49] Beholder: add NewMessage constructor to global --- pkg/beholder/global/global.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/beholder/global/global.go b/pkg/beholder/global/global.go index 79e9c0dae..e905000ed 100644 --- a/pkg/beholder/global/global.go +++ b/pkg/beholder/global/global.go @@ -78,3 +78,7 @@ func Bootstrap(cfg beholder.Config, errorHandler func(error)) error { func NewConfig() beholder.Config { return beholder.DefaultConfig() } + +func NewMessage(body []byte, attrKVs ...any) beholder.Message { + return beholder.NewMessage(body, attrKVs...) +} From 51a50c671f8d5a03ced7046a171c947c30246905 Mon Sep 17 00:00:00 2001 From: Pavel <177363085+pkcll@users.noreply.github.com> Date: Fri, 16 Aug 2024 00:54:58 -0400 Subject: [PATCH 19/49] Beholder: update example_test --- pkg/beholder/example_test.go | 177 ++++++++++++----------------------- 1 file changed, 58 insertions(+), 119 deletions(-) diff --git a/pkg/beholder/example_test.go b/pkg/beholder/example_test.go index 97eba2426..1c1670e8e 100644 --- a/pkg/beholder/example_test.go +++ b/pkg/beholder/example_test.go @@ -2,152 +2,91 @@ package beholder_test import ( "context" - "fmt" "log" - "sync" - "time" + "math/rand" - otelattribute "go.opentelemetry.io/otel/attribute" - otellog "go.opentelemetry.io/otel/log" - oteltrace "go.opentelemetry.io/otel/trace" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" "google.golang.org/protobuf/proto" - "github.com/smartcontractkit/chainlink-common/pkg/beholder" - "github.com/smartcontractkit/chainlink-common/pkg/beholder/global" + // chainlink-common + beholder "github.com/smartcontractkit/chainlink-common/pkg/beholder/global" "github.com/smartcontractkit/chainlink-common/pkg/beholder/pb" ) -func ExampleClient() { - ctx := context.Background() +func ExampleBeholderCustomMessage() { + beholderConfig := beholder.NewConfig() - client, err := beholder.NewOtelClient(beholder.DefaultConfig(), errorHandler) - if err != nil { - log.Fatalf("Error creating beholder client: %v", err) - } - var wg sync.WaitGroup - for i := range 3 { - wg.Add(1) - fmt.Printf("Emitting message %d\n", i) - go func(i int) { - // Create message metadata - metadata := beholder.Metadata{ - DonID: "test_don_id", - NetworkName: []string{"test_network"}, - NetworkChainID: "test_chain_id", - BeholderDataSchema: "/custom-message/versions/1", - } - // Create custom message - customMessage := beholder.Message{ - // Set protobuf message bytes as body - Body: newTestMessageBytes(i), - // Set metadata attributes - Attrs: metadata.Attributes().Add( - // Add custom attributes - "timestamp", time.Now().Unix(), - "sender", "example-client", - ), - } - // Emit custom message - err := client.Emitter.EmitMessage(ctx, customMessage) - if err != nil { - log.Fatalf("Error emitting message: %v", err) - } - wg.Done() - }(i) - } - wg.Wait() - // Output: - // Emitting message 0 - // Emitting message 1 - // Emitting message 2 -} - -func newTestMessageBytes(i int) []byte { - // Create protobuf message - customMessagePb := &pb.TestCustomMessage{} - customMessagePb.BoolVal = true - customMessagePb.IntVal = int64(i) - customMessagePb.FloatVal = float32(i) - customMessagePb.StringVal = fmt.Sprintf("string-value-%d", i) - customMessagePb.BytesVal = []byte{byte(i)} - // Encode protobuf message - customMessageBytes, err := proto.Marshal(customMessagePb) + // Bootstrap Beholder Client + err := beholder.Bootstrap(beholderConfig, errorHandler) if err != nil { - log.Fatalf("Error encoding message: %v", err) - } - return customMessageBytes -} - -func errorHandler(e error) { - if e != nil { - log.Fatalf("otel error: %v", e) + log.Fatalf("Error bootstrapping Beholder: %v", err) } -} -func asseetNoError(err error) { - if err != nil { - panic(err) + // Define a custom protobuf payload to emit + payload := &pb.TestCustomMessage{ + BoolVal: true, + IntVal: 42, + FloatVal: 3.14, + StringVal: "Hello, World!", } -} - -func ExampleEmitter() { - ctx := context.Background() - // Initialize beholder client - client, err := beholder.NewOtelClient(beholder.DefaultConfig(), asseetNoError) + payloadBytes, err := proto.Marshal(payload) if err != nil { - log.Fatalf("Error creating beholder client: %v", err) + log.Fatalf("Failed to marshal protobuf") } - // Set global client so it will be accessible from anywhere through beholder/global functions - global.SetClient(&client) - // After that you can use global functions to get logger, tracer, meter, messageEmitter - logger, tracer, meter, messageEmitter := global.Logger(), global.Tracer(), global.Meter(), global.Emitter() - - fmt.Println("Emit otel log record") - logger.Emit(ctx, otellog.Record{}) - fmt.Println("Create trace span") - ctx, span := tracer.Start(ctx, "ExampleGlobalClient", oteltrace.WithAttributes(otelattribute.String("key", "value"))) - defer span.End() + // Initialise custom message to wrap the payload + customMessage := beholder.NewMessage(payloadBytes, + "beholder_data_schema", "/custom-message/versions/1", // required + "beholder_data_type", "custom_message", + "foo", "bar", + ) - fmt.Println("Create metric counter") - counter, _ := meter.Int64Counter("global_counter") - counter.Add(ctx, 1) - - fmt.Println("Emit custom message") - err = messageEmitter.Emit(ctx, []byte("test"), beholder.Attributes{ - "key": "value", - "beholder_data_schema": "/test/versions/1", - }) - if err != nil { - log.Fatalf("Error emitting message: %v", err) + // Emit the custom message anywhere from application logic + for range 10 { + err := beholder.EmitMessage(context.Background(), customMessage) + if err != nil { + log.Printf("Error emitting message: %v", err) + } } // Output: - // Emit otel log record - // Create trace span - // Create metric counter - // Emit custom message } -func ExampleBootstrap() { - beholderConfig := beholder.DefaultConfig() +func ExampleBeholderMetricTraces() { + beholderConfig := beholder.NewConfig() // Bootstrap Beholder Client - err := global.Bootstrap(beholderConfig, errorHandler) + err := beholder.Bootstrap(beholderConfig, errorHandler) if err != nil { log.Fatalf("Error bootstrapping Beholder: %v", err) } - payloadBytes := newTestMessageBytes(0) + // Define a new counter + counter, err := beholder.Meter().Int64Counter("custom_message.count") + if err != nil { + log.Fatalf("failed to create new counter") + } - // Emit custom message - for range 3 { - err := global.Emit(context.Background(), payloadBytes, beholder.Attributes{ - "beholder_data_type": "custom_message", - "foo": "bar", - }) - if err != nil { - log.Printf("Error emitting message: %v", err) - } + // Define a new gauge + gauge, err := beholder.Meter().Int64Gauge("custom_message.gauge") + if err != nil { + log.Fatalf("failed to create new gauge") } + + // Use the counter and gauge for metrics within application logic + counter.Add(context.Background(), 1) + gauge.Record(context.Background(), rand.Int63n(101)) + + // Create a new trace span + _, rootSpan := beholder.Tracer().Start(context.Background(), "foo", trace.WithAttributes( + attribute.String("app_name", "beholderdemo"), + )) + defer rootSpan.End() // Output: } + +func errorHandler(e error) { + if e != nil { + log.Printf("otel error: %v", e) + } +} From b69b21e9e30864a913aed32713de6ec0d4255d4e Mon Sep 17 00:00:00 2001 From: Pavel <177363085+pkcll@users.noreply.github.com> Date: Fri, 16 Aug 2024 01:18:40 -0400 Subject: [PATCH 20/49] Beholder: Add ForPackage method to OtelClient, remove PackageName field from config --- pkg/beholder/client.go | 46 ++++++++++++++++++------- pkg/beholder/client_test.go | 28 +++++++++++++++ pkg/beholder/config.go | 6 ++-- pkg/beholder/config_test.go | 3 +- pkg/beholder/noop.go | 68 +++++++++++++++++++++++++++++-------- 5 files changed, 119 insertions(+), 32 deletions(-) diff --git a/pkg/beholder/client.go b/pkg/beholder/client.go index 7fc8bd898..9f0b37b1c 100644 --- a/pkg/beholder/client.go +++ b/pkg/beholder/client.go @@ -34,7 +34,6 @@ type Client interface { } type messageEmitter struct { - exporter sdklog.Exporter messageLogger otellog.Logger } @@ -50,12 +49,13 @@ type OtelClient struct { Emitter Emitter // Providers - LoggerProvider otellog.LoggerProvider - TracerProvider oteltrace.TracerProvider - MeterProvider otelmetric.MeterProvider + LoggerProvider otellog.LoggerProvider + TracerProvider oteltrace.TracerProvider + MeterProvider otelmetric.MeterProvider + MessageLoggerProvider otellog.LoggerProvider // OnClose - onClose func() error + OnClose func() error } // NewOtelClient creates a new Client with OTel exporter @@ -111,21 +111,21 @@ func newOtelClient(cfg Config, errorHandler errorHandlerFunc, otlploggrpcNew otl sdklog.WithResource(loggerResource), sdklog.WithProcessor(loggerProcessor), ) - logger := loggerProvider.Logger(cfg.PackageName) + logger := loggerProvider.Logger(defaultPackageName) // Tracer tracerProvider, err := newTracerProvider(cfg, baseResource, creds) if err != nil { return noop, err } - tracer := tracerProvider.Tracer(cfg.PackageName) + tracer := tracerProvider.Tracer(defaultPackageName) // Meter meterProvider, err := newMeterProvider(cfg, baseResource, creds) if err != nil { return noop, err } - meter := meterProvider.Meter(cfg.PackageName) + meter := meterProvider.Meter(defaultPackageName) // Message Emitter messageLogProcessor := sdklog.NewBatchProcessor( @@ -146,7 +146,7 @@ func newOtelClient(cfg Config, errorHandler errorHandlerFunc, otlploggrpcNew otl sdklog.WithResource(messageLoggerResource), sdklog.WithProcessor(messageLogProcessor), ) - messageLogger := messageLoggerProvider.Logger(cfg.PackageName) + messageLogger := messageLoggerProvider.Logger(defaultPackageName) messageEmitter := &messageEmitter{ messageLogger: messageLogger, @@ -155,12 +155,12 @@ func newOtelClient(cfg Config, errorHandler errorHandlerFunc, otlploggrpcNew otl setOtelErrorHandler(errorHandler) onClose := func() (err error) { - for _, provider := range []shutdowner{messageLoggerProvider, loggerProvider, tracerProvider, meterProvider} { + for _, provider := range []shutdowner{messageLoggerProvider, loggerProvider, tracerProvider, meterProvider, messageLoggerProvider} { err = errors.Join(err, provider.Shutdown(context.Background())) } return } - client := OtelClient{cfg, logger, tracer, meter, messageEmitter, loggerProvider, tracerProvider, meterProvider, onClose} + client := OtelClient{cfg, logger, tracer, meter, messageEmitter, loggerProvider, tracerProvider, meterProvider, messageLoggerProvider, onClose} return client, nil } @@ -185,12 +185,32 @@ func (c OtelClient) SetGlobals() { // Closes all providers, flushes all data and stops all background processes func (c OtelClient) Close() (err error) { - if c.onClose != nil { - return c.onClose() + if c.OnClose != nil { + return c.OnClose() } return } +// Returns a new OtelClient with the same configuration but with a different package name +func (c OtelClient) ForPackage(name string) OtelClient { + // Logger + logger := c.LoggerProvider.Logger(name) + // Tracer + tracer := c.TracerProvider.Tracer(name) + // Meter + meter := c.MeterProvider.Meter(name) + // Message Emitter + messageLogger := c.MessageLoggerProvider.Logger(name) + messageEmitter := &messageEmitter{messageLogger: messageLogger} + + newClient := c // copy + newClient.Logger = logger + newClient.Tracer = tracer + newClient.Meter = meter + newClient.Emitter = messageEmitter + return newClient +} + type errorHandlerFunc func(err error) // Sets the global error handler for OpenTelemetry diff --git a/pkg/beholder/client_test.go b/pkg/beholder/client_test.go index 1808c573c..d533a78e1 100644 --- a/pkg/beholder/client_test.go +++ b/pkg/beholder/client_test.go @@ -3,6 +3,7 @@ package beholder import ( "context" "fmt" + "strings" "testing" "time" @@ -344,3 +345,30 @@ func restoreGlobas(t *testing.T, g globals) { assert.Equal(t, g.textMapPropagator, otel.GetTextMapPropagator()) assert.Equal(t, g.meterProvider, otel.GetMeterProvider()) } + +func TestClient_ForPackage(t *testing.T) { + exporterMock := mocks.NewOTLPExporter(t) + defer exporterMock.AssertExpectations(t) + var b strings.Builder + client := NewStdoutClient(WithWriter(&b)) + clientForTest := client.ForPackage("TestClient_ForPackage") + + // Log + clientForTest.Logger.Emit(tests.Context(t), otellog.Record{}) + assert.Contains(t, b.String(), `"Name":"TestClient_ForPackage"`) + b.Reset() + + // Trace + _, span := clientForTest.Tracer.Start(tests.Context(t), "testSpan") + span.End() + assert.Contains(t, b.String(), `"Name":"TestClient_ForPackage"`) + assert.Contains(t, b.String(), "testSpan") + b.Reset() + + // Meter + counter, _ := clientForTest.Meter.Int64Counter("testMetric") + counter.Add(tests.Context(t), 1) + clientForTest.Close() + assert.Contains(t, b.String(), `"Name":"TestClient_ForPackage"`) + assert.Contains(t, b.String(), "testMetric") +} diff --git a/pkg/beholder/config.go b/pkg/beholder/config.go index 70d9af010..83c5312bb 100644 --- a/pkg/beholder/config.go +++ b/pkg/beholder/config.go @@ -11,7 +11,6 @@ type Config struct { CACertFile string OtelExporterGRPCEndpoint string - PackageName string // OTel Resource ResourceAttributes []otelattr.KeyValue // Message Emitter @@ -25,6 +24,10 @@ type Config struct { LogExportTimeout time.Duration } +const ( + defaultPackageName = "beholder" +) + var defaultOtelAttributes = []otelattr.KeyValue{ otelattr.String("package_name", "beholder"), } @@ -34,7 +37,6 @@ func DefaultConfig() Config { InsecureConnection: true, CACertFile: "", OtelExporterGRPCEndpoint: "localhost:4317", - PackageName: "beholder", // Resource ResourceAttributes: defaultOtelAttributes, // Message Emitter diff --git a/pkg/beholder/config_test.go b/pkg/beholder/config_test.go index 02938eee4..181ad0551 100644 --- a/pkg/beholder/config_test.go +++ b/pkg/beholder/config_test.go @@ -18,7 +18,6 @@ func ExampleConfig() { InsecureConnection: true, CACertFile: "", OtelExporterGRPCEndpoint: "localhost:4317", - PackageName: packageName, // Resource ResourceAttributes: []otelattr.KeyValue{ otelattr.String("package_name", packageName), @@ -36,5 +35,5 @@ func ExampleConfig() { } fmt.Printf("%+v", config) // Output: - // {InsecureConnection:true CACertFile: OtelExporterGRPCEndpoint:localhost:4317 PackageName:beholder ResourceAttributes:[{Key:package_name Value:{vtype:4 numeric:0 stringly:beholder slice:}} {Key:sender Value:{vtype:4 numeric:0 stringly:beholdeclient slice:}}] EmitterExportTimeout:1s TraceSampleRate:1 TraceBatchTimeout:1s MetricReaderInterval:1s LogExportTimeout:1s} + // {InsecureConnection:true CACertFile: OtelExporterGRPCEndpoint:localhost:4317 ResourceAttributes:[{Key:package_name Value:{vtype:4 numeric:0 stringly:beholder slice:}} {Key:sender Value:{vtype:4 numeric:0 stringly:beholdeclient slice:}}] EmitterExportTimeout:1s TraceSampleRate:1 TraceBatchTimeout:1s MetricReaderInterval:1s LogExportTimeout:1s} } diff --git a/pkg/beholder/noop.go b/pkg/beholder/noop.go index 04b559da7..405198f05 100644 --- a/pkg/beholder/noop.go +++ b/pkg/beholder/noop.go @@ -2,7 +2,9 @@ package beholder import ( "context" + "errors" "fmt" + "io" "time" "go.opentelemetry.io/otel/exporters/stdout/stdoutlog" @@ -21,19 +23,19 @@ func NewNoopClient() OtelClient { cfg := DefaultConfig() // Logger loggerProvider := otellognoop.NewLoggerProvider() - logger := loggerProvider.Logger(cfg.PackageName) + logger := loggerProvider.Logger(defaultPackageName) // Tracer tracerProvider := oteltracenoop.NewTracerProvider() - tracer := tracerProvider.Tracer(cfg.PackageName) + tracer := tracerProvider.Tracer(defaultPackageName) // Meter meterProvider := otelmetricnoop.NewMeterProvider() - meter := meterProvider.Meter(cfg.PackageName) + meter := meterProvider.Meter(defaultPackageName) // MessageEmitter messageEmitter := noopMessageEmitter{} - client := OtelClient{cfg, logger, tracer, meter, messageEmitter, loggerProvider, tracerProvider, meterProvider, noopOnClose} + client := OtelClient{cfg, logger, tracer, meter, messageEmitter, loggerProvider, tracerProvider, meterProvider, loggerProvider, noopOnClose} return client } @@ -41,38 +43,51 @@ func NewNoopClient() OtelClient { // NewStdoutClient creates a new Client with stdout exporters // Use for testing and debugging // Also this client is used as a noop client when otel exporter is not initialized properly -func NewStdoutClient() OtelClient { - cfg := DefaultConfig() +func NewStdoutClient(opts ...StddutClientOption) OtelClient { + cfg := DefaultStdoutClientConfig() + for _, opt := range opts { + opt(&cfg) + } // Logger - loggerExporter, _ := stdoutlog.New(stdoutlog.WithoutTimestamps()) // stdoutlog.New() never returns an error + loggerExporter, _ := stdoutlog.New( + append([]stdoutlog.Option{stdoutlog.WithoutTimestamps()}, cfg.LogOptions...)..., + ) // stdoutlog.New() never returns an error loggerProvider := sdklog.NewLoggerProvider(sdklog.WithProcessor(sdklog.NewSimpleProcessor(loggerExporter))) - logger := loggerProvider.Logger(cfg.PackageName) + logger := loggerProvider.Logger(defaultPackageName) setOtelErrorHandler(func(err error) { fmt.Printf("OTel error %s", err) }) // Tracer - traceExporter, _ := stdouttrace.New() // stdouttrace.New() never returns an error + traceExporter, _ := stdouttrace.New(cfg.TraceOptions...) // stdouttrace.New() never returns an error + tracerProvider := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor( sdktrace.NewSimpleSpanProcessor(traceExporter), )) - tracer := tracerProvider.Tracer(cfg.PackageName) + tracer := tracerProvider.Tracer(defaultPackageName) // Meter - metricExporter, _ := stdoutmetric.New() // stdoutmetric.New() never returns an error + metricExporter, _ := stdoutmetric.New(cfg.MetricOptions...) // stdoutmetric.New() never returns an error meterProvider := sdkmetric.NewMeterProvider( sdkmetric.WithReader( sdkmetric.NewPeriodicReader( metricExporter, - sdkmetric.WithInterval(time.Second), // Default is 10s + sdkmetric.WithInterval(100*time.Millisecond), // Default is 10s )), ) - meter := meterProvider.Meter(cfg.PackageName) + meter := meterProvider.Meter(defaultPackageName) // MessageEmitter - emitter := messageEmitter{loggerExporter, logger} + emitter := messageEmitter{messageLogger: logger} + + onClose := func() (err error) { + for _, provider := range []shutdowner{loggerProvider, tracerProvider, meterProvider} { + err = errors.Join(err, provider.Shutdown(context.Background())) + } + return + } - client := OtelClient{cfg, logger, tracer, meter, emitter, loggerProvider, tracerProvider, meterProvider, noopOnClose} + client := OtelClient{cfg.Config, logger, tracer, meter, emitter, loggerProvider, tracerProvider, meterProvider, loggerProvider, onClose} return client } @@ -89,3 +104,26 @@ func (noopMessageEmitter) EmitMessage(ctx context.Context, message Message) erro func noopOnClose() error { return nil } + +type StddutClientOption func(*StdoutClientConfig) + +type StdoutClientConfig struct { + Config + LogOptions []stdoutlog.Option + TraceOptions []stdouttrace.Option + MetricOptions []stdoutmetric.Option +} + +func DefaultStdoutClientConfig() StdoutClientConfig { + return StdoutClientConfig{ + Config: DefaultConfig(), + } +} + +func WithWriter(w io.Writer) StddutClientOption { + return func(cfg *StdoutClientConfig) { + cfg.LogOptions = append(cfg.LogOptions, stdoutlog.WithWriter(w)) + cfg.TraceOptions = append(cfg.TraceOptions, stdouttrace.WithWriter(w)) + cfg.MetricOptions = append(cfg.MetricOptions, stdoutmetric.WithWriter(w)) + } +} From 1392fd20dad391512f255acf8a2c827d74b3d66c Mon Sep 17 00:00:00 2001 From: Pavel <177363085+pkcll@users.noreply.github.com> Date: Fri, 16 Aug 2024 01:37:05 -0400 Subject: [PATCH 21/49] Beholder: remove EmitMessage method --- pkg/beholder/client.go | 2 -- pkg/beholder/client_test.go | 21 --------------------- pkg/beholder/example_test.go | 13 +++++-------- pkg/beholder/global/global.go | 4 ---- 4 files changed, 5 insertions(+), 35 deletions(-) diff --git a/pkg/beholder/client.go b/pkg/beholder/client.go index 9f0b37b1c..5a8424d99 100644 --- a/pkg/beholder/client.go +++ b/pkg/beholder/client.go @@ -24,8 +24,6 @@ import ( ) type Emitter interface { - // Sends message to OTel Collector - EmitMessage(ctx context.Context, m Message) error // Sends message with bytes and attributes to OTel Collector Emit(ctx context.Context, body []byte, attrKVs ...any) error } diff --git a/pkg/beholder/client_test.go b/pkg/beholder/client_test.go index d533a78e1..570009769 100644 --- a/pkg/beholder/client_test.go +++ b/pkg/beholder/client_test.go @@ -79,18 +79,6 @@ func TestClient(t *testing.T) { err := client.Emitter.Emit(tests.Context(t), messageBody, customAttributes) assert.NoError(t, err) }, - }, { - name: "Test EmitMessage", - makeCustomAttributes: defaultCustomAttributes, - messageBody: defaultMessageBody, - messageCount: 10, - exporterMockErrorCount: 0, - exporterOutputExpected: true, - messageGenerator: func(client OtelClient, messageBody []byte, customAttributes map[string]any) { - message := NewMessage(messageBody, customAttributes) - err := client.Emitter.EmitMessage(tests.Context(t), message) - assert.NoError(t, err) - }, }, } @@ -259,15 +247,6 @@ func TestEmitterMessageValidation(t *testing.T) { } return } - - t.Run("Emitter.EmitMessage", func(t *testing.T) { - emitter, message, assertExpectations := setupTest() - - err := emitter.EmitMessage(tests.Context(t), message) - - assertExpectations(err) - }) - t.Run("Emitter.Emit", func(t *testing.T) { emitter, message, assertExpectations := setupTest() diff --git a/pkg/beholder/example_test.go b/pkg/beholder/example_test.go index 1c1670e8e..92b0d8ef5 100644 --- a/pkg/beholder/example_test.go +++ b/pkg/beholder/example_test.go @@ -35,16 +35,13 @@ func ExampleBeholderCustomMessage() { log.Fatalf("Failed to marshal protobuf") } - // Initialise custom message to wrap the payload - customMessage := beholder.NewMessage(payloadBytes, - "beholder_data_schema", "/custom-message/versions/1", // required - "beholder_data_type", "custom_message", - "foo", "bar", - ) - // Emit the custom message anywhere from application logic for range 10 { - err := beholder.EmitMessage(context.Background(), customMessage) + err := beholder.Emit(context.Background(), payloadBytes, + "beholder_data_schema", "/custom-message/versions/1", // required + "beholder_data_type", "custom_message", + "foo", "bar", + ) if err != nil { log.Printf("Error emitting message: %v", err) } diff --git a/pkg/beholder/global/global.go b/pkg/beholder/global/global.go index e905000ed..3db22d5b4 100644 --- a/pkg/beholder/global/global.go +++ b/pkg/beholder/global/global.go @@ -56,10 +56,6 @@ func defaultClient() *atomic.Pointer[beholder.OtelClient] { return ptr } -func EmitMessage(ctx context.Context, message beholder.Message) error { - return Emitter().EmitMessage(ctx, message) -} - func Emit(ctx context.Context, body []byte, attrKVs ...any) error { return Emitter().Emit(ctx, body, attrKVs...) } From edb1e1175e0d265ab74b12916a78c1c3b07a88fd Mon Sep 17 00:00:00 2001 From: Pavel <177363085+pkcll@users.noreply.github.com> Date: Fri, 16 Aug 2024 16:32:43 -0400 Subject: [PATCH 22/49] Rename config field Co-authored-by: Jordan Krage --- pkg/beholder/client.go | 2 +- pkg/beholder/config.go | 4 ++-- pkg/beholder/config_test.go | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pkg/beholder/client.go b/pkg/beholder/client.go index 5a8424d99..004da1c59 100644 --- a/pkg/beholder/client.go +++ b/pkg/beholder/client.go @@ -283,7 +283,7 @@ func newTracerProvider(config Config, resource *sdkresource.Resource, creds cred sdktrace.WithResource(resource), sdktrace.WithSampler( sdktrace.ParentBased( - sdktrace.TraceIDRatioBased(config.TraceSampleRate), + sdktrace.TraceIDRatioBased(config.TraceSampleRatio), ), ), ) diff --git a/pkg/beholder/config.go b/pkg/beholder/config.go index 83c5312bb..4881816d5 100644 --- a/pkg/beholder/config.go +++ b/pkg/beholder/config.go @@ -16,7 +16,7 @@ type Config struct { // Message Emitter EmitterExportTimeout time.Duration // OTel Trace - TraceSampleRate float64 + TraceSampleRatio float64 TraceBatchTimeout time.Duration // OTel Metric MetricReaderInterval time.Duration @@ -42,7 +42,7 @@ func DefaultConfig() Config { // Message Emitter EmitterExportTimeout: 1 * time.Second, // Trace - TraceSampleRate: 1, + TraceSampleRatio: 1, TraceBatchTimeout: 1 * time.Second, // Metric MetricReaderInterval: 1 * time.Second, diff --git a/pkg/beholder/config_test.go b/pkg/beholder/config_test.go index 181ad0551..9243935ea 100644 --- a/pkg/beholder/config_test.go +++ b/pkg/beholder/config_test.go @@ -26,7 +26,7 @@ func ExampleConfig() { // Message Emitter EmitterExportTimeout: 1 * time.Second, // Trace - TraceSampleRate: 1, + TraceSampleRatio: 1, TraceBatchTimeout: 1 * time.Second, // Metric MetricReaderInterval: 1 * time.Second, @@ -35,5 +35,5 @@ func ExampleConfig() { } fmt.Printf("%+v", config) // Output: - // {InsecureConnection:true CACertFile: OtelExporterGRPCEndpoint:localhost:4317 ResourceAttributes:[{Key:package_name Value:{vtype:4 numeric:0 stringly:beholder slice:}} {Key:sender Value:{vtype:4 numeric:0 stringly:beholdeclient slice:}}] EmitterExportTimeout:1s TraceSampleRate:1 TraceBatchTimeout:1s MetricReaderInterval:1s LogExportTimeout:1s} + // {InsecureConnection:true CACertFile: OtelExporterGRPCEndpoint:localhost:4317 ResourceAttributes:[{Key:package_name Value:{vtype:4 numeric:0 stringly:beholder slice:}} {Key:sender Value:{vtype:4 numeric:0 stringly:beholdeclient slice:}}] EmitterExportTimeout:1s TraceSampleRatio:1 TraceBatchTimeout:1s MetricReaderInterval:1s LogExportTimeout:1s} } From bdc0025e42b546cb92108134dba0cc71caebb28b Mon Sep 17 00:00:00 2001 From: Pavel <177363085+pkcll@users.noreply.github.com> Date: Sun, 18 Aug 2024 21:29:40 -0400 Subject: [PATCH 23/49] Beholder: pass emitter struct by value to OtelClient --- pkg/beholder/client.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/beholder/client.go b/pkg/beholder/client.go index 004da1c59..5aa1ca8a9 100644 --- a/pkg/beholder/client.go +++ b/pkg/beholder/client.go @@ -146,7 +146,7 @@ func newOtelClient(cfg Config, errorHandler errorHandlerFunc, otlploggrpcNew otl ) messageLogger := messageLoggerProvider.Logger(defaultPackageName) - messageEmitter := &messageEmitter{ + emitter := messageEmitter{ messageLogger: messageLogger, } @@ -158,7 +158,7 @@ func newOtelClient(cfg Config, errorHandler errorHandlerFunc, otlploggrpcNew otl } return } - client := OtelClient{cfg, logger, tracer, meter, messageEmitter, loggerProvider, tracerProvider, meterProvider, messageLoggerProvider, onClose} + client := OtelClient{cfg, logger, tracer, meter, emitter, loggerProvider, tracerProvider, meterProvider, messageLoggerProvider, onClose} return client, nil } From 0e3f58736fb69ed015f5378dca5e80bd9f8fd57f Mon Sep 17 00:00:00 2001 From: Pavel <177363085+pkcll@users.noreply.github.com> Date: Sun, 18 Aug 2024 22:24:22 -0400 Subject: [PATCH 24/49] Beholder: add test for noop otel client --- pkg/beholder/noop_test.go | 55 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 pkg/beholder/noop_test.go diff --git a/pkg/beholder/noop_test.go b/pkg/beholder/noop_test.go new file mode 100644 index 000000000..78e13c43c --- /dev/null +++ b/pkg/beholder/noop_test.go @@ -0,0 +1,55 @@ +package beholder + +import ( + "context" + "log" + "math/rand" + "testing" + + "github.com/stretchr/testify/assert" + "go.opentelemetry.io/otel/attribute" + otellog "go.opentelemetry.io/otel/log" + "go.opentelemetry.io/otel/trace" + + "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" +) + +func TestNoopOtelClient(t *testing.T) { + noopOtelClient := NewNoopClient() + assert.NotNil(t, noopOtelClient) + + // Message Emitter + err := noopOtelClient.Emitter.Emit(tests.Context(t), []byte("test"), + "key1", "value1", + ) + assert.NoError(t, err) + + // Logger + noopOtelClient.Logger.Emit(tests.Context(t), otellog.Record{}) + + // Define a new counter + counter, err := noopOtelClient.Meter.Int64Counter("custom_message.count") + if err != nil { + log.Fatalf("failed to create new counter") + } + + // Define a new gauge + gauge, err := noopOtelClient.Meter.Int64Gauge("custom_message.gauge") + if err != nil { + log.Fatalf("failed to create new gauge") + } + assert.NoError(t, err) + + // Use the counter and gauge for metrics within application logic + counter.Add(tests.Context(t), 1) + gauge.Record(tests.Context(t), rand.Int63n(101)) + + // Create a new trace span + _, rootSpan := noopOtelClient.Tracer.Start(context.Background(), "foo", trace.WithAttributes( + attribute.String("app_name", "beholderdemo"), + )) + rootSpan.End() + + err = noopOtelClient.Close() + assert.NoError(t, err) +} From cd6f5a8399c982e16d2a32a8e0fc9cf9c7901f5a Mon Sep 17 00:00:00 2001 From: Pavel <177363085+pkcll@users.noreply.github.com> Date: Mon, 19 Aug 2024 19:09:07 -0400 Subject: [PATCH 25/49] Beholder: remove SdkOtelRecord --- pkg/beholder/message.go | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/pkg/beholder/message.go b/pkg/beholder/message.go index f6f58c57f..ae9843474 100644 --- a/pkg/beholder/message.go +++ b/pkg/beholder/message.go @@ -6,7 +6,6 @@ import ( "github.com/go-playground/validator/v10" "go.opentelemetry.io/otel/attribute" otellog "go.opentelemetry.io/otel/log" - otelsdklog "go.opentelemetry.io/otel/sdk/log" ) type Message struct { @@ -131,10 +130,6 @@ func (e *Message) OtelRecord() otellog.Record { return newRecord(e.Body, e.Attrs) } -func (e *Message) SdkOtelRecord() otelsdklog.Record { - return newSdkRecord(e.Body, e.Attrs) -} - func (e *Message) Copy() Message { attrs := make(Attributes, len(e.Attrs)) for k, v := range e.Attrs { @@ -162,19 +157,6 @@ func newRecord(body []byte, attrs map[string]any) otellog.Record { return otelRecord } -// Creates otelsdklog.Record from body and attributes -// NOTE: internal function otelsdklog.newRecord returns value not pointer -func newSdkRecord(body []byte, attrs map[string]any) otelsdklog.Record { - sdkRecord := otelsdklog.Record{} - if body != nil { - sdkRecord.SetBody(otellog.BytesValue(body)) - } - for k, v := range attrs { - sdkRecord.AddAttributes(OtelAttr(k, v)) - } - return sdkRecord -} - func OtelAttr(key string, value any) otellog.KeyValue { switch v := value.(type) { case string: From adcbefac63c645216cd5913a05558d6da7894612 Mon Sep 17 00:00:00 2001 From: Pavel <177363085+pkcll@users.noreply.github.com> Date: Mon, 19 Aug 2024 19:22:01 -0400 Subject: [PATCH 26/49] Beholder: refactor AddAttributes to take list of KV attributes --- pkg/beholder/message.go | 3 ++- pkg/beholder/message_test.go | 9 +++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/pkg/beholder/message.go b/pkg/beholder/message.go index ae9843474..faf72521d 100644 --- a/pkg/beholder/message.go +++ b/pkg/beholder/message.go @@ -108,7 +108,8 @@ func NewMessage(body []byte, attrKVs ...any) Message { } } -func (e *Message) AddAttributes(attrs Attributes) { +func (e *Message) AddAttributes(attrKVs ...any) { + attrs := NewAttributes(attrKVs...) if e.Attrs == nil { e.Attrs = make(map[string]any, len(attrs)) } diff --git a/pkg/beholder/message_test.go b/pkg/beholder/message_test.go index e6cd7ecd3..57521d5a4 100644 --- a/pkg/beholder/message_test.go +++ b/pkg/beholder/message_test.go @@ -79,6 +79,14 @@ func ExampleMessage() { }, ) fmt.Println("#13", m5) + // Create message with no attributes + m6 := beholder.NewMessage([]byte{1}, beholder.Attributes{}) + // Add attributes using AddAttributes + m6.AddAttributes( + "key1", "value1", + "key2", "value2", + ) + fmt.Println("#14", m6) // Output: // #1 Message{Attrs: map[key_string:value], Body: [1]} // #2 Message{Attrs: map[key3:true key_int32:2 key_string:updated value], Body: [1]} @@ -93,6 +101,7 @@ func ExampleMessage() { // #11 Message{Attrs: map[key3:true key_int32:2 key_string:updated value], Body: [2]} // #12 Message{Attrs: map[key3:true key_int32:2 key_string:updated value], Body: [2]} // #13 Message{Attrs: map[key1:value5 key2:value6 key3:value3 key4:value4], Body: [1]} + // #14 Message{Attrs: map[key1:value1 key2:value2], Body: [1]} } func testMetadata() beholder.Metadata { From d0a857af3419bae08cbc01c1fbb6c6f9d26db407 Mon Sep 17 00:00:00 2001 From: Pavel <177363085+pkcll@users.noreply.github.com> Date: Mon, 19 Aug 2024 19:33:31 -0400 Subject: [PATCH 27/49] Beholder: pass context through the example --- pkg/beholder/example_test.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pkg/beholder/example_test.go b/pkg/beholder/example_test.go index 92b0d8ef5..84a5cae98 100644 --- a/pkg/beholder/example_test.go +++ b/pkg/beholder/example_test.go @@ -58,6 +58,8 @@ func ExampleBeholderMetricTraces() { log.Fatalf("Error bootstrapping Beholder: %v", err) } + ctx := context.Background() + // Define a new counter counter, err := beholder.Meter().Int64Counter("custom_message.count") if err != nil { @@ -71,11 +73,11 @@ func ExampleBeholderMetricTraces() { } // Use the counter and gauge for metrics within application logic - counter.Add(context.Background(), 1) - gauge.Record(context.Background(), rand.Int63n(101)) + counter.Add(ctx, 1) + gauge.Record(ctx, rand.Int63n(101)) // Create a new trace span - _, rootSpan := beholder.Tracer().Start(context.Background(), "foo", trace.WithAttributes( + _, rootSpan := beholder.Tracer().Start(ctx, "foo", trace.WithAttributes( attribute.String("app_name", "beholderdemo"), )) defer rootSpan.End() From 2e9c6c10d7693b3677dd137c18c2c0ca8578290c Mon Sep 17 00:00:00 2001 From: Pavel <177363085+pkcll@users.noreply.github.com> Date: Mon, 19 Aug 2024 19:40:13 -0400 Subject: [PATCH 28/49] Beholder: fix typos --- pkg/beholder/client_test.go | 6 +++--- pkg/beholder/config_test.go | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pkg/beholder/client_test.go b/pkg/beholder/client_test.go index 570009769..054803a1d 100644 --- a/pkg/beholder/client_test.go +++ b/pkg/beholder/client_test.go @@ -270,7 +270,7 @@ func TestClient_Close(t *testing.T) { exporterMock.AssertExpectations(t) } -func TestClient_SetGlobas(t *testing.T) { +func TestClient_SetGlobals(t *testing.T) { exporterMock := mocks.NewOTLPExporter(t) defer exporterMock.AssertExpectations(t) @@ -288,7 +288,7 @@ func TestClient_SetGlobas(t *testing.T) { assert.NoError(t, err) globals := getGlobals() - defer restoreGlobas(t, globals) + defer restoreGlobals(t, globals) client.SetGlobals() @@ -313,7 +313,7 @@ func getGlobals() globals { } } -func restoreGlobas(t *testing.T, g globals) { +func restoreGlobals(t *testing.T, g globals) { otelglobal.SetLoggerProvider(g.loggerProvider) otel.SetTracerProvider(g.tracerProvider) otel.SetTextMapPropagator(g.textMapPropagator) diff --git a/pkg/beholder/config_test.go b/pkg/beholder/config_test.go index 9243935ea..3dbfd3a4e 100644 --- a/pkg/beholder/config_test.go +++ b/pkg/beholder/config_test.go @@ -21,7 +21,7 @@ func ExampleConfig() { // Resource ResourceAttributes: []otelattr.KeyValue{ otelattr.String("package_name", packageName), - otelattr.String("sender", "beholdeclient"), + otelattr.String("sender", "beholderclient"), }, // Message Emitter EmitterExportTimeout: 1 * time.Second, @@ -35,5 +35,5 @@ func ExampleConfig() { } fmt.Printf("%+v", config) // Output: - // {InsecureConnection:true CACertFile: OtelExporterGRPCEndpoint:localhost:4317 ResourceAttributes:[{Key:package_name Value:{vtype:4 numeric:0 stringly:beholder slice:}} {Key:sender Value:{vtype:4 numeric:0 stringly:beholdeclient slice:}}] EmitterExportTimeout:1s TraceSampleRatio:1 TraceBatchTimeout:1s MetricReaderInterval:1s LogExportTimeout:1s} + // {InsecureConnection:true CACertFile: OtelExporterGRPCEndpoint:localhost:4317 ResourceAttributes:[{Key:package_name Value:{vtype:4 numeric:0 stringly:beholder slice:}} {Key:sender Value:{vtype:4 numeric:0 stringly:beholderclient slice:}}] EmitterExportTimeout:1s TraceSampleRatio:1 TraceBatchTimeout:1s MetricReaderInterval:1s LogExportTimeout:1s} } From c8e8291c0792cd0d2f6f4cca0478361477481dd8 Mon Sep 17 00:00:00 2001 From: Pavel <177363085+pkcll@users.noreply.github.com> Date: Mon, 19 Aug 2024 20:03:35 -0400 Subject: [PATCH 29/49] Beholder: use simple log processor in tests --- pkg/beholder/client.go | 28 +++++++++---- pkg/beholder/client_test.go | 82 +++++++++---------------------------- pkg/beholder/config.go | 20 ++++++++- pkg/beholder/config_test.go | 8 ++-- 4 files changed, 62 insertions(+), 76 deletions(-) diff --git a/pkg/beholder/client.go b/pkg/beholder/client.go index 5aa1ca8a9..7b2e528d4 100644 --- a/pkg/beholder/client.go +++ b/pkg/beholder/client.go @@ -91,10 +91,15 @@ func newOtelClient(cfg Config, errorHandler errorHandlerFunc, otlploggrpcNew otl } // Logger - loggerProcessor := sdklog.NewBatchProcessor( - sharedLogExporter, - sdklog.WithExportTimeout(cfg.LogExportTimeout), // Default is 30s - ) + var loggerProcessor sdklog.Processor + if cfg.LogBatchProcessor { + loggerProcessor = sdklog.NewBatchProcessor( + sharedLogExporter, + sdklog.WithExportTimeout(cfg.LogExportTimeout), // Default is 30s + ) + } else { + loggerProcessor = sdklog.NewSimpleProcessor(sharedLogExporter) + } loggerAttributes := []attribute.KeyValue{ attribute.String("beholder_data_type", "zap_log_message"), } @@ -126,10 +131,16 @@ func newOtelClient(cfg Config, errorHandler errorHandlerFunc, otlploggrpcNew otl meter := meterProvider.Meter(defaultPackageName) // Message Emitter - messageLogProcessor := sdklog.NewBatchProcessor( - sharedLogExporter, - sdklog.WithExportTimeout(cfg.EmitterExportTimeout), // Default is 30s - ) + var messageLogProcessor sdklog.Processor + if cfg.EmitterBatchProcessor { + messageLogProcessor = sdklog.NewBatchProcessor( + sharedLogExporter, + sdklog.WithExportTimeout(cfg.EmitterExportTimeout), // Default is 30s + ) + } else { + messageLogProcessor = sdklog.NewSimpleProcessor(sharedLogExporter) + } + messageAttributes := []attribute.KeyValue{ attribute.String("beholder_data_type", "custom_message"), } @@ -140,6 +151,7 @@ func newOtelClient(cfg Config, errorHandler errorHandlerFunc, otlploggrpcNew otl if err != nil { return noop, err } + messageLoggerProvider := sdklog.NewLoggerProvider( sdklog.WithResource(messageLoggerResource), sdklog.WithProcessor(messageLogProcessor), diff --git a/pkg/beholder/client_test.go b/pkg/beholder/client_test.go index 054803a1d..85a9c014e 100644 --- a/pkg/beholder/client_test.go +++ b/pkg/beholder/client_test.go @@ -5,7 +5,6 @@ import ( "fmt" "strings" "testing" - "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -94,26 +93,24 @@ func TestClient(t *testing.T) { exporterFactory := func(context.Context, ...otlploggrpc.Option) (sdklog.Exporter, error) { return exporterMock, nil } - client, err := newOtelClient(DefaultConfig(), otelErrorHandler, exporterFactory) + client, err := newOtelClient(TestDefaultConfig(), otelErrorHandler, exporterFactory) if err != nil { t.Fatalf("Error creating beholder client: %v", err) } - // Number of messages to emit - done := make(chan struct{}, 1) + // Number of exported messages + exportedMessageCount := 0 // Simulate exporter error if configured if tc.exporterMockErrorCount > 0 { exporterMock.On("Export", mock.Anything, mock.Anything).Return(fmt.Errorf("an error occurred")).Times(tc.exporterMockErrorCount) } - customAttributes := tc.makeCustomAttributes() - if tc.exporterOutputExpected { - exporterMock.On("Export", mock.Anything, mock.Anything).Return(nil).Once(). + exporterMock.On("Export", mock.Anything, mock.Anything).Return(nil).Times(tc.messageCount). Run(func(args mock.Arguments) { assert.IsType(t, args.Get(1), []sdklog.Record{}, "Record type mismatch") records := args.Get(1).([]sdklog.Record) - assert.Equal(t, tc.messageCount, len(records), "Record count mismatch") + assert.Equal(t, 1, len(records), "batching is disabled, expecte 1 record") record := records[0] assert.Equal(t, tc.messageBody, record.Body().AsBytes(), "Record body mismatch") actualAttributeKeys := map[string]struct{}{} @@ -134,18 +131,13 @@ func TestClient(t *testing.T) { t.Fatalf("Record attribute key not found: %s", key) } } - done <- struct{}{} + exportedMessageCount += len(records) }) } for i := 0; i < tc.messageCount; i++ { - go tc.messageGenerator(client, tc.messageBody, customAttributes) - } - - select { - case <-done: - case <-time.After(10 * time.Second): - t.Fatalf("Timed out waiting for messages to be emitted") + tc.messageGenerator(client, tc.messageBody, customAttributes) } + assert.Equal(t, tc.messageCount, exportedMessageCount, "Expect all emitted messages to be exported") }) } } @@ -153,7 +145,7 @@ func TestClient(t *testing.T) { func TestEmitterMessageValidation(t *testing.T) { getEmitter := func(exporterMock *mocks.OTLPExporter) Emitter { client, err := newOtelClient( - DefaultConfig(), + TestDefaultConfig(), func(err error) { t.Fatalf("otel error: %v", err) }, // Override exporter factory which is used by Client func(context.Context, ...otlploggrpc.Option) (sdklog.Exporter, error) { @@ -196,63 +188,27 @@ func TestEmitterMessageValidation(t *testing.T) { }, } { t.Run(tc.name, func(t *testing.T) { - setupMock := func(exporterMock *mocks.OTLPExporter) (*mocks.OTLPExporter, <-chan struct{}) { - done := make(chan struct{}, tc.exporterCalledTimes) + t.Run("Emitter.Emit", func(t *testing.T) { + // Setup + exporterMock := mocks.NewOTLPExporter(t) if tc.exporterCalledTimes > 0 { - exporterMock.On("Export", mock.Anything, mock.Anything).Return(nil).Times(tc.exporterCalledTimes). - Run(func(args mock.Arguments) { - done <- struct{}{} - }) + exporterMock.On("Export", mock.Anything, mock.Anything).Return(nil).Times(tc.exporterCalledTimes) } - return exporterMock, done - } - - assertError := func(err error, expected string) { + emitter := getEmitter(exporterMock) + message := NewMessage([]byte("test"), tc.attrs) + // Emit + err := emitter.Emit(tests.Context(t), message.Body, tc.attrs) + // Assert expectations if tc.expectedError != "" { - assert.ErrorContains(t, err, expected) + assert.ErrorContains(t, err, tc.expectedError) } else { assert.NoError(t, err) } - } - - assertMock := func(exporterMock *mocks.OTLPExporter) { if tc.exporterCalledTimes > 0 { exporterMock.AssertExpectations(t) } else { exporterMock.AssertNotCalled(t, "Export") } - } - - waitUntilSent := func(done <-chan struct{}) { - for range tc.exporterCalledTimes { - select { - case <-done: - case <-time.After(10 * time.Second): - t.Fatalf("Timed out waiting for messages to be emitted") - } - } - } - - setupTest := func() (emitter Emitter, message Message, assertExpectations func(err error)) { - exporterMock, done := setupMock(mocks.NewOTLPExporter(t)) - emitter = getEmitter(exporterMock) - message = NewMessage([]byte("test"), tc.attrs) - - assertExpectations = func(err error) { - assertError(err, tc.expectedError) - if err == nil { - waitUntilSent(done) - } - assertMock(exporterMock) - } - return - } - t.Run("Emitter.Emit", func(t *testing.T) { - emitter, message, assertExpectations := setupTest() - - err := emitter.Emit(tests.Context(t), message.Body, tc.attrs) - - assertExpectations(err) }) }) } diff --git a/pkg/beholder/config.go b/pkg/beholder/config.go index 4881816d5..34641185d 100644 --- a/pkg/beholder/config.go +++ b/pkg/beholder/config.go @@ -15,6 +15,9 @@ type Config struct { ResourceAttributes []otelattr.KeyValue // Message Emitter EmitterExportTimeout time.Duration + // Batch processing is enabled by default + // Disable it only for testing + EmitterBatchProcessor bool // OTel Trace TraceSampleRatio float64 TraceBatchTimeout time.Duration @@ -22,6 +25,9 @@ type Config struct { MetricReaderInterval time.Duration // OTel Log LogExportTimeout time.Duration + // Batch processing is enabled by default + // Disable it only for testing + LogBatchProcessor bool } const ( @@ -40,13 +46,23 @@ func DefaultConfig() Config { // Resource ResourceAttributes: defaultOtelAttributes, // Message Emitter - EmitterExportTimeout: 1 * time.Second, + EmitterExportTimeout: 1 * time.Second, + EmitterBatchProcessor: true, // Trace TraceSampleRatio: 1, TraceBatchTimeout: 1 * time.Second, // Metric MetricReaderInterval: 1 * time.Second, // Log - LogExportTimeout: 1 * time.Second, + LogExportTimeout: 1 * time.Second, + LogBatchProcessor: true, } } + +func TestDefaultConfig() Config { + config := DefaultConfig() + // Should be only disabled for testing + config.EmitterBatchProcessor = false + config.LogBatchProcessor = false + return config +} diff --git a/pkg/beholder/config_test.go b/pkg/beholder/config_test.go index 3dbfd3a4e..edf578310 100644 --- a/pkg/beholder/config_test.go +++ b/pkg/beholder/config_test.go @@ -24,16 +24,18 @@ func ExampleConfig() { otelattr.String("sender", "beholderclient"), }, // Message Emitter - EmitterExportTimeout: 1 * time.Second, + EmitterExportTimeout: 1 * time.Second, + EmitterBatchProcessor: true, // Trace TraceSampleRatio: 1, TraceBatchTimeout: 1 * time.Second, // Metric MetricReaderInterval: 1 * time.Second, // Log - LogExportTimeout: 1 * time.Second, + LogExportTimeout: 1 * time.Second, + LogBatchProcessor: true, } fmt.Printf("%+v", config) // Output: - // {InsecureConnection:true CACertFile: OtelExporterGRPCEndpoint:localhost:4317 ResourceAttributes:[{Key:package_name Value:{vtype:4 numeric:0 stringly:beholder slice:}} {Key:sender Value:{vtype:4 numeric:0 stringly:beholderclient slice:}}] EmitterExportTimeout:1s TraceSampleRatio:1 TraceBatchTimeout:1s MetricReaderInterval:1s LogExportTimeout:1s} + // {InsecureConnection:true CACertFile: OtelExporterGRPCEndpoint:localhost:4317 ResourceAttributes:[{Key:package_name Value:{vtype:4 numeric:0 stringly:beholder slice:}} {Key:sender Value:{vtype:4 numeric:0 stringly:beholderclient slice:}}] EmitterExportTimeout:1s EmitterBatchProcessor:true TraceSampleRatio:1 TraceBatchTimeout:1s MetricReaderInterval:1s LogExportTimeout:1s LogBatchProcessor:true} } From faa61c20702f1c1ccc1bebd6ca2e64c450f63df1 Mon Sep 17 00:00:00 2001 From: Pavel <177363085+pkcll@users.noreply.github.com> Date: Tue, 20 Aug 2024 00:01:12 -0400 Subject: [PATCH 30/49] Beholder: move SetGlobals to global package --- pkg/beholder/client.go | 20 ---------- pkg/beholder/client_test.go | 60 ------------------------------ pkg/beholder/global/global.go | 22 +++++++++++ pkg/beholder/global/global_test.go | 50 +++++++++++++++++++++++++ 4 files changed, 72 insertions(+), 80 deletions(-) diff --git a/pkg/beholder/client.go b/pkg/beholder/client.go index 7b2e528d4..8f430c97d 100644 --- a/pkg/beholder/client.go +++ b/pkg/beholder/client.go @@ -10,9 +10,7 @@ import ( "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc" "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" otellog "go.opentelemetry.io/otel/log" - otelglobal "go.opentelemetry.io/otel/log/global" otelmetric "go.opentelemetry.io/otel/metric" - "go.opentelemetry.io/otel/propagation" sdklog "go.opentelemetry.io/otel/sdk/log" sdkmetric "go.opentelemetry.io/otel/sdk/metric" sdkresource "go.opentelemetry.io/otel/sdk/resource" @@ -175,24 +173,6 @@ func newOtelClient(cfg Config, errorHandler errorHandlerFunc, otlploggrpcNew otl return client, nil } -// Sets the global OTel logger, tracer, meter providers. -// Makes them accessible from anywhere in the code via global otel getters: -// - otelglobal.GetLoggerProvider() -// - otel.GetTracerProvider() -// - otel.GetTextMapPropagator() -// - otel.GetMeterProvider() -// Any package that relies on go.opentelemetry.io will be able to pick up configured global providers -// e.g [otelgrpc](https://pkg.go.dev/go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc#example-NewServerHandler) -func (c OtelClient) SetGlobals() { - // Logger - otelglobal.SetLoggerProvider(c.LoggerProvider) - // Tracer - otel.SetTracerProvider(c.TracerProvider) - otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{})) - // Meter - otel.SetMeterProvider(c.MeterProvider) -} - // Closes all providers, flushes all data and stops all background processes func (c OtelClient) Close() (err error) { if c.OnClose != nil { diff --git a/pkg/beholder/client_test.go b/pkg/beholder/client_test.go index 85a9c014e..20a21d28c 100644 --- a/pkg/beholder/client_test.go +++ b/pkg/beholder/client_test.go @@ -8,14 +8,9 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" - "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc" otellog "go.opentelemetry.io/otel/log" - otelglobal "go.opentelemetry.io/otel/log/global" - otelmetric "go.opentelemetry.io/otel/metric" - "go.opentelemetry.io/otel/propagation" sdklog "go.opentelemetry.io/otel/sdk/log" - oteltrace "go.opentelemetry.io/otel/trace" "github.com/smartcontractkit/chainlink-common/pkg/beholder/internal/mocks" "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" @@ -226,61 +221,6 @@ func TestClient_Close(t *testing.T) { exporterMock.AssertExpectations(t) } -func TestClient_SetGlobals(t *testing.T) { - exporterMock := mocks.NewOTLPExporter(t) - defer exporterMock.AssertExpectations(t) - - otelErrorHandler := func(err error) { - t.Fatalf("otel error: %v", err) - } - // Override exporter factory which is used by Client - exporterFactory := func(context.Context, ...otlploggrpc.Option) (sdklog.Exporter, error) { - return exporterMock, nil - } - client, err := newOtelClient(DefaultConfig(), otelErrorHandler, exporterFactory) - if err != nil { - t.Fatalf("Error creating beholder client: %v", err) - } - assert.NoError(t, err) - - globals := getGlobals() - defer restoreGlobals(t, globals) - - client.SetGlobals() - - assert.Equal(t, client.LoggerProvider, otelglobal.GetLoggerProvider()) - assert.Equal(t, client.TracerProvider, otel.GetTracerProvider()) - assert.Equal(t, client.MeterProvider, otel.GetMeterProvider()) -} - -type globals struct { - loggerProvider otellog.LoggerProvider - tracerProvider oteltrace.TracerProvider - textMapPropagator propagation.TextMapPropagator - meterProvider otelmetric.MeterProvider -} - -func getGlobals() globals { - return globals{ - otelglobal.GetLoggerProvider(), - otel.GetTracerProvider(), - otel.GetTextMapPropagator(), - otel.GetMeterProvider(), - } -} - -func restoreGlobals(t *testing.T, g globals) { - otelglobal.SetLoggerProvider(g.loggerProvider) - otel.SetTracerProvider(g.tracerProvider) - otel.SetTextMapPropagator(g.textMapPropagator) - otel.SetMeterProvider(g.meterProvider) - - assert.Equal(t, g.loggerProvider, otelglobal.GetLoggerProvider()) - assert.Equal(t, g.tracerProvider, otel.GetTracerProvider()) - assert.Equal(t, g.textMapPropagator, otel.GetTextMapPropagator()) - assert.Equal(t, g.meterProvider, otel.GetMeterProvider()) -} - func TestClient_ForPackage(t *testing.T) { exporterMock := mocks.NewOTLPExporter(t) defer exporterMock.AssertExpectations(t) diff --git a/pkg/beholder/global/global.go b/pkg/beholder/global/global.go index 3db22d5b4..41952c3d1 100644 --- a/pkg/beholder/global/global.go +++ b/pkg/beholder/global/global.go @@ -4,8 +4,11 @@ import ( "context" "sync/atomic" + "go.opentelemetry.io/otel" otellog "go.opentelemetry.io/otel/log" + otellogglobal "go.opentelemetry.io/otel/log/global" otelmetric "go.opentelemetry.io/otel/metric" + "go.opentelemetry.io/otel/propagation" oteltrace "go.opentelemetry.io/otel/trace" "github.com/smartcontractkit/chainlink-common/pkg/beholder" @@ -78,3 +81,22 @@ func NewConfig() beholder.Config { func NewMessage(body []byte, attrKVs ...any) beholder.Message { return beholder.NewMessage(body, attrKVs...) } + +// Sets the global OTel logger, tracer, meter providers from OtelClient +// Makes them accessible from anywhere in the code via global otel getters: +// - otellog.GetLoggerProvider() +// - otel.GetTracerProvider() +// - otel.GetTextMapPropagator() +// - otel.GetMeterProvider() +// Any package that relies on go.opentelemetry.io will be able to pick up configured global providers +// e.g [otelgrpc](https://pkg.go.dev/go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc#example-NewServerHandler) +func SetGlobalOtelProviders() { + c := GetClient() + // Logger + otellogglobal.SetLoggerProvider(c.LoggerProvider) + // Tracer + otel.SetTracerProvider(c.TracerProvider) + otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{})) + // Meter + otel.SetMeterProvider(c.MeterProvider) +} diff --git a/pkg/beholder/global/global_test.go b/pkg/beholder/global/global_test.go index 081d4dad1..b585918d2 100644 --- a/pkg/beholder/global/global_test.go +++ b/pkg/beholder/global/global_test.go @@ -1,19 +1,25 @@ package global_test import ( + "strings" "testing" "github.com/stretchr/testify/assert" + "go.opentelemetry.io/otel" otelattribute "go.opentelemetry.io/otel/attribute" otellog "go.opentelemetry.io/otel/log" + otellogglobal "go.opentelemetry.io/otel/log/global" otellognoop "go.opentelemetry.io/otel/log/noop" + otelmetric "go.opentelemetry.io/otel/metric" otelmetricnoop "go.opentelemetry.io/otel/metric/noop" + "go.opentelemetry.io/otel/propagation" oteltrace "go.opentelemetry.io/otel/trace" oteltracenoop "go.opentelemetry.io/otel/trace/noop" "github.com/smartcontractkit/chainlink-common/pkg/beholder" "github.com/smartcontractkit/chainlink-common/pkg/beholder/global" + "github.com/smartcontractkit/chainlink-common/pkg/beholder/internal/mocks" "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" ) @@ -56,3 +62,47 @@ func TestGlobal(t *testing.T) { t.Fatalf("Error emitting message: %v", err) } } + +func TestClient_SetGlobalOtelProviders(t *testing.T) { + exporterMock := mocks.NewOTLPExporter(t) + defer exporterMock.AssertExpectations(t) + + // Restore global providers after test + defer restoreProviders(t, providers{ + otellogglobal.GetLoggerProvider(), + otel.GetTracerProvider(), + otel.GetTextMapPropagator(), + otel.GetMeterProvider(), + }) + + var b strings.Builder + client := beholder.NewStdoutClient(beholder.WithWriter(&b)) + // Set global Otel Client + global.SetClient(&client) + + // Set global otel tracer, meter, logger providers from global client + global.SetGlobalOtelProviders() + + assert.Equal(t, client.LoggerProvider, otellogglobal.GetLoggerProvider()) + assert.Equal(t, client.TracerProvider, otel.GetTracerProvider()) + assert.Equal(t, client.MeterProvider, otel.GetMeterProvider()) +} + +type providers struct { + loggerProvider otellog.LoggerProvider + tracerProvider oteltrace.TracerProvider + textMapPropagator propagation.TextMapPropagator + meterProvider otelmetric.MeterProvider +} + +func restoreProviders(t *testing.T, p providers) { + otellogglobal.SetLoggerProvider(p.loggerProvider) + otel.SetTracerProvider(p.tracerProvider) + otel.SetTextMapPropagator(p.textMapPropagator) + otel.SetMeterProvider(p.meterProvider) + + assert.Equal(t, p.loggerProvider, otellogglobal.GetLoggerProvider()) + assert.Equal(t, p.tracerProvider, otel.GetTracerProvider()) + assert.Equal(t, p.textMapPropagator, otel.GetTextMapPropagator()) + assert.Equal(t, p.meterProvider, otel.GetMeterProvider()) +} From 4e80921db29d6f144890789656570af35a17f528 Mon Sep 17 00:00:00 2001 From: Pavel <177363085+pkcll@users.noreply.github.com> Date: Wed, 21 Aug 2024 17:35:12 -0400 Subject: [PATCH 31/49] Beholder: make message Attributes to be an alias of map --- pkg/beholder/message.go | 30 ++++++------------------------ pkg/beholder/message_test.go | 16 +++++----------- 2 files changed, 11 insertions(+), 35 deletions(-) diff --git a/pkg/beholder/message.go b/pkg/beholder/message.go index faf72521d..2b1d89d3a 100644 --- a/pkg/beholder/message.go +++ b/pkg/beholder/message.go @@ -64,23 +64,14 @@ func (m Metadata) Attributes() Attributes { } } -type Attributes map[string]any +type Attributes = map[string]any -func NewAttributes(attrKVs ...any) Attributes { - attrs := make(Attributes, len(attrKVs)/2) - attrs.Add(attrKVs...) - return attrs -} +func newAttributes(attrKVs ...any) Attributes { + a := make(Attributes, len(attrKVs)/2) -func (a Attributes) Add(attrKVs ...any) Attributes { l := len(attrKVs) for i := 0; i < l; { switch t := attrKVs[i].(type) { - case map[string]any: - for k, v := range t { - a[k] = v - } - i++ case Attributes: for k, v := range t { a[k] = v @@ -104,29 +95,20 @@ func (a Attributes) Add(attrKVs ...any) Attributes { func NewMessage(body []byte, attrKVs ...any) Message { return Message{ Body: body, - Attrs: NewAttributes(attrKVs...), + Attrs: newAttributes(attrKVs...), } } func (e *Message) AddAttributes(attrKVs ...any) { - attrs := NewAttributes(attrKVs...) + attrs := newAttributes(attrKVs...) if e.Attrs == nil { - e.Attrs = make(map[string]any, len(attrs)) + e.Attrs = make(map[string]any, len(attrs)/2) } for k, v := range attrs { e.Attrs[k] = v } } -func (e *Message) AddOtelAttributes(attrs ...attribute.KeyValue) { - if e.Attrs == nil { - e.Attrs = make(map[string]any, len(attrs)) - } - for _, v := range attrs { - e.Attrs[string(v.Key)] = v.Value - } -} - func (e *Message) OtelRecord() otellog.Record { return newRecord(e.Body, e.Attrs) } diff --git a/pkg/beholder/message_test.go b/pkg/beholder/message_test.go index 57521d5a4..1f8f990fb 100644 --- a/pkg/beholder/message_test.go +++ b/pkg/beholder/message_test.go @@ -22,12 +22,6 @@ func ExampleMessage() { "key_string": "new value", "key_int32": int32(1), } - // Add more attributes - additionalAttributes.Add( - "key_string", "updated value", // this will overrider previous value - "key_int32", int32(2), - "key3", true, - ) // Add attributes to message m1.AddAttributes(additionalAttributes) fmt.Println("#2", m1) @@ -89,17 +83,17 @@ func ExampleMessage() { fmt.Println("#14", m6) // Output: // #1 Message{Attrs: map[key_string:value], Body: [1]} - // #2 Message{Attrs: map[key3:true key_int32:2 key_string:updated value], Body: [1]} + // #2 Message{Attrs: map[key_int32:1 key_string:new value], Body: [1]} // #3 Message{Attrs: map[], Body: []} // #4 Message{Attrs: map[key_int:1], Body: []} // #5 Message{Attrs: map[key_int:2], Body: []} // #6 Message{Attrs: map[key_int:2], Body: [48 49 50 51]} // #7 Message{Attrs: map[], Body: [48 49 50 51]} // #8 Message{Attrs: map[], Body: []} - // #9 Message{Attrs: map[key3:true key_int32:2 key_string:updated value], Body: [1]} - // #10 Message{Attrs: map[key3:true key_int32:2 key_string:updated value], Body: [2]} - // #11 Message{Attrs: map[key3:true key_int32:2 key_string:updated value], Body: [2]} - // #12 Message{Attrs: map[key3:true key_int32:2 key_string:updated value], Body: [2]} + // #9 Message{Attrs: map[key_int32:1 key_string:new value], Body: [1]} + // #10 Message{Attrs: map[key_int32:1 key_string:new value], Body: [2]} + // #11 Message{Attrs: map[key_int32:1 key_string:new value], Body: [2]} + // #12 Message{Attrs: map[key_int32:1 key_string:new value], Body: [2]} // #13 Message{Attrs: map[key1:value5 key2:value6 key3:value3 key4:value4], Body: [1]} // #14 Message{Attrs: map[key1:value1 key2:value2], Body: [1]} } From 99140c81d0b5c0a394cfb09a718b65889045bef8 Mon Sep 17 00:00:00 2001 From: Pavel <177363085+pkcll@users.noreply.github.com> Date: Wed, 21 Aug 2024 18:10:24 -0400 Subject: [PATCH 32/49] Beholder: remove code not related to setting globals from 'global' package --- pkg/beholder/example_test.go | 32 +++++++++++++++++++------------- pkg/beholder/global/global.go | 32 -------------------------------- 2 files changed, 19 insertions(+), 45 deletions(-) diff --git a/pkg/beholder/example_test.go b/pkg/beholder/example_test.go index 84a5cae98..e537b659e 100644 --- a/pkg/beholder/example_test.go +++ b/pkg/beholder/example_test.go @@ -10,18 +10,21 @@ import ( "google.golang.org/protobuf/proto" // chainlink-common - beholder "github.com/smartcontractkit/chainlink-common/pkg/beholder/global" + "github.com/smartcontractkit/chainlink-common/pkg/beholder" + "github.com/smartcontractkit/chainlink-common/pkg/beholder/global" "github.com/smartcontractkit/chainlink-common/pkg/beholder/pb" ) func ExampleBeholderCustomMessage() { - beholderConfig := beholder.NewConfig() + config := beholder.DefaultConfig() - // Bootstrap Beholder Client - err := beholder.Bootstrap(beholderConfig, errorHandler) + // Initialize beholder otel client which sets up OTel components + otelClient, err := beholder.NewOtelClient(config, errorHandler) if err != nil { - log.Fatalf("Error bootstrapping Beholder: %v", err) + log.Fatalf("Error creating Beholder client: %v", err) } + // Set global client so it will be accessible from anywhere through beholder/global functions + global.SetClient(&otelClient) // Define a custom protobuf payload to emit payload := &pb.TestCustomMessage{ @@ -37,7 +40,8 @@ func ExampleBeholderCustomMessage() { // Emit the custom message anywhere from application logic for range 10 { - err := beholder.Emit(context.Background(), payloadBytes, + // global.Emitter().Emit() can be used as well if passing otelClient is not an option + err := otelClient.Emitter.Emit(context.Background(), payloadBytes, "beholder_data_schema", "/custom-message/versions/1", // required "beholder_data_type", "custom_message", "foo", "bar", @@ -50,24 +54,26 @@ func ExampleBeholderCustomMessage() { } func ExampleBeholderMetricTraces() { - beholderConfig := beholder.NewConfig() + config := beholder.DefaultConfig() - // Bootstrap Beholder Client - err := beholder.Bootstrap(beholderConfig, errorHandler) + // Initialize beholder otel client which sets up OTel components + otelClient, err := beholder.NewOtelClient(config, errorHandler) if err != nil { - log.Fatalf("Error bootstrapping Beholder: %v", err) + log.Fatalf("Error creating Beholder client: %v", err) } + // Set global client so it will be accessible from anywhere through beholder/global functions + global.SetClient(&otelClient) ctx := context.Background() // Define a new counter - counter, err := beholder.Meter().Int64Counter("custom_message.count") + counter, err := global.Meter().Int64Counter("custom_message.count") if err != nil { log.Fatalf("failed to create new counter") } // Define a new gauge - gauge, err := beholder.Meter().Int64Gauge("custom_message.gauge") + gauge, err := global.Meter().Int64Gauge("custom_message.gauge") if err != nil { log.Fatalf("failed to create new gauge") } @@ -77,7 +83,7 @@ func ExampleBeholderMetricTraces() { gauge.Record(ctx, rand.Int63n(101)) // Create a new trace span - _, rootSpan := beholder.Tracer().Start(ctx, "foo", trace.WithAttributes( + _, rootSpan := global.Tracer().Start(ctx, "foo", trace.WithAttributes( attribute.String("app_name", "beholderdemo"), )) defer rootSpan.End() diff --git a/pkg/beholder/global/global.go b/pkg/beholder/global/global.go index 41952c3d1..e620fbb55 100644 --- a/pkg/beholder/global/global.go +++ b/pkg/beholder/global/global.go @@ -1,7 +1,6 @@ package global import ( - "context" "sync/atomic" "go.opentelemetry.io/otel" @@ -44,14 +43,6 @@ func Emitter() beholder.Emitter { return GetClient().Emitter } -func Close() error { - return GetClient().Close() -} - -func SpanFromContext(ctx context.Context) oteltrace.Span { - return oteltrace.SpanFromContext(ctx) -} - func defaultClient() *atomic.Pointer[beholder.OtelClient] { ptr := &atomic.Pointer[beholder.OtelClient]{} client := beholder.NewNoopClient() @@ -59,29 +50,6 @@ func defaultClient() *atomic.Pointer[beholder.OtelClient] { return ptr } -func Emit(ctx context.Context, body []byte, attrKVs ...any) error { - return Emitter().Emit(ctx, body, attrKVs...) -} - -func Bootstrap(cfg beholder.Config, errorHandler func(error)) error { - // Initialize beholder client - c, err := beholder.NewOtelClient(cfg, errorHandler) - if err != nil { - return err - } - // Set global client so it will be accessible from anywhere through beholder/global functions - SetClient(&c) - return nil -} - -func NewConfig() beholder.Config { - return beholder.DefaultConfig() -} - -func NewMessage(body []byte, attrKVs ...any) beholder.Message { - return beholder.NewMessage(body, attrKVs...) -} - // Sets the global OTel logger, tracer, meter providers from OtelClient // Makes them accessible from anywhere in the code via global otel getters: // - otellog.GetLoggerProvider() From 7154f0d575a82b030b90317a03365f1346525326 Mon Sep 17 00:00:00 2001 From: Pavel <177363085+pkcll@users.noreply.github.com> Date: Wed, 21 Aug 2024 20:45:42 -0400 Subject: [PATCH 33/49] Beholder: add example when beholder client is not initialized and noop client is used as a fallback --- pkg/beholder/example_test.go | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/pkg/beholder/example_test.go b/pkg/beholder/example_test.go index e537b659e..837ce1530 100644 --- a/pkg/beholder/example_test.go +++ b/pkg/beholder/example_test.go @@ -2,6 +2,7 @@ package beholder_test import ( "context" + "fmt" "log" "math/rand" @@ -39,9 +40,10 @@ func ExampleBeholderCustomMessage() { } // Emit the custom message anywhere from application logic + fmt.Println("Emit custom messages") for range 10 { // global.Emitter().Emit() can be used as well if passing otelClient is not an option - err := otelClient.Emitter.Emit(context.Background(), payloadBytes, + err := global.Emitter().Emit(context.Background(), payloadBytes, "beholder_data_schema", "/custom-message/versions/1", // required "beholder_data_type", "custom_message", "foo", "bar", @@ -51,6 +53,7 @@ func ExampleBeholderCustomMessage() { } } // Output: + // Emit custom messages } func ExampleBeholderMetricTraces() { @@ -79,15 +82,34 @@ func ExampleBeholderMetricTraces() { } // Use the counter and gauge for metrics within application logic + fmt.Println("Update metrics") counter.Add(ctx, 1) gauge.Record(ctx, rand.Int63n(101)) - // Create a new trace span + fmt.Println("Create new trace span") _, rootSpan := global.Tracer().Start(ctx, "foo", trace.WithAttributes( attribute.String("app_name", "beholderdemo"), )) defer rootSpan.End() // Output: + // Update metrics + // Create new trace span +} + +func ExampleNoopBeholder() { + fmt.Println("Beholder is not initialized. Fall back to Noop OTel Client") + + fmt.Println("Emitting custom message via noop otel client") + + err := global.Emitter().Emit(context.Background(), []byte("test message"), + "beholder_data_schema", "/custom-message/versions/1", // required + ) + if err != nil { + log.Printf("Error emitting message: %v", err) + } + // Output: + // Beholder is not initialized. Fall back to Noop OTel Client + // Emitting custom message via noop otel client } func errorHandler(e error) { From 87e46aeb79b9fbb0482d366491a075f7621da061 Mon Sep 17 00:00:00 2001 From: Pavel <177363085+pkcll@users.noreply.github.com> Date: Thu, 22 Aug 2024 12:21:06 -0400 Subject: [PATCH 34/49] update go.mod --- go.mod | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/go.mod b/go.mod index 3d916b7e3..5a69c1a5f 100644 --- a/go.mod +++ b/go.mod @@ -29,6 +29,7 @@ require ( github.com/pelletier/go-toml/v2 v2.2.0 github.com/prometheus/client_golang v1.17.0 github.com/riferrei/srclient v0.5.4 + github.com/santhosh-tekuri/jsonschema/v5 v5.2.0 github.com/shopspring/decimal v1.4.0 github.com/smartcontractkit/libocr v0.0.0-20240419185742-fd3cab206b2c github.com/stretchr/testify v1.9.0 @@ -41,6 +42,7 @@ require ( go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.28.0 go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.28.0 go.opentelemetry.io/otel/log v0.4.0 + go.opentelemetry.io/otel/metric v1.28.0 go.opentelemetry.io/otel/sdk v1.28.0 go.opentelemetry.io/otel/sdk/log v0.4.0 go.opentelemetry.io/otel/sdk/metric v1.28.0 @@ -55,12 +57,6 @@ require ( sigs.k8s.io/yaml v1.4.0 ) -require ( - github.com/go-playground/locales v0.13.0 // indirect - github.com/go-playground/universal-translator v0.17.0 // indirect - github.com/leodido/go-urn v1.2.0 // indirect -) - require ( github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/beorn7/perks v1.0.1 // indirect @@ -71,12 +67,15 @@ require ( github.com/fatih/color v1.16.0 // 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.13.0 // indirect + github.com/go-playground/universal-translator v0.17.0 // indirect github.com/goccy/go-yaml v1.12.0 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb // indirect; indirec github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // indirect github.com/hashicorp/yamux v0.1.1 // indirect + github.com/leodido/go-urn v1.2.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect @@ -91,12 +90,10 @@ require ( github.com/prometheus/common v0.44.0 // indirect github.com/prometheus/procfs v0.11.1 // indirect github.com/sanity-io/litter v1.5.5 // indirect - github.com/santhosh-tekuri/jsonschema/v5 v5.2.0 github.com/stretchr/objx v0.5.2 // indirect github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect github.com/x448/float16 v0.8.4 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 // indirect - go.opentelemetry.io/otel/metric v1.28.0 go.opentelemetry.io/proto/otlp v1.3.1 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/net v0.28.0 // indirect From 11c86c564cb226cc3b226c4e488b28aa63fa691a Mon Sep 17 00:00:00 2001 From: Pavel <177363085+pkcll@users.noreply.github.com> Date: Thu, 22 Aug 2024 12:51:58 -0400 Subject: [PATCH 35/49] Beholder: add context to NewOtelClient --- pkg/beholder/client.go | 7 +++---- pkg/beholder/client_test.go | 7 ++++--- pkg/beholder/example_test.go | 10 ++++++---- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/pkg/beholder/client.go b/pkg/beholder/client.go index 8f430c97d..4ecf46d7c 100644 --- a/pkg/beholder/client.go +++ b/pkg/beholder/client.go @@ -55,18 +55,17 @@ type OtelClient struct { } // NewOtelClient creates a new Client with OTel exporter -func NewOtelClient(cfg Config, errorHandler errorHandlerFunc) (OtelClient, error) { +func NewOtelClient(ctx context.Context, cfg Config, errorHandler errorHandlerFunc) (OtelClient, error) { factory := func(ctx context.Context, options ...otlploggrpc.Option) (sdklog.Exporter, error) { return otlploggrpc.New(ctx, options...) } - return newOtelClient(cfg, errorHandler, factory) + return newOtelClient(ctx, cfg, errorHandler, factory) } // Used for testing to override the default exporter type otlploggrpcFactory func(ctx context.Context, options ...otlploggrpc.Option) (sdklog.Exporter, error) -func newOtelClient(cfg Config, errorHandler errorHandlerFunc, otlploggrpcNew otlploggrpcFactory) (OtelClient, error) { - ctx := context.Background() +func newOtelClient(ctx context.Context, cfg Config, errorHandler errorHandlerFunc, otlploggrpcNew otlploggrpcFactory) (OtelClient, error) { baseResource, err := newOtelResource(cfg) noop := NewNoopClient() if err != nil { diff --git a/pkg/beholder/client_test.go b/pkg/beholder/client_test.go index 20a21d28c..6faa28ecc 100644 --- a/pkg/beholder/client_test.go +++ b/pkg/beholder/client_test.go @@ -53,7 +53,7 @@ func TestClient(t *testing.T) { } defaultMessageBody := []byte("body bytes") - tests := []struct { + testCases := []struct { name string makeCustomAttributes func() map[string]any messageBody []byte @@ -76,7 +76,7 @@ func TestClient(t *testing.T) { }, } - for _, tc := range tests { + for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { exporterMock := mocks.NewOTLPExporter(t) defer exporterMock.AssertExpectations(t) @@ -88,7 +88,7 @@ func TestClient(t *testing.T) { exporterFactory := func(context.Context, ...otlploggrpc.Option) (sdklog.Exporter, error) { return exporterMock, nil } - client, err := newOtelClient(TestDefaultConfig(), otelErrorHandler, exporterFactory) + client, err := newOtelClient(tests.Context(t), TestDefaultConfig(), otelErrorHandler, exporterFactory) if err != nil { t.Fatalf("Error creating beholder client: %v", err) } @@ -140,6 +140,7 @@ func TestClient(t *testing.T) { func TestEmitterMessageValidation(t *testing.T) { getEmitter := func(exporterMock *mocks.OTLPExporter) Emitter { client, err := newOtelClient( + tests.Context(t), TestDefaultConfig(), func(err error) { t.Fatalf("otel error: %v", err) }, // Override exporter factory which is used by Client diff --git a/pkg/beholder/example_test.go b/pkg/beholder/example_test.go index 837ce1530..b573976f0 100644 --- a/pkg/beholder/example_test.go +++ b/pkg/beholder/example_test.go @@ -17,10 +17,12 @@ import ( ) func ExampleBeholderCustomMessage() { + ctx := context.Background() + config := beholder.DefaultConfig() // Initialize beholder otel client which sets up OTel components - otelClient, err := beholder.NewOtelClient(config, errorHandler) + otelClient, err := beholder.NewOtelClient(ctx, config, errorHandler) if err != nil { log.Fatalf("Error creating Beholder client: %v", err) } @@ -57,18 +59,18 @@ func ExampleBeholderCustomMessage() { } func ExampleBeholderMetricTraces() { + ctx := context.Background() + config := beholder.DefaultConfig() // Initialize beholder otel client which sets up OTel components - otelClient, err := beholder.NewOtelClient(config, errorHandler) + otelClient, err := beholder.NewOtelClient(ctx, config, errorHandler) if err != nil { log.Fatalf("Error creating Beholder client: %v", err) } // Set global client so it will be accessible from anywhere through beholder/global functions global.SetClient(&otelClient) - ctx := context.Background() - // Define a new counter counter, err := global.Meter().Int64Counter("custom_message.count") if err != nil { From 555b582978b4936916442ff5eb92bb394728474b Mon Sep 17 00:00:00 2001 From: Pavel <177363085+pkcll@users.noreply.github.com> Date: Thu, 22 Aug 2024 16:44:21 -0400 Subject: [PATCH 36/49] Beholder: move global to beholder package --- pkg/beholder/client.go | 6 +++--- pkg/beholder/client_test.go | 2 +- pkg/beholder/example_test.go | 21 ++++++++---------- pkg/beholder/{global => }/global.go | 24 ++++++++------------- pkg/beholder/{global => }/global_test.go | 27 ++++++++++++------------ 5 files changed, 35 insertions(+), 45 deletions(-) rename pkg/beholder/{global => }/global.go (72%) rename pkg/beholder/{global => }/global_test.go (79%) diff --git a/pkg/beholder/client.go b/pkg/beholder/client.go index 4ecf46d7c..408a30cec 100644 --- a/pkg/beholder/client.go +++ b/pkg/beholder/client.go @@ -21,7 +21,7 @@ import ( "google.golang.org/grpc/credentials/insecure" ) -type Emitter interface { +type MessageEmitter interface { // Sends message with bytes and attributes to OTel Collector Emit(ctx context.Context, body []byte, attrKVs ...any) error } @@ -42,7 +42,7 @@ type OtelClient struct { // Meter Meter otelmetric.Meter // Message Emitter - Emitter Emitter + Emitter MessageEmitter // Providers LoggerProvider otellog.LoggerProvider @@ -202,7 +202,7 @@ func (c OtelClient) ForPackage(name string) OtelClient { type errorHandlerFunc func(err error) -// Sets the global error handler for OpenTelemetry +// Sets global error handler for OpenTelemetry func setOtelErrorHandler(h errorHandlerFunc) { otel.SetErrorHandler(otel.ErrorHandlerFunc(h)) } diff --git a/pkg/beholder/client_test.go b/pkg/beholder/client_test.go index 6faa28ecc..2749b7bfb 100644 --- a/pkg/beholder/client_test.go +++ b/pkg/beholder/client_test.go @@ -138,7 +138,7 @@ func TestClient(t *testing.T) { } func TestEmitterMessageValidation(t *testing.T) { - getEmitter := func(exporterMock *mocks.OTLPExporter) Emitter { + getEmitter := func(exporterMock *mocks.OTLPExporter) MessageEmitter { client, err := newOtelClient( tests.Context(t), TestDefaultConfig(), diff --git a/pkg/beholder/example_test.go b/pkg/beholder/example_test.go index b573976f0..739e8583d 100644 --- a/pkg/beholder/example_test.go +++ b/pkg/beholder/example_test.go @@ -10,9 +10,7 @@ import ( "go.opentelemetry.io/otel/trace" "google.golang.org/protobuf/proto" - // chainlink-common "github.com/smartcontractkit/chainlink-common/pkg/beholder" - "github.com/smartcontractkit/chainlink-common/pkg/beholder/global" "github.com/smartcontractkit/chainlink-common/pkg/beholder/pb" ) @@ -26,8 +24,8 @@ func ExampleBeholderCustomMessage() { if err != nil { log.Fatalf("Error creating Beholder client: %v", err) } - // Set global client so it will be accessible from anywhere through beholder/global functions - global.SetClient(&otelClient) + // Set global client so it will be accessible from anywhere through beholder functions + beholder.SetClient(&otelClient) // Define a custom protobuf payload to emit payload := &pb.TestCustomMessage{ @@ -44,8 +42,7 @@ func ExampleBeholderCustomMessage() { // Emit the custom message anywhere from application logic fmt.Println("Emit custom messages") for range 10 { - // global.Emitter().Emit() can be used as well if passing otelClient is not an option - err := global.Emitter().Emit(context.Background(), payloadBytes, + err := beholder.Emitter().Emit(context.Background(), payloadBytes, "beholder_data_schema", "/custom-message/versions/1", // required "beholder_data_type", "custom_message", "foo", "bar", @@ -68,17 +65,17 @@ func ExampleBeholderMetricTraces() { if err != nil { log.Fatalf("Error creating Beholder client: %v", err) } - // Set global client so it will be accessible from anywhere through beholder/global functions - global.SetClient(&otelClient) + // Set global client so it will be accessible from anywhere through beholder functions + beholder.SetClient(&otelClient) // Define a new counter - counter, err := global.Meter().Int64Counter("custom_message.count") + counter, err := beholder.Meter().Int64Counter("custom_message.count") if err != nil { log.Fatalf("failed to create new counter") } // Define a new gauge - gauge, err := global.Meter().Int64Gauge("custom_message.gauge") + gauge, err := beholder.Meter().Int64Gauge("custom_message.gauge") if err != nil { log.Fatalf("failed to create new gauge") } @@ -89,7 +86,7 @@ func ExampleBeholderMetricTraces() { gauge.Record(ctx, rand.Int63n(101)) fmt.Println("Create new trace span") - _, rootSpan := global.Tracer().Start(ctx, "foo", trace.WithAttributes( + _, rootSpan := beholder.Tracer().Start(ctx, "foo", trace.WithAttributes( attribute.String("app_name", "beholderdemo"), )) defer rootSpan.End() @@ -103,7 +100,7 @@ func ExampleNoopBeholder() { fmt.Println("Emitting custom message via noop otel client") - err := global.Emitter().Emit(context.Background(), []byte("test message"), + err := beholder.Emitter().Emit(context.Background(), []byte("test message"), "beholder_data_schema", "/custom-message/versions/1", // required ) if err != nil { diff --git a/pkg/beholder/global/global.go b/pkg/beholder/global.go similarity index 72% rename from pkg/beholder/global/global.go rename to pkg/beholder/global.go index e620fbb55..e942c470d 100644 --- a/pkg/beholder/global/global.go +++ b/pkg/beholder/global.go @@ -1,4 +1,4 @@ -package global +package beholder import ( "sync/atomic" @@ -9,21 +9,19 @@ import ( otelmetric "go.opentelemetry.io/otel/metric" "go.opentelemetry.io/otel/propagation" oteltrace "go.opentelemetry.io/otel/trace" - - "github.com/smartcontractkit/chainlink-common/pkg/beholder" ) // Pointer to the global Beholder Client var globalClient = defaultClient() // SetClient sets the global Beholder Client -func SetClient(client *beholder.OtelClient) { +func SetClient(client *OtelClient) { globalClient.Store(client) } // Returns the global Beholder Client // Its thread-safe and can be used concurrently -func GetClient() *beholder.OtelClient { +func GetClient() *OtelClient { return globalClient.Load() } @@ -39,23 +37,19 @@ func Meter() otelmetric.Meter { return GetClient().Meter } -func Emitter() beholder.Emitter { +func Emitter() MessageEmitter { return GetClient().Emitter } -func defaultClient() *atomic.Pointer[beholder.OtelClient] { - ptr := &atomic.Pointer[beholder.OtelClient]{} - client := beholder.NewNoopClient() +func defaultClient() *atomic.Pointer[OtelClient] { + ptr := &atomic.Pointer[OtelClient]{} + client := NewNoopClient() ptr.Store(&client) return ptr } -// Sets the global OTel logger, tracer, meter providers from OtelClient -// Makes them accessible from anywhere in the code via global otel getters: -// - otellog.GetLoggerProvider() -// - otel.GetTracerProvider() -// - otel.GetTextMapPropagator() -// - otel.GetMeterProvider() +// Sets global OTel logger, tracer, meter providers from OtelClient. +// Makes them accessible from anywhere in the code via global otel getters. // Any package that relies on go.opentelemetry.io will be able to pick up configured global providers // e.g [otelgrpc](https://pkg.go.dev/go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc#example-NewServerHandler) func SetGlobalOtelProviders() { diff --git a/pkg/beholder/global/global_test.go b/pkg/beholder/global_test.go similarity index 79% rename from pkg/beholder/global/global_test.go rename to pkg/beholder/global_test.go index b585918d2..0bd0aba48 100644 --- a/pkg/beholder/global/global_test.go +++ b/pkg/beholder/global_test.go @@ -1,4 +1,4 @@ -package global_test +package beholder_test import ( "strings" @@ -18,15 +18,14 @@ import ( oteltracenoop "go.opentelemetry.io/otel/trace/noop" "github.com/smartcontractkit/chainlink-common/pkg/beholder" - "github.com/smartcontractkit/chainlink-common/pkg/beholder/global" "github.com/smartcontractkit/chainlink-common/pkg/beholder/internal/mocks" "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" ) func TestGlobal(t *testing.T) { // Get global logger, tracer, meter, messageEmitter - // If not initialized with global.SetClient will return noop client - logger, tracer, meter, messageEmitter := global.Logger(), global.Tracer(), global.Meter(), global.Emitter() + // If not initialized with beholder.SetClient will return noop client + logger, tracer, meter, messageEmitter := beholder.Logger(), beholder.Tracer(), beholder.Meter(), beholder.Emitter() noopClient := beholder.NewNoopClient() assert.IsType(t, otellognoop.Logger{}, logger) assert.IsType(t, oteltracenoop.Tracer{}, tracer) @@ -35,15 +34,15 @@ func TestGlobal(t *testing.T) { assert.IsType(t, expectedMessageEmitter, messageEmitter) var noopClientPtr *beholder.OtelClient = &noopClient - assert.IsType(t, noopClientPtr, global.GetClient()) - assert.NotSame(t, noopClientPtr, global.GetClient()) + assert.IsType(t, noopClientPtr, beholder.GetClient()) + assert.NotSame(t, noopClientPtr, beholder.GetClient()) - // Set global client so it will be accessible from anywhere through beholder/global functions - global.SetClient(noopClientPtr) - assert.Same(t, noopClientPtr, global.GetClient()) + // Set beholder client so it will be accessible from anywhere through beholder functions + beholder.SetClient(noopClientPtr) + assert.Same(t, noopClientPtr, beholder.GetClient()) - // After that use global functions to get logger, tracer, meter, messageEmitter - logger, tracer, meter, messageEmitter = global.Logger(), global.Tracer(), global.Meter(), global.Emitter() + // After that use beholder functions to get logger, tracer, meter, messageEmitter + logger, tracer, meter, messageEmitter = beholder.Logger(), beholder.Tracer(), beholder.Meter(), beholder.Emitter() // Emit otel log record logger.Emit(tests.Context(t), otellog.Record{}) @@ -78,10 +77,10 @@ func TestClient_SetGlobalOtelProviders(t *testing.T) { var b strings.Builder client := beholder.NewStdoutClient(beholder.WithWriter(&b)) // Set global Otel Client - global.SetClient(&client) + beholder.SetClient(&client) - // Set global otel tracer, meter, logger providers from global client - global.SetGlobalOtelProviders() + // Set global otel tracer, meter, logger providers from global beholder otel client + beholder.SetGlobalOtelProviders() assert.Equal(t, client.LoggerProvider, otellogglobal.GetLoggerProvider()) assert.Equal(t, client.TracerProvider, otel.GetTracerProvider()) From e749bb1f5386ca4f06a3eb239e53a812c7182cba Mon Sep 17 00:00:00 2001 From: Pavel <177363085+pkcll@users.noreply.github.com> Date: Fri, 23 Aug 2024 09:42:50 -0400 Subject: [PATCH 37/49] Beholder: revert renaming of Emitter interface --- pkg/beholder/client.go | 4 ++-- pkg/beholder/client_test.go | 2 +- pkg/beholder/example_test.go | 4 ++-- pkg/beholder/global.go | 2 +- pkg/beholder/global_test.go | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pkg/beholder/client.go b/pkg/beholder/client.go index 408a30cec..64725bd28 100644 --- a/pkg/beholder/client.go +++ b/pkg/beholder/client.go @@ -21,7 +21,7 @@ import ( "google.golang.org/grpc/credentials/insecure" ) -type MessageEmitter interface { +type Emitter interface { // Sends message with bytes and attributes to OTel Collector Emit(ctx context.Context, body []byte, attrKVs ...any) error } @@ -42,7 +42,7 @@ type OtelClient struct { // Meter Meter otelmetric.Meter // Message Emitter - Emitter MessageEmitter + Emitter Emitter // Providers LoggerProvider otellog.LoggerProvider diff --git a/pkg/beholder/client_test.go b/pkg/beholder/client_test.go index 2749b7bfb..6faa28ecc 100644 --- a/pkg/beholder/client_test.go +++ b/pkg/beholder/client_test.go @@ -138,7 +138,7 @@ func TestClient(t *testing.T) { } func TestEmitterMessageValidation(t *testing.T) { - getEmitter := func(exporterMock *mocks.OTLPExporter) MessageEmitter { + getEmitter := func(exporterMock *mocks.OTLPExporter) Emitter { client, err := newOtelClient( tests.Context(t), TestDefaultConfig(), diff --git a/pkg/beholder/example_test.go b/pkg/beholder/example_test.go index 739e8583d..870a185f5 100644 --- a/pkg/beholder/example_test.go +++ b/pkg/beholder/example_test.go @@ -42,7 +42,7 @@ func ExampleBeholderCustomMessage() { // Emit the custom message anywhere from application logic fmt.Println("Emit custom messages") for range 10 { - err := beholder.Emitter().Emit(context.Background(), payloadBytes, + err := beholder.MessageEmitter().Emit(context.Background(), payloadBytes, "beholder_data_schema", "/custom-message/versions/1", // required "beholder_data_type", "custom_message", "foo", "bar", @@ -100,7 +100,7 @@ func ExampleNoopBeholder() { fmt.Println("Emitting custom message via noop otel client") - err := beholder.Emitter().Emit(context.Background(), []byte("test message"), + err := beholder.MessageEmitter().Emit(context.Background(), []byte("test message"), "beholder_data_schema", "/custom-message/versions/1", // required ) if err != nil { diff --git a/pkg/beholder/global.go b/pkg/beholder/global.go index e942c470d..e25fbeaab 100644 --- a/pkg/beholder/global.go +++ b/pkg/beholder/global.go @@ -37,7 +37,7 @@ func Meter() otelmetric.Meter { return GetClient().Meter } -func Emitter() MessageEmitter { +func MessageEmitter() Emitter { return GetClient().Emitter } diff --git a/pkg/beholder/global_test.go b/pkg/beholder/global_test.go index 0bd0aba48..6cca9acae 100644 --- a/pkg/beholder/global_test.go +++ b/pkg/beholder/global_test.go @@ -25,7 +25,7 @@ import ( func TestGlobal(t *testing.T) { // Get global logger, tracer, meter, messageEmitter // If not initialized with beholder.SetClient will return noop client - logger, tracer, meter, messageEmitter := beholder.Logger(), beholder.Tracer(), beholder.Meter(), beholder.Emitter() + logger, tracer, meter, messageEmitter := beholder.Logger(), beholder.Tracer(), beholder.Meter(), beholder.MessageEmitter() noopClient := beholder.NewNoopClient() assert.IsType(t, otellognoop.Logger{}, logger) assert.IsType(t, oteltracenoop.Tracer{}, tracer) @@ -42,7 +42,7 @@ func TestGlobal(t *testing.T) { assert.Same(t, noopClientPtr, beholder.GetClient()) // After that use beholder functions to get logger, tracer, meter, messageEmitter - logger, tracer, meter, messageEmitter = beholder.Logger(), beholder.Tracer(), beholder.Meter(), beholder.Emitter() + logger, tracer, meter, messageEmitter = beholder.Logger(), beholder.Tracer(), beholder.Meter(), beholder.MessageEmitter() // Emit otel log record logger.Emit(tests.Context(t), otellog.Record{}) From 1582cc2709b6da497f708e62971162128c9942d4 Mon Sep 17 00:00:00 2001 From: Pavel <177363085+pkcll@users.noreply.github.com> Date: Fri, 23 Aug 2024 09:50:03 -0400 Subject: [PATCH 38/49] Beholder: remove Client interface, rename OtelClient struct --- pkg/beholder/client.go | 15 ++++++--------- pkg/beholder/client_test.go | 4 ++-- pkg/beholder/global.go | 8 ++++---- pkg/beholder/global_test.go | 2 +- pkg/beholder/noop.go | 8 ++++---- 5 files changed, 17 insertions(+), 20 deletions(-) diff --git a/pkg/beholder/client.go b/pkg/beholder/client.go index 64725bd28..56bf78e91 100644 --- a/pkg/beholder/client.go +++ b/pkg/beholder/client.go @@ -25,15 +25,12 @@ type Emitter interface { // Sends message with bytes and attributes to OTel Collector Emit(ctx context.Context, body []byte, attrKVs ...any) error } -type Client interface { - Close() error -} type messageEmitter struct { messageLogger otellog.Logger } -type OtelClient struct { +type Client struct { Config Config // Logger Logger otellog.Logger @@ -55,7 +52,7 @@ type OtelClient struct { } // NewOtelClient creates a new Client with OTel exporter -func NewOtelClient(ctx context.Context, cfg Config, errorHandler errorHandlerFunc) (OtelClient, error) { +func NewOtelClient(ctx context.Context, cfg Config, errorHandler errorHandlerFunc) (Client, error) { factory := func(ctx context.Context, options ...otlploggrpc.Option) (sdklog.Exporter, error) { return otlploggrpc.New(ctx, options...) } @@ -65,7 +62,7 @@ func NewOtelClient(ctx context.Context, cfg Config, errorHandler errorHandlerFun // Used for testing to override the default exporter type otlploggrpcFactory func(ctx context.Context, options ...otlploggrpc.Option) (sdklog.Exporter, error) -func newOtelClient(ctx context.Context, cfg Config, errorHandler errorHandlerFunc, otlploggrpcNew otlploggrpcFactory) (OtelClient, error) { +func newOtelClient(ctx context.Context, cfg Config, errorHandler errorHandlerFunc, otlploggrpcNew otlploggrpcFactory) (Client, error) { baseResource, err := newOtelResource(cfg) noop := NewNoopClient() if err != nil { @@ -167,13 +164,13 @@ func newOtelClient(ctx context.Context, cfg Config, errorHandler errorHandlerFun } return } - client := OtelClient{cfg, logger, tracer, meter, emitter, loggerProvider, tracerProvider, meterProvider, messageLoggerProvider, onClose} + client := Client{cfg, logger, tracer, meter, emitter, loggerProvider, tracerProvider, meterProvider, messageLoggerProvider, onClose} return client, nil } // Closes all providers, flushes all data and stops all background processes -func (c OtelClient) Close() (err error) { +func (c Client) Close() (err error) { if c.OnClose != nil { return c.OnClose() } @@ -181,7 +178,7 @@ func (c OtelClient) Close() (err error) { } // Returns a new OtelClient with the same configuration but with a different package name -func (c OtelClient) ForPackage(name string) OtelClient { +func (c Client) ForPackage(name string) Client { // Logger logger := c.LoggerProvider.Logger(name) // Tracer diff --git a/pkg/beholder/client_test.go b/pkg/beholder/client_test.go index 6faa28ecc..fc07207ca 100644 --- a/pkg/beholder/client_test.go +++ b/pkg/beholder/client_test.go @@ -60,7 +60,7 @@ func TestClient(t *testing.T) { messageCount int exporterMockErrorCount int exporterOutputExpected bool - messageGenerator func(client OtelClient, messageBody []byte, customAttributes map[string]any) + messageGenerator func(client Client, messageBody []byte, customAttributes map[string]any) }{ { name: "Test Emit", @@ -69,7 +69,7 @@ func TestClient(t *testing.T) { messageCount: 10, exporterMockErrorCount: 0, exporterOutputExpected: true, - messageGenerator: func(client OtelClient, messageBody []byte, customAttributes map[string]any) { + messageGenerator: func(client Client, messageBody []byte, customAttributes map[string]any) { err := client.Emitter.Emit(tests.Context(t), messageBody, customAttributes) assert.NoError(t, err) }, diff --git a/pkg/beholder/global.go b/pkg/beholder/global.go index e25fbeaab..d1dba7468 100644 --- a/pkg/beholder/global.go +++ b/pkg/beholder/global.go @@ -15,13 +15,13 @@ import ( var globalClient = defaultClient() // SetClient sets the global Beholder Client -func SetClient(client *OtelClient) { +func SetClient(client *Client) { globalClient.Store(client) } // Returns the global Beholder Client // Its thread-safe and can be used concurrently -func GetClient() *OtelClient { +func GetClient() *Client { return globalClient.Load() } @@ -41,8 +41,8 @@ func MessageEmitter() Emitter { return GetClient().Emitter } -func defaultClient() *atomic.Pointer[OtelClient] { - ptr := &atomic.Pointer[OtelClient]{} +func defaultClient() *atomic.Pointer[Client] { + ptr := &atomic.Pointer[Client]{} client := NewNoopClient() ptr.Store(&client) return ptr diff --git a/pkg/beholder/global_test.go b/pkg/beholder/global_test.go index 6cca9acae..fd83361df 100644 --- a/pkg/beholder/global_test.go +++ b/pkg/beholder/global_test.go @@ -33,7 +33,7 @@ func TestGlobal(t *testing.T) { expectedMessageEmitter := beholder.NewNoopClient().Emitter assert.IsType(t, expectedMessageEmitter, messageEmitter) - var noopClientPtr *beholder.OtelClient = &noopClient + var noopClientPtr *beholder.Client = &noopClient assert.IsType(t, noopClientPtr, beholder.GetClient()) assert.NotSame(t, noopClientPtr, beholder.GetClient()) diff --git a/pkg/beholder/noop.go b/pkg/beholder/noop.go index 405198f05..493434100 100644 --- a/pkg/beholder/noop.go +++ b/pkg/beholder/noop.go @@ -19,7 +19,7 @@ import ( ) // Default client to fallback when is is not initialized properly -func NewNoopClient() OtelClient { +func NewNoopClient() Client { cfg := DefaultConfig() // Logger loggerProvider := otellognoop.NewLoggerProvider() @@ -35,7 +35,7 @@ func NewNoopClient() OtelClient { // MessageEmitter messageEmitter := noopMessageEmitter{} - client := OtelClient{cfg, logger, tracer, meter, messageEmitter, loggerProvider, tracerProvider, meterProvider, loggerProvider, noopOnClose} + client := Client{cfg, logger, tracer, meter, messageEmitter, loggerProvider, tracerProvider, meterProvider, loggerProvider, noopOnClose} return client } @@ -43,7 +43,7 @@ func NewNoopClient() OtelClient { // NewStdoutClient creates a new Client with stdout exporters // Use for testing and debugging // Also this client is used as a noop client when otel exporter is not initialized properly -func NewStdoutClient(opts ...StddutClientOption) OtelClient { +func NewStdoutClient(opts ...StddutClientOption) Client { cfg := DefaultStdoutClientConfig() for _, opt := range opts { opt(&cfg) @@ -87,7 +87,7 @@ func NewStdoutClient(opts ...StddutClientOption) OtelClient { return } - client := OtelClient{cfg.Config, logger, tracer, meter, emitter, loggerProvider, tracerProvider, meterProvider, loggerProvider, onClose} + client := Client{cfg.Config, logger, tracer, meter, emitter, loggerProvider, tracerProvider, meterProvider, loggerProvider, onClose} return client } From ebf2a1ef1e73ebb7dc9ff64063541f31b8d46a42 Mon Sep 17 00:00:00 2001 From: Pavel <177363085+pkcll@users.noreply.github.com> Date: Fri, 23 Aug 2024 09:58:33 -0400 Subject: [PATCH 39/49] Beholder: rename Otel Client references --- pkg/beholder/client.go | 10 +++++----- pkg/beholder/client_test.go | 4 ++-- pkg/beholder/example_test.go | 8 ++++---- pkg/beholder/global.go | 2 +- pkg/beholder/noop_test.go | 18 +++++++++--------- 5 files changed, 21 insertions(+), 21 deletions(-) diff --git a/pkg/beholder/client.go b/pkg/beholder/client.go index 56bf78e91..6b5b8bec3 100644 --- a/pkg/beholder/client.go +++ b/pkg/beholder/client.go @@ -51,18 +51,18 @@ type Client struct { OnClose func() error } -// NewOtelClient creates a new Client with OTel exporter -func NewOtelClient(ctx context.Context, cfg Config, errorHandler errorHandlerFunc) (Client, error) { +// NewClient creates a new Client with OTel exporter +func NewClient(ctx context.Context, cfg Config, errorHandler errorHandlerFunc) (Client, error) { factory := func(ctx context.Context, options ...otlploggrpc.Option) (sdklog.Exporter, error) { return otlploggrpc.New(ctx, options...) } - return newOtelClient(ctx, cfg, errorHandler, factory) + return newClient(ctx, cfg, errorHandler, factory) } // Used for testing to override the default exporter type otlploggrpcFactory func(ctx context.Context, options ...otlploggrpc.Option) (sdklog.Exporter, error) -func newOtelClient(ctx context.Context, cfg Config, errorHandler errorHandlerFunc, otlploggrpcNew otlploggrpcFactory) (Client, error) { +func newClient(ctx context.Context, cfg Config, errorHandler errorHandlerFunc, otlploggrpcNew otlploggrpcFactory) (Client, error) { baseResource, err := newOtelResource(cfg) noop := NewNoopClient() if err != nil { @@ -177,7 +177,7 @@ func (c Client) Close() (err error) { return } -// Returns a new OtelClient with the same configuration but with a different package name +// Returns a new Client with the same configuration but with a different package name func (c Client) ForPackage(name string) Client { // Logger logger := c.LoggerProvider.Logger(name) diff --git a/pkg/beholder/client_test.go b/pkg/beholder/client_test.go index fc07207ca..ba179eb59 100644 --- a/pkg/beholder/client_test.go +++ b/pkg/beholder/client_test.go @@ -88,7 +88,7 @@ func TestClient(t *testing.T) { exporterFactory := func(context.Context, ...otlploggrpc.Option) (sdklog.Exporter, error) { return exporterMock, nil } - client, err := newOtelClient(tests.Context(t), TestDefaultConfig(), otelErrorHandler, exporterFactory) + client, err := newClient(tests.Context(t), TestDefaultConfig(), otelErrorHandler, exporterFactory) if err != nil { t.Fatalf("Error creating beholder client: %v", err) } @@ -139,7 +139,7 @@ func TestClient(t *testing.T) { func TestEmitterMessageValidation(t *testing.T) { getEmitter := func(exporterMock *mocks.OTLPExporter) Emitter { - client, err := newOtelClient( + client, err := newClient( tests.Context(t), TestDefaultConfig(), func(err error) { t.Fatalf("otel error: %v", err) }, diff --git a/pkg/beholder/example_test.go b/pkg/beholder/example_test.go index 870a185f5..d8076d267 100644 --- a/pkg/beholder/example_test.go +++ b/pkg/beholder/example_test.go @@ -20,12 +20,12 @@ func ExampleBeholderCustomMessage() { config := beholder.DefaultConfig() // Initialize beholder otel client which sets up OTel components - otelClient, err := beholder.NewOtelClient(ctx, config, errorHandler) + client, err := beholder.NewClient(ctx, config, errorHandler) if err != nil { log.Fatalf("Error creating Beholder client: %v", err) } // Set global client so it will be accessible from anywhere through beholder functions - beholder.SetClient(&otelClient) + beholder.SetClient(&client) // Define a custom protobuf payload to emit payload := &pb.TestCustomMessage{ @@ -61,12 +61,12 @@ func ExampleBeholderMetricTraces() { config := beholder.DefaultConfig() // Initialize beholder otel client which sets up OTel components - otelClient, err := beholder.NewOtelClient(ctx, config, errorHandler) + client, err := beholder.NewClient(ctx, config, errorHandler) if err != nil { log.Fatalf("Error creating Beholder client: %v", err) } // Set global client so it will be accessible from anywhere through beholder functions - beholder.SetClient(&otelClient) + beholder.SetClient(&client) // Define a new counter counter, err := beholder.Meter().Int64Counter("custom_message.count") diff --git a/pkg/beholder/global.go b/pkg/beholder/global.go index d1dba7468..7ba504a96 100644 --- a/pkg/beholder/global.go +++ b/pkg/beholder/global.go @@ -48,7 +48,7 @@ func defaultClient() *atomic.Pointer[Client] { return ptr } -// Sets global OTel logger, tracer, meter providers from OtelClient. +// Sets global OTel logger, tracer, meter providers from Client. // Makes them accessible from anywhere in the code via global otel getters. // Any package that relies on go.opentelemetry.io will be able to pick up configured global providers // e.g [otelgrpc](https://pkg.go.dev/go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc#example-NewServerHandler) diff --git a/pkg/beholder/noop_test.go b/pkg/beholder/noop_test.go index 78e13c43c..ee1fb7209 100644 --- a/pkg/beholder/noop_test.go +++ b/pkg/beholder/noop_test.go @@ -14,27 +14,27 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" ) -func TestNoopOtelClient(t *testing.T) { - noopOtelClient := NewNoopClient() - assert.NotNil(t, noopOtelClient) +func TestNoopClient(t *testing.T) { + noopClient := NewNoopClient() + assert.NotNil(t, noopClient) // Message Emitter - err := noopOtelClient.Emitter.Emit(tests.Context(t), []byte("test"), + err := noopClient.Emitter.Emit(tests.Context(t), []byte("test"), "key1", "value1", ) assert.NoError(t, err) // Logger - noopOtelClient.Logger.Emit(tests.Context(t), otellog.Record{}) + noopClient.Logger.Emit(tests.Context(t), otellog.Record{}) // Define a new counter - counter, err := noopOtelClient.Meter.Int64Counter("custom_message.count") + counter, err := noopClient.Meter.Int64Counter("custom_message.count") if err != nil { log.Fatalf("failed to create new counter") } // Define a new gauge - gauge, err := noopOtelClient.Meter.Int64Gauge("custom_message.gauge") + gauge, err := noopClient.Meter.Int64Gauge("custom_message.gauge") if err != nil { log.Fatalf("failed to create new gauge") } @@ -45,11 +45,11 @@ func TestNoopOtelClient(t *testing.T) { gauge.Record(tests.Context(t), rand.Int63n(101)) // Create a new trace span - _, rootSpan := noopOtelClient.Tracer.Start(context.Background(), "foo", trace.WithAttributes( + _, rootSpan := noopClient.Tracer.Start(context.Background(), "foo", trace.WithAttributes( attribute.String("app_name", "beholderdemo"), )) rootSpan.End() - err = noopOtelClient.Close() + err = noopClient.Close() assert.NoError(t, err) } From 4ebb5fc4fcd38e805ac6290d3d04ff7c44e9d729 Mon Sep 17 00:00:00 2001 From: Pavel <177363085+pkcll@users.noreply.github.com> Date: Fri, 23 Aug 2024 10:09:11 -0400 Subject: [PATCH 40/49] Beholder: handle errors in NewStdoutClient --- pkg/beholder/client.go | 2 +- pkg/beholder/client_test.go | 8 +++++--- pkg/beholder/global_test.go | 3 ++- pkg/beholder/noop.go | 21 +++++++++++++++------ 4 files changed, 23 insertions(+), 11 deletions(-) diff --git a/pkg/beholder/client.go b/pkg/beholder/client.go index 6b5b8bec3..e7e21e7ee 100644 --- a/pkg/beholder/client.go +++ b/pkg/beholder/client.go @@ -51,7 +51,7 @@ type Client struct { OnClose func() error } -// NewClient creates a new Client with OTel exporter +// NewClient creates a new Client with initialized OpenTelemetry components func NewClient(ctx context.Context, cfg Config, errorHandler errorHandlerFunc) (Client, error) { factory := func(ctx context.Context, options ...otlploggrpc.Option) (sdklog.Exporter, error) { return otlploggrpc.New(ctx, options...) diff --git a/pkg/beholder/client_test.go b/pkg/beholder/client_test.go index ba179eb59..d1fbfcb3c 100644 --- a/pkg/beholder/client_test.go +++ b/pkg/beholder/client_test.go @@ -214,9 +214,10 @@ func TestClient_Close(t *testing.T) { exporterMock := mocks.NewOTLPExporter(t) defer exporterMock.AssertExpectations(t) - client := NewStdoutClient() + client, err := NewStdoutClient() + assert.NoError(t, err) - err := client.Close() + err = client.Close() assert.NoError(t, err) exporterMock.AssertExpectations(t) @@ -226,7 +227,8 @@ func TestClient_ForPackage(t *testing.T) { exporterMock := mocks.NewOTLPExporter(t) defer exporterMock.AssertExpectations(t) var b strings.Builder - client := NewStdoutClient(WithWriter(&b)) + client, err := NewStdoutClient(WithWriter(&b)) + assert.NoError(t, err) clientForTest := client.ForPackage("TestClient_ForPackage") // Log diff --git a/pkg/beholder/global_test.go b/pkg/beholder/global_test.go index fd83361df..50e9e9689 100644 --- a/pkg/beholder/global_test.go +++ b/pkg/beholder/global_test.go @@ -75,7 +75,8 @@ func TestClient_SetGlobalOtelProviders(t *testing.T) { }) var b strings.Builder - client := beholder.NewStdoutClient(beholder.WithWriter(&b)) + client, err := beholder.NewStdoutClient(beholder.WithWriter(&b)) + assert.NoError(t, err) // Set global Otel Client beholder.SetClient(&client) diff --git a/pkg/beholder/noop.go b/pkg/beholder/noop.go index 493434100..91801ea13 100644 --- a/pkg/beholder/noop.go +++ b/pkg/beholder/noop.go @@ -43,15 +43,18 @@ func NewNoopClient() Client { // NewStdoutClient creates a new Client with stdout exporters // Use for testing and debugging // Also this client is used as a noop client when otel exporter is not initialized properly -func NewStdoutClient(opts ...StddutClientOption) Client { +func NewStdoutClient(opts ...StddutClientOption) (Client, error) { cfg := DefaultStdoutClientConfig() for _, opt := range opts { opt(&cfg) } // Logger - loggerExporter, _ := stdoutlog.New( + loggerExporter, err := stdoutlog.New( append([]stdoutlog.Option{stdoutlog.WithoutTimestamps()}, cfg.LogOptions...)..., - ) // stdoutlog.New() never returns an error + ) + if err != nil { + return NewNoopClient(), err + } loggerProvider := sdklog.NewLoggerProvider(sdklog.WithProcessor(sdklog.NewSimpleProcessor(loggerExporter))) logger := loggerProvider.Logger(defaultPackageName) setOtelErrorHandler(func(err error) { @@ -59,7 +62,10 @@ func NewStdoutClient(opts ...StddutClientOption) Client { }) // Tracer - traceExporter, _ := stdouttrace.New(cfg.TraceOptions...) // stdouttrace.New() never returns an error + traceExporter, err := stdouttrace.New(cfg.TraceOptions...) + if err != nil { + return NewNoopClient(), err + } tracerProvider := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor( sdktrace.NewSimpleSpanProcessor(traceExporter), @@ -67,7 +73,10 @@ func NewStdoutClient(opts ...StddutClientOption) Client { tracer := tracerProvider.Tracer(defaultPackageName) // Meter - metricExporter, _ := stdoutmetric.New(cfg.MetricOptions...) // stdoutmetric.New() never returns an error + metricExporter, err := stdoutmetric.New(cfg.MetricOptions...) + if err != nil { + return NewNoopClient(), err + } meterProvider := sdkmetric.NewMeterProvider( sdkmetric.WithReader( sdkmetric.NewPeriodicReader( @@ -89,7 +98,7 @@ func NewStdoutClient(opts ...StddutClientOption) Client { client := Client{cfg.Config, logger, tracer, meter, emitter, loggerProvider, tracerProvider, meterProvider, loggerProvider, onClose} - return client + return client, nil } type noopMessageEmitter struct{} From 7785057214f8d0dba719efeed9985fd8f7de5b5f Mon Sep 17 00:00:00 2001 From: Pavel <177363085+pkcll@users.noreply.github.com> Date: Fri, 23 Aug 2024 10:13:25 -0400 Subject: [PATCH 41/49] Beholder: return pointer from Client constructors --- pkg/beholder/client.go | 6 +++--- pkg/beholder/client_test.go | 4 ++-- pkg/beholder/example_test.go | 4 ++-- pkg/beholder/global.go | 2 +- pkg/beholder/global_test.go | 4 ++-- pkg/beholder/noop.go | 8 ++++---- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/pkg/beholder/client.go b/pkg/beholder/client.go index e7e21e7ee..2a0e6576c 100644 --- a/pkg/beholder/client.go +++ b/pkg/beholder/client.go @@ -52,7 +52,7 @@ type Client struct { } // NewClient creates a new Client with initialized OpenTelemetry components -func NewClient(ctx context.Context, cfg Config, errorHandler errorHandlerFunc) (Client, error) { +func NewClient(ctx context.Context, cfg Config, errorHandler errorHandlerFunc) (*Client, error) { factory := func(ctx context.Context, options ...otlploggrpc.Option) (sdklog.Exporter, error) { return otlploggrpc.New(ctx, options...) } @@ -62,7 +62,7 @@ func NewClient(ctx context.Context, cfg Config, errorHandler errorHandlerFunc) ( // Used for testing to override the default exporter type otlploggrpcFactory func(ctx context.Context, options ...otlploggrpc.Option) (sdklog.Exporter, error) -func newClient(ctx context.Context, cfg Config, errorHandler errorHandlerFunc, otlploggrpcNew otlploggrpcFactory) (Client, error) { +func newClient(ctx context.Context, cfg Config, errorHandler errorHandlerFunc, otlploggrpcNew otlploggrpcFactory) (*Client, error) { baseResource, err := newOtelResource(cfg) noop := NewNoopClient() if err != nil { @@ -166,7 +166,7 @@ func newClient(ctx context.Context, cfg Config, errorHandler errorHandlerFunc, o } client := Client{cfg, logger, tracer, meter, emitter, loggerProvider, tracerProvider, meterProvider, messageLoggerProvider, onClose} - return client, nil + return &client, nil } // Closes all providers, flushes all data and stops all background processes diff --git a/pkg/beholder/client_test.go b/pkg/beholder/client_test.go index d1fbfcb3c..a861b75d6 100644 --- a/pkg/beholder/client_test.go +++ b/pkg/beholder/client_test.go @@ -60,7 +60,7 @@ func TestClient(t *testing.T) { messageCount int exporterMockErrorCount int exporterOutputExpected bool - messageGenerator func(client Client, messageBody []byte, customAttributes map[string]any) + messageGenerator func(client *Client, messageBody []byte, customAttributes map[string]any) }{ { name: "Test Emit", @@ -69,7 +69,7 @@ func TestClient(t *testing.T) { messageCount: 10, exporterMockErrorCount: 0, exporterOutputExpected: true, - messageGenerator: func(client Client, messageBody []byte, customAttributes map[string]any) { + messageGenerator: func(client *Client, messageBody []byte, customAttributes map[string]any) { err := client.Emitter.Emit(tests.Context(t), messageBody, customAttributes) assert.NoError(t, err) }, diff --git a/pkg/beholder/example_test.go b/pkg/beholder/example_test.go index d8076d267..29de3fad1 100644 --- a/pkg/beholder/example_test.go +++ b/pkg/beholder/example_test.go @@ -25,7 +25,7 @@ func ExampleBeholderCustomMessage() { log.Fatalf("Error creating Beholder client: %v", err) } // Set global client so it will be accessible from anywhere through beholder functions - beholder.SetClient(&client) + beholder.SetClient(client) // Define a custom protobuf payload to emit payload := &pb.TestCustomMessage{ @@ -66,7 +66,7 @@ func ExampleBeholderMetricTraces() { log.Fatalf("Error creating Beholder client: %v", err) } // Set global client so it will be accessible from anywhere through beholder functions - beholder.SetClient(&client) + beholder.SetClient(client) // Define a new counter counter, err := beholder.Meter().Int64Counter("custom_message.count") diff --git a/pkg/beholder/global.go b/pkg/beholder/global.go index 7ba504a96..e3706f786 100644 --- a/pkg/beholder/global.go +++ b/pkg/beholder/global.go @@ -44,7 +44,7 @@ func MessageEmitter() Emitter { func defaultClient() *atomic.Pointer[Client] { ptr := &atomic.Pointer[Client]{} client := NewNoopClient() - ptr.Store(&client) + ptr.Store(client) return ptr } diff --git a/pkg/beholder/global_test.go b/pkg/beholder/global_test.go index 50e9e9689..af6285372 100644 --- a/pkg/beholder/global_test.go +++ b/pkg/beholder/global_test.go @@ -33,7 +33,7 @@ func TestGlobal(t *testing.T) { expectedMessageEmitter := beholder.NewNoopClient().Emitter assert.IsType(t, expectedMessageEmitter, messageEmitter) - var noopClientPtr *beholder.Client = &noopClient + var noopClientPtr *beholder.Client = noopClient assert.IsType(t, noopClientPtr, beholder.GetClient()) assert.NotSame(t, noopClientPtr, beholder.GetClient()) @@ -78,7 +78,7 @@ func TestClient_SetGlobalOtelProviders(t *testing.T) { client, err := beholder.NewStdoutClient(beholder.WithWriter(&b)) assert.NoError(t, err) // Set global Otel Client - beholder.SetClient(&client) + beholder.SetClient(client) // Set global otel tracer, meter, logger providers from global beholder otel client beholder.SetGlobalOtelProviders() diff --git a/pkg/beholder/noop.go b/pkg/beholder/noop.go index 91801ea13..488c50d43 100644 --- a/pkg/beholder/noop.go +++ b/pkg/beholder/noop.go @@ -19,7 +19,7 @@ import ( ) // Default client to fallback when is is not initialized properly -func NewNoopClient() Client { +func NewNoopClient() *Client { cfg := DefaultConfig() // Logger loggerProvider := otellognoop.NewLoggerProvider() @@ -37,13 +37,13 @@ func NewNoopClient() Client { client := Client{cfg, logger, tracer, meter, messageEmitter, loggerProvider, tracerProvider, meterProvider, loggerProvider, noopOnClose} - return client + return &client } // NewStdoutClient creates a new Client with stdout exporters // Use for testing and debugging // Also this client is used as a noop client when otel exporter is not initialized properly -func NewStdoutClient(opts ...StddutClientOption) (Client, error) { +func NewStdoutClient(opts ...StddutClientOption) (*Client, error) { cfg := DefaultStdoutClientConfig() for _, opt := range opts { opt(&cfg) @@ -98,7 +98,7 @@ func NewStdoutClient(opts ...StddutClientOption) (Client, error) { client := Client{cfg.Config, logger, tracer, meter, emitter, loggerProvider, tracerProvider, meterProvider, loggerProvider, onClose} - return client, nil + return &client, nil } type noopMessageEmitter struct{} From 26c09d7b0c62e7fdf69559393440bb076ac84864 Mon Sep 17 00:00:00 2001 From: Pavel <177363085+pkcll@users.noreply.github.com> Date: Fri, 23 Aug 2024 13:59:16 -0400 Subject: [PATCH 42/49] Beholder: remove errorHandler parameter from NewClient --- pkg/beholder/client.go | 13 +++++-------- pkg/beholder/client_test.go | 5 +++-- pkg/beholder/example_test.go | 11 ++++++++--- pkg/beholder/noop.go | 2 +- 4 files changed, 17 insertions(+), 14 deletions(-) diff --git a/pkg/beholder/client.go b/pkg/beholder/client.go index 2a0e6576c..edfd23daa 100644 --- a/pkg/beholder/client.go +++ b/pkg/beholder/client.go @@ -52,17 +52,18 @@ type Client struct { } // NewClient creates a new Client with initialized OpenTelemetry components -func NewClient(ctx context.Context, cfg Config, errorHandler errorHandlerFunc) (*Client, error) { +// To handle OpenTelemetry errors use SetOtelErrorHandler +func NewClient(ctx context.Context, cfg Config) (*Client, error) { factory := func(ctx context.Context, options ...otlploggrpc.Option) (sdklog.Exporter, error) { return otlploggrpc.New(ctx, options...) } - return newClient(ctx, cfg, errorHandler, factory) + return newClient(ctx, cfg, factory) } // Used for testing to override the default exporter type otlploggrpcFactory func(ctx context.Context, options ...otlploggrpc.Option) (sdklog.Exporter, error) -func newClient(ctx context.Context, cfg Config, errorHandler errorHandlerFunc, otlploggrpcNew otlploggrpcFactory) (*Client, error) { +func newClient(ctx context.Context, cfg Config, otlploggrpcNew otlploggrpcFactory) (*Client, error) { baseResource, err := newOtelResource(cfg) noop := NewNoopClient() if err != nil { @@ -156,8 +157,6 @@ func newClient(ctx context.Context, cfg Config, errorHandler errorHandlerFunc, o messageLogger: messageLogger, } - setOtelErrorHandler(errorHandler) - onClose := func() (err error) { for _, provider := range []shutdowner{messageLoggerProvider, loggerProvider, tracerProvider, meterProvider, messageLoggerProvider} { err = errors.Join(err, provider.Shutdown(context.Background())) @@ -197,10 +196,8 @@ func (c Client) ForPackage(name string) Client { return newClient } -type errorHandlerFunc func(err error) - // Sets global error handler for OpenTelemetry -func setOtelErrorHandler(h errorHandlerFunc) { +func SetOtelErrorHandler(h func(err error)) { otel.SetErrorHandler(otel.ErrorHandlerFunc(h)) } diff --git a/pkg/beholder/client_test.go b/pkg/beholder/client_test.go index a861b75d6..b50fd99ce 100644 --- a/pkg/beholder/client_test.go +++ b/pkg/beholder/client_test.go @@ -88,10 +88,11 @@ func TestClient(t *testing.T) { exporterFactory := func(context.Context, ...otlploggrpc.Option) (sdklog.Exporter, error) { return exporterMock, nil } - client, err := newClient(tests.Context(t), TestDefaultConfig(), otelErrorHandler, exporterFactory) + client, err := newClient(tests.Context(t), TestDefaultConfig(), exporterFactory) if err != nil { t.Fatalf("Error creating beholder client: %v", err) } + SetOtelErrorHandler(otelErrorHandler) // Number of exported messages exportedMessageCount := 0 @@ -142,12 +143,12 @@ func TestEmitterMessageValidation(t *testing.T) { client, err := newClient( tests.Context(t), TestDefaultConfig(), - func(err error) { t.Fatalf("otel error: %v", err) }, // Override exporter factory which is used by Client func(context.Context, ...otlploggrpc.Option) (sdklog.Exporter, error) { return exporterMock, nil }, ) + SetOtelErrorHandler(func(err error) { t.Fatalf("otel error: %v", err) }) assert.NoError(t, err) return client.Emitter } diff --git a/pkg/beholder/example_test.go b/pkg/beholder/example_test.go index 29de3fad1..1f549501d 100644 --- a/pkg/beholder/example_test.go +++ b/pkg/beholder/example_test.go @@ -20,10 +20,12 @@ func ExampleBeholderCustomMessage() { config := beholder.DefaultConfig() // Initialize beholder otel client which sets up OTel components - client, err := beholder.NewClient(ctx, config, errorHandler) + client, err := beholder.NewClient(ctx, config) if err != nil { log.Fatalf("Error creating Beholder client: %v", err) } + // Handle OTel errors + beholder.SetOtelErrorHandler(errorHandler) // Set global client so it will be accessible from anywhere through beholder functions beholder.SetClient(client) @@ -40,8 +42,9 @@ func ExampleBeholderCustomMessage() { } // Emit the custom message anywhere from application logic - fmt.Println("Emit custom messages") for range 10 { + fmt.Println("Emit custom messages from mercury") + err := beholder.MessageEmitter().Emit(context.Background(), payloadBytes, "beholder_data_schema", "/custom-message/versions/1", // required "beholder_data_type", "custom_message", @@ -61,10 +64,12 @@ func ExampleBeholderMetricTraces() { config := beholder.DefaultConfig() // Initialize beholder otel client which sets up OTel components - client, err := beholder.NewClient(ctx, config, errorHandler) + client, err := beholder.NewClient(ctx, config) if err != nil { log.Fatalf("Error creating Beholder client: %v", err) } + // Handle OTel errors + beholder.SetOtelErrorHandler(errorHandler) // Set global client so it will be accessible from anywhere through beholder functions beholder.SetClient(client) diff --git a/pkg/beholder/noop.go b/pkg/beholder/noop.go index 488c50d43..1b9161f96 100644 --- a/pkg/beholder/noop.go +++ b/pkg/beholder/noop.go @@ -57,7 +57,7 @@ func NewStdoutClient(opts ...StddutClientOption) (*Client, error) { } loggerProvider := sdklog.NewLoggerProvider(sdklog.WithProcessor(sdklog.NewSimpleProcessor(loggerExporter))) logger := loggerProvider.Logger(defaultPackageName) - setOtelErrorHandler(func(err error) { + SetOtelErrorHandler(func(err error) { fmt.Printf("OTel error %s", err) }) From f2b74d4b305ea72f131ad7bcdd277883ff36dfe0 Mon Sep 17 00:00:00 2001 From: Pavel <177363085+pkcll@users.noreply.github.com> Date: Fri, 23 Aug 2024 14:07:45 -0400 Subject: [PATCH 43/49] Fix test --- pkg/beholder/example_test.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pkg/beholder/example_test.go b/pkg/beholder/example_test.go index 1f549501d..a24a334b4 100644 --- a/pkg/beholder/example_test.go +++ b/pkg/beholder/example_test.go @@ -42,9 +42,8 @@ func ExampleBeholderCustomMessage() { } // Emit the custom message anywhere from application logic + fmt.Println("Emit custom messages") for range 10 { - fmt.Println("Emit custom messages from mercury") - err := beholder.MessageEmitter().Emit(context.Background(), payloadBytes, "beholder_data_schema", "/custom-message/versions/1", // required "beholder_data_type", "custom_message", From 0d12d2c70a341015c90aae2302afa488cec39df4 Mon Sep 17 00:00:00 2001 From: Pavel <177363085+pkcll@users.noreply.github.com> Date: Fri, 23 Aug 2024 16:58:34 -0400 Subject: [PATCH 44/49] Beholder: update example test to pass golangci-lint --- pkg/beholder/example_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/beholder/example_test.go b/pkg/beholder/example_test.go index a24a334b4..38ae87dc2 100644 --- a/pkg/beholder/example_test.go +++ b/pkg/beholder/example_test.go @@ -14,7 +14,7 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/beholder/pb" ) -func ExampleBeholderCustomMessage() { +func ExampleNewClient() { ctx := context.Background() config := beholder.DefaultConfig() @@ -57,7 +57,7 @@ func ExampleBeholderCustomMessage() { // Emit custom messages } -func ExampleBeholderMetricTraces() { +func ExampleTracer() { ctx := context.Background() config := beholder.DefaultConfig() @@ -99,7 +99,7 @@ func ExampleBeholderMetricTraces() { // Create new trace span } -func ExampleNoopBeholder() { +func ExampleNewNoopClient() { fmt.Println("Beholder is not initialized. Fall back to Noop OTel Client") fmt.Println("Emitting custom message via noop otel client") From fc72e2e0417d39c60cc993b6056e76993b025c41 Mon Sep 17 00:00:00 2001 From: Pavel <177363085+pkcll@users.noreply.github.com> Date: Fri, 23 Aug 2024 17:01:31 -0400 Subject: [PATCH 45/49] go mod tidy --- go.mod | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/go.mod b/go.mod index 52f4e9b31..512e8fca0 100644 --- a/go.mod +++ b/go.mod @@ -29,6 +29,7 @@ require ( github.com/pelletier/go-toml/v2 v2.2.0 github.com/prometheus/client_golang v1.17.0 github.com/riferrei/srclient v0.5.4 + github.com/santhosh-tekuri/jsonschema/v5 v5.2.0 github.com/shopspring/decimal v1.4.0 github.com/smartcontractkit/libocr v0.0.0-20240419185742-fd3cab206b2c github.com/stretchr/testify v1.9.0 @@ -41,6 +42,7 @@ require ( go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.28.0 go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.28.0 go.opentelemetry.io/otel/log v0.4.0 + go.opentelemetry.io/otel/metric v1.28.0 go.opentelemetry.io/otel/sdk v1.28.0 go.opentelemetry.io/otel/sdk/log v0.4.0 go.opentelemetry.io/otel/sdk/metric v1.28.0 @@ -56,12 +58,6 @@ require ( sigs.k8s.io/yaml v1.4.0 ) -require ( - github.com/go-playground/locales v0.13.0 // indirect - github.com/go-playground/universal-translator v0.17.0 // indirect - github.com/leodido/go-urn v1.2.0 // indirect -) - require ( github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/beorn7/perks v1.0.1 // indirect @@ -72,12 +68,15 @@ require ( github.com/fatih/color v1.16.0 // 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.13.0 // indirect + github.com/go-playground/universal-translator v0.17.0 // indirect github.com/goccy/go-yaml v1.12.0 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb // indirect; indirec github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // indirect github.com/hashicorp/yamux v0.1.1 // indirect + github.com/leodido/go-urn v1.2.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect @@ -92,12 +91,10 @@ require ( github.com/prometheus/common v0.44.0 // indirect github.com/prometheus/procfs v0.11.1 // indirect github.com/sanity-io/litter v1.5.5 // indirect - github.com/santhosh-tekuri/jsonschema/v5 v5.2.0 github.com/stretchr/objx v0.5.2 // indirect github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect github.com/x448/float16 v0.8.4 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 // indirect - go.opentelemetry.io/otel/metric v1.28.0 go.opentelemetry.io/proto/otlp v1.3.1 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/mod v0.20.0 // indirect From 64f6e213fde4005d04a9047647c3342e693b7eaf Mon Sep 17 00:00:00 2001 From: Pavel <177363085+pkcll@users.noreply.github.com> Date: Mon, 26 Aug 2024 17:27:53 -0400 Subject: [PATCH 46/49] Beholder: delete SetOtelErrorHandler, use otel.SetErrorHandler instead --- pkg/beholder/client.go | 8 +------- pkg/beholder/client_test.go | 5 +++-- pkg/beholder/example_test.go | 6 ++++-- pkg/beholder/noop.go | 5 +++-- 4 files changed, 11 insertions(+), 13 deletions(-) diff --git a/pkg/beholder/client.go b/pkg/beholder/client.go index edfd23daa..1b3c74985 100644 --- a/pkg/beholder/client.go +++ b/pkg/beholder/client.go @@ -4,7 +4,6 @@ import ( "context" "errors" - "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc" "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc" @@ -52,7 +51,7 @@ type Client struct { } // NewClient creates a new Client with initialized OpenTelemetry components -// To handle OpenTelemetry errors use SetOtelErrorHandler +// To handle OpenTelemetry errors use [otel.SetErrorHandler](https://pkg.go.dev/go.opentelemetry.io/otel#SetErrorHandler) func NewClient(ctx context.Context, cfg Config) (*Client, error) { factory := func(ctx context.Context, options ...otlploggrpc.Option) (sdklog.Exporter, error) { return otlploggrpc.New(ctx, options...) @@ -196,11 +195,6 @@ func (c Client) ForPackage(name string) Client { return newClient } -// Sets global error handler for OpenTelemetry -func SetOtelErrorHandler(h func(err error)) { - otel.SetErrorHandler(otel.ErrorHandlerFunc(h)) -} - func newOtelResource(cfg Config) (resource *sdkresource.Resource, err error) { extraResources, err := sdkresource.New( context.Background(), diff --git a/pkg/beholder/client_test.go b/pkg/beholder/client_test.go index b50fd99ce..fe7eac6f8 100644 --- a/pkg/beholder/client_test.go +++ b/pkg/beholder/client_test.go @@ -8,6 +8,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" + "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc" otellog "go.opentelemetry.io/otel/log" sdklog "go.opentelemetry.io/otel/sdk/log" @@ -92,7 +93,7 @@ func TestClient(t *testing.T) { if err != nil { t.Fatalf("Error creating beholder client: %v", err) } - SetOtelErrorHandler(otelErrorHandler) + otel.SetErrorHandler(otel.ErrorHandlerFunc(otelErrorHandler)) // Number of exported messages exportedMessageCount := 0 @@ -148,7 +149,7 @@ func TestEmitterMessageValidation(t *testing.T) { return exporterMock, nil }, ) - SetOtelErrorHandler(func(err error) { t.Fatalf("otel error: %v", err) }) + otel.SetErrorHandler(otel.ErrorHandlerFunc(func(err error) { t.Fatalf("otel error: %v", err) })) assert.NoError(t, err) return client.Emitter } diff --git a/pkg/beholder/example_test.go b/pkg/beholder/example_test.go index 38ae87dc2..b582fd616 100644 --- a/pkg/beholder/example_test.go +++ b/pkg/beholder/example_test.go @@ -6,6 +6,7 @@ import ( "log" "math/rand" + "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" "google.golang.org/protobuf/proto" @@ -25,7 +26,7 @@ func ExampleNewClient() { log.Fatalf("Error creating Beholder client: %v", err) } // Handle OTel errors - beholder.SetOtelErrorHandler(errorHandler) + otel.SetErrorHandler(otel.ErrorHandlerFunc(errorHandler)) // Set global client so it will be accessible from anywhere through beholder functions beholder.SetClient(client) @@ -68,7 +69,8 @@ func ExampleTracer() { log.Fatalf("Error creating Beholder client: %v", err) } // Handle OTel errors - beholder.SetOtelErrorHandler(errorHandler) + otel.SetErrorHandler(otel.ErrorHandlerFunc(errorHandler)) + // Set global client so it will be accessible from anywhere through beholder functions beholder.SetClient(client) diff --git a/pkg/beholder/noop.go b/pkg/beholder/noop.go index 1b9161f96..bca842e3e 100644 --- a/pkg/beholder/noop.go +++ b/pkg/beholder/noop.go @@ -7,6 +7,7 @@ import ( "io" "time" + "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/exporters/stdout/stdoutlog" "go.opentelemetry.io/otel/exporters/stdout/stdoutmetric" "go.opentelemetry.io/otel/exporters/stdout/stdouttrace" @@ -57,9 +58,9 @@ func NewStdoutClient(opts ...StddutClientOption) (*Client, error) { } loggerProvider := sdklog.NewLoggerProvider(sdklog.WithProcessor(sdklog.NewSimpleProcessor(loggerExporter))) logger := loggerProvider.Logger(defaultPackageName) - SetOtelErrorHandler(func(err error) { + otel.SetErrorHandler(otel.ErrorHandlerFunc(func(err error) { fmt.Printf("OTel error %s", err) - }) + })) // Tracer traceExporter, err := stdouttrace.New(cfg.TraceOptions...) From fa6cec4a1dd1a5d01f48dfc36220bc66086771db Mon Sep 17 00:00:00 2001 From: Pavel <177363085+pkcll@users.noreply.github.com> Date: Mon, 26 Aug 2024 17:30:56 -0400 Subject: [PATCH 47/49] Beholder: prefix global getters with 'Get*' --- pkg/beholder/example_test.go | 10 +++++----- pkg/beholder/global.go | 8 ++++---- pkg/beholder/global_test.go | 4 ++-- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/pkg/beholder/example_test.go b/pkg/beholder/example_test.go index b582fd616..92fb0acde 100644 --- a/pkg/beholder/example_test.go +++ b/pkg/beholder/example_test.go @@ -45,7 +45,7 @@ func ExampleNewClient() { // Emit the custom message anywhere from application logic fmt.Println("Emit custom messages") for range 10 { - err := beholder.MessageEmitter().Emit(context.Background(), payloadBytes, + err := beholder.GetEmitter().Emit(context.Background(), payloadBytes, "beholder_data_schema", "/custom-message/versions/1", // required "beholder_data_type", "custom_message", "foo", "bar", @@ -75,13 +75,13 @@ func ExampleTracer() { beholder.SetClient(client) // Define a new counter - counter, err := beholder.Meter().Int64Counter("custom_message.count") + counter, err := beholder.GetMeter().Int64Counter("custom_message.count") if err != nil { log.Fatalf("failed to create new counter") } // Define a new gauge - gauge, err := beholder.Meter().Int64Gauge("custom_message.gauge") + gauge, err := beholder.GetMeter().Int64Gauge("custom_message.gauge") if err != nil { log.Fatalf("failed to create new gauge") } @@ -92,7 +92,7 @@ func ExampleTracer() { gauge.Record(ctx, rand.Int63n(101)) fmt.Println("Create new trace span") - _, rootSpan := beholder.Tracer().Start(ctx, "foo", trace.WithAttributes( + _, rootSpan := beholder.GetTracer().Start(ctx, "foo", trace.WithAttributes( attribute.String("app_name", "beholderdemo"), )) defer rootSpan.End() @@ -106,7 +106,7 @@ func ExampleNewNoopClient() { fmt.Println("Emitting custom message via noop otel client") - err := beholder.MessageEmitter().Emit(context.Background(), []byte("test message"), + err := beholder.GetEmitter().Emit(context.Background(), []byte("test message"), "beholder_data_schema", "/custom-message/versions/1", // required ) if err != nil { diff --git a/pkg/beholder/global.go b/pkg/beholder/global.go index e3706f786..b9f2cb366 100644 --- a/pkg/beholder/global.go +++ b/pkg/beholder/global.go @@ -25,19 +25,19 @@ func GetClient() *Client { return globalClient.Load() } -func Logger() otellog.Logger { +func GetLogger() otellog.Logger { return GetClient().Logger } -func Tracer() oteltrace.Tracer { +func GetTracer() oteltrace.Tracer { return GetClient().Tracer } -func Meter() otelmetric.Meter { +func GetMeter() otelmetric.Meter { return GetClient().Meter } -func MessageEmitter() Emitter { +func GetEmitter() Emitter { return GetClient().Emitter } diff --git a/pkg/beholder/global_test.go b/pkg/beholder/global_test.go index af6285372..9b877b922 100644 --- a/pkg/beholder/global_test.go +++ b/pkg/beholder/global_test.go @@ -25,7 +25,7 @@ import ( func TestGlobal(t *testing.T) { // Get global logger, tracer, meter, messageEmitter // If not initialized with beholder.SetClient will return noop client - logger, tracer, meter, messageEmitter := beholder.Logger(), beholder.Tracer(), beholder.Meter(), beholder.MessageEmitter() + logger, tracer, meter, messageEmitter := beholder.GetLogger(), beholder.GetTracer(), beholder.GetMeter(), beholder.GetEmitter() noopClient := beholder.NewNoopClient() assert.IsType(t, otellognoop.Logger{}, logger) assert.IsType(t, oteltracenoop.Tracer{}, tracer) @@ -42,7 +42,7 @@ func TestGlobal(t *testing.T) { assert.Same(t, noopClientPtr, beholder.GetClient()) // After that use beholder functions to get logger, tracer, meter, messageEmitter - logger, tracer, meter, messageEmitter = beholder.Logger(), beholder.Tracer(), beholder.Meter(), beholder.MessageEmitter() + logger, tracer, meter, messageEmitter = beholder.GetLogger(), beholder.GetTracer(), beholder.GetMeter(), beholder.GetEmitter() // Emit otel log record logger.Emit(tests.Context(t), otellog.Record{}) From 3504324de7b444664cd40e5ed1ab70f42a6c4480 Mon Sep 17 00:00:00 2001 From: Pavel <177363085+pkcll@users.noreply.github.com> Date: Tue, 27 Aug 2024 14:13:00 -0400 Subject: [PATCH 48/49] Beholder: remove StdoutClientOption, rename NewStdoutClient to NewWriterClient --- pkg/beholder/client_test.go | 2 +- pkg/beholder/global_test.go | 2 +- pkg/beholder/noop.go | 38 ++++++++++++++++++------------------- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/pkg/beholder/client_test.go b/pkg/beholder/client_test.go index fe7eac6f8..6d3203034 100644 --- a/pkg/beholder/client_test.go +++ b/pkg/beholder/client_test.go @@ -229,7 +229,7 @@ func TestClient_ForPackage(t *testing.T) { exporterMock := mocks.NewOTLPExporter(t) defer exporterMock.AssertExpectations(t) var b strings.Builder - client, err := NewStdoutClient(WithWriter(&b)) + client, err := NewWriterClient(&b) assert.NoError(t, err) clientForTest := client.ForPackage("TestClient_ForPackage") diff --git a/pkg/beholder/global_test.go b/pkg/beholder/global_test.go index 9b877b922..1dbc9e373 100644 --- a/pkg/beholder/global_test.go +++ b/pkg/beholder/global_test.go @@ -75,7 +75,7 @@ func TestClient_SetGlobalOtelProviders(t *testing.T) { }) var b strings.Builder - client, err := beholder.NewStdoutClient(beholder.WithWriter(&b)) + client, err := beholder.NewWriterClient(&b) assert.NoError(t, err) // Set global Otel Client beholder.SetClient(client) diff --git a/pkg/beholder/noop.go b/pkg/beholder/noop.go index bca842e3e..a39d3b901 100644 --- a/pkg/beholder/noop.go +++ b/pkg/beholder/noop.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "io" + "os" "time" "go.opentelemetry.io/otel" @@ -41,14 +42,17 @@ func NewNoopClient() *Client { return &client } -// NewStdoutClient creates a new Client with stdout exporters -// Use for testing and debugging -// Also this client is used as a noop client when otel exporter is not initialized properly -func NewStdoutClient(opts ...StddutClientOption) (*Client, error) { - cfg := DefaultStdoutClientConfig() - for _, opt := range opts { - opt(&cfg) - } +// NewStdoutClient creates a new Client with exporters which send telemetry data to standard output +// Used for testing and debugging +func NewStdoutClient() (*Client, error) { + return NewWriterClient(os.Stdout) +} + +// NewWriterClient creates a new Client with otel exporters which send telemetry data to custom io.Writer +func NewWriterClient(w io.Writer) (*Client, error) { + cfg := DefaultWriterClientConfig() + cfg.WithWriter(w) + // Logger loggerExporter, err := stdoutlog.New( append([]stdoutlog.Option{stdoutlog.WithoutTimestamps()}, cfg.LogOptions...)..., @@ -115,25 +119,21 @@ func noopOnClose() error { return nil } -type StddutClientOption func(*StdoutClientConfig) - -type StdoutClientConfig struct { +type WriterClientConfig struct { Config LogOptions []stdoutlog.Option TraceOptions []stdouttrace.Option MetricOptions []stdoutmetric.Option } -func DefaultStdoutClientConfig() StdoutClientConfig { - return StdoutClientConfig{ +func DefaultWriterClientConfig() WriterClientConfig { + return WriterClientConfig{ Config: DefaultConfig(), } } -func WithWriter(w io.Writer) StddutClientOption { - return func(cfg *StdoutClientConfig) { - cfg.LogOptions = append(cfg.LogOptions, stdoutlog.WithWriter(w)) - cfg.TraceOptions = append(cfg.TraceOptions, stdouttrace.WithWriter(w)) - cfg.MetricOptions = append(cfg.MetricOptions, stdoutmetric.WithWriter(w)) - } +func (cfg *WriterClientConfig) WithWriter(w io.Writer) { + cfg.LogOptions = append(cfg.LogOptions, stdoutlog.WithWriter(w)) + cfg.TraceOptions = append(cfg.TraceOptions, stdouttrace.WithWriter(w)) + cfg.MetricOptions = append(cfg.MetricOptions, stdoutmetric.WithWriter(w)) } From 2187c017d32052632b16fe25f40034fa0d5a4ae9 Mon Sep 17 00:00:00 2001 From: Pavel <177363085+pkcll@users.noreply.github.com> Date: Tue, 27 Aug 2024 14:18:34 -0400 Subject: [PATCH 49/49] Beholder: make writerClientConfig not exported --- pkg/beholder/noop.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/beholder/noop.go b/pkg/beholder/noop.go index a39d3b901..2238453ec 100644 --- a/pkg/beholder/noop.go +++ b/pkg/beholder/noop.go @@ -119,20 +119,20 @@ func noopOnClose() error { return nil } -type WriterClientConfig struct { +type writerClientConfig struct { Config LogOptions []stdoutlog.Option TraceOptions []stdouttrace.Option MetricOptions []stdoutmetric.Option } -func DefaultWriterClientConfig() WriterClientConfig { - return WriterClientConfig{ +func DefaultWriterClientConfig() writerClientConfig { + return writerClientConfig{ Config: DefaultConfig(), } } -func (cfg *WriterClientConfig) WithWriter(w io.Writer) { +func (cfg *writerClientConfig) WithWriter(w io.Writer) { cfg.LogOptions = append(cfg.LogOptions, stdoutlog.WithWriter(w)) cfg.TraceOptions = append(cfg.TraceOptions, stdouttrace.WithWriter(w)) cfg.MetricOptions = append(cfg.MetricOptions, stdoutmetric.WithWriter(w))