diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 2e0315a6cd8..95816ded71c 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -256,3 +256,13 @@ updates: schedule: day: sunday interval: weekly + - + package-ecosystem: gomod + directory: /exporters/otlp/otlptrace/otlptracehttp + labels: + - dependencies + - go + - "Skip Changelog" + schedule: + day: sunday + interval: weekly diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d7b3fbb773..597f780f08e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - Creates package `go.opentelemetry.io/otel/exporters/otlp/otlptrace` that defines a trace exporter that uses a `otlptrace.Client` to send data. Creates package `go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc` implementing a gRPC `otlptrace.Client` and offers convenience functions, `NewExportPipeline` and `InstallNewPipeline`, to setup and install a `otlptrace.Exporter` in tracing .(#1922) - The `OTEL_SERVICE_NAME` environment variable is the preferred source for `service.name`, used by the environment resource detector if a service name is present both there and in `OTEL_RESOURCE_ATTRIBUTES`. (#1969) +- Creates package `go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp` implementing a HTTP `otlptrace.Client` and offers convenience functions, `NewExportPipeline` and `InstallNewPipeline`, to setup and install a `otlptrace.Exporter` in tracing. (#1963) ### Changed diff --git a/bridge/opencensus/go.mod b/bridge/opencensus/go.mod index 639db827d80..a788bbfa996 100644 --- a/bridge/opencensus/go.mod +++ b/bridge/opencensus/go.mod @@ -61,3 +61,5 @@ replace go.opentelemetry.io/otel/example/passthrough => ../../example/passthroug replace go.opentelemetry.io/otel/exporters/otlp/otlptrace => ../../exporters/otlp/otlptrace replace go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc => ../../exporters/otlp/otlptrace/otlptracegrpc + +replace go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp => ../../exporters/otlp/otlptrace/otlptracehttp diff --git a/bridge/opentracing/go.mod b/bridge/opentracing/go.mod index 68418ebcbaf..70304940630 100644 --- a/bridge/opentracing/go.mod +++ b/bridge/opentracing/go.mod @@ -57,3 +57,5 @@ replace go.opentelemetry.io/otel/example/passthrough => ../../example/passthroug replace go.opentelemetry.io/otel/exporters/otlp/otlptrace => ../../exporters/otlp/otlptrace replace go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc => ../../exporters/otlp/otlptrace/otlptracegrpc + +replace go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp => ../../exporters/otlp/otlptrace/otlptracehttp diff --git a/example/jaeger/go.mod b/example/jaeger/go.mod index 6b07a9603c3..51b95ae2927 100644 --- a/example/jaeger/go.mod +++ b/example/jaeger/go.mod @@ -57,3 +57,5 @@ replace go.opentelemetry.io/otel/example/passthrough => ../passthrough replace go.opentelemetry.io/otel/exporters/otlp/otlptrace => ../../exporters/otlp/otlptrace replace go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc => ../../exporters/otlp/otlptrace/otlptracegrpc + +replace go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp => ../../exporters/otlp/otlptrace/otlptracehttp diff --git a/example/namedtracer/go.mod b/example/namedtracer/go.mod index ec10a3c4a3c..9de96a9e08f 100644 --- a/example/namedtracer/go.mod +++ b/example/namedtracer/go.mod @@ -58,3 +58,5 @@ replace go.opentelemetry.io/otel/example/passthrough => ../passthrough replace go.opentelemetry.io/otel/exporters/otlp/otlptrace => ../../exporters/otlp/otlptrace replace go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc => ../../exporters/otlp/otlptrace/otlptracegrpc + +replace go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp => ../../exporters/otlp/otlptrace/otlptracehttp diff --git a/example/opencensus/go.mod b/example/opencensus/go.mod index 26a0e98146a..6d24147ac63 100644 --- a/example/opencensus/go.mod +++ b/example/opencensus/go.mod @@ -59,3 +59,5 @@ replace go.opentelemetry.io/otel/example/passthrough => ../passthrough replace go.opentelemetry.io/otel/exporters/otlp/otlptrace => ../../exporters/otlp/otlptrace replace go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc => ../../exporters/otlp/otlptrace/otlptracegrpc + +replace go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp => ../../exporters/otlp/otlptrace/otlptracehttp diff --git a/example/otel-collector/go.mod b/example/otel-collector/go.mod index 0c51c83aace..c992f277231 100644 --- a/example/otel-collector/go.mod +++ b/example/otel-collector/go.mod @@ -61,3 +61,5 @@ replace go.opentelemetry.io/otel/example/passthrough => ../passthrough replace go.opentelemetry.io/otel/exporters/otlp/otlptrace => ../../exporters/otlp/otlptrace replace go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc => ../../exporters/otlp/otlptrace/otlptracegrpc + +replace go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp => ../../exporters/otlp/otlptrace/otlptracehttp diff --git a/example/passthrough/go.mod b/example/passthrough/go.mod index b286054b7b3..af55e50a41f 100644 --- a/example/passthrough/go.mod +++ b/example/passthrough/go.mod @@ -59,3 +59,5 @@ replace go.opentelemetry.io/otel/sdk/trace => ../../sdk/trace replace go.opentelemetry.io/otel/exporters/otlp/otlptrace => ../../exporters/otlp/otlptrace replace go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc => ../../exporters/otlp/otlptrace/otlptracegrpc + +replace go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp => ../../exporters/otlp/otlptrace/otlptracehttp diff --git a/example/prom-collector/go.mod b/example/prom-collector/go.mod index 74e9c573944..bf47db5d4bb 100644 --- a/example/prom-collector/go.mod +++ b/example/prom-collector/go.mod @@ -60,3 +60,5 @@ replace go.opentelemetry.io/otel/example/passthrough => ../passthrough replace go.opentelemetry.io/otel/exporters/otlp/otlptrace => ../../exporters/otlp/otlptrace replace go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc => ../../exporters/otlp/otlptrace/otlptracegrpc + +replace go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp => ../../exporters/otlp/otlptrace/otlptracehttp diff --git a/example/prometheus/go.mod b/example/prometheus/go.mod index b1c91cf7d6c..7fcf5ccc33d 100644 --- a/example/prometheus/go.mod +++ b/example/prometheus/go.mod @@ -57,3 +57,5 @@ replace go.opentelemetry.io/otel/example/passthrough => ../passthrough replace go.opentelemetry.io/otel/exporters/otlp/otlptrace => ../../exporters/otlp/otlptrace replace go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc => ../../exporters/otlp/otlptrace/otlptracegrpc + +replace go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp => ../../exporters/otlp/otlptrace/otlptracehttp diff --git a/example/zipkin/go.mod b/example/zipkin/go.mod index 228f1758cfe..bb8f45d8062 100644 --- a/example/zipkin/go.mod +++ b/example/zipkin/go.mod @@ -58,3 +58,5 @@ replace go.opentelemetry.io/otel/example/passthrough => ../passthrough replace go.opentelemetry.io/otel/exporters/otlp/otlptrace => ../../exporters/otlp/otlptrace replace go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc => ../../exporters/otlp/otlptrace/otlptracegrpc + +replace go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp => ../../exporters/otlp/otlptrace/otlptracehttp diff --git a/exporters/metric/prometheus/go.mod b/exporters/metric/prometheus/go.mod index 82f8fbb2fd4..5857eaa5946 100644 --- a/exporters/metric/prometheus/go.mod +++ b/exporters/metric/prometheus/go.mod @@ -62,3 +62,5 @@ replace go.opentelemetry.io/otel/example/passthrough => ../../../example/passthr replace go.opentelemetry.io/otel/exporters/otlp/otlptrace => ../../otlp/otlptrace replace go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc => ../../otlp/otlptrace/otlptracegrpc + +replace go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp => ../../otlp/otlptrace/otlptracehttp diff --git a/exporters/otlp/go.mod b/exporters/otlp/go.mod index 8e5b68ce84f..e3c5a116653 100644 --- a/exporters/otlp/go.mod +++ b/exporters/otlp/go.mod @@ -69,3 +69,5 @@ 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/example/passthrough => ../../example/passthrough + +replace go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp => ./otlptrace/otlptracehttp diff --git a/exporters/otlp/otlptrace/go.mod b/exporters/otlp/otlptrace/go.mod index f2140ec67ff..bbedfb964af 100644 --- a/exporters/otlp/otlptrace/go.mod +++ b/exporters/otlp/otlptrace/go.mod @@ -65,3 +65,5 @@ replace go.opentelemetry.io/otel/sdk/export/metric => ../../../sdk/export/metric replace go.opentelemetry.io/otel/sdk/metric => ../../../sdk/metric replace go.opentelemetry.io/otel/example/passthrough => ../../../example/passthrough + +replace go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp => ./otlptracehttp diff --git a/exporters/otlp/otlptrace/internal/otlpconfig/options.go b/exporters/otlp/otlptrace/internal/otlpconfig/options.go index cf688c534ea..63ab5bb534d 100644 --- a/exporters/otlp/otlptrace/internal/otlpconfig/options.go +++ b/exporters/otlp/otlptrace/internal/otlpconfig/options.go @@ -68,7 +68,6 @@ type ( Traces SignalConfig // HTTP configurations - Marshaler Marshaler MaxAttempts int Backoff time.Duration diff --git a/exporters/otlp/otlptrace/otlptracegrpc/go.mod b/exporters/otlp/otlptrace/otlptracegrpc/go.mod index 3dbe8d2e100..2fd6143b7c7 100644 --- a/exporters/otlp/otlptrace/otlptracegrpc/go.mod +++ b/exporters/otlp/otlptrace/otlptracegrpc/go.mod @@ -62,3 +62,5 @@ replace go.opentelemetry.io/otel/sdk/export/metric => ../../../../sdk/export/met replace go.opentelemetry.io/otel/sdk/metric => ../../../../sdk/metric replace go.opentelemetry.io/otel/example/passthrough => ../../../../example/passthrough + +replace go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp => ../otlptracehttp diff --git a/exporters/otlp/otlptrace/otlptracehttp/certificate_test.go b/exporters/otlp/otlptrace/otlptracehttp/certificate_test.go new file mode 100644 index 00000000000..92f89cc3b29 --- /dev/null +++ b/exporters/otlp/otlptrace/otlptracehttp/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 otlptracehttp_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/otlptrace/otlptracehttp/client.go b/exporters/otlp/otlptrace/otlptracehttp/client.go new file mode 100644 index 00000000000..ca0fa18bb97 --- /dev/null +++ b/exporters/otlp/otlptrace/otlptracehttp/client.go @@ -0,0 +1,280 @@ +// 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 otlptracehttp + +import ( + "bytes" + "compress/gzip" + "context" + "fmt" + "io" + "io/ioutil" + "math/rand" + "net" + "net/http" + "path" + "strings" + "time" + + "go.opentelemetry.io/otel/exporters/otlp/otlptrace" + tracepb "go.opentelemetry.io/proto/otlp/trace/v1" + + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/internal/otlpconfig" + + "google.golang.org/protobuf/proto" + + "go.opentelemetry.io/otel" + coltracepb "go.opentelemetry.io/proto/otlp/collector/trace/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{} +} + +var _ otlptrace.Client = (*client)(nil) + +// NewClient creates a new HTTP trace client. +func NewClient(opts ...Option) otlptrace.Client { + cfg := otlpconfig.NewDefaultConfig() + otlpconfig.ApplyHTTPEnvConfigs(&cfg) + for _, opt := range opts { + opt.applyHTTPOption(&cfg) + } + + for pathPtr, defaultPath := range map[*string]string{ + &cfg.Traces.URLPath: defaultTracesPath, + } { + 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.Traces.Timeout, + } + if cfg.Traces.TLSCfg != nil { + transport := ourTransport.Clone() + transport.TLSClientConfig = cfg.Traces.TLSCfg + httpClient.Transport = transport + } + + stopCh := make(chan struct{}) + return &client{ + name: "traces", + cfg: cfg.Traces, + 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 +} + +// UploadTraces sends a batch of spans to the collector. +func (d *client) UploadTraces(ctx context.Context, protoSpans []*tracepb.ResourceSpans) error { + pbRequest := &coltracepb.ExportTraceServiceRequest{ + ResourceSpans: protoSpans, + } + 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/otlptrace/otlptracehttp/client_test.go b/exporters/otlp/otlptrace/otlptracehttp/client_test.go new file mode 100644 index 00000000000..33340a4e668 --- /dev/null +++ b/exporters/otlp/otlptrace/otlptracehttp/client_test.go @@ -0,0 +1,422 @@ +// 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 otlptracehttp_test + +import ( + "context" + "fmt" + "net/http" + "os" + "testing" + "time" + + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "go.opentelemetry.io/otel/exporters/otlp/otlptrace" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/internal/otlptracetest" +) + +const ( + relOtherTracesPath = "post/traces/here" + otherTracesPath = "/post/traces/here" +) + +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 []otlptracehttp.Option + mcCfg mockCollectorConfig + tls bool + }{ + { + name: "no extra options", + opts: nil, + }, + { + name: "with gzip compression", + opts: []otlptracehttp.Option{ + otlptracehttp.WithCompression(otlptracehttp.GzipCompression), + }, + }, + { + name: "with empty paths (forced to defaults)", + opts: []otlptracehttp.Option{ + otlptracehttp.WithURLPath(""), + }, + }, + { + name: "with relative paths", + opts: []otlptracehttp.Option{ + otlptracehttp.WithURLPath(relOtherTracesPath), + }, + mcCfg: mockCollectorConfig{ + TracesURLPath: otherTracesPath, + }, + }, + { + name: "with TLS", + opts: nil, + mcCfg: mockCollectorConfig{ + WithTLS: true, + }, + tls: true, + }, + { + name: "with extra headers", + opts: []otlptracehttp.Option{ + otlptracehttp.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 := []otlptracehttp.Option{ + otlptracehttp.WithEndpoint(mc.Endpoint()), + } + if tc.tls { + tlsConfig := mc.ClientTLSConfig() + require.NotNil(t, tlsConfig) + allOpts = append(allOpts, otlptracehttp.WithTLSClientConfig(tlsConfig)) + } else { + allOpts = append(allOpts, otlptracehttp.WithInsecure()) + } + allOpts = append(allOpts, tc.opts...) + client := otlptracehttp.NewClient(allOpts...) + ctx := context.Background() + exporter, err := otlptrace.NewExporter(ctx, client) + if assert.NoError(t, err) { + defer func() { + assert.NoError(t, exporter.Shutdown(ctx)) + }() + otlptracetest.RunEndToEndTest(ctx, t, exporter, mc) + } + }) + } +} + +func TestExporterShutdown(t *testing.T) { + mc := runMockCollector(t, mockCollectorConfig{}) + defer func() { + _ = mc.Stop() + }() + + <-time.After(5 * time.Millisecond) + + otlptracetest.RunExporterShutdownTest(t, func() otlptrace.Client { + return otlptracehttp.NewClient( + otlptracehttp.WithInsecure(), + otlptracehttp.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 := otlptracehttp.NewClient( + otlptracehttp.WithEndpoint(mc.Endpoint()), + otlptracehttp.WithInsecure(), + otlptracehttp.WithMaxAttempts(len(statuses)+1), + ) + ctx := context.Background() + exporter, err := otlptrace.NewExporter(ctx, client) + require.NoError(t, err) + defer func() { + assert.NoError(t, exporter.Shutdown(ctx)) + }() + err = exporter.ExportSpans(ctx, otlptracetest.SingleReadOnlySpan()) + assert.NoError(t, err) + assert.Len(t, mc.GetSpans(), 1) +} + +func TestTimeout(t *testing.T) { + mcCfg := mockCollectorConfig{ + InjectDelay: 100 * time.Millisecond, + } + mc := runMockCollector(t, mcCfg) + defer mc.MustStop(t) + client := otlptracehttp.NewClient( + otlptracehttp.WithEndpoint(mc.Endpoint()), + otlptracehttp.WithInsecure(), + otlptracehttp.WithTimeout(50*time.Millisecond), + ) + ctx := context.Background() + exporter, err := otlptrace.NewExporter(ctx, client) + require.NoError(t, err) + defer func() { + assert.NoError(t, exporter.Shutdown(ctx)) + }() + err = exporter.ExportSpans(ctx, otlptracetest.SingleReadOnlySpan()) + 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 := otlptracehttp.NewClient( + otlptracehttp.WithEndpoint(mc.Endpoint()), + otlptracehttp.WithInsecure(), + otlptracehttp.WithMaxAttempts(1), + ) + ctx := context.Background() + exporter, err := otlptrace.NewExporter(ctx, driver) + require.NoError(t, err) + defer func() { + assert.NoError(t, exporter.Shutdown(ctx)) + }() + err = exporter.ExportSpans(ctx, otlptracetest.SingleReadOnlySpan()) + assert.Error(t, err) + assert.Empty(t, mc.GetSpans()) +} + +func TestNoRetry(t *testing.T) { + statuses := []int{ + http.StatusBadRequest, + } + mcCfg := mockCollectorConfig{ + InjectHTTPStatus: statuses, + } + mc := runMockCollector(t, mcCfg) + defer mc.MustStop(t) + driver := otlptracehttp.NewClient( + otlptracehttp.WithEndpoint(mc.Endpoint()), + otlptracehttp.WithInsecure(), + otlptracehttp.WithMaxAttempts(len(statuses)+1), + ) + ctx := context.Background() + exporter, err := otlptrace.NewExporter(ctx, driver) + require.NoError(t, err) + defer func() { + assert.NoError(t, exporter.Shutdown(ctx)) + }() + err = exporter.ExportSpans(ctx, otlptracetest.SingleReadOnlySpan()) + assert.Error(t, err) + assert.Equal(t, fmt.Sprintf("failed to send traces to http://%s/v1/traces with HTTP status 400 Bad Request", mc.endpoint), err.Error()) + assert.Empty(t, mc.GetSpans()) +} + +func TestEmptyData(t *testing.T) { + mcCfg := mockCollectorConfig{} + mc := runMockCollector(t, mcCfg) + defer mc.MustStop(t) + driver := otlptracehttp.NewClient( + otlptracehttp.WithEndpoint(mc.Endpoint()), + otlptracehttp.WithInsecure(), + ) + ctx := context.Background() + exporter, err := otlptrace.NewExporter(ctx, driver) + require.NoError(t, err) + defer func() { + assert.NoError(t, exporter.Shutdown(ctx)) + }() + assert.NoError(t, err) + err = exporter.ExportSpans(ctx, nil) + assert.NoError(t, err) + assert.Empty(t, mc.GetSpans()) +} + +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 := otlptracehttp.NewClient( + otlptracehttp.WithEndpoint(mc.Endpoint()), + otlptracehttp.WithInsecure(), + otlptracehttp.WithMaxAttempts(tc.maxAttempts), + otlptracehttp.WithBackoff(time.Millisecond), + ) + ctx := context.Background() + exporter, err := otlptrace.NewExporter(ctx, driver) + require.NoError(t, err) + defer func() { + assert.NoError(t, exporter.Shutdown(ctx)) + }() + err = exporter.ExportSpans(ctx, otlptracetest.SingleReadOnlySpan()) + assert.Error(t, err) + assert.Empty(t, mc.GetSpans()) + }) + } +} + +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 := otlptracehttp.NewClient( + otlptracehttp.WithEndpoint(mc.Endpoint()), + otlptracehttp.WithInsecure(), + otlptracehttp.WithBackoff(-time.Millisecond), + ) + ctx, cancel := context.WithTimeout(context.Background(), 3*(300*time.Millisecond)) + defer cancel() + exporter, err := otlptrace.NewExporter(ctx, driver) + require.NoError(t, err) + defer func() { + assert.NoError(t, exporter.Shutdown(context.Background())) + }() + err = exporter.ExportSpans(ctx, otlptracetest.SingleReadOnlySpan()) + assert.Error(t, err) + assert.Empty(t, mc.GetSpans()) +} + +func TestCancelledContext(t *testing.T) { + mcCfg := mockCollectorConfig{} + mc := runMockCollector(t, mcCfg) + defer mc.MustStop(t) + driver := otlptracehttp.NewClient( + otlptracehttp.WithEndpoint(mc.Endpoint()), + otlptracehttp.WithInsecure(), + ) + ctx, cancel := context.WithCancel(context.Background()) + exporter, err := otlptrace.NewExporter(ctx, driver) + require.NoError(t, err) + defer func() { + assert.NoError(t, exporter.Shutdown(context.Background())) + }() + cancel() + err = exporter.ExportSpans(ctx, otlptracetest.SingleReadOnlySpan()) + assert.Error(t, err) + assert.Empty(t, mc.GetSpans()) +} + +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 := otlptracehttp.NewClient( + otlptracehttp.WithEndpoint(mc.Endpoint()), + otlptracehttp.WithInsecure(), + otlptracehttp.WithBackoff(time.Minute), + ) + ctx := context.Background() + exporter, err := otlptrace.NewExporter(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.ExportSpans(ctx, otlptracetest.SingleReadOnlySpan()) + assert.Error(t, err) + assert.Empty(t, mc.GetSpans()) +} + +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 := otlptracehttp.NewClient( + otlptracehttp.WithEndpoint(mc.Endpoint()), + otlptracehttp.WithInsecure(), + otlptracehttp.WithBackoff(time.Minute), + ) + ctx := context.Background() + exporter, err := otlptrace.NewExporter(ctx, driver) + require.NoError(t, err) + defer func() { + assert.NoError(t, exporter.Shutdown(ctx)) + }() + doneCh := make(chan struct{}) + go func() { + err := exporter.ExportSpans(ctx, otlptracetest.SingleReadOnlySpan()) + assert.Error(t, err) + assert.Empty(t, mc.GetSpans()) + close(doneCh) + }() + <-time.After(time.Second) + err = exporter.Shutdown(ctx) + assert.NoError(t, err) + <-doneCh +} diff --git a/exporters/otlp/otlptrace/otlptracehttp/doc.go b/exporters/otlp/otlptrace/otlptracehttp/doc.go new file mode 100644 index 00000000000..9c4c90edcee --- /dev/null +++ b/exporters/otlp/otlptrace/otlptracehttp/doc.go @@ -0,0 +1,24 @@ +// 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 otlptracehttp a client that sends traces 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 otlptracehttp // import "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" diff --git a/exporters/otlp/otlptrace/otlptracehttp/exporter.go b/exporters/otlp/otlptrace/otlptracehttp/exporter.go new file mode 100644 index 00000000000..833935ac439 --- /dev/null +++ b/exporters/otlp/otlptrace/otlptracehttp/exporter.go @@ -0,0 +1,44 @@ +// 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 otlptracehttp // import "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" + +import ( + "context" + + "go.opentelemetry.io/otel/exporters/otlp/otlptrace" + tracesdk "go.opentelemetry.io/otel/sdk/trace" +) + +// NewExporter constructs a new Exporter and starts it. +func NewExporter(ctx context.Context, opts ...Option) (*otlptrace.Exporter, error) { + return otlptrace.NewExporter(ctx, NewClient(opts...)) +} + +// NewUnstartedExporter constructs a new Exporter and does not start it. +func NewUnstartedExporter(opts ...Option) *otlptrace.Exporter { + return otlptrace.NewUnstartedExporter(NewClient(opts...)) +} + +// NewExportPipeline sets up a complete export pipeline +// with the recommended TracerProvider setup. +func NewExportPipeline(ctx context.Context, opts ...Option) (*otlptrace.Exporter, *tracesdk.TracerProvider, error) { + return otlptrace.NewExportPipeline(ctx, NewClient(opts...)) +} + +// InstallNewPipeline instantiates a NewExportPipeline with the +// recommended configuration and registers it globally. +func InstallNewPipeline(ctx context.Context, opts ...Option) (*otlptrace.Exporter, *tracesdk.TracerProvider, error) { + return otlptrace.InstallNewPipeline(ctx, NewClient(opts...)) +} diff --git a/exporters/otlp/otlptrace/otlptracehttp/go.mod b/exporters/otlp/otlptrace/otlptracehttp/go.mod new file mode 100644 index 00000000000..c5a1c4a5e30 --- /dev/null +++ b/exporters/otlp/otlptrace/otlptracehttp/go.mod @@ -0,0 +1,64 @@ +module go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp + +go 1.15 + +require ( + github.com/stretchr/testify v1.7.0 + go.opentelemetry.io/otel v0.20.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace v0.0.0-00010101000000-000000000000 + go.opentelemetry.io/otel/sdk v0.20.0 + go.opentelemetry.io/proto/otlp v0.9.0 + google.golang.org/protobuf v1.26.0 +) + +replace go.opentelemetry.io/otel/exporters/otlp/otlptrace => ../ + +replace go.opentelemetry.io/otel => ../../../.. + +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 => ../.. + +replace go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc => ../otlptracegrpc + +replace go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp => ./ + +replace go.opentelemetry.io/otel/exporters/stdout => ../../../stdout + +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/metric => ../../../../metric + +replace go.opentelemetry.io/otel/oteltest => ../../../../oteltest + +replace go.opentelemetry.io/otel/sdk => ../../../../sdk + +replace go.opentelemetry.io/otel/sdk/export/metric => ../../../../sdk/export/metric + +replace go.opentelemetry.io/otel/sdk/metric => ../../../../sdk/metric + +replace go.opentelemetry.io/otel/trace => ../../../../trace diff --git a/exporters/otlp/otlptrace/otlptracehttp/go.sum b/exporters/otlp/otlptrace/otlptracehttp/go.sum new file mode 100644 index 00000000000..a7a26d3040c --- /dev/null +++ b/exporters/otlp/otlptrace/otlptracehttp/go.sum @@ -0,0 +1,122 @@ +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/cenkalti/backoff/v4 v4.1.0/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/otlptrace/otlptracehttp/mock_collector_test.go b/exporters/otlp/otlptrace/otlptracehttp/mock_collector_test.go new file mode 100644 index 00000000000..6ca62a05910 --- /dev/null +++ b/exporters/otlp/otlptrace/otlptracehttp/mock_collector_test.go @@ -0,0 +1,244 @@ +// 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 otlptracehttp_test + +import ( + "bytes" + "compress/gzip" + "context" + "crypto/tls" + "fmt" + "io" + "io/ioutil" + "net" + "net/http" + "sync" + "testing" + "time" + + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/internal/otlpconfig" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/internal/otlptracetest" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" + + collectortracepb "go.opentelemetry.io/proto/otlp/collector/trace/v1" + tracepb "go.opentelemetry.io/proto/otlp/trace/v1" +) + +type mockCollector struct { + endpoint string + server *http.Server + + spanLock sync.Mutex + spansStorage otlptracetest.SpansStorage + + 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) GetSpans() []*tracepb.Span { + c.spanLock.Lock() + defer c.spanLock.Unlock() + return c.spansStorage.GetSpans() +} + +func (c *mockCollector) GetResourceSpans() []*tracepb.ResourceSpans { + c.spanLock.Lock() + defer c.spanLock.Unlock() + return c.spansStorage.GetResourceSpans() +} + +func (c *mockCollector) Endpoint() string { + return c.endpoint +} + +func (c *mockCollector) ClientTLSConfig() *tls.Config { + return c.clientTLSConfig +} + +func (c *mockCollector) serveTraces(w http.ResponseWriter, r *http.Request) { + if c.injectDelay != 0 { + time.Sleep(c.injectDelay) + } + + if !c.checkHeaders(r) { + w.WriteHeader(http.StatusBadRequest) + return + } + response := collectortracepb.ExportTraceServiceResponse{} + 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 := unmarshalTraceRequest(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.spansStorage.AddSpans(request) +} + +func unmarshalTraceRequest(rawRequest []byte, contentType string) (*collectortracepb.ExportTraceServiceRequest, error) { + request := &collectortracepb.ExportTraceServiceRequest{} + 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 { + TracesURLPath string + Port int + InjectHTTPStatus []int + InjectContentType string + InjectDelay time.Duration + WithTLS bool + ExpectedHeaders map[string]string +} + +func (c *mockCollectorConfig) fillInDefaults() { + if c.TracesURLPath == "" { + c.TracesURLPath = otlpconfig.DefaultTracesPath + } +} + +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), + spansStorage: otlptracetest.NewSpansStorage(), + injectHTTPStatus: cfg.InjectHTTPStatus, + injectContentType: cfg.InjectContentType, + injectDelay: cfg.InjectDelay, + expectedHeaders: cfg.ExpectedHeaders, + } + mux := http.NewServeMux() + mux.Handle(cfg.TracesURLPath, http.HandlerFunc(m.serveTraces)) + 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/otlptrace/otlptracehttp/options.go b/exporters/otlp/otlptrace/otlptracehttp/options.go new file mode 100644 index 00000000000..f073baabe80 --- /dev/null +++ b/exporters/otlp/otlptrace/otlptracehttp/options.go @@ -0,0 +1,126 @@ +// 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 otlptracehttp + +import ( + "crypto/tls" + "time" + + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/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 + // defaultTracesPath is a default URL path for endpoint that + // receives spans. + defaultTracesPath string = "/v1/traces" + // 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) +} + +// RetrySettings defines configuration for retrying batches in case of export failure +// using an exponential backoff. +type RetrySettings otlpconfig.RetrySettings + +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 spans. 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 traces. If unset, default ("/v1/traces") will be used. +func WithURLPath(urlPath string) Option { + return wrappedOption{otlpconfig.WithTracesURLPath(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 spans batch. If unset, the default will be 10 seconds. +func WithTimeout(duration time.Duration) Option { + return wrappedOption{otlpconfig.WithTimeout(duration)} +} diff --git a/exporters/stdout/go.mod b/exporters/stdout/go.mod index d71415bfc07..5b927d14c82 100644 --- a/exporters/stdout/go.mod +++ b/exporters/stdout/go.mod @@ -63,3 +63,5 @@ replace go.opentelemetry.io/otel/example/passthrough => ../../example/passthroug replace go.opentelemetry.io/otel/exporters/otlp/otlptrace => ../otlp/otlptrace replace go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc => ../otlp/otlptrace/otlptracegrpc + +replace go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp => ../otlp/otlptrace/otlptracehttp diff --git a/exporters/trace/jaeger/go.mod b/exporters/trace/jaeger/go.mod index 6e09b654836..8fd0f8bc634 100644 --- a/exporters/trace/jaeger/go.mod +++ b/exporters/trace/jaeger/go.mod @@ -60,3 +60,5 @@ replace go.opentelemetry.io/otel/example/passthrough => ../../../example/passthr replace go.opentelemetry.io/otel/exporters/otlp/otlptrace => ../../otlp/otlptrace replace go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc => ../../otlp/otlptrace/otlptracegrpc + +replace go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp => ../../otlp/otlptrace/otlptracehttp diff --git a/exporters/trace/zipkin/go.mod b/exporters/trace/zipkin/go.mod index 6f5ebc6da07..7d084e68546 100644 --- a/exporters/trace/zipkin/go.mod +++ b/exporters/trace/zipkin/go.mod @@ -61,3 +61,5 @@ replace go.opentelemetry.io/otel/example/passthrough => ../../../example/passthr replace go.opentelemetry.io/otel/exporters/otlp/otlptrace => ../../otlp/otlptrace replace go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc => ../../otlp/otlptrace/otlptracegrpc + +replace go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp => ../../otlp/otlptrace/otlptracehttp diff --git a/go.mod b/go.mod index 7fc962aa89d..3b37117fb3e 100644 --- a/go.mod +++ b/go.mod @@ -59,3 +59,5 @@ replace go.opentelemetry.io/otel/example/passthrough => ./example/passthrough replace go.opentelemetry.io/otel/exporters/otlp/otlptrace => ./exporters/otlp/otlptrace replace go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc => ./exporters/otlp/otlptrace/otlptracegrpc + +replace go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp => ./exporters/otlp/otlptrace/otlptracehttp diff --git a/internal/tools/go.mod b/internal/tools/go.mod index 51fcda082e3..eee8584223d 100644 --- a/internal/tools/go.mod +++ b/internal/tools/go.mod @@ -61,3 +61,5 @@ replace go.opentelemetry.io/otel/example/passthrough => ../../example/passthroug replace go.opentelemetry.io/otel/exporters/otlp/otlptrace => ../../exporters/otlp/otlptrace replace go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc => ../../exporters/otlp/otlptrace/otlptracegrpc + +replace go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp => ../../exporters/otlp/otlptrace/otlptracehttp diff --git a/metric/go.mod b/metric/go.mod index 2c5e655c43f..82ac8852048 100644 --- a/metric/go.mod +++ b/metric/go.mod @@ -58,3 +58,5 @@ replace go.opentelemetry.io/otel/example/passthrough => ../example/passthrough replace go.opentelemetry.io/otel/exporters/otlp/otlptrace => ../exporters/otlp/otlptrace replace go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc => ../exporters/otlp/otlptrace/otlptracegrpc + +replace go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp => ../exporters/otlp/otlptrace/otlptracehttp diff --git a/oteltest/go.mod b/oteltest/go.mod index 2dfa3562ea9..f3ac5942a18 100644 --- a/oteltest/go.mod +++ b/oteltest/go.mod @@ -58,3 +58,5 @@ replace go.opentelemetry.io/otel/example/passthrough => ../example/passthrough replace go.opentelemetry.io/otel/exporters/otlp/otlptrace => ../exporters/otlp/otlptrace replace go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc => ../exporters/otlp/otlptrace/otlptracegrpc + +replace go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp => ../exporters/otlp/otlptrace/otlptracehttp diff --git a/sdk/export/metric/go.mod b/sdk/export/metric/go.mod index b999aab9ef0..f1b6b0ec10b 100644 --- a/sdk/export/metric/go.mod +++ b/sdk/export/metric/go.mod @@ -58,3 +58,5 @@ replace go.opentelemetry.io/otel/example/passthrough => ../../../example/passthr replace go.opentelemetry.io/otel/exporters/otlp/otlptrace => ../../../exporters/otlp/otlptrace replace go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc => ../../../exporters/otlp/otlptrace/otlptracegrpc + +replace go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp => ../../../exporters/otlp/otlptrace/otlptracehttp diff --git a/sdk/go.mod b/sdk/go.mod index 46f516491fd..79cbbf0f08d 100644 --- a/sdk/go.mod +++ b/sdk/go.mod @@ -59,3 +59,5 @@ replace go.opentelemetry.io/otel/example/passthrough => ../example/passthrough replace go.opentelemetry.io/otel/exporters/otlp/otlptrace => ../exporters/otlp/otlptrace replace go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc => ../exporters/otlp/otlptrace/otlptracegrpc + +replace go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp => ../exporters/otlp/otlptrace/otlptracehttp diff --git a/sdk/metric/go.mod b/sdk/metric/go.mod index b42dcd5da57..488eb2bdb5c 100644 --- a/sdk/metric/go.mod +++ b/sdk/metric/go.mod @@ -60,3 +60,5 @@ replace go.opentelemetry.io/otel/example/passthrough => ../../example/passthroug replace go.opentelemetry.io/otel/exporters/otlp/otlptrace => ../../exporters/otlp/otlptrace replace go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc => ../../exporters/otlp/otlptrace/otlptracegrpc + +replace go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp => ../../exporters/otlp/otlptrace/otlptracehttp diff --git a/trace/go.mod b/trace/go.mod index 5874210ad62..ced132c7b14 100644 --- a/trace/go.mod +++ b/trace/go.mod @@ -57,3 +57,5 @@ replace go.opentelemetry.io/otel/example/passthrough => ../example/passthrough replace go.opentelemetry.io/otel/exporters/otlp/otlptrace => ../exporters/otlp/otlptrace replace go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc => ../exporters/otlp/otlptrace/otlptracegrpc + +replace go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp => ../exporters/otlp/otlptrace/otlptracehttp