Add metrics service with user registration API endpoints
- Add complete metrics microservice structure - Implement StarRocks database integration - Add user registration data query APIs: - Daily registered users by date range - Recent N days registration data - Registration data by start date and days - Registration summary statistics - Add comprehensive error handling and logging - Include test scripts and documentation
This commit is contained in:
parent
046f9ffdd2
commit
44f08eee68
75
apps/metrics/.gitignore
vendored
Normal file
75
apps/metrics/.gitignore
vendored
Normal file
@ -0,0 +1,75 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# Virtual environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# Logs
|
||||
logs/
|
||||
*.log
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
|
||||
# Test coverage
|
||||
htmlcov/
|
||||
.tox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# Environments
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
37
apps/metrics/Dockerfile
Normal file
37
apps/metrics/Dockerfile
Normal file
@ -0,0 +1,37 @@
|
||||
FROM python:3.10-slim-bullseye
|
||||
|
||||
# docker settings
|
||||
ARG CONTAINER_APP_ROOT="/app"
|
||||
ENV APP_NAME="metrics"
|
||||
|
||||
# Service dependencies
|
||||
ENV DEVSVC_WEBAPI_URL_BASE="http://devsvc:8007/api/devsvc"
|
||||
ENV NOTIFICATION_WEBAPI_URL_BASE="http://notification:8003/api/notification/"
|
||||
|
||||
# JWT settings
|
||||
ENV JWT_SECRET_KEY="8f87ca8c3c9c3df09a9c78e0adb0927855568f6072d9efc892534aee35f5867b"
|
||||
ENV JWT_ALGORITHM="HS256"
|
||||
|
||||
# Site settings
|
||||
ENV SERVICE_API_ACCESS_HOST=0.0.0.0
|
||||
ENV SERVICE_API_ACCESS_PORT=8009
|
||||
ENV MONGODB_NAME=freeleaps2
|
||||
ENV MONGODB_PORT=27017
|
||||
ENV MONGODB_URI="mongodb://localhost:27017/"
|
||||
|
||||
# Log settings
|
||||
ENV LOG_BASE_PATH=$CONTAINER_APP_ROOT/log/$APP_NAME
|
||||
ENV BACKEND_LOG_FILE_NAME=$APP_NAME
|
||||
ENV APPLICATION_ACTIVITY_LOG=$APP_NAME-activity
|
||||
|
||||
WORKDIR ${CONTAINER_APP_ROOT}
|
||||
COPY requirements.txt .
|
||||
|
||||
RUN pip install --upgrade pip
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY . ${CONTAINER_APP_ROOT}
|
||||
|
||||
EXPOSE ${SERVICE_API_ACCESS_PORT}
|
||||
# Using shell to expand environment to ensure pass the actual environment value to uvicorn
|
||||
CMD uvicorn webapi.main:app --reload --port=$SERVICE_API_ACCESS_PORT --host=$SERVICE_API_ACCESS_HOST
|
||||
1
apps/metrics/__init__.py
Normal file
1
apps/metrics/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
# Metrics Service
|
||||
1
apps/metrics/backend/__init__.py
Normal file
1
apps/metrics/backend/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
# Backend module
|
||||
0
apps/metrics/backend/annotation/__init__.py
Normal file
0
apps/metrics/backend/annotation/__init__.py
Normal file
0
apps/metrics/backend/application/__init__.py
Normal file
0
apps/metrics/backend/application/__init__.py
Normal file
0
apps/metrics/backend/business/__init__.py
Normal file
0
apps/metrics/backend/business/__init__.py
Normal file
0
apps/metrics/backend/infra/__init__.py
Normal file
0
apps/metrics/backend/infra/__init__.py
Normal file
90
apps/metrics/backend/infra/starrocks_client.py
Normal file
90
apps/metrics/backend/infra/starrocks_client.py
Normal file
@ -0,0 +1,90 @@
|
||||
import pymysql
|
||||
from typing import List, Dict, Any, Optional
|
||||
from datetime import date
|
||||
from loguru import logger
|
||||
from webapi.config.site_settings import site_settings
|
||||
|
||||
|
||||
class StarRocksClient:
|
||||
"""StarRocks database client for querying user registration data"""
|
||||
|
||||
def __init__(self):
|
||||
self.host = site_settings.STARROCKS_HOST
|
||||
self.port = site_settings.STARROCKS_PORT
|
||||
self.user = site_settings.STARROCKS_USER
|
||||
self.password = site_settings.STARROCKS_PASSWORD
|
||||
self.database = site_settings.STARROCKS_DATABASE
|
||||
self.connection = None
|
||||
|
||||
def connect(self) -> bool:
|
||||
"""Establish connection to StarRocks database"""
|
||||
try:
|
||||
self.connection = pymysql.connect(
|
||||
host=self.host,
|
||||
port=self.port,
|
||||
user=self.user,
|
||||
password=self.password,
|
||||
database=self.database,
|
||||
charset='utf8mb4',
|
||||
autocommit=True
|
||||
)
|
||||
logger.info(f"Successfully connected to StarRocks at {self.host}:{self.port}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to connect to StarRocks: {e}")
|
||||
return False
|
||||
|
||||
def disconnect(self):
|
||||
"""Close database connection"""
|
||||
if self.connection:
|
||||
self.connection.close()
|
||||
self.connection = None
|
||||
logger.info("Disconnected from StarRocks")
|
||||
|
||||
def execute_query(self, query: str, params: Optional[tuple] = None) -> List[Dict[str, Any]]:
|
||||
"""Execute SQL query and return results"""
|
||||
if not self.connection:
|
||||
if not self.connect():
|
||||
raise Exception("Failed to connect to StarRocks database")
|
||||
|
||||
try:
|
||||
with self.connection.cursor(pymysql.cursors.DictCursor) as cursor:
|
||||
cursor.execute(query, params)
|
||||
results = cursor.fetchall()
|
||||
logger.info(f"Query executed successfully, returned {len(results)} rows")
|
||||
return results
|
||||
except Exception as e:
|
||||
logger.error(f"Query execution failed: {e}")
|
||||
raise e
|
||||
|
||||
def get_daily_registered_users(
|
||||
self,
|
||||
start_date: date,
|
||||
end_date: date,
|
||||
product_id: str = "freeleaps"
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Query daily registered users from StarRocks"""
|
||||
query = """
|
||||
SELECT
|
||||
date_id,
|
||||
product_id,
|
||||
registered_cnt,
|
||||
updated_at
|
||||
FROM dws_daily_registered_users
|
||||
WHERE date_id >= %s
|
||||
AND date_id <= %s
|
||||
AND product_id = %s
|
||||
ORDER BY date_id ASC
|
||||
"""
|
||||
|
||||
params = (start_date, end_date, product_id)
|
||||
return self.execute_query(query, params)
|
||||
|
||||
def __enter__(self):
|
||||
"""Context manager entry"""
|
||||
self.connect()
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
"""Context manager exit"""
|
||||
self.disconnect()
|
||||
0
apps/metrics/backend/models/__init__.py
Normal file
0
apps/metrics/backend/models/__init__.py
Normal file
26
apps/metrics/backend/models/registered_users.py
Normal file
26
apps/metrics/backend/models/registered_users.py
Normal file
@ -0,0 +1,26 @@
|
||||
from pydantic import BaseModel
|
||||
from datetime import date, datetime
|
||||
from typing import List, Optional
|
||||
|
||||
|
||||
class DailyRegisteredUsers(BaseModel):
|
||||
"""Daily registered users data model"""
|
||||
date_id: date
|
||||
product_id: str = "freeleaps"
|
||||
registered_cnt: int
|
||||
updated_at: Optional[datetime] = None
|
||||
|
||||
|
||||
class UserRegistrationQuery(BaseModel):
|
||||
"""Query parameters for user registration data"""
|
||||
start_date: date
|
||||
end_date: date
|
||||
product_id: str = "freeleaps"
|
||||
|
||||
|
||||
class UserRegistrationResponse(BaseModel):
|
||||
"""Response model for user registration data"""
|
||||
dates: List[str]
|
||||
counts: List[int]
|
||||
total_registrations: int
|
||||
query_period: str
|
||||
0
apps/metrics/backend/services/__init__.py
Normal file
0
apps/metrics/backend/services/__init__.py
Normal file
125
apps/metrics/backend/services/registration_service.py
Normal file
125
apps/metrics/backend/services/registration_service.py
Normal file
@ -0,0 +1,125 @@
|
||||
from typing import List, Dict, Any
|
||||
from datetime import date, timedelta
|
||||
from loguru import logger
|
||||
from backend.infra.starrocks_client import StarRocksClient
|
||||
from backend.models.registered_users import UserRegistrationResponse, DailyRegisteredUsers
|
||||
|
||||
|
||||
class RegistrationService:
|
||||
"""Service for handling user registration data queries"""
|
||||
|
||||
def __init__(self):
|
||||
self.starrocks_client = StarRocksClient()
|
||||
|
||||
def get_daily_registered_users(
|
||||
self,
|
||||
start_date: date,
|
||||
end_date: date,
|
||||
product_id: str = "freeleaps"
|
||||
) -> UserRegistrationResponse:
|
||||
"""
|
||||
Get daily registered users count for a date range
|
||||
|
||||
Args:
|
||||
start_date: Start date for the query
|
||||
end_date: End date for the query
|
||||
product_id: Product identifier (default: freeleaps)
|
||||
|
||||
Returns:
|
||||
UserRegistrationResponse with dates and counts
|
||||
"""
|
||||
try:
|
||||
# Query data from StarRocks
|
||||
raw_data = self.starrocks_client.get_daily_registered_users(
|
||||
start_date, end_date, product_id
|
||||
)
|
||||
|
||||
# Convert to DailyRegisteredUsers objects
|
||||
daily_data = [
|
||||
DailyRegisteredUsers(
|
||||
date_id=row['date_id'],
|
||||
product_id=row['product_id'],
|
||||
registered_cnt=row['registered_cnt'],
|
||||
updated_at=row.get('updated_at')
|
||||
)
|
||||
for row in raw_data
|
||||
]
|
||||
|
||||
# Create date-to-count mapping
|
||||
data_dict = {str(item.date_id): item.registered_cnt for item in daily_data}
|
||||
|
||||
# Generate complete date range
|
||||
dates = []
|
||||
counts = []
|
||||
current_date = start_date
|
||||
|
||||
while current_date <= end_date:
|
||||
date_str = str(current_date)
|
||||
dates.append(date_str)
|
||||
counts.append(data_dict.get(date_str, 0))
|
||||
current_date += timedelta(days=1)
|
||||
|
||||
# Calculate total registrations
|
||||
total_registrations = sum(counts)
|
||||
|
||||
logger.info(
|
||||
f"Retrieved registration data for {len(dates)} days, "
|
||||
f"total registrations: {total_registrations}"
|
||||
)
|
||||
|
||||
return UserRegistrationResponse(
|
||||
dates=dates,
|
||||
counts=counts,
|
||||
total_registrations=total_registrations,
|
||||
query_period=f"{start_date} to {end_date}"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get daily registered users: {e}")
|
||||
raise e
|
||||
|
||||
def get_registration_summary(
|
||||
self,
|
||||
start_date: date,
|
||||
end_date: date,
|
||||
product_id: str = "freeleaps"
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Get summary statistics for user registrations
|
||||
|
||||
Args:
|
||||
start_date: Start date for the query
|
||||
end_date: End date for the query
|
||||
product_id: Product identifier
|
||||
|
||||
Returns:
|
||||
Dictionary with summary statistics
|
||||
"""
|
||||
try:
|
||||
response = self.get_daily_registered_users(start_date, end_date, product_id)
|
||||
|
||||
if not response.counts:
|
||||
return {
|
||||
"total_registrations": 0,
|
||||
"average_daily": 0,
|
||||
"max_daily": 0,
|
||||
"min_daily": 0,
|
||||
"days_with_registrations": 0,
|
||||
"total_days": len(response.dates)
|
||||
}
|
||||
|
||||
counts = response.counts
|
||||
non_zero_counts = [c for c in counts if c > 0]
|
||||
|
||||
return {
|
||||
"total_registrations": response.total_registrations,
|
||||
"average_daily": round(sum(counts) / len(counts), 2),
|
||||
"max_daily": max(counts),
|
||||
"min_daily": min(counts),
|
||||
"days_with_registrations": len(non_zero_counts),
|
||||
"total_days": len(response.dates)
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get registration summary: {e}")
|
||||
raise e
|
||||
0
apps/metrics/common/__init__.py
Normal file
0
apps/metrics/common/__init__.py
Normal file
16
apps/metrics/requirements.txt
Normal file
16
apps/metrics/requirements.txt
Normal file
@ -0,0 +1,16 @@
|
||||
fastapi==0.114.0
|
||||
pydantic==2.9.2
|
||||
loguru==0.7.2
|
||||
uvicorn==0.23.2
|
||||
beanie==1.21.0
|
||||
pika==1.3.2
|
||||
aio-pika
|
||||
httpx
|
||||
pydantic-settings
|
||||
python-jose
|
||||
passlib[bcrypt]
|
||||
prometheus-fastapi-instrumentator==7.0.2
|
||||
pytest==8.4.1
|
||||
pytest-asyncio==0.21.2
|
||||
pymysql==1.1.0
|
||||
sqlalchemy==2.0.23
|
||||
38
apps/metrics/start_fastapi.sh
Executable file
38
apps/metrics/start_fastapi.sh
Executable file
@ -0,0 +1,38 @@
|
||||
#!/bin/bash
|
||||
rp=$(dirname "$(realpath '$1')")
|
||||
pushd $rp
|
||||
|
||||
APP_NAME=metrics
|
||||
APP_PARENT_FOLDER=apps
|
||||
|
||||
GIT_REPO_ROOT=$(git rev-parse --show-toplevel)
|
||||
CODEBASE_ROOT=$GIT_REPO_ROOT/$APP_PARENT_FOLDER/$APP_NAME
|
||||
SITE_DEPLOY_FOLDER=$GIT_REPO_ROOT/sites/$APP_NAME/deploy
|
||||
|
||||
echo APP_NAME=$APP_NAME > .env
|
||||
cat $SITE_DEPLOY_FOLDER/common/.env >> .env
|
||||
echo GIT_REPO_ROOT=$(git rev-parse --show-toplevel) >> .env
|
||||
echo CODEBASE_ROOT=$GIT_REPO_ROOT/$APP_PARENT_FOLDER/$APP_NAME >> .env
|
||||
echo SITE_DEPLOY_FOLDER=$GIT_REPO_ROOT/sites/$APP_NAME/deploy >> .env
|
||||
cat $SITE_DEPLOY_FOLDER/common/.host.env >> .env
|
||||
cat $SITE_DEPLOY_FOLDER/local/.env >> .env
|
||||
|
||||
. .env
|
||||
|
||||
if [ -d "$VENV_DIR" ]
|
||||
then
|
||||
echo "Folder $VENV_DIR exists. Proceed to next steps"
|
||||
else
|
||||
echo "Folder $VENV_DIR doesn't exist. create it"
|
||||
sudo apt install python3-pip
|
||||
python3 -m pip install virtualenv
|
||||
python3 -m virtualenv $VENV_DIR
|
||||
fi
|
||||
|
||||
source $VENV_DIR/bin/activate
|
||||
pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
|
||||
uvicorn webapi.main:app --reload --host 0.0.0.0 --port $SERVICE_API_ACCESS_PORT
|
||||
|
||||
popd
|
||||
106
apps/metrics/test_registration_api.py
Executable file
106
apps/metrics/test_registration_api.py
Executable file
@ -0,0 +1,106 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script for registration API endpoints
|
||||
"""
|
||||
import requests
|
||||
import json
|
||||
from datetime import date, timedelta
|
||||
|
||||
# API base URL
|
||||
BASE_URL = "http://localhost:8009"
|
||||
|
||||
def test_daily_registered_users():
|
||||
"""Test the daily registered users endpoint"""
|
||||
print("Testing daily registered users endpoint...")
|
||||
|
||||
# Test with last 7 days
|
||||
end_date = date.today()
|
||||
start_date = end_date - timedelta(days=6)
|
||||
|
||||
url = f"{BASE_URL}/api/metrics/daily-registered-users"
|
||||
params = {
|
||||
"start_date": str(start_date),
|
||||
"end_date": str(end_date),
|
||||
"product_id": "freeleaps"
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.get(url, params=params)
|
||||
print(f"Status Code: {response.status_code}")
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
print(f"Response: {json.dumps(data, indent=2)}")
|
||||
print(f"Number of days: {len(data['dates'])}")
|
||||
print(f"Total registrations: {data['total_registrations']}")
|
||||
else:
|
||||
print(f"Error: {response.text}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Request failed: {e}")
|
||||
|
||||
def test_registration_summary():
|
||||
"""Test the registration summary endpoint"""
|
||||
print("\nTesting registration summary endpoint...")
|
||||
|
||||
end_date = date.today()
|
||||
start_date = end_date - timedelta(days=6)
|
||||
|
||||
url = f"{BASE_URL}/api/metrics/registration-summary"
|
||||
params = {
|
||||
"start_date": str(start_date),
|
||||
"end_date": str(end_date),
|
||||
"product_id": "freeleaps"
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.get(url, params=params)
|
||||
print(f"Status Code: {response.status_code}")
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
print(f"Summary: {json.dumps(data, indent=2)}")
|
||||
else:
|
||||
print(f"Error: {response.text}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Request failed: {e}")
|
||||
|
||||
def test_post_method():
|
||||
"""Test the POST method for daily registered users"""
|
||||
print("\nTesting POST method for daily registered users...")
|
||||
|
||||
end_date = date.today()
|
||||
start_date = end_date - timedelta(days=6)
|
||||
|
||||
url = f"{BASE_URL}/api/metrics/daily-registered-users"
|
||||
payload = {
|
||||
"start_date": str(start_date),
|
||||
"end_date": str(end_date),
|
||||
"product_id": "freeleaps"
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.post(url, json=payload)
|
||||
print(f"Status Code: {response.status_code}")
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
print(f"Response: {json.dumps(data, indent=2)}")
|
||||
else:
|
||||
print(f"Error: {response.text}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Request failed: {e}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("Starting registration API tests...")
|
||||
print(f"Testing against: {BASE_URL}")
|
||||
print("=" * 50)
|
||||
|
||||
test_daily_registered_users()
|
||||
test_registration_summary()
|
||||
test_post_method()
|
||||
|
||||
print("\n" + "=" * 50)
|
||||
print("Tests completed!")
|
||||
0
apps/metrics/tests/__init__.py
Normal file
0
apps/metrics/tests/__init__.py
Normal file
1
apps/metrics/webapi/__init__.py
Normal file
1
apps/metrics/webapi/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
# WebAPI module
|
||||
0
apps/metrics/webapi/bootstrap/__init__.py
Normal file
0
apps/metrics/webapi/bootstrap/__init__.py
Normal file
69
apps/metrics/webapi/bootstrap/application.py
Normal file
69
apps/metrics/webapi/bootstrap/application.py
Normal file
@ -0,0 +1,69 @@
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from prometheus_fastapi_instrumentator import Instrumentator
|
||||
from webapi.config.site_settings import site_settings
|
||||
from loguru import logger
|
||||
import os
|
||||
|
||||
|
||||
def create_app() -> FastAPI:
|
||||
"""
|
||||
Create and configure the FastAPI application
|
||||
"""
|
||||
app = FastAPI(
|
||||
title="Metrics Service API",
|
||||
description="Metrics Service for Freeleaps Platform",
|
||||
version="1.0.0",
|
||||
docs_url="/docs",
|
||||
redoc_url="/redoc"
|
||||
)
|
||||
|
||||
# Add CORS middleware
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# Setup logging
|
||||
setup_logging()
|
||||
|
||||
# Setup Prometheus metrics
|
||||
Instrumentator().instrument(app).expose(app)
|
||||
|
||||
# Include routers
|
||||
# from webapi.routes import health, api
|
||||
# app.include_router(health.router, prefix="/health", tags=["health"])
|
||||
# app.include_router(api.router, prefix="/api/metrics", tags=["metrics"])
|
||||
# Note: Registration router is included in main.py
|
||||
|
||||
return app
|
||||
|
||||
|
||||
def setup_logging():
|
||||
"""
|
||||
Setup logging configuration
|
||||
"""
|
||||
# Create log directory if it doesn't exist
|
||||
log_dir = site_settings.LOG_BASE_PATH
|
||||
os.makedirs(log_dir, exist_ok=True)
|
||||
|
||||
# Configure loguru
|
||||
logger.add(
|
||||
f"{log_dir}/{site_settings.BACKEND_LOG_FILE_NAME}.log",
|
||||
rotation="1 day",
|
||||
retention="30 days",
|
||||
level="INFO",
|
||||
format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {name}:{function}:{line} | {message}"
|
||||
)
|
||||
|
||||
logger.add(
|
||||
f"{log_dir}/{site_settings.APPLICATION_ACTIVITY_LOG}.log",
|
||||
rotation="1 day",
|
||||
retention="30 days",
|
||||
level="INFO",
|
||||
format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {name}:{function}:{line} | {message}",
|
||||
filter=lambda record: record["level"].name == "INFO"
|
||||
)
|
||||
0
apps/metrics/webapi/config/__init__.py
Normal file
0
apps/metrics/webapi/config/__init__.py
Normal file
41
apps/metrics/webapi/config/site_settings.py
Normal file
41
apps/metrics/webapi/config/site_settings.py
Normal file
@ -0,0 +1,41 @@
|
||||
from pydantic_settings import BaseSettings
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class SiteSettings(BaseSettings):
|
||||
# Server settings
|
||||
SERVER_HOST: str = "0.0.0.0"
|
||||
SERVER_PORT: int = 8009
|
||||
SERVICE_API_ACCESS_HOST: str = "0.0.0.0"
|
||||
SERVICE_API_ACCESS_PORT: int = 8009
|
||||
|
||||
# Database settings
|
||||
MONGODB_URI: str = "mongodb://localhost:27017/"
|
||||
MONGODB_NAME: str = "freeleaps2"
|
||||
MONGODB_PORT: int = 27017
|
||||
|
||||
# JWT settings
|
||||
JWT_SECRET_KEY: str = "8f87ca8c3c9c3df09a9c78e0adb0927855568f6072d9efc892534aee35f5867b"
|
||||
JWT_ALGORITHM: str = "HS256"
|
||||
|
||||
# Log settings
|
||||
LOG_BASE_PATH: str = "./logs"
|
||||
BACKEND_LOG_FILE_NAME: str = "metrics"
|
||||
APPLICATION_ACTIVITY_LOG: str = "metrics-activity"
|
||||
|
||||
# Service dependencies
|
||||
DEVSVC_WEBAPI_URL_BASE: str = "http://devsvc:8007/api/devsvc"
|
||||
NOTIFICATION_WEBAPI_URL_BASE: str = "http://notification:8003/api/notification/"
|
||||
|
||||
# StarRocks database settings
|
||||
STARROCKS_HOST: str = "freeleaps-starrocks-cluster-fe-service.freeleaps-data-platform.svc"
|
||||
STARROCKS_PORT: int = 9030
|
||||
STARROCKS_USER: str = "root"
|
||||
STARROCKS_PASSWORD: str = ""
|
||||
STARROCKS_DATABASE: str = "freeleaps"
|
||||
|
||||
class Config:
|
||||
env_file = ".env"
|
||||
|
||||
|
||||
site_settings = SiteSettings()
|
||||
36
apps/metrics/webapi/main.py
Normal file
36
apps/metrics/webapi/main.py
Normal file
@ -0,0 +1,36 @@
|
||||
from webapi.bootstrap.application import create_app
|
||||
from webapi.config.site_settings import site_settings
|
||||
from fastapi.responses import RedirectResponse
|
||||
import uvicorn
|
||||
from typing import Any
|
||||
from webapi.routes import registration
|
||||
|
||||
|
||||
app = create_app()
|
||||
|
||||
# Include routers
|
||||
app.include_router(registration.router)
|
||||
|
||||
|
||||
@app.get("/", status_code=301)
|
||||
async def root():
|
||||
"""
|
||||
TODO: redirect client to /docs
|
||||
"""
|
||||
return RedirectResponse("docs")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
uvicorn.run(
|
||||
app="main:app", host=site_settings.SERVER_HOST, port=site_settings.SERVER_PORT
|
||||
)
|
||||
|
||||
|
||||
def get_context() -> Any:
|
||||
# Define your context function. This is where you can set up authentication, database connections, etc.
|
||||
return {}
|
||||
|
||||
|
||||
def get_root_value() -> Any:
|
||||
# Define your root value function. This is where you can set up the root value for GraphQL.
|
||||
return {}
|
||||
0
apps/metrics/webapi/providers/__init__.py
Normal file
0
apps/metrics/webapi/providers/__init__.py
Normal file
0
apps/metrics/webapi/routes/__init__.py
Normal file
0
apps/metrics/webapi/routes/__init__.py
Normal file
229
apps/metrics/webapi/routes/registration.py
Normal file
229
apps/metrics/webapi/routes/registration.py
Normal file
@ -0,0 +1,229 @@
|
||||
from fastapi import APIRouter, HTTPException, Query
|
||||
from datetime import date, datetime, timedelta
|
||||
from typing import Optional
|
||||
from loguru import logger
|
||||
from backend.services.registration_service import RegistrationService
|
||||
from backend.models.registered_users import UserRegistrationResponse, UserRegistrationQuery
|
||||
|
||||
router = APIRouter(prefix="/api/metrics", 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)}"
|
||||
)
|
||||
Loading…
Reference in New Issue
Block a user