tp-common 0.0.1__py3-none-any.whl → 0.0.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
tp_common/logging.py DELETED
@@ -1,339 +0,0 @@
1
- """
2
- Централизованный модуль для получения логгеров в проекте.
3
-
4
- Единая точка входа — класс Logger(env, job=...).get_logger(name).
5
- Все настройки форматирования и обработки логов находятся здесь.
6
- Очередь и поток для асинхронного логирования общие для всех экземпляров (синглтоны).
7
- """
8
-
9
- from __future__ import annotations
10
-
11
- import logging
12
- import queue
13
- import sys
14
- import threading
15
- import traceback
16
- import uuid
17
- from typing import Any
18
-
19
- # from logging_loki import LokiHandler
20
- from pythonjsonlogger import json
21
- from tp_helper.types.environment_type import EnvironmentType
22
-
23
- # ============================================================================
24
- # ЕДИНАЯ КОНФИГУРАЦИЯ ФОРМАТТЕРА
25
- # ============================================================================
26
- # Все изменения формата логов должны производиться здесь
27
-
28
-
29
- def _create_formatter(
30
- env: EnvironmentType, job: str | int | uuid.UUID | None = None
31
- ) -> json.JsonFormatter:
32
- """
33
- Создаёт JSON-форматтер для логов.
34
-
35
- Единая точка настройки формата логов для всего проекта.
36
-
37
- Args:
38
- env: Окружение (для статического поля в логах).
39
- job: Идентификатор задачи (для статических полей, опционально).
40
-
41
- Returns:
42
- Настроенный JSON форматтер.
43
- """
44
- static_fields = {"env": env.value}
45
- if job is not None:
46
- static_fields["job"] = str(job)
47
-
48
- return json.JsonFormatter(
49
- "{asctime}{levelname}{message}{env}{trace_id}",
50
- style="{",
51
- json_ensure_ascii=False,
52
- rename_fields={
53
- "asctime": "timestamp",
54
- "levelname": "level",
55
- "message": "msg",
56
- },
57
- static_fields=static_fields,
58
- )
59
-
60
-
61
- # ============================================================================
62
- # ФИЛЬТР ДЛЯ ОБРАБОТКИ ИСКЛЮЧЕНИЙ
63
- # ============================================================================
64
-
65
-
66
- class ExceptionFormatterFilter(logging.Filter):
67
- """Filter that formats exceptions into JSON structure and adds to extra."""
68
-
69
- def filter(self, record: logging.LogRecord) -> bool:
70
- """Format exception info into structured JSON if present."""
71
- if record.exc_info and record.exc_info[0] is not None:
72
- e_type, e, exc_traceback = record.exc_info
73
-
74
- if exc_traceback:
75
- tb = traceback.extract_tb(exc_traceback)
76
- if tb:
77
- last = tb[-1]
78
- record.last_tb_line = (
79
- f"{last.filename}:{last.lineno} — {last.name} → {last.line}"
80
- )
81
-
82
- record.error = e_type if e_type else ""
83
- record.error_message = str(e) if e else ""
84
-
85
- return True
86
-
87
-
88
- # ============================================================================
89
- # АСИНХРОННАЯ ОБРАБОТКА ЛОГОВ
90
- # ============================================================================
91
-
92
-
93
- class QueueHandler(logging.Handler):
94
- """Handler that sends log records to a queue for async processing."""
95
-
96
- def __init__(self, log_queue: queue.Queue[logging.LogRecord | None]) -> None:
97
- super().__init__()
98
- self.queue = log_queue
99
-
100
- def emit(self, record: logging.LogRecord) -> None:
101
- """Put the record into the queue."""
102
- try:
103
- self.queue.put_nowait(record)
104
- except queue.Full:
105
- self.handleError(record)
106
- except Exception:
107
- self.handleError(record)
108
-
109
-
110
- def _log_listener_thread(
111
- log_queue: queue.Queue[logging.LogRecord | None],
112
- env: EnvironmentType,
113
- ) -> None:
114
- """Поток для обработки логов из очереди."""
115
-
116
- formatter = _create_formatter(env)
117
- stdout_handler = logging.StreamHandler(sys.stdout)
118
- stdout_handler.setFormatter(formatter)
119
-
120
- while True:
121
- try:
122
- record = log_queue.get()
123
- if record is None: # Сигнал для завершения
124
- break
125
- stdout_handler.emit(record)
126
- except (KeyboardInterrupt, SystemExit):
127
- break
128
- except Exception:
129
- import traceback
130
-
131
- traceback.print_exc(file=sys.stderr)
132
-
133
-
134
- # -----------------------------------------------------------------------------
135
- # Общая очередь и поток (синглтоны)
136
- # -----------------------------------------------------------------------------
137
- # Все экземпляры Logger с use_queue=True используют одну очередь и один поток.
138
- # Это стандартная практика для асинхронного логирования: один listener на всё
139
- # приложение — меньше памяти и контекстных переключений, единый формат вывода.
140
-
141
- _log_queue: queue.Queue[logging.LogRecord | None] | None = None
142
- _log_thread: threading.Thread | None = None
143
- _log_lock = threading.Lock()
144
-
145
-
146
- def _get_log_queue(env: EnvironmentType) -> queue.Queue[logging.LogRecord | None]:
147
- """Получает или создаёт глобальную очередь для логов (один раз на приложение)."""
148
- global _log_queue, _log_thread
149
-
150
- if _log_queue is None:
151
- with _log_lock:
152
- if _log_queue is None:
153
- _log_queue = queue.Queue(-1) # -1 = без ограничения размера
154
- _log_thread = threading.Thread(
155
- target=_log_listener_thread,
156
- args=(_log_queue, env),
157
- daemon=True,
158
- name="LogListenerThread",
159
- )
160
- _log_thread.start()
161
-
162
- return _log_queue
163
-
164
-
165
- # ============================================================================
166
- # ОБЁРТКА ЛОГГЕРА С TRACE_ID
167
- # ============================================================================
168
-
169
-
170
- class TracingLogger:
171
- """
172
- Обёртка над logging.Logger с методами set_trace_id / clear_trace_id.
173
-
174
- trace_id хранится в экземпляре: простой вариант, не зависит от contextvars.
175
- Передавайте этот логгер по цепочке вызовов — везде будет один и тот же trace_id.
176
-
177
- Ограничение: один экземпляр логгера не должен использоваться из нескольких
178
- параллельных asyncio-задач с разными trace_id (будут перезаписывать друг друга).
179
- Для цикла «одна задача за раз» — подходит идеально.
180
- """
181
-
182
- def __init__(self, logger: logging.Logger) -> None:
183
- self._logger = logger
184
- self._trace_id: str | uuid.UUID | None = None
185
-
186
- def set_trace_id(self, value: str | uuid.UUID | None) -> None:
187
- """Устанавливает trace_id для этого экземпляра логгера."""
188
- self._trace_id = value
189
-
190
- def clear_trace_id(self) -> None:
191
- """Сбрасывает trace_id."""
192
- self._trace_id = None
193
-
194
- def _merge_extra(self, kwargs: dict) -> dict:
195
- extra = dict(kwargs.get("extra") or {})
196
- if self._trace_id is not None:
197
- extra["trace_id"] = str(self._trace_id)
198
- return {**kwargs, "extra": extra}
199
-
200
- def debug(self, msg: str, *args: Any, **kwargs: Any) -> None:
201
- self._logger.debug(msg, *args, **self._merge_extra(kwargs))
202
-
203
- def info(self, msg: str, *args: Any, **kwargs: Any) -> None:
204
- self._logger.info(msg, *args, **self._merge_extra(kwargs))
205
-
206
- def warning(self, msg: str, *args: Any, **kwargs: Any) -> None:
207
- self._logger.warning(msg, *args, **self._merge_extra(kwargs))
208
-
209
- def error(self, msg: str, *args: Any, **kwargs: Any) -> None:
210
- self._logger.error(msg, *args, **self._merge_extra(kwargs))
211
-
212
- def exception(self, msg: str, *args: Any, **kwargs: Any) -> None:
213
- self._logger.exception(msg, *args, **self._merge_extra(kwargs))
214
-
215
- def critical(self, msg: str, *args: Any, **kwargs: Any) -> None:
216
- self._logger.critical(msg, *args, **self._merge_extra(kwargs))
217
-
218
- def log(self, level: int, msg: str, *args: Any, **kwargs: Any) -> None:
219
- self._logger.log(level, msg, *args, **self._merge_extra(kwargs))
220
-
221
- @property
222
- def logger(self) -> logging.Logger:
223
- """Доступ к исходному logging.Logger при необходимости."""
224
- return self._logger
225
-
226
-
227
- # ============================================================================
228
- # КЛАСС LOGGER — ЕДИНАЯ ТОЧКА ВХОДА ДЛЯ ЛОГГЕРОВ
229
- # ============================================================================
230
-
231
-
232
- class Logger:
233
- """
234
- Фабрика логгеров с фиксированной средой (env) и общей очередью.
235
-
236
- Создаётся один раз на уровне приложения с нужной средой (DEV/PROD/...).
237
- В разных местах из неё получают логгеры через get_logger(name, job=...).
238
- job передаётся в get_logger, а не в конструктор — у каждого логгера свой job.
239
-
240
- Все экземпляры Logger с use_queue=True пишут в одну общую очередь и один
241
- фоновый поток (синглтоны на уровне модуля).
242
-
243
- Использование:
244
- from tp_helper.logging import Logger
245
- from tp_helper.types.environment_type import EnvironmentType
246
-
247
- # Один раз на уровне приложения — фабрика с желаемой средой
248
- log_factory = Logger(EnvironmentType.DEV)
249
-
250
- # В разных местах получаем логгеры (job — при необходимости)
251
- logger = log_factory.get_logger("api_service")
252
- worker_logger = log_factory.get_logger("task_worker", job="task_123")
253
-
254
- # trace_id для сквозной трассировки (например, в воркере по одной задаче)
255
- # logger.set_trace_id(uuid.uuid4())
256
- # logger.debug("Получена задача", extra=task) # в логе будет trace_id
257
-
258
- # Настройка uvicorn/fastapi при старте
259
- Logger.setup_standard_loggers(EnvironmentType.DEV)
260
- """
261
-
262
- def __init__(self, env: EnvironmentType) -> None:
263
- """
264
- Args:
265
- env: Окружение (local/dev/staging/prod) для поля в логах.
266
- """
267
- self._env = env
268
-
269
- def get_logger(
270
- self,
271
- name: str | int,
272
- job: str | uuid.UUID | int | None = None,
273
- # loki_handler: LokiHandler | None = None,
274
- use_queue: bool = True,
275
- ) -> TracingLogger:
276
- """
277
- Возвращает логгер с именем name и опциональным job.
278
-
279
- Возвращаемый объект — TracingLogger (обёртка с set_trace_id/clear_trace_id).
280
- trace_id хранится в экземпляре и попадает во все записи лога; можно
281
- передавать логгер по цепочке вызовов.
282
-
283
- Args:
284
- name: Имя логгера (идентификация в логах).
285
- job: Идентификатор задачи (опционально), попадает в статические поля.
286
- loki_handler: Опциональный handler для отправки в Loki.
287
- use_queue: True — асинхронная запись через общую очередь (по умолчанию).
288
-
289
- Returns:
290
- TracingLogger с методами set_trace_id/clear_trace_id.
291
- """
292
- log_queue = _get_log_queue(self._env) if use_queue else None
293
-
294
- logger = logging.getLogger(f"app_logger_{name}")
295
- logger.setLevel(logging.DEBUG)
296
- logger.propagate = False
297
- logger.addFilter(ExceptionFormatterFilter())
298
-
299
- if use_queue and log_queue:
300
- logger.addHandler(QueueHandler(log_queue))
301
- else:
302
- formatter = _create_formatter(self._env, job)
303
- stdout_handler = logging.StreamHandler(sys.stdout)
304
- stdout_handler.setFormatter(formatter)
305
- logger.addHandler(stdout_handler)
306
-
307
- # if loki_handler:
308
- # formatter = _create_formatter(self._env, job)
309
- # loki_handler.setFormatter(formatter)
310
- # logger.addHandler(loki_handler)
311
-
312
- return TracingLogger(logger)
313
-
314
- @staticmethod
315
- def setup_standard_loggers(env: EnvironmentType) -> None:
316
- """
317
- Настраивает логирование для uvicorn и fastapi (единый JSON-формат).
318
-
319
- Вызывать при инициализации API-приложения.
320
- """
321
- json_formatter = _create_formatter(env)
322
- standard_handler = logging.StreamHandler(sys.stdout)
323
- standard_handler.setFormatter(json_formatter)
324
-
325
- uvicorn_logger = logging.getLogger("uvicorn")
326
- uvicorn_logger.setLevel(logging.INFO)
327
- uvicorn_logger.handlers = [standard_handler]
328
- uvicorn_logger.propagate = False
329
-
330
- uvicorn_access_logger = logging.getLogger("uvicorn.access")
331
- uvicorn_access_logger.setLevel(logging.WARNING)
332
- uvicorn_access_logger.propagate = False
333
-
334
- fastapi_logger = logging.getLogger("fastapi")
335
- fastapi_logger.setLevel(logging.INFO)
336
- fastapi_logger.handlers = [standard_handler]
337
- fastapi_logger.propagate = False
338
-
339
- logging.getLogger("src.infrastructure.clients").setLevel(logging.INFO)
@@ -1,12 +0,0 @@
1
- tp_common/__init__.py,sha256=2PQG0ZnJt11WcRYF5rBYhYjXq_1oKrjM760LTEBW8sU,1621
2
- tp_common/base_client/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
- tp_common/base_client/base_client.py,sha256=WYjuANyoczyoavnxXIQzvd3GIzeczG6w8jRxdsEpOG4,24996
4
- tp_common/base_client/base_exception.py,sha256=vN7Qq-2QIHPsMNUdQ-2G_WDnF-UEcM9gA0L5LwETPlA,57
5
- tp_common/base_client/base_request.py,sha256=U_ucIFkxVbRMe2gjx_bXATZU4-OvK7r3u6vk1LVanCM,332
6
- tp_common/base_client/base_response.py,sha256=Y65bNcNIswRi1x49jhC7nuoxs9wuzdzYvZWZ4eFTy4Q,301
7
- tp_common/base_client/client_exceptions.py,sha256=pC5KmcDEnP28r8lfMzuQNKsGfZhSoVua2JXmyGwaUxM,2234
8
- tp_common/base_client/domain_exceptions.py,sha256=JoDklz4Uxvk3BJZDxjWsWc5zjASisypQvtA0wEPngsA,1880
9
- tp_common/logging.py,sha256=YiH3NJmWKIYKB7Uoe81w-Xlov7Rtz9SF7r9yKTuXnbc,14538
10
- tp_common-0.0.1.dist-info/METADATA,sha256=N6T0o_UdcHpoURKID2U70c7b5iLV5JI23v9sr8EGceQ,6582
11
- tp_common-0.0.1.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
12
- tp_common-0.0.1.dist-info/RECORD,,