diff --git a/CHANGELOG.md b/CHANGELOG.md index 4612342d96f..99b8a28907d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,7 +29,6 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - Update `go.opentelemetry.io/proto/otlp` from v1.1.0 to v1.2.0. (#5177) - Improve performance of baggage member character validation in `go.opentelemetry.io/otel/baggage`. (#5214) -- `go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp` no longer depends on `google.golang.org/grpc`. (#5222) ## [1.25.0/0.47.0/0.0.8/0.1.0-alpha] 2024-04-05 diff --git a/exporters/otlp/otlplog/otlploghttp/client.go b/exporters/otlp/otlplog/otlploghttp/client.go index 1ff61d0d662..09a950838c4 100644 --- a/exporters/otlp/otlplog/otlploghttp/client.go +++ b/exporters/otlp/otlplog/otlploghttp/client.go @@ -20,8 +20,8 @@ import ( "google.golang.org/protobuf/proto" "go.opentelemetry.io/otel" - collogpb "go.opentelemetry.io/proto/slim/otlp/collector/logs/v1" - logpb "go.opentelemetry.io/proto/slim/otlp/logs/v1" + collogpb "go.opentelemetry.io/proto/otlp/collector/logs/v1" + logpb "go.opentelemetry.io/proto/otlp/logs/v1" "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp/internal/retry" ) diff --git a/exporters/otlp/otlplog/otlploghttp/client_test.go b/exporters/otlp/otlplog/otlploghttp/client_test.go index 799d4022d58..42473892c54 100644 --- a/exporters/otlp/otlplog/otlploghttp/client_test.go +++ b/exporters/otlp/otlplog/otlploghttp/client_test.go @@ -32,10 +32,10 @@ import ( "google.golang.org/protobuf/proto" "go.opentelemetry.io/otel" - collogpb "go.opentelemetry.io/proto/slim/otlp/collector/logs/v1" - cpb "go.opentelemetry.io/proto/slim/otlp/common/v1" - lpb "go.opentelemetry.io/proto/slim/otlp/logs/v1" - rpb "go.opentelemetry.io/proto/slim/otlp/resource/v1" + collogpb "go.opentelemetry.io/proto/otlp/collector/logs/v1" + cpb "go.opentelemetry.io/proto/otlp/common/v1" + lpb "go.opentelemetry.io/proto/otlp/logs/v1" + rpb "go.opentelemetry.io/proto/otlp/resource/v1" "go.opentelemetry.io/otel/sdk/log" semconv "go.opentelemetry.io/otel/semconv/v1.24.0" diff --git a/exporters/otlp/otlplog/otlploghttp/exporter_test.go b/exporters/otlp/otlplog/otlploghttp/exporter_test.go index 5123298f302..418a3148df1 100644 --- a/exporters/otlp/otlplog/otlploghttp/exporter_test.go +++ b/exporters/otlp/otlplog/otlploghttp/exporter_test.go @@ -15,7 +15,7 @@ import ( "github.com/stretchr/testify/require" "go.opentelemetry.io/otel/sdk/log" - logpb "go.opentelemetry.io/proto/slim/otlp/logs/v1" + logpb "go.opentelemetry.io/proto/otlp/logs/v1" ) func TestExporterExportErrors(t *testing.T) { diff --git a/exporters/otlp/otlplog/otlploghttp/go.mod b/exporters/otlp/otlplog/otlploghttp/go.mod index c629bc4406e..ee4c781a5d3 100644 --- a/exporters/otlp/otlplog/otlploghttp/go.mod +++ b/exporters/otlp/otlplog/otlploghttp/go.mod @@ -11,7 +11,7 @@ require ( go.opentelemetry.io/otel/sdk v1.24.0 go.opentelemetry.io/otel/sdk/log v0.0.0-20240403115316-6c6e1e7416e9 go.opentelemetry.io/otel/trace v1.25.0 - go.opentelemetry.io/proto/slim/otlp v1.2.0 + go.opentelemetry.io/proto/otlp v1.2.0 google.golang.org/protobuf v1.33.0 ) @@ -19,9 +19,17 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/stdr v1.2.2 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 // indirect + github.com/kr/text v0.2.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rogpeppe/go-internal v1.12.0 // indirect go.opentelemetry.io/otel/metric v1.25.0 // indirect + golang.org/x/net v0.23.0 // indirect golang.org/x/sys v0.19.0 // indirect + golang.org/x/text v0.14.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de // indirect + google.golang.org/grpc v1.63.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/exporters/otlp/otlplog/otlploghttp/go.sum b/exporters/otlp/otlplog/otlploghttp/go.sum index 46a5388d5e5..f05d2309362 100644 --- a/exporters/otlp/otlplog/otlploghttp/go.sum +++ b/exporters/otlp/otlplog/otlploghttp/go.sum @@ -1,5 +1,6 @@ github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -9,17 +10,38 @@ 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/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 h1:/c3QmbOGMGTOumP2iT/rCwB7b0QDGLKzqOmktBjT+Is= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1/go.mod h1:5SN9VR2LTsRFsrEC6FHgRbTWrTHu6tqPeKxEQv15giM= +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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -go.opentelemetry.io/proto/slim/otlp v1.2.0 h1:90eMxPHyObsdi/dB1ZP8FP3s3txzxVXjArYqLxPuLZg= -go.opentelemetry.io/proto/slim/otlp v1.2.0/go.mod h1:DeSHUkdUaCemrUs/Nmnsdo8BtM+XmdTEVjYWYFiLQhU= +go.opentelemetry.io/proto/otlp v1.2.0 h1:pVeZGk7nXDC9O2hncA6nHldxEjm6LByfA2aN8IOkz94= +go.opentelemetry.io/proto/otlp v1.2.0/go.mod h1:gGpR8txAl5M03pDhMC79G6SdqNV26naRm/KDsgaHD8A= +golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= +golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de h1:F6qOa9AZTYJXOUEr4jDysRDLrm4PHePlge4v4TGAlxY= +google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:VUhTRKeHn9wwcdrk73nvdC9gF178Tzhmt/qyaFcPLSo= +google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de h1:jFNzHPIeuzhdRwVhbZdiym9q0ory/xY3sA+v2wPg8I0= +google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:5iCWqnniDlqZHrd3neWVTOwvh/v6s3232omMecelax8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de h1:cZGRis4/ot9uVm639a+rHCUaG0JJHEsdyzSQTMX+suY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:H4O17MA/PE9BsGx3w+a+W2VOLLD1Qf7oJneAoU6WktY= +google.golang.org/grpc v1.63.0 h1:WjKe+dnvABXyPJMD7KDNLxtoGk5tgk+YFWN6cBWjZE8= +google.golang.org/grpc v1.63.0/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA= google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/exporters/otlp/otlplog/otlploghttp/internal/transform/attr_test.go b/exporters/otlp/otlplog/otlploghttp/internal/transform/attr_test.go index 86d8bf1d66d..99320b4eae0 100644 --- a/exporters/otlp/otlplog/otlploghttp/internal/transform/attr_test.go +++ b/exporters/otlp/otlplog/otlploghttp/internal/transform/attr_test.go @@ -9,7 +9,7 @@ import ( "github.com/stretchr/testify/assert" "go.opentelemetry.io/otel/attribute" - cpb "go.opentelemetry.io/proto/slim/otlp/common/v1" + cpb "go.opentelemetry.io/proto/otlp/common/v1" ) var ( diff --git a/exporters/otlp/otlplog/otlploghttp/internal/transform/log.go b/exporters/otlp/otlplog/otlploghttp/internal/transform/log.go index 0ad67e1f68e..a59536aa704 100644 --- a/exporters/otlp/otlplog/otlploghttp/internal/transform/log.go +++ b/exporters/otlp/otlplog/otlploghttp/internal/transform/log.go @@ -8,9 +8,9 @@ package transform // import "go.opentelemetry.io/otel/exporters/otlp/otlplog/otl import ( "time" - cpb "go.opentelemetry.io/proto/slim/otlp/common/v1" - lpb "go.opentelemetry.io/proto/slim/otlp/logs/v1" - rpb "go.opentelemetry.io/proto/slim/otlp/resource/v1" + cpb "go.opentelemetry.io/proto/otlp/common/v1" + lpb "go.opentelemetry.io/proto/otlp/logs/v1" + rpb "go.opentelemetry.io/proto/otlp/resource/v1" "go.opentelemetry.io/otel/attribute" api "go.opentelemetry.io/otel/log" diff --git a/exporters/otlp/otlplog/otlploghttp/internal/transform/log_attr_test.go b/exporters/otlp/otlplog/otlploghttp/internal/transform/log_attr_test.go index c3165164b18..570a6927cad 100644 --- a/exporters/otlp/otlplog/otlploghttp/internal/transform/log_attr_test.go +++ b/exporters/otlp/otlplog/otlploghttp/internal/transform/log_attr_test.go @@ -9,7 +9,7 @@ import ( "github.com/stretchr/testify/assert" "go.opentelemetry.io/otel/log" - cpb "go.opentelemetry.io/proto/slim/otlp/common/v1" + cpb "go.opentelemetry.io/proto/otlp/common/v1" ) var ( diff --git a/exporters/otlp/otlplog/otlploghttp/internal/transform/log_test.go b/exporters/otlp/otlplog/otlploghttp/internal/transform/log_test.go index db8063e1587..2ce4557f7e9 100644 --- a/exporters/otlp/otlplog/otlploghttp/internal/transform/log_test.go +++ b/exporters/otlp/otlplog/otlploghttp/internal/transform/log_test.go @@ -9,8 +9,8 @@ import ( "github.com/stretchr/testify/assert" - cpb "go.opentelemetry.io/proto/slim/otlp/common/v1" - lpb "go.opentelemetry.io/proto/slim/otlp/logs/v1" + cpb "go.opentelemetry.io/proto/otlp/common/v1" + lpb "go.opentelemetry.io/proto/otlp/logs/v1" api "go.opentelemetry.io/otel/log" "go.opentelemetry.io/otel/sdk/log" diff --git a/exporters/otlp/otlpmetric/otlpmetricgrpc/internal/gen.go b/exporters/otlp/otlpmetric/otlpmetricgrpc/internal/gen.go index 57d4c7ada52..95e2f4ba3b0 100644 --- a/exporters/otlp/otlpmetric/otlpmetricgrpc/internal/gen.go +++ b/exporters/otlp/otlpmetric/otlpmetricgrpc/internal/gen.go @@ -12,15 +12,20 @@ package internal // import "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/o //go:generate gotmpl --body=../../../../../internal/shared/otlp/envconfig/envconfig.go.tmpl "--data={}" --out=envconfig/envconfig.go //go:generate gotmpl --body=../../../../../internal/shared/otlp/envconfig/envconfig_test.go.tmpl "--data={}" --out=envconfig/envconfig_test.go +//go:generate gotmpl --body=../../../../../internal/shared/otlp/otlpmetric/oconf/envconfig.go.tmpl "--data={\"envconfigImportPath\": \"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc/internal/envconfig\"}" --out=oconf/envconfig.go +//go:generate gotmpl --body=../../../../../internal/shared/otlp/otlpmetric/oconf/envconfig_test.go.tmpl "--data={}" --out=oconf/envconfig_test.go +//go:generate gotmpl --body=../../../../../internal/shared/otlp/otlpmetric/oconf/options.go.tmpl "--data={\"retryImportPath\": \"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc/internal/retry\"}" --out=oconf/options.go +//go:generate gotmpl --body=../../../../../internal/shared/otlp/otlpmetric/oconf/options_test.go.tmpl "--data={\"envconfigImportPath\": \"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc/internal/envconfig\"}" --out=oconf/options_test.go //go:generate gotmpl --body=../../../../../internal/shared/otlp/otlpmetric/oconf/optiontypes.go.tmpl "--data={}" --out=oconf/optiontypes.go //go:generate gotmpl --body=../../../../../internal/shared/otlp/otlpmetric/oconf/tls.go.tmpl "--data={}" --out=oconf/tls.go -//go:generate gotmpl --body=../../../../../internal/shared/otlp/otlpmetric/otest/client.go.tmpl "--data={\"protoImportPrefix\": \"go.opentelemetry.io/proto\"}" --out=otest/client.go -//go:generate gotmpl --body=../../../../../internal/shared/otlp/otlpmetric/otest/client_test.go.tmpl "--data={\"protoImportPrefix\": \"go.opentelemetry.io/proto\", \"internalImportPath\": \"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc/internal\"}" --out=otest/client_test.go +//go:generate gotmpl --body=../../../../../internal/shared/otlp/otlpmetric/otest/client.go.tmpl "--data={}" --out=otest/client.go +//go:generate gotmpl --body=../../../../../internal/shared/otlp/otlpmetric/otest/client_test.go.tmpl "--data={\"internalImportPath\": \"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc/internal\"}" --out=otest/client_test.go +//go:generate gotmpl --body=../../../../../internal/shared/otlp/otlpmetric/otest/collector.go.tmpl "--data={\"oconfImportPath\": \"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc/internal/oconf\"}" --out=otest/collector.go -//go:generate gotmpl --body=../../../../../internal/shared/otlp/otlpmetric/transform/attribute.go.tmpl "--data={\"protoImportPrefix\": \"go.opentelemetry.io/proto\"}" --out=transform/attribute.go -//go:generate gotmpl --body=../../../../../internal/shared/otlp/otlpmetric/transform/attribute_test.go.tmpl "--data={\"protoImportPrefix\": \"go.opentelemetry.io/proto\"}" --out=transform/attribute_test.go -//go:generate gotmpl --body=../../../../../internal/shared/otlp/otlpmetric/transform/error.go.tmpl "--data={\"protoImportPrefix\": \"go.opentelemetry.io/proto\"}" --out=transform/error.go +//go:generate gotmpl --body=../../../../../internal/shared/otlp/otlpmetric/transform/attribute.go.tmpl "--data={}" --out=transform/attribute.go +//go:generate gotmpl --body=../../../../../internal/shared/otlp/otlpmetric/transform/attribute_test.go.tmpl "--data={}" --out=transform/attribute_test.go +//go:generate gotmpl --body=../../../../../internal/shared/otlp/otlpmetric/transform/error.go.tmpl "--data={}" --out=transform/error.go //go:generate gotmpl --body=../../../../../internal/shared/otlp/otlpmetric/transform/error_test.go.tmpl "--data={}" --out=transform/error_test.go -//go:generate gotmpl --body=../../../../../internal/shared/otlp/otlpmetric/transform/metricdata.go.tmpl "--data={\"protoImportPrefix\": \"go.opentelemetry.io/proto\"}" --out=transform/metricdata.go -//go:generate gotmpl --body=../../../../../internal/shared/otlp/otlpmetric/transform/metricdata_test.go.tmpl "--data={\"protoImportPrefix\": \"go.opentelemetry.io/proto\"}" --out=transform/metricdata_test.go +//go:generate gotmpl --body=../../../../../internal/shared/otlp/otlpmetric/transform/metricdata.go.tmpl "--data={}" --out=transform/metricdata.go +//go:generate gotmpl --body=../../../../../internal/shared/otlp/otlpmetric/transform/metricdata_test.go.tmpl "--data={}" --out=transform/metricdata_test.go diff --git a/exporters/otlp/otlpmetric/otlpmetricgrpc/internal/oconf/envconfig.go b/exporters/otlp/otlpmetric/otlpmetricgrpc/internal/oconf/envconfig.go index 860c806b462..7ae53f2d181 100644 --- a/exporters/otlp/otlpmetric/otlpmetricgrpc/internal/oconf/envconfig.go +++ b/exporters/otlp/otlpmetric/otlpmetricgrpc/internal/oconf/envconfig.go @@ -1,3 +1,6 @@ +// Code created by gotmpl. DO NOT MODIFY. +// source: internal/shared/otlp/otlpmetric/oconf/envconfig.go.tmpl + // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 @@ -34,18 +37,46 @@ func ApplyGRPCEnvConfigs(cfg Config) Config { return cfg } -func getOptionsFromEnv() []GRPCOption { - opts := []GRPCOption{} +// ApplyHTTPEnvConfigs applies the env configurations for HTTP. +func ApplyHTTPEnvConfigs(cfg Config) Config { + opts := getOptionsFromEnv() + for _, opt := range opts { + cfg = opt.ApplyHTTPOption(cfg) + } + return cfg +} + +func getOptionsFromEnv() []GenericOption { + opts := []GenericOption{} tlsConf := &tls.Config{} DefaultEnvOptionsReader.Apply( envconfig.WithURL("ENDPOINT", func(u *url.URL) { opts = append(opts, withEndpointScheme(u)) - opts = append(opts, NewGRPCOption(withEndpointForGRPC(u))) + opts = append(opts, newSplitOption(func(cfg Config) Config { + cfg.Metrics.Endpoint = u.Host + // For OTLP/HTTP endpoint URLs without a per-signal + // configuration, the passed endpoint is used as a base URL + // and the signals are sent to these paths relative to that. + cfg.Metrics.URLPath = path.Join(u.Path, DefaultMetricsPath) + return cfg + }, withEndpointForGRPC(u))) }), envconfig.WithURL("METRICS_ENDPOINT", func(u *url.URL) { opts = append(opts, withEndpointScheme(u)) - opts = append(opts, NewGRPCOption(withEndpointForGRPC(u))) + opts = append(opts, newSplitOption(func(cfg Config) Config { + cfg.Metrics.Endpoint = u.Host + // For endpoint URLs for OTLP/HTTP per-signal variables, the + // URL MUST be used as-is without any modification. The only + // exception is that if an URL contains no path part, the root + // path / MUST be used. + path := u.Path + if path == "" { + path = "/" + } + cfg.Metrics.URLPath = path + return cfg + }, withEndpointForGRPC(u))) }), envconfig.WithCertPool("CERTIFICATE", func(p *x509.CertPool) { tlsConf.RootCAs = p }), envconfig.WithCertPool("METRICS_CERTIFICATE", func(p *x509.CertPool) { tlsConf.RootCAs = p }), @@ -90,7 +121,7 @@ func WithEnvCompression(n string, fn func(Compression)) func(e *envconfig.EnvOpt } } -func withEndpointScheme(u *url.URL) GRPCOption { +func withEndpointScheme(u *url.URL) GenericOption { switch strings.ToLower(u.Scheme) { case "http", "unix": return WithInsecure() @@ -100,7 +131,7 @@ func withEndpointScheme(u *url.URL) GRPCOption { } // revive:disable-next-line:flag-parameter -func withInsecure(b bool) GRPCOption { +func withInsecure(b bool) GenericOption { if b { return WithInsecure() } diff --git a/exporters/otlp/otlpmetric/otlpmetricgrpc/internal/oconf/envconfig_test.go b/exporters/otlp/otlpmetric/otlpmetricgrpc/internal/oconf/envconfig_test.go index ee90ff821b1..5c621d7b47e 100644 --- a/exporters/otlp/otlpmetric/otlpmetricgrpc/internal/oconf/envconfig_test.go +++ b/exporters/otlp/otlpmetric/otlpmetricgrpc/internal/oconf/envconfig_test.go @@ -1,3 +1,6 @@ +// Code created by gotmpl. DO NOT MODIFY. +// source: internal/shared/otlp/otlpmetric/oconf/envconfig_test.go.tmpl + // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 diff --git a/exporters/otlp/otlpmetric/otlpmetricgrpc/internal/oconf/options.go b/exporters/otlp/otlpmetric/otlpmetricgrpc/internal/oconf/options.go index 3c17f44190f..b6ed9a2bb65 100644 --- a/exporters/otlp/otlpmetric/otlpmetricgrpc/internal/oconf/options.go +++ b/exporters/otlp/otlpmetric/otlpmetricgrpc/internal/oconf/options.go @@ -1,3 +1,6 @@ +// Code created by gotmpl. DO NOT MODIFY. +// source: internal/shared/otlp/otlpmetric/oconf/options.go.tmpl + // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 @@ -44,7 +47,6 @@ type ( // This type is compatible with `http.Transport.Proxy` and can be used to set a custom proxy function to the OTLP HTTP client. HTTPTransportProxyFunc func(*http.Request) (*url.URL, error) - // SignalConfig represents signal specific configuration. SignalConfig struct { Endpoint string Insecure bool @@ -63,8 +65,8 @@ type ( Proxy HTTPTransportProxyFunc } - // Config represents exporter configuration. Config struct { + // Signal specific configurations Metrics SignalConfig RetryConfig retry.Config @@ -77,6 +79,29 @@ type ( } ) +// NewHTTPConfig returns a new Config with all settings applied from opts and +// any unset setting using the default HTTP config values. +func NewHTTPConfig(opts ...HTTPOption) Config { + cfg := Config{ + Metrics: SignalConfig{ + Endpoint: fmt.Sprintf("%s:%d", DefaultCollectorHost, DefaultCollectorHTTPPort), + URLPath: DefaultMetricsPath, + Compression: NoCompression, + Timeout: DefaultTimeout, + + TemporalitySelector: metric.DefaultTemporalitySelector, + AggregationSelector: metric.DefaultAggregationSelector, + }, + RetryConfig: retry.DefaultConfig, + } + cfg = ApplyHTTPEnvConfigs(cfg) + for _, opt := range opts { + cfg = opt.ApplyHTTPOption(cfg) + } + cfg.Metrics.URLPath = cleanPath(cfg.Metrics.URLPath, DefaultMetricsPath) + return cfg +} + // cleanPath returns a path with all spaces trimmed and all redundancies // removed. If urlPath is empty or cleaning it results in an empty string, // defaultPath is returned instead. @@ -139,14 +164,93 @@ func NewGRPCConfig(opts ...GRPCOption) Config { return cfg } -// GRPCOption applies an option to the gRPC driver. -type GRPCOption interface { - ApplyGRPCOption(Config) Config +type ( + // GenericOption applies an option to the HTTP or gRPC driver. + GenericOption interface { + ApplyHTTPOption(Config) Config + ApplyGRPCOption(Config) Config + + // A private method to prevent users implementing the + // interface and so future additions to it will not + // violate compatibility. + private() + } + + // HTTPOption applies an option to the HTTP driver. + HTTPOption interface { + ApplyHTTPOption(Config) Config - // A private method to prevent users implementing the - // interface and so future additions to it will not - // violate compatibility. - private() + // A private method to prevent users implementing the + // interface and so future additions to it will not + // violate compatibility. + private() + } + + // GRPCOption applies an option to the gRPC driver. + GRPCOption interface { + ApplyGRPCOption(Config) Config + + // A private method to prevent users implementing the + // interface and so future additions to it will not + // violate compatibility. + private() + } +) + +// genericOption is an option that applies the same logic +// for both gRPC and HTTP. +type genericOption struct { + fn func(Config) Config +} + +func (g *genericOption) ApplyGRPCOption(cfg Config) Config { + return g.fn(cfg) +} + +func (g *genericOption) ApplyHTTPOption(cfg Config) Config { + return g.fn(cfg) +} + +func (genericOption) private() {} + +func newGenericOption(fn func(cfg Config) Config) GenericOption { + return &genericOption{fn: fn} +} + +// splitOption is an option that applies different logics +// for gRPC and HTTP. +type splitOption struct { + httpFn func(Config) Config + grpcFn func(Config) Config +} + +func (g *splitOption) ApplyGRPCOption(cfg Config) Config { + return g.grpcFn(cfg) +} + +func (g *splitOption) ApplyHTTPOption(cfg Config) Config { + return g.httpFn(cfg) +} + +func (splitOption) private() {} + +func newSplitOption(httpFn func(cfg Config) Config, grpcFn func(cfg Config) Config) GenericOption { + return &splitOption{httpFn: httpFn, grpcFn: grpcFn} +} + +// httpOption is an option that is only applied to the HTTP driver. +type httpOption struct { + fn func(Config) Config +} + +func (h *httpOption) ApplyHTTPOption(cfg Config) Config { + return h.fn(cfg) +} + +func (httpOption) private() {} + +func NewHTTPOption(fn func(cfg Config) Config) HTTPOption { + return &httpOption{fn: fn} } // grpcOption is an option that is only applied to the gRPC driver. @@ -166,15 +270,15 @@ func NewGRPCOption(fn func(cfg Config) Config) GRPCOption { // Generic Options -func WithEndpoint(endpoint string) GRPCOption { - return NewGRPCOption(func(cfg Config) Config { +func WithEndpoint(endpoint string) GenericOption { + return newGenericOption(func(cfg Config) Config { cfg.Metrics.Endpoint = endpoint return cfg }) } -func WithEndpointURL(v string) GRPCOption { - return NewGRPCOption(func(cfg Config) Config { +func WithEndpointURL(v string) GenericOption { + return newGenericOption(func(cfg Config) Config { u, err := url.Parse(v) if err != nil { global.Error(err, "otlpmetric: parse endpoint url", "url", v) @@ -191,78 +295,81 @@ func WithEndpointURL(v string) GRPCOption { }) } -func WithCompression(compression Compression) GRPCOption { - return NewGRPCOption(func(cfg Config) Config { +func WithCompression(compression Compression) GenericOption { + return newGenericOption(func(cfg Config) Config { cfg.Metrics.Compression = compression return cfg }) } -func WithURLPath(urlPath string) GRPCOption { - return NewGRPCOption(func(cfg Config) Config { +func WithURLPath(urlPath string) GenericOption { + return newGenericOption(func(cfg Config) Config { cfg.Metrics.URLPath = urlPath return cfg }) } -func WithRetry(rc retry.Config) GRPCOption { - return NewGRPCOption(func(cfg Config) Config { +func WithRetry(rc retry.Config) GenericOption { + return newGenericOption(func(cfg Config) Config { cfg.RetryConfig = rc return cfg }) } -func WithTLSClientConfig(tlsCfg *tls.Config) GRPCOption { - return NewGRPCOption(func(cfg Config) Config { +func WithTLSClientConfig(tlsCfg *tls.Config) GenericOption { + return newSplitOption(func(cfg Config) Config { + cfg.Metrics.TLSCfg = tlsCfg.Clone() + return cfg + }, func(cfg Config) Config { cfg.Metrics.GRPCCredentials = credentials.NewTLS(tlsCfg) return cfg }) } -func WithInsecure() GRPCOption { - return NewGRPCOption(func(cfg Config) Config { +func WithInsecure() GenericOption { + return newGenericOption(func(cfg Config) Config { cfg.Metrics.Insecure = true return cfg }) } -func WithSecure() GRPCOption { - return NewGRPCOption(func(cfg Config) Config { +func WithSecure() GenericOption { + return newGenericOption(func(cfg Config) Config { cfg.Metrics.Insecure = false return cfg }) } -func WithHeaders(headers map[string]string) GRPCOption { - return NewGRPCOption(func(cfg Config) Config { +func WithHeaders(headers map[string]string) GenericOption { + return newGenericOption(func(cfg Config) Config { cfg.Metrics.Headers = headers return cfg }) } -func WithTimeout(duration time.Duration) GRPCOption { - return NewGRPCOption(func(cfg Config) Config { +func WithTimeout(duration time.Duration) GenericOption { + return newGenericOption(func(cfg Config) Config { cfg.Metrics.Timeout = duration return cfg }) } -func WithTemporalitySelector(selector metric.TemporalitySelector) GRPCOption { - return NewGRPCOption(func(cfg Config) Config { +func WithTemporalitySelector(selector metric.TemporalitySelector) GenericOption { + return newGenericOption(func(cfg Config) Config { cfg.Metrics.TemporalitySelector = selector return cfg }) } -func WithAggregationSelector(selector metric.AggregationSelector) GRPCOption { - return NewGRPCOption(func(cfg Config) Config { +func WithAggregationSelector(selector metric.AggregationSelector) GenericOption { + return newGenericOption(func(cfg Config) Config { cfg.Metrics.AggregationSelector = selector return cfg }) } -func WithProxy(pf HTTPTransportProxyFunc) GRPCOption { - return NewGRPCOption(func(cfg Config) Config { +func WithProxy(pf HTTPTransportProxyFunc) GenericOption { + return newGenericOption(func(cfg Config) Config { cfg.Metrics.Proxy = pf return cfg }) diff --git a/exporters/otlp/otlpmetric/otlpmetricgrpc/internal/oconf/options_test.go b/exporters/otlp/otlpmetric/otlpmetricgrpc/internal/oconf/options_test.go index 9b701950be8..a24a9544556 100644 --- a/exporters/otlp/otlpmetric/otlpmetricgrpc/internal/oconf/options_test.go +++ b/exporters/otlp/otlpmetric/otlpmetricgrpc/internal/oconf/options_test.go @@ -1,3 +1,6 @@ +// Code created by gotmpl. DO NOT MODIFY. +// source: internal/shared/otlp/otlpmetric/oconf/options_test.go.tmpl + // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 @@ -61,15 +64,19 @@ func TestConfigs(t *testing.T) { tests := []struct { name string - opts []GRPCOption + opts []GenericOption env env fileReader fileReader - asserts func(t *testing.T, c *Config) + asserts func(t *testing.T, c *Config, grpcOption bool) }{ { name: "Test default configs", - asserts: func(t *testing.T, c *Config) { - assert.Equal(t, "localhost:4317", c.Metrics.Endpoint) + asserts: func(t *testing.T, c *Config, grpcOption bool) { + if grpcOption { + assert.Equal(t, "localhost:4317", c.Metrics.Endpoint) + } else { + assert.Equal(t, "localhost:4318", c.Metrics.Endpoint) + } assert.Equal(t, NoCompression, c.Metrics.Compression) assert.Equal(t, map[string]string(nil), c.Metrics.Headers) assert.Equal(t, 10*time.Second, c.Metrics.Timeout) @@ -79,19 +86,19 @@ func TestConfigs(t *testing.T) { // Endpoint Tests { name: "Test With Endpoint", - opts: []GRPCOption{ + opts: []GenericOption{ WithEndpoint("someendpoint"), }, - asserts: func(t *testing.T, c *Config) { + asserts: func(t *testing.T, c *Config, grpcOption bool) { assert.Equal(t, "someendpoint", c.Metrics.Endpoint) }, }, { name: "Test With Endpoint URL", - opts: []GRPCOption{ + opts: []GenericOption{ WithEndpointURL("http://someendpoint/somepath"), }, - asserts: func(t *testing.T, c *Config) { + asserts: func(t *testing.T, c *Config, grpcOption bool) { assert.Equal(t, "someendpoint", c.Metrics.Endpoint) assert.Equal(t, "/somepath", c.Metrics.URLPath) assert.Equal(t, true, c.Metrics.Insecure) @@ -99,10 +106,10 @@ func TestConfigs(t *testing.T) { }, { name: "Test With Secure Endpoint URL", - opts: []GRPCOption{ + opts: []GenericOption{ WithEndpointURL("https://someendpoint/somepath"), }, - asserts: func(t *testing.T, c *Config) { + asserts: func(t *testing.T, c *Config, grpcOption bool) { assert.Equal(t, "someendpoint", c.Metrics.Endpoint) assert.Equal(t, "/somepath", c.Metrics.URLPath) assert.Equal(t, false, c.Metrics.Insecure) @@ -110,11 +117,15 @@ func TestConfigs(t *testing.T) { }, { name: "Test With Invalid Endpoint URL", - opts: []GRPCOption{ + opts: []GenericOption{ WithEndpointURL("%invalid"), }, - asserts: func(t *testing.T, c *Config) { - assert.Equal(t, "localhost:4317", c.Metrics.Endpoint) + asserts: func(t *testing.T, c *Config, grpcOption bool) { + if grpcOption { + assert.Equal(t, "localhost:4317", c.Metrics.Endpoint) + } else { + assert.Equal(t, "localhost:4318", c.Metrics.Endpoint) + } assert.Equal(t, "/v1/metrics", c.Metrics.URLPath) }, }, @@ -123,9 +134,14 @@ func TestConfigs(t *testing.T) { env: map[string]string{ "OTEL_EXPORTER_OTLP_ENDPOINT": "https://env.endpoint/prefix", }, - asserts: func(t *testing.T, c *Config) { + asserts: func(t *testing.T, c *Config, grpcOption bool) { assert.False(t, c.Metrics.Insecure) - assert.Equal(t, "env.endpoint/prefix", c.Metrics.Endpoint) + if grpcOption { + assert.Equal(t, "env.endpoint/prefix", c.Metrics.Endpoint) + } else { + assert.Equal(t, "env.endpoint", c.Metrics.Endpoint) + assert.Equal(t, "/prefix/v1/metrics", c.Metrics.URLPath) + } }, }, { @@ -134,20 +150,23 @@ func TestConfigs(t *testing.T) { "OTEL_EXPORTER_OTLP_ENDPOINT": "https://overrode.by.signal.specific/env/var", "OTEL_EXPORTER_OTLP_METRICS_ENDPOINT": "http://env.metrics.endpoint", }, - asserts: func(t *testing.T, c *Config) { + asserts: func(t *testing.T, c *Config, grpcOption bool) { assert.True(t, c.Metrics.Insecure) assert.Equal(t, "env.metrics.endpoint", c.Metrics.Endpoint) + if !grpcOption { + assert.Equal(t, "/", c.Metrics.URLPath) + } }, }, { name: "Test Mixed Environment and With Endpoint", - opts: []GRPCOption{ + opts: []GenericOption{ WithEndpoint("metrics_endpoint"), }, env: map[string]string{ "OTEL_EXPORTER_OTLP_ENDPOINT": "env_endpoint", }, - asserts: func(t *testing.T, c *Config) { + asserts: func(t *testing.T, c *Config, grpcOption bool) { assert.Equal(t, "metrics_endpoint", c.Metrics.Endpoint) }, }, @@ -156,7 +175,7 @@ func TestConfigs(t *testing.T) { env: map[string]string{ "OTEL_EXPORTER_OTLP_ENDPOINT": "http://env_endpoint", }, - asserts: func(t *testing.T, c *Config) { + asserts: func(t *testing.T, c *Config, grpcOption bool) { assert.Equal(t, "env_endpoint", c.Metrics.Endpoint) assert.Equal(t, true, c.Metrics.Insecure) }, @@ -166,7 +185,7 @@ func TestConfigs(t *testing.T) { env: map[string]string{ "OTEL_EXPORTER_OTLP_ENDPOINT": " http://env_endpoint ", }, - asserts: func(t *testing.T, c *Config) { + asserts: func(t *testing.T, c *Config, grpcOption bool) { assert.Equal(t, "env_endpoint", c.Metrics.Endpoint) assert.Equal(t, true, c.Metrics.Insecure) }, @@ -176,7 +195,7 @@ func TestConfigs(t *testing.T) { env: map[string]string{ "OTEL_EXPORTER_OTLP_ENDPOINT": "https://env_endpoint", }, - asserts: func(t *testing.T, c *Config) { + asserts: func(t *testing.T, c *Config, grpcOption bool) { assert.Equal(t, "env_endpoint", c.Metrics.Endpoint) assert.Equal(t, false, c.Metrics.Insecure) }, @@ -187,7 +206,7 @@ func TestConfigs(t *testing.T) { "OTEL_EXPORTER_OTLP_ENDPOINT": "HTTPS://overrode_by_signal_specific", "OTEL_EXPORTER_OTLP_METRICS_ENDPOINT": "HtTp://env_metrics_endpoint", }, - asserts: func(t *testing.T, c *Config) { + asserts: func(t *testing.T, c *Config, grpcOption bool) { assert.Equal(t, "env_metrics_endpoint", c.Metrics.Endpoint) assert.Equal(t, true, c.Metrics.Insecure) }, @@ -196,18 +215,27 @@ func TestConfigs(t *testing.T) { // Certificate tests { name: "Test Default Certificate", - asserts: func(t *testing.T, c *Config) { - assert.NotNil(t, c.Metrics.GRPCCredentials) + asserts: func(t *testing.T, c *Config, grpcOption bool) { + if grpcOption { + assert.NotNil(t, c.Metrics.GRPCCredentials) + } else { + assert.Nil(t, c.Metrics.TLSCfg) + } }, }, { name: "Test With Certificate", - opts: []GRPCOption{ + opts: []GenericOption{ WithTLSClientConfig(tlsCert), }, - asserts: func(t *testing.T, c *Config) { - // TODO: make sure gRPC's credentials actually works - assert.NotNil(t, c.Metrics.GRPCCredentials) + asserts: func(t *testing.T, c *Config, grpcOption bool) { + if grpcOption { + // TODO: make sure gRPC's credentials actually works + assert.NotNil(t, c.Metrics.GRPCCredentials) + } else { + // nolint:staticcheck // ignoring tlsCert.RootCAs.Subjects is deprecated ERR because cert does not come from SystemCertPool. + assert.Equal(t, tlsCert.RootCAs.Subjects(), c.Metrics.TLSCfg.RootCAs.Subjects()) + } }, }, { @@ -218,8 +246,13 @@ func TestConfigs(t *testing.T) { fileReader: fileReader{ "cert_path": []byte(WeakCertificate), }, - asserts: func(t *testing.T, c *Config) { - assert.NotNil(t, c.Metrics.GRPCCredentials) + asserts: func(t *testing.T, c *Config, grpcOption bool) { + if grpcOption { + assert.NotNil(t, c.Metrics.GRPCCredentials) + } else { + // nolint:staticcheck // ignoring tlsCert.RootCAs.Subjects is deprecated ERR because cert does not come from SystemCertPool. + assert.Equal(t, tlsCert.RootCAs.Subjects(), c.Metrics.TLSCfg.RootCAs.Subjects()) + } }, }, { @@ -232,38 +265,48 @@ func TestConfigs(t *testing.T) { "cert_path": []byte(WeakCertificate), "invalid_cert": []byte("invalid certificate file."), }, - asserts: func(t *testing.T, c *Config) { - assert.NotNil(t, c.Metrics.GRPCCredentials) + asserts: func(t *testing.T, c *Config, grpcOption bool) { + if grpcOption { + assert.NotNil(t, c.Metrics.GRPCCredentials) + } else { + // nolint:staticcheck // ignoring tlsCert.RootCAs.Subjects is deprecated ERR because cert does not come from SystemCertPool. + assert.Equal(t, tlsCert.RootCAs.Subjects(), c.Metrics.TLSCfg.RootCAs.Subjects()) + } }, }, { name: "Test Mixed Environment and With Certificate", - opts: []GRPCOption{}, + opts: []GenericOption{}, env: map[string]string{ "OTEL_EXPORTER_OTLP_CERTIFICATE": "cert_path", }, fileReader: fileReader{ "cert_path": []byte(WeakCertificate), }, - asserts: func(t *testing.T, c *Config) { - assert.NotNil(t, c.Metrics.GRPCCredentials) + asserts: func(t *testing.T, c *Config, grpcOption bool) { + if grpcOption { + assert.NotNil(t, c.Metrics.GRPCCredentials) + } else { + // nolint:staticcheck // ignoring tlsCert.RootCAs.Subjects is deprecated ERR because cert does not come from SystemCertPool. + assert.Equal(t, 1, len(c.Metrics.TLSCfg.RootCAs.Subjects())) + } }, }, // Headers tests { name: "Test With Headers", - opts: []GRPCOption{ + opts: []GenericOption{ WithHeaders(map[string]string{"h1": "v1"}), }, - asserts: func(t *testing.T, c *Config) { + asserts: func(t *testing.T, c *Config, grpcOption bool) { assert.Equal(t, map[string]string{"h1": "v1"}, c.Metrics.Headers) }, }, { name: "Test Environment Headers", env: map[string]string{"OTEL_EXPORTER_OTLP_HEADERS": "h1=v1,h2=v2"}, - asserts: func(t *testing.T, c *Config) { + asserts: func(t *testing.T, c *Config, grpcOption bool) { assert.Equal(t, map[string]string{"h1": "v1", "h2": "v2"}, c.Metrics.Headers) }, }, @@ -273,17 +316,17 @@ func TestConfigs(t *testing.T) { "OTEL_EXPORTER_OTLP_HEADERS": "overrode_by_signal_specific", "OTEL_EXPORTER_OTLP_METRICS_HEADERS": "h1=v1,h2=v2", }, - asserts: func(t *testing.T, c *Config) { + asserts: func(t *testing.T, c *Config, grpcOption bool) { assert.Equal(t, map[string]string{"h1": "v1", "h2": "v2"}, c.Metrics.Headers) }, }, { name: "Test Mixed Environment and With Headers", env: map[string]string{"OTEL_EXPORTER_OTLP_HEADERS": "h1=v1,h2=v2"}, - opts: []GRPCOption{ + opts: []GenericOption{ WithHeaders(map[string]string{"m1": "mv1"}), }, - asserts: func(t *testing.T, c *Config) { + asserts: func(t *testing.T, c *Config, grpcOption bool) { assert.Equal(t, map[string]string{"m1": "mv1"}, c.Metrics.Headers) }, }, @@ -291,10 +334,10 @@ func TestConfigs(t *testing.T) { // Compression Tests { name: "Test With Compression", - opts: []GRPCOption{ + opts: []GenericOption{ WithCompression(GzipCompression), }, - asserts: func(t *testing.T, c *Config) { + asserts: func(t *testing.T, c *Config, grpcOption bool) { assert.Equal(t, GzipCompression, c.Metrics.Compression) }, }, @@ -303,7 +346,7 @@ func TestConfigs(t *testing.T) { env: map[string]string{ "OTEL_EXPORTER_OTLP_COMPRESSION": "gzip", }, - asserts: func(t *testing.T, c *Config) { + asserts: func(t *testing.T, c *Config, grpcOption bool) { assert.Equal(t, GzipCompression, c.Metrics.Compression) }, }, @@ -312,19 +355,19 @@ func TestConfigs(t *testing.T) { env: map[string]string{ "OTEL_EXPORTER_OTLP_METRICS_COMPRESSION": "gzip", }, - asserts: func(t *testing.T, c *Config) { + asserts: func(t *testing.T, c *Config, grpcOption bool) { assert.Equal(t, GzipCompression, c.Metrics.Compression) }, }, { name: "Test Mixed Environment and With Compression", - opts: []GRPCOption{ + opts: []GenericOption{ WithCompression(NoCompression), }, env: map[string]string{ "OTEL_EXPORTER_OTLP_METRICS_COMPRESSION": "gzip", }, - asserts: func(t *testing.T, c *Config) { + asserts: func(t *testing.T, c *Config, grpcOption bool) { assert.Equal(t, NoCompression, c.Metrics.Compression) }, }, @@ -332,10 +375,10 @@ func TestConfigs(t *testing.T) { // Timeout Tests { name: "Test With Timeout", - opts: []GRPCOption{ + opts: []GenericOption{ WithTimeout(time.Duration(5 * time.Second)), }, - asserts: func(t *testing.T, c *Config) { + asserts: func(t *testing.T, c *Config, grpcOption bool) { assert.Equal(t, 5*time.Second, c.Metrics.Timeout) }, }, @@ -344,7 +387,7 @@ func TestConfigs(t *testing.T) { env: map[string]string{ "OTEL_EXPORTER_OTLP_TIMEOUT": "15000", }, - asserts: func(t *testing.T, c *Config) { + asserts: func(t *testing.T, c *Config, grpcOption bool) { assert.Equal(t, c.Metrics.Timeout, 15*time.Second) }, }, @@ -354,7 +397,7 @@ func TestConfigs(t *testing.T) { "OTEL_EXPORTER_OTLP_TIMEOUT": "15000", "OTEL_EXPORTER_OTLP_METRICS_TIMEOUT": "28000", }, - asserts: func(t *testing.T, c *Config) { + asserts: func(t *testing.T, c *Config, grpcOption bool) { assert.Equal(t, c.Metrics.Timeout, 28*time.Second) }, }, @@ -364,10 +407,10 @@ func TestConfigs(t *testing.T) { "OTEL_EXPORTER_OTLP_TIMEOUT": "15000", "OTEL_EXPORTER_OTLP_METRICS_TIMEOUT": "28000", }, - opts: []GRPCOption{ + opts: []GenericOption{ WithTimeout(5 * time.Second), }, - asserts: func(t *testing.T, c *Config) { + asserts: func(t *testing.T, c *Config, grpcOption bool) { assert.Equal(t, c.Metrics.Timeout, 5*time.Second) }, }, @@ -375,10 +418,10 @@ func TestConfigs(t *testing.T) { // Temporality Selector Tests { name: "WithTemporalitySelector", - opts: []GRPCOption{ + opts: []GenericOption{ WithTemporalitySelector(deltaSelector), }, - asserts: func(t *testing.T, c *Config) { + asserts: func(t *testing.T, c *Config, grpcOption bool) { // Function value comparisons are disallowed, test non-default // behavior of a TemporalitySelector here to ensure our "catch // all" was set. @@ -391,10 +434,10 @@ func TestConfigs(t *testing.T) { // Aggregation Selector Tests { name: "WithAggregationSelector", - opts: []GRPCOption{ + opts: []GenericOption{ WithAggregationSelector(dropSelector), }, - asserts: func(t *testing.T, c *Config) { + asserts: func(t *testing.T, c *Config, grpcOption bool) { // Function value comparisons are disallowed, test non-default // behavior of a AggregationSelector here to ensure our "catch // all" was set. @@ -407,12 +450,12 @@ func TestConfigs(t *testing.T) { // Proxy Tests { name: "Test With Proxy", - opts: []GRPCOption{ + opts: []GenericOption{ WithProxy(func(r *http.Request) (*url.URL, error) { return url.Parse("http://proxy.com") }), }, - asserts: func(t *testing.T, c *Config) { + asserts: func(t *testing.T, c *Config, grpcOption bool) { assert.NotNil(t, c.Metrics.Proxy) proxyURL, err := c.Metrics.Proxy(&http.Request{}) assert.NoError(t, err) @@ -421,8 +464,8 @@ func TestConfigs(t *testing.T) { }, { name: "Test Without Proxy", - opts: []GRPCOption{}, - asserts: func(t *testing.T, c *Config) { + opts: []GenericOption{}, + asserts: func(t *testing.T, c *Config, grpcOption bool) { assert.Nil(t, c.Metrics.Proxy) }, }, @@ -438,9 +481,13 @@ func TestConfigs(t *testing.T) { } t.Cleanup(func() { DefaultEnvOptionsReader = origEOR }) + // Tests Generic options as HTTP Options + cfg := NewHTTPConfig(asHTTPOptions(tt.opts)...) + tt.asserts(t, &cfg, false) + // Tests Generic options as gRPC Options - cfg := NewGRPCConfig(tt.opts...) - tt.asserts(t, &cfg) + cfg = NewGRPCConfig(asGRPCOptions(tt.opts)...) + tt.asserts(t, &cfg, true) }) } } @@ -453,6 +500,22 @@ func deltaSelector(metric.InstrumentKind) metricdata.Temporality { return metricdata.DeltaTemporality } +func asHTTPOptions(opts []GenericOption) []HTTPOption { + converted := make([]HTTPOption, len(opts)) + for i, o := range opts { + converted[i] = NewHTTPOption(o.ApplyHTTPOption) + } + return converted +} + +func asGRPCOptions(opts []GenericOption) []GRPCOption { + converted := make([]GRPCOption, len(opts)) + for i, o := range opts { + converted[i] = NewGRPCOption(o.ApplyGRPCOption) + } + return converted +} + func TestCleanPath(t *testing.T) { type args struct { urlPath string diff --git a/exporters/otlp/otlpmetric/otlpmetricgrpc/internal/otest/collector.go b/exporters/otlp/otlpmetric/otlpmetricgrpc/internal/otest/collector.go index 870ba5d685b..6eea8d39a75 100644 --- a/exporters/otlp/otlpmetric/otlpmetricgrpc/internal/otest/collector.go +++ b/exporters/otlp/otlpmetric/otlpmetricgrpc/internal/otest/collector.go @@ -1,16 +1,37 @@ +// Code created by gotmpl. DO NOT MODIFY. +// source: internal/shared/otlp/otlpmetric/otest/collector.go.tmpl + // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 package otest // import "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc/internal/otest" import ( - "context" // nolint:depguard // This is for testing. + "bytes" + "compress/gzip" + "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" // nolint:depguard // This is for testing. + "encoding/pem" + "errors" + "fmt" + "io" + "math/big" "net" + "net/http" + "net/url" "sync" + "time" "google.golang.org/grpc" "google.golang.org/grpc/metadata" + "google.golang.org/protobuf/proto" + "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc/internal/oconf" collpb "go.opentelemetry.io/proto/otlp/collector/metrics/v1" mpb "go.opentelemetry.io/proto/otlp/metrics/v1" ) @@ -20,7 +41,6 @@ type Collector interface { Collect() *Storage } -// ExportResult represents an export response. type ExportResult struct { Response *collpb.ExportMetricsServiceResponse Err error @@ -140,3 +160,292 @@ func (c *GRPCCollector) Export(ctx context.Context, req *collpb.ExportMetricsSer } return &collpb.ExportMetricsServiceResponse{}, nil } + +var emptyExportMetricsServiceResponse = func() []byte { + body := collpb.ExportMetricsServiceResponse{} + r, err := proto.Marshal(&body) + if err != nil { + panic(err) + } + return r +}() + +type HTTPResponseError struct { + Err error + Status int + Header http.Header +} + +func (e *HTTPResponseError) Error() string { + return fmt.Sprintf("%d: %s", e.Status, e.Err) +} + +func (e *HTTPResponseError) Unwrap() error { return e.Err } + +// HTTPCollector is an OTLP HTTP server that collects all requests it receives. +type HTTPCollector struct { + plainTextResponse bool + + headersMu sync.Mutex + headers http.Header + storage *Storage + + resultCh <-chan ExportResult + listener net.Listener + srv *http.Server +} + +// NewHTTPCollector returns a *HTTPCollector that is listening at the provided +// endpoint. +// +// If endpoint is an empty string, the returned collector will be listening on +// the localhost interface at an OS chosen port, not use TLS, and listen at the +// default OTLP metric endpoint path ("/v1/metrics"). If the endpoint contains +// a prefix of "https" the server will generate weak self-signed TLS +// certificates and use them to server data. If the endpoint contains a path, +// that path will be used instead of the default OTLP metric endpoint path. +// +// If errCh is not nil, the collector will respond to HTTP requests with errors +// sent on that channel. This means that if errCh is not nil Export calls will +// block until an error is received. +func NewHTTPCollector(endpoint string, resultCh <-chan ExportResult, opts ...func(*HTTPCollector)) (*HTTPCollector, error) { + u, err := url.Parse(endpoint) + if err != nil { + return nil, err + } + if u.Host == "" { + u.Host = "localhost:0" + } + if u.Path == "" { + u.Path = oconf.DefaultMetricsPath + } + + c := &HTTPCollector{ + headers: http.Header{}, + storage: NewStorage(), + resultCh: resultCh, + } + for _, opt := range opts { + opt(c) + } + + c.listener, err = net.Listen("tcp", u.Host) + if err != nil { + return nil, err + } + + mux := http.NewServeMux() + mux.Handle(u.Path, http.HandlerFunc(c.handler)) + c.srv = &http.Server{ + Handler: mux, + ReadTimeout: 10 * time.Second, + WriteTimeout: 10 * time.Second, + } + if u.Scheme == "https" { + cert, err := weakCertificate() + if err != nil { + return nil, err + } + c.srv.TLSConfig = &tls.Config{ + Certificates: []tls.Certificate{cert}, + } + go func() { _ = c.srv.ServeTLS(c.listener, "", "") }() + } else { + go func() { _ = c.srv.Serve(c.listener) }() + } + return c, nil +} + +// WithHTTPCollectorRespondingPlainText makes the HTTPCollector return +// a plaintext, instead of protobuf, response. +func WithHTTPCollectorRespondingPlainText() func(*HTTPCollector) { + return func(s *HTTPCollector) { + s.plainTextResponse = true + } +} + +// Shutdown shuts down the HTTP server closing all open connections and +// listeners. +func (c *HTTPCollector) Shutdown(ctx context.Context) error { + return c.srv.Shutdown(ctx) +} + +// Addr returns the net.Addr c is listening at. +func (c *HTTPCollector) Addr() net.Addr { + return c.listener.Addr() +} + +// Collect returns the Storage holding all collected requests. +func (c *HTTPCollector) Collect() *Storage { + return c.storage +} + +// Headers returns the headers received for all requests. +func (c *HTTPCollector) Headers() map[string][]string { + // Makes a copy. + c.headersMu.Lock() + defer c.headersMu.Unlock() + return c.headers.Clone() +} + +func (c *HTTPCollector) handler(w http.ResponseWriter, r *http.Request) { + c.respond(w, c.record(r)) +} + +func (c *HTTPCollector) record(r *http.Request) ExportResult { + // Currently only supports protobuf. + if v := r.Header.Get("Content-Type"); v != "application/x-protobuf" { + err := fmt.Errorf("content-type not supported: %s", v) + return ExportResult{Err: err} + } + + body, err := c.readBody(r) + if err != nil { + return ExportResult{Err: err} + } + pbRequest := &collpb.ExportMetricsServiceRequest{} + err = proto.Unmarshal(body, pbRequest) + if err != nil { + return ExportResult{ + Err: &HTTPResponseError{ + Err: err, + Status: http.StatusInternalServerError, + }, + } + } + c.storage.Add(pbRequest) + + c.headersMu.Lock() + for k, vals := range r.Header { + for _, v := range vals { + c.headers.Add(k, v) + } + } + c.headersMu.Unlock() + + if c.resultCh != nil { + return <-c.resultCh + } + return ExportResult{Err: err} +} + +func (c *HTTPCollector) readBody(r *http.Request) (body []byte, err error) { + var reader io.ReadCloser + switch r.Header.Get("Content-Encoding") { + case "gzip": + reader, err = gzip.NewReader(r.Body) + if err != nil { + _ = reader.Close() + return nil, &HTTPResponseError{ + Err: err, + Status: http.StatusInternalServerError, + } + } + default: + reader = r.Body + } + + defer func() { + cErr := reader.Close() + if err == nil && cErr != nil { + err = &HTTPResponseError{ + Err: cErr, + Status: http.StatusInternalServerError, + } + } + }() + body, err = io.ReadAll(reader) + if err != nil { + err = &HTTPResponseError{ + Err: err, + Status: http.StatusInternalServerError, + } + } + return body, err +} + +func (c *HTTPCollector) respond(w http.ResponseWriter, resp ExportResult) { + if resp.Err != nil { + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + w.Header().Set("X-Content-Type-Options", "nosniff") + var e *HTTPResponseError + if errors.As(resp.Err, &e) { + for k, vals := range e.Header { + for _, v := range vals { + w.Header().Add(k, v) + } + } + w.WriteHeader(e.Status) + fmt.Fprintln(w, e.Error()) + } else { + w.WriteHeader(http.StatusBadRequest) + fmt.Fprintln(w, resp.Err.Error()) + } + return + } + + if c.plainTextResponse { + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("OK")) + return + } + + w.Header().Set("Content-Type", "application/x-protobuf") + w.WriteHeader(http.StatusOK) + if resp.Response == nil { + _, _ = w.Write(emptyExportMetricsServiceResponse) + } else { + r, err := proto.Marshal(resp.Response) + if err != nil { + panic(err) + } + _, _ = w.Write(r) + } +} + +// Based on https://golang.org/src/crypto/tls/generate_cert.go, +// simplified and weakened. +func weakCertificate() (tls.Certificate, error) { + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return tls.Certificate{}, err + } + notBefore := time.Now() + notAfter := notBefore.Add(time.Hour) + max := new(big.Int).Lsh(big.NewInt(1), 128) + sn, err := rand.Int(rand.Reader, max) + if err != nil { + return tls.Certificate{}, err + } + tmpl := x509.Certificate{ + SerialNumber: sn, + Subject: pkix.Name{Organization: []string{"otel-go"}}, + NotBefore: notBefore, + NotAfter: notAfter, + KeyUsage: x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + DNSNames: []string{"localhost"}, + IPAddresses: []net.IP{net.IPv6loopback, net.IPv4(127, 0, 0, 1)}, + } + derBytes, err := x509.CreateCertificate(rand.Reader, &tmpl, &tmpl, &priv.PublicKey, priv) + if err != nil { + return tls.Certificate{}, err + } + var certBuf bytes.Buffer + err = pem.Encode(&certBuf, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}) + if err != nil { + return tls.Certificate{}, err + } + privBytes, err := x509.MarshalPKCS8PrivateKey(priv) + if err != nil { + return tls.Certificate{}, err + } + var privBuf bytes.Buffer + err = pem.Encode(&privBuf, &pem.Block{Type: "PRIVATE KEY", Bytes: privBytes}) + if err != nil { + return tls.Certificate{}, err + } + return tls.X509KeyPair(certBuf.Bytes(), privBuf.Bytes()) +} diff --git a/exporters/otlp/otlpmetric/otlpmetrichttp/client.go b/exporters/otlp/otlpmetric/otlpmetrichttp/client.go index cca7fb41cbe..eeb39339d45 100644 --- a/exporters/otlp/otlpmetric/otlpmetrichttp/client.go +++ b/exporters/otlp/otlpmetric/otlpmetrichttp/client.go @@ -23,8 +23,8 @@ import ( "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp/internal" "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp/internal/oconf" "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp/internal/retry" - colmetricpb "go.opentelemetry.io/proto/slim/otlp/collector/metrics/v1" - metricpb "go.opentelemetry.io/proto/slim/otlp/metrics/v1" + colmetricpb "go.opentelemetry.io/proto/otlp/collector/metrics/v1" + metricpb "go.opentelemetry.io/proto/otlp/metrics/v1" ) type client struct { diff --git a/exporters/otlp/otlpmetric/otlpmetrichttp/client_test.go b/exporters/otlp/otlpmetric/otlpmetrichttp/client_test.go index cd04a8a1c0b..2838fd9d011 100644 --- a/exporters/otlp/otlpmetric/otlpmetrichttp/client_test.go +++ b/exporters/otlp/otlpmetric/otlpmetrichttp/client_test.go @@ -21,7 +21,7 @@ import ( "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp/internal/otest" "go.opentelemetry.io/otel/sdk/metric" "go.opentelemetry.io/otel/sdk/metric/metricdata" - mpb "go.opentelemetry.io/proto/slim/otlp/metrics/v1" + mpb "go.opentelemetry.io/proto/otlp/metrics/v1" ) type clientShim struct { diff --git a/exporters/otlp/otlpmetric/otlpmetrichttp/exporter.go b/exporters/otlp/otlpmetric/otlpmetrichttp/exporter.go index f8af8c65e22..442d8096103 100644 --- a/exporters/otlp/otlpmetric/otlpmetrichttp/exporter.go +++ b/exporters/otlp/otlpmetric/otlpmetrichttp/exporter.go @@ -13,7 +13,7 @@ import ( "go.opentelemetry.io/otel/internal/global" "go.opentelemetry.io/otel/sdk/metric" "go.opentelemetry.io/otel/sdk/metric/metricdata" - metricpb "go.opentelemetry.io/proto/slim/otlp/metrics/v1" + metricpb "go.opentelemetry.io/proto/otlp/metrics/v1" ) // Exporter is a OpenTelemetry metric Exporter using protobufs over HTTP. diff --git a/exporters/otlp/otlpmetric/otlpmetrichttp/go.mod b/exporters/otlp/otlpmetric/otlpmetrichttp/go.mod index 41f8fb1bbad..4d8b0199025 100644 --- a/exporters/otlp/otlpmetric/otlpmetrichttp/go.mod +++ b/exporters/otlp/otlpmetric/otlpmetrichttp/go.mod @@ -11,7 +11,8 @@ require ( go.opentelemetry.io/otel v1.25.0 go.opentelemetry.io/otel/sdk v1.25.0 go.opentelemetry.io/otel/sdk/metric v1.25.0 - go.opentelemetry.io/proto/slim/otlp v1.2.0 + go.opentelemetry.io/proto/otlp v1.2.0 + google.golang.org/grpc v1.63.2 google.golang.org/protobuf v1.33.0 ) @@ -19,13 +20,17 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/kr/pretty v0.3.1 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 // indirect + github.com/kr/text v0.2.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rogpeppe/go-internal v1.11.0 // indirect go.opentelemetry.io/otel/metric v1.25.0 // indirect go.opentelemetry.io/otel/trace v1.25.0 // indirect + golang.org/x/net v0.23.0 // indirect golang.org/x/sys v0.19.0 // indirect - gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect + golang.org/x/text v0.14.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/exporters/otlp/otlpmetric/otlpmetrichttp/go.sum b/exporters/otlp/otlpmetric/otlpmetrichttp/go.sum index 3bf21ce4259..ea18bbbebdb 100644 --- a/exporters/otlp/otlpmetric/otlpmetrichttp/go.sum +++ b/exporters/otlp/otlpmetric/otlpmetrichttp/go.sum @@ -10,25 +10,34 @@ 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/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 h1:/c3QmbOGMGTOumP2iT/rCwB7b0QDGLKzqOmktBjT+Is= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1/go.mod h1:5SN9VR2LTsRFsrEC6FHgRbTWrTHu6tqPeKxEQv15giM= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -go.opentelemetry.io/proto/slim/otlp v1.2.0 h1:90eMxPHyObsdi/dB1ZP8FP3s3txzxVXjArYqLxPuLZg= -go.opentelemetry.io/proto/slim/otlp v1.2.0/go.mod h1:DeSHUkdUaCemrUs/Nmnsdo8BtM+XmdTEVjYWYFiLQhU= +go.opentelemetry.io/proto/otlp v1.2.0 h1:pVeZGk7nXDC9O2hncA6nHldxEjm6LByfA2aN8IOkz94= +go.opentelemetry.io/proto/otlp v1.2.0/go.mod h1:gGpR8txAl5M03pDhMC79G6SdqNV26naRm/KDsgaHD8A= +golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= +golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de h1:F6qOa9AZTYJXOUEr4jDysRDLrm4PHePlge4v4TGAlxY= +google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:VUhTRKeHn9wwcdrk73nvdC9gF178Tzhmt/qyaFcPLSo= +google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de h1:jFNzHPIeuzhdRwVhbZdiym9q0ory/xY3sA+v2wPg8I0= +google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:5iCWqnniDlqZHrd3neWVTOwvh/v6s3232omMecelax8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda h1:LI5DOvAxUPMv/50agcLLoo+AdWc1irS9Rzz4vPuD1V4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= +google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM= +google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA= google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/exporters/otlp/otlpmetric/otlpmetrichttp/internal/gen.go b/exporters/otlp/otlpmetric/otlpmetrichttp/internal/gen.go index 51890a033a1..1b379f10c5e 100644 --- a/exporters/otlp/otlpmetric/otlpmetrichttp/internal/gen.go +++ b/exporters/otlp/otlpmetric/otlpmetrichttp/internal/gen.go @@ -12,15 +12,20 @@ package internal // import "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/o //go:generate gotmpl --body=../../../../../internal/shared/otlp/envconfig/envconfig.go.tmpl "--data={}" --out=envconfig/envconfig.go //go:generate gotmpl --body=../../../../../internal/shared/otlp/envconfig/envconfig_test.go.tmpl "--data={}" --out=envconfig/envconfig_test.go +//go:generate gotmpl --body=../../../../../internal/shared/otlp/otlpmetric/oconf/envconfig.go.tmpl "--data={\"envconfigImportPath\": \"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp/internal/envconfig\"}" --out=oconf/envconfig.go +//go:generate gotmpl --body=../../../../../internal/shared/otlp/otlpmetric/oconf/envconfig_test.go.tmpl "--data={}" --out=oconf/envconfig_test.go +//go:generate gotmpl --body=../../../../../internal/shared/otlp/otlpmetric/oconf/options.go.tmpl "--data={\"retryImportPath\": \"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp/internal/retry\"}" --out=oconf/options.go +//go:generate gotmpl --body=../../../../../internal/shared/otlp/otlpmetric/oconf/options_test.go.tmpl "--data={\"envconfigImportPath\": \"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp/internal/envconfig\"}" --out=oconf/options_test.go //go:generate gotmpl --body=../../../../../internal/shared/otlp/otlpmetric/oconf/optiontypes.go.tmpl "--data={}" --out=oconf/optiontypes.go //go:generate gotmpl --body=../../../../../internal/shared/otlp/otlpmetric/oconf/tls.go.tmpl "--data={}" --out=oconf/tls.go -//go:generate gotmpl --body=../../../../../internal/shared/otlp/otlpmetric/otest/client.go.tmpl "--data={\"protoImportPrefix\": \"go.opentelemetry.io/proto/slim\"}" --out=otest/client.go -//go:generate gotmpl --body=../../../../../internal/shared/otlp/otlpmetric/otest/client_test.go.tmpl "--data={\"protoImportPrefix\": \"go.opentelemetry.io/proto/slim\", \"internalImportPath\": \"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp/internal\"}" --out=otest/client_test.go +//go:generate gotmpl --body=../../../../../internal/shared/otlp/otlpmetric/otest/client.go.tmpl "--data={}" --out=otest/client.go +//go:generate gotmpl --body=../../../../../internal/shared/otlp/otlpmetric/otest/client_test.go.tmpl "--data={\"internalImportPath\": \"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp/internal\"}" --out=otest/client_test.go +//go:generate gotmpl --body=../../../../../internal/shared/otlp/otlpmetric/otest/collector.go.tmpl "--data={\"oconfImportPath\": \"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp/internal/oconf\"}" --out=otest/collector.go -//go:generate gotmpl --body=../../../../../internal/shared/otlp/otlpmetric/transform/attribute.go.tmpl "--data={\"protoImportPrefix\": \"go.opentelemetry.io/proto/slim\"}" --out=transform/attribute.go -//go:generate gotmpl --body=../../../../../internal/shared/otlp/otlpmetric/transform/attribute_test.go.tmpl "--data={\"protoImportPrefix\": \"go.opentelemetry.io/proto/slim\"}" --out=transform/attribute_test.go -//go:generate gotmpl --body=../../../../../internal/shared/otlp/otlpmetric/transform/error.go.tmpl "--data={\"protoImportPrefix\": \"go.opentelemetry.io/proto/slim\"}" --out=transform/error.go +//go:generate gotmpl --body=../../../../../internal/shared/otlp/otlpmetric/transform/attribute.go.tmpl "--data={}" --out=transform/attribute.go +//go:generate gotmpl --body=../../../../../internal/shared/otlp/otlpmetric/transform/attribute_test.go.tmpl "--data={}" --out=transform/attribute_test.go +//go:generate gotmpl --body=../../../../../internal/shared/otlp/otlpmetric/transform/error.go.tmpl "--data={}" --out=transform/error.go //go:generate gotmpl --body=../../../../../internal/shared/otlp/otlpmetric/transform/error_test.go.tmpl "--data={}" --out=transform/error_test.go -//go:generate gotmpl --body=../../../../../internal/shared/otlp/otlpmetric/transform/metricdata.go.tmpl "--data={\"protoImportPrefix\": \"go.opentelemetry.io/proto/slim\"}" --out=transform/metricdata.go -//go:generate gotmpl --body=../../../../../internal/shared/otlp/otlpmetric/transform/metricdata_test.go.tmpl "--data={\"protoImportPrefix\": \"go.opentelemetry.io/proto/slim\"}" --out=transform/metricdata_test.go +//go:generate gotmpl --body=../../../../../internal/shared/otlp/otlpmetric/transform/metricdata.go.tmpl "--data={}" --out=transform/metricdata.go +//go:generate gotmpl --body=../../../../../internal/shared/otlp/otlpmetric/transform/metricdata_test.go.tmpl "--data={}" --out=transform/metricdata_test.go diff --git a/exporters/otlp/otlpmetric/otlpmetrichttp/internal/oconf/envconfig.go b/exporters/otlp/otlpmetric/otlpmetrichttp/internal/oconf/envconfig.go index ba641347081..89b134a39fb 100644 --- a/exporters/otlp/otlpmetric/otlpmetrichttp/internal/oconf/envconfig.go +++ b/exporters/otlp/otlpmetric/otlpmetrichttp/internal/oconf/envconfig.go @@ -1,3 +1,6 @@ +// Code created by gotmpl. DO NOT MODIFY. +// source: internal/shared/otlp/otlpmetric/oconf/envconfig.go.tmpl + // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 @@ -25,6 +28,15 @@ var DefaultEnvOptionsReader = envconfig.EnvOptionsReader{ Namespace: "OTEL_EXPORTER_OTLP", } +// ApplyGRPCEnvConfigs applies the env configurations for gRPC. +func ApplyGRPCEnvConfigs(cfg Config) Config { + opts := getOptionsFromEnv() + for _, opt := range opts { + cfg = opt.ApplyGRPCOption(cfg) + } + return cfg +} + // ApplyHTTPEnvConfigs applies the env configurations for HTTP. func ApplyHTTPEnvConfigs(cfg Config) Config { opts := getOptionsFromEnv() @@ -34,25 +46,25 @@ func ApplyHTTPEnvConfigs(cfg Config) Config { return cfg } -func getOptionsFromEnv() []HTTPOption { - opts := []HTTPOption{} +func getOptionsFromEnv() []GenericOption { + opts := []GenericOption{} tlsConf := &tls.Config{} DefaultEnvOptionsReader.Apply( envconfig.WithURL("ENDPOINT", func(u *url.URL) { opts = append(opts, withEndpointScheme(u)) - opts = append(opts, NewHTTPOption(func(cfg Config) Config { + opts = append(opts, newSplitOption(func(cfg Config) Config { cfg.Metrics.Endpoint = u.Host // For OTLP/HTTP endpoint URLs without a per-signal // configuration, the passed endpoint is used as a base URL // and the signals are sent to these paths relative to that. cfg.Metrics.URLPath = path.Join(u.Path, DefaultMetricsPath) return cfg - })) + }, withEndpointForGRPC(u))) }), envconfig.WithURL("METRICS_ENDPOINT", func(u *url.URL) { opts = append(opts, withEndpointScheme(u)) - opts = append(opts, NewHTTPOption(func(cfg Config) Config { + opts = append(opts, newSplitOption(func(cfg Config) Config { cfg.Metrics.Endpoint = u.Host // For endpoint URLs for OTLP/HTTP per-signal variables, the // URL MUST be used as-is without any modification. The only @@ -64,7 +76,7 @@ func getOptionsFromEnv() []HTTPOption { } cfg.Metrics.URLPath = path return cfg - })) + }, withEndpointForGRPC(u))) }), envconfig.WithCertPool("CERTIFICATE", func(p *x509.CertPool) { tlsConf.RootCAs = p }), envconfig.WithCertPool("METRICS_CERTIFICATE", func(p *x509.CertPool) { tlsConf.RootCAs = p }), @@ -86,6 +98,15 @@ func getOptionsFromEnv() []HTTPOption { return opts } +func withEndpointForGRPC(u *url.URL) func(cfg Config) Config { + return func(cfg Config) Config { + // For OTLP/gRPC endpoints, this is the target to which the + // exporter is going to send telemetry. + cfg.Metrics.Endpoint = path.Join(u.Host, u.Path) + return cfg + } +} + // WithEnvCompression retrieves the specified config and passes it to ConfigFn as a Compression. func WithEnvCompression(n string, fn func(Compression)) func(e *envconfig.EnvOptionsReader) { return func(e *envconfig.EnvOptionsReader) { @@ -100,7 +121,7 @@ func WithEnvCompression(n string, fn func(Compression)) func(e *envconfig.EnvOpt } } -func withEndpointScheme(u *url.URL) HTTPOption { +func withEndpointScheme(u *url.URL) GenericOption { switch strings.ToLower(u.Scheme) { case "http", "unix": return WithInsecure() @@ -110,7 +131,7 @@ func withEndpointScheme(u *url.URL) HTTPOption { } // revive:disable-next-line:flag-parameter -func withInsecure(b bool) HTTPOption { +func withInsecure(b bool) GenericOption { if b { return WithInsecure() } diff --git a/exporters/otlp/otlpmetric/otlpmetrichttp/internal/oconf/envconfig_test.go b/exporters/otlp/otlpmetric/otlpmetrichttp/internal/oconf/envconfig_test.go index ba99959db56..5c621d7b47e 100644 --- a/exporters/otlp/otlpmetric/otlpmetrichttp/internal/oconf/envconfig_test.go +++ b/exporters/otlp/otlpmetric/otlpmetrichttp/internal/oconf/envconfig_test.go @@ -1,3 +1,6 @@ +// Code created by gotmpl. DO NOT MODIFY. +// source: internal/shared/otlp/otlpmetric/oconf/envconfig_test.go.tmpl + // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 @@ -74,7 +77,7 @@ func TestWithEnvTemporalityPreference(t *testing.T) { return origReader(key) } cfg := Config{} - cfg = ApplyHTTPEnvConfigs(cfg) + cfg = ApplyGRPCEnvConfigs(cfg) if tt.want == nil { // There is no function set, the SDK's default is used. @@ -144,7 +147,7 @@ func TestWithEnvAggPreference(t *testing.T) { return origReader(key) } cfg := Config{} - cfg = ApplyHTTPEnvConfigs(cfg) + cfg = ApplyGRPCEnvConfigs(cfg) if tt.want == nil { // There is no function set, the SDK's default is used. diff --git a/exporters/otlp/otlpmetric/otlpmetrichttp/internal/oconf/options.go b/exporters/otlp/otlpmetric/otlpmetrichttp/internal/oconf/options.go index 1763b381988..9bbf0941f94 100644 --- a/exporters/otlp/otlpmetric/otlpmetrichttp/internal/oconf/options.go +++ b/exporters/otlp/otlpmetric/otlpmetrichttp/internal/oconf/options.go @@ -1,3 +1,6 @@ +// Code created by gotmpl. DO NOT MODIFY. +// source: internal/shared/otlp/otlpmetric/oconf/options.go.tmpl + // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 @@ -12,6 +15,12 @@ import ( "strings" "time" + "google.golang.org/grpc" + "google.golang.org/grpc/backoff" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/encoding/gzip" + "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp/internal/retry" "go.opentelemetry.io/otel/internal/global" "go.opentelemetry.io/otel/sdk/metric" @@ -38,7 +47,6 @@ type ( // This type is compatible with `http.Transport.Proxy` and can be used to set a custom proxy function to the OTLP HTTP client. HTTPTransportProxyFunc func(*http.Request) (*url.URL, error) - // SignalConfig represents signal specific configuration. SignalConfig struct { Endpoint string Insecure bool @@ -48,17 +56,26 @@ type ( Timeout time.Duration URLPath string + // gRPC configurations + GRPCCredentials credentials.TransportCredentials + TemporalitySelector metric.TemporalitySelector AggregationSelector metric.AggregationSelector Proxy HTTPTransportProxyFunc } - // Config represents exporter configuration. Config struct { + // Signal specific configurations Metrics SignalConfig RetryConfig retry.Config + + // gRPC configurations + ReconnectionPeriod time.Duration + ServiceConfig string + DialOptions []grpc.DialOption + GRPCConn *grpc.ClientConn } ) @@ -99,14 +116,126 @@ func cleanPath(urlPath string, defaultPath string) string { return tmp } -// HTTPOption applies an option to the HTTP driver. -type HTTPOption interface { - ApplyHTTPOption(Config) Config +// NewGRPCConfig returns a new Config with all settings applied from opts and +// any unset setting using the default gRPC config values. +func NewGRPCConfig(opts ...GRPCOption) Config { + cfg := Config{ + Metrics: SignalConfig{ + Endpoint: fmt.Sprintf("%s:%d", DefaultCollectorHost, DefaultCollectorGRPCPort), + URLPath: DefaultMetricsPath, + Compression: NoCompression, + Timeout: DefaultTimeout, + + TemporalitySelector: metric.DefaultTemporalitySelector, + AggregationSelector: metric.DefaultAggregationSelector, + }, + RetryConfig: retry.DefaultConfig, + } + cfg = ApplyGRPCEnvConfigs(cfg) + for _, opt := range opts { + cfg = opt.ApplyGRPCOption(cfg) + } + + if cfg.ServiceConfig != "" { + cfg.DialOptions = append(cfg.DialOptions, grpc.WithDefaultServiceConfig(cfg.ServiceConfig)) + } + // Priroritize GRPCCredentials over Insecure (passing both is an error). + if cfg.Metrics.GRPCCredentials != nil { + cfg.DialOptions = append(cfg.DialOptions, grpc.WithTransportCredentials(cfg.Metrics.GRPCCredentials)) + } else if cfg.Metrics.Insecure { + cfg.DialOptions = append(cfg.DialOptions, grpc.WithTransportCredentials(insecure.NewCredentials())) + } else { + // Default to using the host's root CA. + creds := credentials.NewTLS(nil) + cfg.Metrics.GRPCCredentials = creds + cfg.DialOptions = append(cfg.DialOptions, grpc.WithTransportCredentials(creds)) + } + if cfg.Metrics.Compression == GzipCompression { + cfg.DialOptions = append(cfg.DialOptions, grpc.WithDefaultCallOptions(grpc.UseCompressor(gzip.Name))) + } + if cfg.ReconnectionPeriod != 0 { + p := grpc.ConnectParams{ + Backoff: backoff.DefaultConfig, + MinConnectTimeout: cfg.ReconnectionPeriod, + } + cfg.DialOptions = append(cfg.DialOptions, grpc.WithConnectParams(p)) + } - // A private method to prevent users implementing the - // interface and so future additions to it will not - // violate compatibility. - private() + return cfg +} + +type ( + // GenericOption applies an option to the HTTP or gRPC driver. + GenericOption interface { + ApplyHTTPOption(Config) Config + ApplyGRPCOption(Config) Config + + // A private method to prevent users implementing the + // interface and so future additions to it will not + // violate compatibility. + private() + } + + // HTTPOption applies an option to the HTTP driver. + HTTPOption interface { + ApplyHTTPOption(Config) Config + + // A private method to prevent users implementing the + // interface and so future additions to it will not + // violate compatibility. + private() + } + + // GRPCOption applies an option to the gRPC driver. + GRPCOption interface { + ApplyGRPCOption(Config) Config + + // A private method to prevent users implementing the + // interface and so future additions to it will not + // violate compatibility. + private() + } +) + +// genericOption is an option that applies the same logic +// for both gRPC and HTTP. +type genericOption struct { + fn func(Config) Config +} + +func (g *genericOption) ApplyGRPCOption(cfg Config) Config { + return g.fn(cfg) +} + +func (g *genericOption) ApplyHTTPOption(cfg Config) Config { + return g.fn(cfg) +} + +func (genericOption) private() {} + +func newGenericOption(fn func(cfg Config) Config) GenericOption { + return &genericOption{fn: fn} +} + +// splitOption is an option that applies different logics +// for gRPC and HTTP. +type splitOption struct { + httpFn func(Config) Config + grpcFn func(Config) Config +} + +func (g *splitOption) ApplyGRPCOption(cfg Config) Config { + return g.grpcFn(cfg) +} + +func (g *splitOption) ApplyHTTPOption(cfg Config) Config { + return g.httpFn(cfg) +} + +func (splitOption) private() {} + +func newSplitOption(httpFn func(cfg Config) Config, grpcFn func(cfg Config) Config) GenericOption { + return &splitOption{httpFn: httpFn, grpcFn: grpcFn} } // httpOption is an option that is only applied to the HTTP driver. @@ -124,17 +253,32 @@ func NewHTTPOption(fn func(cfg Config) Config) HTTPOption { return &httpOption{fn: fn} } +// grpcOption is an option that is only applied to the gRPC driver. +type grpcOption struct { + fn func(Config) Config +} + +func (h *grpcOption) ApplyGRPCOption(cfg Config) Config { + return h.fn(cfg) +} + +func (grpcOption) private() {} + +func NewGRPCOption(fn func(cfg Config) Config) GRPCOption { + return &grpcOption{fn: fn} +} + // Generic Options -func WithEndpoint(endpoint string) HTTPOption { - return NewHTTPOption(func(cfg Config) Config { +func WithEndpoint(endpoint string) GenericOption { + return newGenericOption(func(cfg Config) Config { cfg.Metrics.Endpoint = endpoint return cfg }) } -func WithEndpointURL(v string) HTTPOption { - return NewHTTPOption(func(cfg Config) Config { +func WithEndpointURL(v string) GenericOption { + return newGenericOption(func(cfg Config) Config { u, err := url.Parse(v) if err != nil { global.Error(err, "otlpmetric: parse endpoint url", "url", v) @@ -151,78 +295,81 @@ func WithEndpointURL(v string) HTTPOption { }) } -func WithCompression(compression Compression) HTTPOption { - return NewHTTPOption(func(cfg Config) Config { +func WithCompression(compression Compression) GenericOption { + return newGenericOption(func(cfg Config) Config { cfg.Metrics.Compression = compression return cfg }) } -func WithURLPath(urlPath string) HTTPOption { - return NewHTTPOption(func(cfg Config) Config { +func WithURLPath(urlPath string) GenericOption { + return newGenericOption(func(cfg Config) Config { cfg.Metrics.URLPath = urlPath return cfg }) } -func WithRetry(rc retry.Config) HTTPOption { - return NewHTTPOption(func(cfg Config) Config { +func WithRetry(rc retry.Config) GenericOption { + return newGenericOption(func(cfg Config) Config { cfg.RetryConfig = rc return cfg }) } -func WithTLSClientConfig(tlsCfg *tls.Config) HTTPOption { - return NewHTTPOption(func(cfg Config) Config { +func WithTLSClientConfig(tlsCfg *tls.Config) GenericOption { + return newSplitOption(func(cfg Config) Config { cfg.Metrics.TLSCfg = tlsCfg.Clone() return cfg + }, func(cfg Config) Config { + cfg.Metrics.GRPCCredentials = credentials.NewTLS(tlsCfg) + return cfg }) } -func WithInsecure() HTTPOption { - return NewHTTPOption(func(cfg Config) Config { +func WithInsecure() GenericOption { + return newGenericOption(func(cfg Config) Config { cfg.Metrics.Insecure = true return cfg }) } -func WithSecure() HTTPOption { - return NewHTTPOption(func(cfg Config) Config { +func WithSecure() GenericOption { + return newGenericOption(func(cfg Config) Config { cfg.Metrics.Insecure = false return cfg }) } -func WithHeaders(headers map[string]string) HTTPOption { - return NewHTTPOption(func(cfg Config) Config { +func WithHeaders(headers map[string]string) GenericOption { + return newGenericOption(func(cfg Config) Config { cfg.Metrics.Headers = headers return cfg }) } -func WithTimeout(duration time.Duration) HTTPOption { - return NewHTTPOption(func(cfg Config) Config { +func WithTimeout(duration time.Duration) GenericOption { + return newGenericOption(func(cfg Config) Config { cfg.Metrics.Timeout = duration return cfg }) } -func WithTemporalitySelector(selector metric.TemporalitySelector) HTTPOption { - return NewHTTPOption(func(cfg Config) Config { +func WithTemporalitySelector(selector metric.TemporalitySelector) GenericOption { + return newGenericOption(func(cfg Config) Config { cfg.Metrics.TemporalitySelector = selector return cfg }) } -func WithAggregationSelector(selector metric.AggregationSelector) HTTPOption { - return NewHTTPOption(func(cfg Config) Config { +func WithAggregationSelector(selector metric.AggregationSelector) GenericOption { + return newGenericOption(func(cfg Config) Config { cfg.Metrics.AggregationSelector = selector return cfg }) } -func WithProxy(pf HTTPTransportProxyFunc) HTTPOption { - return NewHTTPOption(func(cfg Config) Config { +func WithProxy(pf HTTPTransportProxyFunc) GenericOption { + return newGenericOption(func(cfg Config) Config { cfg.Metrics.Proxy = pf return cfg }) diff --git a/exporters/otlp/otlpmetric/otlpmetrichttp/internal/oconf/options_test.go b/exporters/otlp/otlpmetric/otlpmetrichttp/internal/oconf/options_test.go index 8f6cd3e81ba..3401f8ec524 100644 --- a/exporters/otlp/otlpmetric/otlpmetrichttp/internal/oconf/options_test.go +++ b/exporters/otlp/otlpmetric/otlpmetrichttp/internal/oconf/options_test.go @@ -1,3 +1,6 @@ +// Code created by gotmpl. DO NOT MODIFY. +// source: internal/shared/otlp/otlpmetric/oconf/options_test.go.tmpl + // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 @@ -61,15 +64,19 @@ func TestConfigs(t *testing.T) { tests := []struct { name string - opts []HTTPOption + opts []GenericOption env env fileReader fileReader - asserts func(t *testing.T, c *Config) + asserts func(t *testing.T, c *Config, grpcOption bool) }{ { name: "Test default configs", - asserts: func(t *testing.T, c *Config) { - assert.Equal(t, "localhost:4318", c.Metrics.Endpoint) + asserts: func(t *testing.T, c *Config, grpcOption bool) { + if grpcOption { + assert.Equal(t, "localhost:4317", c.Metrics.Endpoint) + } else { + assert.Equal(t, "localhost:4318", c.Metrics.Endpoint) + } assert.Equal(t, NoCompression, c.Metrics.Compression) assert.Equal(t, map[string]string(nil), c.Metrics.Headers) assert.Equal(t, 10*time.Second, c.Metrics.Timeout) @@ -79,19 +86,19 @@ func TestConfigs(t *testing.T) { // Endpoint Tests { name: "Test With Endpoint", - opts: []HTTPOption{ + opts: []GenericOption{ WithEndpoint("someendpoint"), }, - asserts: func(t *testing.T, c *Config) { + asserts: func(t *testing.T, c *Config, grpcOption bool) { assert.Equal(t, "someendpoint", c.Metrics.Endpoint) }, }, { name: "Test With Endpoint URL", - opts: []HTTPOption{ + opts: []GenericOption{ WithEndpointURL("http://someendpoint/somepath"), }, - asserts: func(t *testing.T, c *Config) { + asserts: func(t *testing.T, c *Config, grpcOption bool) { assert.Equal(t, "someendpoint", c.Metrics.Endpoint) assert.Equal(t, "/somepath", c.Metrics.URLPath) assert.Equal(t, true, c.Metrics.Insecure) @@ -99,10 +106,10 @@ func TestConfigs(t *testing.T) { }, { name: "Test With Secure Endpoint URL", - opts: []HTTPOption{ + opts: []GenericOption{ WithEndpointURL("https://someendpoint/somepath"), }, - asserts: func(t *testing.T, c *Config) { + asserts: func(t *testing.T, c *Config, grpcOption bool) { assert.Equal(t, "someendpoint", c.Metrics.Endpoint) assert.Equal(t, "/somepath", c.Metrics.URLPath) assert.Equal(t, false, c.Metrics.Insecure) @@ -110,11 +117,15 @@ func TestConfigs(t *testing.T) { }, { name: "Test With Invalid Endpoint URL", - opts: []HTTPOption{ + opts: []GenericOption{ WithEndpointURL("%invalid"), }, - asserts: func(t *testing.T, c *Config) { - assert.Equal(t, "localhost:4318", c.Metrics.Endpoint) + asserts: func(t *testing.T, c *Config, grpcOption bool) { + if grpcOption { + assert.Equal(t, "localhost:4317", c.Metrics.Endpoint) + } else { + assert.Equal(t, "localhost:4318", c.Metrics.Endpoint) + } assert.Equal(t, "/v1/metrics", c.Metrics.URLPath) }, }, @@ -123,10 +134,14 @@ func TestConfigs(t *testing.T) { env: map[string]string{ "OTEL_EXPORTER_OTLP_ENDPOINT": "https://env.endpoint/prefix", }, - asserts: func(t *testing.T, c *Config) { + asserts: func(t *testing.T, c *Config, grpcOption bool) { assert.False(t, c.Metrics.Insecure) - assert.Equal(t, "env.endpoint", c.Metrics.Endpoint) - assert.Equal(t, "/prefix/v1/metrics", c.Metrics.URLPath) + if grpcOption { + assert.Equal(t, "env.endpoint/prefix", c.Metrics.Endpoint) + } else { + assert.Equal(t, "env.endpoint", c.Metrics.Endpoint) + assert.Equal(t, "/prefix/v1/metrics", c.Metrics.URLPath) + } }, }, { @@ -135,21 +150,23 @@ func TestConfigs(t *testing.T) { "OTEL_EXPORTER_OTLP_ENDPOINT": "https://overrode.by.signal.specific/env/var", "OTEL_EXPORTER_OTLP_METRICS_ENDPOINT": "http://env.metrics.endpoint", }, - asserts: func(t *testing.T, c *Config) { + asserts: func(t *testing.T, c *Config, grpcOption bool) { assert.True(t, c.Metrics.Insecure) assert.Equal(t, "env.metrics.endpoint", c.Metrics.Endpoint) - assert.Equal(t, "/", c.Metrics.URLPath) + if !grpcOption { + assert.Equal(t, "/", c.Metrics.URLPath) + } }, }, { name: "Test Mixed Environment and With Endpoint", - opts: []HTTPOption{ + opts: []GenericOption{ WithEndpoint("metrics_endpoint"), }, env: map[string]string{ "OTEL_EXPORTER_OTLP_ENDPOINT": "env_endpoint", }, - asserts: func(t *testing.T, c *Config) { + asserts: func(t *testing.T, c *Config, grpcOption bool) { assert.Equal(t, "metrics_endpoint", c.Metrics.Endpoint) }, }, @@ -158,7 +175,7 @@ func TestConfigs(t *testing.T) { env: map[string]string{ "OTEL_EXPORTER_OTLP_ENDPOINT": "http://env_endpoint", }, - asserts: func(t *testing.T, c *Config) { + asserts: func(t *testing.T, c *Config, grpcOption bool) { assert.Equal(t, "env_endpoint", c.Metrics.Endpoint) assert.Equal(t, true, c.Metrics.Insecure) }, @@ -168,7 +185,7 @@ func TestConfigs(t *testing.T) { env: map[string]string{ "OTEL_EXPORTER_OTLP_ENDPOINT": " http://env_endpoint ", }, - asserts: func(t *testing.T, c *Config) { + asserts: func(t *testing.T, c *Config, grpcOption bool) { assert.Equal(t, "env_endpoint", c.Metrics.Endpoint) assert.Equal(t, true, c.Metrics.Insecure) }, @@ -178,7 +195,7 @@ func TestConfigs(t *testing.T) { env: map[string]string{ "OTEL_EXPORTER_OTLP_ENDPOINT": "https://env_endpoint", }, - asserts: func(t *testing.T, c *Config) { + asserts: func(t *testing.T, c *Config, grpcOption bool) { assert.Equal(t, "env_endpoint", c.Metrics.Endpoint) assert.Equal(t, false, c.Metrics.Insecure) }, @@ -189,7 +206,7 @@ func TestConfigs(t *testing.T) { "OTEL_EXPORTER_OTLP_ENDPOINT": "HTTPS://overrode_by_signal_specific", "OTEL_EXPORTER_OTLP_METRICS_ENDPOINT": "HtTp://env_metrics_endpoint", }, - asserts: func(t *testing.T, c *Config) { + asserts: func(t *testing.T, c *Config, grpcOption bool) { assert.Equal(t, "env_metrics_endpoint", c.Metrics.Endpoint) assert.Equal(t, true, c.Metrics.Insecure) }, @@ -198,18 +215,27 @@ func TestConfigs(t *testing.T) { // Certificate tests { name: "Test Default Certificate", - asserts: func(t *testing.T, c *Config) { - assert.Nil(t, c.Metrics.TLSCfg) + asserts: func(t *testing.T, c *Config, grpcOption bool) { + if grpcOption { + assert.NotNil(t, c.Metrics.GRPCCredentials) + } else { + assert.Nil(t, c.Metrics.TLSCfg) + } }, }, { name: "Test With Certificate", - opts: []HTTPOption{ + opts: []GenericOption{ WithTLSClientConfig(tlsCert), }, - asserts: func(t *testing.T, c *Config) { - // nolint:staticcheck // ignoring tlsCert.RootCAs.Subjects is deprecated ERR because cert does not come from SystemCertPool. - assert.Equal(t, tlsCert.RootCAs.Subjects(), c.Metrics.TLSCfg.RootCAs.Subjects()) + asserts: func(t *testing.T, c *Config, grpcOption bool) { + if grpcOption { + // TODO: make sure gRPC's credentials actually works + assert.NotNil(t, c.Metrics.GRPCCredentials) + } else { + // nolint:staticcheck // ignoring tlsCert.RootCAs.Subjects is deprecated ERR because cert does not come from SystemCertPool. + assert.Equal(t, tlsCert.RootCAs.Subjects(), c.Metrics.TLSCfg.RootCAs.Subjects()) + } }, }, { @@ -220,9 +246,13 @@ func TestConfigs(t *testing.T) { fileReader: fileReader{ "cert_path": []byte(WeakCertificate), }, - asserts: func(t *testing.T, c *Config) { - // nolint:staticcheck // ignoring tlsCert.RootCAs.Subjects is deprecated ERR because cert does not come from SystemCertPool. - assert.Equal(t, tlsCert.RootCAs.Subjects(), c.Metrics.TLSCfg.RootCAs.Subjects()) + asserts: func(t *testing.T, c *Config, grpcOption bool) { + if grpcOption { + assert.NotNil(t, c.Metrics.GRPCCredentials) + } else { + // nolint:staticcheck // ignoring tlsCert.RootCAs.Subjects is deprecated ERR because cert does not come from SystemCertPool. + assert.Equal(t, tlsCert.RootCAs.Subjects(), c.Metrics.TLSCfg.RootCAs.Subjects()) + } }, }, { @@ -235,40 +265,48 @@ func TestConfigs(t *testing.T) { "cert_path": []byte(WeakCertificate), "invalid_cert": []byte("invalid certificate file."), }, - asserts: func(t *testing.T, c *Config) { - // nolint:staticcheck // ignoring tlsCert.RootCAs.Subjects is deprecated ERR because cert does not come from SystemCertPool. - assert.Equal(t, tlsCert.RootCAs.Subjects(), c.Metrics.TLSCfg.RootCAs.Subjects()) + asserts: func(t *testing.T, c *Config, grpcOption bool) { + if grpcOption { + assert.NotNil(t, c.Metrics.GRPCCredentials) + } else { + // nolint:staticcheck // ignoring tlsCert.RootCAs.Subjects is deprecated ERR because cert does not come from SystemCertPool. + assert.Equal(t, tlsCert.RootCAs.Subjects(), c.Metrics.TLSCfg.RootCAs.Subjects()) + } }, }, { name: "Test Mixed Environment and With Certificate", - opts: []HTTPOption{}, + opts: []GenericOption{}, env: map[string]string{ "OTEL_EXPORTER_OTLP_CERTIFICATE": "cert_path", }, fileReader: fileReader{ "cert_path": []byte(WeakCertificate), }, - asserts: func(t *testing.T, c *Config) { - // nolint:staticcheck // ignoring tlsCert.RootCAs.Subjects is deprecated ERR because cert does not come from SystemCertPool. - assert.Equal(t, 1, len(c.Metrics.TLSCfg.RootCAs.Subjects())) + asserts: func(t *testing.T, c *Config, grpcOption bool) { + if grpcOption { + assert.NotNil(t, c.Metrics.GRPCCredentials) + } else { + // nolint:staticcheck // ignoring tlsCert.RootCAs.Subjects is deprecated ERR because cert does not come from SystemCertPool. + assert.Equal(t, 1, len(c.Metrics.TLSCfg.RootCAs.Subjects())) + } }, }, // Headers tests { name: "Test With Headers", - opts: []HTTPOption{ + opts: []GenericOption{ WithHeaders(map[string]string{"h1": "v1"}), }, - asserts: func(t *testing.T, c *Config) { + asserts: func(t *testing.T, c *Config, grpcOption bool) { assert.Equal(t, map[string]string{"h1": "v1"}, c.Metrics.Headers) }, }, { name: "Test Environment Headers", env: map[string]string{"OTEL_EXPORTER_OTLP_HEADERS": "h1=v1,h2=v2"}, - asserts: func(t *testing.T, c *Config) { + asserts: func(t *testing.T, c *Config, grpcOption bool) { assert.Equal(t, map[string]string{"h1": "v1", "h2": "v2"}, c.Metrics.Headers) }, }, @@ -278,17 +316,17 @@ func TestConfigs(t *testing.T) { "OTEL_EXPORTER_OTLP_HEADERS": "overrode_by_signal_specific", "OTEL_EXPORTER_OTLP_METRICS_HEADERS": "h1=v1,h2=v2", }, - asserts: func(t *testing.T, c *Config) { + asserts: func(t *testing.T, c *Config, grpcOption bool) { assert.Equal(t, map[string]string{"h1": "v1", "h2": "v2"}, c.Metrics.Headers) }, }, { name: "Test Mixed Environment and With Headers", env: map[string]string{"OTEL_EXPORTER_OTLP_HEADERS": "h1=v1,h2=v2"}, - opts: []HTTPOption{ + opts: []GenericOption{ WithHeaders(map[string]string{"m1": "mv1"}), }, - asserts: func(t *testing.T, c *Config) { + asserts: func(t *testing.T, c *Config, grpcOption bool) { assert.Equal(t, map[string]string{"m1": "mv1"}, c.Metrics.Headers) }, }, @@ -296,10 +334,10 @@ func TestConfigs(t *testing.T) { // Compression Tests { name: "Test With Compression", - opts: []HTTPOption{ + opts: []GenericOption{ WithCompression(GzipCompression), }, - asserts: func(t *testing.T, c *Config) { + asserts: func(t *testing.T, c *Config, grpcOption bool) { assert.Equal(t, GzipCompression, c.Metrics.Compression) }, }, @@ -308,7 +346,7 @@ func TestConfigs(t *testing.T) { env: map[string]string{ "OTEL_EXPORTER_OTLP_COMPRESSION": "gzip", }, - asserts: func(t *testing.T, c *Config) { + asserts: func(t *testing.T, c *Config, grpcOption bool) { assert.Equal(t, GzipCompression, c.Metrics.Compression) }, }, @@ -317,19 +355,19 @@ func TestConfigs(t *testing.T) { env: map[string]string{ "OTEL_EXPORTER_OTLP_METRICS_COMPRESSION": "gzip", }, - asserts: func(t *testing.T, c *Config) { + asserts: func(t *testing.T, c *Config, grpcOption bool) { assert.Equal(t, GzipCompression, c.Metrics.Compression) }, }, { name: "Test Mixed Environment and With Compression", - opts: []HTTPOption{ + opts: []GenericOption{ WithCompression(NoCompression), }, env: map[string]string{ "OTEL_EXPORTER_OTLP_METRICS_COMPRESSION": "gzip", }, - asserts: func(t *testing.T, c *Config) { + asserts: func(t *testing.T, c *Config, grpcOption bool) { assert.Equal(t, NoCompression, c.Metrics.Compression) }, }, @@ -337,10 +375,10 @@ func TestConfigs(t *testing.T) { // Timeout Tests { name: "Test With Timeout", - opts: []HTTPOption{ + opts: []GenericOption{ WithTimeout(time.Duration(5 * time.Second)), }, - asserts: func(t *testing.T, c *Config) { + asserts: func(t *testing.T, c *Config, grpcOption bool) { assert.Equal(t, 5*time.Second, c.Metrics.Timeout) }, }, @@ -349,7 +387,7 @@ func TestConfigs(t *testing.T) { env: map[string]string{ "OTEL_EXPORTER_OTLP_TIMEOUT": "15000", }, - asserts: func(t *testing.T, c *Config) { + asserts: func(t *testing.T, c *Config, grpcOption bool) { assert.Equal(t, c.Metrics.Timeout, 15*time.Second) }, }, @@ -359,7 +397,7 @@ func TestConfigs(t *testing.T) { "OTEL_EXPORTER_OTLP_TIMEOUT": "15000", "OTEL_EXPORTER_OTLP_METRICS_TIMEOUT": "28000", }, - asserts: func(t *testing.T, c *Config) { + asserts: func(t *testing.T, c *Config, grpcOption bool) { assert.Equal(t, c.Metrics.Timeout, 28*time.Second) }, }, @@ -369,10 +407,10 @@ func TestConfigs(t *testing.T) { "OTEL_EXPORTER_OTLP_TIMEOUT": "15000", "OTEL_EXPORTER_OTLP_METRICS_TIMEOUT": "28000", }, - opts: []HTTPOption{ + opts: []GenericOption{ WithTimeout(5 * time.Second), }, - asserts: func(t *testing.T, c *Config) { + asserts: func(t *testing.T, c *Config, grpcOption bool) { assert.Equal(t, c.Metrics.Timeout, 5*time.Second) }, }, @@ -380,10 +418,10 @@ func TestConfigs(t *testing.T) { // Temporality Selector Tests { name: "WithTemporalitySelector", - opts: []HTTPOption{ + opts: []GenericOption{ WithTemporalitySelector(deltaSelector), }, - asserts: func(t *testing.T, c *Config) { + asserts: func(t *testing.T, c *Config, grpcOption bool) { // Function value comparisons are disallowed, test non-default // behavior of a TemporalitySelector here to ensure our "catch // all" was set. @@ -396,10 +434,10 @@ func TestConfigs(t *testing.T) { // Aggregation Selector Tests { name: "WithAggregationSelector", - opts: []HTTPOption{ + opts: []GenericOption{ WithAggregationSelector(dropSelector), }, - asserts: func(t *testing.T, c *Config) { + asserts: func(t *testing.T, c *Config, grpcOption bool) { // Function value comparisons are disallowed, test non-default // behavior of a AggregationSelector here to ensure our "catch // all" was set. @@ -412,12 +450,12 @@ func TestConfigs(t *testing.T) { // Proxy Tests { name: "Test With Proxy", - opts: []HTTPOption{ + opts: []GenericOption{ WithProxy(func(r *http.Request) (*url.URL, error) { return url.Parse("http://proxy.com") }), }, - asserts: func(t *testing.T, c *Config) { + asserts: func(t *testing.T, c *Config, grpcOption bool) { assert.NotNil(t, c.Metrics.Proxy) proxyURL, err := c.Metrics.Proxy(&http.Request{}) assert.NoError(t, err) @@ -426,8 +464,8 @@ func TestConfigs(t *testing.T) { }, { name: "Test Without Proxy", - opts: []HTTPOption{}, - asserts: func(t *testing.T, c *Config) { + opts: []GenericOption{}, + asserts: func(t *testing.T, c *Config, grpcOption bool) { assert.Nil(t, c.Metrics.Proxy) }, }, @@ -444,8 +482,12 @@ func TestConfigs(t *testing.T) { t.Cleanup(func() { DefaultEnvOptionsReader = origEOR }) // Tests Generic options as HTTP Options - cfg := NewHTTPConfig(tt.opts...) - tt.asserts(t, &cfg) + cfg := NewHTTPConfig(asHTTPOptions(tt.opts)...) + tt.asserts(t, &cfg, false) + + // Tests Generic options as gRPC Options + cfg = NewGRPCConfig(asGRPCOptions(tt.opts)...) + tt.asserts(t, &cfg, true) }) } } @@ -458,6 +500,22 @@ func deltaSelector(metric.InstrumentKind) metricdata.Temporality { return metricdata.DeltaTemporality } +func asHTTPOptions(opts []GenericOption) []HTTPOption { + converted := make([]HTTPOption, len(opts)) + for i, o := range opts { + converted[i] = NewHTTPOption(o.ApplyHTTPOption) + } + return converted +} + +func asGRPCOptions(opts []GenericOption) []GRPCOption { + converted := make([]GRPCOption, len(opts)) + for i, o := range opts { + converted[i] = NewGRPCOption(o.ApplyGRPCOption) + } + return converted +} + func TestCleanPath(t *testing.T) { type args struct { urlPath string diff --git a/exporters/otlp/otlpmetric/otlpmetrichttp/internal/otest/client.go b/exporters/otlp/otlpmetric/otlpmetrichttp/internal/otest/client.go index dcc2bbb58e0..9f6e6122fcc 100644 --- a/exporters/otlp/otlpmetric/otlpmetrichttp/internal/otest/client.go +++ b/exporters/otlp/otlpmetric/otlpmetrichttp/internal/otest/client.go @@ -19,10 +19,10 @@ import ( "go.opentelemetry.io/otel" semconv "go.opentelemetry.io/otel/semconv/v1.24.0" - collpb "go.opentelemetry.io/proto/slim/otlp/collector/metrics/v1" - cpb "go.opentelemetry.io/proto/slim/otlp/common/v1" - mpb "go.opentelemetry.io/proto/slim/otlp/metrics/v1" - rpb "go.opentelemetry.io/proto/slim/otlp/resource/v1" + collpb "go.opentelemetry.io/proto/otlp/collector/metrics/v1" + cpb "go.opentelemetry.io/proto/otlp/common/v1" + mpb "go.opentelemetry.io/proto/otlp/metrics/v1" + rpb "go.opentelemetry.io/proto/otlp/resource/v1" ) var ( diff --git a/exporters/otlp/otlpmetric/otlpmetrichttp/internal/otest/client_test.go b/exporters/otlp/otlpmetric/otlpmetrichttp/internal/otest/client_test.go index 12f71d7cbb9..ccf3a578d70 100644 --- a/exporters/otlp/otlpmetric/otlpmetrichttp/internal/otest/client_test.go +++ b/exporters/otlp/otlpmetric/otlpmetrichttp/internal/otest/client_test.go @@ -14,8 +14,8 @@ import ( "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp/internal" "go.opentelemetry.io/otel/sdk/metric" "go.opentelemetry.io/otel/sdk/metric/metricdata" - cpb "go.opentelemetry.io/proto/slim/otlp/collector/metrics/v1" - mpb "go.opentelemetry.io/proto/slim/otlp/metrics/v1" + cpb "go.opentelemetry.io/proto/otlp/collector/metrics/v1" + mpb "go.opentelemetry.io/proto/otlp/metrics/v1" ) type client struct { diff --git a/exporters/otlp/otlpmetric/otlpmetrichttp/internal/otest/collector.go b/exporters/otlp/otlpmetric/otlpmetrichttp/internal/otest/collector.go index 71489ff2d0f..178dcde6c36 100644 --- a/exporters/otlp/otlpmetric/otlpmetrichttp/internal/otest/collector.go +++ b/exporters/otlp/otlpmetric/otlpmetrichttp/internal/otest/collector.go @@ -1,3 +1,6 @@ +// Code created by gotmpl. DO NOT MODIFY. +// source: internal/shared/otlp/otlpmetric/otest/collector.go.tmpl + // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 @@ -24,11 +27,13 @@ import ( "sync" "time" + "google.golang.org/grpc" + "google.golang.org/grpc/metadata" "google.golang.org/protobuf/proto" "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp/internal/oconf" - collpb "go.opentelemetry.io/proto/slim/otlp/collector/metrics/v1" - mpb "go.opentelemetry.io/proto/slim/otlp/metrics/v1" + collpb "go.opentelemetry.io/proto/otlp/collector/metrics/v1" + mpb "go.opentelemetry.io/proto/otlp/metrics/v1" ) // Collector is the collection target a Client sends metric uploads to. @@ -36,7 +41,6 @@ type Collector interface { Collect() *Storage } -// ExportResult represents an export response. type ExportResult struct { Response *collpb.ExportMetricsServiceResponse Err error @@ -70,6 +74,93 @@ func (s *Storage) Dump() []*mpb.ResourceMetrics { return data } +// GRPCCollector is an OTLP gRPC server that collects all requests it receives. +type GRPCCollector struct { + collpb.UnimplementedMetricsServiceServer + + headersMu sync.Mutex + headers metadata.MD + storage *Storage + + resultCh <-chan ExportResult + listener net.Listener + srv *grpc.Server +} + +// NewGRPCCollector returns a *GRPCCollector that is listening at the provided +// endpoint. +// +// If endpoint is an empty string, the returned collector will be listening on +// the localhost interface at an OS chosen port. +// +// If errCh is not nil, the collector will respond to Export calls with errors +// sent on that channel. This means that if errCh is not nil Export calls will +// block until an error is received. +func NewGRPCCollector(endpoint string, resultCh <-chan ExportResult) (*GRPCCollector, error) { + if endpoint == "" { + endpoint = "localhost:0" + } + + c := &GRPCCollector{ + storage: NewStorage(), + resultCh: resultCh, + } + + var err error + c.listener, err = net.Listen("tcp", endpoint) + if err != nil { + return nil, err + } + + c.srv = grpc.NewServer() + collpb.RegisterMetricsServiceServer(c.srv, c) + go func() { _ = c.srv.Serve(c.listener) }() + + return c, nil +} + +// Shutdown shuts down the gRPC server closing all open connections and +// listeners immediately. +func (c *GRPCCollector) Shutdown() { c.srv.Stop() } + +// Addr returns the net.Addr c is listening at. +func (c *GRPCCollector) Addr() net.Addr { + return c.listener.Addr() +} + +// Collect returns the Storage holding all collected requests. +func (c *GRPCCollector) Collect() *Storage { + return c.storage +} + +// Headers returns the headers received for all requests. +func (c *GRPCCollector) Headers() map[string][]string { + // Makes a copy. + c.headersMu.Lock() + defer c.headersMu.Unlock() + return metadata.Join(c.headers) +} + +// Export handles the export req. +func (c *GRPCCollector) Export(ctx context.Context, req *collpb.ExportMetricsServiceRequest) (*collpb.ExportMetricsServiceResponse, error) { + c.storage.Add(req) + + if h, ok := metadata.FromIncomingContext(ctx); ok { + c.headersMu.Lock() + c.headers = metadata.Join(c.headers, h) + c.headersMu.Unlock() + } + + if c.resultCh != nil { + r := <-c.resultCh + if r.Response == nil { + return &collpb.ExportMetricsServiceResponse{}, r.Err + } + return r.Response, r.Err + } + return &collpb.ExportMetricsServiceResponse{}, nil +} + var emptyExportMetricsServiceResponse = func() []byte { body := collpb.ExportMetricsServiceResponse{} r, err := proto.Marshal(&body) @@ -79,7 +170,6 @@ var emptyExportMetricsServiceResponse = func() []byte { return r }() -// HTTPResponseError is used to mock a HTTP response error. type HTTPResponseError struct { Err error Status int diff --git a/exporters/otlp/otlpmetric/otlpmetrichttp/internal/transform/attribute.go b/exporters/otlp/otlpmetric/otlpmetrichttp/internal/transform/attribute.go index 8cfe42191e3..d607da78eaf 100644 --- a/exporters/otlp/otlpmetric/otlpmetrichttp/internal/transform/attribute.go +++ b/exporters/otlp/otlpmetric/otlpmetrichttp/internal/transform/attribute.go @@ -8,7 +8,7 @@ package transform // import "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/ import ( "go.opentelemetry.io/otel/attribute" - cpb "go.opentelemetry.io/proto/slim/otlp/common/v1" + cpb "go.opentelemetry.io/proto/otlp/common/v1" ) // AttrIter transforms an attribute iterator into OTLP key-values. diff --git a/exporters/otlp/otlpmetric/otlpmetrichttp/internal/transform/attribute_test.go b/exporters/otlp/otlpmetric/otlpmetrichttp/internal/transform/attribute_test.go index bd2b6c305a7..0815ba988d2 100644 --- a/exporters/otlp/otlpmetric/otlpmetrichttp/internal/transform/attribute_test.go +++ b/exporters/otlp/otlpmetric/otlpmetrichttp/internal/transform/attribute_test.go @@ -12,7 +12,7 @@ import ( "github.com/stretchr/testify/assert" "go.opentelemetry.io/otel/attribute" - cpb "go.opentelemetry.io/proto/slim/otlp/common/v1" + cpb "go.opentelemetry.io/proto/otlp/common/v1" ) var ( diff --git a/exporters/otlp/otlpmetric/otlpmetrichttp/internal/transform/error.go b/exporters/otlp/otlpmetric/otlpmetrichttp/internal/transform/error.go index f9700704ad8..60d0d1f72ae 100644 --- a/exporters/otlp/otlpmetric/otlpmetrichttp/internal/transform/error.go +++ b/exporters/otlp/otlpmetric/otlpmetrichttp/internal/transform/error.go @@ -11,7 +11,7 @@ import ( "fmt" "strings" - mpb "go.opentelemetry.io/proto/slim/otlp/metrics/v1" + mpb "go.opentelemetry.io/proto/otlp/metrics/v1" ) var ( diff --git a/exporters/otlp/otlpmetric/otlpmetrichttp/internal/transform/metricdata.go b/exporters/otlp/otlpmetric/otlpmetrichttp/internal/transform/metricdata.go index 34fca043c26..04c2ce75704 100644 --- a/exporters/otlp/otlpmetric/otlpmetrichttp/internal/transform/metricdata.go +++ b/exporters/otlp/otlpmetric/otlpmetrichttp/internal/transform/metricdata.go @@ -13,9 +13,9 @@ import ( "time" "go.opentelemetry.io/otel/sdk/metric/metricdata" - cpb "go.opentelemetry.io/proto/slim/otlp/common/v1" - mpb "go.opentelemetry.io/proto/slim/otlp/metrics/v1" - rpb "go.opentelemetry.io/proto/slim/otlp/resource/v1" + cpb "go.opentelemetry.io/proto/otlp/common/v1" + mpb "go.opentelemetry.io/proto/otlp/metrics/v1" + rpb "go.opentelemetry.io/proto/otlp/resource/v1" ) // ResourceMetrics returns an OTLP ResourceMetrics generated from rm. If rm diff --git a/exporters/otlp/otlpmetric/otlpmetrichttp/internal/transform/metricdata_test.go b/exporters/otlp/otlpmetric/otlpmetrichttp/internal/transform/metricdata_test.go index 00b20c0ed67..b0bc71e9edb 100644 --- a/exporters/otlp/otlpmetric/otlpmetrichttp/internal/transform/metricdata_test.go +++ b/exporters/otlp/otlpmetric/otlpmetrichttp/internal/transform/metricdata_test.go @@ -18,9 +18,9 @@ import ( "go.opentelemetry.io/otel/sdk/metric/metricdata" "go.opentelemetry.io/otel/sdk/resource" semconv "go.opentelemetry.io/otel/semconv/v1.24.0" - cpb "go.opentelemetry.io/proto/slim/otlp/common/v1" - mpb "go.opentelemetry.io/proto/slim/otlp/metrics/v1" - rpb "go.opentelemetry.io/proto/slim/otlp/resource/v1" + cpb "go.opentelemetry.io/proto/otlp/common/v1" + mpb "go.opentelemetry.io/proto/otlp/metrics/v1" + rpb "go.opentelemetry.io/proto/otlp/resource/v1" ) type unknownAggT struct { diff --git a/internal/shared/otlp/otlpmetric/oconf/envconfig.go.tmpl b/internal/shared/otlp/otlpmetric/oconf/envconfig.go.tmpl new file mode 100644 index 00000000000..5334c9b3911 --- /dev/null +++ b/internal/shared/otlp/otlpmetric/oconf/envconfig.go.tmpl @@ -0,0 +1,210 @@ +// Code created by gotmpl. DO NOT MODIFY. +// source: internal/shared/otlp/otlpmetric/oconf/envconfig.go.tmpl + +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package oconf + +import ( + "crypto/tls" + "crypto/x509" + "net/url" + "os" + "path" + "strings" + "time" + + "{{ .envconfigImportPath }}" + "go.opentelemetry.io/otel/internal/global" + "go.opentelemetry.io/otel/sdk/metric" + "go.opentelemetry.io/otel/sdk/metric/metricdata" +) + +// DefaultEnvOptionsReader is the default environments reader. +var DefaultEnvOptionsReader = envconfig.EnvOptionsReader{ + GetEnv: os.Getenv, + ReadFile: os.ReadFile, + Namespace: "OTEL_EXPORTER_OTLP", +} + +// ApplyGRPCEnvConfigs applies the env configurations for gRPC. +func ApplyGRPCEnvConfigs(cfg Config) Config { + opts := getOptionsFromEnv() + for _, opt := range opts { + cfg = opt.ApplyGRPCOption(cfg) + } + return cfg +} + +// ApplyHTTPEnvConfigs applies the env configurations for HTTP. +func ApplyHTTPEnvConfigs(cfg Config) Config { + opts := getOptionsFromEnv() + for _, opt := range opts { + cfg = opt.ApplyHTTPOption(cfg) + } + return cfg +} + +func getOptionsFromEnv() []GenericOption { + opts := []GenericOption{} + + tlsConf := &tls.Config{} + DefaultEnvOptionsReader.Apply( + envconfig.WithURL("ENDPOINT", func(u *url.URL) { + opts = append(opts, withEndpointScheme(u)) + opts = append(opts, newSplitOption(func(cfg Config) Config { + cfg.Metrics.Endpoint = u.Host + // For OTLP/HTTP endpoint URLs without a per-signal + // configuration, the passed endpoint is used as a base URL + // and the signals are sent to these paths relative to that. + cfg.Metrics.URLPath = path.Join(u.Path, DefaultMetricsPath) + return cfg + }, withEndpointForGRPC(u))) + }), + envconfig.WithURL("METRICS_ENDPOINT", func(u *url.URL) { + opts = append(opts, withEndpointScheme(u)) + opts = append(opts, newSplitOption(func(cfg Config) Config { + cfg.Metrics.Endpoint = u.Host + // For endpoint URLs for OTLP/HTTP per-signal variables, the + // URL MUST be used as-is without any modification. The only + // exception is that if an URL contains no path part, the root + // path / MUST be used. + path := u.Path + if path == "" { + path = "/" + } + cfg.Metrics.URLPath = path + return cfg + }, withEndpointForGRPC(u))) + }), + envconfig.WithCertPool("CERTIFICATE", func(p *x509.CertPool) { tlsConf.RootCAs = p }), + envconfig.WithCertPool("METRICS_CERTIFICATE", func(p *x509.CertPool) { tlsConf.RootCAs = p }), + envconfig.WithClientCert("CLIENT_CERTIFICATE", "CLIENT_KEY", func(c tls.Certificate) { tlsConf.Certificates = []tls.Certificate{c} }), + envconfig.WithClientCert("METRICS_CLIENT_CERTIFICATE", "METRICS_CLIENT_KEY", func(c tls.Certificate) { tlsConf.Certificates = []tls.Certificate{c} }), + envconfig.WithBool("INSECURE", func(b bool) { opts = append(opts, withInsecure(b)) }), + envconfig.WithBool("METRICS_INSECURE", func(b bool) { opts = append(opts, withInsecure(b)) }), + withTLSConfig(tlsConf, func(c *tls.Config) { opts = append(opts, WithTLSClientConfig(c)) }), + envconfig.WithHeaders("HEADERS", func(h map[string]string) { opts = append(opts, WithHeaders(h)) }), + envconfig.WithHeaders("METRICS_HEADERS", func(h map[string]string) { opts = append(opts, WithHeaders(h)) }), + WithEnvCompression("COMPRESSION", func(c Compression) { opts = append(opts, WithCompression(c)) }), + WithEnvCompression("METRICS_COMPRESSION", func(c Compression) { opts = append(opts, WithCompression(c)) }), + envconfig.WithDuration("TIMEOUT", func(d time.Duration) { opts = append(opts, WithTimeout(d)) }), + envconfig.WithDuration("METRICS_TIMEOUT", func(d time.Duration) { opts = append(opts, WithTimeout(d)) }), + withEnvTemporalityPreference("METRICS_TEMPORALITY_PREFERENCE", func(t metric.TemporalitySelector) { opts = append(opts, WithTemporalitySelector(t)) }), + withEnvAggPreference("METRICS_DEFAULT_HISTOGRAM_AGGREGATION", func(a metric.AggregationSelector) { opts = append(opts, WithAggregationSelector(a)) }), + ) + + return opts +} + +func withEndpointForGRPC(u *url.URL) func(cfg Config) Config { + return func(cfg Config) Config { + // For OTLP/gRPC endpoints, this is the target to which the + // exporter is going to send telemetry. + cfg.Metrics.Endpoint = path.Join(u.Host, u.Path) + return cfg + } +} + +// WithEnvCompression retrieves the specified config and passes it to ConfigFn as a Compression. +func WithEnvCompression(n string, fn func(Compression)) func(e *envconfig.EnvOptionsReader) { + return func(e *envconfig.EnvOptionsReader) { + if v, ok := e.GetEnvValue(n); ok { + cp := NoCompression + if v == "gzip" { + cp = GzipCompression + } + + fn(cp) + } + } +} + +func withEndpointScheme(u *url.URL) GenericOption { + switch strings.ToLower(u.Scheme) { + case "http", "unix": + return WithInsecure() + default: + return WithSecure() + } +} + +// revive:disable-next-line:flag-parameter +func withInsecure(b bool) GenericOption { + if b { + return WithInsecure() + } + return WithSecure() +} + +func withTLSConfig(c *tls.Config, fn func(*tls.Config)) func(e *envconfig.EnvOptionsReader) { + return func(e *envconfig.EnvOptionsReader) { + if c.RootCAs != nil || len(c.Certificates) > 0 { + fn(c) + } + } +} + +func withEnvTemporalityPreference(n string, fn func(metric.TemporalitySelector)) func(e *envconfig.EnvOptionsReader) { + return func(e *envconfig.EnvOptionsReader) { + if s, ok := e.GetEnvValue(n); ok { + switch strings.ToLower(s) { + case "cumulative": + fn(cumulativeTemporality) + case "delta": + fn(deltaTemporality) + case "lowmemory": + fn(lowMemory) + default: + global.Warn("OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE is set to an invalid value, ignoring.", "value", s) + } + } + } +} + +func cumulativeTemporality(metric.InstrumentKind) metricdata.Temporality { + return metricdata.CumulativeTemporality +} + +func deltaTemporality(ik metric.InstrumentKind) metricdata.Temporality { + switch ik { + case metric.InstrumentKindCounter, metric.InstrumentKindHistogram, metric.InstrumentKindObservableCounter: + return metricdata.DeltaTemporality + default: + return metricdata.CumulativeTemporality + } +} + +func lowMemory(ik metric.InstrumentKind) metricdata.Temporality { + switch ik { + case metric.InstrumentKindCounter, metric.InstrumentKindHistogram: + return metricdata.DeltaTemporality + default: + return metricdata.CumulativeTemporality + } +} + +func withEnvAggPreference(n string, fn func(metric.AggregationSelector)) func(e *envconfig.EnvOptionsReader) { + return func(e *envconfig.EnvOptionsReader) { + if s, ok := e.GetEnvValue(n); ok { + switch strings.ToLower(s) { + case "explicit_bucket_histogram": + fn(metric.DefaultAggregationSelector) + case "base2_exponential_bucket_histogram": + fn(func(kind metric.InstrumentKind) metric.Aggregation { + if kind == metric.InstrumentKindHistogram { + return metric.AggregationBase2ExponentialHistogram{ + MaxSize: 160, + MaxScale: 20, + NoMinMax: false, + } + } + return metric.DefaultAggregationSelector(kind) + }) + default: + global.Warn("OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION is set to an invalid value, ignoring.", "value", s) + } + } + } +} diff --git a/internal/shared/otlp/otlpmetric/oconf/envconfig_test.go.tmpl b/internal/shared/otlp/otlpmetric/oconf/envconfig_test.go.tmpl new file mode 100644 index 00000000000..5c621d7b47e --- /dev/null +++ b/internal/shared/otlp/otlpmetric/oconf/envconfig_test.go.tmpl @@ -0,0 +1,165 @@ +// Code created by gotmpl. DO NOT MODIFY. +// source: internal/shared/otlp/otlpmetric/oconf/envconfig_test.go.tmpl + +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package oconf + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "go.opentelemetry.io/otel/sdk/metric" + "go.opentelemetry.io/otel/sdk/metric/metricdata" +) + +func TestWithEnvTemporalityPreference(t *testing.T) { + origReader := DefaultEnvOptionsReader.GetEnv + tests := []struct { + name string + envValue string + want map[metric.InstrumentKind]metricdata.Temporality + }{ + { + name: "default do not set the selector", + envValue: "", + }, + { + name: "non-normative do not set the selector", + envValue: "non-normative", + }, + { + name: "cumulative", + envValue: "cumulative", + want: map[metric.InstrumentKind]metricdata.Temporality{ + metric.InstrumentKindCounter: metricdata.CumulativeTemporality, + metric.InstrumentKindHistogram: metricdata.CumulativeTemporality, + metric.InstrumentKindUpDownCounter: metricdata.CumulativeTemporality, + metric.InstrumentKindObservableCounter: metricdata.CumulativeTemporality, + metric.InstrumentKindObservableUpDownCounter: metricdata.CumulativeTemporality, + metric.InstrumentKindObservableGauge: metricdata.CumulativeTemporality, + }, + }, + { + name: "delta", + envValue: "delta", + want: map[metric.InstrumentKind]metricdata.Temporality{ + metric.InstrumentKindCounter: metricdata.DeltaTemporality, + metric.InstrumentKindHistogram: metricdata.DeltaTemporality, + metric.InstrumentKindUpDownCounter: metricdata.CumulativeTemporality, + metric.InstrumentKindObservableCounter: metricdata.DeltaTemporality, + metric.InstrumentKindObservableUpDownCounter: metricdata.CumulativeTemporality, + metric.InstrumentKindObservableGauge: metricdata.CumulativeTemporality, + }, + }, + { + name: "lowmemory", + envValue: "lowmemory", + want: map[metric.InstrumentKind]metricdata.Temporality{ + metric.InstrumentKindCounter: metricdata.DeltaTemporality, + metric.InstrumentKindHistogram: metricdata.DeltaTemporality, + metric.InstrumentKindUpDownCounter: metricdata.CumulativeTemporality, + metric.InstrumentKindObservableCounter: metricdata.CumulativeTemporality, + metric.InstrumentKindObservableUpDownCounter: metricdata.CumulativeTemporality, + metric.InstrumentKindObservableGauge: metricdata.CumulativeTemporality, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + DefaultEnvOptionsReader.GetEnv = func(key string) string { + if key == "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE" { + return tt.envValue + } + return origReader(key) + } + cfg := Config{} + cfg = ApplyGRPCEnvConfigs(cfg) + + if tt.want == nil { + // There is no function set, the SDK's default is used. + assert.Nil(t, cfg.Metrics.TemporalitySelector) + return + } + + require.NotNil(t, cfg.Metrics.TemporalitySelector) + for ik, want := range tt.want { + assert.Equal(t, want, cfg.Metrics.TemporalitySelector(ik)) + } + }) + } + DefaultEnvOptionsReader.GetEnv = origReader +} + +func TestWithEnvAggPreference(t *testing.T) { + origReader := DefaultEnvOptionsReader.GetEnv + tests := []struct { + name string + envValue string + want map[metric.InstrumentKind]metric.Aggregation + }{ + { + name: "default do not set the selector", + envValue: "", + }, + { + name: "non-normative do not set the selector", + envValue: "non-normative", + }, + { + name: "explicit_bucket_histogram", + envValue: "explicit_bucket_histogram", + want: map[metric.InstrumentKind]metric.Aggregation{ + metric.InstrumentKindCounter: metric.DefaultAggregationSelector(metric.InstrumentKindCounter), + metric.InstrumentKindHistogram: metric.DefaultAggregationSelector(metric.InstrumentKindHistogram), + metric.InstrumentKindUpDownCounter: metric.DefaultAggregationSelector(metric.InstrumentKindUpDownCounter), + metric.InstrumentKindObservableCounter: metric.DefaultAggregationSelector(metric.InstrumentKindObservableCounter), + metric.InstrumentKindObservableUpDownCounter: metric.DefaultAggregationSelector(metric.InstrumentKindObservableUpDownCounter), + metric.InstrumentKindObservableGauge: metric.DefaultAggregationSelector(metric.InstrumentKindObservableGauge), + }, + }, + { + name: "base2_exponential_bucket_histogram", + envValue: "base2_exponential_bucket_histogram", + want: map[metric.InstrumentKind]metric.Aggregation{ + metric.InstrumentKindCounter: metric.DefaultAggregationSelector(metric.InstrumentKindCounter), + metric.InstrumentKindHistogram: metric.AggregationBase2ExponentialHistogram{ + MaxSize: 160, + MaxScale: 20, + NoMinMax: false, + }, + metric.InstrumentKindUpDownCounter: metric.DefaultAggregationSelector(metric.InstrumentKindUpDownCounter), + metric.InstrumentKindObservableCounter: metric.DefaultAggregationSelector(metric.InstrumentKindObservableCounter), + metric.InstrumentKindObservableUpDownCounter: metric.DefaultAggregationSelector(metric.InstrumentKindObservableUpDownCounter), + metric.InstrumentKindObservableGauge: metric.DefaultAggregationSelector(metric.InstrumentKindObservableGauge), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + DefaultEnvOptionsReader.GetEnv = func(key string) string { + if key == "OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION" { + return tt.envValue + } + return origReader(key) + } + cfg := Config{} + cfg = ApplyGRPCEnvConfigs(cfg) + + if tt.want == nil { + // There is no function set, the SDK's default is used. + assert.Nil(t, cfg.Metrics.AggregationSelector) + return + } + + require.NotNil(t, cfg.Metrics.AggregationSelector) + for ik, want := range tt.want { + assert.Equal(t, want, cfg.Metrics.AggregationSelector(ik)) + } + }) + } + DefaultEnvOptionsReader.GetEnv = origReader +} diff --git a/internal/shared/otlp/otlpmetric/oconf/options.go.tmpl b/internal/shared/otlp/otlpmetric/oconf/options.go.tmpl new file mode 100644 index 00000000000..071a144fdf5 --- /dev/null +++ b/internal/shared/otlp/otlpmetric/oconf/options.go.tmpl @@ -0,0 +1,376 @@ +// Code created by gotmpl. DO NOT MODIFY. +// source: internal/shared/otlp/otlpmetric/oconf/options.go.tmpl + +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package oconf + +import ( + "crypto/tls" + "fmt" + "net/http" + "net/url" + "path" + "strings" + "time" + + "google.golang.org/grpc" + "google.golang.org/grpc/backoff" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/encoding/gzip" + + "{{ .retryImportPath }}" + "go.opentelemetry.io/otel/internal/global" + "go.opentelemetry.io/otel/sdk/metric" +) + +const ( + // DefaultMaxAttempts describes how many times the driver + // should retry the sending of the payload in case of a + // retryable error. + DefaultMaxAttempts int = 5 + // DefaultMetricsPath is a default URL path for endpoint that + // receives metrics. + DefaultMetricsPath string = "/v1/metrics" + // DefaultBackoff is a default base backoff time used in the + // exponential backoff strategy. + DefaultBackoff time.Duration = 300 * time.Millisecond + // DefaultTimeout is a default max waiting time for the backend to process + // each span or metrics batch. + DefaultTimeout time.Duration = 10 * time.Second +) + +type ( + // HTTPTransportProxyFunc is a function that resolves which URL to use as proxy for a given request. + // This type is compatible with `http.Transport.Proxy` and can be used to set a custom proxy function to the OTLP HTTP client. + HTTPTransportProxyFunc func(*http.Request) (*url.URL, error) + + SignalConfig struct { + Endpoint string + Insecure bool + TLSCfg *tls.Config + Headers map[string]string + Compression Compression + Timeout time.Duration + URLPath string + + // gRPC configurations + GRPCCredentials credentials.TransportCredentials + + TemporalitySelector metric.TemporalitySelector + AggregationSelector metric.AggregationSelector + + Proxy HTTPTransportProxyFunc + } + + Config struct { + // Signal specific configurations + Metrics SignalConfig + + RetryConfig retry.Config + + // gRPC configurations + ReconnectionPeriod time.Duration + ServiceConfig string + DialOptions []grpc.DialOption + GRPCConn *grpc.ClientConn + } +) + +// NewHTTPConfig returns a new Config with all settings applied from opts and +// any unset setting using the default HTTP config values. +func NewHTTPConfig(opts ...HTTPOption) Config { + cfg := Config{ + Metrics: SignalConfig{ + Endpoint: fmt.Sprintf("%s:%d", DefaultCollectorHost, DefaultCollectorHTTPPort), + URLPath: DefaultMetricsPath, + Compression: NoCompression, + Timeout: DefaultTimeout, + + TemporalitySelector: metric.DefaultTemporalitySelector, + AggregationSelector: metric.DefaultAggregationSelector, + }, + RetryConfig: retry.DefaultConfig, + } + cfg = ApplyHTTPEnvConfigs(cfg) + for _, opt := range opts { + cfg = opt.ApplyHTTPOption(cfg) + } + cfg.Metrics.URLPath = cleanPath(cfg.Metrics.URLPath, DefaultMetricsPath) + return cfg +} + +// cleanPath returns a path with all spaces trimmed and all redundancies +// removed. If urlPath is empty or cleaning it results in an empty string, +// defaultPath is returned instead. +func cleanPath(urlPath string, defaultPath string) string { + tmp := path.Clean(strings.TrimSpace(urlPath)) + if tmp == "." { + return defaultPath + } + if !path.IsAbs(tmp) { + tmp = fmt.Sprintf("/%s", tmp) + } + return tmp +} + +// NewGRPCConfig returns a new Config with all settings applied from opts and +// any unset setting using the default gRPC config values. +func NewGRPCConfig(opts ...GRPCOption) Config { + cfg := Config{ + Metrics: SignalConfig{ + Endpoint: fmt.Sprintf("%s:%d", DefaultCollectorHost, DefaultCollectorGRPCPort), + URLPath: DefaultMetricsPath, + Compression: NoCompression, + Timeout: DefaultTimeout, + + TemporalitySelector: metric.DefaultTemporalitySelector, + AggregationSelector: metric.DefaultAggregationSelector, + }, + RetryConfig: retry.DefaultConfig, + } + cfg = ApplyGRPCEnvConfigs(cfg) + for _, opt := range opts { + cfg = opt.ApplyGRPCOption(cfg) + } + + if cfg.ServiceConfig != "" { + cfg.DialOptions = append(cfg.DialOptions, grpc.WithDefaultServiceConfig(cfg.ServiceConfig)) + } + // Priroritize GRPCCredentials over Insecure (passing both is an error). + if cfg.Metrics.GRPCCredentials != nil { + cfg.DialOptions = append(cfg.DialOptions, grpc.WithTransportCredentials(cfg.Metrics.GRPCCredentials)) + } else if cfg.Metrics.Insecure { + cfg.DialOptions = append(cfg.DialOptions, grpc.WithTransportCredentials(insecure.NewCredentials())) + } else { + // Default to using the host's root CA. + creds := credentials.NewTLS(nil) + cfg.Metrics.GRPCCredentials = creds + cfg.DialOptions = append(cfg.DialOptions, grpc.WithTransportCredentials(creds)) + } + if cfg.Metrics.Compression == GzipCompression { + cfg.DialOptions = append(cfg.DialOptions, grpc.WithDefaultCallOptions(grpc.UseCompressor(gzip.Name))) + } + if cfg.ReconnectionPeriod != 0 { + p := grpc.ConnectParams{ + Backoff: backoff.DefaultConfig, + MinConnectTimeout: cfg.ReconnectionPeriod, + } + cfg.DialOptions = append(cfg.DialOptions, grpc.WithConnectParams(p)) + } + + return cfg +} + +type ( + // GenericOption applies an option to the HTTP or gRPC driver. + GenericOption interface { + ApplyHTTPOption(Config) Config + ApplyGRPCOption(Config) Config + + // A private method to prevent users implementing the + // interface and so future additions to it will not + // violate compatibility. + private() + } + + // HTTPOption applies an option to the HTTP driver. + HTTPOption interface { + ApplyHTTPOption(Config) Config + + // A private method to prevent users implementing the + // interface and so future additions to it will not + // violate compatibility. + private() + } + + // GRPCOption applies an option to the gRPC driver. + GRPCOption interface { + ApplyGRPCOption(Config) Config + + // A private method to prevent users implementing the + // interface and so future additions to it will not + // violate compatibility. + private() + } +) + +// genericOption is an option that applies the same logic +// for both gRPC and HTTP. +type genericOption struct { + fn func(Config) Config +} + +func (g *genericOption) ApplyGRPCOption(cfg Config) Config { + return g.fn(cfg) +} + +func (g *genericOption) ApplyHTTPOption(cfg Config) Config { + return g.fn(cfg) +} + +func (genericOption) private() {} + +func newGenericOption(fn func(cfg Config) Config) GenericOption { + return &genericOption{fn: fn} +} + +// splitOption is an option that applies different logics +// for gRPC and HTTP. +type splitOption struct { + httpFn func(Config) Config + grpcFn func(Config) Config +} + +func (g *splitOption) ApplyGRPCOption(cfg Config) Config { + return g.grpcFn(cfg) +} + +func (g *splitOption) ApplyHTTPOption(cfg Config) Config { + return g.httpFn(cfg) +} + +func (splitOption) private() {} + +func newSplitOption(httpFn func(cfg Config) Config, grpcFn func(cfg Config) Config) GenericOption { + return &splitOption{httpFn: httpFn, grpcFn: grpcFn} +} + +// httpOption is an option that is only applied to the HTTP driver. +type httpOption struct { + fn func(Config) Config +} + +func (h *httpOption) ApplyHTTPOption(cfg Config) Config { + return h.fn(cfg) +} + +func (httpOption) private() {} + +func NewHTTPOption(fn func(cfg Config) Config) HTTPOption { + return &httpOption{fn: fn} +} + +// grpcOption is an option that is only applied to the gRPC driver. +type grpcOption struct { + fn func(Config) Config +} + +func (h *grpcOption) ApplyGRPCOption(cfg Config) Config { + return h.fn(cfg) +} + +func (grpcOption) private() {} + +func NewGRPCOption(fn func(cfg Config) Config) GRPCOption { + return &grpcOption{fn: fn} +} + +// Generic Options + +func WithEndpoint(endpoint string) GenericOption { + return newGenericOption(func(cfg Config) Config { + cfg.Metrics.Endpoint = endpoint + return cfg + }) +} + +func WithEndpointURL(v string) GenericOption { + return newGenericOption(func(cfg Config) Config { + u, err := url.Parse(v) + if err != nil { + global.Error(err, "otlpmetric: parse endpoint url", "url", v) + return cfg + } + + cfg.Metrics.Endpoint = u.Host + cfg.Metrics.URLPath = u.Path + if u.Scheme != "https" { + cfg.Metrics.Insecure = true + } + + return cfg + }) +} + +func WithCompression(compression Compression) GenericOption { + return newGenericOption(func(cfg Config) Config { + cfg.Metrics.Compression = compression + return cfg + }) +} + +func WithURLPath(urlPath string) GenericOption { + return newGenericOption(func(cfg Config) Config { + cfg.Metrics.URLPath = urlPath + return cfg + }) +} + +func WithRetry(rc retry.Config) GenericOption { + return newGenericOption(func(cfg Config) Config { + cfg.RetryConfig = rc + return cfg + }) +} + +func WithTLSClientConfig(tlsCfg *tls.Config) GenericOption { + return newSplitOption(func(cfg Config) Config { + cfg.Metrics.TLSCfg = tlsCfg.Clone() + return cfg + }, func(cfg Config) Config { + cfg.Metrics.GRPCCredentials = credentials.NewTLS(tlsCfg) + return cfg + }) +} + +func WithInsecure() GenericOption { + return newGenericOption(func(cfg Config) Config { + cfg.Metrics.Insecure = true + return cfg + }) +} + +func WithSecure() GenericOption { + return newGenericOption(func(cfg Config) Config { + cfg.Metrics.Insecure = false + return cfg + }) +} + +func WithHeaders(headers map[string]string) GenericOption { + return newGenericOption(func(cfg Config) Config { + cfg.Metrics.Headers = headers + return cfg + }) +} + +func WithTimeout(duration time.Duration) GenericOption { + return newGenericOption(func(cfg Config) Config { + cfg.Metrics.Timeout = duration + return cfg + }) +} + +func WithTemporalitySelector(selector metric.TemporalitySelector) GenericOption { + return newGenericOption(func(cfg Config) Config { + cfg.Metrics.TemporalitySelector = selector + return cfg + }) +} + +func WithAggregationSelector(selector metric.AggregationSelector) GenericOption { + return newGenericOption(func(cfg Config) Config { + cfg.Metrics.AggregationSelector = selector + return cfg + }) +} + +func WithProxy(pf HTTPTransportProxyFunc) GenericOption { + return newGenericOption(func(cfg Config) Config { + cfg.Metrics.Proxy = pf + return cfg + }) +} diff --git a/internal/shared/otlp/otlpmetric/oconf/options_test.go.tmpl b/internal/shared/otlp/otlpmetric/oconf/options_test.go.tmpl new file mode 100644 index 00000000000..f266628e2ae --- /dev/null +++ b/internal/shared/otlp/otlpmetric/oconf/options_test.go.tmpl @@ -0,0 +1,583 @@ +// Code created by gotmpl. DO NOT MODIFY. +// source: internal/shared/otlp/otlpmetric/oconf/options_test.go.tmpl + +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package oconf + +import ( + "errors" + "net/http" + "net/url" + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "{{ .envconfigImportPath }}" + "go.opentelemetry.io/otel/sdk/metric" + "go.opentelemetry.io/otel/sdk/metric/metricdata" +) + +const ( + WeakCertificate = ` +-----BEGIN CERTIFICATE----- +MIIBhzCCASygAwIBAgIRANHpHgAWeTnLZpTSxCKs0ggwCgYIKoZIzj0EAwIwEjEQ +MA4GA1UEChMHb3RlbC1nbzAeFw0yMTA0MDExMzU5MDNaFw0yMTA0MDExNDU5MDNa +MBIxEDAOBgNVBAoTB290ZWwtZ28wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAS9 +nWSkmPCxShxnp43F+PrOtbGV7sNfkbQ/kxzi9Ego0ZJdiXxkmv/C05QFddCW7Y0Z +sJCLHGogQsYnWJBXUZOVo2MwYTAOBgNVHQ8BAf8EBAMCB4AwEwYDVR0lBAwwCgYI +KwYBBQUHAwEwDAYDVR0TAQH/BAIwADAsBgNVHREEJTAjgglsb2NhbGhvc3SHEAAA +AAAAAAAAAAAAAAAAAAGHBH8AAAEwCgYIKoZIzj0EAwIDSQAwRgIhANwZVVKvfvQ/ +1HXsTvgH+xTQswOwSSKYJ1cVHQhqK7ZbAiEAus8NxpTRnp5DiTMuyVmhVNPB+bVH +Lhnm4N/QDk5rek0= +-----END CERTIFICATE----- +` + WeakPrivateKey = ` +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgN8HEXiXhvByrJ1zK +SFT6Y2l2KqDWwWzKf+t4CyWrNKehRANCAAS9nWSkmPCxShxnp43F+PrOtbGV7sNf +kbQ/kxzi9Ego0ZJdiXxkmv/C05QFddCW7Y0ZsJCLHGogQsYnWJBXUZOV +-----END PRIVATE KEY----- +` +) + +type env map[string]string + +func (e *env) getEnv(env string) string { + return (*e)[env] +} + +type fileReader map[string][]byte + +func (f *fileReader) readFile(filename string) ([]byte, error) { + if b, ok := (*f)[filename]; ok { + return b, nil + } + return nil, errors.New("file not found") +} + +func TestConfigs(t *testing.T) { + tlsCert, err := CreateTLSConfig([]byte(WeakCertificate)) + assert.NoError(t, err) + + tests := []struct { + name string + opts []GenericOption + env env + fileReader fileReader + asserts func(t *testing.T, c *Config, grpcOption bool) + }{ + { + name: "Test default configs", + asserts: func(t *testing.T, c *Config, grpcOption bool) { + if grpcOption { + assert.Equal(t, "localhost:4317", c.Metrics.Endpoint) + } else { + assert.Equal(t, "localhost:4318", c.Metrics.Endpoint) + } + assert.Equal(t, NoCompression, c.Metrics.Compression) + assert.Equal(t, map[string]string(nil), c.Metrics.Headers) + assert.Equal(t, 10*time.Second, c.Metrics.Timeout) + }, + }, + + // Endpoint Tests + { + name: "Test With Endpoint", + opts: []GenericOption{ + WithEndpoint("someendpoint"), + }, + asserts: func(t *testing.T, c *Config, grpcOption bool) { + assert.Equal(t, "someendpoint", c.Metrics.Endpoint) + }, + }, + { + name: "Test With Endpoint URL", + opts: []GenericOption{ + WithEndpointURL("http://someendpoint/somepath"), + }, + asserts: func(t *testing.T, c *Config, grpcOption bool) { + assert.Equal(t, "someendpoint", c.Metrics.Endpoint) + assert.Equal(t, "/somepath", c.Metrics.URLPath) + assert.Equal(t, true, c.Metrics.Insecure) + }, + }, + { + name: "Test With Secure Endpoint URL", + opts: []GenericOption{ + WithEndpointURL("https://someendpoint/somepath"), + }, + asserts: func(t *testing.T, c *Config, grpcOption bool) { + assert.Equal(t, "someendpoint", c.Metrics.Endpoint) + assert.Equal(t, "/somepath", c.Metrics.URLPath) + assert.Equal(t, false, c.Metrics.Insecure) + }, + }, + { + name: "Test With Invalid Endpoint URL", + opts: []GenericOption{ + WithEndpointURL("%invalid"), + }, + asserts: func(t *testing.T, c *Config, grpcOption bool) { + if grpcOption { + assert.Equal(t, "localhost:4317", c.Metrics.Endpoint) + } else { + assert.Equal(t, "localhost:4318", c.Metrics.Endpoint) + } + assert.Equal(t, "/v1/metrics", c.Metrics.URLPath) + }, + }, + { + name: "Test Environment Endpoint", + env: map[string]string{ + "OTEL_EXPORTER_OTLP_ENDPOINT": "https://env.endpoint/prefix", + }, + asserts: func(t *testing.T, c *Config, grpcOption bool) { + assert.False(t, c.Metrics.Insecure) + if grpcOption { + assert.Equal(t, "env.endpoint/prefix", c.Metrics.Endpoint) + } else { + assert.Equal(t, "env.endpoint", c.Metrics.Endpoint) + assert.Equal(t, "/prefix/v1/metrics", c.Metrics.URLPath) + } + }, + }, + { + name: "Test Environment Signal Specific Endpoint", + env: map[string]string{ + "OTEL_EXPORTER_OTLP_ENDPOINT": "https://overrode.by.signal.specific/env/var", + "OTEL_EXPORTER_OTLP_METRICS_ENDPOINT": "http://env.metrics.endpoint", + }, + asserts: func(t *testing.T, c *Config, grpcOption bool) { + assert.True(t, c.Metrics.Insecure) + assert.Equal(t, "env.metrics.endpoint", c.Metrics.Endpoint) + if !grpcOption { + assert.Equal(t, "/", c.Metrics.URLPath) + } + }, + }, + { + name: "Test Mixed Environment and With Endpoint", + opts: []GenericOption{ + WithEndpoint("metrics_endpoint"), + }, + env: map[string]string{ + "OTEL_EXPORTER_OTLP_ENDPOINT": "env_endpoint", + }, + asserts: func(t *testing.T, c *Config, grpcOption bool) { + assert.Equal(t, "metrics_endpoint", c.Metrics.Endpoint) + }, + }, + { + name: "Test Environment Endpoint with HTTP scheme", + env: map[string]string{ + "OTEL_EXPORTER_OTLP_ENDPOINT": "http://env_endpoint", + }, + asserts: func(t *testing.T, c *Config, grpcOption bool) { + assert.Equal(t, "env_endpoint", c.Metrics.Endpoint) + assert.Equal(t, true, c.Metrics.Insecure) + }, + }, + { + name: "Test Environment Endpoint with HTTP scheme and leading & trailingspaces", + env: map[string]string{ + "OTEL_EXPORTER_OTLP_ENDPOINT": " http://env_endpoint ", + }, + asserts: func(t *testing.T, c *Config, grpcOption bool) { + assert.Equal(t, "env_endpoint", c.Metrics.Endpoint) + assert.Equal(t, true, c.Metrics.Insecure) + }, + }, + { + name: "Test Environment Endpoint with HTTPS scheme", + env: map[string]string{ + "OTEL_EXPORTER_OTLP_ENDPOINT": "https://env_endpoint", + }, + asserts: func(t *testing.T, c *Config, grpcOption bool) { + assert.Equal(t, "env_endpoint", c.Metrics.Endpoint) + assert.Equal(t, false, c.Metrics.Insecure) + }, + }, + { + name: "Test Environment Signal Specific Endpoint with uppercase scheme", + env: map[string]string{ + "OTEL_EXPORTER_OTLP_ENDPOINT": "HTTPS://overrode_by_signal_specific", + "OTEL_EXPORTER_OTLP_METRICS_ENDPOINT": "HtTp://env_metrics_endpoint", + }, + asserts: func(t *testing.T, c *Config, grpcOption bool) { + assert.Equal(t, "env_metrics_endpoint", c.Metrics.Endpoint) + assert.Equal(t, true, c.Metrics.Insecure) + }, + }, + + // Certificate tests + { + name: "Test Default Certificate", + asserts: func(t *testing.T, c *Config, grpcOption bool) { + if grpcOption { + assert.NotNil(t, c.Metrics.GRPCCredentials) + } else { + assert.Nil(t, c.Metrics.TLSCfg) + } + }, + }, + { + name: "Test With Certificate", + opts: []GenericOption{ + WithTLSClientConfig(tlsCert), + }, + asserts: func(t *testing.T, c *Config, grpcOption bool) { + if grpcOption { + // TODO: make sure gRPC's credentials actually works + assert.NotNil(t, c.Metrics.GRPCCredentials) + } else { + // nolint:staticcheck // ignoring tlsCert.RootCAs.Subjects is deprecated ERR because cert does not come from SystemCertPool. + assert.Equal(t, tlsCert.RootCAs.Subjects(), c.Metrics.TLSCfg.RootCAs.Subjects()) + } + }, + }, + { + name: "Test Environment Certificate", + env: map[string]string{ + "OTEL_EXPORTER_OTLP_CERTIFICATE": "cert_path", + }, + fileReader: fileReader{ + "cert_path": []byte(WeakCertificate), + }, + asserts: func(t *testing.T, c *Config, grpcOption bool) { + if grpcOption { + assert.NotNil(t, c.Metrics.GRPCCredentials) + } else { + // nolint:staticcheck // ignoring tlsCert.RootCAs.Subjects is deprecated ERR because cert does not come from SystemCertPool. + assert.Equal(t, tlsCert.RootCAs.Subjects(), c.Metrics.TLSCfg.RootCAs.Subjects()) + } + }, + }, + { + name: "Test Environment Signal Specific Certificate", + env: map[string]string{ + "OTEL_EXPORTER_OTLP_CERTIFICATE": "overrode_by_signal_specific", + "OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE": "cert_path", + }, + fileReader: fileReader{ + "cert_path": []byte(WeakCertificate), + "invalid_cert": []byte("invalid certificate file."), + }, + asserts: func(t *testing.T, c *Config, grpcOption bool) { + if grpcOption { + assert.NotNil(t, c.Metrics.GRPCCredentials) + } else { + // nolint:staticcheck // ignoring tlsCert.RootCAs.Subjects is deprecated ERR because cert does not come from SystemCertPool. + assert.Equal(t, tlsCert.RootCAs.Subjects(), c.Metrics.TLSCfg.RootCAs.Subjects()) + } + }, + }, + { + name: "Test Mixed Environment and With Certificate", + opts: []GenericOption{}, + env: map[string]string{ + "OTEL_EXPORTER_OTLP_CERTIFICATE": "cert_path", + }, + fileReader: fileReader{ + "cert_path": []byte(WeakCertificate), + }, + asserts: func(t *testing.T, c *Config, grpcOption bool) { + if grpcOption { + assert.NotNil(t, c.Metrics.GRPCCredentials) + } else { + // nolint:staticcheck // ignoring tlsCert.RootCAs.Subjects is deprecated ERR because cert does not come from SystemCertPool. + assert.Equal(t, 1, len(c.Metrics.TLSCfg.RootCAs.Subjects())) + } + }, + }, + + // Headers tests + { + name: "Test With Headers", + opts: []GenericOption{ + WithHeaders(map[string]string{"h1": "v1"}), + }, + asserts: func(t *testing.T, c *Config, grpcOption bool) { + assert.Equal(t, map[string]string{"h1": "v1"}, c.Metrics.Headers) + }, + }, + { + name: "Test Environment Headers", + env: map[string]string{"OTEL_EXPORTER_OTLP_HEADERS": "h1=v1,h2=v2"}, + asserts: func(t *testing.T, c *Config, grpcOption bool) { + assert.Equal(t, map[string]string{"h1": "v1", "h2": "v2"}, c.Metrics.Headers) + }, + }, + { + name: "Test Environment Signal Specific Headers", + env: map[string]string{ + "OTEL_EXPORTER_OTLP_HEADERS": "overrode_by_signal_specific", + "OTEL_EXPORTER_OTLP_METRICS_HEADERS": "h1=v1,h2=v2", + }, + asserts: func(t *testing.T, c *Config, grpcOption bool) { + assert.Equal(t, map[string]string{"h1": "v1", "h2": "v2"}, c.Metrics.Headers) + }, + }, + { + name: "Test Mixed Environment and With Headers", + env: map[string]string{"OTEL_EXPORTER_OTLP_HEADERS": "h1=v1,h2=v2"}, + opts: []GenericOption{ + WithHeaders(map[string]string{"m1": "mv1"}), + }, + asserts: func(t *testing.T, c *Config, grpcOption bool) { + assert.Equal(t, map[string]string{"m1": "mv1"}, c.Metrics.Headers) + }, + }, + + // Compression Tests + { + name: "Test With Compression", + opts: []GenericOption{ + WithCompression(GzipCompression), + }, + asserts: func(t *testing.T, c *Config, grpcOption bool) { + assert.Equal(t, GzipCompression, c.Metrics.Compression) + }, + }, + { + name: "Test Environment Compression", + env: map[string]string{ + "OTEL_EXPORTER_OTLP_COMPRESSION": "gzip", + }, + asserts: func(t *testing.T, c *Config, grpcOption bool) { + assert.Equal(t, GzipCompression, c.Metrics.Compression) + }, + }, + { + name: "Test Environment Signal Specific Compression", + env: map[string]string{ + "OTEL_EXPORTER_OTLP_METRICS_COMPRESSION": "gzip", + }, + asserts: func(t *testing.T, c *Config, grpcOption bool) { + assert.Equal(t, GzipCompression, c.Metrics.Compression) + }, + }, + { + name: "Test Mixed Environment and With Compression", + opts: []GenericOption{ + WithCompression(NoCompression), + }, + env: map[string]string{ + "OTEL_EXPORTER_OTLP_METRICS_COMPRESSION": "gzip", + }, + asserts: func(t *testing.T, c *Config, grpcOption bool) { + assert.Equal(t, NoCompression, c.Metrics.Compression) + }, + }, + + // Timeout Tests + { + name: "Test With Timeout", + opts: []GenericOption{ + WithTimeout(time.Duration(5 * time.Second)), + }, + asserts: func(t *testing.T, c *Config, grpcOption bool) { + assert.Equal(t, 5*time.Second, c.Metrics.Timeout) + }, + }, + { + name: "Test Environment Timeout", + env: map[string]string{ + "OTEL_EXPORTER_OTLP_TIMEOUT": "15000", + }, + asserts: func(t *testing.T, c *Config, grpcOption bool) { + assert.Equal(t, c.Metrics.Timeout, 15*time.Second) + }, + }, + { + name: "Test Environment Signal Specific Timeout", + env: map[string]string{ + "OTEL_EXPORTER_OTLP_TIMEOUT": "15000", + "OTEL_EXPORTER_OTLP_METRICS_TIMEOUT": "28000", + }, + asserts: func(t *testing.T, c *Config, grpcOption bool) { + assert.Equal(t, c.Metrics.Timeout, 28*time.Second) + }, + }, + { + name: "Test Mixed Environment and With Timeout", + env: map[string]string{ + "OTEL_EXPORTER_OTLP_TIMEOUT": "15000", + "OTEL_EXPORTER_OTLP_METRICS_TIMEOUT": "28000", + }, + opts: []GenericOption{ + WithTimeout(5 * time.Second), + }, + asserts: func(t *testing.T, c *Config, grpcOption bool) { + assert.Equal(t, c.Metrics.Timeout, 5*time.Second) + }, + }, + + // Temporality Selector Tests + { + name: "WithTemporalitySelector", + opts: []GenericOption{ + WithTemporalitySelector(deltaSelector), + }, + asserts: func(t *testing.T, c *Config, grpcOption bool) { + // Function value comparisons are disallowed, test non-default + // behavior of a TemporalitySelector here to ensure our "catch + // all" was set. + var undefinedKind metric.InstrumentKind + got := c.Metrics.TemporalitySelector + assert.Equal(t, metricdata.DeltaTemporality, got(undefinedKind)) + }, + }, + + // Aggregation Selector Tests + { + name: "WithAggregationSelector", + opts: []GenericOption{ + WithAggregationSelector(dropSelector), + }, + asserts: func(t *testing.T, c *Config, grpcOption bool) { + // Function value comparisons are disallowed, test non-default + // behavior of a AggregationSelector here to ensure our "catch + // all" was set. + var undefinedKind metric.InstrumentKind + got := c.Metrics.AggregationSelector + assert.Equal(t, metric.AggregationDrop{}, got(undefinedKind)) + }, + }, + + // Proxy Tests + { + name: "Test With Proxy", + opts: []GenericOption{ + WithProxy(func(r *http.Request) (*url.URL, error) { + return url.Parse("http://proxy.com") + }), + }, + asserts: func(t *testing.T, c *Config, grpcOption bool) { + assert.NotNil(t, c.Metrics.Proxy) + proxyURL, err := c.Metrics.Proxy(&http.Request{}) + assert.NoError(t, err) + assert.Equal(t, "http://proxy.com", proxyURL.String()) + }, + }, + { + name: "Test Without Proxy", + opts: []GenericOption{}, + asserts: func(t *testing.T, c *Config, grpcOption bool) { + assert.Nil(t, c.Metrics.Proxy) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + origEOR := DefaultEnvOptionsReader + DefaultEnvOptionsReader = envconfig.EnvOptionsReader{ + GetEnv: tt.env.getEnv, + ReadFile: tt.fileReader.readFile, + Namespace: "OTEL_EXPORTER_OTLP", + } + t.Cleanup(func() { DefaultEnvOptionsReader = origEOR }) + + // Tests Generic options as HTTP Options + cfg := NewHTTPConfig(asHTTPOptions(tt.opts)...) + tt.asserts(t, &cfg, false) + + // Tests Generic options as gRPC Options + cfg = NewGRPCConfig(asGRPCOptions(tt.opts)...) + tt.asserts(t, &cfg, true) + }) + } +} + +func dropSelector(metric.InstrumentKind) metric.Aggregation { + return metric.AggregationDrop{} +} + +func deltaSelector(metric.InstrumentKind) metricdata.Temporality { + return metricdata.DeltaTemporality +} + +func asHTTPOptions(opts []GenericOption) []HTTPOption { + converted := make([]HTTPOption, len(opts)) + for i, o := range opts { + converted[i] = NewHTTPOption(o.ApplyHTTPOption) + } + return converted +} + +func asGRPCOptions(opts []GenericOption) []GRPCOption { + converted := make([]GRPCOption, len(opts)) + for i, o := range opts { + converted[i] = NewGRPCOption(o.ApplyGRPCOption) + } + return converted +} + +func TestCleanPath(t *testing.T) { + type args struct { + urlPath string + defaultPath string + } + tests := []struct { + name string + args args + want string + }{ + { + name: "clean empty path", + args: args{ + urlPath: "", + defaultPath: "DefaultPath", + }, + want: "DefaultPath", + }, + { + name: "clean metrics path", + args: args{ + urlPath: "/prefix/v1/metrics", + defaultPath: "DefaultMetricsPath", + }, + want: "/prefix/v1/metrics", + }, + { + name: "clean traces path", + args: args{ + urlPath: "https://env_endpoint", + defaultPath: "DefaultTracesPath", + }, + want: "/https:/env_endpoint", + }, + { + name: "spaces trimmed", + args: args{ + urlPath: " /dir", + }, + want: "/dir", + }, + { + name: "clean path empty", + args: args{ + urlPath: "dir/..", + defaultPath: "DefaultTracesPath", + }, + want: "DefaultTracesPath", + }, + { + name: "make absolute", + args: args{ + urlPath: "dir/a", + }, + want: "/dir/a", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := cleanPath(tt.args.urlPath, tt.args.defaultPath); got != tt.want { + t.Errorf("CleanPath() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/internal/shared/otlp/otlpmetric/otest/client.go.tmpl b/internal/shared/otlp/otlpmetric/otest/client.go.tmpl index be4de10ba52..95af8bbff1b 100644 --- a/internal/shared/otlp/otlpmetric/otest/client.go.tmpl +++ b/internal/shared/otlp/otlpmetric/otest/client.go.tmpl @@ -19,10 +19,10 @@ import ( "go.opentelemetry.io/otel" semconv "go.opentelemetry.io/otel/semconv/v1.24.0" - collpb "{{ .protoImportPrefix }}/otlp/collector/metrics/v1" - cpb "{{ .protoImportPrefix }}/otlp/common/v1" - mpb "{{ .protoImportPrefix }}/otlp/metrics/v1" - rpb "{{ .protoImportPrefix }}/otlp/resource/v1" + collpb "go.opentelemetry.io/proto/otlp/collector/metrics/v1" + cpb "go.opentelemetry.io/proto/otlp/common/v1" + mpb "go.opentelemetry.io/proto/otlp/metrics/v1" + rpb "go.opentelemetry.io/proto/otlp/resource/v1" ) var ( diff --git a/internal/shared/otlp/otlpmetric/otest/client_test.go.tmpl b/internal/shared/otlp/otlpmetric/otest/client_test.go.tmpl index dec0a07ddbf..79ead4d91f2 100644 --- a/internal/shared/otlp/otlpmetric/otest/client_test.go.tmpl +++ b/internal/shared/otlp/otlpmetric/otest/client_test.go.tmpl @@ -14,8 +14,8 @@ import ( "{{ .internalImportPath }}" "go.opentelemetry.io/otel/sdk/metric" "go.opentelemetry.io/otel/sdk/metric/metricdata" - cpb "{{ .protoImportPrefix }}/otlp/collector/metrics/v1" - mpb "{{ .protoImportPrefix }}/otlp/metrics/v1" + cpb "go.opentelemetry.io/proto/otlp/collector/metrics/v1" + mpb "go.opentelemetry.io/proto/otlp/metrics/v1" ) type client struct { diff --git a/internal/shared/otlp/otlpmetric/otest/collector.go.tmpl b/internal/shared/otlp/otlpmetric/otest/collector.go.tmpl new file mode 100644 index 00000000000..22ffd923790 --- /dev/null +++ b/internal/shared/otlp/otlpmetric/otest/collector.go.tmpl @@ -0,0 +1,451 @@ +// Code created by gotmpl. DO NOT MODIFY. +// source: internal/shared/otlp/otlpmetric/otest/collector.go.tmpl + +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package otest + +import ( + "bytes" + "compress/gzip" + "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" // nolint:depguard // This is for testing. + "encoding/pem" + "errors" + "fmt" + "io" + "math/big" + "net" + "net/http" + "net/url" + "sync" + "time" + + "google.golang.org/grpc" + "google.golang.org/grpc/metadata" + "google.golang.org/protobuf/proto" + + "{{ .oconfImportPath }}" + collpb "go.opentelemetry.io/proto/otlp/collector/metrics/v1" + mpb "go.opentelemetry.io/proto/otlp/metrics/v1" +) + +// Collector is the collection target a Client sends metric uploads to. +type Collector interface { + Collect() *Storage +} + +type ExportResult struct { + Response *collpb.ExportMetricsServiceResponse + Err error +} + +// Storage stores uploaded OTLP metric data in their proto form. +type Storage struct { + dataMu sync.Mutex + data []*mpb.ResourceMetrics +} + +// NewStorage returns a configure storage ready to store received requests. +func NewStorage() *Storage { + return &Storage{} +} + +// Add adds the request to the Storage. +func (s *Storage) Add(request *collpb.ExportMetricsServiceRequest) { + s.dataMu.Lock() + defer s.dataMu.Unlock() + s.data = append(s.data, request.ResourceMetrics...) +} + +// Dump returns all added ResourceMetrics and clears the storage. +func (s *Storage) Dump() []*mpb.ResourceMetrics { + s.dataMu.Lock() + defer s.dataMu.Unlock() + + var data []*mpb.ResourceMetrics + data, s.data = s.data, []*mpb.ResourceMetrics{} + return data +} + +// GRPCCollector is an OTLP gRPC server that collects all requests it receives. +type GRPCCollector struct { + collpb.UnimplementedMetricsServiceServer + + headersMu sync.Mutex + headers metadata.MD + storage *Storage + + resultCh <-chan ExportResult + listener net.Listener + srv *grpc.Server +} + +// NewGRPCCollector returns a *GRPCCollector that is listening at the provided +// endpoint. +// +// If endpoint is an empty string, the returned collector will be listening on +// the localhost interface at an OS chosen port. +// +// If errCh is not nil, the collector will respond to Export calls with errors +// sent on that channel. This means that if errCh is not nil Export calls will +// block until an error is received. +func NewGRPCCollector(endpoint string, resultCh <-chan ExportResult) (*GRPCCollector, error) { + if endpoint == "" { + endpoint = "localhost:0" + } + + c := &GRPCCollector{ + storage: NewStorage(), + resultCh: resultCh, + } + + var err error + c.listener, err = net.Listen("tcp", endpoint) + if err != nil { + return nil, err + } + + c.srv = grpc.NewServer() + collpb.RegisterMetricsServiceServer(c.srv, c) + go func() { _ = c.srv.Serve(c.listener) }() + + return c, nil +} + +// Shutdown shuts down the gRPC server closing all open connections and +// listeners immediately. +func (c *GRPCCollector) Shutdown() { c.srv.Stop() } + +// Addr returns the net.Addr c is listening at. +func (c *GRPCCollector) Addr() net.Addr { + return c.listener.Addr() +} + +// Collect returns the Storage holding all collected requests. +func (c *GRPCCollector) Collect() *Storage { + return c.storage +} + +// Headers returns the headers received for all requests. +func (c *GRPCCollector) Headers() map[string][]string { + // Makes a copy. + c.headersMu.Lock() + defer c.headersMu.Unlock() + return metadata.Join(c.headers) +} + +// Export handles the export req. +func (c *GRPCCollector) Export(ctx context.Context, req *collpb.ExportMetricsServiceRequest) (*collpb.ExportMetricsServiceResponse, error) { + c.storage.Add(req) + + if h, ok := metadata.FromIncomingContext(ctx); ok { + c.headersMu.Lock() + c.headers = metadata.Join(c.headers, h) + c.headersMu.Unlock() + } + + if c.resultCh != nil { + r := <-c.resultCh + if r.Response == nil { + return &collpb.ExportMetricsServiceResponse{}, r.Err + } + return r.Response, r.Err + } + return &collpb.ExportMetricsServiceResponse{}, nil +} + +var emptyExportMetricsServiceResponse = func() []byte { + body := collpb.ExportMetricsServiceResponse{} + r, err := proto.Marshal(&body) + if err != nil { + panic(err) + } + return r +}() + +type HTTPResponseError struct { + Err error + Status int + Header http.Header +} + +func (e *HTTPResponseError) Error() string { + return fmt.Sprintf("%d: %s", e.Status, e.Err) +} + +func (e *HTTPResponseError) Unwrap() error { return e.Err } + +// HTTPCollector is an OTLP HTTP server that collects all requests it receives. +type HTTPCollector struct { + plainTextResponse bool + + headersMu sync.Mutex + headers http.Header + storage *Storage + + resultCh <-chan ExportResult + listener net.Listener + srv *http.Server +} + +// NewHTTPCollector returns a *HTTPCollector that is listening at the provided +// endpoint. +// +// If endpoint is an empty string, the returned collector will be listening on +// the localhost interface at an OS chosen port, not use TLS, and listen at the +// default OTLP metric endpoint path ("/v1/metrics"). If the endpoint contains +// a prefix of "https" the server will generate weak self-signed TLS +// certificates and use them to server data. If the endpoint contains a path, +// that path will be used instead of the default OTLP metric endpoint path. +// +// If errCh is not nil, the collector will respond to HTTP requests with errors +// sent on that channel. This means that if errCh is not nil Export calls will +// block until an error is received. +func NewHTTPCollector(endpoint string, resultCh <-chan ExportResult, opts ...func(*HTTPCollector)) (*HTTPCollector, error) { + u, err := url.Parse(endpoint) + if err != nil { + return nil, err + } + if u.Host == "" { + u.Host = "localhost:0" + } + if u.Path == "" { + u.Path = oconf.DefaultMetricsPath + } + + c := &HTTPCollector{ + headers: http.Header{}, + storage: NewStorage(), + resultCh: resultCh, + } + for _, opt := range opts { + opt(c) + } + + c.listener, err = net.Listen("tcp", u.Host) + if err != nil { + return nil, err + } + + mux := http.NewServeMux() + mux.Handle(u.Path, http.HandlerFunc(c.handler)) + c.srv = &http.Server{ + Handler: mux, + ReadTimeout: 10 * time.Second, + WriteTimeout: 10 * time.Second, + } + if u.Scheme == "https" { + cert, err := weakCertificate() + if err != nil { + return nil, err + } + c.srv.TLSConfig = &tls.Config{ + Certificates: []tls.Certificate{cert}, + } + go func() { _ = c.srv.ServeTLS(c.listener, "", "") }() + } else { + go func() { _ = c.srv.Serve(c.listener) }() + } + return c, nil +} + +// WithHTTPCollectorRespondingPlainText makes the HTTPCollector return +// a plaintext, instead of protobuf, response. +func WithHTTPCollectorRespondingPlainText() func(*HTTPCollector) { + return func(s *HTTPCollector) { + s.plainTextResponse = true + } +} + +// Shutdown shuts down the HTTP server closing all open connections and +// listeners. +func (c *HTTPCollector) Shutdown(ctx context.Context) error { + return c.srv.Shutdown(ctx) +} + +// Addr returns the net.Addr c is listening at. +func (c *HTTPCollector) Addr() net.Addr { + return c.listener.Addr() +} + +// Collect returns the Storage holding all collected requests. +func (c *HTTPCollector) Collect() *Storage { + return c.storage +} + +// Headers returns the headers received for all requests. +func (c *HTTPCollector) Headers() map[string][]string { + // Makes a copy. + c.headersMu.Lock() + defer c.headersMu.Unlock() + return c.headers.Clone() +} + +func (c *HTTPCollector) handler(w http.ResponseWriter, r *http.Request) { + c.respond(w, c.record(r)) +} + +func (c *HTTPCollector) record(r *http.Request) ExportResult { + // Currently only supports protobuf. + if v := r.Header.Get("Content-Type"); v != "application/x-protobuf" { + err := fmt.Errorf("content-type not supported: %s", v) + return ExportResult{Err: err} + } + + body, err := c.readBody(r) + if err != nil { + return ExportResult{Err: err} + } + pbRequest := &collpb.ExportMetricsServiceRequest{} + err = proto.Unmarshal(body, pbRequest) + if err != nil { + return ExportResult{ + Err: &HTTPResponseError{ + Err: err, + Status: http.StatusInternalServerError, + }, + } + } + c.storage.Add(pbRequest) + + c.headersMu.Lock() + for k, vals := range r.Header { + for _, v := range vals { + c.headers.Add(k, v) + } + } + c.headersMu.Unlock() + + if c.resultCh != nil { + return <-c.resultCh + } + return ExportResult{Err: err} +} + +func (c *HTTPCollector) readBody(r *http.Request) (body []byte, err error) { + var reader io.ReadCloser + switch r.Header.Get("Content-Encoding") { + case "gzip": + reader, err = gzip.NewReader(r.Body) + if err != nil { + _ = reader.Close() + return nil, &HTTPResponseError{ + Err: err, + Status: http.StatusInternalServerError, + } + } + default: + reader = r.Body + } + + defer func() { + cErr := reader.Close() + if err == nil && cErr != nil { + err = &HTTPResponseError{ + Err: cErr, + Status: http.StatusInternalServerError, + } + } + }() + body, err = io.ReadAll(reader) + if err != nil { + err = &HTTPResponseError{ + Err: err, + Status: http.StatusInternalServerError, + } + } + return body, err +} + +func (c *HTTPCollector) respond(w http.ResponseWriter, resp ExportResult) { + if resp.Err != nil { + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + w.Header().Set("X-Content-Type-Options", "nosniff") + var e *HTTPResponseError + if errors.As(resp.Err, &e) { + for k, vals := range e.Header { + for _, v := range vals { + w.Header().Add(k, v) + } + } + w.WriteHeader(e.Status) + fmt.Fprintln(w, e.Error()) + } else { + w.WriteHeader(http.StatusBadRequest) + fmt.Fprintln(w, resp.Err.Error()) + } + return + } + + if c.plainTextResponse { + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("OK")) + return + } + + w.Header().Set("Content-Type", "application/x-protobuf") + w.WriteHeader(http.StatusOK) + if resp.Response == nil { + _, _ = w.Write(emptyExportMetricsServiceResponse) + } else { + r, err := proto.Marshal(resp.Response) + if err != nil { + panic(err) + } + _, _ = w.Write(r) + } +} + +// Based on https://golang.org/src/crypto/tls/generate_cert.go, +// simplified and weakened. +func weakCertificate() (tls.Certificate, error) { + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return tls.Certificate{}, err + } + notBefore := time.Now() + notAfter := notBefore.Add(time.Hour) + max := new(big.Int).Lsh(big.NewInt(1), 128) + sn, err := rand.Int(rand.Reader, max) + if err != nil { + return tls.Certificate{}, err + } + tmpl := x509.Certificate{ + SerialNumber: sn, + Subject: pkix.Name{Organization: []string{"otel-go"}}, + NotBefore: notBefore, + NotAfter: notAfter, + KeyUsage: x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + DNSNames: []string{"localhost"}, + IPAddresses: []net.IP{net.IPv6loopback, net.IPv4(127, 0, 0, 1)}, + } + derBytes, err := x509.CreateCertificate(rand.Reader, &tmpl, &tmpl, &priv.PublicKey, priv) + if err != nil { + return tls.Certificate{}, err + } + var certBuf bytes.Buffer + err = pem.Encode(&certBuf, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}) + if err != nil { + return tls.Certificate{}, err + } + privBytes, err := x509.MarshalPKCS8PrivateKey(priv) + if err != nil { + return tls.Certificate{}, err + } + var privBuf bytes.Buffer + err = pem.Encode(&privBuf, &pem.Block{Type: "PRIVATE KEY", Bytes: privBytes}) + if err != nil { + return tls.Certificate{}, err + } + return tls.X509KeyPair(certBuf.Bytes(), privBuf.Bytes()) +} diff --git a/internal/shared/otlp/otlpmetric/transform/attribute.go.tmpl b/internal/shared/otlp/otlpmetric/transform/attribute.go.tmpl index a244099ee73..63cd40dda7a 100644 --- a/internal/shared/otlp/otlpmetric/transform/attribute.go.tmpl +++ b/internal/shared/otlp/otlpmetric/transform/attribute.go.tmpl @@ -8,7 +8,7 @@ package transform import ( "go.opentelemetry.io/otel/attribute" - cpb "{{ .protoImportPrefix }}/otlp/common/v1" + cpb "go.opentelemetry.io/proto/otlp/common/v1" ) // AttrIter transforms an attribute iterator into OTLP key-values. diff --git a/internal/shared/otlp/otlpmetric/transform/attribute_test.go.tmpl b/internal/shared/otlp/otlpmetric/transform/attribute_test.go.tmpl index 77208b1e7e1..0815ba988d2 100644 --- a/internal/shared/otlp/otlpmetric/transform/attribute_test.go.tmpl +++ b/internal/shared/otlp/otlpmetric/transform/attribute_test.go.tmpl @@ -12,7 +12,7 @@ import ( "github.com/stretchr/testify/assert" "go.opentelemetry.io/otel/attribute" - cpb "{{ .protoImportPrefix }}/otlp/common/v1" + cpb "go.opentelemetry.io/proto/otlp/common/v1" ) var ( diff --git a/internal/shared/otlp/otlpmetric/transform/error.go.tmpl b/internal/shared/otlp/otlpmetric/transform/error.go.tmpl index 40006b6d251..69f0dd5d76c 100644 --- a/internal/shared/otlp/otlpmetric/transform/error.go.tmpl +++ b/internal/shared/otlp/otlpmetric/transform/error.go.tmpl @@ -11,7 +11,7 @@ import ( "fmt" "strings" - mpb "{{ .protoImportPrefix }}/otlp/metrics/v1" + mpb "go.opentelemetry.io/proto/otlp/metrics/v1" ) var ( diff --git a/internal/shared/otlp/otlpmetric/transform/metricdata.go.tmpl b/internal/shared/otlp/otlpmetric/transform/metricdata.go.tmpl index 4246bca2a66..b6d0b76fa1f 100644 --- a/internal/shared/otlp/otlpmetric/transform/metricdata.go.tmpl +++ b/internal/shared/otlp/otlpmetric/transform/metricdata.go.tmpl @@ -13,9 +13,9 @@ import ( "time" "go.opentelemetry.io/otel/sdk/metric/metricdata" - cpb "{{ .protoImportPrefix }}/otlp/common/v1" - mpb "{{ .protoImportPrefix }}/otlp/metrics/v1" - rpb "{{ .protoImportPrefix }}/otlp/resource/v1" + cpb "go.opentelemetry.io/proto/otlp/common/v1" + mpb "go.opentelemetry.io/proto/otlp/metrics/v1" + rpb "go.opentelemetry.io/proto/otlp/resource/v1" ) // ResourceMetrics returns an OTLP ResourceMetrics generated from rm. If rm diff --git a/internal/shared/otlp/otlpmetric/transform/metricdata_test.go.tmpl b/internal/shared/otlp/otlpmetric/transform/metricdata_test.go.tmpl index f7a30b467c2..b0bc71e9edb 100644 --- a/internal/shared/otlp/otlpmetric/transform/metricdata_test.go.tmpl +++ b/internal/shared/otlp/otlpmetric/transform/metricdata_test.go.tmpl @@ -18,9 +18,9 @@ import ( "go.opentelemetry.io/otel/sdk/metric/metricdata" "go.opentelemetry.io/otel/sdk/resource" semconv "go.opentelemetry.io/otel/semconv/v1.24.0" - cpb "{{ .protoImportPrefix }}/otlp/common/v1" - mpb "{{ .protoImportPrefix }}/otlp/metrics/v1" - rpb "{{ .protoImportPrefix }}/otlp/resource/v1" + cpb "go.opentelemetry.io/proto/otlp/common/v1" + mpb "go.opentelemetry.io/proto/otlp/metrics/v1" + rpb "go.opentelemetry.io/proto/otlp/resource/v1" ) type unknownAggT struct {