Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix per-signal endpoint parsing in OTLP exporters #2433

Merged
merged 7 commits into from Dec 9, 2021
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Expand Up @@ -22,6 +22,12 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
- The `"go.opentelemetry.io/otel/exporter/otel/otlpmetric/otlpmetricgrpc".Client` now uses the underlying gRPC `ClientConn` to handle name resolution, TCP connection establishment (with retries and backoff) and TLS handshakes, and handling errors on established connections by re-resolving the name and reconnecting. (#2425)
- The `"go.opentelemetry.io/otel/exporter/otel/otlpmetric/otlpmetricgrpc".RetrySettings` type is renamed to `RetryConfig`. (#2425)

### Fixed

- The `go.opentelemetry.io/otel/exporter/otel/*` exporters are updated to handle per-signal and universal endpoints according to the OpenTelemetry specification.
Any per-signal endpoint set via an `OTEL_EXPORTER_OTLP_<signal>_ENDPOINT` environment variable is now used without modification of the path.
When `OTEL_EXPORTER_OTLP_ENDPOINT` is set, if it contains a path, that path is used as a base path which per-signal paths are appended to. (#2338 #2433)
MrAlias marked this conversation as resolved.
Show resolved Hide resolved

### Deprecated

- Deprecated the `"go.opentelemetry.io/otel/exporter/otel/otlpmetric/otlpmetrichttp".WithMaxAttempts` `Option`, use the new `WithRetry` `Option` instead. (#2425)
Expand Down
79 changes: 55 additions & 24 deletions exporters/otlp/otlpmetric/internal/otlpconfig/envconfig.go
Expand Up @@ -20,16 +20,14 @@ import (
"io/ioutil"
"net/url"
"os"
"regexp"
"path"
"strconv"
"strings"
"time"

"go.opentelemetry.io/otel"
)

var httpSchemeRegexp = regexp.MustCompile(`(?i)^http://|https://`)

func ApplyGRPCEnvConfigs(cfg *Config) {
e := EnvOptionsReader{
GetEnv: os.Getenv,
Expand Down Expand Up @@ -71,23 +69,55 @@ func (e *EnvOptionsReader) GetOptionsFromEnv() []GenericOption {
var opts []GenericOption

// Endpoint
if v, ok := e.getEnvValue("ENDPOINT"); ok {
if isInsecureEndpoint(v) {
opts = append(opts, WithInsecure())
} else {
opts = append(opts, WithSecure())
}

opts = append(opts, WithEndpoint(trimSchema(v)))
}
if v, ok := e.getEnvValue("METRICS_ENDPOINT"); ok {
if isInsecureEndpoint(v) {
opts = append(opts, WithInsecure())
} else {
opts = append(opts, WithSecure())
u, err := url.Parse(v)
// Ignore invalid values.
if err == nil {
// This is used to set the scheme for OTLP/HTTP.
if insecureSchema(u.Scheme) {
opts = append(opts, WithInsecure())
} else {
opts = append(opts, WithSecure())
}
opts = append(opts, newSplitOption(func(cfg *Config) {
cfg.Metrics.Endpoint = u.Host
// For endpoint URLs for OTLP/HTTP per-signal variables, the
// URL MUST be used as-is without any modification. The only
// exception is that if an URL contains no path part, the root
// path / MUST be used.
path := u.Path
if path == "" {
path = "/"
}
cfg.Metrics.URLPath = path
}, func(cfg *Config) {
// For OTLP/gRPC endpoints, this is the target to which the
// exporter is going to send telemetry.
cfg.Metrics.Endpoint = path.Join(u.Host, u.Path)
}))
}
} else if v, ok = e.getEnvValue("ENDPOINT"); ok {
u, err := url.Parse(v)
// Ignore invalid values.
if err == nil {
// This is used to set the scheme for OTLP/HTTP.
if insecureSchema(u.Scheme) {
opts = append(opts, WithInsecure())
} else {
opts = append(opts, WithSecure())
}
opts = append(opts, newSplitOption(func(cfg *Config) {
cfg.Metrics.Endpoint = u.Host
// For OTLP/HTTP endpoint URLs without a per-signal
// configuration, the passed endpoint is used as a base URL
// and the signals are sent to these paths relative to that.
cfg.Metrics.URLPath = path.Join(u.Path, DefaultMetricsPath)
}, func(cfg *Config) {
// For OTLP/gRPC endpoints, this is the target to which the
// exporter is going to send telemetry.
cfg.Metrics.Endpoint = path.Join(u.Host, u.Path)
}))
}

opts = append(opts, WithEndpoint(trimSchema(v)))
}

// Certificate File
Expand Down Expand Up @@ -137,12 +167,13 @@ func (e *EnvOptionsReader) GetOptionsFromEnv() []GenericOption {
return opts
}

func isInsecureEndpoint(endpoint string) bool {
return strings.HasPrefix(strings.ToLower(endpoint), "http://") || strings.HasPrefix(strings.ToLower(endpoint), "unix://")
}

func trimSchema(endpoint string) string {
return httpSchemeRegexp.ReplaceAllString(endpoint, "")
func insecureSchema(schema string) bool {
switch strings.ToLower(schema) {
case "http", "unix":
return true
default:
return false
}
}

// getEnvValue gets an OTLP environment variable value of the specified key using the GetEnv function.
Expand Down
48 changes: 18 additions & 30 deletions exporters/otlp/otlpmetric/internal/otlpconfig/options_test.go
Expand Up @@ -96,20 +96,30 @@ func TestConfigs(t *testing.T) {
{
name: "Test Environment Endpoint",
env: map[string]string{
"OTEL_EXPORTER_OTLP_ENDPOINT": "env_endpoint",
"OTEL_EXPORTER_OTLP_ENDPOINT": "https://env.endpoint/prefix",
},
asserts: func(t *testing.T, c *otlpconfig.Config, grpcOption bool) {
assert.Equal(t, "env_endpoint", c.Metrics.Endpoint)
assert.False(t, c.Metrics.Insecure)
if grpcOption {
assert.Equal(t, "env.endpoint/prefix", c.Metrics.Endpoint)
} else {
assert.Equal(t, "env.endpoint", c.Metrics.Endpoint)
assert.Equal(t, "/prefix/v1/metrics", c.Metrics.URLPath)
}
},
},
{
name: "Test Environment Signal Specific Endpoint",
env: map[string]string{
"OTEL_EXPORTER_OTLP_ENDPOINT": "overrode_by_signal_specific",
"OTEL_EXPORTER_OTLP_METRICS_ENDPOINT": "env_metrics_endpoint",
"OTEL_EXPORTER_OTLP_ENDPOINT": "https://overrode.by.signal.specific/env/var",
"OTEL_EXPORTER_OTLP_METRICS_ENDPOINT": "http://env.metrics.endpoint",
},
asserts: func(t *testing.T, c *otlpconfig.Config, grpcOption bool) {
assert.Equal(t, "env_metrics_endpoint", c.Metrics.Endpoint)
assert.True(t, c.Metrics.Insecure)
assert.Equal(t, "env.metrics.endpoint", c.Metrics.Endpoint)
if !grpcOption {
assert.Equal(t, "/", c.Metrics.URLPath)
}
},
},
{
Expand Down Expand Up @@ -154,37 +164,15 @@ func TestConfigs(t *testing.T) {
assert.Equal(t, false, c.Metrics.Insecure)
},
},
{
name: "Test Environment Signal Specific Endpoint",
env: map[string]string{
"OTEL_EXPORTER_OTLP_ENDPOINT": "http://overrode_by_signal_specific",
"OTEL_EXPORTER_OTLP_METRICS_ENDPOINT": "https://env_metrics_endpoint",
},
asserts: func(t *testing.T, c *otlpconfig.Config, grpcOption bool) {
assert.Equal(t, "env_metrics_endpoint", c.Metrics.Endpoint)
assert.Equal(t, false, c.Metrics.Insecure)
},
},
{
name: "Test Environment Signal Specific Endpoint #2",
env: map[string]string{
"OTEL_EXPORTER_OTLP_ENDPOINT": "http://overrode_by_signal_specific",
"OTEL_EXPORTER_OTLP_METRICS_ENDPOINT": "env_metrics_endpoint",
},
asserts: func(t *testing.T, c *otlpconfig.Config, grpcOption bool) {
assert.Equal(t, "env_metrics_endpoint", c.Metrics.Endpoint)
assert.Equal(t, false, c.Metrics.Insecure)
},
},
{
name: "Test Environment Signal Specific Endpoint with uppercase scheme",
env: map[string]string{
"OTEL_EXPORTER_OTLP_ENDPOINT": "HTTP://overrode_by_signal_specific",
"OTEL_EXPORTER_OTLP_METRICS_ENDPOINT": "env_metrics_endpoint",
"OTEL_EXPORTER_OTLP_ENDPOINT": "HTTPS://overrode_by_signal_specific",
"OTEL_EXPORTER_OTLP_METRICS_ENDPOINT": "HtTp://env_metrics_endpoint",
},
asserts: func(t *testing.T, c *otlpconfig.Config, grpcOption bool) {
assert.Equal(t, "env_metrics_endpoint", c.Metrics.Endpoint)
assert.Equal(t, false, c.Metrics.Insecure)
assert.Equal(t, true, c.Metrics.Insecure)
},
},

Expand Down
5 changes: 3 additions & 2 deletions exporters/otlp/otlpmetric/otlpmetrichttp/client.go
Expand Up @@ -23,6 +23,7 @@ import (
"io/ioutil"
"net"
"net/http"
"net/url"
"path"
"strconv"
"strings"
Expand Down Expand Up @@ -199,8 +200,8 @@ func (d *client) UploadMetrics(ctx context.Context, protoMetrics []*metricpb.Res
}

func (d *client) newRequest(body []byte) (request, error) {
address := fmt.Sprintf("%s://%s%s", d.getScheme(), d.cfg.Endpoint, d.cfg.URLPath)
r, err := http.NewRequest(http.MethodPost, address, nil)
u := url.URL{Scheme: d.getScheme(), Host: d.cfg.Endpoint, Path: d.cfg.URLPath}
r, err := http.NewRequest(http.MethodPost, u.String(), nil)
if err != nil {
return request{Request: r}, err
}
Expand Down
79 changes: 55 additions & 24 deletions exporters/otlp/otlptrace/internal/otlpconfig/envconfig.go
Expand Up @@ -20,16 +20,14 @@ import (
"io/ioutil"
"net/url"
"os"
"regexp"
"path"
"strconv"
"strings"
"time"

"go.opentelemetry.io/otel"
)

var httpSchemeRegexp = regexp.MustCompile(`(?i)^(http://|https://)`)

func ApplyGRPCEnvConfigs(cfg *Config) {
e := EnvOptionsReader{
GetEnv: os.Getenv,
Expand Down Expand Up @@ -71,23 +69,55 @@ func (e *EnvOptionsReader) GetOptionsFromEnv() []GenericOption {
var opts []GenericOption

// Endpoint
if v, ok := e.getEnvValue("ENDPOINT"); ok {
if isInsecureEndpoint(v) {
opts = append(opts, WithInsecure())
} else {
opts = append(opts, WithSecure())
}

opts = append(opts, WithEndpoint(trimSchema(v)))
}
if v, ok := e.getEnvValue("TRACES_ENDPOINT"); ok {
if isInsecureEndpoint(v) {
opts = append(opts, WithInsecure())
} else {
opts = append(opts, WithSecure())
u, err := url.Parse(v)
// Ignore invalid values.
if err == nil {
// This is used to set the scheme for OTLP/HTTP.
if insecureSchema(u.Scheme) {
opts = append(opts, WithInsecure())
} else {
opts = append(opts, WithSecure())
}
opts = append(opts, newSplitOption(func(cfg *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
}, func(cfg *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)
}))
}
} else if v, ok = e.getEnvValue("ENDPOINT"); ok {
u, err := url.Parse(v)
// Ignore invalid values.
if err == nil {
// This is used to set the scheme for OTLP/HTTP.
if insecureSchema(u.Scheme) {
opts = append(opts, WithInsecure())
} else {
opts = append(opts, WithSecure())
}
opts = append(opts, newSplitOption(func(cfg *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)
}, func(cfg *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)
}))
}

opts = append(opts, WithEndpoint(trimSchema(v)))
}

// Certificate File
Expand Down Expand Up @@ -136,12 +166,13 @@ func (e *EnvOptionsReader) GetOptionsFromEnv() []GenericOption {
return opts
}

func isInsecureEndpoint(endpoint string) bool {
return strings.HasPrefix(strings.ToLower(endpoint), "http://") || strings.HasPrefix(strings.ToLower(endpoint), "unix://")
}

func trimSchema(endpoint string) string {
return httpSchemeRegexp.ReplaceAllString(endpoint, "")
func insecureSchema(schema string) bool {
switch strings.ToLower(schema) {
case "http", "unix":
return true
default:
return false
}
}

// getEnvValue gets an OTLP environment variable value of the specified key using the GetEnv function.
Expand Down
44 changes: 16 additions & 28 deletions exporters/otlp/otlptrace/internal/otlpconfig/options_test.go
Expand Up @@ -96,20 +96,30 @@ func TestConfigs(t *testing.T) {
{
name: "Test Environment Endpoint",
env: map[string]string{
"OTEL_EXPORTER_OTLP_ENDPOINT": "env_endpoint",
"OTEL_EXPORTER_OTLP_ENDPOINT": "https://env.endpoint/prefix",
},
asserts: func(t *testing.T, c *otlpconfig.Config, grpcOption bool) {
assert.Equal(t, "env_endpoint", c.Traces.Endpoint)
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": "overrode_by_signal_specific",
"OTEL_EXPORTER_OTLP_TRACES_ENDPOINT": "env_traces_endpoint",
"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 *otlpconfig.Config, grpcOption bool) {
assert.Equal(t, "env_traces_endpoint", c.Traces.Endpoint)
assert.True(t, c.Traces.Insecure)
assert.Equal(t, "env.traces.endpoint", c.Traces.Endpoint)
if !grpcOption {
assert.Equal(t, "/", c.Traces.URLPath)
}
},
},
{
Expand Down Expand Up @@ -154,32 +164,10 @@ func TestConfigs(t *testing.T) {
assert.Equal(t, false, c.Traces.Insecure)
},
},
{
name: "Test Environment Signal Specific Endpoint",
env: map[string]string{
"OTEL_EXPORTER_OTLP_ENDPOINT": "http://overrode_by_signal_specific",
"OTEL_EXPORTER_OTLP_TRACES_ENDPOINT": "http://env_traces_endpoint",
},
asserts: func(t *testing.T, c *otlpconfig.Config, grpcOption bool) {
assert.Equal(t, "env_traces_endpoint", c.Traces.Endpoint)
assert.Equal(t, true, c.Traces.Insecure)
},
},
{
name: "Test Environment Signal Specific Endpoint #2",
env: map[string]string{
"OTEL_EXPORTER_OTLP_ENDPOINT": "http://overrode_by_signal_specific",
"OTEL_EXPORTER_OTLP_TRACES_ENDPOINT": "http://env_traces_endpoint",
},
asserts: func(t *testing.T, c *otlpconfig.Config, grpcOption bool) {
assert.Equal(t, "env_traces_endpoint", c.Traces.Endpoint)
assert.Equal(t, true, c.Traces.Insecure)
},
},
{
name: "Test Environment Signal Specific Endpoint with uppercase scheme",
env: map[string]string{
"OTEL_EXPORTER_OTLP_ENDPOINT": "HTTP://overrode_by_signal_specific",
"OTEL_EXPORTER_OTLP_ENDPOINT": "HTTPS://overrode_by_signal_specific",
"OTEL_EXPORTER_OTLP_TRACES_ENDPOINT": "HtTp://env_traces_endpoint",
},
asserts: func(t *testing.T, c *otlpconfig.Config, grpcOption bool) {
Expand Down