Эффективная отладка в Python: понимание трассировки стека для разработчиков

Пройдите тест, узнайте какой профессии подходите
Сколько вам лет
0%
До 18
От 18 до 24
От 25 до 34
От 35 до 44
От 45 до 49
От 50 до 54
Больше 55

Для кого эта статья:

  • Python-разработчики, желающие улучшить навыки отладки и обработки исключений
  • Студенты и участники курсов по программированию на Python
  • Профессионалы, работающие с системами, требующими надежного логирования и мониторинга ошибок

    Разработчики Python знают: ошибки — не повод для паники, а ценный источник информации. Понимание структуры исключений и умение извлекать из них максимум данных — это не просто навык, а суперспособность, отличающая профессионала от новичка. Трассировка стека — ваша карта к источнику проблемы, позволяющая видеть не только место крушения, но и весь путь, который привел к катастрофе. 🔍 Освоив инструменты анализа и форматирования стек-трейса, вы превратите загадочные ошибки в понятные сигналы к действию.

Хотите превратиться из охотника за багами в настоящего укротителя исключений? На курсе Обучение Python-разработке от Skypro вы освоите не только базовые принципы работы с ошибками, но и профессиональные техники отладки. Вместо паники при виде красного текста на экране вы будете методично анализировать трассировку стека и устранять проблемы на корню. Эксперты индустрии научат вас превращать исключения в мощный инструмент диагностики и улучшения кода!

Структура и компоненты исключений в Python

Исключения в Python — это не просто сигналы об ошибках, а полноценные объекты с богатой внутренней структурой. Понимание их анатомии критично для эффективной отладки.

В Python все исключения являются экземплярами классов, которые наследуются от базового класса Exception. Этот класс, в свою очередь, наследуется от BaseException. Иерархия наследования позволяет создавать специализированные типы исключений и обрабатывать их соответствующим образом.

Антон Михайлов, ведущий Python-разработчик

Однажды наша команда столкнулась с загадочным сбоем в производственной среде. Сервис периодически падал без видимых причин. В логах мы видели только сообщение о неопределенной ошибке, без деталей трассировки. Первым делом я добавил полный перехват исключений с сохранением всех атрибутов: типа исключения, сообщения и, что критично, полной трассировки стека.

Через три дня мы поймали виновника — это был ConnectionResetError, возникающий при определенной последовательности действий клиента. Но ключом к решению стало не само исключение, а его контекст — предыдущее исключение TimeoutError, которое скрывалось в атрибуте __cause__. Без понимания структуры исключений и извлечения полной трассировки мы бы потратили недели на поиск проблемы.

Каждое исключение в Python содержит несколько ключевых компонентов:

  • Тип исключения — определяет класс ошибки (например, ValueError, TypeError)
  • Сообщение — текстовое описание ошибки
  • Трассировка стека — последовательность вызовов функций, приведших к исключению
  • Связанные исключения — информация о предшествующих или порождающих ошибках

Особую важность имеют атрибуты исключений, связанные с контекстом. С Python 3 появились механизмы, позволяющие отслеживать цепочки исключений:

Атрибут Описание Когда доступен
__cause__ Явно указанная причина исключения (через raise X from Y) При использовании синтаксиса raise ... from
__context__ Предшествующее исключение при возникновении нового в блоке except При возникновении исключения во время обработки другого
__traceback__ Объект трассировки стека Всегда

Доступ к этим компонентам можно получить как из самого объекта исключения, так и с помощью различных служебных функций Python. Например:

Python
Скопировать код
try:
1 / 0
except Exception as e:
print(f"Тип: {type(e)}")
print(f"Сообщение: {str(e)}")
print(f"Трассировка: {e.__traceback__}")

Важно понимать, что трассировка стека — это не просто строка текста, а объект специального типа traceback, который содержит подробную информацию о каждом кадре стека вызовов. Этот объект можно анализировать программно, извлекая из него имена файлов, номера строк, имена функций и локальные переменные.

Пошаговый план для смены профессии

Методы получения трассировки стека при обработке ошибок

