diff --git a/.github/workflows/build_and_publish.yml b/.github/workflows/build_and_publish.yml new file mode 100644 index 0000000..a236b7c --- /dev/null +++ b/.github/workflows/build_and_publish.yml @@ -0,0 +1,25 @@ +name: Publish Python 🐍 distributions 📦 + +on: + release: + types: [published] + +jobs: + build-and-publish: + name: Build and publish Python 🐍 distributions 📦 to PyPI and TestPyPI + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@master + - name: Set up Python 3.11 + uses: actions/setup-python@v3 + with: + python-version: "3.11" + - name: Install dependencies + run: | + python -m ensurepip + pip install build --user + python -m build --wheel --sdist --outdir dist/ + - name: Publish a Python distribution to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + password: ${{ secrets.PYPI_API_TOKEN }} diff --git a/README.md b/README.md new file mode 100644 index 0000000..0038b45 --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +# logging-lib +Библиотека для логирования сервисов Твой ФФ! + +## Как подключить +1. В requirements.txt добавьте logging-profcomff +2. Скопируйте из /gunicorn_logging_examples обе конфигурации +3. Вставьте их в корень проекта +4. Добаввьте в Dockerfile ARG CONF_FILE +5. В Dockerfile добавьте GUNICORN_CMD_ARGS в качестве env переменной +6. Пропишите туда "--log-config $CONF_FILE" +7. В Actions в запуск добавьте(прод) --build-args: docker build --build-arg CONF_FILE=logging_prod.conf +8. В Actions в запуск добавьте(тест) --build-args: docker build --build-arg CONF_FILE=logging_test.conf diff --git a/gunicorn_logging_examples/logging_prod.conf b/gunicorn_logging_examples/logging_prod.conf new file mode 100644 index 0000000..971f309 --- /dev/null +++ b/gunicorn_logging_examples/logging_prod.conf @@ -0,0 +1,35 @@ +[loggers] +keys=root,gunicorn.error,gunicorn.access + +[handlers] +keys=all + +[formatters] +keys=json + +[logger_root] +level=INFO +handlers=all + +[logger_gunicorn.error] +level=INFO +handlers=all +propagate=0 +qualname=gunicorn.error +formatter=json + +[logger_gunicorn.access] +level=INFO +handlers=all +propagate=0 +qualname=gunicorn.access +formatter=json + +[handler_all] +class=StreamHandler +formatter=json +level=INFO +args=(sys.stdout,) + +[formatter_json] +class=logger.formatter.JSONLogFormatter diff --git a/gunicorn_logging_examples/logging_test.conf b/gunicorn_logging_examples/logging_test.conf new file mode 100644 index 0000000..8904c93 --- /dev/null +++ b/gunicorn_logging_examples/logging_test.conf @@ -0,0 +1,35 @@ +[loggers] +keys=root,gunicorn.error,gunicorn.access + +[handlers] +keys=all + +[formatters] +keys=json + +[logger_root] +level=DEBUG +handlers=all + +[logger_gunicorn.error] +level=DEBUG +handlers=all +propagate=0 +qualname=gunicorn.error +formatter=json + +[logger_gunicorn.access] +level=DEBUG +handlers=all +propagate=0 +qualname=gunicorn.access +formatter=json + +[handler_all] +class=StreamHandler +formatter=json +level=DEBUG +args=(sys.stdout,) + +[formatter_json] +class=logger.formatter.JSONLogFormatter diff --git a/logger/__init__.py b/logger/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/logger/formatter.py b/logger/formatter.py new file mode 100644 index 0000000..1bbdd5e --- /dev/null +++ b/logger/formatter.py @@ -0,0 +1,67 @@ +import datetime +import json +import logging +import traceback + + +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_ms = record.duration if hasattr(record, "duration") else record.msecs + # Инициализация тела журнала + json_log_fields = dict() + json_log_fields["thread"] = record.process + json_log_fields["timestamp"] = now + json_log_fields["level"] = record.levelno + json_log_fields["level_name"] = record.levelname + json_log_fields["message"] = message + json_log_fields["source"] = record.name + json_log_fields["duration_ms"] = duration_ms + json_log_fields["func"] = record.funcName + json_log_fields["file"] = record.filename + empty_record = logging.LogRecord( + str(), int(), str(), int(), object(), exc_info=None, args=(object,) + ) + keys = set(dir(record)) - set(dir(empty_record)) + json_log_fields.update({key: getattr(record, key) for key in keys}) + + 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 + + if hasattr(record, "request_json_fields"): + json_log_fields.update(record.request_json_fields) + + return json_log_fields diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..f2e7d23 --- /dev/null +++ b/setup.py @@ -0,0 +1,18 @@ +from setuptools import setup, find_packages + +with open("README.md", "r") as readme_file: + readme = readme_file.read() + +setup( + name="logging_profcomff", + version="2023.03.11", + author="Semyon Grigoriev", + long_description=readme, + long_description_content_type="text/markdown", + url="https://github.com/profcomff/logging-lib", + packages=find_packages(), + install_requires=["setuptools"], + classifiers=[ + "Programming Language :: Python :: 3.11", + ], +)