diff --git a/internal/controller/agent_config.go b/internal/controller/agent_config.go index 21372e3..9bf3e0e 100644 --- a/internal/controller/agent_config.go +++ b/internal/controller/agent_config.go @@ -36,6 +36,8 @@ 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) +// - 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) // @@ -80,6 +82,21 @@ 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", + }) + + // 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 b96db6b..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(6)) + Expect(envVars).To(HaveLen(8)) // Verify all required template variables are present with correct values agentNameVar := findEnvVar(envVars, "AGENT_NAME") @@ -57,6 +57,14 @@ 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")) + + 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("{}")) @@ -101,7 +109,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(8)) agentDescVar := findEnvVar(envVars, "AGENT_DESCRIPTION") Expect(agentDescVar.Value).To(Equal("Test agent description")) @@ -291,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 (6) + LiteLLM variables (3) = 9 total - Expect(envVars).To(HaveLen(9)) + // 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") @@ -322,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 (6), no LiteLLM variables - Expect(envVars).To(HaveLen(6)) + // 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()) @@ -365,6 +373,55 @@ 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")) + }) + + 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 71ffcf3..daf96ff 100644 --- a/internal/controller/toolserver_reconciler.go +++ b/internal/controller/toolserver_reconciler.go @@ -43,6 +43,21 @@ 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(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, + }, + } +} + // ToolServerReconciler reconciles a ToolServer object type ToolServerReconciler struct { client.Client @@ -214,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 = 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 7e5843f..43b5346 100644 --- a/internal/controller/toolserver_reconciler_test.go +++ b/internal/controller/toolserver_reconciler_test.go @@ -111,6 +111,14 @@ 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.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))