Python предлагает несколько способов получения трассировки стека при возникновении исключений. Каждый метод имеет свои особенности и применим в различных сценариях. 🔧

Самый простой способ — использовать конструкцию try-except с перехватом объекта исключения:

Python
Скопировать код
try:
# Код, который может вызвать исключение
result = dangerous_function()
except Exception as e:
# Получение информации об исключении
exception_type = type(e).__name__
exception_message = str(e)
traceback_object = e.__traceback__

# Далее можно работать с этой информацией

Модуль sys предоставляет функцию exc_info(), которая возвращает тройку значений: тип исключения, экземпляр исключения и объект трассировки:

Python
Скопировать код
import sys

try:
1 / 0
except:
exc_type, exc_value, exc_traceback = sys.exc_info()
print(f"Тип: {exc_type}")
print(f"Значение: {exc_value}")
print(f"Трассировка: {exc_traceback}")

Для более детального анализа трассировки можно использовать атрибуты самого объекта traceback:

Python
Скопировать код
try:
1 / 0
except Exception as e:
tb = e.__traceback__
while tb:
print(f"Файл: {tb.tb_frame.f_code.co_filename}")
print(f"Строка: {tb.tb_lineno}")
print(f"Функция: {tb.tb_frame.f_code.co_name}")
tb = tb.tb_next

Сравнение различных методов получения трассировки стека:

Метод Преимущества Недостатки Рекомендуемое использование
except Exception as e Простой, встроенный в синтаксис языка Ограниченный доступ к деталям Для простых случаев обработки ошибок
sys.exc_info() Доступ ко всем компонентам исключения Устаревший метод, может быть заменён в будущих версиях Для совместимости с legacy-кодом
Прямой доступ к __traceback__ Полный программный доступ к каждому кадру стека Требует хорошего понимания структуры трассировки Для глубокого анализа и отладки
Модуль traceback Готовые функции для извлечения и форматирования Дополнительная зависимость Для большинства практических задач

При разработке сложных приложений часто требуется не просто получить трассировку, но и обогатить её дополнительным контекстом. Для этого можно использовать технику повторного возбуждения исключений с контекстом:

Python
Скопировать код
try:
process_data()
except Exception as original_error:
try:
context_info = get_additional_context()
except:
# Избегаем перезаписи оригинального исключения
raise original_error

# Добавляем контекст к оригинальной ошибке
raise RuntimeError(f"Ошибка при обработке данных: {context_info}") from original_error

Это позволяет сохранить оригинальную трассировку и дополнить её информацией о высокоуровневом контексте выполнения.

Для автоматического получения трассировки стека в любом месте программы, даже без возникновения исключения, можно использовать следующий подход:

Python
Скопировать код
import traceback

def get_current_stack_trace():
return "".join(traceback.format_stack())

# Использование
print("Текущая трассировка стека:")
print(get_current_stack_trace())

Это особенно полезно при диагностике сложных потоков выполнения или при отладке многопоточных приложений.

Использование модуля traceback для анализа исключений

Модуль traceback в Python — это мощный инструмент для работы с трассировками стека. Он предоставляет функции для извлечения, форматирования и анализа информации о стеке вызовов при возникновении исключений.

Базовые функции модуля traceback позволяют получить информацию об исключении в различных форматах:

Python
Скопировать код
import traceback

try:
# Вызываем ошибку
x = 1 / 0
except Exception:
# Получаем строковое представление трассировки
trace_string = traceback.format_exc()
print(trace_string)

# Альтернативный вариант — вывод трассировки напрямую
traceback.print_exc()

Для более тонкого контроля над форматированием можно использовать следующие функции:

  • traceback.format_exception(etype, value, tb) — возвращает список строк, представляющих трассировку
  • traceback.format_exception_only(etype, value) — возвращает только информацию о типе исключения и сообщении
  • traceback.extract_tb(tb) — извлекает информацию о каждом кадре стека в виде объектов FrameSummary

Пример использования этих функций:

Python
Скопировать код
import traceback
import sys

try:
# Намеренно вызываем ошибку
def func_a():
return func_b()

def func_b():
return func_c()

def func_c():
return 1/0

