dictConfig, fileConfig, конфигурация через YAML/JSON
Production-приложения требуют гибкой конфигурации логирования.
basicConfig()не хватает — нуженdictConfig(). Правильная конфигурация — это разница между часами отладки и минутами.
Проблема: basicConfig() не подходит для production.
Ограничения basicConfig:
# ❌ basicConfig не подходит для production
logging.basicConfig(
level=logging.INFO,
format='%(levelname)s: %(message)s'
)
# Проблемы:
# 1. Все логи идут в root logger
# 2. Нельзя настроить разные уровни для разных модулей
# 3. Нельзя добавить кастомный filter
# 4. Нельзя легко переключиться между dev/prod конфигурациямиРешение: dictConfig() для полной конфигурации.
import logging
import logging.config
LOGGING_CONFIG = {
'version': 1, # Обязательно! Только значение 1 поддерживается
'disable_existing_loggers': False, # Не отключать существующие logger'ы
# Форматтеры
'formatters': {
'simple': {
'format': '%(levelname)s: %(message)s'
},
'verbose': {
'format': '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
}
},
# Handlers
'handlers': {
'console': {
'class': 'logging.StreamHandler',
'level': 'INFO',
'formatter': 'simple',
'stream': 'ext://sys.stdout'
},
'file': {
'class': 'logging.FileHandler',
'level': 'DEBUG',
'formatter': 'verbose',
'filename': 'app.log'
}
},
# Logger'ы
'loggers': {
'app': {
'level': 'DEBUG',
'handlers': ['console', 'file'],
'propagate': False
},
'app.api': {
'level': 'INFO',
'handlers': ['console'],
'propagate': False
}
},
# Root logger
'root': {
'level': 'WARNING',
'handlers': ['console']
}
}
# Применение конфигурации
logging.config.dictConfig(LOGGING_CONFIG)| Секция | Назначение | Обязательность |
|---|---|---|
version | Версия схемы (всегда 1) | ✅ Обязательно |
disable_existing_loggers | Не отключать logger'ы из других модулей | ❌ Рекомендуется |
formatters | Форматтеры для handlers | ❌ Опционально |
handlers | Обработчики: куда отправлять логи | ❌ Опционально |
loggers | Настройки logger'ов | ❌ Опционально |
root | Настройки root logger | ❌ Опционально |
filters | Кастомные фильтры | ❌ Опционально |
| Префикс | Назначение | Пример |
|---|---|---|
ext:// | Импорт объекта из Python | ext://sys.stdout |
cfg:// | Значение из другой секции config | cfg://handlers.file.filename |
() | Кастомный класс | '()': 'myapp.CustomHandler' |
Примеры использования префиксов:
handlers:
console:
class: logging.StreamHandler
# ext:// — импортируем sys.stdout вместо хардкодинга
stream: ext://sys.stdout
file:
class: logging.FileHandler
# cfg:// — берём значение из другой секции config
filename: cfg://handlers.file.base_path
base_path: /var/log/app
custom:
# () — создаём экземпляр кастомного класса
'()': myapp.logging.CustomHandler
level: DEBUGПроблема: После dictConfig logger'ы из сторонних библиотек перестают работать.
Решение: disable_existing_loggers: False
# ❌ Плохо: logger'ы библиотек отключаются
LOGGING_CONFIG = {
'version': 1,
# disable_existing_loggers по умолчанию True
# Все logger'ы, созданные до dictConfig, отключаются
}
# ✅ Хорошо: logger'ы библиотек сохраняются
LOGGING_CONFIG = {
'version': 1,
'disable_existing_loggers': False, # Важно!
# ...
}Когда использовать True:
Проблема: Нужно хранить конфигурацию в JSON файле.
Решение: Загрузить JSON и передать в dictConfig.
// logging.json
{
"version": 1,
"disable_existing_loggers": false,
"formatters": {
"simple": {
"format": "%(levelname)s: %(message)s"
},
"verbose": {
"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
}
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"level": "INFO",
"formatter": "simple",
"stream": "ext://sys.stdout"
},
"file": {
"class": "logging.handlers.RotatingFileHandler",
"level": "DEBUG",
"formatter": "verbose",
"filename": "app.log",
"maxBytes": 10485760,
"backupCount": 5
}
},
"loggers": {
"app": {
"level": "DEBUG",
"handlers": ["console", "file"],
"propagate": false
}
},
"root": {
"level": "WARNING",
"handlers": ["console"]
}
}# Загрузка конфигурации
import json
import logging.config
with open('logging.json', 'r') as f:
config = json.load(f)
logging.config.dictConfig(config)
logger = logging.getLogger(__name__)
logger.info("Application started")Проблема: JSON неудобен для комментариев и сложной структуры.
Решение: YAML более читаем и поддерживает комментарии.
# logging.yaml
version: 1
disable_existing_loggers: false
formatters:
simple:
format: '%(levelname)s: %(message)s'
verbose:
format: '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
datefmt: '%Y-%m-%d %H:%M:%S'
handlers:
console:
class: logging.StreamHandler
level: INFO
formatter: simple
stream: ext://sys.stdout
file:
class: logging.handlers.RotatingFileHandler
level: DEBUG
formatter: verbose
filename: app.log
maxBytes: 10485760 # 10 MB
backupCount: 5
mail:
class: logging.handlers.SMTPHandler
level: ERROR
formatter: verbose
mailhost: smtp.example.com
fromaddr: app@example.com
toaddrs:
- admin@example.com
subject: Application Error
credentials:
- username
- password
secure: ()
loggers:
app:
level: DEBUG
handlers:
- console
- file
propagate: false
app.api:
level: INFO
handlers:
- console
propagate: false
app.db:
level: WARNING # Меньше шума от DB логов
handlers:
- file
propagate: false
root:
level: WARNING
handlers:
- console# Загрузка YAML конфигурации
import yaml
import logging.config
with open('logging.yaml', 'r') as f:
config = yaml.safe_load(f)
logging.config.dictConfig(config)Проблема: Нужна простая конфигурация в INI формате.
Решение: fileConfig() для INI файлов.
# logging.ini
[loggers]
keys=root,app,app_api
[handlers]
keys=consoleHandler,fileHandler,emailHandler
[formatters]
keys=simpleFormatter,verboseFormatter
[logger_root]
level=WARNING
handlers=consoleHandler,emailHandler
[logger_app]
level=DEBUG
handlers=consoleHandler,fileHandler
qualname=app
propagate=0
[logger_app_api]
level=INFO
handlers=consoleHandler
qualname=app.api
propagate=0
[handler_consoleHandler]
class=StreamHandler
level=INFO
formatter=simpleFormatter
args=(sys.stdout,)
[handler_fileHandler]
class=handlers.RotatingFileHandler
level=DEBUG
formatter=verboseFormatter
args=('app.log', 'a', 10485760, 5)
[handler_emailHandler]
class=handlers.SMTPHandler
level=ERROR
formatter=verboseFormatter
args=(('smtp.example.com', 587), 'app@example.com', ['admin@example.com'], 'Application Error')
[formatter_simpleFormatter]
format=%(levelname)s: %(message)s
[formatter_verboseFormatter]
format=%(asctime)s - %(name)s - %(levelname)s - %(message)s
datefmt=%Y-%m-%d %H:%M:%S# Загрузка INI конфигурации
import logging.config
logging.config.fileConfig('logging.ini')
logger = logging.getLogger('app')Ограничения fileConfig:
'()'disable_existing_loggers# myapp/logging.py
import logging
from datetime import datetime
class CustomHandler(logging.Handler):
def __init__(self, log_dir='/var/log'):
super().__init__()
self.log_dir = log_dir
def emit(self, record):
# Кастомная логика
filename = f"{self.log_dir}/{datetime.now().strftime('%Y%m%d')}.log"
with open(filename, 'a') as f:
f.write(self.format(record) + '\n')# logging.yaml
handlers:
custom:
'()': myapp.logging.CustomHandler
level: DEBUG
formatter: verbose
log_dir: /var/log/myapp# myapp/logging.py
import logging
import json
class JsonFormatter(logging.Formatter):
def format(self, record):
log_data = {
'timestamp': self.formatTime(record),
'level': record.levelname,
'logger': record.name,
'message': record.getMessage(),
}
# Добавляем extra поля
for key, value in record.__dict__.items():
if key not in ('name', 'msg', 'args', 'created', 'filename',
'funcName', 'levelname', 'levelno', 'lineno',
'module', 'msecs', 'pathname', 'process',
'processName', 'relativeCreated', 'stack_info',
'exc_info', 'exc_text', 'thread', 'threadName',
'message', 'asctime'):
log_data[key] = value
return json.dumps(log_data)# logging.yaml
formatters:
json:
'()': myapp.logging.JsonFormatter# myapp/logging.py
import logging
class HealthCheckFilter(logging.Filter):
def filter(self, record):
# Исключаем health check логи
return 'health check' not in record.getMessage().lower()
class SensitiveDataFilter(logging.Filter):
def filter(self, record):
# Маскируем чувствительные данные
msg = record.getMessage()
if 'password' in msg.lower():
return False
return True# logging.yaml
filters:
healthcheck:
'()': myapp.logging.HealthCheckFilter
sensitive:
'()': myapp.logging.SensitiveDataFilter
handlers:
console:
class: logging.StreamHandler
filters:
- healthcheck
- sensitiveПроблема: Нужно изменить уровень логирования без перезапуска приложения.
Решение: incremental: True в конфигурации.
import logging.config
# Начальная конфигурация
logging.config.dictConfig({
'version': 1,
'loggers': {
'app': {'level': 'INFO'}
}
})
# ... приложение работает ...
# Динамическое изменение уровня
logging.config.dictConfig({
'version': 1,
'incremental': True, # Важно!
'loggers': {
'app': {'level': 'DEBUG'}
}
})
# Теперь app logger выводит DEBUG сообщенияUse case: Admin endpoint для изменения уровня логирования.
# app.py
from flask import Flask, request, jsonify
import logging.config
app = Flask(__name__)
@app.post('/admin/logging')
def update_logging():
data = request.json
level = data.get('level', 'INFO').upper()
logging.config.dictConfig({
'version': 1,
'incremental': True,
'loggers': {
'app': {'level': level}
}
})
return jsonify({'status': 'ok', 'level': level})Ограничения incremental:
Проблема: Разные настройки для development, staging, production.
Решение: Загрузка конфигурации в зависимости от окружения.
# config.py
import os
import logging.config
import yaml
def setup_logging():
env = os.getenv('APP_ENV', 'development')
config_file = f'logging.{env}.yaml'
with open(config_file, 'r') as f:
config = yaml.safe_load(f)
logging.config.dictConfig(config)
# main.py
from config import setup_logging
setup_logging()
logger = logging.getLogger(__name__)# logging.development.yaml
version: 1
disable_existing_loggers: false
handlers:
console:
class: logging.StreamHandler
level: DEBUG
formatter: simple
loggers:
app:
level: DEBUG
handlers: [console]
root:
level: DEBUG
handlers: [console]# logging.production.yaml
version: 1
disable_existing_loggers: false
handlers:
console:
class: logging.StreamHandler
level: INFO
formatter: json
file:
class: logging.handlers.RotatingFileHandler
level: DEBUG
formatter: json
filename: /var/log/app/app.log
maxBytes: 10485760
backupCount: 10
sentry:
'()': sentry_sdk.integrations.logging.SentryHandler
level: ERROR
loggers:
app:
level: INFO
handlers: [console, file]
propagate: false
root:
level: WARNING
handlers: [console, file, sentry]Проблема: Нужно проверить конфигурацию перед применением.
Решение: Тестовый скрипт для валидации.
# validate_logging.py
import logging.config
import yaml
import sys
def validate_config(config_file):
with open(config_file, 'r') as f:
config = yaml.safe_load(f)
try:
logging.config.dictConfig(config)
print(f"✅ Конфигурация {config_file} валидна")
# Тестовое логирование
logger = logging.getLogger('test')
logger.info("Test message")
return True
except Exception as e:
print(f"❌ Ошибка в {config_file}: {e}")
return False
if __name__ == '__main__':
config_file = sys.argv[1] if len(sys.argv) > 1 else 'logging.yaml'
sys.exit(0 if validate_config(config_file) else 1)Использование в CI:
# Локальная проверка
python validate_logging.py logging.yaml
# Проверка всех конфигов в проекте
python validate_logging.py logging.development.yaml
python validate_logging.py logging.staging.yaml
python validate_logging.py logging.production.yamlПолный пример для production:
# logging.production.yaml
version: 1
disable_existing_loggers: false
formatters:
json:
'()': pythonjsonlogger.jsonlogger.JsonFormatter
format: '%(asctime)s %(name)s %(levelname)s %(message)s %(user_id)s %(request_id)s'
console:
format: '%(levelname)s: %(name)s - %(message)s'
filters:
sensitive:
'()': myapp.logging.SensitiveDataFilter
healthcheck:
'()': myapp.logging.HealthCheckFilter
handlers:
console:
class: logging.StreamHandler
level: INFO
formatter: console
filters: [healthcheck, sensitive]
stream: ext://sys.stdout
file:
class: logging.handlers.RotatingFileHandler
level: DEBUG
formatter: json
filename: /var/log/app/app.log
maxBytes: 104857600 # 100 MB
backupCount: 10
encoding: utf-8
error_file:
class: logging.handlers.RotatingFileHandler
level: ERROR
formatter: json
filename: /var/log/app/errors.log
maxBytes: 52428800 # 50 MB
backupCount: 5
encoding: utf-8
loggers:
app:
level: INFO
handlers: [console, file, error_file]
propagate: false
app.api:
level: INFO
propagate: true # Использует handlers от app
app.db:
level: WARNING # Меньше шума от DB
propagate: true
# Сторонние библиотеки
urllib3:
level: WARNING
propagate: false
requests:
level: WARNING
propagate: false
sqlalchemy:
level: WARNING
propagate: false
root:
level: WARNING
handlers: [console]# ✅ Хорошо
import logging.config
import yaml
with open('logging.yaml') as f:
config = yaml.safe_load(f)
logging.config.dictConfig(config)
# ❌ Избегайте для production
logging.basicConfig(level=logging.INFO)# ✅ Хорошо
LOGGING_CONFIG = {
'version': 1,
'disable_existing_loggers': False, # Важно!
# ...
}
# ❌ Плохо: logger'ы библиотек будут отключены
LOGGING_CONFIG = {
'version': 1,
# disable_existing_loggers по умолчанию True
}# ✅ Хорошо
loggers:
app:
level: DEBUG
handlers: [console, file]
propagate: false # Не дублировать в root
# ❌ Плохо: логи дублируются
loggers:
app:
level: DEBUG
handlers: [console, file]
propagate: true # По умолчаниюlogging.yaml # Базовая конфигурация
logging.development.yaml # Development
logging.staging.yaml # Staging
logging.production.yaml # Production
loggers:
urllib3:
level: WARNING # Иначе слишком много DEBUG логов
requests:
level: WARNING
sqlalchemy:
level: WARNING| Метод | Формат | Гибкость | Кастомные компоненты | Рекомендация |
|---|---|---|---|---|
basicConfig | Python dict | Низкая | ❌ Нет | Только для скриптов |
dictConfig | Python dict, JSON, YAML | Высокая | ✅ Да | Production |
fileConfig | INI | Средняя | ❌ Нет | Legacy проекты |
| Поле | Значение |
|---|---|
version | Всегда 1 |
disable_existing_loggers | False для production |
propagate | False для application logger'ов |
'()' | Для кастомных классов |
ext:// | Для импорта объектов |
incremental | True для динамического изменения |
dictConfig для productiondisable_existing_loggers: False для сохранения logger'ов библиотекpropagate: False для предотвращения дублированияВопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.