Skip to content

Commit 8abada0

Browse files
yroblataskbot
andauthored
fix issues with propagating server and transport on telemetry (#1973)
* fix issues with propagating server and transport on telemetry Closes: #1898 * modify server name extraction, add e2e tests --------- Co-authored-by: taskbot <[email protected]>
1 parent f89b103 commit 8abada0

File tree

6 files changed

+871
-26
lines changed

6 files changed

+871
-26
lines changed

cmd/thv/app/run_flags.go

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,14 @@ func buildRunnerConfig(
404404
transportType = serverMetadata.GetTransport()
405405
}
406406

407+
// Determine server name for telemetry (similar to validateConfig logic)
408+
// This ensures telemetry middleware gets the correct server name
409+
imageMetadata, _ := serverMetadata.(*registry.ImageMetadata)
410+
serverName := runFlags.Name
411+
if serverName == "" && imageMetadata != nil {
412+
serverName = imageMetadata.Name
413+
}
414+
407415
// set default options
408416
opts := []runner.RunConfigBuilderOption{
409417
runner.WithRuntime(rt),
@@ -443,6 +451,7 @@ func buildRunnerConfig(
443451

444452
opts = append(opts, runner.WithToolsOverride(toolsOverride))
445453
// Configure middleware from flags
454+
// Use computed serverName and transportType for correct telemetry labels
446455
opts = append(
447456
opts,
448457
runner.WithMiddlewareFromFlags(
@@ -453,8 +462,8 @@ func buildRunnerConfig(
453462
runFlags.AuthzConfig,
454463
runFlags.EnableAudit,
455464
runFlags.AuditConfig,
456-
runFlags.Name,
457-
runFlags.Transport,
465+
serverName,
466+
transportType,
458467
),
459468
)
460469

@@ -490,14 +499,9 @@ func buildRunnerConfig(
490499
),
491500
runner.WithToolsFilter(runFlags.ToolsFilter))
492501

493-
imageMetadata, _ := serverMetadata.(*registry.ImageMetadata)
494502
// Process environment files
495-
var err error
496503
if runFlags.EnvFile != "" {
497504
opts = append(opts, runner.WithEnvFile(runFlags.EnvFile))
498-
if err != nil {
499-
return nil, fmt.Errorf("failed to process env file %s: %v", runFlags.EnvFile, err)
500-
}
501505
}
502506
if runFlags.EnvFileDir != "" {
503507
opts = append(opts, runner.WithEnvFilesFromDirectory(runFlags.EnvFileDir))

cmd/thv/app/run_flags_test.go

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package app
33
import (
44
"os"
55
"path/filepath"
6+
"strings"
67
"testing"
78

89
"github.com/spf13/cobra"
@@ -216,6 +217,97 @@ func TestBuildRunnerConfig_TelemetryProcessing(t *testing.T) {
216217
}
217218
}
218219

220+
func TestTelemetryMiddlewareParameterComputation(t *testing.T) {
221+
// This test validates the telemetry middleware parameter computation
222+
// by testing the logic that computes server name and transport type
223+
// before calling WithMiddlewareFromFlags
224+
t.Parallel()
225+
226+
logger.Initialize()
227+
228+
tests := []struct {
229+
name string
230+
runFlags *RunFlags
231+
serverOrImage string
232+
expectedServer string
233+
expectedTransport string
234+
}{
235+
{
236+
name: "explicit name and transport should use provided values",
237+
runFlags: &RunFlags{
238+
Name: "custom-server",
239+
Transport: "http",
240+
},
241+
serverOrImage: "custom-server",
242+
expectedServer: "custom-server",
243+
expectedTransport: "http",
244+
},
245+
{
246+
name: "empty name should be computed from image name",
247+
runFlags: &RunFlags{
248+
Transport: "sse",
249+
},
250+
serverOrImage: "docker://registry.test/my-test-server:latest",
251+
expectedServer: "my-test-server", // Extracted from image name
252+
expectedTransport: "sse",
253+
},
254+
{
255+
name: "empty transport should use default",
256+
runFlags: &RunFlags{
257+
Name: "named-server",
258+
},
259+
serverOrImage: "named-server",
260+
expectedServer: "named-server",
261+
expectedTransport: "streamable-http", // Default from constant
262+
},
263+
{
264+
name: "both empty should compute name and use default transport",
265+
runFlags: &RunFlags{},
266+
serverOrImage: "docker://example.com/path/server-name:v1.0",
267+
expectedServer: "server-name", // Extracted from image
268+
expectedTransport: "streamable-http", // Default
269+
},
270+
}
271+
272+
for _, tt := range tests {
273+
t.Run(tt.name, func(t *testing.T) {
274+
t.Parallel()
275+
276+
// Test the server name computation logic that was fixed
277+
// This simulates the logic in BuildRunnerConfig before WithMiddlewareFromFlags
278+
279+
// 1. Test transport type computation (this was already working)
280+
transportType := tt.runFlags.Transport
281+
if transportType == "" {
282+
transportType = defaultTransportType // "streamable-http"
283+
}
284+
assert.Equal(t, tt.expectedTransport, transportType, "Transport type should match expected")
285+
286+
// 2. Test server name computation
287+
serverName := tt.runFlags.Name
288+
if serverName == "" {
289+
// This simulates the image metadata extraction logic
290+
if strings.HasPrefix(tt.serverOrImage, "docker://") {
291+
imagePath := strings.TrimPrefix(tt.serverOrImage, "docker://")
292+
parts := strings.Split(imagePath, "/")
293+
imageName := parts[len(parts)-1]
294+
if colonIndex := strings.Index(imageName, ":"); colonIndex != -1 {
295+
imageName = imageName[:colonIndex]
296+
}
297+
serverName = imageName
298+
} else {
299+
serverName = tt.serverOrImage
300+
}
301+
}
302+
assert.Equal(t, tt.expectedServer, serverName, "Server name should match expected")
303+
304+
// 3. Verify both parameters are non-empty for proper middleware function
305+
assert.NotEmpty(t, serverName, "Server name should never be empty for middleware")
306+
assert.NotEmpty(t, transportType, "Transport type should never be empty for middleware")
307+
})
308+
}
309+
}
310+
219311
func TestBuildRunnerConfig_TelemetryProcessing_Integration(t *testing.T) {
220312
t.Parallel()
221313
// This is a more complete integration test that tests telemetry processing

pkg/api/v1/workload_service.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ func (s *WorkloadService) BuildFullRunConfig(ctx context.Context, req *createReq
120120
var remoteAuthConfig *runner.RemoteAuthConfig
121121
var imageURL string
122122
var imageMetadata *registry.ImageMetadata
123+
var serverMetadata registry.ServerMetadata
123124

124125
if req.URL != "" {
125126
// Configure remote authentication if OAuth config is provided
@@ -131,7 +132,6 @@ func (s *WorkloadService) BuildFullRunConfig(ctx context.Context, req *createReq
131132
return nil, err
132133
}
133134
} else {
134-
var serverMetadata registry.ServerMetadata
135135
// Create a dedicated context with longer timeout for image retrieval
136136
imageCtx, cancel := context.WithTimeout(ctx, imageRetrievalTimeout)
137137
defer cancel()
@@ -216,6 +216,14 @@ func (s *WorkloadService) BuildFullRunConfig(ctx context.Context, req *createReq
216216
runner.WithTelemetryConfig("", false, false, false, "", 0.0, nil, false, nil),
217217
}
218218

219+
// Determine transport type
220+
transportType := "streamable-http"
221+
if req.Transport != "" {
222+
transportType = req.Transport
223+
} else if serverMetadata != nil {
224+
transportType = serverMetadata.GetTransport()
225+
}
226+
219227
// Configure middleware from flags
220228
options = append(options,
221229
runner.WithMiddlewareFromFlags(
@@ -227,7 +235,7 @@ func (s *WorkloadService) BuildFullRunConfig(ctx context.Context, req *createReq
227235
false,
228236
"",
229237
req.Name,
230-
req.Transport,
238+
transportType,
231239
),
232240
)
233241

pkg/telemetry/middleware.go

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,9 @@ func (m *HTTPMiddleware) addMCPAttributes(ctx context.Context, span trace.Span,
238238
span.SetAttributes(attribute.String("mcp.server.name", serverName))
239239

240240
// Determine backend transport type
241-
// Note: ToolHive always serves SSE to clients, but backends can be stdio or sse
241+
// Note: ToolHive supports multiple transport types including stdio, sse, streamable-http
242+
// The transport should never be empty as both CLI and API have fallbacks to "streamable-http"
243+
// If transport is still empty, it indicates a configuration issue in middleware construction
242244
backendTransport := m.extractBackendTransport(r)
243245
span.SetAttributes(attribute.String("mcp.transport", backendTransport))
244246

@@ -281,25 +283,31 @@ func (m *HTTPMiddleware) addMethodSpecificAttributes(span trace.Span, parsedMCP
281283
}
282284
}
283285

284-
// extractServerName extracts the MCP server name from the HTTP request using multiple fallback strategies.
285-
// It first checks for the X-MCP-Server-Name header, then extracts from URL path segments
286-
// (skipping common prefixes like "sse", "messages", "api", "v1"), and finally falls back
287-
// to the middleware's configured server name.
286+
// extractServerName extracts the MCP server name from the HTTP request.
287+
// It checks for an explicit X-MCP-Server-Name header first, then falls back to the
288+
// configured server name. This approach is more reliable than parsing URL paths since
289+
// the server name is already known during middleware construction.
288290
func (m *HTTPMiddleware) extractServerName(r *http.Request) string {
291+
// Check for explicit server name header (for advanced routing scenarios)
289292
if serverName := r.Header.Get("X-MCP-Server-Name"); serverName != "" {
290293
return serverName
291294
}
292-
pathParts := strings.Split(strings.Trim(r.URL.Path, "/"), "/")
293-
for _, part := range pathParts {
294-
if part != "" && part != "sse" && part != "messages" && part != "api" && part != "v1" {
295-
return part
296-
}
297-
}
295+
296+
// Always use the configured server name - this is the correct server name
297+
// that was passed during middleware construction and doesn't depend on URL structure
298+
//
299+
// NOTE: Previously this function attempted to parse server names from URL paths by
300+
// splitting r.URL.Path and filtering out known endpoint segments like "sse", "messages",
301+
// "api", "v1", etc. This approach was fundamentally flawed because:
302+
// 1. It incorrectly treated endpoint names like "message" as server names
303+
// 2. It made assumptions about URL structure that don't always hold
304+
// 3. The actual server name is already available via m.serverName
305+
// Adding more exclusions (like "message") would just be treating symptoms, not the root cause.
298306
return m.serverName
299307
}
300308

301309
// extractBackendTransport determines the backend transport type.
302-
// ToolHive always serves SSE to clients, but backends can be stdio or sse.
310+
// ToolHive supports multiple transport types: stdio, sse, streamable-http.
303311
func (m *HTTPMiddleware) extractBackendTransport(r *http.Request) string {
304312
// Try to get transport info from custom headers (if set by proxy)
305313
if transport := r.Header.Get("X-MCP-Transport"); transport != "" {

pkg/telemetry/middleware_test.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -438,7 +438,9 @@ func TestHTTPMiddleware_FormatRequestID(t *testing.T) {
438438
func TestHTTPMiddleware_ExtractServerName(t *testing.T) {
439439
t.Parallel()
440440

441-
middleware := &HTTPMiddleware{}
441+
middleware := &HTTPMiddleware{
442+
serverName: "test-server", // Set a configured server name for testing
443+
}
442444

443445
tests := []struct {
444446
name string
@@ -456,23 +458,23 @@ func TestHTTPMiddleware_ExtractServerName(t *testing.T) {
456458
{
457459
name: "from path",
458460
path: "/api/v1/github/messages",
459-
expected: "github",
461+
expected: "test-server", // Now uses configured server name instead of path parsing
460462
},
461463
{
462464
name: "from path with sse",
463465
path: "/sse/weather/messages",
464-
expected: "weather",
466+
expected: "test-server", // Now uses configured server name instead of path parsing
465467
},
466468
{
467469
name: "fallback to serverName",
468470
path: "/messages",
469471
query: "session_id=abc123",
470-
expected: "", // Falls back to m.serverName which is empty in test
472+
expected: "test-server", // Uses configured server name
471473
},
472474
{
473475
name: "unknown",
474476
path: "/health",
475-
expected: "health", // "health" is not in the skip list, so it's extracted from path
477+
expected: "test-server", // Now uses configured server name instead of path parsing
476478
},
477479
}
478480

0 commit comments

Comments
 (0)