diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 3a63212c94b..a96d1f8ede0 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -316,3 +316,13 @@ updates: schedule: day: sunday interval: weekly + - + package-ecosystem: gomod + directory: /exporters/otlp/otlpmetric/otlpmetrichttp + labels: + - dependencies + - go + - "Skip Changelog" + schedule: + day: sunday + interval: weekly diff --git a/CHANGELOG.md b/CHANGELOG.md index d826b6055fd..3891d7ba275 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ### Added +- Adds HTTP support for OTLP metrics exporter. (#2022) + ### Changed ### Deprecated diff --git a/bridge/opencensus/go.mod b/bridge/opencensus/go.mod index 602f2ebeb5e..76b1d5fc2d0 100644 --- a/bridge/opencensus/go.mod +++ b/bridge/opencensus/go.mod @@ -69,3 +69,5 @@ replace go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc => ../ replace go.opentelemetry.io/otel/exporters/stdout/stdoutmetric => ../../exporters/stdout/stdoutmetric replace go.opentelemetry.io/otel/exporters/stdout/stdouttrace => ../../exporters/stdout/stdouttrace + +replace go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp => ../../exporters/otlp/otlpmetric/otlpmetrichttp diff --git a/bridge/opentracing/go.mod b/bridge/opentracing/go.mod index 2a1804be783..ea1aae95fe6 100644 --- a/bridge/opentracing/go.mod +++ b/bridge/opentracing/go.mod @@ -65,3 +65,5 @@ replace go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc => ../ replace go.opentelemetry.io/otel/exporters/stdout/stdoutmetric => ../../exporters/stdout/stdoutmetric replace go.opentelemetry.io/otel/exporters/stdout/stdouttrace => ../../exporters/stdout/stdouttrace + +replace go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp => ../../exporters/otlp/otlpmetric/otlpmetrichttp diff --git a/example/jaeger/go.mod b/example/jaeger/go.mod index 3dbeab40727..c3b22a1485b 100644 --- a/example/jaeger/go.mod +++ b/example/jaeger/go.mod @@ -65,3 +65,5 @@ replace go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc => ../ replace go.opentelemetry.io/otel/exporters/stdout/stdoutmetric => ../../exporters/stdout/stdoutmetric replace go.opentelemetry.io/otel/exporters/stdout/stdouttrace => ../../exporters/stdout/stdouttrace + +replace go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp => ../../exporters/otlp/otlpmetric/otlpmetrichttp diff --git a/example/namedtracer/go.mod b/example/namedtracer/go.mod index 67038b8eefa..8e99b8fbab8 100644 --- a/example/namedtracer/go.mod +++ b/example/namedtracer/go.mod @@ -67,3 +67,5 @@ replace go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc => ../ replace go.opentelemetry.io/otel/exporters/stdout/stdoutmetric => ../../exporters/stdout/stdoutmetric replace go.opentelemetry.io/otel/exporters/stdout/stdouttrace => ../../exporters/stdout/stdouttrace + +replace go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp => ../../exporters/otlp/otlpmetric/otlpmetrichttp diff --git a/example/opencensus/go.mod b/example/opencensus/go.mod index 8c827b37317..75a94cea656 100644 --- a/example/opencensus/go.mod +++ b/example/opencensus/go.mod @@ -69,3 +69,5 @@ replace go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc => ../ replace go.opentelemetry.io/otel/exporters/stdout/stdoutmetric => ../../exporters/stdout/stdoutmetric replace go.opentelemetry.io/otel/exporters/stdout/stdouttrace => ../../exporters/stdout/stdouttrace + +replace go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp => ../../exporters/otlp/otlpmetric/otlpmetrichttp diff --git a/example/otel-collector/go.mod b/example/otel-collector/go.mod index edaab823b68..2a25d7bc657 100644 --- a/example/otel-collector/go.mod +++ b/example/otel-collector/go.mod @@ -68,3 +68,5 @@ replace go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc => ../ replace go.opentelemetry.io/otel/exporters/stdout/stdoutmetric => ../../exporters/stdout/stdoutmetric replace go.opentelemetry.io/otel/exporters/stdout/stdouttrace => ../../exporters/stdout/stdouttrace + +replace go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp => ../../exporters/otlp/otlpmetric/otlpmetrichttp diff --git a/example/passthrough/go.mod b/example/passthrough/go.mod index 0e4c3f19aa9..17f90cced7b 100644 --- a/example/passthrough/go.mod +++ b/example/passthrough/go.mod @@ -68,3 +68,5 @@ replace go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc => ../ replace go.opentelemetry.io/otel/exporters/stdout/stdoutmetric => ../../exporters/stdout/stdoutmetric replace go.opentelemetry.io/otel/exporters/stdout/stdouttrace => ../../exporters/stdout/stdouttrace + +replace go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp => ../../exporters/otlp/otlpmetric/otlpmetrichttp diff --git a/example/prometheus/go.mod b/example/prometheus/go.mod index ba2de1eb45c..b5765ab73ff 100644 --- a/example/prometheus/go.mod +++ b/example/prometheus/go.mod @@ -67,3 +67,5 @@ replace go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc => ../ replace go.opentelemetry.io/otel/exporters/stdout/stdoutmetric => ../../exporters/stdout/stdoutmetric replace go.opentelemetry.io/otel/exporters/stdout/stdouttrace => ../../exporters/stdout/stdouttrace + +replace go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp => ../../exporters/otlp/otlpmetric/otlpmetrichttp diff --git a/example/zipkin/go.mod b/example/zipkin/go.mod index d2b751c9d46..ed25ef8b1d5 100644 --- a/example/zipkin/go.mod +++ b/example/zipkin/go.mod @@ -66,3 +66,5 @@ replace go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc => ../ replace go.opentelemetry.io/otel/exporters/stdout/stdoutmetric => ../../exporters/stdout/stdoutmetric replace go.opentelemetry.io/otel/exporters/stdout/stdouttrace => ../../exporters/stdout/stdouttrace + +replace go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp => ../../exporters/otlp/otlpmetric/otlpmetrichttp diff --git a/exporters/jaeger/go.mod b/exporters/jaeger/go.mod index 44cabfa71f1..648a1756256 100644 --- a/exporters/jaeger/go.mod +++ b/exporters/jaeger/go.mod @@ -69,3 +69,5 @@ replace go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc => ../ replace go.opentelemetry.io/otel/exporters/stdout/stdoutmetric => ../stdout/stdoutmetric replace go.opentelemetry.io/otel/exporters/stdout/stdouttrace => ../stdout/stdouttrace + +replace go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp => ../otlp/otlpmetric/otlpmetrichttp diff --git a/exporters/otlp/otlpmetric/go.mod b/exporters/otlp/otlpmetric/go.mod index b7f34dca6e1..2e39775c5c6 100644 --- a/exporters/otlp/otlpmetric/go.mod +++ b/exporters/otlp/otlpmetric/go.mod @@ -75,3 +75,5 @@ replace go.opentelemetry.io/otel/exporters/zipkin => ../../zipkin replace go.opentelemetry.io/otel/exporters/stdout/stdoutmetric => ../../stdout/stdoutmetric replace go.opentelemetry.io/otel/exporters/stdout/stdouttrace => ../../stdout/stdouttrace + +replace go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp => ./otlpmetrichttp diff --git a/exporters/otlp/otlpmetric/internal/otlpconfig/options.go b/exporters/otlp/otlpmetric/internal/otlpconfig/options.go index 9860e2268a5..cc46fc775ca 100644 --- a/exporters/otlp/otlpmetric/internal/otlpconfig/options.go +++ b/exporters/otlp/otlpmetric/internal/otlpconfig/options.go @@ -24,9 +24,16 @@ import ( ) const ( + // DefaultMaxAttempts describes how many times the driver + // should retry the sending of the payload in case of a + // retryable error. + DefaultMaxAttempts int = 5 // DefaultMetricsPath is a default URL path for endpoint that // receives metrics. DefaultMetricsPath string = "/v1/metrics" + // DefaultBackoff is a default base backoff time used in the + // exponential backoff strategy. + DefaultBackoff time.Duration = 300 * time.Millisecond // DefaultTimeout is a default max waiting time for the backend to process // each span or metrics batch. DefaultTimeout time.Duration = 10 * time.Second @@ -60,6 +67,10 @@ type ( // Signal specific configurations Metrics SignalConfig + // HTTP configurations + MaxAttempts int + Backoff time.Duration + // gRPC configurations ReconnectionPeriod time.Duration ServiceConfig string @@ -243,3 +254,15 @@ func WithTimeout(duration time.Duration) GenericOption { cfg.Metrics.Timeout = duration }) } + +func WithMaxAttempts(maxAttempts int) GenericOption { + return newGenericOption(func(cfg *Config) { + cfg.MaxAttempts = maxAttempts + }) +} + +func WithBackoff(duration time.Duration) GenericOption { + return newGenericOption(func(cfg *Config) { + cfg.Backoff = duration + }) +} diff --git a/exporters/otlp/otlpmetric/internal/otlpconfig/options_test.go b/exporters/otlp/otlpmetric/internal/otlpconfig/options_test.go index a4b2bb80ff9..3f82f633faf 100644 --- a/exporters/otlp/otlpmetric/internal/otlpconfig/options_test.go +++ b/exporters/otlp/otlpmetric/internal/otlpconfig/options_test.go @@ -321,6 +321,19 @@ func TestConfigs(t *testing.T) { assert.Equal(t, otlpconfig.GzipCompression, c.Metrics.Compression) }, }, + { + name: "Test Mixed Environment and With Compression", + opts: []otlpconfig.GenericOption{ + otlpconfig.WithCompression(otlpconfig.NoCompression), + }, + env: map[string]string{ + "OTEL_EXPORTER_OTLP_METRICS_COMPRESSION": "gzip", + }, + asserts: func(t *testing.T, c *otlpconfig.Config, grpcOption bool) { + assert.Equal(t, otlpconfig.NoCompression, c.Metrics.Compression) + }, + }, + // Timeout Tests { name: "Test With Timeout", diff --git a/exporters/otlp/otlpmetric/otlpmetricgrpc/go.mod b/exporters/otlp/otlpmetric/otlpmetricgrpc/go.mod index 7c0b2132a45..4fdc964c4b4 100644 --- a/exporters/otlp/otlpmetric/otlpmetricgrpc/go.mod +++ b/exporters/otlp/otlpmetric/otlpmetricgrpc/go.mod @@ -73,3 +73,5 @@ replace go.opentelemetry.io/otel/exporters/zipkin => ../../../zipkin replace go.opentelemetry.io/otel/exporters/stdout/stdoutmetric => ../../../stdout/stdoutmetric replace go.opentelemetry.io/otel/exporters/stdout/stdouttrace => ../../../stdout/stdouttrace + +replace go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp => ../otlpmetrichttp diff --git a/exporters/otlp/otlpmetric/otlpmetrichttp/certificate_test.go b/exporters/otlp/otlpmetric/otlpmetrichttp/certificate_test.go new file mode 100644 index 00000000000..d75547f6e4c --- /dev/null +++ b/exporters/otlp/otlpmetric/otlpmetrichttp/certificate_test.go @@ -0,0 +1,92 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package otlpmetrichttp_test + +import ( + "bytes" + "crypto/ecdsa" + "crypto/elliptic" + cryptorand "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "math/big" + mathrand "math/rand" + "net" + "time" +) + +type mathRandReader struct{} + +func (mathRandReader) Read(p []byte) (n int, err error) { + return mathrand.Read(p) +} + +var randReader mathRandReader + +type pemCertificate struct { + Certificate []byte + PrivateKey []byte +} + +// Based on https://golang.org/src/crypto/tls/generate_cert.go, +// simplified and weakened. +func generateWeakCertificate() (*pemCertificate, error) { + priv, err := ecdsa.GenerateKey(elliptic.P256(), randReader) + if err != nil { + return nil, err + } + keyUsage := x509.KeyUsageDigitalSignature + notBefore := time.Now() + notAfter := notBefore.Add(time.Hour) + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) + serialNumber, err := cryptorand.Int(randReader, serialNumberLimit) + if err != nil { + return nil, err + } + template := x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + Organization: []string{"otel-go"}, + }, + NotBefore: notBefore, + NotAfter: notAfter, + KeyUsage: keyUsage, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + DNSNames: []string{"localhost"}, + IPAddresses: []net.IP{net.IPv6loopback, net.IPv4(127, 0, 0, 1)}, + } + derBytes, err := x509.CreateCertificate(randReader, &template, &template, &priv.PublicKey, priv) + if err != nil { + return nil, err + } + certificateBuffer := new(bytes.Buffer) + if err := pem.Encode(certificateBuffer, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}); err != nil { + return nil, err + } + privDERBytes, err := x509.MarshalPKCS8PrivateKey(priv) + if err != nil { + return nil, err + } + privBuffer := new(bytes.Buffer) + if err := pem.Encode(privBuffer, &pem.Block{Type: "PRIVATE KEY", Bytes: privDERBytes}); err != nil { + return nil, err + } + return &pemCertificate{ + Certificate: certificateBuffer.Bytes(), + PrivateKey: privBuffer.Bytes(), + }, nil +} diff --git a/exporters/otlp/otlpmetric/otlpmetrichttp/client.go b/exporters/otlp/otlpmetric/otlpmetrichttp/client.go new file mode 100644 index 00000000000..ce933c5651e --- /dev/null +++ b/exporters/otlp/otlpmetric/otlpmetrichttp/client.go @@ -0,0 +1,278 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package otlpmetrichttp + +import ( + "bytes" + "compress/gzip" + "context" + "fmt" + "io" + "io/ioutil" + "math/rand" + "net" + "net/http" + "path" + "strings" + "time" + + "go.opentelemetry.io/otel/exporters/otlp/otlpmetric" + metricpb "go.opentelemetry.io/proto/otlp/metrics/v1" + + "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/internal/otlpconfig" + + "google.golang.org/protobuf/proto" + + "go.opentelemetry.io/otel" + colmetricpb "go.opentelemetry.io/proto/otlp/collector/metrics/v1" +) + +const contentTypeProto = "application/x-protobuf" + +// Keep it in sync with golang's DefaultTransport from net/http! We +// have our own copy to avoid handling a situation where the +// DefaultTransport is overwritten with some different implementation +// of http.RoundTripper or it's modified by other package. +var ourTransport = &http.Transport{ + Proxy: http.ProxyFromEnvironment, + DialContext: (&net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + }).DialContext, + ForceAttemptHTTP2: true, + MaxIdleConns: 100, + IdleConnTimeout: 90 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: 1 * time.Second, +} + +type client struct { + name string + cfg otlpconfig.SignalConfig + generalCfg otlpconfig.Config + client *http.Client + stopCh chan struct{} +} + +// NewClient creates a new HTTP metric client. +func NewClient(opts ...Option) otlpmetric.Client { + cfg := otlpconfig.NewDefaultConfig() + otlpconfig.ApplyHTTPEnvConfigs(&cfg) + for _, opt := range opts { + opt.applyHTTPOption(&cfg) + } + + for pathPtr, defaultPath := range map[*string]string{ + &cfg.Metrics.URLPath: defaultMetricsPath, + } { + tmp := strings.TrimSpace(*pathPtr) + if tmp == "" { + tmp = defaultPath + } else { + tmp = path.Clean(tmp) + if !path.IsAbs(tmp) { + tmp = fmt.Sprintf("/%s", tmp) + } + } + *pathPtr = tmp + } + if cfg.MaxAttempts <= 0 { + cfg.MaxAttempts = defaultMaxAttempts + } + if cfg.MaxAttempts > defaultMaxAttempts { + cfg.MaxAttempts = defaultMaxAttempts + } + if cfg.Backoff <= 0 { + cfg.Backoff = defaultBackoff + } + + httpClient := &http.Client{ + Transport: ourTransport, + Timeout: cfg.Metrics.Timeout, + } + if cfg.Metrics.TLSCfg != nil { + transport := ourTransport.Clone() + transport.TLSClientConfig = cfg.Metrics.TLSCfg + httpClient.Transport = transport + } + + stopCh := make(chan struct{}) + return &client{ + name: "metrics", + cfg: cfg.Metrics, + generalCfg: cfg, + stopCh: stopCh, + client: httpClient, + } +} + +// Start does nothing in a HTTP client +func (d *client) Start(ctx context.Context) error { + // nothing to do + select { + case <-ctx.Done(): + return ctx.Err() + default: + } + return nil +} + +// Stop shuts down the client and interrupt any in-flight request. +func (d *client) Stop(ctx context.Context) error { + close(d.stopCh) + select { + case <-ctx.Done(): + return ctx.Err() + default: + } + return nil +} + +// UploadMetrics sends a batch of metrics to the collector. +func (d *client) UploadMetrics(ctx context.Context, protoMetrics []*metricpb.ResourceMetrics) error { + pbRequest := &colmetricpb.ExportMetricsServiceRequest{ + ResourceMetrics: protoMetrics, + } + rawRequest, err := proto.Marshal(pbRequest) + if err != nil { + return err + } + return d.send(ctx, rawRequest) +} + +func (d *client) send(ctx context.Context, rawRequest []byte) error { + address := fmt.Sprintf("%s://%s%s", d.getScheme(), d.cfg.Endpoint, d.cfg.URLPath) + var cancel context.CancelFunc + ctx, cancel = d.contextWithStop(ctx) + defer cancel() + for i := 0; i < d.generalCfg.MaxAttempts; i++ { + response, err := d.singleSend(ctx, rawRequest, address) + if err != nil { + return err + } + // We don't care about the body, so try to read it + // into /dev/null and close it immediately. The + // reading part is to facilitate connection reuse. + _, _ = io.Copy(ioutil.Discard, response.Body) + _ = response.Body.Close() + switch response.StatusCode { + case http.StatusOK: + return nil + case http.StatusTooManyRequests: + fallthrough + case http.StatusServiceUnavailable: + select { + case <-time.After(getWaitDuration(d.generalCfg.Backoff, i)): + continue + case <-ctx.Done(): + return ctx.Err() + } + default: + return fmt.Errorf("failed to send %s to %s with HTTP status %s", d.name, address, response.Status) + } + } + return fmt.Errorf("failed to send data to %s after %d tries", address, d.generalCfg.MaxAttempts) +} + +func (d *client) getScheme() string { + if d.cfg.Insecure { + return "http" + } + return "https" +} + +func getWaitDuration(backoff time.Duration, i int) time.Duration { + // Strategy: after nth failed attempt, attempt resending after + // k * initialBackoff + jitter, where k is a random number in + // range [0, 2^n-1), and jitter is a random percentage of + // initialBackoff from [-5%, 5%). + // + // Based on + // https://en.wikipedia.org/wiki/Exponential_backoff#Example_exponential_backoff_algorithm + // + // Jitter is our addition. + + // There won't be an overflow, since i is capped to + // defaultMaxAttempts (5). + upperK := (int64)(1) << (i + 1) + jitterPercent := (rand.Float64() - 0.5) / 10. + jitter := jitterPercent * (float64)(backoff) + k := rand.Int63n(upperK) + return (time.Duration)(k)*backoff + (time.Duration)(jitter) +} + +func (d *client) contextWithStop(ctx context.Context) (context.Context, context.CancelFunc) { + // Unify the parent context Done signal with the client's stop + // channel. + ctx, cancel := context.WithCancel(ctx) + go func(ctx context.Context, cancel context.CancelFunc) { + select { + case <-ctx.Done(): + // Nothing to do, either cancelled or deadline + // happened. + case <-d.stopCh: + cancel() + } + }(ctx, cancel) + return ctx, cancel +} + +func (d *client) singleSend(ctx context.Context, rawRequest []byte, address string) (*http.Response, error) { + request, err := http.NewRequestWithContext(ctx, http.MethodPost, address, nil) + if err != nil { + return nil, err + } + bodyReader, contentLength, headers := d.prepareBody(rawRequest) + // Not closing bodyReader through defer, the HTTP Client's + // Transport will do it for us + request.Body = bodyReader + request.ContentLength = contentLength + for key, values := range headers { + for _, value := range values { + request.Header.Add(key, value) + } + } + return d.client.Do(request) +} + +func (d *client) prepareBody(rawRequest []byte) (io.ReadCloser, int64, http.Header) { + var bodyReader io.ReadCloser + headers := http.Header{} + for k, v := range d.cfg.Headers { + headers.Set(k, v) + } + contentLength := (int64)(len(rawRequest)) + headers.Set("Content-Type", contentTypeProto) + requestReader := bytes.NewBuffer(rawRequest) + switch Compression(d.cfg.Compression) { + case NoCompression: + bodyReader = ioutil.NopCloser(requestReader) + case GzipCompression: + preader, pwriter := io.Pipe() + go func() { + defer pwriter.Close() + gzipper := gzip.NewWriter(pwriter) + defer gzipper.Close() + _, err := io.Copy(gzipper, requestReader) + if err != nil { + otel.Handle(fmt.Errorf("otlphttp: failed to gzip request: %v", err)) + } + }() + headers.Set("Content-Encoding", "gzip") + bodyReader = preader + contentLength = -1 + } + return bodyReader, contentLength, headers +} diff --git a/exporters/otlp/otlpmetric/otlpmetrichttp/client_test.go b/exporters/otlp/otlpmetric/otlpmetrichttp/client_test.go new file mode 100644 index 00000000000..763a3475d42 --- /dev/null +++ b/exporters/otlp/otlpmetric/otlpmetrichttp/client_test.go @@ -0,0 +1,430 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package otlpmetrichttp_test + +import ( + "context" + "fmt" + "net/http" + "os" + "testing" + "time" + + "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "go.opentelemetry.io/otel/exporters/otlp/otlpmetric" + "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/internal/otlpmetrictest" +) + +const ( + relOtherMetricsPath = "post/metrics/here" + otherMetricsPath = "/post/metrics/here" +) + +var ( + oneRecord = otlpmetrictest.OneRecordCheckpointSet{} +) + +var ( + testHeaders = map[string]string{ + "Otel-Go-Key-1": "somevalue", + "Otel-Go-Key-2": "someothervalue", + } +) + +func TestEndToEnd(t *testing.T) { + tests := []struct { + name string + opts []otlpmetrichttp.Option + mcCfg mockCollectorConfig + tls bool + }{ + { + name: "no extra options", + opts: nil, + }, + { + name: "with gzip compression", + opts: []otlpmetrichttp.Option{ + otlpmetrichttp.WithCompression(otlpmetrichttp.GzipCompression), + }, + }, + { + name: "with empty paths (forced to defaults)", + opts: []otlpmetrichttp.Option{ + otlpmetrichttp.WithURLPath(""), + }, + }, + { + name: "with relative paths", + opts: []otlpmetrichttp.Option{ + otlpmetrichttp.WithURLPath(relOtherMetricsPath), + }, + mcCfg: mockCollectorConfig{ + MetricsURLPath: otherMetricsPath, + }, + }, + { + name: "with TLS", + opts: nil, + mcCfg: mockCollectorConfig{ + WithTLS: true, + }, + tls: true, + }, + { + name: "with extra headers", + opts: []otlpmetrichttp.Option{ + otlpmetrichttp.WithHeaders(testHeaders), + }, + mcCfg: mockCollectorConfig{ + ExpectedHeaders: testHeaders, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + mc := runMockCollector(t, tc.mcCfg) + defer mc.MustStop(t) + allOpts := []otlpmetrichttp.Option{ + otlpmetrichttp.WithEndpoint(mc.Endpoint()), + } + if tc.tls { + tlsConfig := mc.ClientTLSConfig() + require.NotNil(t, tlsConfig) + allOpts = append(allOpts, otlpmetrichttp.WithTLSClientConfig(tlsConfig)) + } else { + allOpts = append(allOpts, otlpmetrichttp.WithInsecure()) + } + allOpts = append(allOpts, tc.opts...) + client := otlpmetrichttp.NewClient(allOpts...) + ctx := context.Background() + exporter, err := otlpmetric.New(ctx, client) + if assert.NoError(t, err) { + defer func() { + assert.NoError(t, exporter.Shutdown(ctx)) + }() + otlpmetrictest.RunEndToEndTest(ctx, t, exporter, mc) + } + }) + } +} + +func TestExporterShutdown(t *testing.T) { + mc := runMockCollector(t, mockCollectorConfig{}) + defer func() { + _ = mc.Stop() + }() + + <-time.After(5 * time.Millisecond) + + otlpmetrictest.RunExporterShutdownTest(t, func() otlpmetric.Client { + return otlpmetrichttp.NewClient( + otlpmetrichttp.WithInsecure(), + otlpmetrichttp.WithEndpoint(mc.endpoint), + ) + }) +} + +func TestRetry(t *testing.T) { + statuses := []int{ + http.StatusTooManyRequests, + http.StatusServiceUnavailable, + } + mcCfg := mockCollectorConfig{ + InjectHTTPStatus: statuses, + } + mc := runMockCollector(t, mcCfg) + defer mc.MustStop(t) + client := otlpmetrichttp.NewClient( + otlpmetrichttp.WithEndpoint(mc.Endpoint()), + otlpmetrichttp.WithInsecure(), + otlpmetrichttp.WithMaxAttempts(len(statuses)+1), + ) + ctx := context.Background() + exporter, err := otlpmetric.New(ctx, client) + require.NoError(t, err) + defer func() { + assert.NoError(t, exporter.Shutdown(ctx)) + }() + err = exporter.Export(ctx, oneRecord) + assert.NoError(t, err) + assert.Len(t, mc.GetMetrics(), 1) +} + +func TestTimeout(t *testing.T) { + mcCfg := mockCollectorConfig{ + InjectDelay: 100 * time.Millisecond, + } + mc := runMockCollector(t, mcCfg) + defer mc.MustStop(t) + client := otlpmetrichttp.NewClient( + otlpmetrichttp.WithEndpoint(mc.Endpoint()), + otlpmetrichttp.WithInsecure(), + otlpmetrichttp.WithTimeout(50*time.Millisecond), + ) + ctx := context.Background() + exporter, err := otlpmetric.New(ctx, client) + require.NoError(t, err) + defer func() { + assert.NoError(t, exporter.Shutdown(ctx)) + }() + err = exporter.Export(ctx, oneRecord) + assert.Equal(t, true, os.IsTimeout(err)) +} + +func TestRetryFailed(t *testing.T) { + statuses := []int{ + http.StatusTooManyRequests, + http.StatusServiceUnavailable, + } + mcCfg := mockCollectorConfig{ + InjectHTTPStatus: statuses, + } + mc := runMockCollector(t, mcCfg) + defer mc.MustStop(t) + driver := otlpmetrichttp.NewClient( + otlpmetrichttp.WithEndpoint(mc.Endpoint()), + otlpmetrichttp.WithInsecure(), + otlpmetrichttp.WithMaxAttempts(1), + ) + ctx := context.Background() + exporter, err := otlpmetric.New(ctx, driver) + require.NoError(t, err) + defer func() { + assert.NoError(t, exporter.Shutdown(ctx)) + }() + err = exporter.Export(ctx, oneRecord) + assert.Error(t, err) + assert.Empty(t, mc.GetMetrics()) +} + +func TestNoRetry(t *testing.T) { + statuses := []int{ + http.StatusBadRequest, + } + mcCfg := mockCollectorConfig{ + InjectHTTPStatus: statuses, + } + mc := runMockCollector(t, mcCfg) + defer mc.MustStop(t) + driver := otlpmetrichttp.NewClient( + otlpmetrichttp.WithEndpoint(mc.Endpoint()), + otlpmetrichttp.WithInsecure(), + otlpmetrichttp.WithMaxAttempts(len(statuses)+1), + ) + ctx := context.Background() + exporter, err := otlpmetric.New(ctx, driver) + require.NoError(t, err) + defer func() { + assert.NoError(t, exporter.Shutdown(ctx)) + }() + err = exporter.Export(ctx, oneRecord) + assert.Error(t, err) + assert.Equal(t, fmt.Sprintf("failed to send metrics to http://%s/v1/metrics with HTTP status 400 Bad Request", mc.endpoint), err.Error()) + assert.Empty(t, mc.GetMetrics()) +} + +func TestEmptyData(t *testing.T) { + mcCfg := mockCollectorConfig{} + mc := runMockCollector(t, mcCfg) + defer mc.MustStop(t) + driver := otlpmetrichttp.NewClient( + otlpmetrichttp.WithEndpoint(mc.Endpoint()), + otlpmetrichttp.WithInsecure(), + ) + ctx := context.Background() + exporter, err := otlpmetric.New(ctx, driver) + require.NoError(t, err) + defer func() { + assert.NoError(t, exporter.Shutdown(ctx)) + }() + assert.NoError(t, err) + err = exporter.Export(ctx, oneRecord) + assert.NoError(t, err) + assert.NotEmpty(t, mc.GetMetrics()) +} + +func TestUnreasonableMaxAttempts(t *testing.T) { + // Max attempts is 5, we set collector to fail 7 times and try + // to configure max attempts to be either negative or too + // large. Since we set max attempts to 5 in such cases, + // exporting to the collector should fail. + type testcase struct { + name string + maxAttempts int + } + for _, tc := range []testcase{ + { + name: "negative max attempts", + maxAttempts: -3, + }, + { + name: "too large max attempts", + maxAttempts: 10, + }, + } { + t.Run(tc.name, func(t *testing.T) { + statuses := make([]int, 0, 7) + for i := 0; i < cap(statuses); i++ { + statuses = append(statuses, http.StatusTooManyRequests) + } + mcCfg := mockCollectorConfig{ + InjectHTTPStatus: statuses, + } + mc := runMockCollector(t, mcCfg) + defer mc.MustStop(t) + driver := otlpmetrichttp.NewClient( + otlpmetrichttp.WithEndpoint(mc.Endpoint()), + otlpmetrichttp.WithInsecure(), + otlpmetrichttp.WithMaxAttempts(tc.maxAttempts), + otlpmetrichttp.WithBackoff(time.Millisecond), + ) + ctx := context.Background() + exporter, err := otlpmetric.New(ctx, driver) + require.NoError(t, err) + defer func() { + assert.NoError(t, exporter.Shutdown(ctx)) + }() + err = exporter.Export(ctx, oneRecord) + assert.Error(t, err) + assert.Empty(t, mc.GetMetrics()) + }) + } +} + +func TestUnreasonableBackoff(t *testing.T) { + // This sets backoff to negative value, which gets corrected + // to default backoff instead of being used. Default max + // attempts is 5, so we set the collector to fail 4 times, but + // we set the deadline to 3 times of the default backoff, so + // this should show that deadline is not met, meaning that the + // retries weren't immediate (as negative backoff could + // imply). + statuses := make([]int, 0, 4) + for i := 0; i < cap(statuses); i++ { + statuses = append(statuses, http.StatusTooManyRequests) + } + mcCfg := mockCollectorConfig{ + InjectHTTPStatus: statuses, + } + mc := runMockCollector(t, mcCfg) + defer mc.MustStop(t) + driver := otlpmetrichttp.NewClient( + otlpmetrichttp.WithEndpoint(mc.Endpoint()), + otlpmetrichttp.WithInsecure(), + otlpmetrichttp.WithBackoff(-time.Millisecond), + ) + ctx, cancel := context.WithTimeout(context.Background(), 3*(300*time.Millisecond)) + defer cancel() + exporter, err := otlpmetric.New(ctx, driver) + require.NoError(t, err) + defer func() { + assert.NoError(t, exporter.Shutdown(context.Background())) + }() + err = exporter.Export(ctx, oneRecord) + assert.Error(t, err) + assert.Empty(t, mc.GetMetrics()) +} + +func TestCancelledContext(t *testing.T) { + statuses := []int{ + http.StatusBadRequest, + } + mcCfg := mockCollectorConfig{ + InjectHTTPStatus: statuses, + } + mc := runMockCollector(t, mcCfg) + defer mc.MustStop(t) + driver := otlpmetrichttp.NewClient( + otlpmetrichttp.WithEndpoint(mc.Endpoint()), + otlpmetrichttp.WithInsecure(), + ) + ctx, cancel := context.WithCancel(context.Background()) + exporter, err := otlpmetric.New(ctx, driver) + require.NoError(t, err) + defer func() { + assert.NoError(t, exporter.Shutdown(context.Background())) + }() + cancel() + _ = exporter.Export(ctx, oneRecord) + assert.Empty(t, mc.GetMetrics()) +} + +func TestDeadlineContext(t *testing.T) { + statuses := make([]int, 0, 5) + for i := 0; i < cap(statuses); i++ { + statuses = append(statuses, http.StatusTooManyRequests) + } + mcCfg := mockCollectorConfig{ + InjectHTTPStatus: statuses, + } + mc := runMockCollector(t, mcCfg) + defer mc.MustStop(t) + driver := otlpmetrichttp.NewClient( + otlpmetrichttp.WithEndpoint(mc.Endpoint()), + otlpmetrichttp.WithInsecure(), + otlpmetrichttp.WithBackoff(time.Minute), + ) + ctx := context.Background() + exporter, err := otlpmetric.New(ctx, driver) + require.NoError(t, err) + defer func() { + assert.NoError(t, exporter.Shutdown(context.Background())) + }() + ctx, cancel := context.WithTimeout(ctx, time.Second) + defer cancel() + err = exporter.Export(ctx, oneRecord) + assert.Error(t, err) + assert.Empty(t, mc.GetMetrics()) +} + +func TestStopWhileExporting(t *testing.T) { + statuses := make([]int, 0, 5) + for i := 0; i < cap(statuses); i++ { + statuses = append(statuses, http.StatusTooManyRequests) + } + mcCfg := mockCollectorConfig{ + InjectHTTPStatus: statuses, + } + mc := runMockCollector(t, mcCfg) + defer mc.MustStop(t) + driver := otlpmetrichttp.NewClient( + otlpmetrichttp.WithEndpoint(mc.Endpoint()), + otlpmetrichttp.WithInsecure(), + otlpmetrichttp.WithBackoff(time.Minute), + ) + ctx := context.Background() + exporter, err := otlpmetric.New(ctx, driver) + require.NoError(t, err) + defer func() { + assert.NoError(t, exporter.Shutdown(ctx)) + }() + doneCh := make(chan struct{}) + go func() { + err := exporter.Export(ctx, oneRecord) + assert.Error(t, err) + assert.Empty(t, mc.GetMetrics()) + close(doneCh) + }() + <-time.After(time.Second) + err = exporter.Shutdown(ctx) + assert.NoError(t, err) + <-doneCh +} diff --git a/exporters/otlp/otlpmetric/otlpmetrichttp/doc.go b/exporters/otlp/otlpmetric/otlpmetrichttp/doc.go new file mode 100644 index 00000000000..d096388320d --- /dev/null +++ b/exporters/otlp/otlpmetric/otlpmetrichttp/doc.go @@ -0,0 +1,23 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/* +Package otlpmetrichttp provides a client that sends metrics to the collector +using HTTP with binary protobuf payloads. + +This package is currently in a pre-GA phase. Backwards incompatible changes +may be introduced in subsequent minor version releases as we work to track the +evolving OpenTelemetry specification and user feedback. +*/ +package otlpmetrichttp // import "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp" diff --git a/exporters/otlp/otlpmetric/otlpmetrichttp/exporter.go b/exporters/otlp/otlpmetric/otlpmetrichttp/exporter.go new file mode 100644 index 00000000000..de09e7cdcaa --- /dev/null +++ b/exporters/otlp/otlpmetric/otlpmetrichttp/exporter.go @@ -0,0 +1,31 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package otlpmetrichttp // import "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp" + +import ( + "context" + + "go.opentelemetry.io/otel/exporters/otlp/otlpmetric" +) + +// New constructs a new Exporter and starts it. +func New(ctx context.Context, opts ...Option) (*otlpmetric.Exporter, error) { + return otlpmetric.New(ctx, NewClient(opts...)) +} + +// NewUnstarted constructs a new Exporter and does not start it. +func NewUnstarted(opts ...Option) *otlpmetric.Exporter { + return otlpmetric.NewUnstarted(NewClient(opts...)) +} diff --git a/exporters/otlp/otlpmetric/otlpmetrichttp/go.mod b/exporters/otlp/otlpmetric/otlpmetrichttp/go.mod new file mode 100644 index 00000000000..15b08668ad2 --- /dev/null +++ b/exporters/otlp/otlpmetric/otlpmetrichttp/go.mod @@ -0,0 +1,79 @@ +module go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp + +go 1.15 + +require ( + github.com/stretchr/testify v1.7.0 + go.opentelemetry.io/otel v1.0.0-RC1 + go.opentelemetry.io/otel/exporters/otlp/otlpmetric v0.21.0 + go.opentelemetry.io/proto/otlp v0.9.0 + google.golang.org/protobuf v1.26.0 +) + +replace go.opentelemetry.io/otel => ../../../.. + +replace go.opentelemetry.io/otel/sdk => ../../../../sdk + +replace go.opentelemetry.io/otel/sdk/metric => ../../../../sdk/metric + +replace go.opentelemetry.io/otel/exporters/otlp => ../.. + +replace go.opentelemetry.io/otel/exporters/otlp/otlpmetric => ../ + +replace go.opentelemetry.io/otel/metric => ../../../../metric + +replace go.opentelemetry.io/otel/oteltest => ../../../../oteltest + +replace go.opentelemetry.io/otel/trace => ../../../../trace + +replace go.opentelemetry.io/otel/bridge/opencensus => ../../../../bridge/opencensus + +replace go.opentelemetry.io/otel/bridge/opentracing => ../../../../bridge/opentracing + +replace go.opentelemetry.io/otel/example/jaeger => ../../../../example/jaeger + +replace go.opentelemetry.io/otel/example/namedtracer => ../../../../example/namedtracer + +replace go.opentelemetry.io/otel/example/opencensus => ../../../../example/opencensus + +replace go.opentelemetry.io/otel/example/otel-collector => ../../../../example/otel-collector + +replace go.opentelemetry.io/otel/example/passthrough => ../../../../example/passthrough + +replace go.opentelemetry.io/otel/example/prom-collector => ../../../../example/prom-collector + +replace go.opentelemetry.io/otel/example/prometheus => ../../../../example/prometheus + +replace go.opentelemetry.io/otel/example/zipkin => ../../../../example/zipkin + +replace go.opentelemetry.io/otel/exporters/metric/prometheus => ../../../metric/prometheus + +replace go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp => ./ + +replace go.opentelemetry.io/otel/exporters/otlp/otlptrace => ../../otlptrace + +replace go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc => ../../otlptrace/otlptracegrpc + +replace go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp => ../../otlptrace/otlptracehttp + +replace go.opentelemetry.io/otel/exporters/trace/jaeger => ../../../trace/jaeger + +replace go.opentelemetry.io/otel/exporters/trace/zipkin => ../../../trace/zipkin + +replace go.opentelemetry.io/otel/internal/tools => ../../../../internal/tools + +replace go.opentelemetry.io/otel/sdk/export/metric => ../../../../sdk/export/metric + +replace go.opentelemetry.io/otel/internal/metric => ../../../../internal/metric + +replace go.opentelemetry.io/otel/exporters/jaeger => ../../../jaeger + +replace go.opentelemetry.io/otel/exporters/prometheus => ../../../prometheus + +replace go.opentelemetry.io/otel/exporters/zipkin => ../../../zipkin + +replace go.opentelemetry.io/otel/exporters/stdout/stdoutmetric => ../../../stdout/stdoutmetric + +replace go.opentelemetry.io/otel/exporters/stdout/stdouttrace => ../../../stdout/stdouttrace + +replace go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc => ../otlpmetricgrpc diff --git a/exporters/otlp/otlpmetric/otlpmetrichttp/go.sum b/exporters/otlp/otlpmetric/otlpmetrichttp/go.sum new file mode 100644 index 00000000000..8f8adb0bb44 --- /dev/null +++ b/exporters/otlp/otlpmetric/otlpmetrichttp/go.sum @@ -0,0 +1,124 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +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_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +go.opentelemetry.io/proto/otlp v0.9.0 h1:C0g6TWmQYvjKRnljRULLWUVJGy8Uvu0NEL/5frY2/t4= +go.opentelemetry.io/proto/otlp v0.9.0/go.mod h1:1vKfU9rv61e9EVGthD1zNvUbiwPcimSsOPU9brfSHJg= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20200822124328-c89045814202 h1:VvcQYSHwXgi7W+TpUR6A9g6Up98WAHf3f/ulnJ62IyA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +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-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 h1:+kGHl1aib/qcwaRi1CbqBZ1rk19r85MNUf8HaBghugY= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.38.0 h1:/9BgsAsa5nWe26HqOlvlgJnqBuktYOLCgjCPqsa56W0= +google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/exporters/otlp/otlpmetric/otlpmetrichttp/mock_collector_test.go b/exporters/otlp/otlpmetric/otlpmetrichttp/mock_collector_test.go new file mode 100644 index 00000000000..4cc2a340f14 --- /dev/null +++ b/exporters/otlp/otlpmetric/otlpmetrichttp/mock_collector_test.go @@ -0,0 +1,238 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package otlpmetrichttp_test + +import ( + "bytes" + "compress/gzip" + "context" + "crypto/tls" + "fmt" + "io" + "io/ioutil" + "net" + "net/http" + "sync" + "testing" + "time" + + "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/internal/otlpconfig" + "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/internal/otlpmetrictest" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" + + collectormetricpb "go.opentelemetry.io/proto/otlp/collector/metrics/v1" + metricpb "go.opentelemetry.io/proto/otlp/metrics/v1" +) + +type mockCollector struct { + endpoint string + server *http.Server + + spanLock sync.Mutex + metricsStorage otlpmetrictest.MetricsStorage + + injectHTTPStatus []int + injectContentType string + injectDelay time.Duration + + clientTLSConfig *tls.Config + expectedHeaders map[string]string +} + +func (c *mockCollector) Stop() error { + return c.server.Shutdown(context.Background()) +} + +func (c *mockCollector) MustStop(t *testing.T) { + assert.NoError(t, c.server.Shutdown(context.Background())) +} + +func (c *mockCollector) GetMetrics() []*metricpb.Metric { + c.spanLock.Lock() + defer c.spanLock.Unlock() + return c.metricsStorage.GetMetrics() +} + +func (c *mockCollector) Endpoint() string { + return c.endpoint +} + +func (c *mockCollector) ClientTLSConfig() *tls.Config { + return c.clientTLSConfig +} + +func (c *mockCollector) serveMetrics(w http.ResponseWriter, r *http.Request) { + if c.injectDelay != 0 { + time.Sleep(c.injectDelay) + } + + if !c.checkHeaders(r) { + w.WriteHeader(http.StatusBadRequest) + return + } + response := collectormetricpb.ExportMetricsServiceResponse{} + rawResponse, err := proto.Marshal(&response) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + if injectedStatus := c.getInjectHTTPStatus(); injectedStatus != 0 { + writeReply(w, rawResponse, injectedStatus, c.injectContentType) + return + } + rawRequest, err := readRequest(r) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + request, err := unmarshalMetricsRequest(rawRequest, r.Header.Get("content-type")) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + return + } + writeReply(w, rawResponse, 0, c.injectContentType) + c.spanLock.Lock() + defer c.spanLock.Unlock() + c.metricsStorage.AddMetrics(request) +} + +func unmarshalMetricsRequest(rawRequest []byte, contentType string) (*collectormetricpb.ExportMetricsServiceRequest, error) { + request := &collectormetricpb.ExportMetricsServiceRequest{} + if contentType != "application/x-protobuf" { + return request, fmt.Errorf("invalid content-type: %s, only application/x-protobuf is supported", contentType) + } + err := proto.Unmarshal(rawRequest, request) + return request, err +} + +func (c *mockCollector) checkHeaders(r *http.Request) bool { + for k, v := range c.expectedHeaders { + got := r.Header.Get(k) + if got != v { + return false + } + } + return true +} + +func (c *mockCollector) getInjectHTTPStatus() int { + if len(c.injectHTTPStatus) == 0 { + return 0 + } + status := c.injectHTTPStatus[0] + c.injectHTTPStatus = c.injectHTTPStatus[1:] + if len(c.injectHTTPStatus) == 0 { + c.injectHTTPStatus = nil + } + return status +} + +func readRequest(r *http.Request) ([]byte, error) { + if r.Header.Get("Content-Encoding") == "gzip" { + return readGzipBody(r.Body) + } + return ioutil.ReadAll(r.Body) +} + +func readGzipBody(body io.Reader) ([]byte, error) { + rawRequest := bytes.Buffer{} + gunzipper, err := gzip.NewReader(body) + if err != nil { + return nil, err + } + defer gunzipper.Close() + _, err = io.Copy(&rawRequest, gunzipper) + if err != nil { + return nil, err + } + return rawRequest.Bytes(), nil +} + +func writeReply(w http.ResponseWriter, rawResponse []byte, injectHTTPStatus int, injectContentType string) { + status := http.StatusOK + if injectHTTPStatus != 0 { + status = injectHTTPStatus + } + contentType := "application/x-protobuf" + if injectContentType != "" { + contentType = injectContentType + } + w.Header().Set("Content-Type", contentType) + w.WriteHeader(status) + _, _ = w.Write(rawResponse) +} + +type mockCollectorConfig struct { + MetricsURLPath string + Port int + InjectHTTPStatus []int + InjectContentType string + InjectDelay time.Duration + WithTLS bool + ExpectedHeaders map[string]string +} + +func (c *mockCollectorConfig) fillInDefaults() { + if c.MetricsURLPath == "" { + c.MetricsURLPath = otlpconfig.DefaultMetricsPath + } +} + +func runMockCollector(t *testing.T, cfg mockCollectorConfig) *mockCollector { + cfg.fillInDefaults() + ln, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", cfg.Port)) + require.NoError(t, err) + _, portStr, err := net.SplitHostPort(ln.Addr().String()) + require.NoError(t, err) + m := &mockCollector{ + endpoint: fmt.Sprintf("localhost:%s", portStr), + metricsStorage: otlpmetrictest.NewMetricsStorage(), + injectHTTPStatus: cfg.InjectHTTPStatus, + injectContentType: cfg.InjectContentType, + injectDelay: cfg.InjectDelay, + expectedHeaders: cfg.ExpectedHeaders, + } + mux := http.NewServeMux() + mux.Handle(cfg.MetricsURLPath, http.HandlerFunc(m.serveMetrics)) + server := &http.Server{ + Handler: mux, + } + if cfg.WithTLS { + pem, err := generateWeakCertificate() + require.NoError(t, err) + tlsCertificate, err := tls.X509KeyPair(pem.Certificate, pem.PrivateKey) + require.NoError(t, err) + server.TLSConfig = &tls.Config{ + Certificates: []tls.Certificate{tlsCertificate}, + } + + m.clientTLSConfig = &tls.Config{ + InsecureSkipVerify: true, + } + } + go func() { + if cfg.WithTLS { + _ = server.ServeTLS(ln, "", "") + } else { + _ = server.Serve(ln) + } + }() + m.server = server + return m +} diff --git a/exporters/otlp/otlpmetric/otlpmetrichttp/options.go b/exporters/otlp/otlpmetric/otlpmetrichttp/options.go new file mode 100644 index 00000000000..645e9166760 --- /dev/null +++ b/exporters/otlp/otlpmetric/otlpmetrichttp/options.go @@ -0,0 +1,122 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package otlpmetrichttp + +import ( + "crypto/tls" + "time" + + "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/internal/otlpconfig" +) + +const ( + // defaultMaxAttempts describes how many times the driver + // should retry the sending of the payload in case of a + // retryable error. + defaultMaxAttempts int = 5 + // defaultMetricsPath is a default URL path for endpoint that + // receives metrics. + defaultMetricsPath string = "/v1/metrics" + // defaultBackoff is a default base backoff time used in the + // exponential backoff strategy. + defaultBackoff time.Duration = 300 * time.Millisecond +) + +// Compression describes the compression used for payloads sent to the +// collector. +type Compression otlpconfig.Compression + +const ( + // NoCompression tells the driver to send payloads without + // compression. + NoCompression = Compression(otlpconfig.NoCompression) + // GzipCompression tells the driver to send payloads after + // compressing them with gzip. + GzipCompression = Compression(otlpconfig.GzipCompression) +) + +// Option applies an option to the HTTP client. +type Option interface { + applyHTTPOption(*otlpconfig.Config) +} + +type wrappedOption struct { + otlpconfig.HTTPOption +} + +func (w wrappedOption) applyHTTPOption(cfg *otlpconfig.Config) { + w.ApplyHTTPOption(cfg) +} + +// WithEndpoint allows one to set the address of the collector +// endpoint that the driver will use to send metrics. If +// unset, it will instead try to use +// the default endpoint (localhost:4317). Note that the endpoint +// must not contain any URL path. +func WithEndpoint(endpoint string) Option { + return wrappedOption{otlpconfig.WithEndpoint(endpoint)} +} + +// WithCompression tells the driver to compress the sent data. +func WithCompression(compression Compression) Option { + return wrappedOption{otlpconfig.WithCompression(otlpconfig.Compression(compression))} +} + +// WithURLPath allows one to override the default URL path used +// for sending metrics. If unset, default ("/v1/metrics") will be used. +func WithURLPath(urlPath string) Option { + return wrappedOption{otlpconfig.WithURLPath(urlPath)} +} + +// WithMaxAttempts allows one to override how many times the driver +// will try to send the payload in case of retryable errors. +// The max attempts is limited to at most 5 retries. If unset, +// default (5) will be used. +func WithMaxAttempts(maxAttempts int) Option { + return wrappedOption{otlpconfig.WithMaxAttempts(maxAttempts)} +} + +// WithBackoff tells the driver to use the duration as a base of the +// exponential backoff strategy. If unset, default (300ms) will be +// used. +func WithBackoff(duration time.Duration) Option { + return wrappedOption{otlpconfig.WithBackoff(duration)} +} + +// WithTLSClientConfig can be used to set up a custom TLS +// configuration for the client used to send payloads to the +// collector. Use it if you want to use a custom certificate. +func WithTLSClientConfig(tlsCfg *tls.Config) Option { + return wrappedOption{otlpconfig.WithTLSClientConfig(tlsCfg)} +} + +// WithInsecure tells the driver to connect to the collector using the +// HTTP scheme, instead of HTTPS. +func WithInsecure() Option { + return wrappedOption{otlpconfig.WithInsecure()} +} + +// WithHeaders allows one to tell the driver to send additional HTTP +// headers with the payloads. Specifying headers like Content-Length, +// Content-Encoding and Content-Type may result in a broken driver. +func WithHeaders(headers map[string]string) Option { + return wrappedOption{otlpconfig.WithHeaders(headers)} +} + +// WithTimeout tells the driver the max waiting time for the backend to process +// each metrics batch. If unset, the default will be 10 seconds. +func WithTimeout(duration time.Duration) Option { + return wrappedOption{otlpconfig.WithTimeout(duration)} +} diff --git a/exporters/otlp/otlptrace/go.mod b/exporters/otlp/otlptrace/go.mod index 0d97473c759..910dfa0a8f6 100644 --- a/exporters/otlp/otlptrace/go.mod +++ b/exporters/otlp/otlptrace/go.mod @@ -73,3 +73,5 @@ replace go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc => ../ replace go.opentelemetry.io/otel/exporters/stdout/stdoutmetric => ../../stdout/stdoutmetric replace go.opentelemetry.io/otel/exporters/stdout/stdouttrace => ../../stdout/stdouttrace + +replace go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp => ../otlpmetric/otlpmetrichttp diff --git a/exporters/otlp/otlptrace/otlptracegrpc/go.mod b/exporters/otlp/otlptrace/otlptracegrpc/go.mod index 711a9c354a3..2ff642dafdf 100644 --- a/exporters/otlp/otlptrace/otlptracegrpc/go.mod +++ b/exporters/otlp/otlptrace/otlptracegrpc/go.mod @@ -70,3 +70,5 @@ replace go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc => ../ replace go.opentelemetry.io/otel/exporters/stdout/stdoutmetric => ../../../stdout/stdoutmetric replace go.opentelemetry.io/otel/exporters/stdout/stdouttrace => ../../../stdout/stdouttrace + +replace go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp => ../../otlpmetric/otlpmetrichttp diff --git a/exporters/otlp/otlptrace/otlptracehttp/go.mod b/exporters/otlp/otlptrace/otlptracehttp/go.mod index d260800ed1e..c89d42c5282 100644 --- a/exporters/otlp/otlptrace/otlptracehttp/go.mod +++ b/exporters/otlp/otlptrace/otlptracehttp/go.mod @@ -67,3 +67,5 @@ replace go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc => ../ replace go.opentelemetry.io/otel/exporters/stdout/stdoutmetric => ../../../stdout/stdoutmetric replace go.opentelemetry.io/otel/exporters/stdout/stdouttrace => ../../../stdout/stdouttrace + +replace go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp => ../../otlpmetric/otlpmetrichttp diff --git a/exporters/prometheus/go.mod b/exporters/prometheus/go.mod index f3b10bd9852..2c5a4d9ac52 100644 --- a/exporters/prometheus/go.mod +++ b/exporters/prometheus/go.mod @@ -71,3 +71,5 @@ replace go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc => ../ replace go.opentelemetry.io/otel/exporters/stdout/stdoutmetric => ../stdout/stdoutmetric replace go.opentelemetry.io/otel/exporters/stdout/stdouttrace => ../stdout/stdouttrace + +replace go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp => ../otlp/otlpmetric/otlpmetrichttp diff --git a/exporters/stdout/stdoutmetric/go.mod b/exporters/stdout/stdoutmetric/go.mod index eda9465e12b..5609d8a47f2 100644 --- a/exporters/stdout/stdoutmetric/go.mod +++ b/exporters/stdout/stdoutmetric/go.mod @@ -69,3 +69,5 @@ replace go.opentelemetry.io/otel/exporters/otlp/otlpmetric => ../../otlp/otlpmet replace go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc => ../../otlp/otlpmetric/otlpmetricgrpc replace go.opentelemetry.io/otel/exporters/stdout/stdouttrace => ../stdouttrace + +replace go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp => ../../otlp/otlpmetric/otlpmetrichttp diff --git a/exporters/stdout/stdouttrace/go.mod b/exporters/stdout/stdouttrace/go.mod index 08f37fa810e..6e043bdc9ea 100644 --- a/exporters/stdout/stdouttrace/go.mod +++ b/exporters/stdout/stdouttrace/go.mod @@ -68,3 +68,5 @@ replace go.opentelemetry.io/otel/exporters/otlp/otlpmetric => ../../otlp/otlpmet replace go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc => ../../otlp/otlpmetric/otlpmetricgrpc replace go.opentelemetry.io/otel/exporters/stdout/stdoutmetric => ../stdoutmetric + +replace go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp => ../../otlp/otlpmetric/otlpmetrichttp diff --git a/exporters/zipkin/go.mod b/exporters/zipkin/go.mod index 42bd334cf19..4e6db000fda 100644 --- a/exporters/zipkin/go.mod +++ b/exporters/zipkin/go.mod @@ -70,3 +70,5 @@ replace go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc => ../ replace go.opentelemetry.io/otel/exporters/stdout/stdoutmetric => ../stdout/stdoutmetric replace go.opentelemetry.io/otel/exporters/stdout/stdouttrace => ../stdout/stdouttrace + +replace go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp => ../otlp/otlpmetric/otlpmetrichttp diff --git a/go.mod b/go.mod index 1b262d45118..38aeb7e0cc0 100644 --- a/go.mod +++ b/go.mod @@ -66,3 +66,5 @@ replace go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc => ./e replace go.opentelemetry.io/otel/exporters/stdout/stdoutmetric => ./exporters/stdout/stdoutmetric replace go.opentelemetry.io/otel/exporters/stdout/stdouttrace => ./exporters/stdout/stdouttrace + +replace go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp => ./exporters/otlp/otlpmetric/otlpmetrichttp diff --git a/internal/metric/go.mod b/internal/metric/go.mod index 0dd812ba323..a00bde57bc1 100644 --- a/internal/metric/go.mod +++ b/internal/metric/go.mod @@ -65,3 +65,5 @@ replace go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc => ../ replace go.opentelemetry.io/otel/exporters/stdout/stdoutmetric => ../../exporters/stdout/stdoutmetric replace go.opentelemetry.io/otel/exporters/stdout/stdouttrace => ../../exporters/stdout/stdouttrace + +replace go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp => ../../exporters/otlp/otlpmetric/otlpmetrichttp diff --git a/internal/tools/go.mod b/internal/tools/go.mod index 9d79bd81d04..6c289b76132 100644 --- a/internal/tools/go.mod +++ b/internal/tools/go.mod @@ -69,3 +69,5 @@ replace go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc => ../ replace go.opentelemetry.io/otel/exporters/stdout/stdoutmetric => ../../exporters/stdout/stdoutmetric replace go.opentelemetry.io/otel/exporters/stdout/stdouttrace => ../../exporters/stdout/stdouttrace + +replace go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp => ../../exporters/otlp/otlpmetric/otlpmetrichttp diff --git a/metric/go.mod b/metric/go.mod index 1d79a51f3bc..cd809aac206 100644 --- a/metric/go.mod +++ b/metric/go.mod @@ -66,3 +66,5 @@ replace go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc => ../ replace go.opentelemetry.io/otel/exporters/stdout/stdoutmetric => ../exporters/stdout/stdoutmetric replace go.opentelemetry.io/otel/exporters/stdout/stdouttrace => ../exporters/stdout/stdouttrace + +replace go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp => ../exporters/otlp/otlpmetric/otlpmetrichttp diff --git a/oteltest/go.mod b/oteltest/go.mod index 359f34a95d1..592fdbbb202 100644 --- a/oteltest/go.mod +++ b/oteltest/go.mod @@ -65,3 +65,5 @@ replace go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc => ../ replace go.opentelemetry.io/otel/exporters/stdout/stdoutmetric => ../exporters/stdout/stdoutmetric replace go.opentelemetry.io/otel/exporters/stdout/stdouttrace => ../exporters/stdout/stdouttrace + +replace go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp => ../exporters/otlp/otlpmetric/otlpmetrichttp diff --git a/sdk/export/metric/go.mod b/sdk/export/metric/go.mod index 3cbc934221a..01b079bcb3d 100644 --- a/sdk/export/metric/go.mod +++ b/sdk/export/metric/go.mod @@ -66,3 +66,5 @@ replace go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc => ../ replace go.opentelemetry.io/otel/exporters/stdout/stdoutmetric => ../../../exporters/stdout/stdoutmetric replace go.opentelemetry.io/otel/exporters/stdout/stdouttrace => ../../../exporters/stdout/stdouttrace + +replace go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp => ../../../exporters/otlp/otlpmetric/otlpmetrichttp diff --git a/sdk/go.mod b/sdk/go.mod index 55718945909..4ddde52d0d6 100644 --- a/sdk/go.mod +++ b/sdk/go.mod @@ -67,3 +67,5 @@ replace go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc => ../ replace go.opentelemetry.io/otel/exporters/stdout/stdoutmetric => ../exporters/stdout/stdoutmetric replace go.opentelemetry.io/otel/exporters/stdout/stdouttrace => ../exporters/stdout/stdouttrace + +replace go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp => ../exporters/otlp/otlpmetric/otlpmetrichttp diff --git a/sdk/metric/go.mod b/sdk/metric/go.mod index 41332c40ee7..50da28b18d0 100644 --- a/sdk/metric/go.mod +++ b/sdk/metric/go.mod @@ -69,3 +69,5 @@ replace go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc => ../ replace go.opentelemetry.io/otel/exporters/stdout/stdoutmetric => ../../exporters/stdout/stdoutmetric replace go.opentelemetry.io/otel/exporters/stdout/stdouttrace => ../../exporters/stdout/stdouttrace + +replace go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp => ../../exporters/otlp/otlpmetric/otlpmetrichttp diff --git a/trace/go.mod b/trace/go.mod index 55ee642e583..83778c09158 100644 --- a/trace/go.mod +++ b/trace/go.mod @@ -65,3 +65,5 @@ replace go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc => ../ replace go.opentelemetry.io/otel/exporters/stdout/stdoutmetric => ../exporters/stdout/stdoutmetric replace go.opentelemetry.io/otel/exporters/stdout/stdouttrace => ../exporters/stdout/stdouttrace + +replace go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp => ../exporters/otlp/otlpmetric/otlpmetrichttp