Всем привет.

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

Пошел искать решение в лоб: искал готовые решения в виде сторонних библиотек. Отыскал пару-тройку годных решений. Например библиотека json-logging. Всё хорошо: почитал, что делает и поставил не глядя. Данная библиотека имеет массу преимуществ:

  • может напрямую работать с FastAPI, Flask, Aiohttp, Sanic,

  • перехватывает uvicorn access log,

  • производит форматирование запроса в JSON формат.

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

Код
{
	"type": "request",
	"written_at": "2017-12-23T16:55:37.280Z",
	"written_ts": 1514048137280721000,
	"component_id": "-",
	"component_name": "-",
	"component_instance_idx": 0,
	"correlation_id": "1975a02e-e802-11e7-8971-28b2bd90b19a",
	"remote_user": "user_a",
	"request": "/index.html",
	"referer": "-",
	"x_forwarded_for": "-",
	"protocol": "HTTP/1.1",
	"method": "GET",
	"remote_ip": "127.0.0.1",
	"request_size_b": 1234,
	"remote_host": "127.0.0.1",
	"remote_port": 50160,
	"request_received_at": "2017-12-23T16:55:37.280Z",
	"response_time_ms": 0,
	"response_status": 200,
	"response_size_b": "122",
	"response_content_type": "text/html; charset=utf-8",
	"response_sent_at": "2017-12-23T16:55:37.280Z"
}

Но у этой библиотеки обнаружились серьёзные недостатки:

  • Название полей в логе изменить нельзя без наследования Formatter и кастомизацию.

  • Нельзя никаким образом поставить в логирование тело ответа без написания собственного Middleware для FastAPI, который перехватит еще и тело ответа, что мы добавили в заголовки.

  • Логгирование перехватывало синхронный поток вывода логгера uvicorn.access.

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

pip uninstall json-logging

Далее я хотел обозначить структуру журнала. Мне очень помог Pydantic для элегантного описания данного вопроса:

from typing import Union

from pydantic import BaseModel, Field


class BaseJsonLogSchema(BaseModel):
    """
    Схема основного тела лога в формате JSON
    """
    thread: Union[int, str]
    level: int
    level_name: str
    message: str
    source: str
    timestamp: str = Field(..., alias='@timestamp')
    app_name: str
    app_version: str
    app_env: str
    duration: int
    exceptions: Union[list[str], str] = None
    trace_id: str = None
    span_id: str = None
    parent_id: str = None

    class Config:
        allow_population_by_field_name = True


class RequestJsonLogSchema(BaseModel):
    """
    Схема части запросов-ответов лога в формате JSON
    """
    request_uri: str
    request_referer: str
    request_protocol: str
    request_method: str
    request_path: str
    request_host: str
    request_size: int
    request_content_type: str
    request_headers: str
    request_body: str
    request_direction: str
    remote_ip: str
    remote_port: str
    response_status_code: int
    response_size: int
    response_headers: str
    response_body: str
    duration: int

После того, как была сформирована структура тела запроса необходимо определить сам механизм форматирования. Формирование, что логично, производится с помощью создания класса JSONLogFormatter с наследованием от стандартного logging.Formatter.
Код test_project.utils.json_logger.py

import datetime
import json
import logging
import traceback

from test_project.main.settings import APP_NAME, \
    APP_VERSION, ENVIRONMENT
from test_project.utils.utils_schemas import BaseJsonLogSchema


