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 # Install dependencies
RUN pip install --no-cache-dir -r requirements.txt RUN pip install --no-cache-dir -r requirements.txt
# Copy environment file
COPY local.env .
# Copy application code # Copy application code
COPY . . 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 port
EXPOSE 8009 EXPOSE 8009

View File

@ -1,35 +1,28 @@
# 📊 Metrics Service # 📊 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) [![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) [![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) [![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 ## ✨ Features
### 📊 User Registration Statistics APIs ### 📊 Registration Analytics (StarRocks)
- **Date Range Query** - Query registration data for specific date ranges - Date Range Query (start_date ~ end_date)
- **Recent N Days Query** - Get registration data for the last N days - Typed responses with Pydantic models
- **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
### 🗄️ Database Integration ### 📈 Prometheus Metrics
- **StarRocks Database Connection** - Predefined PromQL metrics per product
- Host: `freeleaps-starrocks-cluster-fe-service.freeleaps-data-platform.svc` - Time-range queries and metric info discovery
- Port: `9030`
- Database: `freeleaps`
- Table: `dws_daily_registered_users`
### 🔧 Technical Features ### 🔧 Technical Features
- **Data Models**: Pydantic validation for data integrity - Data Models: Pydantic v2
- **Connection Management**: Automatic database connection and disconnection - Async HTTP and robust error handling
- **Error Handling**: Comprehensive exception handling with user-friendly error messages - Structured logging
- **Logging**: Structured logging using Loguru - Auto-generated Swagger/OpenAPI docs
- **API Documentation**: Auto-generated Swagger/OpenAPI documentation
## 📁 Project Structure ## 📁 Project Structure
@ -37,31 +30,43 @@ The Metrics service provides real-time APIs for querying user registration data
metrics/ metrics/
├── backend/ # Business logic layer ├── backend/ # Business logic layer
│ ├── infra/ # Infrastructure components │ ├── infra/ # Infrastructure components
│ │ └── database_client.py │ │ └── external_service/
│ │ ├── prometheus_client.py
│ │ └── starrocks_client.py
│ ├── models/ # Data models │ ├── models/ # Data models
│ │ └── user_registration_models.py │ │ └── user_registration_models.py
│ └── services/ # Business services │ └── services/ # Business services
│ ├── prometheus_metrics_service.py
│ └── registration_analytics_service.py │ └── registration_analytics_service.py
├── webapi/ # API layer ├── webapi/ # API layer
│ ├── routes/ # API endpoints │ ├── routes/ # API endpoints
│ │ └── registration_metrics.py │ │ ├── metrics/ # Aggregated routers (prefix: /api/metrics)
│ ├── config/ # Configuration │ │ ├── prometheus_metrics/
│ │ └── app_settings.py │ │ │ ├── __init__.py
│ ├── bootstrap/ # App initialization │ │ │ ├── available_metrics.py
│ │ └── app_factory.py │ │ │ ├── metric_info.py
│ └── main.py # FastAPI app entry point │ │ │ └── metrics_query.py
├── common/ # Shared utilities │ │ └── starrocks_metrics/
├── requirements.txt # Dependencies │ │ ├── __init__.py
├── Dockerfile # Container config │ │ ├── available_metrics.py
├── local.env # Environment variables │ │ ├── metric_info.py
└── README.md # Documentation │ │ └── metrics_query.py
│ ├── bootstrap/
│ │ └── application.py
│ └── main.py
├── common/
├── requirements.txt
├── Dockerfile
├── local.env
└── README.md
``` ```
## 🚀 Quick Start ## 🚀 Quick Start
### Prerequisites ### Prerequisites
- Python 3.12+ or Docker - Python 3.12+ or Docker
- Access to StarRocks database - Access to StarRocks database (for registration analytics)
- Access to Prometheus (for monitoring metrics)
### 🐍 Python Setup ### 🐍 Python Setup
@ -79,8 +84,12 @@ python3 -m uvicorn webapi.main:app --host 0.0.0.0 --port 8009 --reload
# 1. Build image # 1. Build image
docker build -t metrics:latest . docker build -t metrics:latest .
# 2. Run container # 2. Run container (env from baked local.env)
docker run --rm -p 8009:8009 metrics:latest 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 ### 📖 Access Documentation
@ -88,69 +97,36 @@ Visit `http://localhost:8009/docs` for interactive API documentation.
## 📊 API Endpoints ## 📊 API Endpoints
| Endpoint | Method | Description | All endpoints are exposed under the API base prefix `/api/metrics`.
|----------|--------|-------------|
| `/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 |
### 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 ```bash
# Get last 7 days curl -X POST "http://localhost:8009/api/metrics/starrocks/dru_query" \
curl "http://localhost:8009/api/metrics/recent-registered-users?days=7" -H "Content-Type: application/json" \
-d '{
# Get date range "product_id": "freeleaps",
curl "http://localhost:8009/api/metrics/daily-registered-users?start_date=2024-09-10&end_date=2024-09-20" "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"
``` ```
### Parameters ### Prometheus
- `start_date` / `end_date`: Date in `YYYY-MM-DD` format - POST `/api/metrics/prometheus/metrics_query` — Query metric time series by product/metric
- `days`: Number of days (max: 365) - GET `/api/metrics/prometheus/product/{product_id}/available-metrics` — List available metrics for product
- `product_id`: Product identifier (default: "freeleaps") - GET `/api/metrics/prometheus/product/{product_id}/metric/{metric_name}/info` — Get metric info
## 📈 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
}
```
## 🧪 Testing ## 🧪 Testing
### Quick Test
```bash ```bash
# Health check # Health check
curl http://localhost:8009/ 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 ## ⚙️ Configuration
### Environment Variables ### Environment Variables
@ -170,9 +146,12 @@ STARROCKS_DATABASE=freeleaps
LOG_BASE_PATH=./logs LOG_BASE_PATH=./logs
BACKEND_LOG_FILE_NAME=metrics BACKEND_LOG_FILE_NAME=metrics
APPLICATION_ACTIVITY_LOG=metrics-activity 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 ### 🐳 Docker Deployment
@ -206,20 +185,16 @@ python -m uvicorn webapi.main:app --reload
``` ```
## 📝 API Documentation ## 📝 API Documentation
- Swagger UI: `http://localhost:8009/docs`
- **Swagger UI**: `http://localhost:8009/docs` - ReDoc: `http://localhost:8009/redoc`
- **ReDoc**: `http://localhost:8009/redoc` - OpenAPI JSON: `http://localhost:8009/openapi.json`
- **OpenAPI JSON**: `http://localhost:8009/openapi.json`
## ⚠️ Important Notes ## ⚠️ Important Notes
- Date format: `YYYY-MM-DD` (single-digit month/day also accepted by API)
- Date format: `YYYY-MM-DD`
- Max query range: 365 days
- Default `product_id`: "freeleaps" - Default `product_id`: "freeleaps"
- Requires StarRocks database access - Requires StarRocks database access and/or Prometheus endpoint
## 🐛 Troubleshooting ## 🐛 Troubleshooting
| Issue | Solution | | Issue | Solution |
|-------|----------| |-------|----------|
| Port in use | `docker stop $(docker ps -q --filter ancestor=metrics:latest)` | | 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): def __exit__(self, exc_type, exc_val, exc_tb):
"""Context manager exit""" """Context manager exit"""
self.disconnect() self.disconnect()

