Добавлен сбор телеметрии на ВМ2
This commit is contained in:
@@ -12,20 +12,38 @@ from sqlalchemy import text
|
||||
from app.config import Settings, load_settings
|
||||
from app.repository import Repository
|
||||
from app.security import WebhookValidationError, parse_bounded_form, validate_webhook
|
||||
from app.telemetry import (
|
||||
init_telemetry,
|
||||
instrument_fastapi,
|
||||
log_event,
|
||||
record_webhook,
|
||||
shutdown_telemetry,
|
||||
)
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
settings = load_settings()
|
||||
app.state.settings = settings
|
||||
app.state.repository = (
|
||||
Repository(settings.database_url.get_secret_value(), settings.db_pool_size)
|
||||
if settings.enabled and settings.database_url
|
||||
else None
|
||||
)
|
||||
yield
|
||||
if app.state.repository:
|
||||
await app.state.repository.close()
|
||||
init_telemetry("bitrix-sync-api")
|
||||
app.state.repository = None
|
||||
try:
|
||||
settings = load_settings()
|
||||
app.state.settings = settings
|
||||
app.state.repository = (
|
||||
Repository(settings.database_url.get_secret_value(), settings.db_pool_size)
|
||||
if settings.enabled and settings.database_url
|
||||
else None
|
||||
)
|
||||
log_event(
|
||||
"service.started",
|
||||
"Bitrix sync API started",
|
||||
attributes={"mode": settings.mode},
|
||||
)
|
||||
yield
|
||||
finally:
|
||||
if app.state.repository:
|
||||
await app.state.repository.close()
|
||||
log_event("service.stopped", "Bitrix sync API stopped")
|
||||
shutdown_telemetry()
|
||||
|
||||
|
||||
app = FastAPI(
|
||||
@@ -35,6 +53,7 @@ app = FastAPI(
|
||||
redoc_url=None,
|
||||
lifespan=lifespan,
|
||||
)
|
||||
instrument_fastapi(app)
|
||||
|
||||
|
||||
def settings(request: Request) -> Settings:
|
||||
@@ -107,29 +126,30 @@ async def alert_webhook(
|
||||
|
||||
|
||||
async def _receive(request: Request, receiver: str, query: dict[str, str]) -> Response:
|
||||
config = settings(request)
|
||||
if not config.enabled:
|
||||
raise HTTPException(status_code=503, detail="sync_disabled")
|
||||
if request.headers.get("content-type", "").split(";", 1)[0].lower() != (
|
||||
"application/x-www-form-urlencoded"
|
||||
):
|
||||
raise HTTPException(status_code=400, detail="invalid_content_type")
|
||||
content_length = request.headers.get("content-length")
|
||||
if content_length and (
|
||||
not content_length.isdigit() or int(content_length) > config.webhook_max_body_bytes
|
||||
):
|
||||
raise HTTPException(status_code=413, detail="body_too_large")
|
||||
body = await request.body()
|
||||
if len(body) > config.webhook_max_body_bytes:
|
||||
raise HTTPException(status_code=413, detail="body_too_large")
|
||||
form = parse_bounded_form(body, max_fields=config.webhook_max_fields)
|
||||
# The container is reachable only from the trusted VM2 nginx network.
|
||||
# nginx overwrites X-Real-IP from the TCP peer after its CIDR check.
|
||||
source_ip = request.headers.get("x-real-ip") or (request.client.host if request.client else "")
|
||||
alert_entity_type_id = (
|
||||
await _alert_entity_type(repository(request)) if receiver == "alert" else None
|
||||
)
|
||||
try:
|
||||
config = settings(request)
|
||||
if not config.enabled:
|
||||
raise HTTPException(status_code=503, detail="sync_disabled")
|
||||
if request.headers.get("content-type", "").split(";", 1)[0].lower() != (
|
||||
"application/x-www-form-urlencoded"
|
||||
):
|
||||
raise HTTPException(status_code=400, detail="invalid_content_type")
|
||||
content_length = request.headers.get("content-length")
|
||||
if content_length and (
|
||||
not content_length.isdigit() or int(content_length) > config.webhook_max_body_bytes
|
||||
):
|
||||
raise HTTPException(status_code=413, detail="body_too_large")
|
||||
body = await request.body()
|
||||
if len(body) > config.webhook_max_body_bytes:
|
||||
raise HTTPException(status_code=413, detail="body_too_large")
|
||||
form = parse_bounded_form(body, max_fields=config.webhook_max_fields)
|
||||
# nginx overwrites X-Real-IP from the TCP peer after its CIDR check.
|
||||
source_ip = request.headers.get("x-real-ip") or (
|
||||
request.client.host if request.client else ""
|
||||
)
|
||||
alert_entity_type_id = (
|
||||
await _alert_entity_type(repository(request)) if receiver == "alert" else None
|
||||
)
|
||||
event = validate_webhook(
|
||||
receiver,
|
||||
query,
|
||||
@@ -139,12 +159,21 @@ async def _receive(request: Request, receiver: str, query: dict[str, str]) -> Re
|
||||
alert_entity_type_id=alert_entity_type_id,
|
||||
)
|
||||
except PermissionError as exc:
|
||||
record_webhook(receiver, "rejected")
|
||||
raise HTTPException(status_code=403, detail="forbidden") from exc
|
||||
except WebhookValidationError as exc:
|
||||
record_webhook(receiver, "rejected")
|
||||
raise HTTPException(status_code=400, detail="malformed_webhook") from exc
|
||||
except HTTPException:
|
||||
record_webhook(receiver, "rejected")
|
||||
raise
|
||||
except Exception:
|
||||
record_webhook(receiver, "error")
|
||||
raise
|
||||
await repository(request).insert_webhook(
|
||||
event.receiver_type, event.event_type, event.entity_id, event.source_ip
|
||||
)
|
||||
record_webhook(receiver, "accepted")
|
||||
return Response(status_code=202)
|
||||
|
||||
|
||||
@@ -170,4 +199,5 @@ def run() -> None:
|
||||
host="0.0.0.0", # noqa: S104 - container-only port, not host-published
|
||||
port=8080,
|
||||
proxy_headers=False,
|
||||
access_log=False,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user