-
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 all 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,13 @@ import ( | |
| "encoding/json" | ||
| "errors" | ||
| "fmt" | ||
| "net/url" | ||
| "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 +53,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 +91,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 +120,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 +345,191 @@ 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). | ||
| // | ||
| //nolint:gocognit,cyclop,funlen | ||
| 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) | ||
| } | ||
|
|
||
| isHTTP := stdCfg.ExporterProtocol.Valid && stdCfg.ExporterProtocol.String == httpExporterProtocol | ||
|
|
||
| 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 | ||
| } | ||
|
|
||
| if isHTTP { | ||
| stdCfg.HTTPExporterInsecure = null.BoolFrom(exporterInsecureBool) | ||
| } else { | ||
| stdCfg.GRPCExporterInsecure = null.BoolFrom(exporterInsecureBool) | ||
| } | ||
| } | ||
|
|
||
| var ( | ||
| exporterEndpointVar string | ||
| exporterEndpointString string | ||
| ) | ||
| if exporterEndpointVal, ok := env["OTEL_EXPORTER_OTLP_METRICS_ENDPOINT"]; ok { | ||
| exporterEndpointVar = "OTEL_EXPORTER_OTLP_METRICS_ENDPOINT" | ||
| exporterEndpointString = exporterEndpointVal | ||
| } else if exporterEndpointVal, ok := env["OTEL_EXPORTER_OTLP_ENDPOINT"]; ok { | ||
| exporterEndpointVar = "OTEL_EXPORTER_OTLP_ENDPOINT" | ||
| exporterEndpointString = exporterEndpointVal | ||
|
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 wonder from this paragraph if we should add
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. Makes sense, I'll try to make sure this gets appended if we populate the endpoint from "standard" OTLP variables 👍🏻 |
||
| } | ||
|
|
||
| if exporterEndpointString != "" { | ||
| exporterEndpoint, insecure, err := parseOTELEndpoint(exporterEndpointString, isHTTP) | ||
| if err != nil { | ||
| return Config{}, fmt.Errorf("failed to parse %s: %w", exporterEndpointVar, err) | ||
| } | ||
|
|
||
| if isHTTP { | ||
| stdCfg.HTTPExporterEndpoint = exporterEndpoint | ||
| stdCfg.HTTPExporterInsecure = insecure | ||
| } else { | ||
| stdCfg.GRPCExporterEndpoint = exporterEndpoint | ||
| } | ||
|
|
||
| if !isHTTP && insecure.Valid { | ||
| stdCfg.GRPCExporterInsecure = insecure | ||
| } | ||
| } | ||
|
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 | ||
| } | ||
|
|
||
| // parseOTELEndpoint tries to parse the given string as an OTEL endpoint URL, extracts the endpoint and whether the | ||
| // connection should be secure or not. It honors what's said in the OTEL exporter docs: | ||
| // https://opentelemetry.io/docs/specs/otel/protocol/exporter/. | ||
| // | ||
| // IMPORTANT: Meant to be used only for parsing OTEL_EXPORTER_OTLP_ENDPOINT-like environment variables, not for k6 | ||
| // configuration options, as Config.HTTPExporterEndpoint, for instance, doesn't accept a scheme on the URLs, while | ||
| // the OTEL exporter configuration requires it (for HTTP) or accepts it (for gRPC). | ||
| func parseOTELEndpoint(endpoint string, isHTTP bool) (null.String, null.Bool, error) { | ||
| isHTTPScheme := strings.HasPrefix(endpoint, "http://") | ||
| isHTTPSScheme := strings.HasPrefix(endpoint, "https://") | ||
|
|
||
| // For HTTP/S, the scheme is required. For gRPC, it's optional. | ||
| if !isHTTPScheme && !isHTTPSScheme { | ||
| if isHTTP { | ||
| return null.String{}, null.Bool{}, errors.New("endpoint must contain the scheme (http or https)") | ||
| } | ||
| // gRPC accepts any form allowed by the underlying client, so use as-is. | ||
| return null.StringFrom(endpoint), null.Bool{}, nil | ||
| } | ||
|
|
||
| // Parse the URL to extract components. | ||
| parsedURL, err := url.Parse(endpoint) | ||
| if err != nil { | ||
| return null.String{}, null.Bool{}, err | ||
| } | ||
|
|
||
| host := parsedURL.Host | ||
| port := parsedURL.Port() | ||
|
|
||
| if host == "" { | ||
| return null.String{}, null.Bool{}, errors.New("endpoint must contain a host") | ||
| } | ||
|
|
||
| // HTTP requires a port, gRPC doesn't. | ||
| if isHTTP && port == "" { | ||
| return null.String{}, null.Bool{}, errors.New("endpoint must contain host and port") | ||
| } | ||
|
Comment on lines
+494
to
+497
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. Where did you read that HTTP requires a port? This is not what I understand from this part (in this paragraph):
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. This is historical behavior from the k6 extension itself, rather than something specific to OTLP specification. See:
(https://grafana.com/docs/k6/latest/results-output/real-time/opentelemetry/) But your comment made me wonder, should I perhaps append the default port, in case we get the endpoint from standard OTLP variables and it doesn't include already a port? 🤔 |
||
|
|
||
| // From OTEL docs: A scheme of "https" indicates a secure connection and takes precedence over the insecure | ||
| // configuration setting. The same applies to "http", but indicating insecure connection. | ||
| var insecure null.Bool | ||
| if isHTTPSScheme { | ||
| insecure = null.BoolFrom(false) | ||
| } else { | ||
| insecure = null.BoolFrom(true) | ||
| } | ||
|
|
||
| return null.StringFrom(host), insecure, 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 | ||
| } | ||
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