Skip to content

Commit cc4d03d

Browse files
yroblataskbot
andauthored
add support for propagating telemetry in configmap (#1858)
Co-authored-by: taskbot <[email protected]>
1 parent df8ada4 commit cc4d03d

File tree

7 files changed

+567
-2
lines changed

7 files changed

+567
-2
lines changed

cmd/thv-operator/controllers/mcpserver_controller.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -626,8 +626,9 @@ func (r *MCPServerReconciler) deploymentForMCPServer(ctx context.Context, m *mcp
626626
}
627627
}
628628

629-
// Add OpenTelemetry configuration args
630-
if m.Spec.Telemetry != nil {
629+
// Add OpenTelemetry configuration args only if not using ConfigMap
630+
// When using ConfigMap, telemetry configuration is included in the runconfig.json
631+
if !useConfigMap && m.Spec.Telemetry != nil {
631632
if m.Spec.Telemetry.OpenTelemetry != nil {
632633
otelArgs := r.generateOpenTelemetryArgs(m)
633634
args = append(args, otelArgs...)

cmd/thv-operator/controllers/mcpserver_runconfig.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"fmt"
99
"os"
1010
"sort"
11+
"strconv"
1112
"strings"
1213

1314
corev1 "k8s.io/api/core/v1"
@@ -289,6 +290,9 @@ func (r *MCPServerReconciler) createRunConfigFromMCPServer(m *mcpv1alpha1.MCPSer
289290
}
290291
}
291292

293+
// Add telemetry configuration if specified
294+
addTelemetryConfigOptions(&options, m.Spec.Telemetry, m.Name)
295+
292296
// Use the RunConfigBuilder for operator context with full builder pattern
293297
return runner.NewOperatorRunConfigBuilder(
294298
context.Background(),
@@ -534,3 +538,76 @@ func convertSecretsFromMCPServer(secs []mcpv1alpha1.SecretRef) []string {
534538
}
535539
return secrets
536540
}
541+
542+
// addTelemetryConfigOptions adds telemetry configuration options to the builder options
543+
func addTelemetryConfigOptions(
544+
options *[]runner.RunConfigBuilderOption,
545+
telemetryConfig *mcpv1alpha1.TelemetryConfig,
546+
mcpServerName string,
547+
) {
548+
if telemetryConfig == nil {
549+
return
550+
}
551+
552+
// Default values
553+
var otelEndpoint string
554+
var otelEnablePrometheusMetricsPath bool
555+
var otelTracingEnabled bool
556+
var otelMetricsEnabled bool
557+
var otelServiceName string
558+
var otelSamplingRate = 0.05 // Default sampling rate
559+
var otelHeaders []string
560+
var otelInsecure bool
561+
var otelEnvironmentVariables []string
562+
563+
// Process OpenTelemetry configuration
564+
if telemetryConfig.OpenTelemetry != nil && telemetryConfig.OpenTelemetry.Enabled {
565+
otel := telemetryConfig.OpenTelemetry
566+
567+
// Strip http:// or https:// prefix if present, as OTLP client expects host:port format
568+
otelEndpoint = strings.TrimPrefix(strings.TrimPrefix(otel.Endpoint, "https://"), "http://")
569+
otelInsecure = otel.Insecure
570+
otelHeaders = otel.Headers
571+
572+
// Use MCPServer name as service name if not specified
573+
if otel.ServiceName != "" {
574+
otelServiceName = otel.ServiceName
575+
} else {
576+
otelServiceName = mcpServerName
577+
}
578+
579+
// Handle tracing configuration
580+
if otel.Tracing != nil {
581+
otelTracingEnabled = otel.Tracing.Enabled
582+
if otel.Tracing.SamplingRate != "" {
583+
// Parse sampling rate string to float64
584+
if rate, err := strconv.ParseFloat(otel.Tracing.SamplingRate, 64); err == nil {
585+
otelSamplingRate = rate
586+
}
587+
}
588+
}
589+
590+
// Handle metrics configuration
591+
if otel.Metrics != nil {
592+
otelMetricsEnabled = otel.Metrics.Enabled
593+
}
594+
}
595+
596+
// Process Prometheus configuration
597+
if telemetryConfig.Prometheus != nil {
598+
otelEnablePrometheusMetricsPath = telemetryConfig.Prometheus.Enabled
599+
}
600+
601+
// Add telemetry config to options
602+
*options = append(*options, runner.WithTelemetryConfig(
603+
otelEndpoint,
604+
otelEnablePrometheusMetricsPath,
605+
otelTracingEnabled,
606+
otelMetricsEnabled,
607+
otelServiceName,
608+
otelSamplingRate,
609+
otelHeaders,
610+
otelInsecure,
611+
otelEnvironmentVariables,
612+
))
613+
}

cmd/thv-operator/controllers/mcpserver_runconfig_test.go

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,124 @@ func TestCreateRunConfigFromMCPServer(t *testing.T) {
282282
assert.Len(t, config.Secrets, 0)
283283
},
284284
},
285+
{
286+
name: "with telemetry configuration",
287+
mcpServer: &mcpv1alpha1.MCPServer{
288+
ObjectMeta: metav1.ObjectMeta{
289+
Name: "telemetry-server",
290+
Namespace: "test-ns",
291+
},
292+
Spec: mcpv1alpha1.MCPServerSpec{
293+
Image: testImage,
294+
Transport: stdioTransport,
295+
Port: 8080,
296+
Telemetry: &mcpv1alpha1.TelemetryConfig{
297+
OpenTelemetry: &mcpv1alpha1.OpenTelemetryConfig{
298+
Enabled: true,
299+
Endpoint: "http://otel-collector:4317",
300+
ServiceName: "custom-service-name",
301+
Insecure: true,
302+
Headers: []string{"Authorization=Bearer token123", "X-API-Key=abc"},
303+
Tracing: &mcpv1alpha1.OpenTelemetryTracingConfig{
304+
Enabled: true,
305+
SamplingRate: "0.25",
306+
},
307+
Metrics: &mcpv1alpha1.OpenTelemetryMetricsConfig{
308+
Enabled: true,
309+
},
310+
},
311+
Prometheus: &mcpv1alpha1.PrometheusConfig{
312+
Enabled: true,
313+
},
314+
},
315+
},
316+
},
317+
//nolint:thelper // We want to see the error at the specific line
318+
expected: func(t *testing.T, config *runner.RunConfig) {
319+
assert.Equal(t, "telemetry-server", config.Name)
320+
321+
// Verify telemetry config is set
322+
assert.NotNil(t, config.TelemetryConfig)
323+
324+
// Check OpenTelemetry settings (endpoint should have http:// prefix stripped)
325+
assert.Equal(t, "otel-collector:4317", config.TelemetryConfig.Endpoint)
326+
assert.Equal(t, "custom-service-name", config.TelemetryConfig.ServiceName)
327+
assert.True(t, config.TelemetryConfig.Insecure)
328+
assert.True(t, config.TelemetryConfig.TracingEnabled)
329+
assert.True(t, config.TelemetryConfig.MetricsEnabled)
330+
assert.Equal(t, 0.25, config.TelemetryConfig.SamplingRate)
331+
assert.Equal(t, map[string]string{"Authorization": "Bearer token123", "X-API-Key": "abc"}, config.TelemetryConfig.Headers)
332+
333+
// Check Prometheus settings
334+
assert.True(t, config.TelemetryConfig.EnablePrometheusMetricsPath)
335+
},
336+
},
337+
{
338+
name: "with minimal telemetry configuration",
339+
mcpServer: &mcpv1alpha1.MCPServer{
340+
ObjectMeta: metav1.ObjectMeta{
341+
Name: "minimal-telemetry-server",
342+
Namespace: "test-ns",
343+
},
344+
Spec: mcpv1alpha1.MCPServerSpec{
345+
Image: testImage,
346+
Transport: stdioTransport,
347+
Port: 8080,
348+
Telemetry: &mcpv1alpha1.TelemetryConfig{
349+
OpenTelemetry: &mcpv1alpha1.OpenTelemetryConfig{
350+
Enabled: true,
351+
Endpoint: "https://secure-otel:4318",
352+
// ServiceName not specified - should default to MCPServer name
353+
},
354+
},
355+
},
356+
},
357+
//nolint:thelper // We want to see the error at the specific line
358+
expected: func(t *testing.T, config *runner.RunConfig) {
359+
assert.Equal(t, "minimal-telemetry-server", config.Name)
360+
361+
// Verify telemetry config is set
362+
assert.NotNil(t, config.TelemetryConfig)
363+
364+
// Check that service name defaults to MCPServer name
365+
assert.Equal(t, "minimal-telemetry-server", config.TelemetryConfig.ServiceName)
366+
assert.Equal(t, "secure-otel:4318", config.TelemetryConfig.Endpoint)
367+
assert.False(t, config.TelemetryConfig.Insecure) // Default should be false
368+
assert.Equal(t, 0.05, config.TelemetryConfig.SamplingRate) // Default sampling rate
369+
},
370+
},
371+
{
372+
name: "with prometheus only telemetry",
373+
mcpServer: &mcpv1alpha1.MCPServer{
374+
ObjectMeta: metav1.ObjectMeta{
375+
Name: "prometheus-only-server",
376+
Namespace: "test-ns",
377+
},
378+
Spec: mcpv1alpha1.MCPServerSpec{
379+
Image: testImage,
380+
Transport: stdioTransport,
381+
Port: 8080,
382+
Telemetry: &mcpv1alpha1.TelemetryConfig{
383+
Prometheus: &mcpv1alpha1.PrometheusConfig{
384+
Enabled: true,
385+
},
386+
},
387+
},
388+
},
389+
//nolint:thelper // We want to see the error at the specific line
390+
expected: func(t *testing.T, config *runner.RunConfig) {
391+
assert.Equal(t, "prometheus-only-server", config.Name)
392+
393+
// Verify telemetry config is set
394+
assert.NotNil(t, config.TelemetryConfig)
395+
396+
// Only Prometheus should be enabled
397+
assert.True(t, config.TelemetryConfig.EnablePrometheusMetricsPath)
398+
assert.False(t, config.TelemetryConfig.TracingEnabled)
399+
assert.False(t, config.TelemetryConfig.MetricsEnabled)
400+
assert.Equal(t, "", config.TelemetryConfig.Endpoint)
401+
},
402+
},
285403
}
286404