class JSONLogFormatter(logging.Formatter):
    """
    Кастомизированный класс-форматер для логов в формате json
    """

    def format(self, record: logging.LogRecord, *args, **kwargs) -> str:
        """
        Преобразование объект журнала в json

        :param record: объект журнала
        :return: строка журнала в JSON формате
        """
        log_object: dict = self._format_log_object(record)
        return json.dumps(log_object, ensure_ascii=False)

    @staticmethod
    def _format_log_object(record: logging.LogRecord) -> dict:
        """
        Перевод записи объекта журнала
        в json формат с необходимым перечнем полей

        :param record: объект журнала
        :return: Словарь с объектами журнала
        """
        now = datetime \
            .datetime \
            .fromtimestamp(record.created) \
            .astimezone() \
            .replace(microsecond=0) \
            .isoformat()
        message = record.getMessage()
        duration = record.duration \
            if hasattr(record, 'duration') \
            else record.msecs
        # Инициализация тела журнала
        json_log_fields = BaseJsonLogSchema(
            thread=record.process,
            timestamp=now,
            level=record.levelno,
            level_name=LEVEL_TO_NAME[record.levelno],
            message=message,
            source=record.name,
            duration=duration,
            app_name=APP_NAME,
            app_version=APP_VERSION,
            app_env=ENVIRONMENT,
        )

        if hasattr(record, 'props'):
            json_log_fields.props = record.props

        if record.exc_info:
            json_log_fields.exceptions = \
                traceback.format_exception(*record.exc_info)

        elif record.exc_text:
            json_log_fields.exceptions = record.exc_text
        # Преобразование Pydantic объекта в словарь
        json_log_object = json_log_fields.dict(
            exclude_unset=True,
            by_alias=True,
        )
        # Соединение дополнительных полей логирования
        if hasattr(record, 'request_json_fields'):
            json_log_object.update(record.request_json_fields)

        return json_log_object

Нужно заставить FastApi ловить request и response, желательно с наличием body. Я сначала пытался обратиться к dependency подходу FastApi. Но понял, что не хочу описывать в каждом методе метод логирования. Естественно, к нам на помощь приходит Middleware.
Я написал класс, который позволяет внедрить в app-объект журналирование тела запроса и ответа, а также их полей: status code, headers, remote ip, protocol, etc. Класс я назвал LoggingMiddleware и все выполнение происходит методу async def __call__()

Код test_project.utils.middlewares.py

from test_project.main.settings import PORT
from test_project.utils.json_logger import EMPTY_VALUE
from test_project.utils.utils_schemas import RequestJsonLogSchema


class LoggingMiddleware:
    """
    Middleware для обработки запросов и ответов с целью журналирования
    """
    @staticmethod
    async def get_protocol(request: Request) -> str:
        protocol = str(request.scope.get('type', ''))
        http_version = str(request.scope.get('http_version', ''))
        if protocol.lower() == 'http' and http_version:
            return f'{protocol.upper()}/{http_version}'
        return EMPTY_VALUE

    @staticmethod
    async def set_body(request: Request, body: bytes) -> None:
        async def receive() -> Message:
            return {'type': 'http.request', 'body': body}

        request._receive = receive

    async def get_body(self, request: Request) -> bytes:
        body = await request.body()
        await self.set_body(request, body)
        return body

    async def __call__(
            self, request: Request, call_next: RequestResponseEndpoint,
            *args, **kwargs
    ):
        start_time = time.time()
        exception_object = None
        # Request Side
        try:
            raw_request_body = await request.body()
            # Последующие действия нужны, 
            # чтобы не перезатереть тело запроса
						# и не уйти в зависание event-loop'a
            # при последующем получении тела ответа
            await self.set_body(request, raw_request_body)
            raw_request_body = await self.get_body(request)
            request_body = raw_request_body.decode()
        except Exception:
            request_body = EMPTY_VALUE

        server: tuple = request.get('server', ('localhost', PORT))
        request_headers: dict = dict(request.headers.items())
        # Response Side
        try:
            response = await call_next(request)
        except Exception as ex:
            response_body = bytes(
                http.HTTPStatus.INTERNAL_SERVER_ERROR.phrase.encode()
            )
            response = Response(
                content=response_body,
                status_code=http.HTTPStatus.INTERNAL_SERVER_ERROR.real,
            )
            exception_object = ex
            response_headers = {}
        else:
            response_headers = dict(response.headers.items())
            response_body = b''
            async for chunk in response.body_iterator:
                response_body += chunk
            response = Response(
                content=response_body,
                status_code=response.status_code,
                headers=dict(response.headers),
                media_type=response.media_type
            )
        duration: int = math.ceil((time.time() - start_time) * 1000)
				# Инициализация и формирования полей для запроса-ответа
        request_json_fields = RequestJsonLogSchema(
            request_uri=str(request.url),
            request_referer=request_headers.get('referer', EMPTY_VALUE),
            request_protocol=await self.get_protocol(request),
            request_method=request.method,
            request_path=request.url.path,
            request_host=f'{server[0]}:{server[1]}',
            request_size=int(request_headers.get('content-length', 0)),
            request_content_type=request_headers.get(
                'content-type', EMPTY_VALUE),
            request_headers=json.dumps(request_headers),
            request_body=request_body,
            request_direction='in',
            remote_ip=request.client[0],
            remote_port=request.client[1],
            response_status_code=response.status_code,
            response_size=int(response_headers.get('content-length', 0)),
            response_headers=json.dumps(response_headers),
            response_body=response_body.decode(),
            duration=duration
        ).dict()
        # Хочется на каждый запрос читать 
        # и понимать в сообщении самое главное, 
        # поэтому message мы сразу делаем типовым
        message = f'{"Ошибка" if exception_object else "Ответ"} ' \
                  f'с кодом {response.status_code} ' \
                  f'на запрос {request.method} \"{str(request.url)}\", ' \
                  f'за {duration} мс'
        logger.info(
            message,
            extra={
                'request_json_fields': request_json_fields,
                'to_mask': True,
            },
            exc_info=exception_object,
        )
        return response

