Skip to content

Commit bca3f0f

Browse files
authored
Merge pull request #126 from golf-mcp/aschlean/fixing-otel-instum
Aschlean/fixing otel instumentation
2 parents f4d9d74 + 02c58dc commit bca3f0f

File tree

4 files changed

+332
-51
lines changed

4 files changed

+332
-51
lines changed

src/golf/auth/factory.py

Lines changed: 30 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,7 @@ def _create_oauth_proxy_provider(config: OAuthProxyConfig) -> "AuthProvider":
264264
env_value = env_value.strip()
265265
try:
266266
from urllib.parse import urlparse
267+
267268
parsed = urlparse(env_value)
268269
if not parsed.scheme or not parsed.netloc:
269270
raise ValueError(
@@ -292,6 +293,7 @@ def _create_oauth_proxy_provider(config: OAuthProxyConfig) -> "AuthProvider":
292293
env_value = env_value.strip()
293294
try:
294295
from urllib.parse import urlparse
296+
295297
parsed = urlparse(env_value)
296298
if not parsed.scheme or not parsed.netloc:
297299
raise ValueError(
@@ -300,26 +302,21 @@ def _create_oauth_proxy_provider(config: OAuthProxyConfig) -> "AuthProvider":
300302
)
301303
if parsed.scheme not in ("http", "https"):
302304
raise ValueError(
303-
f"Token endpoint from {config.token_endpoint_env_var} "
304-
f"must use http or https: '{env_value}'"
305+
f"Token endpoint from {config.token_endpoint_env_var} must use http or https: '{env_value}'"
305306
)
306307
token_endpoint = env_value
307308
except Exception as e:
308309
if isinstance(e, ValueError):
309310
raise
310-
raise ValueError(
311-
f"Invalid token_endpoint from {config.token_endpoint_env_var}: {e}"
312-
) from e
311+
raise ValueError(f"Invalid token_endpoint from {config.token_endpoint_env_var}: {e}") from e
313312

314313
client_id = config.client_id
315314
if config.client_id_env_var:
316315
env_value = os.environ.get(config.client_id_env_var)
317316
if env_value:
318317
client_id = env_value.strip()
319318
if not client_id:
320-
raise ValueError(
321-
f"Client ID from environment variable {config.client_id_env_var} cannot be empty"
322-
)
319+
raise ValueError(f"Client ID from environment variable {config.client_id_env_var} cannot be empty")
323320

324321
client_secret = config.client_secret
325322
if config.client_secret_env_var:
@@ -339,24 +336,19 @@ def _create_oauth_proxy_provider(config: OAuthProxyConfig) -> "AuthProvider":
339336
env_value = env_value.strip()
340337
try:
341338
from urllib.parse import urlparse
339+
342340
parsed = urlparse(env_value)
343341
if not parsed.scheme or not parsed.netloc:
344342
raise ValueError(
345-
f"Invalid base_url from environment variable "
346-
f"{config.base_url_env_var}: '{env_value}'"
343+
f"Invalid base_url from environment variable {config.base_url_env_var}: '{env_value}'"
347344
)
348345
if parsed.scheme not in ("http", "https"):
349-
raise ValueError(
350-
f"Base URL from {config.base_url_env_var} "
351-
f"must use http or https: '{env_value}'"
352-
)
346+
raise ValueError(f"Base URL from {config.base_url_env_var} must use http or https: '{env_value}'")
353347
base_url = env_value
354348
except Exception as e:
355349
if isinstance(e, ValueError):
356350
raise
357-
raise ValueError(
358-
f"Invalid base_url from {config.base_url_env_var}: {e}"
359-
) from e
351+
raise ValueError(f"Invalid base_url from {config.base_url_env_var}: {e}") from e
360352

361353
revocation_endpoint = config.revocation_endpoint
362354
if config.revocation_endpoint_env_var:
@@ -367,6 +359,7 @@ def _create_oauth_proxy_provider(config: OAuthProxyConfig) -> "AuthProvider":
367359
if env_value: # Only validate if not empty
368360
try:
369361
from urllib.parse import urlparse
362+
370363
parsed = urlparse(env_value)
371364
if not parsed.scheme or not parsed.netloc:
372365
raise ValueError(
@@ -388,28 +381,37 @@ def _create_oauth_proxy_provider(config: OAuthProxyConfig) -> "AuthProvider":
388381

389382
# Final validation: ensure all required fields have values after env resolution
390383
if not authorization_endpoint:
391-
env_var_hint = f" (environment variable {config.authorization_endpoint_env_var} is not set)" \
392-
if config.authorization_endpoint_env_var else ""
384+
env_var_hint = (
385+
f" (environment variable {config.authorization_endpoint_env_var} is not set)"
386+
if config.authorization_endpoint_env_var
387+
else ""
388+
)
393389
raise ValueError(f"Authorization endpoint is required but not provided{env_var_hint}")
394390

395391
if not token_endpoint:
396-
env_var_hint = f" (environment variable {config.token_endpoint_env_var} is not set)" \
397-
if config.token_endpoint_env_var else ""
392+
env_var_hint = (
393+
f" (environment variable {config.token_endpoint_env_var} is not set)"
394+
if config.token_endpoint_env_var
395+
else ""
396+
)
398397
raise ValueError(f"Token endpoint is required but not provided{env_var_hint}")
399398

400399
if not client_id:
401-
env_var_hint = f" (environment variable {config.client_id_env_var} is not set)" \
402-
if config.client_id_env_var else ""
400+
env_var_hint = (
401+
f" (environment variable {config.client_id_env_var} is not set)" if config.client_id_env_var else ""
402+
)
403403
raise ValueError(f"Client ID is required but not provided{env_var_hint}")
404404

405405
if not client_secret:
406-
env_var_hint = f" (environment variable {config.client_secret_env_var} is not set)" \
407-
if config.client_secret_env_var else ""
406+
env_var_hint = (
407+
f" (environment variable {config.client_secret_env_var} is not set)" if config.client_secret_env_var else ""
408+
)
408409
raise ValueError(f"Client secret is required but not provided{env_var_hint}")
409410

410411
if not base_url:
411-
env_var_hint = f" (environment variable {config.base_url_env_var} is not set)" \
412-
if config.base_url_env_var else ""
412+
env_var_hint = (
413+
f" (environment variable {config.base_url_env_var} is not set)" if config.base_url_env_var else ""
414+
)
413415
raise ValueError(f"Base URL is required but not provided{env_var_hint}")
414416

415417
# Production security checks
@@ -430,9 +432,7 @@ def _create_oauth_proxy_provider(config: OAuthProxyConfig) -> "AuthProvider":
430432
]:
431433
parsed = urlparse(url_value)
432434
if parsed.scheme == "http":
433-
raise ValueError(
434-
f"OAuth proxy {url_name} must use HTTPS in production environment: '{url_value}'"
435-
)
435+
raise ValueError(f"OAuth proxy {url_name} must use HTTPS in production environment: '{url_value}'")
436436

437437
# Check for localhost in production
438438
parsed_base = urlparse(base_url)

src/golf/core/builder.py

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -990,23 +990,20 @@ def _generate_server(self) -> None:
990990
imports.append(f"import {full_module_path}")
991991

992992
# Add code to register this component
993+
# Note: When opentelemetry_enabled, we use OpenTelemetryMiddleware for span creation
994+
# instead of wrapping individual functions, to ensure proper context propagation
993995
if self.settings.opentelemetry_enabled:
994-
# Use telemetry instrumentation
995-
registration = f"# Register the {component_type.value} '{component.name}' with telemetry"
996+
# Register components without function wrapping - middleware handles tracing
996997
entry_func = (
997998
component.entry_function
998999
if hasattr(component, "entry_function") and component.entry_function
9991000
else "export"
10001001
)
10011002

1002-
registration += (
1003-
f"\n_wrapped_func = instrument_{component_type.value}("
1004-
f"{full_module_path}.{entry_func}, '{component.name}')"
1005-
)
1006-
10071003
if component_type == ComponentType.TOOL:
1004+
registration = f"# Register the tool '{component.name}'"
10081005
registration += (
1009-
f"\n_tool = Tool.from_function(_wrapped_func, "
1006+
f"\n_tool = Tool.from_function({full_module_path}.{entry_func}, "
10101007
f'name="{component.name}", '
10111008
f"description={repr(component.docstring or '')})"
10121009
)
@@ -1015,23 +1012,25 @@ def _generate_server(self) -> None:
10151012
registration += f".with_annotations({component.annotations})"
10161013
registration += "\nmcp.add_tool(_tool)"
10171014
elif component_type == ComponentType.RESOURCE:
1015+
registration = f"# Register the resource '{component.name}'"
10181016
if self._is_resource_template(component):
10191017
registration += (
1020-
f"\n_template = ResourceTemplate.from_function(_wrapped_func, "
1018+
f"\n_template = ResourceTemplate.from_function({full_module_path}.{entry_func}, "
10211019
f'uri_template="{component.uri_template}", name="{component.name}", '
10221020
f"description={repr(component.docstring or '')})\n"
10231021
f"mcp.add_template(_template)"
10241022
)
10251023
else:
10261024
registration += (
1027-
f"\n_resource = Resource.from_function(_wrapped_func, "
1025+
f"\n_resource = Resource.from_function({full_module_path}.{entry_func}, "
10281026
f'uri="{component.uri_template}", name="{component.name}", '
10291027
f"description={repr(component.docstring or '')})\n"
10301028
f"mcp.add_resource(_resource)"
10311029
)
10321030
else: # PROMPT
1031+
registration = f"# Register the prompt '{component.name}'"
10331032
registration += (
1034-
f"\n_prompt = Prompt.from_function(_wrapped_func, "
1033+
f"\n_prompt = Prompt.from_function({full_module_path}.{entry_func}, "
10351034
f'name="{component.name}", '
10361035
f"description={repr(component.docstring or '')})\n"
10371036
f"mcp.add_prompt(_prompt)"
@@ -1198,6 +1197,13 @@ def _generate_server(self) -> None:
11981197
imports.append("")
11991198
component_registrations.append("")
12001199

1200+
# Add OpenTelemetry FastMCP middleware if enabled (must be added before other middleware)
1201+
if self.settings.opentelemetry_enabled:
1202+
component_registrations.append("# Register OpenTelemetry middleware for proper span context propagation")
1203+
component_registrations.append("from golf.telemetry import OpenTelemetryMiddleware")
1204+
component_registrations.append("mcp.add_middleware(OpenTelemetryMiddleware())")
1205+
component_registrations.append("")
1206+
12011207
# Check for custom middleware.py file and register middleware classes
12021208
middleware_classes = self._discover_middleware_classes(self.project_path)
12031209
if middleware_classes:
@@ -1308,10 +1314,11 @@ def _generate_server(self) -> None:
13081314
middleware_list.append("Middleware(MetricsMiddleware)")
13091315

13101316
# Add OpenTelemetry middleware if enabled
1317+
# Note: We intentionally do NOT use opentelemetry.instrumentation.asgi.OpenTelemetryMiddleware
1318+
# because it creates noisy low-level ASGI spans (http receive/send). Golf's FastMCP middleware
1319+
# (OpenTelemetryMiddleware) creates cleaner, more meaningful MCP-level spans.
13111320
if self.settings.opentelemetry_enabled:
1312-
middleware_setup.append(" from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware")
1313-
middleware_setup.append(" from starlette.middleware import Middleware")
1314-
middleware_list.append("Middleware(OpenTelemetryMiddleware)")
1321+
pass # OpenTelemetry middleware is added via mcp.add_middleware() earlier in the code
13151322

13161323
if middleware_setup:
13171324
main_code.extend(middleware_setup)
@@ -1368,10 +1375,11 @@ def _generate_server(self) -> None:
13681375
middleware_list.append("Middleware(MetricsMiddleware)")
13691376

13701377
# Add OpenTelemetry middleware if enabled
1378+
# Note: We intentionally do NOT use opentelemetry.instrumentation.asgi.OpenTelemetryMiddleware
1379+
# because it creates noisy low-level ASGI spans (http receive/send). Golf's FastMCP middleware
1380+
# (OpenTelemetryMiddleware) creates cleaner, more meaningful MCP-level spans.
13711381
if self.settings.opentelemetry_enabled:
1372-
middleware_setup.append(" from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware")
1373-
middleware_setup.append(" from starlette.middleware import Middleware")
1374-
middleware_list.append("Middleware(OpenTelemetryMiddleware)")
1382+
pass # OpenTelemetry middleware is added via mcp.add_middleware() earlier in the code
13751383

13761384
if middleware_setup:
13771385
main_code.extend(middleware_setup)

src/golf/telemetry/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Golf telemetry module for OpenTelemetry instrumentation."""
22

33
from golf.telemetry.instrumentation import (
4+
get_provider,
45
get_tracer,
56
init_telemetry,
67
instrument_elicitation,
@@ -9,6 +10,8 @@
910
instrument_sampling,
1011
instrument_tool,
1112
telemetry_lifespan,
13+
OpenTelemetryMiddleware,
14+
OTelContextCapturingMiddleware,
1215
)
1316

1417
__all__ = [
@@ -19,5 +22,8 @@
1922
"instrument_sampling",
2023
"telemetry_lifespan",
2124
"init_telemetry",
25+
"get_provider",
2226
"get_tracer",
27+
"OpenTelemetryMiddleware",
28+
"OTelContextCapturingMiddleware",
2329
]

0 commit comments

Comments
 (0)