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
10 changes: 4 additions & 6 deletions cmd/thv-operator/pkg/spectoconfig/telemetry.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,10 @@ func ConvertTelemetryConfig(
// This includes:
// - Stripping http:// or https:// prefixes from the endpoint (OTLP clients expect host:port format)
// - Defaulting ServiceName to the provided default name if not specified
// - Defaulting ServiceVersion to the build version if not specified
//
// Note: ServiceVersion is intentionally NOT defaulted here. It is resolved at
// runtime in telemetry.NewProvider() to always reflect the running binary version,
// avoiding stale versions persisted in configs. See #2296.
//
// This function is used by both the VirtualMCPServer converter (for spec.config.telemetry)
// and indirectly by ConvertTelemetryConfig (for CRD-style configs).
Expand All @@ -127,10 +130,5 @@ func NormalizeTelemetryConfig(config *telemetry.Config, defaultServiceName strin
normalized.ServiceName = defaultServiceName
}

// Default service version to build version if not specified
if normalized.ServiceVersion == "" {
normalized.ServiceVersion = telemetry.DefaultConfig().ServiceVersion
}

return &normalized
}
45 changes: 16 additions & 29 deletions cmd/thv-operator/pkg/spectoconfig/telemetry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,6 @@ import (
func TestNormalizeTelemetryConfig(t *testing.T) {
t.Parallel()

// Get the expected build version for tests
buildVersion := telemetry.DefaultConfig().ServiceVersion

tests := []struct {
name string
input *telemetry.Config
Expand All @@ -40,9 +37,8 @@ func TestNormalizeTelemetryConfig(t *testing.T) {
},
defaultName: "default-service",
expected: &telemetry.Config{
Endpoint: "otlp-collector:4317",
ServiceName: "my-service",
ServiceVersion: buildVersion,
Endpoint: "otlp-collector:4317",
ServiceName: "my-service",
},
},
{
Expand All @@ -53,9 +49,8 @@ func TestNormalizeTelemetryConfig(t *testing.T) {
},
defaultName: "default-service",
expected: &telemetry.Config{
Endpoint: "localhost:4317",
ServiceName: "my-service",
ServiceVersion: buildVersion,
Endpoint: "localhost:4317",
ServiceName: "my-service",
},
},
{
Expand All @@ -66,9 +61,8 @@ func TestNormalizeTelemetryConfig(t *testing.T) {
},
defaultName: "default-service",
expected: &telemetry.Config{
Endpoint: "otlp-collector:4317",
ServiceName: "my-service",
ServiceVersion: buildVersion,
Endpoint: "otlp-collector:4317",
ServiceName: "my-service",
},
},
{
Expand All @@ -79,23 +73,21 @@ func TestNormalizeTelemetryConfig(t *testing.T) {
},
defaultName: "default-service",
expected: &telemetry.Config{
Endpoint: "localhost:4317",
ServiceName: "default-service",
ServiceVersion: buildVersion,
Endpoint: "localhost:4317",
ServiceName: "default-service",
},
},
{
name: "defaults ServiceVersion to build version when empty",
name: "ServiceVersion left empty for runtime resolution",
input: &telemetry.Config{
Endpoint: "localhost:4317",
ServiceName: "my-service",
ServiceVersion: "",
},
defaultName: "default-service",
expected: &telemetry.Config{
Endpoint: "localhost:4317",
ServiceName: "my-service",
ServiceVersion: buildVersion,
Endpoint: "localhost:4317",
ServiceName: "my-service",
},
},
{
Expand Down Expand Up @@ -193,11 +185,10 @@ func TestNormalizeTelemetryConfig_DoesNotModifyInput(t *testing.T) {
func TestConvertTelemetryConfig_UsesNormalization(t *testing.T) {
t.Parallel()

// Get the expected build version for tests
buildVersion := telemetry.DefaultConfig().ServiceVersion

// This test verifies that ConvertTelemetryConfig uses NormalizeTelemetryConfig
// to apply endpoint prefix stripping and service name/version defaults
// to apply endpoint prefix stripping and service name defaults.
// ServiceVersion is intentionally left empty — it is resolved at runtime
// in telemetry.NewProvider() to always reflect the running binary version.
tests := []struct {
name string
input *v1alpha1.TelemetryConfig
Expand All @@ -221,7 +212,6 @@ func TestConvertTelemetryConfig_UsesNormalization(t *testing.T) {
expected: &telemetry.Config{
Endpoint: "otlp-collector:4317", // Prefix stripped
ServiceName: "my-mcp-server", // Defaulted
ServiceVersion: buildVersion, // Defaulted to build version
TracingEnabled: true,
SamplingRate: "0.1",
},
Expand All @@ -237,9 +227,8 @@ func TestConvertTelemetryConfig_UsesNormalization(t *testing.T) {
},
serverName: "default-server",
expected: &telemetry.Config{
Endpoint: "localhost:4317", // Prefix stripped
ServiceName: "custom-service", // Preserved
ServiceVersion: buildVersion, // Defaulted to build version
Endpoint: "localhost:4317", // Prefix stripped
ServiceName: "custom-service", // Preserved
},
},
{
Expand All @@ -253,7 +242,6 @@ func TestConvertTelemetryConfig_UsesNormalization(t *testing.T) {
expected: &telemetry.Config{
EnablePrometheusMetricsPath: true,
ServiceName: "prom-server", // Defaulted
ServiceVersion: buildVersion, // Defaulted to build version
UseLegacyAttributes: true, // Default when OTEL block absent
},
},
Expand All @@ -273,7 +261,6 @@ func TestConvertTelemetryConfig_UsesNormalization(t *testing.T) {
expected: &telemetry.Config{
Endpoint: "otlp:4317",
ServiceName: "legacy-test",
ServiceVersion: buildVersion,
TracingEnabled: true,
UseLegacyAttributes: false,
},
Expand Down
9 changes: 2 additions & 7 deletions cmd/thv-operator/pkg/vmcpconfig/converter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1346,16 +1346,13 @@ func TestConverter_TelemetryConfigPreserved(t *testing.T) {
func TestConverter_TelemetryDefaults(t *testing.T) {
t.Parallel()

// Get the expected build version for tests
buildVersion := telemetry.DefaultConfig().ServiceVersion

tests := []struct {
name string
inputTelemetry *telemetry.Config
expectedTelemetry *telemetry.Config
}{
{
name: "defaults ServiceName to vmcp name and ServiceVersion to build version",
name: "defaults ServiceName to vmcp name, ServiceVersion left for runtime",
inputTelemetry: &telemetry.Config{
Endpoint: "localhost:4317",
EnablePrometheusMetricsPath: true,
Expand All @@ -1366,11 +1363,10 @@ func TestConverter_TelemetryDefaults(t *testing.T) {
Endpoint: "localhost:4317",
EnablePrometheusMetricsPath: true,
ServiceName: "my-vmcp-server",
ServiceVersion: buildVersion,
},
},
{
name: "defaults ServiceVersion to build version when ServiceName is specified",
name: "ServiceVersion left for runtime when ServiceName is specified",
inputTelemetry: &telemetry.Config{
Endpoint: "localhost:4317",
EnablePrometheusMetricsPath: true,
Expand All @@ -1381,7 +1377,6 @@ func TestConverter_TelemetryDefaults(t *testing.T) {
Endpoint: "localhost:4317",
EnablePrometheusMetricsPath: true,
ServiceName: "custom-service",
ServiceVersion: buildVersion,
},
},
{
Expand Down
2 changes: 1 addition & 1 deletion cmd/thv/app/run_flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -1025,7 +1025,7 @@ func createTelemetryConfig(otelEndpoint string, otelEnablePrometheusMetricsPath
telemetryCfg := &telemetry.Config{
Endpoint: otelEndpoint,
ServiceName: otelServiceName,
ServiceVersion: telemetry.DefaultConfig().ServiceVersion,
ServiceVersion: "", // resolved at runtime in NewProvider()
TracingEnabled: otelTracingEnabled,
MetricsEnabled: otelMetricsEnabled,
Headers: headers,
Expand Down
17 changes: 12 additions & 5 deletions pkg/telemetry/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,10 +147,9 @@ const DefaultServiceNamePrefix = "thv-"

// DefaultConfig returns a default telemetry configuration.
func DefaultConfig() Config {
versionInfo := versions.GetVersionInfo()
return Config{
ServiceName: "", // empty — resolved at runtime from the workload name
ServiceVersion: versionInfo.Version,
ServiceName: "", // empty — resolved at runtime from the workload name
ServiceVersion: "", // resolved at runtime in NewProvider()
TracingEnabled: true, // Enable tracing by default if endpoint is configured
MetricsEnabled: true, // Enable metrics by default if endpoint is configured
SamplingRate: "0.05", // 5% sampling by default
Expand Down Expand Up @@ -203,7 +202,7 @@ func MaybeMakeConfig(
return &Config{
Endpoint: otelEndpoint,
ServiceName: otelServiceName,
ServiceVersion: DefaultConfig().ServiceVersion,
ServiceVersion: "", // resolved at runtime in NewProvider()
TracingEnabled: otelTracingEnabled,
MetricsEnabled: otelMetricsEnabled,
SamplingRate: strconv.FormatFloat(otelSamplingRate, 'f', -1, 64),
Expand Down Expand Up @@ -243,9 +242,17 @@ func NewProvider(ctx context.Context, config Config) (*Provider, error) {
return nil, err
}

// Always use the current binary version so that restarts and exports
// report the version actually running, not the version that originally
// created the config. See https://github.com/stacklok/toolhive/issues/2296
serviceVersion := config.ServiceVersion
if serviceVersion == "" {
serviceVersion = versions.GetVersionInfo().Version
}

telemetryOptions := []providers.ProviderOption{
providers.WithServiceName(config.ServiceName),
providers.WithServiceVersion(config.ServiceVersion),
providers.WithServiceVersion(serviceVersion),
providers.WithOTLPEndpoint(config.Endpoint),
providers.WithHeaders(config.Headers),
providers.WithInsecure(config.Insecure),
Expand Down
2 changes: 1 addition & 1 deletion pkg/telemetry/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ func TestDefaultConfig(t *testing.T) {
config := DefaultConfig()

assert.Empty(t, config.ServiceName, "ServiceName should be empty by default (resolved at runtime from workload name)")
assert.NotEmpty(t, config.ServiceVersion)
assert.Empty(t, config.ServiceVersion, "ServiceVersion should be empty by default (resolved at runtime in NewProvider)")
assert.Equal(t, "0.05", config.SamplingRate)
assert.NotNil(t, config.Headers)
assert.Empty(t, config.Headers)
Expand Down
Loading