func_a()
except Exception:
# Получаем компоненты текущего исключения
exc_type, exc_value, exc_traceback = sys.exc_info()

# Форматируем полную трассировку
formatted_traceback = traceback.format_exception(exc_type, exc_value, exc_traceback)
print("Полная трассировка:")
print("".join(formatted_traceback))

# Получаем только сообщение об ошибке
error_message = "".join(traceback.format_exception_only(exc_type, exc_value))
print("Сообщение об ошибке:")
print(error_message)

# Извлекаем и анализируем отдельные кадры стека
stack_frames = traceback.extract_tb(exc_traceback)
print("Анализ стека вызовов:")
for frame in stack_frames:
print(f"Файл: {frame.filename}, Строка: {frame.lineno}, Функция: {frame.name}")
print(f"Код: {frame.line}")

Максим Петров, DevOps-инженер

В нашей компании долгое время была проблема с микросервисной архитектурой — ошибки в одном сервисе приводили к каскадным сбоям, но логи содержали минимум информации для диагностики. Я предложил модифицировать middleware для обработки исключений, использовав модуль traceback.

Мы внедрили стандарт, по которому каждое исключение форматировалось с помощью traceback.format_exception(), сериализовалось в JSON и передавалось в центральную систему логирования с сохранением контекста запроса. Главной инновацией стала функция "резолюции трассировки" — наш анализатор логов автоматически сопоставлял трассировки из разных сервисов, восстанавливая полную цепочку вызовов между микросервисами.

Когда через месяц произошел очередной сбой, мы впервые увидели полную картину: ошибка аутентификации в одном сервисе вызывала неправильную обработку HTTP-статуса в промежуточном сервисе, что приводило к передаче невалидных данных в основной бизнес-логике. Полная трассировка стека позволила решить проблему за час вместо обычных двух дней простоя.

Модуль traceback также предоставляет функции для программного анализа трассировок. Это особенно полезно при создании систем автоматической отладки или при разработке собственных инструментов для работы с исключениями:

Python
Скопировать код
import traceback

def analyze_exception():
try:
# Генерируем исключение
raise ValueError("Демонстрационная ошибка")
except Exception as e:
# Извлекаем объект трассировки
tb = e.__traceback__

# Получаем список кадров стека
frames = traceback.extract_tb(tb)

# Анализируем кадры
for i, frame in enumerate(frames):
print(f"Кадр #{i}:")
# Извлекаем имя файла без пути
filename = frame.filename.split("/")[-1]
print(f" Модуль: {filename}")
print(f" Строка: {frame.lineno}")
print(f" Функция: {frame.name}")
print(f" Контекст: {frame.line}")

# Определяем корневую причину (последний кадр стека)
root_cause = frames[-1] if frames else None
if root_cause:
print(f"Корневая причина: {root_cause.line} в {root_cause.filename}:{root_cause.lineno}")

# Запускаем анализ
analyze_exception()

Для систем логирования и мониторинга часто требуется ограничение длины трассировки или её фильтрация. Модуль traceback позволяет это сделать:

Python
Скопировать код
import traceback
import sys

def log_exception(limit=None, exclude_modules=None):
"""
Логирует текущее исключение с возможностью ограничения глубины стека
и исключения определенных модулей

:param limit: максимальное количество кадров стека для вывода
:param exclude_modules: список модулей, которые нужно исключить из трассировки
"""
exc_type, exc_value, exc_traceback = sys.exc_info()

# Извлекаем кадры стека
frames = traceback.extract_tb(exc_traceback)

# Фильтруем кадры, если указаны модули для исключения
if exclude_modules:
frames = [frame for frame in frames if not any(
frame.filename.startswith(module) for module in exclude_modules)]

# Ограничиваем количество кадров, если указан лимит
if limit is not None and limit > 0:
frames = frames[-limit:]

# Форматируем отфильтрованную трассировку
lines = [f"Трассировка (последние {len(frames)} кадров):"]
for frame in frames:
lines.append(f" Файл '{frame.filename}', строка {frame.lineno}, в {frame.name}")
if frame.line:
lines.append(f" {frame.line}")

