You've already forked opentelemetry-go
mirror of
https://github.com/open-telemetry/opentelemetry-go.git
synced 2025-07-15 01:04:25 +02:00
Refactor OTLP exporter env config to be shared across all exporters (#2608)
* setup global envconfig package for otlp exporter * use envconfig in otlpmetrics package * fix lint * add changelog entry * Update exporters/otlp/internal/envconfig/envconfig.go Co-authored-by: Chester Cheung <cheung.zhy.csu@gmail.com> * fix lint Co-authored-by: Chester Cheung <cheung.zhy.csu@gmail.com> Co-authored-by: Anthony Mirabella <a9@aneurysm9.com>
This commit is contained in:
@ -31,6 +31,7 @@ This update is a breaking change of the unstable Metrics API. Code instrumented
|
|||||||
- The metrics API has been significantly changed. (#2587)
|
- The metrics API has been significantly changed. (#2587)
|
||||||
- Unify path cleaning functionally in the `otlpmetric` and `otlptrace` config. (#2639)
|
- Unify path cleaning functionally in the `otlpmetric` and `otlptrace` config. (#2639)
|
||||||
- Change the debug message from the `sdk/trace.BatchSpanProcessor` to reflect the count is cumulative. (#2640)
|
- Change the debug message from the `sdk/trace.BatchSpanProcessor` to reflect the count is cumulative. (#2640)
|
||||||
|
- Introduce new internal envconfig package for OTLP exporters (#2608)
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
148
exporters/otlp/internal/envconfig/envconfig.go
Normal file
148
exporters/otlp/internal/envconfig/envconfig.go
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
// 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/internal/envconfig"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
if d, err := strconv.Atoi(v); err == nil {
|
||||||
|
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 {
|
||||||
|
if u, err := url.Parse(v); err == nil {
|
||||||
|
fn(u)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithTLSConfig retrieves the specified config and passes it to ConfigFn as a crypto/tls.Config
|
||||||
|
func WithTLSConfig(n string, fn func(*tls.Config)) func(e *EnvOptionsReader) {
|
||||||
|
return func(e *EnvOptionsReader) {
|
||||||
|
if v, ok := e.GetEnvValue(n); ok {
|
||||||
|
if b, err := e.ReadFile(v); err == nil {
|
||||||
|
if c, err := createTLSConfig(b); err == nil {
|
||||||
|
fn(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
nameValue := strings.SplitN(header, "=", 2)
|
||||||
|
if len(nameValue) < 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
name, err := url.QueryUnescape(nameValue[0])
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
trimmedName := strings.TrimSpace(name)
|
||||||
|
value, err := url.QueryUnescape(nameValue[1])
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
trimmedValue := strings.TrimSpace(value)
|
||||||
|
|
||||||
|
headers[trimmedName] = trimmedValue
|
||||||
|
}
|
||||||
|
|
||||||
|
return headers
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
345
exporters/otlp/internal/envconfig/envconfig_test.go
Normal file
345
exporters/otlp/internal/envconfig/envconfig_test.go
Normal file
@ -0,0 +1,345 @@
|
|||||||
|
// 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/internal/envconfig"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"net/url"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
const WeakCertificate = `
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIBhzCCASygAwIBAgIRANHpHgAWeTnLZpTSxCKs0ggwCgYIKoZIzj0EAwIwEjEQ
|
||||||
|
MA4GA1UEChMHb3RlbC1nbzAeFw0yMTA0MDExMzU5MDNaFw0yMTA0MDExNDU5MDNa
|
||||||
|
MBIxEDAOBgNVBAoTB290ZWwtZ28wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAS9
|
||||||
|
nWSkmPCxShxnp43F+PrOtbGV7sNfkbQ/kxzi9Ego0ZJdiXxkmv/C05QFddCW7Y0Z
|
||||||
|
sJCLHGogQsYnWJBXUZOVo2MwYTAOBgNVHQ8BAf8EBAMCB4AwEwYDVR0lBAwwCgYI
|
||||||
|
KwYBBQUHAwEwDAYDVR0TAQH/BAIwADAsBgNVHREEJTAjgglsb2NhbGhvc3SHEAAA
|
||||||
|
AAAAAAAAAAAAAAAAAAGHBH8AAAEwCgYIKoZIzj0EAwIDSQAwRgIhANwZVVKvfvQ/
|
||||||
|
1HXsTvgH+xTQswOwSSKYJ1cVHQhqK7ZbAiEAus8NxpTRnp5DiTMuyVmhVNPB+bVH
|
||||||
|
Lhnm4N/QDk5rek0=
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
`
|
||||||
|
|
||||||
|
type testOption struct {
|
||||||
|
TestString string
|
||||||
|
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 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) {
|
||||||
|
tlsCert, err := createTLSConfig([]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(
|
||||||
|
WithTLSConfig("CERTIFICATE", func(v *tls.Config) {
|
||||||
|
option = testOption{TestTLS: v}
|
||||||
|
}))
|
||||||
|
assert.Equal(t, tlsCert.RootCAs.Subjects(), option.TestTLS.RootCAs.Subjects())
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -16,66 +16,58 @@ package otlpconfig // import "go.opentelemetry.io/otel/exporters/otlp/otlpmetric
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go.opentelemetry.io/otel"
|
"go.opentelemetry.io/otel/exporters/otlp/internal/envconfig"
|
||||||
)
|
)
|
||||||
|
|
||||||
var DefaultEnvOptionsReader = EnvOptionsReader{
|
// DefaultEnvOptionsReader is the default environments reader
|
||||||
GetEnv: os.Getenv,
|
var DefaultEnvOptionsReader = envconfig.EnvOptionsReader{
|
||||||
ReadFile: ioutil.ReadFile,
|
GetEnv: os.Getenv,
|
||||||
|
ReadFile: ioutil.ReadFile,
|
||||||
|
Namespace: "OTEL_EXPORTER_OTLP",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ApplyGRPCEnvConfigs applies the env configurations for gRPC
|
||||||
func ApplyGRPCEnvConfigs(cfg Config) Config {
|
func ApplyGRPCEnvConfigs(cfg Config) Config {
|
||||||
return DefaultEnvOptionsReader.ApplyGRPCEnvConfigs(cfg)
|
opts := getOptionsFromEnv()
|
||||||
}
|
|
||||||
|
|
||||||
func ApplyHTTPEnvConfigs(cfg Config) Config {
|
|
||||||
return DefaultEnvOptionsReader.ApplyHTTPEnvConfigs(cfg)
|
|
||||||
}
|
|
||||||
|
|
||||||
type EnvOptionsReader struct {
|
|
||||||
GetEnv func(string) string
|
|
||||||
ReadFile func(filename string) ([]byte, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *EnvOptionsReader) ApplyHTTPEnvConfigs(cfg Config) Config {
|
|
||||||
opts := e.GetOptionsFromEnv()
|
|
||||||
for _, opt := range opts {
|
|
||||||
cfg = opt.ApplyHTTPOption(cfg)
|
|
||||||
}
|
|
||||||
return cfg
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *EnvOptionsReader) ApplyGRPCEnvConfigs(cfg Config) Config {
|
|
||||||
opts := e.GetOptionsFromEnv()
|
|
||||||
for _, opt := range opts {
|
for _, opt := range opts {
|
||||||
cfg = opt.ApplyGRPCOption(cfg)
|
cfg = opt.ApplyGRPCOption(cfg)
|
||||||
}
|
}
|
||||||
return cfg
|
return cfg
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *EnvOptionsReader) GetOptionsFromEnv() []GenericOption {
|
// ApplyHTTPEnvConfigs applies the env configurations for HTTP
|
||||||
var opts []GenericOption
|
func ApplyHTTPEnvConfigs(cfg Config) Config {
|
||||||
|
opts := getOptionsFromEnv()
|
||||||
|
for _, opt := range opts {
|
||||||
|
cfg = opt.ApplyHTTPOption(cfg)
|
||||||
|
}
|
||||||
|
return cfg
|
||||||
|
}
|
||||||
|
|
||||||
// Endpoint
|
func getOptionsFromEnv() []GenericOption {
|
||||||
if v, ok := e.getEnvValue("METRICS_ENDPOINT"); ok {
|
opts := []GenericOption{}
|
||||||
u, err := url.Parse(v)
|
|
||||||
// Ignore invalid values.
|
DefaultEnvOptionsReader.Apply(
|
||||||
if err == nil {
|
envconfig.WithURL("ENDPOINT", func(u *url.URL) {
|
||||||
// This is used to set the scheme for OTLP/HTTP.
|
opts = append(opts, withEndpointScheme(u))
|
||||||
if insecureSchema(u.Scheme) {
|
opts = append(opts, newSplitOption(func(cfg Config) Config {
|
||||||
opts = append(opts, WithInsecure())
|
cfg.Metrics.Endpoint = u.Host
|
||||||
} else {
|
// For OTLP/HTTP endpoint URLs without a per-signal
|
||||||
opts = append(opts, WithSecure())
|
// 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)
|
||||||
|
return cfg
|
||||||
|
}, withEndpointForGRPC(u)))
|
||||||
|
}),
|
||||||
|
envconfig.WithURL("METRICS_ENDPOINT", func(u *url.URL) {
|
||||||
|
opts = append(opts, withEndpointScheme(u))
|
||||||
opts = append(opts, newSplitOption(func(cfg Config) Config {
|
opts = append(opts, newSplitOption(func(cfg Config) Config {
|
||||||
cfg.Metrics.Endpoint = u.Host
|
cfg.Metrics.Endpoint = u.Host
|
||||||
// For endpoint URLs for OTLP/HTTP per-signal variables, the
|
// For endpoint URLs for OTLP/HTTP per-signal variables, the
|
||||||
@ -88,141 +80,50 @@ func (e *EnvOptionsReader) GetOptionsFromEnv() []GenericOption {
|
|||||||
}
|
}
|
||||||
cfg.Metrics.URLPath = path
|
cfg.Metrics.URLPath = path
|
||||||
return cfg
|
return cfg
|
||||||
}, func(cfg Config) Config {
|
}, withEndpointForGRPC(u)))
|
||||||
// For OTLP/gRPC endpoints, this is the target to which the
|
}),
|
||||||
// exporter is going to send telemetry.
|
envconfig.WithTLSConfig("CERTIFICATE", func(c *tls.Config) { opts = append(opts, WithTLSClientConfig(c)) }),
|
||||||
cfg.Metrics.Endpoint = path.Join(u.Host, u.Path)
|
envconfig.WithTLSConfig("METRICS_CERTIFICATE", func(c *tls.Config) { opts = append(opts, WithTLSClientConfig(c)) }),
|
||||||
return cfg
|
envconfig.WithHeaders("HEADERS", func(h map[string]string) { opts = append(opts, WithHeaders(h)) }),
|
||||||
}))
|
envconfig.WithHeaders("METRICS_HEADERS", func(h map[string]string) { opts = append(opts, WithHeaders(h)) }),
|
||||||
}
|
WithEnvCompression("COMPRESSION", func(c Compression) { opts = append(opts, WithCompression(c)) }),
|
||||||
} else if v, ok = e.getEnvValue("ENDPOINT"); ok {
|
WithEnvCompression("METRICS_COMPRESSION", func(c Compression) { opts = append(opts, WithCompression(c)) }),
|
||||||
u, err := url.Parse(v)
|
envconfig.WithDuration("TIMEOUT", func(d time.Duration) { opts = append(opts, WithTimeout(d)) }),
|
||||||
// Ignore invalid values.
|
envconfig.WithDuration("METRICS_TIMEOUT", func(d time.Duration) { opts = append(opts, WithTimeout(d)) }),
|
||||||
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) 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)
|
|
||||||
return cfg
|
|
||||||
}, func(cfg Config) 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)
|
|
||||||
return cfg
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Certificate File
|
|
||||||
if path, ok := e.getEnvValue("CERTIFICATE"); ok {
|
|
||||||
if tls, err := e.readTLSConfig(path); err == nil {
|
|
||||||
opts = append(opts, WithTLSClientConfig(tls))
|
|
||||||
} else {
|
|
||||||
otel.Handle(fmt.Errorf("failed to configure otlp exporter certificate '%s': %w", path, err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if path, ok := e.getEnvValue("METRICS_CERTIFICATE"); ok {
|
|
||||||
if tls, err := e.readTLSConfig(path); err == nil {
|
|
||||||
opts = append(opts, WithTLSClientConfig(tls))
|
|
||||||
} else {
|
|
||||||
otel.Handle(fmt.Errorf("failed to configure otlp exporter certificate '%s': %w", path, err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Headers
|
|
||||||
if h, ok := e.getEnvValue("HEADERS"); ok {
|
|
||||||
opts = append(opts, WithHeaders(stringToHeader(h)))
|
|
||||||
}
|
|
||||||
if h, ok := e.getEnvValue("METRICS_HEADERS"); ok {
|
|
||||||
opts = append(opts, WithHeaders(stringToHeader(h)))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compression
|
|
||||||
if c, ok := e.getEnvValue("COMPRESSION"); ok {
|
|
||||||
opts = append(opts, WithCompression(stringToCompression(c)))
|
|
||||||
}
|
|
||||||
if c, ok := e.getEnvValue("METRICS_COMPRESSION"); ok {
|
|
||||||
opts = append(opts, WithCompression(stringToCompression(c)))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Timeout
|
|
||||||
if t, ok := e.getEnvValue("TIMEOUT"); ok {
|
|
||||||
if d, err := strconv.Atoi(t); err == nil {
|
|
||||||
opts = append(opts, WithTimeout(time.Duration(d)*time.Millisecond))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if t, ok := e.getEnvValue("METRICS_TIMEOUT"); ok {
|
|
||||||
if d, err := strconv.Atoi(t); err == nil {
|
|
||||||
opts = append(opts, WithTimeout(time.Duration(d)*time.Millisecond))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return opts
|
return opts
|
||||||
}
|
}
|
||||||
|
|
||||||
func insecureSchema(schema string) bool {
|
func withEndpointForGRPC(u *url.URL) func(cfg Config) Config {
|
||||||
switch strings.ToLower(schema) {
|
return func(cfg Config) 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)
|
||||||
|
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
|
||||||
|
switch v {
|
||||||
|
case "gzip":
|
||||||
|
cp = GzipCompression
|
||||||
|
}
|
||||||
|
|
||||||
|
fn(cp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func withEndpointScheme(u *url.URL) GenericOption {
|
||||||
|
switch strings.ToLower(u.Scheme) {
|
||||||
case "http", "unix":
|
case "http", "unix":
|
||||||
return true
|
return WithInsecure()
|
||||||
default:
|
default:
|
||||||
return false
|
return WithSecure()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// getEnvValue gets an OTLP environment variable value of the specified key using the GetEnv function.
|
|
||||||
// This function already prepends the OTLP prefix to all key lookup.
|
|
||||||
func (e *EnvOptionsReader) getEnvValue(key string) (string, bool) {
|
|
||||||
v := strings.TrimSpace(e.GetEnv(fmt.Sprintf("OTEL_EXPORTER_OTLP_%s", key)))
|
|
||||||
return v, v != ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *EnvOptionsReader) readTLSConfig(path string) (*tls.Config, error) {
|
|
||||||
b, err := e.ReadFile(path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return CreateTLSConfig(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
func stringToCompression(value string) Compression {
|
|
||||||
switch value {
|
|
||||||
case "gzip":
|
|
||||||
return GzipCompression
|
|
||||||
}
|
|
||||||
|
|
||||||
return NoCompression
|
|
||||||
}
|
|
||||||
|
|
||||||
func stringToHeader(value string) map[string]string {
|
|
||||||
headersPairs := strings.Split(value, ",")
|
|
||||||
headers := make(map[string]string)
|
|
||||||
|
|
||||||
for _, header := range headersPairs {
|
|
||||||
nameValue := strings.SplitN(header, "=", 2)
|
|
||||||
if len(nameValue) < 2 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
name, err := url.QueryUnescape(nameValue[0])
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
trimmedName := strings.TrimSpace(name)
|
|
||||||
value, err := url.QueryUnescape(nameValue[1])
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
trimmedValue := strings.TrimSpace(value)
|
|
||||||
|
|
||||||
headers[trimmedName] = trimmedValue
|
|
||||||
}
|
|
||||||
|
|
||||||
return headers
|
|
||||||
}
|
|
||||||
|
@ -13,63 +13,3 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
package otlpconfig
|
package otlpconfig
|
||||||
|
|
||||||
import (
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
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) {
|
|
||||||
if got := stringToHeader(tt.value); !reflect.DeepEqual(got, tt.want) {
|
|
||||||
t.Errorf("stringToHeader() = %v, want %v", got, tt.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -21,6 +21,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/otlpmetric/internal/otlpconfig"
|
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/internal/otlpconfig"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -383,9 +384,10 @@ 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 := otlpconfig.DefaultEnvOptionsReader
|
||||||
otlpconfig.DefaultEnvOptionsReader = otlpconfig.EnvOptionsReader{
|
otlpconfig.DefaultEnvOptionsReader = envconfig.EnvOptionsReader{
|
||||||
GetEnv: tt.env.getEnv,
|
GetEnv: tt.env.getEnv,
|
||||||
ReadFile: tt.fileReader.readFile,
|
ReadFile: tt.fileReader.readFile,
|
||||||
|
Namespace: "OTEL_EXPORTER_OTLP",
|
||||||
}
|
}
|
||||||
t.Cleanup(func() { otlpconfig.DefaultEnvOptionsReader = origEOR })
|
t.Cleanup(func() { otlpconfig.DefaultEnvOptionsReader = origEOR })
|
||||||
|
|
||||||
|
@ -16,66 +16,58 @@ package otlpconfig // import "go.opentelemetry.io/otel/exporters/otlp/otlptrace/
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go.opentelemetry.io/otel"
|
"go.opentelemetry.io/otel/exporters/otlp/internal/envconfig"
|
||||||
)
|
)
|
||||||
|
|
||||||
var DefaultEnvOptionsReader = EnvOptionsReader{
|
// DefaultEnvOptionsReader is the default environments reader
|
||||||
GetEnv: os.Getenv,
|
var DefaultEnvOptionsReader = envconfig.EnvOptionsReader{
|
||||||
ReadFile: ioutil.ReadFile,
|
GetEnv: os.Getenv,
|
||||||
|
ReadFile: ioutil.ReadFile,
|
||||||
|
Namespace: "OTEL_EXPORTER_OTLP",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ApplyGRPCEnvConfigs applies the env configurations for gRPC
|
||||||
func ApplyGRPCEnvConfigs(cfg Config) Config {
|
func ApplyGRPCEnvConfigs(cfg Config) Config {
|
||||||
return DefaultEnvOptionsReader.ApplyGRPCEnvConfigs(cfg)
|
opts := getOptionsFromEnv()
|
||||||
}
|
|
||||||
|
|
||||||
func ApplyHTTPEnvConfigs(cfg Config) Config {
|
|
||||||
return DefaultEnvOptionsReader.ApplyHTTPEnvConfigs(cfg)
|
|
||||||
}
|
|
||||||
|
|
||||||
type EnvOptionsReader struct {
|
|
||||||
GetEnv func(string) string
|
|
||||||
ReadFile func(filename string) ([]byte, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *EnvOptionsReader) ApplyHTTPEnvConfigs(cfg Config) Config {
|
|
||||||
opts := e.GetOptionsFromEnv()
|
|
||||||
for _, opt := range opts {
|
|
||||||
cfg = opt.ApplyHTTPOption(cfg)
|
|
||||||
}
|
|
||||||
return cfg
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *EnvOptionsReader) ApplyGRPCEnvConfigs(cfg Config) Config {
|
|
||||||
opts := e.GetOptionsFromEnv()
|
|
||||||
for _, opt := range opts {
|
for _, opt := range opts {
|
||||||
cfg = opt.ApplyGRPCOption(cfg)
|
cfg = opt.ApplyGRPCOption(cfg)
|
||||||
}
|
}
|
||||||
return cfg
|
return cfg
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *EnvOptionsReader) GetOptionsFromEnv() []GenericOption {
|
// ApplyHTTPEnvConfigs applies the env configurations for HTTP
|
||||||
var opts []GenericOption
|
func ApplyHTTPEnvConfigs(cfg Config) Config {
|
||||||
|
opts := getOptionsFromEnv()
|
||||||
|
for _, opt := range opts {
|
||||||
|
cfg = opt.ApplyHTTPOption(cfg)
|
||||||
|
}
|
||||||
|
return cfg
|
||||||
|
}
|
||||||
|
|
||||||
// Endpoint
|
func getOptionsFromEnv() []GenericOption {
|
||||||
if v, ok := e.getEnvValue("TRACES_ENDPOINT"); ok {
|
opts := []GenericOption{}
|
||||||
u, err := url.Parse(v)
|
|
||||||
// Ignore invalid values.
|
DefaultEnvOptionsReader.Apply(
|
||||||
if err == nil {
|
envconfig.WithURL("ENDPOINT", func(u *url.URL) {
|
||||||
// This is used to set the scheme for OTLP/HTTP.
|
opts = append(opts, withEndpointScheme(u))
|
||||||
if insecureSchema(u.Scheme) {
|
opts = append(opts, newSplitOption(func(cfg Config) Config {
|
||||||
opts = append(opts, WithInsecure())
|
cfg.Traces.Endpoint = u.Host
|
||||||
} else {
|
// For OTLP/HTTP endpoint URLs without a per-signal
|
||||||
opts = append(opts, WithSecure())
|
// 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 {
|
opts = append(opts, newSplitOption(func(cfg Config) Config {
|
||||||
cfg.Traces.Endpoint = u.Host
|
cfg.Traces.Endpoint = u.Host
|
||||||
// For endpoint URLs for OTLP/HTTP per-signal variables, the
|
// For endpoint URLs for OTLP/HTTP per-signal variables, the
|
||||||
@ -88,140 +80,50 @@ func (e *EnvOptionsReader) GetOptionsFromEnv() []GenericOption {
|
|||||||
}
|
}
|
||||||
cfg.Traces.URLPath = path
|
cfg.Traces.URLPath = path
|
||||||
return cfg
|
return cfg
|
||||||
}, func(cfg Config) Config {
|
}, withEndpointForGRPC(u)))
|
||||||
// For OTLP/gRPC endpoints, this is the target to which the
|
}),
|
||||||
// exporter is going to send telemetry.
|
envconfig.WithTLSConfig("CERTIFICATE", func(c *tls.Config) { opts = append(opts, WithTLSClientConfig(c)) }),
|
||||||
cfg.Traces.Endpoint = path.Join(u.Host, u.Path)
|
envconfig.WithTLSConfig("TRACES_CERTIFICATE", func(c *tls.Config) { opts = append(opts, WithTLSClientConfig(c)) }),
|
||||||
return cfg
|
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)) }),
|
||||||
} else if v, ok = e.getEnvValue("ENDPOINT"); ok {
|
WithEnvCompression("TRACES_COMPRESSION", func(c Compression) { opts = append(opts, WithCompression(c)) }),
|
||||||
u, err := url.Parse(v)
|
envconfig.WithDuration("TIMEOUT", func(d time.Duration) { opts = append(opts, WithTimeout(d)) }),
|
||||||
// Ignore invalid values.
|
envconfig.WithDuration("TRACES_TIMEOUT", func(d time.Duration) { opts = append(opts, WithTimeout(d)) }),
|
||||||
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) 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
|
|
||||||
}, 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
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Certificate File
|
|
||||||
if path, ok := e.getEnvValue("CERTIFICATE"); ok {
|
|
||||||
if tls, err := e.readTLSConfig(path); err == nil {
|
|
||||||
opts = append(opts, WithTLSClientConfig(tls))
|
|
||||||
} else {
|
|
||||||
otel.Handle(fmt.Errorf("failed to configure otlp exporter certificate '%s': %w", path, err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if path, ok := e.getEnvValue("TRACES_CERTIFICATE"); ok {
|
|
||||||
if tls, err := e.readTLSConfig(path); err == nil {
|
|
||||||
opts = append(opts, WithTLSClientConfig(tls))
|
|
||||||
} else {
|
|
||||||
otel.Handle(fmt.Errorf("failed to configure otlp traces exporter certificate '%s': %w", path, err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Headers
|
|
||||||
if h, ok := e.getEnvValue("HEADERS"); ok {
|
|
||||||
opts = append(opts, WithHeaders(stringToHeader(h)))
|
|
||||||
}
|
|
||||||
if h, ok := e.getEnvValue("TRACES_HEADERS"); ok {
|
|
||||||
opts = append(opts, WithHeaders(stringToHeader(h)))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compression
|
|
||||||
if c, ok := e.getEnvValue("COMPRESSION"); ok {
|
|
||||||
opts = append(opts, WithCompression(stringToCompression(c)))
|
|
||||||
}
|
|
||||||
if c, ok := e.getEnvValue("TRACES_COMPRESSION"); ok {
|
|
||||||
opts = append(opts, WithCompression(stringToCompression(c)))
|
|
||||||
}
|
|
||||||
// Timeout
|
|
||||||
if t, ok := e.getEnvValue("TIMEOUT"); ok {
|
|
||||||
if d, err := strconv.Atoi(t); err == nil {
|
|
||||||
opts = append(opts, WithTimeout(time.Duration(d)*time.Millisecond))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if t, ok := e.getEnvValue("TRACES_TIMEOUT"); ok {
|
|
||||||
if d, err := strconv.Atoi(t); err == nil {
|
|
||||||
opts = append(opts, WithTimeout(time.Duration(d)*time.Millisecond))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return opts
|
return opts
|
||||||
}
|
}
|
||||||
|
|
||||||
func insecureSchema(schema string) bool {
|
func withEndpointScheme(u *url.URL) GenericOption {
|
||||||
switch strings.ToLower(schema) {
|
switch strings.ToLower(u.Scheme) {
|
||||||
case "http", "unix":
|
case "http", "unix":
|
||||||
return true
|
return WithInsecure()
|
||||||
default:
|
default:
|
||||||
return false
|
return WithSecure()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// getEnvValue gets an OTLP environment variable value of the specified key using the GetEnv function.
|
func withEndpointForGRPC(u *url.URL) func(cfg Config) Config {
|
||||||
// This function already prepends the OTLP prefix to all key lookup.
|
return func(cfg Config) Config {
|
||||||
func (e *EnvOptionsReader) getEnvValue(key string) (string, bool) {
|
// For OTLP/gRPC endpoints, this is the target to which the
|
||||||
v := strings.TrimSpace(e.GetEnv(fmt.Sprintf("OTEL_EXPORTER_OTLP_%s", key)))
|
// exporter is going to send telemetry.
|
||||||
return v, v != ""
|
cfg.Traces.Endpoint = path.Join(u.Host, u.Path)
|
||||||
}
|
return cfg
|
||||||
|
|
||||||
func (e *EnvOptionsReader) readTLSConfig(path string) (*tls.Config, error) {
|
|
||||||
b, err := e.ReadFile(path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
return CreateTLSConfig(b)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func stringToCompression(value string) Compression {
|
// WithEnvCompression retrieves the specified config and passes it to ConfigFn as a Compression
|
||||||
switch value {
|
func WithEnvCompression(n string, fn func(Compression)) func(e *envconfig.EnvOptionsReader) {
|
||||||
case "gzip":
|
return func(e *envconfig.EnvOptionsReader) {
|
||||||
return GzipCompression
|
if v, ok := e.GetEnvValue(n); ok {
|
||||||
}
|
cp := NoCompression
|
||||||
|
switch v {
|
||||||
|
case "gzip":
|
||||||
|
cp = GzipCompression
|
||||||
|
}
|
||||||
|
|
||||||
return NoCompression
|
fn(cp)
|
||||||
}
|
|
||||||
|
|
||||||
func stringToHeader(value string) map[string]string {
|
|
||||||
headersPairs := strings.Split(value, ",")
|
|
||||||
headers := make(map[string]string)
|
|
||||||
|
|
||||||
for _, header := range headersPairs {
|
|
||||||
nameValue := strings.SplitN(header, "=", 2)
|
|
||||||
if len(nameValue) < 2 {
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
name, err := url.QueryUnescape(nameValue[0])
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
trimmedName := strings.TrimSpace(name)
|
|
||||||
value, err := url.QueryUnescape(nameValue[1])
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
trimmedValue := strings.TrimSpace(value)
|
|
||||||
|
|
||||||
headers[trimmedName] = trimmedValue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return headers
|
|
||||||
}
|
}
|
||||||
|
@ -13,63 +13,3 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
package otlpconfig
|
package otlpconfig
|
||||||
|
|
||||||
import (
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
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) {
|
|
||||||
if got := stringToHeader(tt.value); !reflect.DeepEqual(got, tt.want) {
|
|
||||||
t.Errorf("stringToHeader() = %v, want %v", got, tt.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -21,6 +21,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/otlpconfig"
|
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/internal/otlpconfig"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -381,9 +382,10 @@ 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 := otlpconfig.DefaultEnvOptionsReader
|
||||||
otlpconfig.DefaultEnvOptionsReader = otlpconfig.EnvOptionsReader{
|
otlpconfig.DefaultEnvOptionsReader = envconfig.EnvOptionsReader{
|
||||||
GetEnv: tt.env.getEnv,
|
GetEnv: tt.env.getEnv,
|
||||||
ReadFile: tt.fileReader.readFile,
|
ReadFile: tt.fileReader.readFile,
|
||||||
|
Namespace: "OTEL_EXPORTER_OTLP",
|
||||||
}
|
}
|
||||||
t.Cleanup(func() { otlpconfig.DefaultEnvOptionsReader = origEOR })
|
t.Cleanup(func() { otlpconfig.DefaultEnvOptionsReader = origEOR })
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user