Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 4 additions & 74 deletions cmd/thv-operator/pkg/runconfig/telemetry.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,9 @@ package runconfig

import (
"context"
"strconv"
"strings"

"sigs.k8s.io/controller-runtime/pkg/log"

mcpv1alpha1 "github.com/stacklok/toolhive/cmd/thv-operator/api/v1alpha1"
"github.com/stacklok/toolhive/cmd/thv-operator/pkg/spectoconfig"
"github.com/stacklok/toolhive/pkg/runner"
)

Expand All @@ -19,79 +16,12 @@ func AddTelemetryConfigOptions(
telemetryConfig *mcpv1alpha1.TelemetryConfig,
mcpServerName string,
) {
if telemetryConfig == nil {
if telemetryConfig == nil || options == nil {
return
}

// Default values
var otelEndpoint string
var otelEnablePrometheusMetricsPath bool
var otelTracingEnabled bool
var otelMetricsEnabled bool
var otelServiceName string
var otelSamplingRate = 0.05 // Default sampling rate
var otelHeaders []string
var otelInsecure bool
var otelEnvironmentVariables []string

// Process OpenTelemetry configuration
if telemetryConfig.OpenTelemetry != nil && telemetryConfig.OpenTelemetry.Enabled {
otel := telemetryConfig.OpenTelemetry

// Strip http:// or https:// prefix if present, as OTLP client expects host:port format
otelEndpoint = strings.TrimPrefix(strings.TrimPrefix(otel.Endpoint, "https://"), "http://")
otelInsecure = otel.Insecure
otelHeaders = otel.Headers

// Use MCPServer name as service name if not specified
if otel.ServiceName != "" {
otelServiceName = otel.ServiceName
} else {
otelServiceName = mcpServerName
}

// Handle tracing configuration
if otel.Tracing != nil {
otelTracingEnabled = otel.Tracing.Enabled
if otel.Tracing.SamplingRate != "" {
// Parse sampling rate string to float64
if rate, err := strconv.ParseFloat(otel.Tracing.SamplingRate, 64); err == nil {
otelSamplingRate = rate
} else {
logger := log.FromContext(ctx)
logger.Error(err, "Failed to parse sampling rate, using default",
"samplingRate", otel.Tracing.SamplingRate,
"default", otelSamplingRate,
"mcpServer", mcpServerName)
}
}
}

// Handle metrics configuration
if otel.Metrics != nil {
otelMetricsEnabled = otel.Metrics.Enabled
}
}

// Process Prometheus configuration
if telemetryConfig.Prometheus != nil {
otelEnablePrometheusMetricsPath = telemetryConfig.Prometheus.Enabled
}

if options == nil {
return
}
config := spectoconfig.ConvertTelemetryConfig(ctx, telemetryConfig, mcpServerName)

// Add telemetry config to options
*options = append(*options, runner.WithTelemetryConfig(
otelEndpoint,
otelEnablePrometheusMetricsPath,
otelTracingEnabled,
otelMetricsEnabled,
otelServiceName,
otelSamplingRate,
otelHeaders,
otelInsecure,
otelEnvironmentVariables,
))
*options = append(*options, runner.WithTelemetryConfig(config))
}
92 changes: 92 additions & 0 deletions cmd/thv-operator/pkg/spectoconfig/telemetry.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// Package spectoconfig provides functionality to convert CRD Telemetry types into telemetry.Config.
Comment thread
jerm-dro marked this conversation as resolved.
package spectoconfig

import (
"context"
"strconv"
"strings"

"sigs.k8s.io/controller-runtime/pkg/log"

"github.com/stacklok/toolhive/cmd/thv-operator/api/v1alpha1"
"github.com/stacklok/toolhive/pkg/telemetry"
)

// ConvertTelemetryConfig converts the CRD TelemetryConfig to a telemetry.Config.
// It may return nil if no telemetry is configured.
func ConvertTelemetryConfig(
ctx context.Context,
telemetryConfig *v1alpha1.TelemetryConfig,
mcpServerName string,
) *telemetry.Config {
if telemetryConfig == nil {
return nil
}

// Default values
var otelEndpoint string
var otelEnablePrometheusMetricsPath bool
var otelTracingEnabled bool
var otelMetricsEnabled bool
var otelServiceName string
var otelSamplingRate = 0.05 // Default sampling rate
var otelHeaders []string
var otelInsecure bool
var otelEnvironmentVariables []string

// Process OpenTelemetry configuration
if telemetryConfig.OpenTelemetry != nil && telemetryConfig.OpenTelemetry.Enabled {
otel := telemetryConfig.OpenTelemetry

// Strip http:// or https:// prefix if present, as OTLP client expects host:port format
otelEndpoint = strings.TrimPrefix(strings.TrimPrefix(otel.Endpoint, "https://"), "http://")
otelInsecure = otel.Insecure
otelHeaders = otel.Headers

// Use MCPServer name as service name if not specified
if otel.ServiceName != "" {
otelServiceName = otel.ServiceName
} else {
otelServiceName = mcpServerName
}

// Handle tracing configuration
if otel.Tracing != nil {
otelTracingEnabled = otel.Tracing.Enabled
if otel.Tracing.SamplingRate != "" {
// Parse sampling rate string to float64
if rate, err := strconv.ParseFloat(otel.Tracing.SamplingRate, 64); err == nil {
otelSamplingRate = rate
} else {
logger := log.FromContext(ctx)
logger.Error(err, "Failed to parse sampling rate, using default",
"samplingRate", otel.Tracing.SamplingRate,
"default", otelSamplingRate,
"mcpServer", mcpServerName)
}
}
}

// Handle metrics configuration
if otel.Metrics != nil {
otelMetricsEnabled = otel.Metrics.Enabled
}
}

// Process Prometheus configuration
if telemetryConfig.Prometheus != nil {
otelEnablePrometheusMetricsPath = telemetryConfig.Prometheus.Enabled
}

return telemetry.MaybeMakeConfig(
otelEndpoint,
otelEnablePrometheusMetricsPath,
otelTracingEnabled,
otelMetricsEnabled,
otelServiceName,
otelSamplingRate,
otelHeaders,
otelInsecure,
otelEnvironmentVariables,
)
}
Comment thread
jerm-dro marked this conversation as resolved.
2 changes: 1 addition & 1 deletion cmd/thv/app/run_flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -549,7 +549,7 @@ func configureMiddlewareAndOptions(
runFlags.ThvCABundle, runFlags.JWKSAuthTokenFile, runFlags.ResourceURL,
runFlags.JWKSAllowPrivateIP, runFlags.InsecureAllowHTTP,
),
runner.WithTelemetryConfig(finalOtelEndpoint, runFlags.OtelEnablePrometheusMetricsPath,
runner.WithTelemetryConfigFromFlags(finalOtelEndpoint, runFlags.OtelEnablePrometheusMetricsPath,
runFlags.OtelTracingEnabled, runFlags.OtelMetricsEnabled, runFlags.OtelServiceName,
finalOtelSamplingRate, runFlags.OtelHeaders, runFlags.OtelInsecure, finalOtelEnvironmentVariables,
),
Expand Down
2 changes: 1 addition & 1 deletion pkg/api/v1/workload_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ func (s *WorkloadService) BuildFullRunConfig(ctx context.Context, req *createReq
"", "", "", "", "", false, false),
runner.WithToolsFilter(req.ToolsFilter),
runner.WithToolsOverride(toolsOverride),
runner.WithTelemetryConfig("", false, false, false, "", 0.0, nil, false, nil),
runner.WithTelemetryConfigFromFlags("", false, false, false, "", 0.0, nil, false, nil),
}

// Determine transport type
Expand Down
65 changes: 19 additions & 46 deletions pkg/runner/config_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -365,8 +365,8 @@ func WithTokenExchangeConfig(config *tokenexchange.Config) RunConfigBuilderOptio
}
}