# Добавляем информацию о типе исключения и сообщении
lines.extend(traceback.format_exception_only(exc_type, exc_value))

return "\n".join(lines)

# Пример использования
try:
# Вызываем цепочку функций с ошибкой
def level1(): return level2()
def level2(): return level3()
def level3(): return level4()
def level4(): return level5()
def level5(): return 1/0

level1()
except Exception:
# Логируем только 3 последних кадра стека, исключая модули стандартной библиотеки
error_log = log_exception(limit=3, exclude_modules=['/usr/lib/python', 'lib/python'])
print(error_log)

Продвинутые техники форматирования стек-трейса

Стандартное форматирование трассировки стека в Python может быть недостаточно информативным для сложных приложений. Продвинутые техники форматирования позволяют создавать более читаемые и информативные отчеты об ошибках. 🛠️

Первый шаг к улучшению форматирования — настройка детализации вывода трассировки. Модуль traceback предлагает несколько параметров для функций форматирования:

Python
Скопировать код
import traceback
import sys

try:
1 / 0
except Exception:
exc_type, exc_value, exc_traceback = sys.exc_info()

# Базовое форматирование
basic = "".join(traceback.format_exception(exc_type, exc_value, exc_traceback))

# Расширенное форматирование с локальными переменными
detailed = "".join(traceback.format_exception(
exc_type, exc_value, exc_traceback, limit=None, chain=True, capture_locals=True
))

print("Базовая трассировка:")
print(basic)
print("\nРасширенная трассировка с локальными переменными:")
print(detailed)

Для создания цветной трассировки, что особенно полезно в консольных приложениях, можно использовать ANSI-коды или специализированные библиотеки:

Python
Скопировать код
def colorized_traceback():
"""Возвращает цветную трассировку стека текущего исключения."""
# ANSI-коды для цветов
RED = "\033[91m"
GREEN = "\033[92m"
YELLOW = "\033[93m"
BLUE = "\033[94m"
MAGENTA = "\033[95m"
RESET = "\033[0m"

exc_type, exc_value, exc_traceback = sys.exc_info()
frames = traceback.extract_tb(exc_traceback)

lines = []
lines.append(f"{RED}Traceback (most recent call last):{RESET}")

for frame in frames:
filename = frame.filename
lineno = frame.lineno
name = frame.name
line = frame.line

lines.append(f" {GREEN}File{RESET} {BLUE}\"{filename}\"{RESET}, {GREEN}line{RESET} {MAGENTA}{lineno}{RESET}, {GREEN}in{RESET} {YELLOW}{name}{RESET}")
if line:
lines.append(f" {line}")

error_msg = "".join(traceback.format_exception_only(exc_type, exc_value))
lines.append(f"{RED}{error_msg}{RESET}")

return "\n".join(lines)

# Пример использования
try:
x = [1, 2, 3]
print(x[10])
except Exception:
print(colorized_traceback())

Для веб-приложений или систем с графическим интерфейсом полезно форматирование трассировки в HTML:

Python
Скопировать код
import html
import traceback
import sys

def html_traceback():
"""Возвращает трассировку стека в формате HTML."""
exc_type, exc_value, exc_traceback = sys.exc_info()
frames = traceback.extract_tb(exc_traceback)

html_output = ["<div class='traceback'>"]
html_output.append("<h3>Traceback (most recent call last):</h3>")
html_output.append("<ul>")

for frame in frames:
filename = html.escape(frame.filename)
lineno = frame.lineno
name = html.escape(frame.name)
line = html.escape(frame.line) if frame.line else ""

html_output.append(f"<li>")
html_output.append(f"<strong>File</strong> \"{filename}\", <strong>line</strong> {lineno}, <strong>in</strong> {name}")
if line:
html_output.append(f"<pre>{line}</pre>")
html_output.append(f"</li>")

html_output.append("</ul>")

error_msg = html.escape("".join(traceback.format_exception_only(exc_type, exc_value)))
html_output.append(f"<div class='error'>{error_msg}</div>")
html_output.append("</div>")

return "\n".join(html_output)

Одна из самых мощных техник — обогащение трассировки контекстной информацией. Это можно сделать путем создания контекстных менеджеров:

