diff --git a/README.md b/README.md index 146bb09..cc505ae 100644 --- a/README.md +++ b/README.md @@ -138,3 +138,19 @@ The MCP server provides the following tools for interacting with the OpenShift A * **Create a cluster**: "Create a new cluster named 'my-cluster' with OpenShift 4.14 and base domain 'example.com'" * **Check cluster events**: "What events happened on cluster abc123?" * **Install a cluster**: "Start the installation for cluster abc123" + +## Prometheus Metrics + +The MCP server exposes Prometheus metrics to monitor tool usage and performance. The metrics are available at `http://localhost:8000/metrics` when the server is running. + +### Available Metrics + +* **assisted_service_mcp_tool_request_count** - Number of tool requests. +* **assisted_service_mcp_tool_request_duration_sum** - Total time to run the tool, in seconds. +* **assisted_service_mcp_tool_request_duration_count** - Total number of tool requests measured. +* **assisted_service_mcp_tool_request_duration_bucket** - Number of tool requests organized in buckets. + +### Metric Labels + +All metrics include the following label: +* **tool** - The name of the tool, for example `cluster_info`, `list_clusters`, etc. diff --git a/pyproject.toml b/pyproject.toml index 4e40393..860d8f9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,6 +11,7 @@ dependencies = [ "requests>=2.32.3", "retry>=0.9.2", "types-requests>=2.32.4.20250611", + "prometheus_client>=0.22.1", ] [dependency-groups] diff --git a/server.py b/server.py index b2605d7..46a3661 100644 --- a/server.py +++ b/server.py @@ -8,13 +8,17 @@ import json import os + import requests -from mcp.server.fastmcp import FastMCP +import uvicorn from assisted_service_client import models +from mcp.server.fastmcp import FastMCP -from service_client import InventoryClient + +from service_client import InventoryClient, metrics, track_tool_usage from service_client.logger import log + mcp = FastMCP("AssistedService", host="0.0.0.0") @@ -115,6 +119,7 @@ def get_access_token() -> str: @mcp.tool() +@track_tool_usage() async def cluster_info(cluster_id: str) -> str: """ Get comprehensive information about a specific assisted installer cluster. @@ -141,6 +146,7 @@ async def cluster_info(cluster_id: str) -> str: @mcp.tool() +@track_tool_usage() async def list_clusters() -> str: """ List all assisted installer clusters for the current user. @@ -174,6 +180,7 @@ async def list_clusters() -> str: @mcp.tool() +@track_tool_usage() async def cluster_events(cluster_id: str) -> str: """ Get the events related to a cluster with the given cluster id. @@ -197,6 +204,7 @@ async def cluster_events(cluster_id: str) -> str: @mcp.tool() +@track_tool_usage() async def host_events(cluster_id: str, host_id: str) -> str: """ Get events specific to a particular host within a cluster. @@ -222,6 +230,7 @@ async def host_events(cluster_id: str, host_id: str) -> str: @mcp.tool() +@track_tool_usage() async def cluster_iso_download_url(cluster_id: str) -> str: """ Get ISO download URL(s) for a cluster. @@ -278,6 +287,7 @@ async def cluster_iso_download_url(cluster_id: str) -> str: @mcp.tool() +@track_tool_usage() async def create_cluster( name: str, version: str, base_domain: str, single_node: bool ) -> str: @@ -328,6 +338,7 @@ async def create_cluster( @mcp.tool() +@track_tool_usage() async def set_cluster_vips(cluster_id: str, api_vip: str, ingress_vip: str) -> str: """ Configure the virtual IP addresses (VIPs) for cluster API and ingress traffic. @@ -362,6 +373,7 @@ async def set_cluster_vips(cluster_id: str, api_vip: str, ingress_vip: str) -> s @mcp.tool() +@track_tool_usage() async def install_cluster(cluster_id: str) -> str: """ Trigger the installation process for a prepared cluster. @@ -391,6 +403,7 @@ async def install_cluster(cluster_id: str) -> str: @mcp.tool() +@track_tool_usage() async def list_versions() -> str: """ List all available OpenShift versions for installation. @@ -411,6 +424,7 @@ async def list_versions() -> str: @mcp.tool() +@track_tool_usage() async def list_operator_bundles() -> str: """ List available operator bundles for cluster installation. @@ -431,6 +445,7 @@ async def list_operator_bundles() -> str: @mcp.tool() +@track_tool_usage() async def add_operator_bundle_to_cluster(cluster_id: str, bundle_name: str) -> str: """ Add an operator bundle to be installed with the cluster. @@ -458,6 +473,7 @@ async def add_operator_bundle_to_cluster(cluster_id: str, bundle_name: str) -> s @mcp.tool() +@track_tool_usage() async def cluster_credentials_download_url(cluster_id: str, file_name: str) -> str: """ Get presigned download URL for cluster credential files. @@ -501,6 +517,7 @@ async def cluster_credentials_download_url(cluster_id: str, file_name: str) -> s @mcp.tool() +@track_tool_usage() async def set_host_role(host_id: str, infraenv_id: str, role: str) -> str: """ Assign a specific role to a discovered host in the cluster. @@ -528,4 +545,6 @@ async def set_host_role(host_id: str, infraenv_id: str, role: str) -> str: if __name__ == "__main__": - mcp.run(transport="sse") + app = mcp.sse_app() + app.add_route("/metrics", metrics) + uvicorn.run(app, host="0.0.0.0") diff --git a/service_client/__init__.py b/service_client/__init__.py index a48c4f1..ced1196 100644 --- a/service_client/__init__.py +++ b/service_client/__init__.py @@ -7,5 +7,6 @@ from .assisted_service_api import InventoryClient from .logger import log +from .metrics import metrics, track_tool_usage -__all__ = ["InventoryClient", "log"] +__all__ = ["InventoryClient", "log", "metrics", "track_tool_usage"] diff --git a/service_client/metrics.py b/service_client/metrics.py new file mode 100644 index 0000000..57c7d16 --- /dev/null +++ b/service_client/metrics.py @@ -0,0 +1,56 @@ +""" +Metrics for the MCP server. + +This module provides metrics for the MCP server. +""" + +from typing import Callable, Any +from functools import wraps + +from prometheus_client import ( + CONTENT_TYPE_LATEST, + Counter, + Histogram, + generate_latest, +) +from starlette.requests import Request +from starlette.responses import PlainTextResponse + + +# Define counter for request count +REQUEST_COUNT = Counter( + "assisted_service_mcp_tool_request_count", + "Request count", + ["tool"], +) + +# Define histogram for request latency +REQUEST_LATENCY = Histogram( + "assisted_service_mcp_tool_request_duration", + "Request latency", + ["tool"], + buckets=(0.1, 1.0, 10.0, 30.0, float("inf")), +) + + +def track_tool_usage() -> Callable: + """Decorate MCP tools with this decorator to track tool usage metrics.""" + + def decorator(func: Callable) -> Callable: + @wraps(func) + async def wrapper(*args: Any, **kwargs: Any) -> Any: + tool_name = func.__name__ + REQUEST_COUNT.labels(tool=tool_name).inc() + with REQUEST_LATENCY.labels(tool=tool_name).time(): + response = await func(*args, **kwargs) + return response + + return wrapper + + return decorator + + +# Metrics route +async def metrics(_request: Request) -> PlainTextResponse: + """Metrics endpoint.""" + return PlainTextResponse(generate_latest(), media_type=CONTENT_TYPE_LATEST)