From 9dafa0d491a24bef6337e1f649167b2cb9922bbe Mon Sep 17 00:00:00 2001 From: lisguo Date: Tue, 9 Jul 2024 00:27:24 -0400 Subject: [PATCH 1/4] Add confighttp and configgrpc --- config/configgrpc/Makefile | 1 + config/configgrpc/README.md | 113 ++ config/configgrpc/configgrpc.go | 447 ++++++ .../configgrpc/configgrpc_benchmark_test.go | 168 ++ config/configgrpc/configgrpc_test.go | 1098 +++++++++++++ config/configgrpc/doc.go | 6 + config/configgrpc/go.mod | 100 ++ config/configgrpc/go.sum | 138 ++ config/configgrpc/gzip.go | 9 + config/configgrpc/package_test.go | 17 + config/configgrpc/testdata/ca.crt | 20 + config/configgrpc/testdata/client.crt | 20 + config/configgrpc/testdata/client.key | 27 + config/configgrpc/testdata/server.crt | 20 + config/configgrpc/testdata/server.key | 27 + config/configgrpc/wrappedstream.go | 34 + config/configgrpc/wrappedstream_test.go | 43 + config/confighttp/Makefile | 1 + config/confighttp/README.md | 113 ++ config/confighttp/clientinfohandler.go | 66 + config/confighttp/clientinfohandler_test.go | 52 + config/confighttp/compression.go | 175 ++ config/confighttp/compression_test.go | 386 +++++ config/confighttp/compressor.go | 75 + config/confighttp/confighttp.go | 461 ++++++ config/confighttp/confighttp_test.go | 1424 +++++++++++++++++ config/confighttp/doc.go | 6 + config/confighttp/go.mod | 94 ++ config/confighttp/go.sum | 135 ++ config/confighttp/package_test.go | 14 + config/confighttp/testdata/ca.crt | 20 + config/confighttp/testdata/client.crt | 20 + config/confighttp/testdata/client.key | 27 + config/confighttp/testdata/server.crt | 20 + config/confighttp/testdata/server.key | 27 + 35 files changed, 5404 insertions(+) create mode 100644 config/configgrpc/Makefile create mode 100644 config/configgrpc/README.md create mode 100644 config/configgrpc/configgrpc.go create mode 100644 config/configgrpc/configgrpc_benchmark_test.go create mode 100644 config/configgrpc/configgrpc_test.go create mode 100644 config/configgrpc/doc.go create mode 100644 config/configgrpc/go.mod create mode 100644 config/configgrpc/go.sum create mode 100644 config/configgrpc/gzip.go create mode 100644 config/configgrpc/package_test.go create mode 100644 config/configgrpc/testdata/ca.crt create mode 100644 config/configgrpc/testdata/client.crt create mode 100644 config/configgrpc/testdata/client.key create mode 100644 config/configgrpc/testdata/server.crt create mode 100644 config/configgrpc/testdata/server.key create mode 100644 config/configgrpc/wrappedstream.go create mode 100644 config/configgrpc/wrappedstream_test.go create mode 100644 config/confighttp/Makefile create mode 100644 config/confighttp/README.md create mode 100644 config/confighttp/clientinfohandler.go create mode 100644 config/confighttp/clientinfohandler_test.go create mode 100644 config/confighttp/compression.go create mode 100644 config/confighttp/compression_test.go create mode 100644 config/confighttp/compressor.go create mode 100644 config/confighttp/confighttp.go create mode 100644 config/confighttp/confighttp_test.go create mode 100644 config/confighttp/doc.go create mode 100644 config/confighttp/go.mod create mode 100644 config/confighttp/go.sum create mode 100644 config/confighttp/package_test.go create mode 100644 config/confighttp/testdata/ca.crt create mode 100644 config/confighttp/testdata/client.crt create mode 100644 config/confighttp/testdata/client.key create mode 100644 config/confighttp/testdata/server.crt create mode 100644 config/confighttp/testdata/server.key diff --git a/config/configgrpc/Makefile b/config/configgrpc/Makefile new file mode 100644 index 000000000000..ded7a36092dc --- /dev/null +++ b/config/configgrpc/Makefile @@ -0,0 +1 @@ +include ../../Makefile.Common diff --git a/config/configgrpc/README.md b/config/configgrpc/README.md new file mode 100644 index 000000000000..b9cf2f01be26 --- /dev/null +++ b/config/configgrpc/README.md @@ -0,0 +1,113 @@ +# gRPC Configuration Settings + +gRPC exposes a [variety of settings](https://godoc.org/google.golang.org/grpc). +Several of these settings are available for configuration within individual +receivers or exporters. In general, none of these settings should need to be +adjusted. + +## Client Configuration + +[Exporters](https://github.com/open-telemetry/opentelemetry-collector/blob/main/exporter/README.md) +leverage client configuration. + +Note that client configuration supports TLS configuration, the +configuration parameters are also defined under `tls` like server +configuration. For more information, see [configtls +README](../configtls/README.md). + +- [`balancer_name`](https://github.com/grpc/grpc-go/blob/master/examples/features/load_balancing/README.md) +- `compression` Compression type to use among `gzip`, `snappy`, `zstd`, and `none`. +- `endpoint`: Valid value syntax available [here](https://github.com/grpc/grpc/blob/master/doc/naming.md) +- [`tls`](../configtls/README.md) +- `headers`: name/value pairs added to the request +- [`keepalive`](https://godoc.org/google.golang.org/grpc/keepalive#ClientParameters) + - `permit_without_stream` + - `time` + - `timeout` +- [`read_buffer_size`](https://godoc.org/google.golang.org/grpc#ReadBufferSize) +- [`write_buffer_size`](https://godoc.org/google.golang.org/grpc#WriteBufferSize) +- [`auth`](../configauth/README.md) + +Please note that [`per_rpc_auth`](https://pkg.go.dev/google.golang.org/grpc#PerRPCCredentials) which allows the credentials to send for every RPC is now moved to become an [extension](https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/extension/bearertokenauthextension). Note that this feature isn't about sending the headers only during the initial connection as an `authorization` header under the `headers` would do: this is sent for every RPC performed during an established connection. + +Example: + +```yaml +exporters: + otlp: + endpoint: otelcol2:55690 + auth: + authenticator: some-authenticator-extension + tls: + ca_file: ca.pem + cert_file: cert.pem + key_file: key.pem + headers: + test1: "value1" + "test 2": "value 2" +``` + +### Compression Comparison + +[configgrpc_benchmark_test.go](./configgrpc_benchmark_test.go) contains benchmarks comparing the supported compression algorithms. It performs compression using `gzip`, `zstd`, and `snappy` compression on small, medium, and large sized log, trace, and metric payloads. Each test case outputs the uncompressed payload size, the compressed payload size, and the average nanoseconds spent on compression. + +The following table summarizes the results, including some additional columns computed from the raw data. The benchmarks were performed on an AWS m5.large EC2 instance with an Intel(R) Xeon(R) Platinum 8259CL CPU @ 2.50GHz. + +| Request | Compressor | Raw Bytes | Compressed bytes | Compression ratio | Ns / op | Mb compressed / second | Mb saved / second | +|-------------------|------------|-----------|------------------|-------------------|---------|------------------------|-------------------| +| lg_log_request | gzip | 5150 | 262 | 19.66 | 49231 | 104.61 | 99.29 | +| lg_metric_request | gzip | 6800 | 201 | 33.83 | 51816 | 131.23 | 127.35 | +| lg_trace_request | gzip | 9200 | 270 | 34.07 | 65174 | 141.16 | 137.02 | +| md_log_request | gzip | 363 | 268 | 1.35 | 37609 | 9.65 | 2.53 | +| md_metric_request | gzip | 320 | 145 | 2.21 | 30141 | 10.62 | 5.81 | +| md_trace_request | gzip | 451 | 288 | 1.57 | 38270 | 11.78 | 4.26 | +| sm_log_request | gzip | 166 | 168 | 0.99 | 30511 | 5.44 | -0.07 | +| sm_metric_request | gzip | 185 | 142 | 1.30 | 29055 | 6.37 | 1.48 | +| sm_trace_request | gzip | 233 | 205 | 1.14 | 33466 | 6.96 | 0.84 | +| lg_log_request | snappy | 5150 | 475 | 10.84 | 1915 | 2,689.30 | 2,441.25 | +| lg_metric_request | snappy | 6800 | 466 | 14.59 | 2266 | 3,000.88 | 2,795.23 | +| lg_trace_request | snappy | 9200 | 644 | 14.29 | 3281 | 2,804.02 | 2,607.74 | +| md_log_request | snappy | 363 | 300 | 1.21 | 770.0 | 471.43 | 81.82 | +| md_metric_request | snappy | 320 | 162 | 1.98 | 588.6 | 543.66 | 268.43 | +| md_trace_request | snappy | 451 | 330 | 1.37 | 907.7 | 496.86 | 133.30 | +| sm_log_request | snappy | 166 | 184 | 0.90 | 551.8 | 300.83 | -32.62 | +| sm_metric_request | snappy | 185 | 154 | 1.20 | 526.3 | 351.51 | 58.90 | +| sm_trace_request | snappy | 233 | 251 | 0.93 | 682.1 | 341.59 | -26.39 | +| lg_log_request | zstd | 5150 | 223 | 23.09 | 17998 | 286.14 | 273.75 | +| lg_metric_request | zstd | 6800 | 144 | 47.22 | 14289 | 475.89 | 465.81 | +| lg_trace_request | zstd | 9200 | 208 | 44.23 | 17160 | 536.13 | 524.01 | +| md_log_request | zstd | 363 | 261 | 1.39 | 11216 | 32.36 | 9.09 | +| md_metric_request | zstd | 320 | 145 | 2.21 | 9318 | 34.34 | 18.78 | +| md_trace_request | zstd | 451 | 301 | 1.50 | 12583 | 35.84 | 11.92 | +| sm_log_request | zstd | 166 | 165 | 1.01 | 12482 | 13.30 | 0.08 | +| sm_metric_request | zstd | 185 | 139 | 1.33 | 8824 | 20.97 | 5.21 | +| sm_trace_request | zstd | 233 | 203 | 1.15 | 10134 | 22.99 | 2.96 | + +Compression ratios will vary in practice as they are highly dependent on the data's information entropy. Compression rates are dependent on the speed of the CPU, and the size of payloads being compressed: smaller payloads compress at slower rates relative to larger payloads, which are able to amortize fixed computation costs over more bytes. + +`gzip` is the only required compression algorithm required for [OTLP servers](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/otlp.md#protocol-details), and is a natural first choice. It is not as fast as `snappy`, but achieves better compression ratios and has reasonable performance. If your collector is CPU bound and your OTLP server supports it, you may benefit from using `snappy` compression. If your collector is CPU bound and has a very fast network link, you may benefit from disabling compression, which is the default. + +## Server Configuration + +[Receivers](https://github.com/open-telemetry/opentelemetry-collector/blob/main/receiver/README.md) +leverage server configuration. + +Note that transport configuration can also be configured. For more information, +see [confignet README](../confignet/README.md). + +- [`keepalive`](https://godoc.org/google.golang.org/grpc/keepalive#ServerParameters) + - [`enforcement_policy`](https://godoc.org/google.golang.org/grpc/keepalive#EnforcementPolicy) + - `min_time` + - `permit_without_stream` + - [`server_parameters`](https://godoc.org/google.golang.org/grpc/keepalive#ServerParameters) + - `max_connection_age` + - `max_connection_age_grace` + - `max_connection_idle` + - `time` + - `timeout` +- [`max_concurrent_streams`](https://godoc.org/google.golang.org/grpc#MaxConcurrentStreams) +- [`max_recv_msg_size_mib`](https://godoc.org/google.golang.org/grpc#MaxRecvMsgSize) +- [`read_buffer_size`](https://godoc.org/google.golang.org/grpc#ReadBufferSize) +- [`tls`](../configtls/README.md) +- [`write_buffer_size`](https://godoc.org/google.golang.org/grpc#WriteBufferSize) +- [`auth`](../configauth/README.md) diff --git a/config/configgrpc/configgrpc.go b/config/configgrpc/configgrpc.go new file mode 100644 index 000000000000..fa1f03d0b6d2 --- /dev/null +++ b/config/configgrpc/configgrpc.go @@ -0,0 +1,447 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package configgrpc // import "go.opentelemetry.io/collector/config/configgrpc" + +import ( + "context" + "crypto/tls" + "errors" + "fmt" + "strings" + "time" + + "github.com/mostynb/go-grpc-compression/nonclobbering/snappy" + "github.com/mostynb/go-grpc-compression/nonclobbering/zstd" + "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" + "go.opentelemetry.io/otel" + "google.golang.org/grpc" + "google.golang.org/grpc/balancer" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/encoding/gzip" + "google.golang.org/grpc/keepalive" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/peer" + + "go.opentelemetry.io/collector/client" + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/config/configauth" + "go.opentelemetry.io/collector/config/configcompression" + "go.opentelemetry.io/collector/config/confignet" + "go.opentelemetry.io/collector/config/configopaque" + "go.opentelemetry.io/collector/config/configtls" + "go.opentelemetry.io/collector/config/internal" + "go.opentelemetry.io/collector/extension/auth" +) + +var errMetadataNotFound = errors.New("no request metadata found") + +// KeepaliveClientConfig exposes the keepalive.ClientParameters to be used by the exporter. +// Refer to the original data-structure for the meaning of each parameter: +// https://godoc.org/google.golang.org/grpc/keepalive#ClientParameters +type KeepaliveClientConfig struct { + Time time.Duration `mapstructure:"time"` + Timeout time.Duration `mapstructure:"timeout"` + PermitWithoutStream bool `mapstructure:"permit_without_stream"` +} + +// ClientConfig defines common settings for a gRPC client configuration. +type ClientConfig struct { + // The target to which the exporter is going to send traces or metrics, + // using the gRPC protocol. The valid syntax is described at + // https://github.com/grpc/grpc/blob/master/doc/naming.md. + Endpoint string `mapstructure:"endpoint"` + + // The compression key for supported compression types within collector. + Compression configcompression.Type `mapstructure:"compression"` + + // TLSSetting struct exposes TLS client configuration. + TLSSetting configtls.ClientConfig `mapstructure:"tls"` + + // The keepalive parameters for gRPC client. See grpc.WithKeepaliveParams. + // (https://godoc.org/google.golang.org/grpc#WithKeepaliveParams). + Keepalive *KeepaliveClientConfig `mapstructure:"keepalive"` + + // ReadBufferSize for gRPC client. See grpc.WithReadBufferSize. + // (https://godoc.org/google.golang.org/grpc#WithReadBufferSize). + ReadBufferSize int `mapstructure:"read_buffer_size"` + + // WriteBufferSize for gRPC gRPC. See grpc.WithWriteBufferSize. + // (https://godoc.org/google.golang.org/grpc#WithWriteBufferSize). + WriteBufferSize int `mapstructure:"write_buffer_size"` + + // WaitForReady parameter configures client to wait for ready state before sending data. + // (https://github.com/grpc/grpc/blob/master/doc/wait-for-ready.md) + WaitForReady bool `mapstructure:"wait_for_ready"` + + // The headers associated with gRPC requests. + Headers map[string]configopaque.String `mapstructure:"headers"` + + // Sets the balancer in grpclb_policy to discover the servers. Default is pick_first. + // https://github.com/grpc/grpc-go/blob/master/examples/features/load_balancing/README.md + BalancerName string `mapstructure:"balancer_name"` + + // WithAuthority parameter configures client to rewrite ":authority" header + // (godoc.org/google.golang.org/grpc#WithAuthority) + Authority string `mapstructure:"authority"` + + // Auth configuration for outgoing RPCs. + Auth *configauth.Authentication `mapstructure:"auth"` +} + +// KeepaliveServerConfig is the configuration for keepalive. +type KeepaliveServerConfig struct { + ServerParameters *KeepaliveServerParameters `mapstructure:"server_parameters"` + EnforcementPolicy *KeepaliveEnforcementPolicy `mapstructure:"enforcement_policy"` +} + +// KeepaliveServerParameters allow configuration of the keepalive.ServerParameters. +// The same default values as keepalive.ServerParameters are applicable and get applied by the server. +// See https://godoc.org/google.golang.org/grpc/keepalive#ServerParameters for details. +type KeepaliveServerParameters struct { + MaxConnectionIdle time.Duration `mapstructure:"max_connection_idle"` + MaxConnectionAge time.Duration `mapstructure:"max_connection_age"` + MaxConnectionAgeGrace time.Duration `mapstructure:"max_connection_age_grace"` + Time time.Duration `mapstructure:"time"` + Timeout time.Duration `mapstructure:"timeout"` +} + +// KeepaliveEnforcementPolicy allow configuration of the keepalive.EnforcementPolicy. +// The same default values as keepalive.EnforcementPolicy are applicable and get applied by the server. +// See https://godoc.org/google.golang.org/grpc/keepalive#EnforcementPolicy for details. +type KeepaliveEnforcementPolicy struct { + MinTime time.Duration `mapstructure:"min_time"` + PermitWithoutStream bool `mapstructure:"permit_without_stream"` +} + +// ServerConfig defines common settings for a gRPC server configuration. +type ServerConfig struct { + // Server net.Addr config. For transport only "tcp" and "unix" are valid options. + NetAddr confignet.AddrConfig `mapstructure:",squash"` + + // Configures the protocol to use TLS. + // The default value is nil, which will cause the protocol to not use TLS. + TLSSetting *configtls.ServerConfig `mapstructure:"tls"` + + // MaxRecvMsgSizeMiB sets the maximum size (in MiB) of messages accepted by the server. + MaxRecvMsgSizeMiB uint64 `mapstructure:"max_recv_msg_size_mib"` + + // MaxConcurrentStreams sets the limit on the number of concurrent streams to each ServerTransport. + // It has effect only for streaming RPCs. + MaxConcurrentStreams uint32 `mapstructure:"max_concurrent_streams"` + + // ReadBufferSize for gRPC server. See grpc.ReadBufferSize. + // (https://godoc.org/google.golang.org/grpc#ReadBufferSize). + ReadBufferSize int `mapstructure:"read_buffer_size"` + + // WriteBufferSize for gRPC server. See grpc.WriteBufferSize. + // (https://godoc.org/google.golang.org/grpc#WriteBufferSize). + WriteBufferSize int `mapstructure:"write_buffer_size"` + + // Keepalive anchor for all the settings related to keepalive. + Keepalive *KeepaliveServerConfig `mapstructure:"keepalive"` + + // Auth for this receiver + Auth *configauth.Authentication `mapstructure:"auth"` + + // Include propagates the incoming connection's metadata to downstream consumers. + // Experimental: *NOTE* this option is subject to change or removal in the future. + IncludeMetadata bool `mapstructure:"include_metadata"` +} + +// sanitizedEndpoint strips the prefix of either http:// or https:// from configgrpc.ClientConfig.Endpoint. +func (gcs *ClientConfig) sanitizedEndpoint() string { + switch { + case gcs.isSchemeHTTP(): + return strings.TrimPrefix(gcs.Endpoint, "http://") + case gcs.isSchemeHTTPS(): + return strings.TrimPrefix(gcs.Endpoint, "https://") + default: + return gcs.Endpoint + } +} + +func (gcs *ClientConfig) isSchemeHTTP() bool { + return strings.HasPrefix(gcs.Endpoint, "http://") +} + +func (gcs *ClientConfig) isSchemeHTTPS() bool { + return strings.HasPrefix(gcs.Endpoint, "https://") +} + +// ToClientConn creates a client connection to the given target. By default, it's +// a non-blocking dial (the function won't wait for connections to be +// established, and connecting happens in the background). To make it a blocking +// dial, use grpc.WithBlock() dial option. +func (gcs *ClientConfig) ToClientConn(ctx context.Context, host component.Host, settings component.TelemetrySettings, extraOpts ...grpc.DialOption) (*grpc.ClientConn, error) { + opts, err := gcs.toDialOptions(host, settings) + if err != nil { + return nil, err + } + opts = append(opts, extraOpts...) + return grpc.DialContext(ctx, gcs.sanitizedEndpoint(), opts...) +} + +func (gcs *ClientConfig) toDialOptions(host component.Host, settings component.TelemetrySettings) ([]grpc.DialOption, error) { + var opts []grpc.DialOption + if gcs.Compression.IsCompressed() { + cp, err := getGRPCCompressionName(gcs.Compression) + if err != nil { + return nil, err + } + opts = append(opts, grpc.WithDefaultCallOptions(grpc.UseCompressor(cp))) + } + + tlsCfg, err := gcs.TLSSetting.LoadTLSConfigContext(context.Background()) + if err != nil { + return nil, err + } + cred := insecure.NewCredentials() + if tlsCfg != nil { + cred = credentials.NewTLS(tlsCfg) + } else if gcs.isSchemeHTTPS() { + cred = credentials.NewTLS(&tls.Config{}) + } + opts = append(opts, grpc.WithTransportCredentials(cred)) + + if gcs.ReadBufferSize > 0 { + opts = append(opts, grpc.WithReadBufferSize(gcs.ReadBufferSize)) + } + + if gcs.WriteBufferSize > 0 { + opts = append(opts, grpc.WithWriteBufferSize(gcs.WriteBufferSize)) + } + + if gcs.Keepalive != nil { + keepAliveOption := grpc.WithKeepaliveParams(keepalive.ClientParameters{ + Time: gcs.Keepalive.Time, + Timeout: gcs.Keepalive.Timeout, + PermitWithoutStream: gcs.Keepalive.PermitWithoutStream, + }) + opts = append(opts, keepAliveOption) + } + + if gcs.Auth != nil { + if host.GetExtensions() == nil { + return nil, errors.New("no extensions configuration available") + } + + grpcAuthenticator, cerr := gcs.Auth.GetClientAuthenticator(host.GetExtensions()) + if cerr != nil { + return nil, cerr + } + + perRPCCredentials, perr := grpcAuthenticator.PerRPCCredentials() + if perr != nil { + return nil, err + } + opts = append(opts, grpc.WithPerRPCCredentials(perRPCCredentials)) + } + + if gcs.BalancerName != "" { + valid := validateBalancerName(gcs.BalancerName) + if !valid { + return nil, fmt.Errorf("invalid balancer_name: %s", gcs.BalancerName) + } + opts = append(opts, grpc.WithDefaultServiceConfig(fmt.Sprintf(`{"loadBalancingPolicy":"%s"}`, gcs.BalancerName))) + } + + if gcs.Authority != "" { + opts = append(opts, grpc.WithAuthority(gcs.Authority)) + } + + otelOpts := []otelgrpc.Option{ + otelgrpc.WithTracerProvider(settings.TracerProvider), + otelgrpc.WithMeterProvider(settings.MeterProvider), + otelgrpc.WithPropagators(otel.GetTextMapPropagator()), + } + + // Enable OpenTelemetry observability plugin. + opts = append(opts, grpc.WithStatsHandler(otelgrpc.NewClientHandler(otelOpts...))) + + return opts, nil +} + +func validateBalancerName(balancerName string) bool { + return balancer.Get(balancerName) != nil +} + +// ToServer returns a grpc.Server for the configuration +func (gss *ServerConfig) ToServer(_ context.Context, host component.Host, settings component.TelemetrySettings, extraOpts ...grpc.ServerOption) (*grpc.Server, error) { + opts, err := gss.toServerOption(host, settings) + if err != nil { + return nil, err + } + opts = append(opts, extraOpts...) + return grpc.NewServer(opts...), nil +} + +func (gss *ServerConfig) toServerOption(host component.Host, settings component.TelemetrySettings) ([]grpc.ServerOption, error) { + switch gss.NetAddr.Transport { + case confignet.TransportTypeTCP, confignet.TransportTypeTCP4, confignet.TransportTypeTCP6, confignet.TransportTypeUDP, confignet.TransportTypeUDP4, confignet.TransportTypeUDP6: + internal.WarnOnUnspecifiedHost(settings.Logger, gss.NetAddr.Endpoint) + } + + var opts []grpc.ServerOption + + if gss.TLSSetting != nil { + tlsCfg, err := gss.TLSSetting.LoadTLSConfigContext(context.Background()) + if err != nil { + return nil, err + } + opts = append(opts, grpc.Creds(credentials.NewTLS(tlsCfg))) + } + + if gss.MaxRecvMsgSizeMiB > 0 { + opts = append(opts, grpc.MaxRecvMsgSize(int(gss.MaxRecvMsgSizeMiB*1024*1024))) + } + + if gss.MaxConcurrentStreams > 0 { + opts = append(opts, grpc.MaxConcurrentStreams(gss.MaxConcurrentStreams)) + } + + if gss.ReadBufferSize > 0 { + opts = append(opts, grpc.ReadBufferSize(gss.ReadBufferSize)) + } + + if gss.WriteBufferSize > 0 { + opts = append(opts, grpc.WriteBufferSize(gss.WriteBufferSize)) + } + + // The default values referenced in the GRPC docs are set within the server, so this code doesn't need + // to apply them over zero/nil values before passing these as grpc.ServerOptions. + // The following shows the server code for applying default grpc.ServerOptions. + // https://github.com/grpc/grpc-go/blob/120728e1f775e40a2a764341939b78d666b08260/internal/transport/http2_server.go#L184-L200 + if gss.Keepalive != nil { + if gss.Keepalive.ServerParameters != nil { + svrParams := gss.Keepalive.ServerParameters + opts = append(opts, grpc.KeepaliveParams(keepalive.ServerParameters{ + MaxConnectionIdle: svrParams.MaxConnectionIdle, + MaxConnectionAge: svrParams.MaxConnectionAge, + MaxConnectionAgeGrace: svrParams.MaxConnectionAgeGrace, + Time: svrParams.Time, + Timeout: svrParams.Timeout, + })) + } + // The default values referenced in the GRPC are set within the server, so this code doesn't need + // to apply them over zero/nil values before passing these as grpc.ServerOptions. + // The following shows the server code for applying default grpc.ServerOptions. + // https://github.com/grpc/grpc-go/blob/120728e1f775e40a2a764341939b78d666b08260/internal/transport/http2_server.go#L202-L205 + if gss.Keepalive.EnforcementPolicy != nil { + enfPol := gss.Keepalive.EnforcementPolicy + opts = append(opts, grpc.KeepaliveEnforcementPolicy(keepalive.EnforcementPolicy{ + MinTime: enfPol.MinTime, + PermitWithoutStream: enfPol.PermitWithoutStream, + })) + } + } + + var uInterceptors []grpc.UnaryServerInterceptor + var sInterceptors []grpc.StreamServerInterceptor + + if gss.Auth != nil { + authenticator, err := gss.Auth.GetServerAuthenticator(host.GetExtensions()) + if err != nil { + return nil, err + } + + uInterceptors = append(uInterceptors, func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp any, err error) { + return authUnaryServerInterceptor(ctx, req, info, handler, authenticator) + }) + sInterceptors = append(sInterceptors, func(srv any, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { + return authStreamServerInterceptor(srv, ss, info, handler, authenticator) + }) + } + + otelOpts := []otelgrpc.Option{ + otelgrpc.WithTracerProvider(settings.TracerProvider), + otelgrpc.WithMeterProvider(settings.MeterProvider), + otelgrpc.WithPropagators(otel.GetTextMapPropagator()), + } + + // Enable OpenTelemetry observability plugin. + + uInterceptors = append(uInterceptors, enhanceWithClientInformation(gss.IncludeMetadata)) + sInterceptors = append(sInterceptors, enhanceStreamWithClientInformation(gss.IncludeMetadata)) + + opts = append(opts, grpc.StatsHandler(otelgrpc.NewServerHandler(otelOpts...)), grpc.ChainUnaryInterceptor(uInterceptors...), grpc.ChainStreamInterceptor(sInterceptors...)) + + return opts, nil +} + +// getGRPCCompressionName returns compression name registered in grpc. +func getGRPCCompressionName(compressionType configcompression.Type) (string, error) { + switch compressionType { + case configcompression.TypeGzip: + return gzip.Name, nil + case configcompression.TypeSnappy: + return snappy.Name, nil + case configcompression.TypeZstd: + return zstd.Name, nil + default: + return "", fmt.Errorf("unsupported compression type %q", compressionType) + } +} + +// enhanceWithClientInformation intercepts the incoming RPC, replacing the incoming context with one that includes +// a client.Info, potentially with the peer's address. +func enhanceWithClientInformation(includeMetadata bool) func(ctx context.Context, req any, _ *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { + return func(ctx context.Context, req any, _ *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { + return handler(contextWithClient(ctx, includeMetadata), req) + } +} + +func enhanceStreamWithClientInformation(includeMetadata bool) func(srv any, ss grpc.ServerStream, _ *grpc.StreamServerInfo, handler grpc.StreamHandler) error { + return func(srv any, ss grpc.ServerStream, _ *grpc.StreamServerInfo, handler grpc.StreamHandler) error { + return handler(srv, wrapServerStream(contextWithClient(ss.Context(), includeMetadata), ss)) + } +} + +// contextWithClient attempts to add the peer address to the client.Info from the context. When no +// client.Info exists in the context, one is created. +func contextWithClient(ctx context.Context, includeMetadata bool) context.Context { + cl := client.FromContext(ctx) + if p, ok := peer.FromContext(ctx); ok { + cl.Addr = p.Addr + } + if includeMetadata { + if md, ok := metadata.FromIncomingContext(ctx); ok { + copiedMD := md.Copy() + if len(md[client.MetadataHostName]) == 0 && len(md[":authority"]) > 0 { + copiedMD[client.MetadataHostName] = md[":authority"] + } + cl.Metadata = client.NewMetadata(copiedMD) + } + } + return client.NewContext(ctx, cl) +} + +func authUnaryServerInterceptor(ctx context.Context, req any, _ *grpc.UnaryServerInfo, handler grpc.UnaryHandler, server auth.Server) (any, error) { + headers, ok := metadata.FromIncomingContext(ctx) + if !ok { + return nil, errMetadataNotFound + } + + ctx, err := server.Authenticate(ctx, headers) + if err != nil { + return nil, err + } + + return handler(ctx, req) +} + +func authStreamServerInterceptor(srv any, stream grpc.ServerStream, _ *grpc.StreamServerInfo, handler grpc.StreamHandler, server auth.Server) error { + ctx := stream.Context() + headers, ok := metadata.FromIncomingContext(ctx) + if !ok { + return errMetadataNotFound + } + + ctx, err := server.Authenticate(ctx, headers) + if err != nil { + return err + } + + return handler(srv, wrapServerStream(ctx, stream)) +} diff --git a/config/configgrpc/configgrpc_benchmark_test.go b/config/configgrpc/configgrpc_benchmark_test.go new file mode 100644 index 000000000000..1ad755f2b4f6 --- /dev/null +++ b/config/configgrpc/configgrpc_benchmark_test.go @@ -0,0 +1,168 @@ +// Copyright The OpenTelemetry Authors +// Copyright 2014 gRPC authors. +// SPDX-License-Identifier: Apache-2.0 + +package configgrpc + +import ( + "bytes" + "fmt" + "testing" + + "github.com/mostynb/go-grpc-compression/nonclobbering/snappy" + "github.com/mostynb/go-grpc-compression/nonclobbering/zstd" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/encoding" + "google.golang.org/grpc/encoding/gzip" + "google.golang.org/grpc/status" + + "go.opentelemetry.io/collector/pdata/plog" + "go.opentelemetry.io/collector/pdata/pmetric" + "go.opentelemetry.io/collector/pdata/ptrace" + "go.opentelemetry.io/collector/pdata/testdata" +) + +func BenchmarkCompressors(b *testing.B) { + payloads := setupTestPayloads() + + compressors := make([]encoding.Compressor, 0) + compressors = append(compressors, encoding.GetCompressor(gzip.Name)) + compressors = append(compressors, encoding.GetCompressor(zstd.Name)) + compressors = append(compressors, encoding.GetCompressor(snappy.Name)) + + for _, payload := range payloads { + for _, compressor := range compressors { + fmt.Printf(payload.name) + messageBytes, err := payload.marshaler.marshal(payload.message) + if err != nil { + b.Errorf("marshal(_) returned an error") + } + + compressedBytes, err := compress(compressor, messageBytes) + if err != nil { + b.Errorf("Compressor.Compress(_) returned an error") + } + + name := fmt.Sprintf("%v/raw_bytes_%v/compressed_bytes_%v/compressor_%v", payload.name, len(messageBytes), len(compressedBytes), compressor.Name()) + + b.Run(name, func(b *testing.B) { + b.ResetTimer() + for i := 0; i < b.N; i++ { + if err != nil { + b.Errorf("marshal(_) returned an error") + } + _, err := compress(compressor, messageBytes) + if err != nil { + b.Errorf("compress(_) returned an error") + } + } + }) + } + } +} + +func compress(compressor encoding.Compressor, in []byte) ([]byte, error) { + if compressor == nil { + return nil, nil + } + wrapErr := func(err error) error { + return status.Errorf(codes.Internal, "error while compressing: %v", err.Error()) + } + cbuf := &bytes.Buffer{} + z, err := compressor.Compress(cbuf) + if err != nil { + return nil, wrapErr(err) + } + if _, err := z.Write(in); err != nil { + return nil, wrapErr(err) + } + if err := z.Close(); err != nil { + return nil, wrapErr(err) + } + return cbuf.Bytes(), nil +} + +type testPayload struct { + name string + message any + marshaler marshaler +} + +type marshaler interface { + marshal(any) ([]byte, error) +} + +type logMarshaler struct { + plog.Marshaler +} + +func (m *logMarshaler) marshal(e any) ([]byte, error) { + return m.MarshalLogs(e.(plog.Logs)) +} + +type traceMarshaler struct { + ptrace.Marshaler +} + +func (m *traceMarshaler) marshal(e any) ([]byte, error) { + return m.MarshalTraces(e.(ptrace.Traces)) +} + +type metricsMarshaler struct { + pmetric.Marshaler +} + +func (m *metricsMarshaler) marshal(e any) ([]byte, error) { + return m.MarshalMetrics(e.(pmetric.Metrics)) +} + +func setupTestPayloads() []testPayload { + payloads := make([]testPayload, 0) + + // log payloads + logMarshaler := &logMarshaler{Marshaler: &plog.ProtoMarshaler{}} + payloads = append(payloads, testPayload{ + name: "sm_log_request", + message: testdata.GenerateLogs(1), + marshaler: logMarshaler}) + payloads = append(payloads, testPayload{ + name: "md_log_request", + message: testdata.GenerateLogs(2), + marshaler: logMarshaler}) + payloads = append(payloads, testPayload{ + name: "lg_log_request", + message: testdata.GenerateLogs(50), + marshaler: logMarshaler}) + + // trace payloads + tracesMarshaler := &traceMarshaler{Marshaler: &ptrace.ProtoMarshaler{}} + payloads = append(payloads, testPayload{ + name: "sm_trace_request", + message: testdata.GenerateTraces(1), + marshaler: tracesMarshaler}) + payloads = append(payloads, testPayload{ + name: "md_trace_request", + message: testdata.GenerateTraces(2), + marshaler: tracesMarshaler}) + payloads = append(payloads, testPayload{ + name: "lg_trace_request", + message: testdata.GenerateTraces(50), + marshaler: tracesMarshaler}) + + // metric payloads + metricsMarshaler := &metricsMarshaler{Marshaler: &pmetric.ProtoMarshaler{}} + payloads = append(payloads, testPayload{ + name: "sm_metric_request", + message: testdata.GenerateMetrics(1), + marshaler: metricsMarshaler}) + payloads = append(payloads, testPayload{ + name: "md_metric_request", + message: testdata.GenerateMetrics(2), + marshaler: metricsMarshaler}) + payloads = append(payloads, testPayload{ + name: "lg_metric_request", + message: testdata.GenerateMetrics(50), + marshaler: metricsMarshaler}) + + return payloads +} diff --git a/config/configgrpc/configgrpc_test.go b/config/configgrpc/configgrpc_test.go new file mode 100644 index 000000000000..2a1b34cb4eeb --- /dev/null +++ b/config/configgrpc/configgrpc_test.go @@ -0,0 +1,1098 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package configgrpc + +import ( + "context" + "errors" + "net" + "os" + "path/filepath" + "runtime" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/zap" + "go.uber.org/zap/zaptest/observer" + "google.golang.org/grpc" + "google.golang.org/grpc/balancer" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/peer" + + "go.opentelemetry.io/collector/client" + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/component/componenttest" + "go.opentelemetry.io/collector/config/configauth" + "go.opentelemetry.io/collector/config/configcompression" + "go.opentelemetry.io/collector/config/confignet" + "go.opentelemetry.io/collector/config/configopaque" + "go.opentelemetry.io/collector/config/configtls" + "go.opentelemetry.io/collector/extension/auth" + "go.opentelemetry.io/collector/extension/auth/authtest" + "go.opentelemetry.io/collector/pdata/ptrace/ptraceotlp" +) + +// testBalancerBuilder facilitates testing validateBalancerName(). +type testBalancerBuilder struct{} + +func (testBalancerBuilder) Build(balancer.ClientConn, balancer.BuildOptions) balancer.Balancer { + return nil +} + +func (testBalancerBuilder) Name() string { + return "configgrpc_balancer_test" +} + +func init() { + balancer.Register(testBalancerBuilder{}) +} + +var ( + componentID = component.MustNewID("component") + testAuthID = component.MustNewID("testauth") + mockID = component.MustNewID("mock") + doesntExistID = component.MustNewID("doesntexist") +) + +func TestDefaultGrpcClientSettings(t *testing.T) { + tt, err := componenttest.SetupTelemetry(componentID) + require.NoError(t, err) + t.Cleanup(func() { require.NoError(t, tt.Shutdown(context.Background())) }) + + gcs := &ClientConfig{ + TLSSetting: configtls.ClientConfig{ + Insecure: true, + }, + } + opts, err := gcs.toDialOptions(componenttest.NewNopHost(), tt.TelemetrySettings()) + assert.NoError(t, err) + assert.Len(t, opts, 2) +} + +func TestAllGrpcClientSettings(t *testing.T) { + tt, err := componenttest.SetupTelemetry(componentID) + require.NoError(t, err) + t.Cleanup(func() { require.NoError(t, tt.Shutdown(context.Background())) }) + + tests := []struct { + settings ClientConfig + name string + host component.Host + }{ + { + name: "test all with gzip compression", + settings: ClientConfig{ + Headers: map[string]configopaque.String{ + "test": "test", + }, + Endpoint: "localhost:1234", + Compression: configcompression.TypeGzip, + TLSSetting: configtls.ClientConfig{ + Insecure: false, + }, + Keepalive: &KeepaliveClientConfig{ + Time: time.Second, + Timeout: time.Second, + PermitWithoutStream: true, + }, + ReadBufferSize: 1024, + WriteBufferSize: 1024, + WaitForReady: true, + BalancerName: "round_robin", + Authority: "pseudo-authority", + Auth: &configauth.Authentication{AuthenticatorID: testAuthID}, + }, + host: &mockHost{ + ext: map[component.ID]component.Component{ + testAuthID: &authtest.MockClient{}, + }, + }, + }, + { + name: "test all with snappy compression", + settings: ClientConfig{ + Headers: map[string]configopaque.String{ + "test": "test", + }, + Endpoint: "localhost:1234", + Compression: configcompression.TypeSnappy, + TLSSetting: configtls.ClientConfig{ + Insecure: false, + }, + Keepalive: &KeepaliveClientConfig{ + Time: time.Second, + Timeout: time.Second, + PermitWithoutStream: true, + }, + ReadBufferSize: 1024, + WriteBufferSize: 1024, + WaitForReady: true, + BalancerName: "round_robin", + Authority: "pseudo-authority", + Auth: &configauth.Authentication{AuthenticatorID: testAuthID}, + }, + host: &mockHost{ + ext: map[component.ID]component.Component{ + testAuthID: &authtest.MockClient{}, + }, + }, + }, + { + name: "test all with zstd compression", + settings: ClientConfig{ + Headers: map[string]configopaque.String{ + "test": "test", + }, + Endpoint: "localhost:1234", + Compression: configcompression.TypeZstd, + TLSSetting: configtls.ClientConfig{ + Insecure: false, + }, + Keepalive: &KeepaliveClientConfig{ + Time: time.Second, + Timeout: time.Second, + PermitWithoutStream: true, + }, + ReadBufferSize: 1024, + WriteBufferSize: 1024, + WaitForReady: true, + BalancerName: "configgrpc_balancer_test", + Authority: "pseudo-authority", + Auth: &configauth.Authentication{AuthenticatorID: testAuthID}, + }, + host: &mockHost{ + ext: map[component.ID]component.Component{ + testAuthID: &authtest.MockClient{}, + }, + }, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + opts, err := test.settings.toDialOptions(test.host, tt.TelemetrySettings()) + assert.NoError(t, err) + assert.Len(t, opts, 9) + }) + } +} + +func TestDefaultGrpcServerSettings(t *testing.T) { + gss := &ServerConfig{ + NetAddr: confignet.AddrConfig{ + Endpoint: "0.0.0.0:1234", + }, + } + opts, err := gss.toServerOption(componenttest.NewNopHost(), componenttest.NewNopTelemetrySettings()) + assert.NoError(t, err) + assert.Len(t, opts, 3) +} + +func TestAllGrpcServerSettingsExceptAuth(t *testing.T) { + gss := &ServerConfig{ + NetAddr: confignet.AddrConfig{ + Endpoint: "localhost:1234", + Transport: confignet.TransportTypeTCP, + }, + TLSSetting: &configtls.ServerConfig{ + Config: configtls.Config{}, + ClientCAFile: "", + }, + MaxRecvMsgSizeMiB: 1, + MaxConcurrentStreams: 1024, + ReadBufferSize: 1024, + WriteBufferSize: 1024, + Keepalive: &KeepaliveServerConfig{ + ServerParameters: &KeepaliveServerParameters{ + MaxConnectionIdle: time.Second, + MaxConnectionAge: time.Second, + MaxConnectionAgeGrace: time.Second, + Time: time.Second, + Timeout: time.Second, + }, + EnforcementPolicy: &KeepaliveEnforcementPolicy{ + MinTime: time.Second, + PermitWithoutStream: true, + }, + }, + } + opts, err := gss.toServerOption(componenttest.NewNopHost(), componenttest.NewNopTelemetrySettings()) + assert.NoError(t, err) + assert.Len(t, opts, 10) +} + +func TestGrpcServerAuthSettings(t *testing.T) { + gss := &ServerConfig{ + NetAddr: confignet.AddrConfig{ + Endpoint: "0.0.0.0:1234", + }, + } + gss.Auth = &configauth.Authentication{ + AuthenticatorID: mockID, + } + host := &mockHost{ + ext: map[component.ID]component.Component{ + mockID: auth.NewServer(), + }, + } + srv, err := gss.ToServer(context.Background(), host, componenttest.NewNopTelemetrySettings()) + assert.NoError(t, err) + assert.NotNil(t, srv) +} + +func TestGRPCClientSettingsError(t *testing.T) { + tt, err := componenttest.SetupTelemetry(componentID) + require.NoError(t, err) + t.Cleanup(func() { require.NoError(t, tt.Shutdown(context.Background())) }) + + tests := []struct { + settings ClientConfig + err string + host component.Host + }{ + { + err: "^failed to load TLS config: failed to load CA CertPool File: failed to load cert /doesnt/exist:", + settings: ClientConfig{ + Headers: nil, + Endpoint: "", + Compression: "", + TLSSetting: configtls.ClientConfig{ + Config: configtls.Config{ + CAFile: "/doesnt/exist", + }, + Insecure: false, + ServerName: "", + }, + Keepalive: nil, + }, + }, + { + err: "^failed to load TLS config: failed to load TLS cert and key: for auth via TLS, provide both certificate and key, or neither", + settings: ClientConfig{ + Headers: nil, + Endpoint: "", + Compression: "", + TLSSetting: configtls.ClientConfig{ + Config: configtls.Config{ + CertFile: "/doesnt/exist", + }, + Insecure: false, + ServerName: "", + }, + Keepalive: nil, + }, + }, + { + err: "invalid balancer_name: test", + settings: ClientConfig{ + Headers: map[string]configopaque.String{ + "test": "test", + }, + Endpoint: "localhost:1234", + Compression: "gzip", + TLSSetting: configtls.ClientConfig{ + Insecure: false, + }, + Keepalive: &KeepaliveClientConfig{ + Time: time.Second, + Timeout: time.Second, + PermitWithoutStream: true, + }, + ReadBufferSize: 1024, + WriteBufferSize: 1024, + WaitForReady: true, + BalancerName: "test", + }, + }, + { + err: "failed to resolve authenticator \"doesntexist\": authenticator not found", + settings: ClientConfig{ + Endpoint: "localhost:1234", + Auth: &configauth.Authentication{AuthenticatorID: doesntExistID}, + }, + host: &mockHost{ext: map[component.ID]component.Component{}}, + }, + { + err: "no extensions configuration available", + settings: ClientConfig{ + Endpoint: "localhost:1234", + Auth: &configauth.Authentication{AuthenticatorID: doesntExistID}, + }, + host: &mockHost{}, + }, + { + err: "unsupported compression type \"zlib\"", + settings: ClientConfig{ + Endpoint: "localhost:1234", + TLSSetting: configtls.ClientConfig{ + Insecure: true, + }, + Compression: "zlib", + }, + host: &mockHost{}, + }, + { + err: "unsupported compression type \"deflate\"", + settings: ClientConfig{ + Endpoint: "localhost:1234", + TLSSetting: configtls.ClientConfig{ + Insecure: true, + }, + Compression: "deflate", + }, + host: &mockHost{}, + }, + { + err: "unsupported compression type \"bad\"", + settings: ClientConfig{ + Endpoint: "localhost:1234", + TLSSetting: configtls.ClientConfig{ + Insecure: true, + }, + Compression: "bad", + }, + host: &mockHost{}, + }, + } + for _, test := range tests { + t.Run(test.err, func(t *testing.T) { + _, err := test.settings.ToClientConn(context.Background(), test.host, tt.TelemetrySettings()) + assert.Error(t, err) + assert.Regexp(t, test.err, err) + }) + } +} + +func TestUseSecure(t *testing.T) { + tt, err := componenttest.SetupTelemetry(componentID) + require.NoError(t, err) + t.Cleanup(func() { require.NoError(t, tt.Shutdown(context.Background())) }) + + gcs := &ClientConfig{ + Headers: nil, + Endpoint: "", + Compression: "", + TLSSetting: configtls.ClientConfig{}, + Keepalive: nil, + } + dialOpts, err := gcs.toDialOptions(componenttest.NewNopHost(), tt.TelemetrySettings()) + assert.NoError(t, err) + assert.Len(t, dialOpts, 2) +} + +func TestGRPCServerWarning(t *testing.T) { + tests := []struct { + name string + settings ServerConfig + len int + }{ + { + settings: ServerConfig{ + NetAddr: confignet.AddrConfig{ + Endpoint: "0.0.0.0:1234", + Transport: confignet.TransportTypeTCP, + }, + }, + len: 1, + }, + { + settings: ServerConfig{ + NetAddr: confignet.AddrConfig{ + Endpoint: "127.0.0.1:1234", + Transport: confignet.TransportTypeTCP, + }, + }, + len: 0, + }, + { + settings: ServerConfig{ + NetAddr: confignet.AddrConfig{ + Endpoint: "0.0.0.0:1234", + Transport: confignet.TransportTypeUnix, + }, + }, + len: 0, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + set := componenttest.NewNopTelemetrySettings() + logger, observed := observer.New(zap.DebugLevel) + set.Logger = zap.New(logger) + + opts, err := test.settings.toServerOption(componenttest.NewNopHost(), set) + require.NoError(t, err) + require.NotNil(t, opts) + _ = grpc.NewServer(opts...) + + require.Len(t, observed.FilterLevelExact(zap.WarnLevel).All(), test.len) + }) + } + +} + +func TestGRPCServerSettingsError(t *testing.T) { + tests := []struct { + settings ServerConfig + err string + }{ + { + err: "^failed to load TLS config: failed to load CA CertPool File: failed to load cert /doesnt/exist:", + settings: ServerConfig{ + NetAddr: confignet.AddrConfig{ + Endpoint: "127.0.0.1:1234", + Transport: confignet.TransportTypeTCP, + }, + TLSSetting: &configtls.ServerConfig{ + Config: configtls.Config{ + CAFile: "/doesnt/exist", + }, + }, + }, + }, + { + err: "^failed to load TLS config: failed to load TLS cert and key: for auth via TLS, provide both certificate and key, or neither", + settings: ServerConfig{ + NetAddr: confignet.AddrConfig{ + Endpoint: "127.0.0.1:1234", + Transport: confignet.TransportTypeTCP, + }, + TLSSetting: &configtls.ServerConfig{ + Config: configtls.Config{ + CertFile: "/doesnt/exist", + }, + }, + }, + }, + { + err: "^failed to load client CA CertPool: failed to load CA /doesnt/exist:", + settings: ServerConfig{ + NetAddr: confignet.AddrConfig{ + Endpoint: "127.0.0.1:1234", + Transport: confignet.TransportTypeTCP, + }, + TLSSetting: &configtls.ServerConfig{ + ClientCAFile: "/doesnt/exist", + }, + }, + }, + } + for _, test := range tests { + t.Run(test.err, func(t *testing.T) { + _, err := test.settings.ToServer(context.Background(), componenttest.NewNopHost(), componenttest.NewNopTelemetrySettings()) + assert.Regexp(t, test.err, err) + }) + } +} + +func TestGRPCServerSettings_ToListener_Error(t *testing.T) { + settings := ServerConfig{ + NetAddr: confignet.AddrConfig{ + Endpoint: "127.0.0.1:1234567", + Transport: confignet.TransportTypeTCP, + }, + } + _, err := settings.NetAddr.Listen(context.Background()) + assert.Error(t, err) +} + +func TestHttpReception(t *testing.T) { + tt, err := componenttest.SetupTelemetry(componentID) + require.NoError(t, err) + t.Cleanup(func() { require.NoError(t, tt.Shutdown(context.Background())) }) + + tests := []struct { + name string + tlsServerCreds *configtls.ServerConfig + tlsClientCreds *configtls.ClientConfig + hasError bool + }{ + { + name: "noTLS", + tlsServerCreds: nil, + tlsClientCreds: &configtls.ClientConfig{ + Insecure: true, + }, + }, + { + name: "TLS", + tlsServerCreds: &configtls.ServerConfig{ + Config: configtls.Config{ + CAFile: filepath.Join("testdata", "ca.crt"), + CertFile: filepath.Join("testdata", "server.crt"), + KeyFile: filepath.Join("testdata", "server.key"), + }, + }, + tlsClientCreds: &configtls.ClientConfig{ + Config: configtls.Config{ + CAFile: filepath.Join("testdata", "ca.crt"), + }, + ServerName: "localhost", + }, + }, + { + name: "NoServerCertificates", + tlsServerCreds: &configtls.ServerConfig{ + Config: configtls.Config{ + CAFile: filepath.Join("testdata", "ca.crt"), + }, + }, + tlsClientCreds: &configtls.ClientConfig{ + Config: configtls.Config{ + CAFile: filepath.Join("testdata", "ca.crt"), + }, + ServerName: "localhost", + }, + hasError: true, + }, + { + name: "mTLS", + tlsServerCreds: &configtls.ServerConfig{ + Config: configtls.Config{ + CAFile: filepath.Join("testdata", "ca.crt"), + CertFile: filepath.Join("testdata", "server.crt"), + KeyFile: filepath.Join("testdata", "server.key"), + }, + ClientCAFile: filepath.Join("testdata", "ca.crt"), + }, + tlsClientCreds: &configtls.ClientConfig{ + Config: configtls.Config{ + CAFile: filepath.Join("testdata", "ca.crt"), + CertFile: filepath.Join("testdata", "client.crt"), + KeyFile: filepath.Join("testdata", "client.key"), + }, + ServerName: "localhost", + }, + }, + { + name: "NoClientCertificate", + tlsServerCreds: &configtls.ServerConfig{ + Config: configtls.Config{ + CAFile: filepath.Join("testdata", "ca.crt"), + CertFile: filepath.Join("testdata", "server.crt"), + KeyFile: filepath.Join("testdata", "server.key"), + }, + ClientCAFile: filepath.Join("testdata", "ca.crt"), + }, + tlsClientCreds: &configtls.ClientConfig{ + Config: configtls.Config{ + CAFile: filepath.Join("testdata", "ca.crt"), + }, + ServerName: "localhost", + }, + hasError: true, + }, + { + name: "WrongClientCA", + tlsServerCreds: &configtls.ServerConfig{ + Config: configtls.Config{ + CAFile: filepath.Join("testdata", "ca.crt"), + CertFile: filepath.Join("testdata", "server.crt"), + KeyFile: filepath.Join("testdata", "server.key"), + }, + ClientCAFile: filepath.Join("testdata", "server.crt"), + }, + tlsClientCreds: &configtls.ClientConfig{ + Config: configtls.Config{ + CAFile: filepath.Join("testdata", "ca.crt"), + CertFile: filepath.Join("testdata", "client.crt"), + KeyFile: filepath.Join("testdata", "client.key"), + }, + ServerName: "localhost", + }, + hasError: true, + }, + } + // prepare + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + gss := &ServerConfig{ + NetAddr: confignet.AddrConfig{ + Endpoint: "localhost:0", + Transport: confignet.TransportTypeTCP, + }, + TLSSetting: test.tlsServerCreds, + } + ln, err := gss.NetAddr.Listen(context.Background()) + assert.NoError(t, err) + s, err := gss.ToServer(context.Background(), componenttest.NewNopHost(), componenttest.NewNopTelemetrySettings()) + assert.NoError(t, err) + ptraceotlp.RegisterGRPCServer(s, &grpcTraceServer{}) + + go func() { + _ = s.Serve(ln) + }() + + gcs := &ClientConfig{ + Endpoint: ln.Addr().String(), + TLSSetting: *test.tlsClientCreds, + } + grpcClientConn, errClient := gcs.ToClientConn(context.Background(), componenttest.NewNopHost(), tt.TelemetrySettings()) + assert.NoError(t, errClient) + defer func() { assert.NoError(t, grpcClientConn.Close()) }() + c := ptraceotlp.NewGRPCClient(grpcClientConn) + ctx, cancelFunc := context.WithTimeout(context.Background(), 2*time.Second) + resp, errResp := c.Export(ctx, ptraceotlp.NewExportRequest(), grpc.WaitForReady(true)) + if test.hasError { + assert.Error(t, errResp) + } else { + assert.NoError(t, errResp) + assert.NotNil(t, resp) + } + cancelFunc() + s.Stop() + }) + } +} + +func TestReceiveOnUnixDomainSocket(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("skipping test on windows") + } + tt, err := componenttest.SetupTelemetry(componentID) + require.NoError(t, err) + t.Cleanup(func() { require.NoError(t, tt.Shutdown(context.Background())) }) + + socketName := tempSocketName(t) + gss := &ServerConfig{ + NetAddr: confignet.AddrConfig{ + Endpoint: socketName, + Transport: confignet.TransportTypeUnix, + }, + } + ln, err := gss.NetAddr.Listen(context.Background()) + assert.NoError(t, err) + srv, err := gss.ToServer(context.Background(), componenttest.NewNopHost(), componenttest.NewNopTelemetrySettings()) + assert.NoError(t, err) + ptraceotlp.RegisterGRPCServer(srv, &grpcTraceServer{}) + + go func() { + _ = srv.Serve(ln) + }() + + gcs := &ClientConfig{ + Endpoint: "unix://" + ln.Addr().String(), + TLSSetting: configtls.ClientConfig{ + Insecure: true, + }, + } + grpcClientConn, errClient := gcs.ToClientConn(context.Background(), componenttest.NewNopHost(), tt.TelemetrySettings()) + assert.NoError(t, errClient) + defer func() { assert.NoError(t, grpcClientConn.Close()) }() + c := ptraceotlp.NewGRPCClient(grpcClientConn) + ctx, cancelFunc := context.WithTimeout(context.Background(), 2*time.Second) + resp, errResp := c.Export(ctx, ptraceotlp.NewExportRequest(), grpc.WaitForReady(true)) + assert.NoError(t, errResp) + assert.NotNil(t, resp) + cancelFunc() + srv.Stop() +} + +func TestContextWithClient(t *testing.T) { + testCases := []struct { + desc string + input context.Context + doMetadata bool + expected client.Info + }{ + { + desc: "no peer information, empty client", + input: context.Background(), + expected: client.Info{}, + }, + { + desc: "existing client with IP, no peer information", + input: client.NewContext(context.Background(), client.Info{ + Addr: &net.IPAddr{ + IP: net.IPv4(1, 2, 3, 4), + }, + }), + expected: client.Info{ + Addr: &net.IPAddr{ + IP: net.IPv4(1, 2, 3, 4), + }, + }, + }, + { + desc: "empty client, with peer information", + input: peer.NewContext(context.Background(), &peer.Peer{ + Addr: &net.IPAddr{ + IP: net.IPv4(1, 2, 3, 4), + }, + }), + expected: client.Info{ + Addr: &net.IPAddr{ + IP: net.IPv4(1, 2, 3, 4), + }, + }, + }, + { + desc: "existing client, existing IP gets overridden with peer information", + input: peer.NewContext(client.NewContext(context.Background(), client.Info{ + Addr: &net.IPAddr{ + IP: net.IPv4(1, 2, 3, 4), + }, + }), &peer.Peer{ + Addr: &net.IPAddr{ + IP: net.IPv4(1, 2, 3, 5), + }, + }), + expected: client.Info{ + Addr: &net.IPAddr{ + IP: net.IPv4(1, 2, 3, 5), + }, + }, + }, + { + desc: "existing client with metadata", + input: client.NewContext(context.Background(), client.Info{ + Metadata: client.NewMetadata(map[string][]string{"test-metadata-key": {"test-value"}}), + }), + doMetadata: true, + expected: client.Info{ + Metadata: client.NewMetadata(map[string][]string{"test-metadata-key": {"test-value"}}), + }, + }, + { + desc: "existing client with metadata in context", + input: metadata.NewIncomingContext( + client.NewContext(context.Background(), client.Info{}), + metadata.Pairs("test-metadata-key", "test-value"), + ), + doMetadata: true, + expected: client.Info{ + Metadata: client.NewMetadata(map[string][]string{"test-metadata-key": {"test-value"}}), + }, + }, + { + desc: "existing client with metadata in context, no metadata processing", + input: metadata.NewIncomingContext( + client.NewContext(context.Background(), client.Info{}), + metadata.Pairs("test-metadata-key", "test-value"), + ), + expected: client.Info{}, + }, + { + desc: "existing client with Host and metadata", + input: metadata.NewIncomingContext( + client.NewContext(context.Background(), client.Info{}), + metadata.Pairs("test-metadata-key", "test-value", ":authority", "localhost:55443"), + ), + doMetadata: true, + expected: client.Info{ + Metadata: client.NewMetadata(map[string][]string{"test-metadata-key": {"test-value"}, ":authority": {"localhost:55443"}, "Host": {"localhost:55443"}}), + }, + }, + } + for _, tC := range testCases { + t.Run(tC.desc, func(t *testing.T) { + cl := client.FromContext(contextWithClient(tC.input, tC.doMetadata)) + assert.Equal(t, tC.expected, cl) + }) + } +} + +func TestStreamInterceptorEnhancesClient(t *testing.T) { + // prepare + inCtx := peer.NewContext(context.Background(), &peer.Peer{ + Addr: &net.IPAddr{IP: net.IPv4(1, 1, 1, 1)}, + }) + var outContext context.Context + + stream := &mockedStream{ + ctx: inCtx, + } + + handler := func(_ any, stream grpc.ServerStream) error { + outContext = stream.Context() + return nil + } + + // test + err := enhanceStreamWithClientInformation(false)(nil, stream, nil, handler) + + // verify + assert.NoError(t, err) + + cl := client.FromContext(outContext) + assert.Equal(t, "1.1.1.1", cl.Addr.String()) +} + +type mockedStream struct { + ctx context.Context + grpc.ServerStream +} + +func (ms *mockedStream) Context() context.Context { + return ms.ctx +} + +func TestClientInfoInterceptors(t *testing.T) { + testCases := []struct { + desc string + tester func(context.Context, ptraceotlp.GRPCClient) + }{ + { + // we only have unary services, we don't have any clients we could use + // to test with streaming services + desc: "unary", + tester: func(ctx context.Context, cl ptraceotlp.GRPCClient) { + resp, errResp := cl.Export(ctx, ptraceotlp.NewExportRequest()) + require.NoError(t, errResp) + require.NotNil(t, resp) + }, + }, + } + for _, tC := range testCases { + t.Run(tC.desc, func(t *testing.T) { + mock := &grpcTraceServer{} + var l net.Listener + + // prepare the server + { + gss := &ServerConfig{ + NetAddr: confignet.AddrConfig{ + Endpoint: "localhost:0", + Transport: confignet.TransportTypeTCP, + }, + } + srv, err := gss.ToServer(context.Background(), componenttest.NewNopHost(), componenttest.NewNopTelemetrySettings()) + require.NoError(t, err) + ptraceotlp.RegisterGRPCServer(srv, mock) + + defer srv.Stop() + + l, err = gss.NetAddr.Listen(context.Background()) + require.NoError(t, err) + + go func() { + _ = srv.Serve(l) + }() + } + + // prepare the client and execute a RPC + { + gcs := &ClientConfig{ + Endpoint: l.Addr().String(), + TLSSetting: configtls.ClientConfig{ + Insecure: true, + }, + } + + tt, err := componenttest.SetupTelemetry(componentID) + require.NoError(t, err) + defer func() { + require.NoError(t, tt.Shutdown(context.Background())) + }() + + grpcClientConn, errClient := gcs.ToClientConn(context.Background(), componenttest.NewNopHost(), tt.TelemetrySettings()) + require.NoError(t, errClient) + defer func() { assert.NoError(t, grpcClientConn.Close()) }() + + cl := ptraceotlp.NewGRPCClient(grpcClientConn) + ctx, cancelFunc := context.WithTimeout(context.Background(), 2*time.Second) + defer cancelFunc() + + // test + tC.tester(ctx, cl) + } + + // verify + cl := client.FromContext(mock.recordedContext) + + // the client address is something like 127.0.0.1:41086 + assert.Contains(t, cl.Addr.String(), "127.0.0.1") + }) + } +} + +func TestDefaultUnaryInterceptorAuthSucceeded(t *testing.T) { + // prepare + handlerCalled := false + authCalled := false + authFunc := func(context.Context, map[string][]string) (context.Context, error) { + authCalled = true + ctx := client.NewContext(context.Background(), client.Info{ + Addr: &net.IPAddr{IP: net.IPv4(1, 2, 3, 4)}, + }) + + return ctx, nil + } + handler := func(ctx context.Context, _ any) (any, error) { + handlerCalled = true + cl := client.FromContext(ctx) + assert.Equal(t, "1.2.3.4", cl.Addr.String()) + return nil, nil + } + ctx := metadata.NewIncomingContext(context.Background(), metadata.Pairs("authorization", "some-auth-data")) + + // test + res, err := authUnaryServerInterceptor(ctx, nil, &grpc.UnaryServerInfo{}, handler, auth.NewServer(auth.WithServerAuthenticate(authFunc))) + + // verify + assert.Nil(t, res) + assert.NoError(t, err) + assert.True(t, authCalled) + assert.True(t, handlerCalled) +} + +func TestDefaultUnaryInterceptorAuthFailure(t *testing.T) { + // prepare + authCalled := false + expectedErr := errors.New("not authenticated") + authFunc := func(context.Context, map[string][]string) (context.Context, error) { + authCalled = true + return context.Background(), expectedErr + } + handler := func(context.Context, any) (any, error) { + assert.FailNow(t, "the handler should not have been called on auth failure!") + return nil, nil + } + ctx := metadata.NewIncomingContext(context.Background(), metadata.Pairs("authorization", "some-auth-data")) + + // test + res, err := authUnaryServerInterceptor(ctx, nil, &grpc.UnaryServerInfo{}, handler, auth.NewServer(auth.WithServerAuthenticate(authFunc))) + + // verify + assert.Nil(t, res) + assert.Equal(t, expectedErr, err) + assert.True(t, authCalled) +} + +func TestDefaultUnaryInterceptorMissingMetadata(t *testing.T) { + // prepare + authFunc := func(context.Context, map[string][]string) (context.Context, error) { + assert.FailNow(t, "the auth func should not have been called!") + return context.Background(), nil + } + handler := func(context.Context, any) (any, error) { + assert.FailNow(t, "the handler should not have been called!") + return nil, nil + } + + // test + res, err := authUnaryServerInterceptor(context.Background(), nil, &grpc.UnaryServerInfo{}, handler, auth.NewServer(auth.WithServerAuthenticate(authFunc))) + + // verify + assert.Nil(t, res) + assert.Equal(t, errMetadataNotFound, err) +} + +func TestDefaultStreamInterceptorAuthSucceeded(t *testing.T) { + // prepare + handlerCalled := false + authCalled := false + authFunc := func(context.Context, map[string][]string) (context.Context, error) { + authCalled = true + ctx := client.NewContext(context.Background(), client.Info{ + Addr: &net.IPAddr{IP: net.IPv4(1, 2, 3, 4)}, + }) + return ctx, nil + } + handler := func(_ any, stream grpc.ServerStream) error { + // ensure that the client information is propagated down to the underlying stream + cl := client.FromContext(stream.Context()) + assert.Equal(t, "1.2.3.4", cl.Addr.String()) + handlerCalled = true + return nil + } + ctx := metadata.NewIncomingContext(context.Background(), metadata.Pairs("authorization", "some-auth-data")) + streamServer := &mockServerStream{ + ctx: ctx, + } + + // test + err := authStreamServerInterceptor(nil, streamServer, &grpc.StreamServerInfo{}, handler, auth.NewServer(auth.WithServerAuthenticate(authFunc))) + + // verify + assert.NoError(t, err) + assert.True(t, authCalled) + assert.True(t, handlerCalled) +} + +func TestDefaultStreamInterceptorAuthFailure(t *testing.T) { + // prepare + authCalled := false + expectedErr := errors.New("not authenticated") + authFunc := func(context.Context, map[string][]string) (context.Context, error) { + authCalled = true + return context.Background(), expectedErr + } + handler := func(any, grpc.ServerStream) error { + assert.FailNow(t, "the handler should not have been called on auth failure!") + return nil + } + ctx := metadata.NewIncomingContext(context.Background(), metadata.Pairs("authorization", "some-auth-data")) + streamServer := &mockServerStream{ + ctx: ctx, + } + + // test + err := authStreamServerInterceptor(nil, streamServer, &grpc.StreamServerInfo{}, handler, auth.NewServer(auth.WithServerAuthenticate(authFunc))) + + // verify + assert.Equal(t, expectedErr, err) + assert.True(t, authCalled) +} + +func TestDefaultStreamInterceptorMissingMetadata(t *testing.T) { + // prepare + authFunc := func(context.Context, map[string][]string) (context.Context, error) { + assert.FailNow(t, "the auth func should not have been called!") + return context.Background(), nil + } + handler := func(any, grpc.ServerStream) error { + assert.FailNow(t, "the handler should not have been called!") + return nil + } + streamServer := &mockServerStream{ + ctx: context.Background(), + } + + // test + err := authStreamServerInterceptor(nil, streamServer, &grpc.StreamServerInfo{}, handler, auth.NewServer(auth.WithServerAuthenticate(authFunc))) + + // verify + assert.Equal(t, errMetadataNotFound, err) +} + +type mockServerStream struct { + grpc.ServerStream + ctx context.Context +} + +func (m *mockServerStream) Context() context.Context { + return m.ctx +} + +type grpcTraceServer struct { + ptraceotlp.UnimplementedGRPCServer + recordedContext context.Context +} + +func (gts *grpcTraceServer) Export(ctx context.Context, _ ptraceotlp.ExportRequest) (ptraceotlp.ExportResponse, error) { + gts.recordedContext = ctx + return ptraceotlp.NewExportResponse(), nil +} + +// tempSocketName provides a temporary Unix socket name for testing. +func tempSocketName(t *testing.T) string { + tmpfile, err := os.CreateTemp("", "sock") + require.NoError(t, err) + require.NoError(t, tmpfile.Close()) + socket := tmpfile.Name() + require.NoError(t, os.Remove(socket)) + return socket +} + +type mockHost struct { + component.Host + ext map[component.ID]component.Component +} + +func (nh *mockHost) GetExtensions() map[component.ID]component.Component { + return nh.ext +} diff --git a/config/configgrpc/doc.go b/config/configgrpc/doc.go new file mode 100644 index 000000000000..36caf4a2dcac --- /dev/null +++ b/config/configgrpc/doc.go @@ -0,0 +1,6 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +// Package configgrpc defines the configuration settings to create +// a gRPC client and server. +package configgrpc // import "go.opentelemetry.io/collector/config/configgrpc" diff --git a/config/configgrpc/go.mod b/config/configgrpc/go.mod new file mode 100644 index 000000000000..ef56d9560a86 --- /dev/null +++ b/config/configgrpc/go.mod @@ -0,0 +1,100 @@ +module go.opentelemetry.io/collector/config/configgrpc + +go 1.21 + +require ( + github.com/mostynb/go-grpc-compression v1.2.2 + github.com/stretchr/testify v1.9.0 + go.opentelemetry.io/collector v0.98.0 + go.opentelemetry.io/collector/component v0.98.0 + go.opentelemetry.io/collector/config/configauth v0.98.0 + go.opentelemetry.io/collector/config/configcompression v1.5.0 + go.opentelemetry.io/collector/config/confignet v0.98.0 + go.opentelemetry.io/collector/config/configopaque v1.5.0 + go.opentelemetry.io/collector/config/configtls v0.98.0 + go.opentelemetry.io/collector/config/internal v0.98.0 + go.opentelemetry.io/collector/extension/auth v0.98.0 + go.opentelemetry.io/collector/pdata v1.5.0 + go.opentelemetry.io/collector/pdata/testdata v0.98.0 + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 + go.opentelemetry.io/otel v1.25.0 + go.uber.org/goleak v1.3.0 + go.uber.org/zap v1.27.0 + google.golang.org/grpc v1.62.1 +) + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/go-logr/logr v1.4.1 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/golang/snappy v0.0.4 // indirect + github.com/hashicorp/go-version v1.6.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.17.2 // indirect + github.com/knadh/koanf/maps v0.1.1 // indirect + github.com/knadh/koanf/providers/confmap v0.1.0 // indirect + github.com/knadh/koanf/v2 v2.1.1 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/prometheus/client_golang v1.19.0 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.48.0 // indirect + github.com/prometheus/procfs v0.12.0 // indirect + go.opentelemetry.io/collector/config/configtelemetry v0.98.0 // indirect + go.opentelemetry.io/collector/confmap v0.98.0 // indirect + go.opentelemetry.io/collector/extension v0.98.0 // indirect + go.opentelemetry.io/collector/featuregate v1.5.0 // indirect + go.opentelemetry.io/otel/exporters/prometheus v0.47.0 // indirect + go.opentelemetry.io/otel/metric v1.25.0 // indirect + go.opentelemetry.io/otel/sdk v1.25.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.25.0 // indirect + go.opentelemetry.io/otel/trace v1.25.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + golang.org/x/net v0.23.0 // indirect + golang.org/x/sys v0.18.0 // indirect + golang.org/x/text v0.14.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80 // indirect + google.golang.org/protobuf v1.33.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +replace go.opentelemetry.io/collector => ../../ + +replace go.opentelemetry.io/collector/config/configauth => ../configauth + +replace go.opentelemetry.io/collector/config/configcompression => ../configcompression + +replace go.opentelemetry.io/collector/config/confignet => ../confignet + +replace go.opentelemetry.io/collector/config/configopaque => ../configopaque + +replace go.opentelemetry.io/collector/config/configtls => ../configtls + +replace go.opentelemetry.io/collector/config/configtelemetry => ../configtelemetry + +replace go.opentelemetry.io/collector/config/internal => ../internal + +replace go.opentelemetry.io/collector/extension => ../../extension + +replace go.opentelemetry.io/collector/extension/auth => ../../extension/auth + +replace go.opentelemetry.io/collector/confmap => ../../confmap + +replace go.opentelemetry.io/collector/featuregate => ../../featuregate + +replace go.opentelemetry.io/collector/pdata => ../../pdata + +replace go.opentelemetry.io/collector/pdata/testdata => ../../pdata/testdata + +replace go.opentelemetry.io/collector/component => ../../component + +replace go.opentelemetry.io/collector/consumer => ../../consumer diff --git a/config/configgrpc/go.sum b/config/configgrpc/go.sum new file mode 100644 index 000000000000..df0afff33c75 --- /dev/null +++ b/config/configgrpc/go.sum @@ -0,0 +1,138 @@ +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 h1:TQcrn6Wq+sKGkpyPvppOz99zsMBaUOKXq6HSv655U1c= +github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +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/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= +github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.17.2 h1:RlWWUY/Dr4fL8qk9YG7DTZ7PDgME2V4csBXA8L/ixi4= +github.com/klauspost/compress v1.17.2/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NIs= +github.com/knadh/koanf/maps v0.1.1/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= +github.com/knadh/koanf/providers/confmap v0.1.0 h1:gOkxhHkemwG4LezxxN8DMOFopOPghxRVp7JbIvdvqzU= +github.com/knadh/koanf/providers/confmap v0.1.0/go.mod h1:2uLhxQzJnyHKfxG927awZC7+fyHFdQkd697K4MdLnIU= +github.com/knadh/koanf/v2 v2.1.1 h1:/R8eXqasSTsmDCsAyYj+81Wteg8AqrV9CP6gvsTsOmM= +github.com/knadh/koanf/v2 v2.1.1/go.mod h1:4mnTRbZCK+ALuBXHZMjDfG9y714L7TykVnZkXbMU3Es= +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/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mostynb/go-grpc-compression v1.2.2 h1:XaDbnRvt2+1vgr0b/l0qh4mJAfIxE0bKXtz2Znl3GGI= +github.com/mostynb/go-grpc-compression v1.2.2/go.mod h1:GOCr2KBxXcblCuczg3YdLQlcin1/NfyDA348ckuCH6w= +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/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU= +github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE= +github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= +github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= +github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +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= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 h1:4Pp6oUg3+e/6M4C0A/3kJ2VYa++dsWVTtGgLVj5xtHg= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0= +go.opentelemetry.io/otel v1.25.0 h1:gldB5FfhRl7OJQbUHt/8s0a7cE8fbsPAtdpRaApKy4k= +go.opentelemetry.io/otel v1.25.0/go.mod h1:Wa2ds5NOXEMkCmUou1WA7ZBfLTHWIsp034OVD7AO+Vg= +go.opentelemetry.io/otel/exporters/prometheus v0.47.0 h1:OL6yk1Z/pEGdDnrBbxSsH+t4FY1zXfBRGd7bjwhlMLU= +go.opentelemetry.io/otel/exporters/prometheus v0.47.0/go.mod h1:xF3N4OSICZDVbbYZydz9MHFro1RjmkPUKEvar2utG+Q= +go.opentelemetry.io/otel/metric v1.25.0 h1:LUKbS7ArpFL/I2jJHdJcqMGxkRdxpPHE0VU/D4NuEwA= +go.opentelemetry.io/otel/metric v1.25.0/go.mod h1:rkDLUSd2lC5lq2dFNrX9LGAbINP5B7WBkC78RXCpH5s= +go.opentelemetry.io/otel/sdk v1.25.0 h1:PDryEJPC8YJZQSyLY5eqLeafHtG+X7FWnf3aXMtxbqo= +go.opentelemetry.io/otel/sdk v1.25.0/go.mod h1:oFgzCM2zdsxKzz6zwpTZYLLQsFwc+K0daArPdIhuxkw= +go.opentelemetry.io/otel/sdk/metric v1.25.0 h1:7CiHOy08LbrxMAp4vWpbiPcklunUshVpAvGBrdDRlGw= +go.opentelemetry.io/otel/sdk/metric v1.25.0/go.mod h1:LzwoKptdbBBdYfvtGCzGwk6GWMA3aUzBOwtQpR6Nz7o= +go.opentelemetry.io/otel/trace v1.25.0 h1:tqukZGLwQYRIFtSQM2u2+yfMVTgGVeqRLPUYx1Dq6RM= +go.opentelemetry.io/otel/trace v1.25.0/go.mod h1:hCCs70XM/ljO+BeQkyFnbK28SBIJ/Emuha+ccrCRT7I= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= +golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80 h1:AjyfHzEPEFp/NpvfN5g+KDla3EMojjhRVZc1i7cj+oM= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80/go.mod h1:PAREbraiVEVGVdTZsVWjSbbTtSyGbAgIIvni8a8CD5s= +google.golang.org/grpc v1.62.1 h1:B4n+nfKzOICUXMgyrNd19h/I9oH0L1pizfk1d4zSgTk= +google.golang.org/grpc v1.62.1/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +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= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/config/configgrpc/gzip.go b/config/configgrpc/gzip.go new file mode 100644 index 000000000000..c1b0b45f38c5 --- /dev/null +++ b/config/configgrpc/gzip.go @@ -0,0 +1,9 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package configgrpc // import "go.opentelemetry.io/collector/config/configgrpc" + +import ( + // Import the gzip package which auto-registers the gzip gRPC compressor. + _ "google.golang.org/grpc/encoding/gzip" +) diff --git a/config/configgrpc/package_test.go b/config/configgrpc/package_test.go new file mode 100644 index 000000000000..519657748dae --- /dev/null +++ b/config/configgrpc/package_test.go @@ -0,0 +1,17 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package configgrpc + +import ( + "testing" + + "go.uber.org/goleak" +) + +// The IgnoreTopFunction call prevents catching the leak generated by opencensus +// defaultWorker.Start which at this time is part of the package's init call. +// See https://github.com/open-telemetry/opentelemetry-collector/issues/9165#issuecomment-1874836336 for more context. +func TestMain(m *testing.M) { + goleak.VerifyTestMain(m, goleak.IgnoreTopFunction("go.opencensus.io/stats/view.(*worker).start")) +} diff --git a/config/configgrpc/testdata/ca.crt b/config/configgrpc/testdata/ca.crt new file mode 100644 index 000000000000..2272f84e64d6 --- /dev/null +++ b/config/configgrpc/testdata/ca.crt @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDNjCCAh4CCQC0I5IQT7eziDANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJB +VTESMBAGA1UECAwJQXVzdHJhbGlhMQ8wDQYDVQQHDAZTeWRuZXkxEjAQBgNVBAoM +CU15T3JnTmFtZTEVMBMGA1UEAwwMTXlDb21tb25OYW1lMB4XDTIyMDgwMzA0MTky +MVoXDTMyMDczMTA0MTkyMVowXTELMAkGA1UEBhMCQVUxEjAQBgNVBAgMCUF1c3Ry +YWxpYTEPMA0GA1UEBwwGU3lkbmV5MRIwEAYDVQQKDAlNeU9yZ05hbWUxFTATBgNV +BAMMDE15Q29tbW9uTmFtZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB +AMhGP0dy3zvkdx9zI+/XVjPOWlER0OUp7Sgzidc3nLOk42+bH4ofIVNtOFVqlNKi +O1bImu238VdBhd6R5IZZ1ZdIMcCeDgSJYu2X9wA3m4PKz8IdXo5ly2OHghhmCvqG +WxgqDj5wPXiczQwuf1EcDMtRWbXJ6Z/XH1U68R/kRdNLkiZ2LwtjoQpis5XYckLL +CrdF+AL6GeDIe0Mh9QGs26Vux+2kvaOGNUWRPE6Wt4GkqyKqmzYfR9HbflJ4xHT2 +I+jE1lg+jMBeom7z8Z90RE4GGcHjO+Vens/88r5EAjTnFj1Kb5gL2deSHY1m/++R +Z/kRyg+zQJyw4fAzlAA4+VkCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAM3gRdTKX +eGwGYVmmKqA2vTxeigQYLHml7OSopcWj2wJfxfp49HXPRuvgpQn9iubxO3Zmhd83 +2X1E+T0A8oy5CfxgpAhHb3lY0jm3TjKXm6m+dSODwL3uND8tX+SqR8sRTFxPvPuo +pmvhdTZoRI3EzIiHLTgCuSU25JNP/vrVoKk0JvCkDYTU/WcVfj0v95DTMoWR4JGz +mtBwrgD0EM2XRw5ZMc7sMPli1gqmCbCQUrDZ+rPB78WDCBILBd8Cz75qYTUp98BY +akJyBckdJHAdyEQYDKa9HpmpexOO7IhSXCTEN1DEBgpZgEi/lBDRG/b0OzenUUgt +LUABtWt3pNQ9HA== +-----END CERTIFICATE----- diff --git a/config/configgrpc/testdata/client.crt b/config/configgrpc/testdata/client.crt new file mode 100644 index 000000000000..89e48fb0a857 --- /dev/null +++ b/config/configgrpc/testdata/client.crt @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDVTCCAj2gAwIBAgIJANt5fkUlfxyiMA0GCSqGSIb3DQEBCwUAMF0xCzAJBgNV +BAYTAkFVMRIwEAYDVQQIDAlBdXN0cmFsaWExDzANBgNVBAcMBlN5ZG5leTESMBAG +A1UECgwJTXlPcmdOYW1lMRUwEwYDVQQDDAxNeUNvbW1vbk5hbWUwHhcNMjIwODAz +MDQxOTIxWhcNMzIwNzMxMDQxOTIxWjBdMQswCQYDVQQGEwJBVTESMBAGA1UECAwJ +QXVzdHJhbGlhMQ8wDQYDVQQHDAZTeWRuZXkxEjAQBgNVBAoMCU15T3JnTmFtZTEV +MBMGA1UEAwwMTXlDb21tb25OYW1lMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB +CgKCAQEAm/gURxkdWTDS0TyL2j920SfOtOZIo7DjubWLbZtNLrNCZNBsV+8c/ko/ +wleWmUJQRHeiZkNFs8TK6d8Grks6ta9oNO4CiCCO1kz4QidA827cL5+WaKWEVn8Y +Z8aiEMjDOnpYnb/ycsXpERN/P22jHpFD3DKSwLXoXQvasbSJsZro+AIaPAurFB7W +rMagCptwzGQDzryqVKEmXo+eN4XRxsoE8yroHsGbQ8GCZ+neftgV3Jhi1qcXZ//A +3ApY5lg06n1A03fYBlXE5L9tYKpIRNl2kq45mJ8DX6Tdp4Z1Y15+keIIyQpx4LRf +rtdbMQNJhBFOwpAajTmaKXxeICFRHQIDAQABoxgwFjAUBgNVHREEDTALgglsb2Nh +bGhvc3QwDQYJKoZIhvcNAQELBQADggEBAKWrbMxms658R/wYwLxzWPrZVKFswOJX +TpSkXGkyRnrhhZi3I8EhLZhlpZ9k8dplcvseVAUdX9hJu0BaDWBiW/VlPVUkWpWR +QZzrssAKhmSYMgl3OiayU30vL9bxYsAX9KeOJfnJ4kWoBpnguToED7wrC1lbzrVK +Vj1AiI3hBdKUdPNO0hyb8yfxbP3MOottMkk89DIebtOhqj2KEU7sKrhW9a5P5D7d +0A+0kf/IunUZ4IYFfha6qy0gRMyayfm9ttrPAY6q3faqtWR7nY87/T/7wHr1LQ1/ +Q622p7v3j3y75lGN50kFnSd77ykag/8avEKxOTFoGOQc5VCRYJnJwb4= +-----END CERTIFICATE----- diff --git a/config/configgrpc/testdata/client.key b/config/configgrpc/testdata/client.key new file mode 100644 index 000000000000..5590878ab576 --- /dev/null +++ b/config/configgrpc/testdata/client.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAm/gURxkdWTDS0TyL2j920SfOtOZIo7DjubWLbZtNLrNCZNBs +V+8c/ko/wleWmUJQRHeiZkNFs8TK6d8Grks6ta9oNO4CiCCO1kz4QidA827cL5+W +aKWEVn8YZ8aiEMjDOnpYnb/ycsXpERN/P22jHpFD3DKSwLXoXQvasbSJsZro+AIa +PAurFB7WrMagCptwzGQDzryqVKEmXo+eN4XRxsoE8yroHsGbQ8GCZ+neftgV3Jhi +1qcXZ//A3ApY5lg06n1A03fYBlXE5L9tYKpIRNl2kq45mJ8DX6Tdp4Z1Y15+keII +yQpx4LRfrtdbMQNJhBFOwpAajTmaKXxeICFRHQIDAQABAoIBAQCWxrT7omi/vzYd +9dUQ8Acx3LS0JmaUb71F2x3loJt1iO+nO+FxBIPXw/ltK3U3xWaJOcnx6Biq15R9 +kBAKUEl6OA6aFHi4FhlfS9s3QHFGo6YSF8m0ckXDxGvYbqpfZWVt07Z1EYkUsQRF +cL6zl454T1/1r6I0z+XIhVwuLGRsHt2+GCwSrLMnF9aTUJvPFy5G7YlxmL5q1BFu +F70AK9FLZcYqa5nP1F1HbIQB/zsQ8admpKIy5tjaZiLgctXv2GTzzXDEwEnaJMrq +SPr1dGDhdGs5iYRMOMT5Pp9dIG2+ZSSMHFAn4IRoB/cPJbNEUkgwQOPmDYETqSg5 +tSjfIUw1AoGBAMjE6PlT1/orlHW4QvKmV73YtKPVfi1Coo/F8G45qFaHDkc6bI9W +ySrnvqWcPs++xOZMoGtLuESw/LEluFo8vMX8aQYrVSz4Pb7AvuYbBRE0EVVui7YB +3B1O0c7QTabmfQYeATYD7qSShLccSpUE+FQa6NdrJOxddJLM0Z9K73q7AoGBAMbg +I2+NYB6XME7tyStOS4pkA4y7brG/M8BCaY34nfOJT4Qh6pqZRnDJ4ReopGoXEqWg +hwFRsBNhsji4GGejRBPnYcfJTSuMXSPromgoH1tR0OQbMJB0pCTavbI9j+endlv4 +/P+KV3ZMYOLhL/gaaTG+o6Wh2ehnE/8/rmqGpoIHAoGAHXVG+c5jkkFytxMiP5hI +p4J0ftWEff+Y+p+Ad6veF1QZtDnOU/nX6oO2ZXZXgQPswB3eK+AgWXPen994/USM +LkCq6EzTYpXJ+YMuf3TXeX66TF68ASiks2gtQLsvqZ2IGq2sX9CT43HcJ0Hvb44b +IbwRDgqakFPmFuQWndjQ6qECgYA9bOlFATOY/zWKi2NBHvOyEOYPx6yO9fF0Bo83 +rHyMxfJra1Zc3c6l85S0jAAMTIgT5BsOyz5JHjm/zwyqpgDW7PaEkKZnNvllqNgG +t63HtOOCMOu1EnHIeE9zCBS0hkLGcYcjHoWZIkoiiU8ZoH6xQKKm+/CkGYJRqkei +22f+bQKBgGHq2/ZzgxfblD3blKWp8mh5Kw7c/2VwJRvLEMlgzrnRgF7QNhEcH3Jm +aD/pqzAkqHnLVVQ5ogMKrrLl11jQp4kX74+Ps2Yul7UgzXFYy020mQSpJF/FMjrl +PEqwfCiOT2nLyE30x9VClUOGXy1CxH52Yn/g81ENq3jKTptwh+fI +-----END RSA PRIVATE KEY----- diff --git a/config/configgrpc/testdata/server.crt b/config/configgrpc/testdata/server.crt new file mode 100644 index 000000000000..288f9394aba5 --- /dev/null +++ b/config/configgrpc/testdata/server.crt @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDVTCCAj2gAwIBAgIJANt5fkUlfxyhMA0GCSqGSIb3DQEBCwUAMF0xCzAJBgNV +BAYTAkFVMRIwEAYDVQQIDAlBdXN0cmFsaWExDzANBgNVBAcMBlN5ZG5leTESMBAG +A1UECgwJTXlPcmdOYW1lMRUwEwYDVQQDDAxNeUNvbW1vbk5hbWUwHhcNMjIwODAz +MDQxOTIxWhcNMzIwNzMxMDQxOTIxWjBdMQswCQYDVQQGEwJBVTESMBAGA1UECAwJ +QXVzdHJhbGlhMQ8wDQYDVQQHDAZTeWRuZXkxEjAQBgNVBAoMCU15T3JnTmFtZTEV +MBMGA1UEAwwMTXlDb21tb25OYW1lMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB +CgKCAQEAv1Pm2elKl/IpJlX5NqQjRTlA1rHws8F7v1IuaXB2qfk1MsDCt37OvlbR +4ARrY6zdUIrEQ/wrhQsZ2M5/yaj0rfeCgd/SDUKMAqDvXQXBY2AaLubTAIEMs4rF +R5Zq/pcBNz1zu8kRvRgvVuOpTCPR1kRvKFWAp689lXZhUU/BrQQXhdA993xVMRM9 +u4fZuJLxNGGR/EhqTec4Z65jAZiUfO7ID94PtaxTrzR/Kjr2CiceR5hwdY40Bcme +D3IAd0J6nN1zIihe+Nqg/ImOG7YS+efQIEWJ8eHOoCK5knFBXRy6WwxeCyAPXyIb +DTrqTy67eTDYc0XZ24F/5Q3GSvfMDwIDAQABoxgwFjAUBgNVHREEDTALgglsb2Nh +bGhvc3QwDQYJKoZIhvcNAQELBQADggEBAKeFHP5rQasRS/XGbPkobfbFyTdGnLay +0Vr6+Rs5+4siKlAIhuUP9A/De61CEkFj8NFi2bmXYv8q3qP/z0lrjw7btrvD7Qc7 +lth73k3U2sUVZoqbYQZz0GHCWfZm8yXjP63SKI+81LHbS40ArO0R44BLc9TbbRiR +/LwO/x2+cxs28KdsEkU6jQ6Ly5jyoxw1ysoIeRfIk+FnQD4w29TyGgtX/G15/NN0 +ytByIZ8wdbUciunQc3nPXoPc41N+hyi2GZaXMuJ4VlsNmgY+wPmp4y3pl4l0bgCb +1FR8Vvtsi8jLH8J15oAMWdmHQKcoJDE49llx+bQGpNekp6mlfX1DIPI= +-----END CERTIFICATE----- diff --git a/config/configgrpc/testdata/server.key b/config/configgrpc/testdata/server.key new file mode 100644 index 000000000000..164775c4a8cc --- /dev/null +++ b/config/configgrpc/testdata/server.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEAv1Pm2elKl/IpJlX5NqQjRTlA1rHws8F7v1IuaXB2qfk1MsDC +t37OvlbR4ARrY6zdUIrEQ/wrhQsZ2M5/yaj0rfeCgd/SDUKMAqDvXQXBY2AaLubT +AIEMs4rFR5Zq/pcBNz1zu8kRvRgvVuOpTCPR1kRvKFWAp689lXZhUU/BrQQXhdA9 +93xVMRM9u4fZuJLxNGGR/EhqTec4Z65jAZiUfO7ID94PtaxTrzR/Kjr2CiceR5hw +dY40BcmeD3IAd0J6nN1zIihe+Nqg/ImOG7YS+efQIEWJ8eHOoCK5knFBXRy6Wwxe +CyAPXyIbDTrqTy67eTDYc0XZ24F/5Q3GSvfMDwIDAQABAoIBAQC/BuxlAhKiJvyC +9DABKFy2zvU35y3mq/X8Dfec+tbf2pwM8nz3bLrLPDAMNR1rxbqqogJXxr1E9tJ1 +r6fTFshFsewx8+DrsFfOgBS9kfOGXvuFfJ2L0U13LcTPNxXY37gtCUQ2aAk3/Z+2 +Z1QvW0w1XNqHMOdlhQg95JZB8xnyvXs1niLT/I9d7KbPBmOWkB5Jp7+JaebmWqNS +alxnNqYnhXcrNSAbuR4bgz0l4I+Jprms26C6sakmgCfeMjfWbd2k3tp06vKXmT6q +qKa0855axP9wuSbKbscTDW5RFYTYnu/CSYJ4nZtzSS8a559iG3m61EgPOoVTnTX6 +0t0I+kwRAoGBAN403NO4FfHG8k2bFpbATQkmC9UwjMbl5RIEL0fFhNVsuM5jTwHc +0wlFm9tMN8xqg66OFCimC/mUNPWX8nrb/MwrAw6/50rbyqOBFnmFKIfVf4ftpLzt +BLhEg7a/FPgdDgldQD+C6XbMyBA1AF3nbpTnbnj5WVQTl672s9teegSzAoGBANxs +1y6Nfh2DyyU16p61376AAP3WfHvuBgJAC0xGCqoTrbyzl4/r06BTMl51PbWJLDjm +FryTtgM7a8XO1jwfWJno71dnT7Bsy+wYnmJ5+9XHwgO9oZfSFUk+ELrEImI/4NZX +dJLkc0SuCG/wa3Wa76+sFNlzAzBBs83RE2j432E1AoGAF+x5GhJnynAxBkn8VJ6/ +rIx8GafwgDmgQCBTNtb9Rj0+aHoot3qe/hCQhzvdhhSxuMlzQi0efPCIAyko4jFt +Nk4rNhtTO6wOVSxAzzSW+Ij0Ah6D7hNWvsAhrjtEdrIqILf5gt0FZdUGdTg/odyY ++08vhbbS90pkumG1W5kAaiECgYEAqjk3eBD26u4jjIn1tTk5H9GUcnMYUVCAvW4e +C3ovtCZcTlTW3+M73B1D0aRy0mWrjAlMV7cuoZJa6TiRQ37lmn5Dj1kONm3ekWZ1 +shEIBZEtaFwila88lwJiQwlCkGNKS9zf/qyDw+8uPtwI8JqFLUIUG9VxCexDYddr +SO6g+10CgYEAwUs2BRJb6Od+8XtH32+8DDOnpfWJARY0CwogN2k+D1dbAB8Wkda1 +BMADasAcjDFRX6xvyyDlqxcDIoCI1JvpS82I/PTNHeT8pEr5Caln7OHD/BtnPwmI +YR0bvKkoN0jdQdjifpMVXEbJS1VfFLdQYQ8iQMwZfkFmzIkYpvqWVtw= +-----END RSA PRIVATE KEY----- diff --git a/config/configgrpc/wrappedstream.go b/config/configgrpc/wrappedstream.go new file mode 100644 index 000000000000..b802b78e1ff4 --- /dev/null +++ b/config/configgrpc/wrappedstream.go @@ -0,0 +1,34 @@ +// Copyright The OpenTelemetry Authors +// Copyright 2016 Michal Witkowski. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package configgrpc // import "go.opentelemetry.io/collector/config/configgrpc" + +import ( + "context" + + "google.golang.org/grpc" +) + +// this functionality was originally copied from grpc-ecosystem/go-grpc-middleware project + +// wrappedServerStream is a thin wrapper around grpc.ServerStream that allows modifying context. +type wrappedServerStream struct { + grpc.ServerStream + // wrappedContext is the wrapper's own Context. You can assign it. + wrappedCtx context.Context +} + +// Context returns the wrapper's wrappedContext, overwriting the nested grpc.ServerStream.Context() +func (w *wrappedServerStream) Context() context.Context { + return w.wrappedCtx +} + +// wrapServerStream returns a ServerStream with the new context. +func wrapServerStream(wrappedCtx context.Context, stream grpc.ServerStream) *wrappedServerStream { + if existing, ok := stream.(*wrappedServerStream); ok { + existing.wrappedCtx = wrappedCtx + return existing + } + return &wrappedServerStream{ServerStream: stream, wrappedCtx: wrappedCtx} +} diff --git a/config/configgrpc/wrappedstream_test.go b/config/configgrpc/wrappedstream_test.go new file mode 100644 index 000000000000..e08ba64426b9 --- /dev/null +++ b/config/configgrpc/wrappedstream_test.go @@ -0,0 +1,43 @@ +// Copyright The OpenTelemetry Authors +// Copyright 2016 Michal Witkowski. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package configgrpc // import "go.opentelemetry.io/collector/internal/middleware" + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "google.golang.org/grpc" +) + +type ctxKey struct{} + +var oneCtxKey = ctxKey{} +var otherCtxKey = ctxKey{} + +func TestWrapServerStream(t *testing.T) { + ctx := context.WithValue(context.TODO(), oneCtxKey, 1) + fake := &fakeServerStream{ctx: ctx} + assert.NotNil(t, fake.Context().Value(oneCtxKey), "values from fake must propagate to wrapper") + wrapped := wrapServerStream(context.WithValue(fake.Context(), otherCtxKey, 2), fake) + assert.NotNil(t, wrapped.Context().Value(oneCtxKey), "values from wrapper must be set") + assert.NotNil(t, wrapped.Context().Value(otherCtxKey), "values from wrapper must be set") +} + +func TestDoubleWrapping(t *testing.T) { + fake := &fakeServerStream{ctx: context.Background()} + wrapped := wrapServerStream(fake.Context(), fake) + assert.Same(t, wrapped, wrapServerStream(wrapped.Context(), wrapped)) // should be noop + assert.Equal(t, fake, wrapped.ServerStream) +} + +type fakeServerStream struct { + grpc.ServerStream + ctx context.Context +} + +func (f *fakeServerStream) Context() context.Context { + return f.ctx +} diff --git a/config/confighttp/Makefile b/config/confighttp/Makefile new file mode 100644 index 000000000000..ded7a36092dc --- /dev/null +++ b/config/confighttp/Makefile @@ -0,0 +1 @@ +include ../../Makefile.Common diff --git a/config/confighttp/README.md b/config/confighttp/README.md new file mode 100644 index 000000000000..a0227c2402b3 --- /dev/null +++ b/config/confighttp/README.md @@ -0,0 +1,113 @@ +# HTTP Configuration Settings + +HTTP exposes a [variety of settings](https://golang.org/pkg/net/http/). +Several of these settings are available for configuration within individual +receivers or exporters. + +## Client Configuration + +[Exporters](https://github.com/open-telemetry/opentelemetry-collector/blob/main/exporter/README.md) +leverage client configuration. + +Note that client configuration supports TLS configuration, the +configuration parameters are also defined under `tls` like server +configuration. For more information, see [configtls +README](../configtls/README.md). + +- `endpoint`: address:port +- [`tls`](../configtls/README.md) +- [`headers`](https://pkg.go.dev/net/http#Request): name/value pairs added to the HTTP request headers + - certain headers such as Content-Length and Connection are automatically written when needed and values in Header may be ignored. + - `Host` header is automatically derived from `endpoint` value. However, this automatic assignment can be overridden by explicitly setting the Host field in the headers field. + - if `Host` header is provided then it overrides `Host` field in [Request](https://pkg.go.dev/net/http#Request) which results as an override of `Host` header value. +- [`read_buffer_size`](https://golang.org/pkg/net/http/#Transport) +- [`timeout`](https://golang.org/pkg/net/http/#Client) +- [`write_buffer_size`](https://golang.org/pkg/net/http/#Transport) +- `compression`: Compression type to use among `gzip`, `zstd`, `snappy`, `zlib`, and `deflate`. + - look at the documentation for the server-side of the communication. + - `none` will be treated as uncompressed, and any other inputs will cause an error. +- [`max_idle_conns`](https://golang.org/pkg/net/http/#Transport) +- [`max_idle_conns_per_host`](https://golang.org/pkg/net/http/#Transport) +- [`max_conns_per_host`](https://golang.org/pkg/net/http/#Transport) +- [`idle_conn_timeout`](https://golang.org/pkg/net/http/#Transport) +- [`auth`](../configauth/README.md) +- [`disable_keep_alives`](https://golang.org/pkg/net/http/#Transport) +- [`http2_read_idle_timeout`](https://pkg.go.dev/golang.org/x/net/http2#Transport) +- [`http2_ping_timeout`](https://pkg.go.dev/golang.org/x/net/http2#Transport) + +Example: + +```yaml +exporter: + otlphttp: + endpoint: otelcol2:55690 + auth: + authenticator: some-authenticator-extension + tls: + ca_file: ca.pem + cert_file: cert.pem + key_file: key.pem + headers: + test1: "value1" + "test 2": "value 2" + compression: zstd +``` + +## Server Configuration + +[Receivers](https://github.com/open-telemetry/opentelemetry-collector/blob/main/receiver/README.md) +leverage server configuration. + +- [`cors`](https://github.com/rs/cors#parameters): Configure [CORS][cors], +allowing the receiver to accept traces from web browsers, even if the receiver +is hosted at a different [origin][origin]. If left blank or set to `null`, CORS +will not be enabled. + - `allowed_origins`: A list of [origins][origin] allowed to send requests to + the receiver. An origin may contain a wildcard (`*`) to replace 0 or more + characters (e.g., `https://*.example.com`). To allow any origin, set to + `["*"]`. If no origins are listed, CORS will not be enabled. + - `allowed_headers`: Allow CORS requests to include headers outside the + [default safelist][cors-headers]. By default, safelist headers and + `X-Requested-With` will be allowed. To allow any request header, set to + `["*"]`. + - `max_age`: Sets the value of the [`Access-Control-Max-Age`][cors-cache] + header, allowing clients to cache the response to CORS preflight requests. If + not set, browsers use a default of 5 seconds. +- `endpoint`: Valid value syntax available [here](https://github.com/grpc/grpc/blob/master/doc/naming.md) +- `max_request_body_size`: configures the maximum allowed body size in bytes for a single request. Default: `0` (no restriction) +- [`tls`](../configtls/README.md) +- [`auth`](../configauth/README.md) + +You can enable [`attribute processor`][attribute-processor] to append any http header to span's attribute using custom key. You also need to enable the "include_metadata" + +Example: + +```yaml +receivers: + otlp: + protocols: + http: + include_metadata: true + auth: + authenticator: some-authenticator-extension + cors: + allowed_origins: + - https://foo.bar.com + - https://*.test.com + allowed_headers: + - Example-Header + max_age: 7200 + endpoint: 0.0.0.0:55690 +processors: + attributes: + actions: + - key: http.client_ip + from_context: X-Forwarded-For + action: upsert +``` + +[cors]: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS +[cors-headers]: https://developer.mozilla.org/en-US/docs/Glossary/CORS-safelisted_request_header +[cors-cache]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age +[origin]: https://developer.mozilla.org/en-US/docs/Glossary/Origin +[attribute-processor]: https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/processor/attributesprocessor/README.md diff --git a/config/confighttp/clientinfohandler.go b/config/confighttp/clientinfohandler.go new file mode 100644 index 000000000000..464153bc2c72 --- /dev/null +++ b/config/confighttp/clientinfohandler.go @@ -0,0 +1,66 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package confighttp // import "go.opentelemetry.io/collector/config/confighttp" + +import ( + "context" + "net" + "net/http" + + "go.opentelemetry.io/collector/client" +) + +// clientInfoHandler is an http.Handler that enhances the incoming request context with client.Info. +type clientInfoHandler struct { + next http.Handler + + // include client metadata or not + includeMetadata bool +} + +// ServeHTTP intercepts incoming HTTP requests, replacing the request's context with one that contains +// a client.Info containing the client's IP address. +func (h *clientInfoHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { + req = req.WithContext(contextWithClient(req, h.includeMetadata)) + h.next.ServeHTTP(w, req) +} + +// contextWithClient attempts to add the client IP address to the client.Info from the context. When no +// client.Info exists in the context, one is created. +func contextWithClient(req *http.Request, includeMetadata bool) context.Context { + cl := client.FromContext(req.Context()) + + ip := parseIP(req.RemoteAddr) + if ip != nil { + cl.Addr = ip + } + + if includeMetadata { + md := req.Header.Clone() + if len(md.Get(client.MetadataHostName)) == 0 && req.Host != "" { + md.Add(client.MetadataHostName, req.Host) + } + + cl.Metadata = client.NewMetadata(md) + } + + ctx := client.NewContext(req.Context(), cl) + return ctx +} + +// parseIP parses the given string for an IP address. The input string might contain the port, +// but must not contain a protocol or path. Suitable for getting the IP part of a client connection. +func parseIP(source string) *net.IPAddr { + ipstr, _, err := net.SplitHostPort(source) + if err == nil { + source = ipstr + } + ip := net.ParseIP(source) + if ip != nil { + return &net.IPAddr{ + IP: ip, + } + } + return nil +} diff --git a/config/confighttp/clientinfohandler_test.go b/config/confighttp/clientinfohandler_test.go new file mode 100644 index 000000000000..bbd18ec52fd4 --- /dev/null +++ b/config/confighttp/clientinfohandler_test.go @@ -0,0 +1,52 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package confighttp // import "go.opentelemetry.io/collector/config/confighttp" + +import ( + "net" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" +) + +var _ http.Handler = (*clientInfoHandler)(nil) + +func TestParseIP(t *testing.T) { + testCases := []struct { + desc string + input string + expected *net.IPAddr + }{ + { + desc: "addr", + input: "1.2.3.4", + expected: &net.IPAddr{ + IP: net.IPv4(1, 2, 3, 4), + }, + }, + { + desc: "addr:port", + input: "1.2.3.4:33455", + expected: &net.IPAddr{ + IP: net.IPv4(1, 2, 3, 4), + }, + }, + { + desc: "protocol://addr:port", + input: "http://1.2.3.4:33455", + expected: nil, + }, + { + desc: "addr/path", + input: "1.2.3.4/orders", + expected: nil, + }, + } + for _, tC := range testCases { + t.Run(tC.desc, func(t *testing.T) { + assert.Equal(t, tC.expected, parseIP(tC.input)) + }) + } +} diff --git a/config/confighttp/compression.go b/config/confighttp/compression.go new file mode 100644 index 000000000000..88ecafe78da5 --- /dev/null +++ b/config/confighttp/compression.go @@ -0,0 +1,175 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +// This file contains helper functions regarding compression/decompression for confighttp. + +package confighttp // import "go.opentelemetry.io/collector/config/confighttp" + +import ( + "bytes" + "compress/gzip" + "compress/zlib" + "fmt" + "io" + "net/http" + + "github.com/golang/snappy" + "github.com/klauspost/compress/zstd" + + "go.opentelemetry.io/collector/config/configcompression" +) + +type compressRoundTripper struct { + rt http.RoundTripper + compressionType configcompression.Type + compressor *compressor +} + +func newCompressRoundTripper(rt http.RoundTripper, compressionType configcompression.Type) (*compressRoundTripper, error) { + encoder, err := newCompressor(compressionType) + if err != nil { + return nil, err + } + return &compressRoundTripper{ + rt: rt, + compressionType: compressionType, + compressor: encoder, + }, nil +} + +func (r *compressRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + if req.Header.Get(headerContentEncoding) != "" { + // If the header already specifies a content encoding then skip compression + // since we don't want to compress it again. This is a safeguard that normally + // should not happen since CompressRoundTripper is not intended to be used + // with http clients which already do their own compression. + return r.rt.RoundTrip(req) + } + + // Compress the body. + buf := bytes.NewBuffer([]byte{}) + if err := r.compressor.compress(buf, req.Body); err != nil { + return nil, err + } + + // Create a new request since the docs say that we cannot modify the "req" + // (see https://golang.org/pkg/net/http/#RoundTripper). + cReq, err := http.NewRequestWithContext(req.Context(), req.Method, req.URL.String(), buf) + if err != nil { + return nil, err + } + + // Clone the headers and add the encoding header. + cReq.Header = req.Header.Clone() + cReq.Header.Add(headerContentEncoding, string(r.compressionType)) + + return r.rt.RoundTrip(cReq) +} + +type decompressor struct { + errHandler func(w http.ResponseWriter, r *http.Request, errorMsg string, statusCode int) + base http.Handler + decoders map[string]func(body io.ReadCloser) (io.ReadCloser, error) +} + +// httpContentDecompressor offloads the task of handling compressed HTTP requests +// by identifying the compression format in the "Content-Encoding" header and re-writing +// request body so that the handlers further in the chain can work on decompressed data. +// It supports gzip and deflate/zlib compression. +func httpContentDecompressor(h http.Handler, eh func(w http.ResponseWriter, r *http.Request, errorMsg string, statusCode int), decoders map[string]func(body io.ReadCloser) (io.ReadCloser, error)) http.Handler { + errHandler := defaultErrorHandler + if eh != nil { + errHandler = eh + } + + d := &decompressor{ + errHandler: errHandler, + base: h, + decoders: map[string]func(body io.ReadCloser) (io.ReadCloser, error){ + "": func(io.ReadCloser) (io.ReadCloser, error) { + // Not a compressed payload. Nothing to do. + return nil, nil + }, + "gzip": func(body io.ReadCloser) (io.ReadCloser, error) { + gr, err := gzip.NewReader(body) + if err != nil { + return nil, err + } + return gr, nil + }, + "zstd": func(body io.ReadCloser) (io.ReadCloser, error) { + zr, err := zstd.NewReader( + body, + // Concurrency 1 disables async decoding. We don't need async decoding, it is pointless + // for our use-case (a server accepting decoding http requests). + // Disabling async improves performance (I benchmarked it previously when working + // on https://github.com/open-telemetry/opentelemetry-collector-contrib/pull/23257). + zstd.WithDecoderConcurrency(1), + ) + if err != nil { + return nil, err + } + return zr.IOReadCloser(), nil + }, + "zlib": func(body io.ReadCloser) (io.ReadCloser, error) { + zr, err := zlib.NewReader(body) + if err != nil { + return nil, err + } + return zr, nil + }, + "snappy": func(body io.ReadCloser) (io.ReadCloser, error) { + sr := snappy.NewReader(body) + sb := new(bytes.Buffer) + _, err := io.Copy(sb, sr) + if err != nil { + return nil, err + } + if err = body.Close(); err != nil { + return nil, err + } + return io.NopCloser(sb), nil + }, + }, + } + d.decoders["deflate"] = d.decoders["zlib"] + + for key, dec := range decoders { + d.decoders[key] = dec + } + + return d +} + +func (d *decompressor) ServeHTTP(w http.ResponseWriter, r *http.Request) { + newBody, err := d.newBodyReader(r) + if err != nil { + d.errHandler(w, r, err.Error(), http.StatusBadRequest) + return + } + if newBody != nil { + defer newBody.Close() + // "Content-Encoding" header is removed to avoid decompressing twice + // in case the next handler(s) have implemented a similar mechanism. + r.Header.Del("Content-Encoding") + // "Content-Length" is set to -1 as the size of the decompressed body is unknown. + r.Header.Del("Content-Length") + r.ContentLength = -1 + r.Body = newBody + } + d.base.ServeHTTP(w, r) +} + +func (d *decompressor) newBodyReader(r *http.Request) (io.ReadCloser, error) { + encoding := r.Header.Get(headerContentEncoding) + decoder, ok := d.decoders[encoding] + if !ok { + return nil, fmt.Errorf("unsupported %s: %s", headerContentEncoding, encoding) + } + return decoder(r.Body) +} + +// defaultErrorHandler writes the error message in plain text. +func defaultErrorHandler(w http.ResponseWriter, _ *http.Request, errMsg string, statusCode int) { + http.Error(w, errMsg, statusCode) +} diff --git a/config/confighttp/compression_test.go b/config/confighttp/compression_test.go new file mode 100644 index 000000000000..ea56d07ff66f --- /dev/null +++ b/config/confighttp/compression_test.go @@ -0,0 +1,386 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package confighttp + +import ( + "bytes" + "compress/gzip" + "compress/zlib" + "context" + "fmt" + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/golang/snappy" + "github.com/klauspost/compress/zstd" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "go.opentelemetry.io/collector/component/componenttest" + "go.opentelemetry.io/collector/config/configcompression" +) + +func TestHTTPClientCompression(t *testing.T) { + testBody := []byte("uncompressed_text") + compressedGzipBody := compressGzip(t, testBody) + compressedZlibBody := compressZlib(t, testBody) + compressedDeflateBody := compressZlib(t, testBody) + compressedSnappyBody := compressSnappy(t, testBody) + compressedZstdBody := compressZstd(t, testBody) + + tests := []struct { + name string + encoding configcompression.Type + reqBody []byte + shouldError bool + }{ + { + name: "ValidEmpty", + encoding: "", + reqBody: testBody, + shouldError: false, + }, + { + name: "ValidNone", + encoding: "none", + reqBody: testBody, + shouldError: false, + }, + { + name: "ValidGzip", + encoding: configcompression.TypeGzip, + reqBody: compressedGzipBody.Bytes(), + shouldError: false, + }, + { + name: "ValidZlib", + encoding: configcompression.TypeZlib, + reqBody: compressedZlibBody.Bytes(), + shouldError: false, + }, + { + name: "ValidDeflate", + encoding: configcompression.TypeDeflate, + reqBody: compressedDeflateBody.Bytes(), + shouldError: false, + }, + { + name: "ValidSnappy", + encoding: configcompression.TypeSnappy, + reqBody: compressedSnappyBody.Bytes(), + shouldError: false, + }, + { + name: "ValidZstd", + encoding: configcompression.TypeZstd, + reqBody: compressedZstdBody.Bytes(), + shouldError: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body, err := io.ReadAll(r.Body) + require.NoError(t, err, "failed to read request body: %v", err) + assert.EqualValues(t, tt.reqBody, body) + w.WriteHeader(http.StatusOK) + })) + t.Cleanup(srv.Close) + + reqBody := bytes.NewBuffer(testBody) + + req, err := http.NewRequest(http.MethodGet, srv.URL, reqBody) + require.NoError(t, err, "failed to create request to test handler") + + clientSettings := ClientConfig{ + Endpoint: srv.URL, + Compression: tt.encoding, + } + client, err := clientSettings.ToClientContext(context.Background(), componenttest.NewNopHost(), componenttest.NewNopTelemetrySettings()) + require.NoError(t, err) + res, err := client.Do(req) + if tt.shouldError { + assert.Error(t, err) + return + } + require.NoError(t, err) + + _, err = io.ReadAll(res.Body) + require.NoError(t, err) + require.NoError(t, res.Body.Close(), "failed to close request body: %v", err) + }) + } +} + +func TestHTTPCustomDecompression(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body, err := io.ReadAll(r.Body) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + _, _ = w.Write([]byte(err.Error())) + return + } + + require.NoError(t, err, "failed to read request body: %v", err) + assert.EqualValues(t, "decompressed body", string(body)) + w.WriteHeader(http.StatusOK) + }) + decoders := map[string]func(io.ReadCloser) (io.ReadCloser, error){ + "custom-encoding": func(io.ReadCloser) (io.ReadCloser, error) { // nolint: unparam + return io.NopCloser(strings.NewReader("decompressed body")), nil + }, + } + srv := httptest.NewServer(httpContentDecompressor(handler, defaultErrorHandler, decoders)) + + t.Cleanup(srv.Close) + + req, err := http.NewRequest(http.MethodGet, srv.URL, bytes.NewBuffer([]byte("123decompressed body"))) + require.NoError(t, err, "failed to create request to test handler") + req.Header.Set("Content-Encoding", "custom-encoding") + + client := http.Client{} + res, err := client.Do(req) + require.NoError(t, err) + + assert.Equal(t, http.StatusOK, res.StatusCode, "test handler returned unexpected status code ") + _, err = io.ReadAll(res.Body) + require.NoError(t, res.Body.Close(), "failed to close request body: %v", err) +} + +func TestHTTPContentDecompressionHandler(t *testing.T) { + testBody := []byte("uncompressed_text") + noDecoders := map[string]func(io.ReadCloser) (io.ReadCloser, error){} + tests := []struct { + name string + encoding string + reqBody *bytes.Buffer + respCode int + respBody string + }{ + { + name: "NoCompression", + encoding: "", + reqBody: bytes.NewBuffer(testBody), + respCode: http.StatusOK, + }, + { + name: "ValidDeflate", + encoding: "deflate", + reqBody: compressZlib(t, testBody), + respCode: http.StatusOK, + }, + { + name: "ValidGzip", + encoding: "gzip", + reqBody: compressGzip(t, testBody), + respCode: http.StatusOK, + }, + { + name: "ValidZlib", + encoding: "zlib", + reqBody: compressZlib(t, testBody), + respCode: http.StatusOK, + }, + { + name: "ValidZstd", + encoding: "zstd", + reqBody: compressZstd(t, testBody), + respCode: http.StatusOK, + }, + { + name: "ValidSnappy", + encoding: "snappy", + reqBody: compressSnappy(t, testBody), + respCode: http.StatusOK, + }, + { + name: "InvalidDeflate", + encoding: "deflate", + reqBody: bytes.NewBuffer(testBody), + respCode: http.StatusBadRequest, + respBody: "zlib: invalid header\n", + }, + { + name: "InvalidGzip", + encoding: "gzip", + reqBody: bytes.NewBuffer(testBody), + respCode: http.StatusBadRequest, + respBody: "gzip: invalid header\n", + }, + { + name: "InvalidZlib", + encoding: "zlib", + reqBody: bytes.NewBuffer(testBody), + respCode: http.StatusBadRequest, + respBody: "zlib: invalid header\n", + }, + { + name: "InvalidZstd", + encoding: "zstd", + reqBody: bytes.NewBuffer(testBody), + respCode: http.StatusBadRequest, + respBody: "invalid input: magic number mismatch", + }, + { + name: "InvalidSnappy", + encoding: "snappy", + reqBody: bytes.NewBuffer(testBody), + respCode: http.StatusBadRequest, + respBody: "snappy: corrupt input\n", + }, + { + name: "UnsupportedCompression", + encoding: "nosuchcompression", + reqBody: bytes.NewBuffer(testBody), + respCode: http.StatusBadRequest, + respBody: "unsupported Content-Encoding: nosuchcompression\n", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + srv := httptest.NewServer(httpContentDecompressor(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body, err := io.ReadAll(r.Body) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + _, _ = w.Write([]byte(err.Error())) + return + } + + require.NoError(t, err, "failed to read request body: %v", err) + assert.EqualValues(t, testBody, string(body)) + w.WriteHeader(http.StatusOK) + }), defaultErrorHandler, noDecoders)) + t.Cleanup(srv.Close) + + req, err := http.NewRequest(http.MethodGet, srv.URL, tt.reqBody) + require.NoError(t, err, "failed to create request to test handler") + req.Header.Set("Content-Encoding", tt.encoding) + + client := http.Client{} + res, err := client.Do(req) + require.NoError(t, err) + + assert.Equal(t, tt.respCode, res.StatusCode, "test handler returned unexpected status code ") + if tt.respBody != "" { + body, err := io.ReadAll(res.Body) + require.NoError(t, res.Body.Close(), "failed to close request body: %v", err) + assert.Equal(t, tt.respBody, string(body)) + } + }) + } +} + +func TestHTTPContentCompressionRequestWithNilBody(t *testing.T) { + compressedGzipBody := compressGzip(t, []byte{}) + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + body, err := io.ReadAll(r.Body) + require.NoError(t, err, "failed to read request body: %v", err) + assert.EqualValues(t, compressedGzipBody.Bytes(), body) + })) + defer server.Close() + + req, err := http.NewRequest(http.MethodGet, server.URL, nil) + require.NoError(t, err, "failed to create request to test handler") + + client := http.Client{} + client.Transport, err = newCompressRoundTripper(http.DefaultTransport, configcompression.TypeGzip) + require.NoError(t, err) + res, err := client.Do(req) + require.NoError(t, err) + + _, err = io.ReadAll(res.Body) + require.NoError(t, err) + require.NoError(t, res.Body.Close(), "failed to close request body: %v", err) +} + +type copyFailBody struct { +} + +func (*copyFailBody) Read([]byte) (n int, err error) { + return 0, fmt.Errorf("read failed") +} + +func (*copyFailBody) Close() error { + return nil +} + +func TestHTTPContentCompressionCopyError(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusOK) + })) + t.Cleanup(server.Close) + + req, err := http.NewRequest(http.MethodGet, server.URL, ©FailBody{}) + require.NoError(t, err) + + client := http.Client{} + client.Transport, err = newCompressRoundTripper(http.DefaultTransport, configcompression.TypeGzip) + require.NoError(t, err) + _, err = client.Do(req) + require.Error(t, err) +} + +type closeFailBody struct { + *bytes.Buffer +} + +func (*closeFailBody) Close() error { + return fmt.Errorf("close failed") +} + +func TestHTTPContentCompressionRequestBodyCloseError(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusOK) + })) + t.Cleanup(server.Close) + + req, err := http.NewRequest(http.MethodGet, server.URL, &closeFailBody{Buffer: bytes.NewBuffer([]byte("blank"))}) + require.NoError(t, err) + + client := http.Client{} + client.Transport, err = newCompressRoundTripper(http.DefaultTransport, configcompression.TypeGzip) + require.NoError(t, err) + _, err = client.Do(req) + require.Error(t, err) +} + +func compressGzip(t testing.TB, body []byte) *bytes.Buffer { + var buf bytes.Buffer + gw := gzip.NewWriter(&buf) + _, err := gw.Write(body) + require.NoError(t, err) + require.NoError(t, gw.Close()) + return &buf +} + +func compressZlib(t testing.TB, body []byte) *bytes.Buffer { + var buf bytes.Buffer + zw := zlib.NewWriter(&buf) + _, err := zw.Write(body) + require.NoError(t, err) + require.NoError(t, zw.Close()) + return &buf +} + +func compressSnappy(t testing.TB, body []byte) *bytes.Buffer { + var buf bytes.Buffer + sw := snappy.NewBufferedWriter(&buf) + _, err := sw.Write(body) + require.NoError(t, err) + require.NoError(t, sw.Close()) + return &buf +} + +func compressZstd(t testing.TB, body []byte) *bytes.Buffer { + var buf bytes.Buffer + zw, _ := zstd.NewWriter(&buf) + _, err := zw.Write(body) + require.NoError(t, err) + require.NoError(t, zw.Close()) + return &buf +} diff --git a/config/confighttp/compressor.go b/config/confighttp/compressor.go new file mode 100644 index 000000000000..3e085bead0da --- /dev/null +++ b/config/confighttp/compressor.go @@ -0,0 +1,75 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package confighttp // import "go.opentelemetry.io/collector/config/confighttp" + +import ( + "bytes" + "compress/gzip" + "compress/zlib" + "errors" + "io" + "sync" + + "github.com/golang/snappy" + "github.com/klauspost/compress/zstd" + + "go.opentelemetry.io/collector/config/configcompression" +) + +type writeCloserReset interface { + io.WriteCloser + Reset(w io.Writer) +} + +var ( + _ writeCloserReset = (*gzip.Writer)(nil) + gZipPool = &compressor{pool: sync.Pool{New: func() any { return gzip.NewWriter(nil) }}} + _ writeCloserReset = (*snappy.Writer)(nil) + snappyPool = &compressor{pool: sync.Pool{New: func() any { return snappy.NewBufferedWriter(nil) }}} + _ writeCloserReset = (*zstd.Encoder)(nil) + zStdPool = &compressor{pool: sync.Pool{New: func() any { zw, _ := zstd.NewWriter(nil); return zw }}} + _ writeCloserReset = (*zlib.Writer)(nil) + zLibPool = &compressor{pool: sync.Pool{New: func() any { return zlib.NewWriter(nil) }}} +) + +type compressor struct { + pool sync.Pool +} + +// writerFactory defines writer field in CompressRoundTripper. +// The validity of input is already checked when NewCompressRoundTripper was called in confighttp, +func newCompressor(compressionType configcompression.Type) (*compressor, error) { + switch compressionType { + case configcompression.TypeGzip: + return gZipPool, nil + case configcompression.TypeSnappy: + return snappyPool, nil + case configcompression.TypeZstd: + return zStdPool, nil + case configcompression.TypeZlib, configcompression.TypeDeflate: + return zLibPool, nil + } + return nil, errors.New("unsupported compression type, ") +} + +func (p *compressor) compress(buf *bytes.Buffer, body io.ReadCloser) error { + writer := p.pool.Get().(writeCloserReset) + defer p.pool.Put(writer) + writer.Reset(buf) + + if body != nil { + _, copyErr := io.Copy(writer, body) + closeErr := body.Close() + + if copyErr != nil { + return copyErr + } + + if closeErr != nil { + return closeErr + } + } + + return writer.Close() +} diff --git a/config/confighttp/confighttp.go b/config/confighttp/confighttp.go new file mode 100644 index 000000000000..985c4f9be7a8 --- /dev/null +++ b/config/confighttp/confighttp.go @@ -0,0 +1,461 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package confighttp // import "go.opentelemetry.io/collector/config/confighttp" + +import ( + "context" + "crypto/tls" + "errors" + "fmt" + "io" + "net" + "net/http" + "net/url" + "time" + + "github.com/rs/cors" + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" + "go.opentelemetry.io/otel" + "golang.org/x/net/http2" + + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/config/configauth" + "go.opentelemetry.io/collector/config/configcompression" + "go.opentelemetry.io/collector/config/configopaque" + "go.opentelemetry.io/collector/config/configtls" + "go.opentelemetry.io/collector/config/internal" + "go.opentelemetry.io/collector/extension/auth" +) + +const headerContentEncoding = "Content-Encoding" + +// ClientConfig defines settings for creating an HTTP client. +type ClientConfig struct { + // The target URL to send data to (e.g.: http://some.url:9411/v1/traces). + Endpoint string `mapstructure:"endpoint"` + + // ProxyURL setting for the collector + ProxyURL string `mapstructure:"proxy_url"` + + // TLSSetting struct exposes TLS client configuration. + TLSSetting configtls.ClientConfig `mapstructure:"tls"` + + // ReadBufferSize for HTTP client. See http.Transport.ReadBufferSize. + ReadBufferSize int `mapstructure:"read_buffer_size"` + + // WriteBufferSize for HTTP client. See http.Transport.WriteBufferSize. + WriteBufferSize int `mapstructure:"write_buffer_size"` + + // Timeout parameter configures `http.Client.Timeout`. + Timeout time.Duration `mapstructure:"timeout"` + + // Additional headers attached to each HTTP request sent by the client. + // Existing header values are overwritten if collision happens. + // Header values are opaque since they may be sensitive. + Headers map[string]configopaque.String `mapstructure:"headers"` + + // Custom Round Tripper to allow for individual components to intercept HTTP requests + CustomRoundTripper func(next http.RoundTripper) (http.RoundTripper, error) + + // Auth configuration for outgoing HTTP calls. + Auth *configauth.Authentication `mapstructure:"auth"` + + // The compression key for supported compression types within collector. + Compression configcompression.Type `mapstructure:"compression"` + + // MaxIdleConns is used to set a limit to the maximum idle HTTP connections the client can keep open. + // There's an already set value, and we want to override it only if an explicit value provided + MaxIdleConns *int `mapstructure:"max_idle_conns"` + + // MaxIdleConnsPerHost is used to set a limit to the maximum idle HTTP connections the host can keep open. + // There's an already set value, and we want to override it only if an explicit value provided + MaxIdleConnsPerHost *int `mapstructure:"max_idle_conns_per_host"` + + // MaxConnsPerHost limits the total number of connections per host, including connections in the dialing, + // active, and idle states. + // There's an already set value, and we want to override it only if an explicit value provided + MaxConnsPerHost *int `mapstructure:"max_conns_per_host"` + + // IdleConnTimeout is the maximum amount of time a connection will remain open before closing itself. + // There's an already set value, and we want to override it only if an explicit value provided + IdleConnTimeout *time.Duration `mapstructure:"idle_conn_timeout"` + + // DisableKeepAlives, if true, disables HTTP keep-alives and will only use the connection to the server + // for a single HTTP request. + // + // WARNING: enabling this option can result in significant overhead establishing a new HTTP(S) + // connection for every request. Before enabling this option please consider whether changes + // to idle connection settings can achieve your goal. + DisableKeepAlives bool `mapstructure:"disable_keep_alives"` + + // This is needed in case you run into + // https://github.com/golang/go/issues/59690 + // https://github.com/golang/go/issues/36026 + // HTTP2ReadIdleTimeout if the connection has been idle for the configured value send a ping frame for health check + // 0s means no health check will be performed. + HTTP2ReadIdleTimeout time.Duration `mapstructure:"http2_read_idle_timeout"` + // HTTP2PingTimeout if there's no response to the ping within the configured value, the connection will be closed. + // If not set or set to 0, it defaults to 15s. + HTTP2PingTimeout time.Duration `mapstructure:"http2_ping_timeout"` +} + +// NewDefaultClientConfig returns ClientConfig type object with +// the default values of 'MaxIdleConns' and 'IdleConnTimeout'. +// Other config options are not added as they are initialized with 'zero value' by GoLang as default. +// We encourage to use this function to create an object of ClientConfig. +func NewDefaultClientConfig() ClientConfig { + // The default values are taken from the values of 'DefaultTransport' of 'http' package. + maxIdleConns := 100 + idleConnTimeout := 90 * time.Second + + return ClientConfig{ + MaxIdleConns: &maxIdleConns, + IdleConnTimeout: &idleConnTimeout, + } +} + +// Deprecated: [v0.98.0] Use ToClientContext instead. +func (hcs *ClientConfig) ToClient(host component.Host, settings component.TelemetrySettings) (*http.Client, error) { + return hcs.ToClientContext(context.Background(), host, settings) +} + +// ToClientContext creates an HTTP client. +func (hcs *ClientConfig) ToClientContext(ctx context.Context, host component.Host, settings component.TelemetrySettings) (*http.Client, error) { + tlsCfg, err := hcs.TLSSetting.LoadTLSConfigContext(ctx) + if err != nil { + return nil, err + } + transport := http.DefaultTransport.(*http.Transport).Clone() + if tlsCfg != nil { + transport.TLSClientConfig = tlsCfg + } + if hcs.ReadBufferSize > 0 { + transport.ReadBufferSize = hcs.ReadBufferSize + } + if hcs.WriteBufferSize > 0 { + transport.WriteBufferSize = hcs.WriteBufferSize + } + + if hcs.MaxIdleConns != nil { + transport.MaxIdleConns = *hcs.MaxIdleConns + } + + if hcs.MaxIdleConnsPerHost != nil { + transport.MaxIdleConnsPerHost = *hcs.MaxIdleConnsPerHost + } + + if hcs.MaxConnsPerHost != nil { + transport.MaxConnsPerHost = *hcs.MaxConnsPerHost + } + + if hcs.IdleConnTimeout != nil { + transport.IdleConnTimeout = *hcs.IdleConnTimeout + } + + // Setting the Proxy URL + if hcs.ProxyURL != "" { + proxyURL, parseErr := url.ParseRequestURI(hcs.ProxyURL) + if parseErr != nil { + return nil, parseErr + } + transport.Proxy = http.ProxyURL(proxyURL) + } + + transport.DisableKeepAlives = hcs.DisableKeepAlives + + if hcs.HTTP2ReadIdleTimeout > 0 { + transport2, transportErr := http2.ConfigureTransports(transport) + if transportErr != nil { + return nil, fmt.Errorf("failed to configure http2 transport: %w", transportErr) + } + transport2.ReadIdleTimeout = hcs.HTTP2ReadIdleTimeout + transport2.PingTimeout = hcs.HTTP2PingTimeout + } + + clientTransport := (http.RoundTripper)(transport) + + // The Auth RoundTripper should always be the innermost to ensure that + // request signing-based auth mechanisms operate after compression + // and header middleware modifies the request + if hcs.Auth != nil { + ext := host.GetExtensions() + if ext == nil { + return nil, errors.New("extensions configuration not found") + } + + httpCustomAuthRoundTripper, aerr := hcs.Auth.GetClientAuthenticator(ext) + if aerr != nil { + return nil, aerr + } + + clientTransport, err = httpCustomAuthRoundTripper.RoundTripper(clientTransport) + if err != nil { + return nil, err + } + } + + if len(hcs.Headers) > 0 { + clientTransport = &headerRoundTripper{ + transport: clientTransport, + headers: hcs.Headers, + } + } + + // Compress the body using specified compression methods if non-empty string is provided. + // Supporting gzip, zlib, deflate, snappy, and zstd; none is treated as uncompressed. + if hcs.Compression.IsCompressed() { + clientTransport, err = newCompressRoundTripper(clientTransport, hcs.Compression) + if err != nil { + return nil, err + } + } + + // wrapping http transport with otelhttp transport to enable otel instrumentation + if settings.TracerProvider != nil && settings.MeterProvider != nil { + clientTransport = otelhttp.NewTransport( + clientTransport, + otelhttp.WithTracerProvider(settings.TracerProvider), + otelhttp.WithMeterProvider(settings.MeterProvider), + otelhttp.WithPropagators(otel.GetTextMapPropagator()), + ) + } + + if hcs.CustomRoundTripper != nil { + clientTransport, err = hcs.CustomRoundTripper(clientTransport) + if err != nil { + return nil, err + } + } + + return &http.Client{ + Transport: clientTransport, + Timeout: hcs.Timeout, + }, nil +} + +// Custom RoundTripper that adds headers. +type headerRoundTripper struct { + transport http.RoundTripper + headers map[string]configopaque.String +} + +// RoundTrip is a custom RoundTripper that adds headers to the request. +func (interceptor *headerRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + // Set Host header if provided + hostHeader, found := interceptor.headers["Host"] + if found && hostHeader != "" { + // `Host` field should be set to override default `Host` header value which is Endpoint + req.Host = string(hostHeader) + } + for k, v := range interceptor.headers { + req.Header.Set(k, string(v)) + } + + // Send the request to next transport. + return interceptor.transport.RoundTrip(req) +} + +// ServerConfig defines settings for creating an HTTP server. +type ServerConfig struct { + // Endpoint configures the listening address for the server. + Endpoint string `mapstructure:"endpoint"` + + // TLSSetting struct exposes TLS client configuration. + TLSSetting *configtls.ServerConfig `mapstructure:"tls"` + + // CORS configures the server for HTTP cross-origin resource sharing (CORS). + CORS *CORSConfig `mapstructure:"cors"` + + // Auth for this receiver + Auth *configauth.Authentication `mapstructure:"auth"` + + // MaxRequestBodySize sets the maximum request body size in bytes + MaxRequestBodySize int64 `mapstructure:"max_request_body_size"` + + // IncludeMetadata propagates the client metadata from the incoming requests to the downstream consumers + // Experimental: *NOTE* this option is subject to change or removal in the future. + IncludeMetadata bool `mapstructure:"include_metadata"` + + // Additional headers attached to each HTTP response sent to the client. + // Header values are opaque since they may be sensitive. + ResponseHeaders map[string]configopaque.String `mapstructure:"response_headers"` +} + +// Deprecated: [v0.98.0] Use ToListenerContext instead. +func (hss *ServerConfig) ToListener() (net.Listener, error) { + return hss.ToListenerContext(context.Background()) +} + +// ToListenerContext creates a net.Listener. +func (hss *ServerConfig) ToListenerContext(ctx context.Context) (net.Listener, error) { + listener, err := net.Listen("tcp", hss.Endpoint) + if err != nil { + return nil, err + } + + if hss.TLSSetting != nil { + var tlsCfg *tls.Config + tlsCfg, err = hss.TLSSetting.LoadTLSConfigContext(ctx) + if err != nil { + return nil, err + } + tlsCfg.NextProtos = []string{http2.NextProtoTLS, "http/1.1"} + listener = tls.NewListener(listener, tlsCfg) + } + + return listener, nil +} + +// toServerOptions has options that change the behavior of the HTTP server +// returned by ServerConfig.ToServer(). +type toServerOptions struct { + errHandler func(w http.ResponseWriter, r *http.Request, errorMsg string, statusCode int) + decoders map[string]func(body io.ReadCloser) (io.ReadCloser, error) +} + +// ToServerOption is an option to change the behavior of the HTTP server +// returned by ServerConfig.ToServer(). +type ToServerOption func(opts *toServerOptions) + +// WithErrorHandler overrides the HTTP error handler that gets invoked +// when there is a failure inside httpContentDecompressor. +func WithErrorHandler(e func(w http.ResponseWriter, r *http.Request, errorMsg string, statusCode int)) ToServerOption { + return func(opts *toServerOptions) { + opts.errHandler = e + } +} + +// WithDecoder provides support for additional decoders to be configured +// by the caller. +func WithDecoder(key string, dec func(body io.ReadCloser) (io.ReadCloser, error)) ToServerOption { + return func(opts *toServerOptions) { + if opts.decoders == nil { + opts.decoders = map[string]func(body io.ReadCloser) (io.ReadCloser, error){} + } + opts.decoders[key] = dec + } +} + +// Deprecated: [v0.98.0] Use ToServerContext instead. +func (hss *ServerConfig) ToServer(host component.Host, settings component.TelemetrySettings, handler http.Handler, opts ...ToServerOption) (*http.Server, error) { + return hss.ToServerContext(context.Background(), host, settings, handler, opts...) +} + +// ToServerContext creates an http.Server from settings object. +func (hss *ServerConfig) ToServerContext(_ context.Context, host component.Host, settings component.TelemetrySettings, handler http.Handler, opts ...ToServerOption) (*http.Server, error) { + internal.WarnOnUnspecifiedHost(settings.Logger, hss.Endpoint) + + serverOpts := &toServerOptions{} + for _, o := range opts { + o(serverOpts) + } + + handler = httpContentDecompressor(handler, serverOpts.errHandler, serverOpts.decoders) + + if hss.MaxRequestBodySize > 0 { + handler = maxRequestBodySizeInterceptor(handler, hss.MaxRequestBodySize) + } + + if hss.Auth != nil { + server, err := hss.Auth.GetServerAuthenticator(host.GetExtensions()) + if err != nil { + return nil, err + } + + handler = authInterceptor(handler, server) + } + + if hss.CORS != nil && len(hss.CORS.AllowedOrigins) > 0 { + co := cors.Options{ + AllowedOrigins: hss.CORS.AllowedOrigins, + AllowCredentials: true, + AllowedHeaders: hss.CORS.AllowedHeaders, + MaxAge: hss.CORS.MaxAge, + } + handler = cors.New(co).Handler(handler) + } + if hss.CORS != nil && len(hss.CORS.AllowedOrigins) == 0 && len(hss.CORS.AllowedHeaders) > 0 { + settings.Logger.Warn("The CORS configuration specifies allowed headers but no allowed origins, and is therefore ignored.") + } + + if hss.ResponseHeaders != nil { + handler = responseHeadersHandler(handler, hss.ResponseHeaders) + } + + // Enable OpenTelemetry observability plugin. + // TODO: Consider to use component ID string as prefix for all the operations. + handler = otelhttp.NewHandler( + handler, + "", + otelhttp.WithTracerProvider(settings.TracerProvider), + otelhttp.WithMeterProvider(settings.MeterProvider), + otelhttp.WithPropagators(otel.GetTextMapPropagator()), + otelhttp.WithSpanNameFormatter(func(_ string, r *http.Request) string { + return r.URL.Path + }), + ) + + // wrap the current handler in an interceptor that will add client.Info to the request's context + handler = &clientInfoHandler{ + next: handler, + includeMetadata: hss.IncludeMetadata, + } + + return &http.Server{ + Handler: handler, + }, nil +} + +func responseHeadersHandler(handler http.Handler, headers map[string]configopaque.String) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + h := w.Header() + + for k, v := range headers { + h.Set(k, string(v)) + } + + handler.ServeHTTP(w, r) + }) +} + +// CORSConfig configures a receiver for HTTP cross-origin resource sharing (CORS). +// See the underlying https://github.com/rs/cors package for details. +type CORSConfig struct { + // AllowedOrigins sets the allowed values of the Origin header for + // HTTP/JSON requests to an OTLP receiver. An origin may contain a + // wildcard (*) to replace 0 or more characters (e.g., + // "http://*.domain.com", or "*" to allow any origin). + AllowedOrigins []string `mapstructure:"allowed_origins"` + + // AllowedHeaders sets what headers will be allowed in CORS requests. + // The Accept, Accept-Language, Content-Type, and Content-Language + // headers are implicitly allowed. If no headers are listed, + // X-Requested-With will also be accepted by default. Include "*" to + // allow any request header. + AllowedHeaders []string `mapstructure:"allowed_headers"` + + // MaxAge sets the value of the Access-Control-Max-Age response header. + // Set it to the number of seconds that browsers should cache a CORS + // preflight response for. + MaxAge int `mapstructure:"max_age"` +} + +func authInterceptor(next http.Handler, server auth.Server) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx, err := server.Authenticate(r.Context(), r.Header) + if err != nil { + http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) + return + } + + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} + +func maxRequestBodySizeInterceptor(next http.Handler, maxRecvSize int64) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + r.Body = http.MaxBytesReader(w, r.Body, maxRecvSize) + next.ServeHTTP(w, r) + }) +} diff --git a/config/confighttp/confighttp_test.go b/config/confighttp/confighttp_test.go new file mode 100644 index 000000000000..3c96c58583df --- /dev/null +++ b/config/confighttp/confighttp_test.go @@ -0,0 +1,1424 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package confighttp + +import ( + "context" + "errors" + "fmt" + "io" + "net" + "net/http" + "net/http/httptest" + "net/url" + "path/filepath" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/zap" + "go.uber.org/zap/zaptest/observer" + + "go.opentelemetry.io/collector/client" + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/component/componenttest" + "go.opentelemetry.io/collector/config/configauth" + "go.opentelemetry.io/collector/config/configcompression" + "go.opentelemetry.io/collector/config/configopaque" + "go.opentelemetry.io/collector/config/configtelemetry" + "go.opentelemetry.io/collector/config/configtls" + "go.opentelemetry.io/collector/extension/auth" + "go.opentelemetry.io/collector/extension/auth/authtest" +) + +type customRoundTripper struct { +} + +var _ http.RoundTripper = (*customRoundTripper)(nil) + +func (c *customRoundTripper) RoundTrip(*http.Request) (*http.Response, error) { + return nil, nil +} + +var ( + testAuthID = component.MustNewID("testauth") + mockID = component.MustNewID("mock") + dummyID = component.MustNewID("dummy") + nonExistingID = component.MustNewID("nonexisting") +) + +func TestAllHTTPClientSettings(t *testing.T) { + host := &mockHost{ + ext: map[component.ID]component.Component{ + testAuthID: &authtest.MockClient{ResultRoundTripper: &customRoundTripper{}}, + }, + } + + maxIdleConns := 50 + maxIdleConnsPerHost := 40 + maxConnsPerHost := 45 + idleConnTimeout := 30 * time.Second + http2PingTimeout := 5 * time.Second + tests := []struct { + name string + settings ClientConfig + shouldError bool + }{ + { + name: "all_valid_settings", + settings: ClientConfig{ + Endpoint: "localhost:1234", + TLSSetting: configtls.ClientConfig{ + Insecure: false, + }, + ReadBufferSize: 1024, + WriteBufferSize: 512, + MaxIdleConns: &maxIdleConns, + MaxIdleConnsPerHost: &maxIdleConnsPerHost, + MaxConnsPerHost: &maxConnsPerHost, + IdleConnTimeout: &idleConnTimeout, + CustomRoundTripper: func(next http.RoundTripper) (http.RoundTripper, error) { return next, nil }, + Compression: "", + DisableKeepAlives: true, + HTTP2ReadIdleTimeout: idleConnTimeout, + HTTP2PingTimeout: http2PingTimeout, + }, + shouldError: false, + }, + { + name: "all_valid_settings_with_none_compression", + settings: ClientConfig{ + Endpoint: "localhost:1234", + TLSSetting: configtls.ClientConfig{ + Insecure: false, + }, + ReadBufferSize: 1024, + WriteBufferSize: 512, + MaxIdleConns: &maxIdleConns, + MaxIdleConnsPerHost: &maxIdleConnsPerHost, + MaxConnsPerHost: &maxConnsPerHost, + IdleConnTimeout: &idleConnTimeout, + CustomRoundTripper: func(next http.RoundTripper) (http.RoundTripper, error) { return next, nil }, + Compression: "none", + DisableKeepAlives: true, + HTTP2ReadIdleTimeout: idleConnTimeout, + HTTP2PingTimeout: http2PingTimeout, + }, + shouldError: false, + }, + { + name: "all_valid_settings_with_gzip_compression", + settings: ClientConfig{ + Endpoint: "localhost:1234", + TLSSetting: configtls.ClientConfig{ + Insecure: false, + }, + ReadBufferSize: 1024, + WriteBufferSize: 512, + MaxIdleConns: &maxIdleConns, + MaxIdleConnsPerHost: &maxIdleConnsPerHost, + MaxConnsPerHost: &maxConnsPerHost, + IdleConnTimeout: &idleConnTimeout, + CustomRoundTripper: func(next http.RoundTripper) (http.RoundTripper, error) { return next, nil }, + Compression: "gzip", + DisableKeepAlives: true, + HTTP2ReadIdleTimeout: idleConnTimeout, + HTTP2PingTimeout: http2PingTimeout, + }, + shouldError: false, + }, + { + name: "all_valid_settings_http2_health_check", + settings: ClientConfig{ + Endpoint: "localhost:1234", + TLSSetting: configtls.ClientConfig{ + Insecure: false, + }, + ReadBufferSize: 1024, + WriteBufferSize: 512, + MaxIdleConns: &maxIdleConns, + MaxIdleConnsPerHost: &maxIdleConnsPerHost, + MaxConnsPerHost: &maxConnsPerHost, + IdleConnTimeout: &idleConnTimeout, + CustomRoundTripper: func(next http.RoundTripper) (http.RoundTripper, error) { return next, nil }, + Compression: "gzip", + DisableKeepAlives: true, + HTTP2ReadIdleTimeout: idleConnTimeout, + HTTP2PingTimeout: http2PingTimeout, + }, + shouldError: false, + }, + { + name: "error_round_tripper_returned", + settings: ClientConfig{ + Endpoint: "localhost:1234", + TLSSetting: configtls.ClientConfig{ + Insecure: false, + }, + ReadBufferSize: 1024, + WriteBufferSize: 512, + CustomRoundTripper: func(http.RoundTripper) (http.RoundTripper, error) { return nil, errors.New("error") }, + }, + shouldError: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + tt := componenttest.NewNopTelemetrySettings() + tt.TracerProvider = nil + client, err := test.settings.ToClientContext(context.Background(), host, tt) + if test.shouldError { + assert.Error(t, err) + return + } + assert.NoError(t, err) + switch transport := client.Transport.(type) { + case *http.Transport: + assert.EqualValues(t, 1024, transport.ReadBufferSize) + assert.EqualValues(t, 512, transport.WriteBufferSize) + assert.EqualValues(t, 50, transport.MaxIdleConns) + assert.EqualValues(t, 40, transport.MaxIdleConnsPerHost) + assert.EqualValues(t, 45, transport.MaxConnsPerHost) + assert.EqualValues(t, 30*time.Second, transport.IdleConnTimeout) + assert.EqualValues(t, true, transport.DisableKeepAlives) + case *compressRoundTripper: + assert.EqualValues(t, "gzip", transport.compressionType) + } + }) + } +} + +func TestPartialHTTPClientSettings(t *testing.T) { + host := &mockHost{ + ext: map[component.ID]component.Component{ + testAuthID: &authtest.MockClient{ResultRoundTripper: &customRoundTripper{}}, + }, + } + + tests := []struct { + name string + settings ClientConfig + shouldError bool + }{ + { + name: "valid_partial_settings", + settings: ClientConfig{ + Endpoint: "localhost:1234", + TLSSetting: configtls.ClientConfig{ + Insecure: false, + }, + ReadBufferSize: 1024, + WriteBufferSize: 512, + CustomRoundTripper: func(next http.RoundTripper) (http.RoundTripper, error) { return next, nil }, + }, + shouldError: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + tt := componenttest.NewNopTelemetrySettings() + tt.TracerProvider = nil + client, err := test.settings.ToClientContext(context.Background(), host, tt) + assert.NoError(t, err) + transport := client.Transport.(*http.Transport) + assert.EqualValues(t, 1024, transport.ReadBufferSize) + assert.EqualValues(t, 512, transport.WriteBufferSize) + assert.EqualValues(t, 100, transport.MaxIdleConns) + assert.EqualValues(t, 0, transport.MaxIdleConnsPerHost) + assert.EqualValues(t, 0, transport.MaxConnsPerHost) + assert.EqualValues(t, 90*time.Second, transport.IdleConnTimeout) + assert.EqualValues(t, false, transport.DisableKeepAlives) + + }) + } +} + +func TestDefaultHTTPClientSettings(t *testing.T) { + httpClientSettings := NewDefaultClientConfig() + assert.EqualValues(t, 100, *httpClientSettings.MaxIdleConns) + assert.EqualValues(t, 90*time.Second, *httpClientSettings.IdleConnTimeout) +} + +func TestProxyURL(t *testing.T) { + testCases := []struct { + desc string + proxyURL string + expectedURL *url.URL + err bool + }{ + { + desc: "default config", + expectedURL: nil, + }, + { + desc: "proxy is set", + proxyURL: "http://proxy.example.com:8080", + expectedURL: &url.URL{Scheme: "http", Host: "proxy.example.com:8080"}, + }, + { + desc: "proxy is invalid", + proxyURL: "://example.com", + err: true, + }, + } + for _, tC := range testCases { + t.Run(tC.desc, func(t *testing.T) { + s := NewDefaultClientConfig() + s.ProxyURL = tC.proxyURL + + tt := componenttest.NewNopTelemetrySettings() + tt.TracerProvider = nil + client, err := s.ToClientContext(context.Background(), componenttest.NewNopHost(), tt) + + if tC.err { + require.Error(t, err) + } else { + require.NoError(t, err) + } + + if err == nil { + transport := client.Transport.(*http.Transport) + require.NotNil(t, transport.Proxy) + + url, err := transport.Proxy(&http.Request{URL: &url.URL{Scheme: "http", Host: "example.com"}}) + require.NoError(t, err) + + if tC.expectedURL == nil { + assert.Nil(t, url) + } else { + require.NotNil(t, url) + assert.Equal(t, tC.expectedURL, url) + } + } + }) + } +} + +func TestHTTPClientSettingsError(t *testing.T) { + host := &mockHost{ + ext: map[component.ID]component.Component{}, + } + tests := []struct { + settings ClientConfig + err string + }{ + { + err: "^failed to load TLS config: failed to load CA CertPool File: failed to load cert /doesnt/exist:", + settings: ClientConfig{ + Endpoint: "", + TLSSetting: configtls.ClientConfig{ + Config: configtls.Config{ + CAFile: "/doesnt/exist", + }, + Insecure: false, + ServerName: "", + }, + }, + }, + { + err: "^failed to load TLS config: failed to load TLS cert and key: for auth via TLS, provide both certificate and key, or neither", + settings: ClientConfig{ + Endpoint: "", + TLSSetting: configtls.ClientConfig{ + Config: configtls.Config{ + CertFile: "/doesnt/exist", + }, + Insecure: false, + ServerName: "", + }, + }, + }, + { + err: "failed to resolve authenticator \"dummy\": authenticator not found", + settings: ClientConfig{ + Endpoint: "https://localhost:1234/v1/traces", + Auth: &configauth.Authentication{AuthenticatorID: dummyID}, + }, + }, + } + for _, test := range tests { + t.Run(test.err, func(t *testing.T) { + _, err := test.settings.ToClientContext(context.Background(), host, componenttest.NewNopTelemetrySettings()) + assert.Regexp(t, test.err, err) + }) + } +} + +func TestHTTPClientSettingWithAuthConfig(t *testing.T) { + tests := []struct { + name string + shouldErr bool + settings ClientConfig + host component.Host + }{ + { + name: "no_auth_extension_enabled", + settings: ClientConfig{ + Endpoint: "localhost:1234", + Auth: nil, + }, + shouldErr: false, + host: &mockHost{ + ext: map[component.ID]component.Component{ + mockID: &authtest.MockClient{ + ResultRoundTripper: &customRoundTripper{}, + }, + }, + }, + }, + { + name: "with_auth_configuration_and_no_extension", + settings: ClientConfig{ + Endpoint: "localhost:1234", + Auth: &configauth.Authentication{AuthenticatorID: dummyID}, + }, + shouldErr: true, + host: &mockHost{ + ext: map[component.ID]component.Component{ + mockID: &authtest.MockClient{ResultRoundTripper: &customRoundTripper{}}, + }, + }, + }, + { + name: "with_auth_configuration_and_no_extension_map", + settings: ClientConfig{ + Endpoint: "localhost:1234", + Auth: &configauth.Authentication{AuthenticatorID: dummyID}, + }, + shouldErr: true, + host: componenttest.NewNopHost(), + }, + { + name: "with_auth_configuration_has_extension", + settings: ClientConfig{ + Endpoint: "localhost:1234", + Auth: &configauth.Authentication{AuthenticatorID: mockID}, + }, + shouldErr: false, + host: &mockHost{ + ext: map[component.ID]component.Component{ + mockID: &authtest.MockClient{ResultRoundTripper: &customRoundTripper{}}, + }, + }, + }, + { + name: "with_auth_configuration_has_extension_and_headers", + settings: ClientConfig{ + Endpoint: "localhost:1234", + Auth: &configauth.Authentication{AuthenticatorID: mockID}, + Headers: map[string]configopaque.String{"foo": "bar"}, + }, + shouldErr: false, + host: &mockHost{ + ext: map[component.ID]component.Component{ + mockID: &authtest.MockClient{ResultRoundTripper: &customRoundTripper{}}, + }, + }, + }, + { + name: "with_auth_configuration_has_extension_and_compression", + settings: ClientConfig{ + Endpoint: "localhost:1234", + Auth: &configauth.Authentication{AuthenticatorID: component.MustNewID("mock")}, + Compression: configcompression.TypeGzip, + }, + shouldErr: false, + host: &mockHost{ + ext: map[component.ID]component.Component{ + mockID: &authtest.MockClient{ResultRoundTripper: &customRoundTripper{}}, + }, + }, + }, + { + name: "with_auth_configuration_has_err_extension", + settings: ClientConfig{ + Endpoint: "localhost:1234", + Auth: &configauth.Authentication{AuthenticatorID: mockID}, + }, + shouldErr: true, + host: &mockHost{ + ext: map[component.ID]component.Component{ + mockID: &authtest.MockClient{ + ResultRoundTripper: &customRoundTripper{}, MustError: true}, + }, + }, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + // Omit TracerProvider and MeterProvider in TelemetrySettings as otelhttp.Transport cannot be introspected + client, err := test.settings.ToClientContext(context.Background(), test.host, component.TelemetrySettings{Logger: zap.NewNop(), MetricsLevel: configtelemetry.LevelNone}) + if test.shouldErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assert.NotNil(t, client) + transport := client.Transport + + // Compression should wrap Auth, unwrap it + if test.settings.Compression.IsCompressed() { + ct, ok := transport.(*compressRoundTripper) + assert.True(t, ok) + assert.Equal(t, test.settings.Compression, ct.compressionType) + transport = ct.rt + } + + // Headers should wrap Auth, unwrap it + if test.settings.Headers != nil { + ht, ok := transport.(*headerRoundTripper) + assert.True(t, ok) + assert.Equal(t, test.settings.Headers, ht.headers) + transport = ht.transport + } + + if test.settings.Auth != nil { + _, ok := transport.(*customRoundTripper) + assert.True(t, ok) + } + }) + } +} + +func TestHTTPServerSettingsError(t *testing.T) { + tests := []struct { + settings ServerConfig + err string + }{ + { + err: "^failed to load TLS config: failed to load CA CertPool File: failed to load cert /doesnt/exist:", + settings: ServerConfig{ + Endpoint: "localhost:0", + TLSSetting: &configtls.ServerConfig{ + Config: configtls.Config{ + CAFile: "/doesnt/exist", + }, + }, + }, + }, + { + err: "^failed to load TLS config: failed to load TLS cert and key: for auth via TLS, provide both certificate and key, or neither", + settings: ServerConfig{ + Endpoint: "localhost:0", + TLSSetting: &configtls.ServerConfig{ + Config: configtls.Config{ + CertFile: "/doesnt/exist", + }, + }, + }, + }, + { + err: "failed to load client CA CertPool: failed to load CA /doesnt/exist:", + settings: ServerConfig{ + Endpoint: "localhost:0", + TLSSetting: &configtls.ServerConfig{ + ClientCAFile: "/doesnt/exist", + }, + }, + }, + } + for _, test := range tests { + t.Run(test.err, func(t *testing.T) { + _, err := test.settings.ToListenerContext(context.Background()) + assert.Regexp(t, test.err, err) + }) + } +} + +func TestHTTPServerWarning(t *testing.T) { + tests := []struct { + name string + settings ServerConfig + len int + }{ + { + settings: ServerConfig{ + Endpoint: "0.0.0.0:0", + }, + len: 1, + }, + { + settings: ServerConfig{ + Endpoint: "127.0.0.1:0", + }, + len: 0, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + set := componenttest.NewNopTelemetrySettings() + logger, observed := observer.New(zap.DebugLevel) + set.Logger = zap.New(logger) + + _, err := test.settings.ToServerContext( + context.Background(), + componenttest.NewNopHost(), + set, + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + _, errWrite := fmt.Fprint(w, "test") + assert.NoError(t, errWrite) + })) + require.NoError(t, err) + require.Len(t, observed.FilterLevelExact(zap.WarnLevel).All(), test.len) + }) + } + +} + +func TestHttpReception(t *testing.T) { + tests := []struct { + name string + tlsServerCreds *configtls.ServerConfig + tlsClientCreds *configtls.ClientConfig + hasError bool + forceHTTP1 bool + }{ + { + name: "noTLS", + tlsServerCreds: nil, + tlsClientCreds: &configtls.ClientConfig{ + Insecure: true, + }, + }, + { + name: "TLS", + tlsServerCreds: &configtls.ServerConfig{ + Config: configtls.Config{ + CAFile: filepath.Join("testdata", "ca.crt"), + CertFile: filepath.Join("testdata", "server.crt"), + KeyFile: filepath.Join("testdata", "server.key"), + }, + }, + tlsClientCreds: &configtls.ClientConfig{ + Config: configtls.Config{ + CAFile: filepath.Join("testdata", "ca.crt"), + }, + ServerName: "localhost", + }, + }, + { + name: "TLS (HTTP/1.1)", + tlsServerCreds: &configtls.ServerConfig{ + Config: configtls.Config{ + CAFile: filepath.Join("testdata", "ca.crt"), + CertFile: filepath.Join("testdata", "server.crt"), + KeyFile: filepath.Join("testdata", "server.key"), + }, + }, + tlsClientCreds: &configtls.ClientConfig{ + Config: configtls.Config{ + CAFile: filepath.Join("testdata", "ca.crt"), + }, + ServerName: "localhost", + }, + forceHTTP1: true, + }, + { + name: "NoServerCertificates", + tlsServerCreds: &configtls.ServerConfig{ + Config: configtls.Config{ + CAFile: filepath.Join("testdata", "ca.crt"), + }, + }, + tlsClientCreds: &configtls.ClientConfig{ + Config: configtls.Config{ + CAFile: filepath.Join("testdata", "ca.crt"), + }, + ServerName: "localhost", + }, + hasError: true, + }, + { + name: "mTLS", + tlsServerCreds: &configtls.ServerConfig{ + Config: configtls.Config{ + CAFile: filepath.Join("testdata", "ca.crt"), + CertFile: filepath.Join("testdata", "server.crt"), + KeyFile: filepath.Join("testdata", "server.key"), + }, + ClientCAFile: filepath.Join("testdata", "ca.crt"), + }, + tlsClientCreds: &configtls.ClientConfig{ + Config: configtls.Config{ + CAFile: filepath.Join("testdata", "ca.crt"), + CertFile: filepath.Join("testdata", "client.crt"), + KeyFile: filepath.Join("testdata", "client.key"), + }, + ServerName: "localhost", + }, + }, + { + name: "NoClientCertificate", + tlsServerCreds: &configtls.ServerConfig{ + Config: configtls.Config{ + CAFile: filepath.Join("testdata", "ca.crt"), + CertFile: filepath.Join("testdata", "server.crt"), + KeyFile: filepath.Join("testdata", "server.key"), + }, + ClientCAFile: filepath.Join("testdata", "ca.crt"), + }, + tlsClientCreds: &configtls.ClientConfig{ + Config: configtls.Config{ + CAFile: filepath.Join("testdata", "ca.crt"), + }, + ServerName: "localhost", + }, + hasError: true, + }, + { + name: "WrongClientCA", + tlsServerCreds: &configtls.ServerConfig{ + Config: configtls.Config{ + CAFile: filepath.Join("testdata", "ca.crt"), + CertFile: filepath.Join("testdata", "server.crt"), + KeyFile: filepath.Join("testdata", "server.key"), + }, + ClientCAFile: filepath.Join("testdata", "server.crt"), + }, + tlsClientCreds: &configtls.ClientConfig{ + Config: configtls.Config{ + CAFile: filepath.Join("testdata", "ca.crt"), + CertFile: filepath.Join("testdata", "client.crt"), + KeyFile: filepath.Join("testdata", "client.key"), + }, + ServerName: "localhost", + }, + hasError: true, + }, + } + // prepare + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + hss := &ServerConfig{ + Endpoint: "localhost:0", + TLSSetting: tt.tlsServerCreds, + } + ln, err := hss.ToListenerContext(context.Background()) + require.NoError(t, err) + + s, err := hss.ToServerContext( + context.Background(), + componenttest.NewNopHost(), + componenttest.NewNopTelemetrySettings(), + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + _, errWrite := fmt.Fprint(w, "test") + assert.NoError(t, errWrite) + })) + require.NoError(t, err) + + go func() { + _ = s.Serve(ln) + }() + + prefix := "https://" + expectedProto := "HTTP/2.0" + if tt.tlsClientCreds.Insecure { + prefix = "http://" + expectedProto = "HTTP/1.1" + } + + hcs := &ClientConfig{ + Endpoint: prefix + ln.Addr().String(), + TLSSetting: *tt.tlsClientCreds, + } + if tt.forceHTTP1 { + expectedProto = "HTTP/1.1" + hcs.CustomRoundTripper = func(rt http.RoundTripper) (http.RoundTripper, error) { + rt.(*http.Transport).ForceAttemptHTTP2 = false + return rt, nil + } + } + client, errClient := hcs.ToClientContext(context.Background(), componenttest.NewNopHost(), component.TelemetrySettings{}) + require.NoError(t, errClient) + + resp, errResp := client.Get(hcs.Endpoint) + if tt.hasError { + assert.Error(t, errResp) + } else { + assert.NoError(t, errResp) + body, errRead := io.ReadAll(resp.Body) + assert.NoError(t, errRead) + assert.Equal(t, "test", string(body)) + assert.Equal(t, expectedProto, resp.Proto) + } + require.NoError(t, s.Close()) + }) + } +} + +func TestHttpCors(t *testing.T) { + tests := []struct { + name string + + *CORSConfig + + allowedWorks bool + disallowedWorks bool + extraHeaderWorks bool + }{ + { + name: "noCORS", + allowedWorks: false, + disallowedWorks: false, + extraHeaderWorks: false, + }, + { + name: "emptyCORS", + CORSConfig: &CORSConfig{}, + allowedWorks: false, + disallowedWorks: false, + extraHeaderWorks: false, + }, + { + name: "OriginCORS", + CORSConfig: &CORSConfig{ + AllowedOrigins: []string{"allowed-*.com"}, + }, + allowedWorks: true, + disallowedWorks: false, + extraHeaderWorks: false, + }, + { + name: "CacheableCORS", + CORSConfig: &CORSConfig{ + AllowedOrigins: []string{"allowed-*.com"}, + MaxAge: 360, + }, + allowedWorks: true, + disallowedWorks: false, + extraHeaderWorks: false, + }, + { + name: "HeaderCORS", + CORSConfig: &CORSConfig{ + AllowedOrigins: []string{"allowed-*.com"}, + AllowedHeaders: []string{"ExtraHeader"}, + }, + allowedWorks: true, + disallowedWorks: false, + extraHeaderWorks: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + hss := &ServerConfig{ + Endpoint: "localhost:0", + CORS: tt.CORSConfig, + } + + ln, err := hss.ToListenerContext(context.Background()) + require.NoError(t, err) + + s, err := hss.ToServerContext( + context.Background(), + componenttest.NewNopHost(), + componenttest.NewNopTelemetrySettings(), + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusOK) + })) + require.NoError(t, err) + + go func() { + _ = s.Serve(ln) + }() + + url := fmt.Sprintf("http://%s", ln.Addr().String()) + + expectedStatus := http.StatusNoContent + if tt.CORSConfig == nil || len(tt.AllowedOrigins) == 0 { + expectedStatus = http.StatusOK + } + + // Verify allowed domain gets responses that allow CORS. + verifyCorsResp(t, url, "allowed-origin.com", tt.CORSConfig, false, expectedStatus, tt.allowedWorks) + + // Verify allowed domain and extra headers gets responses that allow CORS. + verifyCorsResp(t, url, "allowed-origin.com", tt.CORSConfig, true, expectedStatus, tt.extraHeaderWorks) + + // Verify disallowed domain gets responses that disallow CORS. + verifyCorsResp(t, url, "disallowed-origin.com", tt.CORSConfig, false, expectedStatus, tt.disallowedWorks) + + require.NoError(t, s.Close()) + }) + } +} + +func TestHttpCorsInvalidSettings(t *testing.T) { + hss := &ServerConfig{ + Endpoint: "localhost:0", + CORS: &CORSConfig{AllowedHeaders: []string{"some-header"}}, + } + + // This effectively does not enable CORS but should also not cause an error + s, err := hss.ToServerContext( + context.Background(), + componenttest.NewNopHost(), + componenttest.NewNopTelemetrySettings(), + http.HandlerFunc(func(http.ResponseWriter, *http.Request) {})) + require.NoError(t, err) + require.NotNil(t, s) + require.NoError(t, s.Close()) +} + +func TestHttpCorsWithSettings(t *testing.T) { + hss := &ServerConfig{ + Endpoint: "localhost:0", + CORS: &CORSConfig{ + AllowedOrigins: []string{"*"}, + }, + Auth: &configauth.Authentication{ + AuthenticatorID: mockID, + }, + } + + host := &mockHost{ + ext: map[component.ID]component.Component{ + mockID: auth.NewServer( + auth.WithServerAuthenticate(func(ctx context.Context, _ map[string][]string) (context.Context, error) { + return ctx, errors.New("Settings failed") + }), + ), + }, + } + + srv, err := hss.ToServerContext(context.Background(), host, componenttest.NewNopTelemetrySettings(), nil) + require.NoError(t, err) + require.NotNil(t, srv) + + rec := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodOptions, "/", nil) + req.Header.Set("Origin", "http://localhost") + req.Header.Set("Access-Control-Request-Method", http.MethodPost) + srv.Handler.ServeHTTP(rec, req) + + assert.Equal(t, http.StatusNoContent, rec.Result().StatusCode) + assert.Equal(t, "*", rec.Header().Get("Access-Control-Allow-Origin")) +} + +func TestHttpServerHeaders(t *testing.T) { + tests := []struct { + name string + headers map[string]configopaque.String + }{ + { + name: "noHeaders", + headers: nil, + }, + { + name: "emptyHeaders", + headers: map[string]configopaque.String{}, + }, + { + name: "withHeaders", + headers: map[string]configopaque.String{ + "x-new-header-1": "value1", + "x-new-header-2": "value2", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + hss := &ServerConfig{ + Endpoint: "localhost:0", + ResponseHeaders: tt.headers, + } + + ln, err := hss.ToListenerContext(context.Background()) + require.NoError(t, err) + + s, err := hss.ToServerContext( + context.Background(), + componenttest.NewNopHost(), + componenttest.NewNopTelemetrySettings(), + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusOK) + })) + require.NoError(t, err) + + go func() { + _ = s.Serve(ln) + }() + + url := fmt.Sprintf("http://%s", ln.Addr().String()) + + // Verify allowed domain gets responses that allow CORS. + verifyHeadersResp(t, url, tt.headers) + + require.NoError(t, s.Close()) + }) + } +} + +func verifyCorsResp(t *testing.T, url string, origin string, set *CORSConfig, extraHeader bool, wantStatus int, wantAllowed bool) { + req, err := http.NewRequest(http.MethodOptions, url, nil) + require.NoError(t, err, "Error creating trace OPTIONS request: %v", err) + req.Header.Set("Origin", origin) + if extraHeader { + req.Header.Set("ExtraHeader", "foo") + req.Header.Set("Access-Control-Request-Headers", "ExtraHeader") + } + req.Header.Set("Access-Control-Request-Method", "POST") + + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err, "Error sending OPTIONS to http server: %v", err) + + err = resp.Body.Close() + if err != nil { + t.Errorf("Error closing OPTIONS response body, %v", err) + } + + assert.Equal(t, wantStatus, resp.StatusCode) + + gotAllowOrigin := resp.Header.Get("Access-Control-Allow-Origin") + gotAllowMethods := resp.Header.Get("Access-Control-Allow-Methods") + + wantAllowOrigin := "" + wantAllowMethods := "" + wantMaxAge := "" + if wantAllowed { + wantAllowOrigin = origin + wantAllowMethods = "POST" + if set != nil && set.MaxAge != 0 { + wantMaxAge = fmt.Sprintf("%d", set.MaxAge) + } + } + assert.Equal(t, wantAllowOrigin, gotAllowOrigin) + assert.Equal(t, wantAllowMethods, gotAllowMethods) + assert.Equal(t, wantMaxAge, resp.Header.Get("Access-Control-Max-Age")) +} + +func verifyHeadersResp(t *testing.T, url string, expected map[string]configopaque.String) { + req, err := http.NewRequest(http.MethodGet, url, nil) + require.NoError(t, err, "Error creating request: %v", err) + + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err, "Error sending request to http server: %v", err) + + err = resp.Body.Close() + if err != nil { + t.Errorf("Error closing response body, %v", err) + } + + assert.Equal(t, http.StatusOK, resp.StatusCode) + + for k, v := range expected { + assert.Equal(t, string(v), resp.Header.Get(k)) + } +} + +func ExampleServerConfig() { + settings := ServerConfig{ + Endpoint: "localhost:443", + } + s, err := settings.ToServerContext( + context.Background(), + componenttest.NewNopHost(), + componenttest.NewNopTelemetrySettings(), + http.HandlerFunc(func(http.ResponseWriter, *http.Request) {})) + if err != nil { + panic(err) + } + + l, err := settings.ToListenerContext(context.Background()) + if err != nil { + panic(err) + } + if err = s.Serve(l); err != nil { + panic(err) + } +} + +func TestHttpClientHeaders(t *testing.T) { + tests := []struct { + name string + headers map[string]configopaque.String + }{ + { + name: "with_headers", + headers: map[string]configopaque.String{ + "header1": "value1", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + for k, v := range tt.headers { + assert.Equal(t, r.Header.Get(k), string(v)) + } + w.WriteHeader(http.StatusOK) + })) + defer server.Close() + serverURL, _ := url.Parse(server.URL) + setting := ClientConfig{ + Endpoint: serverURL.String(), + TLSSetting: configtls.ClientConfig{}, + ReadBufferSize: 0, + WriteBufferSize: 0, + Timeout: 0, + Headers: tt.headers, + } + client, _ := setting.ToClientContext(context.Background(), componenttest.NewNopHost(), componenttest.NewNopTelemetrySettings()) + req, err := http.NewRequest(http.MethodGet, setting.Endpoint, nil) + assert.NoError(t, err) + _, err = client.Do(req) + assert.NoError(t, err) + }) + } +} + +func TestHttpClientHostHeader(t *testing.T) { + hostHeader := "th" + tt := struct { + name string + headers map[string]configopaque.String + }{ + name: "with_host_header", + headers: map[string]configopaque.String{ + "Host": configopaque.String(hostHeader), + }, + } + + t.Run(tt.name, func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, hostHeader, r.Host) + w.WriteHeader(http.StatusOK) + })) + defer server.Close() + serverURL, _ := url.Parse(server.URL) + setting := ClientConfig{ + Endpoint: serverURL.String(), + TLSSetting: configtls.ClientConfig{}, + ReadBufferSize: 0, + WriteBufferSize: 0, + Timeout: 0, + Headers: tt.headers, + } + client, _ := setting.ToClientContext(context.Background(), componenttest.NewNopHost(), componenttest.NewNopTelemetrySettings()) + req, err := http.NewRequest(http.MethodGet, setting.Endpoint, nil) + assert.NoError(t, err) + _, err = client.Do(req) + assert.NoError(t, err) + }) +} + +func TestContextWithClient(t *testing.T) { + testCases := []struct { + desc string + input *http.Request + doMetadata bool + expected client.Info + }{ + { + desc: "request without client IP or headers", + input: &http.Request{}, + expected: client.Info{}, + }, + { + desc: "request with client IP", + input: &http.Request{ + RemoteAddr: "1.2.3.4:55443", + }, + expected: client.Info{ + Addr: &net.IPAddr{ + IP: net.IPv4(1, 2, 3, 4), + }, + }, + }, + { + desc: "request with client headers, no metadata processing", + input: &http.Request{ + Header: map[string][]string{"x-test-header": {"test-value"}}, + }, + doMetadata: false, + expected: client.Info{}, + }, + { + desc: "request with client headers", + input: &http.Request{ + Header: map[string][]string{"x-test-header": {"test-value"}}, + }, + doMetadata: true, + expected: client.Info{ + Metadata: client.NewMetadata(map[string][]string{"x-test-header": {"test-value"}}), + }, + }, + { + desc: "request with Host and client headers", + input: &http.Request{ + Header: map[string][]string{"x-test-header": {"test-value"}}, + Host: "localhost:55443", + }, + doMetadata: true, + expected: client.Info{ + Metadata: client.NewMetadata(map[string][]string{"x-test-header": {"test-value"}, "Host": {"localhost:55443"}}), + }, + }, + } + for _, tC := range testCases { + t.Run(tC.desc, func(t *testing.T) { + ctx := contextWithClient(tC.input, tC.doMetadata) + assert.Equal(t, tC.expected, client.FromContext(ctx)) + }) + } +} + +func TestServerAuth(t *testing.T) { + // prepare + authCalled := false + hss := ServerConfig{ + Endpoint: "localhost:0", + Auth: &configauth.Authentication{ + AuthenticatorID: mockID, + }, + } + + host := &mockHost{ + ext: map[component.ID]component.Component{ + mockID: auth.NewServer( + auth.WithServerAuthenticate(func(ctx context.Context, _ map[string][]string) (context.Context, error) { + authCalled = true + return ctx, nil + }), + ), + }, + } + + handlerCalled := false + handler := http.HandlerFunc(func(http.ResponseWriter, *http.Request) { + handlerCalled = true + }) + + srv, err := hss.ToServerContext(context.Background(), host, componenttest.NewNopTelemetrySettings(), handler) + require.NoError(t, err) + + // test + srv.Handler.ServeHTTP(&httptest.ResponseRecorder{}, httptest.NewRequest("GET", "/", nil)) + + // verify + assert.True(t, handlerCalled) + assert.True(t, authCalled) +} + +func TestInvalidServerAuth(t *testing.T) { + hss := ServerConfig{ + Auth: &configauth.Authentication{ + AuthenticatorID: nonExistingID, + }, + } + + srv, err := hss.ToServerContext(context.Background(), componenttest.NewNopHost(), componenttest.NewNopTelemetrySettings(), http.NewServeMux()) + require.Error(t, err) + require.Nil(t, srv) +} + +func TestFailedServerAuth(t *testing.T) { + // prepare + hss := ServerConfig{ + Endpoint: "localhost:0", + Auth: &configauth.Authentication{ + AuthenticatorID: mockID, + }, + } + host := &mockHost{ + ext: map[component.ID]component.Component{ + mockID: auth.NewServer( + auth.WithServerAuthenticate(func(ctx context.Context, _ map[string][]string) (context.Context, error) { + return ctx, errors.New("Settings failed") + }), + ), + }, + } + + srv, err := hss.ToServerContext(context.Background(), host, componenttest.NewNopTelemetrySettings(), http.HandlerFunc(func(http.ResponseWriter, *http.Request) {})) + require.NoError(t, err) + + // test + response := &httptest.ResponseRecorder{} + srv.Handler.ServeHTTP(response, httptest.NewRequest("GET", "/", nil)) + + // verify + assert.Equal(t, response.Result().StatusCode, http.StatusUnauthorized) + assert.Equal(t, response.Result().Status, fmt.Sprintf("%v %s", http.StatusUnauthorized, http.StatusText(http.StatusUnauthorized))) +} + +func TestServerWithErrorHandler(t *testing.T) { + // prepare + hss := ServerConfig{ + Endpoint: "localhost:0", + } + eh := func(w http.ResponseWriter, _ *http.Request, _ string, statusCode int) { + assert.Equal(t, statusCode, http.StatusBadRequest) + // custom error handler changes returned status code + http.Error(w, "invalid request", http.StatusInternalServerError) + } + + srv, err := hss.ToServerContext( + context.Background(), + componenttest.NewNopHost(), + componenttest.NewNopTelemetrySettings(), + http.HandlerFunc(func(http.ResponseWriter, *http.Request) {}), + WithErrorHandler(eh), + ) + require.NoError(t, err) + // test + response := &httptest.ResponseRecorder{} + + req, err := http.NewRequest(http.MethodGet, srv.Addr, nil) + require.NoError(t, err, "Error creating request: %v", err) + req.Header.Set("Content-Encoding", "something-invalid") + + srv.Handler.ServeHTTP(response, req) + // verify + assert.Equal(t, response.Result().StatusCode, http.StatusInternalServerError) +} + +func TestServerWithDecoder(t *testing.T) { + // prepare + hss := ServerConfig{ + Endpoint: "localhost:0", + } + decoder := func(body io.ReadCloser) (io.ReadCloser, error) { + return body, nil + } + + srv, err := hss.ToServerContext( + context.Background(), + componenttest.NewNopHost(), + componenttest.NewNopTelemetrySettings(), + http.HandlerFunc(func(http.ResponseWriter, *http.Request) {}), + WithDecoder("something-else", decoder), + ) + require.NoError(t, err) + // test + response := &httptest.ResponseRecorder{} + + req, err := http.NewRequest(http.MethodGet, srv.Addr, nil) + require.NoError(t, err, "Error creating request: %v", err) + req.Header.Set("Content-Encoding", "something-else") + + srv.Handler.ServeHTTP(response, req) + // verify + assert.Equal(t, response.Result().StatusCode, http.StatusOK) + +} + +type mockHost struct { + component.Host + ext map[component.ID]component.Component +} + +func (nh *mockHost) GetExtensions() map[component.ID]component.Component { + return nh.ext +} + +func BenchmarkHttpRequest(b *testing.B) { + tests := []struct { + name string + forceHTTP1 bool + clientPerThread bool + }{ + { + name: "HTTP/2.0, shared client (like load balancer)", + forceHTTP1: false, + clientPerThread: false, + }, + { + name: "HTTP/1.1, shared client (like load balancer)", + forceHTTP1: true, + clientPerThread: false, + }, + { + name: "HTTP/2.0, client per thread (like single app)", + forceHTTP1: false, + clientPerThread: true, + }, + { + name: "HTTP/1.1, client per thread (like single app)", + forceHTTP1: true, + clientPerThread: true, + }, + } + + tlsServerCreds := &configtls.ServerConfig{ + Config: configtls.Config{ + CAFile: filepath.Join("testdata", "ca.crt"), + CertFile: filepath.Join("testdata", "server.crt"), + KeyFile: filepath.Join("testdata", "server.key"), + }, + } + tlsClientCreds := &configtls.ClientConfig{ + Config: configtls.Config{ + CAFile: filepath.Join("testdata", "ca.crt"), + }, + ServerName: "localhost", + } + + hss := &ServerConfig{ + Endpoint: "localhost:0", + TLSSetting: tlsServerCreds, + } + + s, err := hss.ToServerContext( + context.Background(), + componenttest.NewNopHost(), + componenttest.NewNopTelemetrySettings(), + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + _, errWrite := fmt.Fprint(w, "test") + require.NoError(b, errWrite) + })) + require.NoError(b, err) + ln, err := hss.ToListenerContext(context.Background()) + require.NoError(b, err) + + go func() { + _ = s.Serve(ln) + }() + defer func() { + _ = s.Close() + }() + + for _, bb := range tests { + hcs := &ClientConfig{ + Endpoint: "https://" + ln.Addr().String(), + TLSSetting: *tlsClientCreds, + } + if bb.forceHTTP1 { + hcs.CustomRoundTripper = func(rt http.RoundTripper) (http.RoundTripper, error) { + rt.(*http.Transport).ForceAttemptHTTP2 = false + return rt, nil + } + } + b.Run(bb.name, func(b *testing.B) { + var c *http.Client + if !bb.clientPerThread { + c, err = hcs.ToClientContext(context.Background(), componenttest.NewNopHost(), component.TelemetrySettings{}) + require.NoError(b, err) + } + b.RunParallel(func(pb *testing.PB) { + if c == nil { + c, err = hcs.ToClientContext(context.Background(), componenttest.NewNopHost(), component.TelemetrySettings{}) + require.NoError(b, err) + } + for pb.Next() { + resp, errResp := c.Get(hcs.Endpoint) + require.NoError(b, errResp) + body, errRead := io.ReadAll(resp.Body) + _ = resp.Body.Close() + require.NoError(b, errRead) + require.Equal(b, "test", string(body)) + } + c.CloseIdleConnections() + }) + // Wait for connections to close before closing server to prevent log spam + <-time.After(10 * time.Millisecond) + }) + } +} diff --git a/config/confighttp/doc.go b/config/confighttp/doc.go new file mode 100644 index 000000000000..2813862672ec --- /dev/null +++ b/config/confighttp/doc.go @@ -0,0 +1,6 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +// Package confighttp defines the configuration settings +// for creating an HTTP client and server. +package confighttp // import "go.opentelemetry.io/collector/config/confighttp" diff --git a/config/confighttp/go.mod b/config/confighttp/go.mod new file mode 100644 index 000000000000..d809c5f19926 --- /dev/null +++ b/config/confighttp/go.mod @@ -0,0 +1,94 @@ +module go.opentelemetry.io/collector/config/confighttp + +go 1.21 + +require ( + github.com/golang/snappy v0.0.4 + github.com/klauspost/compress v1.17.8 + github.com/rs/cors v1.10.1 + github.com/stretchr/testify v1.9.0 + go.opentelemetry.io/collector v0.98.0 + go.opentelemetry.io/collector/component v0.98.0 + go.opentelemetry.io/collector/config/configauth v0.98.0 + go.opentelemetry.io/collector/config/configcompression v1.5.0 + go.opentelemetry.io/collector/config/configopaque v1.5.0 + go.opentelemetry.io/collector/config/configtelemetry v0.98.0 + go.opentelemetry.io/collector/config/configtls v0.98.0 + go.opentelemetry.io/collector/config/internal v0.98.0 + go.opentelemetry.io/collector/extension/auth v0.98.0 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 + go.opentelemetry.io/otel v1.25.0 + go.uber.org/goleak v1.3.0 + go.uber.org/zap v1.27.0 + golang.org/x/net v0.24.0 +) + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/go-logr/logr v1.4.1 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/hashicorp/go-version v1.6.0 // indirect + github.com/knadh/koanf/maps v0.1.1 // indirect + github.com/knadh/koanf/providers/confmap v0.1.0 // indirect + github.com/knadh/koanf/v2 v2.1.1 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/prometheus/client_golang v1.19.0 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.48.0 // indirect + github.com/prometheus/procfs v0.12.0 // indirect + go.opentelemetry.io/collector/confmap v0.98.0 // indirect + go.opentelemetry.io/collector/extension v0.98.0 // indirect + go.opentelemetry.io/collector/featuregate v1.5.0 // indirect + go.opentelemetry.io/collector/pdata v1.5.0 // indirect + go.opentelemetry.io/otel/exporters/prometheus v0.47.0 // indirect + go.opentelemetry.io/otel/metric v1.25.0 // indirect + go.opentelemetry.io/otel/sdk v1.25.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.25.0 // indirect + go.opentelemetry.io/otel/trace v1.25.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + golang.org/x/sys v0.19.0 // indirect + golang.org/x/text v0.14.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80 // indirect + google.golang.org/grpc v1.62.1 // indirect + google.golang.org/protobuf v1.33.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +replace go.opentelemetry.io/collector => ../../ + +replace go.opentelemetry.io/collector/config/configauth => ../configauth + +replace go.opentelemetry.io/collector/config/configcompression => ../configcompression + +replace go.opentelemetry.io/collector/config/configopaque => ../configopaque + +replace go.opentelemetry.io/collector/config/configtls => ../configtls + +replace go.opentelemetry.io/collector/config/configtelemetry => ../configtelemetry + +replace go.opentelemetry.io/collector/config/internal => ../internal + +replace go.opentelemetry.io/collector/extension => ../../extension + +replace go.opentelemetry.io/collector/extension/auth => ../../extension/auth + +replace go.opentelemetry.io/collector/confmap => ../../confmap + +replace go.opentelemetry.io/collector/featuregate => ../../featuregate + +replace go.opentelemetry.io/collector/pdata => ../../pdata + +replace go.opentelemetry.io/collector/component => ../../component + +replace go.opentelemetry.io/collector/consumer => ../../consumer + +replace go.opentelemetry.io/collector/pdata/testdata => ../../pdata/testdata diff --git a/config/confighttp/go.sum b/config/confighttp/go.sum new file mode 100644 index 000000000000..43e9e71064cf --- /dev/null +++ b/config/confighttp/go.sum @@ -0,0 +1,135 @@ +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +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/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 h1:TQcrn6Wq+sKGkpyPvppOz99zsMBaUOKXq6HSv655U1c= +github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +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/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= +github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU= +github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NIs= +github.com/knadh/koanf/maps v0.1.1/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= +github.com/knadh/koanf/providers/confmap v0.1.0 h1:gOkxhHkemwG4LezxxN8DMOFopOPghxRVp7JbIvdvqzU= +github.com/knadh/koanf/providers/confmap v0.1.0/go.mod h1:2uLhxQzJnyHKfxG927awZC7+fyHFdQkd697K4MdLnIU= +github.com/knadh/koanf/v2 v2.1.1 h1:/R8eXqasSTsmDCsAyYj+81Wteg8AqrV9CP6gvsTsOmM= +github.com/knadh/koanf/v2 v2.1.1/go.mod h1:4mnTRbZCK+ALuBXHZMjDfG9y714L7TykVnZkXbMU3Es= +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/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +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/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU= +github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE= +github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= +github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= +github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/rs/cors v1.10.1 h1:L0uuZVXIKlI1SShY2nhFfo44TYvDPQ1w4oFkUJNfhyo= +github.com/rs/cors v1.10.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +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= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= +go.opentelemetry.io/otel v1.25.0 h1:gldB5FfhRl7OJQbUHt/8s0a7cE8fbsPAtdpRaApKy4k= +go.opentelemetry.io/otel v1.25.0/go.mod h1:Wa2ds5NOXEMkCmUou1WA7ZBfLTHWIsp034OVD7AO+Vg= +go.opentelemetry.io/otel/exporters/prometheus v0.47.0 h1:OL6yk1Z/pEGdDnrBbxSsH+t4FY1zXfBRGd7bjwhlMLU= +go.opentelemetry.io/otel/exporters/prometheus v0.47.0/go.mod h1:xF3N4OSICZDVbbYZydz9MHFro1RjmkPUKEvar2utG+Q= +go.opentelemetry.io/otel/metric v1.25.0 h1:LUKbS7ArpFL/I2jJHdJcqMGxkRdxpPHE0VU/D4NuEwA= +go.opentelemetry.io/otel/metric v1.25.0/go.mod h1:rkDLUSd2lC5lq2dFNrX9LGAbINP5B7WBkC78RXCpH5s= +go.opentelemetry.io/otel/sdk v1.25.0 h1:PDryEJPC8YJZQSyLY5eqLeafHtG+X7FWnf3aXMtxbqo= +go.opentelemetry.io/otel/sdk v1.25.0/go.mod h1:oFgzCM2zdsxKzz6zwpTZYLLQsFwc+K0daArPdIhuxkw= +go.opentelemetry.io/otel/sdk/metric v1.25.0 h1:7CiHOy08LbrxMAp4vWpbiPcklunUshVpAvGBrdDRlGw= +go.opentelemetry.io/otel/sdk/metric v1.25.0/go.mod h1:LzwoKptdbBBdYfvtGCzGwk6GWMA3aUzBOwtQpR6Nz7o= +go.opentelemetry.io/otel/trace v1.25.0 h1:tqukZGLwQYRIFtSQM2u2+yfMVTgGVeqRLPUYx1Dq6RM= +go.opentelemetry.io/otel/trace v1.25.0/go.mod h1:hCCs70XM/ljO+BeQkyFnbK28SBIJ/Emuha+ccrCRT7I= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= +golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +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.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80 h1:AjyfHzEPEFp/NpvfN5g+KDla3EMojjhRVZc1i7cj+oM= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80/go.mod h1:PAREbraiVEVGVdTZsVWjSbbTtSyGbAgIIvni8a8CD5s= +google.golang.org/grpc v1.62.1 h1:B4n+nfKzOICUXMgyrNd19h/I9oH0L1pizfk1d4zSgTk= +google.golang.org/grpc v1.62.1/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +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= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/config/confighttp/package_test.go b/config/confighttp/package_test.go new file mode 100644 index 000000000000..30797439baf3 --- /dev/null +++ b/config/confighttp/package_test.go @@ -0,0 +1,14 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package confighttp + +import ( + "testing" + + "go.uber.org/goleak" +) + +func TestMain(m *testing.M) { + goleak.VerifyTestMain(m) +} diff --git a/config/confighttp/testdata/ca.crt b/config/confighttp/testdata/ca.crt new file mode 100644 index 000000000000..e850c66f4f78 --- /dev/null +++ b/config/confighttp/testdata/ca.crt @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDNjCCAh4CCQCBqDI24JacNTANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJB +VTESMBAGA1UECAwJQXVzdHJhbGlhMQ8wDQYDVQQHDAZTeWRuZXkxEjAQBgNVBAoM +CU15T3JnTmFtZTEVMBMGA1UEAwwMTXlDb21tb25OYW1lMB4XDTIyMDgwMzA0MTAx +N1oXDTMyMDczMTA0MTAxN1owXTELMAkGA1UEBhMCQVUxEjAQBgNVBAgMCUF1c3Ry +YWxpYTEPMA0GA1UEBwwGU3lkbmV5MRIwEAYDVQQKDAlNeU9yZ05hbWUxFTATBgNV +BAMMDE15Q29tbW9uTmFtZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB +ANFDGZCSxNdaJQ1q7P5qVAZDKCj+3ClpFWAVT5HYDlOqgIjYRZ45/YGfKsDbK6rj +/+v5xTnTZt8e8kRfPgrZTBtPtefu6tKwxYYr3sSCR3TU4bX4dIM9ZwrFaTrPP1hk +UXR8zlk7F6gcWS+WX7LdupMs5SdZIePhkzpkxYIBatdRMf7w2f5v6M3UnOrCoyz0 +YPvvyZKq5zo9uBlVkrUL/QDrOB5yYjith7l8FkLHAirGTaszfF+8pZwzZn4ykVbn +eQQs1g6ujR0DgQh/k6A6XDQfg4JWQqNt3kkiO7NPIHTrl+W/nKdqve864ECAHSM2 +NIgGtQqVavPKD6pr15V328cCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEALZuCeu8U +yjJlR+BbDqiZw7/EBpGgTX4mv9CofJc9ErxFGJTYiaLZ1Jf3bwE+0G9ym8UfxKG8 +9xCYmoVbEQhgLzYDpYPxkKi5X7RUMw9fKlbRqwy2Ek1mDnSIYPRalhfOXBT5E652 +OUeILLRJlAPL3SrALjJM5Jjn4pkwankE53mfU4LpC7xWOjuwkSPRor1XCwoAZMTz +EZsZGUQf5M69ZAy0wWHu4C94rlgD37hybUEzhsr9UiK2v1Gn3a7BSAgtkbD0OIe5 +BzCu+UHQO4u841SbXn4q8aO1idaR8UhPAqfVno+L7ZmHRGsgvMk1vlMbroIIYaAz +2LQP6IwYUWRM9Q== +-----END CERTIFICATE----- diff --git a/config/confighttp/testdata/client.crt b/config/confighttp/testdata/client.crt new file mode 100644 index 000000000000..375ffd32a246 --- /dev/null +++ b/config/confighttp/testdata/client.crt @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDVTCCAj2gAwIBAgIJANt5fkUlfxyOMA0GCSqGSIb3DQEBCwUAMF0xCzAJBgNV +BAYTAkFVMRIwEAYDVQQIDAlBdXN0cmFsaWExDzANBgNVBAcMBlN5ZG5leTESMBAG +A1UECgwJTXlPcmdOYW1lMRUwEwYDVQQDDAxNeUNvbW1vbk5hbWUwHhcNMjIwODAz +MDQxMDE3WhcNMzIwNzMxMDQxMDE3WjBdMQswCQYDVQQGEwJBVTESMBAGA1UECAwJ +QXVzdHJhbGlhMQ8wDQYDVQQHDAZTeWRuZXkxEjAQBgNVBAoMCU15T3JnTmFtZTEV +MBMGA1UEAwwMTXlDb21tb25OYW1lMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB +CgKCAQEApmnIUees+b/V5gEgEOwyjPo9xeejzd/DSo02x/syauoU4mhINwa8cuB+ +nvGhCUY5w+bZfOnWhY03wjnqM1Ay+sg7yHZgqeHclzQh5d39ojeaMa3x8c3IQsKZ +j/UTyYmkX6Gap8Z8fk1ucM43clM3nUUf3THzeo0X3MTm7FIs2OCxcbCYo8GUo6GV +rn7nhutOrYgyPrgo7bi8xF2sJRRKC/C7MBCeNaJlgj53jB93gF15/ZxPbAQCtI5R +J+3YrKp987RSuXtDeqm1zSCmV6LnT2vVsSNmnDdvWHQF7SqgauqT0Z7gm4/M/xXw +ge6GJaENOV1wIUsfbYj6UCMt6tZpjQIDAQABoxgwFjAUBgNVHREEDTALgglsb2Nh +bGhvc3QwDQYJKoZIhvcNAQELBQADggEBAHwMOQIz++7JPWRb3xHTkt6jIlAw/zVl +3UmdXMqEEscLGcjmDqu0u1qPGlS48Ukb4efAQsp+/2KLQex7jdl3r4p0+3O530AY +MEtlK2oH1uE3KDMyswA0INrGeZpHRP2BI6QJGbxcJN22MuHFmKwPBJlltgVtK0Gt +IhG/cRJnVMcX+iBdsxygW5u7P8MpC8Eoved/F8yKB66uJ4LyZC5KL9RG58Y0JJin +KDvpzSGLMnHgnuJlGOGz/uUYXZ4IX5g95yKiDZ9+GkVo+DRHAUNEUjfQSRujmO10 +INTHs4/hd0MI6NaHXwObvmOsa8VRRVWa995O5Y9rZMYg/4mjm1Axcm4= +-----END CERTIFICATE----- diff --git a/config/confighttp/testdata/client.key b/config/confighttp/testdata/client.key new file mode 100644 index 000000000000..e15ea84e6821 --- /dev/null +++ b/config/confighttp/testdata/client.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEApmnIUees+b/V5gEgEOwyjPo9xeejzd/DSo02x/syauoU4mhI +Nwa8cuB+nvGhCUY5w+bZfOnWhY03wjnqM1Ay+sg7yHZgqeHclzQh5d39ojeaMa3x +8c3IQsKZj/UTyYmkX6Gap8Z8fk1ucM43clM3nUUf3THzeo0X3MTm7FIs2OCxcbCY +o8GUo6GVrn7nhutOrYgyPrgo7bi8xF2sJRRKC/C7MBCeNaJlgj53jB93gF15/ZxP +bAQCtI5RJ+3YrKp987RSuXtDeqm1zSCmV6LnT2vVsSNmnDdvWHQF7SqgauqT0Z7g +m4/M/xXwge6GJaENOV1wIUsfbYj6UCMt6tZpjQIDAQABAoIBAEAIGfUyAMPEhchP +jIgWakkGjLhWrhesTtejyH1gcYDj+w828vqBVAeby/zamo0YAWgYrny6+TlAIkFQ +yYXfCQ6n9yDmM8GKT7e6boSlS0+ct28AMEVLWhAeErpqoad9l8rYQsrlu8dZgfJT +1s/dp1uTWnRhIP95xMHE3dn2sJzuEV9HunkgS6re6YyTqNZFLAPVn33wDRVSaxH6 +3VJG6T3Y2xs77AKTKz5Mji/gqK3XFAed0rehowgS4A8mEUrZt9gzioRlmV2tshvx +st5KDw7lGEZKbgGB/+cwLJhHgEK7KyJRsR/shtDyXKRlEC19qV6zSupz9BSQNqRN ++vWQ070CgYEA0NTYgdxz8msVMXtLCectwMLfLU99H/frGCvjBv0wLni2XF+eS9gX +D6W4nFbDA+wZfSBMpEqriHvrDpMk7RN3Q9rAKtqeyzDboWiJRRT4DZeRz9o+WEt3 ++wDP8W0dS7IRw8Jnrsj9JgmY35fGXZDUWZRnPuvrpolojUnMTKNK/ccCgYEAzAA1 +roKRNOn08P3SZ0jCUA0O+Ke5sycV6QPoLk5qQnL5eEjOlvd+6U128DAf1DW5O6/8 +C0YsTMOY/sQxXliSt/iygaJVoo7Tt1L60ctNY4KX5EiWs2AgvvTu89ESwbhvvfvw +Xt7PNfa0eBSgC+Lrg5y2Lahs8DijWkTbxo3ebgsCgYEAmyLjzGUnRZnDXsUHE85H +sQGTpid8/rjAT26a82A34O4QG0N1Z0aaqycjpBDYQxusO8Y46XwHPhdAoc0yC2UA +nsntJGjQuoYLQzdTcpyHQiGtUsoAsrst4KvTzriOoOMiS1kqiTAKz60lgkVQOcYT +2pBiut2sbEV8BCokuXI9jZUCgYBbg9yRJNGvQyU21ycEXoeNEc6djeColegmWDJY +U6Unmhx/8Wl8IBs23iF1LqGYuWEXfaM8C4bkCPshjzH2eRWYomCx9vkjq58epoMO +in11Hqi1KDsyzPTjtU1c43Xeoba/K75xUNL0CnB7TgVeT7YHnM29PclhGodtf2Z4 +dDxMcQKBgQC46rtyosn/2uR+88DCWSVTnc4cA6tOEB8WP7Z7mk8yOv1CGgyP4187 +iDPL+91O4C5sT02MFuwElCmxuO1RQQhVTGzIxPd9RQ7t+l1PorTeodYF/ezrRl47 +xuxp4nF50ThlTf1AuhQxpPC1JsXqkH3d582ZLErzrgijMYxk5sYJRA== +-----END RSA PRIVATE KEY----- diff --git a/config/confighttp/testdata/server.crt b/config/confighttp/testdata/server.crt new file mode 100644 index 000000000000..cc48c740762b --- /dev/null +++ b/config/confighttp/testdata/server.crt @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDVTCCAj2gAwIBAgIJANt5fkUlfxyNMA0GCSqGSIb3DQEBCwUAMF0xCzAJBgNV +BAYTAkFVMRIwEAYDVQQIDAlBdXN0cmFsaWExDzANBgNVBAcMBlN5ZG5leTESMBAG +A1UECgwJTXlPcmdOYW1lMRUwEwYDVQQDDAxNeUNvbW1vbk5hbWUwHhcNMjIwODAz +MDQxMDE3WhcNMzIwNzMxMDQxMDE3WjBdMQswCQYDVQQGEwJBVTESMBAGA1UECAwJ +QXVzdHJhbGlhMQ8wDQYDVQQHDAZTeWRuZXkxEjAQBgNVBAoMCU15T3JnTmFtZTEV +MBMGA1UEAwwMTXlDb21tb25OYW1lMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB +CgKCAQEAupEXxNIX+2k0PC/yLfuWSbFr22eLwYSYkPDydFENKYID905hcWqaqr4o +u1Fu8nofbeKrpTgmx7lwXB5VIkdNcnwLyt40nPETGN2m0vBa7Bm6z/KJTW0iHmTz +5fVzt9rggnRTRSovNy43weKoZXHcl/lmGbGy+lqQ69jJTT9yXipkMTIkscYgSHK3 +GVxUhrsBc/mZyBLSj3Tpt3Az71N4npTp6XYbgx4MhllbH9xFlw2hLs1DU+vwJhSE +w99UrLhRz8L3ePmShdkySD9QZxCKD5euiumVfPLfzWV2JmLejC3MD3sd5ql+5itv +WrnYPcky3OclXcnZgDo+ZloWLYPWhQIDAQABoxgwFjAUBgNVHREEDTALgglsb2Nh +bGhvc3QwDQYJKoZIhvcNAQELBQADggEBAJGfixdI6T6dxb37fJPNa0Uerq5aZPb9 +GLJg3CqkZ4yjfsveZ91lH7fRPRjXld3aMb6978kpoczGYJeAdY8j0BPvA+UfsuYO +bzm6HUgiTjyCnLPQIuAlzuYDsBQhzz6gdRclgnuQ8JO/xTDIxVLJDuueannqEn9x +s/RQ5PeWf0D+zvSu4p8UENKfaPqoI5q7nb93IHL38PMDfB7n4oEDIkG0zfxX/zYI +h5ZhzlWSscfCRE4Hdcb8RcSuzwI2JzidMY/ZXUr20iNuCSbv4zhsoDASk7Ud+yj4 +d4JkdOA4NbaT8/aR+Ectc2ItBc2lb0xzEqQnjg3zRQjEzeryHN0hxWU= +-----END CERTIFICATE----- diff --git a/config/confighttp/testdata/server.key b/config/confighttp/testdata/server.key new file mode 100644 index 000000000000..16818a165995 --- /dev/null +++ b/config/confighttp/testdata/server.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEAupEXxNIX+2k0PC/yLfuWSbFr22eLwYSYkPDydFENKYID905h +cWqaqr4ou1Fu8nofbeKrpTgmx7lwXB5VIkdNcnwLyt40nPETGN2m0vBa7Bm6z/KJ +TW0iHmTz5fVzt9rggnRTRSovNy43weKoZXHcl/lmGbGy+lqQ69jJTT9yXipkMTIk +scYgSHK3GVxUhrsBc/mZyBLSj3Tpt3Az71N4npTp6XYbgx4MhllbH9xFlw2hLs1D +U+vwJhSEw99UrLhRz8L3ePmShdkySD9QZxCKD5euiumVfPLfzWV2JmLejC3MD3sd +5ql+5itvWrnYPcky3OclXcnZgDo+ZloWLYPWhQIDAQABAoIBAF8u/01vUsT126yJ +WamUHgzi9AAwR+EnYR8xjsFBSNHQf22BE73lgZtzARzwYwZawAY0CxZ0G3TyaxzU +bOLcNese1nVeAMHBTNj23NHpxrmGNwU43EwgTbPsFXNRUwSOKtTjvEghSY2Bivjk +Rr3a5YyztR+OxZ1s71skcy9yG0tmveGRQS2Gn3SrLgrX7PjgUULpd7TNkWlAyTB7 +sFvVeHVEv8AcFDvHCBvlNJvMG2WT7kD4vjbRc0V1y89D4wO3kNVumajNjiSUopsU +zFe1vegcSLIgf1i2QEtXLob6mrQJplDA3G8oQX4FF0Oovk/bLwzC6IHNeFOo5eFM +TfGzd9ECgYEA6Ixx+2Bai2zdSz19NVaQEksDVdT/ivorgYGpIllLOwjU3ZCIEs2J +iNNPlIX9uFcwIGcfoHsU66iUWrwprfnbloezq5aPdHuhW2PE+JUEBqzmQDy+ax/i +HI2IaD42+OJY5RdRfS7cvczIFrW9XECCTkhr08CxZTO7W7VCP0OnpPMCgYEAzWGQ +hHgpyj5kfco/KlRrK6bQBkhbqWkbkoZohVL6hJ2UxC/NBFS3fWYQ2LFyFPBdu+lI +RXkDxroCQJfWGNAe4PlbKcWh5Q9Ps/s8wl0DIR/KM2yhzE2GqfDC6pQ3xFpizbcm +jl+AL2U6Y9Et1nvPjZKo6STf5yrEdIuHgaq61KcCgYEA4g/tmf2/53vr4AGlXx2I +LpBHbMADrzmk41+FaMO/M2NRcxXWgdjW03EAEpTy4am4Ojelch9UZgZaOZ5jMiIL +Slke2zYgvI6WfD4Ps8tAv7CCoH2sanzzFOitaxDX5bg7zHCPog7VPZj+Bb2kmDKJ +ucoDMDVI/eV9RBh/jvqY1OsCgYEAx5iYxVSucGFgcis6Zd3y5VJRermZczO12xmK +vH9e/cDTUjKOUTYvuMuXdbBFiXnr7nIRjYrFE72z8KhfJnAkgklzwk3SP3U45VY1 +v0J7hxaJAJ8DQzTYuZFFLIptBAM/YGMtMlI3llgPffBNVtOuawzr4OC4RMV4dTcg +svCEb6MCgYEAnRx87p5bAgDVug3pdFI7qRi5Tgxa25c5GsFwhmmr3EgtxKQKLFIH +G6KOWJBpFePVrz3PH71d+J1mB/g8GqzpHBuYTBDD3hyz8iKHN+pmoM4m2/tTqKRY +26XUGOUIxnksgsw2O2R9AASrEm4hhAg5AxmANgqOIZijIcHWF8q9QpE= +-----END RSA PRIVATE KEY----- From 24f2df8b4ce47982c8093b11df6a89d143581fdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juraci=20Paix=C3=A3o=20Kr=C3=B6hling?= Date: Wed, 5 Jun 2024 15:40:14 +0200 Subject: [PATCH 2/4] [configgrpc] Use own compressors for zstd (#10323) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Uses our own version of the zstd compressor for gRPC servers. The code for it is based on the gzip compressor that comes built-in with gRPC. Benchmarks before this PR: ``` Running tool: /usr/bin/go test -benchmem -run=^$ -bench ^BenchmarkCompressors$ go.opentelemetry.io/collector/config/configgrpc sm_log_requestgoos: linux goarch: amd64 pkg: go.opentelemetry.io/collector/config/configgrpc cpu: 11th Gen Intel(R) Core(TM) i7-11800H @ 2.30GHz BenchmarkCompressors/sm_log_request/raw_bytes_160/compressed_bytes_162/compressor_gzip-16 71594 19066 ns/op 615 B/op 4 allocs/op sm_log_requestBenchmarkCompressors/sm_log_request/raw_bytes_160/compressed_bytes_159/compressor_zstd-16 151503 8544 ns/op 640 B/op 6 allocs/op sm_log_requestBenchmarkCompressors/sm_log_request/raw_bytes_160/compressed_bytes_178/compressor_snappy-16 3632570 303.8 ns/op 304 B/op 3 allocs/op md_log_requestBenchmarkCompressors/md_log_request/raw_bytes_242/compressed_bytes_219/compressor_gzip-16 68114 16938 ns/op 748 B/op 4 allocs/op md_log_requestBenchmarkCompressors/md_log_request/raw_bytes_242/compressed_bytes_209/compressor_zstd-16 138091 8047 ns/op 896 B/op 6 allocs/op md_log_requestBenchmarkCompressors/md_log_request/raw_bytes_242/compressed_bytes_260/compressor_snappy-16 3081198 402.5 ns/op 400 B/op 3 allocs/op lg_log_requestBenchmarkCompressors/lg_log_request/raw_bytes_4850/compressed_bytes_253/compressor_gzip-16 43414 27174 ns/op 386 B/op 3 allocs/op lg_log_requestBenchmarkCompressors/lg_log_request/raw_bytes_4850/compressed_bytes_216/compressor_zstd-16 117534 9903 ns/op 10112 B/op 6 allocs/op lg_log_requestBenchmarkCompressors/lg_log_request/raw_bytes_4850/compressed_bytes_454/compressor_snappy-16 1000000 1190 ns/op 528 B/op 2 allocs/op sm_trace_requestBenchmarkCompressors/sm_trace_request/raw_bytes_231/compressed_bytes_203/compressor_gzip-16 67275 17508 ns/op 700 B/op 4 allocs/op sm_trace_requestBenchmarkCompressors/sm_trace_request/raw_bytes_231/compressed_bytes_201/compressor_zstd-16 196862 6137 ns/op 848 B/op 6 allocs/op sm_trace_requestBenchmarkCompressors/sm_trace_request/raw_bytes_231/compressed_bytes_220/compressor_snappy-16 3595815 331.7 ns/op 272 B/op 2 allocs/op md_trace_requestBenchmarkCompressors/md_trace_request/raw_bytes_329/compressed_bytes_249/compressor_gzip-16 64105 19104 ns/op 844 B/op 4 allocs/op md_trace_requestBenchmarkCompressors/md_trace_request/raw_bytes_329/compressed_bytes_256/compressor_zstd-16 169221 6929 ns/op 1120 B/op 6 allocs/op md_trace_requestBenchmarkCompressors/md_trace_request/raw_bytes_329/compressed_bytes_279/compressor_snappy-16 2602239 473.0 ns/op 336 B/op 2 allocs/op lg_trace_requestBenchmarkCompressors/lg_trace_request/raw_bytes_7025/compressed_bytes_303/compressor_gzip-16 33861 36473 ns/op 904 B/op 4 allocs/op lg_trace_requestBenchmarkCompressors/lg_trace_request/raw_bytes_7025/compressed_bytes_258/compressor_zstd-16 107828 10596 ns/op 16832 B/op 6 allocs/op lg_trace_requestBenchmarkCompressors/lg_trace_request/raw_bytes_7025/compressed_bytes_591/compressor_snappy-16 725080 1540 ns/op 689 B/op 2 allocs/op sm_metric_requestBenchmarkCompressors/sm_metric_request/raw_bytes_183/compressed_bytes_140/compressor_gzip-16 76315 16394 ns/op 496 B/op 4 allocs/op sm_metric_requestBenchmarkCompressors/sm_metric_request/raw_bytes_183/compressed_bytes_137/compressor_zstd-16 193314 5957 ns/op 688 B/op 6 allocs/op sm_metric_requestBenchmarkCompressors/sm_metric_request/raw_bytes_183/compressed_bytes_152/compressor_snappy-16 3558649 345.2 ns/op 208 B/op 2 allocs/op md_metric_requestBenchmarkCompressors/md_metric_request/raw_bytes_376/compressed_bytes_194/compressor_gzip-16 68497 18413 ns/op 699 B/op 4 allocs/op md_metric_requestBenchmarkCompressors/md_metric_request/raw_bytes_376/compressed_bytes_198/compressor_zstd-16 177841 6520 ns/op 1136 B/op 6 allocs/op md_metric_requestBenchmarkCompressors/md_metric_request/raw_bytes_376/compressed_bytes_222/compressor_snappy-16 2354102 497.4 ns/op 272 B/op 2 allocs/op lg_metric_requestBenchmarkCompressors/lg_metric_request/raw_bytes_10991/compressed_bytes_601/compressor_gzip-16 21943 54603 ns/op 1941 B/op 5 allocs/op lg_metric_requestBenchmarkCompressors/lg_metric_request/raw_bytes_10991/compressed_bytes_559/compressor_zstd-16 71260 16077 ns/op 25312 B/op 6 allocs/op lg_metric_requestBenchmarkCompressors/lg_metric_request/raw_bytes_10991/compressed_bytes_1055/compressor_snappy-16 335415 3026 ns/op 1200 B/op 2 allocs/op PASS ok go.opentelemetry.io/collector/config/configgrpc 37.766s ``` After this version: ``` Running tool: /usr/bin/go test -benchmem -run=^$ -bench ^BenchmarkCompressors$ go.opentelemetry.io/collector/config/configgrpc sm_log_requestgoos: linux goarch: amd64 pkg: go.opentelemetry.io/collector/config/configgrpc cpu: 11th Gen Intel(R) Core(TM) i7-11800H @ 2.30GHz BenchmarkCompressors/sm_log_request/raw_bytes_160/compressed_bytes_162/compressor_gzip-16 74952 15710 ns/op 603 B/op 4 allocs/op sm_log_requestBenchmarkCompressors/sm_log_request/raw_bytes_160/compressed_bytes_159/compressor_zstd-16 156784 6966 ns/op 208 B/op 2 allocs/op sm_log_requestBenchmarkCompressors/sm_log_request/raw_bytes_160/compressed_bytes_178/compressor_snappy-16 2216174 510.4 ns/op 308 B/op 3 allocs/op md_log_requestBenchmarkCompressors/md_log_request/raw_bytes_242/compressed_bytes_219/compressor_gzip-16 68095 18569 ns/op 736 B/op 4 allocs/op md_log_requestBenchmarkCompressors/md_log_request/raw_bytes_242/compressed_bytes_209/compressor_zstd-16 150705 8849 ns/op 294 B/op 2 allocs/op md_log_requestBenchmarkCompressors/md_log_request/raw_bytes_242/compressed_bytes_260/compressor_snappy-16 2149710 556.8 ns/op 406 B/op 3 allocs/op lg_log_requestBenchmarkCompressors/lg_log_request/raw_bytes_4850/compressed_bytes_253/compressor_gzip-16 40040 26159 ns/op 368 B/op 3 allocs/op lg_log_requestBenchmarkCompressors/lg_log_request/raw_bytes_4850/compressed_bytes_216/compressor_zstd-16 123043 10254 ns/op 299 B/op 2 allocs/op lg_log_requestBenchmarkCompressors/lg_log_request/raw_bytes_4850/compressed_bytes_454/compressor_snappy-16 726780 1457 ns/op 533 B/op 2 allocs/op sm_trace_requestBenchmarkCompressors/sm_trace_request/raw_bytes_231/compressed_bytes_203/compressor_gzip-16 64660 18186 ns/op 701 B/op 4 allocs/op sm_trace_requestBenchmarkCompressors/sm_trace_request/raw_bytes_231/compressed_bytes_201/compressor_zstd-16 193225 6267 ns/op 273 B/op 2 allocs/op sm_trace_requestBenchmarkCompressors/sm_trace_request/raw_bytes_231/compressed_bytes_220/compressor_snappy-16 2925073 418.2 ns/op 276 B/op 2 allocs/op md_trace_requestBenchmarkCompressors/md_trace_request/raw_bytes_329/compressed_bytes_249/compressor_gzip-16 61320 20641 ns/op 846 B/op 4 allocs/op md_trace_requestBenchmarkCompressors/md_trace_request/raw_bytes_329/compressed_bytes_256/compressor_zstd-16 190965 6440 ns/op 321 B/op 2 allocs/op md_trace_requestBenchmarkCompressors/md_trace_request/raw_bytes_329/compressed_bytes_279/compressor_snappy-16 2051575 656.8 ns/op 341 B/op 2 allocs/op lg_trace_requestBenchmarkCompressors/lg_trace_request/raw_bytes_7025/compressed_bytes_303/compressor_gzip-16 30097 40680 ns/op 907 B/op 4 allocs/op lg_trace_requestBenchmarkCompressors/lg_trace_request/raw_bytes_7025/compressed_bytes_258/compressor_zstd-16 127027 8437 ns/op 363 B/op 2 allocs/op lg_trace_requestBenchmarkCompressors/lg_trace_request/raw_bytes_7025/compressed_bytes_591/compressor_snappy-16 716541 1803 ns/op 694 B/op 2 allocs/op sm_metric_requestBenchmarkCompressors/sm_metric_request/raw_bytes_183/compressed_bytes_140/compressor_gzip-16 82287 15054 ns/op 496 B/op 4 allocs/op sm_metric_requestBenchmarkCompressors/sm_metric_request/raw_bytes_183/compressed_bytes_137/compressor_zstd-16 230558 5470 ns/op 221 B/op 2 allocs/op sm_metric_requestBenchmarkCompressors/sm_metric_request/raw_bytes_183/compressed_bytes_152/compressor_snappy-16 2759403 417.1 ns/op 211 B/op 2 allocs/op md_metric_requestBenchmarkCompressors/md_metric_request/raw_bytes_376/compressed_bytes_194/compressor_gzip-16 58208 18925 ns/op 702 B/op 4 allocs/op md_metric_requestBenchmarkCompressors/md_metric_request/raw_bytes_376/compressed_bytes_198/compressor_zstd-16 199226 6247 ns/op 256 B/op 2 allocs/op md_metric_requestBenchmarkCompressors/md_metric_request/raw_bytes_376/compressed_bytes_222/compressor_snappy-16 2065202 609.8 ns/op 276 B/op 2 allocs/op lg_metric_requestBenchmarkCompressors/lg_metric_request/raw_bytes_10991/compressed_bytes_601/compressor_gzip-16 20583 59762 ns/op 1945 B/op 5 allocs/op lg_metric_requestBenchmarkCompressors/lg_metric_request/raw_bytes_10991/compressed_bytes_559/compressor_zstd-16 98254 13152 ns/op 728 B/op 2 allocs/op lg_metric_requestBenchmarkCompressors/lg_metric_request/raw_bytes_10991/compressed_bytes_1055/compressor_snappy-16 389401 3976 ns/op 1209 B/op 2 allocs/op PASS ok go.opentelemetry.io/collector/config/configgrpc 40.394s ``` Signed-off-by: Juraci Paixão Kröhling --------- Signed-off-by: Juraci Paixão Kröhling --- ...nfiggrpc-use-own-compressors-for-zstd.yaml | 13 + config/configgrpc/configgrpc.go | 4 +- .../configgrpc/configgrpc_benchmark_test.go | 4 +- config/configgrpc/go.mod | 2 +- config/configgrpc/internal/zstd.go | 83 ++ config/configgrpc/internal/zstd_test.go | 41 + receiver/otlpreceiver/otlp_test.go | 1195 +++++++++++++++++ 7 files changed, 1337 insertions(+), 5 deletions(-) create mode 100644 .chloggen/jpkroehling-configgrpc-use-own-compressors-for-zstd.yaml create mode 100644 config/configgrpc/internal/zstd.go create mode 100644 config/configgrpc/internal/zstd_test.go create mode 100644 receiver/otlpreceiver/otlp_test.go diff --git a/.chloggen/jpkroehling-configgrpc-use-own-compressors-for-zstd.yaml b/.chloggen/jpkroehling-configgrpc-use-own-compressors-for-zstd.yaml new file mode 100644 index 000000000000..a04c4f89012a --- /dev/null +++ b/.chloggen/jpkroehling-configgrpc-use-own-compressors-for-zstd.yaml @@ -0,0 +1,13 @@ +# Use this changelog template to create an entry for release notes. + +# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix' +change_type: 'bug_fix' + +# The name of the component, or a single word describing the area of concern, (e.g. otlpreceiver) +component: Use own compressors for zstd + +# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`). +note: Before this change, the zstd compressor we used didn't respect the max message size. + +# One or more tracking issues or pull requests related to the change +issues: [10323] diff --git a/config/configgrpc/configgrpc.go b/config/configgrpc/configgrpc.go index fa1f03d0b6d2..f3dcae962ffd 100644 --- a/config/configgrpc/configgrpc.go +++ b/config/configgrpc/configgrpc.go @@ -12,7 +12,6 @@ import ( "time" "github.com/mostynb/go-grpc-compression/nonclobbering/snappy" - "github.com/mostynb/go-grpc-compression/nonclobbering/zstd" "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" "go.opentelemetry.io/otel" "google.golang.org/grpc" @@ -28,6 +27,7 @@ import ( "go.opentelemetry.io/collector/component" "go.opentelemetry.io/collector/config/configauth" "go.opentelemetry.io/collector/config/configcompression" + grpcInternal "go.opentelemetry.io/collector/config/configgrpc/internal" "go.opentelemetry.io/collector/config/confignet" "go.opentelemetry.io/collector/config/configopaque" "go.opentelemetry.io/collector/config/configtls" @@ -378,7 +378,7 @@ func getGRPCCompressionName(compressionType configcompression.Type) (string, err case configcompression.TypeSnappy: return snappy.Name, nil case configcompression.TypeZstd: - return zstd.Name, nil + return grpcInternal.ZstdName, nil default: return "", fmt.Errorf("unsupported compression type %q", compressionType) } diff --git a/config/configgrpc/configgrpc_benchmark_test.go b/config/configgrpc/configgrpc_benchmark_test.go index 1ad755f2b4f6..3254655e9ec1 100644 --- a/config/configgrpc/configgrpc_benchmark_test.go +++ b/config/configgrpc/configgrpc_benchmark_test.go @@ -10,12 +10,12 @@ import ( "testing" "github.com/mostynb/go-grpc-compression/nonclobbering/snappy" - "github.com/mostynb/go-grpc-compression/nonclobbering/zstd" "google.golang.org/grpc/codes" "google.golang.org/grpc/encoding" "google.golang.org/grpc/encoding/gzip" "google.golang.org/grpc/status" + "go.opentelemetry.io/collector/config/configgrpc/internal" "go.opentelemetry.io/collector/pdata/plog" "go.opentelemetry.io/collector/pdata/pmetric" "go.opentelemetry.io/collector/pdata/ptrace" @@ -27,7 +27,7 @@ func BenchmarkCompressors(b *testing.B) { compressors := make([]encoding.Compressor, 0) compressors = append(compressors, encoding.GetCompressor(gzip.Name)) - compressors = append(compressors, encoding.GetCompressor(zstd.Name)) + compressors = append(compressors, encoding.GetCompressor(internal.ZstdName)) compressors = append(compressors, encoding.GetCompressor(snappy.Name)) for _, payload := range payloads { diff --git a/config/configgrpc/go.mod b/config/configgrpc/go.mod index ef56d9560a86..d6a0573f42e3 100644 --- a/config/configgrpc/go.mod +++ b/config/configgrpc/go.mod @@ -3,6 +3,7 @@ module go.opentelemetry.io/collector/config/configgrpc go 1.21 require ( + github.com/klauspost/compress v1.17.2 github.com/mostynb/go-grpc-compression v1.2.2 github.com/stretchr/testify v1.9.0 go.opentelemetry.io/collector v0.98.0 @@ -36,7 +37,6 @@ require ( github.com/golang/snappy v0.0.4 // indirect github.com/hashicorp/go-version v1.6.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/compress v1.17.2 // indirect github.com/knadh/koanf/maps v0.1.1 // indirect github.com/knadh/koanf/providers/confmap v0.1.0 // indirect github.com/knadh/koanf/v2 v2.1.1 // indirect diff --git a/config/configgrpc/internal/zstd.go b/config/configgrpc/internal/zstd.go new file mode 100644 index 000000000000..0718b73535fb --- /dev/null +++ b/config/configgrpc/internal/zstd.go @@ -0,0 +1,83 @@ +// Copyright The OpenTelemetry Authors +// Copyright 2017 gRPC authors +// SPDX-License-Identifier: Apache-2.0 + +package internal // import "go.opentelemetry.io/collector/config/configgrpc/internal" + +import ( + "errors" + "io" + "sync" + + "github.com/klauspost/compress/zstd" + "google.golang.org/grpc/encoding" +) + +const ZstdName = "zstd" + +func init() { + encoding.RegisterCompressor(NewZstdCodec()) +} + +type writer struct { + *zstd.Encoder + pool *sync.Pool +} + +func NewZstdCodec() encoding.Compressor { + c := &compressor{} + c.poolCompressor.New = func() any { + zw, _ := zstd.NewWriter(nil, zstd.WithEncoderConcurrency(1), zstd.WithWindowSize(512*1024)) + return &writer{Encoder: zw, pool: &c.poolCompressor} + } + return c +} + +func (c *compressor) Compress(w io.Writer) (io.WriteCloser, error) { + z := c.poolCompressor.Get().(*writer) + z.Encoder.Reset(w) + return z, nil +} + +func (z *writer) Close() error { + defer z.pool.Put(z) + return z.Encoder.Close() +} + +type reader struct { + *zstd.Decoder + pool *sync.Pool +} + +func (c *compressor) Decompress(r io.Reader) (io.Reader, error) { + z, inPool := c.poolDecompressor.Get().(*reader) + if !inPool { + newZ, err := zstd.NewReader(r) + if err != nil { + return nil, err + } + return &reader{Decoder: newZ, pool: &c.poolDecompressor}, nil + } + if err := z.Reset(r); err != nil { + c.poolDecompressor.Put(z) + return nil, err + } + return z, nil +} + +func (z *reader) Read(p []byte) (n int, err error) { + n, err = z.Decoder.Read(p) + if errors.Is(err, io.EOF) { + z.pool.Put(z) + } + return n, err +} + +func (c *compressor) Name() string { + return ZstdName +} + +type compressor struct { + poolCompressor sync.Pool + poolDecompressor sync.Pool +} diff --git a/config/configgrpc/internal/zstd_test.go b/config/configgrpc/internal/zstd_test.go new file mode 100644 index 000000000000..e16336c8ccbe --- /dev/null +++ b/config/configgrpc/internal/zstd_test.go @@ -0,0 +1,41 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package internal + +import ( + "bytes" + "io" + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_zstdCodec_CompressDecompress(t *testing.T) { + // prepare + msg := []byte("Hello world.") + compressed := &bytes.Buffer{} + + // zstd header, for sanity checking + header := []byte{40, 181, 47, 253} + + c := NewZstdCodec() + cWriter, err := c.Compress(compressed) + require.NoError(t, err) + require.NotNil(t, cWriter) + + _, err = cWriter.Write(msg) + require.NoError(t, err) + cWriter.Close() + + cReader, err := c.Decompress(compressed) + require.NoError(t, err) + require.NotNil(t, cReader) + + uncompressed, err := io.ReadAll(cReader) + require.NoError(t, err) + require.Equal(t, msg, uncompressed) + + // test header + require.Equal(t, header, compressed.Bytes()[:4]) +} diff --git a/receiver/otlpreceiver/otlp_test.go b/receiver/otlpreceiver/otlp_test.go new file mode 100644 index 000000000000..51f99bb4cb8f --- /dev/null +++ b/receiver/otlpreceiver/otlp_test.go @@ -0,0 +1,1195 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package otlpreceiver + +import ( + "bytes" + "compress/gzip" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net" + "net/http" + "strings" + "sync" + "testing" + "time" + + "github.com/klauspost/compress/zstd" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + spb "google.golang.org/genproto/googleapis/rpc/status" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/proto" + + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/component/componenttest" + "go.opentelemetry.io/collector/config/configgrpc" + "go.opentelemetry.io/collector/config/confighttp" + "go.opentelemetry.io/collector/config/confignet" + "go.opentelemetry.io/collector/config/configtls" + "go.opentelemetry.io/collector/consumer" + "go.opentelemetry.io/collector/consumer/consumererror" + "go.opentelemetry.io/collector/consumer/consumertest" + "go.opentelemetry.io/collector/internal/testutil" + "go.opentelemetry.io/collector/pdata/plog" + "go.opentelemetry.io/collector/pdata/pmetric" + "go.opentelemetry.io/collector/pdata/ptrace" + "go.opentelemetry.io/collector/pdata/ptrace/ptraceotlp" + "go.opentelemetry.io/collector/pdata/testdata" + "go.opentelemetry.io/collector/receiver/receivertest" +) + +const otlpReceiverName = "receiver_test" + +var otlpReceiverID = component.MustNewIDWithName("otlp", otlpReceiverName) + +func TestJsonHttp(t *testing.T) { + tests := []struct { + name string + encoding string + contentType string + err error + expectedStatus *spb.Status + expectedStatusCode int + }{ + { + name: "JSONUncompressed", + encoding: "", + contentType: "application/json", + }, + { + name: "JSONUncompressedUTF8", + encoding: "", + contentType: "application/json; charset=utf-8", + }, + { + name: "JSONUncompressedUppercase", + encoding: "", + contentType: "APPLICATION/JSON", + }, + { + name: "JSONGzipCompressed", + encoding: "gzip", + contentType: "application/json", + }, + { + name: "JSONZstdCompressed", + encoding: "zstd", + contentType: "application/json", + }, + { + name: "Permanent NotGRPCError", + encoding: "", + contentType: "application/json", + err: consumererror.NewPermanent(errors.New("my error")), + expectedStatus: &spb.Status{Code: int32(codes.Internal), Message: "Permanent error: my error"}, + expectedStatusCode: http.StatusInternalServerError, + }, + { + name: "Retryable NotGRPCError", + encoding: "", + contentType: "application/json", + err: errors.New("my error"), + expectedStatus: &spb.Status{Code: int32(codes.Unavailable), Message: "my error"}, + expectedStatusCode: http.StatusServiceUnavailable, + }, + { + name: "Permanent GRPCError", + encoding: "", + contentType: "application/json", + err: status.New(codes.InvalidArgument, "").Err(), + expectedStatus: &spb.Status{Code: int32(codes.InvalidArgument), Message: ""}, + expectedStatusCode: http.StatusInternalServerError, + }, + { + name: "Retryable GRPCError", + encoding: "", + contentType: "application/json", + err: status.New(codes.Unavailable, "").Err(), + expectedStatus: &spb.Status{Code: int32(codes.Unavailable), Message: ""}, + expectedStatusCode: http.StatusServiceUnavailable, + }, + } + addr := testutil.GetAvailableLocalAddress(t) + sink := newErrOrSinkConsumer() + recv := newHTTPReceiver(t, componenttest.NewNopTelemetrySettings(), addr, sink) + require.NoError(t, recv.Start(context.Background(), componenttest.NewNopHost()), "Failed to start trace receiver") + t.Cleanup(func() { require.NoError(t, recv.Shutdown(context.Background())) }) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sink.Reset() + sink.SetConsumeError(tt.err) + + for _, dr := range generateDataRequests(t) { + url := "http://" + addr + dr.path + respBytes := doHTTPRequest(t, url, tt.encoding, tt.contentType, dr.jsonBytes, tt.expectedStatusCode) + if tt.err == nil { + tr := ptraceotlp.NewExportResponse() + assert.NoError(t, tr.UnmarshalJSON(respBytes), "Unable to unmarshal response to Response") + sink.checkData(t, dr.data, 1) + } else { + errStatus := &spb.Status{} + assert.NoError(t, json.Unmarshal(respBytes, errStatus)) + if s, ok := status.FromError(tt.err); ok { + assert.True(t, proto.Equal(errStatus, s.Proto())) + } else { + fmt.Println(errStatus) + assert.True(t, proto.Equal(errStatus, tt.expectedStatus)) + } + sink.checkData(t, dr.data, 0) + } + } + }) + } +} + +func TestHandleInvalidRequests(t *testing.T) { + addr := testutil.GetAvailableLocalAddress(t) + sink := newErrOrSinkConsumer() + recv := newHTTPReceiver(t, componenttest.NewNopTelemetrySettings(), addr, sink) + require.NoError(t, recv.Start(context.Background(), componenttest.NewNopHost()), "Failed to start trace receiver") + t.Cleanup(func() { require.NoError(t, recv.Shutdown(context.Background())) }) + + tests := []struct { + name string + uri string + method string + contentType string + + expectedStatus int + expectedResponseBody string + }{ + { + name: "no content type", + uri: defaultTracesURLPath, + method: http.MethodPost, + contentType: "", + + expectedStatus: http.StatusUnsupportedMediaType, + expectedResponseBody: "415 unsupported media type, supported: [application/json, application/x-protobuf]", + }, + { + name: "invalid content type", + uri: defaultTracesURLPath, + method: http.MethodPost, + contentType: "invalid", + + expectedStatus: http.StatusUnsupportedMediaType, + expectedResponseBody: "415 unsupported media type, supported: [application/json, application/x-protobuf]", + }, + { + name: "invalid request", + uri: defaultTracesURLPath, + method: http.MethodPost, + contentType: "application/json", + + expectedStatus: http.StatusBadRequest, + }, + { + uri: defaultTracesURLPath, + method: http.MethodPatch, + contentType: "application/json", + + expectedStatus: http.StatusMethodNotAllowed, + expectedResponseBody: "405 method not allowed, supported: [POST]", + }, + { + uri: defaultTracesURLPath, + method: http.MethodGet, + contentType: "application/json", + + expectedStatus: http.StatusMethodNotAllowed, + expectedResponseBody: "405 method not allowed, supported: [POST]", + }, + { + name: "no content type", + uri: defaultMetricsURLPath, + method: http.MethodPost, + contentType: "", + + expectedStatus: http.StatusUnsupportedMediaType, + expectedResponseBody: "415 unsupported media type, supported: [application/json, application/x-protobuf]", + }, + { + name: "invalid content type", + uri: defaultMetricsURLPath, + method: http.MethodPost, + contentType: "invalid", + + expectedStatus: http.StatusUnsupportedMediaType, + expectedResponseBody: "415 unsupported media type, supported: [application/json, application/x-protobuf]", + }, + { + name: "invalid request", + uri: defaultMetricsURLPath, + method: http.MethodPost, + contentType: "application/json", + + expectedStatus: http.StatusBadRequest, + }, + { + uri: defaultMetricsURLPath, + method: http.MethodPatch, + contentType: "application/json", + + expectedStatus: http.StatusMethodNotAllowed, + expectedResponseBody: "405 method not allowed, supported: [POST]", + }, + { + uri: defaultMetricsURLPath, + method: http.MethodGet, + contentType: "application/json", + + expectedStatus: http.StatusMethodNotAllowed, + expectedResponseBody: "405 method not allowed, supported: [POST]", + }, + { + name: "no content type", + uri: defaultLogsURLPath, + method: http.MethodPost, + contentType: "", + + expectedStatus: http.StatusUnsupportedMediaType, + expectedResponseBody: "415 unsupported media type, supported: [application/json, application/x-protobuf]", + }, + { + name: "invalid content type", + uri: defaultLogsURLPath, + method: http.MethodPost, + contentType: "invalid", + + expectedStatus: http.StatusUnsupportedMediaType, + expectedResponseBody: "415 unsupported media type, supported: [application/json, application/x-protobuf]", + }, + { + name: "invalid request", + uri: defaultLogsURLPath, + method: http.MethodPost, + contentType: "application/json", + + expectedStatus: http.StatusBadRequest, + }, + { + uri: defaultLogsURLPath, + method: http.MethodPatch, + contentType: "application/json", + + expectedStatus: http.StatusMethodNotAllowed, + expectedResponseBody: "405 method not allowed, supported: [POST]", + }, + { + uri: defaultLogsURLPath, + method: http.MethodGet, + contentType: "application/json", + + expectedStatus: http.StatusMethodNotAllowed, + expectedResponseBody: "405 method not allowed, supported: [POST]", + }, + } + + for _, tt := range tests { + t.Run(tt.method+" "+tt.uri+" "+tt.name, func(t *testing.T) { + url := "http://" + addr + tt.uri + req, err := http.NewRequest(tt.method, url, bytes.NewReader([]byte(`1234`))) + require.NoError(t, err) + req.Header.Set("Content-Type", tt.contentType) + + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + + if tt.name == "invalid request" { + assert.Equal(t, "application/json", resp.Header.Get("Content-Type")) + assert.Equal(t, tt.expectedStatus, resp.StatusCode) + return + } + assert.Equal(t, "text/plain", resp.Header.Get("Content-Type")) + assert.Equal(t, tt.expectedStatus, resp.StatusCode) + assert.Equal(t, tt.expectedResponseBody, string(body)) + }) + } + + require.NoError(t, recv.Shutdown(context.Background())) +} + +func TestProtoHttp(t *testing.T) { + tests := []struct { + name string + encoding string + err error + expectedStatus *spb.Status + expectedStatusCode int + }{ + { + name: "ProtoUncompressed", + encoding: "", + }, + { + name: "ProtoGzipCompressed", + encoding: "gzip", + }, + { + name: "ProtoZstdCompressed", + encoding: "zstd", + }, + { + name: "Permanent NotGRPCError", + encoding: "", + err: consumererror.NewPermanent(errors.New("my error")), + expectedStatus: &spb.Status{Code: int32(codes.Internal), Message: "Permanent error: my error"}, + expectedStatusCode: http.StatusInternalServerError, + }, + { + name: "Retryable NotGRPCError", + encoding: "", + err: errors.New("my error"), + expectedStatus: &spb.Status{Code: int32(codes.Unavailable), Message: "my error"}, + expectedStatusCode: http.StatusServiceUnavailable, + }, + { + name: "Permanent GRPCError", + encoding: "", + err: status.New(codes.InvalidArgument, "").Err(), + expectedStatus: &spb.Status{Code: int32(codes.InvalidArgument), Message: ""}, + expectedStatusCode: http.StatusInternalServerError, + }, + { + name: "Retryable GRPCError", + encoding: "", + err: status.New(codes.Unavailable, "").Err(), + expectedStatus: &spb.Status{Code: int32(codes.Unavailable), Message: ""}, + expectedStatusCode: http.StatusServiceUnavailable, + }, + } + addr := testutil.GetAvailableLocalAddress(t) + + // Set the buffer count to 1 to make it flush the test span immediately. + sink := newErrOrSinkConsumer() + recv := newHTTPReceiver(t, componenttest.NewNopTelemetrySettings(), addr, sink) + + require.NoError(t, recv.Start(context.Background(), componenttest.NewNopHost()), "Failed to start trace receiver") + t.Cleanup(func() { require.NoError(t, recv.Shutdown(context.Background())) }) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sink.Reset() + sink.SetConsumeError(tt.err) + + for _, dr := range generateDataRequests(t) { + url := "http://" + addr + dr.path + respBytes := doHTTPRequest(t, url, tt.encoding, "application/x-protobuf", dr.protoBytes, tt.expectedStatusCode) + if tt.err == nil { + tr := ptraceotlp.NewExportResponse() + assert.NoError(t, tr.UnmarshalProto(respBytes)) + sink.checkData(t, dr.data, 1) + } else { + errStatus := &spb.Status{} + assert.NoError(t, proto.Unmarshal(respBytes, errStatus)) + if s, ok := status.FromError(tt.err); ok { + assert.True(t, proto.Equal(errStatus, s.Proto())) + } else { + assert.True(t, proto.Equal(errStatus, tt.expectedStatus)) + } + sink.checkData(t, dr.data, 0) + } + } + }) + } +} + +func TestOTLPReceiverInvalidContentEncoding(t *testing.T) { + tests := []struct { + name string + content string + encoding string + reqBodyFunc func() (*bytes.Buffer, error) + resBodyFunc func() ([]byte, error) + status int + }{ + { + name: "JsonGzipUncompressed", + content: "application/json", + encoding: "gzip", + reqBodyFunc: func() (*bytes.Buffer, error) { + return bytes.NewBuffer([]byte(`{"key": "value"}`)), nil + }, + resBodyFunc: func() ([]byte, error) { + return json.Marshal(status.New(codes.InvalidArgument, "gzip: invalid header").Proto()) + }, + status: 400, + }, + { + name: "ProtoGzipUncompressed", + content: "application/x-protobuf", + encoding: "gzip", + reqBodyFunc: func() (*bytes.Buffer, error) { + return bytes.NewBuffer([]byte(`{"key": "value"}`)), nil + }, + resBodyFunc: func() ([]byte, error) { + return proto.Marshal(status.New(codes.InvalidArgument, "gzip: invalid header").Proto()) + }, + status: 400, + }, + { + name: "ProtoZstdUncompressed", + content: "application/x-protobuf", + encoding: "zstd", + reqBodyFunc: func() (*bytes.Buffer, error) { + return bytes.NewBuffer([]byte(`{"key": "value"}`)), nil + }, + resBodyFunc: func() ([]byte, error) { + return proto.Marshal(status.New(codes.InvalidArgument, "invalid input: magic number mismatch").Proto()) + }, + status: 400, + }, + } + addr := testutil.GetAvailableLocalAddress(t) + + // Set the buffer count to 1 to make it flush the test span immediately. + recv := newHTTPReceiver(t, componenttest.NewNopTelemetrySettings(), addr, consumertest.NewNop()) + + require.NoError(t, recv.Start(context.Background(), componenttest.NewNopHost()), "Failed to start trace receiver") + t.Cleanup(func() { require.NoError(t, recv.Shutdown(context.Background())) }) + + url := fmt.Sprintf("http://%s%s", addr, defaultTracesURLPath) + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + body, err := test.reqBodyFunc() + require.NoError(t, err, "Error creating request body: %v", err) + + req, err := http.NewRequest(http.MethodPost, url, body) + require.NoError(t, err, "Error creating trace POST request: %v", err) + req.Header.Set("Content-Type", test.content) + req.Header.Set("Content-Encoding", test.encoding) + + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err, "Error posting trace to grpc-gateway server: %v", err) + + respBytes, err := io.ReadAll(resp.Body) + require.NoError(t, err, "Error reading response from trace grpc-gateway") + exRespBytes, err := test.resBodyFunc() + require.NoError(t, err, "Error creating expecting response body") + require.NoError(t, resp.Body.Close(), "Error closing response body") + + require.Equal(t, test.status, resp.StatusCode, "Unexpected return status") + require.Equal(t, test.content, resp.Header.Get("Content-Type"), "Unexpected response Content-Type") + require.Equal(t, exRespBytes, respBytes, "Unexpected response content") + }) + } +} + +func TestGRPCNewPortAlreadyUsed(t *testing.T) { + addr := testutil.GetAvailableLocalAddress(t) + ln, err := net.Listen("tcp", addr) + require.NoError(t, err, "failed to listen on %q: %v", addr, err) + t.Cleanup(func() { + assert.NoError(t, ln.Close()) + }) + + r := newGRPCReceiver(t, componenttest.NewNopTelemetrySettings(), addr, consumertest.NewNop()) + require.NotNil(t, r) + + require.Error(t, r.Start(context.Background(), componenttest.NewNopHost())) +} + +func TestHTTPNewPortAlreadyUsed(t *testing.T) { + addr := testutil.GetAvailableLocalAddress(t) + ln, err := net.Listen("tcp", addr) + require.NoError(t, err, "failed to listen on %q: %v", addr, err) + t.Cleanup(func() { + assert.NoError(t, ln.Close()) + }) + + r := newHTTPReceiver(t, componenttest.NewNopTelemetrySettings(), addr, consumertest.NewNop()) + require.NotNil(t, r) + + require.Error(t, r.Start(context.Background(), componenttest.NewNopHost())) +} + +// TestOTLPReceiverGRPCTracesIngestTest checks that the gRPC trace receiver +// is returning the proper response (return and metrics) when the next consumer +// in the pipeline reports error. The test changes the responses returned by the +// next trace consumer, checks if data was passed down the pipeline and if +// proper metrics were recorded. It also uses all endpoints supported by the +// trace receiver. +func TestOTLPReceiverGRPCTracesIngestTest(t *testing.T) { + type ingestionStateTest struct { + okToIngest bool + permanent bool + expectedCode codes.Code + } + + expectedReceivedBatches := 2 + expectedIngestionBlockedRPCs := 2 + ingestionStates := []ingestionStateTest{ + { + okToIngest: true, + expectedCode: codes.OK, + }, + { + okToIngest: false, + expectedCode: codes.Unavailable, + }, + { + okToIngest: false, + expectedCode: codes.Internal, + permanent: true, + }, + { + okToIngest: true, + expectedCode: codes.OK, + }, + } + + addr := testutil.GetAvailableLocalAddress(t) + td := testdata.GenerateTraces(1) + + tt, err := componenttest.SetupTelemetry(otlpReceiverID) + require.NoError(t, err) + t.Cleanup(func() { require.NoError(t, tt.Shutdown(context.Background())) }) + + sink := &errOrSinkConsumer{TracesSink: new(consumertest.TracesSink)} + + recv := newGRPCReceiver(t, tt.TelemetrySettings(), addr, sink) + require.NotNil(t, recv) + require.NoError(t, recv.Start(context.Background(), componenttest.NewNopHost())) + t.Cleanup(func() { require.NoError(t, recv.Shutdown(context.Background())) }) + + cc, err := grpc.NewClient(addr, grpc.WithTransportCredentials(insecure.NewCredentials())) + require.NoError(t, err) + defer func() { + assert.NoError(t, cc.Close()) + }() + + for _, ingestionState := range ingestionStates { + if ingestionState.okToIngest { + sink.SetConsumeError(nil) + } else { + if ingestionState.permanent { + sink.SetConsumeError(consumererror.NewPermanent(errors.New("consumer error"))) + } else { + sink.SetConsumeError(errors.New("consumer error")) + } + } + + _, err = ptraceotlp.NewGRPCClient(cc).Export(context.Background(), ptraceotlp.NewExportRequestFromTraces(td)) + errStatus, ok := status.FromError(err) + require.True(t, ok) + assert.Equal(t, ingestionState.expectedCode, errStatus.Code()) + } + + require.Equal(t, expectedReceivedBatches, len(sink.AllTraces())) + + require.NoError(t, tt.CheckReceiverTraces("grpc", int64(expectedReceivedBatches), int64(expectedIngestionBlockedRPCs))) +} + +// TestOTLPReceiverHTTPTracesIngestTest checks that the HTTP trace receiver +// is returning the proper response (return and metrics) when the next consumer +// in the pipeline reports error. The test changes the responses returned by the +// next trace consumer, checks if data was passed down the pipeline and if +// proper metrics were recorded. It also uses all endpoints supported by the +// trace receiver. +func TestOTLPReceiverHTTPTracesIngestTest(t *testing.T) { + type ingestionStateTest struct { + okToIngest bool + err error + expectedCode codes.Code + expectedStatusCode int + } + + expectedReceivedBatches := 2 + expectedIngestionBlockedRPCs := 2 + ingestionStates := []ingestionStateTest{ + { + okToIngest: true, + expectedCode: codes.OK, + }, + { + okToIngest: false, + err: consumererror.NewPermanent(errors.New("consumer error")), + expectedCode: codes.Internal, + expectedStatusCode: http.StatusInternalServerError, + }, + { + okToIngest: false, + err: errors.New("consumer error"), + expectedCode: codes.Unavailable, + expectedStatusCode: http.StatusServiceUnavailable, + }, + { + okToIngest: true, + expectedCode: codes.OK, + }, + } + + addr := testutil.GetAvailableLocalAddress(t) + td := testdata.GenerateTraces(1) + + tt, err := componenttest.SetupTelemetry(otlpReceiverID) + require.NoError(t, err) + t.Cleanup(func() { require.NoError(t, tt.Shutdown(context.Background())) }) + + sink := &errOrSinkConsumer{TracesSink: new(consumertest.TracesSink)} + + recv := newHTTPReceiver(t, tt.TelemetrySettings(), addr, sink) + require.NotNil(t, recv) + require.NoError(t, recv.Start(context.Background(), componenttest.NewNopHost())) + t.Cleanup(func() { require.NoError(t, recv.Shutdown(context.Background())) }) + + for _, ingestionState := range ingestionStates { + if ingestionState.okToIngest { + sink.SetConsumeError(nil) + } else { + sink.SetConsumeError(ingestionState.err) + } + + pbMarshaler := ptrace.ProtoMarshaler{} + pbBytes, err := pbMarshaler.MarshalTraces(td) + require.NoError(t, err) + req, err := http.NewRequest(http.MethodPost, "http://"+addr+defaultTracesURLPath, bytes.NewReader(pbBytes)) + require.NoError(t, err) + req.Header.Set("Content-Type", pbContentType) + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + respBytes, err := io.ReadAll(resp.Body) + require.NoError(t, err) + + if ingestionState.expectedCode == codes.OK { + require.Equal(t, 200, resp.StatusCode) + tr := ptraceotlp.NewExportResponse() + assert.NoError(t, tr.UnmarshalProto(respBytes)) + } else { + errStatus := &spb.Status{} + assert.NoError(t, proto.Unmarshal(respBytes, errStatus)) + assert.Equal(t, ingestionState.expectedStatusCode, resp.StatusCode) + assert.Equal(t, ingestionState.expectedCode, codes.Code(errStatus.Code)) + } + } + + require.Equal(t, expectedReceivedBatches, len(sink.AllTraces())) + + require.NoError(t, tt.CheckReceiverTraces("http", int64(expectedReceivedBatches), int64(expectedIngestionBlockedRPCs))) +} + +func TestGRPCInvalidTLSCredentials(t *testing.T) { + cfg := &Config{ + Protocols: Protocols{ + GRPC: &configgrpc.ServerConfig{ + NetAddr: confignet.AddrConfig{ + Endpoint: testutil.GetAvailableLocalAddress(t), + Transport: confignet.TransportTypeTCP, + }, + TLSSetting: &configtls.ServerConfig{ + Config: configtls.Config{ + CertFile: "willfail", + }, + }, + }, + }, + } + + r, err := NewFactory().CreateTracesReceiver( + context.Background(), + receivertest.NewNopCreateSettings(), + cfg, + consumertest.NewNop()) + require.NoError(t, err) + assert.NotNil(t, r) + + assert.EqualError(t, + r.Start(context.Background(), componenttest.NewNopHost()), + `failed to load TLS config: failed to load TLS cert and key: for auth via TLS, provide both certificate and key, or neither`) +} + +func TestGRPCMaxRecvSize(t *testing.T) { + addr := testutil.GetAvailableLocalAddress(t) + sink := newErrOrSinkConsumer() + + cfg := createDefaultConfig().(*Config) + cfg.GRPC.NetAddr.Endpoint = addr + cfg.HTTP = nil + recv := newReceiver(t, componenttest.NewNopTelemetrySettings(), cfg, otlpReceiverID, sink) + require.NoError(t, recv.Start(context.Background(), componenttest.NewNopHost())) + + cc, err := grpc.NewClient(addr, grpc.WithTransportCredentials(insecure.NewCredentials())) + require.NoError(t, err) + + td := testdata.GenerateTraces(50000) + require.Error(t, exportTraces(cc, td)) + assert.NoError(t, cc.Close()) + require.NoError(t, recv.Shutdown(context.Background())) + + cfg.GRPC.MaxRecvMsgSizeMiB = 100 + recv = newReceiver(t, componenttest.NewNopTelemetrySettings(), cfg, otlpReceiverID, sink) + require.NoError(t, recv.Start(context.Background(), componenttest.NewNopHost())) + t.Cleanup(func() { require.NoError(t, recv.Shutdown(context.Background())) }) + + cc, err = grpc.NewClient(addr, grpc.WithTransportCredentials(insecure.NewCredentials())) + require.NoError(t, err) + defer func() { + assert.NoError(t, cc.Close()) + }() + + td = testdata.GenerateTraces(50000) + require.NoError(t, exportTraces(cc, td)) + require.Len(t, sink.AllTraces(), 1) + assert.Equal(t, td, sink.AllTraces()[0]) +} + +func TestHTTPInvalidTLSCredentials(t *testing.T) { + cfg := &Config{ + Protocols: Protocols{ + HTTP: &HTTPConfig{ + ServerConfig: &confighttp.ServerConfig{ + Endpoint: testutil.GetAvailableLocalAddress(t), + TLSSetting: &configtls.ServerConfig{ + Config: configtls.Config{ + CertFile: "willfail", + }, + }, + }, + TracesURLPath: defaultTracesURLPath, + MetricsURLPath: defaultMetricsURLPath, + LogsURLPath: defaultLogsURLPath, + }, + }, + } + + // TLS is resolved during Start for HTTP. + r, err := NewFactory().CreateTracesReceiver( + context.Background(), + receivertest.NewNopCreateSettings(), + cfg, + consumertest.NewNop()) + require.NoError(t, err) + assert.NotNil(t, r) + assert.EqualError(t, r.Start(context.Background(), componenttest.NewNopHost()), + `failed to load TLS config: failed to load TLS cert and key: for auth via TLS, provide both certificate and key, or neither`) +} + +func testHTTPMaxRequestBodySize(t *testing.T, path string, contentType string, payload []byte, size int, expectedStatusCode int) { + addr := testutil.GetAvailableLocalAddress(t) + url := "http://" + addr + path + cfg := &Config{ + Protocols: Protocols{ + HTTP: &HTTPConfig{ + ServerConfig: &confighttp.ServerConfig{ + Endpoint: addr, + MaxRequestBodySize: int64(size), + }, + TracesURLPath: defaultTracesURLPath, + MetricsURLPath: defaultMetricsURLPath, + LogsURLPath: defaultLogsURLPath, + }, + }, + } + + recv := newReceiver(t, componenttest.NewNopTelemetrySettings(), cfg, otlpReceiverID, consumertest.NewNop()) + require.NoError(t, recv.Start(context.Background(), componenttest.NewNopHost())) + + req := createHTTPRequest(t, url, "", contentType, payload) + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + _, err = io.ReadAll(resp.Body) + require.NoError(t, err) + require.Equal(t, expectedStatusCode, resp.StatusCode) + + require.NoError(t, recv.Shutdown(context.Background())) +} + +func TestHTTPMaxRequestBodySize(t *testing.T) { + dataReqs := generateDataRequests(t) + + for _, dr := range dataReqs { + testHTTPMaxRequestBodySize(t, dr.path, "application/json", dr.jsonBytes, len(dr.jsonBytes), 200) + testHTTPMaxRequestBodySize(t, dr.path, "application/json", dr.jsonBytes, len(dr.jsonBytes)-1, 400) + + testHTTPMaxRequestBodySize(t, dr.path, "application/x-protobuf", dr.protoBytes, len(dr.protoBytes), 200) + testHTTPMaxRequestBodySize(t, dr.path, "application/x-protobuf", dr.protoBytes, len(dr.protoBytes)-1, 400) + } +} + +func newGRPCReceiver(t *testing.T, settings component.TelemetrySettings, endpoint string, c consumertest.Consumer) component.Component { + cfg := createDefaultConfig().(*Config) + cfg.GRPC.NetAddr.Endpoint = endpoint + cfg.HTTP = nil + return newReceiver(t, settings, cfg, otlpReceiverID, c) +} + +func newHTTPReceiver(t *testing.T, settings component.TelemetrySettings, endpoint string, c consumertest.Consumer) component.Component { + cfg := createDefaultConfig().(*Config) + cfg.HTTP.Endpoint = endpoint + cfg.GRPC = nil + return newReceiver(t, settings, cfg, otlpReceiverID, c) +} + +func newReceiver(t *testing.T, settings component.TelemetrySettings, cfg *Config, id component.ID, c consumertest.Consumer) component.Component { + set := receivertest.NewNopCreateSettings() + set.TelemetrySettings = settings + set.ID = id + r, err := newOtlpReceiver(cfg, &set) + require.NoError(t, err) + r.registerTraceConsumer(c) + r.registerMetricsConsumer(c) + r.registerLogsConsumer(c) + return r +} + +type dataRequest struct { + data any + path string + jsonBytes []byte + protoBytes []byte +} + +func generateDataRequests(t *testing.T) []dataRequest { + return []dataRequest{generateTracesRequest(t), generateMetricsRequests(t), generateLogsRequest(t)} +} + +func generateTracesRequest(t *testing.T) dataRequest { + protoMarshaler := &ptrace.ProtoMarshaler{} + jsonMarshaler := &ptrace.JSONMarshaler{} + + td := testdata.GenerateTraces(2) + traceProto, err := protoMarshaler.MarshalTraces(td) + require.NoError(t, err) + + traceJSON, err := jsonMarshaler.MarshalTraces(td) + require.NoError(t, err) + + return dataRequest{data: td, path: defaultTracesURLPath, jsonBytes: traceJSON, protoBytes: traceProto} +} + +func generateMetricsRequests(t *testing.T) dataRequest { + protoMarshaler := &pmetric.ProtoMarshaler{} + jsonMarshaler := &pmetric.JSONMarshaler{} + + md := testdata.GenerateMetrics(2) + metricProto, err := protoMarshaler.MarshalMetrics(md) + require.NoError(t, err) + + metricJSON, err := jsonMarshaler.MarshalMetrics(md) + require.NoError(t, err) + + return dataRequest{data: md, path: defaultMetricsURLPath, jsonBytes: metricJSON, protoBytes: metricProto} +} + +func generateLogsRequest(t *testing.T) dataRequest { + protoMarshaler := &plog.ProtoMarshaler{} + jsonMarshaler := &plog.JSONMarshaler{} + + ld := testdata.GenerateLogs(2) + logProto, err := protoMarshaler.MarshalLogs(ld) + require.NoError(t, err) + + logJSON, err := jsonMarshaler.MarshalLogs(ld) + require.NoError(t, err) + + return dataRequest{data: ld, path: defaultLogsURLPath, jsonBytes: logJSON, protoBytes: logProto} +} + +func doHTTPRequest( + t *testing.T, + url string, + encoding string, + contentType string, + data []byte, + expectStatusCode int, +) []byte { + req := createHTTPRequest(t, url, encoding, contentType, data) + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + + respBytes, err := io.ReadAll(resp.Body) + require.NoError(t, err) + + require.NoError(t, resp.Body.Close()) + // For cases like "application/json; charset=utf-8", the response will be only "application/json" + require.True(t, strings.HasPrefix(strings.ToLower(contentType), resp.Header.Get("Content-Type"))) + + if expectStatusCode == 0 { + require.Equal(t, http.StatusOK, resp.StatusCode) + } else { + require.Equal(t, expectStatusCode, resp.StatusCode) + } + + return respBytes +} + +func createHTTPRequest( + t *testing.T, + url string, + encoding string, + contentType string, + data []byte, +) *http.Request { + var buf *bytes.Buffer + switch encoding { + case "gzip": + buf = compressGzip(t, data) + case "zstd": + buf = compressZstd(t, data) + case "": + buf = bytes.NewBuffer(data) + default: + t.Fatalf("Unsupported compression type %v", encoding) + } + + req, err := http.NewRequest(http.MethodPost, url, buf) + require.NoError(t, err) + req.Header.Set("Content-Type", contentType) + req.Header.Set("Content-Encoding", encoding) + + return req +} + +func compressGzip(t *testing.T, body []byte) *bytes.Buffer { + var buf bytes.Buffer + + gw := gzip.NewWriter(&buf) + defer func() { + require.NoError(t, gw.Close()) + }() + + _, err := gw.Write(body) + require.NoError(t, err) + + return &buf +} + +func compressZstd(t *testing.T, body []byte) *bytes.Buffer { + var buf bytes.Buffer + + zw, err := zstd.NewWriter(&buf) + require.NoError(t, err) + + defer func() { + require.NoError(t, zw.Close()) + }() + + _, err = zw.Write(body) + require.NoError(t, err) + + return &buf +} + +type senderFunc func(td ptrace.Traces) + +func TestShutdown(t *testing.T) { + endpointGrpc := testutil.GetAvailableLocalAddress(t) + endpointHTTP := testutil.GetAvailableLocalAddress(t) + + nextSink := new(consumertest.TracesSink) + + // Create OTLP receiver with gRPC and HTTP protocols. + factory := NewFactory() + cfg := factory.CreateDefaultConfig().(*Config) + cfg.GRPC.NetAddr.Endpoint = endpointGrpc + cfg.HTTP.Endpoint = endpointHTTP + set := receivertest.NewNopCreateSettings() + set.ID = otlpReceiverID + r, err := NewFactory().CreateTracesReceiver( + context.Background(), + set, + cfg, + nextSink) + require.NoError(t, err) + require.NotNil(t, r) + require.NoError(t, r.Start(context.Background(), componenttest.NewNopHost())) + + conn, err := grpc.NewClient(endpointGrpc, grpc.WithTransportCredentials(insecure.NewCredentials())) + require.NoError(t, err) + t.Cleanup(func() { + require.NoError(t, conn.Close()) + }) + + doneSignalGrpc := make(chan bool) + doneSignalHTTP := make(chan bool) + + senderGrpc := func(td ptrace.Traces) { + // Ignore error, may be executed after the receiver shutdown. + _ = exportTraces(conn, td) + } + senderHTTP := func(td ptrace.Traces) { + // Send request via OTLP/HTTP. + marshaler := &ptrace.ProtoMarshaler{} + traceBytes, err2 := marshaler.MarshalTraces(td) + require.NoError(t, err2) + url := "http://" + endpointHTTP + defaultTracesURLPath + req := createHTTPRequest(t, url, "", "application/x-protobuf", traceBytes) + if resp, errResp := http.DefaultClient.Do(req); errResp == nil { + require.NoError(t, resp.Body.Close()) + } + } + + // Send traces to the receiver until we signal via done channel, and then + // send one more trace after that. + go generateTraces(senderGrpc, doneSignalGrpc) + go generateTraces(senderHTTP, doneSignalHTTP) + + // Wait until the receiver outputs anything to the sink. + assert.Eventually(t, func() bool { + return nextSink.SpanCount() > 0 + }, time.Second, 10*time.Millisecond) + + // Now shutdown the receiver, while continuing sending traces to it. + ctx, cancelFn := context.WithTimeout(context.Background(), 10*time.Second) + defer cancelFn() + assert.NoError(t, r.Shutdown(ctx)) + + // Remember how many spans the sink received. This number should not change after this + // point because after Shutdown() returns the component is not allowed to produce + // any more data. + sinkSpanCountAfterShutdown := nextSink.SpanCount() + + // Now signal to generateTraces to exit the main generation loop, then send + // one more trace and stop. + doneSignalGrpc <- true + doneSignalHTTP <- true + + // Wait until all follow up traces are sent. + <-doneSignalGrpc + <-doneSignalHTTP + + // The last, additional trace should not be received by sink, so the number of spans in + // the sink should not change. + assert.Equal(t, sinkSpanCountAfterShutdown, nextSink.SpanCount()) +} + +func generateTraces(senderFn senderFunc, doneSignal chan bool) { + // Continuously generate spans until signaled to stop. +loop: + for { + select { + case <-doneSignal: + break loop + default: + } + senderFn(testdata.GenerateTraces(1)) + } + + // After getting the signal to stop, send one more span and then + // finally stop. We should never receive this last span. + senderFn(testdata.GenerateTraces(1)) + + // Indicate that we are done. + close(doneSignal) +} + +func exportTraces(cc *grpc.ClientConn, td ptrace.Traces) error { + acc := ptraceotlp.NewGRPCClient(cc) + req := ptraceotlp.NewExportRequestFromTraces(td) + _, err := acc.Export(context.Background(), req) + + return err +} + +type errOrSinkConsumer struct { + consumertest.Consumer + *consumertest.TracesSink + *consumertest.MetricsSink + *consumertest.LogsSink + mu sync.Mutex + consumeError error // to be returned by ConsumeTraces, if set +} + +func newErrOrSinkConsumer() *errOrSinkConsumer { + return &errOrSinkConsumer{ + TracesSink: new(consumertest.TracesSink), + MetricsSink: new(consumertest.MetricsSink), + LogsSink: new(consumertest.LogsSink), + } +} + +// SetConsumeError sets an error that will be returned by the Consume function. +func (esc *errOrSinkConsumer) SetConsumeError(err error) { + esc.mu.Lock() + defer esc.mu.Unlock() + esc.consumeError = err +} + +func (esc *errOrSinkConsumer) Capabilities() consumer.Capabilities { + return consumer.Capabilities{MutatesData: false} +} + +// ConsumeTraces stores traces to this sink. +func (esc *errOrSinkConsumer) ConsumeTraces(ctx context.Context, td ptrace.Traces) error { + esc.mu.Lock() + defer esc.mu.Unlock() + + if esc.consumeError != nil { + return esc.consumeError + } + + return esc.TracesSink.ConsumeTraces(ctx, td) +} + +// ConsumeMetrics stores metrics to this sink. +func (esc *errOrSinkConsumer) ConsumeMetrics(ctx context.Context, md pmetric.Metrics) error { + esc.mu.Lock() + defer esc.mu.Unlock() + + if esc.consumeError != nil { + return esc.consumeError + } + + return esc.MetricsSink.ConsumeMetrics(ctx, md) +} + +// ConsumeLogs stores metrics to this sink. +func (esc *errOrSinkConsumer) ConsumeLogs(ctx context.Context, ld plog.Logs) error { + esc.mu.Lock() + defer esc.mu.Unlock() + + if esc.consumeError != nil { + return esc.consumeError + } + + return esc.LogsSink.ConsumeLogs(ctx, ld) +} + +// Reset deletes any stored in the sinks, resets error to nil. +func (esc *errOrSinkConsumer) Reset() { + esc.mu.Lock() + defer esc.mu.Unlock() + + esc.consumeError = nil + esc.TracesSink.Reset() + esc.MetricsSink.Reset() + esc.LogsSink.Reset() +} + +// Reset deletes any stored in the sinks, resets error to nil. +func (esc *errOrSinkConsumer) checkData(t *testing.T, data any, len int) { + switch data.(type) { + case ptrace.Traces: + allTraces := esc.TracesSink.AllTraces() + require.Len(t, allTraces, len) + if len > 0 { + require.Equal(t, allTraces[0], data) + } + case pmetric.Metrics: + allMetrics := esc.MetricsSink.AllMetrics() + require.Len(t, allMetrics, len) + if len > 0 { + require.Equal(t, allMetrics[0], data) + } + case plog.Logs: + allLogs := esc.LogsSink.AllLogs() + require.Len(t, allLogs, len) + if len > 0 { + require.Equal(t, allLogs[0], data) + } + } +} From 890f0464789fab6428dbbcaff460bf843d5c8bc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juraci=20Paix=C3=A3o=20Kr=C3=B6hling?= Date: Mon, 3 Jun 2024 17:10:16 +0200 Subject: [PATCH 3/4] [confighttp] Apply MaxRequestBodySize to the result of a decompressed body (#10289) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This change applies a restriction on the size of the decompressed payload. Before this change, this is the result of the test added in this PR: ``` --- FAIL: TestServerWithDecompression (0.03s) /home/jpkroehling/Projects/src/github.com/open-telemetry/opentelemetry-collector/config/confighttp/confighttp_test.go:1327: Error Trace: /home/jpkroehling/Projects/src/github.com/open-telemetry/opentelemetry-collector/config/confighttp/confighttp_test.go:1327 /usr/lib/golang/src/net/http/server.go:2166 /home/jpkroehling/Projects/src/github.com/open-telemetry/opentelemetry-collector/config/confighttp/compression.go:163 /home/jpkroehling/Projects/src/github.com/open-telemetry/opentelemetry-collector/config/confighttp/confighttp.go:455 /usr/lib/golang/src/net/http/server.go:2166 /home/jpkroehling/go/pkg/mod/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp@v0.52.0/handler.go:212 /home/jpkroehling/go/pkg/mod/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp@v0.52.0/handler.go:73 /usr/lib/golang/src/net/http/server.go:2166 /home/jpkroehling/Projects/src/github.com/open-telemetry/opentelemetry-collector/config/confighttp/clientinfohandler.go:26 /usr/lib/golang/src/net/http/server.go:3137 /usr/lib/golang/src/net/http/server.go:2039 /usr/lib/golang/src/runtime/asm_amd64.s:1695 Error: An error is expected but got nil. Test: TestServerWithDecompression /home/jpkroehling/Projects/src/github.com/open-telemetry/opentelemetry-collector/config/confighttp/confighttp_test.go:1328: Error Trace: /home/jpkroehling/Projects/src/github.com/open-telemetry/opentelemetry-collector/config/confighttp/confighttp_test.go:1328 /usr/lib/golang/src/net/http/server.go:2166 /home/jpkroehling/Projects/src/github.com/open-telemetry/opentelemetry-collector/config/confighttp/compression.go:163 /home/jpkroehling/Projects/src/github.com/open-telemetry/opentelemetry-collector/config/confighttp/confighttp.go:455 /usr/lib/golang/src/net/http/server.go:2166 /home/jpkroehling/go/pkg/mod/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp@v0.52.0/handler.go:212 /home/jpkroehling/go/pkg/mod/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp@v0.52.0/handler.go:73 /usr/lib/golang/src/net/http/server.go:2166 /home/jpkroehling/Projects/src/github.com/open-telemetry/opentelemetry-collector/config/confighttp/clientinfohandler.go:26 /usr/lib/golang/src/net/http/server.go:3137 /usr/lib/golang/src/net/http/server.go:2039 /usr/lib/golang/src/runtime/asm_amd64.s:1695 Error: Test: TestServerWithDecompression /home/jpkroehling/Projects/src/github.com/open-telemetry/opentelemetry-collector/config/confighttp/confighttp_test.go:1357: Error Trace: /home/jpkroehling/Projects/src/github.com/open-telemetry/opentelemetry-collector/config/confighttp/confighttp_test.go:1357 Error: Not equal: expected: 200 actual : 400 Test: TestServerWithDecompression FAIL FAIL go.opentelemetry.io/collector/config/confighttp 0.036s FAIL ``` Signed-off-by: Juraci Paixão Kröhling --------- Signed-off-by: Juraci Paixão Kröhling --- ..._set-maxrequestbodysize-to-compressed.yaml | 22 +++++ config/confighttp/compression.go | 16 ++-- config/confighttp/compression_test.go | 4 +- config/confighttp/confighttp.go | 9 +- config/confighttp/confighttp_test.go | 91 ++++++++++++++++++- 5 files changed, 130 insertions(+), 12 deletions(-) create mode 100644 .chloggen/jpkroehling_set-maxrequestbodysize-to-compressed.yaml diff --git a/.chloggen/jpkroehling_set-maxrequestbodysize-to-compressed.yaml b/.chloggen/jpkroehling_set-maxrequestbodysize-to-compressed.yaml new file mode 100644 index 000000000000..0d2ad5d82c13 --- /dev/null +++ b/.chloggen/jpkroehling_set-maxrequestbodysize-to-compressed.yaml @@ -0,0 +1,22 @@ +# Use this changelog template to create an entry for release notes. + +# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix' +change_type: 'breaking' + +# The name of the component, or a single word describing the area of concern, (e.g. otlpreceiver) +component: confighttp + +# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`). +note: Apply MaxRequestBodySize to the result of a decompressed body + +# One or more tracking issues or pull requests related to the change +issues: [10289] + +# (Optional) One or more lines of additional information to render under the primary note. +# These lines will be padded with 2 spaces and then inserted directly into the document. +# Use pipe (|) for multiline entries. +subtext: | + When using compressed payloads, the Collector would verify only the size of the compressed payload. + This change applies the same restriction to the decompressed content. As a security measure, a limit of 20 MiB was added, which makes this a breaking change. + For most clients, this shouldn't be a problem, but if you often have payloads that decompress to more than 20 MiB, you might want to either configure your + client to send smaller batches (recommended), or increase the limit using the MaxRequestBodySize option. diff --git a/config/confighttp/compression.go b/config/confighttp/compression.go index 88ecafe78da5..a700bec845bf 100644 --- a/config/confighttp/compression.go +++ b/config/confighttp/compression.go @@ -67,24 +67,26 @@ func (r *compressRoundTripper) RoundTrip(req *http.Request) (*http.Response, err } type decompressor struct { - errHandler func(w http.ResponseWriter, r *http.Request, errorMsg string, statusCode int) - base http.Handler - decoders map[string]func(body io.ReadCloser) (io.ReadCloser, error) + errHandler func(w http.ResponseWriter, r *http.Request, errorMsg string, statusCode int) + base http.Handler + decoders map[string]func(body io.ReadCloser) (io.ReadCloser, error) + maxRequestBodySize int64 } // httpContentDecompressor offloads the task of handling compressed HTTP requests // by identifying the compression format in the "Content-Encoding" header and re-writing // request body so that the handlers further in the chain can work on decompressed data. // It supports gzip and deflate/zlib compression. -func httpContentDecompressor(h http.Handler, eh func(w http.ResponseWriter, r *http.Request, errorMsg string, statusCode int), decoders map[string]func(body io.ReadCloser) (io.ReadCloser, error)) http.Handler { +func httpContentDecompressor(h http.Handler, maxRequestBodySize int64, eh func(w http.ResponseWriter, r *http.Request, errorMsg string, statusCode int), decoders map[string]func(body io.ReadCloser) (io.ReadCloser, error)) http.Handler { errHandler := defaultErrorHandler if eh != nil { errHandler = eh } d := &decompressor{ - errHandler: errHandler, - base: h, + maxRequestBodySize: maxRequestBodySize, + errHandler: errHandler, + base: h, decoders: map[string]func(body io.ReadCloser) (io.ReadCloser, error){ "": func(io.ReadCloser) (io.ReadCloser, error) { // Not a compressed payload. Nothing to do. @@ -155,7 +157,7 @@ func (d *decompressor) ServeHTTP(w http.ResponseWriter, r *http.Request) { // "Content-Length" is set to -1 as the size of the decompressed body is unknown. r.Header.Del("Content-Length") r.ContentLength = -1 - r.Body = newBody + r.Body = http.MaxBytesReader(w, newBody, d.maxRequestBodySize) } d.base.ServeHTTP(w, r) } diff --git a/config/confighttp/compression_test.go b/config/confighttp/compression_test.go index ea56d07ff66f..1bdea0d912a0 100644 --- a/config/confighttp/compression_test.go +++ b/config/confighttp/compression_test.go @@ -134,7 +134,7 @@ func TestHTTPCustomDecompression(t *testing.T) { return io.NopCloser(strings.NewReader("decompressed body")), nil }, } - srv := httptest.NewServer(httpContentDecompressor(handler, defaultErrorHandler, decoders)) + srv := httptest.NewServer(httpContentDecompressor(handler, defaultMaxRequestBodySize, defaultErrorHandler, decoders)) t.Cleanup(srv.Close) @@ -253,7 +253,7 @@ func TestHTTPContentDecompressionHandler(t *testing.T) { require.NoError(t, err, "failed to read request body: %v", err) assert.EqualValues(t, testBody, string(body)) w.WriteHeader(http.StatusOK) - }), defaultErrorHandler, noDecoders)) + }), defaultMaxRequestBodySize, defaultErrorHandler, noDecoders)) t.Cleanup(srv.Close) req, err := http.NewRequest(http.MethodGet, srv.URL, tt.reqBody) diff --git a/config/confighttp/confighttp.go b/config/confighttp/confighttp.go index 985c4f9be7a8..c02b179e2052 100644 --- a/config/confighttp/confighttp.go +++ b/config/confighttp/confighttp.go @@ -29,6 +29,7 @@ import ( ) const headerContentEncoding = "Content-Encoding" +const defaultMaxRequestBodySize = 20 * 1024 * 1024 // 20MiB // ClientConfig defines settings for creating an HTTP client. type ClientConfig struct { @@ -270,7 +271,7 @@ type ServerConfig struct { // Auth for this receiver Auth *configauth.Authentication `mapstructure:"auth"` - // MaxRequestBodySize sets the maximum request body size in bytes + // MaxRequestBodySize sets the maximum request body size in bytes. Default: 20MiB. MaxRequestBodySize int64 `mapstructure:"max_request_body_size"` // IncludeMetadata propagates the client metadata from the incoming requests to the downstream consumers @@ -351,7 +352,11 @@ func (hss *ServerConfig) ToServerContext(_ context.Context, host component.Host, o(serverOpts) } - handler = httpContentDecompressor(handler, serverOpts.errHandler, serverOpts.decoders) + if hss.MaxRequestBodySize <= 0 { + hss.MaxRequestBodySize = defaultMaxRequestBodySize + } + + handler = httpContentDecompressor(handler, hss.MaxRequestBodySize, serverOpts.errHandler, serverOpts.decoders) if hss.MaxRequestBodySize > 0 { handler = maxRequestBodySizeInterceptor(handler, hss.MaxRequestBodySize) diff --git a/config/confighttp/confighttp_test.go b/config/confighttp/confighttp_test.go index 3c96c58583df..762c363ba36c 100644 --- a/config/confighttp/confighttp_test.go +++ b/config/confighttp/confighttp_test.go @@ -4,6 +4,7 @@ package confighttp import ( + "bytes" "context" "errors" "fmt" @@ -13,6 +14,7 @@ import ( "net/http/httptest" "net/url" "path/filepath" + "strings" "testing" "time" @@ -1300,7 +1302,7 @@ func TestServerWithDecoder(t *testing.T) { // test response := &httptest.ResponseRecorder{} - req, err := http.NewRequest(http.MethodGet, srv.Addr, nil) + req, err := http.NewRequest(http.MethodGet, srv.Addr, bytes.NewBuffer([]byte("something"))) require.NoError(t, err, "Error creating request: %v", err) req.Header.Set("Content-Encoding", "something-else") @@ -1310,6 +1312,93 @@ func TestServerWithDecoder(t *testing.T) { } +func TestServerWithDecompression(t *testing.T) { + // prepare + hss := ServerConfig{ + MaxRequestBodySize: 1000, // 1 KB + } + body := []byte(strings.Repeat("a", 1000*1000)) // 1 MB + + srv, err := hss.ToServer( + context.Background(), + componenttest.NewNopHost(), + componenttest.NewNopTelemetrySettings(), + http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { + actualBody, err := io.ReadAll(req.Body) + assert.ErrorContains(t, err, "http: request body too large") + assert.Len(t, actualBody, 1000) + + if err != nil { + resp.WriteHeader(http.StatusBadRequest) + } else { + resp.WriteHeader(http.StatusOK) + } + }), + ) + require.NoError(t, err) + + testSrv := httptest.NewServer(srv.Handler) + defer testSrv.Close() + + req, err := http.NewRequest(http.MethodGet, testSrv.URL, compressZstd(t, body)) + require.NoError(t, err, "Error creating request: %v", err) + + req.Header.Set("Content-Encoding", "zstd") + + // test + c := http.Client{} + resp, err := c.Do(req) + require.NoError(t, err, "Error sending request: %v", err) + + _, err = io.ReadAll(resp.Body) + require.NoError(t, err, "Error reading response body: %v", err) + + // verifications is done mostly within the test, but this is only a sanity check + // that we got into the test handler + assert.Equal(t, resp.StatusCode, http.StatusBadRequest) +} + +func TestDefaultMaxRequestBodySize(t *testing.T) { + tests := []struct { + name string + settings ServerConfig + expected int64 + }{ + { + name: "default", + settings: ServerConfig{}, + expected: defaultMaxRequestBodySize, + }, + { + name: "zero", + settings: ServerConfig{MaxRequestBodySize: 0}, + expected: defaultMaxRequestBodySize, + }, + { + name: "negative", + settings: ServerConfig{MaxRequestBodySize: -1}, + expected: defaultMaxRequestBodySize, + }, + { + name: "custom", + settings: ServerConfig{MaxRequestBodySize: 100}, + expected: 100, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := tt.settings.ToServer( + context.Background(), + componenttest.NewNopHost(), + componenttest.NewNopTelemetrySettings(), + http.HandlerFunc(func(http.ResponseWriter, *http.Request) {}), + ) + require.NoError(t, err) + assert.Equal(t, tt.expected, tt.settings.MaxRequestBodySize) + }) + } +} + type mockHost struct { component.Host ext map[component.ID]component.Component From af6cea9e45c0c1eddee0b2359a8a18da4b4365bf Mon Sep 17 00:00:00 2001 From: lisguo Date: Tue, 9 Jul 2024 13:45:19 -0400 Subject: [PATCH 4/4] Update golang to 1.22.4. Fix go mod. Remove otlpreceiver test code --- cmd/configschema/go.mod | 4 +- config/configgrpc/go.mod | 32 - config/configgrpc/go.sum | 32 + config/confighttp/go.mod | 30 - config/confighttp/go.sum | 28 + go.mod | 29 +- receiver/otlpreceiver/otlp_test.go | 1195 ---------------------------- 7 files changed, 76 insertions(+), 1274 deletions(-) delete mode 100644 receiver/otlpreceiver/otlp_test.go diff --git a/cmd/configschema/go.mod b/cmd/configschema/go.mod index b54a1bee54b9..32cb00f37a77 100644 --- a/cmd/configschema/go.mod +++ b/cmd/configschema/go.mod @@ -2,9 +2,9 @@ // See https://github.com/open-telemetry/opentelemetry-collector-contrib/issues/30187 module github.com/open-telemetry/opentelemetry-collector-contrib/cmd/configschema -go 1.22.0 +go 1.22.4 -toolchain go1.22.2 +toolchain go1.22.5 require ( github.com/fatih/structtag v1.2.0 diff --git a/config/configgrpc/go.mod b/config/configgrpc/go.mod index d6a0573f42e3..26593ca6d748 100644 --- a/config/configgrpc/go.mod +++ b/config/configgrpc/go.mod @@ -66,35 +66,3 @@ require ( google.golang.org/protobuf v1.33.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) - -replace go.opentelemetry.io/collector => ../../ - -replace go.opentelemetry.io/collector/config/configauth => ../configauth - -replace go.opentelemetry.io/collector/config/configcompression => ../configcompression - -replace go.opentelemetry.io/collector/config/confignet => ../confignet - -replace go.opentelemetry.io/collector/config/configopaque => ../configopaque - -replace go.opentelemetry.io/collector/config/configtls => ../configtls - -replace go.opentelemetry.io/collector/config/configtelemetry => ../configtelemetry - -replace go.opentelemetry.io/collector/config/internal => ../internal - -replace go.opentelemetry.io/collector/extension => ../../extension - -replace go.opentelemetry.io/collector/extension/auth => ../../extension/auth - -replace go.opentelemetry.io/collector/confmap => ../../confmap - -replace go.opentelemetry.io/collector/featuregate => ../../featuregate - -replace go.opentelemetry.io/collector/pdata => ../../pdata - -replace go.opentelemetry.io/collector/pdata/testdata => ../../pdata/testdata - -replace go.opentelemetry.io/collector/component => ../../component - -replace go.opentelemetry.io/collector/consumer => ../../consumer diff --git a/config/configgrpc/go.sum b/config/configgrpc/go.sum index df0afff33c75..9e7519da04b1 100644 --- a/config/configgrpc/go.sum +++ b/config/configgrpc/go.sum @@ -72,6 +72,38 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.opentelemetry.io/collector v0.98.0 h1:O7bpARGWzNfFQEYevLl4iigDrpGTJY3vV/kKqNZzMOk= +go.opentelemetry.io/collector v0.98.0/go.mod h1:fvPM+tBML07uvAP1MV2msYPSYJ9U/lgE1jDb3AFBaMM= +go.opentelemetry.io/collector/component v0.98.0 h1:0TMaBOyCdABiVLFdGOgG8zd/1IeGldCinYonbY08xWk= +go.opentelemetry.io/collector/component v0.98.0/go.mod h1:F6zyQLsoExl6r2q6WWZm8rmSSALbwG2zwIHLrMzZVio= +go.opentelemetry.io/collector/config/configauth v0.98.0 h1:FPffZ1dRL6emStrDUEGpL0rCChbUZNAQgpArXD0SESI= +go.opentelemetry.io/collector/config/configauth v0.98.0/go.mod h1:5pMzf2zgFwS7tujNq0AtOOli5vxIvnrNi7JlZwrBOFo= +go.opentelemetry.io/collector/config/configcompression v1.5.0 h1:FTxKbFPN4LznRCH/GQ+b+0tAWmg80Y2eEka79S2sLZ0= +go.opentelemetry.io/collector/config/configcompression v1.5.0/go.mod h1:O0fOPCADyGwGLLIf5lf7N3960NsnIfxsm6dr/mIpL+M= +go.opentelemetry.io/collector/config/confignet v0.98.0 h1:pXDBb2hFe10T/NMHlL/oMgk1aFfe4NmmJFdFoioyC9o= +go.opentelemetry.io/collector/config/confignet v0.98.0/go.mod h1:3naWoPss70RhDHhYjGACi7xh4NcVRvs9itzIRVWyu1k= +go.opentelemetry.io/collector/config/configopaque v1.5.0 h1:WJzgmsFU2v63BypPBNGL31ACwWn6PwumPJNpLZplcdE= +go.opentelemetry.io/collector/config/configopaque v1.5.0/go.mod h1:/otnfj2E8r5EfaAdNV4qHkTclmiBCZXaahV5EcLwT7k= +go.opentelemetry.io/collector/config/configtelemetry v0.98.0 h1:f8RNZ1l/kYPPoxFmKKvTUli8iON7CMsm85KM38PVNts= +go.opentelemetry.io/collector/config/configtelemetry v0.98.0/go.mod h1:YV5PaOdtnU1xRomPcYqoHmyCr48tnaAREeGO96EZw8o= +go.opentelemetry.io/collector/config/configtls v0.98.0 h1:g+MADy01ge8iGC6v2tbJ5G27CWNG1BaJtmYdmpvm8e4= +go.opentelemetry.io/collector/config/configtls v0.98.0/go.mod h1:9RHArziz0mNEEkti0kz5LIdvbQGT7/Unu/0whKKazHQ= +go.opentelemetry.io/collector/config/internal v0.98.0 h1:wz/6ncawMX5cfIiXJEYSUm1g1U6iE/VxFRm4/WhVBPI= +go.opentelemetry.io/collector/config/internal v0.98.0/go.mod h1:xPnEE6QaTSXr+ctYMSTBxI2qwTntTUM4cYk7OTm6Ugc= +go.opentelemetry.io/collector/confmap v0.98.0 h1:qQreBlrqio1y7uhrAvr+W86YbQ6fw7StgkbYpvJ2vVc= +go.opentelemetry.io/collector/confmap v0.98.0/go.mod h1:BWKPIpYeUzSG6ZgCJMjF7xsLvyrvJCfYURl57E5vhiQ= +go.opentelemetry.io/collector/consumer v0.98.0 h1:47zJ5HFKXVA0RciuwkZnPU5W8j0TYUxToB1/zzzgEhs= +go.opentelemetry.io/collector/consumer v0.98.0/go.mod h1:c2edTq38uVJET/NE6VV7/Qpyznnlz8b6VE7J6TXD57c= +go.opentelemetry.io/collector/extension v0.98.0 h1:08B5ipEsoNmPHY96j5EUsUrFre01GOZ4zgttUDtPUkY= +go.opentelemetry.io/collector/extension v0.98.0/go.mod h1:fZ1Hnnahszl5j3xcW2sMRJ0FLWDOFkFMQeVDP0Se7i8= +go.opentelemetry.io/collector/extension/auth v0.98.0 h1:7b1jioijJbTMqaOCrz5Hoqf+zJn2iPlGmtN7pXLNWbA= +go.opentelemetry.io/collector/extension/auth v0.98.0/go.mod h1:gssWC4AxAwAEKI2CqS93lhjWffsVdzD8q7UGL6LaRr0= +go.opentelemetry.io/collector/featuregate v1.5.0 h1:uK8qnYQKz1TMkK+FDTFsywg/EybW/gbnOUaPNUkRznM= +go.opentelemetry.io/collector/featuregate v1.5.0/go.mod h1:w7nUODKxEi3FLf1HslCiE6YWtMtOOrMnSwsDam8Mg9w= +go.opentelemetry.io/collector/pdata v1.5.0 h1:1fKTmUpr0xCOhP/B0VEvtz7bYPQ45luQ8XFyA07j8LE= +go.opentelemetry.io/collector/pdata v1.5.0/go.mod h1:TYj8aKRWZyT/KuKQXKyqSEvK/GV+slFaDMEI+Ke64Yw= +go.opentelemetry.io/collector/pdata/testdata v0.98.0 h1:8gohV+LFXqMzuDwfOOQy9GcZBOX0C9xGoQkoeXFTzmI= +go.opentelemetry.io/collector/pdata/testdata v0.98.0/go.mod h1:B/IaHcf6+RtxI292CZu9TjfYQdi1n4+v6b8rHEonpKs= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 h1:4Pp6oUg3+e/6M4C0A/3kJ2VYa++dsWVTtGgLVj5xtHg= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0= go.opentelemetry.io/otel v1.25.0 h1:gldB5FfhRl7OJQbUHt/8s0a7cE8fbsPAtdpRaApKy4k= diff --git a/config/confighttp/go.mod b/config/confighttp/go.mod index d809c5f19926..c0367e6a6ed4 100644 --- a/config/confighttp/go.mod +++ b/config/confighttp/go.mod @@ -62,33 +62,3 @@ require ( google.golang.org/protobuf v1.33.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) - -replace go.opentelemetry.io/collector => ../../ - -replace go.opentelemetry.io/collector/config/configauth => ../configauth - -replace go.opentelemetry.io/collector/config/configcompression => ../configcompression - -replace go.opentelemetry.io/collector/config/configopaque => ../configopaque - -replace go.opentelemetry.io/collector/config/configtls => ../configtls - -replace go.opentelemetry.io/collector/config/configtelemetry => ../configtelemetry - -replace go.opentelemetry.io/collector/config/internal => ../internal - -replace go.opentelemetry.io/collector/extension => ../../extension - -replace go.opentelemetry.io/collector/extension/auth => ../../extension/auth - -replace go.opentelemetry.io/collector/confmap => ../../confmap - -replace go.opentelemetry.io/collector/featuregate => ../../featuregate - -replace go.opentelemetry.io/collector/pdata => ../../pdata - -replace go.opentelemetry.io/collector/component => ../../component - -replace go.opentelemetry.io/collector/consumer => ../../consumer - -replace go.opentelemetry.io/collector/pdata/testdata => ../../pdata/testdata diff --git a/config/confighttp/go.sum b/config/confighttp/go.sum index 43e9e71064cf..2187800ccacb 100644 --- a/config/confighttp/go.sum +++ b/config/confighttp/go.sum @@ -69,6 +69,34 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.opentelemetry.io/collector v0.98.0 h1:O7bpARGWzNfFQEYevLl4iigDrpGTJY3vV/kKqNZzMOk= +go.opentelemetry.io/collector v0.98.0/go.mod h1:fvPM+tBML07uvAP1MV2msYPSYJ9U/lgE1jDb3AFBaMM= +go.opentelemetry.io/collector/component v0.98.0 h1:0TMaBOyCdABiVLFdGOgG8zd/1IeGldCinYonbY08xWk= +go.opentelemetry.io/collector/component v0.98.0/go.mod h1:F6zyQLsoExl6r2q6WWZm8rmSSALbwG2zwIHLrMzZVio= +go.opentelemetry.io/collector/config/configauth v0.98.0 h1:FPffZ1dRL6emStrDUEGpL0rCChbUZNAQgpArXD0SESI= +go.opentelemetry.io/collector/config/configauth v0.98.0/go.mod h1:5pMzf2zgFwS7tujNq0AtOOli5vxIvnrNi7JlZwrBOFo= +go.opentelemetry.io/collector/config/configcompression v1.5.0 h1:FTxKbFPN4LznRCH/GQ+b+0tAWmg80Y2eEka79S2sLZ0= +go.opentelemetry.io/collector/config/configcompression v1.5.0/go.mod h1:O0fOPCADyGwGLLIf5lf7N3960NsnIfxsm6dr/mIpL+M= +go.opentelemetry.io/collector/config/configopaque v1.5.0 h1:WJzgmsFU2v63BypPBNGL31ACwWn6PwumPJNpLZplcdE= +go.opentelemetry.io/collector/config/configopaque v1.5.0/go.mod h1:/otnfj2E8r5EfaAdNV4qHkTclmiBCZXaahV5EcLwT7k= +go.opentelemetry.io/collector/config/configtelemetry v0.98.0 h1:f8RNZ1l/kYPPoxFmKKvTUli8iON7CMsm85KM38PVNts= +go.opentelemetry.io/collector/config/configtelemetry v0.98.0/go.mod h1:YV5PaOdtnU1xRomPcYqoHmyCr48tnaAREeGO96EZw8o= +go.opentelemetry.io/collector/config/configtls v0.98.0 h1:g+MADy01ge8iGC6v2tbJ5G27CWNG1BaJtmYdmpvm8e4= +go.opentelemetry.io/collector/config/configtls v0.98.0/go.mod h1:9RHArziz0mNEEkti0kz5LIdvbQGT7/Unu/0whKKazHQ= +go.opentelemetry.io/collector/config/internal v0.98.0 h1:wz/6ncawMX5cfIiXJEYSUm1g1U6iE/VxFRm4/WhVBPI= +go.opentelemetry.io/collector/config/internal v0.98.0/go.mod h1:xPnEE6QaTSXr+ctYMSTBxI2qwTntTUM4cYk7OTm6Ugc= +go.opentelemetry.io/collector/confmap v0.98.0 h1:qQreBlrqio1y7uhrAvr+W86YbQ6fw7StgkbYpvJ2vVc= +go.opentelemetry.io/collector/confmap v0.98.0/go.mod h1:BWKPIpYeUzSG6ZgCJMjF7xsLvyrvJCfYURl57E5vhiQ= +go.opentelemetry.io/collector/consumer v0.98.0 h1:47zJ5HFKXVA0RciuwkZnPU5W8j0TYUxToB1/zzzgEhs= +go.opentelemetry.io/collector/consumer v0.98.0/go.mod h1:c2edTq38uVJET/NE6VV7/Qpyznnlz8b6VE7J6TXD57c= +go.opentelemetry.io/collector/extension v0.98.0 h1:08B5ipEsoNmPHY96j5EUsUrFre01GOZ4zgttUDtPUkY= +go.opentelemetry.io/collector/extension v0.98.0/go.mod h1:fZ1Hnnahszl5j3xcW2sMRJ0FLWDOFkFMQeVDP0Se7i8= +go.opentelemetry.io/collector/extension/auth v0.98.0 h1:7b1jioijJbTMqaOCrz5Hoqf+zJn2iPlGmtN7pXLNWbA= +go.opentelemetry.io/collector/extension/auth v0.98.0/go.mod h1:gssWC4AxAwAEKI2CqS93lhjWffsVdzD8q7UGL6LaRr0= +go.opentelemetry.io/collector/featuregate v1.5.0 h1:uK8qnYQKz1TMkK+FDTFsywg/EybW/gbnOUaPNUkRznM= +go.opentelemetry.io/collector/featuregate v1.5.0/go.mod h1:w7nUODKxEi3FLf1HslCiE6YWtMtOOrMnSwsDam8Mg9w= +go.opentelemetry.io/collector/pdata v1.5.0 h1:1fKTmUpr0xCOhP/B0VEvtz7bYPQ45luQ8XFyA07j8LE= +go.opentelemetry.io/collector/pdata v1.5.0/go.mod h1:TYj8aKRWZyT/KuKQXKyqSEvK/GV+slFaDMEI+Ke64Yw= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= go.opentelemetry.io/otel v1.25.0 h1:gldB5FfhRl7OJQbUHt/8s0a7cE8fbsPAtdpRaApKy4k= diff --git a/go.mod b/go.mod index c89ab3951944..e5a837f5a7e4 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,9 @@ module github.com/open-telemetry/opentelemetry-collector-contrib -go 1.22.0 - -toolchain go1.22.2 +go 1.22.4 require ( + github.com/klauspost/compress v1.17.8 github.com/open-telemetry/opentelemetry-collector-contrib/connector/countconnector v0.98.0 github.com/open-telemetry/opentelemetry-collector-contrib/connector/datadogconnector v0.98.0 github.com/open-telemetry/opentelemetry-collector-contrib/connector/exceptionsconnector v0.98.0 @@ -169,9 +168,16 @@ require ( github.com/open-telemetry/opentelemetry-collector-contrib/receiver/windowsperfcountersreceiver v0.98.0 github.com/open-telemetry/opentelemetry-collector-contrib/receiver/zipkinreceiver v0.98.0 github.com/open-telemetry/opentelemetry-collector-contrib/receiver/zookeeperreceiver v0.98.0 + github.com/stretchr/testify v1.9.0 + go.opentelemetry.io/collector v0.98.0 go.opentelemetry.io/collector/component v0.98.0 + go.opentelemetry.io/collector/config/configgrpc v0.98.0 + go.opentelemetry.io/collector/config/confighttp v0.98.0 + go.opentelemetry.io/collector/config/confignet v0.98.0 + go.opentelemetry.io/collector/config/configtls v0.98.0 go.opentelemetry.io/collector/connector v0.98.0 go.opentelemetry.io/collector/connector/forwardconnector v0.98.0 + go.opentelemetry.io/collector/consumer v0.98.0 go.opentelemetry.io/collector/exporter v0.98.0 go.opentelemetry.io/collector/exporter/debugexporter v0.98.0 go.opentelemetry.io/collector/exporter/loggingexporter v0.98.0 @@ -181,11 +187,16 @@ require ( go.opentelemetry.io/collector/extension/ballastextension v0.98.0 go.opentelemetry.io/collector/extension/zpagesextension v0.98.0 go.opentelemetry.io/collector/otelcol v0.98.0 + go.opentelemetry.io/collector/pdata v1.5.0 + go.opentelemetry.io/collector/pdata/testdata v0.98.0 go.opentelemetry.io/collector/processor v0.98.0 go.opentelemetry.io/collector/processor/batchprocessor v0.98.0 go.opentelemetry.io/collector/processor/memorylimiterprocessor v0.98.0 go.opentelemetry.io/collector/receiver v0.98.0 go.opentelemetry.io/collector/receiver/otlpreceiver v0.98.0 + google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda + google.golang.org/grpc v1.63.2 + google.golang.org/protobuf v1.33.0 ) require ( @@ -460,7 +471,6 @@ require ( github.com/json-iterator/go v1.1.12 // indirect github.com/julienschmidt/httprouter v1.3.0 // indirect github.com/karrick/godirwalk v1.17.0 // indirect - github.com/klauspost/compress v1.17.8 // indirect github.com/klauspost/cpuid/v2 v2.2.5 // indirect github.com/knadh/koanf/maps v0.1.1 // indirect github.com/knadh/koanf/providers/confmap v0.1.0 // indirect @@ -601,7 +611,6 @@ require ( github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/viper v1.18.2 // indirect github.com/stretchr/objx v0.5.2 // indirect - github.com/stretchr/testify v1.9.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 // indirect github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.896 // indirect @@ -633,16 +642,11 @@ require ( go.mongodb.org/atlas v0.36.0 // indirect go.mongodb.org/mongo-driver v1.14.0 // indirect go.opencensus.io v0.24.0 // indirect - go.opentelemetry.io/collector v0.98.0 // indirect go.opentelemetry.io/collector/config/configauth v0.98.0 // indirect go.opentelemetry.io/collector/config/configcompression v1.5.0 // indirect - go.opentelemetry.io/collector/config/configgrpc v0.98.0 // indirect - go.opentelemetry.io/collector/config/confighttp v0.98.0 // indirect - go.opentelemetry.io/collector/config/confignet v0.98.0 // indirect go.opentelemetry.io/collector/config/configopaque v1.5.0 // indirect go.opentelemetry.io/collector/config/configretry v0.98.0 // indirect go.opentelemetry.io/collector/config/configtelemetry v0.98.0 // indirect - go.opentelemetry.io/collector/config/configtls v0.98.0 // indirect go.opentelemetry.io/collector/config/internal v0.98.0 // indirect go.opentelemetry.io/collector/confmap v0.98.0 // indirect go.opentelemetry.io/collector/confmap/converter/expandconverter v0.98.0 // indirect @@ -651,10 +655,8 @@ require ( go.opentelemetry.io/collector/confmap/provider/httpprovider v0.98.0 // indirect go.opentelemetry.io/collector/confmap/provider/httpsprovider v0.98.0 // indirect go.opentelemetry.io/collector/confmap/provider/yamlprovider v0.98.0 // indirect - go.opentelemetry.io/collector/consumer v0.98.0 // indirect go.opentelemetry.io/collector/extension/auth v0.98.0 // indirect go.opentelemetry.io/collector/featuregate v1.5.0 // indirect - go.opentelemetry.io/collector/pdata v1.5.0 // indirect go.opentelemetry.io/collector/semconv v0.98.0 // indirect go.opentelemetry.io/collector/service v0.98.0 // indirect go.opentelemetry.io/contrib/config v0.4.0 // indirect @@ -697,9 +699,6 @@ require ( google.golang.org/appengine v1.6.8 // indirect google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda // indirect - google.golang.org/grpc v1.63.2 // indirect - google.golang.org/protobuf v1.33.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect diff --git a/receiver/otlpreceiver/otlp_test.go b/receiver/otlpreceiver/otlp_test.go deleted file mode 100644 index 51f99bb4cb8f..000000000000 --- a/receiver/otlpreceiver/otlp_test.go +++ /dev/null @@ -1,1195 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -package otlpreceiver - -import ( - "bytes" - "compress/gzip" - "context" - "encoding/json" - "errors" - "fmt" - "io" - "net" - "net/http" - "strings" - "sync" - "testing" - "time" - - "github.com/klauspost/compress/zstd" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - spb "google.golang.org/genproto/googleapis/rpc/status" - "google.golang.org/grpc" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/credentials/insecure" - "google.golang.org/grpc/status" - "google.golang.org/protobuf/proto" - - "go.opentelemetry.io/collector/component" - "go.opentelemetry.io/collector/component/componenttest" - "go.opentelemetry.io/collector/config/configgrpc" - "go.opentelemetry.io/collector/config/confighttp" - "go.opentelemetry.io/collector/config/confignet" - "go.opentelemetry.io/collector/config/configtls" - "go.opentelemetry.io/collector/consumer" - "go.opentelemetry.io/collector/consumer/consumererror" - "go.opentelemetry.io/collector/consumer/consumertest" - "go.opentelemetry.io/collector/internal/testutil" - "go.opentelemetry.io/collector/pdata/plog" - "go.opentelemetry.io/collector/pdata/pmetric" - "go.opentelemetry.io/collector/pdata/ptrace" - "go.opentelemetry.io/collector/pdata/ptrace/ptraceotlp" - "go.opentelemetry.io/collector/pdata/testdata" - "go.opentelemetry.io/collector/receiver/receivertest" -) - -const otlpReceiverName = "receiver_test" - -var otlpReceiverID = component.MustNewIDWithName("otlp", otlpReceiverName) - -func TestJsonHttp(t *testing.T) { - tests := []struct { - name string - encoding string - contentType string - err error - expectedStatus *spb.Status - expectedStatusCode int - }{ - { - name: "JSONUncompressed", - encoding: "", - contentType: "application/json", - }, - { - name: "JSONUncompressedUTF8", - encoding: "", - contentType: "application/json; charset=utf-8", - }, - { - name: "JSONUncompressedUppercase", - encoding: "", - contentType: "APPLICATION/JSON", - }, - { - name: "JSONGzipCompressed", - encoding: "gzip", - contentType: "application/json", - }, - { - name: "JSONZstdCompressed", - encoding: "zstd", - contentType: "application/json", - }, - { - name: "Permanent NotGRPCError", - encoding: "", - contentType: "application/json", - err: consumererror.NewPermanent(errors.New("my error")), - expectedStatus: &spb.Status{Code: int32(codes.Internal), Message: "Permanent error: my error"}, - expectedStatusCode: http.StatusInternalServerError, - }, - { - name: "Retryable NotGRPCError", - encoding: "", - contentType: "application/json", - err: errors.New("my error"), - expectedStatus: &spb.Status{Code: int32(codes.Unavailable), Message: "my error"}, - expectedStatusCode: http.StatusServiceUnavailable, - }, - { - name: "Permanent GRPCError", - encoding: "", - contentType: "application/json", - err: status.New(codes.InvalidArgument, "").Err(), - expectedStatus: &spb.Status{Code: int32(codes.InvalidArgument), Message: ""}, - expectedStatusCode: http.StatusInternalServerError, - }, - { - name: "Retryable GRPCError", - encoding: "", - contentType: "application/json", - err: status.New(codes.Unavailable, "").Err(), - expectedStatus: &spb.Status{Code: int32(codes.Unavailable), Message: ""}, - expectedStatusCode: http.StatusServiceUnavailable, - }, - } - addr := testutil.GetAvailableLocalAddress(t) - sink := newErrOrSinkConsumer() - recv := newHTTPReceiver(t, componenttest.NewNopTelemetrySettings(), addr, sink) - require.NoError(t, recv.Start(context.Background(), componenttest.NewNopHost()), "Failed to start trace receiver") - t.Cleanup(func() { require.NoError(t, recv.Shutdown(context.Background())) }) - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - sink.Reset() - sink.SetConsumeError(tt.err) - - for _, dr := range generateDataRequests(t) { - url := "http://" + addr + dr.path - respBytes := doHTTPRequest(t, url, tt.encoding, tt.contentType, dr.jsonBytes, tt.expectedStatusCode) - if tt.err == nil { - tr := ptraceotlp.NewExportResponse() - assert.NoError(t, tr.UnmarshalJSON(respBytes), "Unable to unmarshal response to Response") - sink.checkData(t, dr.data, 1) - } else { - errStatus := &spb.Status{} - assert.NoError(t, json.Unmarshal(respBytes, errStatus)) - if s, ok := status.FromError(tt.err); ok { - assert.True(t, proto.Equal(errStatus, s.Proto())) - } else { - fmt.Println(errStatus) - assert.True(t, proto.Equal(errStatus, tt.expectedStatus)) - } - sink.checkData(t, dr.data, 0) - } - } - }) - } -} - -func TestHandleInvalidRequests(t *testing.T) { - addr := testutil.GetAvailableLocalAddress(t) - sink := newErrOrSinkConsumer() - recv := newHTTPReceiver(t, componenttest.NewNopTelemetrySettings(), addr, sink) - require.NoError(t, recv.Start(context.Background(), componenttest.NewNopHost()), "Failed to start trace receiver") - t.Cleanup(func() { require.NoError(t, recv.Shutdown(context.Background())) }) - - tests := []struct { - name string - uri string - method string - contentType string - - expectedStatus int - expectedResponseBody string - }{ - { - name: "no content type", - uri: defaultTracesURLPath, - method: http.MethodPost, - contentType: "", - - expectedStatus: http.StatusUnsupportedMediaType, - expectedResponseBody: "415 unsupported media type, supported: [application/json, application/x-protobuf]", - }, - { - name: "invalid content type", - uri: defaultTracesURLPath, - method: http.MethodPost, - contentType: "invalid", - - expectedStatus: http.StatusUnsupportedMediaType, - expectedResponseBody: "415 unsupported media type, supported: [application/json, application/x-protobuf]", - }, - { - name: "invalid request", - uri: defaultTracesURLPath, - method: http.MethodPost, - contentType: "application/json", - - expectedStatus: http.StatusBadRequest, - }, - { - uri: defaultTracesURLPath, - method: http.MethodPatch, - contentType: "application/json", - - expectedStatus: http.StatusMethodNotAllowed, - expectedResponseBody: "405 method not allowed, supported: [POST]", - }, - { - uri: defaultTracesURLPath, - method: http.MethodGet, - contentType: "application/json", - - expectedStatus: http.StatusMethodNotAllowed, - expectedResponseBody: "405 method not allowed, supported: [POST]", - }, - { - name: "no content type", - uri: defaultMetricsURLPath, - method: http.MethodPost, - contentType: "", - - expectedStatus: http.StatusUnsupportedMediaType, - expectedResponseBody: "415 unsupported media type, supported: [application/json, application/x-protobuf]", - }, - { - name: "invalid content type", - uri: defaultMetricsURLPath, - method: http.MethodPost, - contentType: "invalid", - - expectedStatus: http.StatusUnsupportedMediaType, - expectedResponseBody: "415 unsupported media type, supported: [application/json, application/x-protobuf]", - }, - { - name: "invalid request", - uri: defaultMetricsURLPath, - method: http.MethodPost, - contentType: "application/json", - - expectedStatus: http.StatusBadRequest, - }, - { - uri: defaultMetricsURLPath, - method: http.MethodPatch, - contentType: "application/json", - - expectedStatus: http.StatusMethodNotAllowed, - expectedResponseBody: "405 method not allowed, supported: [POST]", - }, - { - uri: defaultMetricsURLPath, - method: http.MethodGet, - contentType: "application/json", - - expectedStatus: http.StatusMethodNotAllowed, - expectedResponseBody: "405 method not allowed, supported: [POST]", - }, - { - name: "no content type", - uri: defaultLogsURLPath, - method: http.MethodPost, - contentType: "", - - expectedStatus: http.StatusUnsupportedMediaType, - expectedResponseBody: "415 unsupported media type, supported: [application/json, application/x-protobuf]", - }, - { - name: "invalid content type", - uri: defaultLogsURLPath, - method: http.MethodPost, - contentType: "invalid", - - expectedStatus: http.StatusUnsupportedMediaType, - expectedResponseBody: "415 unsupported media type, supported: [application/json, application/x-protobuf]", - }, - { - name: "invalid request", - uri: defaultLogsURLPath, - method: http.MethodPost, - contentType: "application/json", - - expectedStatus: http.StatusBadRequest, - }, - { - uri: defaultLogsURLPath, - method: http.MethodPatch, - contentType: "application/json", - - expectedStatus: http.StatusMethodNotAllowed, - expectedResponseBody: "405 method not allowed, supported: [POST]", - }, - { - uri: defaultLogsURLPath, - method: http.MethodGet, - contentType: "application/json", - - expectedStatus: http.StatusMethodNotAllowed, - expectedResponseBody: "405 method not allowed, supported: [POST]", - }, - } - - for _, tt := range tests { - t.Run(tt.method+" "+tt.uri+" "+tt.name, func(t *testing.T) { - url := "http://" + addr + tt.uri - req, err := http.NewRequest(tt.method, url, bytes.NewReader([]byte(`1234`))) - require.NoError(t, err) - req.Header.Set("Content-Type", tt.contentType) - - resp, err := http.DefaultClient.Do(req) - require.NoError(t, err) - - body, err := io.ReadAll(resp.Body) - require.NoError(t, err) - - if tt.name == "invalid request" { - assert.Equal(t, "application/json", resp.Header.Get("Content-Type")) - assert.Equal(t, tt.expectedStatus, resp.StatusCode) - return - } - assert.Equal(t, "text/plain", resp.Header.Get("Content-Type")) - assert.Equal(t, tt.expectedStatus, resp.StatusCode) - assert.Equal(t, tt.expectedResponseBody, string(body)) - }) - } - - require.NoError(t, recv.Shutdown(context.Background())) -} - -func TestProtoHttp(t *testing.T) { - tests := []struct { - name string - encoding string - err error - expectedStatus *spb.Status - expectedStatusCode int - }{ - { - name: "ProtoUncompressed", - encoding: "", - }, - { - name: "ProtoGzipCompressed", - encoding: "gzip", - }, - { - name: "ProtoZstdCompressed", - encoding: "zstd", - }, - { - name: "Permanent NotGRPCError", - encoding: "", - err: consumererror.NewPermanent(errors.New("my error")), - expectedStatus: &spb.Status{Code: int32(codes.Internal), Message: "Permanent error: my error"}, - expectedStatusCode: http.StatusInternalServerError, - }, - { - name: "Retryable NotGRPCError", - encoding: "", - err: errors.New("my error"), - expectedStatus: &spb.Status{Code: int32(codes.Unavailable), Message: "my error"}, - expectedStatusCode: http.StatusServiceUnavailable, - }, - { - name: "Permanent GRPCError", - encoding: "", - err: status.New(codes.InvalidArgument, "").Err(), - expectedStatus: &spb.Status{Code: int32(codes.InvalidArgument), Message: ""}, - expectedStatusCode: http.StatusInternalServerError, - }, - { - name: "Retryable GRPCError", - encoding: "", - err: status.New(codes.Unavailable, "").Err(), - expectedStatus: &spb.Status{Code: int32(codes.Unavailable), Message: ""}, - expectedStatusCode: http.StatusServiceUnavailable, - }, - } - addr := testutil.GetAvailableLocalAddress(t) - - // Set the buffer count to 1 to make it flush the test span immediately. - sink := newErrOrSinkConsumer() - recv := newHTTPReceiver(t, componenttest.NewNopTelemetrySettings(), addr, sink) - - require.NoError(t, recv.Start(context.Background(), componenttest.NewNopHost()), "Failed to start trace receiver") - t.Cleanup(func() { require.NoError(t, recv.Shutdown(context.Background())) }) - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - sink.Reset() - sink.SetConsumeError(tt.err) - - for _, dr := range generateDataRequests(t) { - url := "http://" + addr + dr.path - respBytes := doHTTPRequest(t, url, tt.encoding, "application/x-protobuf", dr.protoBytes, tt.expectedStatusCode) - if tt.err == nil { - tr := ptraceotlp.NewExportResponse() - assert.NoError(t, tr.UnmarshalProto(respBytes)) - sink.checkData(t, dr.data, 1) - } else { - errStatus := &spb.Status{} - assert.NoError(t, proto.Unmarshal(respBytes, errStatus)) - if s, ok := status.FromError(tt.err); ok { - assert.True(t, proto.Equal(errStatus, s.Proto())) - } else { - assert.True(t, proto.Equal(errStatus, tt.expectedStatus)) - } - sink.checkData(t, dr.data, 0) - } - } - }) - } -} - -func TestOTLPReceiverInvalidContentEncoding(t *testing.T) { - tests := []struct { - name string - content string - encoding string - reqBodyFunc func() (*bytes.Buffer, error) - resBodyFunc func() ([]byte, error) - status int - }{ - { - name: "JsonGzipUncompressed", - content: "application/json", - encoding: "gzip", - reqBodyFunc: func() (*bytes.Buffer, error) { - return bytes.NewBuffer([]byte(`{"key": "value"}`)), nil - }, - resBodyFunc: func() ([]byte, error) { - return json.Marshal(status.New(codes.InvalidArgument, "gzip: invalid header").Proto()) - }, - status: 400, - }, - { - name: "ProtoGzipUncompressed", - content: "application/x-protobuf", - encoding: "gzip", - reqBodyFunc: func() (*bytes.Buffer, error) { - return bytes.NewBuffer([]byte(`{"key": "value"}`)), nil - }, - resBodyFunc: func() ([]byte, error) { - return proto.Marshal(status.New(codes.InvalidArgument, "gzip: invalid header").Proto()) - }, - status: 400, - }, - { - name: "ProtoZstdUncompressed", - content: "application/x-protobuf", - encoding: "zstd", - reqBodyFunc: func() (*bytes.Buffer, error) { - return bytes.NewBuffer([]byte(`{"key": "value"}`)), nil - }, - resBodyFunc: func() ([]byte, error) { - return proto.Marshal(status.New(codes.InvalidArgument, "invalid input: magic number mismatch").Proto()) - }, - status: 400, - }, - } - addr := testutil.GetAvailableLocalAddress(t) - - // Set the buffer count to 1 to make it flush the test span immediately. - recv := newHTTPReceiver(t, componenttest.NewNopTelemetrySettings(), addr, consumertest.NewNop()) - - require.NoError(t, recv.Start(context.Background(), componenttest.NewNopHost()), "Failed to start trace receiver") - t.Cleanup(func() { require.NoError(t, recv.Shutdown(context.Background())) }) - - url := fmt.Sprintf("http://%s%s", addr, defaultTracesURLPath) - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - body, err := test.reqBodyFunc() - require.NoError(t, err, "Error creating request body: %v", err) - - req, err := http.NewRequest(http.MethodPost, url, body) - require.NoError(t, err, "Error creating trace POST request: %v", err) - req.Header.Set("Content-Type", test.content) - req.Header.Set("Content-Encoding", test.encoding) - - resp, err := http.DefaultClient.Do(req) - require.NoError(t, err, "Error posting trace to grpc-gateway server: %v", err) - - respBytes, err := io.ReadAll(resp.Body) - require.NoError(t, err, "Error reading response from trace grpc-gateway") - exRespBytes, err := test.resBodyFunc() - require.NoError(t, err, "Error creating expecting response body") - require.NoError(t, resp.Body.Close(), "Error closing response body") - - require.Equal(t, test.status, resp.StatusCode, "Unexpected return status") - require.Equal(t, test.content, resp.Header.Get("Content-Type"), "Unexpected response Content-Type") - require.Equal(t, exRespBytes, respBytes, "Unexpected response content") - }) - } -} - -func TestGRPCNewPortAlreadyUsed(t *testing.T) { - addr := testutil.GetAvailableLocalAddress(t) - ln, err := net.Listen("tcp", addr) - require.NoError(t, err, "failed to listen on %q: %v", addr, err) - t.Cleanup(func() { - assert.NoError(t, ln.Close()) - }) - - r := newGRPCReceiver(t, componenttest.NewNopTelemetrySettings(), addr, consumertest.NewNop()) - require.NotNil(t, r) - - require.Error(t, r.Start(context.Background(), componenttest.NewNopHost())) -} - -func TestHTTPNewPortAlreadyUsed(t *testing.T) { - addr := testutil.GetAvailableLocalAddress(t) - ln, err := net.Listen("tcp", addr) - require.NoError(t, err, "failed to listen on %q: %v", addr, err) - t.Cleanup(func() { - assert.NoError(t, ln.Close()) - }) - - r := newHTTPReceiver(t, componenttest.NewNopTelemetrySettings(), addr, consumertest.NewNop()) - require.NotNil(t, r) - - require.Error(t, r.Start(context.Background(), componenttest.NewNopHost())) -} - -// TestOTLPReceiverGRPCTracesIngestTest checks that the gRPC trace receiver -// is returning the proper response (return and metrics) when the next consumer -// in the pipeline reports error. The test changes the responses returned by the -// next trace consumer, checks if data was passed down the pipeline and if -// proper metrics were recorded. It also uses all endpoints supported by the -// trace receiver. -func TestOTLPReceiverGRPCTracesIngestTest(t *testing.T) { - type ingestionStateTest struct { - okToIngest bool - permanent bool - expectedCode codes.Code - } - - expectedReceivedBatches := 2 - expectedIngestionBlockedRPCs := 2 - ingestionStates := []ingestionStateTest{ - { - okToIngest: true, - expectedCode: codes.OK, - }, - { - okToIngest: false, - expectedCode: codes.Unavailable, - }, - { - okToIngest: false, - expectedCode: codes.Internal, - permanent: true, - }, - { - okToIngest: true, - expectedCode: codes.OK, - }, - } - - addr := testutil.GetAvailableLocalAddress(t) - td := testdata.GenerateTraces(1) - - tt, err := componenttest.SetupTelemetry(otlpReceiverID) - require.NoError(t, err) - t.Cleanup(func() { require.NoError(t, tt.Shutdown(context.Background())) }) - - sink := &errOrSinkConsumer{TracesSink: new(consumertest.TracesSink)} - - recv := newGRPCReceiver(t, tt.TelemetrySettings(), addr, sink) - require.NotNil(t, recv) - require.NoError(t, recv.Start(context.Background(), componenttest.NewNopHost())) - t.Cleanup(func() { require.NoError(t, recv.Shutdown(context.Background())) }) - - cc, err := grpc.NewClient(addr, grpc.WithTransportCredentials(insecure.NewCredentials())) - require.NoError(t, err) - defer func() { - assert.NoError(t, cc.Close()) - }() - - for _, ingestionState := range ingestionStates { - if ingestionState.okToIngest { - sink.SetConsumeError(nil) - } else { - if ingestionState.permanent { - sink.SetConsumeError(consumererror.NewPermanent(errors.New("consumer error"))) - } else { - sink.SetConsumeError(errors.New("consumer error")) - } - } - - _, err = ptraceotlp.NewGRPCClient(cc).Export(context.Background(), ptraceotlp.NewExportRequestFromTraces(td)) - errStatus, ok := status.FromError(err) - require.True(t, ok) - assert.Equal(t, ingestionState.expectedCode, errStatus.Code()) - } - - require.Equal(t, expectedReceivedBatches, len(sink.AllTraces())) - - require.NoError(t, tt.CheckReceiverTraces("grpc", int64(expectedReceivedBatches), int64(expectedIngestionBlockedRPCs))) -} - -// TestOTLPReceiverHTTPTracesIngestTest checks that the HTTP trace receiver -// is returning the proper response (return and metrics) when the next consumer -// in the pipeline reports error. The test changes the responses returned by the -// next trace consumer, checks if data was passed down the pipeline and if -// proper metrics were recorded. It also uses all endpoints supported by the -// trace receiver. -func TestOTLPReceiverHTTPTracesIngestTest(t *testing.T) { - type ingestionStateTest struct { - okToIngest bool - err error - expectedCode codes.Code - expectedStatusCode int - } - - expectedReceivedBatches := 2 - expectedIngestionBlockedRPCs := 2 - ingestionStates := []ingestionStateTest{ - { - okToIngest: true, - expectedCode: codes.OK, - }, - { - okToIngest: false, - err: consumererror.NewPermanent(errors.New("consumer error")), - expectedCode: codes.Internal, - expectedStatusCode: http.StatusInternalServerError, - }, - { - okToIngest: false, - err: errors.New("consumer error"), - expectedCode: codes.Unavailable, - expectedStatusCode: http.StatusServiceUnavailable, - }, - { - okToIngest: true, - expectedCode: codes.OK, - }, - } - - addr := testutil.GetAvailableLocalAddress(t) - td := testdata.GenerateTraces(1) - - tt, err := componenttest.SetupTelemetry(otlpReceiverID) - require.NoError(t, err) - t.Cleanup(func() { require.NoError(t, tt.Shutdown(context.Background())) }) - - sink := &errOrSinkConsumer{TracesSink: new(consumertest.TracesSink)} - - recv := newHTTPReceiver(t, tt.TelemetrySettings(), addr, sink) - require.NotNil(t, recv) - require.NoError(t, recv.Start(context.Background(), componenttest.NewNopHost())) - t.Cleanup(func() { require.NoError(t, recv.Shutdown(context.Background())) }) - - for _, ingestionState := range ingestionStates { - if ingestionState.okToIngest { - sink.SetConsumeError(nil) - } else { - sink.SetConsumeError(ingestionState.err) - } - - pbMarshaler := ptrace.ProtoMarshaler{} - pbBytes, err := pbMarshaler.MarshalTraces(td) - require.NoError(t, err) - req, err := http.NewRequest(http.MethodPost, "http://"+addr+defaultTracesURLPath, bytes.NewReader(pbBytes)) - require.NoError(t, err) - req.Header.Set("Content-Type", pbContentType) - resp, err := http.DefaultClient.Do(req) - require.NoError(t, err) - respBytes, err := io.ReadAll(resp.Body) - require.NoError(t, err) - - if ingestionState.expectedCode == codes.OK { - require.Equal(t, 200, resp.StatusCode) - tr := ptraceotlp.NewExportResponse() - assert.NoError(t, tr.UnmarshalProto(respBytes)) - } else { - errStatus := &spb.Status{} - assert.NoError(t, proto.Unmarshal(respBytes, errStatus)) - assert.Equal(t, ingestionState.expectedStatusCode, resp.StatusCode) - assert.Equal(t, ingestionState.expectedCode, codes.Code(errStatus.Code)) - } - } - - require.Equal(t, expectedReceivedBatches, len(sink.AllTraces())) - - require.NoError(t, tt.CheckReceiverTraces("http", int64(expectedReceivedBatches), int64(expectedIngestionBlockedRPCs))) -} - -func TestGRPCInvalidTLSCredentials(t *testing.T) { - cfg := &Config{ - Protocols: Protocols{ - GRPC: &configgrpc.ServerConfig{ - NetAddr: confignet.AddrConfig{ - Endpoint: testutil.GetAvailableLocalAddress(t), - Transport: confignet.TransportTypeTCP, - }, - TLSSetting: &configtls.ServerConfig{ - Config: configtls.Config{ - CertFile: "willfail", - }, - }, - }, - }, - } - - r, err := NewFactory().CreateTracesReceiver( - context.Background(), - receivertest.NewNopCreateSettings(), - cfg, - consumertest.NewNop()) - require.NoError(t, err) - assert.NotNil(t, r) - - assert.EqualError(t, - r.Start(context.Background(), componenttest.NewNopHost()), - `failed to load TLS config: failed to load TLS cert and key: for auth via TLS, provide both certificate and key, or neither`) -} - -func TestGRPCMaxRecvSize(t *testing.T) { - addr := testutil.GetAvailableLocalAddress(t) - sink := newErrOrSinkConsumer() - - cfg := createDefaultConfig().(*Config) - cfg.GRPC.NetAddr.Endpoint = addr - cfg.HTTP = nil - recv := newReceiver(t, componenttest.NewNopTelemetrySettings(), cfg, otlpReceiverID, sink) - require.NoError(t, recv.Start(context.Background(), componenttest.NewNopHost())) - - cc, err := grpc.NewClient(addr, grpc.WithTransportCredentials(insecure.NewCredentials())) - require.NoError(t, err) - - td := testdata.GenerateTraces(50000) - require.Error(t, exportTraces(cc, td)) - assert.NoError(t, cc.Close()) - require.NoError(t, recv.Shutdown(context.Background())) - - cfg.GRPC.MaxRecvMsgSizeMiB = 100 - recv = newReceiver(t, componenttest.NewNopTelemetrySettings(), cfg, otlpReceiverID, sink) - require.NoError(t, recv.Start(context.Background(), componenttest.NewNopHost())) - t.Cleanup(func() { require.NoError(t, recv.Shutdown(context.Background())) }) - - cc, err = grpc.NewClient(addr, grpc.WithTransportCredentials(insecure.NewCredentials())) - require.NoError(t, err) - defer func() { - assert.NoError(t, cc.Close()) - }() - - td = testdata.GenerateTraces(50000) - require.NoError(t, exportTraces(cc, td)) - require.Len(t, sink.AllTraces(), 1) - assert.Equal(t, td, sink.AllTraces()[0]) -} - -func TestHTTPInvalidTLSCredentials(t *testing.T) { - cfg := &Config{ - Protocols: Protocols{ - HTTP: &HTTPConfig{ - ServerConfig: &confighttp.ServerConfig{ - Endpoint: testutil.GetAvailableLocalAddress(t), - TLSSetting: &configtls.ServerConfig{ - Config: configtls.Config{ - CertFile: "willfail", - }, - }, - }, - TracesURLPath: defaultTracesURLPath, - MetricsURLPath: defaultMetricsURLPath, - LogsURLPath: defaultLogsURLPath, - }, - }, - } - - // TLS is resolved during Start for HTTP. - r, err := NewFactory().CreateTracesReceiver( - context.Background(), - receivertest.NewNopCreateSettings(), - cfg, - consumertest.NewNop()) - require.NoError(t, err) - assert.NotNil(t, r) - assert.EqualError(t, r.Start(context.Background(), componenttest.NewNopHost()), - `failed to load TLS config: failed to load TLS cert and key: for auth via TLS, provide both certificate and key, or neither`) -} - -func testHTTPMaxRequestBodySize(t *testing.T, path string, contentType string, payload []byte, size int, expectedStatusCode int) { - addr := testutil.GetAvailableLocalAddress(t) - url := "http://" + addr + path - cfg := &Config{ - Protocols: Protocols{ - HTTP: &HTTPConfig{ - ServerConfig: &confighttp.ServerConfig{ - Endpoint: addr, - MaxRequestBodySize: int64(size), - }, - TracesURLPath: defaultTracesURLPath, - MetricsURLPath: defaultMetricsURLPath, - LogsURLPath: defaultLogsURLPath, - }, - }, - } - - recv := newReceiver(t, componenttest.NewNopTelemetrySettings(), cfg, otlpReceiverID, consumertest.NewNop()) - require.NoError(t, recv.Start(context.Background(), componenttest.NewNopHost())) - - req := createHTTPRequest(t, url, "", contentType, payload) - resp, err := http.DefaultClient.Do(req) - require.NoError(t, err) - _, err = io.ReadAll(resp.Body) - require.NoError(t, err) - require.Equal(t, expectedStatusCode, resp.StatusCode) - - require.NoError(t, recv.Shutdown(context.Background())) -} - -func TestHTTPMaxRequestBodySize(t *testing.T) { - dataReqs := generateDataRequests(t) - - for _, dr := range dataReqs { - testHTTPMaxRequestBodySize(t, dr.path, "application/json", dr.jsonBytes, len(dr.jsonBytes), 200) - testHTTPMaxRequestBodySize(t, dr.path, "application/json", dr.jsonBytes, len(dr.jsonBytes)-1, 400) - - testHTTPMaxRequestBodySize(t, dr.path, "application/x-protobuf", dr.protoBytes, len(dr.protoBytes), 200) - testHTTPMaxRequestBodySize(t, dr.path, "application/x-protobuf", dr.protoBytes, len(dr.protoBytes)-1, 400) - } -} - -func newGRPCReceiver(t *testing.T, settings component.TelemetrySettings, endpoint string, c consumertest.Consumer) component.Component { - cfg := createDefaultConfig().(*Config) - cfg.GRPC.NetAddr.Endpoint = endpoint - cfg.HTTP = nil - return newReceiver(t, settings, cfg, otlpReceiverID, c) -} - -func newHTTPReceiver(t *testing.T, settings component.TelemetrySettings, endpoint string, c consumertest.Consumer) component.Component { - cfg := createDefaultConfig().(*Config) - cfg.HTTP.Endpoint = endpoint - cfg.GRPC = nil - return newReceiver(t, settings, cfg, otlpReceiverID, c) -} - -func newReceiver(t *testing.T, settings component.TelemetrySettings, cfg *Config, id component.ID, c consumertest.Consumer) component.Component { - set := receivertest.NewNopCreateSettings() - set.TelemetrySettings = settings - set.ID = id - r, err := newOtlpReceiver(cfg, &set) - require.NoError(t, err) - r.registerTraceConsumer(c) - r.registerMetricsConsumer(c) - r.registerLogsConsumer(c) - return r -} - -type dataRequest struct { - data any - path string - jsonBytes []byte - protoBytes []byte -} - -func generateDataRequests(t *testing.T) []dataRequest { - return []dataRequest{generateTracesRequest(t), generateMetricsRequests(t), generateLogsRequest(t)} -} - -func generateTracesRequest(t *testing.T) dataRequest { - protoMarshaler := &ptrace.ProtoMarshaler{} - jsonMarshaler := &ptrace.JSONMarshaler{} - - td := testdata.GenerateTraces(2) - traceProto, err := protoMarshaler.MarshalTraces(td) - require.NoError(t, err) - - traceJSON, err := jsonMarshaler.MarshalTraces(td) - require.NoError(t, err) - - return dataRequest{data: td, path: defaultTracesURLPath, jsonBytes: traceJSON, protoBytes: traceProto} -} - -func generateMetricsRequests(t *testing.T) dataRequest { - protoMarshaler := &pmetric.ProtoMarshaler{} - jsonMarshaler := &pmetric.JSONMarshaler{} - - md := testdata.GenerateMetrics(2) - metricProto, err := protoMarshaler.MarshalMetrics(md) - require.NoError(t, err) - - metricJSON, err := jsonMarshaler.MarshalMetrics(md) - require.NoError(t, err) - - return dataRequest{data: md, path: defaultMetricsURLPath, jsonBytes: metricJSON, protoBytes: metricProto} -} - -func generateLogsRequest(t *testing.T) dataRequest { - protoMarshaler := &plog.ProtoMarshaler{} - jsonMarshaler := &plog.JSONMarshaler{} - - ld := testdata.GenerateLogs(2) - logProto, err := protoMarshaler.MarshalLogs(ld) - require.NoError(t, err) - - logJSON, err := jsonMarshaler.MarshalLogs(ld) - require.NoError(t, err) - - return dataRequest{data: ld, path: defaultLogsURLPath, jsonBytes: logJSON, protoBytes: logProto} -} - -func doHTTPRequest( - t *testing.T, - url string, - encoding string, - contentType string, - data []byte, - expectStatusCode int, -) []byte { - req := createHTTPRequest(t, url, encoding, contentType, data) - resp, err := http.DefaultClient.Do(req) - require.NoError(t, err) - - respBytes, err := io.ReadAll(resp.Body) - require.NoError(t, err) - - require.NoError(t, resp.Body.Close()) - // For cases like "application/json; charset=utf-8", the response will be only "application/json" - require.True(t, strings.HasPrefix(strings.ToLower(contentType), resp.Header.Get("Content-Type"))) - - if expectStatusCode == 0 { - require.Equal(t, http.StatusOK, resp.StatusCode) - } else { - require.Equal(t, expectStatusCode, resp.StatusCode) - } - - return respBytes -} - -func createHTTPRequest( - t *testing.T, - url string, - encoding string, - contentType string, - data []byte, -) *http.Request { - var buf *bytes.Buffer - switch encoding { - case "gzip": - buf = compressGzip(t, data) - case "zstd": - buf = compressZstd(t, data) - case "": - buf = bytes.NewBuffer(data) - default: - t.Fatalf("Unsupported compression type %v", encoding) - } - - req, err := http.NewRequest(http.MethodPost, url, buf) - require.NoError(t, err) - req.Header.Set("Content-Type", contentType) - req.Header.Set("Content-Encoding", encoding) - - return req -} - -func compressGzip(t *testing.T, body []byte) *bytes.Buffer { - var buf bytes.Buffer - - gw := gzip.NewWriter(&buf) - defer func() { - require.NoError(t, gw.Close()) - }() - - _, err := gw.Write(body) - require.NoError(t, err) - - return &buf -} - -func compressZstd(t *testing.T, body []byte) *bytes.Buffer { - var buf bytes.Buffer - - zw, err := zstd.NewWriter(&buf) - require.NoError(t, err) - - defer func() { - require.NoError(t, zw.Close()) - }() - - _, err = zw.Write(body) - require.NoError(t, err) - - return &buf -} - -type senderFunc func(td ptrace.Traces) - -func TestShutdown(t *testing.T) { - endpointGrpc := testutil.GetAvailableLocalAddress(t) - endpointHTTP := testutil.GetAvailableLocalAddress(t) - - nextSink := new(consumertest.TracesSink) - - // Create OTLP receiver with gRPC and HTTP protocols. - factory := NewFactory() - cfg := factory.CreateDefaultConfig().(*Config) - cfg.GRPC.NetAddr.Endpoint = endpointGrpc - cfg.HTTP.Endpoint = endpointHTTP - set := receivertest.NewNopCreateSettings() - set.ID = otlpReceiverID - r, err := NewFactory().CreateTracesReceiver( - context.Background(), - set, - cfg, - nextSink) - require.NoError(t, err) - require.NotNil(t, r) - require.NoError(t, r.Start(context.Background(), componenttest.NewNopHost())) - - conn, err := grpc.NewClient(endpointGrpc, grpc.WithTransportCredentials(insecure.NewCredentials())) - require.NoError(t, err) - t.Cleanup(func() { - require.NoError(t, conn.Close()) - }) - - doneSignalGrpc := make(chan bool) - doneSignalHTTP := make(chan bool) - - senderGrpc := func(td ptrace.Traces) { - // Ignore error, may be executed after the receiver shutdown. - _ = exportTraces(conn, td) - } - senderHTTP := func(td ptrace.Traces) { - // Send request via OTLP/HTTP. - marshaler := &ptrace.ProtoMarshaler{} - traceBytes, err2 := marshaler.MarshalTraces(td) - require.NoError(t, err2) - url := "http://" + endpointHTTP + defaultTracesURLPath - req := createHTTPRequest(t, url, "", "application/x-protobuf", traceBytes) - if resp, errResp := http.DefaultClient.Do(req); errResp == nil { - require.NoError(t, resp.Body.Close()) - } - } - - // Send traces to the receiver until we signal via done channel, and then - // send one more trace after that. - go generateTraces(senderGrpc, doneSignalGrpc) - go generateTraces(senderHTTP, doneSignalHTTP) - - // Wait until the receiver outputs anything to the sink. - assert.Eventually(t, func() bool { - return nextSink.SpanCount() > 0 - }, time.Second, 10*time.Millisecond) - - // Now shutdown the receiver, while continuing sending traces to it. - ctx, cancelFn := context.WithTimeout(context.Background(), 10*time.Second) - defer cancelFn() - assert.NoError(t, r.Shutdown(ctx)) - - // Remember how many spans the sink received. This number should not change after this - // point because after Shutdown() returns the component is not allowed to produce - // any more data. - sinkSpanCountAfterShutdown := nextSink.SpanCount() - - // Now signal to generateTraces to exit the main generation loop, then send - // one more trace and stop. - doneSignalGrpc <- true - doneSignalHTTP <- true - - // Wait until all follow up traces are sent. - <-doneSignalGrpc - <-doneSignalHTTP - - // The last, additional trace should not be received by sink, so the number of spans in - // the sink should not change. - assert.Equal(t, sinkSpanCountAfterShutdown, nextSink.SpanCount()) -} - -func generateTraces(senderFn senderFunc, doneSignal chan bool) { - // Continuously generate spans until signaled to stop. -loop: - for { - select { - case <-doneSignal: - break loop - default: - } - senderFn(testdata.GenerateTraces(1)) - } - - // After getting the signal to stop, send one more span and then - // finally stop. We should never receive this last span. - senderFn(testdata.GenerateTraces(1)) - - // Indicate that we are done. - close(doneSignal) -} - -func exportTraces(cc *grpc.ClientConn, td ptrace.Traces) error { - acc := ptraceotlp.NewGRPCClient(cc) - req := ptraceotlp.NewExportRequestFromTraces(td) - _, err := acc.Export(context.Background(), req) - - return err -} - -type errOrSinkConsumer struct { - consumertest.Consumer - *consumertest.TracesSink - *consumertest.MetricsSink - *consumertest.LogsSink - mu sync.Mutex - consumeError error // to be returned by ConsumeTraces, if set -} - -func newErrOrSinkConsumer() *errOrSinkConsumer { - return &errOrSinkConsumer{ - TracesSink: new(consumertest.TracesSink), - MetricsSink: new(consumertest.MetricsSink), - LogsSink: new(consumertest.LogsSink), - } -} - -// SetConsumeError sets an error that will be returned by the Consume function. -func (esc *errOrSinkConsumer) SetConsumeError(err error) { - esc.mu.Lock() - defer esc.mu.Unlock() - esc.consumeError = err -} - -func (esc *errOrSinkConsumer) Capabilities() consumer.Capabilities { - return consumer.Capabilities{MutatesData: false} -} - -// ConsumeTraces stores traces to this sink. -func (esc *errOrSinkConsumer) ConsumeTraces(ctx context.Context, td ptrace.Traces) error { - esc.mu.Lock() - defer esc.mu.Unlock() - - if esc.consumeError != nil { - return esc.consumeError - } - - return esc.TracesSink.ConsumeTraces(ctx, td) -} - -// ConsumeMetrics stores metrics to this sink. -func (esc *errOrSinkConsumer) ConsumeMetrics(ctx context.Context, md pmetric.Metrics) error { - esc.mu.Lock() - defer esc.mu.Unlock() - - if esc.consumeError != nil { - return esc.consumeError - } - - return esc.MetricsSink.ConsumeMetrics(ctx, md) -} - -// ConsumeLogs stores metrics to this sink. -func (esc *errOrSinkConsumer) ConsumeLogs(ctx context.Context, ld plog.Logs) error { - esc.mu.Lock() - defer esc.mu.Unlock() - - if esc.consumeError != nil { - return esc.consumeError - } - - return esc.LogsSink.ConsumeLogs(ctx, ld) -} - -// Reset deletes any stored in the sinks, resets error to nil. -func (esc *errOrSinkConsumer) Reset() { - esc.mu.Lock() - defer esc.mu.Unlock() - - esc.consumeError = nil - esc.TracesSink.Reset() - esc.MetricsSink.Reset() - esc.LogsSink.Reset() -} - -// Reset deletes any stored in the sinks, resets error to nil. -func (esc *errOrSinkConsumer) checkData(t *testing.T, data any, len int) { - switch data.(type) { - case ptrace.Traces: - allTraces := esc.TracesSink.AllTraces() - require.Len(t, allTraces, len) - if len > 0 { - require.Equal(t, allTraces[0], data) - } - case pmetric.Metrics: - allMetrics := esc.MetricsSink.AllMetrics() - require.Len(t, allMetrics, len) - if len > 0 { - require.Equal(t, allMetrics[0], data) - } - case plog.Logs: - allLogs := esc.LogsSink.AllLogs() - require.Len(t, allLogs, len) - if len > 0 { - require.Equal(t, allLogs[0], data) - } - } -}