tp-common 0.0.2__tar.gz → 0.0.3__tar.gz
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-0.0.2 → tp_common-0.0.3}/PKG-INFO +1 -1
- {tp_common-0.0.2 → tp_common-0.0.3}/pyproject.toml +4 -1
- tp_common-0.0.2/src/tp_common/base_client/base_client.py → tp_common-0.0.3/src/tp_common/base_client/client.py +1 -1
- tp_common-0.0.3/src/tp_common/base_client/exceptions.py +138 -0
- tp_common-0.0.2/src/tp_common/base_client/base_request.py → tp_common-0.0.3/src/tp_common/route/shames.py +7 -2
- tp_common-0.0.2/src/tp_common/__init__.py +0 -52
- tp_common-0.0.2/src/tp_common/base_client/base_exception.py +0 -2
- tp_common-0.0.2/src/tp_common/base_client/base_response.py +0 -11
- tp_common-0.0.2/src/tp_common/base_client/client_exceptions.py +0 -76
- tp_common-0.0.2/src/tp_common/base_client/domain_exceptions.py +0 -63
- tp_common-0.0.2/src/tp_common/logging.py +0 -339
- {tp_common-0.0.2 → tp_common-0.0.3}/README.md +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "tp-common"
|
|
3
|
-
version = "0.0.
|
|
3
|
+
version = "0.0.3"
|
|
4
4
|
description = ""
|
|
5
5
|
authors = [
|
|
6
6
|
{name = "Developer",email = "front-gold@mail.ru"}
|
|
@@ -18,6 +18,9 @@ dependencies = [
|
|
|
18
18
|
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
|
19
19
|
build-backend = "poetry.core.masonry.api"
|
|
20
20
|
|
|
21
|
+
#[tool.poetry.packages]
|
|
22
|
+
#packages = [{include = "tp_common", from = "src"}]
|
|
23
|
+
|
|
21
24
|
# === настройки проверки типизации ===
|
|
22
25
|
[tool.isort]
|
|
23
26
|
profile = "black"
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""Исключения для базового HTTP клиента."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ClientException(Exception):
|
|
5
|
+
"""Базовое исключение для всех ошибок клиента."""
|
|
6
|
+
|
|
7
|
+
def __init__(
|
|
8
|
+
self,
|
|
9
|
+
message: str,
|
|
10
|
+
url: str | None = None,
|
|
11
|
+
status_code: int | None = None,
|
|
12
|
+
response_body: str | None = None,
|
|
13
|
+
) -> None:
|
|
14
|
+
super().__init__(message)
|
|
15
|
+
self.url = url
|
|
16
|
+
self.status_code = status_code
|
|
17
|
+
self.response_body = response_body
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ClientResponseErrorException(ClientException):
|
|
22
|
+
"""Исключение при неуспешном HTTP статусе (>=400)."""
|
|
23
|
+
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
message: str,
|
|
27
|
+
url: str | None = None,
|
|
28
|
+
status_code: int | None = None,
|
|
29
|
+
response_body: str | None = None,
|
|
30
|
+
) -> None:
|
|
31
|
+
super().__init__(message, url, status_code, response_body)
|
|
32
|
+
|
|
33
|
+
class ClientConnectionException(ClientException):
|
|
34
|
+
"""Исключение при ошибке соединения (не удалось установить соединение)."""
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
message: str,
|
|
39
|
+
url: str | None = None,
|
|
40
|
+
) -> None:
|
|
41
|
+
super().__init__(message, url)
|
|
42
|
+
|
|
43
|
+
class ClientTimeoutException(ClientConnectionException):
|
|
44
|
+
"""Исключение при таймауте соединения."""
|
|
45
|
+
|
|
46
|
+
def __init__(
|
|
47
|
+
self,
|
|
48
|
+
message: str,
|
|
49
|
+
url: str | None = None,
|
|
50
|
+
) -> None:
|
|
51
|
+
super().__init__(message, url)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class ClientProxyException(ClientConnectionException):
|
|
55
|
+
"""Исключение при ошибке прокси."""
|
|
56
|
+
|
|
57
|
+
def __init__(
|
|
58
|
+
self,
|
|
59
|
+
message: str,
|
|
60
|
+
url: str | None = None,
|
|
61
|
+
proxy: str | None = None,
|
|
62
|
+
) -> None:
|
|
63
|
+
super().__init__(message, url)
|
|
64
|
+
self.proxy = proxy
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class ClientDNSException(ClientConnectionException):
|
|
71
|
+
"""Исключение при ошибке DNS (не удалось разрешить доменное имя)."""
|
|
72
|
+
|
|
73
|
+
def __init__(
|
|
74
|
+
self,
|
|
75
|
+
message: str,
|
|
76
|
+
url: str | None = None,
|
|
77
|
+
) -> None:
|
|
78
|
+
super().__init__(message, url)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class ClientBusinessErrorException(ClientException):
|
|
82
|
+
"""Базовое исключение для бизнес-ошибок (400, 401, 403, 404, 422)."""
|
|
83
|
+
|
|
84
|
+
pass
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class ClientServerErrorException(ClientException):
|
|
88
|
+
"""Базовое исключение для ошибок сервера (500, 502, 503, 504)."""
|
|
89
|
+
|
|
90
|
+
pass
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class BaseNetworkErrorException(ClientException):
|
|
94
|
+
"""Базовое исключение для сетевых ошибок (таймауты, соединение, DNS)."""
|
|
95
|
+
|
|
96
|
+
pass
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class ClientProxyErrorException(ClientException):
|
|
100
|
+
"""Базовое исключение для ошибок прокси."""
|
|
101
|
+
|
|
102
|
+
pass
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class ClientAuthorizationException(ClientBusinessErrorException):
|
|
106
|
+
"""Исключение при ошибке авторизации (401, 403)."""
|
|
107
|
+
|
|
108
|
+
pass
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class ClientValidationException(ClientBusinessErrorException):
|
|
112
|
+
"""Исключение при ошибке валидации данных (400, 422)."""
|
|
113
|
+
|
|
114
|
+
pass
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class ClientResourceNotFoundException(ClientBusinessErrorException):
|
|
118
|
+
"""Исключение при отсутствии ресурса (404)."""
|
|
119
|
+
|
|
120
|
+
pass
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class ClientServerException(ClientServerErrorException):
|
|
124
|
+
"""Исключение при ошибке сервера (500, 502, 503, 504)."""
|
|
125
|
+
|
|
126
|
+
pass
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class ClientTooManyRequestsException(ClientProxyErrorException):
|
|
130
|
+
"""Исключение при превышении лимита запросов (429)."""
|
|
131
|
+
|
|
132
|
+
pass
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class ClientServiceUnavailableException(ClientServerErrorException):
|
|
136
|
+
"""Исключение при недоступности сервиса (502, 503, 504)."""
|
|
137
|
+
|
|
138
|
+
pass
|
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
class BaseResponse(BaseModel):
|
|
2
|
+
model_config = ConfigDict(
|
|
3
|
+
alias_generator=to_camel,
|
|
4
|
+
validate_by_name=True,
|
|
5
|
+
validate_by_alias=True,
|
|
6
|
+
serialize_by_alias=True,
|
|
7
|
+
)
|
|
3
8
|
|
|
4
9
|
|
|
5
10
|
class BaseRequest(BaseModel):
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
from tp_common.base.base_client.base_client import BaseClient
|
|
2
|
-
from tp_common.base.base_client.base_exception import BaseInfrastructureException
|
|
3
|
-
from tp_common.base.base_client.base_request import BaseRequest
|
|
4
|
-
from tp_common.base.base_client.base_response import BaseResponse
|
|
5
|
-
from tp_common.base.base_client.client_exceptions import (
|
|
6
|
-
BaseClientException,
|
|
7
|
-
ClientConnectionException,
|
|
8
|
-
ClientDNSException,
|
|
9
|
-
ClientProxyException,
|
|
10
|
-
ClientResponseErrorException,
|
|
11
|
-
ClientTimeoutException,
|
|
12
|
-
)
|
|
13
|
-
from tp_common.base.base_client.domain_exceptions import (
|
|
14
|
-
AuthorizationException,
|
|
15
|
-
BaseBusinessErrorException,
|
|
16
|
-
BaseNetworkErrorException,
|
|
17
|
-
BaseProxyErrorException,
|
|
18
|
-
BaseServerErrorException,
|
|
19
|
-
ResourceNotFoundException,
|
|
20
|
-
ServerException,
|
|
21
|
-
ServiceUnavailableException,
|
|
22
|
-
TooManyRequestsException,
|
|
23
|
-
ValidationException,
|
|
24
|
-
)
|
|
25
|
-
from tp_common.logging import Logger, TracingLogger
|
|
26
|
-
|
|
27
|
-
__all__ = [
|
|
28
|
-
# BaseClient
|
|
29
|
-
"BaseClient",
|
|
30
|
-
"BaseInfrastructureException",
|
|
31
|
-
"BaseRequest",
|
|
32
|
-
"BaseResponse",
|
|
33
|
-
"BaseClientException",
|
|
34
|
-
"ClientResponseErrorException",
|
|
35
|
-
"ClientTimeoutException",
|
|
36
|
-
"ClientProxyException",
|
|
37
|
-
"ClientConnectionException",
|
|
38
|
-
"ClientDNSException",
|
|
39
|
-
"BaseBusinessErrorException",
|
|
40
|
-
"BaseServerErrorException",
|
|
41
|
-
"BaseNetworkErrorException",
|
|
42
|
-
"BaseProxyErrorException",
|
|
43
|
-
"AuthorizationException",
|
|
44
|
-
"ValidationException",
|
|
45
|
-
"ResourceNotFoundException",
|
|
46
|
-
"ServerException",
|
|
47
|
-
"TooManyRequestsException",
|
|
48
|
-
"ServiceUnavailableException",
|
|
49
|
-
# Logging
|
|
50
|
-
"TracingLogger",
|
|
51
|
-
"Logger",
|
|
52
|
-
]
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
from pydantic import BaseModel, ConfigDict
|
|
2
|
-
from pydantic.alias_generators import to_camel
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
class BaseResponse(BaseModel):
|
|
6
|
-
model_config = ConfigDict(
|
|
7
|
-
alias_generator=to_camel,
|
|
8
|
-
validate_by_name=True,
|
|
9
|
-
validate_by_alias=True,
|
|
10
|
-
serialize_by_alias=True,
|
|
11
|
-
)
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
"""Исключения для базового HTTP клиента."""
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
class BaseClientException(Exception):
|
|
5
|
-
"""Базовое исключение для всех ошибок клиента."""
|
|
6
|
-
|
|
7
|
-
def __init__(
|
|
8
|
-
self,
|
|
9
|
-
message: str,
|
|
10
|
-
url: str | None = None,
|
|
11
|
-
status_code: int | None = None,
|
|
12
|
-
response_body: str | None = None,
|
|
13
|
-
) -> None:
|
|
14
|
-
super().__init__(message)
|
|
15
|
-
self.url = url
|
|
16
|
-
self.status_code = status_code
|
|
17
|
-
self.response_body = response_body
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
class ClientResponseErrorException(BaseClientException):
|
|
21
|
-
"""Исключение при неуспешном HTTP статусе (>=400)."""
|
|
22
|
-
|
|
23
|
-
def __init__(
|
|
24
|
-
self,
|
|
25
|
-
message: str,
|
|
26
|
-
url: str | None = None,
|
|
27
|
-
status_code: int | None = None,
|
|
28
|
-
response_body: str | None = None,
|
|
29
|
-
) -> None:
|
|
30
|
-
super().__init__(message, url, status_code, response_body)
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
class ClientTimeoutException(BaseClientException):
|
|
34
|
-
"""Исключение при таймауте соединения."""
|
|
35
|
-
|
|
36
|
-
def __init__(
|
|
37
|
-
self,
|
|
38
|
-
message: str,
|
|
39
|
-
url: str | None = None,
|
|
40
|
-
) -> None:
|
|
41
|
-
super().__init__(message, url)
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
class ClientProxyException(BaseClientException):
|
|
45
|
-
"""Исключение при ошибке прокси."""
|
|
46
|
-
|
|
47
|
-
def __init__(
|
|
48
|
-
self,
|
|
49
|
-
message: str,
|
|
50
|
-
url: str | None = None,
|
|
51
|
-
proxy: str | None = None,
|
|
52
|
-
) -> None:
|
|
53
|
-
super().__init__(message, url)
|
|
54
|
-
self.proxy = proxy
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
class ClientConnectionException(BaseClientException):
|
|
58
|
-
"""Исключение при ошибке соединения (не удалось установить соединение)."""
|
|
59
|
-
|
|
60
|
-
def __init__(
|
|
61
|
-
self,
|
|
62
|
-
message: str,
|
|
63
|
-
url: str | None = None,
|
|
64
|
-
) -> None:
|
|
65
|
-
super().__init__(message, url)
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
class ClientDNSException(BaseClientException):
|
|
69
|
-
"""Исключение при ошибке DNS (не удалось разрешить доменное имя)."""
|
|
70
|
-
|
|
71
|
-
def __init__(
|
|
72
|
-
self,
|
|
73
|
-
message: str,
|
|
74
|
-
url: str | None = None,
|
|
75
|
-
) -> None:
|
|
76
|
-
super().__init__(message, url)
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
"""Доменные исключения для обработки ошибок в воркерах."""
|
|
2
|
-
|
|
3
|
-
from tp_common.base.base_client.client_exceptions import BaseClientException
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
class BaseBusinessErrorException(BaseClientException):
|
|
7
|
-
"""Базовое исключение для бизнес-ошибок (400, 401, 403, 404, 422)."""
|
|
8
|
-
|
|
9
|
-
pass
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
class BaseServerErrorException(BaseClientException):
|
|
13
|
-
"""Базовое исключение для ошибок сервера (500, 502, 503, 504)."""
|
|
14
|
-
|
|
15
|
-
pass
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
class BaseNetworkErrorException(BaseClientException):
|
|
19
|
-
"""Базовое исключение для сетевых ошибок (таймауты, соединение, DNS)."""
|
|
20
|
-
|
|
21
|
-
pass
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
class BaseProxyErrorException(BaseClientException):
|
|
25
|
-
"""Базовое исключение для ошибок прокси."""
|
|
26
|
-
|
|
27
|
-
pass
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
class AuthorizationException(BaseBusinessErrorException):
|
|
31
|
-
"""Исключение при ошибке авторизации (401, 403)."""
|
|
32
|
-
|
|
33
|
-
pass
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
class ValidationException(BaseBusinessErrorException):
|
|
37
|
-
"""Исключение при ошибке валидации данных (400, 422)."""
|
|
38
|
-
|
|
39
|
-
pass
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
class ResourceNotFoundException(BaseBusinessErrorException):
|
|
43
|
-
"""Исключение при отсутствии ресурса (404)."""
|
|
44
|
-
|
|
45
|
-
pass
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
class ServerException(BaseServerErrorException):
|
|
49
|
-
"""Исключение при ошибке сервера (500, 502, 503, 504)."""
|
|
50
|
-
|
|
51
|
-
pass
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
class TooManyRequestsException(BaseProxyErrorException):
|
|
55
|
-
"""Исключение при превышении лимита запросов (429)."""
|
|
56
|
-
|
|
57
|
-
pass
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
class ServiceUnavailableException(BaseServerErrorException):
|
|
61
|
-
"""Исключение при недоступности сервиса (502, 503, 504)."""
|
|
62
|
-
|
|
63
|
-
pass
|
|
@@ -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)
|
|
File without changes
|