Coverage for /Users/antonigmitruk/golf/src/golf/metrics/collector.py: 0%

95 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-08-16 18:46 +0200

1"""Metrics collector for Golf MCP servers.""" 

2 

3from typing import Optional 

4 

5# Global metrics collector instance 

6_metrics_collector: Optional["MetricsCollector"] = None 

7 

8 

9class 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 # Sampling metrics 

70 self._metrics["sampling_requests"] = Counter( 

71 "golf_sampling_requests_total", 

72 "Total number of sampling requests", 

73 ["sampling_type", "status"], 

74 ) 

75 

76 self._metrics["sampling_duration"] = Histogram( 

77 "golf_sampling_duration_seconds", 

78 "Sampling request duration in seconds", 

79 ["sampling_type"], 

80 ) 

81 

82 self._metrics["sampling_tokens"] = Histogram( 

83 "golf_sampling_tokens", 

84 "Number of tokens in sampling responses", 

85 ["sampling_type"], 

86 ) 

87 

88 # Elicitation metrics 

89 self._metrics["elicitation_requests"] = Counter( 

90 "golf_elicitation_requests_total", 

91 "Total number of elicitation requests", 

92 ["elicitation_type", "status"], 

93 ) 

94 

95 self._metrics["elicitation_duration"] = Histogram( 

96 "golf_elicitation_duration_seconds", 

97 "Elicitation request duration in seconds", 

98 ["elicitation_type"], 

99 ) 

100 

101 # Error metrics 

102 self._metrics["errors"] = Counter( 

103 "golf_errors_total", 

104 "Total number of errors", 

105 ["component_type", "error_type"], 

106 ) 

107 

108 # Session metrics 

109 self._metrics["sessions_total"] = Counter("golf_sessions_total", "Total number of sessions created") 

110 

111 self._metrics["session_duration"] = Histogram( 

112 "golf_session_duration_seconds", "Session duration in seconds" 

113 ) 

114 

115 # System metrics 

116 self._metrics["uptime"] = Gauge("golf_uptime_seconds", "Server uptime in seconds") 

117 

118 except ImportError: 

119 # Prometheus client not available, disable metrics 

120 self.enabled = False 

121 

122 def increment_tool_execution(self, tool_name: str, status: str) -> None: 

123 """Record a tool execution. 

124 

125 Args: 

126 tool_name: Name of the tool that was executed 

127 status: Execution status ('success' or 'error') 

128 """ 

129 if not self.enabled or "tool_executions" not in self._metrics: 

130 return 

131 

132 self._metrics["tool_executions"].labels(tool_name=tool_name, status=status).inc() 

133 

134 def record_tool_duration(self, tool_name: str, duration: float) -> None: 

135 """Record tool execution duration. 

136 

137 Args: 

138 tool_name: Name of the tool 

139 duration: Execution duration in seconds 

140 """ 

141 if not self.enabled or "tool_duration" not in self._metrics: 

142 return 

143 

144 self._metrics["tool_duration"].labels(tool_name=tool_name).observe(duration) 

145 

146 def increment_http_request(self, method: str, status_code: int, path: str) -> None: 

147 """Record an HTTP request. 

148 

149 Args: 

150 method: HTTP method (GET, POST, etc.) 

151 status_code: HTTP status code 

152 path: Request path 

153 """ 

154 if not self.enabled or "http_requests" not in self._metrics: 

155 return 

156 

157 self._metrics["http_requests"].labels(method=method, status_code=str(status_code), path=path).inc() 

158 

159 def record_http_duration(self, method: str, path: str, duration: float) -> None: 

160 """Record HTTP request duration. 

161 

162 Args: 

163 method: HTTP method 

164 path: Request path 

165 duration: Request duration in seconds 

166 """ 

167 if not self.enabled or "http_duration" not in self._metrics: 

168 return 

169 

170 self._metrics["http_duration"].labels(method=method, path=path).observe(duration) 

171 

172 def increment_resource_read(self, resource_uri: str) -> None: 

173 """Record a resource read. 

174 

175 Args: 

176 resource_uri: URI of the resource that was read 

177 """ 

178 if not self.enabled or "resource_reads" not in self._metrics: 

179 return 

180 

181 self._metrics["resource_reads"].labels(resource_uri=resource_uri).inc() 

182 

183 def increment_prompt_generation(self, prompt_name: str) -> None: 

184 """Record a prompt generation. 

185 

186 Args: 

187 prompt_name: Name of the prompt that was generated 

188 """ 

189 if not self.enabled or "prompt_generations" not in self._metrics: 

