golf-mcp 0.1.19__py3-none-any.whl → 0.2.0__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 +188 -84
- golf/auth/api_key.py +6 -14
- golf/auth/factory.py +333 -0
- golf/auth/helpers.py +12 -42
- golf/auth/providers.py +396 -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 +356 -412
- 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 +496 -174
- 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.19.dist-info → golf_mcp-0.2.0.dist-info}/METADATA +56 -110
- golf_mcp-0.2.0.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.19.dist-info/RECORD +0 -60
- {golf_mcp-0.1.19.dist-info → golf_mcp-0.2.0.dist-info}/WHEEL +0 -0
- {golf_mcp-0.1.19.dist-info → golf_mcp-0.2.0.dist-info}/entry_points.txt +0 -0
- {golf_mcp-0.1.19.dist-info → golf_mcp-0.2.0.dist-info}/licenses/LICENSE +0 -0
- {golf_mcp-0.1.19.dist-info → golf_mcp-0.2.0.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:
|
|
@@ -36,12 +71,25 @@ def init_telemetry(service_name: str = "golf-mcp-server") -> TracerProvider | No
|
|
|
36
71
|
|
|
37
72
|
# Check for Golf platform integration first
|
|
38
73
|
golf_api_key = os.environ.get("GOLF_API_KEY")
|
|
39
|
-
if golf_api_key
|
|
40
|
-
# Auto-configure for Golf platform
|
|
74
|
+
if golf_api_key:
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
78
|
+
|
|
79
|
+
# Only set endpoint if not already configured (allow user override)
|
|
80
|
+
if not os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT"):
|
|
81
|
+
os.environ["OTEL_EXPORTER_OTLP_ENDPOINT"] = _endpoints.OTEL_ENDPOINT
|
|
82
|
+
|
|
83
|
+
# Set Golf platform headers (append to existing if present)
|
|
84
|
+
existing_headers = os.environ.get("OTEL_EXPORTER_OTLP_HEADERS", "")
|
|
85
|
+
golf_header = f"X-Golf-Key={golf_api_key}"
|
|
86
|
+
|
|
87
|
+
if existing_headers:
|
|
88
|
+
# Check if Golf key is already in headers
|
|
89
|
+
if "X-Golf-Key=" not in existing_headers:
|
|
90
|
+
os.environ["OTEL_EXPORTER_OTLP_HEADERS"] = f"{existing_headers},{golf_header}"
|
|
91
|
+
else:
|
|
92
|
+
os.environ["OTEL_EXPORTER_OTLP_HEADERS"] = golf_header
|
|
45
93
|
|
|
46
94
|
# Check for required environment variables based on exporter type
|
|
47
95
|
exporter_type = os.environ.get("OTEL_TRACES_EXPORTER", "console").lower()
|
|
@@ -78,9 +126,7 @@ def init_telemetry(service_name: str = "golf-mcp-server") -> TracerProvider | No
|
|
|
78
126
|
# Configure exporter based on type
|
|
79
127
|
try:
|
|
80
128
|
if exporter_type == "otlp_http":
|
|
81
|
-
endpoint = os.environ.get(
|
|
82
|
-
"OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4318/v1/traces"
|
|
83
|
-
)
|
|
129
|
+
endpoint = os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4318/v1/traces")
|
|
84
130
|
headers = os.environ.get("OTEL_EXPORTER_OTLP_HEADERS", "")
|
|
85
131
|
|
|
86
132
|
# Parse headers if provided
|
|
@@ -91,13 +137,8 @@ def init_telemetry(service_name: str = "golf-mcp-server") -> TracerProvider | No
|
|
|
91
137
|
key, value = header.split("=", 1)
|
|
92
138
|
header_dict[key.strip()] = value.strip()
|
|
93
139
|
|
|
94
|
-
exporter = OTLPSpanExporter(
|
|
95
|
-
endpoint=endpoint, headers=header_dict if header_dict else None
|
|
96
|
-
)
|
|
140
|
+
exporter = OTLPSpanExporter(endpoint=endpoint, headers=header_dict if header_dict else None)
|
|
97
141
|
|
|
98
|
-
# Log successful configuration for Golf platform
|
|
99
|
-
if golf_api_key:
|
|
100
|
-
print(f"[INFO] OpenTelemetry configured for Golf platform: {endpoint}")
|
|
101
142
|
else:
|
|
102
143
|
# Default to console exporter
|
|
103
144
|
exporter = ConsoleSpanExporter(out=sys.stderr)
|
|
@@ -112,7 +153,8 @@ def init_telemetry(service_name: str = "golf-mcp-server") -> TracerProvider | No
|
|
|
112
153
|
processor = BatchSpanProcessor(
|
|
113
154
|
exporter,
|
|
114
155
|
max_queue_size=2048,
|
|
115
|
-
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
|
|
116
158
|
max_export_batch_size=512,
|
|
117
159
|
export_timeout_millis=5000,
|
|
118
160
|
)
|
|
@@ -127,10 +169,7 @@ def init_telemetry(service_name: str = "golf-mcp-server") -> TracerProvider | No
|
|
|
127
169
|
try:
|
|
128
170
|
# Check if a provider is already set to avoid the warning
|
|
129
171
|
existing_provider = trace.get_tracer_provider()
|
|
130
|
-
if (
|
|
131
|
-
existing_provider is None
|
|
132
|
-
or str(type(existing_provider).__name__) == "ProxyTracerProvider"
|
|
133
|
-
):
|
|
172
|
+
if existing_provider is None or str(type(existing_provider).__name__) == "ProxyTracerProvider":
|
|
134
173
|
# Only set if no provider exists or it's the default proxy provider
|
|
135
174
|
trace.set_tracer_provider(provider)
|
|
136
175
|
_provider = provider
|
|
@@ -167,7 +206,7 @@ def instrument_tool(func: Callable[..., T], tool_name: str) -> Callable[..., T]:
|
|
|
167
206
|
tracer = get_tracer()
|
|
168
207
|
|
|
169
208
|
@functools.wraps(func)
|
|
170
|
-
async def async_wrapper(*args, **kwargs):
|
|
209
|
+
async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
171
210
|
# Record metrics timing
|
|
172
211
|
import time
|
|
173
212
|
|
|
@@ -178,20 +217,25 @@ def instrument_tool(func: Callable[..., T], tool_name: str) -> Callable[..., T]:
|
|
|
178
217
|
|
|
179
218
|
# start_as_current_span automatically uses the current context and manages it
|
|
180
219
|
with tracer.start_as_current_span(span_name) as span:
|
|
181
|
-
# Add
|
|
220
|
+
# Add essential attributes only
|
|
182
221
|
span.set_attribute("mcp.component.type", "tool")
|
|
183
|
-
span.set_attribute("mcp.component.name", tool_name)
|
|
184
222
|
span.set_attribute("mcp.tool.name", tool_name)
|
|
185
|
-
span.set_attribute("mcp.tool.function", func.__name__)
|
|
186
223
|
span.set_attribute(
|
|
187
224
|
"mcp.tool.module",
|
|
188
225
|
func.__module__ if hasattr(func, "__module__") else "unknown",
|
|
189
226
|
)
|
|
190
227
|
|
|
191
|
-
# Add execution context
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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)
|
|
195
239
|
|
|
196
240
|
# Extract Context parameter if present
|
|
197
241
|
ctx = kwargs.get("ctx")
|
|
@@ -231,42 +275,25 @@ def instrument_tool(func: Callable[..., T], tool_name: str) -> Callable[..., T]:
|
|
|
231
275
|
|
|
232
276
|
metrics_collector = get_metrics_collector()
|
|
233
277
|
metrics_collector.increment_tool_execution(tool_name, "success")
|
|
234
|
-
metrics_collector.record_tool_duration(
|
|
235
|
-
tool_name, time.time() - start_time
|
|
236
|
-
)
|
|
278
|
+
metrics_collector.record_tool_duration(tool_name, time.time() - start_time)
|
|
237
279
|
except ImportError:
|
|
238
280
|
# Metrics not available, continue without metrics
|
|
239
281
|
pass
|
|
240
282
|
|
|
241
|
-
# Capture result metadata
|
|
283
|
+
# Capture result metadata
|
|
242
284
|
if result is not None:
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
elif isinstance(result, list):
|
|
249
|
-
span.set_attribute("mcp.tool.result.count", len(result))
|
|
250
|
-
span.set_attribute("mcp.tool.result.type", "array")
|
|
251
|
-
elif isinstance(result, dict):
|
|
252
|
-
span.set_attribute("mcp.tool.result.count", len(result))
|
|
253
|
-
span.set_attribute("mcp.tool.result.type", "object")
|
|
254
|
-
# Only show first few keys to avoid exceeding attribute limits
|
|
255
|
-
if len(result) > 0 and len(result) <= 5:
|
|
256
|
-
keys_list = list(result.keys())[:5]
|
|
257
|
-
# Limit key length and join
|
|
258
|
-
truncated_keys = [
|
|
259
|
-
str(k)[:20] + "..." if len(str(k)) > 20 else str(k)
|
|
260
|
-
for k in keys_list
|
|
261
|
-
]
|
|
262
|
-
span.set_attribute(
|
|
263
|
-
"mcp.tool.result.sample_keys", ",".join(truncated_keys)
|
|
264
|
-
)
|
|
265
|
-
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):
|
|
266
290
|
span.set_attribute("mcp.tool.result.length", len(result))
|
|
267
291
|
|
|
268
|
-
#
|
|
269
|
-
|
|
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)
|
|
270
297
|
|
|
271
298
|
return result
|
|
272
299
|
except Exception as e:
|
|
@@ -297,7 +324,7 @@ def instrument_tool(func: Callable[..., T], tool_name: str) -> Callable[..., T]:
|
|
|
297
324
|
raise
|
|
298
325
|
|
|
299
326
|
@functools.wraps(func)
|
|
300
|
-
def sync_wrapper(*args, **kwargs):
|
|
327
|
+
def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
301
328
|
# Record metrics timing
|
|
302
329
|
import time
|
|
303
330
|
|
|
@@ -308,11 +335,9 @@ def instrument_tool(func: Callable[..., T], tool_name: str) -> Callable[..., T]:
|
|
|
308
335
|
|
|
309
336
|
# start_as_current_span automatically uses the current context and manages it
|
|
310
337
|
with tracer.start_as_current_span(span_name) as span:
|
|
311
|
-
# Add
|
|
338
|
+
# Add essential attributes only
|
|
312
339
|
span.set_attribute("mcp.component.type", "tool")
|
|
313
|
-
span.set_attribute("mcp.component.name", tool_name)
|
|
314
340
|
span.set_attribute("mcp.tool.name", tool_name)
|
|
315
|
-
span.set_attribute("mcp.tool.function", func.__name__)
|
|
316
341
|
span.set_attribute(
|
|
317
342
|
"mcp.tool.module",
|
|
318
343
|
func.__module__ if hasattr(func, "__module__") else "unknown",
|
|
@@ -321,7 +346,6 @@ def instrument_tool(func: Callable[..., T], tool_name: str) -> Callable[..., T]:
|
|
|
321
346
|
# Add execution context
|
|
322
347
|
span.set_attribute("mcp.execution.args_count", len(args))
|
|
323
348
|
span.set_attribute("mcp.execution.kwargs_count", len(kwargs))
|
|
324
|
-
span.set_attribute("mcp.execution.async", False)
|
|
325
349
|
|
|
326
350
|
# Extract Context parameter if present
|
|
327
351
|
ctx = kwargs.get("ctx")
|
|
@@ -361,42 +385,25 @@ def instrument_tool(func: Callable[..., T], tool_name: str) -> Callable[..., T]:
|
|
|
361
385
|
|
|
362
386
|
metrics_collector = get_metrics_collector()
|
|
363
387
|
metrics_collector.increment_tool_execution(tool_name, "success")
|
|
364
|
-
metrics_collector.record_tool_duration(
|
|
365
|
-
tool_name, time.time() - start_time
|
|
366
|
-
)
|
|
388
|
+
metrics_collector.record_tool_duration(tool_name, time.time() - start_time)
|
|
367
389
|
except ImportError:
|
|
368
390
|
# Metrics not available, continue without metrics
|
|
369
391
|
pass
|
|
370
392
|
|
|
371
|
-
# Capture result metadata
|
|
393
|
+
# Capture result metadata
|
|
372
394
|
if result is not None:
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
elif isinstance(result, list):
|
|
379
|
-
span.set_attribute("mcp.tool.result.count", len(result))
|
|
380
|
-
span.set_attribute("mcp.tool.result.type", "array")
|
|
381
|
-
elif isinstance(result, dict):
|
|
382
|
-
span.set_attribute("mcp.tool.result.count", len(result))
|
|
383
|
-
span.set_attribute("mcp.tool.result.type", "object")
|
|
384
|
-
# Only show first few keys to avoid exceeding attribute limits
|
|
385
|
-
if len(result) > 0 and len(result) <= 5:
|
|
386
|
-
keys_list = list(result.keys())[:5]
|
|
387
|
-
# Limit key length and join
|
|
388
|
-
truncated_keys = [
|
|
389
|
-
str(k)[:20] + "..." if len(str(k)) > 20 else str(k)
|
|
390
|
-
for k in keys_list
|
|
391
|
-
]
|
|
392
|
-
span.set_attribute(
|
|
393
|
-
"mcp.tool.result.sample_keys", ",".join(truncated_keys)
|
|
394
|
-
)
|
|
395
|
-
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):
|
|
396
400
|
span.set_attribute("mcp.tool.result.length", len(result))
|
|
397
401
|
|
|
398
|
-
#
|
|
399
|
-
|
|
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)
|
|
400
407
|
|
|
401
408
|
return result
|
|
402
409
|
except Exception as e:
|
|
@@ -447,21 +454,18 @@ def instrument_resource(func: Callable[..., T], resource_uri: str) -> Callable[.
|
|
|
447
454
|
is_template = "{" in resource_uri
|
|
448
455
|
|
|
449
456
|
@functools.wraps(func)
|
|
450
|
-
async def async_wrapper(*args, **kwargs):
|
|
457
|
+
async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
451
458
|
# Create a more descriptive span name
|
|
452
459
|
span_name = f"mcp.resource.{'template' if is_template else 'static'}.read"
|
|
453
460
|
with tracer.start_as_current_span(span_name) as span:
|
|
454
|
-
# Add
|
|
461
|
+
# Add essential attributes only
|
|
455
462
|
span.set_attribute("mcp.component.type", "resource")
|
|
456
|
-
span.set_attribute("mcp.component.name", resource_uri)
|
|
457
463
|
span.set_attribute("mcp.resource.uri", resource_uri)
|
|
458
464
|
span.set_attribute("mcp.resource.is_template", is_template)
|
|
459
|
-
span.set_attribute("mcp.resource.function", func.__name__)
|
|
460
465
|
span.set_attribute(
|
|
461
466
|
"mcp.resource.module",
|
|
462
467
|
func.__module__ if hasattr(func, "__module__") else "unknown",
|
|
463
468
|
)
|
|
464
|
-
span.set_attribute("mcp.execution.async", True)
|
|
465
469
|
|
|
466
470
|
# Extract Context parameter if present
|
|
467
471
|
ctx = kwargs.get("ctx")
|
|
@@ -493,9 +497,7 @@ def instrument_resource(func: Callable[..., T], resource_uri: str) -> Callable[.
|
|
|
493
497
|
span.set_status(Status(StatusCode.OK))
|
|
494
498
|
|
|
495
499
|
# Add event for successful read
|
|
496
|
-
span.add_event(
|
|
497
|
-
"resource.read.completed", {"resource.uri": resource_uri}
|
|
498
|
-
)
|
|
500
|
+
span.add_event("resource.read.completed", {"resource.uri": resource_uri})
|
|
499
501
|
|
|
500
502
|
# Add result metadata
|
|
501
503
|
if hasattr(result, "__len__"):
|
|
@@ -532,21 +534,18 @@ def instrument_resource(func: Callable[..., T], resource_uri: str) -> Callable[.
|
|
|
532
534
|
raise
|
|
533
535
|
|
|
534
536
|
@functools.wraps(func)
|
|
535
|
-
def sync_wrapper(*args, **kwargs):
|
|
537
|
+
def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
536
538
|
# Create a more descriptive span name
|
|
537
539
|
span_name = f"mcp.resource.{'template' if is_template else 'static'}.read"
|
|
538
540
|
with tracer.start_as_current_span(span_name) as span:
|
|
539
|
-
# Add
|
|
541
|
+
# Add essential attributes only
|
|
540
542
|
span.set_attribute("mcp.component.type", "resource")
|
|
541
|
-
span.set_attribute("mcp.component.name", resource_uri)
|
|
542
543
|
span.set_attribute("mcp.resource.uri", resource_uri)
|
|
543
544
|
span.set_attribute("mcp.resource.is_template", is_template)
|
|
544
|
-
span.set_attribute("mcp.resource.function", func.__name__)
|
|
545
545
|
span.set_attribute(
|
|
546
546
|
"mcp.resource.module",
|
|
547
547
|
func.__module__ if hasattr(func, "__module__") else "unknown",
|
|
548
548
|
)
|
|
549
|
-
span.set_attribute("mcp.execution.async", False)
|
|
550
549
|
|
|
551
550
|
# Extract Context parameter if present
|
|
552
551
|
ctx = kwargs.get("ctx")
|
|
@@ -578,9 +577,7 @@ def instrument_resource(func: Callable[..., T], resource_uri: str) -> Callable[.
|
|
|
578
577
|
span.set_status(Status(StatusCode.OK))
|
|
579
578
|
|
|
580
579
|
# Add event for successful read
|
|
581
|
-
span.add_event(
|
|
582
|
-
"resource.read.completed", {"resource.uri": resource_uri}
|
|
583
|
-
)
|
|
580
|
+
span.add_event("resource.read.completed", {"resource.uri": resource_uri})
|
|
584
581
|
|
|
585
582
|
# Add result metadata
|
|
586
583
|
if hasattr(result, "__len__"):
|
|
@@ -622,6 +619,370 @@ def instrument_resource(func: Callable[..., T], resource_uri: str) -> Callable[.
|
|
|
622
619
|
return sync_wrapper
|
|
623
620
|
|
|
624
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
|
+
|
|
625
986
|
def instrument_prompt(func: Callable[..., T], prompt_name: str) -> Callable[..., T]:
|
|
626
987
|
"""Instrument a prompt function with OpenTelemetry tracing."""
|
|
627
988
|
global _provider
|
|
@@ -633,20 +994,17 @@ def instrument_prompt(func: Callable[..., T], prompt_name: str) -> Callable[...,
|
|
|
633
994
|
tracer = get_tracer()
|
|
634
995
|
|
|
635
996
|
@functools.wraps(func)
|
|
636
|
-
async def async_wrapper(*args, **kwargs):
|
|
997
|
+
async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
637
998
|
# Create a more descriptive span name
|
|
638
999
|
span_name = f"mcp.prompt.{prompt_name}.generate"
|
|
639
1000
|
with tracer.start_as_current_span(span_name) as span:
|
|
640
|
-
# Add
|
|
1001
|
+
# Add essential attributes only
|
|
641
1002
|
span.set_attribute("mcp.component.type", "prompt")
|
|
642
|
-
span.set_attribute("mcp.component.name", prompt_name)
|
|
643
1003
|
span.set_attribute("mcp.prompt.name", prompt_name)
|
|
644
|
-
span.set_attribute("mcp.prompt.function", func.__name__)
|
|
645
1004
|
span.set_attribute(
|
|
646
1005
|
"mcp.prompt.module",
|
|
647
1006
|
func.__module__ if hasattr(func, "__module__") else "unknown",
|
|
648
1007
|
)
|
|
649
|
-
span.set_attribute("mcp.execution.async", True)
|
|
650
1008
|
|
|
651
1009
|
# Extract Context parameter if present
|
|
652
1010
|
ctx = kwargs.get("ctx")
|
|
@@ -678,9 +1036,7 @@ def instrument_prompt(func: Callable[..., T], prompt_name: str) -> Callable[...,
|
|
|
678
1036
|
span.set_status(Status(StatusCode.OK))
|
|
679
1037
|
|
|
680
1038
|
# Add event for successful generation
|
|
681
|
-
span.add_event(
|
|
682
|
-
"prompt.generation.completed", {"prompt.name": prompt_name}
|
|
683
|
-
)
|
|
1039
|
+
span.add_event("prompt.generation.completed", {"prompt.name": prompt_name})
|
|
684
1040
|
|
|
685
1041
|
# Add message count and type information
|
|
686
1042
|
if isinstance(result, list):
|
|
@@ -697,9 +1053,7 @@ def instrument_prompt(func: Callable[..., T], prompt_name: str) -> Callable[...,
|
|
|
697
1053
|
|
|
698
1054
|
if roles:
|
|
699
1055
|
unique_roles = list(set(roles))
|
|
700
|
-
span.set_attribute(
|
|
701
|
-
"mcp.prompt.result.roles", ",".join(unique_roles)
|
|
702
|
-
)
|
|
1056
|
+
span.set_attribute("mcp.prompt.result.roles", ",".join(unique_roles))
|
|
703
1057
|
span.set_attribute(
|
|
704
1058
|
"mcp.prompt.result.role_counts",
|
|
705
1059
|
str({role: roles.count(role) for role in unique_roles}),
|
|
@@ -727,20 +1081,17 @@ def instrument_prompt(func: Callable[..., T], prompt_name: str) -> Callable[...,
|
|
|
727
1081
|
raise
|
|
728
1082
|
|
|
729
1083
|
@functools.wraps(func)
|
|
730
|
-
def sync_wrapper(*args, **kwargs):
|
|
1084
|
+
def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
731
1085
|
# Create a more descriptive span name
|
|
732
1086
|
span_name = f"mcp.prompt.{prompt_name}.generate"
|
|
733
1087
|
with tracer.start_as_current_span(span_name) as span:
|
|
734
|
-
# Add
|
|
1088
|
+
# Add essential attributes only
|
|
735
1089
|
span.set_attribute("mcp.component.type", "prompt")
|
|
736
|
-
span.set_attribute("mcp.component.name", prompt_name)
|
|
737
1090
|
span.set_attribute("mcp.prompt.name", prompt_name)
|
|
738
|
-
span.set_attribute("mcp.prompt.function", func.__name__)
|
|
739
1091
|
span.set_attribute(
|
|
740
1092
|
"mcp.prompt.module",
|
|
741
1093
|
func.__module__ if hasattr(func, "__module__") else "unknown",
|
|
742
1094
|
)
|
|
743
|
-
span.set_attribute("mcp.execution.async", False)
|
|
744
1095
|
|
|
745
1096
|
# Extract Context parameter if present
|
|
746
1097
|
ctx = kwargs.get("ctx")
|
|
@@ -772,9 +1123,7 @@ def instrument_prompt(func: Callable[..., T], prompt_name: str) -> Callable[...,
|
|
|
772
1123
|
span.set_status(Status(StatusCode.OK))
|
|
773
1124
|
|
|
774
1125
|
# Add event for successful generation
|
|
775
|
-
span.add_event(
|
|
776
|
-
"prompt.generation.completed", {"prompt.name": prompt_name}
|
|
777
|
-
)
|
|
1126
|
+
span.add_event("prompt.generation.completed", {"prompt.name": prompt_name})
|
|
778
1127
|
|
|
779
1128
|
# Add message count and type information
|
|
780
1129
|
if isinstance(result, list):
|
|
@@ -791,9 +1140,7 @@ def instrument_prompt(func: Callable[..., T], prompt_name: str) -> Callable[...,
|
|
|
791
1140
|
|
|
792
1141
|
if roles:
|
|
793
1142
|
unique_roles = list(set(roles))
|
|
794
|
-
span.set_attribute(
|
|
795
|
-
"mcp.prompt.result.roles", ",".join(unique_roles)
|
|
796
|
-
)
|
|
1143
|
+
span.set_attribute("mcp.prompt.result.roles", ",".join(unique_roles))
|
|
797
1144
|
span.set_attribute(
|
|
798
1145
|
"mcp.prompt.result.role_counts",
|
|
799
1146
|
str({role: roles.count(role) for role in unique_roles}),
|
|
@@ -830,7 +1177,7 @@ def instrument_prompt(func: Callable[..., T], prompt_name: str) -> Callable[...,
|
|
|
830
1177
|
class BoundedSessionTracker:
|
|
831
1178
|
"""Memory-safe session tracker with automatic expiration."""
|
|
832
1179
|
|
|
833
|
-
def __init__(self, max_sessions: int = 1000, session_ttl: int = 3600):
|
|
1180
|
+
def __init__(self, max_sessions: int = 1000, session_ttl: int = 3600) -> None:
|
|
834
1181
|
self.max_sessions = max_sessions
|
|
835
1182
|
self.session_ttl = session_ttl
|
|
836
1183
|
self.sessions: OrderedDict[str, float] = OrderedDict()
|
|
@@ -860,13 +1207,9 @@ class BoundedSessionTracker:
|
|
|
860
1207
|
|
|
861
1208
|
return True
|
|
862
1209
|
|
|
863
|
-
def _cleanup_expired(self, current_time: float):
|
|
1210
|
+
def _cleanup_expired(self, current_time: float) -> None:
|
|
864
1211
|
"""Remove expired sessions."""
|
|
865
|
-
expired = [
|
|
866
|
-
sid
|
|
867
|
-
for sid, timestamp in self.sessions.items()
|
|
868
|
-
if current_time - timestamp > self.session_ttl
|
|
869
|
-
]
|
|
1212
|
+
expired = [sid for sid, timestamp in self.sessions.items() if current_time - timestamp > self.session_ttl]
|
|
870
1213
|
for sid in expired:
|
|
871
1214
|
del self.sessions[sid]
|
|
872
1215
|
|
|
@@ -875,14 +1218,12 @@ class BoundedSessionTracker:
|
|
|
875
1218
|
|
|
876
1219
|
|
|
877
1220
|
class SessionTracingMiddleware(BaseHTTPMiddleware):
|
|
878
|
-
def __init__(self, app):
|
|
1221
|
+
def __init__(self, app: Any) -> None:
|
|
879
1222
|
super().__init__(app)
|
|
880
1223
|
# Use memory-safe session tracker instead of unbounded collections
|
|
881
|
-
self.session_tracker = BoundedSessionTracker(
|
|
882
|
-
max_sessions=1000, session_ttl=3600
|
|
883
|
-
)
|
|
1224
|
+
self.session_tracker = BoundedSessionTracker(max_sessions=1000, session_ttl=3600)
|
|
884
1225
|
|
|
885
|
-
async def dispatch(self, request:
|
|
1226
|
+
async def dispatch(self, request: Any, call_next: Callable[..., Any]) -> Any:
|
|
886
1227
|
# Record HTTP request timing
|
|
887
1228
|
import time
|
|
888
1229
|
|
|
@@ -912,7 +1253,8 @@ class SessionTracingMiddleware(BaseHTTPMiddleware):
|
|
|
912
1253
|
from golf.metrics import get_metrics_collector
|
|
913
1254
|
|
|
914
1255
|
metrics_collector = get_metrics_collector()
|
|
915
|
-
# 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
|
|
916
1258
|
# This is less precise but memory-safe
|
|
917
1259
|
metrics_collector.record_session_duration(300.0) # 5 min default
|
|
918
1260
|
except ImportError:
|
|
@@ -935,15 +1277,10 @@ class SessionTracingMiddleware(BaseHTTPMiddleware):
|
|
|
935
1277
|
|
|
936
1278
|
tracer = get_tracer()
|
|
937
1279
|
with tracer.start_as_current_span(span_name) as span:
|
|
938
|
-
# Add
|
|
1280
|
+
# Add essential HTTP attributes
|
|
939
1281
|
span.set_attribute("http.method", method)
|
|
940
|
-
span.set_attribute("http.url", str(request.url))
|
|
941
|
-
span.set_attribute("http.scheme", request.url.scheme)
|
|
942
|
-
span.set_attribute("http.host", request.url.hostname or "unknown")
|
|
943
1282
|
span.set_attribute("http.target", path)
|
|
944
|
-
span.set_attribute(
|
|
945
|
-
"http.user_agent", request.headers.get("user-agent", "unknown")
|
|
946
|
-
)
|
|
1283
|
+
span.set_attribute("http.host", request.url.hostname or "unknown")
|
|
947
1284
|
|
|
948
1285
|
# Add session tracking
|
|
949
1286
|
if session_id:
|
|
@@ -973,15 +1310,10 @@ class SessionTracingMiddleware(BaseHTTPMiddleware):
|
|
|
973
1310
|
|
|
974
1311
|
# Add response attributes
|
|
975
1312
|
span.set_attribute("http.status_code", response.status_code)
|
|
976
|
-
span.set_attribute(
|
|
977
|
-
"http.status_class", f"{response.status_code // 100}xx"
|
|
978
|
-
)
|
|
979
1313
|
|
|
980
1314
|
# Set span status based on HTTP status
|
|
981
1315
|
if response.status_code >= 400:
|
|
982
|
-
span.set_status(
|
|
983
|
-
Status(StatusCode.ERROR, f"HTTP {response.status_code}")
|
|
984
|
-
)
|
|
1316
|
+
span.set_status(Status(StatusCode.ERROR, f"HTTP {response.status_code}"))
|
|
985
1317
|
else:
|
|
986
1318
|
span.set_status(Status(StatusCode.OK))
|
|
987
1319
|
|
|
@@ -1004,16 +1336,10 @@ class SessionTracingMiddleware(BaseHTTPMiddleware):
|
|
|
1004
1336
|
# Clean up path for metrics (remove query params, normalize)
|
|
1005
1337
|
clean_path = path.split("?")[0] # Remove query parameters
|
|
1006
1338
|
if clean_path.startswith("/"):
|
|
1007
|
-
clean_path =
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
metrics_collector.increment_http_request(
|
|
1012
|
-
method, response.status_code, clean_path
|
|
1013
|
-
)
|
|
1014
|
-
metrics_collector.record_http_duration(
|
|
1015
|
-
method, clean_path, time.time() - start_time
|
|
1016
|
-
)
|
|
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)
|
|
1017
1343
|
except ImportError:
|
|
1018
1344
|
# Metrics not available, continue without metrics
|
|
1019
1345
|
pass
|
|
@@ -1045,9 +1371,7 @@ class SessionTracingMiddleware(BaseHTTPMiddleware):
|
|
|
1045
1371
|
if clean_path.startswith("/"):
|
|
1046
1372
|
clean_path = clean_path[1:] or "root"
|
|
1047
1373
|
|
|
1048
|
-
metrics_collector.increment_http_request(
|
|
1049
|
-
method, 500, clean_path
|
|
1050
|
-
) # Assume 500 for exceptions
|
|
1374
|
+
metrics_collector.increment_http_request(method, 500, clean_path) # Assume 500 for exceptions
|
|
1051
1375
|
metrics_collector.increment_error("http", type(e).__name__)
|
|
1052
1376
|
except ImportError:
|
|
1053
1377
|
pass
|
|
@@ -1059,7 +1383,7 @@ class SessionTracingMiddleware(BaseHTTPMiddleware):
|
|
|
1059
1383
|
|
|
1060
1384
|
|
|
1061
1385
|
@asynccontextmanager
|
|
1062
|
-
async def telemetry_lifespan(mcp_instance):
|
|
1386
|
+
async def telemetry_lifespan(mcp_instance: Any) -> AsyncGenerator[None, None]:
|
|
1063
1387
|
"""Simplified lifespan for telemetry initialization and cleanup."""
|
|
1064
1388
|
global _provider
|
|
1065
1389
|
|
|
@@ -1081,9 +1405,7 @@ async def telemetry_lifespan(mcp_instance):
|
|
|
1081
1405
|
app.add_middleware(SessionTracingMiddleware)
|
|
1082
1406
|
|
|
1083
1407
|
# Also try to instrument FastMCP's internal handlers
|
|
1084
|
-
if hasattr(mcp_instance, "_tool_manager") and hasattr(
|
|
1085
|
-
mcp_instance._tool_manager, "tools"
|
|
1086
|
-
):
|
|
1408
|
+
if hasattr(mcp_instance, "_tool_manager") and hasattr(mcp_instance._tool_manager, "tools"):
|
|
1087
1409
|
# The tools should already be instrumented when they were registered
|
|
1088
1410
|
pass
|
|
1089
1411
|
|
|
@@ -1091,7 +1413,7 @@ async def telemetry_lifespan(mcp_instance):
|
|
|
1091
1413
|
if hasattr(mcp_instance, "handle_request"):
|
|
1092
1414
|
original_handle_request = mcp_instance.handle_request
|
|
1093
1415
|
|
|
1094
|
-
async def traced_handle_request(*args, **kwargs):
|
|
1416
|
+
async def traced_handle_request(*args: Any, **kwargs: Any) -> Any:
|
|
1095
1417
|
tracer = get_tracer()
|
|
1096
1418
|
with tracer.start_as_current_span("mcp.handle_request") as span:
|
|
1097
1419
|
span.set_attribute("mcp.request.handler", "handle_request")
|