mirror of
https://github.com/open-telemetry/opentelemetry-go.git
synced 2024-12-26 21:05:00 +02:00
Decouple otlp/otlptrace/internal
from otlp/internal
using gotmpl (#4397)
* Add gotmpl * Add shared retry pkg * Use template retry pkg in otlpconfig * Update license-check to look at first 4 lines * Template out all otlptrace internal * Add envconfig pkg to otlptrace/internal * Generate otlptrace/internal/otlpconfig * Revert templatizing otlptracegrpc * Add changes to changelog * Fix lint --------- Co-authored-by: Robert Pająk <pellared@hotmail.com>
This commit is contained in:
parent
c4d2d7c9b8
commit
4928282877
@ -53,6 +53,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
|
|||||||
- Log a suggested view that fixes instrument conflicts in `go.opentelemetry.io/otel/sdk/metric`. (#4349)
|
- Log a suggested view that fixes instrument conflicts in `go.opentelemetry.io/otel/sdk/metric`. (#4349)
|
||||||
- Fix possible panic, deadlock and race condition in batch span processor in `go.opentelemetry.io/otel/sdk/trace`. (#4353)
|
- Fix possible panic, deadlock and race condition in batch span processor in `go.opentelemetry.io/otel/sdk/trace`. (#4353)
|
||||||
- Improve context cancelation handling in batch span processor's `ForceFlush` in `go.opentelemetry.io/otel/sdk/trace`. (#4369)
|
- Improve context cancelation handling in batch span processor's `ForceFlush` in `go.opentelemetry.io/otel/sdk/trace`. (#4369)
|
||||||
|
- Decouple `go.opentelemetry.io/otel/exporters/otlp/otlptrace/internal` from `go.opentelemetry.io/otel/exporters/otlp/internal` using gotmpl. (#4397, #3846)
|
||||||
- Do not block the metric SDK when OTLP metric exports are blocked in `go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc` and `go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp`. (#3925, #4395)
|
- Do not block the metric SDK when OTLP metric exports are blocked in `go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc` and `go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp`. (#3925, #4395)
|
||||||
|
|
||||||
## [1.16.0/0.39.0] 2023-05-18
|
## [1.16.0/0.39.0] 2023-05-18
|
||||||
|
9
Makefile
9
Makefile
@ -71,8 +71,11 @@ $(TOOLS)/porto: PACKAGE=github.com/jcchavezs/porto/cmd/porto
|
|||||||
GOJQ = $(TOOLS)/gojq
|
GOJQ = $(TOOLS)/gojq
|
||||||
$(TOOLS)/gojq: PACKAGE=github.com/itchyny/gojq/cmd/gojq
|
$(TOOLS)/gojq: PACKAGE=github.com/itchyny/gojq/cmd/gojq
|
||||||
|
|
||||||
|
GOTMPL = $(TOOLS)/gotmpl
|
||||||
|
$(GOTMPL): PACKAGE=go.opentelemetry.io/build-tools/gotmpl
|
||||||
|
|
||||||
.PHONY: tools
|
.PHONY: tools
|
||||||
tools: $(CROSSLINK) $(DBOTCONF) $(GOLANGCI_LINT) $(MISSPELL) $(GOCOVMERGE) $(STRINGER) $(PORTO) $(GOJQ) $(SEMCONVGEN) $(MULTIMOD) $(SEMCONVKIT)
|
tools: $(CROSSLINK) $(DBOTCONF) $(GOLANGCI_LINT) $(MISSPELL) $(GOCOVMERGE) $(STRINGER) $(PORTO) $(GOJQ) $(SEMCONVGEN) $(MULTIMOD) $(SEMCONVKIT) $(GOTMPL)
|
||||||
|
|
||||||
# Virtualized python tools via docker
|
# Virtualized python tools via docker
|
||||||
|
|
||||||
@ -115,7 +118,7 @@ generate: go-generate vanity-import-fix
|
|||||||
.PHONY: go-generate
|
.PHONY: go-generate
|
||||||
go-generate: $(OTEL_GO_MOD_DIRS:%=go-generate/%)
|
go-generate: $(OTEL_GO_MOD_DIRS:%=go-generate/%)
|
||||||
go-generate/%: DIR=$*
|
go-generate/%: DIR=$*
|
||||||
go-generate/%: | $(STRINGER)
|
go-generate/%: | $(STRINGER) $(GOTMPL)
|
||||||
@echo "$(GO) generate $(DIR)/..." \
|
@echo "$(GO) generate $(DIR)/..." \
|
||||||
&& cd $(DIR) \
|
&& cd $(DIR) \
|
||||||
&& PATH="$(TOOLS):$${PATH}" $(GO) generate ./...
|
&& PATH="$(TOOLS):$${PATH}" $(GO) generate ./...
|
||||||
@ -227,7 +230,7 @@ codespell: | $(CODESPELL)
|
|||||||
.PHONY: license-check
|
.PHONY: license-check
|
||||||
license-check:
|
license-check:
|
||||||
@licRes=$$(for f in $$(find . -type f \( -iname '*.go' -o -iname '*.sh' \) ! -path '**/third_party/*' ! -path './.git/*' ) ; do \
|
@licRes=$$(for f in $$(find . -type f \( -iname '*.go' -o -iname '*.sh' \) ! -path '**/third_party/*' ! -path './.git/*' ) ; do \
|
||||||
awk '/Copyright The OpenTelemetry Authors|generated|GENERATED/ && NR<=3 { found=1; next } END { if (!found) print FILENAME }' $$f; \
|
awk '/Copyright The OpenTelemetry Authors|generated|GENERATED/ && NR<=4 { found=1; next } END { if (!found) print FILENAME }' $$f; \
|
||||||
done); \
|
done); \
|
||||||
if [ -n "$${licRes}" ]; then \
|
if [ -n "$${licRes}" ]; then \
|
||||||
echo "license header checking failed:"; echo "$${licRes}"; \
|
echo "license header checking failed:"; echo "$${licRes}"; \
|
||||||
|
@ -21,7 +21,6 @@ require (
|
|||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
github.com/golang/protobuf v1.5.3 // indirect
|
github.com/golang/protobuf v1.5.3 // indirect
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 // indirect
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.16.0 // indirect
|
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.16.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.16.0 // indirect
|
||||||
go.opentelemetry.io/otel/metric v1.16.0 // indirect
|
go.opentelemetry.io/otel/metric v1.16.0 // indirect
|
||||||
go.opentelemetry.io/proto/otlp v1.0.0 // indirect
|
go.opentelemetry.io/proto/otlp v1.0.0 // indirect
|
||||||
@ -39,6 +38,4 @@ replace go.opentelemetry.io/otel/exporters/otlp/otlptrace => ../../exporters/otl
|
|||||||
|
|
||||||
replace go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc => ../../exporters/otlp/otlptrace/otlptracegrpc
|
replace go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc => ../../exporters/otlp/otlptrace/otlptracegrpc
|
||||||
|
|
||||||
replace go.opentelemetry.io/otel/exporters/otlp/internal/retry => ../../exporters/otlp/internal/retry
|
|
||||||
|
|
||||||
replace go.opentelemetry.io/otel/metric => ../../metric
|
replace go.opentelemetry.io/otel/metric => ../../metric
|
||||||
|
@ -3,10 +3,10 @@ module go.opentelemetry.io/otel/exporters/otlp/otlptrace
|
|||||||
go 1.19
|
go 1.19
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/cenkalti/backoff/v4 v4.2.1
|
||||||
github.com/google/go-cmp v0.5.9
|
github.com/google/go-cmp v0.5.9
|
||||||
github.com/stretchr/testify v1.8.4
|
github.com/stretchr/testify v1.8.4
|
||||||
go.opentelemetry.io/otel v1.16.0
|
go.opentelemetry.io/otel v1.16.0
|
||||||
go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.16.0
|
|
||||||
go.opentelemetry.io/otel/sdk v1.16.0
|
go.opentelemetry.io/otel/sdk v1.16.0
|
||||||
go.opentelemetry.io/otel/trace v1.16.0
|
go.opentelemetry.io/otel/trace v1.16.0
|
||||||
go.opentelemetry.io/proto/otlp v1.0.0
|
go.opentelemetry.io/proto/otlp v1.0.0
|
||||||
@ -15,7 +15,6 @@ require (
|
|||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
|
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/go-logr/logr v1.2.4 // indirect
|
github.com/go-logr/logr v1.2.4 // indirect
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
@ -39,6 +38,4 @@ replace go.opentelemetry.io/otel/sdk => ../../../sdk
|
|||||||
|
|
||||||
replace go.opentelemetry.io/otel/trace => ../../../trace
|
replace go.opentelemetry.io/otel/trace => ../../../trace
|
||||||
|
|
||||||
replace go.opentelemetry.io/otel/exporters/otlp/internal/retry => ../internal/retry
|
|
||||||
|
|
||||||
replace go.opentelemetry.io/otel/metric => ../../../metric
|
replace go.opentelemetry.io/otel/metric => ../../../metric
|
||||||
|
202
exporters/otlp/otlptrace/internal/envconfig/envconfig.go
Normal file
202
exporters/otlp/otlptrace/internal/envconfig/envconfig.go
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
// Code created by gotmpl. DO NOT MODIFY.
|
||||||
|
// source: internal/shared/otlp/envconfig/envconfig.go.tmpl
|
||||||
|
|
||||||
|
// 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 envconfig // import "go.opentelemetry.io/otel/exporters/otlp/otlptrace/internal/envconfig"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel/internal/global"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ConfigFn is the generic function used to set a config.
|
||||||
|
type ConfigFn func(*EnvOptionsReader)
|
||||||
|
|
||||||
|
// EnvOptionsReader reads the required environment variables.
|
||||||
|
type EnvOptionsReader struct {
|
||||||
|
GetEnv func(string) string
|
||||||
|
ReadFile func(string) ([]byte, error)
|
||||||
|
Namespace string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply runs every ConfigFn.
|
||||||
|
func (e *EnvOptionsReader) Apply(opts ...ConfigFn) {
|
||||||
|
for _, o := range opts {
|
||||||
|
o(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetEnvValue gets an OTLP environment variable value of the specified key
|
||||||
|
// using the GetEnv function.
|
||||||
|
// This function prepends the OTLP specified namespace to all key lookups.
|
||||||
|
func (e *EnvOptionsReader) GetEnvValue(key string) (string, bool) {
|
||||||
|
v := strings.TrimSpace(e.GetEnv(keyWithNamespace(e.Namespace, key)))
|
||||||
|
return v, v != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithString retrieves the specified config and passes it to ConfigFn as a string.
|
||||||
|
func WithString(n string, fn func(string)) func(e *EnvOptionsReader) {
|
||||||
|
return func(e *EnvOptionsReader) {
|
||||||
|
if v, ok := e.GetEnvValue(n); ok {
|
||||||
|
fn(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithBool returns a ConfigFn that reads the environment variable n and if it exists passes its parsed bool value to fn.
|
||||||
|
func WithBool(n string, fn func(bool)) ConfigFn {
|
||||||
|
return func(e *EnvOptionsReader) {
|
||||||
|
if v, ok := e.GetEnvValue(n); ok {
|
||||||
|
b := strings.ToLower(v) == "true"
|
||||||
|
fn(b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithDuration retrieves the specified config and passes it to ConfigFn as a duration.
|
||||||
|
func WithDuration(n string, fn func(time.Duration)) func(e *EnvOptionsReader) {
|
||||||
|
return func(e *EnvOptionsReader) {
|
||||||
|
if v, ok := e.GetEnvValue(n); ok {
|
||||||
|
d, err := strconv.Atoi(v)
|
||||||
|
if err != nil {
|
||||||
|
global.Error(err, "parse duration", "input", v)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fn(time.Duration(d) * time.Millisecond)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithHeaders retrieves the specified config and passes it to ConfigFn as a map of HTTP headers.
|
||||||
|
func WithHeaders(n string, fn func(map[string]string)) func(e *EnvOptionsReader) {
|
||||||
|
return func(e *EnvOptionsReader) {
|
||||||
|
if v, ok := e.GetEnvValue(n); ok {
|
||||||
|
fn(stringToHeader(v))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithURL retrieves the specified config and passes it to ConfigFn as a net/url.URL.
|
||||||
|
func WithURL(n string, fn func(*url.URL)) func(e *EnvOptionsReader) {
|
||||||
|
return func(e *EnvOptionsReader) {
|
||||||
|
if v, ok := e.GetEnvValue(n); ok {
|
||||||
|
u, err := url.Parse(v)
|
||||||
|
if err != nil {
|
||||||
|
global.Error(err, "parse url", "input", v)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fn(u)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithCertPool returns a ConfigFn that reads the environment variable n as a filepath to a TLS certificate pool. If it exists, it is parsed as a crypto/x509.CertPool and it is passed to fn.
|
||||||
|
func WithCertPool(n string, fn func(*x509.CertPool)) ConfigFn {
|
||||||
|
return func(e *EnvOptionsReader) {
|
||||||
|
if v, ok := e.GetEnvValue(n); ok {
|
||||||
|
b, err := e.ReadFile(v)
|
||||||
|
if err != nil {
|
||||||
|
global.Error(err, "read tls ca cert file", "file", v)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c, err := createCertPool(b)
|
||||||
|
if err != nil {
|
||||||
|
global.Error(err, "create tls cert pool")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fn(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithClientCert returns a ConfigFn that reads the environment variable nc and nk as filepaths to a client certificate and key pair. If they exists, they are parsed as a crypto/tls.Certificate and it is passed to fn.
|
||||||
|
func WithClientCert(nc, nk string, fn func(tls.Certificate)) ConfigFn {
|
||||||
|
return func(e *EnvOptionsReader) {
|
||||||
|
vc, okc := e.GetEnvValue(nc)
|
||||||
|
vk, okk := e.GetEnvValue(nk)
|
||||||
|
if !okc || !okk {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cert, err := e.ReadFile(vc)
|
||||||
|
if err != nil {
|
||||||
|
global.Error(err, "read tls client cert", "file", vc)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
key, err := e.ReadFile(vk)
|
||||||
|
if err != nil {
|
||||||
|
global.Error(err, "read tls client key", "file", vk)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
crt, err := tls.X509KeyPair(cert, key)
|
||||||
|
if err != nil {
|
||||||
|
global.Error(err, "create tls client key pair")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fn(crt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func keyWithNamespace(ns, key string) string {
|
||||||
|
if ns == "" {
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s_%s", ns, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func stringToHeader(value string) map[string]string {
|
||||||
|
headersPairs := strings.Split(value, ",")
|
||||||
|
headers := make(map[string]string)
|
||||||
|
|
||||||
|
for _, header := range headersPairs {
|
||||||
|
n, v, found := strings.Cut(header, "=")
|
||||||
|
if !found {
|
||||||
|
global.Error(errors.New("missing '="), "parse headers", "input", header)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
name, err := url.QueryUnescape(n)
|
||||||
|
if err != nil {
|
||||||
|
global.Error(err, "escape header key", "key", n)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
trimmedName := strings.TrimSpace(name)
|
||||||
|
value, err := url.QueryUnescape(v)
|
||||||
|
if err != nil {
|
||||||
|
global.Error(err, "escape header value", "value", v)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
trimmedValue := strings.TrimSpace(value)
|
||||||
|
|
||||||
|
headers[trimmedName] = trimmedValue
|
||||||
|
}
|
||||||
|
|
||||||
|
return headers
|
||||||
|
}
|
||||||
|
|
||||||
|
func createCertPool(certBytes []byte) (*x509.CertPool, error) {
|
||||||
|
cp := x509.NewCertPool()
|
||||||
|
if ok := cp.AppendCertsFromPEM(certBytes); !ok {
|
||||||
|
return nil, errors.New("failed to append certificate to the cert pool")
|
||||||
|
}
|
||||||
|
return cp, nil
|
||||||
|
}
|
464
exporters/otlp/otlptrace/internal/envconfig/envconfig_test.go
Normal file
464
exporters/otlp/otlptrace/internal/envconfig/envconfig_test.go
Normal file
@ -0,0 +1,464 @@
|
|||||||
|
// Code created by gotmpl. DO NOT MODIFY.
|
||||||
|
// source: internal/shared/otlp/envconfig/envconfig_test.go.tmpl
|
||||||
|
|
||||||
|
// 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 envconfig
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"errors"
|
||||||
|
"net/url"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
const WeakKey = `
|
||||||
|
-----BEGIN EC PRIVATE KEY-----
|
||||||
|
MHcCAQEEIEbrSPmnlSOXvVzxCyv+VR3a0HDeUTvOcqrdssZ2k4gFoAoGCCqGSM49
|
||||||
|
AwEHoUQDQgAEDMTfv75J315C3K9faptS9iythKOMEeV/Eep73nWX531YAkmmwBSB
|
||||||
|
2dXRD/brsgLnfG57WEpxZuY7dPRbxu33BA==
|
||||||
|
-----END EC PRIVATE KEY-----
|
||||||
|
`
|
||||||
|
|
||||||
|
const WeakCertificate = `
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIBjjCCATWgAwIBAgIUKQSMC66MUw+kPp954ZYOcyKAQDswCgYIKoZIzj0EAwIw
|
||||||
|
EjEQMA4GA1UECgwHb3RlbC1nbzAeFw0yMjEwMTkwMDA5MTlaFw0yMzEwMTkwMDA5
|
||||||
|
MTlaMBIxEDAOBgNVBAoMB290ZWwtZ28wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNC
|
||||||
|
AAQMxN+/vknfXkLcr19qm1L2LK2Eo4wR5X8R6nvedZfnfVgCSabAFIHZ1dEP9uuy
|
||||||
|
Aud8bntYSnFm5jt09FvG7fcEo2kwZzAdBgNVHQ4EFgQUicGuhnTTkYLZwofXMNLK
|
||||||
|
SHFeCWgwHwYDVR0jBBgwFoAUicGuhnTTkYLZwofXMNLKSHFeCWgwDwYDVR0TAQH/
|
||||||
|
BAUwAwEB/zAUBgNVHREEDTALgglsb2NhbGhvc3QwCgYIKoZIzj0EAwIDRwAwRAIg
|
||||||
|
Lfma8FnnxeSOi6223AsFfYwsNZ2RderNsQrS0PjEHb0CIBkrWacqARUAu7uT4cGu
|
||||||
|
jVcIxYQqhId5L8p/mAv2PWZS
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
`
|
||||||
|
|
||||||
|
type testOption struct {
|
||||||
|
TestString string
|
||||||
|
TestBool bool
|
||||||
|
TestDuration time.Duration
|
||||||
|
TestHeaders map[string]string
|
||||||
|
TestURL *url.URL
|
||||||
|
TestTLS *tls.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEnvConfig(t *testing.T) {
|
||||||
|
parsedURL, err := url.Parse("https://example.com")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
options := []testOption{}
|
||||||
|
for _, testcase := range []struct {
|
||||||
|
name string
|
||||||
|
reader EnvOptionsReader
|
||||||
|
configs []ConfigFn
|
||||||
|
expectedOptions []testOption
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "with no namespace and a matching key",
|
||||||
|
reader: EnvOptionsReader{
|
||||||
|
GetEnv: func(n string) string {
|
||||||
|
if n == "HELLO" {
|
||||||
|
return "world"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
},
|
||||||
|
},
|
||||||
|
configs: []ConfigFn{
|
||||||
|
WithString("HELLO", func(v string) {
|
||||||
|
options = append(options, testOption{TestString: v})
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
expectedOptions: []testOption{
|
||||||
|
{
|
||||||
|
TestString: "world",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with no namespace and a non-matching key",
|
||||||
|
reader: EnvOptionsReader{
|
||||||
|
GetEnv: func(n string) string {
|
||||||
|
if n == "HELLO" {
|
||||||
|
return "world"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
},
|
||||||
|
},
|
||||||
|
configs: []ConfigFn{
|
||||||
|
WithString("HOLA", func(v string) {
|
||||||
|
options = append(options, testOption{TestString: v})
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
expectedOptions: []testOption{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with a namespace and a matching key",
|
||||||
|
reader: EnvOptionsReader{
|
||||||
|
Namespace: "MY_NAMESPACE",
|
||||||
|
GetEnv: func(n string) string {
|
||||||
|
if n == "MY_NAMESPACE_HELLO" {
|
||||||
|
return "world"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
},
|
||||||
|
},
|
||||||
|
configs: []ConfigFn{
|
||||||
|
WithString("HELLO", func(v string) {
|
||||||
|
options = append(options, testOption{TestString: v})
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
expectedOptions: []testOption{
|
||||||
|
{
|
||||||
|
TestString: "world",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with no namespace and a non-matching key",
|
||||||
|
reader: EnvOptionsReader{
|
||||||
|
Namespace: "MY_NAMESPACE",
|
||||||
|
GetEnv: func(n string) string {
|
||||||
|
if n == "HELLO" {
|
||||||
|
return "world"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
},
|
||||||
|
},
|
||||||
|
configs: []ConfigFn{
|
||||||
|
WithString("HELLO", func(v string) {
|
||||||
|
options = append(options, testOption{TestString: v})
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
expectedOptions: []testOption{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with a bool config",
|
||||||
|
reader: EnvOptionsReader{
|
||||||
|
GetEnv: func(n string) string {
|
||||||
|
if n == "HELLO" {
|
||||||
|
return "true"
|
||||||
|
} else if n == "WORLD" {
|
||||||
|
return "false"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
},
|
||||||
|
},
|
||||||
|
configs: []ConfigFn{
|
||||||
|
WithBool("HELLO", func(b bool) {
|
||||||
|
options = append(options, testOption{TestBool: b})
|
||||||
|
}),
|
||||||
|
WithBool("WORLD", func(b bool) {
|
||||||
|
options = append(options, testOption{TestBool: b})
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
expectedOptions: []testOption{
|
||||||
|
{
|
||||||
|
TestBool: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
TestBool: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with an invalid bool config",
|
||||||
|
reader: EnvOptionsReader{
|
||||||
|
GetEnv: func(n string) string {
|
||||||
|
if n == "HELLO" {
|
||||||
|
return "world"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
},
|
||||||
|
},
|
||||||
|
configs: []ConfigFn{
|
||||||
|
WithBool("HELLO", func(b bool) {
|
||||||
|
options = append(options, testOption{TestBool: b})
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
expectedOptions: []testOption{
|
||||||
|
{
|
||||||
|
TestBool: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with a duration config",
|
||||||
|
reader: EnvOptionsReader{
|
||||||
|
GetEnv: func(n string) string {
|
||||||
|
if n == "HELLO" {
|
||||||
|
return "60"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
},
|
||||||
|
},
|
||||||
|
configs: []ConfigFn{
|
||||||
|
WithDuration("HELLO", func(v time.Duration) {
|
||||||
|
options = append(options, testOption{TestDuration: v})
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
expectedOptions: []testOption{
|
||||||
|
{
|
||||||
|
TestDuration: 60_000_000, // 60 milliseconds
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with an invalid duration config",
|
||||||
|
reader: EnvOptionsReader{
|
||||||
|
GetEnv: func(n string) string {
|
||||||
|
if n == "HELLO" {
|
||||||
|
return "world"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
},
|
||||||
|
},
|
||||||
|
configs: []ConfigFn{
|
||||||
|
WithDuration("HELLO", func(v time.Duration) {
|
||||||
|
options = append(options, testOption{TestDuration: v})
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
expectedOptions: []testOption{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with headers",
|
||||||
|
reader: EnvOptionsReader{
|
||||||
|
GetEnv: func(n string) string {
|
||||||
|
if n == "HELLO" {
|
||||||
|
return "userId=42,userName=alice"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
},
|
||||||
|
},
|
||||||
|
configs: []ConfigFn{
|
||||||
|
WithHeaders("HELLO", func(v map[string]string) {
|
||||||
|
options = append(options, testOption{TestHeaders: v})
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
expectedOptions: []testOption{
|
||||||
|
{
|
||||||
|
TestHeaders: map[string]string{
|
||||||
|
"userId": "42",
|
||||||
|
"userName": "alice",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with invalid headers",
|
||||||
|
reader: EnvOptionsReader{
|
||||||
|
GetEnv: func(n string) string {
|
||||||
|
if n == "HELLO" {
|
||||||
|
return "world"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
},
|
||||||
|
},
|
||||||
|
configs: []ConfigFn{
|
||||||
|
WithHeaders("HELLO", func(v map[string]string) {
|
||||||
|
options = append(options, testOption{TestHeaders: v})
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
expectedOptions: []testOption{
|
||||||
|
{
|
||||||
|
TestHeaders: map[string]string{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with URL",
|
||||||
|
reader: EnvOptionsReader{
|
||||||
|
GetEnv: func(n string) string {
|
||||||
|
if n == "HELLO" {
|
||||||
|
return "https://example.com"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
},
|
||||||
|
},
|
||||||
|
configs: []ConfigFn{
|
||||||
|
WithURL("HELLO", func(v *url.URL) {
|
||||||
|
options = append(options, testOption{TestURL: v})
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
expectedOptions: []testOption{
|
||||||
|
{
|
||||||
|
TestURL: parsedURL,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with invalid URL",
|
||||||
|
reader: EnvOptionsReader{
|
||||||
|
GetEnv: func(n string) string {
|
||||||
|
if n == "HELLO" {
|
||||||
|
return "i nvalid://url"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
},
|
||||||
|
},
|
||||||
|
configs: []ConfigFn{
|
||||||
|
WithURL("HELLO", func(v *url.URL) {
|
||||||
|
options = append(options, testOption{TestURL: v})
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
expectedOptions: []testOption{},
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(testcase.name, func(t *testing.T) {
|
||||||
|
testcase.reader.Apply(testcase.configs...)
|
||||||
|
assert.Equal(t, testcase.expectedOptions, options)
|
||||||
|
options = []testOption{}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWithTLSConfig(t *testing.T) {
|
||||||
|
pool, err := createCertPool([]byte(WeakCertificate))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
reader := EnvOptionsReader{
|
||||||
|
GetEnv: func(n string) string {
|
||||||
|
if n == "CERTIFICATE" {
|
||||||
|
return "/path/cert.pem"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
},
|
||||||
|
ReadFile: func(p string) ([]byte, error) {
|
||||||
|
if p == "/path/cert.pem" {
|
||||||
|
return []byte(WeakCertificate), nil
|
||||||
|
}
|
||||||
|
return []byte{}, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var option testOption
|
||||||
|
reader.Apply(
|
||||||
|
WithCertPool("CERTIFICATE", func(cp *x509.CertPool) {
|
||||||
|
option = testOption{TestTLS: &tls.Config{RootCAs: cp}}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
// nolint:staticcheck // ignoring tlsCert.RootCAs.Subjects is deprecated ERR because cert does not come from SystemCertPool.
|
||||||
|
assert.Equal(t, pool.Subjects(), option.TestTLS.RootCAs.Subjects())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWithClientCert(t *testing.T) {
|
||||||
|
cert, err := tls.X509KeyPair([]byte(WeakCertificate), []byte(WeakKey))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
reader := EnvOptionsReader{
|
||||||
|
GetEnv: func(n string) string {
|
||||||
|
switch n {
|
||||||
|
case "CLIENT_CERTIFICATE":
|
||||||
|
return "/path/tls.crt"
|
||||||
|
case "CLIENT_KEY":
|
||||||
|
return "/path/tls.key"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
},
|
||||||
|
ReadFile: func(n string) ([]byte, error) {
|
||||||
|
switch n {
|
||||||
|
case "/path/tls.crt":
|
||||||
|
return []byte(WeakCertificate), nil
|
||||||
|
case "/path/tls.key":
|
||||||
|
return []byte(WeakKey), nil
|
||||||
|
}
|
||||||
|
return []byte{}, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var option testOption
|
||||||
|
reader.Apply(
|
||||||
|
WithClientCert("CLIENT_CERTIFICATE", "CLIENT_KEY", func(c tls.Certificate) {
|
||||||
|
option = testOption{TestTLS: &tls.Config{Certificates: []tls.Certificate{c}}}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
assert.Equal(t, cert, option.TestTLS.Certificates[0])
|
||||||
|
|
||||||
|
reader.ReadFile = func(s string) ([]byte, error) { return nil, errors.New("oops") }
|
||||||
|
option.TestTLS = nil
|
||||||
|
reader.Apply(
|
||||||
|
WithClientCert("CLIENT_CERTIFICATE", "CLIENT_KEY", func(c tls.Certificate) {
|
||||||
|
option = testOption{TestTLS: &tls.Config{Certificates: []tls.Certificate{c}}}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
assert.Nil(t, option.TestTLS)
|
||||||
|
|
||||||
|
reader.GetEnv = func(s string) string { return "" }
|
||||||
|
option.TestTLS = nil
|
||||||
|
reader.Apply(
|
||||||
|
WithClientCert("CLIENT_CERTIFICATE", "CLIENT_KEY", func(c tls.Certificate) {
|
||||||
|
option = testOption{TestTLS: &tls.Config{Certificates: []tls.Certificate{c}}}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
assert.Nil(t, option.TestTLS)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStringToHeader(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
value string
|
||||||
|
want map[string]string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "simple test",
|
||||||
|
value: "userId=alice",
|
||||||
|
want: map[string]string{"userId": "alice"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "simple test with spaces",
|
||||||
|
value: " userId = alice ",
|
||||||
|
want: map[string]string{"userId": "alice"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiples headers encoded",
|
||||||
|
value: "userId=alice,serverNode=DF%3A28,isProduction=false",
|
||||||
|
want: map[string]string{
|
||||||
|
"userId": "alice",
|
||||||
|
"serverNode": "DF:28",
|
||||||
|
"isProduction": "false",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid headers format",
|
||||||
|
value: "userId:alice",
|
||||||
|
want: map[string]string{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid key",
|
||||||
|
value: "%XX=missing,userId=alice",
|
||||||
|
want: map[string]string{
|
||||||
|
"userId": "alice",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid value",
|
||||||
|
value: "missing=%XX,userId=alice",
|
||||||
|
want: map[string]string{
|
||||||
|
"userId": "alice",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
assert.Equal(t, tt.want, stringToHeader(tt.value))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
43
exporters/otlp/otlptrace/internal/gen.go
Normal file
43
exporters/otlp/otlptrace/internal/gen.go
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
// 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 internal // import "go.opentelemetry.io/otel/exporters/otlp/otlptrace/internal"
|
||||||
|
|
||||||
|
//go:generate gotmpl --body=../../../../internal/shared/otlp/otlptrace/header.go.tmpl "--data={}" --out=header.go
|
||||||
|
//go:generate gotmpl --body=../../../../internal/shared/otlp/otlptrace/header_test.go.tmpl "--data={}" --out=header_test.go
|
||||||
|
|
||||||
|
//go:generate gotmpl --body=../../../../internal/shared/otlp/envconfig/envconfig.go.tmpl "--data={}" --out=envconfig/envconfig.go
|
||||||
|
//go:generate gotmpl --body=../../../../internal/shared/otlp/envconfig/envconfig_test.go.tmpl "--data={}" --out=envconfig/envconfig_test.go
|
||||||
|
|
||||||
|
//go:generate gotmpl --body=../../../../internal/shared/otlp/otlptrace/otlpconfig/envconfig.go.tmpl "--data={\"envconfigImportPath\": \"go.opentelemetry.io/otel/exporters/otlp/otlptrace/internal/envconfig\"}" --out=otlpconfig/envconfig.go
|
||||||
|
//go:generate gotmpl --body=../../../../internal/shared/otlp/otlptrace/otlpconfig/options.go.tmpl "--data={\"retryImportPath\": \"go.opentelemetry.io/otel/exporters/otlp/otlptrace/internal/retry\"}" --out=otlpconfig/options.go
|
||||||
|
//go:generate gotmpl --body=../../../../internal/shared/otlp/otlptrace/otlpconfig/options_test.go.tmpl "--data={\"envconfigImportPath\": \"go.opentelemetry.io/otel/exporters/otlp/otlptrace/internal/envconfig\"}" --out=otlpconfig/options_test.go
|
||||||
|
//go:generate gotmpl --body=../../../../internal/shared/otlp/otlptrace/otlpconfig/optiontypes.go.tmpl "--data={}" --out=otlpconfig/optiontypes.go
|
||||||
|
//go:generate gotmpl --body=../../../../internal/shared/otlp/otlptrace/otlpconfig/tls.go.tmpl "--data={}" --out=otlpconfig/tls.go
|
||||||
|
|
||||||
|
//go:generate gotmpl --body=../../../../internal/shared/otlp/otlptrace/otlptracetest/client.go.tmpl "--data={}" --out=otlptracetest/client.go
|
||||||
|
//go:generate gotmpl --body=../../../../internal/shared/otlp/otlptrace/otlptracetest/collector.go.tmpl "--data={}" --out=otlptracetest/collector.go
|
||||||
|
//go:generate gotmpl --body=../../../../internal/shared/otlp/otlptrace/otlptracetest/data.go.tmpl "--data={}" --out=otlptracetest/data.go
|
||||||
|
//go:generate gotmpl --body=../../../../internal/shared/otlp/otlptrace/otlptracetest/otlptest.go.tmpl "--data={}" --out=otlptracetest/otlptest.go
|
||||||
|
|
||||||
|
//go:generate gotmpl --body=../../../../internal/shared/otlp/retry/retry.go.tmpl "--data={}" --out=retry/retry.go
|
||||||
|
//go:generate gotmpl --body=../../../../internal/shared/otlp/retry/retry_test.go.tmpl "--data={}" --out=retry/retry_test.go
|
||||||
|
|
||||||
|
//go:generate gotmpl --body=../../../../internal/shared/otlp/otlptrace/tracetransform/attribute.go.tmpl "--data={}" --out=tracetransform/attribute.go
|
||||||
|
//go:generate gotmpl --body=../../../../internal/shared/otlp/otlptrace/tracetransform/attribute_test.go.tmpl "--data={}" --out=tracetransform/attribute_test.go
|
||||||
|
//go:generate gotmpl --body=../../../../internal/shared/otlp/otlptrace/tracetransform/instrumentation.go.tmpl "--data={}" --out=tracetransform/instrumentation.go
|
||||||
|
//go:generate gotmpl --body=../../../../internal/shared/otlp/otlptrace/tracetransform/resource.go.tmpl "--data={}" --out=tracetransform/resource.go
|
||||||
|
//go:generate gotmpl --body=../../../../internal/shared/otlp/otlptrace/tracetransform/resource_test.go.tmpl "--data={}" --out=tracetransform/resource_test.go
|
||||||
|
//go:generate gotmpl --body=../../../../internal/shared/otlp/otlptrace/tracetransform/span.go.tmpl "--data={}" --out=tracetransform/span.go
|
||||||
|
//go:generate gotmpl --body=../../../../internal/shared/otlp/otlptrace/tracetransform/span_test.go.tmpl "--data={}" --out=tracetransform/span_test.go
|
@ -1,3 +1,6 @@
|
|||||||
|
// Code created by gotmpl. DO NOT MODIFY.
|
||||||
|
// source: internal/shared/otlp/otlptrace/header.go.tmpl
|
||||||
|
|
||||||
// Copyright The OpenTelemetry Authors
|
// Copyright The OpenTelemetry Authors
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
@ -18,7 +21,7 @@ import (
|
|||||||
"go.opentelemetry.io/otel/exporters/otlp/otlptrace"
|
"go.opentelemetry.io/otel/exporters/otlp/otlptrace"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetUserAgentHeader returns an OTLP header value form "OTel OTLP Exporter Go/{{ .Version }}"
|
// GetUserAgentHeader returns an OTLP header value form "OTel OTLP Exporter Go/{ .Version }"
|
||||||
// https://github.com/open-telemetry/opentelemetry-specification/blob/v1.20.0/specification/protocol/exporter.md#user-agent
|
// https://github.com/open-telemetry/opentelemetry-specification/blob/v1.20.0/specification/protocol/exporter.md#user-agent
|
||||||
func GetUserAgentHeader() string {
|
func GetUserAgentHeader() string {
|
||||||
return "OTel OTLP Exporter Go/" + otlptrace.Version()
|
return "OTel OTLP Exporter Go/" + otlptrace.Version()
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
// Code created by gotmpl. DO NOT MODIFY.
|
||||||
|
// source: internal/shared/otlp/otlptrace/header_test.go.tmpl
|
||||||
|
|
||||||
// Copyright The OpenTelemetry Authors
|
// Copyright The OpenTelemetry Authors
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
// Code created by gotmpl. DO NOT MODIFY.
|
||||||
|
// source: internal/shared/otlp/otlptrace/otlpconfig/envconfig.go.tmpl
|
||||||
|
|
||||||
// Copyright The OpenTelemetry Authors
|
// Copyright The OpenTelemetry Authors
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
@ -23,7 +26,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go.opentelemetry.io/otel/exporters/otlp/internal/envconfig"
|
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/internal/envconfig"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DefaultEnvOptionsReader is the default environments reader.
|
// DefaultEnvOptionsReader is the default environments reader.
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
// Code created by gotmpl. DO NOT MODIFY.
|
||||||
|
// source: internal/shared/otlp/otlptrace/otlpconfig/options.go.tmpl
|
||||||
|
|
||||||
// Copyright The OpenTelemetry Authors
|
// Copyright The OpenTelemetry Authors
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
@ -17,6 +20,8 @@ package otlpconfig // import "go.opentelemetry.io/otel/exporters/otlp/otlptrace/
|
|||||||
import (
|
import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
@ -25,9 +30,8 @@ import (
|
|||||||
"google.golang.org/grpc/credentials/insecure"
|
"google.golang.org/grpc/credentials/insecure"
|
||||||
"google.golang.org/grpc/encoding/gzip"
|
"google.golang.org/grpc/encoding/gzip"
|
||||||
|
|
||||||
"go.opentelemetry.io/otel/exporters/otlp/internal"
|
"go.opentelemetry.io/otel/exporters/otlp/otlptrace"
|
||||||
"go.opentelemetry.io/otel/exporters/otlp/internal/retry"
|
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/internal/retry"
|
||||||
otinternal "go.opentelemetry.io/otel/exporters/otlp/otlptrace/internal"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -83,13 +87,28 @@ func NewHTTPConfig(opts ...HTTPOption) Config {
|
|||||||
for _, opt := range opts {
|
for _, opt := range opts {
|
||||||
cfg = opt.ApplyHTTPOption(cfg)
|
cfg = opt.ApplyHTTPOption(cfg)
|
||||||
}
|
}
|
||||||
cfg.Traces.URLPath = internal.CleanPath(cfg.Traces.URLPath, DefaultTracesPath)
|
cfg.Traces.URLPath = cleanPath(cfg.Traces.URLPath, DefaultTracesPath)
|
||||||
return cfg
|
return cfg
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// cleanPath returns a path with all spaces trimmed and all redundancies
|
||||||
|
// removed. If urlPath is empty or cleaning it results in an empty string,
|
||||||
|
// defaultPath is returned instead.
|
||||||
|
func cleanPath(urlPath string, defaultPath string) string {
|
||||||
|
tmp := path.Clean(strings.TrimSpace(urlPath))
|
||||||
|
if tmp == "." {
|
||||||
|
return defaultPath
|
||||||
|
}
|
||||||
|
if !path.IsAbs(tmp) {
|
||||||
|
tmp = fmt.Sprintf("/%s", tmp)
|
||||||
|
}
|
||||||
|
return tmp
|
||||||
|
}
|
||||||
|
|
||||||
// NewGRPCConfig returns a new Config with all settings applied from opts and
|
// NewGRPCConfig returns a new Config with all settings applied from opts and
|
||||||
// any unset setting using the default gRPC config values.
|
// any unset setting using the default gRPC config values.
|
||||||
func NewGRPCConfig(opts ...GRPCOption) Config {
|
func NewGRPCConfig(opts ...GRPCOption) Config {
|
||||||
|
userAgent := "OTel OTLP Exporter Go/" + otlptrace.Version()
|
||||||
cfg := Config{
|
cfg := Config{
|
||||||
Traces: SignalConfig{
|
Traces: SignalConfig{
|
||||||
Endpoint: fmt.Sprintf("%s:%d", DefaultCollectorHost, DefaultCollectorGRPCPort),
|
Endpoint: fmt.Sprintf("%s:%d", DefaultCollectorHost, DefaultCollectorGRPCPort),
|
||||||
@ -98,7 +117,7 @@ func NewGRPCConfig(opts ...GRPCOption) Config {
|
|||||||
Timeout: DefaultTimeout,
|
Timeout: DefaultTimeout,
|
||||||
},
|
},
|
||||||
RetryConfig: retry.DefaultConfig,
|
RetryConfig: retry.DefaultConfig,
|
||||||
DialOptions: []grpc.DialOption{grpc.WithUserAgent(otinternal.GetUserAgentHeader())},
|
DialOptions: []grpc.DialOption{grpc.WithUserAgent(userAgent)},
|
||||||
}
|
}
|
||||||
cfg = ApplyGRPCEnvConfigs(cfg)
|
cfg = ApplyGRPCEnvConfigs(cfg)
|
||||||
for _, opt := range opts {
|
for _, opt := range opts {
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
// Code created by gotmpl. DO NOT MODIFY.
|
||||||
|
// source: internal/shared/otlp/otlptrace/otlpconfig/options_test.go.tmpl
|
||||||
|
|
||||||
// Copyright The OpenTelemetry Authors
|
// Copyright The OpenTelemetry Authors
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
@ -12,7 +15,7 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
package otlpconfig_test
|
package otlpconfig
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
@ -21,8 +24,7 @@ import (
|
|||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
"go.opentelemetry.io/otel/exporters/otlp/internal/envconfig"
|
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/internal/envconfig"
|
||||||
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/internal/otlpconfig"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -64,25 +66,25 @@ func (f *fileReader) readFile(filename string) ([]byte, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestConfigs(t *testing.T) {
|
func TestConfigs(t *testing.T) {
|
||||||
tlsCert, err := otlpconfig.CreateTLSConfig([]byte(WeakCertificate))
|
tlsCert, err := CreateTLSConfig([]byte(WeakCertificate))
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
opts []otlpconfig.GenericOption
|
opts []GenericOption
|
||||||
env env
|
env env
|
||||||
fileReader fileReader
|
fileReader fileReader
|
||||||
asserts func(t *testing.T, c *otlpconfig.Config, grpcOption bool)
|
asserts func(t *testing.T, c *Config, grpcOption bool)
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "Test default configs",
|
name: "Test default configs",
|
||||||
asserts: func(t *testing.T, c *otlpconfig.Config, grpcOption bool) {
|
asserts: func(t *testing.T, c *Config, grpcOption bool) {
|
||||||
if grpcOption {
|
if grpcOption {
|
||||||
assert.Equal(t, "localhost:4317", c.Traces.Endpoint)
|
assert.Equal(t, "localhost:4317", c.Traces.Endpoint)
|
||||||
} else {
|
} else {
|
||||||
assert.Equal(t, "localhost:4318", c.Traces.Endpoint)
|
assert.Equal(t, "localhost:4318", c.Traces.Endpoint)
|
||||||
}
|
}
|
||||||
assert.Equal(t, otlpconfig.NoCompression, c.Traces.Compression)
|
assert.Equal(t, NoCompression, c.Traces.Compression)
|
||||||
assert.Equal(t, map[string]string(nil), c.Traces.Headers)
|
assert.Equal(t, map[string]string(nil), c.Traces.Headers)
|
||||||
assert.Equal(t, 10*time.Second, c.Traces.Timeout)
|
assert.Equal(t, 10*time.Second, c.Traces.Timeout)
|
||||||
},
|
},
|
||||||
@ -91,10 +93,10 @@ func TestConfigs(t *testing.T) {
|
|||||||
// Endpoint Tests
|
// Endpoint Tests
|
||||||
{
|
{
|
||||||
name: "Test With Endpoint",
|
name: "Test With Endpoint",
|
||||||
opts: []otlpconfig.GenericOption{
|
opts: []GenericOption{
|
||||||
otlpconfig.WithEndpoint("someendpoint"),
|
WithEndpoint("someendpoint"),
|
||||||
},
|
},
|
||||||
asserts: func(t *testing.T, c *otlpconfig.Config, grpcOption bool) {
|
asserts: func(t *testing.T, c *Config, grpcOption bool) {
|
||||||
assert.Equal(t, "someendpoint", c.Traces.Endpoint)
|
assert.Equal(t, "someendpoint", c.Traces.Endpoint)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -103,7 +105,7 @@ func TestConfigs(t *testing.T) {
|
|||||||
env: map[string]string{
|
env: map[string]string{
|
||||||
"OTEL_EXPORTER_OTLP_ENDPOINT": "https://env.endpoint/prefix",
|
"OTEL_EXPORTER_OTLP_ENDPOINT": "https://env.endpoint/prefix",
|
||||||
},
|
},
|
||||||
asserts: func(t *testing.T, c *otlpconfig.Config, grpcOption bool) {
|
asserts: func(t *testing.T, c *Config, grpcOption bool) {
|
||||||
assert.False(t, c.Traces.Insecure)
|
assert.False(t, c.Traces.Insecure)
|
||||||
if grpcOption {
|
if grpcOption {
|
||||||
assert.Equal(t, "env.endpoint/prefix", c.Traces.Endpoint)
|
assert.Equal(t, "env.endpoint/prefix", c.Traces.Endpoint)
|
||||||
@ -119,7 +121,7 @@ func TestConfigs(t *testing.T) {
|
|||||||
"OTEL_EXPORTER_OTLP_ENDPOINT": "https://overrode.by.signal.specific/env/var",
|
"OTEL_EXPORTER_OTLP_ENDPOINT": "https://overrode.by.signal.specific/env/var",
|
||||||
"OTEL_EXPORTER_OTLP_TRACES_ENDPOINT": "http://env.traces.endpoint",
|
"OTEL_EXPORTER_OTLP_TRACES_ENDPOINT": "http://env.traces.endpoint",
|
||||||
},
|
},
|
||||||
asserts: func(t *testing.T, c *otlpconfig.Config, grpcOption bool) {
|
asserts: func(t *testing.T, c *Config, grpcOption bool) {
|
||||||
assert.True(t, c.Traces.Insecure)
|
assert.True(t, c.Traces.Insecure)
|
||||||
assert.Equal(t, "env.traces.endpoint", c.Traces.Endpoint)
|
assert.Equal(t, "env.traces.endpoint", c.Traces.Endpoint)
|
||||||
if !grpcOption {
|
if !grpcOption {
|
||||||
@ -129,13 +131,13 @@ func TestConfigs(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Test Mixed Environment and With Endpoint",
|
name: "Test Mixed Environment and With Endpoint",
|
||||||
opts: []otlpconfig.GenericOption{
|
opts: []GenericOption{
|
||||||
otlpconfig.WithEndpoint("traces_endpoint"),
|
WithEndpoint("traces_endpoint"),
|
||||||
},
|
},
|
||||||
env: map[string]string{
|
env: map[string]string{
|
||||||
"OTEL_EXPORTER_OTLP_ENDPOINT": "env_endpoint",
|
"OTEL_EXPORTER_OTLP_ENDPOINT": "env_endpoint",
|
||||||
},
|
},
|
||||||
asserts: func(t *testing.T, c *otlpconfig.Config, grpcOption bool) {
|
asserts: func(t *testing.T, c *Config, grpcOption bool) {
|
||||||
assert.Equal(t, "traces_endpoint", c.Traces.Endpoint)
|
assert.Equal(t, "traces_endpoint", c.Traces.Endpoint)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -144,7 +146,7 @@ func TestConfigs(t *testing.T) {
|
|||||||
env: map[string]string{
|
env: map[string]string{
|
||||||
"OTEL_EXPORTER_OTLP_ENDPOINT": "http://env_endpoint",
|
"OTEL_EXPORTER_OTLP_ENDPOINT": "http://env_endpoint",
|
||||||
},
|
},
|
||||||
asserts: func(t *testing.T, c *otlpconfig.Config, grpcOption bool) {
|
asserts: func(t *testing.T, c *Config, grpcOption bool) {
|
||||||
assert.Equal(t, "env_endpoint", c.Traces.Endpoint)
|
assert.Equal(t, "env_endpoint", c.Traces.Endpoint)
|
||||||
assert.Equal(t, true, c.Traces.Insecure)
|
assert.Equal(t, true, c.Traces.Insecure)
|
||||||
},
|
},
|
||||||
@ -154,7 +156,7 @@ func TestConfigs(t *testing.T) {
|
|||||||
env: map[string]string{
|
env: map[string]string{
|
||||||
"OTEL_EXPORTER_OTLP_ENDPOINT": " http://env_endpoint ",
|
"OTEL_EXPORTER_OTLP_ENDPOINT": " http://env_endpoint ",
|
||||||
},
|
},
|
||||||
asserts: func(t *testing.T, c *otlpconfig.Config, grpcOption bool) {
|
asserts: func(t *testing.T, c *Config, grpcOption bool) {
|
||||||
assert.Equal(t, "env_endpoint", c.Traces.Endpoint)
|
assert.Equal(t, "env_endpoint", c.Traces.Endpoint)
|
||||||
assert.Equal(t, true, c.Traces.Insecure)
|
assert.Equal(t, true, c.Traces.Insecure)
|
||||||
},
|
},
|
||||||
@ -164,7 +166,7 @@ func TestConfigs(t *testing.T) {
|
|||||||
env: map[string]string{
|
env: map[string]string{
|
||||||
"OTEL_EXPORTER_OTLP_ENDPOINT": "https://env_endpoint",
|
"OTEL_EXPORTER_OTLP_ENDPOINT": "https://env_endpoint",
|
||||||
},
|
},
|
||||||
asserts: func(t *testing.T, c *otlpconfig.Config, grpcOption bool) {
|
asserts: func(t *testing.T, c *Config, grpcOption bool) {
|
||||||
assert.Equal(t, "env_endpoint", c.Traces.Endpoint)
|
assert.Equal(t, "env_endpoint", c.Traces.Endpoint)
|
||||||
assert.Equal(t, false, c.Traces.Insecure)
|
assert.Equal(t, false, c.Traces.Insecure)
|
||||||
},
|
},
|
||||||
@ -175,7 +177,7 @@ func TestConfigs(t *testing.T) {
|
|||||||
"OTEL_EXPORTER_OTLP_ENDPOINT": "HTTPS://overrode_by_signal_specific",
|
"OTEL_EXPORTER_OTLP_ENDPOINT": "HTTPS://overrode_by_signal_specific",
|
||||||
"OTEL_EXPORTER_OTLP_TRACES_ENDPOINT": "HtTp://env_traces_endpoint",
|
"OTEL_EXPORTER_OTLP_TRACES_ENDPOINT": "HtTp://env_traces_endpoint",
|
||||||
},
|
},
|
||||||
asserts: func(t *testing.T, c *otlpconfig.Config, grpcOption bool) {
|
asserts: func(t *testing.T, c *Config, grpcOption bool) {
|
||||||
assert.Equal(t, "env_traces_endpoint", c.Traces.Endpoint)
|
assert.Equal(t, "env_traces_endpoint", c.Traces.Endpoint)
|
||||||
assert.Equal(t, true, c.Traces.Insecure)
|
assert.Equal(t, true, c.Traces.Insecure)
|
||||||
},
|
},
|
||||||
@ -184,7 +186,7 @@ func TestConfigs(t *testing.T) {
|
|||||||
// Certificate tests
|
// Certificate tests
|
||||||
{
|
{
|
||||||
name: "Test Default Certificate",
|
name: "Test Default Certificate",
|
||||||
asserts: func(t *testing.T, c *otlpconfig.Config, grpcOption bool) {
|
asserts: func(t *testing.T, c *Config, grpcOption bool) {
|
||||||
if grpcOption {
|
if grpcOption {
|
||||||
assert.NotNil(t, c.Traces.GRPCCredentials)
|
assert.NotNil(t, c.Traces.GRPCCredentials)
|
||||||
} else {
|
} else {
|
||||||
@ -194,10 +196,10 @@ func TestConfigs(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Test With Certificate",
|
name: "Test With Certificate",
|
||||||
opts: []otlpconfig.GenericOption{
|
opts: []GenericOption{
|
||||||
otlpconfig.WithTLSClientConfig(tlsCert),
|
WithTLSClientConfig(tlsCert),
|
||||||
},
|
},
|
||||||
asserts: func(t *testing.T, c *otlpconfig.Config, grpcOption bool) {
|
asserts: func(t *testing.T, c *Config, grpcOption bool) {
|
||||||
if grpcOption {
|
if grpcOption {
|
||||||
//TODO: make sure gRPC's credentials actually works
|
//TODO: make sure gRPC's credentials actually works
|
||||||
assert.NotNil(t, c.Traces.GRPCCredentials)
|
assert.NotNil(t, c.Traces.GRPCCredentials)
|
||||||
@ -215,7 +217,7 @@ func TestConfigs(t *testing.T) {
|
|||||||
fileReader: fileReader{
|
fileReader: fileReader{
|
||||||
"cert_path": []byte(WeakCertificate),
|
"cert_path": []byte(WeakCertificate),
|
||||||
},
|
},
|
||||||
asserts: func(t *testing.T, c *otlpconfig.Config, grpcOption bool) {
|
asserts: func(t *testing.T, c *Config, grpcOption bool) {
|
||||||
if grpcOption {
|
if grpcOption {
|
||||||
assert.NotNil(t, c.Traces.GRPCCredentials)
|
assert.NotNil(t, c.Traces.GRPCCredentials)
|
||||||
} else {
|
} else {
|
||||||
@ -234,7 +236,7 @@ func TestConfigs(t *testing.T) {
|
|||||||
"cert_path": []byte(WeakCertificate),
|
"cert_path": []byte(WeakCertificate),
|
||||||
"invalid_cert": []byte("invalid certificate file."),
|
"invalid_cert": []byte("invalid certificate file."),
|
||||||
},
|
},
|
||||||
asserts: func(t *testing.T, c *otlpconfig.Config, grpcOption bool) {
|
asserts: func(t *testing.T, c *Config, grpcOption bool) {
|
||||||
if grpcOption {
|
if grpcOption {
|
||||||
assert.NotNil(t, c.Traces.GRPCCredentials)
|
assert.NotNil(t, c.Traces.GRPCCredentials)
|
||||||
} else {
|
} else {
|
||||||
@ -245,14 +247,14 @@ func TestConfigs(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Test Mixed Environment and With Certificate",
|
name: "Test Mixed Environment and With Certificate",
|
||||||
opts: []otlpconfig.GenericOption{},
|
opts: []GenericOption{},
|
||||||
env: map[string]string{
|
env: map[string]string{
|
||||||
"OTEL_EXPORTER_OTLP_CERTIFICATE": "cert_path",
|
"OTEL_EXPORTER_OTLP_CERTIFICATE": "cert_path",
|
||||||
},
|
},
|
||||||
fileReader: fileReader{
|
fileReader: fileReader{
|
||||||
"cert_path": []byte(WeakCertificate),
|
"cert_path": []byte(WeakCertificate),
|
||||||
},
|
},
|
||||||
asserts: func(t *testing.T, c *otlpconfig.Config, grpcOption bool) {
|
asserts: func(t *testing.T, c *Config, grpcOption bool) {
|
||||||
if grpcOption {
|
if grpcOption {
|
||||||
assert.NotNil(t, c.Traces.GRPCCredentials)
|
assert.NotNil(t, c.Traces.GRPCCredentials)
|
||||||
} else {
|
} else {
|
||||||
@ -265,17 +267,17 @@ func TestConfigs(t *testing.T) {
|
|||||||
// Headers tests
|
// Headers tests
|
||||||
{
|
{
|
||||||
name: "Test With Headers",
|
name: "Test With Headers",
|
||||||
opts: []otlpconfig.GenericOption{
|
opts: []GenericOption{
|
||||||
otlpconfig.WithHeaders(map[string]string{"h1": "v1"}),
|
WithHeaders(map[string]string{"h1": "v1"}),
|
||||||
},
|
},
|
||||||
asserts: func(t *testing.T, c *otlpconfig.Config, grpcOption bool) {
|
asserts: func(t *testing.T, c *Config, grpcOption bool) {
|
||||||
assert.Equal(t, map[string]string{"h1": "v1"}, c.Traces.Headers)
|
assert.Equal(t, map[string]string{"h1": "v1"}, c.Traces.Headers)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Test Environment Headers",
|
name: "Test Environment Headers",
|
||||||
env: map[string]string{"OTEL_EXPORTER_OTLP_HEADERS": "h1=v1,h2=v2"},
|
env: map[string]string{"OTEL_EXPORTER_OTLP_HEADERS": "h1=v1,h2=v2"},
|
||||||
asserts: func(t *testing.T, c *otlpconfig.Config, grpcOption bool) {
|
asserts: func(t *testing.T, c *Config, grpcOption bool) {
|
||||||
assert.Equal(t, map[string]string{"h1": "v1", "h2": "v2"}, c.Traces.Headers)
|
assert.Equal(t, map[string]string{"h1": "v1", "h2": "v2"}, c.Traces.Headers)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -285,15 +287,15 @@ func TestConfigs(t *testing.T) {
|
|||||||
"OTEL_EXPORTER_OTLP_HEADERS": "overrode_by_signal_specific",
|
"OTEL_EXPORTER_OTLP_HEADERS": "overrode_by_signal_specific",
|
||||||
"OTEL_EXPORTER_OTLP_TRACES_HEADERS": "h1=v1,h2=v2",
|
"OTEL_EXPORTER_OTLP_TRACES_HEADERS": "h1=v1,h2=v2",
|
||||||
},
|
},
|
||||||
asserts: func(t *testing.T, c *otlpconfig.Config, grpcOption bool) {
|
asserts: func(t *testing.T, c *Config, grpcOption bool) {
|
||||||
assert.Equal(t, map[string]string{"h1": "v1", "h2": "v2"}, c.Traces.Headers)
|
assert.Equal(t, map[string]string{"h1": "v1", "h2": "v2"}, c.Traces.Headers)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Test Mixed Environment and With Headers",
|
name: "Test Mixed Environment and With Headers",
|
||||||
env: map[string]string{"OTEL_EXPORTER_OTLP_HEADERS": "h1=v1,h2=v2"},
|
env: map[string]string{"OTEL_EXPORTER_OTLP_HEADERS": "h1=v1,h2=v2"},
|
||||||
opts: []otlpconfig.GenericOption{},
|
opts: []GenericOption{},
|
||||||
asserts: func(t *testing.T, c *otlpconfig.Config, grpcOption bool) {
|
asserts: func(t *testing.T, c *Config, grpcOption bool) {
|
||||||
assert.Equal(t, map[string]string{"h1": "v1", "h2": "v2"}, c.Traces.Headers)
|
assert.Equal(t, map[string]string{"h1": "v1", "h2": "v2"}, c.Traces.Headers)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -301,11 +303,11 @@ func TestConfigs(t *testing.T) {
|
|||||||
// Compression Tests
|
// Compression Tests
|
||||||
{
|
{
|
||||||
name: "Test With Compression",
|
name: "Test With Compression",
|
||||||
opts: []otlpconfig.GenericOption{
|
opts: []GenericOption{
|
||||||
otlpconfig.WithCompression(otlpconfig.GzipCompression),
|
WithCompression(GzipCompression),
|
||||||
},
|
},
|
||||||
asserts: func(t *testing.T, c *otlpconfig.Config, grpcOption bool) {
|
asserts: func(t *testing.T, c *Config, grpcOption bool) {
|
||||||
assert.Equal(t, otlpconfig.GzipCompression, c.Traces.Compression)
|
assert.Equal(t, GzipCompression, c.Traces.Compression)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -313,8 +315,8 @@ func TestConfigs(t *testing.T) {
|
|||||||
env: map[string]string{
|
env: map[string]string{
|
||||||
"OTEL_EXPORTER_OTLP_COMPRESSION": "gzip",
|
"OTEL_EXPORTER_OTLP_COMPRESSION": "gzip",
|
||||||
},
|
},
|
||||||
asserts: func(t *testing.T, c *otlpconfig.Config, grpcOption bool) {
|
asserts: func(t *testing.T, c *Config, grpcOption bool) {
|
||||||
assert.Equal(t, otlpconfig.GzipCompression, c.Traces.Compression)
|
assert.Equal(t, GzipCompression, c.Traces.Compression)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -322,30 +324,30 @@ func TestConfigs(t *testing.T) {
|
|||||||
env: map[string]string{
|
env: map[string]string{
|
||||||
"OTEL_EXPORTER_OTLP_TRACES_COMPRESSION": "gzip",
|
"OTEL_EXPORTER_OTLP_TRACES_COMPRESSION": "gzip",
|
||||||
},
|
},
|
||||||
asserts: func(t *testing.T, c *otlpconfig.Config, grpcOption bool) {
|
asserts: func(t *testing.T, c *Config, grpcOption bool) {
|
||||||
assert.Equal(t, otlpconfig.GzipCompression, c.Traces.Compression)
|
assert.Equal(t, GzipCompression, c.Traces.Compression)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Test Mixed Environment and With Compression",
|
name: "Test Mixed Environment and With Compression",
|
||||||
opts: []otlpconfig.GenericOption{
|
opts: []GenericOption{
|
||||||
otlpconfig.WithCompression(otlpconfig.NoCompression),
|
WithCompression(NoCompression),
|
||||||
},
|
},
|
||||||
env: map[string]string{
|
env: map[string]string{
|
||||||
"OTEL_EXPORTER_OTLP_TRACES_COMPRESSION": "gzip",
|
"OTEL_EXPORTER_OTLP_TRACES_COMPRESSION": "gzip",
|
||||||
},
|
},
|
||||||
asserts: func(t *testing.T, c *otlpconfig.Config, grpcOption bool) {
|
asserts: func(t *testing.T, c *Config, grpcOption bool) {
|
||||||
assert.Equal(t, otlpconfig.NoCompression, c.Traces.Compression)
|
assert.Equal(t, NoCompression, c.Traces.Compression)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// Timeout Tests
|
// Timeout Tests
|
||||||
{
|
{
|
||||||
name: "Test With Timeout",
|
name: "Test With Timeout",
|
||||||
opts: []otlpconfig.GenericOption{
|
opts: []GenericOption{
|
||||||
otlpconfig.WithTimeout(time.Duration(5 * time.Second)),
|
WithTimeout(time.Duration(5 * time.Second)),
|
||||||
},
|
},
|
||||||
asserts: func(t *testing.T, c *otlpconfig.Config, grpcOption bool) {
|
asserts: func(t *testing.T, c *Config, grpcOption bool) {
|
||||||
assert.Equal(t, 5*time.Second, c.Traces.Timeout)
|
assert.Equal(t, 5*time.Second, c.Traces.Timeout)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -354,7 +356,7 @@ func TestConfigs(t *testing.T) {
|
|||||||
env: map[string]string{
|
env: map[string]string{
|
||||||
"OTEL_EXPORTER_OTLP_TIMEOUT": "15000",
|
"OTEL_EXPORTER_OTLP_TIMEOUT": "15000",
|
||||||
},
|
},
|
||||||
asserts: func(t *testing.T, c *otlpconfig.Config, grpcOption bool) {
|
asserts: func(t *testing.T, c *Config, grpcOption bool) {
|
||||||
assert.Equal(t, c.Traces.Timeout, 15*time.Second)
|
assert.Equal(t, c.Traces.Timeout, 15*time.Second)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -364,7 +366,7 @@ func TestConfigs(t *testing.T) {
|
|||||||
"OTEL_EXPORTER_OTLP_TIMEOUT": "15000",
|
"OTEL_EXPORTER_OTLP_TIMEOUT": "15000",
|
||||||
"OTEL_EXPORTER_OTLP_TRACES_TIMEOUT": "27000",
|
"OTEL_EXPORTER_OTLP_TRACES_TIMEOUT": "27000",
|
||||||
},
|
},
|
||||||
asserts: func(t *testing.T, c *otlpconfig.Config, grpcOption bool) {
|
asserts: func(t *testing.T, c *Config, grpcOption bool) {
|
||||||
assert.Equal(t, c.Traces.Timeout, 27*time.Second)
|
assert.Equal(t, c.Traces.Timeout, 27*time.Second)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -374,10 +376,10 @@ func TestConfigs(t *testing.T) {
|
|||||||
"OTEL_EXPORTER_OTLP_TIMEOUT": "15000",
|
"OTEL_EXPORTER_OTLP_TIMEOUT": "15000",
|
||||||
"OTEL_EXPORTER_OTLP_TRACES_TIMEOUT": "27000",
|
"OTEL_EXPORTER_OTLP_TRACES_TIMEOUT": "27000",
|
||||||
},
|
},
|
||||||
opts: []otlpconfig.GenericOption{
|
opts: []GenericOption{
|
||||||
otlpconfig.WithTimeout(5 * time.Second),
|
WithTimeout(5 * time.Second),
|
||||||
},
|
},
|
||||||
asserts: func(t *testing.T, c *otlpconfig.Config, grpcOption bool) {
|
asserts: func(t *testing.T, c *Config, grpcOption bool) {
|
||||||
assert.Equal(t, c.Traces.Timeout, 5*time.Second)
|
assert.Equal(t, c.Traces.Timeout, 5*time.Second)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -385,37 +387,103 @@ func TestConfigs(t *testing.T) {
|
|||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
origEOR := otlpconfig.DefaultEnvOptionsReader
|
origEOR := DefaultEnvOptionsReader
|
||||||
otlpconfig.DefaultEnvOptionsReader = envconfig.EnvOptionsReader{
|
DefaultEnvOptionsReader = envconfig.EnvOptionsReader{
|
||||||
GetEnv: tt.env.getEnv,
|
GetEnv: tt.env.getEnv,
|
||||||
ReadFile: tt.fileReader.readFile,
|
ReadFile: tt.fileReader.readFile,
|
||||||
Namespace: "OTEL_EXPORTER_OTLP",
|
Namespace: "OTEL_EXPORTER_OTLP",
|
||||||
}
|
}
|
||||||
t.Cleanup(func() { otlpconfig.DefaultEnvOptionsReader = origEOR })
|
t.Cleanup(func() { DefaultEnvOptionsReader = origEOR })
|
||||||
|
|
||||||
// Tests Generic options as HTTP Options
|
// Tests Generic options as HTTP Options
|
||||||
cfg := otlpconfig.NewHTTPConfig(asHTTPOptions(tt.opts)...)
|
cfg := NewHTTPConfig(asHTTPOptions(tt.opts)...)
|
||||||
tt.asserts(t, &cfg, false)
|
tt.asserts(t, &cfg, false)
|
||||||
|
|
||||||
// Tests Generic options as gRPC Options
|
// Tests Generic options as gRPC Options
|
||||||
cfg = otlpconfig.NewGRPCConfig(asGRPCOptions(tt.opts)...)
|
cfg = NewGRPCConfig(asGRPCOptions(tt.opts)...)
|
||||||
tt.asserts(t, &cfg, true)
|
tt.asserts(t, &cfg, true)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func asHTTPOptions(opts []otlpconfig.GenericOption) []otlpconfig.HTTPOption {
|
func asHTTPOptions(opts []GenericOption) []HTTPOption {
|
||||||
converted := make([]otlpconfig.HTTPOption, len(opts))
|
converted := make([]HTTPOption, len(opts))
|
||||||
for i, o := range opts {
|
for i, o := range opts {
|
||||||
converted[i] = otlpconfig.NewHTTPOption(o.ApplyHTTPOption)
|
converted[i] = NewHTTPOption(o.ApplyHTTPOption)
|
||||||
}
|
}
|
||||||
return converted
|
return converted
|
||||||
}
|
}
|
||||||
|
|
||||||
func asGRPCOptions(opts []otlpconfig.GenericOption) []otlpconfig.GRPCOption {
|
func asGRPCOptions(opts []GenericOption) []GRPCOption {
|
||||||
converted := make([]otlpconfig.GRPCOption, len(opts))
|
converted := make([]GRPCOption, len(opts))
|
||||||
for i, o := range opts {
|
for i, o := range opts {
|
||||||
converted[i] = otlpconfig.NewGRPCOption(o.ApplyGRPCOption)
|
converted[i] = NewGRPCOption(o.ApplyGRPCOption)
|
||||||
}
|
}
|
||||||
return converted
|
return converted
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCleanPath(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
urlPath string
|
||||||
|
defaultPath string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "clean empty path",
|
||||||
|
args: args{
|
||||||
|
urlPath: "",
|
||||||
|
defaultPath: "DefaultPath",
|
||||||
|
},
|
||||||
|
want: "DefaultPath",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "clean metrics path",
|
||||||
|
args: args{
|
||||||
|
urlPath: "/prefix/v1/metrics",
|
||||||
|
defaultPath: "DefaultMetricsPath",
|
||||||
|
},
|
||||||
|
want: "/prefix/v1/metrics",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "clean traces path",
|
||||||
|
args: args{
|
||||||
|
urlPath: "https://env_endpoint",
|
||||||
|
defaultPath: "DefaultTracesPath",
|
||||||
|
},
|
||||||
|
want: "/https:/env_endpoint",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "spaces trimmed",
|
||||||
|
args: args{
|
||||||
|
urlPath: " /dir",
|
||||||
|
},
|
||||||
|
want: "/dir",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "clean path empty",
|
||||||
|
args: args{
|
||||||
|
urlPath: "dir/..",
|
||||||
|
defaultPath: "DefaultTracesPath",
|
||||||
|
},
|
||||||
|
want: "DefaultTracesPath",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "make absolute",
|
||||||
|
args: args{
|
||||||
|
urlPath: "dir/a",
|
||||||
|
},
|
||||||
|
want: "/dir/a",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if got := cleanPath(tt.args.urlPath, tt.args.defaultPath); got != tt.want {
|
||||||
|
t.Errorf("CleanPath() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
// Code created by gotmpl. DO NOT MODIFY.
|
||||||
|
// source: internal/shared/otlp/otlptrace/otlpconfig/optiontypes.go.tmpl
|
||||||
|
|
||||||
// Copyright The OpenTelemetry Authors
|
// Copyright The OpenTelemetry Authors
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
// Code created by gotmpl. DO NOT MODIFY.
|
||||||
|
// source: internal/shared/otlp/otlptrace/otlpconfig/tls.go.tmpl
|
||||||
|
|
||||||
// Copyright The OpenTelemetry Authors
|
// Copyright The OpenTelemetry Authors
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
// Code created by gotmpl. DO NOT MODIFY.
|
||||||
|
// source: internal/shared/otlp/otlptrace/otlptracetest/client.go.tmpl
|
||||||
|
|
||||||
// Copyright The OpenTelemetry Authors
|
// Copyright The OpenTelemetry Authors
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
// Code created by gotmpl. DO NOT MODIFY.
|
||||||
|
// source: internal/shared/otlp/otlptrace/otlptracetest/collector.go.tmpl
|
||||||
|
|
||||||
// Copyright The OpenTelemetry Authors
|
// Copyright The OpenTelemetry Authors
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
// Code created by gotmpl. DO NOT MODIFY.
|
||||||
|
// source: internal/shared/otlp/otlptrace/otlptracetest/data.go.tmpl
|
||||||
|
|
||||||
// Copyright The OpenTelemetry Authors
|
// Copyright The OpenTelemetry Authors
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
// Code created by gotmpl. DO NOT MODIFY.
|
||||||
|
// source: internal/shared/otlp/otlptrace/otlptracetest/otlptest.go.tmpl
|
||||||
|
|
||||||
// Copyright The OpenTelemetry Authors
|
// Copyright The OpenTelemetry Authors
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
156
exporters/otlp/otlptrace/internal/retry/retry.go
Normal file
156
exporters/otlp/otlptrace/internal/retry/retry.go
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
// Code created by gotmpl. DO NOT MODIFY.
|
||||||
|
// source: internal/shared/otlp/retry/retry.go.tmpl
|
||||||
|
|
||||||
|
// 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 retry provides request retry functionality that can perform
|
||||||
|
// configurable exponential backoff for transient errors and honor any
|
||||||
|
// explicit throttle responses received.
|
||||||
|
package retry // import "go.opentelemetry.io/otel/exporters/otlp/otlptrace/internal/retry"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/cenkalti/backoff/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DefaultConfig are the recommended defaults to use.
|
||||||
|
var DefaultConfig = Config{
|
||||||
|
Enabled: true,
|
||||||
|
InitialInterval: 5 * time.Second,
|
||||||
|
MaxInterval: 30 * time.Second,
|
||||||
|
MaxElapsedTime: time.Minute,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config defines configuration for retrying batches in case of export failure
|
||||||
|
// using an exponential backoff.
|
||||||
|
type Config struct {
|
||||||
|
// Enabled indicates whether to not retry sending batches in case of
|
||||||
|
// export failure.
|
||||||
|
Enabled bool
|
||||||
|
// InitialInterval the time to wait after the first failure before
|
||||||
|
// retrying.
|
||||||
|
InitialInterval time.Duration
|
||||||
|
// MaxInterval is the upper bound on backoff interval. Once this value is
|
||||||
|
// reached the delay between consecutive retries will always be
|
||||||
|
// `MaxInterval`.
|
||||||
|
MaxInterval time.Duration
|
||||||
|
// MaxElapsedTime is the maximum amount of time (including retries) spent
|
||||||
|
// trying to send a request/batch. Once this value is reached, the data
|
||||||
|
// is discarded.
|
||||||
|
MaxElapsedTime time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// RequestFunc wraps a request with retry logic.
|
||||||
|
type RequestFunc func(context.Context, func(context.Context) error) error
|
||||||
|
|
||||||
|
// EvaluateFunc returns if an error is retry-able and if an explicit throttle
|
||||||
|
// duration should be honored that was included in the error.
|
||||||
|
//
|
||||||
|
// The function must return true if the error argument is retry-able,
|
||||||
|
// otherwise it must return false for the first return parameter.
|
||||||
|
//
|
||||||
|
// The function must return a non-zero time.Duration if the error contains
|
||||||
|
// explicit throttle duration that should be honored, otherwise it must return
|
||||||
|
// a zero valued time.Duration.
|
||||||
|
type EvaluateFunc func(error) (bool, time.Duration)
|
||||||
|
|
||||||
|
// RequestFunc returns a RequestFunc using the evaluate function to determine
|
||||||
|
// if requests can be retried and based on the exponential backoff
|
||||||
|
// configuration of c.
|
||||||
|
func (c Config) RequestFunc(evaluate EvaluateFunc) RequestFunc {
|
||||||
|
if !c.Enabled {
|
||||||
|
return func(ctx context.Context, fn func(context.Context) error) error {
|
||||||
|
return fn(ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return func(ctx context.Context, fn func(context.Context) error) error {
|
||||||
|
// Do not use NewExponentialBackOff since it calls Reset and the code here
|
||||||
|
// must call Reset after changing the InitialInterval (this saves an
|
||||||
|
// unnecessary call to Now).
|
||||||
|
b := &backoff.ExponentialBackOff{
|
||||||
|
InitialInterval: c.InitialInterval,
|
||||||
|
RandomizationFactor: backoff.DefaultRandomizationFactor,
|
||||||
|
Multiplier: backoff.DefaultMultiplier,
|
||||||
|
MaxInterval: c.MaxInterval,
|
||||||
|
MaxElapsedTime: c.MaxElapsedTime,
|
||||||
|
Stop: backoff.Stop,
|
||||||
|
Clock: backoff.SystemClock,
|
||||||
|
}
|
||||||
|
b.Reset()
|
||||||
|
|
||||||
|
for {
|
||||||
|
err := fn(ctx)
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
retryable, throttle := evaluate(err)
|
||||||
|
if !retryable {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
bOff := b.NextBackOff()
|
||||||
|
if bOff == backoff.Stop {
|
||||||
|
return fmt.Errorf("max retry time elapsed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for the greater of the backoff or throttle delay.
|
||||||
|
var delay time.Duration
|
||||||
|
if bOff > throttle {
|
||||||
|
delay = bOff
|
||||||
|
} else {
|
||||||
|
elapsed := b.GetElapsedTime()
|
||||||
|
if b.MaxElapsedTime != 0 && elapsed+throttle > b.MaxElapsedTime {
|
||||||
|
return fmt.Errorf("max retry time would elapse: %w", err)
|
||||||
|
}
|
||||||
|
delay = throttle
|
||||||
|
}
|
||||||
|
|
||||||
|
if ctxErr := waitFunc(ctx, delay); ctxErr != nil {
|
||||||
|
return fmt.Errorf("%w: %s", ctxErr, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow override for testing.
|
||||||
|
var waitFunc = wait
|
||||||
|
|
||||||
|
// wait takes the caller's context, and the amount of time to wait. It will
|
||||||
|
// return nil if the timer fires before or at the same time as the context's
|
||||||
|
// deadline. This indicates that the call can be retried.
|
||||||
|
func wait(ctx context.Context, delay time.Duration) error {
|
||||||
|
timer := time.NewTimer(delay)
|
||||||
|
defer timer.Stop()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
// Handle the case where the timer and context deadline end
|
||||||
|
// simultaneously by prioritizing the timer expiration nil value
|
||||||
|
// response.
|
||||||
|
select {
|
||||||
|
case <-timer.C:
|
||||||
|
default:
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
case <-timer.C:
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
261
exporters/otlp/otlptrace/internal/retry/retry_test.go
Normal file
261
exporters/otlp/otlptrace/internal/retry/retry_test.go
Normal file
@ -0,0 +1,261 @@
|
|||||||
|
// Code created by gotmpl. DO NOT MODIFY.
|
||||||
|
// source: internal/shared/otlp/retry/retry_test.go.tmpl
|
||||||
|
|
||||||
|
// 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 retry
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"math"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/cenkalti/backoff/v4"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestWait(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
ctx context.Context
|
||||||
|
delay time.Duration
|
||||||
|
expected error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
ctx: context.Background(),
|
||||||
|
delay: time.Duration(0),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ctx: context.Background(),
|
||||||
|
delay: time.Duration(1),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ctx: context.Background(),
|
||||||
|
delay: time.Duration(-1),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ctx: func() context.Context {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
cancel()
|
||||||
|
return ctx
|
||||||
|
}(),
|
||||||
|
// Ensure the timer and context do not end simultaneously.
|
||||||
|
delay: 1 * time.Hour,
|
||||||
|
expected: context.Canceled,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
err := wait(test.ctx, test.delay)
|
||||||
|
if test.expected == nil {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
} else {
|
||||||
|
assert.ErrorIs(t, err, test.expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNonRetryableError(t *testing.T) {
|
||||||
|
ev := func(error) (bool, time.Duration) { return false, 0 }
|
||||||
|
|
||||||
|
reqFunc := Config{
|
||||||
|
Enabled: true,
|
||||||
|
InitialInterval: 1 * time.Nanosecond,
|
||||||
|
MaxInterval: 1 * time.Nanosecond,
|
||||||
|
// Never stop retrying.
|
||||||
|
MaxElapsedTime: 0,
|
||||||
|
}.RequestFunc(ev)
|
||||||
|
ctx := context.Background()
|
||||||
|
assert.NoError(t, reqFunc(ctx, func(context.Context) error {
|
||||||
|
return nil
|
||||||
|
}))
|
||||||
|
assert.ErrorIs(t, reqFunc(ctx, func(context.Context) error {
|
||||||
|
return assert.AnError
|
||||||
|
}), assert.AnError)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestThrottledRetry(t *testing.T) {
|
||||||
|
// Ensure the throttle delay is used by making longer than backoff delay.
|
||||||
|
throttleDelay, backoffDelay := time.Second, time.Nanosecond
|
||||||
|
|
||||||
|
ev := func(error) (bool, time.Duration) {
|
||||||
|
// Retry everything with a throttle delay.
|
||||||
|
return true, throttleDelay
|
||||||
|
}
|
||||||
|
|
||||||
|
reqFunc := Config{
|
||||||
|
Enabled: true,
|
||||||
|
InitialInterval: backoffDelay,
|
||||||
|
MaxInterval: backoffDelay,
|
||||||
|
// Never stop retrying.
|
||||||
|
MaxElapsedTime: 0,
|
||||||
|
}.RequestFunc(ev)
|
||||||
|
|
||||||
|
origWait := waitFunc
|
||||||
|
var done bool
|
||||||
|
waitFunc = func(_ context.Context, delay time.Duration) error {
|
||||||
|
assert.Equal(t, throttleDelay, delay, "retry not throttled")
|
||||||
|
// Try twice to ensure call is attempted again after delay.
|
||||||
|
if done {
|
||||||
|
return assert.AnError
|
||||||
|
}
|
||||||
|
done = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
defer func() { waitFunc = origWait }()
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
assert.ErrorIs(t, reqFunc(ctx, func(context.Context) error {
|
||||||
|
return errors.New("not this error")
|
||||||
|
}), assert.AnError)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBackoffRetry(t *testing.T) {
|
||||||
|
ev := func(error) (bool, time.Duration) { return true, 0 }
|
||||||
|
|
||||||
|
delay := time.Nanosecond
|
||||||
|
reqFunc := Config{
|
||||||
|
Enabled: true,
|
||||||
|
InitialInterval: delay,
|
||||||
|
MaxInterval: delay,
|
||||||
|
// Never stop retrying.
|
||||||
|
MaxElapsedTime: 0,
|
||||||
|
}.RequestFunc(ev)
|
||||||
|
|
||||||
|
origWait := waitFunc
|
||||||
|
var done bool
|
||||||
|
waitFunc = func(_ context.Context, d time.Duration) error {
|
||||||
|
delta := math.Ceil(float64(delay) * backoff.DefaultRandomizationFactor)
|
||||||
|
assert.InDelta(t, delay, d, delta, "retry not backoffed")
|
||||||
|
// Try twice to ensure call is attempted again after delay.
|
||||||
|
if done {
|
||||||
|
return assert.AnError
|
||||||
|
}
|
||||||
|
done = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { waitFunc = origWait })
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
assert.ErrorIs(t, reqFunc(ctx, func(context.Context) error {
|
||||||
|
return errors.New("not this error")
|
||||||
|
}), assert.AnError)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBackoffRetryCanceledContext(t *testing.T) {
|
||||||
|
ev := func(error) (bool, time.Duration) { return true, 0 }
|
||||||
|
|
||||||
|
delay := time.Millisecond
|
||||||
|
reqFunc := Config{
|
||||||
|
Enabled: true,
|
||||||
|
InitialInterval: delay,
|
||||||
|
MaxInterval: delay,
|
||||||
|
// Never stop retrying.
|
||||||
|
MaxElapsedTime: 10 * time.Millisecond,
|
||||||
|
}.RequestFunc(ev)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
count := 0
|
||||||
|
cancel()
|
||||||
|
err := reqFunc(ctx, func(context.Context) error {
|
||||||
|
count++
|
||||||
|
return assert.AnError
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.ErrorIs(t, err, context.Canceled)
|
||||||
|
assert.Contains(t, err.Error(), assert.AnError.Error())
|
||||||
|
assert.Equal(t, 1, count)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestThrottledRetryGreaterThanMaxElapsedTime(t *testing.T) {
|
||||||
|
// Ensure the throttle delay is used by making longer than backoff delay.
|
||||||
|
tDelay, bDelay := time.Hour, time.Nanosecond
|
||||||
|
ev := func(error) (bool, time.Duration) { return true, tDelay }
|
||||||
|
reqFunc := Config{
|
||||||
|
Enabled: true,
|
||||||
|
InitialInterval: bDelay,
|
||||||
|
MaxInterval: bDelay,
|
||||||
|
MaxElapsedTime: tDelay - (time.Nanosecond),
|
||||||
|
}.RequestFunc(ev)
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
assert.Contains(t, reqFunc(ctx, func(context.Context) error {
|
||||||
|
return assert.AnError
|
||||||
|
}).Error(), "max retry time would elapse: ")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMaxElapsedTime(t *testing.T) {
|
||||||
|
ev := func(error) (bool, time.Duration) { return true, 0 }
|
||||||
|
delay := time.Nanosecond
|
||||||
|
reqFunc := Config{
|
||||||
|
Enabled: true,
|
||||||
|
// InitialInterval > MaxElapsedTime means immediate return.
|
||||||
|
InitialInterval: 2 * delay,
|
||||||
|
MaxElapsedTime: delay,
|
||||||
|
}.RequestFunc(ev)
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
assert.Contains(t, reqFunc(ctx, func(context.Context) error {
|
||||||
|
return assert.AnError
|
||||||
|
}).Error(), "max retry time elapsed: ")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRetryNotEnabled(t *testing.T) {
|
||||||
|
ev := func(error) (bool, time.Duration) {
|
||||||
|
t.Error("evaluated retry when not enabled")
|
||||||
|
return false, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
reqFunc := Config{}.RequestFunc(ev)
|
||||||
|
ctx := context.Background()
|
||||||
|
assert.NoError(t, reqFunc(ctx, func(context.Context) error {
|
||||||
|
return nil
|
||||||
|
}))
|
||||||
|
assert.ErrorIs(t, reqFunc(ctx, func(context.Context) error {
|
||||||
|
return assert.AnError
|
||||||
|
}), assert.AnError)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRetryConcurrentSafe(t *testing.T) {
|
||||||
|
ev := func(error) (bool, time.Duration) { return true, 0 }
|
||||||
|
reqFunc := Config{
|
||||||
|
Enabled: true,
|
||||||
|
}.RequestFunc(ev)
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
for i := 1; i < 5; i++ {
|
||||||
|
wg.Add(1)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
var done bool
|
||||||
|
assert.NoError(t, reqFunc(ctx, func(context.Context) error {
|
||||||
|
if !done {
|
||||||
|
done = true
|
||||||
|
return assert.AnError
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}))
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
}
|
@ -1,3 +1,6 @@
|
|||||||
|
// Code created by gotmpl. DO NOT MODIFY.
|
||||||
|
// source: internal/shared/otlp/otlptrace/tracetransform/attribute.go.tmpl
|
||||||
|
|
||||||
// Copyright The OpenTelemetry Authors
|
// Copyright The OpenTelemetry Authors
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
// Code created by gotmpl. DO NOT MODIFY.
|
||||||
|
// source: internal/shared/otlp/otlptrace/tracetransform/attribute_test.go.tmpl
|
||||||
|
|
||||||
// Copyright The OpenTelemetry Authors
|
// Copyright The OpenTelemetry Authors
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
// Code created by gotmpl. DO NOT MODIFY.
|
||||||
|
// source: internal/shared/otlp/otlptrace/tracetransform/instrumentation.go.tmpl
|
||||||
|
|
||||||
// Copyright The OpenTelemetry Authors
|
// Copyright The OpenTelemetry Authors
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
// Code created by gotmpl. DO NOT MODIFY.
|
||||||
|
// source: internal/shared/otlp/otlptrace/tracetransform/resource.go.tmpl
|
||||||
|
|
||||||
// Copyright The OpenTelemetry Authors
|
// Copyright The OpenTelemetry Authors
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
// Code created by gotmpl. DO NOT MODIFY.
|
||||||
|
// source: internal/shared/otlp/otlptrace/tracetransform/resource_test.go.tmpl
|
||||||
|
|
||||||
// Copyright The OpenTelemetry Authors
|
// Copyright The OpenTelemetry Authors
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
// Code created by gotmpl. DO NOT MODIFY.
|
||||||
|
// source: internal/shared/otlp/otlptrace/tracetransform/span.go.tmpl
|
||||||
|
|
||||||
// Copyright The OpenTelemetry Authors
|
// Copyright The OpenTelemetry Authors
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
// Code created by gotmpl. DO NOT MODIFY.
|
||||||
|
// source: internal/shared/otlp/otlptrace/tracetransform/span_test.go.tmpl
|
||||||
|
|
||||||
// Copyright The OpenTelemetry Authors
|
// Copyright The OpenTelemetry Authors
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
@ -311,7 +314,9 @@ func TestSpanData(t *testing.T) {
|
|||||||
|
|
||||||
// Empty parent span ID should be treated as root span.
|
// Empty parent span ID should be treated as root span.
|
||||||
func TestRootSpanData(t *testing.T) {
|
func TestRootSpanData(t *testing.T) {
|
||||||
sd := Spans(tracetest.SpanStubs{{}}.Snapshots())
|
sd := Spans(tracetest.SpanStubs{
|
||||||
|
{},
|
||||||
|
}.Snapshots())
|
||||||
require.Len(t, sd, 1)
|
require.Len(t, sd, 1)
|
||||||
rs := sd[0]
|
rs := sd[0]
|
||||||
scopeSpans := rs.GetScopeSpans()
|
scopeSpans := rs.GetScopeSpans()
|
||||||
@ -323,5 +328,9 @@ func TestRootSpanData(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestSpanDataNilResource(t *testing.T) {
|
func TestSpanDataNilResource(t *testing.T) {
|
||||||
assert.NotPanics(t, func() { Spans(tracetest.SpanStubs{{}}.Snapshots()) })
|
assert.NotPanics(t, func() {
|
||||||
|
Spans(tracetest.SpanStubs{
|
||||||
|
{},
|
||||||
|
}.Snapshots())
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
@ -28,9 +28,9 @@ import (
|
|||||||
|
|
||||||
"go.opentelemetry.io/otel"
|
"go.opentelemetry.io/otel"
|
||||||
"go.opentelemetry.io/otel/exporters/otlp/internal"
|
"go.opentelemetry.io/otel/exporters/otlp/internal"
|
||||||
"go.opentelemetry.io/otel/exporters/otlp/internal/retry"
|
|
||||||
"go.opentelemetry.io/otel/exporters/otlp/otlptrace"
|
"go.opentelemetry.io/otel/exporters/otlp/otlptrace"
|
||||||
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/internal/otlpconfig"
|
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/internal/otlpconfig"
|
||||||
|
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/internal/retry"
|
||||||
coltracepb "go.opentelemetry.io/proto/otlp/collector/trace/v1"
|
coltracepb "go.opentelemetry.io/proto/otlp/collector/trace/v1"
|
||||||
tracepb "go.opentelemetry.io/proto/otlp/trace/v1"
|
tracepb "go.opentelemetry.io/proto/otlp/trace/v1"
|
||||||
)
|
)
|
||||||
|
@ -5,7 +5,6 @@ go 1.19
|
|||||||
require (
|
require (
|
||||||
github.com/stretchr/testify v1.8.4
|
github.com/stretchr/testify v1.8.4
|
||||||
go.opentelemetry.io/otel v1.16.0
|
go.opentelemetry.io/otel v1.16.0
|
||||||
go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.16.0
|
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.16.0
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.16.0
|
||||||
go.opentelemetry.io/otel/sdk v1.16.0
|
go.opentelemetry.io/otel/sdk v1.16.0
|
||||||
go.opentelemetry.io/proto/otlp v1.0.0
|
go.opentelemetry.io/proto/otlp v1.0.0
|
||||||
@ -40,6 +39,4 @@ replace go.opentelemetry.io/otel/exporters/otlp/otlptrace => ../
|
|||||||
|
|
||||||
replace go.opentelemetry.io/otel/trace => ../../../../trace
|
replace go.opentelemetry.io/otel/trace => ../../../../trace
|
||||||
|
|
||||||
replace go.opentelemetry.io/otel/exporters/otlp/internal/retry => ../../internal/retry
|
|
||||||
|
|
||||||
replace go.opentelemetry.io/otel/metric => ../../../../metric
|
replace go.opentelemetry.io/otel/metric => ../../../../metric
|
||||||
|
@ -22,8 +22,8 @@ import (
|
|||||||
"google.golang.org/grpc/credentials"
|
"google.golang.org/grpc/credentials"
|
||||||
|
|
||||||
"go.opentelemetry.io/otel"
|
"go.opentelemetry.io/otel"
|
||||||
"go.opentelemetry.io/otel/exporters/otlp/internal/retry"
|
|
||||||
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/internal/otlpconfig"
|
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/internal/otlpconfig"
|
||||||
|
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/internal/retry"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Option applies an option to the gRPC driver.
|
// Option applies an option to the gRPC driver.
|
||||||
|
@ -31,10 +31,10 @@ import (
|
|||||||
|
|
||||||
"go.opentelemetry.io/otel"
|
"go.opentelemetry.io/otel"
|
||||||
"go.opentelemetry.io/otel/exporters/otlp/internal"
|
"go.opentelemetry.io/otel/exporters/otlp/internal"
|
||||||
"go.opentelemetry.io/otel/exporters/otlp/internal/retry"
|
|
||||||
"go.opentelemetry.io/otel/exporters/otlp/otlptrace"
|
"go.opentelemetry.io/otel/exporters/otlp/otlptrace"
|
||||||
otinternal "go.opentelemetry.io/otel/exporters/otlp/otlptrace/internal"
|
otinternal "go.opentelemetry.io/otel/exporters/otlp/otlptrace/internal"
|
||||||
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/internal/otlpconfig"
|
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/internal/otlpconfig"
|
||||||
|
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/internal/retry"
|
||||||
coltracepb "go.opentelemetry.io/proto/otlp/collector/trace/v1"
|
coltracepb "go.opentelemetry.io/proto/otlp/collector/trace/v1"
|
||||||
tracepb "go.opentelemetry.io/proto/otlp/trace/v1"
|
tracepb "go.opentelemetry.io/proto/otlp/trace/v1"
|
||||||
)
|
)
|
||||||
|
@ -5,7 +5,6 @@ go 1.19
|
|||||||
require (
|
require (
|
||||||
github.com/stretchr/testify v1.8.4
|
github.com/stretchr/testify v1.8.4
|
||||||
go.opentelemetry.io/otel v1.16.0
|
go.opentelemetry.io/otel v1.16.0
|
||||||
go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.16.0
|
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.16.0
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.16.0
|
||||||
go.opentelemetry.io/otel/sdk v1.16.0
|
go.opentelemetry.io/otel/sdk v1.16.0
|
||||||
go.opentelemetry.io/otel/trace v1.16.0
|
go.opentelemetry.io/otel/trace v1.16.0
|
||||||
@ -39,6 +38,4 @@ replace go.opentelemetry.io/otel/sdk => ../../../../sdk
|
|||||||
|
|
||||||
replace go.opentelemetry.io/otel/trace => ../../../../trace
|
replace go.opentelemetry.io/otel/trace => ../../../../trace
|
||||||
|
|
||||||
replace go.opentelemetry.io/otel/exporters/otlp/internal/retry => ../../internal/retry
|
|
||||||
|
|
||||||
replace go.opentelemetry.io/otel/metric => ../../../../metric
|
replace go.opentelemetry.io/otel/metric => ../../../../metric
|
||||||
|
@ -18,8 +18,8 @@ import (
|
|||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go.opentelemetry.io/otel/exporters/otlp/internal/retry"
|
|
||||||
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/internal/otlpconfig"
|
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/internal/otlpconfig"
|
||||||
|
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/internal/retry"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Compression describes the compression used for payloads sent to the
|
// Compression describes the compression used for payloads sent to the
|
||||||
|
3
internal/shared/README.md
Normal file
3
internal/shared/README.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# Shared
|
||||||
|
|
||||||
|
Code under this directory contains reusable internal code which is distributed across packages using `//go:generate gotmpl` in `gen.go` files.
|
202
internal/shared/otlp/envconfig/envconfig.go.tmpl
Normal file
202
internal/shared/otlp/envconfig/envconfig.go.tmpl
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
// Code created by gotmpl. DO NOT MODIFY.
|
||||||
|
// source: internal/shared/otlp/envconfig/envconfig.go.tmpl
|
||||||
|
|
||||||
|
// 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 envconfig
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel/internal/global"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ConfigFn is the generic function used to set a config.
|
||||||
|
type ConfigFn func(*EnvOptionsReader)
|
||||||
|
|
||||||
|
// EnvOptionsReader reads the required environment variables.
|
||||||
|
type EnvOptionsReader struct {
|
||||||
|
GetEnv func(string) string
|
||||||
|
ReadFile func(string) ([]byte, error)
|
||||||
|
Namespace string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply runs every ConfigFn.
|
||||||
|
func (e *EnvOptionsReader) Apply(opts ...ConfigFn) {
|
||||||
|
for _, o := range opts {
|
||||||
|
o(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetEnvValue gets an OTLP environment variable value of the specified key
|
||||||
|
// using the GetEnv function.
|
||||||
|
// This function prepends the OTLP specified namespace to all key lookups.
|
||||||
|
func (e *EnvOptionsReader) GetEnvValue(key string) (string, bool) {
|
||||||
|
v := strings.TrimSpace(e.GetEnv(keyWithNamespace(e.Namespace, key)))
|
||||||
|
return v, v != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithString retrieves the specified config and passes it to ConfigFn as a string.
|
||||||
|
func WithString(n string, fn func(string)) func(e *EnvOptionsReader) {
|
||||||
|
return func(e *EnvOptionsReader) {
|
||||||
|
if v, ok := e.GetEnvValue(n); ok {
|
||||||
|
fn(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithBool returns a ConfigFn that reads the environment variable n and if it exists passes its parsed bool value to fn.
|
||||||
|
func WithBool(n string, fn func(bool)) ConfigFn {
|
||||||
|
return func(e *EnvOptionsReader) {
|
||||||
|
if v, ok := e.GetEnvValue(n); ok {
|
||||||
|
b := strings.ToLower(v) == "true"
|
||||||
|
fn(b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithDuration retrieves the specified config and passes it to ConfigFn as a duration.
|
||||||
|
func WithDuration(n string, fn func(time.Duration)) func(e *EnvOptionsReader) {
|
||||||
|
return func(e *EnvOptionsReader) {
|
||||||
|
if v, ok := e.GetEnvValue(n); ok {
|
||||||
|
d, err := strconv.Atoi(v)
|
||||||
|
if err != nil {
|
||||||
|
global.Error(err, "parse duration", "input", v)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fn(time.Duration(d) * time.Millisecond)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithHeaders retrieves the specified config and passes it to ConfigFn as a map of HTTP headers.
|
||||||
|
func WithHeaders(n string, fn func(map[string]string)) func(e *EnvOptionsReader) {
|
||||||
|
return func(e *EnvOptionsReader) {
|
||||||
|
if v, ok := e.GetEnvValue(n); ok {
|
||||||
|
fn(stringToHeader(v))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithURL retrieves the specified config and passes it to ConfigFn as a net/url.URL.
|
||||||
|
func WithURL(n string, fn func(*url.URL)) func(e *EnvOptionsReader) {
|
||||||
|
return func(e *EnvOptionsReader) {
|
||||||
|
if v, ok := e.GetEnvValue(n); ok {
|
||||||
|
u, err := url.Parse(v)
|
||||||
|
if err != nil {
|
||||||
|
global.Error(err, "parse url", "input", v)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fn(u)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithCertPool returns a ConfigFn that reads the environment variable n as a filepath to a TLS certificate pool. If it exists, it is parsed as a crypto/x509.CertPool and it is passed to fn.
|
||||||
|
func WithCertPool(n string, fn func(*x509.CertPool)) ConfigFn {
|
||||||
|
return func(e *EnvOptionsReader) {
|
||||||
|
if v, ok := e.GetEnvValue(n); ok {
|
||||||
|
b, err := e.ReadFile(v)
|
||||||
|
if err != nil {
|
||||||
|
global.Error(err, "read tls ca cert file", "file", v)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c, err := createCertPool(b)
|
||||||
|
if err != nil {
|
||||||
|
global.Error(err, "create tls cert pool")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fn(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithClientCert returns a ConfigFn that reads the environment variable nc and nk as filepaths to a client certificate and key pair. If they exists, they are parsed as a crypto/tls.Certificate and it is passed to fn.
|
||||||
|
func WithClientCert(nc, nk string, fn func(tls.Certificate)) ConfigFn {
|
||||||
|
return func(e *EnvOptionsReader) {
|
||||||
|
vc, okc := e.GetEnvValue(nc)
|
||||||
|
vk, okk := e.GetEnvValue(nk)
|
||||||
|
if !okc || !okk {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cert, err := e.ReadFile(vc)
|
||||||
|
if err != nil {
|
||||||
|
global.Error(err, "read tls client cert", "file", vc)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
key, err := e.ReadFile(vk)
|
||||||
|
if err != nil {
|
||||||
|
global.Error(err, "read tls client key", "file", vk)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
crt, err := tls.X509KeyPair(cert, key)
|
||||||
|
if err != nil {
|
||||||
|
global.Error(err, "create tls client key pair")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fn(crt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func keyWithNamespace(ns, key string) string {
|
||||||
|
if ns == "" {
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s_%s", ns, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func stringToHeader(value string) map[string]string {
|
||||||
|
headersPairs := strings.Split(value, ",")
|
||||||
|
headers := make(map[string]string)
|
||||||
|
|
||||||
|
for _, header := range headersPairs {
|
||||||
|
n, v, found := strings.Cut(header, "=")
|
||||||
|
if !found {
|
||||||
|
global.Error(errors.New("missing '="), "parse headers", "input", header)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
name, err := url.QueryUnescape(n)
|
||||||
|
if err != nil {
|
||||||
|
global.Error(err, "escape header key", "key", n)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
trimmedName := strings.TrimSpace(name)
|
||||||
|
value, err := url.QueryUnescape(v)
|
||||||
|
if err != nil {
|
||||||
|
global.Error(err, "escape header value", "value", v)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
trimmedValue := strings.TrimSpace(value)
|
||||||
|
|
||||||
|
headers[trimmedName] = trimmedValue
|
||||||
|
}
|
||||||
|
|
||||||
|
return headers
|
||||||
|
}
|
||||||
|
|
||||||
|
func createCertPool(certBytes []byte) (*x509.CertPool, error) {
|
||||||
|
cp := x509.NewCertPool()
|
||||||
|
if ok := cp.AppendCertsFromPEM(certBytes); !ok {
|
||||||
|
return nil, errors.New("failed to append certificate to the cert pool")
|
||||||
|
}
|
||||||
|
return cp, nil
|
||||||
|
}
|
464
internal/shared/otlp/envconfig/envconfig_test.go.tmpl
Normal file
464
internal/shared/otlp/envconfig/envconfig_test.go.tmpl
Normal file
@ -0,0 +1,464 @@
|
|||||||
|
// Code created by gotmpl. DO NOT MODIFY.
|
||||||
|
// source: internal/shared/otlp/envconfig/envconfig_test.go.tmpl
|
||||||
|
|
||||||
|
// 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 envconfig
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"errors"
|
||||||
|
"net/url"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
const WeakKey = `
|
||||||
|
-----BEGIN EC PRIVATE KEY-----
|
||||||
|
MHcCAQEEIEbrSPmnlSOXvVzxCyv+VR3a0HDeUTvOcqrdssZ2k4gFoAoGCCqGSM49
|
||||||
|
AwEHoUQDQgAEDMTfv75J315C3K9faptS9iythKOMEeV/Eep73nWX531YAkmmwBSB
|
||||||
|
2dXRD/brsgLnfG57WEpxZuY7dPRbxu33BA==
|
||||||
|
-----END EC PRIVATE KEY-----
|
||||||
|
`
|
||||||
|
|
||||||
|
const WeakCertificate = `
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIBjjCCATWgAwIBAgIUKQSMC66MUw+kPp954ZYOcyKAQDswCgYIKoZIzj0EAwIw
|
||||||
|
EjEQMA4GA1UECgwHb3RlbC1nbzAeFw0yMjEwMTkwMDA5MTlaFw0yMzEwMTkwMDA5
|
||||||
|
MTlaMBIxEDAOBgNVBAoMB290ZWwtZ28wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNC
|
||||||
|
AAQMxN+/vknfXkLcr19qm1L2LK2Eo4wR5X8R6nvedZfnfVgCSabAFIHZ1dEP9uuy
|
||||||
|
Aud8bntYSnFm5jt09FvG7fcEo2kwZzAdBgNVHQ4EFgQUicGuhnTTkYLZwofXMNLK
|
||||||
|
SHFeCWgwHwYDVR0jBBgwFoAUicGuhnTTkYLZwofXMNLKSHFeCWgwDwYDVR0TAQH/
|
||||||
|
BAUwAwEB/zAUBgNVHREEDTALgglsb2NhbGhvc3QwCgYIKoZIzj0EAwIDRwAwRAIg
|
||||||
|
Lfma8FnnxeSOi6223AsFfYwsNZ2RderNsQrS0PjEHb0CIBkrWacqARUAu7uT4cGu
|
||||||
|
jVcIxYQqhId5L8p/mAv2PWZS
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
`
|
||||||
|
|
||||||
|
type testOption struct {
|
||||||
|
TestString string
|
||||||
|
TestBool bool
|
||||||
|
TestDuration time.Duration
|
||||||
|
TestHeaders map[string]string
|
||||||
|
TestURL *url.URL
|
||||||
|
TestTLS *tls.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEnvConfig(t *testing.T) {
|
||||||
|
parsedURL, err := url.Parse("https://example.com")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
options := []testOption{}
|
||||||
|
for _, testcase := range []struct {
|
||||||
|
name string
|
||||||
|
reader EnvOptionsReader
|
||||||
|
configs []ConfigFn
|
||||||
|
expectedOptions []testOption
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "with no namespace and a matching key",
|
||||||
|
reader: EnvOptionsReader{
|
||||||
|
GetEnv: func(n string) string {
|
||||||
|
if n == "HELLO" {
|
||||||
|
return "world"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
},
|
||||||
|
},
|
||||||
|
configs: []ConfigFn{
|
||||||
|
WithString("HELLO", func(v string) {
|
||||||
|
options = append(options, testOption{TestString: v})
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
expectedOptions: []testOption{
|
||||||
|
{
|
||||||
|
TestString: "world",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with no namespace and a non-matching key",
|
||||||
|
reader: EnvOptionsReader{
|
||||||
|
GetEnv: func(n string) string {
|
||||||
|
if n == "HELLO" {
|
||||||
|
return "world"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
},
|
||||||
|
},
|
||||||
|
configs: []ConfigFn{
|
||||||
|
WithString("HOLA", func(v string) {
|
||||||
|
options = append(options, testOption{TestString: v})
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
expectedOptions: []testOption{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with a namespace and a matching key",
|
||||||
|
reader: EnvOptionsReader{
|
||||||
|
Namespace: "MY_NAMESPACE",
|
||||||
|
GetEnv: func(n string) string {
|
||||||
|
if n == "MY_NAMESPACE_HELLO" {
|
||||||
|
return "world"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
},
|
||||||
|
},
|
||||||
|
configs: []ConfigFn{
|
||||||
|
WithString("HELLO", func(v string) {
|
||||||
|
options = append(options, testOption{TestString: v})
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
expectedOptions: []testOption{
|
||||||
|
{
|
||||||
|
TestString: "world",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with no namespace and a non-matching key",
|
||||||
|
reader: EnvOptionsReader{
|
||||||
|
Namespace: "MY_NAMESPACE",
|
||||||
|
GetEnv: func(n string) string {
|
||||||
|
if n == "HELLO" {
|
||||||
|
return "world"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
},
|
||||||
|
},
|
||||||
|
configs: []ConfigFn{
|
||||||
|
WithString("HELLO", func(v string) {
|
||||||
|
options = append(options, testOption{TestString: v})
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
expectedOptions: []testOption{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with a bool config",
|
||||||
|
reader: EnvOptionsReader{
|
||||||
|
GetEnv: func(n string) string {
|
||||||
|
if n == "HELLO" {
|
||||||
|
return "true"
|
||||||
|
} else if n == "WORLD" {
|
||||||
|
return "false"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
},
|
||||||
|
},
|
||||||
|
configs: []ConfigFn{
|
||||||
|
WithBool("HELLO", func(b bool) {
|
||||||
|
options = append(options, testOption{TestBool: b})
|
||||||
|
}),
|
||||||
|
WithBool("WORLD", func(b bool) {
|
||||||
|
options = append(options, testOption{TestBool: b})
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
expectedOptions: []testOption{
|
||||||
|
{
|
||||||
|
TestBool: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
TestBool: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with an invalid bool config",
|
||||||
|
reader: EnvOptionsReader{
|
||||||
|
GetEnv: func(n string) string {
|
||||||
|
if n == "HELLO" {
|
||||||
|
return "world"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
},
|
||||||
|
},
|
||||||
|
configs: []ConfigFn{
|
||||||
|
WithBool("HELLO", func(b bool) {
|
||||||
|
options = append(options, testOption{TestBool: b})
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
expectedOptions: []testOption{
|
||||||
|
{
|
||||||
|
TestBool: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with a duration config",
|
||||||
|
reader: EnvOptionsReader{
|
||||||
|
GetEnv: func(n string) string {
|
||||||
|
if n == "HELLO" {
|
||||||
|
return "60"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
},
|
||||||
|
},
|
||||||
|
configs: []ConfigFn{
|
||||||
|
WithDuration("HELLO", func(v time.Duration) {
|
||||||
|
options = append(options, testOption{TestDuration: v})
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
expectedOptions: []testOption{
|
||||||
|
{
|
||||||
|
TestDuration: 60_000_000, // 60 milliseconds
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with an invalid duration config",
|
||||||
|
reader: EnvOptionsReader{
|
||||||
|
GetEnv: func(n string) string {
|
||||||
|
if n == "HELLO" {
|
||||||
|
return "world"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
},
|
||||||
|
},
|
||||||
|
configs: []ConfigFn{
|
||||||
|
WithDuration("HELLO", func(v time.Duration) {
|
||||||
|
options = append(options, testOption{TestDuration: v})
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
expectedOptions: []testOption{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with headers",
|
||||||
|
reader: EnvOptionsReader{
|
||||||
|
GetEnv: func(n string) string {
|
||||||
|
if n == "HELLO" {
|
||||||
|
return "userId=42,userName=alice"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
},
|
||||||
|
},
|
||||||
|
configs: []ConfigFn{
|
||||||
|
WithHeaders("HELLO", func(v map[string]string) {
|
||||||
|
options = append(options, testOption{TestHeaders: v})
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
expectedOptions: []testOption{
|
||||||
|
{
|
||||||
|
TestHeaders: map[string]string{
|
||||||
|
"userId": "42",
|
||||||
|
"userName": "alice",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with invalid headers",
|
||||||
|
reader: EnvOptionsReader{
|
||||||
|
GetEnv: func(n string) string {
|
||||||
|
if n == "HELLO" {
|
||||||
|
return "world"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
},
|
||||||
|
},
|
||||||
|
configs: []ConfigFn{
|
||||||
|
WithHeaders("HELLO", func(v map[string]string) {
|
||||||
|
options = append(options, testOption{TestHeaders: v})
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
expectedOptions: []testOption{
|
||||||
|
{
|
||||||
|
TestHeaders: map[string]string{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with URL",
|
||||||
|
reader: EnvOptionsReader{
|
||||||
|
GetEnv: func(n string) string {
|
||||||
|
if n == "HELLO" {
|
||||||
|
return "https://example.com"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
},
|
||||||
|
},
|
||||||
|
configs: []ConfigFn{
|
||||||
|
WithURL("HELLO", func(v *url.URL) {
|
||||||
|
options = append(options, testOption{TestURL: v})
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
expectedOptions: []testOption{
|
||||||
|
{
|
||||||
|
TestURL: parsedURL,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with invalid URL",
|
||||||
|
reader: EnvOptionsReader{
|
||||||
|
GetEnv: func(n string) string {
|
||||||
|
if n == "HELLO" {
|
||||||
|
return "i nvalid://url"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
},
|
||||||
|
},
|
||||||
|
configs: []ConfigFn{
|
||||||
|
WithURL("HELLO", func(v *url.URL) {
|
||||||
|
options = append(options, testOption{TestURL: v})
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
expectedOptions: []testOption{},
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(testcase.name, func(t *testing.T) {
|
||||||
|
testcase.reader.Apply(testcase.configs...)
|
||||||
|
assert.Equal(t, testcase.expectedOptions, options)
|
||||||
|
options = []testOption{}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWithTLSConfig(t *testing.T) {
|
||||||
|
pool, err := createCertPool([]byte(WeakCertificate))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
reader := EnvOptionsReader{
|
||||||
|
GetEnv: func(n string) string {
|
||||||
|
if n == "CERTIFICATE" {
|
||||||
|
return "/path/cert.pem"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
},
|
||||||
|
ReadFile: func(p string) ([]byte, error) {
|
||||||
|
if p == "/path/cert.pem" {
|
||||||
|
return []byte(WeakCertificate), nil
|
||||||
|
}
|
||||||
|
return []byte{}, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var option testOption
|
||||||
|
reader.Apply(
|
||||||
|
WithCertPool("CERTIFICATE", func(cp *x509.CertPool) {
|
||||||
|
option = testOption{TestTLS: &tls.Config{RootCAs: cp}}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
// nolint:staticcheck // ignoring tlsCert.RootCAs.Subjects is deprecated ERR because cert does not come from SystemCertPool.
|
||||||
|
assert.Equal(t, pool.Subjects(), option.TestTLS.RootCAs.Subjects())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWithClientCert(t *testing.T) {
|
||||||
|
cert, err := tls.X509KeyPair([]byte(WeakCertificate), []byte(WeakKey))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
reader := EnvOptionsReader{
|
||||||
|
GetEnv: func(n string) string {
|
||||||
|
switch n {
|
||||||
|
case "CLIENT_CERTIFICATE":
|
||||||
|
return "/path/tls.crt"
|
||||||
|
case "CLIENT_KEY":
|
||||||
|
return "/path/tls.key"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
},
|
||||||
|
ReadFile: func(n string) ([]byte, error) {
|
||||||
|
switch n {
|
||||||
|
case "/path/tls.crt":
|
||||||
|
return []byte(WeakCertificate), nil
|
||||||
|
case "/path/tls.key":
|
||||||
|
return []byte(WeakKey), nil
|
||||||
|
}
|
||||||
|
return []byte{}, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var option testOption
|
||||||
|
reader.Apply(
|
||||||
|
WithClientCert("CLIENT_CERTIFICATE", "CLIENT_KEY", func(c tls.Certificate) {
|
||||||
|
option = testOption{TestTLS: &tls.Config{Certificates: []tls.Certificate{c}}}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
assert.Equal(t, cert, option.TestTLS.Certificates[0])
|
||||||
|
|
||||||
|
reader.ReadFile = func(s string) ([]byte, error) { return nil, errors.New("oops") }
|
||||||
|
option.TestTLS = nil
|
||||||
|
reader.Apply(
|
||||||
|
WithClientCert("CLIENT_CERTIFICATE", "CLIENT_KEY", func(c tls.Certificate) {
|
||||||
|
option = testOption{TestTLS: &tls.Config{Certificates: []tls.Certificate{c}}}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
assert.Nil(t, option.TestTLS)
|
||||||
|
|
||||||
|
reader.GetEnv = func(s string) string { return "" }
|
||||||
|
option.TestTLS = nil
|
||||||
|
reader.Apply(
|
||||||
|
WithClientCert("CLIENT_CERTIFICATE", "CLIENT_KEY", func(c tls.Certificate) {
|
||||||
|
option = testOption{TestTLS: &tls.Config{Certificates: []tls.Certificate{c}}}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
assert.Nil(t, option.TestTLS)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStringToHeader(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
value string
|
||||||
|
want map[string]string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "simple test",
|
||||||
|
value: "userId=alice",
|
||||||
|
want: map[string]string{"userId": "alice"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "simple test with spaces",
|
||||||
|
value: " userId = alice ",
|
||||||
|
want: map[string]string{"userId": "alice"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiples headers encoded",
|
||||||
|
value: "userId=alice,serverNode=DF%3A28,isProduction=false",
|
||||||
|
want: map[string]string{
|
||||||
|
"userId": "alice",
|
||||||
|
"serverNode": "DF:28",
|
||||||
|
"isProduction": "false",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid headers format",
|
||||||
|
value: "userId:alice",
|
||||||
|
want: map[string]string{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid key",
|
||||||
|
value: "%XX=missing,userId=alice",
|
||||||
|
want: map[string]string{
|
||||||
|
"userId": "alice",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid value",
|
||||||
|
value: "missing=%XX,userId=alice",
|
||||||
|
want: map[string]string{
|
||||||
|
"userId": "alice",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
assert.Equal(t, tt.want, stringToHeader(tt.value))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
28
internal/shared/otlp/otlptrace/header.go.tmpl
Normal file
28
internal/shared/otlp/otlptrace/header.go.tmpl
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
// Code created by gotmpl. DO NOT MODIFY.
|
||||||
|
// source: internal/shared/otlp/otlptrace/header.go.tmpl
|
||||||
|
|
||||||
|
// 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 internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"go.opentelemetry.io/otel/exporters/otlp/otlptrace"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetUserAgentHeader returns an OTLP header value form "OTel OTLP Exporter Go/{ .Version }"
|
||||||
|
// https://github.com/open-telemetry/opentelemetry-specification/blob/v1.20.0/specification/protocol/exporter.md#user-agent
|
||||||
|
func GetUserAgentHeader() string {
|
||||||
|
return "OTel OTLP Exporter Go/" + otlptrace.Version()
|
||||||
|
}
|
28
internal/shared/otlp/otlptrace/header_test.go.tmpl
Normal file
28
internal/shared/otlp/otlptrace/header_test.go.tmpl
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
// Code created by gotmpl. DO NOT MODIFY.
|
||||||
|
// source: internal/shared/otlp/otlptrace/header_test.go.tmpl
|
||||||
|
|
||||||
|
// 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 internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetUserAgentHeader(t *testing.T) {
|
||||||
|
require.Regexp(t, "OTel OTLP Exporter Go/1\\..*", GetUserAgentHeader())
|
||||||
|
}
|
153
internal/shared/otlp/otlptrace/otlpconfig/envconfig.go.tmpl
Normal file
153
internal/shared/otlp/otlptrace/otlpconfig/envconfig.go.tmpl
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
// Code created by gotmpl. DO NOT MODIFY.
|
||||||
|
// source: internal/shared/otlp/otlptrace/otlpconfig/envconfig.go.tmpl
|
||||||
|
|
||||||
|
// 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 otlpconfig
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"{{ .envconfigImportPath }}"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DefaultEnvOptionsReader is the default environments reader.
|
||||||
|
var DefaultEnvOptionsReader = envconfig.EnvOptionsReader{
|
||||||
|
GetEnv: os.Getenv,
|
||||||
|
ReadFile: os.ReadFile,
|
||||||
|
Namespace: "OTEL_EXPORTER_OTLP",
|
||||||
|
}
|
||||||
|
|
||||||
|
// ApplyGRPCEnvConfigs applies the env configurations for gRPC.
|
||||||
|
func ApplyGRPCEnvConfigs(cfg Config) Config {
|
||||||
|
opts := getOptionsFromEnv()
|
||||||
|
for _, opt := range opts {
|
||||||
|
cfg = opt.ApplyGRPCOption(cfg)
|
||||||
|
}
|
||||||
|
return cfg
|
||||||
|
}
|
||||||
|
|
||||||
|
// ApplyHTTPEnvConfigs applies the env configurations for HTTP.
|
||||||
|
func ApplyHTTPEnvConfigs(cfg Config) Config {
|
||||||
|
opts := getOptionsFromEnv()
|
||||||
|
for _, opt := range opts {
|
||||||
|
cfg = opt.ApplyHTTPOption(cfg)
|
||||||
|
}
|
||||||
|
return cfg
|
||||||
|
}
|
||||||
|
|
||||||
|
func getOptionsFromEnv() []GenericOption {
|
||||||
|
opts := []GenericOption{}
|
||||||
|
|
||||||
|
tlsConf := &tls.Config{}
|
||||||
|
DefaultEnvOptionsReader.Apply(
|
||||||
|
envconfig.WithURL("ENDPOINT", func(u *url.URL) {
|
||||||
|
opts = append(opts, withEndpointScheme(u))
|
||||||
|
opts = append(opts, newSplitOption(func(cfg Config) Config {
|
||||||
|
cfg.Traces.Endpoint = u.Host
|
||||||
|
// For OTLP/HTTP endpoint URLs without a per-signal
|
||||||
|
// configuration, the passed endpoint is used as a base URL
|
||||||
|
// and the signals are sent to these paths relative to that.
|
||||||
|
cfg.Traces.URLPath = path.Join(u.Path, DefaultTracesPath)
|
||||||
|
return cfg
|
||||||
|
}, withEndpointForGRPC(u)))
|
||||||
|
}),
|
||||||
|
envconfig.WithURL("TRACES_ENDPOINT", func(u *url.URL) {
|
||||||
|
opts = append(opts, withEndpointScheme(u))
|
||||||
|
opts = append(opts, newSplitOption(func(cfg Config) Config {
|
||||||
|
cfg.Traces.Endpoint = u.Host
|
||||||
|
// For endpoint URLs for OTLP/HTTP per-signal variables, the
|
||||||
|
// URL MUST be used as-is without any modification. The only
|
||||||
|
// exception is that if an URL contains no path part, the root
|
||||||
|
// path / MUST be used.
|
||||||
|
path := u.Path
|
||||||
|
if path == "" {
|
||||||
|
path = "/"
|
||||||
|
}
|
||||||
|
cfg.Traces.URLPath = path
|
||||||
|
return cfg
|
||||||
|
}, withEndpointForGRPC(u)))
|
||||||
|
}),
|
||||||
|
envconfig.WithCertPool("CERTIFICATE", func(p *x509.CertPool) { tlsConf.RootCAs = p }),
|
||||||
|
envconfig.WithCertPool("TRACES_CERTIFICATE", func(p *x509.CertPool) { tlsConf.RootCAs = p }),
|
||||||
|
envconfig.WithClientCert("CLIENT_CERTIFICATE", "CLIENT_KEY", func(c tls.Certificate) { tlsConf.Certificates = []tls.Certificate{c} }),
|
||||||
|
envconfig.WithClientCert("TRACES_CLIENT_CERTIFICATE", "TRACES_CLIENT_KEY", func(c tls.Certificate) { tlsConf.Certificates = []tls.Certificate{c} }),
|
||||||
|
withTLSConfig(tlsConf, func(c *tls.Config) { opts = append(opts, WithTLSClientConfig(c)) }),
|
||||||
|
envconfig.WithBool("INSECURE", func(b bool) { opts = append(opts, withInsecure(b)) }),
|
||||||
|
envconfig.WithBool("TRACES_INSECURE", func(b bool) { opts = append(opts, withInsecure(b)) }),
|
||||||
|
envconfig.WithHeaders("HEADERS", func(h map[string]string) { opts = append(opts, WithHeaders(h)) }),
|
||||||
|
envconfig.WithHeaders("TRACES_HEADERS", func(h map[string]string) { opts = append(opts, WithHeaders(h)) }),
|
||||||
|
WithEnvCompression("COMPRESSION", func(c Compression) { opts = append(opts, WithCompression(c)) }),
|
||||||
|
WithEnvCompression("TRACES_COMPRESSION", func(c Compression) { opts = append(opts, WithCompression(c)) }),
|
||||||
|
envconfig.WithDuration("TIMEOUT", func(d time.Duration) { opts = append(opts, WithTimeout(d)) }),
|
||||||
|
envconfig.WithDuration("TRACES_TIMEOUT", func(d time.Duration) { opts = append(opts, WithTimeout(d)) }),
|
||||||
|
)
|
||||||
|
|
||||||
|
return opts
|
||||||
|
}
|
||||||
|
|
||||||
|
func withEndpointScheme(u *url.URL) GenericOption {
|
||||||
|
switch strings.ToLower(u.Scheme) {
|
||||||
|
case "http", "unix":
|
||||||
|
return WithInsecure()
|
||||||
|
default:
|
||||||
|
return WithSecure()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func withEndpointForGRPC(u *url.URL) func(cfg Config) Config {
|
||||||
|
return func(cfg Config) Config {
|
||||||
|
// For OTLP/gRPC endpoints, this is the target to which the
|
||||||
|
// exporter is going to send telemetry.
|
||||||
|
cfg.Traces.Endpoint = path.Join(u.Host, u.Path)
|
||||||
|
return cfg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithEnvCompression retrieves the specified config and passes it to ConfigFn as a Compression.
|
||||||
|
func WithEnvCompression(n string, fn func(Compression)) func(e *envconfig.EnvOptionsReader) {
|
||||||
|
return func(e *envconfig.EnvOptionsReader) {
|
||||||
|
if v, ok := e.GetEnvValue(n); ok {
|
||||||
|
cp := NoCompression
|
||||||
|
if v == "gzip" {
|
||||||
|
cp = GzipCompression
|
||||||
|
}
|
||||||
|
|
||||||
|
fn(cp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// revive:disable-next-line:flag-parameter
|
||||||
|
func withInsecure(b bool) GenericOption {
|
||||||
|
if b {
|
||||||
|
return WithInsecure()
|
||||||
|
}
|
||||||
|
return WithSecure()
|
||||||
|
}
|
||||||
|
|
||||||
|
func withTLSConfig(c *tls.Config, fn func(*tls.Config)) func(e *envconfig.EnvOptionsReader) {
|
||||||
|
return func(e *envconfig.EnvOptionsReader) {
|
||||||
|
if c.RootCAs != nil || len(c.Certificates) > 0 {
|
||||||
|
fn(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
328
internal/shared/otlp/otlptrace/otlpconfig/options.go.tmpl
Normal file
328
internal/shared/otlp/otlptrace/otlpconfig/options.go.tmpl
Normal file
@ -0,0 +1,328 @@
|
|||||||
|
// Code created by gotmpl. DO NOT MODIFY.
|
||||||
|
// source: internal/shared/otlp/otlptrace/otlpconfig/options.go.tmpl
|
||||||
|
|
||||||
|
// 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 otlpconfig
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"fmt"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/backoff"
|
||||||
|
"google.golang.org/grpc/credentials"
|
||||||
|
"google.golang.org/grpc/credentials/insecure"
|
||||||
|
"google.golang.org/grpc/encoding/gzip"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel/exporters/otlp/otlptrace"
|
||||||
|
"{{ .retryImportPath }}"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// DefaultTracesPath is a default URL path for endpoint that
|
||||||
|
// receives spans.
|
||||||
|
DefaultTracesPath string = "/v1/traces"
|
||||||
|
// DefaultTimeout is a default max waiting time for the backend to process
|
||||||
|
// each span batch.
|
||||||
|
DefaultTimeout time.Duration = 10 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
SignalConfig struct {
|
||||||
|
Endpoint string
|
||||||
|
Insecure bool
|
||||||
|
TLSCfg *tls.Config
|
||||||
|
Headers map[string]string
|
||||||
|
Compression Compression
|
||||||
|
Timeout time.Duration
|
||||||
|
URLPath string
|
||||||
|
|
||||||
|
// gRPC configurations
|
||||||
|
GRPCCredentials credentials.TransportCredentials
|
||||||
|
}
|
||||||
|
|
||||||
|
Config struct {
|
||||||
|
// Signal specific configurations
|
||||||
|
Traces SignalConfig
|
||||||
|
|
||||||
|
RetryConfig retry.Config
|
||||||
|
|
||||||
|
// gRPC configurations
|
||||||
|
ReconnectionPeriod time.Duration
|
||||||
|
ServiceConfig string
|
||||||
|
DialOptions []grpc.DialOption
|
||||||
|
GRPCConn *grpc.ClientConn
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewHTTPConfig returns a new Config with all settings applied from opts and
|
||||||
|
// any unset setting using the default HTTP config values.
|
||||||
|
func NewHTTPConfig(opts ...HTTPOption) Config {
|
||||||
|
cfg := Config{
|
||||||
|
Traces: SignalConfig{
|
||||||
|
Endpoint: fmt.Sprintf("%s:%d", DefaultCollectorHost, DefaultCollectorHTTPPort),
|
||||||
|
URLPath: DefaultTracesPath,
|
||||||
|
Compression: NoCompression,
|
||||||
|
Timeout: DefaultTimeout,
|
||||||
|
},
|
||||||
|
RetryConfig: retry.DefaultConfig,
|
||||||
|
}
|
||||||
|
cfg = ApplyHTTPEnvConfigs(cfg)
|
||||||
|
for _, opt := range opts {
|
||||||
|
cfg = opt.ApplyHTTPOption(cfg)
|
||||||
|
}
|
||||||
|
cfg.Traces.URLPath = cleanPath(cfg.Traces.URLPath, DefaultTracesPath)
|
||||||
|
return cfg
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanPath returns a path with all spaces trimmed and all redundancies
|
||||||
|
// removed. If urlPath is empty or cleaning it results in an empty string,
|
||||||
|
// defaultPath is returned instead.
|
||||||
|
func cleanPath(urlPath string, defaultPath string) string {
|
||||||
|
tmp := path.Clean(strings.TrimSpace(urlPath))
|
||||||
|
if tmp == "." {
|
||||||
|
return defaultPath
|
||||||
|
}
|
||||||
|
if !path.IsAbs(tmp) {
|
||||||
|
tmp = fmt.Sprintf("/%s", tmp)
|
||||||
|
}
|
||||||
|
return tmp
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewGRPCConfig returns a new Config with all settings applied from opts and
|
||||||
|
// any unset setting using the default gRPC config values.
|
||||||
|
func NewGRPCConfig(opts ...GRPCOption) Config {
|
||||||
|
userAgent := "OTel OTLP Exporter Go/" + otlptrace.Version()
|
||||||
|
cfg := Config{
|
||||||
|
Traces: SignalConfig{
|
||||||
|
Endpoint: fmt.Sprintf("%s:%d", DefaultCollectorHost, DefaultCollectorGRPCPort),
|
||||||
|
URLPath: DefaultTracesPath,
|
||||||
|
Compression: NoCompression,
|
||||||
|
Timeout: DefaultTimeout,
|
||||||
|
},
|
||||||
|
RetryConfig: retry.DefaultConfig,
|
||||||
|
DialOptions: []grpc.DialOption{grpc.WithUserAgent(userAgent)},
|
||||||
|
}
|
||||||
|
cfg = ApplyGRPCEnvConfigs(cfg)
|
||||||
|
for _, opt := range opts {
|
||||||
|
cfg = opt.ApplyGRPCOption(cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.ServiceConfig != "" {
|
||||||
|
cfg.DialOptions = append(cfg.DialOptions, grpc.WithDefaultServiceConfig(cfg.ServiceConfig))
|
||||||
|
}
|
||||||
|
// Priroritize GRPCCredentials over Insecure (passing both is an error).
|
||||||
|
if cfg.Traces.GRPCCredentials != nil {
|
||||||
|
cfg.DialOptions = append(cfg.DialOptions, grpc.WithTransportCredentials(cfg.Traces.GRPCCredentials))
|
||||||
|
} else if cfg.Traces.Insecure {
|
||||||
|
cfg.DialOptions = append(cfg.DialOptions, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||||
|
} else {
|
||||||
|
// Default to using the host's root CA.
|
||||||
|
creds := credentials.NewTLS(nil)
|
||||||
|
cfg.Traces.GRPCCredentials = creds
|
||||||
|
cfg.DialOptions = append(cfg.DialOptions, grpc.WithTransportCredentials(creds))
|
||||||
|
}
|
||||||
|
if cfg.Traces.Compression == GzipCompression {
|
||||||
|
cfg.DialOptions = append(cfg.DialOptions, grpc.WithDefaultCallOptions(grpc.UseCompressor(gzip.Name)))
|
||||||
|
}
|
||||||
|
if len(cfg.DialOptions) != 0 {
|
||||||
|
cfg.DialOptions = append(cfg.DialOptions, cfg.DialOptions...)
|
||||||
|
}
|
||||||
|
if cfg.ReconnectionPeriod != 0 {
|
||||||
|
p := grpc.ConnectParams{
|
||||||
|
Backoff: backoff.DefaultConfig,
|
||||||
|
MinConnectTimeout: cfg.ReconnectionPeriod,
|
||||||
|
}
|
||||||
|
cfg.DialOptions = append(cfg.DialOptions, grpc.WithConnectParams(p))
|
||||||
|
}
|
||||||
|
|
||||||
|
return cfg
|
||||||
|
}
|
||||||
|
|
||||||
|
type (
|
||||||
|
// GenericOption applies an option to the HTTP or gRPC driver.
|
||||||
|
GenericOption interface {
|
||||||
|
ApplyHTTPOption(Config) Config
|
||||||
|
ApplyGRPCOption(Config) Config
|
||||||
|
|
||||||
|
// A private method to prevent users implementing the
|
||||||
|
// interface and so future additions to it will not
|
||||||
|
// violate compatibility.
|
||||||
|
private()
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTPOption applies an option to the HTTP driver.
|
||||||
|
HTTPOption interface {
|
||||||
|
ApplyHTTPOption(Config) Config
|
||||||
|
|
||||||
|
// A private method to prevent users implementing the
|
||||||
|
// interface and so future additions to it will not
|
||||||
|
// violate compatibility.
|
||||||
|
private()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GRPCOption applies an option to the gRPC driver.
|
||||||
|
GRPCOption interface {
|
||||||
|
ApplyGRPCOption(Config) Config
|
||||||
|
|
||||||
|
// A private method to prevent users implementing the
|
||||||
|
// interface and so future additions to it will not
|
||||||
|
// violate compatibility.
|
||||||
|
private()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// genericOption is an option that applies the same logic
|
||||||
|
// for both gRPC and HTTP.
|
||||||
|
type genericOption struct {
|
||||||
|
fn func(Config) Config
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *genericOption) ApplyGRPCOption(cfg Config) Config {
|
||||||
|
return g.fn(cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *genericOption) ApplyHTTPOption(cfg Config) Config {
|
||||||
|
return g.fn(cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (genericOption) private() {}
|
||||||
|
|
||||||
|
func newGenericOption(fn func(cfg Config) Config) GenericOption {
|
||||||
|
return &genericOption{fn: fn}
|
||||||
|
}
|
||||||
|
|
||||||
|
// splitOption is an option that applies different logics
|
||||||
|
// for gRPC and HTTP.
|
||||||
|
type splitOption struct {
|
||||||
|
httpFn func(Config) Config
|
||||||
|
grpcFn func(Config) Config
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *splitOption) ApplyGRPCOption(cfg Config) Config {
|
||||||
|
return g.grpcFn(cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *splitOption) ApplyHTTPOption(cfg Config) Config {
|
||||||
|
return g.httpFn(cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (splitOption) private() {}
|
||||||
|
|
||||||
|
func newSplitOption(httpFn func(cfg Config) Config, grpcFn func(cfg Config) Config) GenericOption {
|
||||||
|
return &splitOption{httpFn: httpFn, grpcFn: grpcFn}
|
||||||
|
}
|
||||||
|
|
||||||
|
// httpOption is an option that is only applied to the HTTP driver.
|
||||||
|
type httpOption struct {
|
||||||
|
fn func(Config) Config
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *httpOption) ApplyHTTPOption(cfg Config) Config {
|
||||||
|
return h.fn(cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (httpOption) private() {}
|
||||||
|
|
||||||
|
func NewHTTPOption(fn func(cfg Config) Config) HTTPOption {
|
||||||
|
return &httpOption{fn: fn}
|
||||||
|
}
|
||||||
|
|
||||||
|
// grpcOption is an option that is only applied to the gRPC driver.
|
||||||
|
type grpcOption struct {
|
||||||
|
fn func(Config) Config
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *grpcOption) ApplyGRPCOption(cfg Config) Config {
|
||||||
|
return h.fn(cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (grpcOption) private() {}
|
||||||
|
|
||||||
|
func NewGRPCOption(fn func(cfg Config) Config) GRPCOption {
|
||||||
|
return &grpcOption{fn: fn}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generic Options
|
||||||
|
|
||||||
|
func WithEndpoint(endpoint string) GenericOption {
|
||||||
|
return newGenericOption(func(cfg Config) Config {
|
||||||
|
cfg.Traces.Endpoint = endpoint
|
||||||
|
return cfg
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithCompression(compression Compression) GenericOption {
|
||||||
|
return newGenericOption(func(cfg Config) Config {
|
||||||
|
cfg.Traces.Compression = compression
|
||||||
|
return cfg
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithURLPath(urlPath string) GenericOption {
|
||||||
|
return newGenericOption(func(cfg Config) Config {
|
||||||
|
cfg.Traces.URLPath = urlPath
|
||||||
|
return cfg
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithRetry(rc retry.Config) GenericOption {
|
||||||
|
return newGenericOption(func(cfg Config) Config {
|
||||||
|
cfg.RetryConfig = rc
|
||||||
|
return cfg
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithTLSClientConfig(tlsCfg *tls.Config) GenericOption {
|
||||||
|
return newSplitOption(func(cfg Config) Config {
|
||||||
|
cfg.Traces.TLSCfg = tlsCfg.Clone()
|
||||||
|
return cfg
|
||||||
|
}, func(cfg Config) Config {
|
||||||
|
cfg.Traces.GRPCCredentials = credentials.NewTLS(tlsCfg)
|
||||||
|
return cfg
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithInsecure() GenericOption {
|
||||||
|
return newGenericOption(func(cfg Config) Config {
|
||||||
|
cfg.Traces.Insecure = true
|
||||||
|
return cfg
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithSecure() GenericOption {
|
||||||
|
return newGenericOption(func(cfg Config) Config {
|
||||||
|
cfg.Traces.Insecure = false
|
||||||
|
return cfg
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithHeaders(headers map[string]string) GenericOption {
|
||||||
|
return newGenericOption(func(cfg Config) Config {
|
||||||
|
cfg.Traces.Headers = headers
|
||||||
|
return cfg
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithTimeout(duration time.Duration) GenericOption {
|
||||||
|
return newGenericOption(func(cfg Config) Config {
|
||||||
|
cfg.Traces.Timeout = duration
|
||||||
|
return cfg
|
||||||
|
})
|
||||||
|
}
|
489
internal/shared/otlp/otlptrace/otlpconfig/options_test.go.tmpl
Normal file
489
internal/shared/otlp/otlptrace/otlpconfig/options_test.go.tmpl
Normal file
@ -0,0 +1,489 @@
|
|||||||
|
// Code created by gotmpl. DO NOT MODIFY.
|
||||||
|
// source: internal/shared/otlp/otlptrace/otlpconfig/options_test.go.tmpl
|
||||||
|
|
||||||
|
// 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 otlpconfig
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"{{ .envconfigImportPath }}"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
WeakCertificate = `
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIBhzCCASygAwIBAgIRANHpHgAWeTnLZpTSxCKs0ggwCgYIKoZIzj0EAwIwEjEQ
|
||||||
|
MA4GA1UEChMHb3RlbC1nbzAeFw0yMTA0MDExMzU5MDNaFw0yMTA0MDExNDU5MDNa
|
||||||
|
MBIxEDAOBgNVBAoTB290ZWwtZ28wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAS9
|
||||||
|
nWSkmPCxShxnp43F+PrOtbGV7sNfkbQ/kxzi9Ego0ZJdiXxkmv/C05QFddCW7Y0Z
|
||||||
|
sJCLHGogQsYnWJBXUZOVo2MwYTAOBgNVHQ8BAf8EBAMCB4AwEwYDVR0lBAwwCgYI
|
||||||
|
KwYBBQUHAwEwDAYDVR0TAQH/BAIwADAsBgNVHREEJTAjgglsb2NhbGhvc3SHEAAA
|
||||||
|
AAAAAAAAAAAAAAAAAAGHBH8AAAEwCgYIKoZIzj0EAwIDSQAwRgIhANwZVVKvfvQ/
|
||||||
|
1HXsTvgH+xTQswOwSSKYJ1cVHQhqK7ZbAiEAus8NxpTRnp5DiTMuyVmhVNPB+bVH
|
||||||
|
Lhnm4N/QDk5rek0=
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
`
|
||||||
|
WeakPrivateKey = `
|
||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgN8HEXiXhvByrJ1zK
|
||||||
|
SFT6Y2l2KqDWwWzKf+t4CyWrNKehRANCAAS9nWSkmPCxShxnp43F+PrOtbGV7sNf
|
||||||
|
kbQ/kxzi9Ego0ZJdiXxkmv/C05QFddCW7Y0ZsJCLHGogQsYnWJBXUZOV
|
||||||
|
-----END PRIVATE KEY-----
|
||||||
|
`
|
||||||
|
)
|
||||||
|
|
||||||
|
type env map[string]string
|
||||||
|
|
||||||
|
func (e *env) getEnv(env string) string {
|
||||||
|
return (*e)[env]
|
||||||
|
}
|
||||||
|
|
||||||
|
type fileReader map[string][]byte
|
||||||
|
|
||||||
|
func (f *fileReader) readFile(filename string) ([]byte, error) {
|
||||||
|
if b, ok := (*f)[filename]; ok {
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
return nil, errors.New("file not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigs(t *testing.T) {
|
||||||
|
tlsCert, err := CreateTLSConfig([]byte(WeakCertificate))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
opts []GenericOption
|
||||||
|
env env
|
||||||
|
fileReader fileReader
|
||||||
|
asserts func(t *testing.T, c *Config, grpcOption bool)
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Test default configs",
|
||||||
|
asserts: func(t *testing.T, c *Config, grpcOption bool) {
|
||||||
|
if grpcOption {
|
||||||
|
assert.Equal(t, "localhost:4317", c.Traces.Endpoint)
|
||||||
|
} else {
|
||||||
|
assert.Equal(t, "localhost:4318", c.Traces.Endpoint)
|
||||||
|
}
|
||||||
|
assert.Equal(t, NoCompression, c.Traces.Compression)
|
||||||
|
assert.Equal(t, map[string]string(nil), c.Traces.Headers)
|
||||||
|
assert.Equal(t, 10*time.Second, c.Traces.Timeout)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Endpoint Tests
|
||||||
|
{
|
||||||
|
name: "Test With Endpoint",
|
||||||
|
opts: []GenericOption{
|
||||||
|
WithEndpoint("someendpoint"),
|
||||||
|
},
|
||||||
|
asserts: func(t *testing.T, c *Config, grpcOption bool) {
|
||||||
|
assert.Equal(t, "someendpoint", c.Traces.Endpoint)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Test Environment Endpoint",
|
||||||
|
env: map[string]string{
|
||||||
|
"OTEL_EXPORTER_OTLP_ENDPOINT": "https://env.endpoint/prefix",
|
||||||
|
},
|
||||||
|
asserts: func(t *testing.T, c *Config, grpcOption bool) {
|
||||||
|
assert.False(t, c.Traces.Insecure)
|
||||||
|
if grpcOption {
|
||||||
|
assert.Equal(t, "env.endpoint/prefix", c.Traces.Endpoint)
|
||||||
|
} else {
|
||||||
|
assert.Equal(t, "env.endpoint", c.Traces.Endpoint)
|
||||||
|
assert.Equal(t, "/prefix/v1/traces", c.Traces.URLPath)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Test Environment Signal Specific Endpoint",
|
||||||
|
env: map[string]string{
|
||||||
|
"OTEL_EXPORTER_OTLP_ENDPOINT": "https://overrode.by.signal.specific/env/var",
|
||||||
|
"OTEL_EXPORTER_OTLP_TRACES_ENDPOINT": "http://env.traces.endpoint",
|
||||||
|
},
|
||||||
|
asserts: func(t *testing.T, c *Config, grpcOption bool) {
|
||||||
|
assert.True(t, c.Traces.Insecure)
|
||||||
|
assert.Equal(t, "env.traces.endpoint", c.Traces.Endpoint)
|
||||||
|
if !grpcOption {
|
||||||
|
assert.Equal(t, "/", c.Traces.URLPath)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Test Mixed Environment and With Endpoint",
|
||||||
|
opts: []GenericOption{
|
||||||
|
WithEndpoint("traces_endpoint"),
|
||||||
|
},
|
||||||
|
env: map[string]string{
|
||||||
|
"OTEL_EXPORTER_OTLP_ENDPOINT": "env_endpoint",
|
||||||
|
},
|
||||||
|
asserts: func(t *testing.T, c *Config, grpcOption bool) {
|
||||||
|
assert.Equal(t, "traces_endpoint", c.Traces.Endpoint)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Test Environment Endpoint with HTTP scheme",
|
||||||
|
env: map[string]string{
|
||||||
|
"OTEL_EXPORTER_OTLP_ENDPOINT": "http://env_endpoint",
|
||||||
|
},
|
||||||
|
asserts: func(t *testing.T, c *Config, grpcOption bool) {
|
||||||
|
assert.Equal(t, "env_endpoint", c.Traces.Endpoint)
|
||||||
|
assert.Equal(t, true, c.Traces.Insecure)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Test Environment Endpoint with HTTP scheme and leading & trailingspaces",
|
||||||
|
env: map[string]string{
|
||||||
|
"OTEL_EXPORTER_OTLP_ENDPOINT": " http://env_endpoint ",
|
||||||
|
},
|
||||||
|
asserts: func(t *testing.T, c *Config, grpcOption bool) {
|
||||||
|
assert.Equal(t, "env_endpoint", c.Traces.Endpoint)
|
||||||
|
assert.Equal(t, true, c.Traces.Insecure)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Test Environment Endpoint with HTTPS scheme",
|
||||||
|
env: map[string]string{
|
||||||
|
"OTEL_EXPORTER_OTLP_ENDPOINT": "https://env_endpoint",
|
||||||
|
},
|
||||||
|
asserts: func(t *testing.T, c *Config, grpcOption bool) {
|
||||||
|
assert.Equal(t, "env_endpoint", c.Traces.Endpoint)
|
||||||
|
assert.Equal(t, false, c.Traces.Insecure)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Test Environment Signal Specific Endpoint with uppercase scheme",
|
||||||
|
env: map[string]string{
|
||||||
|
"OTEL_EXPORTER_OTLP_ENDPOINT": "HTTPS://overrode_by_signal_specific",
|
||||||
|
"OTEL_EXPORTER_OTLP_TRACES_ENDPOINT": "HtTp://env_traces_endpoint",
|
||||||
|
},
|
||||||
|
asserts: func(t *testing.T, c *Config, grpcOption bool) {
|
||||||
|
assert.Equal(t, "env_traces_endpoint", c.Traces.Endpoint)
|
||||||
|
assert.Equal(t, true, c.Traces.Insecure)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Certificate tests
|
||||||
|
{
|
||||||
|
name: "Test Default Certificate",
|
||||||
|
asserts: func(t *testing.T, c *Config, grpcOption bool) {
|
||||||
|
if grpcOption {
|
||||||
|
assert.NotNil(t, c.Traces.GRPCCredentials)
|
||||||
|
} else {
|
||||||
|
assert.Nil(t, c.Traces.TLSCfg)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Test With Certificate",
|
||||||
|
opts: []GenericOption{
|
||||||
|
WithTLSClientConfig(tlsCert),
|
||||||
|
},
|
||||||
|
asserts: func(t *testing.T, c *Config, grpcOption bool) {
|
||||||
|
if grpcOption {
|
||||||
|
//TODO: make sure gRPC's credentials actually works
|
||||||
|
assert.NotNil(t, c.Traces.GRPCCredentials)
|
||||||
|
} else {
|
||||||
|
// nolint:staticcheck // ignoring tlsCert.RootCAs.Subjects is deprecated ERR because cert does not come from SystemCertPool.
|
||||||
|
assert.Equal(t, tlsCert.RootCAs.Subjects(), c.Traces.TLSCfg.RootCAs.Subjects())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Test Environment Certificate",
|
||||||
|
env: map[string]string{
|
||||||
|
"OTEL_EXPORTER_OTLP_CERTIFICATE": "cert_path",
|
||||||
|
},
|
||||||
|
fileReader: fileReader{
|
||||||
|
"cert_path": []byte(WeakCertificate),
|
||||||
|
},
|
||||||
|
asserts: func(t *testing.T, c *Config, grpcOption bool) {
|
||||||
|
if grpcOption {
|
||||||
|
assert.NotNil(t, c.Traces.GRPCCredentials)
|
||||||
|
} else {
|
||||||
|
// nolint:staticcheck // ignoring tlsCert.RootCAs.Subjects is deprecated ERR because cert does not come from SystemCertPool.
|
||||||
|
assert.Equal(t, tlsCert.RootCAs.Subjects(), c.Traces.TLSCfg.RootCAs.Subjects())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Test Environment Signal Specific Certificate",
|
||||||
|
env: map[string]string{
|
||||||
|
"OTEL_EXPORTER_OTLP_CERTIFICATE": "overrode_by_signal_specific",
|
||||||
|
"OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE": "cert_path",
|
||||||
|
},
|
||||||
|
fileReader: fileReader{
|
||||||
|
"cert_path": []byte(WeakCertificate),
|
||||||
|
"invalid_cert": []byte("invalid certificate file."),
|
||||||
|
},
|
||||||
|
asserts: func(t *testing.T, c *Config, grpcOption bool) {
|
||||||
|
if grpcOption {
|
||||||
|
assert.NotNil(t, c.Traces.GRPCCredentials)
|
||||||
|
} else {
|
||||||
|
// nolint:staticcheck // ignoring tlsCert.RootCAs.Subjects is deprecated ERR because cert does not come from SystemCertPool.
|
||||||
|
assert.Equal(t, tlsCert.RootCAs.Subjects(), c.Traces.TLSCfg.RootCAs.Subjects())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Test Mixed Environment and With Certificate",
|
||||||
|
opts: []GenericOption{},
|
||||||
|
env: map[string]string{
|
||||||
|
"OTEL_EXPORTER_OTLP_CERTIFICATE": "cert_path",
|
||||||
|
},
|
||||||
|
fileReader: fileReader{
|
||||||
|
"cert_path": []byte(WeakCertificate),
|
||||||
|
},
|
||||||
|
asserts: func(t *testing.T, c *Config, grpcOption bool) {
|
||||||
|
if grpcOption {
|
||||||
|
assert.NotNil(t, c.Traces.GRPCCredentials)
|
||||||
|
} else {
|
||||||
|
// nolint:staticcheck // ignoring tlsCert.RootCAs.Subjects is deprecated ERR because cert does not come from SystemCertPool.
|
||||||
|
assert.Equal(t, tlsCert.RootCAs.Subjects(), c.Traces.TLSCfg.RootCAs.Subjects())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Headers tests
|
||||||
|
{
|
||||||
|
name: "Test With Headers",
|
||||||
|
opts: []GenericOption{
|
||||||
|
WithHeaders(map[string]string{"h1": "v1"}),
|
||||||
|
},
|
||||||
|
asserts: func(t *testing.T, c *Config, grpcOption bool) {
|
||||||
|
assert.Equal(t, map[string]string{"h1": "v1"}, c.Traces.Headers)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Test Environment Headers",
|
||||||
|
env: map[string]string{"OTEL_EXPORTER_OTLP_HEADERS": "h1=v1,h2=v2"},
|
||||||
|
asserts: func(t *testing.T, c *Config, grpcOption bool) {
|
||||||
|
assert.Equal(t, map[string]string{"h1": "v1", "h2": "v2"}, c.Traces.Headers)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Test Environment Signal Specific Headers",
|
||||||
|
env: map[string]string{
|
||||||
|
"OTEL_EXPORTER_OTLP_HEADERS": "overrode_by_signal_specific",
|
||||||
|
"OTEL_EXPORTER_OTLP_TRACES_HEADERS": "h1=v1,h2=v2",
|
||||||
|
},
|
||||||
|
asserts: func(t *testing.T, c *Config, grpcOption bool) {
|
||||||
|
assert.Equal(t, map[string]string{"h1": "v1", "h2": "v2"}, c.Traces.Headers)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Test Mixed Environment and With Headers",
|
||||||
|
env: map[string]string{"OTEL_EXPORTER_OTLP_HEADERS": "h1=v1,h2=v2"},
|
||||||
|
opts: []GenericOption{},
|
||||||
|
asserts: func(t *testing.T, c *Config, grpcOption bool) {
|
||||||
|
assert.Equal(t, map[string]string{"h1": "v1", "h2": "v2"}, c.Traces.Headers)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Compression Tests
|
||||||
|
{
|
||||||
|
name: "Test With Compression",
|
||||||
|
opts: []GenericOption{
|
||||||
|
WithCompression(GzipCompression),
|
||||||
|
},
|
||||||
|
asserts: func(t *testing.T, c *Config, grpcOption bool) {
|
||||||
|
assert.Equal(t, GzipCompression, c.Traces.Compression)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Test Environment Compression",
|
||||||
|
env: map[string]string{
|
||||||
|
"OTEL_EXPORTER_OTLP_COMPRESSION": "gzip",
|
||||||
|
},
|
||||||
|
asserts: func(t *testing.T, c *Config, grpcOption bool) {
|
||||||
|
assert.Equal(t, GzipCompression, c.Traces.Compression)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Test Environment Signal Specific Compression",
|
||||||
|
env: map[string]string{
|
||||||
|
"OTEL_EXPORTER_OTLP_TRACES_COMPRESSION": "gzip",
|
||||||
|
},
|
||||||
|
asserts: func(t *testing.T, c *Config, grpcOption bool) {
|
||||||
|
assert.Equal(t, GzipCompression, c.Traces.Compression)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Test Mixed Environment and With Compression",
|
||||||
|
opts: []GenericOption{
|
||||||
|
WithCompression(NoCompression),
|
||||||
|
},
|
||||||
|
env: map[string]string{
|
||||||
|
"OTEL_EXPORTER_OTLP_TRACES_COMPRESSION": "gzip",
|
||||||
|
},
|
||||||
|
asserts: func(t *testing.T, c *Config, grpcOption bool) {
|
||||||
|
assert.Equal(t, NoCompression, c.Traces.Compression)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Timeout Tests
|
||||||
|
{
|
||||||
|
name: "Test With Timeout",
|
||||||
|
opts: []GenericOption{
|
||||||
|
WithTimeout(time.Duration(5 * time.Second)),
|
||||||
|
},
|
||||||
|
asserts: func(t *testing.T, c *Config, grpcOption bool) {
|
||||||
|
assert.Equal(t, 5*time.Second, c.Traces.Timeout)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Test Environment Timeout",
|
||||||
|
env: map[string]string{
|
||||||
|
"OTEL_EXPORTER_OTLP_TIMEOUT": "15000",
|
||||||
|
},
|
||||||
|
asserts: func(t *testing.T, c *Config, grpcOption bool) {
|
||||||
|
assert.Equal(t, c.Traces.Timeout, 15*time.Second)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Test Environment Signal Specific Timeout",
|
||||||
|
env: map[string]string{
|
||||||
|
"OTEL_EXPORTER_OTLP_TIMEOUT": "15000",
|
||||||
|
"OTEL_EXPORTER_OTLP_TRACES_TIMEOUT": "27000",
|
||||||
|
},
|
||||||
|
asserts: func(t *testing.T, c *Config, grpcOption bool) {
|
||||||
|
assert.Equal(t, c.Traces.Timeout, 27*time.Second)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Test Mixed Environment and With Timeout",
|
||||||
|
env: map[string]string{
|
||||||
|
"OTEL_EXPORTER_OTLP_TIMEOUT": "15000",
|
||||||
|
"OTEL_EXPORTER_OTLP_TRACES_TIMEOUT": "27000",
|
||||||
|
},
|
||||||
|
opts: []GenericOption{
|
||||||
|
WithTimeout(5 * time.Second),
|
||||||
|
},
|
||||||
|
asserts: func(t *testing.T, c *Config, grpcOption bool) {
|
||||||
|
assert.Equal(t, c.Traces.Timeout, 5*time.Second)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
origEOR := DefaultEnvOptionsReader
|
||||||
|
DefaultEnvOptionsReader = envconfig.EnvOptionsReader{
|
||||||
|
GetEnv: tt.env.getEnv,
|
||||||
|
ReadFile: tt.fileReader.readFile,
|
||||||
|
Namespace: "OTEL_EXPORTER_OTLP",
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { DefaultEnvOptionsReader = origEOR })
|
||||||
|
|
||||||
|
// Tests Generic options as HTTP Options
|
||||||
|
cfg := NewHTTPConfig(asHTTPOptions(tt.opts)...)
|
||||||
|
tt.asserts(t, &cfg, false)
|
||||||
|
|
||||||
|
// Tests Generic options as gRPC Options
|
||||||
|
cfg = NewGRPCConfig(asGRPCOptions(tt.opts)...)
|
||||||
|
tt.asserts(t, &cfg, true)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func asHTTPOptions(opts []GenericOption) []HTTPOption {
|
||||||
|
converted := make([]HTTPOption, len(opts))
|
||||||
|
for i, o := range opts {
|
||||||
|
converted[i] = NewHTTPOption(o.ApplyHTTPOption)
|
||||||
|
}
|
||||||
|
return converted
|
||||||
|
}
|
||||||
|
|
||||||
|
func asGRPCOptions(opts []GenericOption) []GRPCOption {
|
||||||
|
converted := make([]GRPCOption, len(opts))
|
||||||
|
for i, o := range opts {
|
||||||
|
converted[i] = NewGRPCOption(o.ApplyGRPCOption)
|
||||||
|
}
|
||||||
|
return converted
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCleanPath(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
urlPath string
|
||||||
|
defaultPath string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "clean empty path",
|
||||||
|
args: args{
|
||||||
|
urlPath: "",
|
||||||
|
defaultPath: "DefaultPath",
|
||||||
|
},
|
||||||
|
want: "DefaultPath",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "clean metrics path",
|
||||||
|
args: args{
|
||||||
|
urlPath: "/prefix/v1/metrics",
|
||||||
|
defaultPath: "DefaultMetricsPath",
|
||||||
|
},
|
||||||
|
want: "/prefix/v1/metrics",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "clean traces path",
|
||||||
|
args: args{
|
||||||
|
urlPath: "https://env_endpoint",
|
||||||
|
defaultPath: "DefaultTracesPath",
|
||||||
|
},
|
||||||
|
want: "/https:/env_endpoint",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "spaces trimmed",
|
||||||
|
args: args{
|
||||||
|
urlPath: " /dir",
|
||||||
|
},
|
||||||
|
want: "/dir",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "clean path empty",
|
||||||
|
args: args{
|
||||||
|
urlPath: "dir/..",
|
||||||
|
defaultPath: "DefaultTracesPath",
|
||||||
|
},
|
||||||
|
want: "DefaultTracesPath",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "make absolute",
|
||||||
|
args: args{
|
||||||
|
urlPath: "dir/a",
|
||||||
|
},
|
||||||
|
want: "/dir/a",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if got := cleanPath(tt.args.urlPath, tt.args.defaultPath); got != tt.want {
|
||||||
|
t.Errorf("CleanPath() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,51 @@
|
|||||||
|
// Code created by gotmpl. DO NOT MODIFY.
|
||||||
|
// source: internal/shared/otlp/otlptrace/otlpconfig/optiontypes.go.tmpl
|
||||||
|
|
||||||
|
// 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 otlpconfig
|
||||||
|
|
||||||
|
const (
|
||||||
|
// DefaultCollectorGRPCPort is the default gRPC port of the collector.
|
||||||
|
DefaultCollectorGRPCPort uint16 = 4317
|
||||||
|
// DefaultCollectorHTTPPort is the default HTTP port of the collector.
|
||||||
|
DefaultCollectorHTTPPort uint16 = 4318
|
||||||
|
// DefaultCollectorHost is the host address the Exporter will attempt
|
||||||
|
// connect to if no collector address is provided.
|
||||||
|
DefaultCollectorHost string = "localhost"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Compression describes the compression used for payloads sent to the
|
||||||
|
// collector.
|
||||||
|
type Compression int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// NoCompression tells the driver to send payloads without
|
||||||
|
// compression.
|
||||||
|
NoCompression Compression = iota
|
||||||
|
// GzipCompression tells the driver to send payloads after
|
||||||
|
// compressing them with gzip.
|
||||||
|
GzipCompression
|
||||||
|
)
|
||||||
|
|
||||||
|
// Marshaler describes the kind of message format sent to the collector.
|
||||||
|
type Marshaler int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// MarshalProto tells the driver to send using the protobuf binary format.
|
||||||
|
MarshalProto Marshaler = iota
|
||||||
|
// MarshalJSON tells the driver to send using json format.
|
||||||
|
MarshalJSON
|
||||||
|
)
|
37
internal/shared/otlp/otlptrace/otlpconfig/tls.go.tmpl
Normal file
37
internal/shared/otlp/otlptrace/otlpconfig/tls.go.tmpl
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
// Code created by gotmpl. DO NOT MODIFY.
|
||||||
|
// source: internal/shared/otlp/otlptrace/otlpconfig/tls.go.tmpl
|
||||||
|
|
||||||
|
// 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 otlpconfig
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CreateTLSConfig creates a tls.Config from a raw certificate bytes
|
||||||
|
// to verify a server certificate.
|
||||||
|
func CreateTLSConfig(certBytes []byte) (*tls.Config, error) {
|
||||||
|
cp := x509.NewCertPool()
|
||||||
|
if ok := cp.AppendCertsFromPEM(certBytes); !ok {
|
||||||
|
return nil, errors.New("failed to append certificate to the cert pool")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &tls.Config{
|
||||||
|
RootCAs: cp,
|
||||||
|
}, nil
|
||||||
|
}
|
136
internal/shared/otlp/otlptrace/otlptracetest/client.go.tmpl
Normal file
136
internal/shared/otlp/otlptrace/otlptracetest/client.go.tmpl
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
// Code created by gotmpl. DO NOT MODIFY.
|
||||||
|
// source: internal/shared/otlp/otlptrace/otlptracetest/client.go.tmpl
|
||||||
|
|
||||||
|
// 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 otlptracetest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel/exporters/otlp/otlptrace"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RunExporterShutdownTest(t *testing.T, factory func() otlptrace.Client) {
|
||||||
|
t.Run("testClientStopHonorsTimeout", func(t *testing.T) {
|
||||||
|
testClientStopHonorsTimeout(t, factory())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("testClientStopHonorsCancel", func(t *testing.T) {
|
||||||
|
testClientStopHonorsCancel(t, factory())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("testClientStopNoError", func(t *testing.T) {
|
||||||
|
testClientStopNoError(t, factory())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("testClientStopManyTimes", func(t *testing.T) {
|
||||||
|
testClientStopManyTimes(t, factory())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func initializeExporter(t *testing.T, client otlptrace.Client) *otlptrace.Exporter {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
e, err := otlptrace.New(ctx, client)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create exporter")
|
||||||
|
}
|
||||||
|
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
func testClientStopHonorsTimeout(t *testing.T, client otlptrace.Client) {
|
||||||
|
t.Cleanup(func() {
|
||||||
|
// The test is looking for a failed shut down. Call Stop a second time
|
||||||
|
// with an un-expired context to give the client a second chance at
|
||||||
|
// cleaning up. There is not guarantee from the Client interface this
|
||||||
|
// will succeed, therefore, no need to check the error (just give it a
|
||||||
|
// best try).
|
||||||
|
_ = client.Stop(context.Background())
|
||||||
|
})
|
||||||
|
e := initializeExporter(t, client)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Nanosecond)
|
||||||
|
defer cancel()
|
||||||
|
<-ctx.Done()
|
||||||
|
|
||||||
|
if err := e.Shutdown(ctx); !errors.Is(err, context.DeadlineExceeded) {
|
||||||
|
t.Errorf("expected context DeadlineExceeded error, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testClientStopHonorsCancel(t *testing.T, client otlptrace.Client) {
|
||||||
|
t.Cleanup(func() {
|
||||||
|
// The test is looking for a failed shut down. Call Stop a second time
|
||||||
|
// with an un-expired context to give the client a second chance at
|
||||||
|
// cleaning up. There is not guarantee from the Client interface this
|
||||||
|
// will succeed, therefore, no need to check the error (just give it a
|
||||||
|
// best try).
|
||||||
|
_ = client.Stop(context.Background())
|
||||||
|
})
|
||||||
|
e := initializeExporter(t, client)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
if err := e.Shutdown(ctx); !errors.Is(err, context.Canceled) {
|
||||||
|
t.Errorf("expected context canceled error, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testClientStopNoError(t *testing.T, client otlptrace.Client) {
|
||||||
|
e := initializeExporter(t, client)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if err := e.Shutdown(ctx); err != nil {
|
||||||
|
t.Errorf("shutdown errored: expected nil, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testClientStopManyTimes(t *testing.T, client otlptrace.Client) {
|
||||||
|
e := initializeExporter(t, client)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
ch := make(chan struct{})
|
||||||
|
wg := sync.WaitGroup{}
|
||||||
|
const num int = 20
|
||||||
|
wg.Add(num)
|
||||||
|
errs := make([]error, num)
|
||||||
|
for i := 0; i < num; i++ {
|
||||||
|
go func(idx int) {
|
||||||
|
defer wg.Done()
|
||||||
|
<-ch
|
||||||
|
errs[idx] = e.Shutdown(ctx)
|
||||||
|
}(i)
|
||||||
|
}
|
||||||
|
close(ch)
|
||||||
|
wg.Wait()
|
||||||
|
for _, err := range errs {
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("failed to shutdown exporter: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
106
internal/shared/otlp/otlptrace/otlptracetest/collector.go.tmpl
Normal file
106
internal/shared/otlp/otlptrace/otlptracetest/collector.go.tmpl
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
// Code created by gotmpl. DO NOT MODIFY.
|
||||||
|
// source: internal/shared/otlp/otlptrace/otlptracetest/collector.go.tmpl
|
||||||
|
|
||||||
|
// 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 otlptracetest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
collectortracepb "go.opentelemetry.io/proto/otlp/collector/trace/v1"
|
||||||
|
commonpb "go.opentelemetry.io/proto/otlp/common/v1"
|
||||||
|
resourcepb "go.opentelemetry.io/proto/otlp/resource/v1"
|
||||||
|
tracepb "go.opentelemetry.io/proto/otlp/trace/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TracesCollector mocks a collector for the end-to-end testing.
|
||||||
|
type TracesCollector interface {
|
||||||
|
Stop() error
|
||||||
|
GetResourceSpans() []*tracepb.ResourceSpans
|
||||||
|
}
|
||||||
|
|
||||||
|
// SpansStorage stores the spans. Mock collectors can use it to
|
||||||
|
// store spans they have received.
|
||||||
|
type SpansStorage struct {
|
||||||
|
rsm map[string]*tracepb.ResourceSpans
|
||||||
|
spanCount int
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSpansStorage creates a new spans storage.
|
||||||
|
func NewSpansStorage() SpansStorage {
|
||||||
|
return SpansStorage{
|
||||||
|
rsm: make(map[string]*tracepb.ResourceSpans),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddSpans adds spans to the spans storage.
|
||||||
|
func (s *SpansStorage) AddSpans(request *collectortracepb.ExportTraceServiceRequest) {
|
||||||
|
for _, rs := range request.GetResourceSpans() {
|
||||||
|
rstr := resourceString(rs.Resource)
|
||||||
|
if existingRs, ok := s.rsm[rstr]; !ok {
|
||||||
|
s.rsm[rstr] = rs
|
||||||
|
// TODO (rghetia): Add support for library Info.
|
||||||
|
if len(rs.ScopeSpans) == 0 {
|
||||||
|
rs.ScopeSpans = []*tracepb.ScopeSpans{
|
||||||
|
{
|
||||||
|
Spans: []*tracepb.Span{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.spanCount += len(rs.ScopeSpans[0].Spans)
|
||||||
|
} else {
|
||||||
|
if len(rs.ScopeSpans) > 0 {
|
||||||
|
newSpans := rs.ScopeSpans[0].GetSpans()
|
||||||
|
existingRs.ScopeSpans[0].Spans = append(existingRs.ScopeSpans[0].Spans, newSpans...)
|
||||||
|
s.spanCount += len(newSpans)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSpans returns the stored spans.
|
||||||
|
func (s *SpansStorage) GetSpans() []*tracepb.Span {
|
||||||
|
spans := make([]*tracepb.Span, 0, s.spanCount)
|
||||||
|
for _, rs := range s.rsm {
|
||||||
|
spans = append(spans, rs.ScopeSpans[0].Spans...)
|
||||||
|
}
|
||||||
|
return spans
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetResourceSpans returns the stored resource spans.
|
||||||
|
func (s *SpansStorage) GetResourceSpans() []*tracepb.ResourceSpans {
|
||||||
|
rss := make([]*tracepb.ResourceSpans, 0, len(s.rsm))
|
||||||
|
for _, rs := range s.rsm {
|
||||||
|
rss = append(rss, rs)
|
||||||
|
}
|
||||||
|
return rss
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceString(res *resourcepb.Resource) string {
|
||||||
|
sAttrs := sortedAttributes(res.GetAttributes())
|
||||||
|
rstr := ""
|
||||||
|
for _, attr := range sAttrs {
|
||||||
|
rstr = rstr + attr.String()
|
||||||
|
}
|
||||||
|
return rstr
|
||||||
|
}
|
||||||
|
|
||||||
|
func sortedAttributes(attrs []*commonpb.KeyValue) []*commonpb.KeyValue {
|
||||||
|
sort.Slice(attrs[:], func(i, j int) bool {
|
||||||
|
return attrs[i].Key < attrs[j].Key
|
||||||
|
})
|
||||||
|
return attrs
|
||||||
|
}
|
66
internal/shared/otlp/otlptrace/otlptracetest/data.go.tmpl
Normal file
66
internal/shared/otlp/otlptrace/otlptracetest/data.go.tmpl
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
// Code created by gotmpl. DO NOT MODIFY.
|
||||||
|
// source: internal/shared/otlp/otlptrace/otlptracetest/data.go.tmpl
|
||||||
|
|
||||||
|
// 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 otlptracetest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel/attribute"
|
||||||
|
"go.opentelemetry.io/otel/codes"
|
||||||
|
"go.opentelemetry.io/otel/sdk/instrumentation"
|
||||||
|
"go.opentelemetry.io/otel/sdk/resource"
|
||||||
|
tracesdk "go.opentelemetry.io/otel/sdk/trace"
|
||||||
|
"go.opentelemetry.io/otel/sdk/trace/tracetest"
|
||||||
|
"go.opentelemetry.io/otel/trace"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SingleReadOnlySpan returns a one-element slice with a read-only span. It
|
||||||
|
// may be useful for testing driver's trace export.
|
||||||
|
func SingleReadOnlySpan() []tracesdk.ReadOnlySpan {
|
||||||
|
return tracetest.SpanStubs{
|
||||||
|
{
|
||||||
|
SpanContext: trace.NewSpanContext(trace.SpanContextConfig{
|
||||||
|
TraceID: trace.TraceID{2, 3, 4, 5, 6, 7, 8, 9, 2, 3, 4, 5, 6, 7, 8, 9},
|
||||||
|
SpanID: trace.SpanID{3, 4, 5, 6, 7, 8, 9, 0},
|
||||||
|
TraceFlags: trace.FlagsSampled,
|
||||||
|
}),
|
||||||
|
Parent: trace.NewSpanContext(trace.SpanContextConfig{
|
||||||
|
TraceID: trace.TraceID{2, 3, 4, 5, 6, 7, 8, 9, 2, 3, 4, 5, 6, 7, 8, 9},
|
||||||
|
SpanID: trace.SpanID{1, 2, 3, 4, 5, 6, 7, 8},
|
||||||
|
TraceFlags: trace.FlagsSampled,
|
||||||
|
}),
|
||||||
|
SpanKind: trace.SpanKindInternal,
|
||||||
|
Name: "foo",
|
||||||
|
StartTime: time.Date(2020, time.December, 8, 20, 23, 0, 0, time.UTC),
|
||||||
|
EndTime: time.Date(2020, time.December, 0, 20, 24, 0, 0, time.UTC),
|
||||||
|
Attributes: []attribute.KeyValue{},
|
||||||
|
Events: []tracesdk.Event{},
|
||||||
|
Links: []tracesdk.Link{},
|
||||||
|
Status: tracesdk.Status{Code: codes.Ok},
|
||||||
|
DroppedAttributes: 0,
|
||||||
|
DroppedEvents: 0,
|
||||||
|
DroppedLinks: 0,
|
||||||
|
ChildSpanCount: 0,
|
||||||
|
Resource: resource.NewSchemaless(attribute.String("a", "b")),
|
||||||
|
InstrumentationLibrary: instrumentation.Library{
|
||||||
|
Name: "bar",
|
||||||
|
Version: "0.0.0",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}.Snapshots()
|
||||||
|
}
|
128
internal/shared/otlp/otlptrace/otlptracetest/otlptest.go.tmpl
Normal file
128
internal/shared/otlp/otlptrace/otlptracetest/otlptest.go.tmpl
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
// Code created by gotmpl. DO NOT MODIFY.
|
||||||
|
// source: internal/shared/otlp/otlptrace/otlptracetest/otlptest.go.tmpl
|
||||||
|
|
||||||
|
// 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 otlptracetest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel/attribute"
|
||||||
|
"go.opentelemetry.io/otel/exporters/otlp/otlptrace"
|
||||||
|
"go.opentelemetry.io/otel/sdk/resource"
|
||||||
|
sdktrace "go.opentelemetry.io/otel/sdk/trace"
|
||||||
|
commonpb "go.opentelemetry.io/proto/otlp/common/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RunEndToEndTest can be used by otlptrace.Client tests to validate
|
||||||
|
// themselves.
|
||||||
|
func RunEndToEndTest(ctx context.Context, t *testing.T, exp *otlptrace.Exporter, tracesCollector TracesCollector) {
|
||||||
|
pOpts := []sdktrace.TracerProviderOption{
|
||||||
|
sdktrace.WithSampler(sdktrace.AlwaysSample()),
|
||||||
|
sdktrace.WithBatcher(
|
||||||
|
exp,
|
||||||
|
// add following two options to ensure flush
|
||||||
|
sdktrace.WithBatchTimeout(5*time.Second),
|
||||||
|
sdktrace.WithMaxExportBatchSize(10),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
tp1 := sdktrace.NewTracerProvider(append(pOpts,
|
||||||
|
sdktrace.WithResource(resource.NewSchemaless(
|
||||||
|
attribute.String("rk1", "rv11)"),
|
||||||
|
attribute.Int64("rk2", 5),
|
||||||
|
)))...)
|
||||||
|
|
||||||
|
tp2 := sdktrace.NewTracerProvider(append(pOpts,
|
||||||
|
sdktrace.WithResource(resource.NewSchemaless(
|
||||||
|
attribute.String("rk1", "rv12)"),
|
||||||
|
attribute.Float64("rk3", 6.5),
|
||||||
|
)))...)
|
||||||
|
|
||||||
|
tr1 := tp1.Tracer("test-tracer1")
|
||||||
|
tr2 := tp2.Tracer("test-tracer2")
|
||||||
|
// Now create few spans
|
||||||
|
m := 4
|
||||||
|
for i := 0; i < m; i++ {
|
||||||
|
_, span := tr1.Start(ctx, "AlwaysSample")
|
||||||
|
span.SetAttributes(attribute.Int64("i", int64(i)))
|
||||||
|
span.End()
|
||||||
|
|
||||||
|
_, span = tr2.Start(ctx, "AlwaysSample")
|
||||||
|
span.SetAttributes(attribute.Int64("i", int64(i)))
|
||||||
|
span.End()
|
||||||
|
}
|
||||||
|
|
||||||
|
func() {
|
||||||
|
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
if err := tp1.Shutdown(ctx); err != nil {
|
||||||
|
t.Fatalf("failed to shut down a tracer provider 1: %v", err)
|
||||||
|
}
|
||||||
|
if err := tp2.Shutdown(ctx); err != nil {
|
||||||
|
t.Fatalf("failed to shut down a tracer provider 2: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Wait >2 cycles.
|
||||||
|
<-time.After(40 * time.Millisecond)
|
||||||
|
|
||||||
|
// Now shutdown the exporter
|
||||||
|
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
if err := exp.Shutdown(ctx); err != nil {
|
||||||
|
t.Fatalf("failed to stop the exporter: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shutdown the collector too so that we can begin
|
||||||
|
// verification checks of expected data back.
|
||||||
|
if err := tracesCollector.Stop(); err != nil {
|
||||||
|
t.Fatalf("failed to stop the mock collector: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now verify that we only got two resources
|
||||||
|
rss := tracesCollector.GetResourceSpans()
|
||||||
|
if got, want := len(rss), 2; got != want {
|
||||||
|
t.Fatalf("resource span count: got %d, want %d\n", got, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now verify spans and attributes for each resource span.
|
||||||
|
for _, rs := range rss {
|
||||||
|
if len(rs.ScopeSpans) == 0 {
|
||||||
|
t.Fatalf("zero ScopeSpans")
|
||||||
|
}
|
||||||
|
if got, want := len(rs.ScopeSpans[0].Spans), m; got != want {
|
||||||
|
t.Fatalf("span counts: got %d, want %d", got, want)
|
||||||
|
}
|
||||||
|
attrMap := map[int64]bool{}
|
||||||
|
for _, s := range rs.ScopeSpans[0].Spans {
|
||||||
|
if gotName, want := s.Name, "AlwaysSample"; gotName != want {
|
||||||
|
t.Fatalf("span name: got %s, want %s", gotName, want)
|
||||||
|
}
|
||||||
|
attrMap[s.Attributes[0].Value.Value.(*commonpb.AnyValue_IntValue).IntValue] = true
|
||||||
|
}
|
||||||
|
if got, want := len(attrMap), m; got != want {
|
||||||
|
t.Fatalf("span attribute unique values: got %d want %d", got, want)
|
||||||
|
}
|
||||||
|
for i := 0; i < m; i++ {
|
||||||
|
_, ok := attrMap[int64(i)]
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("span with attribute %d missing", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
161
internal/shared/otlp/otlptrace/tracetransform/attribute.go.tmpl
Normal file
161
internal/shared/otlp/otlptrace/tracetransform/attribute.go.tmpl
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
// Code created by gotmpl. DO NOT MODIFY.
|
||||||
|
// source: internal/shared/otlp/otlptrace/tracetransform/attribute.go.tmpl
|
||||||
|
|
||||||
|
// 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 tracetransform
|
||||||
|
|
||||||
|
import (
|
||||||
|
"go.opentelemetry.io/otel/attribute"
|
||||||
|
"go.opentelemetry.io/otel/sdk/resource"
|
||||||
|
commonpb "go.opentelemetry.io/proto/otlp/common/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// KeyValues transforms a slice of attribute KeyValues into OTLP key-values.
|
||||||
|
func KeyValues(attrs []attribute.KeyValue) []*commonpb.KeyValue {
|
||||||
|
if len(attrs) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
out := make([]*commonpb.KeyValue, 0, len(attrs))
|
||||||
|
for _, kv := range attrs {
|
||||||
|
out = append(out, KeyValue(kv))
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iterator transforms an attribute iterator into OTLP key-values.
|
||||||
|
func Iterator(iter attribute.Iterator) []*commonpb.KeyValue {
|
||||||
|
l := iter.Len()
|
||||||
|
if l == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
out := make([]*commonpb.KeyValue, 0, l)
|
||||||
|
for iter.Next() {
|
||||||
|
out = append(out, KeyValue(iter.Attribute()))
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResourceAttributes transforms a Resource OTLP key-values.
|
||||||
|
func ResourceAttributes(res *resource.Resource) []*commonpb.KeyValue {
|
||||||
|
return Iterator(res.Iter())
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeyValue transforms an attribute KeyValue into an OTLP key-value.
|
||||||
|
func KeyValue(kv attribute.KeyValue) *commonpb.KeyValue {
|
||||||
|
return &commonpb.KeyValue{Key: string(kv.Key), Value: Value(kv.Value)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Value transforms an attribute Value into an OTLP AnyValue.
|
||||||
|
func Value(v attribute.Value) *commonpb.AnyValue {
|
||||||
|
av := new(commonpb.AnyValue)
|
||||||
|
switch v.Type() {
|
||||||
|
case attribute.BOOL:
|
||||||
|
av.Value = &commonpb.AnyValue_BoolValue{
|
||||||
|
BoolValue: v.AsBool(),
|
||||||
|
}
|
||||||
|
case attribute.BOOLSLICE:
|
||||||
|
av.Value = &commonpb.AnyValue_ArrayValue{
|
||||||
|
ArrayValue: &commonpb.ArrayValue{
|
||||||
|
Values: boolSliceValues(v.AsBoolSlice()),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
case attribute.INT64:
|
||||||
|
av.Value = &commonpb.AnyValue_IntValue{
|
||||||
|
IntValue: v.AsInt64(),
|
||||||
|
}
|
||||||
|
case attribute.INT64SLICE:
|
||||||
|
av.Value = &commonpb.AnyValue_ArrayValue{
|
||||||
|
ArrayValue: &commonpb.ArrayValue{
|
||||||
|
Values: int64SliceValues(v.AsInt64Slice()),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
case attribute.FLOAT64:
|
||||||
|
av.Value = &commonpb.AnyValue_DoubleValue{
|
||||||
|
DoubleValue: v.AsFloat64(),
|
||||||
|
}
|
||||||
|
case attribute.FLOAT64SLICE:
|
||||||
|
av.Value = &commonpb.AnyValue_ArrayValue{
|
||||||
|
ArrayValue: &commonpb.ArrayValue{
|
||||||
|
Values: float64SliceValues(v.AsFloat64Slice()),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
case attribute.STRING:
|
||||||
|
av.Value = &commonpb.AnyValue_StringValue{
|
||||||
|
StringValue: v.AsString(),
|
||||||
|
}
|
||||||
|
case attribute.STRINGSLICE:
|
||||||
|
av.Value = &commonpb.AnyValue_ArrayValue{
|
||||||
|
ArrayValue: &commonpb.ArrayValue{
|
||||||
|
Values: stringSliceValues(v.AsStringSlice()),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
av.Value = &commonpb.AnyValue_StringValue{
|
||||||
|
StringValue: "INVALID",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return av
|
||||||
|
}
|
||||||
|
|
||||||
|
func boolSliceValues(vals []bool) []*commonpb.AnyValue {
|
||||||
|
converted := make([]*commonpb.AnyValue, len(vals))
|
||||||
|
for i, v := range vals {
|
||||||
|
converted[i] = &commonpb.AnyValue{
|
||||||
|
Value: &commonpb.AnyValue_BoolValue{
|
||||||
|
BoolValue: v,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return converted
|
||||||
|
}
|
||||||
|
|
||||||
|
func int64SliceValues(vals []int64) []*commonpb.AnyValue {
|
||||||
|
converted := make([]*commonpb.AnyValue, len(vals))
|
||||||
|
for i, v := range vals {
|
||||||
|
converted[i] = &commonpb.AnyValue{
|
||||||
|
Value: &commonpb.AnyValue_IntValue{
|
||||||
|
IntValue: v,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return converted
|
||||||
|
}
|
||||||
|
|
||||||
|
func float64SliceValues(vals []float64) []*commonpb.AnyValue {
|
||||||
|
converted := make([]*commonpb.AnyValue, len(vals))
|
||||||
|
for i, v := range vals {
|
||||||
|
converted[i] = &commonpb.AnyValue{
|
||||||
|
Value: &commonpb.AnyValue_DoubleValue{
|
||||||
|
DoubleValue: v,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return converted
|
||||||
|
}
|
||||||
|
|
||||||
|
func stringSliceValues(vals []string) []*commonpb.AnyValue {
|
||||||
|
converted := make([]*commonpb.AnyValue, len(vals))
|
||||||
|
for i, v := range vals {
|
||||||
|
converted[i] = &commonpb.AnyValue{
|
||||||
|
Value: &commonpb.AnyValue_StringValue{
|
||||||
|
StringValue: v,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return converted
|
||||||
|
}
|
@ -0,0 +1,261 @@
|
|||||||
|
// Code created by gotmpl. DO NOT MODIFY.
|
||||||
|
// source: internal/shared/otlp/otlptrace/tracetransform/attribute_test.go.tmpl
|
||||||
|
|
||||||
|
// 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 tracetransform
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel/attribute"
|
||||||
|
commonpb "go.opentelemetry.io/proto/otlp/common/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
type attributeTest struct {
|
||||||
|
attrs []attribute.KeyValue
|
||||||
|
expected []*commonpb.KeyValue
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAttributes(t *testing.T) {
|
||||||
|
for _, test := range []attributeTest{
|
||||||
|
{nil, nil},
|
||||||
|
{
|
||||||
|
[]attribute.KeyValue{
|
||||||
|
attribute.Int("int to int", 123),
|
||||||
|
attribute.Int64("int64 to int64", 1234567),
|
||||||
|
attribute.Float64("float64 to double", 1.61),
|
||||||
|
attribute.String("string to string", "string"),
|
||||||
|
attribute.Bool("bool to bool", true),
|
||||||
|
},
|
||||||
|
[]*commonpb.KeyValue{
|
||||||
|
{
|
||||||
|
Key: "int to int",
|
||||||
|
Value: &commonpb.AnyValue{
|
||||||
|
Value: &commonpb.AnyValue_IntValue{
|
||||||
|
IntValue: 123,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Key: "int64 to int64",
|
||||||
|
Value: &commonpb.AnyValue{
|
||||||
|
Value: &commonpb.AnyValue_IntValue{
|
||||||
|
IntValue: 1234567,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Key: "float64 to double",
|
||||||
|
Value: &commonpb.AnyValue{
|
||||||
|
Value: &commonpb.AnyValue_DoubleValue{
|
||||||
|
DoubleValue: 1.61,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Key: "string to string",
|
||||||
|
Value: &commonpb.AnyValue{
|
||||||
|
Value: &commonpb.AnyValue_StringValue{
|
||||||
|
StringValue: "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Key: "bool to bool",
|
||||||
|
Value: &commonpb.AnyValue{
|
||||||
|
Value: &commonpb.AnyValue_BoolValue{
|
||||||
|
BoolValue: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
got := KeyValues(test.attrs)
|
||||||
|
if !assert.Len(t, got, len(test.expected)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for i, actual := range got {
|
||||||
|
if a, ok := actual.Value.Value.(*commonpb.AnyValue_DoubleValue); ok {
|
||||||
|
e, ok := test.expected[i].Value.Value.(*commonpb.AnyValue_DoubleValue)
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("expected AnyValue_DoubleValue, got %T", test.expected[i].Value.Value)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !assert.InDelta(t, e.DoubleValue, a.DoubleValue, 0.01) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
e.DoubleValue = a.DoubleValue
|
||||||
|
}
|
||||||
|
assert.Equal(t, test.expected[i], actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestArrayAttributes(t *testing.T) {
|
||||||
|
// Array KeyValue supports only arrays of primitive types:
|
||||||
|
// "bool", "int", "int64",
|
||||||
|
// "float64", "string",
|
||||||
|
for _, test := range []attributeTest{
|
||||||
|
{nil, nil},
|
||||||
|
{
|
||||||
|
[]attribute.KeyValue{
|
||||||
|
{
|
||||||
|
Key: attribute.Key("invalid"),
|
||||||
|
Value: attribute.Value{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[]*commonpb.KeyValue{
|
||||||
|
{
|
||||||
|
Key: "invalid",
|
||||||
|
Value: &commonpb.AnyValue{
|
||||||
|
Value: &commonpb.AnyValue_StringValue{
|
||||||
|
StringValue: "INVALID",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[]attribute.KeyValue{
|
||||||
|
attribute.BoolSlice("bool slice to bool array", []bool{true, false}),
|
||||||
|
attribute.IntSlice("int slice to int64 array", []int{1, 2, 3}),
|
||||||
|
attribute.Int64Slice("int64 slice to int64 array", []int64{1, 2, 3}),
|
||||||
|
attribute.Float64Slice("float64 slice to double array", []float64{1.11, 2.22, 3.33}),
|
||||||
|
attribute.StringSlice("string slice to string array", []string{"foo", "bar", "baz"}),
|
||||||
|
},
|
||||||
|
[]*commonpb.KeyValue{
|
||||||
|
newOTelBoolArray("bool slice to bool array", []bool{true, false}),
|
||||||
|
newOTelIntArray("int slice to int64 array", []int64{1, 2, 3}),
|
||||||
|
newOTelIntArray("int64 slice to int64 array", []int64{1, 2, 3}),
|
||||||
|
newOTelDoubleArray("float64 slice to double array", []float64{1.11, 2.22, 3.33}),
|
||||||
|
newOTelStringArray("string slice to string array", []string{"foo", "bar", "baz"}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
actualArrayAttributes := KeyValues(test.attrs)
|
||||||
|
expectedArrayAttributes := test.expected
|
||||||
|
if !assert.Len(t, actualArrayAttributes, len(expectedArrayAttributes)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, actualArrayAttr := range actualArrayAttributes {
|
||||||
|
expectedArrayAttr := expectedArrayAttributes[i]
|
||||||
|
expectedKey, actualKey := expectedArrayAttr.Key, actualArrayAttr.Key
|
||||||
|
if !assert.Equal(t, expectedKey, actualKey) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := expectedArrayAttr.Value.GetArrayValue()
|
||||||
|
actual := actualArrayAttr.Value.GetArrayValue()
|
||||||
|
if expected == nil {
|
||||||
|
assert.Nil(t, actual)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if assert.NotNil(t, actual, "expected not nil for %s", actualKey) {
|
||||||
|
assertExpectedArrayValues(t, expected.Values, actual.Values)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertExpectedArrayValues(t *testing.T, expectedValues, actualValues []*commonpb.AnyValue) {
|
||||||
|
for i, actual := range actualValues {
|
||||||
|
expected := expectedValues[i]
|
||||||
|
if a, ok := actual.Value.(*commonpb.AnyValue_DoubleValue); ok {
|
||||||
|
e, ok := expected.Value.(*commonpb.AnyValue_DoubleValue)
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("expected AnyValue_DoubleValue, got %T", expected.Value)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !assert.InDelta(t, e.DoubleValue, a.DoubleValue, 0.01) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
e.DoubleValue = a.DoubleValue
|
||||||
|
}
|
||||||
|
assert.Equal(t, expected, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newOTelBoolArray(key string, values []bool) *commonpb.KeyValue {
|
||||||
|
arrayValues := []*commonpb.AnyValue{}
|
||||||
|
for _, b := range values {
|
||||||
|
arrayValues = append(arrayValues, &commonpb.AnyValue{
|
||||||
|
Value: &commonpb.AnyValue_BoolValue{
|
||||||
|
BoolValue: b,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return newOTelArray(key, arrayValues)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newOTelIntArray(key string, values []int64) *commonpb.KeyValue {
|
||||||
|
arrayValues := []*commonpb.AnyValue{}
|
||||||
|
|
||||||
|
for _, i := range values {
|
||||||
|
arrayValues = append(arrayValues, &commonpb.AnyValue{
|
||||||
|
Value: &commonpb.AnyValue_IntValue{
|
||||||
|
IntValue: i,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return newOTelArray(key, arrayValues)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newOTelDoubleArray(key string, values []float64) *commonpb.KeyValue {
|
||||||
|
arrayValues := []*commonpb.AnyValue{}
|
||||||
|
|
||||||
|
for _, d := range values {
|
||||||
|
arrayValues = append(arrayValues, &commonpb.AnyValue{
|
||||||
|
Value: &commonpb.AnyValue_DoubleValue{
|
||||||
|
DoubleValue: d,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return newOTelArray(key, arrayValues)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newOTelStringArray(key string, values []string) *commonpb.KeyValue {
|
||||||
|
arrayValues := []*commonpb.AnyValue{}
|
||||||
|
|
||||||
|
for _, s := range values {
|
||||||
|
arrayValues = append(arrayValues, &commonpb.AnyValue{
|
||||||
|
Value: &commonpb.AnyValue_StringValue{
|
||||||
|
StringValue: s,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return newOTelArray(key, arrayValues)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newOTelArray(key string, arrayValues []*commonpb.AnyValue) *commonpb.KeyValue {
|
||||||
|
return &commonpb.KeyValue{
|
||||||
|
Key: key,
|
||||||
|
Value: &commonpb.AnyValue{
|
||||||
|
Value: &commonpb.AnyValue_ArrayValue{
|
||||||
|
ArrayValue: &commonpb.ArrayValue{
|
||||||
|
Values: arrayValues,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
// Code created by gotmpl. DO NOT MODIFY.
|
||||||
|
// source: internal/shared/otlp/otlptrace/tracetransform/instrumentation.go.tmpl
|
||||||
|
|
||||||
|
// 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 tracetransform
|
||||||
|
|
||||||
|
import (
|
||||||
|
"go.opentelemetry.io/otel/sdk/instrumentation"
|
||||||
|
commonpb "go.opentelemetry.io/proto/otlp/common/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func InstrumentationScope(il instrumentation.Scope) *commonpb.InstrumentationScope {
|
||||||
|
if il == (instrumentation.Scope{}) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &commonpb.InstrumentationScope{
|
||||||
|
Name: il.Name,
|
||||||
|
Version: il.Version,
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
// Code created by gotmpl. DO NOT MODIFY.
|
||||||
|
// source: internal/shared/otlp/otlptrace/tracetransform/resource.go.tmpl
|
||||||
|
|
||||||
|
// 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 tracetransform
|
||||||
|
|
||||||
|
import (
|
||||||
|
"go.opentelemetry.io/otel/sdk/resource"
|
||||||
|
resourcepb "go.opentelemetry.io/proto/otlp/resource/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Resource transforms a Resource into an OTLP Resource.
|
||||||
|
func Resource(r *resource.Resource) *resourcepb.Resource {
|
||||||
|
if r == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &resourcepb.Resource{Attributes: ResourceAttributes(r)}
|
||||||
|
}
|
@ -0,0 +1,51 @@
|
|||||||
|
// Code created by gotmpl. DO NOT MODIFY.
|
||||||
|
// source: internal/shared/otlp/otlptrace/tracetransform/resource_test.go.tmpl
|
||||||
|
|
||||||
|
// 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 tracetransform
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel/attribute"
|
||||||
|
"go.opentelemetry.io/otel/sdk/resource"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNilResource(t *testing.T) {
|
||||||
|
assert.Empty(t, Resource(nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEmptyResource(t *testing.T) {
|
||||||
|
assert.Empty(t, Resource(&resource.Resource{}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This does not include any testing on the ordering of Resource Attributes.
|
||||||
|
* They are stored as a map internally to the Resource and their order is not
|
||||||
|
* guaranteed.
|
||||||
|
*/
|
||||||
|
|
||||||
|
func TestResourceAttributes(t *testing.T) {
|
||||||
|
attrs := []attribute.KeyValue{attribute.Int("one", 1), attribute.Int("two", 2)}
|
||||||
|
|
||||||
|
got := Resource(resource.NewSchemaless(attrs...)).GetAttributes()
|
||||||
|
if !assert.Len(t, attrs, 2) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
assert.ElementsMatch(t, KeyValues(attrs), got)
|
||||||
|
}
|
208
internal/shared/otlp/otlptrace/tracetransform/span.go.tmpl
Normal file
208
internal/shared/otlp/otlptrace/tracetransform/span.go.tmpl
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
// Code created by gotmpl. DO NOT MODIFY.
|
||||||
|
// source: internal/shared/otlp/otlptrace/tracetransform/span.go.tmpl
|
||||||
|
|
||||||
|
// 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 tracetransform
|
||||||
|
|
||||||
|
import (
|
||||||
|
"go.opentelemetry.io/otel/attribute"
|
||||||
|
"go.opentelemetry.io/otel/codes"
|
||||||
|
"go.opentelemetry.io/otel/sdk/instrumentation"
|
||||||
|
tracesdk "go.opentelemetry.io/otel/sdk/trace"
|
||||||
|
"go.opentelemetry.io/otel/trace"
|
||||||
|
tracepb "go.opentelemetry.io/proto/otlp/trace/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Spans transforms a slice of OpenTelemetry spans into a slice of OTLP
|
||||||
|
// ResourceSpans.
|
||||||
|
func Spans(sdl []tracesdk.ReadOnlySpan) []*tracepb.ResourceSpans {
|
||||||
|
if len(sdl) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
rsm := make(map[attribute.Distinct]*tracepb.ResourceSpans)
|
||||||
|
|
||||||
|
type key struct {
|
||||||
|
r attribute.Distinct
|
||||||
|
is instrumentation.Scope
|
||||||
|
}
|
||||||
|
ssm := make(map[key]*tracepb.ScopeSpans)
|
||||||
|
|
||||||
|
var resources int
|
||||||
|
for _, sd := range sdl {
|
||||||
|
if sd == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
rKey := sd.Resource().Equivalent()
|
||||||
|
k := key{
|
||||||
|
r: rKey,
|
||||||
|
is: sd.InstrumentationScope(),
|
||||||
|
}
|
||||||
|
scopeSpan, iOk := ssm[k]
|
||||||
|
if !iOk {
|
||||||
|
// Either the resource or instrumentation scope were unknown.
|
||||||
|
scopeSpan = &tracepb.ScopeSpans{
|
||||||
|
Scope: InstrumentationScope(sd.InstrumentationScope()),
|
||||||
|
Spans: []*tracepb.Span{},
|
||||||
|
SchemaUrl: sd.InstrumentationScope().SchemaURL,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
scopeSpan.Spans = append(scopeSpan.Spans, span(sd))
|
||||||
|
ssm[k] = scopeSpan
|
||||||
|
|
||||||
|
rs, rOk := rsm[rKey]
|
||||||
|
if !rOk {
|
||||||
|
resources++
|
||||||
|
// The resource was unknown.
|
||||||
|
rs = &tracepb.ResourceSpans{
|
||||||
|
Resource: Resource(sd.Resource()),
|
||||||
|
ScopeSpans: []*tracepb.ScopeSpans{scopeSpan},
|
||||||
|
SchemaUrl: sd.Resource().SchemaURL(),
|
||||||
|
}
|
||||||
|
rsm[rKey] = rs
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// The resource has been seen before. Check if the instrumentation
|
||||||
|
// library lookup was unknown because if so we need to add it to the
|
||||||
|
// ResourceSpans. Otherwise, the instrumentation library has already
|
||||||
|
// been seen and the append we did above will be included it in the
|
||||||
|
// ScopeSpans reference.
|
||||||
|
if !iOk {
|
||||||
|
rs.ScopeSpans = append(rs.ScopeSpans, scopeSpan)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transform the categorized map into a slice
|
||||||
|
rss := make([]*tracepb.ResourceSpans, 0, resources)
|
||||||
|
for _, rs := range rsm {
|
||||||
|
rss = append(rss, rs)
|
||||||
|
}
|
||||||
|
return rss
|
||||||
|
}
|
||||||
|
|
||||||
|
// span transforms a Span into an OTLP span.
|
||||||
|
func span(sd tracesdk.ReadOnlySpan) *tracepb.Span {
|
||||||
|
if sd == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
tid := sd.SpanContext().TraceID()
|
||||||
|
sid := sd.SpanContext().SpanID()
|
||||||
|
|
||||||
|
s := &tracepb.Span{
|
||||||
|
TraceId: tid[:],
|
||||||
|
SpanId: sid[:],
|
||||||
|
TraceState: sd.SpanContext().TraceState().String(),
|
||||||
|
Status: status(sd.Status().Code, sd.Status().Description),
|
||||||
|
StartTimeUnixNano: uint64(sd.StartTime().UnixNano()),
|
||||||
|
EndTimeUnixNano: uint64(sd.EndTime().UnixNano()),
|
||||||
|
Links: links(sd.Links()),
|
||||||
|
Kind: spanKind(sd.SpanKind()),
|
||||||
|
Name: sd.Name(),
|
||||||
|
Attributes: KeyValues(sd.Attributes()),
|
||||||
|
Events: spanEvents(sd.Events()),
|
||||||
|
DroppedAttributesCount: uint32(sd.DroppedAttributes()),
|
||||||
|
DroppedEventsCount: uint32(sd.DroppedEvents()),
|
||||||
|
DroppedLinksCount: uint32(sd.DroppedLinks()),
|
||||||
|
}
|
||||||
|
|
||||||
|
if psid := sd.Parent().SpanID(); psid.IsValid() {
|
||||||
|
s.ParentSpanId = psid[:]
|
||||||
|
}
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// status transform a span code and message into an OTLP span status.
|
||||||
|
func status(status codes.Code, message string) *tracepb.Status {
|
||||||
|
var c tracepb.Status_StatusCode
|
||||||
|
switch status {
|
||||||
|
case codes.Ok:
|
||||||
|
c = tracepb.Status_STATUS_CODE_OK
|
||||||
|
case codes.Error:
|
||||||
|
c = tracepb.Status_STATUS_CODE_ERROR
|
||||||
|
default:
|
||||||
|
c = tracepb.Status_STATUS_CODE_UNSET
|
||||||
|
}
|
||||||
|
return &tracepb.Status{
|
||||||
|
Code: c,
|
||||||
|
Message: message,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// links transforms span Links to OTLP span links.
|
||||||
|
func links(links []tracesdk.Link) []*tracepb.Span_Link {
|
||||||
|
if len(links) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
sl := make([]*tracepb.Span_Link, 0, len(links))
|
||||||
|
for _, otLink := range links {
|
||||||
|
// This redefinition is necessary to prevent otLink.*ID[:] copies
|
||||||
|
// being reused -- in short we need a new otLink per iteration.
|
||||||
|
otLink := otLink
|
||||||
|
|
||||||
|
tid := otLink.SpanContext.TraceID()
|
||||||
|
sid := otLink.SpanContext.SpanID()
|
||||||
|
|
||||||
|
sl = append(sl, &tracepb.Span_Link{
|
||||||
|
TraceId: tid[:],
|
||||||
|
SpanId: sid[:],
|
||||||
|
Attributes: KeyValues(otLink.Attributes),
|
||||||
|
DroppedAttributesCount: uint32(otLink.DroppedAttributeCount),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return sl
|
||||||
|
}
|
||||||
|
|
||||||
|
// spanEvents transforms span Events to an OTLP span events.
|
||||||
|
func spanEvents(es []tracesdk.Event) []*tracepb.Span_Event {
|
||||||
|
if len(es) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
events := make([]*tracepb.Span_Event, len(es))
|
||||||
|
// Transform message events
|
||||||
|
for i := 0; i < len(es); i++ {
|
||||||
|
events[i] = &tracepb.Span_Event{
|
||||||
|
Name: es[i].Name,
|
||||||
|
TimeUnixNano: uint64(es[i].Time.UnixNano()),
|
||||||
|
Attributes: KeyValues(es[i].Attributes),
|
||||||
|
DroppedAttributesCount: uint32(es[i].DroppedAttributeCount),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return events
|
||||||
|
}
|
||||||
|
|
||||||
|
// spanKind transforms a SpanKind to an OTLP span kind.
|
||||||
|
func spanKind(kind trace.SpanKind) tracepb.Span_SpanKind {
|
||||||
|
switch kind {
|
||||||
|
case trace.SpanKindInternal:
|
||||||
|
return tracepb.Span_SPAN_KIND_INTERNAL
|
||||||
|
case trace.SpanKindClient:
|
||||||
|
return tracepb.Span_SPAN_KIND_CLIENT
|
||||||
|
case trace.SpanKindServer:
|
||||||
|
return tracepb.Span_SPAN_KIND_SERVER
|
||||||
|
case trace.SpanKindProducer:
|
||||||
|
return tracepb.Span_SPAN_KIND_PRODUCER
|
||||||
|
case trace.SpanKindConsumer:
|
||||||
|
return tracepb.Span_SPAN_KIND_CONSUMER
|
||||||
|
default:
|
||||||
|
return tracepb.Span_SPAN_KIND_UNSPECIFIED
|
||||||
|
}
|
||||||
|
}
|
336
internal/shared/otlp/otlptrace/tracetransform/span_test.go.tmpl
Normal file
336
internal/shared/otlp/otlptrace/tracetransform/span_test.go.tmpl
Normal file
@ -0,0 +1,336 @@
|
|||||||
|
// Code created by gotmpl. DO NOT MODIFY.
|
||||||
|
// source: internal/shared/otlp/otlptrace/tracetransform/span_test.go.tmpl
|
||||||
|
|
||||||
|
// 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 tracetransform
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel/attribute"
|
||||||
|
"go.opentelemetry.io/otel/codes"
|
||||||
|
"go.opentelemetry.io/otel/sdk/instrumentation"
|
||||||
|
"go.opentelemetry.io/otel/sdk/resource"
|
||||||
|
tracesdk "go.opentelemetry.io/otel/sdk/trace"
|
||||||
|
"go.opentelemetry.io/otel/sdk/trace/tracetest"
|
||||||
|
semconv "go.opentelemetry.io/otel/semconv/v1.17.0"
|
||||||
|
"go.opentelemetry.io/otel/trace"
|
||||||
|
tracepb "go.opentelemetry.io/proto/otlp/trace/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSpanKind(t *testing.T) {
|
||||||
|
for _, test := range []struct {
|
||||||
|
kind trace.SpanKind
|
||||||
|
expected tracepb.Span_SpanKind
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
trace.SpanKindInternal,
|
||||||
|
tracepb.Span_SPAN_KIND_INTERNAL,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
trace.SpanKindClient,
|
||||||
|
tracepb.Span_SPAN_KIND_CLIENT,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
trace.SpanKindServer,
|
||||||
|
tracepb.Span_SPAN_KIND_SERVER,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
trace.SpanKindProducer,
|
||||||
|
tracepb.Span_SPAN_KIND_PRODUCER,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
trace.SpanKindConsumer,
|
||||||
|
tracepb.Span_SPAN_KIND_CONSUMER,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
trace.SpanKind(-1),
|
||||||
|
tracepb.Span_SPAN_KIND_UNSPECIFIED,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
assert.Equal(t, test.expected, spanKind(test.kind))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNilSpanEvent(t *testing.T) {
|
||||||
|
assert.Nil(t, spanEvents(nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEmptySpanEvent(t *testing.T) {
|
||||||
|
assert.Nil(t, spanEvents([]tracesdk.Event{}))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSpanEvent(t *testing.T) {
|
||||||
|
attrs := []attribute.KeyValue{attribute.Int("one", 1), attribute.Int("two", 2)}
|
||||||
|
eventTime := time.Date(2020, 5, 20, 0, 0, 0, 0, time.UTC)
|
||||||
|
got := spanEvents([]tracesdk.Event{
|
||||||
|
{
|
||||||
|
Name: "test 1",
|
||||||
|
Attributes: []attribute.KeyValue{},
|
||||||
|
Time: eventTime,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "test 2",
|
||||||
|
Attributes: attrs,
|
||||||
|
Time: eventTime,
|
||||||
|
DroppedAttributeCount: 2,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if !assert.Len(t, got, 2) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
eventTimestamp := uint64(1589932800 * 1e9)
|
||||||
|
assert.Equal(t, &tracepb.Span_Event{Name: "test 1", Attributes: nil, TimeUnixNano: eventTimestamp}, got[0])
|
||||||
|
// Do not test Attributes directly, just that the return value goes to the correct field.
|
||||||
|
assert.Equal(t, &tracepb.Span_Event{Name: "test 2", Attributes: KeyValues(attrs), TimeUnixNano: eventTimestamp, DroppedAttributesCount: 2}, got[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNilLinks(t *testing.T) {
|
||||||
|
assert.Nil(t, links(nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEmptyLinks(t *testing.T) {
|
||||||
|
assert.Nil(t, links([]tracesdk.Link{}))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLinks(t *testing.T) {
|
||||||
|
attrs := []attribute.KeyValue{attribute.Int("one", 1), attribute.Int("two", 2)}
|
||||||
|
l := []tracesdk.Link{
|
||||||
|
{
|
||||||
|
DroppedAttributeCount: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
SpanContext: trace.SpanContext{},
|
||||||
|
Attributes: attrs,
|
||||||
|
DroppedAttributeCount: 3,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
got := links(l)
|
||||||
|
|
||||||
|
// Make sure we get the same number back first.
|
||||||
|
if !assert.Len(t, got, 2) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Empty should be empty.
|
||||||
|
expected := &tracepb.Span_Link{
|
||||||
|
TraceId: []uint8{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0},
|
||||||
|
SpanId: []uint8{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0},
|
||||||
|
DroppedAttributesCount: 3,
|
||||||
|
}
|
||||||
|
assert.Equal(t, expected, got[0])
|
||||||
|
|
||||||
|
// Do not test Attributes directly, just that the return value goes to the correct field.
|
||||||
|
expected.Attributes = KeyValues(attrs)
|
||||||
|
assert.Equal(t, expected, got[1])
|
||||||
|
|
||||||
|
// Changes to our links should not change the produced links.
|
||||||
|
l[1].SpanContext = l[1].SpanContext.WithTraceID(trace.TraceID{})
|
||||||
|
assert.Equal(t, expected, got[1])
|
||||||
|
assert.Equal(t, l[1].DroppedAttributeCount, int(got[1].DroppedAttributesCount))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStatus(t *testing.T) {
|
||||||
|
for _, test := range []struct {
|
||||||
|
code codes.Code
|
||||||
|
message string
|
||||||
|
otlpStatus tracepb.Status_StatusCode
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
codes.Ok,
|
||||||
|
"test Ok",
|
||||||
|
tracepb.Status_STATUS_CODE_OK,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
codes.Unset,
|
||||||
|
"test Unset",
|
||||||
|
tracepb.Status_STATUS_CODE_UNSET,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "default code is unset",
|
||||||
|
otlpStatus: tracepb.Status_STATUS_CODE_UNSET,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
codes.Error,
|
||||||
|
"test Error",
|
||||||
|
tracepb.Status_STATUS_CODE_ERROR,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
expected := &tracepb.Status{Code: test.otlpStatus, Message: test.message}
|
||||||
|
assert.Equal(t, expected, status(test.code, test.message))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNilSpan(t *testing.T) {
|
||||||
|
assert.Nil(t, span(nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNilSpanData(t *testing.T) {
|
||||||
|
assert.Nil(t, Spans(nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEmptySpanData(t *testing.T) {
|
||||||
|
assert.Nil(t, Spans(nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSpanData(t *testing.T) {
|
||||||
|
// Full test of span data
|
||||||
|
|
||||||
|
// March 31, 2020 5:01:26 1234nanos (UTC)
|
||||||
|
startTime := time.Unix(1585674086, 1234)
|
||||||
|
endTime := startTime.Add(10 * time.Second)
|
||||||
|
traceState, _ := trace.ParseTraceState("key1=val1,key2=val2")
|
||||||
|
spanData := tracetest.SpanStub{
|
||||||
|
SpanContext: trace.NewSpanContext(trace.SpanContextConfig{
|
||||||
|
TraceID: trace.TraceID{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F},
|
||||||
|
SpanID: trace.SpanID{0xFF, 0xFE, 0xFD, 0xFC, 0xFB, 0xFA, 0xF9, 0xF8},
|
||||||
|
TraceState: traceState,
|
||||||
|
}),
|
||||||
|
Parent: trace.NewSpanContext(trace.SpanContextConfig{
|
||||||
|
TraceID: trace.TraceID{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F},
|
||||||
|
SpanID: trace.SpanID{0xEF, 0xEE, 0xED, 0xEC, 0xEB, 0xEA, 0xE9, 0xE8},
|
||||||
|
TraceState: traceState,
|
||||||
|
Remote: true,
|
||||||
|
}),
|
||||||
|
SpanKind: trace.SpanKindServer,
|
||||||
|
Name: "span data to span data",
|
||||||
|
StartTime: startTime,
|
||||||
|
EndTime: endTime,
|
||||||
|
Events: []tracesdk.Event{
|
||||||
|
{Time: startTime,
|
||||||
|
Attributes: []attribute.KeyValue{
|
||||||
|
attribute.Int64("CompressedByteSize", 512),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{Time: endTime,
|
||||||
|
Attributes: []attribute.KeyValue{
|
||||||
|
attribute.String("EventType", "Recv"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Links: []tracesdk.Link{
|
||||||
|
{
|
||||||
|
SpanContext: trace.NewSpanContext(trace.SpanContextConfig{
|
||||||
|
TraceID: trace.TraceID{0xC0, 0xC1, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xCB, 0xCC, 0xCD, 0xCE, 0xCF},
|
||||||
|
SpanID: trace.SpanID{0xB0, 0xB1, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7},
|
||||||
|
TraceFlags: 0,
|
||||||
|
}),
|
||||||
|
Attributes: []attribute.KeyValue{
|
||||||
|
attribute.String("LinkType", "Parent"),
|
||||||
|
},
|
||||||
|
DroppedAttributeCount: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
SpanContext: trace.NewSpanContext(trace.SpanContextConfig{
|
||||||
|
TraceID: trace.TraceID{0xE0, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 0xEB, 0xEC, 0xED, 0xEE, 0xEF},
|
||||||
|
SpanID: trace.SpanID{0xD0, 0xD1, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7},
|
||||||
|
TraceFlags: 0,
|
||||||
|
}),
|
||||||
|
Attributes: []attribute.KeyValue{
|
||||||
|
attribute.String("LinkType", "Child"),
|
||||||
|
},
|
||||||
|
DroppedAttributeCount: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Status: tracesdk.Status{
|
||||||
|
Code: codes.Error,
|
||||||
|
Description: "utterly unrecognized",
|
||||||
|
},
|
||||||
|
Attributes: []attribute.KeyValue{
|
||||||
|
attribute.Int64("timeout_ns", 12e9),
|
||||||
|
},
|
||||||
|
DroppedAttributes: 1,
|
||||||
|
DroppedEvents: 2,
|
||||||
|
DroppedLinks: 3,
|
||||||
|
Resource: resource.NewWithAttributes(
|
||||||
|
"http://example.com/custom-resource-schema",
|
||||||
|
attribute.String("rk1", "rv1"),
|
||||||
|
attribute.Int64("rk2", 5),
|
||||||
|
attribute.StringSlice("rk3", []string{"sv1", "sv2"}),
|
||||||
|
),
|
||||||
|
InstrumentationLibrary: instrumentation.Scope{
|
||||||
|
Name: "go.opentelemetry.io/test/otel",
|
||||||
|
Version: "v0.0.1",
|
||||||
|
SchemaURL: semconv.SchemaURL,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not checking resource as the underlying map of our Resource makes
|
||||||
|
// ordering impossible to guarantee on the output. The Resource
|
||||||
|
// transform function has unit tests that should suffice.
|
||||||
|
expectedSpan := &tracepb.Span{
|
||||||
|
TraceId: []byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F},
|
||||||
|
SpanId: []byte{0xFF, 0xFE, 0xFD, 0xFC, 0xFB, 0xFA, 0xF9, 0xF8},
|
||||||
|
ParentSpanId: []byte{0xEF, 0xEE, 0xED, 0xEC, 0xEB, 0xEA, 0xE9, 0xE8},
|
||||||
|
TraceState: "key1=val1,key2=val2",
|
||||||
|
Name: spanData.Name,
|
||||||
|
Kind: tracepb.Span_SPAN_KIND_SERVER,
|
||||||
|
StartTimeUnixNano: uint64(startTime.UnixNano()),
|
||||||
|
EndTimeUnixNano: uint64(endTime.UnixNano()),
|
||||||
|
Status: status(spanData.Status.Code, spanData.Status.Description),
|
||||||
|
Events: spanEvents(spanData.Events),
|
||||||
|
Links: links(spanData.Links),
|
||||||
|
Attributes: KeyValues(spanData.Attributes),
|
||||||
|
DroppedAttributesCount: 1,
|
||||||
|
DroppedEventsCount: 2,
|
||||||
|
DroppedLinksCount: 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
got := Spans(tracetest.SpanStubs{spanData}.Snapshots())
|
||||||
|
require.Len(t, got, 1)
|
||||||
|
|
||||||
|
assert.Equal(t, got[0].GetResource(), Resource(spanData.Resource))
|
||||||
|
assert.Equal(t, got[0].SchemaUrl, spanData.Resource.SchemaURL())
|
||||||
|
scopeSpans := got[0].GetScopeSpans()
|
||||||
|
require.Len(t, scopeSpans, 1)
|
||||||
|
assert.Equal(t, scopeSpans[0].SchemaUrl, spanData.InstrumentationLibrary.SchemaURL)
|
||||||
|
assert.Equal(t, scopeSpans[0].GetScope(), InstrumentationScope(spanData.InstrumentationLibrary))
|
||||||
|
require.Len(t, scopeSpans[0].Spans, 1)
|
||||||
|
actualSpan := scopeSpans[0].Spans[0]
|
||||||
|
|
||||||
|
if diff := cmp.Diff(expectedSpan, actualSpan, cmp.Comparer(proto.Equal)); diff != "" {
|
||||||
|
t.Fatalf("transformed span differs %v\n", diff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Empty parent span ID should be treated as root span.
|
||||||
|
func TestRootSpanData(t *testing.T) {
|
||||||
|
sd := Spans(tracetest.SpanStubs{
|
||||||
|
{},
|
||||||
|
}.Snapshots())
|
||||||
|
require.Len(t, sd, 1)
|
||||||
|
rs := sd[0]
|
||||||
|
scopeSpans := rs.GetScopeSpans()
|
||||||
|
require.Len(t, scopeSpans, 1)
|
||||||
|
got := scopeSpans[0].GetSpans()[0].GetParentSpanId()
|
||||||
|
|
||||||
|
// Empty means root span.
|
||||||
|
assert.Nil(t, got, "incorrect transform of root parent span ID")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSpanDataNilResource(t *testing.T) {
|
||||||
|
assert.NotPanics(t, func() {
|
||||||
|
Spans(tracetest.SpanStubs{
|
||||||
|
{},
|
||||||
|
}.Snapshots())
|
||||||
|
})
|
||||||
|
}
|
156
internal/shared/otlp/retry/retry.go.tmpl
Normal file
156
internal/shared/otlp/retry/retry.go.tmpl
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
// Code created by gotmpl. DO NOT MODIFY.
|
||||||
|
// source: internal/shared/otlp/retry/retry.go.tmpl
|
||||||
|
|
||||||
|
// 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 retry provides request retry functionality that can perform
|
||||||
|
// configurable exponential backoff for transient errors and honor any
|
||||||
|
// explicit throttle responses received.
|
||||||
|
package retry
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/cenkalti/backoff/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DefaultConfig are the recommended defaults to use.
|
||||||
|
var DefaultConfig = Config{
|
||||||
|
Enabled: true,
|
||||||
|
InitialInterval: 5 * time.Second,
|
||||||
|
MaxInterval: 30 * time.Second,
|
||||||
|
MaxElapsedTime: time.Minute,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config defines configuration for retrying batches in case of export failure
|
||||||
|
// using an exponential backoff.
|
||||||
|
type Config struct {
|
||||||
|
// Enabled indicates whether to not retry sending batches in case of
|
||||||
|
// export failure.
|
||||||
|
Enabled bool
|
||||||
|
// InitialInterval the time to wait after the first failure before
|
||||||
|
// retrying.
|
||||||
|
InitialInterval time.Duration
|
||||||
|
// MaxInterval is the upper bound on backoff interval. Once this value is
|
||||||
|
// reached the delay between consecutive retries will always be
|
||||||
|
// `MaxInterval`.
|
||||||
|
MaxInterval time.Duration
|
||||||
|
// MaxElapsedTime is the maximum amount of time (including retries) spent
|
||||||
|
// trying to send a request/batch. Once this value is reached, the data
|
||||||
|
// is discarded.
|
||||||
|
MaxElapsedTime time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// RequestFunc wraps a request with retry logic.
|
||||||
|
type RequestFunc func(context.Context, func(context.Context) error) error
|
||||||
|
|
||||||
|
// EvaluateFunc returns if an error is retry-able and if an explicit throttle
|
||||||
|
// duration should be honored that was included in the error.
|
||||||
|
//
|
||||||
|
// The function must return true if the error argument is retry-able,
|
||||||
|
// otherwise it must return false for the first return parameter.
|
||||||
|
//
|
||||||
|
// The function must return a non-zero time.Duration if the error contains
|
||||||
|
// explicit throttle duration that should be honored, otherwise it must return
|
||||||
|
// a zero valued time.Duration.
|
||||||
|
type EvaluateFunc func(error) (bool, time.Duration)
|
||||||
|
|
||||||
|
// RequestFunc returns a RequestFunc using the evaluate function to determine
|
||||||
|
// if requests can be retried and based on the exponential backoff
|
||||||
|
// configuration of c.
|
||||||
|
func (c Config) RequestFunc(evaluate EvaluateFunc) RequestFunc {
|
||||||
|
if !c.Enabled {
|
||||||
|
return func(ctx context.Context, fn func(context.Context) error) error {
|
||||||
|
return fn(ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return func(ctx context.Context, fn func(context.Context) error) error {
|
||||||
|
// Do not use NewExponentialBackOff since it calls Reset and the code here
|
||||||
|
// must call Reset after changing the InitialInterval (this saves an
|
||||||
|
// unnecessary call to Now).
|
||||||
|
b := &backoff.ExponentialBackOff{
|
||||||
|
InitialInterval: c.InitialInterval,
|
||||||
|
RandomizationFactor: backoff.DefaultRandomizationFactor,
|
||||||
|
Multiplier: backoff.DefaultMultiplier,
|
||||||
|
MaxInterval: c.MaxInterval,
|
||||||
|
MaxElapsedTime: c.MaxElapsedTime,
|
||||||
|
Stop: backoff.Stop,
|
||||||
|
Clock: backoff.SystemClock,
|
||||||
|
}
|
||||||
|
b.Reset()
|
||||||
|
|
||||||
|
for {
|
||||||
|
err := fn(ctx)
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
retryable, throttle := evaluate(err)
|
||||||
|
if !retryable {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
bOff := b.NextBackOff()
|
||||||
|
if bOff == backoff.Stop {
|
||||||
|
return fmt.Errorf("max retry time elapsed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for the greater of the backoff or throttle delay.
|
||||||
|
var delay time.Duration
|
||||||
|
if bOff > throttle {
|
||||||
|
delay = bOff
|
||||||
|
} else {
|
||||||
|
elapsed := b.GetElapsedTime()
|
||||||
|
if b.MaxElapsedTime != 0 && elapsed+throttle > b.MaxElapsedTime {
|
||||||
|
return fmt.Errorf("max retry time would elapse: %w", err)
|
||||||
|
}
|
||||||
|
delay = throttle
|
||||||
|
}
|
||||||
|
|
||||||
|
if ctxErr := waitFunc(ctx, delay); ctxErr != nil {
|
||||||
|
return fmt.Errorf("%w: %s", ctxErr, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow override for testing.
|
||||||
|
var waitFunc = wait
|
||||||
|
|
||||||
|
// wait takes the caller's context, and the amount of time to wait. It will
|
||||||
|
// return nil if the timer fires before or at the same time as the context's
|
||||||
|
// deadline. This indicates that the call can be retried.
|
||||||
|
func wait(ctx context.Context, delay time.Duration) error {
|
||||||
|
timer := time.NewTimer(delay)
|
||||||
|
defer timer.Stop()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
// Handle the case where the timer and context deadline end
|
||||||
|
// simultaneously by prioritizing the timer expiration nil value
|
||||||
|
// response.
|
||||||
|
select {
|
||||||
|
case <-timer.C:
|
||||||
|
default:
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
case <-timer.C:
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
261
internal/shared/otlp/retry/retry_test.go.tmpl
Normal file
261
internal/shared/otlp/retry/retry_test.go.tmpl
Normal file
@ -0,0 +1,261 @@
|
|||||||
|
// Code created by gotmpl. DO NOT MODIFY.
|
||||||
|
// source: internal/shared/otlp/retry/retry_test.go.tmpl
|
||||||
|
|
||||||
|
// 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 retry
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"math"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/cenkalti/backoff/v4"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestWait(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
ctx context.Context
|
||||||
|
delay time.Duration
|
||||||
|
expected error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
ctx: context.Background(),
|
||||||
|
delay: time.Duration(0),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ctx: context.Background(),
|
||||||
|
delay: time.Duration(1),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ctx: context.Background(),
|
||||||
|
delay: time.Duration(-1),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ctx: func() context.Context {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
cancel()
|
||||||
|
return ctx
|
||||||
|
}(),
|
||||||
|
// Ensure the timer and context do not end simultaneously.
|
||||||
|
delay: 1 * time.Hour,
|
||||||
|
expected: context.Canceled,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
err := wait(test.ctx, test.delay)
|
||||||
|
if test.expected == nil {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
} else {
|
||||||
|
assert.ErrorIs(t, err, test.expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNonRetryableError(t *testing.T) {
|
||||||
|
ev := func(error) (bool, time.Duration) { return false, 0 }
|
||||||
|
|
||||||
|
reqFunc := Config{
|
||||||
|
Enabled: true,
|
||||||
|
InitialInterval: 1 * time.Nanosecond,
|
||||||
|
MaxInterval: 1 * time.Nanosecond,
|
||||||
|
// Never stop retrying.
|
||||||
|
MaxElapsedTime: 0,
|
||||||
|
}.RequestFunc(ev)
|
||||||
|
ctx := context.Background()
|
||||||
|
assert.NoError(t, reqFunc(ctx, func(context.Context) error {
|
||||||
|
return nil
|
||||||
|
}))
|
||||||
|
assert.ErrorIs(t, reqFunc(ctx, func(context.Context) error {
|
||||||
|
return assert.AnError
|
||||||
|
}), assert.AnError)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestThrottledRetry(t *testing.T) {
|
||||||
|
// Ensure the throttle delay is used by making longer than backoff delay.
|
||||||
|
throttleDelay, backoffDelay := time.Second, time.Nanosecond
|
||||||
|
|
||||||
|
ev := func(error) (bool, time.Duration) {
|
||||||
|
// Retry everything with a throttle delay.
|
||||||
|
return true, throttleDelay
|
||||||
|
}
|
||||||
|
|
||||||
|
reqFunc := Config{
|
||||||
|
Enabled: true,
|
||||||
|
InitialInterval: backoffDelay,
|
||||||
|
MaxInterval: backoffDelay,
|
||||||
|
// Never stop retrying.
|
||||||
|
MaxElapsedTime: 0,
|
||||||
|
}.RequestFunc(ev)
|
||||||
|
|
||||||
|
origWait := waitFunc
|
||||||
|
var done bool
|
||||||
|
waitFunc = func(_ context.Context, delay time.Duration) error {
|
||||||
|
assert.Equal(t, throttleDelay, delay, "retry not throttled")
|
||||||
|
// Try twice to ensure call is attempted again after delay.
|
||||||
|
if done {
|
||||||
|
return assert.AnError
|
||||||
|
}
|
||||||
|
done = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
defer func() { waitFunc = origWait }()
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
assert.ErrorIs(t, reqFunc(ctx, func(context.Context) error {
|
||||||
|
return errors.New("not this error")
|
||||||
|
}), assert.AnError)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBackoffRetry(t *testing.T) {
|
||||||
|
ev := func(error) (bool, time.Duration) { return true, 0 }
|
||||||
|
|
||||||
|
delay := time.Nanosecond
|
||||||
|
reqFunc := Config{
|
||||||
|
Enabled: true,
|
||||||
|
InitialInterval: delay,
|
||||||
|
MaxInterval: delay,
|
||||||
|
// Never stop retrying.
|
||||||
|
MaxElapsedTime: 0,
|
||||||
|
}.RequestFunc(ev)
|
||||||
|
|
||||||
|
origWait := waitFunc
|
||||||
|
var done bool
|
||||||
|
waitFunc = func(_ context.Context, d time.Duration) error {
|
||||||
|
delta := math.Ceil(float64(delay) * backoff.DefaultRandomizationFactor)
|
||||||
|
assert.InDelta(t, delay, d, delta, "retry not backoffed")
|
||||||
|
// Try twice to ensure call is attempted again after delay.
|
||||||
|
if done {
|
||||||
|
return assert.AnError
|
||||||
|
}
|
||||||
|
done = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { waitFunc = origWait })
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
assert.ErrorIs(t, reqFunc(ctx, func(context.Context) error {
|
||||||
|
return errors.New("not this error")
|
||||||
|
}), assert.AnError)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBackoffRetryCanceledContext(t *testing.T) {
|
||||||
|
ev := func(error) (bool, time.Duration) { return true, 0 }
|
||||||
|
|
||||||
|
delay := time.Millisecond
|
||||||
|
reqFunc := Config{
|
||||||
|
Enabled: true,
|
||||||
|
InitialInterval: delay,
|
||||||
|
MaxInterval: delay,
|
||||||
|
// Never stop retrying.
|
||||||
|
MaxElapsedTime: 10 * time.Millisecond,
|
||||||
|
}.RequestFunc(ev)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
count := 0
|
||||||
|
cancel()
|
||||||
|
err := reqFunc(ctx, func(context.Context) error {
|
||||||
|
count++
|
||||||
|
return assert.AnError
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.ErrorIs(t, err, context.Canceled)
|
||||||
|
assert.Contains(t, err.Error(), assert.AnError.Error())
|
||||||
|
assert.Equal(t, 1, count)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestThrottledRetryGreaterThanMaxElapsedTime(t *testing.T) {
|
||||||
|
// Ensure the throttle delay is used by making longer than backoff delay.
|
||||||
|
tDelay, bDelay := time.Hour, time.Nanosecond
|
||||||
|
ev := func(error) (bool, time.Duration) { return true, tDelay }
|
||||||
|
reqFunc := Config{
|
||||||
|
Enabled: true,
|
||||||
|
InitialInterval: bDelay,
|
||||||
|
MaxInterval: bDelay,
|
||||||
|
MaxElapsedTime: tDelay - (time.Nanosecond),
|
||||||
|
}.RequestFunc(ev)
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
assert.Contains(t, reqFunc(ctx, func(context.Context) error {
|
||||||
|
return assert.AnError
|
||||||
|
}).Error(), "max retry time would elapse: ")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMaxElapsedTime(t *testing.T) {
|
||||||
|
ev := func(error) (bool, time.Duration) { return true, 0 }
|
||||||
|
delay := time.Nanosecond
|
||||||
|
reqFunc := Config{
|
||||||
|
Enabled: true,
|
||||||
|
// InitialInterval > MaxElapsedTime means immediate return.
|
||||||
|
InitialInterval: 2 * delay,
|
||||||
|
MaxElapsedTime: delay,
|
||||||
|
}.RequestFunc(ev)
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
assert.Contains(t, reqFunc(ctx, func(context.Context) error {
|
||||||
|
return assert.AnError
|
||||||
|
}).Error(), "max retry time elapsed: ")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRetryNotEnabled(t *testing.T) {
|
||||||
|
ev := func(error) (bool, time.Duration) {
|
||||||
|
t.Error("evaluated retry when not enabled")
|
||||||
|
return false, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
reqFunc := Config{}.RequestFunc(ev)
|
||||||
|
ctx := context.Background()
|
||||||
|
assert.NoError(t, reqFunc(ctx, func(context.Context) error {
|
||||||
|
return nil
|
||||||
|
}))
|
||||||
|
assert.ErrorIs(t, reqFunc(ctx, func(context.Context) error {
|
||||||
|
return assert.AnError
|
||||||
|
}), assert.AnError)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRetryConcurrentSafe(t *testing.T) {
|
||||||
|
ev := func(error) (bool, time.Duration) { return true, 0 }
|
||||||
|
reqFunc := Config{
|
||||||
|
Enabled: true,
|
||||||
|
}.RequestFunc(ev)
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
for i := 1; i < 5; i++ {
|
||||||
|
wg.Add(1)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
var done bool
|
||||||
|
assert.NoError(t, reqFunc(ctx, func(context.Context) error {
|
||||||
|
if !done {
|
||||||
|
done = true
|
||||||
|
return assert.AnError
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}))
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
}
|
@ -11,6 +11,7 @@ require (
|
|||||||
github.com/wadey/gocovmerge v0.0.0-20160331181800-b5bfa59ec0ad
|
github.com/wadey/gocovmerge v0.0.0-20160331181800-b5bfa59ec0ad
|
||||||
go.opentelemetry.io/build-tools/crosslink v0.10.0
|
go.opentelemetry.io/build-tools/crosslink v0.10.0
|
||||||
go.opentelemetry.io/build-tools/dbotconf v0.10.0
|
go.opentelemetry.io/build-tools/dbotconf v0.10.0
|
||||||
|
go.opentelemetry.io/build-tools/gotmpl v0.10.0
|
||||||
go.opentelemetry.io/build-tools/multimod v0.10.0
|
go.opentelemetry.io/build-tools/multimod v0.10.0
|
||||||
go.opentelemetry.io/build-tools/semconvgen v0.10.0
|
go.opentelemetry.io/build-tools/semconvgen v0.10.0
|
||||||
golang.org/x/tools v0.11.0
|
golang.org/x/tools v0.11.0
|
||||||
|
@ -628,6 +628,8 @@ go.opentelemetry.io/build-tools/crosslink v0.10.0 h1:lyXOJP2z3G/o+e0w00q3vkzFZw4
|
|||||||
go.opentelemetry.io/build-tools/crosslink v0.10.0/go.mod h1:B//a0+OAU8kjgLLJnxjDNnlGv8CJt9pF/BaGc0nChvU=
|
go.opentelemetry.io/build-tools/crosslink v0.10.0/go.mod h1:B//a0+OAU8kjgLLJnxjDNnlGv8CJt9pF/BaGc0nChvU=
|
||||||
go.opentelemetry.io/build-tools/dbotconf v0.10.0 h1:BKk+Q2BoHqcA9lbrbVqYeQVHPJJb+jHePoO4TIyCy3Y=
|
go.opentelemetry.io/build-tools/dbotconf v0.10.0 h1:BKk+Q2BoHqcA9lbrbVqYeQVHPJJb+jHePoO4TIyCy3Y=
|
||||||
go.opentelemetry.io/build-tools/dbotconf v0.10.0/go.mod h1:SOyfUbctj4X9QakCfGy/eTlYlFo/HTNtyjJs7hHP9gE=
|
go.opentelemetry.io/build-tools/dbotconf v0.10.0/go.mod h1:SOyfUbctj4X9QakCfGy/eTlYlFo/HTNtyjJs7hHP9gE=
|
||||||
|
go.opentelemetry.io/build-tools/gotmpl v0.10.0 h1:pytcJ20iHs87Y/gMEb3pDgQuZ9g+wBgYYlexzUSJLV4=
|
||||||
|
go.opentelemetry.io/build-tools/gotmpl v0.10.0/go.mod h1:FzweYUfAJC1i5ATrtFI4KJggnO9QQGPdSVKWA8RHjdE=
|
||||||
go.opentelemetry.io/build-tools/multimod v0.10.0 h1:jYlpTXAJCZqjzYZdEdwdDIX90qnqQvZkgaPkMt9r6BA=
|
go.opentelemetry.io/build-tools/multimod v0.10.0 h1:jYlpTXAJCZqjzYZdEdwdDIX90qnqQvZkgaPkMt9r6BA=
|
||||||
go.opentelemetry.io/build-tools/multimod v0.10.0/go.mod h1:jn2a5TTq9wSR9ragaruZ2ilqOV+VerkNtNPTqwtIMyM=
|
go.opentelemetry.io/build-tools/multimod v0.10.0/go.mod h1:jn2a5TTq9wSR9ragaruZ2ilqOV+VerkNtNPTqwtIMyM=
|
||||||
go.opentelemetry.io/build-tools/semconvgen v0.10.0 h1:enthiZ2ucgEh6GdSf2FjPJh+tW7lWTKzLPS+oxxWqDY=
|
go.opentelemetry.io/build-tools/semconvgen v0.10.0 h1:enthiZ2ucgEh6GdSf2FjPJh+tW7lWTKzLPS+oxxWqDY=
|
||||||
|
@ -26,6 +26,7 @@ import (
|
|||||||
_ "github.com/wadey/gocovmerge"
|
_ "github.com/wadey/gocovmerge"
|
||||||
_ "go.opentelemetry.io/build-tools/crosslink"
|
_ "go.opentelemetry.io/build-tools/crosslink"
|
||||||
_ "go.opentelemetry.io/build-tools/dbotconf"
|
_ "go.opentelemetry.io/build-tools/dbotconf"
|
||||||
|
_ "go.opentelemetry.io/build-tools/gotmpl"
|
||||||
_ "go.opentelemetry.io/build-tools/multimod"
|
_ "go.opentelemetry.io/build-tools/multimod"
|
||||||
_ "go.opentelemetry.io/build-tools/semconvgen"
|
_ "go.opentelemetry.io/build-tools/semconvgen"
|
||||||
_ "golang.org/x/tools/cmd/stringer"
|
_ "golang.org/x/tools/cmd/stringer"
|
||||||
|
Loading…
Reference in New Issue
Block a user