190 return 

191 

192 self._metrics["prompt_generations"].labels(prompt_name=prompt_name).inc() 

193 

194 def increment_error(self, component_type: str, error_type: str) -> None: 

195 """Record an error. 

196 

197 Args: 

198 component_type: Type of component ('tool', 'resource', 'prompt', 'http') 

199 error_type: Type of error ('timeout', 'auth_error', 

200 'validation_error', etc.) 

201 """ 

202 if not self.enabled or "errors" not in self._metrics: 

203 return 

204 

205 self._metrics["errors"].labels(component_type=component_type, error_type=error_type).inc() 

206 

207 def increment_session(self) -> None: 

208 """Record a new session.""" 

209 if not self.enabled or "sessions_total" not in self._metrics: 

210 return 

211 

212 self._metrics["sessions_total"].inc() 

213 

214 def record_session_duration(self, duration: float) -> None: 

215 """Record session duration. 

216 

217 Args: 

218 duration: Session duration in seconds 

219 """ 

220 if not self.enabled or "session_duration" not in self._metrics: 

221 return 

222 

223 self._metrics["session_duration"].observe(duration) 

224 

225 def set_uptime(self, seconds: float) -> None: 

226 """Set the server uptime. 

227 

228 Args: 

229 seconds: Server uptime in seconds 

230 """ 

231 if not self.enabled or "uptime" not in self._metrics: 

232 return 

233 

234 self._metrics["uptime"].set(seconds) 

235 

236 def increment_sampling(self, sampling_type: str, status: str) -> None: 

237 """Record a sampling request. 

238 

239 Args: 

240 sampling_type: Type of sampling ('sample', 'structured', 'context') 

241 status: Request status ('success' or 'error') 

242 """ 

243 if not self.enabled or "sampling_requests" not in self._metrics: 

244 return 

245 

246 self._metrics["sampling_requests"].labels(sampling_type=sampling_type, status=status).inc() 

247 

248 def record_sampling_duration(self, sampling_type: str, duration: float) -> None: 

249 """Record sampling request duration. 

250 

251 Args: 

252 sampling_type: Type of sampling 

253 duration: Request duration in seconds 

254 """ 

255 if not self.enabled or "sampling_duration" not in self._metrics: 

256 return 

257 

258 self._metrics["sampling_duration"].labels(sampling_type=sampling_type).observe(duration) 

259 

260 def record_sampling_tokens(self, sampling_type: str, token_count: int) -> None: 

261 """Record sampling token count. 

262 

263 Args: 

264 sampling_type: Type of sampling 

265 token_count: Number of tokens in the response 

266 """ 

267 if not self.enabled or "sampling_tokens" not in self._metrics: 

268 return 

269 

270 self._metrics["sampling_tokens"].labels(sampling_type=sampling_type).observe(token_count) 

271 

272 def increment_elicitation(self, elicitation_type: str, status: str) -> None: 

273 """Record an elicitation request. 

274 

275 Args: 

276 elicitation_type: Type of elicitation ('elicit', 'confirmation') 

277 status: Request status ('success' or 'error') 

278 """ 

279 if not self.enabled or "elicitation_requests" not in self._metrics: 

280 return 

281 

282 self._metrics["elicitation_requests"].labels(elicitation_type=elicitation_type, status=status).inc() 

283 

284 def record_elicitation_duration(self, elicitation_type: str, duration: float) -> None: 

285 """Record elicitation request duration. 

286 

287 Args: 

288 elicitation_type: Type of elicitation 

289 duration: Request duration in seconds 

290 """ 

291 if not self.enabled or "elicitation_duration" not in self._metrics: 

292 return 

293 

294 self._metrics["elicitation_duration"].labels(elicitation_type=elicitation_type).observe(duration) 

295 

296 

297def init_metrics_collector(enabled: bool = False) -> MetricsCollector: 

298 """Initialize the global metrics collector. 

299 

300 Args: 

301 enabled: Whether to enable metrics collection 

302 

303 Returns: 

304 The initialized metrics collector 

305 """ 

306 global _metrics_collector 

307 _metrics_collector = MetricsCollector(enabled=enabled) 

308 return _metrics_collector 

309 

310 

311def get_metrics_collector() -> MetricsCollector: 

312 """Get the global metrics collector instance. 

313 

314 Returns: 

315 The metrics collector, or a disabled one if not initialized 

316 """ 

317 global _metrics_collector 

318 if _metrics_collector is None: 

319 _metrics_collector = MetricsCollector(enabled=False) 

320 return _metrics_collector