Skip to content

Commit

Permalink
add new env config options for OTLP exporter (#3363)
Browse files Browse the repository at this point in the history
* add new env config options for OTLP exporter

* error path

* Update exporters/otlp/internal/envconfig/envconfig.go

Co-authored-by: Tyler Yahn <[email protected]>

* Update exporters/otlp/internal/envconfig/envconfig.go

Co-authored-by: Tyler Yahn <[email protected]>

* Update exporters/otlp/internal/envconfig/envconfig.go

Co-authored-by: Tyler Yahn <[email protected]>

* Update exporters/otlp/internal/envconfig/envconfig.go

Co-authored-by: Tyler Yahn <[email protected]>

* Update exporters/otlp/internal/envconfig/envconfig.go

Co-authored-by: Tyler Yahn <[email protected]>

* Update exporters/otlp/internal/envconfig/envconfig.go

Co-authored-by: Tyler Yahn <[email protected]>

* add error logging

* add missing early returns

Co-authored-by: Tyler Yahn <[email protected]>
Co-authored-by: Anthony Mirabella <[email protected]>
  • Loading branch information
3 people authored Nov 8, 2022
1 parent c21b6b6 commit 2694dbf
Show file tree
Hide file tree
Showing 5 changed files with 254 additions and 33 deletions.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,16 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
This option is used to configure the view(s) a `MeterProvider` will use for all `Reader`s that are registered with it. (#3387)
- Add Instrumentation Scope and Version as info metric and label in Prometheus exporter.
This can be disabled using the `WithoutScopeInfo()` option added to that package.(#3273, #3357)
- OTLP exporters now recognize: (#3363)
- `OTEL_EXPORTER_OTLP_INSECURE`
- `OTEL_EXPORTER_OTLP_TRACES_INSECURE`
- `OTEL_EXPORTER_OTLP_METRICS_INSECURE`
- `OTEL_EXPORTER_OTLP_CLIENT_KEY`
- `OTEL_EXPORTER_OTLP_TRACES_CLIENT_KEY`
- `OTEL_EXPORTER_OTLP_METRICS_CLIENT_KEY`
- `OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE`
- `OTEL_EXPORTER_OTLP_TRACES_CLIENT_CERTIFICATE`
- `OTEL_EXPORTER_OTLP_METRICS_CLIENT_CERTIFICATE`

### Changed

Expand Down
81 changes: 66 additions & 15 deletions exporters/otlp/internal/envconfig/envconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import (
"strconv"
"strings"
"time"

"go.opentelemetry.io/otel/internal/global"
)

// ConfigFn is the generic function used to set a config.
Expand Down Expand Up @@ -59,13 +61,26 @@ func WithString(n string, fn func(string)) func(e *EnvOptionsReader) {
}
}

// 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 {
if d, err := strconv.Atoi(v); err == nil {
fn(time.Duration(d) * time.Millisecond)
d, err := strconv.Atoi(v)
if err != nil {
global.Error(err, "parse duration", "input", v)
return
}
fn(time.Duration(d) * time.Millisecond)
}
}
}
Expand All @@ -83,23 +98,59 @@ func WithHeaders(n string, fn func(map[string]string)) func(e *EnvOptionsReader)
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)
u, err := url.Parse(v)
if err != nil {
global.Error(err, "parse url", "input", v)
return
}
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) {
// 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 {
if b, err := e.ReadFile(v); err == nil {
if c, err := createTLSConfig(b); err == nil {
fn(c)
}
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)
}
}

