Шаг 07. Логирование, prod-конфиг и финальный чеклист
⚡ Кратко: что делаем на этом шаге
Цель: Настроить Python logging (ротируемые файлы + stdout), production-конфиг через .env, проверить API в Postman, пройти финальный чеклист production-ready приложения.
- Файлы:
config.py(обновление),app/__init__.py(logging),.env.example - Инструмент: Postman / curl для полного smoke-тест прогона
- Результат: проект готов к деплою: конфиг через env, логирование, все эндпоинты проверены
🎯 Цель этапа
Последний шаг переводит проект из «работает на моём ноутбуке» в «готово к деплою». Три задачи: настроить стандартное Python-логирование (не print!), вынести конфиг в переменные окружения, провести полный smoke-тест через Postman.
print() не имеет уровней (DEBUG/INFO/WARNING/ERROR), не поддерживает
ротацию файлов, не различает источник сообщения и нельзя отключить в продакшне.
Python logging решает всё это. В Flask логирование уже встроено:
app.logger — это стандартный logging.Logger.
После этого шага у нас будет
- Логирование в stdout (dev) и в ротируемый файл (prod)
- Все секреты и URLs в переменных окружения
- Полная Postman-коллекция / curl smoke-тест
- Пройденный production-ready чеклист
📄 Затрагиваемые файлы
| Файл | Действие | Описание |
|---|---|---|
app/logging_config.py | Создать | Конфигурация Python logging |
app/__init__.py | Изменить | Подключить logging в create_app() |
config.py | Изменить | Добавить LOG_LEVEL в конфиги |
.env.example | Изменить | Полный список переменных окружения |
🔨 Шаги
1. Конфигурация логирования
# app/logging_config.py
import logging
import logging.handlers
import os
from flask import Flask
def configure_logging(app: Flask) -> None:
"""
Настраивает логирование для Flask-приложения.
Dev (DEBUG=True): лог в stdout, уровень DEBUG
Prod (DEBUG=False): лог в файл с ротацией (10 МБ, 5 файлов) + stdout WARNING
"""
log_level_str: str = app.config.get("LOG_LEVEL", "INFO")
log_level: int = getattr(logging, log_level_str.upper(), logging.INFO)
# Формат логов: время · уровень · модуль · сообщение
fmt = logging.Formatter(
fmt="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
# Очищаем существующие handlers (Flask добавляет свой по умолчанию)
app.logger.handlers.clear()
# Handler 1: stdout — всегда
stdout_handler = logging.StreamHandler()
stdout_handler.setFormatter(fmt)
stdout_handler.setLevel(log_level)
app.logger.addHandler(stdout_handler)
# Handler 2: ротируемый файл — только в production
if not app.debug:
logs_dir = os.path.join(os.path.dirname(app.root_path), "logs")
os.makedirs(logs_dir, exist_ok=True)
log_path = os.path.join(logs_dir, "community_pulse.log")
file_handler = logging.handlers.RotatingFileHandler(
log_path,
maxBytes=10 * 1024 * 1024, # 10 МБ
backupCount=5,
encoding="utf-8",
)
file_handler.setFormatter(fmt)
file_handler.setLevel(logging.WARNING) # в файл только WARNING и выше
app.logger.addHandler(file_handler)
app.logger.setLevel(log_level)
app.logger.info("Logging configured: level=%s debug=%s", log_level_str, app.debug)
2. Обновляем config.py — добавляем LOG_LEVEL
# config.py
import os
from dotenv import load_dotenv
load_dotenv()
class Config:
SECRET_KEY: str = os.environ.get("SECRET_KEY", "dev-secret-key-change-in-prod")
SQLALCHEMY_TRACK_MODIFICATIONS: bool = False
LOG_LEVEL: str = os.environ.get("LOG_LEVEL", "INFO")
class DevelopmentConfig(Config):
DEBUG: bool = True
LOG_LEVEL: str = "DEBUG"
SQLALCHEMY_DATABASE_URI: str = os.environ.get(
"DEV_DATABASE_URL",
"sqlite:///community_pulse_dev.db",
)
# Логировать SQL-запросы в dev (полезно для отладки)
SQLALCHEMY_ECHO: bool = os.environ.get("SQLALCHEMY_ECHO", "false").lower() == "true"
class TestingConfig(Config):
TESTING: bool = True
LOG_LEVEL: str = "WARNING"
SQLALCHEMY_DATABASE_URI: str = "sqlite:///:memory:"
WTF_CSRF_ENABLED: bool = False
class ProductionConfig(Config):
DEBUG: bool = False
LOG_LEVEL: str = os.environ.get("LOG_LEVEL", "WARNING")
SQLALCHEMY_DATABASE_URI: str = os.environ.get("DATABASE_URL", "")
def __init__(self) -> None:
if not self.SQLALCHEMY_DATABASE_URI:
raise RuntimeError(
"DATABASE_URL environment variable is not set. "
"Set it to a PostgreSQL connection string."
)
config_map: dict[str, type[Config]] = {
"development": DevelopmentConfig,
"testing": TestingConfig,
"production": ProductionConfig,
}
3. Подключаем logging в create_app()
# app/__init__.py
from flask import Flask, jsonify
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from config import config_map, Config
db: SQLAlchemy = SQLAlchemy()
migrate: Migrate = Migrate()
def create_app(config_name: str = "development") -> Flask:
"""
Application factory — создаёт настроенный экземпляр Flask.
Args:
config_name: 'development' | 'testing' | 'production'
Returns:
Сконфигурированный Flask app
"""
app = Flask(__name__)
config_class: type[Config] = config_map[config_name]
app.config.from_object(config_class)
# Расширения
db.init_app(app)
migrate.init_app(app, db)
# Импорт моделей для Alembic auto-detect
with app.app_context():
from app.models import Question, Response # noqa: F401
# API blueprints
from app.api import register_blueprints
register_blueprints(app)
# Error handlers
from app.errors import register_error_handlers
register_error_handlers(app)
# Логирование
from app.logging_config import configure_logging
configure_logging(app)
@app.route("/health")
def health_check():
app.logger.debug("Health check called")
return jsonify({"status": "ok", "env": config_name}), 200
app.logger.info("Application '%s' created with config '%s'", app.name, config_name)
return app
4. Обновлённый .env.example
# .env.example — скопируйте в .env и заполните своими значениями
# НЕ коммитьте .env в git!
# Окружение: development | testing | production
APP_ENV=development
# Секретный ключ Flask (обязателен в production, минимум 32 символа)
SECRET_KEY=your-very-long-secret-key-change-this-in-production
# URL базы данных
# Для разработки (SQLite):
DEV_DATABASE_URL=sqlite:///community_pulse_dev.db
# Для production (PostgreSQL):
# DATABASE_URL=postgresql://user:password@localhost:5432/community_pulse
# Уровень логирования: DEBUG | INFO | WARNING | ERROR
LOG_LEVEL=INFO
# Логировать SQL-запросы (только для отладки в dev):
# SQLALCHEMY_ECHO=true
5. Добавляем logs/ в .gitignore
# Добавьте в существующий .gitignore:
logs/
*.log
🧠 Объяснение логики
Стратегия логирования: dev vs prod
В разработке мы хотим видеть все DEBUG-сообщения в терминале — это помогает отслеживать каждый запрос и SQL-запрос. В production важны только WARNING и ERROR — весь поток DEBUG-логов будет огромным и бесполезным. Поэтому два handler'а: stdout всегда, файл только в prod с уровнем WARNING.
RotatingFileHandler
RotatingFileHandler ограничивает размер лог-файла: когда файл
достигает maxBytes, он переименовывается в .log.1,
создаётся новый. Хранятся последние backupCount архивов.
Без ротации лог-файлы растут до переполнения диска.
Переменные окружения — единственный источник секретов
SECRET_KEY, пароли БД, API-ключи — никогда в коде или git.
python-dotenv читает .env при разработке.
В production эти переменные устанавливает хостинг-платформа (Heroku, Railway,
Docker Compose, Kubernetes secrets).
python run.py запускает Werkzeug dev-сервер — однопоточный,
без TLS, без graceful reload. Для production нужен WSGI-сервер:
gunicorn или waitress (Windows).
✅ Полный smoke-тест (Postman / curl)
Запускаем сервер
python run.py
Smoke-тест — последовательность запросов
# 1. Health check
curl http://localhost:5000/health
# Ожидаем: {"env": "development", "status": "ok"}
# 2. Создать 2 вопроса
curl -X POST http://localhost:5000/api/questions/ \
-H "Content-Type: application/json" \
-d "{\"text\": \"Согласны ли вы с четырёхдневной рабочей неделей?\"}"
curl -X POST http://localhost:5000/api/questions/ \
-H "Content-Type: application/json" \
-d "{\"text\": \"Стоит ли переходить на Python 3.14?\"}"
# 3. Список вопросов
curl http://localhost:5000/api/questions/
# Ожидаем: {"items": [...], "total": 2}
# 4. Поиск по тексту
curl "http://localhost:5000/api/questions/?search=python"
# Ожидаем: один вопрос про Python
# 5. Добавить голоса
curl -X POST http://localhost:5000/api/questions/1/responses/ \
-H "Content-Type: application/json" \
-d "{\"is_agree\": true}"
curl -X POST http://localhost:5000/api/questions/1/responses/ \
-H "Content-Type: application/json" \
-d "{\"is_agree\": true}"
curl -X POST http://localhost:5000/api/questions/1/responses/ \
-H "Content-Type: application/json" \
-d "{\"is_agree\": false}"
# 6. Статистика
curl http://localhost:5000/api/stats/questions/
# Ожидаем: agree_count=2, disagree_count=1, total_count=3, agree_percent=66.7
curl http://localhost:5000/api/stats/questions/1
# Ожидаем: статистика по вопросу 1
# 7. Обновить вопрос
curl -X PUT http://localhost:5000/api/questions/2 \
-H "Content-Type: application/json" \
-d "{\"text\": \"Стоит ли изучать Python 3.14 прямо сейчас?\"}"
# 8. Тест валидации — слишком короткий текст
curl -X POST http://localhost:5000/api/questions/ \
-H "Content-Type: application/json" \
-d "{\"text\": \"Hi\"}"
# Ожидаем: 422 Unprocessable Entity
# 9. Несуществующий ресурс — JSON, не HTML
curl http://localhost:5000/api/questions/99999
# Ожидаем: {"error": "Not found", "detail": "..."} код 404
# 10. Удалить вопрос (каскадно удалятся его ответы)
curl -X DELETE http://localhost:5000/api/questions/1
curl http://localhost:5000/api/questions/1/responses/
# Ожидаем: 404 (вопрос удалён)
Ожидаемый результат
- Health check → 200 OK
- CRUD вопросов работает (GET/POST/PUT/DELETE)
- Фильтрация ilike работает
- Голосование работает
- Статистика считается верно
- Валидация Pydantic отклоняет короткий текст (422)
- 404 возвращается в JSON, не в HTML
- Каскадное удаление работает
✅ Финальный чеклист production-ready
Архитектура и код
- Application Factory — нет глобального
app - Config-классы без
FLASK_ENV - Blueprints — маршруты разбиты по модулям
- Pydantic v2 — входные данные валидируются, выходные сериализуются
- SQLAlchemy 2.x —
Mapped[T]/mapped_column()
База данных
- Flask-Migrate — миграции в git, не
db.create_all() - Каскадное удаление — нет осиротевших записей
- Индекс на
question_idв таблице ответов - Агрегация через SQL GROUP BY, не N+1 запросов
Безопасность и конфиг
- Секреты в
.env, не в коде .envв.gitignore- Централизованные error handlers (JSON, не HTML)
- Логирование через Python logging, не print()
Что добавить для настоящего production
🎓 Что дальше
Вы завершили Капстоун A — Community Pulse API. Проект демонстрирует production-grade подход к Flask-приложению: factory, config-классы, SQLAlchemy 2.x, Flask-Migrate, Pydantic v2, blueprints, централизованные ошибки и логирование.
Темы callout-verify из уроков 09 и 11 закрыты: правильный конфиг без FLASK_ENV, Mapped/mapped_column, фильтрация через ilike.
Продолжение в следующих капстоунах
- Капстоун B — Django: Library / Каталог — модели, Django Admin, ORM, N+1 (уроки 15–24)
- Капстоун C — Django + DRF + JWT: Task Manager API — флагманский проект, связывающий весь курс (уроки 26–45)