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

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 # 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

297 def 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

311 def 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