У меня есть одержимость по поводу асинхронного логирования в FastApi. Методы вывода потока в консоль, запись в файл, logstash - это тот же I/O процесс, как и сгонять по сетке в БД или в другой сервис, согласны? А так как это I/O процесс, то в асинхронном FastApi очень важно не замедлять работу такой простой штукой как логирование. Я нашел очень хорошую библиотеку, которая за одну конфигурацию перестраивает вывод в консоль в асинхронный режим (также можно провернуть и с файлами и отправкой в logstash, и другие ресурсы). Данная библиотека называется: asynclog. Настройка и установка производится очень просто. Не нужно иметь никаких классов и переопределять handler-ы: Нужно просто настроить логирование через словарь в конфигурации проекта и задать handler.

Создаем конфигурацию логера и использовать его везде. Благо, в python есть для этого конфигурация, которую можно воплотить в настройках проекта.

LOG_CONFIG = {
    'version': 1,
    'disable_existing_loggers': False,
    'formatters': {
        'json': {
            '()': 'test_project.utils.json_logger.JSONLogFormatter',
        },
    },
    'handlers': {
# Используем AsyncLogDispatcher для асинхронного вывода потока. 
        'json': {
            'formatter': 'json',
            'class': 'asynclog.AsyncLogDispatcher',
            'func': 'test_project.utils.json_logger.write_log',
        },
    },
    'loggers': {
        'test_project': {
            'handlers': ['json'],
            'level': 'DEBUG' if DEBUG else 'INFO',
            'propagate': False,
        },
        'uvicorn': {
            'handlers': ['json'],
            'level': 'INFO',
            'propagate': False,
        },
      # Не даем стандартному логгеру fastapi работать по пустякам и замедлять работу сервиса
        'uvicorn.access': {
            'handlers': ['json'],
            'level': 'ERROR',
            'propagate': False,
        },
    },
}

Здесь у handler есть особенность в поле func, тут должна быть указана функция вывода потока. Это может быть stdout, print, write file. Что угодно, вплоть до передачи файла по сети в logstash. В нашем случае я решил просто вывести в консоль простейшим способом для примера.
Код test_project.utils.json_logger.py:

def write_log(msg):
    print(msg)

Настраиваем приложение на работу с данным middleware, инициализируем logging и его конфигурацию. Далее производим написание обычного route с логгированием сообщения 'Test log'.

import logging
from logging.config import dictConfig

from fastapi import FastAPI
from test_project.utils.middlewares import LoggingMiddleware
dictConfig(LOG_CONFIG)
logger = logging.getLogger(name)
app = FastAPI()
app.middleware('http')(
    LoggingMiddleware()
)
router = APIRouter(
    tags=['root'],
    responses={404: {'description': 'Not found'}},
)
@router.get('/')
async def root(request: Request):
    logger.info('Test log')
    return {'foo': 'bar'}
app.include_router(router)

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