View File

@ -1,7 +1,7 @@
from typing import List, Dict, Any from typing import List, Dict, Any
from datetime import date, timedelta from datetime import date, timedelta
from loguru import logger 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 from backend.models.user_registration_models import UserRegistrationResponse, DailyRegisteredUsers
@ -33,7 +33,6 @@ class RegistrationService:
raw_data = self.starrocks_client.get_daily_registered_users( raw_data = self.starrocks_client.get_daily_registered_users(
start_date, end_date, product_id start_date, end_date, product_id
) )
# Convert to DailyRegisteredUsers objects # Convert to DailyRegisteredUsers objects
daily_data = [ daily_data = [
DailyRegisteredUsers( DailyRegisteredUsers(
@ -44,7 +43,6 @@ class RegistrationService:
) )
for row in raw_data for row in raw_data
] ]
# Create date-to-count mapping # Create date-to-count mapping
data_dict = {str(item.date_id): item.registered_cnt for item in daily_data} 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 pytest-asyncio==0.21.2
pymysql==1.1.0 pymysql==1.1.0
sqlalchemy==2.0.23 sqlalchemy==2.0.23
python-dotenv

View File

@ -1,7 +1,7 @@
from fastapi import APIRouter 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 from webapi.routes.prometheus_metrics import api_router as prometheus_metrics_router
router = APIRouter() 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"]) 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