From e8e20a2b0aae2bf0c6702f20dcb642fe7b7a3bfd Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Fri, 7 Jul 2023 18:58:47 -0700 Subject: [PATCH] rpcapi: Emit top-level tracing spans for requests rpcapi is a normal gRPC server once it's running, so if the overall Terraform CLI process has been configured to export telemetry then this will emit a span for each incoming gRPC request. In later commits we'll hopefully add further telemetry downstream to cover interesting child spans such as running Terraform Core operations, but this is at least enough to observe which requests a client is making and what their outcomes were. --- go.mod | 21 ++- go.sum | 45 +++-- internal/rpcapi/grpc_testing.go | 8 +- internal/rpcapi/server.go | 8 +- internal/rpcapi/setup.go | 9 +- internal/rpcapi/telemetry.go | 20 ++ internal/rpcapi/telemetry_test.go | 297 ++++++++++++++++++++++++++++++ 7 files changed, 376 insertions(+), 32 deletions(-) create mode 100644 internal/rpcapi/telemetry.go create mode 100644 internal/rpcapi/telemetry_test.go diff --git a/go.mod b/go.mod index a402cf7025..74cd969ad9 100644 --- a/go.mod +++ b/go.mod @@ -1,7 +1,7 @@ module github.com/hashicorp/terraform require ( - cloud.google.com/go/kms v1.12.1 + cloud.google.com/go/kms v1.15.0 cloud.google.com/go/storage v1.30.1 github.com/Azure/azure-sdk-for-go v59.2.0+incompatible github.com/Azure/go-autorest/autorest v0.11.27 @@ -32,7 +32,7 @@ require ( github.com/go-test/deep v1.0.3 github.com/golang/mock v1.6.0 github.com/google/go-cmp v0.6.0 - github.com/google/uuid v1.3.0 + github.com/google/uuid v1.3.1 github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.40 github.com/hashicorp/consul/api v1.13.0 github.com/hashicorp/consul/sdk v0.8.0 @@ -46,6 +46,7 @@ require ( github.com/hashicorp/go-multierror v1.1.1 github.com/hashicorp/go-plugin v1.4.3 github.com/hashicorp/go-retryablehttp v0.7.4 + github.com/hashicorp/go-slug v0.12.2 github.com/hashicorp/go-tfe v1.34.0 github.com/hashicorp/go-uuid v1.0.3 github.com/hashicorp/go-version v1.6.0 @@ -87,6 +88,7 @@ require ( github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b github.com/zclconf/go-cty-yaml v1.0.3 go.opentelemetry.io/contrib/exporters/autoexport v0.45.0 + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.0 go.opentelemetry.io/otel v1.20.0 go.opentelemetry.io/otel/sdk v1.20.0 go.opentelemetry.io/otel/trace v1.20.0 @@ -94,15 +96,15 @@ require ( golang.org/x/exp v0.0.0-20230905200255-921286631fa9 golang.org/x/mod v0.12.0 golang.org/x/net v0.17.0 - golang.org/x/oauth2 v0.10.0 + golang.org/x/oauth2 v0.11.0 golang.org/x/sys v0.14.0 golang.org/x/term v0.13.0 golang.org/x/text v0.14.0 golang.org/x/tools v0.13.0 golang.org/x/tools/cmd/cover v0.1.0-deprecated google.golang.org/api v0.126.0 - google.golang.org/genproto v0.0.0-20230711160842-782d3b101e98 - google.golang.org/grpc v1.58.3 + google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d + google.golang.org/grpc v1.59.0 google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0 google.golang.org/protobuf v1.31.0 honnef.co/go/tools v0.5.0-0.dev.0.20230826160118-ad5ca31ff221 @@ -113,8 +115,8 @@ require ( ) require ( - cloud.google.com/go v0.110.4 // indirect - cloud.google.com/go/compute v1.21.0 // indirect + cloud.google.com/go v0.110.7 // indirect + cloud.google.com/go/compute v1.23.0 // indirect cloud.google.com/go/compute/metadata v0.2.3 // indirect cloud.google.com/go/iam v1.1.1 // indirect github.com/AlecAivazis/survey/v2 v2.3.6 // indirect @@ -200,7 +202,6 @@ require ( github.com/hashicorp/go-msgpack v0.5.4 // indirect github.com/hashicorp/go-rootcerts v1.0.2 // indirect github.com/hashicorp/go-safetemp v1.0.0 // indirect - github.com/hashicorp/go-slug v0.12.2 // indirect github.com/hashicorp/golang-lru v0.5.1 // indirect github.com/hashicorp/serf v0.9.6 // indirect github.com/hashicorp/terraform-plugin-log v0.9.0 // indirect @@ -261,8 +262,8 @@ require ( golang.org/x/time v0.3.0 // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect google.golang.org/appengine v1.6.7 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/ini.v1 v1.66.2 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/go.sum b/go.sum index 3ec3cfba02..fb5019fa45 100644 --- a/go.sum +++ b/go.sum @@ -32,8 +32,8 @@ cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w9 cloud.google.com/go v0.102.0/go.mod h1:oWcCzKlqJ5zgHQt9YsaeTY9KzIvjyy0ArmiBUgpQ+nc= cloud.google.com/go v0.102.1/go.mod h1:XZ77E9qnTEnrgEOvr4xzfdX5TRo7fB4T2F4O6+34hIU= cloud.google.com/go v0.104.0/go.mod h1:OO6xxXdJyvuJPcEPBLN9BJPD+jep5G1+2U5B5gkRYtA= -cloud.google.com/go v0.110.4 h1:1JYyxKMN9hd5dR2MYTPWkGUgcoxVVhg0LKNKEo0qvmk= -cloud.google.com/go v0.110.4/go.mod h1:+EYjdK8e5RME/VY/qLCAtuyALQ9q67dvuum8i+H5xsI= +cloud.google.com/go v0.110.7 h1:rJyC7nWRg2jWGZ4wSJ5nY65GTdYJkg0cd/uXb+ACI6o= +cloud.google.com/go v0.110.7/go.mod h1:+EYjdK8e5RME/VY/qLCAtuyALQ9q67dvuum8i+H5xsI= cloud.google.com/go/aiplatform v1.22.0/go.mod h1:ig5Nct50bZlzV6NvKaTwmplLLddFx0YReh9WfTO5jKw= cloud.google.com/go/aiplatform v1.24.0/go.mod h1:67UUvRBKG6GTayHKV8DBv2RtR1t93YRu5B1P3x99mYY= cloud.google.com/go/analytics v0.11.0/go.mod h1:DjEWCu41bVbYcKyvlws9Er60YE4a//bK6mnhWvQeFNI= @@ -70,8 +70,8 @@ cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU= cloud.google.com/go/compute v1.7.0/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQHHZWZxy9U= cloud.google.com/go/compute v1.10.0/go.mod h1:ER5CLbMxl90o2jtNbGSbtfOpQKR0t15FOtRsugnLrlU= -cloud.google.com/go/compute v1.21.0 h1:JNBsyXVoOoNJtTQcnEY5uYpZIbeCTYIeDe0Xh1bySMk= -cloud.google.com/go/compute v1.21.0/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM= +cloud.google.com/go/compute v1.23.0 h1:tP41Zoavr8ptEqaW6j+LQOnyBBhO7OkOMAGrgLopTwY= +cloud.google.com/go/compute v1.23.0/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM= cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= cloud.google.com/go/containeranalysis v0.5.1/go.mod h1:1D92jd8gRR/c0fGMlymRgxWD3Qw9C1ff6/T7mLgVL8I= @@ -113,8 +113,8 @@ cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp cloud.google.com/go/iam v0.5.0/go.mod h1:wPU9Vt0P4UmCux7mqtRu6jcpPAb74cP1fh50J3QpkUc= cloud.google.com/go/iam v1.1.1 h1:lW7fzj15aVIXYHREOqjRBV9PsH0Z6u8Y46a1YGvQP4Y= cloud.google.com/go/iam v1.1.1/go.mod h1:A5avdyVL2tCppe4unb0951eI9jreack+RJ0/d+KUZOU= -cloud.google.com/go/kms v1.12.1 h1:xZmZuwy2cwzsocmKDOPu4BL7umg8QXagQx6fKVmf45U= -cloud.google.com/go/kms v1.12.1/go.mod h1:c9J991h5DTl+kg7gi3MYomh12YEENGrf48ee/N/2CDM= +cloud.google.com/go/kms v1.15.0 h1:xYl5WEaSekKYN5gGRyhjvZKM22GVBBCzegGNVPy+aIs= +cloud.google.com/go/kms v1.15.0/go.mod h1:c9J991h5DTl+kg7gi3MYomh12YEENGrf48ee/N/2CDM= cloud.google.com/go/language v1.4.0/go.mod h1:F9dRpNFQmJbkaop6g0JhSBXCNlO90e1KWx5iDdxbWic= cloud.google.com/go/language v1.6.0/go.mod h1:6dJ8t3B+lUYfStgls25GusK04NLh3eDLQnWM3mdEbhI= cloud.google.com/go/lifesciences v0.5.0/go.mod h1:3oIKy8ycWGPUyZDR/8RNnTOYevhaMLqh5vLUXs9zvT8= @@ -425,6 +425,8 @@ github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4 h1:/inchEIKaYC1Akx+H+gqO04wryn5h75LSazbRlnya1k= +github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d h1:t5Wuyh53qYyg9eqn4BbnlIT+vmhyww0TatL+zT3uWgI= github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= @@ -461,6 +463,8 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.m github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/envoyproxy/protoc-gen-validate v1.0.2 h1:QkIBuU5k+x7/QXPvPPnWXWlCdaBFApVqftFV6k087DA= +github.com/envoyproxy/protoc-gen-validate v1.0.2/go.mod h1:GpiZQP3dDbg4JouG/NNS7QWXpgx6x8QiMKdmN72jogE= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= @@ -522,8 +526,8 @@ github.com/golang-jwt/jwt/v4 v4.4.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w github.com/golang-jwt/jwt/v4 v4.4.2 h1:rcc4lwaZgFMCZ5jxF9ABolDcIHdBytAFgqFPbSJQAYs= github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/glog v1.1.0 h1:/d3pCKDPWNnvIWe0vVUpNP32qc8U3PDVxySP/y360qE= -github.com/golang/glog v1.1.0/go.mod h1:pfYeQZ3JWZoXTV5sFc986z3HTpwQs9At6P4ImfuP3NQ= +github.com/golang/glog v1.1.2 h1:DVjP2PbBOzHyzA+dn3WhHIq4NdVu3Q+pvivFICf/7fo= +github.com/golang/glog v1.1.2/go.mod h1:zR+okUeTbrL6EL3xHUDxZuEtGv04p5shwip1+mL/rLQ= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -619,8 +623,9 @@ github.com/google/s2a-go v0.1.4 h1:1kZ/sQM3srePvKs3tXAvQzo66XfcReoqFpIpIccE7Oc= github.com/google/s2a-go v0.1.4/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= +github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg= @@ -1082,6 +1087,8 @@ go.opentelemetry.io/contrib/exporters/autoexport v0.45.0 h1:KU3hwb3O+fc2F15lltmD go.opentelemetry.io/contrib/exporters/autoexport v0.45.0/go.mod h1:9hFI4YY6Ehe9enzw9qGlKAjJGQAtEo75Ysrb3byOZtI= go.opentelemetry.io/contrib/instrumentation/github.com/aws/aws-sdk-go-v2/otelaws v0.46.0 h1:qmQcwJOEOfNvVOD8H7bVAEipp+6UtnDK3qHGCcjwB9o= go.opentelemetry.io/contrib/instrumentation/github.com/aws/aws-sdk-go-v2/otelaws v0.46.0/go.mod h1:d1FGIeeryqx0a2Oa5oQrK1Ug85AGfFUx+nMtoAwJ4VI= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.0 h1:PzIubN4/sjByhDRHLviCjJuweBXWFZWhghjg7cS28+M= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.0/go.mod h1:Ct6zzQEuGK3WpJs2n4dn+wfJYzd/+hNnxMRTWjGn30M= go.opentelemetry.io/otel v1.20.0 h1:vsb/ggIY+hUjD/zCAQHpzTmndPqv/ml2ArbsbfBYTAc= go.opentelemetry.io/otel v1.20.0/go.mod h1:oUIGj3D77RwJdM6PPZImDpSZGDvkD9fhesHny69JFrs= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U= @@ -1262,8 +1269,8 @@ golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094/go.mod h1:h4gKUeWbJ4rQPri golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= golang.org/x/oauth2 v0.1.0/go.mod h1:G9FE4dLTsbXUu90h/Pf85g4w1D+SSAgR+q46nJZ8M4A= -golang.org/x/oauth2 v0.10.0 h1:zHCpF2Khkwy4mMB4bv0U37YtJdTGW8jI0glAApi0Kh8= -golang.org/x/oauth2 v0.10.0/go.mod h1:kTpgurOux7LqtuxjuyZa4Gj2gdezIt/jQtGnNFfypQI= +golang.org/x/oauth2 v0.11.0 h1:vPL4xzxBM4niKCW6g9whtaWVXTJf1U5e4aZxxFx/gbU= +golang.org/x/oauth2 v0.11.0/go.mod h1:LdF7O/8bLR/qWK9DrpXmbHLTouvRHK0SgJl0GmDBchk= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1644,12 +1651,12 @@ google.golang.org/genproto v0.0.0-20221010155953-15ba04fc1c0e/go.mod h1:3526vdqw google.golang.org/genproto v0.0.0-20221014173430-6e2ab493f96b/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM= google.golang.org/genproto v0.0.0-20221014213838-99cd37c6964a/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM= google.golang.org/genproto v0.0.0-20221025140454-527a21cfbd71/go.mod h1:9qHF0xnpdSfF6knlcsnpzUu5y+rpwgbvsyGAZPBMg4s= -google.golang.org/genproto v0.0.0-20230711160842-782d3b101e98 h1:Z0hjGZePRE0ZBWotvtrwxFNrNE9CUAGtplaDK5NNI/g= -google.golang.org/genproto v0.0.0-20230711160842-782d3b101e98/go.mod h1:S7mY02OqCJTD0E1OiQy1F72PWFB4bZJ87cAtLPYgDR0= -google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98 h1:FmF5cCW94Ij59cfpoLiwTgodWmm60eEV0CjlsVg2fuw= -google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98/go.mod h1:rsr7RhLuwsDKL7RmgDDCUc6yaGr1iqceVb5Wv6f6YvQ= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 h1:bVf09lpb+OJbByTj913DRJioFFAjf/ZGxEz7MajTp2U= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98/go.mod h1:TUfxEVdsvPg18p6AslUXFoLdpED4oBnGwyqk3dV1XzM= +google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d h1:VBu5YqKPv6XiJ199exd8Br+Aetz+o08F+PLMnwJQHAY= +google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d/go.mod h1:yZTlhN0tQnXo3h00fuXNCxJdLdIdnVFVBaRJ5LWBbw4= +google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d h1:DoPTO70H+bcDXcd39vOqb2viZxgqeBeSGtZ55yZU4/Q= +google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d/go.mod h1:KjSP20unUpOx5kyQUFa7k4OJg0qeJ7DEZflGDu2p6Bk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d h1:uvYuEyMHKNt+lT4K3bN6fGswmK8qSvcreM3BwjDh+y4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M= google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= @@ -1688,8 +1695,8 @@ google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACu google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= google.golang.org/grpc v1.50.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= -google.golang.org/grpc v1.58.3 h1:BjnpXut1btbtgN/6sp+brB2Kbm2LjNXnidYujAVbSoQ= -google.golang.org/grpc v1.58.3/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0= +google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk= +google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0 h1:M1YKkFIboKNieVO5DLUEVzQfGwJD30Nv2jfUgzb5UcE= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= diff --git a/internal/rpcapi/grpc_testing.go b/internal/rpcapi/grpc_testing.go index 4f649e2cdf..d1648640d8 100644 --- a/internal/rpcapi/grpc_testing.go +++ b/internal/rpcapi/grpc_testing.go @@ -5,6 +5,7 @@ import ( "net" "testing" + "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" "google.golang.org/grpc" "google.golang.org/grpc/test/bufconn" ) @@ -19,7 +20,10 @@ import ( // server end of this fake connection. func grpcClientForTesting(ctx context.Context, t *testing.T, registerServices func(srv *grpc.Server)) (conn grpc.ClientConnInterface, close func()) { fakeListener := bufconn.Listen(1024 /* buffer size */) - srv := grpc.NewServer() + srv := grpc.NewServer( + grpc.UnaryInterceptor(otelgrpc.UnaryServerInterceptor()), + grpc.StreamInterceptor(otelgrpc.StreamServerInterceptor()), + ) // Caller gets an opportunity to register specific services before // we actually start "serving". @@ -40,6 +44,8 @@ func grpcClientForTesting(ctx context.Context, t *testing.T, registerServices fu ctx, "testfake", grpc.WithContextDialer(fakeDialer), grpc.WithInsecure(), + grpc.WithUnaryInterceptor(otelgrpc.UnaryClientInterceptor()), + grpc.WithStreamInterceptor(otelgrpc.StreamClientInterceptor()), ) if err != nil { t.Fatalf("failed to connect to the fake server: %s", err) diff --git a/internal/rpcapi/server.go b/internal/rpcapi/server.go index 30b9a53138..1928e3a563 100644 --- a/internal/rpcapi/server.go +++ b/internal/rpcapi/server.go @@ -6,6 +6,7 @@ import ( "os" "github.com/hashicorp/go-plugin" + "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" "google.golang.org/grpc" ) @@ -34,7 +35,12 @@ func ServePlugin(ctx context.Context, opts ServerOpts) error { }, }, GRPCServer: func(opts []grpc.ServerOption) *grpc.Server { - server := grpc.NewServer(opts...) + fullOpts := []grpc.ServerOption{ + grpc.UnaryInterceptor(otelgrpc.UnaryServerInterceptor()), + grpc.StreamInterceptor(otelgrpc.StreamServerInterceptor()), + } + fullOpts = append(fullOpts, opts...) + server := grpc.NewServer(fullOpts...) // We'll also monitor the given context for cancellation // and terminate the server gracefully if we get cancelled. go func() { diff --git a/internal/rpcapi/setup.go b/internal/rpcapi/setup.go index a3e18b4f7b..0aae5d5843 100644 --- a/internal/rpcapi/setup.go +++ b/internal/rpcapi/setup.go @@ -5,6 +5,7 @@ import ( "sync" "github.com/hashicorp/terraform/internal/rpcapi/terraform1" + "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) @@ -39,7 +40,13 @@ func (s *setupServer) Handshake(ctx context.Context, req *terraform1.Handshake_R return nil, status.Error(codes.FailedPrecondition, "handshake already completed") } - serverCaps, err := s.initOthers(ctx, req.Capabilities) + var serverCaps *terraform1.ServerCapabilities + var err error + { + ctx, span := tracer.Start(ctx, "initialize RPC services") + serverCaps, err = s.initOthers(ctx, req.Capabilities) + span.End() + } s.initOthers = nil // cannot handshake again if err != nil { return nil, err diff --git a/internal/rpcapi/telemetry.go b/internal/rpcapi/telemetry.go new file mode 100644 index 0000000000..d8485a9854 --- /dev/null +++ b/internal/rpcapi/telemetry.go @@ -0,0 +1,20 @@ +package rpcapi + +import ( + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/trace" +) + +// tracer is the OpenTelemetry tracer to use for tracing for code in this +// package. +// +// When creating tracing spans in gRPC service functions, always use the +// a [context.Context] descended from the one passed in to the service +// function so that the spans can attach to the automatically-generated +// server request span and, if the client is also using OpenTelemetry, +// to the client's request span. +var tracer trace.Tracer + +func init() { + tracer = otel.Tracer("github.com/hashicorp/terraform/internal/rpcapi") +} diff --git a/internal/rpcapi/telemetry_test.go b/internal/rpcapi/telemetry_test.go new file mode 100644 index 0000000000..33919b2475 --- /dev/null +++ b/internal/rpcapi/telemetry_test.go @@ -0,0 +1,297 @@ +package rpcapi + +import ( + "context" + "testing" + "time" + + "github.com/davecgh/go-spew/spew" + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform/internal/rpcapi/terraform1" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/propagation" + "go.opentelemetry.io/otel/sdk/instrumentation" + "go.opentelemetry.io/otel/sdk/resource" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + "go.opentelemetry.io/otel/sdk/trace/tracetest" + semconv "go.opentelemetry.io/otel/semconv/v1.17.0" + "go.opentelemetry.io/otel/trace" + "google.golang.org/grpc" +) + +// initTelemetryForTest configures OpenTelemetry to collect spans into a +// local in-memory buffer and returns an object that provides access to that +// buffer. +// +// The OpenTelemetry tracer provider is a global cross-cutting concern shared +// throughout the program, so it isn't valid to use this function in any test +// that calls t.Parallel, or in subtests of a parent test that has already +// used this function. +func initTelemetryForTest(t *testing.T, providerOptions ...sdktrace.TracerProviderOption) *tracetest.InMemoryExporter { + t.Helper() + + exp := tracetest.NewInMemoryExporter() + sp := sdktrace.NewSimpleSpanProcessor(exp) + providerOptions = append( + []sdktrace.TracerProviderOption{ + sdktrace.WithSpanProcessor(sp), + }, + providerOptions..., + ) + provider := sdktrace.NewTracerProvider(providerOptions...) + otel.SetTracerProvider(provider) + + pgtr := propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{}) + otel.SetTextMapPropagator(pgtr) + + // We'll automatically shut down the provider at the end of the test run, + // because otherwise a subsequent test which runs something that generates + // telemetry _without_ calling initTelemetryForTest (which is optional) + // could end up appending irrelevant spans to an earlier test's exporter. + t.Cleanup(func() { + provider.Shutdown(context.Background()) + otel.SetTracerProvider(nil) + otel.SetTextMapPropagator(nil) + }) + + t.Log("OpenTelemetry initialized") + return exp +} + +// findTestTelemetrySpan tests each of the spans that have been reported to the +// given [tracetest.InMemoryExporter] with the given predicate function and +// returns the first one for which the predicate matches. +// +// If the predicate returns false for all spans then this function will fail +// the test using the given [testing.T]. +func findTestTelemetrySpan(t *testing.T, exp *tracetest.InMemoryExporter, predicate func(tracetest.SpanStub) bool) tracetest.SpanStub { + for _, span := range exp.GetSpans() { + if predicate(span) { + return span + } + } + t.Fatal("no spans matched the predicate") + return tracetest.SpanStub{} +} + +// findTestTelemetrySpans tests each of the spans that have been reported to the +// given [tracetest.InMemoryExporter] with the given predicate function and +// returns only those for which the predicate matches. +// +// If no spans match at all then the result is a zero-length slice. If you are +// expecting to find exactly one matching span then [findTestTelemetrySpan] +// (singular) might be more convenient. +func findTestTelemetrySpans(t *testing.T, exp *tracetest.InMemoryExporter, predicate func(tracetest.SpanStub) bool) tracetest.SpanStubs { + var ret tracetest.SpanStubs + for _, span := range exp.GetSpans() { + if predicate(span) { + ret = append(ret, span) + } + } + return ret +} + +// overwriteTestSpanTimestamps overwrites the timestamps in all of the given +// spans to be exactly the given fakeTime, as a way to avoid considering exact +// timestamps when comparing actual spans with desired spans. +// +// This function overwrites both the start and end times of the spans themselves +// and also the timestamps of any events associated with the spans. +func overwriteTestSpanTimestamps(spans tracetest.SpanStubs, fakeTime time.Time) { + for i := range spans { + spans[i].StartTime = fakeTime + spans[i].EndTime = fakeTime + for j := range spans[i].Events { + spans[i].Events[j].Time = fakeTime + } + } +} + +func fixedTraceID(n uint32) trace.TraceID { + return trace.TraceID{ + 0xfe, 0xed, 0xfa, 0xce, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + uint8(n >> 24), uint8(n >> 16), uint8(n >> 8), uint8(n >> 0), + } +} + +func fixedSpanID(n uint32) trace.SpanID { + return trace.SpanID{ + 0xfa, 0xce, 0xfe, 0xed, + uint8(n >> 24), uint8(n >> 16), uint8(n >> 8), uint8(n >> 0), + } +} + +func TestTelemetryInTests(t *testing.T) { + ctx := context.Background() + + testResource := resource.NewWithAttributes( + semconv.SchemaURL, + semconv.ServiceNameKey.String("telemetry test"), + semconv.ServiceVersionKey.String("1.2.3"), + ) + + telemetry := initTelemetryForTest(t, + sdktrace.WithResource(testResource), + ) + + var parentSpanContext, childSpanContext trace.SpanContext + + tracer := otel.Tracer("test thingy") + { + ctx, parentSpan := tracer.Start(ctx, "parent span") + parentSpanContext = parentSpan.SpanContext() + { + _, childSpan := tracer.Start(ctx, "child span") + childSpanContext = childSpan.SpanContext() + childSpan.AddEvent("did something totally hilarious") + childSpan.SetStatus(codes.Error, "it went wrong") + childSpan.End() + } + parentSpan.End() + } + + gotSpans := telemetry.GetSpans() + + // The spans contain real timestamps that make them annoying to compare, + // so we'll just replace those with fixed timestamps so we can easily + // compare everything else. + fakeTime := time.Now() + overwriteTestSpanTimestamps(gotSpans, fakeTime) + + wantSpans := tracetest.SpanStubs{ + // These are ordered by the calls to Span.End above, so child should + // always appear first. (That's a detail of this in-memory-only + // exporter, not a general guarantee about OpenTracing.) + { + Name: "child span", + SpanContext: childSpanContext, + Parent: parentSpanContext, + SpanKind: trace.SpanKindInternal, + StartTime: fakeTime, + EndTime: fakeTime, + Events: []sdktrace.Event{ + { + Name: "did something totally hilarious", + Time: fakeTime, + }, + }, + Status: sdktrace.Status{ + Code: codes.Error, + Description: "it went wrong", + }, + Resource: testResource, + InstrumentationLibrary: instrumentation.Scope{ + Name: "test thingy", + }, + }, + { + Name: "parent span", + SpanContext: parentSpanContext, + SpanKind: trace.SpanKindInternal, + StartTime: fakeTime, + EndTime: fakeTime, + ChildSpanCount: 1, + Resource: testResource, + InstrumentationLibrary: instrumentation.Scope{ + Name: "test thingy", + }, + }, + } + + if diff := cmp.Diff(wantSpans, gotSpans); diff != "" { + t.Errorf("wrong spans\n%s", diff) + } +} + +func TestTelemetryInTestsGRPC(t *testing.T) { + ctx := context.Background() + + testResource := resource.NewWithAttributes( + semconv.SchemaURL, + semconv.ServiceNameKey.String("TestTelemetryInTestsGRPC"), + ) + telemetry := initTelemetryForTest(t, + sdktrace.WithResource(testResource), + ) + + client, close := grpcClientForTesting(ctx, t, func(srv *grpc.Server) { + setup := &setupServer{ + initOthers: func(ctx context.Context, cc *terraform1.ClientCapabilities) (*terraform1.ServerCapabilities, error) { + return &terraform1.ServerCapabilities{}, nil + }, + } + terraform1.RegisterSetupServer(srv, setup) + }) + defer close() + setupClient := terraform1.NewSetupClient(client) + + { + ctx, span := otel.Tracer("TestTelemetryInTestsGRPC").Start(ctx, "root") + _, err := setupClient.Handshake(ctx, &terraform1.Handshake_Request{ + Capabilities: &terraform1.ClientCapabilities{}, + }) + if err != nil { + t.Fatal(err) + } + span.End() + } + + clientSpan := findTestTelemetrySpan(t, telemetry, func(ss tracetest.SpanStub) bool { + return ss.SpanKind == trace.SpanKindClient + }) + serverSpan := findTestTelemetrySpan(t, telemetry, func(ss tracetest.SpanStub) bool { + return ss.SpanKind == trace.SpanKindServer + }) + t.Run("client span", func(t *testing.T) { + span := clientSpan + t.Logf("client span: %s", spew.Sdump(span)) + if got, want := span.Name, "terraform1.Setup/Handshake"; got != want { + t.Errorf("wrong name\ngot: %s\nwant: %s", got, want) + } + attrs := otelAttributesMap(span.Attributes) + if got, want := attrs["rpc.system"], "grpc"; got != want { + t.Errorf("wrong rpc.system\ngot: %s\nwant: %s", got, want) + } + if got, want := attrs["rpc.service"], "terraform1.Setup"; got != want { + t.Errorf("wrong rpc.service\ngot: %s\nwant: %s", got, want) + } + if got, want := attrs["rpc.method"], "Handshake"; got != want { + t.Errorf("wrong rpc.method\ngot: %s\nwant: %s", got, want) + } + }) + t.Run("server span", func(t *testing.T) { + span := serverSpan + t.Logf("server span: %s", spew.Sdump(span)) + if got, want := span.Name, "terraform1.Setup/Handshake"; got != want { + t.Errorf("wrong name\ngot: %s\nwant: %s", got, want) + } + if got, want := span.Parent.SpanID(), clientSpan.SpanContext.SpanID(); got != want { + t.Errorf("server span is not a child of the client span\nclient span ID: %s\nserver span parent ID: %s", want, got) + } + if got, want := serverSpan.SpanContext.TraceID(), clientSpan.SpanContext.TraceID(); got != want { + t.Errorf("server span belongs to different trace than client span\nclient trace ID: %s\nserver trace ID: %s", want, got) + } + attrs := otelAttributesMap(span.Attributes) + if got, want := attrs["rpc.system"], "grpc"; got != want { + t.Errorf("wrong rpc.system\ngot: %s\nwant: %s", got, want) + } + if got, want := attrs["rpc.service"], "terraform1.Setup"; got != want { + t.Errorf("wrong rpc.service\ngot: %s\nwant: %s", got, want) + } + if got, want := attrs["rpc.method"], "Handshake"; got != want { + t.Errorf("wrong rpc.method\ngot: %s\nwant: %s", got, want) + } + }) +} + +func otelAttributesMap(kvs []attribute.KeyValue) map[string]any { + ret := make(map[string]any, len(kvs)) + for _, kv := range kvs { + ret[string(kv.Key)] = kv.Value.AsInterface() + } + return ret +}