{
    "thread": 60535,
    "level": 20,
    "level_name": "Information",
    "message": "Ответ с кодом 200 на запрос GET \"http://localhost:8080/\", за 4 мс",
    "source": "test_project.utils.middlewares",
    "@timestamp": "2021-08-28T01:49:23+03:00",
    "app_name": "test_project",
    "app_version": "1.0.0",
    "app_env": "LOCAL",
    "duration": 4,
    "request_uri": "http://localhost:8080/",
    "request_referer": "",
    "request_protocol": "HTTP/1.1",
    "request_method": "GET",
    "request_path": "/",
    "request_host": "127.0.0.1:8080",
    "request_size": 0,
    "request_content_type": "",
    "request_headers": "{\"host\": \"localhost:8080\"}",
    "request_body": "",
    "request_direction": "in",
    "remote_ip": "127.0.0.1",
    "remote_port": "50259",
    "response_status_code": 200,
    "response_size": 13,
    "response_headers": "{\"content-length\": \"13\", \"content-type\": \"application/json\"}",
    "response_body": "{\"foo\":\"baz\"}"
}

Второе: Наше сообщение 'Test Log' вне контекста HTTP запроса, а уже в контексте работы внутри route

{
    "thread": 60535,
    "level": 20,
    "level_name": "Information",
    "message": "Test log",
    "source": "test_project.main.routes",
    "@timestamp": "2021-08-28T01:49:23+03:00",
    "app_name": "test_project",
    "app_version": "1.0.0",
    "app_env": "LOCAL",
    "duration": 385,
    "trace_id": "3bd6f269dbbbd275d46320c7e240bfe5",
    "span_id": "2417303bd1af1ee6",
    "parent_id": null
}

Бонус. Демонстрация производительности асинхронного вывода.

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

  • Количество воркеров: 10

  • Количество одновременно-подключенных клиентов: 200

  • Таймаут: 15 секунд.

  • Время выполнения теста: 15 секунд (больше и не надо)

  • Логгирование одного и того же сообщения: test.

  • Formatter не менял свой код.

Команда запуска нагрузочного тестирования:

wrk -c200 -t1 -d15s http://localhost:8080/users/public/ --timeout 15

Результат нагрузочного тестирования синхронного логгирования:

Результат нагрузочного тестирования асинхронного логгирования:

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

Вывод

Никогда не стоит соглашаться на сторонние библиотеки, если чувствуете, что они не могут удовлетворить ваших потребностей. Наследоваться от сторонних библиотек может обернуться плохой шуткой и лучше сразу не полениться и попробовать написать что-то своё. Да, это решение вряд-ли подойдет 99.99% пользователям, есть моменты, которые бы улучшил, оптимизировал и сделал бы более гибкими. Как раз про это я бы хотел почитать в комментариях, обсудить, как можно улучшить данный подход.

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

Telegram: @Lev_key