Python
Скопировать код
import traceback
import sys
import contextlib
import time
from functools import wraps

# Контекстный словарь для хранения информации
execution_context = {}

@contextlib.contextmanager
def context(name, **kwargs):
"""Контекстный менеджер для добавления информации в трассировку."""
parent = execution_context.get('current', None)
context_info = {'name': name, 'parent': parent, 'data': kwargs, 'timestamp': time.time()}
execution_context['current'] = context_info

try:
yield
except Exception:
# При возникновении ошибки форматируем обогащенную трассировку
print(format_enriched_traceback())
raise
finally:
execution_context['current'] = parent

def format_enriched_traceback():
"""Форматирует трассировку с добавлением контекстной информации."""
exc_type, exc_value, exc_traceback = sys.exc_info()
standard_traceback = "".join(traceback.format_exception(exc_type, exc_value, exc_traceback))

# Добавляем контекстную информацию
context_info = []
current = execution_context.get('current')
if current:
context_info.append("Execution Context:")
while current:
context_str = f" In context '{current['name']}' at {time.strftime('%H:%M:%S', time.localtime(current['timestamp']))}"
if current['data']:
context_str += f" with data: {current['data']}"
context_info.append(context_str)
current = current['parent']

if context_info:
return "\n".join(context_info) + "\n\n" + standard_traceback
return standard_traceback

# Пример использования
try:
with context("data_processing", file="example.csv", mode="read"):
with context("parsing", line=42, column="price"):
1/0 # Ошибка
except:
# Обработка уже выполнена в контекстном менеджере
pass

Сравнение различных техник форматирования трассировки:

Техника Сложность реализации Информативность Подходит для
Стандартное форматирование Низкая Средняя Большинство случаев
С захватом локальных переменных Низкая Высокая Отладка сложной логики
Цветное форматирование Средняя Средняя CLI-приложения
HTML-форматирование Средняя Высокая Веб-приложения
Обогащение контекстом Высокая Очень высокая Корпоративные приложения

Для максимальной информативности можно также включить в трассировку информацию о значениях выражений, которые привели к ошибке. Для этого можно использовать декораторы функций:

Python
Скопировать код
def debug_call(func):
@wraps(func)
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except Exception as e:
print(f"Exception in {func.__name__}:")
print(f"Arguments: {args}")
print(f"Keyword arguments: {kwargs}")
# Перевозбуждаем исключение с сохранением трассировки
raise
return wrapper

@debug_call
def divide(a, b):
return a / b

try:
divide(10, 0)
except:
pass # Трассировка уже выведена в декораторе

Наконец, для создания действительно информативных отчетов об ошибках можно интегрировать анализ кода и автоматическое определение проблемных участков:

Python
Скопировать код
import inspect
import ast
import traceback
import sys

def analyze_code_context(frame, lineno):
"""Анализирует контекст кода вокруг указанной строки."""
filename = frame.f_code.co_filename
try:
with open(filename, 'r') as f:
source = f.read()

tree = ast.parse(source)
analyzer = CodeAnalyzer(lineno)
analyzer.visit(tree)
return analyzer.get_context()
except:
return "Не удалось проанализировать исходный код"

class CodeAnalyzer(ast.NodeVisitor):
def __init__(self, target_line):
self.target_line = target_line
self.context = {}

def visit(self, node):
if hasattr(node, 'lineno'):
if abs(node.lineno – self.target_line) <= 2: # Анализируем строки рядом с ошибкой
if isinstance(node, ast.Call):
self.context['call'] = ast.unparse(node)
elif isinstance(node, ast.BinOp):
self.context['operation'] = ast.unparse(node)
ast.NodeVisitor.generic_visit(self, node)

def get_context(self):
return self.context

def smart_traceback():
"""Создает умную трассировку стека с анализом кода."""
exc_type, exc_value, exc_traceback = sys.exc_info()

frames = []
tb = exc_traceback
while tb:
frames.append(tb.tb_frame)
tb = tb.tb_next

lines = ["Smart Traceback:"]
for frame in reversed(frames):
filename = frame.f_code.co_filename
lineno = frame.f_lineno
function = frame.f_code.co_name

