Skip to content

Commit c0ec91b

Browse files
committed
opentelemetry: Overwrite defaults with standard OTLP exporter env vars
1 parent 1e282bf commit c0ec91b

File tree

3 files changed

+219
-18
lines changed

3 files changed

+219
-18
lines changed

internal/output/opentelemetry/config.go

Lines changed: 102 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@ import (
44
"encoding/json"
55
"errors"
66
"fmt"
7+
"strconv"
78
"strings"
89
"time"
910

1011
"github.com/mstoykov/envconfig"
12+
"github.com/sirupsen/logrus"
1113
"go.k6.io/k6/errext"
1214
"go.k6.io/k6/errext/exitcodes"
1315
"go.k6.io/k6/internal/build"
@@ -50,7 +52,7 @@ type Config struct {
5052
// ExportInterval configures the intervening time between metrics exports
5153
ExportInterval types.NullDuration `json:"exportInterval" envconfig:"K6_OTEL_EXPORT_INTERVAL"`
5254

53-
// Headers in W3C Correlation-Context format without additional semi-colon delimited metadata (i.e. "k1=v1,k2=v2")
55+
// Headers in W3C Correlation-Context format without additional semicolon-delimited metadata (i.e. "k1=v1,k2=v2")
5456
Headers null.String `json:"headers" envconfig:"K6_OTEL_HEADERS"`
5557

5658
// TLSInsecureSkipVerify disables verification of the server's certificate chain
@@ -88,13 +90,27 @@ type Config struct {
8890
// GetConsolidatedConfig combines the options' values from the different sources
8991
// and returns the merged options. The Order of precedence used is documented
9092
// in the k6 Documentation https://grafana.com/docs/k6/latest/using-k6/k6-options/how-to/#order-of-precedence.
91-
func GetConsolidatedConfig(jsonRawConf json.RawMessage, env map[string]string) (Config, error) {
93+
func GetConsolidatedConfig(
94+
jsonRawConf json.RawMessage,
95+
env map[string]string,
96+
logger logrus.FieldLogger,
97+
) (Config, error) {
98+
// We start from the defaults.
9299
cfg := newDefaultConfig()
100+
101+
// Then, we apply the OTLP exporter environment variables. So, these are used as the "defaults", if defined,
102+
// while the k6-specific configuration options / environment variables remain with higher precedence, as follows.
103+
cfg, err := applyOTELEnvVars(cfg, env)
104+
if err != nil {
105+
return cfg, fmt.Errorf("parse standard OTEL environment variables options failed: %w", err)
106+
}
107+
93108
if jsonRawConf != nil {
94109
jsonConf, err := parseJSON(jsonRawConf)
95110
if err != nil {
96111
return cfg, fmt.Errorf("parse JSON options failed: %w", err)
97112
}
113+
warnIfConfigMismatch(jsonConf, logger)
98114
cfg = cfg.Apply(jsonConf)
99115
}
100116

@@ -103,11 +119,11 @@ func GetConsolidatedConfig(jsonRawConf json.RawMessage, env map[string]string) (
103119
if err != nil {
104120
return cfg, fmt.Errorf("parse environment variables options failed: %w", err)
105121
}
122+
warnIfConfigMismatch(envConf, logger)
106123
cfg = cfg.Apply(envConf)
107124
}
108125

109126
if err := cfg.Validate(); err != nil {
110-
// TODO: check why k6's still exiting with 255
111127
return cfg, errext.WithExitCodeIfNone(
112128
fmt.Errorf("error validating OpenTelemetry output config: %w", err),
113129
exitcodes.InvalidConfig,
@@ -328,14 +344,93 @@ func parseJSON(data json.RawMessage) (Config, error) {
328344
func parseEnvs(env map[string]string) (Config, error) {
329345
cfg := Config{}
330346

331-
if serviceName, ok := env["OTEL_SERVICE_NAME"]; ok {
332-
cfg.ServiceName = null.StringFrom(serviceName)
333-
}
334-
335347
err := envconfig.Process("K6_OTEL_", &cfg, func(key string) (string, bool) {
336348
v, ok := env[key]
337349
return v, ok
338350
})
339351

340352
return cfg, err
341353
}
354+
355+
// applyOTELEnvVars applies the OTLP exporter environment variables, if defined, to the supplied Config.
356+
// It only considers their general version (e.g., OTEL_EXPORTER_OTLP_PROTOCOL) rather than their
357+
// signal-specific ones (e.g., OTEL_EXPORTER_OTLP_METRICS_PROTOCOL).
358+
// In the future we might consider parsing also the `_METRICS_` ones.
359+
func applyOTELEnvVars(defaultCfg Config, env map[string]string) (Config, error) {
360+
stdCfg := Config{}
361+
362+
if serviceName, ok := env["OTEL_SERVICE_NAME"]; ok {
363+
stdCfg.ServiceName = null.StringFrom(serviceName)
364+
}
365+
366+
if exporterProtocol, ok := env["OTEL_EXPORTER_OTLP_PROTOCOL"]; ok {
367+
stdCfg.ExporterProtocol = null.StringFrom(exporterProtocol)
368+
}
369+
370+
if exportInterval, ok := env["OTEL_METRIC_EXPORT_INTERVAL"]; ok {
371+
exportIntervalDuration, err := types.ParseExtendedDuration(exportInterval)
372+
if err != nil {
373+
return Config{}, err
374+
}
375+
stdCfg.ExportInterval = types.NullDurationFrom(exportIntervalDuration)
376+
}
377+
378+
if exporterHeaders, ok := env["OTEL_EXPORTER_OTLP_HEADERS"]; ok {
379+
stdCfg.Headers = null.StringFrom(exporterHeaders)
380+
}
381+
382+
if exporterCertificate, ok := env["OTEL_EXPORTER_OTLP_CERTIFICATE"]; ok {
383+
stdCfg.TLSCertificate = null.StringFrom(exporterCertificate)
384+
}
385+
386+
if exporterClientCertificate, ok := env["OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE"]; ok {
387+
stdCfg.TLSClientCertificate = null.StringFrom(exporterClientCertificate)
388+
}
389+
390+
if exporterClientKey, ok := env["OTEL_EXPORTER_OTLP_CLIENT_KEY"]; ok {
391+
stdCfg.TLSClientKey = null.StringFrom(exporterClientKey)
392+
}
393+
394+
if exporterInsecure, ok := env["OTEL_EXPORTER_OTLP_INSECURE"]; ok {
395+
exporterInsecureBool, err := strconv.ParseBool(exporterInsecure)
396+
if err != nil {
397+
return Config{}, err
398+
}
399+
400+
stdCfg.GRPCExporterInsecure = null.BoolFrom(exporterInsecureBool)
401+
stdCfg.HTTPExporterInsecure = null.BoolFrom(exporterInsecureBool)
402+
}
403+
404+
if exporterEndpoint, ok := env["OTEL_EXPORTER_OTLP_ENDPOINT"]; ok {
405+
stdCfg.GRPCExporterEndpoint = null.StringFrom(exporterEndpoint)
406+
stdCfg.HTTPExporterEndpoint = null.StringFrom(exporterEndpoint)
407+
}
408+
409+
return defaultCfg.Apply(stdCfg), nil
410+
}
411+
412+
// warnIfConfigMismatch writes a warning log message in case of discrepancies between the configured
413+
// Config.ExporterProtocol and other configuration attributes. E.g., in case the user explicitly configured
414+
// Config.HTTPExporterEndpoint while the chosen protocol is `grpc`.
415+
func warnIfConfigMismatch(cfg Config, logger logrus.FieldLogger) {
416+
exporterProtocol := cfg.ExporterProtocol.String
417+
switch {
418+
case exporterProtocol == grpcExporterProtocol && anyHTTPOptionSet(cfg):
419+
logger.Warn("Configuration mismatch detected: the gRPC exporter type is set, but also some HTTP " +
420+
"configuration options")
421+
case exporterProtocol == httpExporterProtocol && anyGRPCOptionSet(cfg):
422+
logger.Warn("Configuration mismatch detected: the HTTP exporter type is set, but also some gRPC " +
423+
"configuration options")
424+
}
425+
}
426+
427+
func anyHTTPOptionSet(cfg Config) bool {
428+
return cfg.HTTPExporterInsecure.Valid ||
429+
cfg.HTTPExporterEndpoint.Valid ||
430+
cfg.HTTPExporterURLPath.Valid
431+
}
432+
433+
func anyGRPCOptionSet(cfg Config) bool {
434+
return cfg.GRPCExporterInsecure.Valid ||
435+
cfg.GRPCExporterEndpoint.Valid
436+
}

internal/output/opentelemetry/config_test.go

Lines changed: 116 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@ package opentelemetry
22

33
import (
44
"encoding/json"
5+
"io"
56
"testing"
67
"time"
78

9+
"github.com/sirupsen/logrus"
810
"github.com/stretchr/testify/require"
911
"go.k6.io/k6/internal/build"
1012
"go.k6.io/k6/lib/types"
@@ -37,6 +39,37 @@ func TestConfig(t *testing.T) {
3739
},
3840
},
3941

42+
"OTLP exporter env vars overwrite defaults": {
43+
env: map[string]string{
44+
"OTEL_SERVICE_NAME": "k6-test",
45+
"OTEL_EXPORTER_OTLP_PROTOCOL": httpExporterProtocol,
46+
"OTEL_METRIC_EXPORT_INTERVAL": "1m",
47+
"OTEL_EXPORTER_OTLP_HEADERS": "k1=v1,k2=v2",
48+
"OTEL_EXPORTER_OTLP_CERTIFICATE": "fake-certificate",
49+
"OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE": "fake-client-certificate",
50+
"OTEL_EXPORTER_OTLP_CLIENT_KEY": "fake-client-key",
51+
"OTEL_EXPORTER_OTLP_INSECURE": "true",
52+
"OTEL_EXPORTER_OTLP_ENDPOINT": "localhost:4317",
53+
},
54+
expectedConfig: Config{
55+
ServiceName: null.StringFrom("k6-test"),
56+
ServiceVersion: null.NewString(build.Version, false),
57+
ExporterProtocol: null.StringFrom(httpExporterProtocol),
58+
HTTPExporterInsecure: null.BoolFrom(true),
59+
HTTPExporterEndpoint: null.StringFrom("localhost:4317"),
60+
HTTPExporterURLPath: null.NewString("/v1/metrics", false),
61+
GRPCExporterInsecure: null.BoolFrom(true),
62+
GRPCExporterEndpoint: null.StringFrom("localhost:4317"),
63+
ExportInterval: types.NullDurationFrom(1 * time.Minute),
64+
FlushInterval: types.NewNullDuration(1*time.Second, false),
65+
SingleCounterForRate: null.NewBool(true, false),
66+
Headers: null.StringFrom("k1=v1,k2=v2"),
67+
TLSCertificate: null.StringFrom("fake-certificate"),
68+
TLSClientCertificate: null.StringFrom("fake-client-certificate"),
69+
TLSClientKey: null.StringFrom("fake-client-key"),
70+
},
71+
},
72+
4073
"environment success merge": {
4174
env: map[string]string{"K6_OTEL_GRPC_EXPORTER_ENDPOINT": "else", "K6_OTEL_EXPORT_INTERVAL": "4ms"},
4275
expectedConfig: Config{
@@ -95,22 +128,45 @@ func TestConfig(t *testing.T) {
95128
},
96129
},
97130

98-
"OTEL environment variables": {
131+
"OTLP exporter env vars overwritten by k6 env vars": {
99132
env: map[string]string{
100-
"OTEL_SERVICE_NAME": "otel-service",
133+
"OTEL_SERVICE_NAME": "k6-test",
134+
"K6_OTEL_SERVICE_NAME": "foo",
135+
"OTEL_EXPORTER_OTLP_PROTOCOL": httpExporterProtocol,
136+
"K6_OTEL_EXPORTER_PROTOCOL": grpcExporterProtocol,
137+
"OTEL_METRIC_EXPORT_INTERVAL": "1m",
138+
"K6_OTEL_EXPORT_INTERVAL": "4ms",
139+
"OTEL_EXPORTER_OTLP_HEADERS": "k1=v1,k2=v2",
140+
"K6_OTEL_HEADERS": "key1=value1,key2=value2",
141+
"OTEL_EXPORTER_OTLP_CERTIFICATE": "fake-certificate",
142+
"K6_OTEL_TLS_CERTIFICATE": "cert_path",
143+
"OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE": "fake-client-certificate",
144+
"K6_OTEL_TLS_CLIENT_CERTIFICATE": "client_cert_path",
145+
"OTEL_EXPORTER_OTLP_CLIENT_KEY": "fake-client-key",
146+
"K6_OTEL_TLS_CLIENT_KEY": "client_key_path",
147+
"OTEL_EXPORTER_OTLP_INSECURE": "true",
148+
"K6_OTEL_HTTP_EXPORTER_INSECURE": "false",
149+
"K6_OTEL_GRPC_EXPORTER_INSECURE": "false",
150+
"OTEL_EXPORTER_OTLP_ENDPOINT": "localhost:4317",
151+
"K6_OTEL_HTTP_EXPORTER_ENDPOINT": "localhost:4318",
152+
"K6_OTEL_GRPC_EXPORTER_ENDPOINT": "localhost:4318",
101153
},
102154
expectedConfig: Config{
103-
ServiceName: null.NewString("otel-service", true),
155+
ServiceName: null.StringFrom("foo"),
104156
ServiceVersion: null.NewString(build.Version, false),
105-
ExporterProtocol: null.NewString(grpcExporterProtocol, false),
106-
HTTPExporterInsecure: null.NewBool(false, false),
107-
HTTPExporterEndpoint: null.NewString("localhost:4318", false),
157+
ExporterProtocol: null.StringFrom(grpcExporterProtocol),
158+
HTTPExporterInsecure: null.BoolFrom(false),
159+
HTTPExporterEndpoint: null.StringFrom("localhost:4318"),
108160
HTTPExporterURLPath: null.NewString("/v1/metrics", false),
109-
GRPCExporterInsecure: null.NewBool(false, false),
110-
GRPCExporterEndpoint: null.NewString("localhost:4317", false),
111-
ExportInterval: types.NewNullDuration(10*time.Second, false),
161+
GRPCExporterInsecure: null.BoolFrom(false),
162+
GRPCExporterEndpoint: null.StringFrom("localhost:4318"),
163+
ExportInterval: types.NullDurationFrom(4 * time.Millisecond),
112164
FlushInterval: types.NewNullDuration(1*time.Second, false),
113165
SingleCounterForRate: null.NewBool(true, false),
166+
Headers: null.StringFrom("key1=value1,key2=value2"),
167+
TLSCertificate: null.StringFrom("cert_path"),
168+
TLSClientCertificate: null.StringFrom("client_cert_path"),
169+
TLSClientKey: null.StringFrom("client_key_path"),
114170
},
115171
},
116172

@@ -157,6 +213,52 @@ func TestConfig(t *testing.T) {
157213
},
158214
},
159215

216+
"OTLP exporter env vars overwritten by JSON config": {
217+
jsonRaw: json.RawMessage(
218+
`{` +
219+
`"serviceName":"foo",` +
220+
`"exporterProtocol":"grpc",` +
221+
`"exportInterval":"15ms",` +
222+
`"httpExporterInsecure":false,` +
223+
`"httpExporterEndpoint":"localhost:4318",` +
224+
`"grpcExporterInsecure":false,` +
225+
`"grpcExporterEndpoint":"localhost:4318",` +
226+
`"tlsCertificate":"cert_path",` +
227+
`"tlsClientCertificate":"client_cert_path",` +
228+
`"tlsClientKey":"client_key_path",` +
229+
`"headers":"key1=value1,key2=value2"` +
230+
`}`,
231+
),
232+
env: map[string]string{
233+
"OTEL_SERVICE_NAME": "k6-test",
234+
"OTEL_EXPORTER_OTLP_PROTOCOL": httpExporterProtocol,
235+
"OTEL_METRIC_EXPORT_INTERVAL": "1m",
236+
"OTEL_EXPORTER_OTLP_HEADERS": "k1=v1,k2=v2",
237+
"OTEL_EXPORTER_OTLP_CERTIFICATE": "fake-certificate",
238+
"OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE": "fake-client-certificate",
239+
"OTEL_EXPORTER_OTLP_CLIENT_KEY": "fake-client-key",
240+
"OTEL_EXPORTER_OTLP_INSECURE": "true",
241+
"OTEL_EXPORTER_OTLP_ENDPOINT": "localhost:4317",
242+
},
243+
expectedConfig: Config{
244+
ServiceName: null.StringFrom("foo"),
245+
ServiceVersion: null.NewString(build.Version, false),
246+
ExporterProtocol: null.StringFrom(grpcExporterProtocol),
247+
HTTPExporterInsecure: null.BoolFrom(false),
248+
HTTPExporterEndpoint: null.StringFrom("localhost:4318"),
249+
HTTPExporterURLPath: null.NewString("/v1/metrics", false),
250+
GRPCExporterInsecure: null.BoolFrom(false),
251+
GRPCExporterEndpoint: null.StringFrom("localhost:4318"),
252+
ExportInterval: types.NullDurationFrom(15 * time.Millisecond),
253+
FlushInterval: types.NewNullDuration(1*time.Second, false),
254+
SingleCounterForRate: null.NewBool(true, false),
255+
Headers: null.StringFrom("key1=value1,key2=value2"),
256+
TLSCertificate: null.StringFrom("cert_path"),
257+
TLSClientCertificate: null.StringFrom("client_cert_path"),
258+
TLSClientKey: null.StringFrom("client_key_path"),
259+
},
260+
},
261+
160262
"JSON success merge": {
161263
jsonRaw: json.RawMessage(`{"exporterType":"http","httpExporterEndpoint":"localhost:5566","httpExporterURLPath":"/lorem/ipsum","exportInterval":"15ms"}`),
162264
expectedConfig: Config{
@@ -213,7 +315,11 @@ func TestConfig(t *testing.T) {
213315
for name, testCase := range testCases {
214316
t.Run(name, func(t *testing.T) {
215317
t.Parallel()
216-
config, err := GetConsolidatedConfig(testCase.jsonRaw, testCase.env)
318+
319+
logger := logrus.New()
320+
logger.SetOutput(io.Discard)
321+
322+
config, err := GetConsolidatedConfig(testCase.jsonRaw, testCase.env, logger)
217323
if testCase.err != "" {
218324
require.Error(t, err)
219325
require.Contains(t, err.Error(), testCase.err)

internal/output/opentelemetry/output.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ var _ output.WithStopWithTestError = new(Output)
3333

3434
// New creates an instance of the collector
3535
func New(p output.Params) (*Output, error) {
36-
conf, err := GetConsolidatedConfig(p.JSONConfig, p.Environment)
36+
conf, err := GetConsolidatedConfig(p.JSONConfig, p.Environment, p.Logger)
3737
if err != nil {
3838
return nil, err
3939
}

0 commit comments

Comments
 (0)