golf-mcp 0.1.20__py3-none-any.whl → 0.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of golf-mcp might be problematic. Click here for more details.
- golf/__init__.py +9 -1
- golf/_endpoints.py +6 -0
- golf/_endpoints_fallback.py +10 -0
- golf/auth/__init__.py +235 -83
- golf/auth/api_key.py +6 -14
- golf/auth/factory.py +358 -0
- golf/auth/helpers.py +12 -42
- golf/auth/providers.py +446 -0
- golf/auth/registry.py +256 -0
- golf/cli/branding.py +192 -0
- golf/cli/main.py +28 -69
- golf/commands/__init__.py +2 -0
- golf/commands/build.py +4 -7
- golf/commands/init.py +30 -53
- golf/commands/run.py +50 -20
- golf/core/builder.py +355 -414
- golf/core/builder_auth.py +63 -144
- golf/core/builder_telemetry.py +26 -3
- golf/core/config.py +38 -59
- golf/core/parser.py +132 -139
- golf/core/platform.py +12 -10
- golf/core/telemetry.py +11 -19
- golf/core/transformer.py +38 -15
- golf/examples/__pycache__/__init__.cpython-311.pyc +0 -0
- golf/examples/basic/.coverage +0 -0
- golf/examples/basic/.env.example +8 -4
- golf/examples/basic/README.md +117 -45
- golf/examples/basic/__pycache__/auth.cpython-311.pyc +0 -0
- golf/examples/basic/auth.py +76 -0
- golf/examples/basic/golf.json +2 -5
- golf/examples/basic/htmlcov/.gitignore +2 -0
- golf/examples/basic/htmlcov/class_index.html +547 -0
- golf/examples/basic/htmlcov/coverage_html_cb_6fb7b396.js +733 -0
- golf/examples/basic/htmlcov/favicon_32_cb_58284776.png +0 -0
- golf/examples/basic/htmlcov/function_index.html +2091 -0
- golf/examples/basic/htmlcov/index.html +349 -0
- golf/examples/basic/htmlcov/keybd_closed_cb_ce680311.png +0 -0
- golf/examples/basic/htmlcov/status.json +1 -0
- golf/examples/basic/htmlcov/style_cb_8e611ae1.css +337 -0
- golf/examples/basic/htmlcov/z_1c9a91c0e91c8496___init___py.html +323 -0
- golf/examples/basic/htmlcov/z_1c9a91c0e91c8496_api_key_py.html +170 -0
- golf/examples/basic/htmlcov/z_1c9a91c0e91c8496_factory_py.html +430 -0
- golf/examples/basic/htmlcov/z_1c9a91c0e91c8496_helpers_py.html +288 -0
- golf/examples/basic/htmlcov/z_1c9a91c0e91c8496_providers_py.html +493 -0
- golf/examples/basic/htmlcov/z_1c9a91c0e91c8496_registry_py.html +353 -0
- golf/examples/basic/htmlcov/z_3ec3b3f490dc0950___init___py.html +120 -0
- golf/examples/basic/htmlcov/z_3ec3b3f490dc0950_instrumentation_py.html +1535 -0
- golf/examples/basic/htmlcov/z_4b8b9dd4ccccc5db___init___py.html +98 -0
- golf/examples/basic/htmlcov/z_4b8b9dd4ccccc5db_branding_py.html +289 -0
- golf/examples/basic/htmlcov/z_4b8b9dd4ccccc5db_main_py.html +476 -0
- golf/examples/basic/htmlcov/z_5a6c4e6bcc86fb2f___init___py.html +97 -0
- golf/examples/basic/htmlcov/z_6cadab9ec0df475d___init___py.html +102 -0
- golf/examples/basic/htmlcov/z_6cadab9ec0df475d_build_py.html +178 -0
- golf/examples/basic/htmlcov/z_6cadab9ec0df475d_init_py.html +387 -0
- golf/examples/basic/htmlcov/z_6cadab9ec0df475d_run_py.html +222 -0
- golf/examples/basic/htmlcov/z_6fcdee0582ba84e4___init___py.html +106 -0
- golf/examples/basic/htmlcov/z_6fcdee0582ba84e4__endpoints_fallback_py.html +107 -0
- golf/examples/basic/htmlcov/z_7ba499ed22986217___init___py.html +98 -0
- golf/examples/basic/htmlcov/z_7ba499ed22986217_builder_auth_py.html +306 -0
- golf/examples/basic/htmlcov/z_7ba499ed22986217_builder_metrics_py.html +329 -0
- golf/examples/basic/htmlcov/z_7ba499ed22986217_builder_py.html +1471 -0
- golf/examples/basic/htmlcov/z_7ba499ed22986217_builder_telemetry_py.html +186 -0
- golf/examples/basic/htmlcov/z_7ba499ed22986217_config_py.html +315 -0
- golf/examples/basic/htmlcov/z_7ba499ed22986217_parser_py.html +1149 -0
- golf/examples/basic/htmlcov/z_7ba499ed22986217_platform_py.html +279 -0
- golf/examples/basic/htmlcov/z_7ba499ed22986217_telemetry_py.html +589 -0
- golf/examples/basic/htmlcov/z_7ba499ed22986217_transformer_py.html +286 -0
- golf/examples/basic/htmlcov/z_7d7da37693a43688___init___py.html +107 -0
- golf/examples/basic/htmlcov/z_7d7da37693a43688_collector_py.html +417 -0
- golf/examples/basic/htmlcov/z_7d7da37693a43688_registry_py.html +109 -0
- golf/examples/basic/htmlcov/z_abe733142b40ad4e___init___py.html +109 -0
- golf/examples/basic/htmlcov/z_abe733142b40ad4e_context_py.html +150 -0
- golf/examples/basic/htmlcov/z_abe733142b40ad4e_elicitation_py.html +267 -0
- golf/examples/basic/htmlcov/z_abe733142b40ad4e_sampling_py.html +318 -0
- golf/examples/basic/prompts/__pycache__/welcome.cpython-311.pyc +0 -0
- golf/examples/basic/prompts/welcome.py +3 -5
- golf/examples/basic/resources/__pycache__/current_time.cpython-311.pyc +0 -0
- golf/examples/basic/resources/__pycache__/info.cpython-311.pyc +0 -0
- golf/examples/basic/resources/current_time.py +5 -13
- golf/examples/basic/resources/weather/__pycache__/common.cpython-311.pyc +0 -0
- golf/examples/basic/resources/weather/__pycache__/current.cpython-311.pyc +0 -0
- golf/examples/basic/resources/weather/__pycache__/forecast.cpython-311.pyc +0 -0
- golf/examples/basic/resources/weather/city.py +46 -0
- golf/examples/basic/resources/weather/common.py +4 -11
- golf/examples/basic/resources/weather/current.py +5 -5
- golf/examples/basic/resources/weather/forecast.py +5 -5
- golf/examples/basic/tools/__pycache__/calculator.cpython-311.pyc +0 -0
- golf/examples/basic/tools/calculator.py +94 -0
- golf/examples/basic/tools/say/__pycache__/hello.cpython-311.pyc +0 -0
- golf/examples/basic/tools/say/hello.py +65 -0
- golf/metrics/collector.py +100 -19
- golf/telemetry/__init__.py +4 -0
- golf/telemetry/instrumentation.py +484 -178
- golf/utilities/__init__.py +12 -0
- golf/utilities/context.py +53 -0
- golf/utilities/elicitation.py +170 -0
- golf/utilities/sampling.py +221 -0
- {golf_mcp-0.1.20.dist-info → golf_mcp-0.2.1.dist-info}/METADATA +51 -104
- golf_mcp-0.2.1.dist-info/RECORD +110 -0
- golf/auth/oauth.py +0 -861
- golf/auth/provider.py +0 -115
- golf/examples/api_key/.env +0 -2
- golf/examples/api_key/.env.example +0 -1
- golf/examples/api_key/README.md +0 -84
- golf/examples/api_key/golf.json +0 -8
- golf/examples/api_key/pre_build.py +0 -11
- golf/examples/api_key/tools/issues/create.py +0 -93
- golf/examples/api_key/tools/issues/list.py +0 -92
- golf/examples/api_key/tools/repos/list.py +0 -111
- golf/examples/api_key/tools/search/code.py +0 -106
- golf/examples/api_key/tools/users/get.py +0 -82
- golf/examples/basic/.env +0 -5
- golf/examples/basic/pre_build.py +0 -28
- golf/examples/basic/tools/github_user.py +0 -65
- golf/examples/basic/tools/hello.py +0 -34
- golf/examples/basic/tools/payments/charge.py +0 -70
- golf/examples/basic/tools/payments/common.py +0 -36
- golf/examples/basic/tools/payments/refund.py +0 -61
- golf_mcp-0.1.20.dist-info/RECORD +0 -60
- {golf_mcp-0.1.20.dist-info → golf_mcp-0.2.1.dist-info}/WHEEL +0 -0
- {golf_mcp-0.1.20.dist-info → golf_mcp-0.2.1.dist-info}/entry_points.txt +0 -0
- {golf_mcp-0.1.20.dist-info → golf_mcp-0.2.1.dist-info}/licenses/LICENSE +0 -0
- {golf_mcp-0.1.20.dist-info → golf_mcp-0.2.1.dist-info}/top_level.txt +0 -0
|
@@ -5,12 +5,22 @@ import functools
|
|
|
5
5
|
import os
|
|
6
6
|
import sys
|
|
7
7
|
import time
|
|
8
|
+
import json
|
|
8
9
|
from collections.abc import Callable
|
|
9
10
|
from contextlib import asynccontextmanager
|
|
10
|
-
from typing import TypeVar
|
|
11
|
+
from typing import Any, TypeVar
|
|
12
|
+
from collections.abc import AsyncGenerator
|
|
11
13
|
from collections import OrderedDict
|
|
12
14
|
|
|
13
15
|
from opentelemetry import baggage, trace
|
|
16
|
+
|
|
17
|
+
# Import endpoints with fallback for dev mode
|
|
18
|
+
try:
|
|
19
|
+
# In built wheels, this exists (generated from _endpoints.py.in)
|
|
20
|
+
from golf import _endpoints # type: ignore
|
|
21
|
+
except ImportError:
|
|
22
|
+
# In editable/dev installs, fall back to env-based values
|
|
23
|
+
from golf import _endpoints_fallback as _endpoints # type: ignore
|
|
14
24
|
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
|
|
15
25
|
from opentelemetry.sdk.resources import Resource
|
|
16
26
|
from opentelemetry.sdk.trace import TracerProvider
|
|
@@ -18,13 +28,38 @@ from opentelemetry.sdk.trace.export import BatchSpanProcessor, ConsoleSpanExport
|
|
|
18
28
|
from opentelemetry.trace import Status, StatusCode
|
|
19
29
|
|
|
20
30
|
from starlette.middleware.base import BaseHTTPMiddleware
|
|
21
|
-
from starlette.requests import Request
|
|
22
31
|
|
|
23
32
|
T = TypeVar("T")
|
|
24
33
|
|
|
25
34
|
# Global tracer instance
|
|
26
35
|
_tracer: trace.Tracer | None = None
|
|
27
36
|
_provider: TracerProvider | None = None
|
|
37
|
+
_detailed_tracing_enabled: bool = False
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _safe_serialize(data: Any, max_length: int = 1000) -> str | None:
|
|
41
|
+
"""Safely serialize data to string with length limit."""
|
|
42
|
+
try:
|
|
43
|
+
if isinstance(data, str):
|
|
44
|
+
serialized = data
|
|
45
|
+
else:
|
|
46
|
+
serialized = json.dumps(data, default=str, ensure_ascii=False)
|
|
47
|
+
|
|
48
|
+
if len(serialized) > max_length:
|
|
49
|
+
return serialized[:max_length] + "..." + f" (truncated from {len(serialized)} chars)"
|
|
50
|
+
return serialized
|
|
51
|
+
except (TypeError, ValueError):
|
|
52
|
+
# Fallback for non-serializable objects
|
|
53
|
+
try:
|
|
54
|
+
return str(data)[:max_length] + "..." if len(str(data)) > max_length else str(data)
|
|
55
|
+
except Exception:
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def set_detailed_tracing(enabled: bool) -> None:
|
|
60
|
+
"""Enable or disable detailed tracing with input/output capture."""
|
|
61
|
+
global _detailed_tracing_enabled
|
|
62
|
+
_detailed_tracing_enabled = enabled
|
|
28
63
|
|
|
29
64
|
|
|
30
65
|
def init_telemetry(service_name: str = "golf-mcp-server") -> TracerProvider | None:
|
|
@@ -37,27 +72,24 @@ def init_telemetry(service_name: str = "golf-mcp-server") -> TracerProvider | No
|
|
|
37
72
|
# Check for Golf platform integration first
|
|
38
73
|
golf_api_key = os.environ.get("GOLF_API_KEY")
|
|
39
74
|
if golf_api_key:
|
|
40
|
-
# Auto-configure for Golf platform - always use OTLP when Golf API
|
|
75
|
+
# Auto-configure for Golf platform - always use OTLP when Golf API
|
|
76
|
+
# key is present
|
|
41
77
|
os.environ["OTEL_TRACES_EXPORTER"] = "otlp_http"
|
|
42
|
-
|
|
78
|
+
|
|
43
79
|
# Only set endpoint if not already configured (allow user override)
|
|
44
80
|
if not os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT"):
|
|
45
|
-
os.environ["OTEL_EXPORTER_OTLP_ENDPOINT"] =
|
|
46
|
-
|
|
47
|
-
)
|
|
48
|
-
|
|
81
|
+
os.environ["OTEL_EXPORTER_OTLP_ENDPOINT"] = _endpoints.OTEL_ENDPOINT
|
|
82
|
+
|
|
49
83
|
# Set Golf platform headers (append to existing if present)
|
|
50
84
|
existing_headers = os.environ.get("OTEL_EXPORTER_OTLP_HEADERS", "")
|
|
51
85
|
golf_header = f"X-Golf-Key={golf_api_key}"
|
|
52
|
-
|
|
86
|
+
|
|
53
87
|
if existing_headers:
|
|
54
88
|
# Check if Golf key is already in headers
|
|
55
89
|
if "X-Golf-Key=" not in existing_headers:
|
|
56
90
|
os.environ["OTEL_EXPORTER_OTLP_HEADERS"] = f"{existing_headers},{golf_header}"
|
|
57
91
|
else:
|
|
58
92
|
os.environ["OTEL_EXPORTER_OTLP_HEADERS"] = golf_header
|
|
59
|
-
|
|
60
|
-
print("[INFO] Auto-configured OpenTelemetry for Golf platform ingestion")
|
|
61
93
|
|
|
62
94
|
# Check for required environment variables based on exporter type
|
|
63
95
|
exporter_type = os.environ.get("OTEL_TRACES_EXPORTER", "console").lower()
|
|
@@ -94,9 +126,7 @@ def init_telemetry(service_name: str = "golf-mcp-server") -> TracerProvider | No
|
|
|
94
126
|
# Configure exporter based on type
|
|
95
127
|
try:
|
|
96
128
|
if exporter_type == "otlp_http":
|
|
97
|
-
endpoint = os.environ.get(
|
|
98
|
-
"OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4318/v1/traces"
|
|
99
|
-
)
|
|
129
|
+
endpoint = os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4318/v1/traces")
|
|
100
130
|
headers = os.environ.get("OTEL_EXPORTER_OTLP_HEADERS", "")
|
|
101
131
|
|
|
102
132
|
# Parse headers if provided
|
|
@@ -107,13 +137,8 @@ def init_telemetry(service_name: str = "golf-mcp-server") -> TracerProvider | No
|
|
|
107
137
|
key, value = header.split("=", 1)
|
|
108
138
|
header_dict[key.strip()] = value.strip()
|
|
109
139
|
|
|
110
|
-
exporter = OTLPSpanExporter(
|
|
111
|
-
endpoint=endpoint, headers=header_dict if header_dict else None
|
|
112
|
-
)
|
|
140
|
+
exporter = OTLPSpanExporter(endpoint=endpoint, headers=header_dict if header_dict else None)
|
|
113
141
|
|
|
114
|
-
# Log successful configuration for Golf platform
|
|
115
|
-
if golf_api_key:
|
|
116
|
-
print(f"[INFO] OpenTelemetry configured for Golf platform: {endpoint}")
|
|
117
142
|
else:
|
|
118
143
|
# Default to console exporter
|
|
119
144
|
exporter = ConsoleSpanExporter(out=sys.stderr)
|
|
@@ -128,7 +153,8 @@ def init_telemetry(service_name: str = "golf-mcp-server") -> TracerProvider | No
|
|
|
128
153
|
processor = BatchSpanProcessor(
|
|
129
154
|
exporter,
|
|
130
155
|
max_queue_size=2048,
|
|
131
|
-
schedule_delay_millis=1000, # Export every 1 second instead of
|
|
156
|
+
schedule_delay_millis=1000, # Export every 1 second instead of
|
|
157
|
+
# default 5 seconds
|
|
132
158
|
max_export_batch_size=512,
|
|
133
159
|
export_timeout_millis=5000,
|
|
134
160
|
)
|
|
@@ -143,10 +169,7 @@ def init_telemetry(service_name: str = "golf-mcp-server") -> TracerProvider | No
|
|
|
143
169
|
try:
|
|
144
170
|
# Check if a provider is already set to avoid the warning
|
|
145
171
|
existing_provider = trace.get_tracer_provider()
|
|
146
|
-
if (
|
|
147
|
-
existing_provider is None
|
|
148
|
-
or str(type(existing_provider).__name__) == "ProxyTracerProvider"
|
|
149
|
-
):
|
|
172
|
+
if existing_provider is None or str(type(existing_provider).__name__) == "ProxyTracerProvider":
|
|
150
173
|
# Only set if no provider exists or it's the default proxy provider
|
|
151
174
|
trace.set_tracer_provider(provider)
|
|
152
175
|
_provider = provider
|
|
@@ -183,7 +206,7 @@ def instrument_tool(func: Callable[..., T], tool_name: str) -> Callable[..., T]:
|
|
|
183
206
|
tracer = get_tracer()
|
|
184
207
|
|
|
185
208
|
@functools.wraps(func)
|
|
186
|
-
async def async_wrapper(*args, **kwargs):
|
|
209
|
+
async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
187
210
|
# Record metrics timing
|
|
188
211
|
import time
|
|
189
212
|
|
|
@@ -194,20 +217,25 @@ def instrument_tool(func: Callable[..., T], tool_name: str) -> Callable[..., T]:
|
|
|
194
217
|
|
|
195
218
|
# start_as_current_span automatically uses the current context and manages it
|
|
196
219
|
with tracer.start_as_current_span(span_name) as span:
|
|
197
|
-
# Add
|
|
220
|
+
# Add essential attributes only
|
|
198
221
|
span.set_attribute("mcp.component.type", "tool")
|
|
199
|
-
span.set_attribute("mcp.component.name", tool_name)
|
|
200
222
|
span.set_attribute("mcp.tool.name", tool_name)
|
|
201
|
-
span.set_attribute("mcp.tool.function", func.__name__)
|
|
202
223
|
span.set_attribute(
|
|
203
224
|
"mcp.tool.module",
|
|
204
225
|
func.__module__ if hasattr(func, "__module__") else "unknown",
|
|
205
226
|
)
|
|
206
227
|
|
|
207
|
-
# Add execution context
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
228
|
+
# Add minimal execution context
|
|
229
|
+
if args or kwargs:
|
|
230
|
+
span.set_attribute("mcp.execution.has_params", True)
|
|
231
|
+
|
|
232
|
+
# Capture inputs if detailed tracing is enabled
|
|
233
|
+
if _detailed_tracing_enabled and (args or kwargs):
|
|
234
|
+
input_data = {"args": args, "kwargs": kwargs} if args or kwargs else None
|
|
235
|
+
if input_data:
|
|
236
|
+
input_str = _safe_serialize(input_data)
|
|
237
|
+
if input_str:
|
|
238
|
+
span.set_attribute("mcp.tool.input", input_str)
|
|
211
239
|
|
|
212
240
|
# Extract Context parameter if present
|
|
213
241
|
ctx = kwargs.get("ctx")
|
|
@@ -247,42 +275,25 @@ def instrument_tool(func: Callable[..., T], tool_name: str) -> Callable[..., T]:
|
|
|
247
275
|
|
|
248
276
|
metrics_collector = get_metrics_collector()
|
|
249
277
|
metrics_collector.increment_tool_execution(tool_name, "success")
|
|
250
|
-
metrics_collector.record_tool_duration(
|
|
251
|
-
tool_name, time.time() - start_time
|
|
252
|
-
)
|
|
278
|
+
metrics_collector.record_tool_duration(tool_name, time.time() - start_time)
|
|
253
279
|
except ImportError:
|
|
254
280
|
# Metrics not available, continue without metrics
|
|
255
281
|
pass
|
|
256
282
|
|
|
257
|
-
# Capture result metadata
|
|
283
|
+
# Capture result metadata
|
|
258
284
|
if result is not None:
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
elif isinstance(result, list):
|
|
265
|
-
span.set_attribute("mcp.tool.result.count", len(result))
|
|
266
|
-
span.set_attribute("mcp.tool.result.type", "array")
|
|
267
|
-
elif isinstance(result, dict):
|
|
268
|
-
span.set_attribute("mcp.tool.result.count", len(result))
|
|
269
|
-
span.set_attribute("mcp.tool.result.type", "object")
|
|
270
|
-
# Only show first few keys to avoid exceeding attribute limits
|
|
271
|
-
if len(result) > 0 and len(result) <= 5:
|
|
272
|
-
keys_list = list(result.keys())[:5]
|
|
273
|
-
# Limit key length and join
|
|
274
|
-
truncated_keys = [
|
|
275
|
-
str(k)[:20] + "..." if len(str(k)) > 20 else str(k)
|
|
276
|
-
for k in keys_list
|
|
277
|
-
]
|
|
278
|
-
span.set_attribute(
|
|
279
|
-
"mcp.tool.result.sample_keys", ",".join(truncated_keys)
|
|
280
|
-
)
|
|
281
|
-
elif hasattr(result, "__len__"):
|
|
285
|
+
span.set_attribute("mcp.tool.result.type", type(result).__name__)
|
|
286
|
+
|
|
287
|
+
if isinstance(result, list | dict) and hasattr(result, "__len__"):
|
|
288
|
+
span.set_attribute("mcp.tool.result.size", len(result))
|
|
289
|
+
elif isinstance(result, str):
|
|
282
290
|
span.set_attribute("mcp.tool.result.length", len(result))
|
|
283
291
|
|
|
284
|
-
#
|
|
285
|
-
|
|
292
|
+
# Capture full output if detailed tracing is enabled
|
|
293
|
+
if _detailed_tracing_enabled:
|
|
294
|
+
output_str = _safe_serialize(result)
|
|
295
|
+
if output_str:
|
|
296
|
+
span.set_attribute("mcp.tool.output", output_str)
|
|
286
297
|
|
|
287
298
|
return result
|
|
288
299
|
except Exception as e:
|
|
@@ -313,7 +324,7 @@ def instrument_tool(func: Callable[..., T], tool_name: str) -> Callable[..., T]:
|
|
|
313
324
|
raise
|
|
314
325
|
|
|
315
326
|
@functools.wraps(func)
|
|
316
|
-
def sync_wrapper(*args, **kwargs):
|
|
327
|
+
def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
317
328
|
# Record metrics timing
|
|
318
329
|
import time
|
|
319
330
|
|
|
@@ -324,11 +335,9 @@ def instrument_tool(func: Callable[..., T], tool_name: str) -> Callable[..., T]:
|
|
|
324
335
|
|
|
325
336
|
# start_as_current_span automatically uses the current context and manages it
|
|
326
337
|
with tracer.start_as_current_span(span_name) as span:
|
|
327
|
-
# Add
|
|
338
|
+
# Add essential attributes only
|
|
328
339
|
span.set_attribute("mcp.component.type", "tool")
|
|
329
|
-
span.set_attribute("mcp.component.name", tool_name)
|
|
330
340
|
span.set_attribute("mcp.tool.name", tool_name)
|
|
331
|
-
span.set_attribute("mcp.tool.function", func.__name__)
|
|
332
341
|
span.set_attribute(
|
|
333
342
|
"mcp.tool.module",
|
|
334
343
|
func.__module__ if hasattr(func, "__module__") else "unknown",
|
|
@@ -337,7 +346,6 @@ def instrument_tool(func: Callable[..., T], tool_name: str) -> Callable[..., T]:
|
|
|
337
346
|
# Add execution context
|
|
338
347
|
span.set_attribute("mcp.execution.args_count", len(args))
|
|
339
348
|
span.set_attribute("mcp.execution.kwargs_count", len(kwargs))
|
|
340
|
-
span.set_attribute("mcp.execution.async", False)
|
|
341
349
|
|
|
342
350
|
# Extract Context parameter if present
|
|
343
351
|
ctx = kwargs.get("ctx")
|
|
@@ -377,42 +385,25 @@ def instrument_tool(func: Callable[..., T], tool_name: str) -> Callable[..., T]:
|
|
|
377
385
|
|
|
378
386
|
metrics_collector = get_metrics_collector()
|
|
379
387
|
metrics_collector.increment_tool_execution(tool_name, "success")
|
|
380
|
-
metrics_collector.record_tool_duration(
|
|
381
|
-
tool_name, time.time() - start_time
|
|
382
|
-
)
|
|
388
|
+
metrics_collector.record_tool_duration(tool_name, time.time() - start_time)
|
|
383
389
|
except ImportError:
|
|
384
390
|
# Metrics not available, continue without metrics
|
|
385
391
|
pass
|
|
386
392
|
|
|
387
|
-
# Capture result metadata
|
|
393
|
+
# Capture result metadata
|
|
388
394
|
if result is not None:
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
elif isinstance(result, list):
|
|
395
|
-
span.set_attribute("mcp.tool.result.count", len(result))
|
|
396
|
-
span.set_attribute("mcp.tool.result.type", "array")
|
|
397
|
-
elif isinstance(result, dict):
|
|
398
|
-
span.set_attribute("mcp.tool.result.count", len(result))
|
|
399
|
-
span.set_attribute("mcp.tool.result.type", "object")
|
|
400
|
-
# Only show first few keys to avoid exceeding attribute limits
|
|
401
|
-
if len(result) > 0 and len(result) <= 5:
|
|
402
|
-
keys_list = list(result.keys())[:5]
|
|
403
|
-
# Limit key length and join
|
|
404
|
-
truncated_keys = [
|
|
405
|
-
str(k)[:20] + "..." if len(str(k)) > 20 else str(k)
|
|
406
|
-
for k in keys_list
|
|
407
|
-
]
|
|
408
|
-
span.set_attribute(
|
|
409
|
-
"mcp.tool.result.sample_keys", ",".join(truncated_keys)
|
|
410
|
-
)
|
|
411
|
-
elif hasattr(result, "__len__"):
|
|
395
|
+
span.set_attribute("mcp.tool.result.type", type(result).__name__)
|
|
396
|
+
|
|
397
|
+
if isinstance(result, list | dict) and hasattr(result, "__len__"):
|
|
398
|
+
span.set_attribute("mcp.tool.result.size", len(result))
|
|
399
|
+
elif isinstance(result, str):
|
|
412
400
|
span.set_attribute("mcp.tool.result.length", len(result))
|
|
413
401
|
|
|
414
|
-
#
|
|
415
|
-
|
|
402
|
+
# Capture full output if detailed tracing is enabled
|
|
403
|
+
if _detailed_tracing_enabled:
|
|
404
|
+
output_str = _safe_serialize(result)
|
|
405
|
+
if output_str:
|
|
406
|
+
span.set_attribute("mcp.tool.output", output_str)
|
|
416
407
|
|
|
417
408
|
return result
|
|
418
409
|
except Exception as e:
|
|
@@ -463,21 +454,18 @@ def instrument_resource(func: Callable[..., T], resource_uri: str) -> Callable[.
|
|
|
463
454
|
is_template = "{" in resource_uri
|
|
464
455
|
|
|
465
456
|
@functools.wraps(func)
|
|
466
|
-
async def async_wrapper(*args, **kwargs):
|
|
457
|
+
async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
467
458
|
# Create a more descriptive span name
|
|
468
459
|
span_name = f"mcp.resource.{'template' if is_template else 'static'}.read"
|
|
469
460
|
with tracer.start_as_current_span(span_name) as span:
|
|
470
|
-
# Add
|
|
461
|
+
# Add essential attributes only
|
|
471
462
|
span.set_attribute("mcp.component.type", "resource")
|
|
472
|
-
span.set_attribute("mcp.component.name", resource_uri)
|
|
473
463
|
span.set_attribute("mcp.resource.uri", resource_uri)
|
|
474
464
|
span.set_attribute("mcp.resource.is_template", is_template)
|
|
475
|
-
span.set_attribute("mcp.resource.function", func.__name__)
|
|
476
465
|
span.set_attribute(
|
|
477
466
|
"mcp.resource.module",
|
|
478
467
|
func.__module__ if hasattr(func, "__module__") else "unknown",
|
|
479
468
|
)
|
|
480
|
-
span.set_attribute("mcp.execution.async", True)
|
|
481
469
|
|
|
482
470
|
# Extract Context parameter if present
|
|
483
471
|
ctx = kwargs.get("ctx")
|
|
@@ -509,9 +497,7 @@ def instrument_resource(func: Callable[..., T], resource_uri: str) -> Callable[.
|
|
|
509
497
|
span.set_status(Status(StatusCode.OK))
|
|
510
498
|
|
|
511
499
|
# Add event for successful read
|
|
512
|
-
span.add_event(
|
|
513
|
-
"resource.read.completed", {"resource.uri": resource_uri}
|
|
514
|
-
)
|
|
500
|
+
span.add_event("resource.read.completed", {"resource.uri": resource_uri})
|
|
515
501
|
|
|
516
502
|
# Add result metadata
|
|
517
503
|
if hasattr(result, "__len__"):
|
|
@@ -548,21 +534,18 @@ def instrument_resource(func: Callable[..., T], resource_uri: str) -> Callable[.
|
|
|
548
534
|
raise
|
|
549
535
|
|
|
550
536
|
@functools.wraps(func)
|
|
551
|
-
def sync_wrapper(*args, **kwargs):
|
|
537
|
+
def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
552
538
|
# Create a more descriptive span name
|
|
553
539
|
span_name = f"mcp.resource.{'template' if is_template else 'static'}.read"
|
|
554
540
|
with tracer.start_as_current_span(span_name) as span:
|
|
555
|
-
# Add
|
|
541
|
+
# Add essential attributes only
|
|
556
542
|
span.set_attribute("mcp.component.type", "resource")
|
|
557
|
-
span.set_attribute("mcp.component.name", resource_uri)
|
|
558
543
|
span.set_attribute("mcp.resource.uri", resource_uri)
|
|
559
544
|
span.set_attribute("mcp.resource.is_template", is_template)
|
|
560
|
-
span.set_attribute("mcp.resource.function", func.__name__)
|
|
561
545
|
span.set_attribute(
|
|
562
546
|
"mcp.resource.module",
|
|
563
547
|
func.__module__ if hasattr(func, "__module__") else "unknown",
|
|
564
548
|
)
|
|
565
|
-
span.set_attribute("mcp.execution.async", False)
|
|
566
549
|
|
|
567
550
|
# Extract Context parameter if present
|
|
568
551
|
ctx = kwargs.get("ctx")
|
|
@@ -594,9 +577,7 @@ def instrument_resource(func: Callable[..., T], resource_uri: str) -> Callable[.
|
|
|
594
577
|
span.set_status(Status(StatusCode.OK))
|
|
595
578
|
|
|
596
579
|
# Add event for successful read
|
|
597
|
-
span.add_event(
|
|
598
|
-
"resource.read.completed", {"resource.uri": resource_uri}
|
|
599
|
-
)
|
|
580
|
+
span.add_event("resource.read.completed", {"resource.uri": resource_uri})
|
|
600
581
|
|
|
601
582
|
# Add result metadata
|
|
602
583
|
if hasattr(result, "__len__"):
|
|
@@ -638,6 +619,370 @@ def instrument_resource(func: Callable[..., T], resource_uri: str) -> Callable[.
|
|
|
638
619
|
return sync_wrapper
|
|
639
620
|
|
|
640
621
|
|
|
622
|
+
def instrument_elicitation(func: Callable[..., T], elicitation_type: str = "elicit") -> Callable[..., T]:
|
|
623
|
+
"""Instrument an elicitation function with OpenTelemetry tracing."""
|
|
624
|
+
global _provider
|
|
625
|
+
|
|
626
|
+
# If telemetry is disabled, return the original function
|
|
627
|
+
if _provider is None:
|
|
628
|
+
return func
|
|
629
|
+
|
|
630
|
+
tracer = get_tracer()
|
|
631
|
+
|
|
632
|
+
@functools.wraps(func)
|
|
633
|
+
async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
634
|
+
# If telemetry is disabled at runtime, call original function
|
|
635
|
+
global _provider
|
|
636
|
+
if _provider is None:
|
|
637
|
+
return await func(*args, **kwargs)
|
|
638
|
+
|
|
639
|
+
# Record metrics timing
|
|
640
|
+
start_time = time.time()
|
|
641
|
+
|
|
642
|
+
# Create a more descriptive span name
|
|
643
|
+
span_name = f"mcp.elicitation.{elicitation_type}.request"
|
|
644
|
+
with tracer.start_as_current_span(span_name) as span:
|
|
645
|
+
# Add essential attributes
|
|
646
|
+
span.set_attribute("mcp.component.type", "elicitation")
|
|
647
|
+
span.set_attribute("mcp.elicitation.type", elicitation_type)
|
|
648
|
+
|
|
649
|
+
# Capture elicitation parameters if detailed tracing is enabled
|
|
650
|
+
if _detailed_tracing_enabled:
|
|
651
|
+
# Extract message from first argument (common pattern)
|
|
652
|
+
if args:
|
|
653
|
+
message = args[0] if isinstance(args[0], str) else None
|
|
654
|
+
if message:
|
|
655
|
+
span.set_attribute("mcp.elicitation.message", _safe_serialize(message, 500))
|
|
656
|
+
|
|
657
|
+
# Extract response_type from kwargs/args
|
|
658
|
+
response_type = kwargs.get("response_type") or (args[1] if len(args) > 1 else None)
|
|
659
|
+
if response_type is not None:
|
|
660
|
+
if isinstance(response_type, list):
|
|
661
|
+
span.set_attribute("mcp.elicitation.response_type", "choice")
|
|
662
|
+
span.set_attribute("mcp.elicitation.choices", str(response_type))
|
|
663
|
+
elif hasattr(response_type, "__name__"):
|
|
664
|
+
span.set_attribute("mcp.elicitation.response_type", response_type.__name__)
|
|
665
|
+
else:
|
|
666
|
+
span.set_attribute("mcp.elicitation.response_type", str(type(response_type).__name__))
|
|
667
|
+
|
|
668
|
+
# Extract Context parameter if present
|
|
669
|
+
ctx = kwargs.get("ctx")
|
|
670
|
+
if ctx:
|
|
671
|
+
ctx_attrs = ["request_id", "session_id", "client_id", "user_id", "tenant_id"]
|
|
672
|
+
for attr in ctx_attrs:
|
|
673
|
+
if hasattr(ctx, attr):
|
|
674
|
+
value = getattr(ctx, attr)
|
|
675
|
+
if value is not None:
|
|
676
|
+
span.set_attribute(f"mcp.context.{attr}", str(value))
|
|
677
|
+
|
|
678
|
+
# Add event for elicitation start
|
|
679
|
+
span.add_event("elicitation.request.started")
|
|
680
|
+
|
|
681
|
+
try:
|
|
682
|
+
result = await func(*args, **kwargs)
|
|
683
|
+
span.set_status(Status(StatusCode.OK))
|
|
684
|
+
|
|
685
|
+
# Add event for successful completion
|
|
686
|
+
span.add_event("elicitation.request.completed")
|
|
687
|
+
|
|
688
|
+
# Capture result metadata
|
|
689
|
+
if result is not None and _detailed_tracing_enabled:
|
|
690
|
+
if isinstance(result, str):
|
|
691
|
+
span.set_attribute("mcp.elicitation.result.content", _safe_serialize(result, 500))
|
|
692
|
+
elif isinstance(result, (list, dict)) and hasattr(result, "__len__"):
|
|
693
|
+
span.set_attribute("mcp.elicitation.result.size", len(result))
|
|
694
|
+
span.set_attribute("mcp.elicitation.result.content", _safe_serialize(result, 1000))
|
|
695
|
+
|
|
696
|
+
# Record metrics for successful elicitation
|
|
697
|
+
try:
|
|
698
|
+
from golf.metrics import get_metrics_collector
|
|
699
|
+
|
|
700
|
+
metrics_collector = get_metrics_collector()
|
|
701
|
+
metrics_collector.increment_elicitation(elicitation_type, "success")
|
|
702
|
+
metrics_collector.record_elicitation_duration(elicitation_type, time.time() - start_time)
|
|
703
|
+
except ImportError:
|
|
704
|
+
pass
|
|
705
|
+
|
|
706
|
+
return result
|
|
707
|
+
except Exception as e:
|
|
708
|
+
span.record_exception(e)
|
|
709
|
+
span.set_status(Status(StatusCode.ERROR, str(e)))
|
|
710
|
+
|
|
711
|
+
# Add event for error
|
|
712
|
+
span.add_event(
|
|
713
|
+
"elicitation.request.error",
|
|
714
|
+
{
|
|
715
|
+
"error.type": type(e).__name__,
|
|
716
|
+
"error.message": str(e),
|
|
717
|
+
},
|
|
718
|
+
)
|
|
719
|
+
|
|
720
|
+
# Record metrics for failed elicitation
|
|
721
|
+
try:
|
|
722
|
+
from golf.metrics import get_metrics_collector
|
|
723
|
+
|
|
724
|
+
metrics_collector = get_metrics_collector()
|
|
725
|
+
metrics_collector.increment_elicitation(elicitation_type, "error")
|
|
726
|
+
metrics_collector.increment_error("elicitation", type(e).__name__)
|
|
727
|
+
except ImportError:
|
|
728
|
+
pass
|
|
729
|
+
|
|
730
|
+
raise
|
|
731
|
+
|
|
732
|
+
@functools.wraps(func)
|
|
733
|
+
def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
734
|
+
# If telemetry is disabled at runtime, call original function
|
|
735
|
+
global _provider
|
|
736
|
+
if _provider is None:
|
|
737
|
+
return func(*args, **kwargs)
|
|
738
|
+
|
|
739
|
+
# Record metrics timing
|
|
740
|
+
start_time = time.time()
|
|
741
|
+
|
|
742
|
+
# Create a more descriptive span name
|
|
743
|
+
span_name = f"mcp.elicitation.{elicitation_type}.request"
|
|
744
|
+
with tracer.start_as_current_span(span_name) as span:
|
|
745
|
+
# Add essential attributes
|
|
746
|
+
span.set_attribute("mcp.component.type", "elicitation")
|
|
747
|
+
span.set_attribute("mcp.elicitation.type", elicitation_type)
|
|
748
|
+
|
|
749
|
+
# Capture elicitation parameters if detailed tracing is enabled
|
|
750
|
+
if _detailed_tracing_enabled:
|
|
751
|
+
if args:
|
|
752
|
+
message = args[0] if isinstance(args[0], str) else None
|
|
753
|
+
if message:
|
|
754
|
+
span.set_attribute("mcp.elicitation.message", _safe_serialize(message, 500))
|
|
755
|
+
|
|
756
|
+
# Add event for elicitation start
|
|
757
|
+
span.add_event("elicitation.request.started")
|
|
758
|
+
|
|
759
|
+
try:
|
|
760
|
+
result = func(*args, **kwargs)
|
|
761
|
+
span.set_status(Status(StatusCode.OK))
|
|
762
|
+
|
|
763
|
+
# Add event for successful completion
|
|
764
|
+
span.add_event("elicitation.request.completed")
|
|
765
|
+
|
|
766
|
+
# Record metrics for successful elicitation
|
|
767
|
+
try:
|
|
768
|
+
from golf.metrics import get_metrics_collector
|
|
769
|
+
|
|
770
|
+
metrics_collector = get_metrics_collector()
|
|
771
|
+
metrics_collector.increment_elicitation(elicitation_type, "success")
|
|
772
|
+
metrics_collector.record_elicitation_duration(elicitation_type, time.time() - start_time)
|
|
773
|
+
except ImportError:
|
|
774
|
+
pass
|
|
775
|
+
|
|
776
|
+
return result
|
|
777
|
+
except Exception as e:
|
|
778
|
+
span.record_exception(e)
|
|
779
|
+
span.set_status(Status(StatusCode.ERROR, str(e)))
|
|
780
|
+
|
|
781
|
+
# Add event for error
|
|
782
|
+
span.add_event(
|
|
783
|
+
"elicitation.request.error",
|
|
784
|
+
{
|
|
785
|
+
"error.type": type(e).__name__,
|
|
786
|
+
"error.message": str(e),
|
|
787
|
+
},
|
|
788
|
+
)
|
|
789
|
+
|
|
790
|
+
# Record metrics for failed elicitation
|
|
791
|
+
try:
|
|
792
|
+
from golf.metrics import get_metrics_collector
|
|
793
|
+
|
|
794
|
+
metrics_collector = get_metrics_collector()
|
|
795
|
+
metrics_collector.increment_elicitation(elicitation_type, "error")
|
|
796
|
+
metrics_collector.increment_error("elicitation", type(e).__name__)
|
|
797
|
+
except ImportError:
|
|
798
|
+
pass
|
|
799
|
+
|
|
800
|
+
raise
|
|
801
|
+
|
|
802
|
+
if asyncio.iscoroutinefunction(func):
|
|
803
|
+
return async_wrapper
|
|
804
|
+
else:
|
|
805
|
+
return sync_wrapper
|
|
806
|
+
|
|
807
|
+
|
|
808
|
+
def instrument_sampling(func: Callable[..., T], sampling_type: str = "sample") -> Callable[..., T]:
|
|
809
|
+
"""Instrument a sampling function with OpenTelemetry tracing."""
|
|
810
|
+
global _provider
|
|
811
|
+
|
|
812
|
+
# If telemetry is disabled, return the original function
|
|
813
|
+
if _provider is None:
|
|
814
|
+
return func
|
|
815
|
+
|
|
816
|
+
tracer = get_tracer()
|
|
817
|
+
|
|
818
|
+
@functools.wraps(func)
|
|
819
|
+
async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
820
|
+
# If telemetry is disabled at runtime, call original function
|
|
821
|
+
global _provider
|
|
822
|
+
if _provider is None:
|
|
823
|
+
return await func(*args, **kwargs)
|
|
824
|
+
|
|
825
|
+
# Record metrics timing
|
|
826
|
+
start_time = time.time()
|
|
827
|
+
|
|
828
|
+
# Create a more descriptive span name
|
|
829
|
+
span_name = f"mcp.sampling.{sampling_type}.request"
|
|
830
|
+
with tracer.start_as_current_span(span_name) as span:
|
|
831
|
+
# Add essential attributes
|
|
832
|
+
span.set_attribute("mcp.component.type", "sampling")
|
|
833
|
+
span.set_attribute("mcp.sampling.type", sampling_type)
|
|
834
|
+
|
|
835
|
+
# Capture sampling parameters
|
|
836
|
+
messages = kwargs.get("messages") or (args[0] if args else None)
|
|
837
|
+
if messages and _detailed_tracing_enabled:
|
|
838
|
+
if isinstance(messages, str):
|
|
839
|
+
span.set_attribute("mcp.sampling.messages.content", _safe_serialize(messages, 1000))
|
|
840
|
+
elif isinstance(messages, list):
|
|
841
|
+
span.set_attribute("mcp.sampling.messages.type", "list")
|
|
842
|
+
span.set_attribute("mcp.sampling.messages.count", len(messages))
|
|
843
|
+
span.set_attribute("mcp.sampling.messages.content", _safe_serialize(messages, 1000))
|
|
844
|
+
|
|
845
|
+
# Capture other sampling parameters
|
|
846
|
+
system_prompt = kwargs.get("system_prompt")
|
|
847
|
+
if system_prompt and _detailed_tracing_enabled:
|
|
848
|
+
span.set_attribute("mcp.sampling.system_prompt.length", len(str(system_prompt)))
|
|
849
|
+
span.set_attribute("mcp.sampling.system_prompt.content", _safe_serialize(system_prompt, 500))
|
|
850
|
+
|
|
851
|
+
temperature = kwargs.get("temperature")
|
|
852
|
+
if temperature is not None:
|
|
853
|
+
span.set_attribute("mcp.sampling.temperature", temperature)
|
|
854
|
+
|
|
855
|
+
max_tokens = kwargs.get("max_tokens")
|
|
856
|
+
if max_tokens is not None:
|
|
857
|
+
span.set_attribute("mcp.sampling.max_tokens", max_tokens)
|
|
858
|
+
|
|
859
|
+
model_preferences = kwargs.get("model_preferences")
|
|
860
|
+
if model_preferences:
|
|
861
|
+
if isinstance(model_preferences, str):
|
|
862
|
+
span.set_attribute("mcp.sampling.model_preferences", model_preferences)
|
|
863
|
+
elif isinstance(model_preferences, list):
|
|
864
|
+
span.set_attribute("mcp.sampling.model_preferences", ",".join(model_preferences))
|
|
865
|
+
|
|
866
|
+
# Extract Context parameter if present
|
|
867
|
+
ctx = kwargs.get("ctx")
|
|
868
|
+
if ctx:
|
|
869
|
+
ctx_attrs = ["request_id", "session_id", "client_id", "user_id", "tenant_id"]
|
|
870
|
+
for attr in ctx_attrs:
|
|
871
|
+
if hasattr(ctx, attr):
|
|
872
|
+
value = getattr(ctx, attr)
|
|
873
|
+
if value is not None:
|
|
874
|
+
span.set_attribute(f"mcp.context.{attr}", str(value))
|
|
875
|
+
|
|
876
|
+
# Add event for sampling start
|
|
877
|
+
span.add_event("sampling.request.started")
|
|
878
|
+
|
|
879
|
+
try:
|
|
880
|
+
result = await func(*args, **kwargs)
|
|
881
|
+
span.set_status(Status(StatusCode.OK))
|
|
882
|
+
|
|
883
|
+
# Add event for successful completion
|
|
884
|
+
span.add_event("sampling.request.completed")
|
|
885
|
+
|
|
886
|
+
# Capture result metadata
|
|
887
|
+
if result is not None and _detailed_tracing_enabled and isinstance(result, str):
|
|
888
|
+
span.set_attribute("mcp.sampling.result.content", _safe_serialize(result, 1000))
|
|
889
|
+
|
|
890
|
+
# Record metrics for successful sampling
|
|
891
|
+
try:
|
|
892
|
+
from golf.metrics import get_metrics_collector
|
|
893
|
+
|
|
894
|
+
metrics_collector = get_metrics_collector()
|
|
895
|
+
metrics_collector.increment_sampling(sampling_type, "success")
|
|
896
|
+
metrics_collector.record_sampling_duration(sampling_type, time.time() - start_time)
|
|
897
|
+
if isinstance(result, str):
|
|
898
|
+
metrics_collector.record_sampling_tokens(sampling_type, len(result.split()))
|
|
899
|
+
except ImportError:
|
|
900
|
+
pass
|
|
901
|
+
|
|
902
|
+
return result
|
|
903
|
+
except Exception as e:
|
|
904
|
+
span.record_exception(e)
|
|
905
|
+
span.set_status(Status(StatusCode.ERROR, str(e)))
|
|
906
|
+
|
|
907
|
+
# Add event for error
|
|
908
|
+
span.add_event(
|
|
909
|
+
"sampling.request.error",
|
|
910
|
+
{
|
|
911
|
+
"error.type": type(e).__name__,
|
|
912
|
+
"error.message": str(e),
|
|
913
|
+
},
|
|
914
|
+
)
|
|
915
|
+
|
|
916
|
+
# Record metrics for failed sampling
|
|
917
|
+
try:
|
|
918
|
+
from golf.metrics import get_metrics_collector
|
|
919
|
+
|
|
920
|
+
metrics_collector = get_metrics_collector()
|
|
921
|
+
metrics_collector.increment_sampling(sampling_type, "error")
|
|
922
|
+
metrics_collector.increment_error("sampling", type(e).__name__)
|
|
923
|
+
except ImportError:
|
|
924
|
+
pass
|
|
925
|
+
|
|
926
|
+
raise
|
|
927
|
+
|
|
928
|
+
@functools.wraps(func)
|
|
929
|
+
def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
930
|
+
# If telemetry is disabled at runtime, call original function
|
|
931
|
+
global _provider
|
|
932
|
+
if _provider is None:
|
|
933
|
+
return func(*args, **kwargs)
|
|
934
|
+
|
|
935
|
+
# Record metrics timing
|
|
936
|
+
start_time = time.time()
|
|
937
|
+
|
|
938
|
+
# Create a more descriptive span name
|
|
939
|
+
span_name = f"mcp.sampling.{sampling_type}.request"
|
|
940
|
+
with tracer.start_as_current_span(span_name) as span:
|
|
941
|
+
# Add essential attributes
|
|
942
|
+
span.set_attribute("mcp.component.type", "sampling")
|
|
943
|
+
span.set_attribute("mcp.sampling.type", sampling_type)
|
|
944
|
+
|
|
945
|
+
# Add event for sampling start
|
|
946
|
+
span.add_event("sampling.request.started")
|
|
947
|
+
|
|
948
|
+
try:
|
|
949
|
+
result = func(*args, **kwargs)
|
|
950
|
+
span.set_status(Status(StatusCode.OK))
|
|
951
|
+
|
|
952
|
+
# Add event for successful completion
|
|
953
|
+
span.add_event("sampling.request.completed")
|
|
954
|
+
|
|
955
|
+
# Record metrics for successful sampling
|
|
956
|
+
try:
|
|
957
|
+
from golf.metrics import get_metrics_collector
|
|
958
|
+
|
|
959
|
+
metrics_collector = get_metrics_collector()
|
|
960
|
+
metrics_collector.increment_sampling(sampling_type, "success")
|
|
961
|
+
metrics_collector.record_sampling_duration(sampling_type, time.time() - start_time)
|
|
962
|
+
except ImportError:
|
|
963
|
+
pass
|
|
964
|
+
|
|
965
|
+
return result
|
|
966
|
+
except Exception as e:
|
|
967
|
+
span.record_exception(e)
|
|
968
|
+
span.set_status(Status(StatusCode.ERROR, str(e)))
|
|
969
|
+
|
|
970
|
+
# Add event for error
|
|
971
|
+
span.add_event(
|
|
972
|
+
"sampling.request.error",
|
|
973
|
+
{
|
|
974
|
+
"error.type": type(e).__name__,
|
|
975
|
+
"error.message": str(e),
|
|
976
|
+
},
|
|
977
|
+
)
|
|
978
|
+
raise
|
|
979
|
+
|
|
980
|
+
if asyncio.iscoroutinefunction(func):
|
|
981
|
+
return async_wrapper
|
|
982
|
+
else:
|
|
983
|
+
return sync_wrapper
|
|
984
|
+
|
|
985
|
+
|
|
641
986
|
def instrument_prompt(func: Callable[..., T], prompt_name: str) -> Callable[..., T]:
|
|
642
987
|
"""Instrument a prompt function with OpenTelemetry tracing."""
|
|
643
988
|
global _provider
|
|
@@ -649,20 +994,17 @@ def instrument_prompt(func: Callable[..., T], prompt_name: str) -> Callable[...,
|
|
|
649
994
|
tracer = get_tracer()
|
|
650
995
|
|
|
651
996
|
@functools.wraps(func)
|
|
652
|
-
async def async_wrapper(*args, **kwargs):
|
|
997
|
+
async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
653
998
|
# Create a more descriptive span name
|
|
654
999
|
span_name = f"mcp.prompt.{prompt_name}.generate"
|
|
655
1000
|
with tracer.start_as_current_span(span_name) as span:
|
|
656
|
-
# Add
|
|
1001
|
+
# Add essential attributes only
|
|
657
1002
|
span.set_attribute("mcp.component.type", "prompt")
|
|
658
|
-
span.set_attribute("mcp.component.name", prompt_name)
|
|
659
1003
|
span.set_attribute("mcp.prompt.name", prompt_name)
|
|
660
|
-
span.set_attribute("mcp.prompt.function", func.__name__)
|
|
661
1004
|
span.set_attribute(
|
|
662
1005
|
"mcp.prompt.module",
|
|
663
1006
|
func.__module__ if hasattr(func, "__module__") else "unknown",
|
|
664
1007
|
)
|
|
665
|
-
span.set_attribute("mcp.execution.async", True)
|
|
666
1008
|
|
|
667
1009
|
# Extract Context parameter if present
|
|
668
1010
|
ctx = kwargs.get("ctx")
|
|
@@ -694,9 +1036,7 @@ def instrument_prompt(func: Callable[..., T], prompt_name: str) -> Callable[...,
|
|
|
694
1036
|
span.set_status(Status(StatusCode.OK))
|
|
695
1037
|
|
|
696
1038
|
# Add event for successful generation
|
|
697
|
-
span.add_event(
|
|
698
|
-
"prompt.generation.completed", {"prompt.name": prompt_name}
|
|
699
|
-
)
|
|
1039
|
+
span.add_event("prompt.generation.completed", {"prompt.name": prompt_name})
|
|
700
1040
|
|
|
701
1041
|
# Add message count and type information
|
|
702
1042
|
if isinstance(result, list):
|
|
@@ -713,9 +1053,7 @@ def instrument_prompt(func: Callable[..., T], prompt_name: str) -> Callable[...,
|
|
|
713
1053
|
|
|
714
1054
|
if roles:
|
|
715
1055
|
unique_roles = list(set(roles))
|
|
716
|
-
span.set_attribute(
|
|
717
|
-
"mcp.prompt.result.roles", ",".join(unique_roles)
|
|
718
|
-
)
|
|
1056
|
+
span.set_attribute("mcp.prompt.result.roles", ",".join(unique_roles))
|
|
719
1057
|
span.set_attribute(
|
|
720
1058
|
"mcp.prompt.result.role_counts",
|
|
721
1059
|
str({role: roles.count(role) for role in unique_roles}),
|
|
@@ -743,20 +1081,17 @@ def instrument_prompt(func: Callable[..., T], prompt_name: str) -> Callable[...,
|
|
|
743
1081
|
raise
|
|
744
1082
|
|
|
745
1083
|
@functools.wraps(func)
|
|
746
|
-
def sync_wrapper(*args, **kwargs):
|
|
1084
|
+
def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
747
1085
|
# Create a more descriptive span name
|
|
748
1086
|
span_name = f"mcp.prompt.{prompt_name}.generate"
|
|
749
1087
|
with tracer.start_as_current_span(span_name) as span:
|
|
750
|
-
# Add
|
|
1088
|
+
# Add essential attributes only
|
|
751
1089
|
span.set_attribute("mcp.component.type", "prompt")
|
|
752
|
-
span.set_attribute("mcp.component.name", prompt_name)
|
|
753
1090
|
span.set_attribute("mcp.prompt.name", prompt_name)
|
|
754
|
-
span.set_attribute("mcp.prompt.function", func.__name__)
|
|
755
1091
|
span.set_attribute(
|
|
756
1092
|
"mcp.prompt.module",
|
|
757
1093
|
func.__module__ if hasattr(func, "__module__") else "unknown",
|
|
758
1094
|
)
|
|
759
|
-
span.set_attribute("mcp.execution.async", False)
|
|
760
1095
|
|
|
761
1096
|
# Extract Context parameter if present
|
|
762
1097
|
ctx = kwargs.get("ctx")
|
|
@@ -788,9 +1123,7 @@ def instrument_prompt(func: Callable[..., T], prompt_name: str) -> Callable[...,
|
|
|
788
1123
|
span.set_status(Status(StatusCode.OK))
|
|
789
1124
|
|
|
790
1125
|
# Add event for successful generation
|
|
791
|
-
span.add_event(
|
|
792
|
-
"prompt.generation.completed", {"prompt.name": prompt_name}
|
|
793
|
-
)
|
|
1126
|
+
span.add_event("prompt.generation.completed", {"prompt.name": prompt_name})
|
|
794
1127
|
|
|
795
1128
|
# Add message count and type information
|
|
796
1129
|
if isinstance(result, list):
|
|
@@ -807,9 +1140,7 @@ def instrument_prompt(func: Callable[..., T], prompt_name: str) -> Callable[...,
|
|
|
807
1140
|
|
|
808
1141
|
if roles:
|
|
809
1142
|
unique_roles = list(set(roles))
|
|
810
|
-
span.set_attribute(
|
|
811
|
-
"mcp.prompt.result.roles", ",".join(unique_roles)
|
|
812
|
-
)
|
|
1143
|
+
span.set_attribute("mcp.prompt.result.roles", ",".join(unique_roles))
|
|
813
1144
|
span.set_attribute(
|
|
814
1145
|
"mcp.prompt.result.role_counts",
|
|
815
1146
|
str({role: roles.count(role) for role in unique_roles}),
|
|
@@ -846,7 +1177,7 @@ def instrument_prompt(func: Callable[..., T], prompt_name: str) -> Callable[...,
|
|
|
846
1177
|
class BoundedSessionTracker:
|
|
847
1178
|
"""Memory-safe session tracker with automatic expiration."""
|
|
848
1179
|
|
|
849
|
-
def __init__(self, max_sessions: int = 1000, session_ttl: int = 3600):
|
|
1180
|
+
def __init__(self, max_sessions: int = 1000, session_ttl: int = 3600) -> None:
|
|
850
1181
|
self.max_sessions = max_sessions
|
|
851
1182
|
self.session_ttl = session_ttl
|
|
852
1183
|
self.sessions: OrderedDict[str, float] = OrderedDict()
|
|
@@ -876,13 +1207,9 @@ class BoundedSessionTracker:
|
|
|
876
1207
|
|
|
877
1208
|
return True
|
|
878
1209
|
|
|
879
|
-
def _cleanup_expired(self, current_time: float):
|
|
1210
|
+
def _cleanup_expired(self, current_time: float) -> None:
|
|
880
1211
|
"""Remove expired sessions."""
|
|
881
|
-
expired = [
|
|
882
|
-
sid
|
|
883
|
-
for sid, timestamp in self.sessions.items()
|
|
884
|
-
if current_time - timestamp > self.session_ttl
|
|
885
|
-
]
|
|
1212
|
+
expired = [sid for sid, timestamp in self.sessions.items() if current_time - timestamp > self.session_ttl]
|
|
886
1213
|
for sid in expired:
|
|
887
1214
|
del self.sessions[sid]
|
|
888
1215
|
|
|
@@ -891,14 +1218,12 @@ class BoundedSessionTracker:
|
|
|
891
1218
|
|
|
892
1219
|
|
|
893
1220
|
class SessionTracingMiddleware(BaseHTTPMiddleware):
|
|
894
|
-
def __init__(self, app):
|
|
1221
|
+
def __init__(self, app: Any) -> None:
|
|
895
1222
|
super().__init__(app)
|
|
896
1223
|
# Use memory-safe session tracker instead of unbounded collections
|
|
897
|
-
self.session_tracker = BoundedSessionTracker(
|
|
898
|
-
max_sessions=1000, session_ttl=3600
|
|
899
|
-
)
|
|
1224
|
+
self.session_tracker = BoundedSessionTracker(max_sessions=1000, session_ttl=3600)
|
|
900
1225
|
|
|
901
|
-
async def dispatch(self, request:
|
|
1226
|
+
async def dispatch(self, request: Any, call_next: Callable[..., Any]) -> Any:
|
|
902
1227
|
# Record HTTP request timing
|
|
903
1228
|
import time
|
|
904
1229
|
|
|
@@ -928,7 +1253,8 @@ class SessionTracingMiddleware(BaseHTTPMiddleware):
|
|
|
928
1253
|
from golf.metrics import get_metrics_collector
|
|
929
1254
|
|
|
930
1255
|
metrics_collector = get_metrics_collector()
|
|
931
|
-
# Use a default duration since we don't track exact start
|
|
1256
|
+
# Use a default duration since we don't track exact start
|
|
1257
|
+
# times anymore
|
|
932
1258
|
# This is less precise but memory-safe
|
|
933
1259
|
metrics_collector.record_session_duration(300.0) # 5 min default
|
|
934
1260
|
except ImportError:
|
|
@@ -951,15 +1277,10 @@ class SessionTracingMiddleware(BaseHTTPMiddleware):
|
|
|
951
1277
|
|
|
952
1278
|
tracer = get_tracer()
|
|
953
1279
|
with tracer.start_as_current_span(span_name) as span:
|
|
954
|
-
# Add
|
|
1280
|
+
# Add essential HTTP attributes
|
|
955
1281
|
span.set_attribute("http.method", method)
|
|
956
|
-
span.set_attribute("http.url", str(request.url))
|
|
957
|
-
span.set_attribute("http.scheme", request.url.scheme)
|
|
958
|
-
span.set_attribute("http.host", request.url.hostname or "unknown")
|
|
959
1282
|
span.set_attribute("http.target", path)
|
|
960
|
-
span.set_attribute(
|
|
961
|
-
"http.user_agent", request.headers.get("user-agent", "unknown")
|
|
962
|
-
)
|
|
1283
|
+
span.set_attribute("http.host", request.url.hostname or "unknown")
|
|
963
1284
|
|
|
964
1285
|
# Add session tracking
|
|
965
1286
|
if session_id:
|
|
@@ -989,15 +1310,10 @@ class SessionTracingMiddleware(BaseHTTPMiddleware):
|
|
|
989
1310
|
|
|
990
1311
|
# Add response attributes
|
|
991
1312
|
span.set_attribute("http.status_code", response.status_code)
|
|
992
|
-
span.set_attribute(
|
|
993
|
-
"http.status_class", f"{response.status_code // 100}xx"
|
|
994
|
-
)
|
|
995
1313
|
|
|
996
1314
|
# Set span status based on HTTP status
|
|
997
1315
|
if response.status_code >= 400:
|
|
998
|
-
span.set_status(
|
|
999
|
-
Status(StatusCode.ERROR, f"HTTP {response.status_code}")
|
|
1000
|
-
)
|
|
1316
|
+
span.set_status(Status(StatusCode.ERROR, f"HTTP {response.status_code}"))
|
|
1001
1317
|
else:
|
|
1002
1318
|
span.set_status(Status(StatusCode.OK))
|
|
1003
1319
|
|
|
@@ -1020,16 +1336,10 @@ class SessionTracingMiddleware(BaseHTTPMiddleware):
|
|
|
1020
1336
|
# Clean up path for metrics (remove query params, normalize)
|
|
1021
1337
|
clean_path = path.split("?")[0] # Remove query parameters
|
|
1022
1338
|
if clean_path.startswith("/"):
|
|
1023
|
-
clean_path =
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
metrics_collector.increment_http_request(
|
|
1028
|
-
method, response.status_code, clean_path
|
|
1029
|
-
)
|
|
1030
|
-
metrics_collector.record_http_duration(
|
|
1031
|
-
method, clean_path, time.time() - start_time
|
|
1032
|
-
)
|
|
1339
|
+
clean_path = clean_path[1:] or "root" # Remove leading slash, handle root
|
|
1340
|
+
|
|
1341
|
+
metrics_collector.increment_http_request(method, response.status_code, clean_path)
|
|
1342
|
+
metrics_collector.record_http_duration(method, clean_path, time.time() - start_time)
|
|
1033
1343
|
except ImportError:
|
|
1034
1344
|
# Metrics not available, continue without metrics
|
|
1035
1345
|
pass
|
|
@@ -1061,9 +1371,7 @@ class SessionTracingMiddleware(BaseHTTPMiddleware):
|
|
|
1061
1371
|
if clean_path.startswith("/"):
|
|
1062
1372
|
clean_path = clean_path[1:] or "root"
|
|
1063
1373
|
|
|
1064
|
-
metrics_collector.increment_http_request(
|
|
1065
|
-
method, 500, clean_path
|
|
1066
|
-
) # Assume 500 for exceptions
|
|
1374
|
+
metrics_collector.increment_http_request(method, 500, clean_path) # Assume 500 for exceptions
|
|
1067
1375
|
metrics_collector.increment_error("http", type(e).__name__)
|
|
1068
1376
|
except ImportError:
|
|
1069
1377
|
pass
|
|
@@ -1075,7 +1383,7 @@ class SessionTracingMiddleware(BaseHTTPMiddleware):
|
|
|
1075
1383
|
|
|
1076
1384
|
|
|
1077
1385
|
@asynccontextmanager
|
|
1078
|
-
async def telemetry_lifespan(mcp_instance):
|
|
1386
|
+
async def telemetry_lifespan(mcp_instance: Any) -> AsyncGenerator[None, None]:
|
|
1079
1387
|
"""Simplified lifespan for telemetry initialization and cleanup."""
|
|
1080
1388
|
global _provider
|
|
1081
1389
|
|
|
@@ -1097,9 +1405,7 @@ async def telemetry_lifespan(mcp_instance):
|
|
|
1097
1405
|
app.add_middleware(SessionTracingMiddleware)
|
|
1098
1406
|
|
|
1099
1407
|
# Also try to instrument FastMCP's internal handlers
|
|
1100
|
-
if hasattr(mcp_instance, "_tool_manager") and hasattr(
|
|
1101
|
-
mcp_instance._tool_manager, "tools"
|
|
1102
|
-
):
|
|
1408
|
+
if hasattr(mcp_instance, "_tool_manager") and hasattr(mcp_instance._tool_manager, "tools"):
|
|
1103
1409
|
# The tools should already be instrumented when they were registered
|
|
1104
1410
|
pass
|
|
1105
1411
|
|
|
@@ -1107,7 +1413,7 @@ async def telemetry_lifespan(mcp_instance):
|
|
|
1107
1413
|
if hasattr(mcp_instance, "handle_request"):
|
|
1108
1414
|
original_handle_request = mcp_instance.handle_request
|
|
1109
1415
|
|
|
1110
|
-
async def traced_handle_request(*args, **kwargs):
|
|
1416
|
+
async def traced_handle_request(*args: Any, **kwargs: Any) -> Any:
|
|
1111
1417
|
tracer = get_tracer()
|
|
1112
1418
|
with tracer.start_as_current_span("mcp.handle_request") as span:
|
|
1113
1419
|
span.set_attribute("mcp.request.handler", "handle_request")
|