golf-mcp 0.1.17__py3-none-any.whl → 0.1.18__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 +1 -1
- golf/core/builder.py +83 -1
- golf/core/builder_auth.py +5 -0
- golf/core/builder_metrics.py +232 -0
- golf/core/config.py +6 -0
- golf/metrics/__init__.py +10 -0
- golf/metrics/collector.py +239 -0
- golf/metrics/registry.py +12 -0
- golf/telemetry/instrumentation.py +153 -98
- {golf_mcp-0.1.17.dist-info → golf_mcp-0.1.18.dist-info}/METADATA +3 -1
- {golf_mcp-0.1.17.dist-info → golf_mcp-0.1.18.dist-info}/RECORD +15 -11
- {golf_mcp-0.1.17.dist-info → golf_mcp-0.1.18.dist-info}/WHEEL +0 -0
- {golf_mcp-0.1.17.dist-info → golf_mcp-0.1.18.dist-info}/entry_points.txt +0 -0
- {golf_mcp-0.1.17.dist-info → golf_mcp-0.1.18.dist-info}/licenses/LICENSE +0 -0
- {golf_mcp-0.1.17.dist-info → golf_mcp-0.1.18.dist-info}/top_level.txt +0 -0
golf/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.1.
|
|
1
|
+
__version__ = "0.1.18"
|
golf/core/builder.py
CHANGED
|
@@ -562,6 +562,18 @@ class CodeGenerator:
|
|
|
562
562
|
if self.settings.opentelemetry_enabled:
|
|
563
563
|
imports.extend(generate_telemetry_imports())
|
|
564
564
|
|
|
565
|
+
# Add metrics imports if enabled
|
|
566
|
+
if self.settings.metrics_enabled:
|
|
567
|
+
from golf.core.builder_metrics import (
|
|
568
|
+
generate_metrics_imports,
|
|
569
|
+
generate_metrics_instrumentation,
|
|
570
|
+
generate_session_tracking,
|
|
571
|
+
)
|
|
572
|
+
|
|
573
|
+
imports.extend(generate_metrics_imports())
|
|
574
|
+
imports.extend(generate_metrics_instrumentation())
|
|
575
|
+
imports.extend(generate_session_tracking())
|
|
576
|
+
|
|
565
577
|
# Add health check imports if enabled
|
|
566
578
|
if self.settings.health_check_enabled:
|
|
567
579
|
imports.extend(
|
|
@@ -672,6 +684,44 @@ class CodeGenerator:
|
|
|
672
684
|
f"{full_module_path}.{entry_func}, '{component.name}')"
|
|
673
685
|
)
|
|
674
686
|
|
|
687
|
+
if component_type == ComponentType.TOOL:
|
|
688
|
+
registration += (
|
|
689
|
+
f'\nmcp.add_tool(_wrapped_func, name="{component.name}", '
|
|
690
|
+
f'description="{component.docstring or ""}"'
|
|
691
|
+
)
|
|
692
|
+
# Add annotations if present
|
|
693
|
+
if hasattr(component, "annotations") and component.annotations:
|
|
694
|
+
registration += f", annotations={component.annotations}"
|
|
695
|
+
registration += ")"
|
|
696
|
+
elif component_type == ComponentType.RESOURCE:
|
|
697
|
+
registration += (
|
|
698
|
+
f"\nmcp.add_resource_fn(_wrapped_func, "
|
|
699
|
+
f'uri="{component.uri_template}", name="{component.name}", '
|
|
700
|
+
f'description="{component.docstring or ""}")'
|
|
701
|
+
)
|
|
702
|
+
else: # PROMPT
|
|
703
|
+
registration += (
|
|
704
|
+
f'\nmcp.add_prompt(_wrapped_func, name="{component.name}", '
|
|
705
|
+
f'description="{component.docstring or ""}")'
|
|
706
|
+
)
|
|
707
|
+
elif self.settings.metrics_enabled:
|
|
708
|
+
# Use metrics instrumentation
|
|
709
|
+
registration = (
|
|
710
|
+
f"# Register the {component_type.value} "
|
|
711
|
+
f"'{component.name}' with metrics"
|
|
712
|
+
)
|
|
713
|
+
entry_func = (
|
|
714
|
+
component.entry_function
|
|
715
|
+
if hasattr(component, "entry_function")
|
|
716
|
+
and component.entry_function
|
|
717
|
+
else "export"
|
|
718
|
+
)
|
|
719
|
+
|
|
720
|
+
registration += (
|
|
721
|
+
f"\n_wrapped_func = instrument_{component_type.value}("
|
|
722
|
+
f"{full_module_path}.{entry_func}, '{component.name}')"
|
|
723
|
+
)
|
|
724
|
+
|
|
675
725
|
if component_type == ComponentType.TOOL:
|
|
676
726
|
registration += (
|
|
677
727
|
f'\nmcp.add_tool(_wrapped_func, name="{component.name}", '
|
|
@@ -826,6 +876,15 @@ class CodeGenerator:
|
|
|
826
876
|
]
|
|
827
877
|
)
|
|
828
878
|
|
|
879
|
+
# Add metrics initialization if enabled
|
|
880
|
+
early_metrics_init = []
|
|
881
|
+
if self.settings.metrics_enabled:
|
|
882
|
+
from golf.core.builder_metrics import generate_metrics_initialization
|
|
883
|
+
|
|
884
|
+
early_metrics_init.extend(
|
|
885
|
+
generate_metrics_initialization(self.settings.name)
|
|
886
|
+
)
|
|
887
|
+
|
|
829
888
|
# Main entry point with transport-specific app initialization
|
|
830
889
|
main_code = [
|
|
831
890
|
'if __name__ == "__main__":',
|
|
@@ -864,6 +923,13 @@ class CodeGenerator:
|
|
|
864
923
|
)
|
|
865
924
|
middleware_list.append("Middleware(ApiKeyMiddleware)")
|
|
866
925
|
|
|
926
|
+
# Add metrics middleware if enabled
|
|
927
|
+
if self.settings.metrics_enabled:
|
|
928
|
+
middleware_setup.append(
|
|
929
|
+
" from starlette.middleware import Middleware"
|
|
930
|
+
)
|
|
931
|
+
middleware_list.append("Middleware(MetricsMiddleware)")
|
|
932
|
+
|
|
867
933
|
# Add OpenTelemetry middleware if enabled
|
|
868
934
|
if self.settings.opentelemetry_enabled:
|
|
869
935
|
middleware_setup.append(
|
|
@@ -904,6 +970,13 @@ class CodeGenerator:
|
|
|
904
970
|
)
|
|
905
971
|
middleware_list.append("Middleware(ApiKeyMiddleware)")
|
|
906
972
|
|
|
973
|
+
# Add metrics middleware if enabled
|
|
974
|
+
if self.settings.metrics_enabled:
|
|
975
|
+
middleware_setup.append(
|
|
976
|
+
" from starlette.middleware import Middleware"
|
|
977
|
+
)
|
|
978
|
+
middleware_list.append("Middleware(MetricsMiddleware)")
|
|
979
|
+
|
|
907
980
|
# Add OpenTelemetry middleware if enabled
|
|
908
981
|
if self.settings.opentelemetry_enabled:
|
|
909
982
|
middleware_setup.append(
|
|
@@ -937,6 +1010,13 @@ class CodeGenerator:
|
|
|
937
1010
|
[" # Run with stdio transport", ' mcp.run(transport="stdio")']
|
|
938
1011
|
)
|
|
939
1012
|
|
|
1013
|
+
# Add metrics route if enabled
|
|
1014
|
+
metrics_route_code = []
|
|
1015
|
+
if self.settings.metrics_enabled:
|
|
1016
|
+
from golf.core.builder_metrics import generate_metrics_route
|
|
1017
|
+
|
|
1018
|
+
metrics_route_code = generate_metrics_route(self.settings.metrics_path)
|
|
1019
|
+
|
|
940
1020
|
# Add health check route if enabled
|
|
941
1021
|
health_check_code = []
|
|
942
1022
|
if self.settings.health_check_enabled:
|
|
@@ -953,14 +1033,16 @@ class CodeGenerator:
|
|
|
953
1033
|
|
|
954
1034
|
# Combine all sections
|
|
955
1035
|
# Order: imports, env_section, auth_setup, server_code (mcp init),
|
|
956
|
-
# early_telemetry_init, component_registrations, health_check_code, main_code (run block)
|
|
1036
|
+
# early_telemetry_init, early_metrics_init, component_registrations, metrics_route_code, health_check_code, main_code (run block)
|
|
957
1037
|
code = "\n".join(
|
|
958
1038
|
imports
|
|
959
1039
|
+ env_section
|
|
960
1040
|
+ auth_setup_code
|
|
961
1041
|
+ server_code_lines
|
|
962
1042
|
+ early_telemetry_init
|
|
1043
|
+
+ early_metrics_init
|
|
963
1044
|
+ component_registrations
|
|
1045
|
+
+ metrics_route_code
|
|
964
1046
|
+ health_check_code
|
|
965
1047
|
+ main_code
|
|
966
1048
|
)
|
golf/core/builder_auth.py
CHANGED
|
@@ -187,6 +187,11 @@ def generate_api_key_auth_components(
|
|
|
187
187
|
" # Debug mode from environment",
|
|
188
188
|
" debug = os.environ.get('GOLF_API_KEY_DEBUG', '').lower() == 'true'",
|
|
189
189
|
" ",
|
|
190
|
+
" # Skip auth for monitoring endpoints",
|
|
191
|
+
" path = request.url.path",
|
|
192
|
+
" if path in ['/metrics', '/health']:",
|
|
193
|
+
" return await call_next(request)",
|
|
194
|
+
" ",
|
|
190
195
|
" api_key_config = get_api_key_config()",
|
|
191
196
|
" ",
|
|
192
197
|
" if api_key_config:",
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
"""Metrics integration for the GolfMCP build process.
|
|
2
|
+
|
|
3
|
+
This module provides functions for generating Prometheus metrics initialization
|
|
4
|
+
and collection code for FastMCP servers built with GolfMCP.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def generate_metrics_imports() -> list[str]:
|
|
9
|
+
"""Generate import statements for metrics collection.
|
|
10
|
+
|
|
11
|
+
Returns:
|
|
12
|
+
List of import statements for metrics
|
|
13
|
+
"""
|
|
14
|
+
return [
|
|
15
|
+
"# Prometheus metrics imports",
|
|
16
|
+
"from golf.metrics import init_metrics, get_metrics_collector",
|
|
17
|
+
"from prometheus_client import generate_latest, CONTENT_TYPE_LATEST",
|
|
18
|
+
"from starlette.responses import Response",
|
|
19
|
+
"from starlette.middleware.base import BaseHTTPMiddleware",
|
|
20
|
+
"from starlette.requests import Request",
|
|
21
|
+
"import time",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def generate_metrics_initialization(server_name: str) -> list[str]:
|
|
26
|
+
"""Generate metrics initialization code.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
server_name: Name of the MCP server
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
List of code lines for metrics initialization
|
|
33
|
+
"""
|
|
34
|
+
return [
|
|
35
|
+
"# Initialize metrics collection",
|
|
36
|
+
"init_metrics(enabled=True)",
|
|
37
|
+
"",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def generate_metrics_route(metrics_path: str) -> list[str]:
|
|
42
|
+
"""Generate the metrics endpoint route code.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
metrics_path: Path for the metrics endpoint (e.g., "/metrics")
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
List of code lines for the metrics route
|
|
49
|
+
"""
|
|
50
|
+
return [
|
|
51
|
+
"# Add metrics endpoint",
|
|
52
|
+
f'@mcp.custom_route("{metrics_path}", methods=["GET"])',
|
|
53
|
+
"async def metrics_endpoint(request):",
|
|
54
|
+
' """Prometheus metrics endpoint for monitoring."""',
|
|
55
|
+
" # Update uptime before returning metrics",
|
|
56
|
+
" update_uptime()",
|
|
57
|
+
" return Response(generate_latest(), media_type=CONTENT_TYPE_LATEST)",
|
|
58
|
+
"",
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def get_metrics_dependencies() -> list[str]:
|
|
63
|
+
"""Get list of metrics dependencies to add to pyproject.toml.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
List of package requirements strings
|
|
67
|
+
"""
|
|
68
|
+
return [
|
|
69
|
+
"prometheus-client>=0.19.0",
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def generate_metrics_instrumentation() -> list[str]:
|
|
74
|
+
"""Generate metrics instrumentation wrapper functions.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
List of code lines for metrics instrumentation
|
|
78
|
+
"""
|
|
79
|
+
return [
|
|
80
|
+
"# Metrics instrumentation wrapper functions",
|
|
81
|
+
"import time",
|
|
82
|
+
"import functools",
|
|
83
|
+
"from typing import Any, Callable",
|
|
84
|
+
"",
|
|
85
|
+
"def instrument_tool(func: Callable, tool_name: str) -> Callable:",
|
|
86
|
+
' """Wrap a tool function with metrics collection."""',
|
|
87
|
+
" @functools.wraps(func)",
|
|
88
|
+
" async def wrapper(*args, **kwargs) -> Any:",
|
|
89
|
+
" collector = get_metrics_collector()",
|
|
90
|
+
" start_time = time.time()",
|
|
91
|
+
" status = 'success'",
|
|
92
|
+
" try:",
|
|
93
|
+
" result = await func(*args, **kwargs)",
|
|
94
|
+
" return result",
|
|
95
|
+
" except Exception as e:",
|
|
96
|
+
" status = 'error'",
|
|
97
|
+
" collector.increment_error('tool', type(e).__name__)",
|
|
98
|
+
" raise",
|
|
99
|
+
" finally:",
|
|
100
|
+
" duration = time.time() - start_time",
|
|
101
|
+
" collector.increment_tool_execution(tool_name, status)",
|
|
102
|
+
" collector.record_tool_duration(tool_name, duration)",
|
|
103
|
+
" return wrapper",
|
|
104
|
+
"",
|
|
105
|
+
"def instrument_resource(func: Callable, resource_name: str) -> Callable:",
|
|
106
|
+
' """Wrap a resource function with metrics collection."""',
|
|
107
|
+
" @functools.wraps(func)",
|
|
108
|
+
" async def wrapper(*args, **kwargs) -> Any:",
|
|
109
|
+
" collector = get_metrics_collector()",
|
|
110
|
+
" try:",
|
|
111
|
+
" result = await func(*args, **kwargs)",
|
|
112
|
+
" # Extract URI from args if available for resource_reads metric",
|
|
113
|
+
" if args and len(args) > 0:",
|
|
114
|
+
" uri = str(args[0]) if args[0] else resource_name",
|
|
115
|
+
" else:",
|
|
116
|
+
" uri = resource_name",
|
|
117
|
+
" collector.increment_resource_read(uri)",
|
|
118
|
+
" return result",
|
|
119
|
+
" except Exception as e:",
|
|
120
|
+
" collector.increment_error('resource', type(e).__name__)",
|
|
121
|
+
" raise",
|
|
122
|
+
" return wrapper",
|
|
123
|
+
"",
|
|
124
|
+
"def instrument_prompt(func: Callable, prompt_name: str) -> Callable:",
|
|
125
|
+
' """Wrap a prompt function with metrics collection."""',
|
|
126
|
+
" @functools.wraps(func)",
|
|
127
|
+
" async def wrapper(*args, **kwargs) -> Any:",
|
|
128
|
+
" collector = get_metrics_collector()",
|
|
129
|
+
" try:",
|
|
130
|
+
" result = await func(*args, **kwargs)",
|
|
131
|
+
" collector.increment_prompt_generation(prompt_name)",
|
|
132
|
+
" return result",
|
|
133
|
+
" except Exception as e:",
|
|
134
|
+
" collector.increment_error('prompt', type(e).__name__)",
|
|
135
|
+
" raise",
|
|
136
|
+
" return wrapper",
|
|
137
|
+
"",
|
|
138
|
+
"# HTTP Request Metrics Middleware",
|
|
139
|
+
"class MetricsMiddleware(BaseHTTPMiddleware):",
|
|
140
|
+
' """Middleware to collect HTTP request metrics."""',
|
|
141
|
+
"",
|
|
142
|
+
" async def dispatch(self, request: Request, call_next):",
|
|
143
|
+
" collector = get_metrics_collector()",
|
|
144
|
+
" start_time = time.time()",
|
|
145
|
+
" ",
|
|
146
|
+
" # Extract path and method",
|
|
147
|
+
" method = request.method",
|
|
148
|
+
" path = request.url.path",
|
|
149
|
+
" ",
|
|
150
|
+
" try:",
|
|
151
|
+
" response = await call_next(request)",
|
|
152
|
+
" status_code = response.status_code",
|
|
153
|
+
" except Exception as e:",
|
|
154
|
+
" status_code = 500",
|
|
155
|
+
" collector.increment_error('http', type(e).__name__)",
|
|
156
|
+
" raise",
|
|
157
|
+
" finally:",
|
|
158
|
+
" duration = time.time() - start_time",
|
|
159
|
+
" collector.increment_http_request(method, status_code, path)",
|
|
160
|
+
" collector.record_http_duration(method, path, duration)",
|
|
161
|
+
" ",
|
|
162
|
+
" return response",
|
|
163
|
+
"",
|
|
164
|
+
"# Session tracking helpers",
|
|
165
|
+
"import atexit",
|
|
166
|
+
"from contextlib import asynccontextmanager",
|
|
167
|
+
"",
|
|
168
|
+
"# Global server start time for uptime tracking",
|
|
169
|
+
"_server_start_time = time.time()",
|
|
170
|
+
"",
|
|
171
|
+
"def track_session_start():",
|
|
172
|
+
' """Track when a new session starts."""',
|
|
173
|
+
" collector = get_metrics_collector()",
|
|
174
|
+
" collector.increment_session()",
|
|
175
|
+
"",
|
|
176
|
+
"def track_session_end(start_time: float):",
|
|
177
|
+
' """Track when a session ends."""',
|
|
178
|
+
" collector = get_metrics_collector()",
|
|
179
|
+
" duration = time.time() - start_time",
|
|
180
|
+
" collector.record_session_duration(duration)",
|
|
181
|
+
"",
|
|
182
|
+
"def update_uptime():",
|
|
183
|
+
' """Update the uptime metric."""',
|
|
184
|
+
" collector = get_metrics_collector()",
|
|
185
|
+
" uptime = time.time() - _server_start_time",
|
|
186
|
+
" collector.set_uptime(uptime)",
|
|
187
|
+
"",
|
|
188
|
+
"# Initialize uptime tracking",
|
|
189
|
+
"update_uptime()",
|
|
190
|
+
"",
|
|
191
|
+
]
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def generate_session_tracking() -> list[str]:
|
|
195
|
+
"""Generate session tracking integration code.
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
List of code lines for session tracking
|
|
199
|
+
"""
|
|
200
|
+
return [
|
|
201
|
+
"# Session tracking integration",
|
|
202
|
+
"import asyncio",
|
|
203
|
+
"from typing import Dict",
|
|
204
|
+
"",
|
|
205
|
+
"# Track active sessions",
|
|
206
|
+
"_active_sessions: Dict[str, float] = {}",
|
|
207
|
+
"",
|
|
208
|
+
"# Hook into FastMCP's session lifecycle if available",
|
|
209
|
+
"try:",
|
|
210
|
+
" from fastmcp.server import SessionManager",
|
|
211
|
+
" ",
|
|
212
|
+
" # Monkey patch session creation if possible",
|
|
213
|
+
" _original_create_session = getattr(mcp, '_create_session', None)",
|
|
214
|
+
" if _original_create_session:",
|
|
215
|
+
" async def _patched_create_session(*args, **kwargs):",
|
|
216
|
+
" session_id = str(id(args)) if args else 'unknown'",
|
|
217
|
+
" _active_sessions[session_id] = time.time()",
|
|
218
|
+
" track_session_start()",
|
|
219
|
+
" try:",
|
|
220
|
+
" return await _original_create_session(*args, **kwargs)",
|
|
221
|
+
" except Exception:",
|
|
222
|
+
" # If session creation fails, clean up",
|
|
223
|
+
" if session_id in _active_sessions:",
|
|
224
|
+
" del _active_sessions[session_id]",
|
|
225
|
+
" raise",
|
|
226
|
+
" ",
|
|
227
|
+
" mcp._create_session = _patched_create_session",
|
|
228
|
+
"except (ImportError, AttributeError):",
|
|
229
|
+
" # Fallback: track sessions via request patterns",
|
|
230
|
+
" pass",
|
|
231
|
+
"",
|
|
232
|
+
]
|
golf/core/config.py
CHANGED
|
@@ -115,6 +115,12 @@ class Settings(BaseSettings):
|
|
|
115
115
|
description="Make Streamable-HTTP transport stateless (new session per request)",
|
|
116
116
|
)
|
|
117
117
|
|
|
118
|
+
# Metrics configuration
|
|
119
|
+
metrics_enabled: bool = Field(
|
|
120
|
+
False, description="Enable Prometheus metrics endpoint"
|
|
121
|
+
)
|
|
122
|
+
metrics_path: str = Field("/metrics", description="Metrics endpoint path")
|
|
123
|
+
|
|
118
124
|
|
|
119
125
|
def find_config_path(start_path: Path | None = None) -> Path | None:
|
|
120
126
|
"""Find the golf config file by searching upwards from the given path.
|
golf/metrics/__init__.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Golf metrics module for Prometheus-compatible metrics collection."""
|
|
2
|
+
|
|
3
|
+
from golf.metrics.collector import MetricsCollector, get_metrics_collector
|
|
4
|
+
from golf.metrics.registry import init_metrics
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"MetricsCollector",
|
|
8
|
+
"get_metrics_collector",
|
|
9
|
+
"init_metrics",
|
|
10
|
+
]
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"""Metrics collector for Golf MCP servers."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
# Global metrics collector instance
|
|
6
|
+
_metrics_collector: Optional["MetricsCollector"] = None
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class MetricsCollector:
|
|
10
|
+
"""Collects metrics for Golf MCP servers using Prometheus client."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, enabled: bool = False) -> None:
|
|
13
|
+
"""Initialize the metrics collector.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
enabled: Whether metrics collection is enabled
|
|
17
|
+
"""
|
|
18
|
+
self.enabled = enabled
|
|
19
|
+
self._metrics = {}
|
|
20
|
+
|
|
21
|
+
if self.enabled:
|
|
22
|
+
self._init_prometheus_metrics()
|
|
23
|
+
|
|
24
|
+
def _init_prometheus_metrics(self) -> None:
|
|
25
|
+
"""Initialize Prometheus metrics if enabled."""
|
|
26
|
+
try:
|
|
27
|
+
from prometheus_client import Counter, Histogram, Gauge
|
|
28
|
+
|
|
29
|
+
# Tool execution metrics
|
|
30
|
+
self._metrics["tool_executions"] = Counter(
|
|
31
|
+
"golf_tool_executions_total",
|
|
32
|
+
"Total number of tool executions",
|
|
33
|
+
["tool_name", "status"],
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
self._metrics["tool_duration"] = Histogram(
|
|
37
|
+
"golf_tool_duration_seconds",
|
|
38
|
+
"Tool execution duration in seconds",
|
|
39
|
+
["tool_name"],
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
# HTTP request metrics
|
|
43
|
+
self._metrics["http_requests"] = Counter(
|
|
44
|
+
"golf_http_requests_total",
|
|
45
|
+
"Total number of HTTP requests",
|
|
46
|
+
["method", "status_code", "path"],
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
self._metrics["http_duration"] = Histogram(
|
|
50
|
+
"golf_http_request_duration_seconds",
|
|
51
|
+
"HTTP request duration in seconds",
|
|
52
|
+
["method", "path"],
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# Resource access metrics
|
|
56
|
+
self._metrics["resource_reads"] = Counter(
|
|
57
|
+
"golf_resource_reads_total",
|
|
58
|
+
"Total number of resource reads",
|
|
59
|
+
["resource_uri"],
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# Prompt generation metrics
|
|
63
|
+
self._metrics["prompt_generations"] = Counter(
|
|
64
|
+
"golf_prompt_generations_total",
|
|
65
|
+
"Total number of prompt generations",
|
|
66
|
+
["prompt_name"],
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# Error metrics
|
|
70
|
+
self._metrics["errors"] = Counter(
|
|
71
|
+
"golf_errors_total",
|
|
72
|
+
"Total number of errors",
|
|
73
|
+
["component_type", "error_type"],
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# Session metrics
|
|
77
|
+
self._metrics["sessions_total"] = Counter(
|
|
78
|
+
"golf_sessions_total", "Total number of sessions created"
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
self._metrics["session_duration"] = Histogram(
|
|
82
|
+
"golf_session_duration_seconds", "Session duration in seconds"
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# System metrics
|
|
86
|
+
self._metrics["uptime"] = Gauge(
|
|
87
|
+
"golf_uptime_seconds", "Server uptime in seconds"
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
except ImportError:
|
|
91
|
+
# Prometheus client not available, disable metrics
|
|
92
|
+
self.enabled = False
|
|
93
|
+
|
|
94
|
+
def increment_tool_execution(self, tool_name: str, status: str) -> None:
|
|
95
|
+
"""Record a tool execution.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
tool_name: Name of the tool that was executed
|
|
99
|
+
status: Execution status ('success' or 'error')
|
|
100
|
+
"""
|
|
101
|
+
if not self.enabled or "tool_executions" not in self._metrics:
|
|
102
|
+
return
|
|
103
|
+
|
|
104
|
+
self._metrics["tool_executions"].labels(
|
|
105
|
+
tool_name=tool_name, status=status
|
|
106
|
+
).inc()
|
|
107
|
+
|
|
108
|
+
def record_tool_duration(self, tool_name: str, duration: float) -> None:
|
|
109
|
+
"""Record tool execution duration.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
tool_name: Name of the tool
|
|
113
|
+
duration: Execution duration in seconds
|
|
114
|
+
"""
|
|
115
|
+
if not self.enabled or "tool_duration" not in self._metrics:
|
|
116
|
+
return
|
|
117
|
+
|
|
118
|
+
self._metrics["tool_duration"].labels(tool_name=tool_name).observe(duration)
|
|
119
|
+
|
|
120
|
+
def increment_http_request(self, method: str, status_code: int, path: str) -> None:
|
|
121
|
+
"""Record an HTTP request.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
method: HTTP method (GET, POST, etc.)
|
|
125
|
+
status_code: HTTP status code
|
|
126
|
+
path: Request path
|
|
127
|
+
"""
|
|
128
|
+
if not self.enabled or "http_requests" not in self._metrics:
|
|
129
|
+
return
|
|
130
|
+
|
|
131
|
+
self._metrics["http_requests"].labels(
|
|
132
|
+
method=method, status_code=str(status_code), path=path
|
|
133
|
+
).inc()
|
|
134
|
+
|
|
135
|
+
def record_http_duration(self, method: str, path: str, duration: float) -> None:
|
|
136
|
+
"""Record HTTP request duration.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
method: HTTP method
|
|
140
|
+
path: Request path
|
|
141
|
+
duration: Request duration in seconds
|
|
142
|
+
"""
|
|
143
|
+
if not self.enabled or "http_duration" not in self._metrics:
|
|
144
|
+
return
|
|
145
|
+
|
|
146
|
+
self._metrics["http_duration"].labels(method=method, path=path).observe(
|
|
147
|
+
duration
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
def increment_resource_read(self, resource_uri: str) -> None:
|
|
151
|
+
"""Record a resource read.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
resource_uri: URI of the resource that was read
|
|
155
|
+
"""
|
|
156
|
+
if not self.enabled or "resource_reads" not in self._metrics:
|
|
157
|
+
return
|
|
158
|
+
|
|
159
|
+
self._metrics["resource_reads"].labels(resource_uri=resource_uri).inc()
|
|
160
|
+
|
|
161
|
+
def increment_prompt_generation(self, prompt_name: str) -> None:
|
|
162
|
+
"""Record a prompt generation.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
prompt_name: Name of the prompt that was generated
|
|
166
|
+
"""
|
|
167
|
+
if not self.enabled or "prompt_generations" not in self._metrics:
|
|
168
|
+
return
|
|
169
|
+
|
|
170
|
+
self._metrics["prompt_generations"].labels(prompt_name=prompt_name).inc()
|
|
171
|
+
|
|
172
|
+
def increment_error(self, component_type: str, error_type: str) -> None:
|
|
173
|
+
"""Record an error.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
component_type: Type of component ('tool', 'resource', 'prompt', 'http')
|
|
177
|
+
error_type: Type of error ('timeout', 'auth_error', 'validation_error', etc.)
|
|
178
|
+
"""
|
|
179
|
+
if not self.enabled or "errors" not in self._metrics:
|
|
180
|
+
return
|
|
181
|
+
|
|
182
|
+
self._metrics["errors"].labels(
|
|
183
|
+
component_type=component_type, error_type=error_type
|
|
184
|
+
).inc()
|
|
185
|
+
|
|
186
|
+
def increment_session(self) -> None:
|
|
187
|
+
"""Record a new session."""
|
|
188
|
+
if not self.enabled or "sessions_total" not in self._metrics:
|
|
189
|
+
return
|
|
190
|
+
|
|
191
|
+
self._metrics["sessions_total"].inc()
|
|
192
|
+
|
|
193
|
+
def record_session_duration(self, duration: float) -> None:
|
|
194
|
+
"""Record session duration.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
duration: Session duration in seconds
|
|
198
|
+
"""
|
|
199
|
+
if not self.enabled or "session_duration" not in self._metrics:
|
|
200
|
+
return
|
|
201
|
+
|
|
202
|
+
self._metrics["session_duration"].observe(duration)
|
|
203
|
+
|
|
204
|
+
def set_uptime(self, seconds: float) -> None:
|
|
205
|
+
"""Set the server uptime.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
seconds: Server uptime in seconds
|
|
209
|
+
"""
|
|
210
|
+
if not self.enabled or "uptime" not in self._metrics:
|
|
211
|
+
return
|
|
212
|
+
|
|
213
|
+
self._metrics["uptime"].set(seconds)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def init_metrics_collector(enabled: bool = False) -> MetricsCollector:
|
|
217
|
+
"""Initialize the global metrics collector.
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
enabled: Whether to enable metrics collection
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
The initialized metrics collector
|
|
224
|
+
"""
|
|
225
|
+
global _metrics_collector
|
|
226
|
+
_metrics_collector = MetricsCollector(enabled=enabled)
|
|
227
|
+
return _metrics_collector
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def get_metrics_collector() -> MetricsCollector:
|
|
231
|
+
"""Get the global metrics collector instance.
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
The metrics collector, or a disabled one if not initialized
|
|
235
|
+
"""
|
|
236
|
+
global _metrics_collector
|
|
237
|
+
if _metrics_collector is None:
|
|
238
|
+
_metrics_collector = MetricsCollector(enabled=False)
|
|
239
|
+
return _metrics_collector
|
golf/metrics/registry.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Metrics registry for Golf MCP servers."""
|
|
2
|
+
|
|
3
|
+
from golf.metrics.collector import init_metrics_collector
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def init_metrics(enabled: bool = False) -> None:
|
|
7
|
+
"""Initialize the metrics system.
|
|
8
|
+
|
|
9
|
+
Args:
|
|
10
|
+
enabled: Whether to enable metrics collection
|
|
11
|
+
"""
|
|
12
|
+
init_metrics_collector(enabled=enabled)
|
|
@@ -163,6 +163,11 @@ def instrument_tool(func: Callable[..., T], tool_name: str) -> Callable[..., T]:
|
|
|
163
163
|
|
|
164
164
|
@functools.wraps(func)
|
|
165
165
|
async def async_wrapper(*args, **kwargs):
|
|
166
|
+
# Record metrics timing
|
|
167
|
+
import time
|
|
168
|
+
|
|
169
|
+
start_time = time.time()
|
|
170
|
+
|
|
166
171
|
# Create a more descriptive span name
|
|
167
172
|
span_name = f"mcp.tool.{tool_name}.execute"
|
|
168
173
|
|
|
@@ -205,45 +210,6 @@ def instrument_tool(func: Callable[..., T], tool_name: str) -> Callable[..., T]:
|
|
|
205
210
|
if session_id_from_baggage:
|
|
206
211
|
span.set_attribute("mcp.session.id", session_id_from_baggage)
|
|
207
212
|
|
|
208
|
-
# Add tool arguments as span attributes (be careful with sensitive data)
|
|
209
|
-
for i, arg in enumerate(args):
|
|
210
|
-
if isinstance(arg, str | int | float | bool) or arg is None:
|
|
211
|
-
span.set_attribute(f"mcp.tool.arg.{i}", str(arg))
|
|
212
|
-
elif hasattr(arg, "__dict__"):
|
|
213
|
-
# For objects, just record the type
|
|
214
|
-
span.set_attribute(f"mcp.tool.arg.{i}.type", type(arg).__name__)
|
|
215
|
-
|
|
216
|
-
# Add named arguments with better naming
|
|
217
|
-
for key, value in kwargs.items():
|
|
218
|
-
if key != "ctx":
|
|
219
|
-
if value is None:
|
|
220
|
-
span.set_attribute(f"mcp.tool.input.{key}", "null")
|
|
221
|
-
elif isinstance(value, str | int | float | bool):
|
|
222
|
-
span.set_attribute(f"mcp.tool.input.{key}", str(value))
|
|
223
|
-
elif isinstance(value, list | tuple):
|
|
224
|
-
span.set_attribute(f"mcp.tool.input.{key}.count", len(value))
|
|
225
|
-
span.set_attribute(f"mcp.tool.input.{key}.type", "array")
|
|
226
|
-
elif isinstance(value, dict):
|
|
227
|
-
span.set_attribute(f"mcp.tool.input.{key}.count", len(value))
|
|
228
|
-
span.set_attribute(f"mcp.tool.input.{key}.type", "object")
|
|
229
|
-
# Only show first few keys to avoid exceeding attribute limits
|
|
230
|
-
if len(value) > 0 and len(value) <= 5:
|
|
231
|
-
keys_list = list(value.keys())[:5]
|
|
232
|
-
# Limit key length and join
|
|
233
|
-
truncated_keys = [
|
|
234
|
-
str(k)[:20] + "..." if len(str(k)) > 20 else str(k)
|
|
235
|
-
for k in keys_list
|
|
236
|
-
]
|
|
237
|
-
span.set_attribute(
|
|
238
|
-
f"mcp.tool.input.{key}.sample_keys",
|
|
239
|
-
",".join(truncated_keys),
|
|
240
|
-
)
|
|
241
|
-
else:
|
|
242
|
-
# For other types, at least record the type
|
|
243
|
-
span.set_attribute(
|
|
244
|
-
f"mcp.tool.input.{key}.type", type(value).__name__
|
|
245
|
-
)
|
|
246
|
-
|
|
247
213
|
# Add event for tool execution start
|
|
248
214
|
span.add_event("tool.execution.started", {"tool.name": tool_name})
|
|
249
215
|
|
|
@@ -254,6 +220,19 @@ def instrument_tool(func: Callable[..., T], tool_name: str) -> Callable[..., T]:
|
|
|
254
220
|
# Add event for successful completion
|
|
255
221
|
span.add_event("tool.execution.completed", {"tool.name": tool_name})
|
|
256
222
|
|
|
223
|
+
# Record metrics for successful execution
|
|
224
|
+
try:
|
|
225
|
+
from golf.metrics import get_metrics_collector
|
|
226
|
+
|
|
227
|
+
metrics_collector = get_metrics_collector()
|
|
228
|
+
metrics_collector.increment_tool_execution(tool_name, "success")
|
|
229
|
+
metrics_collector.record_tool_duration(
|
|
230
|
+
tool_name, time.time() - start_time
|
|
231
|
+
)
|
|
232
|
+
except ImportError:
|
|
233
|
+
# Metrics not available, continue without metrics
|
|
234
|
+
pass
|
|
235
|
+
|
|
257
236
|
# Capture result metadata with better structure
|
|
258
237
|
if result is not None:
|
|
259
238
|
if isinstance(result, str | int | float | bool):
|
|
@@ -298,10 +277,27 @@ def instrument_tool(func: Callable[..., T], tool_name: str) -> Callable[..., T]:
|
|
|
298
277
|
"error.message": str(e),
|
|
299
278
|
},
|
|
300
279
|
)
|
|
280
|
+
|
|
281
|
+
# Record metrics for failed execution
|
|
282
|
+
try:
|
|
283
|
+
from golf.metrics import get_metrics_collector
|
|
284
|
+
|
|
285
|
+
metrics_collector = get_metrics_collector()
|
|
286
|
+
metrics_collector.increment_tool_execution(tool_name, "error")
|
|
287
|
+
metrics_collector.increment_error("tool", type(e).__name__)
|
|
288
|
+
except ImportError:
|
|
289
|
+
# Metrics not available, continue without metrics
|
|
290
|
+
pass
|
|
291
|
+
|
|
301
292
|
raise
|
|
302
293
|
|
|
303
294
|
@functools.wraps(func)
|
|
304
295
|
def sync_wrapper(*args, **kwargs):
|
|
296
|
+
# Record metrics timing
|
|
297
|
+
import time
|
|
298
|
+
|
|
299
|
+
start_time = time.time()
|
|
300
|
+
|
|
305
301
|
# Create a more descriptive span name
|
|
306
302
|
span_name = f"mcp.tool.{tool_name}.execute"
|
|
307
303
|
|
|
@@ -344,45 +340,6 @@ def instrument_tool(func: Callable[..., T], tool_name: str) -> Callable[..., T]:
|
|
|
344
340
|
if session_id_from_baggage:
|
|
345
341
|
span.set_attribute("mcp.session.id", session_id_from_baggage)
|
|
346
342
|
|
|
347
|
-
# Add tool arguments as span attributes (be careful with sensitive data)
|
|
348
|
-
for i, arg in enumerate(args):
|
|
349
|
-
if isinstance(arg, str | int | float | bool) or arg is None:
|
|
350
|
-
span.set_attribute(f"mcp.tool.arg.{i}", str(arg))
|
|
351
|
-
elif hasattr(arg, "__dict__"):
|
|
352
|
-
# For objects, just record the type
|
|
353
|
-
span.set_attribute(f"mcp.tool.arg.{i}.type", type(arg).__name__)
|
|
354
|
-
|
|
355
|
-
# Add named arguments with better naming
|
|
356
|
-
for key, value in kwargs.items():
|
|
357
|
-
if key != "ctx":
|
|
358
|
-
if value is None:
|
|
359
|
-
span.set_attribute(f"mcp.tool.input.{key}", "null")
|
|
360
|
-
elif isinstance(value, str | int | float | bool):
|
|
361
|
-
span.set_attribute(f"mcp.tool.input.{key}", str(value))
|
|
362
|
-
elif isinstance(value, list | tuple):
|
|
363
|
-
span.set_attribute(f"mcp.tool.input.{key}.count", len(value))
|
|
364
|
-
span.set_attribute(f"mcp.tool.input.{key}.type", "array")
|
|
365
|
-
elif isinstance(value, dict):
|
|
366
|
-
span.set_attribute(f"mcp.tool.input.{key}.count", len(value))
|
|
367
|
-
span.set_attribute(f"mcp.tool.input.{key}.type", "object")
|
|
368
|
-
# Only show first few keys to avoid exceeding attribute limits
|
|
369
|
-
if len(value) > 0 and len(value) <= 5:
|
|
370
|
-
keys_list = list(value.keys())[:5]
|
|
371
|
-
# Limit key length and join
|
|
372
|
-
truncated_keys = [
|
|
373
|
-
str(k)[:20] + "..." if len(str(k)) > 20 else str(k)
|
|
374
|
-
for k in keys_list
|
|
375
|
-
]
|
|
376
|
-
span.set_attribute(
|
|
377
|
-
f"mcp.tool.input.{key}.sample_keys",
|
|
378
|
-
",".join(truncated_keys),
|
|
379
|
-
)
|
|
380
|
-
else:
|
|
381
|
-
# For other types, at least record the type
|
|
382
|
-
span.set_attribute(
|
|
383
|
-
f"mcp.tool.input.{key}.type", type(value).__name__
|
|
384
|
-
)
|
|
385
|
-
|
|
386
343
|
# Add event for tool execution start
|
|
387
344
|
span.add_event("tool.execution.started", {"tool.name": tool_name})
|
|
388
345
|
|
|
@@ -393,6 +350,19 @@ def instrument_tool(func: Callable[..., T], tool_name: str) -> Callable[..., T]:
|
|
|
393
350
|
# Add event for successful completion
|
|
394
351
|
span.add_event("tool.execution.completed", {"tool.name": tool_name})
|
|
395
352
|
|
|
353
|
+
# Record metrics for successful execution
|
|
354
|
+
try:
|
|
355
|
+
from golf.metrics import get_metrics_collector
|
|
356
|
+
|
|
357
|
+
metrics_collector = get_metrics_collector()
|
|
358
|
+
metrics_collector.increment_tool_execution(tool_name, "success")
|
|
359
|
+
metrics_collector.record_tool_duration(
|
|
360
|
+
tool_name, time.time() - start_time
|
|
361
|
+
)
|
|
362
|
+
except ImportError:
|
|
363
|
+
# Metrics not available, continue without metrics
|
|
364
|
+
pass
|
|
365
|
+
|
|
396
366
|
# Capture result metadata with better structure
|
|
397
367
|
if result is not None:
|
|
398
368
|
if isinstance(result, str | int | float | bool):
|
|
@@ -437,6 +407,18 @@ def instrument_tool(func: Callable[..., T], tool_name: str) -> Callable[..., T]:
|
|
|
437
407
|
"error.message": str(e),
|
|
438
408
|
},
|
|
439
409
|
)
|
|
410
|
+
|
|
411
|
+
# Record metrics for failed execution
|
|
412
|
+
try:
|
|
413
|
+
from golf.metrics import get_metrics_collector
|
|
414
|
+
|
|
415
|
+
metrics_collector = get_metrics_collector()
|
|
416
|
+
metrics_collector.increment_tool_execution(tool_name, "error")
|
|
417
|
+
metrics_collector.increment_error("tool", type(e).__name__)
|
|
418
|
+
except ImportError:
|
|
419
|
+
# Metrics not available, continue without metrics
|
|
420
|
+
pass
|
|
421
|
+
|
|
440
422
|
raise
|
|
441
423
|
|
|
442
424
|
# Return appropriate wrapper based on function type
|
|
@@ -683,16 +665,6 @@ def instrument_prompt(func: Callable[..., T], prompt_name: str) -> Callable[...,
|
|
|
683
665
|
if session_id_from_baggage:
|
|
684
666
|
span.set_attribute("mcp.session.id", session_id_from_baggage)
|
|
685
667
|
|
|
686
|
-
# Add prompt arguments
|
|
687
|
-
for key, value in kwargs.items():
|
|
688
|
-
if key != "ctx":
|
|
689
|
-
if isinstance(value, str | int | float | bool) or value is None:
|
|
690
|
-
span.set_attribute(f"mcp.prompt.arg.{key}", str(value))
|
|
691
|
-
else:
|
|
692
|
-
span.set_attribute(
|
|
693
|
-
f"mcp.prompt.arg.{key}.type", type(value).__name__
|
|
694
|
-
)
|
|
695
|
-
|
|
696
668
|
# Add event for prompt generation start
|
|
697
669
|
span.add_event("prompt.generation.started", {"prompt.name": prompt_name})
|
|
698
670
|
|
|
@@ -787,16 +759,6 @@ def instrument_prompt(func: Callable[..., T], prompt_name: str) -> Callable[...,
|
|
|
787
759
|
if session_id_from_baggage:
|
|
788
760
|
span.set_attribute("mcp.session.id", session_id_from_baggage)
|
|
789
761
|
|
|
790
|
-
# Add prompt arguments
|
|
791
|
-
for key, value in kwargs.items():
|
|
792
|
-
if key != "ctx":
|
|
793
|
-
if isinstance(value, str | int | float | bool) or value is None:
|
|
794
|
-
span.set_attribute(f"mcp.prompt.arg.{key}", str(value))
|
|
795
|
-
else:
|
|
796
|
-
span.set_attribute(
|
|
797
|
-
f"mcp.prompt.arg.{key}.type", type(value).__name__
|
|
798
|
-
)
|
|
799
|
-
|
|
800
762
|
# Add event for prompt generation start
|
|
801
763
|
span.add_event("prompt.generation.started", {"prompt.name": prompt_name})
|
|
802
764
|
|
|
@@ -879,13 +841,64 @@ async def telemetry_lifespan(mcp_instance):
|
|
|
879
841
|
from starlette.requests import Request
|
|
880
842
|
|
|
881
843
|
class SessionTracingMiddleware(BaseHTTPMiddleware):
|
|
844
|
+
def __init__(self, app):
|
|
845
|
+
super().__init__(app)
|
|
846
|
+
# Track seen sessions to count unique sessions
|
|
847
|
+
self.seen_sessions = set()
|
|
848
|
+
# Track session start times for duration calculation
|
|
849
|
+
self.session_start_times = {}
|
|
850
|
+
|
|
882
851
|
async def dispatch(self, request: Request, call_next):
|
|
852
|
+
# Record HTTP request timing
|
|
853
|
+
import time
|
|
854
|
+
|
|
855
|
+
start_time = time.time()
|
|
856
|
+
|
|
883
857
|
# Extract session ID from query params or headers
|
|
884
858
|
session_id = request.query_params.get("session_id")
|
|
885
859
|
if not session_id:
|
|
886
860
|
# Check headers as fallback
|
|
887
861
|
session_id = request.headers.get("x-session-id")
|
|
888
862
|
|
|
863
|
+
# Track session metrics
|
|
864
|
+
if session_id:
|
|
865
|
+
current_time = time.time()
|
|
866
|
+
|
|
867
|
+
# Record new session if we haven't seen this session ID before
|
|
868
|
+
if session_id not in self.seen_sessions:
|
|
869
|
+
self.seen_sessions.add(session_id)
|
|
870
|
+
self.session_start_times[session_id] = current_time
|
|
871
|
+
try:
|
|
872
|
+
from golf.metrics import get_metrics_collector
|
|
873
|
+
|
|
874
|
+
metrics_collector = get_metrics_collector()
|
|
875
|
+
metrics_collector.increment_session()
|
|
876
|
+
except ImportError:
|
|
877
|
+
pass
|
|
878
|
+
else:
|
|
879
|
+
# Update session duration (time since first request)
|
|
880
|
+
if session_id in self.session_start_times:
|
|
881
|
+
duration = (
|
|
882
|
+
current_time - self.session_start_times[session_id]
|
|
883
|
+
)
|
|
884
|
+
try:
|
|
885
|
+
from golf.metrics import get_metrics_collector
|
|
886
|
+
|
|
887
|
+
metrics_collector = get_metrics_collector()
|
|
888
|
+
metrics_collector.record_session_duration(duration)
|
|
889
|
+
except ImportError:
|
|
890
|
+
pass
|
|
891
|
+
|
|
892
|
+
# Clean up old session data periodically
|
|
893
|
+
if len(self.seen_sessions) > 10000:
|
|
894
|
+
# Keep only the most recent 5000 sessions
|
|
895
|
+
recent_sessions = list(self.seen_sessions)[-5000:]
|
|
896
|
+
self.seen_sessions = set(recent_sessions)
|
|
897
|
+
# Clean up start times for removed sessions
|
|
898
|
+
for old_session in list(self.session_start_times.keys()):
|
|
899
|
+
if old_session not in self.seen_sessions:
|
|
900
|
+
self.session_start_times.pop(old_session, None)
|
|
901
|
+
|
|
889
902
|
# Create a descriptive span name based on the request
|
|
890
903
|
method = request.method
|
|
891
904
|
path = request.url.path
|
|
@@ -961,6 +974,29 @@ async def telemetry_lifespan(mcp_instance):
|
|
|
961
974
|
},
|
|
962
975
|
)
|
|
963
976
|
|
|
977
|
+
# Record HTTP request metrics
|
|
978
|
+
try:
|
|
979
|
+
from golf.metrics import get_metrics_collector
|
|
980
|
+
|
|
981
|
+
metrics_collector = get_metrics_collector()
|
|
982
|
+
|
|
983
|
+
# Clean up path for metrics (remove query params, normalize)
|
|
984
|
+
clean_path = path.split("?")[0] # Remove query parameters
|
|
985
|
+
if clean_path.startswith("/"):
|
|
986
|
+
clean_path = (
|
|
987
|
+
clean_path[1:] or "root"
|
|
988
|
+
) # Remove leading slash, handle root
|
|
989
|
+
|
|
990
|
+
metrics_collector.increment_http_request(
|
|
991
|
+
method, response.status_code, clean_path
|
|
992
|
+
)
|
|
993
|
+
metrics_collector.record_http_duration(
|
|
994
|
+
method, clean_path, time.time() - start_time
|
|
995
|
+
)
|
|
996
|
+
except ImportError:
|
|
997
|
+
# Metrics not available, continue without metrics
|
|
998
|
+
pass
|
|
999
|
+
|
|
964
1000
|
return response
|
|
965
1001
|
except Exception as e:
|
|
966
1002
|
span.record_exception(e)
|
|
@@ -976,6 +1012,25 @@ async def telemetry_lifespan(mcp_instance):
|
|
|
976
1012
|
"error.message": str(e),
|
|
977
1013
|
},
|
|
978
1014
|
)
|
|
1015
|
+
|
|
1016
|
+
# Record HTTP error metrics
|
|
1017
|
+
try:
|
|
1018
|
+
from golf.metrics import get_metrics_collector
|
|
1019
|
+
|
|
1020
|
+
metrics_collector = get_metrics_collector()
|
|
1021
|
+
|
|
1022
|
+
# Clean up path for metrics
|
|
1023
|
+
clean_path = path.split("?")[0]
|
|
1024
|
+
if clean_path.startswith("/"):
|
|
1025
|
+
clean_path = clean_path[1:] or "root"
|
|
1026
|
+
|
|
1027
|
+
metrics_collector.increment_http_request(
|
|
1028
|
+
method, 500, clean_path
|
|
1029
|
+
) # Assume 500 for exceptions
|
|
1030
|
+
metrics_collector.increment_error("http", type(e).__name__)
|
|
1031
|
+
except ImportError:
|
|
1032
|
+
pass
|
|
1033
|
+
|
|
979
1034
|
raise
|
|
980
1035
|
finally:
|
|
981
1036
|
if token:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: golf-mcp
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.18
|
|
4
4
|
Summary: Framework for building MCP servers
|
|
5
5
|
Author-email: Antoni Gmitruk <antoni@golf.dev>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -34,6 +34,8 @@ Requires-Dist: opentelemetry-sdk>=1.33.1; extra == "telemetry"
|
|
|
34
34
|
Requires-Dist: opentelemetry-instrumentation-asgi>=0.40b0; extra == "telemetry"
|
|
35
35
|
Requires-Dist: opentelemetry-exporter-otlp-proto-http>=0.40b0; extra == "telemetry"
|
|
36
36
|
Requires-Dist: wrapt>=1.17.0; extra == "telemetry"
|
|
37
|
+
Provides-Extra: metrics
|
|
38
|
+
Requires-Dist: prometheus-client>=0.22.1; extra == "metrics"
|
|
37
39
|
Dynamic: license-file
|
|
38
40
|
|
|
39
41
|
<div align="center">
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
golf/__init__.py,sha256=
|
|
1
|
+
golf/__init__.py,sha256=6BiuMUkhwQp6bzUZSF8np8F1NwCltEtK0sPBF__tepU,23
|
|
2
2
|
golf/auth/__init__.py,sha256=Rj4yUngJklk6xrDCrxqLTtoDAMzF1HcTvy_l8wREeao,4103
|
|
3
3
|
golf/auth/api_key.py,sha256=LiIraLiH2v7s3yavidaI6BDlAEfK8XnWF15QmaJn9G4,2378
|
|
4
4
|
golf/auth/helpers.py,sha256=ZogdcHM7J2PN6cL6F6OLZ3gyoUR3dwAFDxOJQ2DW_bc,6526
|
|
@@ -11,10 +11,11 @@ golf/commands/build.py,sha256=jhdxB5EwwCC_8PgqdXLUKuBpnycjh0gft3_7EuTo6ro,2319
|
|
|
11
11
|
golf/commands/init.py,sha256=DUAvGqOUapWdF2cgWPscqHRvyOZDiajR0F0Wkn_jm-k,10355
|
|
12
12
|
golf/commands/run.py,sha256=xsiG5LZw4qVt3cRTTfIoWP4Bf4AxNBBJKx0NNfoua40,2884
|
|
13
13
|
golf/core/__init__.py,sha256=4bKeskJ2fPaZqkz2xQScSa3phRLLrmrczwSL632jv-o,52
|
|
14
|
-
golf/core/builder.py,sha256=
|
|
15
|
-
golf/core/builder_auth.py,sha256=
|
|
14
|
+
golf/core/builder.py,sha256=flsXnlwOTUhXFMpmZwoxWgnK_oLj4zpSyqcKuDnXezw,59526
|
|
15
|
+
golf/core/builder_auth.py,sha256=nGgyMTiRAqaNfh1FSvoFe6oTVq9RgfMf9JoFGAv2_do,14050
|
|
16
|
+
golf/core/builder_metrics.py,sha256=j6Gtgd867o46JbDfSNGNsHt1QtV1XHKUJs1z8r4siQM,8830
|
|
16
17
|
golf/core/builder_telemetry.py,sha256=jobFgRSspLQLuitL4ytk6asSUdTqYcDxGY3sTjkrZd4,2654
|
|
17
|
-
golf/core/config.py,sha256=
|
|
18
|
+
golf/core/config.py,sha256=6yPtwzVTJauufEnrfUbxsz69H8jC0Ra427oDaRM0-xE,7397
|
|
18
19
|
golf/core/parser.py,sha256=BQRus1O9zmzSmyavwLVfN8BpYFkbrWUDrgQ7yrYfAKw,42457
|
|
19
20
|
golf/core/platform.py,sha256=Z2yEi6ilmQCLC_uAD_oZdVO0WvkL4tsyw7sx0vHhysI,6440
|
|
20
21
|
golf/core/telemetry.py,sha256=CjZ7urbizaRjyFiVBjfGW8V4PmNCG1_quk3FvbVTcjw,15772
|
|
@@ -46,11 +47,14 @@ golf/examples/basic/tools/hello.py,sha256=s7Soiq9Wn7oKIvA6Hid8UKB14iyR7HZppIbIT4
|
|
|
46
47
|
golf/examples/basic/tools/payments/charge.py,sha256=PIYdFV90hu35H1veLI8ueuYwebzrr5SCTX-x6lxRmU4,1800
|
|
47
48
|
golf/examples/basic/tools/payments/common.py,sha256=hfyuQRIjrQfSqGjyY55W6pZSD5jL6O0geCE0DGx0v10,1302
|
|
48
49
|
golf/examples/basic/tools/payments/refund.py,sha256=Qpl4GWvUw-L06oGQMbBzG8pikfCWfBCFcpkRiDOzmyQ,1607
|
|
50
|
+
golf/metrics/__init__.py,sha256=O91y-hj_E9R06gqV8pDZrzHxOIl-1T415Hj9RvFAp3o,283
|
|
51
|
+
golf/metrics/collector.py,sha256=iyRszP8TAAigyOsUFTGdKN8Xeob36LhUvXW9tntJrbA,7617
|
|
52
|
+
golf/metrics/registry.py,sha256=mXQE4Pwf3PopGYjcUu4eGgPDAe085YWcsvcvWk0ny8Q,310
|
|
49
53
|
golf/telemetry/__init__.py,sha256=ESGCg5HKwTCIfID1e_K7EE0bOWkSzMidlLtdqQgBd0w,396
|
|
50
|
-
golf/telemetry/instrumentation.py,sha256=
|
|
51
|
-
golf_mcp-0.1.
|
|
52
|
-
golf_mcp-0.1.
|
|
53
|
-
golf_mcp-0.1.
|
|
54
|
-
golf_mcp-0.1.
|
|
55
|
-
golf_mcp-0.1.
|
|
56
|
-
golf_mcp-0.1.
|
|
54
|
+
golf/telemetry/instrumentation.py,sha256=Sn7KBJSCf0Kih9ILEdminR0HggpbOlBKToJhnC4PmZE,44934
|
|
55
|
+
golf_mcp-0.1.18.dist-info/licenses/LICENSE,sha256=5_j2f6fTJmvfmUewzElhkpAaXg2grVoxKouOA8ihV6E,11348
|
|
56
|
+
golf_mcp-0.1.18.dist-info/METADATA,sha256=w8GR9q2Ln0UzWTXRFG6XdA1EzbAVUsF7D0tq1nEMqd4,12956
|
|
57
|
+
golf_mcp-0.1.18.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
58
|
+
golf_mcp-0.1.18.dist-info/entry_points.txt,sha256=5y7rHYM8jGpU-nfwdknCm5XsApLulqsnA37MO6BUTYg,43
|
|
59
|
+
golf_mcp-0.1.18.dist-info/top_level.txt,sha256=BQToHcBUufdyhp9ONGMIvPE40jMEtmI20lYaKb4hxOg,5
|
|
60
|
+
golf_mcp-0.1.18.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|