lines.append(f"In file '{filename}', line {lineno}, function '{function}':")

# Анализ кода
context = analyze_code_context(frame, lineno)
if context:
lines.append(" Code analysis:")
for key, value in context.items():
lines.append(f" {key}: {value}")

# Локальные переменные
lines.append(" Local variables:")
for key, value in frame.f_locals.items():
# Ограничиваем длину представления значения
value_str = str(value)
if len(value_str) > 100:
value_str = value_str[:97] + "..."
lines.append(f" {key} = {value_str}")

# Информация об исключении
lines.append(f"Exception: {exc_type.__name__}: {exc_value}")

return "\n".join(lines)

# Пример использования
try:
x = 10
y = 0
result = x / y
except:
print(smart_traceback())

Практики логирования исключений в Python-приложениях

Эффективное логирование исключений — ключевой компонент надёжных Python-приложений. Правильно настроенное логирование облегчает отладку, мониторинг и поддержку программного обеспечения в продакшене. 📊

Начнем с основ — настройки стандартного модуля logging для работы с исключениями:

Python
Скопировать код
import logging
import traceback

# Настройка логгера
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s – %(name)s – %(levelname)s – %(message)s',
filename='app.log'
)

logger = logging.getLogger('app')

try:
# Код, который может вызвать исключение
1 / 0
except Exception as e:
# Базовый подход к логированию исключений
logger.error(f"Произошла ошибка: {e}")

# Более информативный подход с трассировкой
logger.error("Произошла ошибка", exc_info=True)

# Альтернативный способ с явным форматированием трассировки
logger.error(f"Произошла ошибка: {e}\n{''.join(traceback.format_tb(e.__traceback__))}")

В продакшн-системах часто требуется структурированное логирование в формате JSON, что упрощает анализ логов с помощью инструментов вроде ELK Stack или Graylog:

Python
Скопировать код
import logging
import json
import traceback
import sys
import datetime

class JsonFormatter(logging.Formatter):
"""Форматировщик логов в JSON."""
def format(self, record):
log_entry = {
'timestamp': datetime.datetime.now().isoformat(),
'level': record.levelname,
'logger': record.name,
'message': record.getMessage()
}

# Добавляем информацию об исключении, если она есть
if record.exc_info:
exc_type, exc_value, exc_tb = record.exc_info
log_entry['exception'] = {
'type': exc_type.__name__,
'message': str(exc_value),
'traceback': traceback.format_exception(exc_type, exc_value, exc_tb)
}

# Добавляем пользовательские атрибуты
for key, value in record.__dict__.items():
if key.startswith('custom_'):
log_entry[key[7:]] = value

return json.dumps(log_entry)

# Настройка JSON-логгера
json_handler = logging.FileHandler('app_json.log')
json_handler.setFormatter(JsonFormatter())

logger = logging.getLogger('app.json')
logger.setLevel(logging.INFO)
logger.addHandler(json_handler)

# Пример использования с пользовательскими атрибутами
def log_with_context(logger, level, msg, **kwargs):
"""Логирует сообщение с дополнительным контекстом."""
extra = {f'custom_{k}': v for k, v in kwargs.items()}
logger.log(level, msg, extra=extra)

try:
# Код, который может вызвать исключение
data = {"users": [{"id": 1}]}
user = data["users"][5] # Вызовет IndexError
except Exception as e:
log_with_context(
logger, 
logging.ERROR, 
"Ошибка при доступе к данным пользователя",
user_id=5,
operation="user_lookup",
component="user_service",
exc_info=sys.exc_info()
)

Для микросервисных архитектур и распределенных систем критично сохранять контекст запросов и корреляционные идентификаторы в логах исключений:

Python
Скопировать код
import logging
import uuid
import threading
from functools import wraps

# Хранилище контекста для текущего потока
context = threading.local()

def set_request_context(request_id=None, user_id=None, **kwargs):
"""Устанавливает контекст запроса для текущего потока."""
if not hasattr(context, 'data'):
context.data = {}

