-
Notifications
You must be signed in to change notification settings - Fork 0
chore: production readiness hardening (deps, logging, Sentry, DB pool) #4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,54 @@ | ||||||||||||||||||
| import json | ||||||||||||||||||
| import logging | ||||||||||||||||||
| import sys | ||||||||||||||||||
| from datetime import UTC, datetime | ||||||||||||||||||
| from typing import Any | ||||||||||||||||||
|
|
||||||||||||||||||
| from app.core.config import settings | ||||||||||||||||||
|
|
||||||||||||||||||
| _configured = False | ||||||||||||||||||
|
|
||||||||||||||||||
|
|
||||||||||||||||||
| class JsonFormatter(logging.Formatter): | ||||||||||||||||||
| """Render log records as single-line JSON for ingestion by log aggregators.""" | ||||||||||||||||||
|
|
||||||||||||||||||
| def format(self, record: logging.LogRecord) -> str: | ||||||||||||||||||
| payload: dict[str, Any] = { | ||||||||||||||||||
| "timestamp": datetime.fromtimestamp(record.created, tz=UTC).isoformat(), | ||||||||||||||||||
| "level": record.levelname, | ||||||||||||||||||
| "logger": record.name, | ||||||||||||||||||
| "message": record.getMessage(), | ||||||||||||||||||
| } | ||||||||||||||||||
| if record.exc_info: | ||||||||||||||||||
| payload["exc_info"] = self.formatException(record.exc_info) | ||||||||||||||||||
| return json.dumps(payload) | ||||||||||||||||||
|
|
||||||||||||||||||
|
|
||||||||||||||||||
| def configure_logging() -> None: | ||||||||||||||||||
| """Configure the root logger's level and output format. | ||||||||||||||||||
|
|
||||||||||||||||||
| Called once at process startup (FastAPI app and Celery workers/beat) so that | ||||||||||||||||||
| `logging.getLogger(__name__)` calls throughout the codebase - e.g. the Redis | ||||||||||||||||||
| fail-open warnings in `app.services.cache` and `app.core.redis` - are emitted | ||||||||||||||||||
| with a consistent, timestamped format at the configured level. | ||||||||||||||||||
| """ | ||||||||||||||||||
| global _configured | ||||||||||||||||||
| if _configured: | ||||||||||||||||||
| return | ||||||||||||||||||
|
|
||||||||||||||||||
| handler = logging.StreamHandler(sys.stdout) | ||||||||||||||||||
| if settings.LOG_FORMAT == "json": | ||||||||||||||||||
| handler.setFormatter(JsonFormatter()) | ||||||||||||||||||
| else: | ||||||||||||||||||
| handler.setFormatter( | ||||||||||||||||||
| logging.Formatter( | ||||||||||||||||||
| fmt="%(asctime)s | %(levelname)-8s | %(name)s | %(message)s", | ||||||||||||||||||
| datefmt="%Y-%m-%dT%H:%M:%S%z", | ||||||||||||||||||
| ) | ||||||||||||||||||
| ) | ||||||||||||||||||
|
|
||||||||||||||||||
| root_logger = logging.getLogger() | ||||||||||||||||||
| root_logger.setLevel(settings.LOG_LEVEL) | ||||||||||||||||||
| root_logger.addHandler(handler) | ||||||||||||||||||
|
|
||||||||||||||||||
|
Comment on lines
+50
to
+53
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Avoid unconditional root handler append (duplicate log emission risk). At Line 52, Proposed fix def configure_logging() -> None:
@@
root_logger = logging.getLogger()
root_logger.setLevel(settings.LOG_LEVEL)
- root_logger.addHandler(handler)
+ # Replace existing root handlers to ensure a single, deterministic format.
+ root_logger.handlers.clear()
+ root_logger.addHandler(handler)📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||
| _configured = True | ||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| import sentry_sdk | ||
|
|
||
| from app.core.config import settings | ||
|
|
||
| _configured = False | ||
|
|
||
|
|
||
| def configure_sentry() -> None: | ||
| """Initialize Sentry error tracking if `SENTRY_DSN` is configured. | ||
|
|
||
| No-op when `SENTRY_DSN` is unset, which is the default for local | ||
| development and the test suite. | ||
| """ | ||
| global _configured | ||
| if _configured or not settings.SENTRY_DSN: | ||
| return | ||
|
|
||
| sentry_sdk.init( | ||
| dsn=settings.SENTRY_DSN, | ||
| environment=settings.ENVIRONMENT, | ||
| traces_sample_rate=settings.SENTRY_TRACES_SAMPLE_RATE, | ||
| ) | ||
| _configured = True |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,99 @@ | ||
| services: | ||
| - type: web | ||
| name: sentinel-api | ||
| runtime: python | ||
| buildCommand: pip install -r requirements.txt && alembic upgrade head | ||
| startCommand: uvicorn app.main:app --host 0.0.0.0 --port $PORT | ||
| envVars: | ||
| - key: ENVIRONMENT | ||
| value: production | ||
| - key: LOG_FORMAT | ||
| value: json | ||
| - key: LOG_LEVEL | ||
| value: INFO | ||
| - key: SECRET_KEY | ||
| generateValue: true | ||
| - key: DATABASE_URL | ||
| fromDatabase: | ||
| name: sentinel-db | ||
| property: connectionURI | ||
| - key: REDIS_URL | ||
| fromService: | ||
| type: redis | ||
| name: sentinel-redis | ||
| property: connectionURI | ||
|
Comment on lines
+20
to
+24
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Lines 20-24, 52-56, and 81-85 all depend on a Redis service named Suggested fix services:
@@
- type: worker
name: sentinel-beat
@@
- key: PYTHON_VERSION
value: "3.12.0"
+
+ - type: redis
+ name: sentinel-redis
+ plan: free
+ ipAllowList: []Also applies to: 52-56, 81-85 🤖 Prompt for AI Agents |
||
| - key: CELERY_TASK_ALWAYS_EAGER | ||
| value: "false" | ||
| - key: BACKEND_CORS_ORIGINS | ||
| value: '["https://sentinel-dashboard.vercel.app"]' | ||
| - key: PYTHON_VERSION | ||
| value: "3.12.0" | ||
| healthCheckPath: /health | ||
|
|
||
| - type: worker | ||
| name: sentinel-worker | ||
| runtime: python | ||
| buildCommand: pip install -r requirements.txt | ||
| startCommand: celery -A app.core.celery_app worker --loglevel=info --concurrency=2 | ||
| envVars: | ||
| - key: ENVIRONMENT | ||
| value: production | ||
| - key: LOG_FORMAT | ||
| value: json | ||
| - key: SECRET_KEY | ||
| fromService: | ||
| type: web | ||
| name: sentinel-api | ||
| envVarKey: SECRET_KEY | ||
| - key: DATABASE_URL | ||
| fromDatabase: | ||
| name: sentinel-db | ||
| property: connectionURI | ||
| - key: REDIS_URL | ||
| fromService: | ||
| type: redis | ||
| name: sentinel-redis | ||
| property: connectionURI | ||
| - key: CELERY_TASK_ALWAYS_EAGER | ||
| value: "false" | ||
| - key: PYTHON_VERSION | ||
| value: "3.12.0" | ||
|
|
||
| - type: worker | ||
| name: sentinel-beat | ||
| runtime: python | ||
| buildCommand: pip install -r requirements.txt | ||
| startCommand: celery -A app.core.celery_app beat --loglevel=info | ||
| envVars: | ||
| - key: ENVIRONMENT | ||
| value: production | ||
| - key: LOG_FORMAT | ||
| value: json | ||
| - key: SECRET_KEY | ||
| fromService: | ||
| type: web | ||
| name: sentinel-api | ||
| envVarKey: SECRET_KEY | ||
| - key: DATABASE_URL | ||
| fromDatabase: | ||
| name: sentinel-db | ||
| property: connectionURI | ||
| - key: REDIS_URL | ||
| fromService: | ||
| type: redis | ||
| name: sentinel-redis | ||
| property: connectionURI | ||
| - key: CELERY_TASK_ALWAYS_EAGER | ||
| value: "false" | ||
| - key: PYTHON_VERSION | ||
| value: "3.12.0" | ||
|
|
||
| databases: | ||
| - name: sentinel-db | ||
| plan: free | ||
| databaseName: sentinel | ||
| user: sentinel | ||
|
|
||
| # Note: Render free Redis is available in some regions. | ||
| # If not available, set CELERY_TASK_ALWAYS_EAGER=true on the web service | ||
| # and remove the worker/beat services. | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add bounds validation for new numeric runtime settings.
Line 25 and Lines 38-41 accept unconstrained numeric values from env, so invalid values can slip through and break startup/runtime behavior instead of failing fast at config load.
Suggested fix
Also applies to: 38-42
🤖 Prompt for AI Agents