Комментарии (23)


  1. PaulIsh
    31.08.2021 05:58

    А если имеются планы потом собирать этот лог в elastic желательно сразу называть поля в журнале согласно ECS (https://www.elastic.co/guide/en/ecs/current/ecs-field-reference.html).


    1. LEVLLN Автор
      31.08.2021 08:56

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


  1. vagon333
    31.08.2021 06:37
    +1

    Больная тема. Спасибо что поделились.

    Насколько сложно расширить вывод?

    Например, критические ошибки мы сбрасываем в закрытый телеграм канал.
    Если подключать ваш логгер, хорошо быть иметь возможность расширения вывода.


    1. LEVLLN Автор
      31.08.2021 08:49

      Я рад, что статья Вам помогла. Отвечу про расширение вывода. Расширение вывода делается очень просто, я описал это в месте, где конфигурирование handler, у которого есть поле func. Вот, эта функция как реализует вывод журнала в нужное место. На примере у меня указан обычный print, но в Вашем случае это может быть отправка в телеграм-канал.

      Спасибо за Вашу обратную связь.


      1. stepalxser
        31.08.2021 12:29

        Функция ведь по идее должна быть асинхронной?


        1. LEVLLN Автор
          31.08.2021 12:33
          +1

          Таки нет, потому что стандартный logging в Python - синхронный. Асинхронность библиотеки достигается очередями через Queue, запуском в asyncio результата по очереди. То есть, если функцию логгирования сделать асинхронной - упадет ошибка о том, что мы вызываем асинхронное в синхронном коде логгера в очереди. Опять же: моя цель была не качать без всякой нужды асинхронные библиотеки, которые есть в pypi, ибо они все имеют свою экосистему и не используют стандартный logging, там запись определения бы была такая: logger = libname_logging(). А вызов через await logger.info(“message”). Не все функции в проекте могут быть асинхронными и это бы привело к неоднородности логеров в проекте. И второе: было бы несколько точек конфигурирования логеров, а мы этого делать не хотим, когда у питона из коробки свой собственный и даже лучший способ это сделать.


  1. vlakir
    31.08.2021 07:37
    +1

    Познавательно, спасибо! Заливайте на pypi, с удовольствием попробую.


    1. LEVLLN Автор
      31.08.2021 08:49

      Спасибо большое! Постараюсь залить и сообщу, как будет сделано:)


  1. Cykooz
    31.08.2021 11:26
    +2

    У вашего решения есть заметные недостатки:

    • Ваша миделвара полностью вычитывает тело запроса ещё до того как основной код примет решение о том нужно ли ему вообще это тело. Например приложение может вообще прервать обработку запроса (и не вычитывать его тело) из-за того, что он не прошёл проверку авторизации.
      А если клиент отправил 100500 МБ данных в теле? У вас это всё загрузится в память и пользователь вашей библиотеки даже не может на это повлиять. Ну и кроме того это ломает обработку заголовка запроса Expect: 100-continue

    • Аналогичная проблема с телом ответа - вы без всяких проверок вычитываете его в память полностью. А там может быть очень много данных. Не просто так тело ответа реализовано как итератор - это позволяет реализовать возврат большого тела без необходимости его полной загрузки в память. Ваша миделвара ломает это стандартное поведение.

    Когда я делал подобное логирование тела запроса и ответа, то я во первых проверял Content-Type, что бы он имел какое-то определённое значение. Например application/json, т.к. в контексте приложения нет смысла логировать что-то другое. И так же проверял Content-Length запроса и ответа, что бы он не превышал определённого лимита (а ещё его вообще может не быть, если тело передаётся чанками). Ну и тело запроса я достаю уже после того как запрос был обработан приложением.


    1. LEVLLN Автор
      31.08.2021 12:38

      Да, Вы верно подметили этот недочет. На самом деле, тут оставляется выбор за разработчиком уже достраивать новые ограничения на контент, на формат данных с помощью pydantic, на то, как будет мидлварь сплитить и декомпозировать отдельные части запроса и ответа. Совет с content-type - очень крутой, возьму на заметку. Спасибо за Ваш комментарий!


  1. Scrloll
    31.08.2021 12:34

    Было бы интересно посмотреть на результаты нагрузочного тестирования в сравнении с библиотекой loguru. Рассматривали ее как альтернативу?


    1. LEVLLN Автор
      31.08.2021 12:36

      Logguru отдал такой же результат, как и приведенный в примере синхронный вариант, не больше 3000 rps. Потому что механизм синхронный в logguru. Но logguru тоже хорошее и гибкое решение, но слишком громоздкое для нашей задачи.


  1. AlexSpaizNet
    31.08.2021 13:12

    Давно не писал на питоне, тем более с async/await поэтому возможно мой вопрос не в тему. Здесь имеется ввиду что асинхронный код не блокирует тред /процесс питона (GIL и все такое) или же логгер вообще будет сам по себе где то буферизировать логи и пушить их в какой-то стрим?

    Я клоню к тому, что не случится ли ситуация когда нагрузка очень большая и код отрабатывает быстрее чем логи пушатся наружу, и какой-нибудь внутренний буфер/очередь забьется и начнет делать проблемы?


    1. LEVLLN Автор
      31.08.2021 13:24

      Никак нет. Как я уже пометил в этом комментарии https://habr.com/ru/post/575454/#comment_23431790 - асинхронная библиотека логирования реализована на принципе очереди Queue стандартного logging в питоне. То есть, есть шанс, что можно набить этот буфер очень быстро, но при 860 rps полет был нормальный. Да, есть эффект того, что код отрабатывает быстрее, а логи уже потом за ним поспевают, это нормально для асинхронного мира в других языках программирования, например в Котлине такой же эффект с корутинами.


      1. AlexSpaizNet
        31.08.2021 17:35

        Это важно. При тех же RPS при большом пейлоаде или сложной структурой данных, время сериализации может увеличиться.

        А что если канал куда логи отправляются тормозит? Память то не резиновая. Обычно в таких случаях explicitly указывают размер очереди и поведение при заполнении. Например что логи будут дропаться если очередь слишком большая.

        А то можно сильно удивиться когда что-то начнет падать по OOM или что канал будет забиваться... или CPU подскочить на сериализации в 3 часа ночи.

        И кстати, такой проблемы нет?

        user = { name: "alex" }
        logger("my name", { user, }; ->> ушло в очередь
        user.name = "ahmed"

        Что уйдет в лог?


        1. LEVLLN Автор
          31.08.2021 18:34

          Решение проблемы со слишком большим объёмом данных из комментаторов предложена очень корректно: ограничение по чанкам, ограничение по content lenght. Также я планирую доработать мидлварь для бан-листа для формирования бинарников(если content length - binary), также ограничивать определенные endpoints.

          И кстати, такой проблемы нет?

          user = { name: "alex" }
          logger("my name", { user, }; ->> ушло в очередь
          user.name = "ahmed"

          Что уйдет в лог?

          уйдет в лог “alex”, потому что мы сразу ставим record объект в очередь логгера, а его результат записи стрима уже будет работать в asyncio. То есть CPU bound задача с форматированием сработает синхронно, а вывод стрима (I/O) - отработает асинхронно. В этом вся прелесть предложенного мной подхода. И да, Мистер “ahmed” останется ждать своего часа побывать в логгере.


  1. savostin
    31.08.2021 13:34

    А все же, почему не tsv? Это ж на каждую запись дублируется структура, да еще и с такими длинными именами. Я понимаю, если структура в каждой записи меняется, но здесь вроде бы нет.

    Да и раз уже JSON, то давайте вложенность: {"request" : {"uri" : "", "referer": "", "protocol": "" }, "response": {}}

    А это вообще странно выглядит: "response_headers": "{\"content-length\": \"13\", \"content-type\": \"application/json\"}", "response_body": "{\"foo\":\"baz\"}"


    1. LEVLLN Автор
      31.08.2021 13:50

      Формат специфичный и дело уже разработчика подстраивать формат: я это описал в части, где инициализировал pydantic структуру. Вот, по сути этой структурой можно сделать что угодно в части формата. Тело ответа и заголовки строкой в связи со спецификациями проектов в компании, требования архитекторов и девопсов для однообразия. Решения придумывались совместно над типом данных некоторых полей, они также изменяемы, как видно в коде примеров. Суть статьи была показать, как заставить все механизмы вместе: нативное логирование, асинхронность, форматирование в удобный кастомный формат, который нужен только автору, а можно еще и под ECS это подбить, также показать это дело со стороны веба(аналогичное можно на http-клиентах сделать, могу показать, как это было реализовано на примере httpx библиотеки).


      1. savostin
        31.08.2021 13:54

        А JSON зачем же?


        1. LEVLLN Автор
          31.08.2021 14:14

          1) JSON для кастомно-настроенного логстэша. Какие никакие, а поля (я убрал из примеров, но были поля еще трассировки OpenTelemetry, посчитал, что для статьи это неважно)

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


  1. baldr
    12.09.2021 16:58

    Судя по всему, asynclog использует потоки. В таком случае о какой асинхронности мы можем говорить здесь? В случайные моменты времени поток переключается и синхронно пишет в файл, блокируя loop.

    Еще он может использовать celery, но это примерно то же самое - запись в брокер будет тоже идти синхронно для всего процесса.

    Буду рад если окажется что я ошибаюсь.


    1. LEVLLN Автор
      14.09.2021 09:57

      Не используются потоки. Используется обычный класс, который наследуется от QueueHandler, если внимательно попрыгать по исходникам этой либы.


      1. baldr
        14.09.2021 10:25

        Либо мы про разные библиотеки говорим, либо у вас вообще ссылка не на ту библиотеку стоит.

        https://github.com/unpluggedcoder/asynclog/blob/master/asynclog/handler.py - вот единственный файл на 85 строк, там явно идет использование futures.ThreadPoolExecutor.

        И где вообще происходит вывод в файл? Средствами logging? Но он синхронный!

        Как вообще вы относитесь к тому факту что asyncio не поддерживает асинхронный io для файлов? Есть aiofiles, который, в свою очередь, тоже использует потоки чтобы эмулировать асинхронность.