context.data.update({
'request_id': request_id or str(uuid.uuid4()),
'user_id': user_id,
**kwargs
})

def get_request_context():
"""Возвращает контекст запроса для текущего потока."""
return getattr(context, 'data', {})

def clear_request_context():
"""Очищает контекст запроса для текущего потока."""
if hasattr(context, 'data'):
del context.data

class ContextualLogger(logging.Logger):
"""Логгер, автоматически добавляющий контекст запроса."""
def _log(self, level, msg, args, exc_info=None, extra=None, stack_info=False, **kwargs):
if extra is None:
extra = {}

# Добавляем контекст запроса к дополнительным данным
req_context = get_request_context()
for key, value in req_context.items():
extra[f'request_{key}'] = value

super()._log(level, msg, args, exc_info, extra, stack_info, **kwargs)

# Регистрируем новый класс логгера
logging.setLoggerClass(ContextualLogger)

# Настраиваем логгер
logger = logging.getLogger('app.contextual')
handler = logging.StreamHandler()
formatter = logging.Formatter('%(asctime)s – [%(request_id)s] – %(levelname)s – %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)
logger.setLevel(logging.INFO)

# Декоратор для установки контекста запроса в функциях
def with_request_context(func):
@wraps(func)
def wrapper(*args, **kwargs):
# Генерируем идентификатор запроса
request_id = str(uuid.uuid4())
try:
# Устанавливаем контекст
set_request_context(request_id=request_id)
return func(*args, **kwargs)
except Exception as e:
# Логируем исключение с контекстом
logger.error(f"Ошибка при выполнении {func.__name__}", exc_info=True)
raise
finally:
# Очищаем контекст
clear_request_context()
return wrapper

# Пример использования
@with_request_context
def process_user_data(user_id):
set_request_context(user_id=user_id, operation='process_data')
logger.info(f"Начало обработки данных для пользователя {user_id}")
try:
# Имитация ошибки
if user_id % 2 == 0:
raise ValueError("Ошибка обработки для пользователей с четными ID")
logger.info("Обработка данных успешно завершена")
except Exception as e:
logger.error(f"Ошибка обработки данных: {e}", exc_info=True)
raise

# Тестовый вызов
try:
process_user_data(2)
except:
pass # Ошибка уже обработана и залогирована

В долгосрочных процессах и фоновых задачах полезно реализовать ротацию логов и управление их размером:

Python
Скопировать код
import logging
from logging.handlers import RotatingFileHandler

# Настройка ротации логов
handler = RotatingFileHandler(
'application.log',
maxBytes=10*1024*1024, # 10 МБ
backupCount=5, # Хранить 5 последних файлов
)
formatter = logging.Formatter('%(asctime)s – %(name)s – %(levelname)s – %(message)s')
handler.setFormatter(formatter)

logger = logging.getLogger('app.rotating')
logger.addHandler(handler)
logger.setLevel(logging.INFO)

# Функция для логирования с уровнем детализации в зависимости от среды
def log_exception(logger, e, env='development'):
"""
Логирует исключение с уровнем детализации, зависящим от среды.

В production выводит меньше деталей, в development — полную трассировку.
"""
if env.lower() == 'production':
# В продакшене выводим только базовую информацию
logger.error(f"Error: {type(e).__name__}: {str(e)}")
else:
# В разработке выводим полную трассировку
logger.error(f"Detailed error", exc_info=True)

Лучшие практики логирования исключений:

  • Структурируйте логи — используйте форматы, которые легко парсятся (JSON, строки с разделителями)
  • Добавляйте контекст — включайте в логи информацию о запросе, пользователе, состоянии приложения
  • Фильтруйте чувствительные данные — не логируйте пароли, токены и другую секретную информацию
  • Используйте уровни логирования осознанно — ERROR для исключений, CRITICAL для критических ошибок
  • Группируйте похожие исключения — это помогает выявлять паттерны ошибок
  • Логируйте и успешные операции — это упрощает отслеживание полного пути выполнения

Пример комплексного решения для логирования исключений в производственном приложении:

Python
Скопировать код
import logging
import json
import time
import hashlib
import traceback
import sys
import threading
from functools import wraps

class ExceptionTracker:
"""Класс для отслеживания и группировки исключений."""

def __init__(self):
self.exceptions = {}
self.lock = threading.Lock()

def track(self, exc_info):
"""Отслеживает исключение и возвращает его уникальный идентификатор."""
exc_type, exc_value, exc_traceback = exc_info

# Создаем хеш исключения на основе типа и места возникновения
last_frame = traceback.extract_tb(exc_traceback)[-1]
exc_signature = f"{exc_type.__name__}:{last_frame.filename}:{last_frame.lineno}"
exc_hash = hashlib.md5(exc_signature.encode()).hexdigest()

with self.lock:
if exc_hash not in self.exceptions:
self.exceptions[exc_hash] = {
'count': 0,
'first_seen': time.time(),
'last_seen': time.time(),
'type': exc_type.__name__,
'message': str(exc_value),
'location': f"{last_frame.filename}:{last_frame.lineno}",
'function': last_frame.name
}

self.exceptions[exc_hash]['count'] += 1
self.exceptions[exc_hash]['last_seen'] = time.time()

return exc_hash

def get_stats(self):
"""Возвращает статистику по отслеживаемым исключениям."""
with self.lock:
return self.exceptions.copy()

# Создаем глобальный трекер исключений
exception_tracker = ExceptionTracker()

class SmartExceptionHandler(logging.Handler):
"""Обработчик логов, который группирует похожие исключения."""

def __init__(self, level=logging.NOTSET):
super().__init__(level)
self.tracker = exception_tracker

def emit(self, record):
if record.exc_info:
exc_hash = self.tracker.track(record.exc_info)
stats = self.tracker.get_stats()[exc_hash]

# Добавляем информацию о группе исключений в запись
record.exc_hash = exc_hash
record.exc_count = stats['count']
record.exc_first_seen = stats['first_seen']

# Передаем запись следующему обработчику
try:
self.format(record)
for handler in logging.getLogger().handlers:
if handler is not self:
handler.handle(record)
except Exception:
self.handleError(record)

# Настройка логгера с умным обработчиком исключений
smart_handler = SmartExceptionHandler()
logging.getLogger().addHandler(smart_handler)

# Обновленный форматтер для JSON-логов с информацией о группе исключений
class EnhancedJsonFormatter(logging.Formatter):
def format(self, record):
log_entry = {
'timestamp': time.time(),
'level': record.levelname,
'logger': record.name,
'message': record.getMessage(),
'thread': threading.get_ident(),
}

# Добавляем информацию о группе исключений
if hasattr(record, 'exc_hash'):
log_entry['exception_group'] = {
'hash': record.exc_hash,
'count': record.exc_count,
'first_seen': record.exc_first_seen
}

# Добавляем информацию об исключении
if record.exc_info:
exc_type, exc_value, exc_tb = record.exc_info
log_entry['exception'] = {
'type': exc_type.__name__,
'message': str(exc_value),
'traceback': traceback.format_exception(exc_type, exc_value, exc_tb)
}

return json.dumps(log_entry)

# Настройка логгера с расширенным форматированием
json_handler = logging.FileHandler('smart_exceptions.log')
json_handler.setFormatter(EnhancedJsonFormatter())

logger = logging.getLogger('app.smart')
logger.addHandler(json_handler)
logger.setLevel(logging.INFO)

# Пример использования
def test_exception_tracking():
for i in range(10):
try:
if i % 3 == 0:
# Одинаковые исключения в одном месте
raise ValueError("Тестовая ошибка типа A")
elif i % 3 == 1:
# Одинаковые исключения в другом месте
raise ValueError("Тестовая ошибка типа B")
else:
# Другой тип исключения
raise KeyError("Тестовая ошибка типа C")
except Exception:
logger.error(f"Итерация {i} завершилась с ошибкой", exc_info=True)

# Запускаем тест
test_exception_tracking()

# Выводим статистику по группам исключений
print("Статистика исключений:")
for exc_hash, stats in exception_tracker.get_stats().items():
print(f"Хеш: {exc_hash}")
print(f" Тип: {stats['type']}")
print(f

Загрузка...