diff --git a/.github/actions/docker-check-containers/action.yml b/.github/actions/docker-check-containers/action.yml index 9730361..851d54f 100644 --- a/.github/actions/docker-check-containers/action.yml +++ b/.github/actions/docker-check-containers/action.yml @@ -31,34 +31,3 @@ runs: fi done shell: bash - - name: Check Prometheus is running - run: | - for i in {1..${{ inputs.num-retries }}}; do - if curl http://localhost:9090; then - echo "Health check passed" - break - else - echo "Health check failed, attempt $i/${{ inputs.num-retries }}" - if [ $i -eq ${{ inputs.num-retries }} ]; then - exit 1 - fi - sleep ${{ inputs.timeout-seconds }} - fi - done - shell: bash - - - name: Check Grafana is running - run: | - for i in {1..${{ inputs.num-retries }}}; do - if curl http://localhost:3000; then - echo "Health check passed" - break - else - echo "Health check failed, attempt $i/${{ inputs.num-retries }}" - if [ $i -eq ${{ inputs.num-retries }} ]; then - exit 1 - fi - sleep ${{ inputs.timeout-seconds }} - fi - done - shell: bash diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index c393213..3532227 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -11,7 +11,7 @@ Developers extend `TemplateServer` to create application-specific servers (see ` ### Application Factory Pattern - Entry: `main.py:run()` → instantiates `ExampleServer` (subclass of `TemplateServer`) → calls `.run()` -- `TemplateServer.__init__()` sets up middleware, rate limiting, metrics, and calls `setup_routes()` +- `TemplateServer.__init__()` sets up middleware, rate limiting, and calls `setup_routes()` - **Critical**: Middleware order matters - request logging → security headers → rate limiting - **Extensibility**: Subclasses implement `setup_routes()` to add custom endpoints and `validate_config()` for config validation @@ -29,7 +29,6 @@ Developers extend `TemplateServer` to create application-specific servers (see ` - **Hash Storage**: Only hash stored in `.env` (API_TOKEN_HASH), raw token shown once - **Token Loading**: `load_hashed_token()` loads hash from .env on server startup, stored in `TemplateServer.hashed_token` - **Verification Flow**: Request → `_verify_api_key()` dependency → `verify_token()` → hash comparison -- **Metrics**: Success/failure counters with labeled reasons (missing/invalid/error) - **Health Endpoint**: `/api/health` does NOT require authentication, reports unhealthy if token not configured - Header: `X-API-Key` (defined in `constants.API_KEY_HEADER_NAME`) @@ -42,8 +41,6 @@ Developers extend `TemplateServer` to create application-specific servers (see ` ### Observability Stack -- **Prometheus**: `/metrics` endpoint always exposed (no auth), custom auth/rate-limit metrics -- **Grafana**: Pre-configured dashboards in `grafana/dashboards/*.json` - **Logging**: Dual output (console + rotating file), 10MB per file, 5 backups in `logs/` - **Request Tracking**: `RequestLoggingMiddleware` logs all requests with client IP @@ -107,7 +104,6 @@ docker compose down # Stop and remove containers - **Prefix**: All routes under `/api` (API_PREFIX constant) - **Authentication**: Applied via `dependencies=[Security(self._verify_api_key)]` in route registration -- **Unauthenticated Endpoints**: `/health` and `/metrics` do not require authentication - **Response Models**: All endpoints return `BaseResponse` subclasses with code/message/timestamp - **Health Status**: `/health` includes `status` field (HEALTHY/DEGRADED/UNHEALTHY), reports unhealthy if no token configured @@ -155,15 +151,14 @@ All PRs must pass: ### Key Files -- `template_server.py` - Base TemplateServer class with middleware/metrics/auth setup +- `template_server.py` - Base TemplateServer class with middleware/auth setup - `main.py` - ExampleServer implementation showing how to extend TemplateServer - `authentication_handler.py` - Token generation, hashing, verification -- `prometheus_handler.py` - Prometheus metrics setup and custom metric definitions - `certificate_handler.py` - Self-signed SSL certificate generation and loading - `logging_setup.py` - Logging configuration (executed on import) - `models.py` - All Pydantic models (config + responses) - `constants.py` - Project constants, logging config -- `docker-compose.yml` - FastAPI + Prometheus + Grafana stack +- `docker-compose.yml` - Container stack ### Environment Variables diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 32232b4..0bba23b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -65,8 +65,6 @@ jobs: REQUIRED_DIRS=( "${SITE_PACKAGES}/${PACKAGE_NAME}" "${SITE_PACKAGES}/configuration" - "${SITE_PACKAGES}/grafana" - "${SITE_PACKAGES}/prometheus" ) for dir in "${REQUIRED_DIRS[@]}"; do @@ -91,13 +89,3 @@ jobs: exit 1 fi done - - # Show directory structure - echo "Package structure in site-packages:" - tree "${SITE_PACKAGES}/${PACKAGE_NAME}" --dirsfirst -F -L 2 - echo "Configuration structure:" - tree "${SITE_PACKAGES}/configuration" --dirsfirst -F - echo "Grafana structure:" - tree "${SITE_PACKAGES}/grafana" --dirsfirst -F -L 2 - echo "Prometheus structure:" - tree "${SITE_PACKAGES}/prometheus" --dirsfirst -F diff --git a/Dockerfile b/Dockerfile index 2ba81be..d150b29 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,8 +10,6 @@ COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ # Copy backend source files COPY python_template_server/ ./python_template_server/ COPY configuration/ ./configuration/ -COPY grafana/ ./grafana/ -COPY prometheus/ ./prometheus/ COPY pyproject.toml .here LICENSE README.md ./ # Build the wheel @@ -38,8 +36,6 @@ RUN mkdir -p /app/logs /app/certs # Copy included files from installed wheel to app directory RUN SITE_PACKAGES_DIR=$(find /usr/local/lib -name "site-packages" -type d | head -1) && \ cp -r "${SITE_PACKAGES_DIR}/configuration" /app/ && \ - cp -r "${SITE_PACKAGES_DIR}/grafana" /app/ && \ - cp -r "${SITE_PACKAGES_DIR}/prometheus" /app/ && \ cp "${SITE_PACKAGES_DIR}/.here" /app/.here && \ cp "${SITE_PACKAGES_DIR}/LICENSE" /app/LICENSE && \ cp "${SITE_PACKAGES_DIR}/README.md" /app/README.md @@ -48,15 +44,6 @@ RUN SITE_PACKAGES_DIR=$(find /usr/local/lib -name "site-packages" -type d | head RUN echo '#!/bin/sh\n\ set -e\n\ \n\ - # Copy monitoring configs to shared volume if they do not exist\n\ - if [ -d "/monitoring-configs" ]; then\n\ - echo "Setting up monitoring configurations..."\n\ - mkdir -p /monitoring-configs/prometheus /monitoring-configs/grafana\n\ - cp -r /app/prometheus/* /monitoring-configs/prometheus/ 2>/dev/null || true\n\ - cp -r /app/grafana/* /monitoring-configs/grafana/ 2>/dev/null || true\n\ - echo "Monitoring configurations ready"\n\ - fi\n\ - \n\ # Generate API token if needed\n\ if [ -z "$API_TOKEN_HASH" ]; then\n\ echo "Generating new token..."\n\ diff --git a/README.md b/README.md index a3d9318..ef40848 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,8 @@ # Python Template Server -A production-ready FastAPI server template with built-in authentication, rate limiting, security headers, and Prometheus metrics. This repository provides a solid foundation for building secure, observable FastAPI applications. +A production-ready FastAPI server template with built-in authentication, rate limiting and security headers. +This repository provides a solid foundation for building secure, observable FastAPI applications. ## Table of Contents @@ -30,7 +31,6 @@ A production-ready FastAPI server template with built-in authentication, rate li - **TemplateServer Base Class**: Reusable foundation - **FastAPI Framework**: Modern, fast, async-ready web framework -- **Observability Stack**: Pre-configured Prometheus + Grafana dashboards - **Docker Support**: Multi-stage builds with docker-compose orchestration - **Production Patterns**: Token generation, SSL certificate handling, health checks @@ -42,7 +42,6 @@ This project uses a **`TemplateServer` base class** that encapsulates cross-cutt - **Security Headers**: HSTS/CSP/X-Frame-Options automatically applied - **API Key Verification**: SHA-256 hashed tokens with secure validation - **Rate Limiting**: Configurable limits using `slowapi` (in-memory/Redis/Memcached) -- **Prometheus Metrics**: Custom authentication/rate-limit metrics + HTTP instrumentation **Application-specific servers** (like `ExampleServer` in `main.py`) extend `TemplateServer` to implement domain-specific endpoints and business logic. The base class handles all infrastructure concerns, letting you focus on your API functionality. @@ -94,7 +93,6 @@ uv run python-template-server # Server runs at https://localhost:443/api # Swagger UI: https://localhost:443/api/docs # Redoc: https://localhost:443/api/redoc -# Metrics: curl -k https://localhost:443/api/metrics # Health check: curl -k https://localhost:443/api/health # Login (requires authentication): curl -k -H "X-API-Key: your-token-here" https://localhost:443/api/login ``` @@ -115,7 +113,7 @@ See the [Software Maintenance Guide](./docs/SMG.md) for detailed setup instructi ## Docker Deployment ```sh -# Start all services (FastAPI + Prometheus + Grafana) +# Start all services docker compose up -d # View logs @@ -123,13 +121,11 @@ docker compose logs -f python-template-server # Access services: # - API: https://localhost:443/api -# - Prometheus: http://localhost:9090 -# - Grafana: http://localhost:3000 (admin/admin) ``` ## Documentation -- **[API Documentation](./docs/API.md)**: Endpoints, authentication, metrics +- **[API Documentation](./docs/API.md)**: API architecture and endpoints - **[Software Maintenance Guide](./docs/SMG.md)**: Development setup, configuration - **[Docker Deployment Guide](./docs/DOCKER_DEPLOYMENT.md)**: Container orchestration - **[Workflows](./docs/WORKFLOWS.md)**: CI/CD pipeline details diff --git a/docker-compose.yml b/docker-compose.yml index 5bba485..ebc7ddb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,6 @@ services: # Python Template Server python-template-server: - image: ${CYBER_QUERY_AI_IMAGE:-ghcr.io/javidahmed64592/cyber-query-ai:latest} build: context: . dockerfile: Dockerfile @@ -14,9 +13,6 @@ services: volumes: - certs:/app/certs - logs:/app/logs - - monitoring-configs:/monitoring-configs - networks: - - monitoring restart: unless-stopped healthcheck: test: @@ -31,58 +27,6 @@ services: retries: 3 start_period: 10s - # Prometheus for metrics collection - prometheus: - image: prom/prometheus:latest - container_name: prometheus - ports: - - "9090:9090" - volumes: - - monitoring-configs:/monitoring-configs:ro - - prometheus-data:/prometheus - command: - - "--config.file=/monitoring-configs/prometheus/prometheus.yml" - - "--storage.tsdb.path=/prometheus" - - "--web.console.libraries=/usr/share/prometheus/console_libraries" - - "--web.console.templates=/usr/share/prometheus/consoles" - - "--web.enable-lifecycle" - networks: - - monitoring - restart: unless-stopped - depends_on: - python-template-server: - condition: service_started - - # Grafana for metrics visualization - grafana: - image: grafana/grafana:latest - container_name: grafana - ports: - - "3000:3000" - environment: - - GF_SECURITY_ADMIN_USER=admin - - GF_SECURITY_ADMIN_PASSWORD=admin - - GF_INSTALL_PLUGINS= - - GF_PATHS_PROVISIONING=/monitoring-configs/grafana/provisioning - volumes: - - monitoring-configs:/monitoring-configs:ro - - grafana-data:/var/lib/grafana - networks: - - monitoring - restart: unless-stopped - depends_on: - python-template-server: - condition: service_started - prometheus: - condition: service_started - -networks: - monitoring: - driver: bridge - volumes: certs: logs: - prometheus-data: - grafana-data: - monitoring-configs: diff --git a/docs/API.md b/docs/API.md index 536592b..0130168 100644 --- a/docs/API.md +++ b/docs/API.md @@ -13,13 +13,6 @@ All endpoints are mounted under the `/api` prefix. - [Request Logging](#request-logging) - [Security Headers](#security-headers) - [Rate Limiting](#rate-limiting) -- [Prometheus Metrics](#prometheus-metrics) - - [GET /api/metrics](#get-apimetrics) - - [Standard HTTP Metrics (via `prometheus-fastapi-instrumentator`)](#standard-http-metrics-via-prometheus-fastapi-instrumentator) - - [Custom Application Metrics](#custom-application-metrics) - - [Accessing Dashboards](#accessing-dashboards) - - [Prometheus Dashboard](#prometheus-dashboard) - - [Grafana Dashboards](#grafana-dashboards) - [Endpoints](#endpoints) - [GET /api/health](#get-apihealth) - [GET /api/login](#get-apilogin) @@ -121,53 +114,6 @@ Default rate limit: **100 requests per minute** per IP address. Rate limits can be configured in `config.json`. -## Prometheus Metrics - -The server exposes Prometheus-compatible metrics for monitoring and observability. - -### GET /api/metrics - -- **Purpose**: Expose Prometheus metrics for scraping and monitoring. -- **Format**: Prometheus text-based exposition format. - -**Metrics Exposed**: - -#### Standard HTTP Metrics (via `prometheus-fastapi-instrumentator`) -- `http_requests_total`: Total number of HTTP requests by method, path, and status code -- `http_request_duration_seconds`: HTTP request latency histogram by method and path -- `http_requests_in_progress`: Number of HTTP requests currently being processed - -#### Custom Application Metrics - -**Authentication Metrics**: -- `auth_success_total`: Counter tracking successful API key validations -- `auth_failure_total{reason}`: Counter tracking failed authentication attempts with labels: - - `reason="missing"`: No API key provided in request - - `reason="invalid"`: Invalid or incorrect API key - - `reason="error"`: Error during token verification - -**Rate Limiting Metrics**: -- `rate_limit_exceeded_total{endpoint}`: Counter tracking requests that exceeded rate limits, labeled by endpoint path - -### Accessing Dashboards - -The application includes pre-configured monitoring dashboards for visualization: - -#### Prometheus Dashboard -- **URL**: http://localhost:9090 -- **Purpose**: Query and visualize raw metrics data -- **Features**: Built-in query interface, graphing, and alerting - -#### Grafana Dashboards -- **URL**: http://localhost:3000 -- **Credentials**: admin / admin (change after first login) -- **Pre-configured Dashboards**: - - **Authentication Metrics**: Tracks successful and failed authentication attempts, including reasons for failures - - **Rate Limiting Metrics**: Monitors requests that exceed rate limits by endpoint - -To access the dashboards, the containers for Grafana and Prometheus must be running. -See the [Docker documentation](./DOCKER_DEPLOYMENT.md) for information on how to run these. - ## Endpoints ### GET /api/health diff --git a/docs/DOCKER_DEPLOYMENT.md b/docs/DOCKER_DEPLOYMENT.md index 12fb3e7..c5d0eb4 100644 --- a/docs/DOCKER_DEPLOYMENT.md +++ b/docs/DOCKER_DEPLOYMENT.md @@ -1,7 +1,7 @@ # Docker Deployment Guide -This guide provides comprehensive instructions for deploying the Python Template Server using Docker and Docker Compose, including metrics visualization with Prometheus and Grafana. +This guide provides comprehensive instructions for deploying the Python Template Server using Docker and Docker Compose. ## Table of Contents @@ -19,14 +19,6 @@ This guide provides comprehensive instructions for deploying the Python Template - [Managing Containers](#managing-containers) - [Accessing Services](#accessing-services) - [Python Template Server](#python-template-server) - - [Prometheus](#prometheus) - - [Grafana](#grafana) -- [Metrics Visualization](#metrics-visualization) - - [Available Metrics](#available-metrics) - - [Authentication Metrics](#authentication-metrics) - - [Rate Limiting Metrics](#rate-limiting-metrics) - - [HTTP Metrics (provided by prometheus-fastapi-instrumentator)](#http-metrics-provided-by-prometheus-fastapi-instrumentator) - - [Custom Dashboard Setup](#custom-dashboard-setup) - [View Container Logs](#view-container-logs) ## Prerequisites @@ -68,7 +60,7 @@ This will: ### 2. Start Services ```bash -# Start all services (FastAPI server, Prometheus, Grafana) +# Start all services docker compose up -d # View logs @@ -95,24 +87,12 @@ The Docker startup script automatically handles token generation with the follow ### Docker Compose Services -The `docker-compose.yml` defines three services: +The `docker-compose.yml` defines the following services: 1. **python-template-server** (Port 443) - FastAPI application with HTTPS - Auto-generates self-signed certificates on first run (if not present) - Uses existing `.env` file if available, otherwise generates a new token on startup - - Exposes `/api/metrics` endpoint for Prometheus - -2. **prometheus** (Port 9090) - - Metrics collection and storage - - Scrapes `/api/metrics` endpoint every 15 seconds - - Persistent storage via Docker volume - -3. **grafana** (Port 3000) - - Metrics visualization dashboards - - Pre-configured Prometheus datasource - - Custom dashboards for authentication and rate limiting - - Default credentials: `admin/admin` ### Environment Variables @@ -174,7 +154,6 @@ docker compose down -v **Base URL**: `https://localhost:443` **API Endpoints**: -- Metrics: `GET /api/metrics` (publicly accessible, no authentication required) - Health Check: `GET /api/health` (publicly accessible, no authentication required) - Login: `GET /api/login` (requires authentication with X-API-Key header) - Custom Endpoints: Defined in your server subclass (authentication may be required) @@ -188,68 +167,8 @@ curl -k https://localhost:443/api/health curl -k -H "X-API-Key: your-token-here" https://localhost:443/api/login ``` -### Prometheus - -**URL**: `http://localhost:9090` - -**Features**: -- Query metrics directly -- View scrape targets and status -- Create custom queries - -### Grafana - -**URL**: `http://localhost:3000` - -**Default Credentials**: -- Username: `admin` -- Password: `admin` (change on first login) - -**Pre-installed Dashboards**: -1. **Authentication Metrics** (`/d/auth-metrics`) - - Success/failure rates - - Total authentication attempts - - Failure reasons breakdown - - Success rate percentage - -2. **Rate Limiting & Performance** (`/d/rate-limit-metrics`) - - Rate limit violations by endpoint - - HTTP request rates - - Request duration percentiles - - Total violations gauge - -## Metrics Visualization - -### Available Metrics - -#### Authentication Metrics -- `auth_success_total`: Successful authentication attempts -- `auth_failure_total{reason}`: Failed attempts by reason (missing, invalid, error) - -#### Rate Limiting Metrics -- `rate_limit_exceeded_total{endpoint}`: Rate limit violations per endpoint - -#### HTTP Metrics (provided by prometheus-fastapi-instrumentator) -- `http_requests_total`: Total HTTP requests -- `http_request_duration_seconds`: Request latency histogram -- `http_requests_in_progress`: Current in-flight requests - -### Custom Dashboard Setup - -1. **Access Grafana**: Navigate to `http://localhost:3000` -2. **Login**: Use `admin/admin` -3. **Navigate**: Go to Dashboards → Browse → Python Template Server folder -4. **View**: Select either dashboard to visualize metrics - ### View Container Logs ```bash -# All services -docker compose logs -f - -# Specific service docker compose logs -f python-template-server - -# Last 100 lines -docker compose logs --tail=100 prometheus ``` diff --git a/docs/SMG.md b/docs/SMG.md index 22f6eb4..f00c73e 100644 --- a/docs/SMG.md +++ b/docs/SMG.md @@ -35,7 +35,6 @@ python_template_server/ ├── logging_setup.py # Logging configuration ├── main.py # Application entry point with ExampleServer ├── models.py # Pydantic models (config + API responses) -├── prometheus_handler.py # Prometheus metrics handler └── template_server.py # TemplateServer base class (reusable foundation) ``` @@ -47,7 +46,6 @@ The Python Template Server uses a **`TemplateServer` base class** that provides - **Middleware Setup**: Request logging and security headers - **Authentication**: API key verification with SHA-256 hashing - **Rate Limiting**: Configurable request throttling per endpoint -- **Metrics**: Prometheus instrumentation for observability - **Configuration**: JSON-based config loading and validation **Application-Specific Servers** (like `ExampleServer` in `main.py`) extend `TemplateServer` to: @@ -55,7 +53,7 @@ The Python Template Server uses a **`TemplateServer` base class** that provides - Implement domain-specific business logic - Validate custom configuration models via `validate_config()` -This separation ensures that cross-cutting concerns (security, logging, metrics) are handled by the base class, while application developers focus on building their API functionality. +This separation ensures that cross-cutting concerns (security, logging etc.) are handled by the base class, while application developers focus on building their API functionality. ### Installing Dependencies @@ -127,15 +125,11 @@ uv run python-template-server The backend will be available at `https://localhost:443/api` by default. **Available Endpoints:** -- Prometheus Metrics: `https://localhost:443/api/metrics` - Health Check: `https://localhost:443/api/health` - Login: `https://localhost:443/api/login` (requires authentication) **Testing the API:** ```sh -# Metrics endpoint (no auth required) -curl -k https://localhost:443/api/metrics - # Health check (no auth required) curl -k https://localhost:443/api/health diff --git a/docs/WORKFLOWS.md b/docs/WORKFLOWS.md index 0cd61e3..4dce120 100644 --- a/docs/WORKFLOWS.md +++ b/docs/WORKFLOWS.md @@ -65,11 +65,7 @@ It consists of the following jobs: - Setup Python environment (via custom action) - Download wheel artifact - Install wheel using `uv pip install` - - Verify installed package structure in site-packages: - - `python_template_server/` - Python package - - `configuration/` - Server configuration - - `grafana/` - Grafana dashboards and provisioning - - `prometheus/` - Prometheus configuration + - Verify installed package structure in site-packages - Display directory structure with tree views for verification ## Docker Workflow @@ -84,7 +80,4 @@ It consists of the following jobs: - Wait for services to start (5 seconds) - Show server logs from `python-template-server` container - **Health check** using reusable composite action `.github/actions/docker-check-containers`: - - Verifies server is running on port 443 - - Checks Prometheus and Grafana services - - Validates Ollama integration - Stop services with full cleanup: `docker compose down --volumes --remove-orphans` diff --git a/grafana/README.md b/grafana/README.md deleted file mode 100644 index 3b13fdb..0000000 --- a/grafana/README.md +++ /dev/null @@ -1,135 +0,0 @@ - -# Grafana Configuration - -This directory contains Grafana provisioning configuration and custom dashboards for the Python Template Server. - - -## Table of Contents -- [Directory Structure](#directory-structure) -- [Dashboards](#dashboards) - - [1. Health Metrics Dashboard](#1-health-metrics-dashboard) - - [2. Authentication Metrics Dashboard](#2-authentication-metrics-dashboard) - - [3. Rate Limiting \& Performance Metrics Dashboard](#3-rate-limiting--performance-metrics-dashboard) -- [Accessing Dashboards](#accessing-dashboards) -- [Customizing Dashboards](#customizing-dashboards) - - [Adding New Panels](#adding-new-panels) - - [Creating New Dashboards](#creating-new-dashboards) -- [Available Metrics](#available-metrics) - - [Health Metrics](#health-metrics) - - [Authentication Metrics](#authentication-metrics) - - [Rate Limiting Metrics](#rate-limiting-metrics) - - [HTTP Metrics (from prometheus-fastapi-instrumentator)](#http-metrics-from-prometheus-fastapi-instrumentator) - - -## Directory Structure - -``` -grafana/ -├── provisioning/ -│ ├── datasources/ -│ │ └── prometheus.yml # Prometheus datasource configuration -│ └── dashboards/ -│ └── dashboards.yml # Dashboard provisioning configuration -└── dashboards/ - ├── authentication-metrics.json # Authentication monitoring dashboard - ├── health-metrics.json # Health monitoring dashboard - └── rate-limiting-metrics.json # Rate limiting & performance dashboard -``` - -## Dashboards - -### 1. Health Metrics Dashboard - -**UID**: `health-metrics` -**Path**: `/d/health-metrics` - -**Panels**: -- **API Token Configuration Status**: Gauge showing if API token is configured -- **Health Checks (Last 5 Minutes)**: Gauge of recent health check requests -- **Health Check Average Response Time**: Gauge of average response time for health checks -- **Token Configuration Status Over Time**: Timeseries of token configuration status -- **Health Check Request Rate (per second)**: Timeseries of health check request rates -- **Health Check Response Time Percentiles**: Timeseries of p50, p95, p99 response times - -**Use Cases**: -- Monitor server health and configuration status -- Track health check performance and frequency -- Detect configuration issues (e.g., missing API token) - -### 2. Authentication Metrics Dashboard - -**UID**: `auth-metrics` -**Path**: `/d/auth-metrics` - -**Panels**: -- **Authentication Rate**: Success and failure rates per second -- **Total Successful Authentications**: Gauge showing cumulative successes -- **Total Failed Authentications**: Gauge showing cumulative failures -- **Authentication Failures by Reason**: Breakdown of failures (missing, invalid, error) -- **Authentication Success Rate**: Percentage of successful attempts - -**Use Cases**: -- Monitor authentication health -- Detect brute force attacks (high failure rates) -- Identify common authentication issues - -### 3. Rate Limiting & Performance Metrics Dashboard - -**UID**: `rate-limit-metrics` -**Path**: `/d/rate-limit-metrics` - -**Panels**: -- **Rate Limit Exceeded Events**: Rate of violations per second by endpoint -- **Total Rate Limit Violations**: Cumulative violation count -- **Rate Limit Violations by Endpoint**: Breakdown by endpoint -- **HTTP Request Rate**: Overall request rates by method, handler, and status -- **HTTP Request Duration**: 95th and 99th percentile latency - -**Use Cases**: -- Monitor rate limit effectiveness -- Identify endpoints being abused -- Track API performance and latency -- Capacity planning - -## Accessing Dashboards - -1. Generate API key (if not done): `uv run generate-new-token` -2. Start services: `docker compose up -d` -3. Open Grafana: http://localhost:3000 -4. Login with default credentials: `admin/admin` -5. Navigate to: Dashboards → Browse → Python Template Server folder - -## Customizing Dashboards - -### Adding New Panels - -1. Open a dashboard in Grafana -2. Click "Add panel" → "Add a new panel" -3. Configure visualization and query -4. Save the dashboard -5. Export JSON: Dashboard settings → JSON Model -6. Save to this directory for version control - -### Creating New Dashboards - -1. Create dashboard in Grafana UI -2. Export as JSON: Dashboard settings → JSON Model → Copy to clipboard -3. Save JSON file in `grafana/dashboards/` -4. Restart Grafana: `docker compose restart grafana` - -## Available Metrics - -### Health Metrics -- `token_configured` - Binary metric indicating if API token is configured (0 or 1) - -### Authentication Metrics -- `auth_success_total` - Successful authentication count -- `auth_failure_total{reason="missing|invalid|error"}` - Failed authentication count by reason - -### Rate Limiting Metrics -- `rate_limit_exceeded_total{endpoint="/api/health"}` - Rate limit violations per endpoint - -### HTTP Metrics (from prometheus-fastapi-instrumentator) -- `http_requests_total{method, handler, status}` - Total requests -- `http_request_duration_seconds_bucket` - Request duration histogram -- `http_requests_in_progress` - Current active requests diff --git a/grafana/dashboards/authentication-metrics.json b/grafana/dashboards/authentication-metrics.json deleted file mode 100644 index fce7811..0000000 --- a/grafana/dashboards/authentication-metrics.json +++ /dev/null @@ -1,373 +0,0 @@ -{ - "annotations": { - "list": [ - { - "builtIn": 1, - "datasource": "-- Grafana --", - "enable": true, - "hide": true, - "iconColor": "rgba(0, 211, 255, 1)", - "name": "Annotations & Alerts", - "type": "dashboard" - } - ] - }, - "editable": true, - "gnetId": null, - "graphTooltip": 0, - "id": null, - "links": [], - "panels": [ - { - "datasource": "Prometheus", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 10, - "gradientMode": "none", - "hideFrom": { - "tooltip": false, - "viz": false, - "legend": false - }, - "lineInterpolation": "linear", - "lineWidth": 2, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "never", - "spanNulls": true - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - } - ] - }, - "unit": "short" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 0 - }, - "id": 2, - "options": { - "legend": { - "calcs": ["last"], - "displayMode": "table", - "placement": "bottom" - }, - "tooltip": { - "mode": "single" - } - }, - "pluginVersion": "8.0.0", - "targets": [ - { - "expr": "rate(auth_success_total[5m])", - "interval": "", - "legendFormat": "Success Rate", - "refId": "A" - }, - { - "expr": "rate(auth_failure_total[5m])", - "interval": "", - "legendFormat": "Failure Rate ({{reason}})", - "refId": "B" - } - ], - "title": "Authentication Rate (per second)", - "type": "timeseries" - }, - { - "datasource": "Prometheus", - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "short" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 6, - "x": 12, - "y": 0 - }, - "id": 3, - "options": { - "orientation": "auto", - "reduceOptions": { - "values": false, - "calcs": ["lastNotNull"], - "fields": "" - }, - "showThresholdLabels": false, - "showThresholdMarkers": true, - "text": {} - }, - "pluginVersion": "8.0.0", - "targets": [ - { - "expr": "auth_success_total", - "interval": "", - "legendFormat": "Total Successes", - "refId": "A" - } - ], - "title": "Total Successful Authentications", - "type": "gauge" - }, - { - "datasource": "Prometheus", - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "yellow", - "value": 10 - }, - { - "color": "red", - "value": 50 - } - ] - }, - "unit": "short" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 6, - "x": 18, - "y": 0 - }, - "id": 4, - "options": { - "orientation": "auto", - "reduceOptions": { - "values": false, - "calcs": ["lastNotNull"], - "fields": "" - }, - "showThresholdLabels": false, - "showThresholdMarkers": true, - "text": {} - }, - "pluginVersion": "8.0.0", - "targets": [ - { - "expr": "sum(auth_failure_total)", - "interval": "", - "legendFormat": "Total Failures", - "refId": "A" - } - ], - "title": "Total Failed Authentications", - "type": "gauge" - }, - { - "datasource": "Prometheus", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "bars", - "fillOpacity": 100, - "gradientMode": "none", - "hideFrom": { - "tooltip": false, - "viz": false, - "legend": false - }, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "never", - "spanNulls": true - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - } - ] - }, - "unit": "short" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 24, - "x": 0, - "y": 8 - }, - "id": 5, - "options": { - "legend": { - "calcs": ["sum"], - "displayMode": "table", - "placement": "bottom" - }, - "tooltip": { - "mode": "single" - } - }, - "pluginVersion": "8.0.0", - "targets": [ - { - "expr": "auth_failure_total", - "interval": "", - "legendFormat": "{{reason}}", - "refId": "A" - } - ], - "title": "Authentication Failures by Reason", - "type": "timeseries" - }, - { - "datasource": "Prometheus", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 10, - "gradientMode": "none", - "hideFrom": { - "tooltip": false, - "viz": false, - "legend": false - }, - "lineInterpolation": "linear", - "lineWidth": 2, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "never", - "spanNulls": true - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - } - ] - }, - "unit": "percentunit" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 24, - "x": 0, - "y": 16 - }, - "id": 6, - "options": { - "legend": { - "calcs": ["mean", "last"], - "displayMode": "table", - "placement": "bottom" - }, - "tooltip": { - "mode": "single" - } - }, - "pluginVersion": "8.0.0", - "targets": [ - { - "expr": "rate(auth_success_total[5m]) / (rate(auth_success_total[5m]) + rate(auth_failure_total[5m]))", - "interval": "", - "legendFormat": "Success Rate", - "refId": "A" - } - ], - "title": "Authentication Success Rate (Percentage)", - "type": "timeseries" - } - ], - "refresh": "5s", - "schemaVersion": 27, - "style": "dark", - "tags": ["python-template-server", "authentication", "security"], - "templating": { - "list": [] - }, - "time": { - "from": "now-15m", - "to": "now" - }, - "timepicker": { - "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h"] - }, - "timezone": "", - "title": "Authentication Metrics", - "uid": "auth-metrics", - "version": 1 -} diff --git a/grafana/dashboards/health-metrics.json b/grafana/dashboards/health-metrics.json deleted file mode 100644 index 132a1c9..0000000 --- a/grafana/dashboards/health-metrics.json +++ /dev/null @@ -1,485 +0,0 @@ -{ - "annotations": { - "list": [ - { - "builtIn": 1, - "datasource": "-- Grafana --", - "enable": true, - "hide": true, - "iconColor": "rgba(0, 211, 255, 1)", - "name": "Annotations & Alerts", - "type": "dashboard" - } - ] - }, - "editable": true, - "gnetId": null, - "graphTooltip": 0, - "id": null, - "links": [], - "panels": [ - { - "datasource": "Prometheus", - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "mappings": [ - { - "options": { - "0": { - "color": "red", - "text": "NOT CONFIGURED" - }, - "1": { - "color": "green", - "text": "CONFIGURED" - } - }, - "type": "value" - } - ], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "red", - "value": null - }, - { - "color": "green", - "value": 1 - } - ] - }, - "unit": "short" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 8, - "x": 0, - "y": 0 - }, - "id": 2, - "options": { - "orientation": "auto", - "reduceOptions": { - "values": false, - "calcs": ["lastNotNull"], - "fields": "" - }, - "showThresholdLabels": false, - "showThresholdMarkers": false, - "text": { - "titleSize": 20, - "valueSize": 60 - } - }, - "pluginVersion": "8.0.0", - "targets": [ - { - "expr": "token_configured", - "interval": "", - "legendFormat": "Token Status", - "refId": "A" - } - ], - "title": "API Token Configuration Status", - "type": "gauge" - }, - { - "datasource": "Prometheus", - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "red", - "value": null - }, - { - "color": "yellow", - "value": 200 - }, - { - "color": "green", - "value": 500 - } - ] - }, - "unit": "short" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 8, - "x": 8, - "y": 0 - }, - "id": 3, - "options": { - "orientation": "auto", - "reduceOptions": { - "values": false, - "calcs": ["lastNotNull"], - "fields": "" - }, - "showThresholdLabels": false, - "showThresholdMarkers": true, - "text": {} - }, - "pluginVersion": "8.0.0", - "targets": [ - { - "expr": "sum(increase(http_requests_total{handler=\"/health\",method=\"GET\"}[5m]))", - "interval": "", - "legendFormat": "Health Checks", - "refId": "A" - } - ], - "title": "Health Checks (Last 5 Minutes)", - "type": "gauge" - }, - { - "datasource": "Prometheus", - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "yellow", - "value": 100 - }, - { - "color": "red", - "value": 500 - } - ] - }, - "unit": "ms" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 8, - "x": 16, - "y": 0 - }, - "id": 4, - "options": { - "orientation": "auto", - "reduceOptions": { - "values": false, - "calcs": ["mean"], - "fields": "" - }, - "showThresholdLabels": false, - "showThresholdMarkers": true, - "text": {} - }, - "pluginVersion": "8.0.0", - "targets": [ - { - "expr": "avg(rate(http_request_duration_seconds_sum{handler=\"/health\",method=\"GET\"}[5m]) / rate(http_request_duration_seconds_count{handler=\"/health\",method=\"GET\"}[5m])) * 1000", - "interval": "", - "legendFormat": "Avg Response Time", - "refId": "A" - } - ], - "title": "Health Check Average Response Time", - "type": "gauge" - }, - { - "datasource": "Prometheus", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 10, - "gradientMode": "none", - "hideFrom": { - "tooltip": false, - "viz": false, - "legend": false - }, - "lineInterpolation": "linear", - "lineWidth": 2, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "never", - "spanNulls": true, - "stacking": { - "group": "A", - "mode": "none" - } - }, - "mappings": [ - { - "options": { - "0": { - "color": "red", - "text": "NOT CONFIGURED" - }, - "1": { - "color": "green", - "text": "CONFIGURED" - } - }, - "type": "value" - } - ], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "red", - "value": null - }, - { - "color": "green", - "value": 1 - } - ] - }, - "unit": "short" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 24, - "x": 0, - "y": 8 - }, - "id": 5, - "options": { - "legend": { - "calcs": ["last", "min", "max"], - "displayMode": "table", - "placement": "bottom" - }, - "tooltip": { - "mode": "single" - } - }, - "pluginVersion": "8.0.0", - "targets": [ - { - "expr": "token_configured", - "interval": "", - "legendFormat": "Token Configuration Status", - "refId": "A" - } - ], - "title": "Token Configuration Status Over Time", - "type": "timeseries" - }, - { - "datasource": "Prometheus", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 10, - "gradientMode": "none", - "hideFrom": { - "tooltip": false, - "viz": false, - "legend": false - }, - "lineInterpolation": "linear", - "lineWidth": 2, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "never", - "spanNulls": true - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - } - ] - }, - "unit": "short" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 16 - }, - "id": 6, - "options": { - "legend": { - "calcs": ["mean", "last"], - "displayMode": "table", - "placement": "bottom" - }, - "tooltip": { - "mode": "single" - } - }, - "pluginVersion": "8.0.0", - "targets": [ - { - "expr": "rate(http_requests_total{handler=\"/health\",method=\"GET\"}[5m])", - "interval": "", - "legendFormat": "Health Check Rate", - "refId": "A" - } - ], - "title": "Health Check Request Rate (per second)", - "type": "timeseries" - }, - { - "datasource": "Prometheus", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 10, - "gradientMode": "none", - "hideFrom": { - "tooltip": false, - "viz": false, - "legend": false - }, - "lineInterpolation": "linear", - "lineWidth": 2, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "never", - "spanNulls": true - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "yellow", - "value": 100 - }, - { - "color": "red", - "value": 500 - } - ] - }, - "unit": "ms" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 12, - "y": 16 - }, - "id": 7, - "options": { - "legend": { - "calcs": ["mean", "last", "max"], - "displayMode": "table", - "placement": "bottom" - }, - "tooltip": { - "mode": "single" - } - }, - "pluginVersion": "8.0.0", - "targets": [ - { - "expr": "histogram_quantile(0.50, sum(rate(http_request_duration_seconds_bucket{handler=\"/health\",method=\"GET\"}[5m])) by (le)) * 1000", - "interval": "", - "legendFormat": "p50", - "refId": "A" - }, - { - "expr": "histogram_quantile(0.95, sum(rate(http_request_duration_seconds_bucket{handler=\"/health\",method=\"GET\"}[5m])) by (le)) * 1000", - "interval": "", - "legendFormat": "p95", - "refId": "B" - }, - { - "expr": "histogram_quantile(0.99, sum(rate(http_request_duration_seconds_bucket{handler=\"/health\",method=\"GET\"}[5m])) by (le)) * 1000", - "interval": "", - "legendFormat": "p99", - "refId": "C" - } - ], - "title": "Health Check Response Time Percentiles", - "type": "timeseries" - } - ], - "refresh": "5s", - "schemaVersion": 27, - "style": "dark", - "tags": ["python-template-server", "health", "monitoring"], - "templating": { - "list": [] - }, - "time": { - "from": "now-15m", - "to": "now" - }, - "timepicker": { - "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h"] - }, - "timezone": "", - "title": "Health Metrics", - "uid": "health-metrics", - "version": 1 -} diff --git a/grafana/dashboards/rate-limiting-metrics.json b/grafana/dashboards/rate-limiting-metrics.json deleted file mode 100644 index ab36014..0000000 --- a/grafana/dashboards/rate-limiting-metrics.json +++ /dev/null @@ -1,393 +0,0 @@ -{ - "annotations": { - "list": [ - { - "builtIn": 1, - "datasource": "-- Grafana --", - "enable": true, - "hide": true, - "iconColor": "rgba(0, 211, 255, 1)", - "name": "Annotations & Alerts", - "type": "dashboard" - } - ] - }, - "editable": true, - "gnetId": null, - "graphTooltip": 0, - "id": null, - "links": [], - "panels": [ - { - "datasource": "Prometheus", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 10, - "gradientMode": "none", - "hideFrom": { - "tooltip": false, - "viz": false, - "legend": false - }, - "lineInterpolation": "linear", - "lineWidth": 2, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "never", - "spanNulls": true - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 0.5 - } - ] - }, - "unit": "short" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 24, - "x": 0, - "y": 0 - }, - "id": 2, - "options": { - "legend": { - "calcs": ["last", "sum"], - "displayMode": "table", - "placement": "bottom" - }, - "tooltip": { - "mode": "single" - } - }, - "pluginVersion": "8.0.0", - "targets": [ - { - "expr": "rate(rate_limit_exceeded_total[5m])", - "interval": "", - "legendFormat": "{{endpoint}}", - "refId": "A" - } - ], - "title": "Rate Limit Exceeded Events (per second)", - "type": "timeseries" - }, - { - "datasource": "Prometheus", - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "yellow", - "value": 50 - }, - { - "color": "red", - "value": 100 - } - ] - }, - "unit": "short" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 8 - }, - "id": 3, - "options": { - "orientation": "auto", - "reduceOptions": { - "values": false, - "calcs": ["lastNotNull"], - "fields": "" - }, - "showThresholdLabels": false, - "showThresholdMarkers": true, - "text": {} - }, - "pluginVersion": "8.0.0", - "targets": [ - { - "expr": "sum(rate_limit_exceeded_total)", - "interval": "", - "legendFormat": "Total Rate Limit Violations", - "refId": "A" - } - ], - "title": "Total Rate Limit Violations", - "type": "gauge" - }, - { - "datasource": "Prometheus", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "bars", - "fillOpacity": 100, - "gradientMode": "none", - "hideFrom": { - "tooltip": false, - "viz": false, - "legend": false - }, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "never", - "spanNulls": true - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - } - ] - }, - "unit": "short" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 12, - "y": 8 - }, - "id": 4, - "options": { - "legend": { - "calcs": ["sum"], - "displayMode": "table", - "placement": "bottom" - }, - "tooltip": { - "mode": "single" - } - }, - "pluginVersion": "8.0.0", - "targets": [ - { - "expr": "rate_limit_exceeded_total", - "interval": "", - "legendFormat": "{{endpoint}}", - "refId": "A" - } - ], - "title": "Rate Limit Violations by Endpoint", - "type": "timeseries" - }, - { - "datasource": "Prometheus", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 10, - "gradientMode": "none", - "hideFrom": { - "tooltip": false, - "viz": false, - "legend": false - }, - "lineInterpolation": "linear", - "lineWidth": 2, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "never", - "spanNulls": true - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - } - ] - }, - "unit": "short" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 24, - "x": 0, - "y": 16 - }, - "id": 5, - "options": { - "legend": { - "calcs": ["mean", "last"], - "displayMode": "table", - "placement": "bottom" - }, - "tooltip": { - "mode": "single" - } - }, - "pluginVersion": "8.0.0", - "targets": [ - { - "expr": "rate(http_requests_total[5m])", - "interval": "", - "legendFormat": "{{method}} {{handler}} ({{status}})", - "refId": "A" - } - ], - "title": "HTTP Request Rate (per second)", - "type": "timeseries" - }, - { - "datasource": "Prometheus", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 10, - "gradientMode": "none", - "hideFrom": { - "tooltip": false, - "viz": false, - "legend": false - }, - "lineInterpolation": "linear", - "lineWidth": 2, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "never", - "spanNulls": true - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - } - ] - }, - "unit": "s" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 24, - "x": 0, - "y": 24 - }, - "id": 6, - "options": { - "legend": { - "calcs": ["mean", "max"], - "displayMode": "table", - "placement": "bottom" - }, - "tooltip": { - "mode": "single" - } - }, - "pluginVersion": "8.0.0", - "targets": [ - { - "expr": "histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m]))", - "interval": "", - "legendFormat": "95th percentile - {{handler}}", - "refId": "A" - }, - { - "expr": "histogram_quantile(0.99, rate(http_request_duration_seconds_bucket[5m]))", - "interval": "", - "legendFormat": "99th percentile - {{handler}}", - "refId": "B" - } - ], - "title": "HTTP Request Duration (95th and 99th Percentile)", - "type": "timeseries" - } - ], - "refresh": "5s", - "schemaVersion": 27, - "style": "dark", - "tags": ["python-template-server", "rate-limiting", "performance"], - "templating": { - "list": [] - }, - "time": { - "from": "now-15m", - "to": "now" - }, - "timepicker": { - "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h"] - }, - "timezone": "", - "title": "Rate Limiting & Performance Metrics", - "uid": "rate-limit-metrics", - "version": 1 -} diff --git a/grafana/provisioning/dashboards/dashboards.yml b/grafana/provisioning/dashboards/dashboards.yml deleted file mode 100644 index 23b83dc..0000000 --- a/grafana/provisioning/dashboards/dashboards.yml +++ /dev/null @@ -1,13 +0,0 @@ -# Grafana Dashboard Provisioning Configuration -apiVersion: 1 - -providers: - - name: 'Python Template Server Dashboards' - orgId: 1 - folder: 'Python Template Server' - type: file - disableDeletion: false - updateIntervalSeconds: 10 - allowUiUpdates: true - options: - path: /var/lib/grafana/dashboards diff --git a/grafana/provisioning/datasources/prometheus-datasource.yml b/grafana/provisioning/datasources/prometheus-datasource.yml deleted file mode 100644 index 0f326b4..0000000 --- a/grafana/provisioning/datasources/prometheus-datasource.yml +++ /dev/null @@ -1,14 +0,0 @@ -# Grafana Datasource Configuration -apiVersion: 1 - -datasources: - - name: Prometheus - type: prometheus - access: proxy - url: http://prometheus:9090 - isDefault: true - editable: true - jsonData: - timeInterval: 15s - queryTimeout: 60s - httpMethod: POST diff --git a/prometheus/prometheus.yml b/prometheus/prometheus.yml deleted file mode 100644 index 8e36acc..0000000 --- a/prometheus/prometheus.yml +++ /dev/null @@ -1,26 +0,0 @@ -# Prometheus Configuration for Python Template Server -global: - scrape_interval: 15s - evaluation_interval: 15s - scrape_timeout: 10s - -# Scrape configurations -scrape_configs: - - job_name: 'python-template-server' - scheme: https - tls_config: - insecure_skip_verify: true - static_configs: - - targets: ['python-template-server:443'] - labels: - service: 'python-template-server-storage' - environment: 'prod' - - metrics_path: '/api/metrics' - - # Prometheus self-monitoring - - job_name: 'prometheus' - static_configs: - - targets: ['localhost:9090'] - labels: - service: 'prometheus' diff --git a/pyproject.toml b/pyproject.toml index ae38ca7..f840383 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "hatchling.build" [project] name = "python-template-server" version = "0.1.0" -description = "A template FastAPI server with authentication, rate limiting and Prometheus metrics." +description = "A template FastAPI server with production-ready configuration." readme = "README.md" requires-python = ">=3.13" license = { file = "LICENSE" } @@ -21,7 +21,6 @@ dependencies = [ "cryptography>=46.0.3", "fastapi>=0.125.0", "httpx>=0.28.1", - "prometheus-fastapi-instrumentator>=7.1.0", "pydantic>=2.12.4", "pyhere>=1.0.3", "python-dotenv>=1.2.1", @@ -57,8 +56,6 @@ allow-direct-references = true include = [ "python_template_server/**", "configuration/**", - "grafana/**", - "prometheus/**", ".here", "LICENSE", "README.md", diff --git a/python_template_server/models.py b/python_template_server/models.py index 26ee43d..e165dfe 100644 --- a/python_template_server/models.py +++ b/python_template_server/models.py @@ -7,9 +7,7 @@ from typing import Any from fastapi.responses import JSONResponse -from prometheus_client import Counter, Gauge from pydantic import BaseModel, Field -from pydantic.dataclasses import dataclass # Template Server Configuration Models @@ -94,42 +92,6 @@ def save_to_file(self, filepath: Path) -> None: config_file.write("\n") -# Prometheus Metric Models -class BaseMetricNames(StrEnum): - """Base metric names.""" - - TOKEN_CONFIGURED = "token_configured" # noqa: S105 - AUTH_SUCCESS_TOTAL = "auth_success_total" - AUTH_FAILURE_TOTAL = "auth_failure_total" - RATE_LIMIT_EXCEEDED_TOTAL = "rate_limit_exceeded_total" - - -class MetricTypes(StrEnum): - """Metric types.""" - - COUNTER = auto() - GAUGE = auto() - - @property - def prometheus_class(self) -> type: - """Get the corresponding Prometheus metric class.""" - match self: - case MetricTypes.COUNTER: - return Counter - case MetricTypes.GAUGE: - return Gauge - - -@dataclass -class MetricConfig: - """Metric configuration.""" - - name: BaseMetricNames - metric_type: MetricTypes - description: str - labels: list[str] | None = None - - # API Response Models class CustomJSONResponse(JSONResponse): """Custom JSONResponse with configurable rendering options.""" diff --git a/python_template_server/prometheus_handler.py b/python_template_server/prometheus_handler.py deleted file mode 100644 index 396a9a7..0000000 --- a/python_template_server/prometheus_handler.py +++ /dev/null @@ -1,92 +0,0 @@ -"""Prometheus metrics handler.""" - -from fastapi import FastAPI -from prometheus_client import Counter, Gauge -from prometheus_fastapi_instrumentator import Instrumentator - -from python_template_server.models import BaseMetricNames, MetricConfig, MetricTypes - -BASE_METRICS_CONFIG = [ - MetricConfig( - name=BaseMetricNames.TOKEN_CONFIGURED, - metric_type=MetricTypes.GAUGE, - description="Whether API token is properly configured (1=configured, 0=not configured)", - ), - MetricConfig( - name=BaseMetricNames.AUTH_SUCCESS_TOTAL, - metric_type=MetricTypes.COUNTER, - description="Total number of successful authentication attempts", - ), - MetricConfig( - name=BaseMetricNames.AUTH_FAILURE_TOTAL, - metric_type=MetricTypes.COUNTER, - description="Total number of failed authentication attempts", - labels=["reason"], - ), - MetricConfig( - name=BaseMetricNames.RATE_LIMIT_EXCEEDED_TOTAL, - metric_type=MetricTypes.COUNTER, - description="Total number of requests that exceeded rate limits", - labels=["endpoint"], - ), -] - - -class PrometheusHandler: - """Prometheus metrics handler.""" - - def __init__(self, app: FastAPI) -> None: - """Initialize PrometheusHandler with FastAPI app. - - :param FastAPI app: FastAPI application instance - """ - self.instrumentator = Instrumentator() - self.instrumentator.instrument(app).expose(app, endpoint="/metrics") - - self.metrics: dict[BaseMetricNames, Counter | Gauge] = {} - self._initialize_metrics() - - def _initialize_metrics(self) -> None: - """Initialize metrics based on the base configuration.""" - for metric_config in BASE_METRICS_CONFIG: - self.metrics[metric_config.name] = metric_config.metric_type.prometheus_class( - metric_config.name.value, - metric_config.description, - metric_config.labels or [], - ) - - def get_metric(self, name: BaseMetricNames) -> Counter | Gauge: - """Get a specific metric by name.""" - if not (metric := self.metrics.get(name)): - msg = f"Metric '{name}' not found." - raise ValueError(msg) - return metric - - def increment_counter(self, name: BaseMetricNames, labels: dict[str, str] | None = None) -> None: - """Increment a counter metric. - - :param BaseMetricNames name: Name of the metric to increment - :param dict[str, str] | None labels: Optional label key-value pairs for the metric - """ - counter = self.get_metric(name) - if not isinstance(counter, Counter): - msg = f"Metric '{name}' is not a Counter." - raise TypeError(msg) - if labels: - counter = counter.labels(**labels) - counter.inc() - - def set_gauge(self, name: BaseMetricNames, value: float, labels: dict[str, str] | None = None) -> None: - """Set a gauge metric. - - :param BaseMetricNames name: Name of the metric to set - :param float value: Value to set the gauge to - :param dict[str, str] | None labels: Optional label key-value pairs for the metric - """ - gauge = self.get_metric(name) - if not isinstance(gauge, Gauge): - msg = f"Metric '{name}' is not a Gauge." - raise TypeError(msg) - if labels: - gauge = gauge.labels(**labels) - gauge.set(value) diff --git a/python_template_server/template_server.py b/python_template_server/template_server.py index 9ebbe95..45cf944 100644 --- a/python_template_server/template_server.py +++ b/python_template_server/template_server.py @@ -31,7 +31,6 @@ ServerHealthStatus, TemplateServerConfig, ) -from python_template_server.prometheus_handler import BaseMetricNames, PrometheusHandler setup_logging() logger = logging.getLogger(__name__) @@ -41,7 +40,7 @@ class TemplateServer(ABC): """Template FastAPI server. This class provides a template for building FastAPI servers with common features - such as request logging, security headers, rate limiting, and Prometheus metrics. + such as request logging, security headers and rate limiting. Ensure you implement the `setup_routes` and `validate_config` methods in subclasses. """ @@ -85,7 +84,6 @@ def __init__( self._setup_request_logging() self._setup_security_headers() self._setup_rate_limiting() - self._setup_metrics() self.setup_routes() @staticmethod @@ -142,7 +140,6 @@ async def _verify_api_key( """ if api_key is None: logger.warning("Missing API key in request!") - self.prometheus_handler.increment_counter(BaseMetricNames.AUTH_FAILURE_TOTAL, labels={"reason": "missing"}) raise HTTPException( status_code=ResponseCode.UNAUTHORIZED, detail="Missing API key", @@ -151,18 +148,13 @@ async def _verify_api_key( try: if not verify_token(api_key, self.hashed_token): logger.warning("Invalid API key attempt!") - self.prometheus_handler.increment_counter( - BaseMetricNames.AUTH_FAILURE_TOTAL, labels={"reason": "invalid"} - ) raise HTTPException( status_code=ResponseCode.UNAUTHORIZED, detail="Invalid API key", ) logger.debug("API key validated successfully.") - self.prometheus_handler.increment_counter(BaseMetricNames.AUTH_SUCCESS_TOTAL) except ValueError as e: logger.exception("Error verifying API key!") - self.prometheus_handler.increment_counter(BaseMetricNames.AUTH_FAILURE_TOTAL, labels={"reason": "error"}) raise HTTPException( status_code=ResponseCode.UNAUTHORIZED, detail=str(e), @@ -188,17 +180,13 @@ def _setup_security_headers(self) -> None: ) async def _rate_limit_exception_handler(self, request: Request, exc: RateLimitExceeded) -> CustomJSONResponse: - """Handle rate limit exceeded exceptions and track metrics. + """Handle rate limit exceeded exceptions. :param Request request: The incoming HTTP request :param RateLimitExceeded exc: The rate limit exceeded exception :return JSONResponse: HTTP 429 JSON response """ - self.prometheus_handler.increment_counter( - BaseMetricNames.RATE_LIMIT_EXCEEDED_TOTAL, labels={"endpoint": request.url.path} - ) - - # Return JSON response with 429 status + logger.warning("Rate limit exceeded for %s", request.url.path) return CustomJSONResponse( status_code=429, content={"detail": "Rate limit exceeded"}, @@ -236,12 +224,6 @@ def _limit_route(self, route_function: Callable[..., Any]) -> Callable[..., Any] return self.limiter.limit(self.config.rate_limit.rate_limit)(route_function) # type: ignore[no-any-return] return route_function - def _setup_metrics(self) -> None: - """Set up Prometheus metrics.""" - self.prometheus_handler = PrometheusHandler(self.app) - self.prometheus_handler.set_gauge(BaseMetricNames.TOKEN_CONFIGURED, 1 if self.hashed_token else 0) - logger.info("Prometheus metrics enabled.") - def run(self) -> None: """Run the server using uvicorn. @@ -338,7 +320,6 @@ async def get_health(self, request: Request) -> GetHealthResponse: :return GetHealthResponse: Health status response """ if not self.hashed_token: - self.prometheus_handler.set_gauge(BaseMetricNames.TOKEN_CONFIGURED, 0) return GetHealthResponse( code=ResponseCode.INTERNAL_SERVER_ERROR, message="Server token is not configured", @@ -346,7 +327,6 @@ async def get_health(self, request: Request) -> GetHealthResponse: status=ServerHealthStatus.UNHEALTHY, ) - self.prometheus_handler.set_gauge(BaseMetricNames.TOKEN_CONFIGURED, 1) return GetHealthResponse( code=ResponseCode.OK, message="Server is healthy", diff --git a/tests/conftest.py b/tests/conftest.py index 26facac..0cc1ee5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,7 +4,6 @@ from unittest.mock import MagicMock, mock_open, patch import pytest -from prometheus_client import REGISTRY from python_template_server.models import ( CertificateConfigModel, @@ -75,20 +74,6 @@ def mock_os_getenv() -> Generator[MagicMock]: yield mock_getenv -@pytest.fixture(autouse=True) -def clear_prometheus_registry() -> Generator[None]: - """Clear Prometheus registry before each test to avoid duplicate metric errors.""" - # Clear all collectors from the registry - collectors = list(REGISTRY._collector_to_names.keys()) - for collector in collectors: - REGISTRY.unregister(collector) - yield - # Clear again after the test - collectors = list(REGISTRY._collector_to_names.keys()) - for collector in collectors: - REGISTRY.unregister(collector) - - # Template Server Configuration Models @pytest.fixture def mock_server_config_dict() -> dict: diff --git a/tests/test_models.py b/tests/test_models.py index 4cd15a3..5118645 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -3,19 +3,15 @@ from pathlib import Path import pytest -from prometheus_client import Counter, Gauge from pydantic import ValidationError from python_template_server.models import ( - BaseMetricNames, BaseResponse, CertificateConfigModel, CustomJSONResponse, GetHealthResponse, GetLoginResponse, JSONResponseConfigModel, - MetricConfig, - MetricTypes, RateLimitConfigModel, ResponseCode, SecurityConfigModel, @@ -137,62 +133,6 @@ def test_save_to_file( assert config_file.read_text(encoding="utf-8") == mock_template_server_config.model_dump_json(indent=2) + "\n" -# Prometheus Metric Models -class TestBaseMetricNames: - """Unit tests for the BaseMetricNames enum.""" - - @pytest.mark.parametrize( - ("metric_name", "value"), - [ - (BaseMetricNames.TOKEN_CONFIGURED, "token_configured"), - (BaseMetricNames.AUTH_SUCCESS_TOTAL, "auth_success_total"), - (BaseMetricNames.AUTH_FAILURE_TOTAL, "auth_failure_total"), - (BaseMetricNames.RATE_LIMIT_EXCEEDED_TOTAL, "rate_limit_exceeded_total"), - ], - ) - def test_enum_values(self, metric_name: BaseMetricNames, value: str) -> None: - """Test the enum values.""" - assert metric_name.value == value - - -class TestMetricTypes: - """Unit tests for the MetricTypes enum.""" - - @pytest.mark.parametrize( - ("metric_type", "value"), - [ - (MetricTypes.COUNTER, "counter"), - (MetricTypes.GAUGE, "gauge"), - ], - ) - def test_enum_values(self, metric_type: MetricTypes, value: str) -> None: - """Test the enum values.""" - assert metric_type.value == value - - def test_prometheus_class_property(self) -> None: - """Test the prometheus_class property.""" - assert MetricTypes.COUNTER.prometheus_class == Counter - assert MetricTypes.GAUGE.prometheus_class == Gauge - - -class TestMetricConfig: - """Unit tests for the MetricConfig dataclass.""" - - def test_metric_config_initialization(self) -> None: - """Test initialization of MetricConfig.""" - metric_config = MetricConfig( - name=BaseMetricNames.AUTH_SUCCESS_TOTAL, - metric_type=MetricTypes.COUNTER, - description="Total number of successful authentication attempts", - labels=["user_id"], - ) - - assert metric_config.name == BaseMetricNames.AUTH_SUCCESS_TOTAL - assert metric_config.metric_type == MetricTypes.COUNTER - assert metric_config.description == "Total number of successful authentication attempts" - assert metric_config.labels == ["user_id"] - - # API Response Models class TestCustomJSONResponse: """Unit tests for the CustomJSONResponse class.""" diff --git a/tests/test_prometheus_handler.py b/tests/test_prometheus_handler.py deleted file mode 100644 index b367459..0000000 --- a/tests/test_prometheus_handler.py +++ /dev/null @@ -1,87 +0,0 @@ -"""Unit tests for the python_template_server.prometheus_handler module.""" - -from unittest.mock import MagicMock - -import pytest -from fastapi import FastAPI - -from python_template_server.models import BaseMetricNames -from python_template_server.prometheus_handler import BASE_METRICS_CONFIG, PrometheusHandler - - -@pytest.fixture -def mock_prometheus_handler() -> PrometheusHandler: - """Fixture to create a PrometheusHandler instance with a mocked FastAPI app.""" - mock_fastapi_app = MagicMock(spec=FastAPI) - return PrometheusHandler(app=mock_fastapi_app) - - -class TestPrometheusHandler: - """Unit tests for the PrometheusHandler class.""" - - def test_initialize_metrics(self, mock_prometheus_handler: PrometheusHandler) -> None: - """Test that metrics are initialized correctly.""" - assert len(mock_prometheus_handler.metrics) == len(BASE_METRICS_CONFIG) - - for metric, config in zip( - mock_prometheus_handler.metrics.values(), - BASE_METRICS_CONFIG, - strict=False, - ): - assert isinstance(metric, config.metric_type.prometheus_class) - - def test_get_metric_valid_name(self, mock_prometheus_handler: PrometheusHandler) -> None: - """Test that getting a metric by a valid name works.""" - for config in BASE_METRICS_CONFIG: - metric = mock_prometheus_handler.get_metric(config.name) - assert isinstance(metric, config.metric_type.prometheus_class) - - def test_get_metric_invalid_name(self, mock_prometheus_handler: PrometheusHandler) -> None: - """Test that getting a metric with an invalid name raises a KeyError.""" - invalid_name = MagicMock("invalid_metric_name") - with pytest.raises(ValueError, match=f"Metric '{invalid_name}' not found"): - mock_prometheus_handler.get_metric(invalid_name) - - def test_increment_counter(self, mock_prometheus_handler: PrometheusHandler) -> None: - """Test incrementing a counter metric.""" - auth_success_metric = mock_prometheus_handler.get_metric(BaseMetricNames.AUTH_SUCCESS_TOTAL) - initial_value = auth_success_metric._value.get() # Accessing protected member for testing - - mock_prometheus_handler.increment_counter(BaseMetricNames.AUTH_SUCCESS_TOTAL) - - updated_value = auth_success_metric._value.get() - assert updated_value == initial_value + 1 - - def test_increment_counter_with_labels(self, mock_prometheus_handler: PrometheusHandler) -> None: - """Test incrementing a counter metric with labels.""" - auth_failure_metric = mock_prometheus_handler.get_metric(BaseMetricNames.AUTH_FAILURE_TOTAL) - initial_value = auth_failure_metric.labels( - reason="invalid_token" - )._value.get() # Accessing protected member for testing - - mock_prometheus_handler.increment_counter( - BaseMetricNames.AUTH_FAILURE_TOTAL, - labels={"reason": "invalid_token"}, - ) - - updated_value = auth_failure_metric.labels(reason="invalid_token")._value.get() - assert updated_value == initial_value + 1 - - def test_increment_counter_invalid_type(self, mock_prometheus_handler: PrometheusHandler) -> None: - """Test that incrementing a non-counter metric raises a TypeError.""" - with pytest.raises(TypeError, match=f"Metric '{BaseMetricNames.TOKEN_CONFIGURED}' is not a Counter"): - mock_prometheus_handler.increment_counter(BaseMetricNames.TOKEN_CONFIGURED) - - def test_set_gauge(self, mock_prometheus_handler: PrometheusHandler) -> None: - """Test setting a gauge metric.""" - token_configured_metric = mock_prometheus_handler.get_metric(BaseMetricNames.TOKEN_CONFIGURED) - - mock_prometheus_handler.set_gauge(BaseMetricNames.TOKEN_CONFIGURED, 1) - - updated_value = token_configured_metric._value.get() - assert updated_value == 1 - - def test_set_gauge_invalid_type(self, mock_prometheus_handler: PrometheusHandler) -> None: - """Test that setting a non-gauge metric raises a TypeError.""" - with pytest.raises(TypeError, match=f"Metric '{BaseMetricNames.AUTH_SUCCESS_TOTAL}' is not a Gauge"): - mock_prometheus_handler.set_gauge(BaseMetricNames.AUTH_SUCCESS_TOTAL, 10) diff --git a/tests/test_template_server.py b/tests/test_template_server.py index b02a2b4..92b4340 100644 --- a/tests/test_template_server.py +++ b/tests/test_template_server.py @@ -21,14 +21,12 @@ from python_template_server.constants import API_PREFIX from python_template_server.middleware import RequestLoggingMiddleware, SecurityHeadersMiddleware from python_template_server.models import ( - BaseMetricNames, BaseResponse, CustomJSONResponse, ResponseCode, ServerHealthStatus, TemplateServerConfig, ) -from python_template_server.prometheus_handler import PrometheusHandler from python_template_server.template_server import TemplateServer @@ -40,7 +38,7 @@ def mock_package_metadata() -> Generator[MagicMock]: metadata_dict = { "Name": "python-template-server", "Version": "0.1.0", - "Summary": "A template FastAPI server with authentication, rate limiting and Prometheus metrics.", + "Summary": "A template FastAPI server with production-ready configuration.", } mock_pkg_metadata.__getitem__.side_effect = lambda key: metadata_dict[key] mock_metadata.return_value = mock_pkg_metadata @@ -139,10 +137,7 @@ def test_init(self, mock_template_server: TemplateServer) -> None: """Test TemplateServer initialization.""" assert isinstance(mock_template_server.app, FastAPI) assert mock_template_server.app.title == "python-template-server" - assert ( - mock_template_server.app.description - == "A template FastAPI server with authentication, rate limiting and Prometheus metrics." - ) + assert mock_template_server.app.description == "A template FastAPI server with production-ready configuration." assert mock_template_server.app.version == "0.1.0" assert mock_template_server.app.root_path == API_PREFIX assert isinstance(mock_template_server.api_key_header, APIKeyHeader) @@ -302,73 +297,6 @@ def test_verify_api_key_value_error( assert exc_info.value.status_code == ResponseCode.UNAUTHORIZED assert "No stored token hash found" in exc_info.value.detail - def test_auth_success_metric_incremented( - self, mock_template_server: TemplateServer, mock_verify_token: MagicMock - ) -> None: - """Test that auth_success_counter is incremented on successful authentication.""" - mock_verify_token.return_value = True - auth_success_counter = mock_template_server.prometheus_handler.get_metric(BaseMetricNames.AUTH_SUCCESS_TOTAL) - assert auth_success_counter is not None - initial_value = auth_success_counter._value.get() - - asyncio.run(mock_template_server._verify_api_key(api_key="valid_token")) - - assert auth_success_counter._value.get() == initial_value + 1 - - def test_auth_failure_missing_metric_incremented(self, mock_template_server: TemplateServer) -> None: - """Test that auth_failure_counter is incremented when API key is missing.""" - auth_failure_counter = mock_template_server.prometheus_handler.get_metric(BaseMetricNames.AUTH_FAILURE_TOTAL) - assert auth_failure_counter is not None - initial_value = auth_failure_counter.labels(reason="missing")._value.get() - - with pytest.raises(HTTPException): - asyncio.run(mock_template_server._verify_api_key(api_key=None)) - - assert auth_failure_counter.labels(reason="missing")._value.get() == initial_value + 1 - - def test_auth_failure_invalid_metric_incremented( - self, mock_template_server: TemplateServer, mock_verify_token: MagicMock - ) -> None: - """Test that auth_failure_counter is incremented when API key is invalid.""" - mock_verify_token.return_value = False - auth_failure_counter = mock_template_server.prometheus_handler.get_metric(BaseMetricNames.AUTH_FAILURE_TOTAL) - assert auth_failure_counter is not None - initial_value = auth_failure_counter.labels(reason="invalid")._value.get() - - with pytest.raises(HTTPException): - asyncio.run(mock_template_server._verify_api_key(api_key="invalid_token")) - - assert auth_failure_counter.labels(reason="invalid")._value.get() == initial_value + 1 - - def test_auth_failure_error_metric_incremented( - self, mock_template_server: TemplateServer, mock_verify_token: MagicMock - ) -> None: - """Test that auth_failure_counter is incremented when verification raises ValueError.""" - mock_verify_token.side_effect = ValueError("Verification error") - auth_failure_counter = mock_template_server.prometheus_handler.get_metric(BaseMetricNames.AUTH_FAILURE_TOTAL) - assert auth_failure_counter is not None - initial_value = auth_failure_counter.labels(reason="error")._value.get() - - with pytest.raises(HTTPException): - asyncio.run(mock_template_server._verify_api_key(api_key="error_token")) - - assert auth_failure_counter.labels(reason="error")._value.get() == initial_value + 1 - - -class TestPrometheusMetrics: - """Unit tests for Prometheus metrics functionality.""" - - def test_metrics_setup(self, mock_template_server: TemplateServer) -> None: - """Test that Prometheus metrics are properly initialized.""" - assert isinstance(mock_template_server.prometheus_handler, PrometheusHandler) - assert mock_template_server.prometheus_handler.get_metric(BaseMetricNames.TOKEN_CONFIGURED)._value.get() == 1 - - def test_metrics_endpoint_exists(self, mock_template_server: TemplateServer) -> None: - """Test that /metrics endpoint is exposed.""" - api_routes = [route for route in mock_template_server.app.routes if isinstance(route, APIRoute)] - routes = [route.path for route in api_routes] - assert "/metrics" in routes - class TestRateLimiting: """Unit tests for rate limiting functionality.""" @@ -381,18 +309,9 @@ def test_rate_limit_exception_handler(self, mock_template_server: TemplateServer exc = MagicMock(spec=RateLimitExceeded) exc.retry_after = 42 - rate_limit_counter = mock_template_server.prometheus_handler.get_metric( - BaseMetricNames.RATE_LIMIT_EXCEEDED_TOTAL - ) - assert rate_limit_counter is not None - initial_value = rate_limit_counter.labels(endpoint=request.url.path)._value.get() - # Call the handler response = asyncio.run(mock_template_server._rate_limit_exception_handler(request, exc)) - # Verify counter incremented - assert rate_limit_counter.labels(endpoint=request.url.path)._value.get() == initial_value + 1 - # Verify JSONResponse status and content assert response.status_code == HTTP_429_TOO_MANY_REQUESTS assert isinstance(response.body, bytes) @@ -564,7 +483,6 @@ def test_setup_routes(self, mock_template_server: MockTemplateServer) -> None: expected_endpoints = [ "/health", "/login", - "/metrics", "/unauthenticated-endpoint", "/authenticated-endpoint", "/unlimited-unauthenticated-endpoint", @@ -585,9 +503,6 @@ def test_get_health(self, mock_template_server: TemplateServer) -> None: assert response.code == ResponseCode.OK assert response.message == "Server is healthy" assert response.status == ServerHealthStatus.HEALTHY - token_gauge = mock_template_server.prometheus_handler.get_metric(BaseMetricNames.TOKEN_CONFIGURED) - assert token_gauge is not None - assert token_gauge._value.get() == 1 def test_get_health_token_not_configured(self, mock_template_server: TemplateServer) -> None: """Test the /health endpoint method when token is not configured.""" @@ -599,9 +514,6 @@ def test_get_health_token_not_configured(self, mock_template_server: TemplateSer assert response.code == ResponseCode.INTERNAL_SERVER_ERROR assert response.message == "Server token is not configured" assert response.status == ServerHealthStatus.UNHEALTHY - token_gauge = mock_template_server.prometheus_handler.get_metric(BaseMetricNames.TOKEN_CONFIGURED) - assert token_gauge is not None - assert token_gauge._value.get() == 0 def test_get_health_endpoint( self, mock_template_server: TemplateServer, mock_verify_token: MagicMock, mock_timestamp: str diff --git a/uv.lock b/uv.lock index 868eb8f..ac0cd42 100644 --- a/uv.lock +++ b/uv.lock @@ -764,28 +764,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5d/19/fd3ef348460c80af7bb4669ea7926651d1f95c23ff2df18b9d24bab4f3fa/pre_commit-4.5.1-py2.py3-none-any.whl", hash = "sha256:3b3afd891e97337708c1674210f8eba659b52a38ea5f822ff142d10786221f77", size = 226437, upload-time = "2025-12-16T21:14:32.409Z" }, ] -[[package]] -name = "prometheus-client" -version = "0.23.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/23/53/3edb5d68ecf6b38fcbcc1ad28391117d2a322d9a1a3eff04bfdb184d8c3b/prometheus_client-0.23.1.tar.gz", hash = "sha256:6ae8f9081eaaaf153a2e959d2e6c4f4fb57b12ef76c8c7980202f1e57b48b2ce", size = 80481, upload-time = "2025-09-18T20:47:25.043Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b8/db/14bafcb4af2139e046d03fd00dea7873e48eafe18b7d2797e73d6681f210/prometheus_client-0.23.1-py3-none-any.whl", hash = "sha256:dd1913e6e76b59cfe44e7a4b83e01afc9873c1bdfd2ed8739f1e76aeca115f99", size = 61145, upload-time = "2025-09-18T20:47:23.875Z" }, -] - -[[package]] -name = "prometheus-fastapi-instrumentator" -version = "7.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "prometheus-client" }, - { name = "starlette" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/69/6d/24d53033cf93826aa7857699a4450c1c67e5b9c710e925b1ed2b320c04df/prometheus_fastapi_instrumentator-7.1.0.tar.gz", hash = "sha256:be7cd61eeea4e5912aeccb4261c6631b3f227d8924542d79eaf5af3f439cbe5e", size = 20220, upload-time = "2025-03-19T19:35:05.351Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/27/72/0824c18f3bc75810f55dacc2dd933f6ec829771180245ae3cc976195dec0/prometheus_fastapi_instrumentator-7.1.0-py3-none-any.whl", hash = "sha256:978130f3c0bb7b8ebcc90d35516a6fe13e02d2eb358c8f83887cdef7020c31e9", size = 19296, upload-time = "2025-03-19T19:35:04.323Z" }, -] - [[package]] name = "py-serializable" version = "2.1.0" @@ -961,7 +939,6 @@ dependencies = [ { name = "cryptography" }, { name = "fastapi" }, { name = "httpx" }, - { name = "prometheus-fastapi-instrumentator" }, { name = "pydantic" }, { name = "pyhere" }, { name = "python-dotenv" }, @@ -991,7 +968,6 @@ requires-dist = [ { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.19.1" }, { name = "pip-audit", marker = "extra == 'dev'", specifier = ">=2.10.0" }, { name = "pre-commit", marker = "extra == 'dev'", specifier = ">=4.5.1" }, - { name = "prometheus-fastapi-instrumentator", specifier = ">=7.1.0" }, { name = "pydantic", specifier = ">=2.12.4" }, { name = "pyhere", specifier = ">=1.0.3" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.4.2" },