feat(monitoring): observability stack + app instrumentation

- /health/ready (DB+MinIO+Casdoor checks, webhook post-deploy gate),
  /metrics via prometheus-fastapi-instrumentator, Sentry/GlitchTip capture,
  X-Request-ID middleware + JSON access logging (PROCUREMENT_LOG_FORMAT)
- server-side analytics (OpenPanel Track API): login, quote lifecycle,
  receipts — fire-and-forget, disabled without env config
- monitoring/ compose project: GlitchTip(+pg), Prometheus(+alerts),
  Grafana(provisioned ERP dashboard), Loki+Promtail, node/postgres
  exporters, cAdvisor — mem-limited (~2.2GB), loopback admin UIs,
  separate from app deploys; erp_shared network for scraping
- frontend: @openpanel/web + @sentry/react (build-arg gated, no-op when
  unset), funnel events (login, wizard steps, quote submitted)
- compose: api healthcheck, observability env wiring, .env.example keys
This commit is contained in:
MMD
2026-09-18 22:18:02 +03:30
parent f7b1f7f751
commit e17d1f0883
26 changed files with 1106 additions and 3 deletions
+10
View File
@@ -21,6 +21,7 @@ from app.auth import _verify_token, context_from_claims
from app.config import settings
from app.db.engine import get_db
from app.routers.users import _user_projects, user_to_dict
from app.services import analytics
from app.utils.errors import ServiceUnavailableError, UnauthorizedError
logger = logging.getLogger("procurement.auth")
@@ -75,6 +76,15 @@ async def login(payload: LoginRequest, db: AsyncSession = Depends(get_db)):
claims = await _verify_token(access_token)
ctx = await context_from_claims(claims, db)
projects = await _user_projects(db, ctx.user)
await analytics.track(
"login_success",
{"username": payload.username, "role": ctx.role},
profile_id=ctx.user.casdoor_id,
)
await analytics.identify(
ctx.user.casdoor_id,
{"name": ctx.display_name, "role": ctx.role, "username": payload.username},
)
return {
"data": {
"access_token": access_token,
+11
View File
@@ -50,6 +50,7 @@ from app.models.procurement import (
Supplier,
)
from app.schemas.procurement import PMReceiptCreate
from app.services import analytics
from app.services.inventory_service import canonical_specs_hash
from app.utils.errors import (
ForbiddenError,
@@ -429,6 +430,16 @@ async def pm_confirm_receipt(
quote.arrived_date = payload.received_at
item_state = arrival_state(li.quantity, already_received + payload.received_qty)
await analytics.track(
"receipt_confirmed",
{
"receipt_id": str(receipt.id),
"quote_id": str(quote.id),
"quantity": str(payload.received_qty),
"state": item_state,
},
profile_id=ctx.display_name,
)
return {"data": {
"receipt_id": str(receipt.id),
"state": item_state,