From 1384f90b1ef03ef00874f3c099f10fc2959f5657 Mon Sep 17 00:00:00 2001 From: Nicolai Ommer Date: Fri, 8 May 2026 15:08:41 +0200 Subject: [PATCH 1/2] feat: Default OTEL_SEMCONV_STABILITY_OPT_IN to gen_ai_latest_experimental Set the env var on agent and tool server deployments so OpenTelemetry GenAI instrumentation emits the latest semantic conventions out of the box. Users can still override the value via Spec.Env. --- internal/controller/agent_config.go | 9 +++++ internal/controller/agent_config_test.go | 39 ++++++++++++++++--- internal/controller/toolserver_reconciler.go | 13 ++++++- .../controller/toolserver_reconciler_test.go | 4 ++ 4 files changed, 58 insertions(+), 7 deletions(-) diff --git a/internal/controller/agent_config.go b/internal/controller/agent_config.go index 21372e3..e783e2c 100644 --- a/internal/controller/agent_config.go +++ b/internal/controller/agent_config.go @@ -36,6 +36,7 @@ import ( // - AGENT_DESCRIPTION: Set to spec.Description (empty string if not specified) // - AGENT_INSTRUCTION: Set to spec.Instruction (empty string if not specified) // - AGENT_MODEL: Set to spec.Model (empty string if not specified) +// - OTEL_SEMCONV_STABILITY_OPT_IN: Defaults to "gen_ai_latest_experimental" (overridable via Spec.Env) // - SUB_AGENTS: JSON-encoded map of sub-agent configurations (empty object if none) // - AGENT_TOOLS: JSON-encoded map of MCP tool configurations (empty object if none) // @@ -80,6 +81,14 @@ func buildTemplateEnvironmentVars(agent *runtimev1alpha1.Agent, resolvedSubAgent Value: agent.Spec.Model, }) + // OTEL_SEMCONV_STABILITY_OPT_IN - default to gen_ai_latest_experimental so agent + // instrumentation emits the latest GenAI semantic conventions. Users can override + // via Spec.Env. + templateEnvVars = append(templateEnvVars, corev1.EnvVar{ + Name: "OTEL_SEMCONV_STABILITY_OPT_IN", + Value: "gen_ai_latest_experimental", + }) + // AGENT_A2A_RPC_URL - construct URL from A2A protocol if present a2aUrl := buildA2AAgentRpcUrl(agent) if a2aUrl != "" { diff --git a/internal/controller/agent_config_test.go b/internal/controller/agent_config_test.go index b96db6b..f07e9ef 100644 --- a/internal/controller/agent_config_test.go +++ b/internal/controller/agent_config_test.go @@ -38,7 +38,7 @@ var _ = Describe("Agent Config", func() { envVars, err := buildTemplateEnvironmentVars(agent, map[string]ResolvedSubAgent{}, map[string]ResolvedTool{}, nil) Expect(err).NotTo(HaveOccurred()) - Expect(envVars).To(HaveLen(6)) + Expect(envVars).To(HaveLen(7)) // Verify all required template variables are present with correct values agentNameVar := findEnvVar(envVars, "AGENT_NAME") @@ -57,6 +57,10 @@ var _ = Describe("Agent Config", func() { Expect(agentModelVar).NotTo(BeNil()) Expect(agentModelVar.Value).To(Equal("")) + otelSemconvVar := findEnvVar(envVars, "OTEL_SEMCONV_STABILITY_OPT_IN") + Expect(otelSemconvVar).NotTo(BeNil()) + Expect(otelSemconvVar.Value).To(Equal("gen_ai_latest_experimental")) + subAgentsVar := findEnvVar(envVars, "SUB_AGENTS") Expect(subAgentsVar).NotTo(BeNil()) Expect(subAgentsVar.Value).To(Equal("{}")) @@ -101,7 +105,7 @@ var _ = Describe("Agent Config", func() { envVars, err := buildTemplateEnvironmentVars(agent, resolvedSubAgents, resolvedTools, nil) Expect(err).NotTo(HaveOccurred()) - Expect(envVars).To(HaveLen(6)) + Expect(envVars).To(HaveLen(7)) agentDescVar := findEnvVar(envVars, "AGENT_DESCRIPTION") Expect(agentDescVar.Value).To(Equal("Test agent description")) @@ -291,8 +295,8 @@ var _ = Describe("Agent Config", func() { envVars, err := buildTemplateEnvironmentVars(agent, map[string]ResolvedSubAgent{}, map[string]ResolvedTool{}, &gatewayUrl) Expect(err).NotTo(HaveOccurred()) - // Should have base variables (6) + LiteLLM variables (3) = 9 total - Expect(envVars).To(HaveLen(9)) + // Should have base variables (7) + LiteLLM variables (3) = 10 total + Expect(envVars).To(HaveLen(10)) // Verify LITELLM_PROXY_API_BASE is set proxyBaseVar := findEnvVar(envVars, "LITELLM_PROXY_API_BASE") @@ -322,8 +326,8 @@ var _ = Describe("Agent Config", func() { envVars, err := buildTemplateEnvironmentVars(agent, map[string]ResolvedSubAgent{}, map[string]ResolvedTool{}, nil) Expect(err).NotTo(HaveOccurred()) - // Should only have base variables (6), no LiteLLM variables - Expect(envVars).To(HaveLen(6)) + // Should only have base variables (7), no LiteLLM variables + Expect(envVars).To(HaveLen(7)) // Verify LiteLLM variables are NOT present Expect(findEnvVar(envVars, "LITELLM_PROXY_API_BASE")).To(BeNil()) @@ -365,6 +369,29 @@ var _ = Describe("Agent Config", func() { Expect(proxyBaseVar).NotTo(BeNil()) Expect(proxyBaseVar.Value).To(Equal("http://ai-gateway.default.svc.cluster.local:4000")) }) + + It("should allow user to override OTEL_SEMCONV_STABILITY_OPT_IN", func() { + agent := &runtimev1alpha1.Agent{ + ObjectMeta: metav1.ObjectMeta{ + Name: "otel-override-agent", + Namespace: "default", + }, + Spec: runtimev1alpha1.AgentSpec{}, + } + + templateVars, err := buildTemplateEnvironmentVars(agent, map[string]ResolvedSubAgent{}, map[string]ResolvedTool{}, nil) + Expect(err).NotTo(HaveOccurred()) + + userVars := []corev1.EnvVar{ + {Name: "OTEL_SEMCONV_STABILITY_OPT_IN", Value: "http"}, + } + + result := mergeEnvironmentVariables(templateVars, userVars) + + otelSemconvVar := findEnvVar(result, "OTEL_SEMCONV_STABILITY_OPT_IN") + Expect(otelSemconvVar).NotTo(BeNil()) + Expect(otelSemconvVar.Value).To(Equal("http")) + }) }) Describe("mergeEnvironmentVariables", func() { diff --git a/internal/controller/toolserver_reconciler.go b/internal/controller/toolserver_reconciler.go index 71ffcf3..a6cee0e 100644 --- a/internal/controller/toolserver_reconciler.go +++ b/internal/controller/toolserver_reconciler.go @@ -43,6 +43,17 @@ const ( sseTransport = "sse" ) +// defaultToolServerEnvVars returns environment variables that are set on every +// ToolServer container by default. Users can override any of these via Spec.Env. +func defaultToolServerEnvVars() []corev1.EnvVar { + return []corev1.EnvVar{ + { + Name: "OTEL_SEMCONV_STABILITY_OPT_IN", + Value: "gen_ai_latest_experimental", + }, + } +} + // ToolServerReconciler reconciles a ToolServer object type ToolServerReconciler struct { client.Client @@ -214,7 +225,7 @@ func (r *ToolServerReconciler) ensureDeployment(ctx context.Context, toolServer container.Command = toolServer.Spec.Command container.Args = toolServer.Spec.Args container.Ports = containerPorts - container.Env = toolServer.Spec.Env + container.Env = mergeEnvironmentVariables(defaultToolServerEnvVars(), toolServer.Spec.Env) container.EnvFrom = toolServer.Spec.EnvFrom container.ReadinessProbe = r.buildReadinessProbe(toolServer.Spec.Port) container.Resources = getOrDefaultToolServerResourceRequirements(toolServer) diff --git a/internal/controller/toolserver_reconciler_test.go b/internal/controller/toolserver_reconciler_test.go index 7e5843f..a5e4248 100644 --- a/internal/controller/toolserver_reconciler_test.go +++ b/internal/controller/toolserver_reconciler_test.go @@ -111,6 +111,10 @@ var _ = Describe("ToolServer Controller", func() { Expect(container.Ports).To(HaveLen(1)) Expect(container.Ports[0].ContainerPort).To(Equal(int32(8080))) Expect(container.Env).To(ContainElement(corev1.EnvVar{Name: "LOGLEVEL", Value: "INFO"})) + Expect(container.Env).To(ContainElement(corev1.EnvVar{ + Name: "OTEL_SEMCONV_STABILITY_OPT_IN", + Value: "gen_ai_latest_experimental", + })) Expect(container.ReadinessProbe).NotTo(BeNil()) Expect(container.ReadinessProbe.TCPSocket).NotTo(BeNil()) Expect(container.ReadinessProbe.TCPSocket.Port.IntValue()).To(Equal(8080)) From f3a726d085220c3ae7843a488ee5f8fc9a762c64 Mon Sep 17 00:00:00 2001 From: Nicolai Ommer Date: Fri, 8 May 2026 15:13:11 +0200 Subject: [PATCH 2/2] feat: Default OTEL_SERVICE_NAME to agent / tool server name Default OTEL_SERVICE_NAME on agent and tool server deployments to the resource's metadata.name so spans and metrics are attributed to the right service. Users can still override the value via Spec.Env. --- internal/controller/agent_config.go | 8 ++++ internal/controller/agent_config_test.go | 42 ++++++++++++++++--- internal/controller/toolserver_reconciler.go | 8 +++- .../controller/toolserver_reconciler_test.go | 4 ++ 4 files changed, 54 insertions(+), 8 deletions(-) diff --git a/internal/controller/agent_config.go b/internal/controller/agent_config.go index e783e2c..9bf3e0e 100644 --- a/internal/controller/agent_config.go +++ b/internal/controller/agent_config.go @@ -37,6 +37,7 @@ import ( // - AGENT_INSTRUCTION: Set to spec.Instruction (empty string if not specified) // - AGENT_MODEL: Set to spec.Model (empty string if not specified) // - OTEL_SEMCONV_STABILITY_OPT_IN: Defaults to "gen_ai_latest_experimental" (overridable via Spec.Env) +// - OTEL_SERVICE_NAME: Defaults to the agent's name (overridable via Spec.Env) // - SUB_AGENTS: JSON-encoded map of sub-agent configurations (empty object if none) // - AGENT_TOOLS: JSON-encoded map of MCP tool configurations (empty object if none) // @@ -89,6 +90,13 @@ func buildTemplateEnvironmentVars(agent *runtimev1alpha1.Agent, resolvedSubAgent Value: "gen_ai_latest_experimental", }) + // OTEL_SERVICE_NAME - default to the agent's name so traces are attributed + // to the right service. Users can override via Spec.Env. + templateEnvVars = append(templateEnvVars, corev1.EnvVar{ + Name: "OTEL_SERVICE_NAME", + Value: agent.Name, + }) + // AGENT_A2A_RPC_URL - construct URL from A2A protocol if present a2aUrl := buildA2AAgentRpcUrl(agent) if a2aUrl != "" { diff --git a/internal/controller/agent_config_test.go b/internal/controller/agent_config_test.go index f07e9ef..4b59429 100644 --- a/internal/controller/agent_config_test.go +++ b/internal/controller/agent_config_test.go @@ -38,7 +38,7 @@ var _ = Describe("Agent Config", func() { envVars, err := buildTemplateEnvironmentVars(agent, map[string]ResolvedSubAgent{}, map[string]ResolvedTool{}, nil) Expect(err).NotTo(HaveOccurred()) - Expect(envVars).To(HaveLen(7)) + Expect(envVars).To(HaveLen(8)) // Verify all required template variables are present with correct values agentNameVar := findEnvVar(envVars, "AGENT_NAME") @@ -61,6 +61,10 @@ var _ = Describe("Agent Config", func() { Expect(otelSemconvVar).NotTo(BeNil()) Expect(otelSemconvVar.Value).To(Equal("gen_ai_latest_experimental")) + otelServiceNameVar := findEnvVar(envVars, "OTEL_SERVICE_NAME") + Expect(otelServiceNameVar).NotTo(BeNil()) + Expect(otelServiceNameVar.Value).To(Equal("test-agent")) + subAgentsVar := findEnvVar(envVars, "SUB_AGENTS") Expect(subAgentsVar).NotTo(BeNil()) Expect(subAgentsVar.Value).To(Equal("{}")) @@ -105,7 +109,7 @@ var _ = Describe("Agent Config", func() { envVars, err := buildTemplateEnvironmentVars(agent, resolvedSubAgents, resolvedTools, nil) Expect(err).NotTo(HaveOccurred()) - Expect(envVars).To(HaveLen(7)) + Expect(envVars).To(HaveLen(8)) agentDescVar := findEnvVar(envVars, "AGENT_DESCRIPTION") Expect(agentDescVar.Value).To(Equal("Test agent description")) @@ -295,8 +299,8 @@ var _ = Describe("Agent Config", func() { envVars, err := buildTemplateEnvironmentVars(agent, map[string]ResolvedSubAgent{}, map[string]ResolvedTool{}, &gatewayUrl) Expect(err).NotTo(HaveOccurred()) - // Should have base variables (7) + LiteLLM variables (3) = 10 total - Expect(envVars).To(HaveLen(10)) + // Should have base variables (8) + LiteLLM variables (3) = 11 total + Expect(envVars).To(HaveLen(11)) // Verify LITELLM_PROXY_API_BASE is set proxyBaseVar := findEnvVar(envVars, "LITELLM_PROXY_API_BASE") @@ -326,8 +330,8 @@ var _ = Describe("Agent Config", func() { envVars, err := buildTemplateEnvironmentVars(agent, map[string]ResolvedSubAgent{}, map[string]ResolvedTool{}, nil) Expect(err).NotTo(HaveOccurred()) - // Should only have base variables (7), no LiteLLM variables - Expect(envVars).To(HaveLen(7)) + // Should only have base variables (8), no LiteLLM variables + Expect(envVars).To(HaveLen(8)) // Verify LiteLLM variables are NOT present Expect(findEnvVar(envVars, "LITELLM_PROXY_API_BASE")).To(BeNil()) @@ -392,6 +396,32 @@ var _ = Describe("Agent Config", func() { Expect(otelSemconvVar).NotTo(BeNil()) Expect(otelSemconvVar.Value).To(Equal("http")) }) + + It("should allow user to override OTEL_SERVICE_NAME", func() { + agent := &runtimev1alpha1.Agent{ + ObjectMeta: metav1.ObjectMeta{ + Name: "service-name-agent", + Namespace: "default", + }, + Spec: runtimev1alpha1.AgentSpec{}, + } + + templateVars, err := buildTemplateEnvironmentVars(agent, map[string]ResolvedSubAgent{}, map[string]ResolvedTool{}, nil) + Expect(err).NotTo(HaveOccurred()) + + // Default uses agent name + Expect(findEnvVar(templateVars, "OTEL_SERVICE_NAME").Value).To(Equal("service-name-agent")) + + userVars := []corev1.EnvVar{ + {Name: "OTEL_SERVICE_NAME", Value: "custom-service"}, + } + + result := mergeEnvironmentVariables(templateVars, userVars) + + otelServiceNameVar := findEnvVar(result, "OTEL_SERVICE_NAME") + Expect(otelServiceNameVar).NotTo(BeNil()) + Expect(otelServiceNameVar.Value).To(Equal("custom-service")) + }) }) Describe("mergeEnvironmentVariables", func() { diff --git a/internal/controller/toolserver_reconciler.go b/internal/controller/toolserver_reconciler.go index a6cee0e..daf96ff 100644 --- a/internal/controller/toolserver_reconciler.go +++ b/internal/controller/toolserver_reconciler.go @@ -45,12 +45,16 @@ const ( // defaultToolServerEnvVars returns environment variables that are set on every // ToolServer container by default. Users can override any of these via Spec.Env. -func defaultToolServerEnvVars() []corev1.EnvVar { +func defaultToolServerEnvVars(toolServer *runtimev1alpha1.ToolServer) []corev1.EnvVar { return []corev1.EnvVar{ { Name: "OTEL_SEMCONV_STABILITY_OPT_IN", Value: "gen_ai_latest_experimental", }, + { + Name: "OTEL_SERVICE_NAME", + Value: toolServer.Name, + }, } } @@ -225,7 +229,7 @@ func (r *ToolServerReconciler) ensureDeployment(ctx context.Context, toolServer container.Command = toolServer.Spec.Command container.Args = toolServer.Spec.Args container.Ports = containerPorts - container.Env = mergeEnvironmentVariables(defaultToolServerEnvVars(), toolServer.Spec.Env) + container.Env = mergeEnvironmentVariables(defaultToolServerEnvVars(toolServer), toolServer.Spec.Env) container.EnvFrom = toolServer.Spec.EnvFrom container.ReadinessProbe = r.buildReadinessProbe(toolServer.Spec.Port) container.Resources = getOrDefaultToolServerResourceRequirements(toolServer) diff --git a/internal/controller/toolserver_reconciler_test.go b/internal/controller/toolserver_reconciler_test.go index a5e4248..43b5346 100644 --- a/internal/controller/toolserver_reconciler_test.go +++ b/internal/controller/toolserver_reconciler_test.go @@ -115,6 +115,10 @@ var _ = Describe("ToolServer Controller", func() { Name: "OTEL_SEMCONV_STABILITY_OPT_IN", Value: "gen_ai_latest_experimental", })) + Expect(container.Env).To(ContainElement(corev1.EnvVar{ + Name: "OTEL_SERVICE_NAME", + Value: resourceName, + })) Expect(container.ReadinessProbe).NotTo(BeNil()) Expect(container.ReadinessProbe.TCPSocket).NotTo(BeNil()) Expect(container.ReadinessProbe.TCPSocket.Port.IntValue()).To(Equal(8080))