287405
for _, tt := range tests {
@@ -533,6 +651,71 @@ func TestEnsureRunConfigConfigMap(t *testing.T) {
533651
assert.NotEmpty(t, cm.Annotations["toolhive.stacklok.dev/content-checksum"])
534652
},
535653
},
654+
{
655+
name: "configmap with telemetry configuration",
656+
mcpServer: &mcpv1alpha1.MCPServer{
657+
ObjectMeta: metav1.ObjectMeta{
658+
Name: "telemetry-test",
659+
Namespace: "toolhive-system",
660+
},
661+
Spec: mcpv1alpha1.MCPServerSpec{
662+
Image: "ghcr.io/example/server:v1.0.0",
663+
Transport: "stdio",
664+
Port: 8080,
665+
Telemetry: &mcpv1alpha1.TelemetryConfig{
666+
OpenTelemetry: &mcpv1alpha1.OpenTelemetryConfig{
667+
Enabled: true,
668+
Endpoint: "http://otel-collector:4317",
669+
ServiceName: "test-service",
670+
Headers: []string{"Authorization=Bearer test-token"},
671+
Insecure: true,
672+
Tracing: &mcpv1alpha1.OpenTelemetryTracingConfig{
673+
Enabled: true,
674+
SamplingRate: "0.1",
675+
},
676+
Metrics: &mcpv1alpha1.OpenTelemetryMetricsConfig{
677+
Enabled: true,
678+
},
679+
},
680+
Prometheus: &mcpv1alpha1.PrometheusConfig{
681+
Enabled: true,
682+
},
683+
},
684+
},
685+
},
686+
existingCM: nil,
687+
expectError: false,
688+
validateContent: func(t *testing.T, cm *corev1.ConfigMap) {
689+
t.Helper()
690+
assert.Equal(t, "telemetry-test-runconfig", cm.Name)
691+
assert.Equal(t, "toolhive-system", cm.Namespace)
692+
assert.Contains(t, cm.Data, "runconfig.json")
693+
694+
// Parse and validate telemetry configuration in runconfig.json
695+
var runConfig runner.RunConfig
696+
err := json.Unmarshal([]byte(cm.Data["runconfig.json"]), &runConfig)
697+
require.NoError(t, err)
698+
699+
// Verify basic fields
700+
assert.Equal(t, "telemetry-test", runConfig.Name)
701+
assert.Equal(t, "ghcr.io/example/server:v1.0.0", runConfig.Image)
702+
703+
// Verify telemetry configuration is properly serialized
704+
assert.NotNil(t, runConfig.TelemetryConfig, "TelemetryConfig should be present in runconfig.json")
705+
706+
// Check OpenTelemetry settings (endpoint should have http:// prefix stripped)
707+
assert.Equal(t, "otel-collector:4317", runConfig.TelemetryConfig.Endpoint)
708+
assert.Equal(t, "test-service", runConfig.TelemetryConfig.ServiceName)
709+
assert.True(t, runConfig.TelemetryConfig.Insecure)
710+
assert.True(t, runConfig.TelemetryConfig.TracingEnabled)
711+
assert.True(t, runConfig.TelemetryConfig.MetricsEnabled)
712+
assert.Equal(t, 0.1, runConfig.TelemetryConfig.SamplingRate)
713+
assert.Equal(t, map[string]string{"Authorization": "Bearer test-token"}, runConfig.TelemetryConfig.Headers)
714+
715+
// Check Prometheus settings
716+
assert.True(t, runConfig.TelemetryConfig.EnablePrometheusMetricsPath)
717+
},
718+
},
536719
}
537720

538721
for _, tt := range tests {
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
apiVersion: v1
2+
kind: Pod
3+
metadata:
4+
namespace: toolhive-system
5+
labels:
6+
app: mcpserver
7+
app.kubernetes.io/instance: telemetry-configmap-test
8+
status:
9+
phase: Running
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
apiVersion: toolhive.stacklok.dev/v1alpha1
2+
kind: MCPServer
3+
metadata:
4+
name: telemetry-configmap-test
5+
namespace: toolhive-system
6+
status:
7+
phase: Running

0 commit comments

Comments
 (0)