1
0
mirror of https://github.com/open-telemetry/opentelemetry-go.git synced 2025-01-16 02:47:20 +02:00

Revert the usage of go.opentelemetry.io/proto/slim (#5253)

* Revert "otlpmetrichttp: Use go.opentelemetry.io/proto/slim/otlp (#5222)"

This reverts commit 6e92163d6a.

* Revert "otlploghttp: Use go.opentelemetry.io/proto/slim/otlp (#5216)"

This reverts commit fe3de7059e.

* Remove slim dep

* Fix CI
This commit is contained in:
Sam Xie 2024-04-22 22:12:25 -07:00 committed by GitHub
parent b34cfc47c4
commit bf37c5a3a4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
46 changed files with 2970 additions and 300 deletions

View File

@ -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

View File

@ -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"
)

View File

@ -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"

View File

@ -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) {

View File

@ -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
)

View File

@ -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=

View File

@ -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 (

View File

@ -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"

View File

@ -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 (

View File

@ -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"

View File

@ -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

View File

@ -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()
}

View File

@ -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

View File

@ -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()
// 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.
@ -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
})

View File

@ -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

View File

@ -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())
}

View File

@ -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 {

View File

@ -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 {

View File

@ -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.

View File

@ -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
)

View File

@ -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=

View File

@ -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

View File

@ -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()
}

View File

@ -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.

View File

@ -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,
// A private method to prevent users implementing the
// interface and so future additions to it will not
// violate compatibility.
private()
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.
@ -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
})

View File

@ -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

View File

@ -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 (

View File

@ -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 {

View File

@ -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

View File

@ -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.

View File

@ -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 (

View File

@ -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 (

View File

@ -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

View File

@ -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 {

View File

@ -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)
}
}
}
}

View File

@ -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
}

View File

@ -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
})
}

View File

@ -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)
}
})
}
}

View File

@ -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 (

View File

@ -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 {

View File

@ -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())
}

View File

@ -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.

View File

@ -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 (

View File

@ -11,7 +11,7 @@ import (
"fmt"
"strings"
mpb "{{ .protoImportPrefix }}/otlp/metrics/v1"
mpb "go.opentelemetry.io/proto/otlp/metrics/v1"
)
var (

View File

@ -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

View File

@ -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 {