|
23 | 23 | from datetime import datetime
|
24 | 24 | from functools import wraps
|
25 | 25 | import io
|
| 26 | +from io import StringIO |
26 | 27 | import json
|
27 | 28 | from pathlib import Path
|
28 | 29 | import time
|
29 | 30 | from typing import Any, cast, Dict, List, Optional, Union
|
30 | 31 | import uuid
|
31 | 32 |
|
32 | 33 | # Third-Party
|
33 |
| -from fastapi import APIRouter, Depends, HTTPException, Request, Response |
| 34 | +from fastapi import APIRouter, Depends, HTTPException, Query, Request, Response |
34 | 35 | from fastapi.responses import FileResponse, HTMLResponse, JSONResponse, RedirectResponse, StreamingResponse
|
35 | 36 | import httpx
|
36 | 37 | from pydantic import ValidationError
|
@@ -670,7 +671,6 @@ async def admin_edit_server(
|
670 | 671 | update operation.
|
671 | 672 |
|
672 | 673 | Expects form fields:
|
673 |
| - - id (optional): Updated UUID for the server |
674 | 674 | - name (optional): The updated name of the server
|
675 | 675 | - description (optional): An updated description of the server's purpose
|
676 | 676 | - icon (optional): Updated URL or path to the server's icon
|
@@ -779,7 +779,6 @@ async def admin_edit_server(
|
779 | 779 | try:
|
780 | 780 | LOGGER.debug(f"User {user} is editing server ID {server_id} with name: {form.get('name')}")
|
781 | 781 | server = ServerUpdate(
|
782 |
| - id=form.get("id"), |
783 | 782 | name=form.get("name"),
|
784 | 783 | description=form.get("description"),
|
785 | 784 | icon=form.get("icon"),
|
@@ -1981,7 +1980,6 @@ async def admin_add_tool(
|
1981 | 1980 |
|
1982 | 1981 | tool_data: dict[str, Any] = {
|
1983 | 1982 | "name": form.get("name"),
|
1984 |
| - "displayName": form.get("displayName"), |
1985 | 1983 | "url": form.get("url"),
|
1986 | 1984 | "description": form.get("description"),
|
1987 | 1985 | "request_type": request_type,
|
@@ -2047,7 +2045,6 @@ async def admin_edit_tool(
|
2047 | 2045 |
|
2048 | 2046 | Expects form fields:
|
2049 | 2047 | - name
|
2050 |
| - - displayName (optional) |
2051 | 2048 | - url
|
2052 | 2049 | - description (optional)
|
2053 | 2050 | - requestType (to be mapped to request_type)
|
@@ -2222,7 +2219,6 @@ async def admin_edit_tool(
|
2222 | 2219 |
|
2223 | 2220 | tool_data: dict[str, Any] = {
|
2224 | 2221 | "name": form.get("name"),
|
2225 |
| - "displayName": form.get("displayName"), |
2226 | 2222 | "custom_name": form.get("customName"),
|
2227 | 2223 | "url": form.get("url"),
|
2228 | 2224 | "description": form.get("description"),
|
@@ -4155,6 +4151,8 @@ async def admin_delete_root(uri: str, request: Request, user: str = Depends(requ
|
4155 | 4151 | # Metrics
|
4156 | 4152 | MetricsDict = Dict[str, Union[ToolMetrics, ResourceMetrics, ServerMetrics, PromptMetrics]]
|
4157 | 4153 |
|
| 4154 | +# Import the response time formatting function |
| 4155 | +from mcpgateway.utils.metrics_common import format_response_time |
4158 | 4156 |
|
4159 | 4157 | # @admin_router.get("/metrics", response_model=MetricsDict)
|
4160 | 4158 | # async def admin_get_metrics(
|
@@ -4233,6 +4231,120 @@ async def get_aggregated_metrics(
|
4233 | 4231 | return metrics
|
4234 | 4232 |
|
4235 | 4233 |
|
| 4234 | +@admin_router.get("/metrics/export", response_class=Response) |
| 4235 | +async def export_metrics_csv( |
| 4236 | + db: Session = Depends(get_db), |
| 4237 | + entity_type: str = Query(..., description="Entity type to export (tools, resources, prompts, servers)"), |
| 4238 | + limit: Optional[int] = Query(None, description="Maximum number of results to return. If not provided, all results are returned."), |
| 4239 | + user: str = Depends(require_auth), |
| 4240 | +) -> Response: |
| 4241 | + """Export metrics for a specific entity type to CSV format. |
| 4242 | +
|
| 4243 | + This endpoint retrieves performance metrics for the specified entity type and |
| 4244 | + exports them to CSV format for download. All rows are exported, not just the top 5. |
| 4245 | + Response times are formatted to 3 decimal places. |
| 4246 | +
|
| 4247 | + Args: |
| 4248 | + db (Session): Database session dependency for querying metrics. |
| 4249 | + entity_type (str): Type of entity to export (tools, resources, prompts, servers). |
| 4250 | + limit (Optional[int]): Maximum number of results to return. If None, all results are returned. |
| 4251 | + user (str): Authenticated user. |
| 4252 | +
|
| 4253 | + Returns: |
| 4254 | + Response: CSV file download response containing the metrics data. |
| 4255 | + |
| 4256 | + Raises: |
| 4257 | + HTTPException: If the entity type is invalid. |
| 4258 | + """ |
| 4259 | + LOGGER.debug(f"User {user} requested CSV export of {entity_type} metrics") |
| 4260 | + |
| 4261 | + # Validate entity type |
| 4262 | + valid_types = ["tools", "resources", "prompts", "servers"] |
| 4263 | + if entity_type not in valid_types: |
| 4264 | + raise HTTPException(status_code=400, detail=f"Invalid entity type. Must be one of: {', '.join(valid_types)}") |
| 4265 | + |
| 4266 | + # Get the top performers for the requested entity type without limit to get all rows |
| 4267 | + try: |
| 4268 | + if entity_type == "tools": |
| 4269 | + if limit: |
| 4270 | + performers = await tool_service.get_top_tools(db, limit=limit) |
| 4271 | + else: |
| 4272 | + performers = await tool_service.get_top_tools(db, limit=None) |
| 4273 | + elif entity_type == "resources": |
| 4274 | + if limit: |
| 4275 | + performers = await resource_service.get_top_resources(db, limit=limit) |
| 4276 | + else: |
| 4277 | + performers = await resource_service.get_top_resources(db, limit=None) |
| 4278 | + elif entity_type == "prompts": |
| 4279 | + if limit: |
| 4280 | + performers = await prompt_service.get_top_prompts(db, limit=limit) |
| 4281 | + else: |
| 4282 | + performers = await prompt_service.get_top_prompts(db, limit=None) |
| 4283 | + elif entity_type == "servers": |
| 4284 | + if limit: |
| 4285 | + performers = await server_service.get_top_servers(db, limit=limit) |
| 4286 | + else: |
| 4287 | + performers = await server_service.get_top_servers(db, limit=None) |
| 4288 | + except Exception as e: |
| 4289 | + LOGGER.error(f"Error exporting {entity_type} metrics to CSV: {str(e)}") |
| 4290 | + raise HTTPException(status_code=500, detail=f"Failed to export metrics: {str(e)}") |
| 4291 | + |
| 4292 | + # Handle empty data case |
| 4293 | + if not performers: |
| 4294 | + # Return empty CSV with headers |
| 4295 | + csv_content = "ID,Name,Execution Count,Average Response Time (s),Success Rate (%),Last Execution\n" |
| 4296 | + return Response( |
| 4297 | + content=csv_content, |
| 4298 | + media_type="text/csv", |
| 4299 | + headers={"Content-Disposition": f"attachment; filename={entity_type}_metrics.csv"} |
| 4300 | + ) |
| 4301 | + |
| 4302 | + # Create CSV content |
| 4303 | + output = StringIO() |
| 4304 | + writer = csv.writer(output) |
| 4305 | + |
| 4306 | + # Write header row |
| 4307 | + writer.writerow([ |
| 4308 | + "ID", |
| 4309 | + "Name", |
| 4310 | + "Execution Count", |
| 4311 | + "Average Response Time (s)", |
| 4312 | + "Success Rate (%)", |
| 4313 | + "Last Execution" |
| 4314 | + ]) |
| 4315 | + |
| 4316 | + # Write data rows with formatted values |
| 4317 | + for performer in performers: |
| 4318 | + # Format response time to 3 decimal places |
| 4319 | + formatted_response_time = format_response_time(performer.avg_response_time) if performer.avg_response_time is not None else "N/A" |
| 4320 | + |
| 4321 | + # Format success rate |
| 4322 | + success_rate = f"{performer.success_rate:.1f}" if performer.success_rate is not None else "N/A" |
| 4323 | + |
| 4324 | + # Format timestamp |
| 4325 | + last_execution = performer.last_execution.isoformat() if performer.last_execution else "N/A" |
| 4326 | + |
| 4327 | + writer.writerow([ |
| 4328 | + performer.id, |
| 4329 | + performer.name, |
| 4330 | + performer.execution_count, |
| 4331 | + formatted_response_time, |
| 4332 | + success_rate, |
| 4333 | + last_execution |
| 4334 | + ]) |
| 4335 | + |
| 4336 | + # Get the CSV content as a string |
| 4337 | + csv_content = output.getvalue() |
| 4338 | + output.close() |
| 4339 | + |
| 4340 | + # Return CSV response |
| 4341 | + return Response( |
| 4342 | + content=csv_content, |
| 4343 | + media_type="text/csv", |
| 4344 | + headers={"Content-Disposition": f"attachment; filename={entity_type}_metrics.csv"} |
| 4345 | + ) |
| 4346 | + |
| 4347 | + |
4236 | 4348 | @admin_router.post("/metrics/reset", response_model=Dict[str, object])
|
4237 | 4349 | async def admin_reset_metrics(db: Session = Depends(get_db), user: str = Depends(require_auth)) -> Dict[str, object]:
|
4238 | 4350 | """
|
|
0 commit comments