Expand All @@ -117,15 +168,18 @@ func stringToHeader(value string) map[string]string {
for _, header := range headersPairs {
nameValue := strings.SplitN(header, "=", 2)
if len(nameValue) < 2 {
global.Error(errors.New("missing '="), "parse headers", "input", nameValue)
continue
}
name, err := url.QueryUnescape(nameValue[0])
if err != nil {
global.Error(err, "escape header key", "key", nameValue[0])
continue
}
trimmedName := strings.TrimSpace(name)
value, err := url.QueryUnescape(nameValue[1])
if err != nil {
global.Error(err, "escape header value", "value", nameValue[1])
continue
}
trimmedValue := strings.TrimSpace(value)
Expand All @@ -136,13 +190,10 @@ func stringToHeader(value string) map[string]string {
return headers
}

func createTLSConfig(certBytes []byte) (*tls.Config, error) {
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 &tls.Config{
RootCAs: cp,
}, nil
return cp, nil
}
142 changes: 128 additions & 14 deletions exporters/otlp/internal/envconfig/envconfig_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,29 +16,40 @@ package envconfig // import "go.opentelemetry.io/otel/exporters/otlp/internal/en

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-----
MIIBhzCCASygAwIBAgIRANHpHgAWeTnLZpTSxCKs0ggwCgYIKoZIzj0EAwIwEjEQ
MA4GA1UEChMHb3RlbC1nbzAeFw0yMTA0MDExMzU5MDNaFw0yMTA0MDExNDU5MDNa
MBIxEDAOBgNVBAoTB290ZWwtZ28wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAS9
nWSkmPCxShxnp43F+PrOtbGV7sNfkbQ/kxzi9Ego0ZJdiXxkmv/C05QFddCW7Y0Z
sJCLHGogQsYnWJBXUZOVo2MwYTAOBgNVHQ8BAf8EBAMCB4AwEwYDVR0lBAwwCgYI
KwYBBQUHAwEwDAYDVR0TAQH/BAIwADAsBgNVHREEJTAjgglsb2NhbGhvc3SHEAAA
AAAAAAAAAAAAAAAAAAGHBH8AAAEwCgYIKoZIzj0EAwIDSQAwRgIhANwZVVKvfvQ/
1HXsTvgH+xTQswOwSSKYJ1cVHQhqK7ZbAiEAus8NxpTRnp5DiTMuyVmhVNPB+bVH
Lhnm4N/QDk5rek0=
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
Expand Down Expand Up @@ -134,6 +145,56 @@ func TestEnvConfig(t *testing.T) {
},
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{
Expand Down Expand Up @@ -265,7 +326,7 @@ func TestEnvConfig(t *testing.T) {
}

func TestWithTLSConfig(t *testing.T) {
tlsCert, err := createTLSConfig([]byte(WeakCertificate))
pool, err := createCertPool([]byte(WeakCertificate))
assert.NoError(t, err)

reader := EnvOptionsReader{
Expand All @@ -285,12 +346,65 @@ func TestWithTLSConfig(t *testing.T) {

var option testOption
reader.Apply(
WithTLSConfig("CERTIFICATE", func(v *tls.Config) {
option = testOption{TestTLS: v}
}))
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, tlsCert.RootCAs.Subjects(), option.TestTLS.RootCAs.Subjects())
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) {
Expand Down
27 changes: 25 additions & 2 deletions exporters/otlp/otlpmetric/internal/oconf/envconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package oconf // import "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/inte

import (
"crypto/tls"
"crypto/x509"
"net/url"
"os"
"path"
Expand Down Expand Up @@ -53,6 +54,7 @@ func ApplyHTTPEnvConfigs(cfg Config) Config {
func getOptionsFromEnv() []GenericOption {
opts := []GenericOption{}

tlsConf := &tls.Config{}
DefaultEnvOptionsReader.Apply(
envconfig.WithURL("ENDPOINT", func(u *url.URL) {
opts = append(opts, withEndpointScheme(u))
Expand Down Expand Up @@ -81,8 +83,13 @@ func getOptionsFromEnv() []GenericOption {
return cfg
}, withEndpointForGRPC(u)))
}),
envconfig.WithTLSConfig("CERTIFICATE", func(c *tls.Config) { opts = append(opts, WithTLSClientConfig(c)) }),
envconfig.WithTLSConfig("METRICS_CERTIFICATE", func(c *tls.Config) { opts = append(opts, WithTLSClientConfig(c)) }),
envconfig.WithCertPool("CERTIFICATE", func(p *x509.CertPool) { tlsConf.RootCAs = p }),
envconfig.WithCertPool("METRICS_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("METRICS_CLIENT_CERTIFICATE", "METRICS_CLIENT_KEY", func(c tls.Certificate) { tlsConf.Certificates = []tls.Certificate{c} }),
envconfig.WithBool("INSECURE", func(b bool) { opts = append(opts, withInsecure(b)) }),
envconfig.WithBool("METRICS_INSECURE", func(b bool) { opts = append(opts, withInsecure(b)) }),
withTLSConfig(tlsConf, func(c *tls.Config) { opts = append(opts, WithTLSClientConfig(c)) }),
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)) }),
Expand Down Expand Up @@ -125,3 +132,19 @@ func withEndpointScheme(u *url.URL) GenericOption {
return WithSecure()
}
}

// 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)
}
}
}
Loading

0 comments on commit 2694dbf

Please sign in to comment.