// WithTelemetryConfig configures telemetry settings (legacy - custom attributes handled via middleware)
func WithTelemetryConfig(
// WithTelemetryConfigFromFlags configures telemetry settings (legacy - custom attributes handled via middleware)
func WithTelemetryConfigFromFlags(
otelEndpoint string,
otelEnablePrometheusMetricsPath bool,
otelTracingEnabled bool,
Expand All @@ -377,51 +377,24 @@ func WithTelemetryConfig(
otelInsecure bool,
otelEnvironmentVariables []string,
) RunConfigBuilderOption {
return func(b *runConfigBuilder) error {
if otelEndpoint == "" && !otelEnablePrometheusMetricsPath {
return nil
}

// Parse headers from key=value format
headers := make(map[string]string)
for _, header := range otelHeaders {
parts := strings.SplitN(header, "=", 2)
if len(parts) == 2 {
headers[parts[0]] = parts[1]
}
}

// Use provided service name or default
serviceName := otelServiceName
if serviceName == "" {
serviceName = telemetry.DefaultConfig().ServiceName
}

// Process environment variables - split comma-separated values
var processedEnvVars []string
for _, envVarEntry := range otelEnvironmentVariables {
// Split by comma and trim whitespace
envVars := strings.Split(envVarEntry, ",")
for _, envVar := range envVars {
trimmed := strings.TrimSpace(envVar)
if trimmed != "" {
processedEnvVars = append(processedEnvVars, trimmed)
}
}
}
config := telemetry.MaybeMakeConfig(
otelEndpoint,
otelEnablePrometheusMetricsPath,
otelTracingEnabled,
otelMetricsEnabled,
otelServiceName,
otelSamplingRate,
otelHeaders,
otelInsecure,
otelEnvironmentVariables,
)
return WithTelemetryConfig(config)
}

b.config.TelemetryConfig = &telemetry.Config{
Endpoint: otelEndpoint,
ServiceName: serviceName,
ServiceVersion: telemetry.DefaultConfig().ServiceVersion,
TracingEnabled: otelTracingEnabled,
MetricsEnabled: otelMetricsEnabled,
SamplingRate: otelSamplingRate,
Headers: headers,
Insecure: otelInsecure,
EnablePrometheusMetricsPath: otelEnablePrometheusMetricsPath,
EnvironmentVariables: processedEnvVars,
}
// WithTelemetryConfig sets the telemetry configuration
func WithTelemetryConfig(config *telemetry.Config) RunConfigBuilderOption {
return func(b *runConfigBuilder) error {
b.config.TelemetryConfig = config
return nil
}
}
Expand Down
14 changes: 7 additions & 7 deletions pkg/runner/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -613,7 +613,7 @@ func TestRunConfigBuilder(t *testing.T) {
WithLabels(nil),
WithGroup(""),
WithOIDCConfig(oidcIssuer, oidcAudience, oidcJwksURL, "", oidcClientID, "", "", "", "", false, false),
WithTelemetryConfig("", false, false, false, "", 0.1, nil, false, nil),
WithTelemetryConfigFromFlags("", false, false, false, "", 0.1, nil, false, nil),
WithToolsFilter(nil),
WithIgnoreConfig(&ignore.Config{
LoadGlobal: false,
Expand Down Expand Up @@ -861,7 +861,7 @@ func TestRunConfigBuilder_MetadataOverrides(t *testing.T) {
WithLabels(nil),
WithGroup(""),
WithOIDCConfig("", "", "", "", "", "", "", "", "", false, false),
WithTelemetryConfig("", false, false, false, "", 0, nil, false, nil),
WithTelemetryConfigFromFlags("", false, false, false, "", 0, nil, false, nil),
WithToolsFilter(nil),
WithIgnoreConfig(&ignore.Config{
LoadGlobal: false,
Expand Down Expand Up @@ -908,7 +908,7 @@ func TestRunConfigBuilder_EnvironmentVariableTransportDependency(t *testing.T) {
WithLabels(nil),
WithGroup(""),
WithOIDCConfig("", "", "", "", "", "", "", "", "", false, false),
WithTelemetryConfig("", false, false, false, "", 0, nil, false, nil),
WithTelemetryConfigFromFlags("", false, false, false, "", 0, nil, false, nil),
WithToolsFilter(nil),
WithIgnoreConfig(&ignore.Config{
LoadGlobal: false,
Expand Down Expand Up @@ -961,7 +961,7 @@ func TestRunConfigBuilder_CmdArgsMetadataOverride(t *testing.T) {
WithLabels(nil),
WithGroup(""),
WithOIDCConfig("", "", "", "", "", "", "", "", "", false, false),
WithTelemetryConfig("", false, false, false, "", 0, nil, false, nil),
WithTelemetryConfigFromFlags("", false, false, false, "", 0, nil, false, nil),
WithToolsFilter(nil),
WithIgnoreConfig(&ignore.Config{
LoadGlobal: false,
Expand Down Expand Up @@ -1016,7 +1016,7 @@ func TestRunConfigBuilder_CmdArgsMetadataDefaults(t *testing.T) {
WithLabels(nil),
WithGroup(""),
WithOIDCConfig("", "", "", "", "", "", "", "", "", false, false),
WithTelemetryConfig("", false, false, false, "", 0, nil, false, nil),
WithTelemetryConfigFromFlags("", false, false, false, "", 0, nil, false, nil),
WithToolsFilter(nil),
WithIgnoreConfig(&ignore.Config{
LoadGlobal: false,
Expand Down Expand Up @@ -1067,7 +1067,7 @@ func TestRunConfigBuilder_VolumeProcessing(t *testing.T) {
WithLabels(nil),
WithGroup(""),
WithOIDCConfig("", "", "", "", "", "", "", "", "", false, false),
WithTelemetryConfig("", false, false, false, "", 0, nil, false, nil),
WithTelemetryConfigFromFlags("", false, false, false, "", 0, nil, false, nil),
WithToolsFilter(nil),
WithIgnoreConfig(&ignore.Config{
LoadGlobal: false,
Expand Down Expand Up @@ -1140,7 +1140,7 @@ func TestRunConfigBuilder_FilesystemMCPScenario(t *testing.T) {
WithLabels(nil),
WithGroup(""),
WithOIDCConfig("", "", "", "", "", "", "", "", "", false, false),
WithTelemetryConfig("", false, false, false, "", 0, nil, false, nil),
WithTelemetryConfigFromFlags("", false, false, false, "", 0, nil, false, nil),
WithToolsFilter(nil),
WithIgnoreConfig(&ignore.Config{
LoadGlobal: false,
Expand Down
Loading
Loading