Merge pull request 'feature/wc' (#51) from feature/wc into feature/icecheng/metrics

Reviewed-on: freeleaps/freeleaps-service-hub#51
This commit is contained in:
icecheng 2025-09-18 09:53:05 +00:00
commit 806778a5b9
11 changed files with 293 additions and 336 deletions

View File

@ -10,9 +10,28 @@ COPY requirements.txt .
# Install dependencies
RUN pip install --no-cache-dir -r requirements.txt
# Copy environment file
COPY local.env .
# Copy application code
COPY . .
ENV MONGODB_NAME = "freeleaps2"
ENV MONGODB_URI = "mongodb://freeleaps2-mongodb:27017"
#app_settings
ENV GITEA_TOKEN = ""
ENV GITEA_URL = ""
ENV GITEA_DEPOT_ORGANIZATION = ""
ENV CODE_DEPOT_HTTP_PORT = ""
ENV CODE_DEPOT_SSH_PORT = ""
ENV CODE_DEPOT_DOMAIN_NAME = ""
#log_settings
ENV LOG_BASE_PATH = "./logs"
ENV BACKEND_LOG_FILE_NAME = "freeleaps-metrics"
ENV APPLICATION_ACTIVITY_LOG = "freeleaps-metrics-activity"
# Expose port
EXPOSE 8009

View File

@ -1,35 +1,28 @@
# 📊 Metrics Service
> A lightweight FastAPI microservice for user registration analytics and statistics
> A lightweight FastAPI microservice for user registration analytics and metrics
[![Python](https://img.shields.io/badge/Python-3.12+-blue.svg)](https://python.org)
[![FastAPI](https://img.shields.io/badge/FastAPI-0.114+-green.svg)](https://fastapi.tiangolo.com)
[![Docker](https://img.shields.io/badge/Docker-Ready-blue.svg)](https://docker.com)
The Metrics service provides real-time APIs for querying user registration data from StarRocks database, offering flexible analytics and insights into user growth patterns.
The Metrics service provides real-time APIs for querying user registration data (via StarRocks) and querying monitoring metrics (via Prometheus).
## ✨ Features
### 📊 User Registration Statistics APIs
- **Date Range Query** - Query registration data for specific date ranges
- **Recent N Days Query** - Get registration data for the last N days
- **Start Date + Days Query** - Query N days starting from a specified date
- **Statistics Summary** - Get comprehensive statistics and analytics
- **POST Method Support** - JSON request body support for complex queries
### 📊 Registration Analytics (StarRocks)
- Date Range Query (start_date ~ end_date)
- Typed responses with Pydantic models
### 🗄️ Database Integration
- **StarRocks Database Connection**
- Host: `freeleaps-starrocks-cluster-fe-service.freeleaps-data-platform.svc`
- Port: `9030`
- Database: `freeleaps`
- Table: `dws_daily_registered_users`
### 📈 Prometheus Metrics
- Predefined PromQL metrics per product
- Time-range queries and metric info discovery
### 🔧 Technical Features
- **Data Models**: Pydantic validation for data integrity
- **Connection Management**: Automatic database connection and disconnection
- **Error Handling**: Comprehensive exception handling with user-friendly error messages
- **Logging**: Structured logging using Loguru
- **API Documentation**: Auto-generated Swagger/OpenAPI documentation
- Data Models: Pydantic v2
- Async HTTP and robust error handling
- Structured logging
- Auto-generated Swagger/OpenAPI docs
## 📁 Project Structure
@ -37,31 +30,43 @@ The Metrics service provides real-time APIs for querying user registration data
metrics/
├── backend/ # Business logic layer
│ ├── infra/ # Infrastructure components
│ │ └── database_client.py
│ │ └── external_service/
│ │ ├── prometheus_client.py
│ │ └── starrocks_client.py
│ ├── models/ # Data models
│ │ └── user_registration_models.py
│ └── services/ # Business services
│ ├── prometheus_metrics_service.py
│ └── registration_analytics_service.py
├── webapi/ # API layer
│ ├── routes/ # API endpoints
│ │ └── registration_metrics.py
│ ├── config/ # Configuration
│ │ └── app_settings.py
│ ├── bootstrap/ # App initialization
│ │ └── app_factory.py
│ └── main.py # FastAPI app entry point
├── common/ # Shared utilities
├── requirements.txt # Dependencies
├── Dockerfile # Container config
├── local.env # Environment variables
└── README.md # Documentation
│ │ ├── metrics/ # Aggregated routers (prefix: /api/metrics)
│ │ ├── prometheus_metrics/
│ │ │ ├── __init__.py
│ │ │ ├── available_metrics.py
│ │ │ ├── metric_info.py
│ │ │ └── metrics_query.py
│ │ └── starrocks_metrics/
│ │ ├── __init__.py
│ │ ├── available_metrics.py
│ │ ├── metric_info.py
│ │ └── metrics_query.py
│ ├── bootstrap/
│ │ └── application.py
│ └── main.py
├── common/
├── requirements.txt
├── Dockerfile
├── local.env
└── README.md
```
## 🚀 Quick Start
### Prerequisites
- Python 3.12+ or Docker
- Access to StarRocks database
- Access to StarRocks database (for registration analytics)
- Access to Prometheus (for monitoring metrics)
### 🐍 Python Setup
@ -79,8 +84,12 @@ python3 -m uvicorn webapi.main:app --host 0.0.0.0 --port 8009 --reload
# 1. Build image
docker build -t metrics:latest .
# 2. Run container
# 2. Run container (env from baked local.env)
docker run --rm -p 8009:8009 metrics:latest
# Alternatively: run with a custom env file
# (this overrides envs copied into the image)
docker run --rm -p 8009:8009 --env-file local.env metrics:latest
```
### 📖 Access Documentation
@ -88,69 +97,36 @@ Visit `http://localhost:8009/docs` for interactive API documentation.
## 📊 API Endpoints
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/api/metrics/daily-registered-users` | GET/POST | Query registration data by date range |
| `/api/metrics/recent-registered-users` | GET | Get recent N days data |
| `/api/metrics/registered-users-by-days` | GET | Query N days from start date |
| `/api/metrics/registration-summary` | GET | Get statistical summary |
All endpoints are exposed under the API base prefix `/api/metrics`.
### Example Requests
### StarRocks (Registration Analytics)
- POST `/api/metrics/starrocks/dru_query` — Query daily registered users time series for a date range
- GET `/api/metrics/starrocks/product/{product_id}/available-metrics` — List available StarRocks-backed metrics for the product
- GET `/api/metrics/starrocks/product/{product_id}/metric/{metric_name}/info` — Get metric info for the product
Example request:
```bash
# Get last 7 days
curl "http://localhost:8009/api/metrics/recent-registered-users?days=7"
# Get date range
curl "http://localhost:8009/api/metrics/daily-registered-users?start_date=2024-09-10&end_date=2024-09-20"
# Get summary statistics
curl "http://localhost:8009/api/metrics/registration-summary?start_date=2024-09-10&end_date=2024-09-20"
curl -X POST "http://localhost:8009/api/metrics/starrocks/dru_query" \
-H "Content-Type: application/json" \
-d '{
"product_id": "freeleaps",
"start_date": "2024-09-10",
"end_date": "2024-09-20"
}'
```
### Parameters
- `start_date` / `end_date`: Date in `YYYY-MM-DD` format
- `days`: Number of days (max: 365)
- `product_id`: Product identifier (default: "freeleaps")
## 📈 Response Format
### Standard Response
```json
{
"dates": ["2024-09-10", "2024-09-11", "2024-09-12"],
"counts": [39, 38, 31],
"total_registrations": 108,
"query_period": "2024-09-10 to 2024-09-12"
}
```
### Summary Response
```json
{
"total_registrations": 282,
"average_daily": 25.64,
"max_daily": 39,
"min_daily": 8,
"days_with_registrations": 10,
"total_days": 11
}
```
### Prometheus
- POST `/api/metrics/prometheus/metrics_query` — Query metric time series by product/metric
- GET `/api/metrics/prometheus/product/{product_id}/available-metrics` — List available metrics for product
- GET `/api/metrics/prometheus/product/{product_id}/metric/{metric_name}/info` — Get metric info
## 🧪 Testing
### Quick Test
```bash
# Health check
curl http://localhost:8009/
# Test recent registrations
curl "http://localhost:8009/api/metrics/recent-registered-users?days=7"
```
### Interactive Testing
Visit `http://localhost:8009/docs` for the Swagger UI interface where you can test all endpoints directly.
## ⚙️ Configuration
### Environment Variables
@ -170,9 +146,12 @@ STARROCKS_DATABASE=freeleaps
LOG_BASE_PATH=./logs
BACKEND_LOG_FILE_NAME=metrics
APPLICATION_ACTIVITY_LOG=metrics-activity
# Prometheus
PROMETHEUS_ENDPOINT=http://localhost:9090
```
> 💡 **Tip**: Copy `local.env` to `.env` and modify as needed for your environment.
> Tip: Copy `local.env` to `.env` and modify as needed for your environment.
### 🐳 Docker Deployment
@ -206,20 +185,16 @@ python -m uvicorn webapi.main:app --reload
```
## 📝 API Documentation
- **Swagger UI**: `http://localhost:8009/docs`
- **ReDoc**: `http://localhost:8009/redoc`
- **OpenAPI JSON**: `http://localhost:8009/openapi.json`
- Swagger UI: `http://localhost:8009/docs`
- ReDoc: `http://localhost:8009/redoc`
- OpenAPI JSON: `http://localhost:8009/openapi.json`
## ⚠️ Important Notes
- Date format: `YYYY-MM-DD`
- Max query range: 365 days
- Date format: `YYYY-MM-DD` (single-digit month/day also accepted by API)
- Default `product_id`: "freeleaps"
- Requires StarRocks database access
- Requires StarRocks database access and/or Prometheus endpoint
## 🐛 Troubleshooting
| Issue | Solution |
|-------|----------|
| Port in use | `docker stop $(docker ps -q --filter ancestor=metrics:latest)` |

View File

@ -88,12 +88,3 @@ class StarRocksClient:
def __exit__(self, exc_type, exc_val, exc_tb):
"""Context manager exit"""
self.disconnect()

View File

@ -1,7 +1,7 @@
from typing import List, Dict, Any
from datetime import date, timedelta
from loguru import logger
from backend.infra.database_client import StarRocksClient
from backend.infra.external_service.starrocks_client import StarRocksClient
from backend.models.user_registration_models import UserRegistrationResponse, DailyRegisteredUsers
@ -33,7 +33,6 @@ class RegistrationService:
raw_data = self.starrocks_client.get_daily_registered_users(
start_date, end_date, product_id
)
# Convert to DailyRegisteredUsers objects
daily_data = [
DailyRegisteredUsers(
@ -44,7 +43,6 @@ class RegistrationService:
)
for row in raw_data
]
# Create date-to-count mapping
data_dict = {str(item.date_id): item.registered_cnt for item in daily_data}

View File

@ -14,3 +14,4 @@ pytest==8.4.1
pytest-asyncio==0.21.2
pymysql==1.1.0
sqlalchemy==2.0.23
python-dotenv

View File

@ -1,7 +1,7 @@
from fastapi import APIRouter
from webapi.routes.metrics.registration_metrics import router as registration_router
from webapi.routes.starrocks_metrics import api_router as starrocks_metrics_router
from webapi.routes.prometheus_metrics import api_router as prometheus_metrics_router
router = APIRouter()
router.include_router(registration_router, prefix="/metrics", tags=["registration-metrics"])
router.include_router(starrocks_metrics_router, prefix="/metrics", tags=["starrocks-metrics"])
router.include_router(prometheus_metrics_router, prefix="/metrics", tags=["prometheus-metrics"])

View File

@ -1,229 +0,0 @@
from fastapi import APIRouter, HTTPException, Query
from datetime import date, datetime, timedelta
from typing import Optional
from loguru import logger
from backend.services.registration_analytics_service import RegistrationService
from backend.models.user_registration_models import UserRegistrationResponse, UserRegistrationQuery
router = APIRouter(tags=["registration"])
# Initialize service
registration_service = RegistrationService()
@router.get("/daily-registered-users", response_model=UserRegistrationResponse)
async def get_daily_registered_users(
start_date: date = Query(..., description="Start date in YYYY-MM-DD format"),
end_date: date = Query(..., description="End date in YYYY-MM-DD format"),
product_id: str = Query("freeleaps", description="Product identifier")
):
"""
Get daily registered users count for a date range
Returns two lists:
- dates: List of dates in YYYY-MM-DD format
- counts: List of daily registration counts
Example:
- GET /api/metrics/daily-registered-users?start_date=2024-01-01&end_date=2024-01-07
"""
try:
# Validate date range
if start_date > end_date:
raise HTTPException(
status_code=400,
detail="Start date must be before or equal to end date"
)
# Check date range is not too large (max 1 year)
if (end_date - start_date).days > 365:
raise HTTPException(
status_code=400,
detail="Date range cannot exceed 365 days"
)
logger.info(f"Querying registration data from {start_date} to {end_date} for product {product_id}")
# Get data from service
result = registration_service.get_daily_registered_users(
start_date, end_date, product_id
)
logger.info(f"Successfully retrieved data for {len(result.dates)} days")
return result
except HTTPException:
raise
except Exception as e:
logger.error(f"Failed to get daily registered users: {e}")
raise HTTPException(
status_code=500,
detail=f"Internal server error: {str(e)}"
)
@router.get("/registration-summary")
async def get_registration_summary(
start_date: date = Query(..., description="Start date in YYYY-MM-DD format"),
end_date: date = Query(..., description="End date in YYYY-MM-DD format"),
product_id: str = Query("freeleaps", description="Product identifier")
):
"""
Get summary statistics for user registrations in a date range
Returns summary statistics including:
- total_registrations: Total number of registrations
- average_daily: Average daily registrations
- max_daily: Maximum daily registrations
- min_daily: Minimum daily registrations
- days_with_registrations: Number of days with registrations
- total_days: Total number of days in range
"""
try:
# Validate date range
if start_date > end_date:
raise HTTPException(
status_code=400,
detail="Start date must be before or equal to end date"
)
if (end_date - start_date).days > 365:
raise HTTPException(
status_code=400,
detail="Date range cannot exceed 365 days"
)
logger.info(f"Querying registration summary from {start_date} to {end_date} for product {product_id}")
# Get summary from service
summary = registration_service.get_registration_summary(
start_date, end_date, product_id
)
return summary
except HTTPException:
raise
except Exception as e:
logger.error(f"Failed to get registration summary: {e}")
raise HTTPException(
status_code=500,
detail=f"Internal server error: {str(e)}"
)
@router.get("/recent-registered-users", response_model=UserRegistrationResponse)
async def get_recent_registered_users(
days: int = Query(7, ge=1, le=365, description="Number of recent days to query"),
product_id: str = Query("freeleaps", description="Product identifier")
):
"""
Get daily registered users count for recent N days
Returns registration data for the last N days from today
Example:
- GET /api/metrics/recent-registered-users?days=7
- GET /api/metrics/recent-registered-users?days=30&product_id=freeleaps
"""
try:
# Calculate date range
end_date = date.today()
start_date = end_date - timedelta(days=days-1)
logger.info(f"Querying recent {days} days registration data from {start_date} to {end_date} for product {product_id}")
# Get data from service
result = registration_service.get_daily_registered_users(
start_date, end_date, product_id
)
logger.info(f"Successfully retrieved recent {days} days data, total registrations: {result.total_registrations}")
return result
except Exception as e:
logger.error(f"Failed to get recent registered users: {e}")
raise HTTPException(
status_code=500,
detail=f"Internal server error: {str(e)}"
)
@router.get("/registered-users-by-days", response_model=UserRegistrationResponse)
async def get_registered_users_by_days(
start_date: date = Query(..., description="Start date in YYYY-MM-DD format"),
days: int = Query(..., ge=1, le=365, description="Number of days from start date"),
product_id: str = Query("freeleaps", description="Product identifier")
):
"""
Get daily registered users count starting from a specific date for N days
Returns registration data for N days starting from the specified start date
Example:
- GET /api/metrics/registered-users-by-days?start_date=2024-01-01&days=7
- GET /api/metrics/registered-users-by-days?start_date=2024-09-01&days=30&product_id=freeleaps
"""
try:
# Calculate end date
end_date = start_date + timedelta(days=days-1)
logger.info(f"Querying registration data from {start_date} for {days} days (until {end_date}) for product {product_id}")
# Get data from service
result = registration_service.get_daily_registered_users(
start_date, end_date, product_id
)
logger.info(f"Successfully retrieved {days} days data from {start_date}, total registrations: {result.total_registrations}")
return result
except Exception as e:
logger.error(f"Failed to get registered users by days: {e}")
raise HTTPException(
status_code=500,
detail=f"Internal server error: {str(e)}"
)
@router.post("/daily-registered-users", response_model=UserRegistrationResponse)
async def get_daily_registered_users_post(
query: UserRegistrationQuery
):
"""
Get daily registered users count for a date range (POST method)
Same as GET method but accepts parameters in request body
"""
try:
# Validate date range
if query.start_date > query.end_date:
raise HTTPException(
status_code=400,
detail="Start date must be before or equal to end date"
)
if (query.end_date - query.start_date).days > 365:
raise HTTPException(
status_code=400,
detail="Date range cannot exceed 365 days"
)
logger.info(f"Querying registration data from {query.start_date} to {query.end_date} for product {query.product_id}")
# Get data from service
result = registration_service.get_daily_registered_users(
query.start_date, query.end_date, query.product_id
)
logger.info(f"Successfully retrieved data for {len(result.dates)} days")
return result
except HTTPException:
raise
except Exception as e:
logger.error(f"Failed to get daily registered users: {e}")
raise HTTPException(
status_code=500,
detail=f"Internal server error: {str(e)}"
)

View File

@ -0,0 +1,9 @@
from fastapi import APIRouter
from .metrics_query import router as metrics_query_router
from .available_metrics import router as available_metrics_router
from .metric_info import router as metric_info_router
api_router = APIRouter()
api_router.include_router(available_metrics_router, tags=["starrocks-metrics"])
api_router.include_router(metrics_query_router, tags=["starrocks-metrics"])
api_router.include_router(metric_info_router, tags=["starrocks-metrics"])

View File

@ -0,0 +1,45 @@
from fastapi import APIRouter, HTTPException
from common.log.module_logger import ModuleLogger
router = APIRouter()
# Initialize logger
module_logger = ModuleLogger(__file__)
# Product -> supported StarRocks-backed metrics
SUPPORTED_STARROCKS_METRICS_MAP = {
"freeleaps": [
"daily_registered_users",
],
"magicleaps": [
"daily_registered_users",
],
}
@router.get("/starrocks/product/{product_id}/available-metrics")
async def get_available_metrics(product_id: str):
"""
Get list of available StarRocks-backed metrics for a specific product.
Args:
product_id: Product ID to get metrics for (required).
Returns a list of metric names available via StarRocks for the specified product.
"""
await module_logger.log_info(
f"Getting StarRocks available metrics list for product_id: {product_id}"
)
if product_id not in SUPPORTED_STARROCKS_METRICS_MAP:
raise HTTPException(status_code=404, detail=f"Unknown product_id: {product_id}")
metrics = SUPPORTED_STARROCKS_METRICS_MAP[product_id]
return {
"product_id": product_id,
"available_metrics": metrics,
"total_count": len(metrics),
"description": f"List of StarRocks-backed metrics for product '{product_id}'",
}

View File

@ -0,0 +1,53 @@
from fastapi import APIRouter, HTTPException
from common.log.module_logger import ModuleLogger
router = APIRouter()
# Initialize logger
module_logger = ModuleLogger(__file__)
# Product -> metric -> description
STARROCKS_METRIC_DESCRIPTIONS = {
"freeleaps": {
"daily_registered_users": "Daily registered users count from StarRocks table dws_daily_registered_users",
},
"magicleaps": {
"daily_registered_users": "Daily registered users count from StarRocks table dws_daily_registered_users",
},
}
@router.get("/starrocks/product/{product_id}/metric/{metric_name}/info")
async def get_metric_info(
product_id: str,
metric_name: str
):
"""
Get information about a specific StarRocks-backed metric.
Args:
product_id: Product identifier for the product's data.
metric_name: Name of the StarRocks-backed metric.
"""
await module_logger.log_info(
f"Getting StarRocks metric info for metric '{metric_name}' from product '{product_id}'"
)
if product_id not in STARROCKS_METRIC_DESCRIPTIONS:
raise HTTPException(status_code=404, detail=f"Unknown product_id: {product_id}")
product_metrics = STARROCKS_METRIC_DESCRIPTIONS[product_id]
if metric_name not in product_metrics:
raise HTTPException(status_code=404, detail=f"Unknown metric '{metric_name}' for product '{product_id}'")
metric_info = {
"product_id": product_id,
"metric_name": metric_name,
"description": product_metrics[metric_name],
}
return {
"metric_info": metric_info,
"description": f"Information about StarRocks metric '{metric_name}' in product '{product_id}'",
}

View File

@ -0,0 +1,95 @@
from fastapi import APIRouter
from typing import Optional, List, Dict, Any
from pydantic import BaseModel, Field
from datetime import date
from common.log.module_logger import ModuleLogger
from backend.services.registration_analytics_service import RegistrationService
class RegistrationDataPoint(BaseModel):
"""Single data point in registration time series."""
date: str = Field(..., description="Date in YYYY-MM-DD format")
value: int = Field(..., description="Number of registered users")
product_id: str = Field(..., description="Product identifier")
class RegistrationTimeSeriesResponse(BaseModel):
"""Response model for registration time series data."""
metric_name: str = Field(..., description="Name of the queried metric")
data_points: List[RegistrationDataPoint] = Field(..., description="List of data points")
total_points: int = Field(..., description="Total number of data points")
time_range: Dict[str, str] = Field(..., description="Start and end date of the query")
total_registrations: int = Field(..., description="Total number of registrations in the period")
class RegistrationQueryRequest(BaseModel):
"""Request model for registration query."""
product_id: str = Field("freeleaps", description="Product ID to identify which product's data to query")
start_date: str = Field(..., description="Start date in YYYY-MM-DD format")
end_date: str = Field(..., description="End date in YYYY-MM-DD format")
router = APIRouter()
# Initialize service and logger
registration_service = RegistrationService()
module_logger = ModuleLogger(__file__)
@router.post("/starrocks/dru_query", response_model=RegistrationTimeSeriesResponse)
async def metrics_query(
request: RegistrationQueryRequest
):
"""
Query registration time series data.
Returns XY curve data (time series) for user registrations within the given date range.
"""
await module_logger.log_info(
f"Querying registration data for product '{request.product_id}' from {request.start_date} to {request.end_date}")
# Parse dates - handle both YYYY-M-D and YYYY-MM-DD formats
def parse_date(date_str: str) -> date:
try:
return date.fromisoformat(date_str)
except ValueError:
# Try to parse YYYY-M-D format and convert to YYYY-MM-DD
parts = date_str.split('-')
if len(parts) == 3:
year, month, day = parts
return date(int(year), int(month), int(day))
raise ValueError(f"Invalid date format: {date_str}")
start_date = parse_date(request.start_date)
end_date = parse_date(request.end_date)
# Query the registration data
result = registration_service.get_daily_registered_users(
start_date=start_date,
end_date=end_date,
product_id=request.product_id
)
# Format response
response = RegistrationTimeSeriesResponse(
metric_name="daily_registered_users",
data_points=[
RegistrationDataPoint(
date=date_str,
value=count,
product_id=request.product_id
)
for date_str, count in zip(result.dates, result.counts)
],
total_points=len(result.dates),
time_range={
"start": request.start_date,
"end": request.end_date
},
total_registrations=result.total_registrations
)
await module_logger.log_info(
f"Successfully queried registration data with {len(result.dates)} data points, total registrations: {result.total_registrations}")
return response