diff --git a/pkg/reconciler/eventtransform/eventtransform_test.go b/pkg/reconciler/eventtransform/eventtransform_test.go index d5db5a464ae..cec039cda7c 100644 --- a/pkg/reconciler/eventtransform/eventtransform_test.go +++ b/pkg/reconciler/eventtransform/eventtransform_test.go @@ -2297,6 +2297,264 @@ func eventJsonataCertificateDeleted() string { return Eventf(corev1.EventTypeNormal, "JsonataCertificateDeleted", fmt.Sprintf("%s-%s", testName, "jsonata")) } +func TestOtelTracingEnvVars(t *testing.T) { + ctx := context.Background() + logger := logtesting.TestLogger(t) + ctx = logging.WithLogger(ctx, logger) + + tests := []struct { + name string + cwFactory func() *reconcilersource.ConfigWatcher + wantEnvVars []corev1.EnvVar + }{ + { + name: "nil config watcher returns nil", + cwFactory: func() *reconcilersource.ConfigWatcher { return nil }, + wantEnvVars: nil, + }, + { + name: "empty observability config returns nil", + cwFactory: func() *reconcilersource.ConfigWatcher { + return reconcilersource.WatchConfigurations(ctx, "test", + configmap.NewStaticWatcher( + &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "config-logging"}}, + &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "config-observability"}}, + ), + ) + }, + wantEnvVars: nil, + }, + { + name: "stdout protocol returns nil", + cwFactory: func() *reconcilersource.ConfigWatcher { + return reconcilersource.WatchConfigurations(ctx, "test", + configmap.NewStaticWatcher( + &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "config-logging"}}, + &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Name: "config-observability"}, + Data: map[string]string{"tracing-protocol": "stdout"}, + }, + ), + ) + }, + wantEnvVars: nil, + }, + { + name: "grpc protocol with sampling rate 0 sets always_off", + cwFactory: func() *reconcilersource.ConfigWatcher { + return reconcilersource.WatchConfigurations(ctx, "test", + configmap.NewStaticWatcher( + &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "config-logging"}}, + &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Name: "config-observability"}, + Data: map[string]string{ + "tracing-protocol": "grpc", + "tracing-endpoint": "http://otel-collector:4317", + "tracing-sampling-rate": "0", + }, + }, + ), + ) + }, + wantEnvVars: []corev1.EnvVar{ + {Name: OTELExporterProtocolEnv, Value: "grpc"}, + {Name: OTELExporterEndpointEnv, Value: "http://otel-collector:4317"}, + {Name: OTELTracesSamplerEnv, Value: "always_off"}, + }, + }, + { + name: "grpc protocol with sampling rate 1 sets always_on", + cwFactory: func() *reconcilersource.ConfigWatcher { + return reconcilersource.WatchConfigurations(ctx, "test", + configmap.NewStaticWatcher( + &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "config-logging"}}, + &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Name: "config-observability"}, + Data: map[string]string{ + "tracing-protocol": "grpc", + "tracing-endpoint": "http://otel-collector:4317", + "tracing-sampling-rate": "1", + }, + }, + ), + ) + }, + wantEnvVars: []corev1.EnvVar{ + {Name: OTELExporterProtocolEnv, Value: "grpc"}, + {Name: OTELExporterEndpointEnv, Value: "http://otel-collector:4317"}, + {Name: OTELTracesSamplerEnv, Value: "always_on"}, + }, + }, + { + name: "grpc protocol with sampling rate 0.5 sets parentbased_traceidratio", + cwFactory: func() *reconcilersource.ConfigWatcher { + return reconcilersource.WatchConfigurations(ctx, "test", + configmap.NewStaticWatcher( + &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "config-logging"}}, + &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Name: "config-observability"}, + Data: map[string]string{ + "tracing-protocol": "grpc", + "tracing-endpoint": "http://otel-collector:4317", + "tracing-sampling-rate": "0.5", + }, + }, + ), + ) + }, + wantEnvVars: []corev1.EnvVar{ + {Name: OTELExporterProtocolEnv, Value: "grpc"}, + {Name: OTELExporterEndpointEnv, Value: "http://otel-collector:4317"}, + {Name: OTELTracesSamplerEnv, Value: "parentbased_traceidratio"}, + {Name: OTELTracesSamplerArgEnv, Value: "0.5"}, + }, + }, + { + name: "http/protobuf protocol with default sampling rate sets always_off", + cwFactory: func() *reconcilersource.ConfigWatcher { + return reconcilersource.WatchConfigurations(ctx, "test", + configmap.NewStaticWatcher( + &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "config-logging"}}, + &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Name: "config-observability"}, + Data: map[string]string{ + "tracing-protocol": "http/protobuf", + "tracing-endpoint": "http://otel-collector:4318/v1/traces", + }, + }, + ), + ) + }, + wantEnvVars: []corev1.EnvVar{ + {Name: OTELExporterProtocolEnv, Value: "http/protobuf"}, + {Name: OTELExporterEndpointEnv, Value: "http://otel-collector:4318/v1/traces"}, + {Name: OTELTracesSamplerEnv, Value: "always_off"}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cw := tt.cwFactory() + got := otelTracingEnvVars(cw) + + if len(got) != len(tt.wantEnvVars) { + t.Fatalf("otelTracingEnvVars() returned %d env vars, want %d\ngot: %+v\nwant: %+v", + len(got), len(tt.wantEnvVars), got, tt.wantEnvVars) + } + for i := range got { + if got[i].Name != tt.wantEnvVars[i].Name || got[i].Value != tt.wantEnvVars[i].Value { + t.Errorf("env var[%d] = {Name: %q, Value: %q}, want {Name: %q, Value: %q}", + i, got[i].Name, got[i].Value, tt.wantEnvVars[i].Name, tt.wantEnvVars[i].Value) + } + } + }) + } +} + +func TestJsonataDeploymentOtelEnvVars(t *testing.T) { + t.Setenv("EVENT_TRANSFORM_JSONATA_IMAGE", "quay.io/event-transform") + + ctx := context.Background() + logger := logtesting.TestLogger(t) + ctx = logging.WithLogger(ctx, logger) + + tests := []struct { + name string + tracingData map[string]string + wantOtelEnvVars map[string]string + }{ + { + name: "deployment with grpc tracing and 0.5 sampling rate", + tracingData: map[string]string{ + "tracing-protocol": "grpc", + "tracing-endpoint": "http://otel-collector:4317", + "tracing-sampling-rate": "0.5", + }, + wantOtelEnvVars: map[string]string{ + OTELExporterProtocolEnv: "grpc", + OTELExporterEndpointEnv: "http://otel-collector:4317", + OTELTracesSamplerEnv: "parentbased_traceidratio", + OTELTracesSamplerArgEnv: "0.5", + }, + }, + { + name: "deployment with grpc tracing and always_off sampling", + tracingData: map[string]string{ + "tracing-protocol": "grpc", + "tracing-endpoint": "http://otel-collector:4317", + "tracing-sampling-rate": "0", + }, + wantOtelEnvVars: map[string]string{ + OTELExporterProtocolEnv: "grpc", + OTELExporterEndpointEnv: "http://otel-collector:4317", + OTELTracesSamplerEnv: "always_off", + }, + }, + { + name: "deployment with grpc tracing and always_on sampling", + tracingData: map[string]string{ + "tracing-protocol": "grpc", + "tracing-endpoint": "http://otel-collector:4317", + "tracing-sampling-rate": "1", + }, + wantOtelEnvVars: map[string]string{ + OTELExporterProtocolEnv: "grpc", + OTELExporterEndpointEnv: "http://otel-collector:4317", + OTELTracesSamplerEnv: "always_on", + }, + }, + { + name: "deployment without tracing has no OTEL env vars", + tracingData: nil, + wantOtelEnvVars: map[string]string{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cw := reconcilersource.WatchConfigurations(ctx, "eventtransform", + configmap.NewStaticWatcher( + &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "config-logging"}}, + &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Name: "config-observability"}, + Data: tt.tracingData, + }, + ), + ) + + d := jsonataTestDeployment(ctx, cw) + envVars := d.Spec.Template.Spec.Containers[0].Env + + envMap := make(map[string]string) + for _, e := range envVars { + envMap[e.Name] = e.Value + } + + for name, wantValue := range tt.wantOtelEnvVars { + gotValue, ok := envMap[name] + if !ok { + t.Errorf("expected env var %q on deployment container, but it was not found", name) + continue + } + if gotValue != wantValue { + t.Errorf("env var %q = %q, want %q", name, gotValue, wantValue) + } + } + + // Verify no unexpected OTEL env vars when tracing is disabled + if tt.tracingData == nil { + otelEnvNames := []string{OTELExporterProtocolEnv, OTELExporterEndpointEnv, OTELTracesSamplerEnv, OTELTracesSamplerArgEnv} + for _, name := range otelEnvNames { + if _, ok := envMap[name]; ok { + t.Errorf("unexpected env var %q found on deployment container when tracing is disabled", name) + } + } + } + }) + } +} + const testAuthProxyImage = "quay.io/fake-auth-proxy" func jsonataTestDeploymentWithAuthProxy(ctx context.Context, cw *reconcilersource.ConfigWatcher, opts ...DeploymentOption) *appsv1.Deployment { diff --git a/pkg/reconciler/eventtransform/resources_jsonata.go b/pkg/reconciler/eventtransform/resources_jsonata.go index 52f188f1846..4bde3428b84 100644 --- a/pkg/reconciler/eventtransform/resources_jsonata.go +++ b/pkg/reconciler/eventtransform/resources_jsonata.go @@ -66,6 +66,13 @@ const ( JsonataTLSKeyPath = JsonataTLSVolumePath + "/" + eventingtls.TLSKey JsonataTLSCertPath = JsonataTLSVolumePath + "/" + eventingtls.TLSCrt + // OTEL environment variable names for tracing auto-configuration + // These are standard OpenTelemetry environment variables that the Node.js SDK + // automatically reads to configure the trace exporter. + OTELExporterEndpointEnv = "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT" + OTELExporterProtocolEnv = "OTEL_EXPORTER_OTLP_TRACES_PROTOCOL" + OTELTracesSamplerEnv = "OTEL_TRACES_SAMPLER" + OTELTracesSamplerArgEnv = "OTEL_TRACES_SAMPLER_ARG" JsonataAuthProxyRoleBindingName = "eventing-auth-proxy-eventtransform" ) @@ -127,15 +134,7 @@ func jsonataDeployment(ctx context.Context, authProxyImage string, withCombinedT { Name: "jsonata-event-transform", Image: image, - Env: append( - []corev1.EnvVar{ - { - Name: "JSONATA_TRANSFORM_FILE_NAME", - Value: filepath.Join(JsonataExpressionPath, JsonataExpressionDataKey), - }, - }, - cw.ToEnvVars()..., - ), + Env: jsonataContainerEnvVars(cw), VolumeMounts: []corev1.VolumeMount{ { Name: expression.GetName(), @@ -648,3 +647,85 @@ func jsonataCertificate(ctx context.Context, transform *eventing.EventTransform) ), ) } + +// jsonataContainerEnvVars builds the base environment variables for the jsonata +// container, combining the transform file path, config watcher env vars, and +// OTEL tracing env vars. +func jsonataContainerEnvVars(cw *reconcilersource.ConfigWatcher) []corev1.EnvVar { + envVars := []corev1.EnvVar{ + { + Name: "JSONATA_TRANSFORM_FILE_NAME", + Value: filepath.Join(JsonataExpressionPath, JsonataExpressionDataKey), + }, + } + envVars = append(envVars, cw.ToEnvVars()...) + envVars = append(envVars, otelTracingEnvVars(cw)...) + return envVars +} + +// otelTracingEnvVars generates standard OpenTelemetry environment variables +// based on the observability configuration. This allows the Node.js OTEL SDK +// in the transform-jsonata container to auto-configure the trace exporter +// and send traces to the configured endpoint instead of logging to stdout. +func otelTracingEnvVars(cw *reconcilersource.ConfigWatcher) []corev1.EnvVar { + if cw == nil { + return nil + } + + obsCfg := cw.ObservabilityConfig() + if obsCfg == nil { + return nil + } + + tracingCfg := obsCfg.Tracing + // Only add OTEL env vars if tracing is enabled with a real exporter + if tracingCfg.Protocol == "" || tracingCfg.Protocol == "none" { + return nil + } + + // For stdout protocol, don't set OTEL env vars - let the app use ConsoleSpanExporter + if tracingCfg.Protocol == "stdout" { + return nil + } + + envVars := []corev1.EnvVar{ + { + Name: OTELExporterProtocolEnv, + Value: tracingCfg.Protocol, + }, + } + + if tracingCfg.Endpoint != "" { + envVars = append(envVars, corev1.EnvVar{ + Name: OTELExporterEndpointEnv, + Value: tracingCfg.Endpoint, + }) + } + + // Configure sampling based on the sampling rate + switch { + case tracingCfg.SamplingRate == 0: + envVars = append(envVars, corev1.EnvVar{ + Name: OTELTracesSamplerEnv, + Value: "always_off", + }) + case tracingCfg.SamplingRate == 1: + envVars = append(envVars, corev1.EnvVar{ + Name: OTELTracesSamplerEnv, + Value: "always_on", + }) + case tracingCfg.SamplingRate > 0: + envVars = append(envVars, + corev1.EnvVar{ + Name: OTELTracesSamplerEnv, + Value: "parentbased_traceidratio", + }, + corev1.EnvVar{ + Name: OTELTracesSamplerArgEnv, + Value: fmt.Sprintf("%g", tracingCfg.SamplingRate), + }, + ) + } + + return envVars +}