-
Notifications
You must be signed in to change notification settings - Fork 1.5k
opentelemetry: Overwrite defaults with standard OTLP exporter env vars #5350
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
base: master
Are you sure you want to change the base?
Changes from 4 commits
c0ec91b
32e2064
429ad39
9bb897e
4d3a1c2
8633d53
29e0c45
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,10 +4,12 @@ import ( | |
| "encoding/json" | ||
| "errors" | ||
| "fmt" | ||
| "strconv" | ||
| "strings" | ||
| "time" | ||
|
|
||
| "github.com/mstoykov/envconfig" | ||
| "github.com/sirupsen/logrus" | ||
| "go.k6.io/k6/errext" | ||
| "go.k6.io/k6/errext/exitcodes" | ||
| "go.k6.io/k6/internal/build" | ||
|
|
@@ -50,7 +52,7 @@ type Config struct { | |
| // ExportInterval configures the intervening time between metrics exports | ||
| ExportInterval types.NullDuration `json:"exportInterval" envconfig:"K6_OTEL_EXPORT_INTERVAL"` | ||
|
|
||
| // Headers in W3C Correlation-Context format without additional semi-colon delimited metadata (i.e. "k1=v1,k2=v2") | ||
| // Headers in W3C Correlation-Context format without additional semicolon-delimited metadata (i.e. "k1=v1,k2=v2") | ||
| Headers null.String `json:"headers" envconfig:"K6_OTEL_HEADERS"` | ||
|
|
||
| // TLSInsecureSkipVerify disables verification of the server's certificate chain | ||
|
|
@@ -88,13 +90,27 @@ type Config struct { | |
| // GetConsolidatedConfig combines the options' values from the different sources | ||
| // and returns the merged options. The Order of precedence used is documented | ||
| // in the k6 Documentation https://grafana.com/docs/k6/latest/using-k6/k6-options/how-to/#order-of-precedence. | ||
| func GetConsolidatedConfig(jsonRawConf json.RawMessage, env map[string]string) (Config, error) { | ||
| func GetConsolidatedConfig( | ||
| jsonRawConf json.RawMessage, | ||
| env map[string]string, | ||
| logger logrus.FieldLogger, | ||
| ) (Config, error) { | ||
| // We start from the defaults. | ||
| cfg := newDefaultConfig() | ||
|
|
||
| // Then, we apply the OTLP exporter environment variables. So, these are used as the "defaults", if defined, | ||
| // while the k6-specific configuration options / environment variables remain with higher precedence, as follows. | ||
| cfg, err := applyOTELEnvVars(cfg, env) | ||
| if err != nil { | ||
| return cfg, fmt.Errorf("parse standard OTEL environment variables options failed: %w", err) | ||
| } | ||
|
|
||
| if jsonRawConf != nil { | ||
| jsonConf, err := parseJSON(jsonRawConf) | ||
| if err != nil { | ||
| return cfg, fmt.Errorf("parse JSON options failed: %w", err) | ||
| } | ||
| warnIfConfigMismatch(jsonConf, logger) | ||
| cfg = cfg.Apply(jsonConf) | ||
| } | ||
|
|
||
|
|
@@ -103,11 +119,11 @@ func GetConsolidatedConfig(jsonRawConf json.RawMessage, env map[string]string) ( | |
| if err != nil { | ||
| return cfg, fmt.Errorf("parse environment variables options failed: %w", err) | ||
| } | ||
| warnIfConfigMismatch(envConf, logger) | ||
| cfg = cfg.Apply(envConf) | ||
| } | ||
|
|
||
| if err := cfg.Validate(); err != nil { | ||
| // TODO: check why k6's still exiting with 255 | ||
| return cfg, errext.WithExitCodeIfNone( | ||
| fmt.Errorf("error validating OpenTelemetry output config: %w", err), | ||
| exitcodes.InvalidConfig, | ||
|
|
@@ -328,14 +344,112 @@ func parseJSON(data json.RawMessage) (Config, error) { | |
| func parseEnvs(env map[string]string) (Config, error) { | ||
| cfg := Config{} | ||
|
|
||
| if serviceName, ok := env["OTEL_SERVICE_NAME"]; ok { | ||
| cfg.ServiceName = null.StringFrom(serviceName) | ||
| } | ||
|
|
||
| err := envconfig.Process("K6_OTEL_", &cfg, func(key string) (string, bool) { | ||
| v, ok := env[key] | ||
| return v, ok | ||
| }) | ||
|
|
||
| return cfg, err | ||
| } | ||
|
|
||
| // applyOTELEnvVars applies the OTLP exporter environment variables, if defined, to the supplied Config. | ||
| // As per OTLP Exporter configuration specification, signal-specific variables (e.g., | ||
| // OTEL_EXPORTER_OTLP_METRICS_PROTOCOL) take precedence over general ones (e.g., OTEL_EXPORTER_OTLP_PROTOCOL). | ||
| func applyOTELEnvVars(defaultCfg Config, env map[string]string) (Config, error) { | ||
| stdCfg := Config{} | ||
|
|
||
| if serviceName, ok := env["OTEL_SERVICE_NAME"]; ok { | ||
| stdCfg.ServiceName = null.StringFrom(serviceName) | ||
| } | ||
|
|
||
| if exporterProtocol, ok := env["OTEL_EXPORTER_OTLP_METRICS_PROTOCOL"]; ok { | ||
| stdCfg.ExporterProtocol = null.StringFrom(exporterProtocol) | ||
| } else if exporterProtocol, ok := env["OTEL_EXPORTER_OTLP_PROTOCOL"]; ok { | ||
| stdCfg.ExporterProtocol = null.StringFrom(exporterProtocol) | ||
| } | ||
|
|
||
| if exportInterval, ok := env["OTEL_METRIC_EXPORT_INTERVAL"]; ok { | ||
| exportIntervalDuration, err := types.ParseExtendedDuration(exportInterval) | ||
| if err != nil { | ||
| return Config{}, err | ||
| } | ||
| stdCfg.ExportInterval = types.NullDurationFrom(exportIntervalDuration) | ||
| } | ||
|
|
||
| if exporterHeaders, ok := env["OTEL_EXPORTER_OTLP_METRICS_HEADERS"]; ok { | ||
| stdCfg.Headers = null.StringFrom(exporterHeaders) | ||
| } else if exporterHeaders, ok := env["OTEL_EXPORTER_OTLP_HEADERS"]; ok { | ||
| stdCfg.Headers = null.StringFrom(exporterHeaders) | ||
| } | ||
|
|
||
| if exporterCertificate, ok := env["OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE"]; ok { | ||
| stdCfg.TLSCertificate = null.StringFrom(exporterCertificate) | ||
| } else if exporterCertificate, ok := env["OTEL_EXPORTER_OTLP_CERTIFICATE"]; ok { | ||
| stdCfg.TLSCertificate = null.StringFrom(exporterCertificate) | ||
| } | ||
|
|
||
| if exporterClientCertificate, ok := env["OTEL_EXPORTER_OTLP_METRICS_CLIENT_CERTIFICATE"]; ok { | ||
| stdCfg.TLSClientCertificate = null.StringFrom(exporterClientCertificate) | ||
| } else if exporterClientCertificate, ok := env["OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE"]; ok { | ||
| stdCfg.TLSClientCertificate = null.StringFrom(exporterClientCertificate) | ||
| } | ||
|
|
||
| if exporterClientKey, ok := env["OTEL_EXPORTER_OTLP_METRICS_CLIENT_KEY"]; ok { | ||
| stdCfg.TLSClientKey = null.StringFrom(exporterClientKey) | ||
| } else if exporterClientKey, ok := env["OTEL_EXPORTER_OTLP_CLIENT_KEY"]; ok { | ||
| stdCfg.TLSClientKey = null.StringFrom(exporterClientKey) | ||
| } | ||
|
|
||
| var exporterInsecureBoolVar string | ||
| if exporterInsecure, ok := env["OTEL_EXPORTER_OTLP_METRICS_INSECURE"]; ok { | ||
| exporterInsecureBoolVar = exporterInsecure | ||
| } else if exporterInsecure, ok := env["OTEL_EXPORTER_OTLP_INSECURE"]; ok { | ||
| exporterInsecureBoolVar = exporterInsecure | ||
| } | ||
|
|
||
| if exporterInsecureBoolVar != "" { | ||
| exporterInsecureBool, err := strconv.ParseBool(exporterInsecureBoolVar) | ||
| if err != nil { | ||
| return Config{}, err | ||
| } | ||
|
|
||
| stdCfg.GRPCExporterInsecure = null.BoolFrom(exporterInsecureBool) | ||
| stdCfg.HTTPExporterInsecure = null.BoolFrom(exporterInsecureBool) | ||
| } | ||
|
|
||
| if exporterEndpoint, ok := env["OTEL_EXPORTER_OTLP_METRICS_ENDPOINT"]; ok { | ||
| stdCfg.GRPCExporterEndpoint = null.StringFrom(exporterEndpoint) | ||
| stdCfg.HTTPExporterEndpoint = null.StringFrom(exporterEndpoint) | ||
| } else if exporterEndpoint, ok := env["OTEL_EXPORTER_OTLP_ENDPOINT"]; ok { | ||
| stdCfg.GRPCExporterEndpoint = null.StringFrom(exporterEndpoint) | ||
| stdCfg.HTTPExporterEndpoint = null.StringFrom(exporterEndpoint) | ||
| } | ||
|
Comment on lines
+415
to
+456
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we have an issue to support these options for the HTTP exporter. I haven't tested it but from k6 doc:
And from OpenTelemetry doc:
So first, users using the endpoint env variable like they are used will have errors when using
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch @AgnesToulet! Thanks! 🙌🏻 🙇🏻 How does 8633d53 look like? 🤔 I tried to apply what's stated in the aforementioned docs. |
||
|
|
||
| return defaultCfg.Apply(stdCfg), nil | ||
| } | ||
|
|
||
| // warnIfConfigMismatch writes a warning log message in case of discrepancies between the configured | ||
| // Config.ExporterProtocol and other configuration attributes. E.g., in case the user explicitly configured | ||
| // Config.HTTPExporterEndpoint while the chosen protocol is `grpc`. | ||
| func warnIfConfigMismatch(cfg Config, logger logrus.FieldLogger) { | ||
| exporterProtocol := cfg.ExporterProtocol.String | ||
| switch { | ||
| case exporterProtocol == grpcExporterProtocol && anyHTTPOptionSet(cfg): | ||
| logger.Warn("Configuration mismatch detected: the gRPC exporter type is set, but also some HTTP " + | ||
| "configuration options") | ||
| case exporterProtocol == httpExporterProtocol && anyGRPCOptionSet(cfg): | ||
| logger.Warn("Configuration mismatch detected: the HTTP exporter type is set, but also some gRPC " + | ||
| "configuration options") | ||
| } | ||
| } | ||
|
|
||
| func anyHTTPOptionSet(cfg Config) bool { | ||
| return cfg.HTTPExporterInsecure.Valid || | ||
| cfg.HTTPExporterEndpoint.Valid || | ||
| cfg.HTTPExporterURLPath.Valid | ||
| } | ||
|
|
||
| func anyGRPCOptionSet(cfg Config) bool { | ||
| return cfg.GRPCExporterInsecure.Valid || | ||
| cfg.GRPCExporterEndpoint.Valid | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,9 +2,11 @@ package opentelemetry | |
|
|
||
| import ( | ||
| "encoding/json" | ||
| "io" | ||
| "testing" | ||
| "time" | ||
|
|
||
| "github.com/sirupsen/logrus" | ||
| "github.com/stretchr/testify/require" | ||
| "go.k6.io/k6/internal/build" | ||
| "go.k6.io/k6/lib/types" | ||
|
|
@@ -37,6 +39,37 @@ func TestConfig(t *testing.T) { | |
| }, | ||
| }, | ||
|
|
||
| "OTLP exporter env vars overwrite defaults": { | ||
| env: map[string]string{ | ||
| "OTEL_SERVICE_NAME": "k6-test", | ||
| "OTEL_EXPORTER_OTLP_PROTOCOL": httpExporterProtocol, | ||
| "OTEL_METRIC_EXPORT_INTERVAL": "1m", | ||
| "OTEL_EXPORTER_OTLP_HEADERS": "k1=v1,k2=v2", | ||
| "OTEL_EXPORTER_OTLP_CERTIFICATE": "fake-certificate", | ||
| "OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE": "fake-client-certificate", | ||
| "OTEL_EXPORTER_OTLP_CLIENT_KEY": "fake-client-key", | ||
| "OTEL_EXPORTER_OTLP_INSECURE": "true", | ||
| "OTEL_EXPORTER_OTLP_ENDPOINT": "localhost:4317", | ||
| }, | ||
| expectedConfig: Config{ | ||
| ServiceName: null.StringFrom("k6-test"), | ||
| ServiceVersion: null.NewString(build.Version, false), | ||
| ExporterProtocol: null.StringFrom(httpExporterProtocol), | ||
| HTTPExporterInsecure: null.BoolFrom(true), | ||
| HTTPExporterEndpoint: null.StringFrom("localhost:4317"), | ||
| HTTPExporterURLPath: null.NewString("/v1/metrics", false), | ||
| GRPCExporterInsecure: null.BoolFrom(true), | ||
| GRPCExporterEndpoint: null.StringFrom("localhost:4317"), | ||
| ExportInterval: types.NullDurationFrom(1 * time.Minute), | ||
| FlushInterval: types.NewNullDuration(1*time.Second, false), | ||
| SingleCounterForRate: null.NewBool(true, false), | ||
| Headers: null.StringFrom("k1=v1,k2=v2"), | ||
| TLSCertificate: null.StringFrom("fake-certificate"), | ||
| TLSClientCertificate: null.StringFrom("fake-client-certificate"), | ||
| TLSClientKey: null.StringFrom("fake-client-key"), | ||
| }, | ||
| }, | ||
|
|
||
| "environment success merge": { | ||
| env: map[string]string{"K6_OTEL_GRPC_EXPORTER_ENDPOINT": "else", "K6_OTEL_EXPORT_INTERVAL": "4ms"}, | ||
| expectedConfig: Config{ | ||
|
|
@@ -95,22 +128,45 @@ func TestConfig(t *testing.T) { | |
| }, | ||
| }, | ||
|
|
||
| "OTEL environment variables": { | ||
| "OTLP exporter env vars overwritten by k6 env vars": { | ||
| env: map[string]string{ | ||
| "OTEL_SERVICE_NAME": "otel-service", | ||
| "OTEL_SERVICE_NAME": "k6-test", | ||
| "K6_OTEL_SERVICE_NAME": "foo", | ||
| "OTEL_EXPORTER_OTLP_PROTOCOL": httpExporterProtocol, | ||
| "K6_OTEL_EXPORTER_PROTOCOL": grpcExporterProtocol, | ||
| "OTEL_METRIC_EXPORT_INTERVAL": "1m", | ||
| "K6_OTEL_EXPORT_INTERVAL": "4ms", | ||
| "OTEL_EXPORTER_OTLP_HEADERS": "k1=v1,k2=v2", | ||
| "K6_OTEL_HEADERS": "key1=value1,key2=value2", | ||
| "OTEL_EXPORTER_OTLP_CERTIFICATE": "fake-certificate", | ||
| "K6_OTEL_TLS_CERTIFICATE": "cert_path", | ||
| "OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE": "fake-client-certificate", | ||
| "K6_OTEL_TLS_CLIENT_CERTIFICATE": "client_cert_path", | ||
| "OTEL_EXPORTER_OTLP_CLIENT_KEY": "fake-client-key", | ||
| "K6_OTEL_TLS_CLIENT_KEY": "client_key_path", | ||
| "OTEL_EXPORTER_OTLP_INSECURE": "true", | ||
| "K6_OTEL_HTTP_EXPORTER_INSECURE": "false", | ||
| "K6_OTEL_GRPC_EXPORTER_INSECURE": "false", | ||
| "OTEL_EXPORTER_OTLP_ENDPOINT": "localhost:4317", | ||
| "K6_OTEL_HTTP_EXPORTER_ENDPOINT": "localhost:4318", | ||
| "K6_OTEL_GRPC_EXPORTER_ENDPOINT": "localhost:4318", | ||
| }, | ||
| expectedConfig: Config{ | ||
| ServiceName: null.NewString("otel-service", true), | ||
| ServiceName: null.StringFrom("foo"), | ||
| ServiceVersion: null.NewString(build.Version, false), | ||
| ExporterProtocol: null.NewString(grpcExporterProtocol, false), | ||
| HTTPExporterInsecure: null.NewBool(false, false), | ||
| HTTPExporterEndpoint: null.NewString("localhost:4318", false), | ||
| ExporterProtocol: null.StringFrom(grpcExporterProtocol), | ||
| HTTPExporterInsecure: null.BoolFrom(false), | ||
| HTTPExporterEndpoint: null.StringFrom("localhost:4318"), | ||
| HTTPExporterURLPath: null.NewString("/v1/metrics", false), | ||
| GRPCExporterInsecure: null.NewBool(false, false), | ||
| GRPCExporterEndpoint: null.NewString("localhost:4317", false), | ||
| ExportInterval: types.NewNullDuration(10*time.Second, false), | ||
| GRPCExporterInsecure: null.BoolFrom(false), | ||
| GRPCExporterEndpoint: null.StringFrom("localhost:4318"), | ||
| ExportInterval: types.NullDurationFrom(4 * time.Millisecond), | ||
| FlushInterval: types.NewNullDuration(1*time.Second, false), | ||
| SingleCounterForRate: null.NewBool(true, false), | ||
| Headers: null.StringFrom("key1=value1,key2=value2"), | ||
| TLSCertificate: null.StringFrom("cert_path"), | ||
| TLSClientCertificate: null.StringFrom("client_cert_path"), | ||
| TLSClientKey: null.StringFrom("client_key_path"), | ||
| }, | ||
| }, | ||
|
|
||
|
|
@@ -157,6 +213,52 @@ func TestConfig(t *testing.T) { | |
| }, | ||
| }, | ||
|
|
||
| "OTLP exporter env vars overwritten by JSON config": { | ||
| jsonRaw: json.RawMessage( | ||
| `{` + | ||
| `"serviceName":"foo",` + | ||
| `"exporterProtocol":"grpc",` + | ||
| `"exportInterval":"15ms",` + | ||
| `"httpExporterInsecure":false,` + | ||
| `"httpExporterEndpoint":"localhost:4318",` + | ||
| `"grpcExporterInsecure":false,` + | ||
| `"grpcExporterEndpoint":"localhost:4318",` + | ||
| `"tlsCertificate":"cert_path",` + | ||
| `"tlsClientCertificate":"client_cert_path",` + | ||
| `"tlsClientKey":"client_key_path",` + | ||
| `"headers":"key1=value1,key2=value2"` + | ||
| `}`, | ||
| ), | ||
| env: map[string]string{ | ||
| "OTEL_SERVICE_NAME": "k6-test", | ||
| "OTEL_EXPORTER_OTLP_PROTOCOL": httpExporterProtocol, | ||
| "OTEL_METRIC_EXPORT_INTERVAL": "1m", | ||
| "OTEL_EXPORTER_OTLP_HEADERS": "k1=v1,k2=v2", | ||
| "OTEL_EXPORTER_OTLP_CERTIFICATE": "fake-certificate", | ||
| "OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE": "fake-client-certificate", | ||
| "OTEL_EXPORTER_OTLP_CLIENT_KEY": "fake-client-key", | ||
| "OTEL_EXPORTER_OTLP_INSECURE": "true", | ||
| "OTEL_EXPORTER_OTLP_ENDPOINT": "localhost:4317", | ||
| }, | ||
| expectedConfig: Config{ | ||
| ServiceName: null.StringFrom("foo"), | ||
| ServiceVersion: null.NewString(build.Version, false), | ||
| ExporterProtocol: null.StringFrom(grpcExporterProtocol), | ||
| HTTPExporterInsecure: null.BoolFrom(false), | ||
| HTTPExporterEndpoint: null.StringFrom("localhost:4318"), | ||
| HTTPExporterURLPath: null.NewString("/v1/metrics", false), | ||
| GRPCExporterInsecure: null.BoolFrom(false), | ||
| GRPCExporterEndpoint: null.StringFrom("localhost:4318"), | ||
| ExportInterval: types.NullDurationFrom(15 * time.Millisecond), | ||
| FlushInterval: types.NewNullDuration(1*time.Second, false), | ||
| SingleCounterForRate: null.NewBool(true, false), | ||
| Headers: null.StringFrom("key1=value1,key2=value2"), | ||
| TLSCertificate: null.StringFrom("cert_path"), | ||
| TLSClientCertificate: null.StringFrom("client_cert_path"), | ||
| TLSClientKey: null.StringFrom("client_key_path"), | ||
| }, | ||
| }, | ||
|
|
||
| "JSON success merge": { | ||
| jsonRaw: json.RawMessage(`{"exporterType":"http","httpExporterEndpoint":"localhost:5566","httpExporterURLPath":"/lorem/ipsum","exportInterval":"15ms"}`), | ||
| expectedConfig: Config{ | ||
|
|
@@ -213,7 +315,11 @@ func TestConfig(t *testing.T) { | |
| for name, testCase := range testCases { | ||
| t.Run(name, func(t *testing.T) { | ||
| t.Parallel() | ||
| config, err := GetConsolidatedConfig(testCase.jsonRaw, testCase.env) | ||
|
|
||
| logger := logrus.New() | ||
| logger.SetOutput(io.Discard) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could we test
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good idea! I have added a new test in 4d3a1c2. I preferred this, because |
||
|
|
||
| config, err := GetConsolidatedConfig(testCase.jsonRaw, testCase.env, logger) | ||
| if testCase.err != "" { | ||
| require.Error(t, err) | ||
| require.Contains(t, err.Error(), testCase.err) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this fixed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, it seemed to me that this comment was outdated, that's why I removed it.
I checked it by running k6 with an invalid configuration (see below), and checking that the exit status is 104, which corresponds to the invalid configuration exit code.
❯ K6_OTEL_EXPORTER_PROTOCOL=error go run main.go run -o opentelemetry examples/docker-compose/opentelemetry/script.js /\ Grafana /‾‾/ /\ / \ |\ __ / / / \/ \ | |/ / / ‾‾\ / \ | ( | (‾) | / __________ \ |_|\_\ \_____/ Init [--------------------------------------] default [--------------------------------------] ERRO[0000] could not create the 'opentelemetry' output: error validating OpenTelemetry output config: unsupported exporter protocol "error", only "grpc" and "http/protobuf" are supported exit status 104