Compare commits
15 Commits
f4ca742f43
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ebff4a3fc | ||
|
|
3dd862b287 | ||
|
|
786f44a29c | ||
|
|
64ef3ff776 | ||
| fdb4cc98e8 | |||
| 903a77102c | |||
| 03be5ff8e7 | |||
| d8607b8efc | |||
| 8b871775e6 | |||
| 90de13452f | |||
| 6323fb6a26 | |||
| 728fe12a1a | |||
| eb6f3402f8 | |||
| f5f55bc8cf | |||
| c286106a7c |
@@ -2,10 +2,11 @@ version: "3.4"
|
||||
|
||||
|
||||
services:
|
||||
queues:
|
||||
configurator:
|
||||
image: mathwave/sprint-repo:configurator
|
||||
networks:
|
||||
- configurator
|
||||
- configurator-development
|
||||
- monitoring
|
||||
environment:
|
||||
MONGO_HOST: "mongo.develop.sprinthub.ru"
|
||||
MONGO_PASSWORD: $MONGO_PASSWORD_DEV
|
||||
@@ -18,5 +19,7 @@ services:
|
||||
order: start-first
|
||||
|
||||
networks:
|
||||
configurator:
|
||||
configurator-development:
|
||||
external: true
|
||||
monitoring:
|
||||
external: true
|
||||
|
||||
@@ -2,10 +2,11 @@ version: "3.4"
|
||||
|
||||
|
||||
services:
|
||||
queues:
|
||||
configurator:
|
||||
image: mathwave/sprint-repo:configurator
|
||||
networks:
|
||||
- configurator
|
||||
- monitoring
|
||||
environment:
|
||||
MONGO_HOST: "mongo.sprinthub.ru"
|
||||
MONGO_PASSWORD: $MONGO_PASSWORD_PROD
|
||||
@@ -20,3 +21,5 @@ services:
|
||||
networks:
|
||||
configurator:
|
||||
external: true
|
||||
monitoring:
|
||||
external: true
|
||||
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
run: docker push mathwave/sprint-repo:configurator
|
||||
deploy-dev:
|
||||
name: Deploy dev
|
||||
runs-on: [dev]
|
||||
runs-on: [prod]
|
||||
needs: push
|
||||
steps:
|
||||
- name: login
|
||||
@@ -37,7 +37,9 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: dev
|
||||
- name: network
|
||||
run: docker network create -d overlay --attachable configurator-development || true
|
||||
- name: deploy
|
||||
env:
|
||||
MONGO_PASSWORD_DEV: ${{ secrets.MONGO_PASSWORD_DEV }}
|
||||
run: docker stack deploy --with-registry-auth -c ./.deploy/deploy-dev.yaml infra
|
||||
run: docker stack deploy --with-registry-auth -c ./.deploy/deploy-dev.yaml infra-development
|
||||
|
||||
@@ -37,6 +37,8 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: prod
|
||||
- name: network
|
||||
run: docker network create -d overlay --attachable configurator || true
|
||||
- name: deploy
|
||||
env:
|
||||
MONGO_PASSWORD_PROD: ${{ secrets.MONGO_PASSWORD_PROD }}
|
||||
|
||||
0
app/middlewares/__init__.py
Normal file
0
app/middlewares/__init__.py
Normal file
14
app/middlewares/metrics.py
Normal file
14
app/middlewares/metrics.py
Normal file
@@ -0,0 +1,14 @@
|
||||
import datetime
|
||||
import zoneinfo
|
||||
from fastapi import Request, Response
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
|
||||
from app.utils.monitoring import monitoring
|
||||
|
||||
class MetricsMiddleware(BaseHTTPMiddleware):
|
||||
async def dispatch(self, request: Request, call_next):
|
||||
start = datetime.datetime.now(zoneinfo.ZoneInfo("Europe/Moscow"))
|
||||
response: Response = await call_next(request)
|
||||
end = datetime.datetime.now(zoneinfo.ZoneInfo("Europe/Moscow"))
|
||||
monitoring.send_metric(start, end, request.url.path, response.status_code, request.method)
|
||||
return response
|
||||
@@ -13,13 +13,19 @@ class RequestPostBody(pydantic.BaseModel):
|
||||
|
||||
class RequestPutBody(pydantic.BaseModel):
|
||||
id: str
|
||||
value: dict
|
||||
value: dict|list
|
||||
|
||||
|
||||
class RequestDeleteBody(pydantic.BaseModel):
|
||||
id: str
|
||||
|
||||
|
||||
class Config(pydantic.BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
value: dict|list
|
||||
|
||||
|
||||
router = fastapi.APIRouter()
|
||||
|
||||
|
||||
@@ -40,3 +46,15 @@ async def delete(body: RequestDeleteBody):
|
||||
changed = await configs.delete(id=bson.ObjectId(body.id))
|
||||
if not changed:
|
||||
raise fastapi.HTTPException(404)
|
||||
|
||||
|
||||
@router.get('/api/v1/configs')
|
||||
async def get(stage: str, project: str) -> list[Config]:
|
||||
return [
|
||||
Config(
|
||||
id=str(config._id),
|
||||
name=config.name,
|
||||
value=config.value,
|
||||
)
|
||||
for config in await configs.get(project=project, stage=stage)
|
||||
]
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import bson
|
||||
import fastapi
|
||||
import pydantic
|
||||
|
||||
@@ -11,9 +12,18 @@ class RequestPostBody(pydantic.BaseModel):
|
||||
|
||||
|
||||
class RequestPutBody(pydantic.BaseModel):
|
||||
id: str
|
||||
enabled: bool
|
||||
condition: str
|
||||
|
||||
|
||||
class RequestDeleteBody(pydantic.BaseModel):
|
||||
id: str
|
||||
|
||||
|
||||
class Experiment(pydantic.BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
stage: str
|
||||
project: str
|
||||
enabled: bool
|
||||
condition: str
|
||||
|
||||
@@ -28,13 +38,26 @@ async def post(body: RequestPostBody):
|
||||
|
||||
@router.put('/api/v1/experiments', status_code=fastapi.status.HTTP_202_ACCEPTED, responses={404: {'description': 'Not found'}})
|
||||
async def put(body: RequestPutBody):
|
||||
changed = await experiments.update(project=body.project, stage=body.stage, name=body.name, enabled=body.enabled, condition=body.condition)
|
||||
changed = await experiments.update(id=bson.ObjectId(body.id), enabled=body.enabled, condition=body.condition)
|
||||
if not changed:
|
||||
raise fastapi.HTTPException(404)
|
||||
|
||||
|
||||
@router.delete('/api/v1/experiments', status_code=fastapi.status.HTTP_202_ACCEPTED, responses={404: {'description': 'Not found'}})
|
||||
async def delete(body: RequestPostBody):
|
||||
changed = await experiments.delete(project=body.project, stage=body.stage, name=body.name)
|
||||
async def delete(body: RequestDeleteBody):
|
||||
changed = await experiments.delete(id=bson.ObjectId(body.id))
|
||||
if not changed:
|
||||
raise fastapi.HTTPException(404)
|
||||
|
||||
|
||||
@router.get('/api/v1/experiments')
|
||||
async def get(stage: str, project: str) -> list[Experiment]:
|
||||
return [
|
||||
Experiment(
|
||||
id=str(experiment._id),
|
||||
name=experiment.name,
|
||||
enabled=experiment.enabled,
|
||||
condition=experiment.condition,
|
||||
)
|
||||
for experiment in await experiments.get(project=project, stage=stage)
|
||||
]
|
||||
|
||||
@@ -20,7 +20,7 @@ class PlatformStaff(pydantic.BaseModel):
|
||||
|
||||
|
||||
class ResponseBody(pydantic.BaseModel):
|
||||
configs: dict[str, dict]
|
||||
configs: dict[str, dict|list]
|
||||
experiments: dict[str, ExperimentData]
|
||||
platform_staff: PlatformStaff
|
||||
|
||||
@@ -28,7 +28,7 @@ class ResponseBody(pydantic.BaseModel):
|
||||
router = fastapi.APIRouter()
|
||||
|
||||
|
||||
@router.post('/api/v1/fetch')
|
||||
@router.get('/api/v1/fetch')
|
||||
async def execute(stage: str, project: str):
|
||||
confs, exps, staffs = await asyncio.gather(
|
||||
configs.get(project=project, stage=stage),
|
||||
|
||||
@@ -18,13 +18,13 @@ def create_indexes():
|
||||
database.get_collection('configs').create_index([
|
||||
('stage', 1),
|
||||
('project', 1),
|
||||
('name', 1)
|
||||
])
|
||||
('name', 1),
|
||||
], unique=True)
|
||||
database.get_collection('experiments').create_index([
|
||||
('stage', 1),
|
||||
('project', 1),
|
||||
('name', 1)
|
||||
])
|
||||
('name', 1),
|
||||
], unique=True)
|
||||
database.get_collection('staff').create_index([
|
||||
('platform_id', 1),
|
||||
])
|
||||
], unique=True)
|
||||
|
||||
@@ -12,7 +12,7 @@ class Config(pydantic.BaseModel):
|
||||
name: str
|
||||
project: str
|
||||
stage: str
|
||||
value: dict
|
||||
value: dict|list
|
||||
_id: bson.ObjectId|None = None
|
||||
|
||||
|
||||
@@ -21,13 +21,13 @@ async def create(config: Config) -> str:
|
||||
return result.inserted_id
|
||||
|
||||
|
||||
async def update_data(project: str, stage: str, name: str, value: dict) -> bool:
|
||||
result = await collection.update_one({'project': project, 'stage': stage, 'name': name}, {'$set': {'value': value}})
|
||||
async def update_data(id: bson.ObjectId, value: dict|list) -> bool:
|
||||
result = await collection.update_one({'_id': id}, {'$set': {'value': value}})
|
||||
return result.modified_count != 0
|
||||
|
||||
|
||||
async def delete(project: str, stage: str, name: str) -> bool:
|
||||
result = await collection.delete_one({'project': project, 'stage': stage, 'name': name})
|
||||
async def delete(id: bson.ObjectId) -> bool:
|
||||
result = await collection.delete_one({'_id': id})
|
||||
return result.deleted_count != 0
|
||||
|
||||
|
||||
@@ -35,4 +35,5 @@ async def get(project: str, stage: str) -> list[Config]:
|
||||
result = []
|
||||
async for item in collection.find({'stage': stage, 'project': project}):
|
||||
result.append(Config.model_validate(item))
|
||||
result[-1]._id = item['_id']
|
||||
return result
|
||||
|
||||
@@ -22,13 +22,13 @@ async def create(experiment: Experiment) -> str:
|
||||
return result.inserted_id
|
||||
|
||||
|
||||
async def update(project: str, stage: str, name: str, enabled: bool, condition: str) -> bool:
|
||||
result = await collection.update_one({'project': project, 'stage': stage, 'name': name}, {'$set': {'enabled': enabled, 'condition': condition}})
|
||||
async def update(id: bson.ObjectId, enabled: bool, condition: str) -> bool:
|
||||
result = await collection.update_one({'_id': id}, {'$set': {'enabled': enabled, 'condition': condition}})
|
||||
return result.modified_count != 0
|
||||
|
||||
|
||||
async def delete(project: str, stage: str, name: str) -> bool:
|
||||
result = await collection.delete_one({'project': project, 'stage': stage, 'name': name})
|
||||
async def delete(id: bson.ObjectId) -> bool:
|
||||
result = await collection.delete_one({'_id': id})
|
||||
return result.deleted_count != 0
|
||||
|
||||
|
||||
@@ -36,4 +36,5 @@ async def get(project: str, stage: str) -> list[Experiment]:
|
||||
result = []
|
||||
async for item in collection.find({'stage': stage, 'project': project}):
|
||||
result.append(Experiment.model_validate(item))
|
||||
result[-1]._id = item['_id']
|
||||
return result
|
||||
|
||||
@@ -13,6 +13,7 @@ class Staff(pydantic.BaseModel):
|
||||
yandex_id: int|None = None
|
||||
telegram_id: int|None = None
|
||||
email: str|None = None
|
||||
telegram_username: str|None = None
|
||||
|
||||
|
||||
async def create(staff: Staff) -> str:
|
||||
@@ -20,8 +21,8 @@ async def create(staff: Staff) -> str:
|
||||
return result.inserted_id
|
||||
|
||||
|
||||
async def update(platform_id: int, vk_id: int|None, yandex_id: int|None, telegram_id: int|None, email: str|None) -> bool:
|
||||
result = await collection.update_one({'platform_id': platform_id}, {'$set': {'vk_id': vk_id, 'yandex_id': yandex_id, 'telegram_id': telegram_id, 'email': email}})
|
||||
async def update(platform_id: int, vk_id: int|None, yandex_id: int|None, telegram_id: int|None, email: str|None, telegram_username: str|None) -> bool:
|
||||
result = await collection.update_one({'platform_id': platform_id}, {'$set': {'vk_id': vk_id, 'yandex_id': yandex_id, 'telegram_id': telegram_id, 'email': email, 'telegram_username': telegram_username}})
|
||||
return result.modified_count != 0
|
||||
|
||||
|
||||
|
||||
25
app/utils/monitoring.py
Normal file
25
app/utils/monitoring.py
Normal file
@@ -0,0 +1,25 @@
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
import datetime
|
||||
import requests
|
||||
|
||||
|
||||
class Monitroing:
|
||||
def __init__(self):
|
||||
self.executor = ThreadPoolExecutor(max_workers=1)
|
||||
|
||||
def send_metric(self, start: datetime.datetime, end: datetime.datetime, endpoint: str, status_code: int, method: str):
|
||||
def send():
|
||||
requests.post(f'http://monitoring:1237/api/v1/metrics/endpoint', json={
|
||||
'timestamp': start.strftime("%Y-%m-%dT%H:%M:%S") + "Z",
|
||||
'service': 'configurator',
|
||||
'endpoint': endpoint,
|
||||
'status_code': status_code,
|
||||
'response_time': (end - start).microseconds // 1000,
|
||||
'method': method,
|
||||
'environment': 'production',
|
||||
})
|
||||
|
||||
self.executor.submit(send)
|
||||
|
||||
|
||||
monitoring = Monitroing()
|
||||
2
main.py
2
main.py
@@ -1,6 +1,7 @@
|
||||
import fastapi
|
||||
import uvicorn
|
||||
|
||||
from app.middlewares.metrics import MetricsMiddleware
|
||||
from app.routers import experiments
|
||||
from app.routers import configs
|
||||
from app.routers import staff
|
||||
@@ -10,6 +11,7 @@ from app.storage import mongo
|
||||
|
||||
|
||||
app = fastapi.FastAPI()
|
||||
app.add_middleware(MetricsMiddleware)
|
||||
|
||||
app.include_router(experiments.router)
|
||||
app.include_router(configs.router)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
annotated-types==0.7.0
|
||||
anyio==4.6.2.post1
|
||||
APScheduler==3.10.4
|
||||
certifi==2025.6.15
|
||||
charset-normalizer==3.4.2
|
||||
click==8.1.7
|
||||
dnspython==2.7.0
|
||||
fastapi==0.115.4
|
||||
@@ -12,9 +14,11 @@ pydantic_core==2.23.4
|
||||
pymongo==4.9.2
|
||||
pytz==2024.2
|
||||
redis==5.2.0
|
||||
requests==2.32.4
|
||||
six==1.16.0
|
||||
sniffio==1.3.1
|
||||
starlette==0.41.2
|
||||
typing_extensions==4.12.2
|
||||
tzlocal==5.2
|
||||
urllib3==2.4.0
|
||||
uvicorn==0.32.0
|
||||
|
||||
Reference in New Issue
Block a user