From f029d41a9420685d2f339b0380f9148c7fd19188 Mon Sep 17 00:00:00 2001 From: yeongseon Date: Sun, 26 Apr 2026 17:55:17 +0900 Subject: [PATCH 1/2] fix: release pipeline + correctness hotfixes (#77) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four P0 issues from external review, shipped together. URLs (#77 §1) - pyproject.toml [project.urls]: align Homepage/Documentation/Repository/Issues with the actual GitHub repo (azure-functions-logging-python). The PyPI project name itself stays 'azure-functions-logging' as published. - README.md, README.ko.md, README.ja.md, README.zh-CN.md badge URLs updated for CI, Release, Security Scans, codecov, Docs links. - mkdocs.yml site_url, llms.txt and llms-full.txt doc URLs, docs/ git clone examples, .github/workflows/stale.yml repository guard updated to the real repo path. Version drift (#77 §2) - Bump __version__ from 0.5.0 to 0.5.2. - Update tests/test_public_api.py to assert the new version. - The phantom v0.5.1 GitHub release and tag (Publish to PyPI failed due to __version__/tag mismatch; PyPI was never updated) is being removed out-of-band so v0.5.0 is once again the latest published release until v0.5.2 ships. JsonFormatter loses logs on unserializable extra (#77 §3) - Add _json_default fallback that coerces unknown values to str(value), with a sentinel string when str() itself raises. - json.dumps now passes default=_json_default so datetime, Decimal, UUID, dataclasses, exceptions, and request objects no longer crash the formatter or drop the log line. - Regression tests for datetime/Decimal/UUID, dataclass, and a hostile __str__ class. FunctionLogger crashes on reserved LogRecord keys (#77 §4) - Add _sanitize_extra() that prefixes any key colliding with a reserved LogRecord attribute (name, msg, levelname, message, etc.) with 'extra_' before forwarding to logging.Logger.log. - Applies to both **kwargs forwarded into extra and the explicit extra= argument. - Regression tests against MagicMock and a real stdlib logger. --- .github/workflows/stale.yml | 4 +- README.ja.md | 14 ++--- README.ko.md | 14 ++--- README.md | 18 +++---- README.zh-CN.md | 14 ++--- docs/development.md | 2 +- docs/installation.md | 2 +- llms-full.txt | 4 +- llms.txt | 14 ++--- mkdocs.yml | 6 +-- pyproject.toml | 8 +-- src/azure_functions_logging/__init__.py | 2 +- .../_json_formatter.py | 21 +++++++- src/azure_functions_logging/_logger.py | 50 +++++++++++++++++ tests/test_json_formatter.py | 51 ++++++++++++++++++ tests/test_logger.py | 53 +++++++++++++++++++ tests/test_public_api.py | 2 +- 17 files changed, 226 insertions(+), 53 deletions(-) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 35c6f45..7278231 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -12,7 +12,7 @@ on: jobs: stale-issues: runs-on: ubuntu-latest - if: github.repository == 'yeongseon/azure-functions-logging' + if: github.repository == 'yeongseon/azure-functions-logging-python' steps: - name: "Stale issues and PRs" @@ -32,7 +32,7 @@ jobs: stale-branches: runs-on: ubuntu-latest - if: github.repository == 'yeongseon/azure-functions-logging' + if: github.repository == 'yeongseon/azure-functions-logging-python' steps: - name: "Checkout code" diff --git a/README.ja.md b/README.ja.md index 03a4ed3..8771bb1 100644 --- a/README.ja.md +++ b/README.ja.md @@ -2,12 +2,12 @@ [![PyPI](https://img.shields.io/pypi/v/azure-functions-logging.svg)](https://pypi.org/project/azure-functions-logging/) [![Python Version](https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12%20%7C%203.13%20%7C%203.14-blue)](https://pypi.org/project/azure-functions-logging/) -[![CI](https://github.com/yeongseon/azure-functions-logging/actions/workflows/ci-test.yml/badge.svg)](https://github.com/yeongseon/azure-functions-logging/actions/workflows/ci-test.yml) -[![Release](https://github.com/yeongseon/azure-functions-logging/actions/workflows/release.yml/badge.svg)](https://github.com/yeongseon/azure-functions-logging/actions/workflows/release.yml) -[![Security Scans](https://github.com/yeongseon/azure-functions-logging/actions/workflows/security.yml/badge.svg)](https://github.com/yeongseon/azure-functions-logging/actions/workflows/security.yml) -[![codecov](https://codecov.io/gh/yeongseon/azure-functions-logging/branch/main/graph/badge.svg)](https://codecov.io/gh/yeongseon/azure-functions-logging) +[![CI](https://github.com/yeongseon/azure-functions-logging-python/actions/workflows/ci-test.yml/badge.svg)](https://github.com/yeongseon/azure-functions-logging-python/actions/workflows/ci-test.yml) +[![Release](https://github.com/yeongseon/azure-functions-logging-python/actions/workflows/release.yml/badge.svg)](https://github.com/yeongseon/azure-functions-logging-python/actions/workflows/release.yml) +[![Security Scans](https://github.com/yeongseon/azure-functions-logging-python/actions/workflows/security.yml/badge.svg)](https://github.com/yeongseon/azure-functions-logging-python/actions/workflows/security.yml) +[![codecov](https://codecov.io/gh/yeongseon/azure-functions-logging-python/branch/main/graph/badge.svg)](https://codecov.io/gh/yeongseon/azure-functions-logging-python) [![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit)](https://pre-commit.com/) -[![Docs](https://img.shields.io/badge/docs-gh--pages-blue)](https://yeongseon.github.io/azure-functions-logging/) +[![Docs](https://img.shields.io/badge/docs-gh--pages-blue)](https://yeongseon.github.io/azure-functions-logging-python/) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) 他の言語: [English](README.md) | [한국어](README.ko.md) | [简体中文](README.zh-CN.md) @@ -56,7 +56,7 @@ pip install azure-functions-logging ローカル開発用: ```bash -git clone https://github.com/yeongseon/azure-functions-logging.git +git clone https://github.com/yeongseon/azure-functions-logging-python.git cd azure-functions-logging pip install -e .[dev] ``` @@ -101,7 +101,7 @@ bound.info("Processing") # includes user_id + operation in every log line ## Documentation -- 全ドキュメント: [yeongseon.github.io/azure-functions-logging](https://yeongseon.github.io/azure-functions-logging/) +- 全ドキュメント: [yeongseon.github.io/azure-functions-logging-python](https://yeongseon.github.io/azure-functions-logging-python/) - 製品要件: `PRD.md` ## Ecosystem diff --git a/README.ko.md b/README.ko.md index 0f6e985..e1fa3ad 100644 --- a/README.ko.md +++ b/README.ko.md @@ -2,12 +2,12 @@ [![PyPI](https://img.shields.io/pypi/v/azure-functions-logging.svg)](https://pypi.org/project/azure-functions-logging/) [![Python Version](https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12%20%7C%203.13%20%7C%203.14-blue)](https://pypi.org/project/azure-functions-logging/) -[![CI](https://github.com/yeongseon/azure-functions-logging/actions/workflows/ci-test.yml/badge.svg)](https://github.com/yeongseon/azure-functions-logging/actions/workflows/ci-test.yml) -[![Release](https://github.com/yeongseon/azure-functions-logging/actions/workflows/release.yml/badge.svg)](https://github.com/yeongseon/azure-functions-logging/actions/workflows/release.yml) -[![Security Scans](https://github.com/yeongseon/azure-functions-logging/actions/workflows/security.yml/badge.svg)](https://github.com/yeongseon/azure-functions-logging/actions/workflows/security.yml) -[![codecov](https://codecov.io/gh/yeongseon/azure-functions-logging/branch/main/graph/badge.svg)](https://codecov.io/gh/yeongseon/azure-functions-logging) +[![CI](https://github.com/yeongseon/azure-functions-logging-python/actions/workflows/ci-test.yml/badge.svg)](https://github.com/yeongseon/azure-functions-logging-python/actions/workflows/ci-test.yml) +[![Release](https://github.com/yeongseon/azure-functions-logging-python/actions/workflows/release.yml/badge.svg)](https://github.com/yeongseon/azure-functions-logging-python/actions/workflows/release.yml) +[![Security Scans](https://github.com/yeongseon/azure-functions-logging-python/actions/workflows/security.yml/badge.svg)](https://github.com/yeongseon/azure-functions-logging-python/actions/workflows/security.yml) +[![codecov](https://codecov.io/gh/yeongseon/azure-functions-logging-python/branch/main/graph/badge.svg)](https://codecov.io/gh/yeongseon/azure-functions-logging-python) [![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit)](https://pre-commit.com/) -[![Docs](https://img.shields.io/badge/docs-gh--pages-blue)](https://yeongseon.github.io/azure-functions-logging/) +[![Docs](https://img.shields.io/badge/docs-gh--pages-blue)](https://yeongseon.github.io/azure-functions-logging-python/) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) 다른 언어: [English](README.md) | [日本語](README.ja.md) | [简体中文](README.zh-CN.md) @@ -56,7 +56,7 @@ pip install azure-functions-logging 로컬 개발용: ```bash -git clone https://github.com/yeongseon/azure-functions-logging.git +git clone https://github.com/yeongseon/azure-functions-logging-python.git cd azure-functions-logging pip install -e .[dev] ``` @@ -101,7 +101,7 @@ bound.info("Processing") # includes user_id + operation in every log line ## Documentation -- 전체 문서: [yeongseon.github.io/azure-functions-logging](https://yeongseon.github.io/azure-functions-logging/) +- 전체 문서: [yeongseon.github.io/azure-functions-logging-python](https://yeongseon.github.io/azure-functions-logging-python/) - 제품 요구 사항: `PRD.md` ## Ecosystem diff --git a/README.md b/README.md index fb783ae..401b1c0 100644 --- a/README.md +++ b/README.md @@ -2,12 +2,12 @@ [![PyPI](https://img.shields.io/pypi/v/azure-functions-logging.svg)](https://pypi.org/project/azure-functions-logging/) [![Python Version](https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12%20%7C%203.13%20%7C%203.14-blue)](https://pypi.org/project/azure-functions-logging/) -[![CI](https://github.com/yeongseon/azure-functions-logging/actions/workflows/ci-test.yml/badge.svg)](https://github.com/yeongseon/azure-functions-logging/actions/workflows/ci-test.yml) -[![Release](https://github.com/yeongseon/azure-functions-logging/actions/workflows/publish-pypi.yml/badge.svg)](https://github.com/yeongseon/azure-functions-logging/actions/workflows/publish-pypi.yml) -[![Security Scans](https://github.com/yeongseon/azure-functions-logging/actions/workflows/security.yml/badge.svg)](https://github.com/yeongseon/azure-functions-logging/actions/workflows/security.yml) -[![codecov](https://codecov.io/gh/yeongseon/azure-functions-logging/branch/main/graph/badge.svg)](https://codecov.io/gh/yeongseon/azure-functions-logging) +[![CI](https://github.com/yeongseon/azure-functions-logging-python/actions/workflows/ci-test.yml/badge.svg)](https://github.com/yeongseon/azure-functions-logging-python/actions/workflows/ci-test.yml) +[![Release](https://github.com/yeongseon/azure-functions-logging-python/actions/workflows/publish-pypi.yml/badge.svg)](https://github.com/yeongseon/azure-functions-logging-python/actions/workflows/publish-pypi.yml) +[![Security Scans](https://github.com/yeongseon/azure-functions-logging-python/actions/workflows/security.yml/badge.svg)](https://github.com/yeongseon/azure-functions-logging-python/actions/workflows/security.yml) +[![codecov](https://codecov.io/gh/yeongseon/azure-functions-logging-python/branch/main/graph/badge.svg)](https://codecov.io/gh/yeongseon/azure-functions-logging-python) [![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit)](https://pre-commit.com/) -[![Docs](https://img.shields.io/badge/docs-gh--pages-blue)](https://yeongseon.github.io/azure-functions-logging/) +[![Docs](https://img.shields.io/badge/docs-gh--pages-blue)](https://yeongseon.github.io/azure-functions-logging-python/) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) Read this in: [한국어](README.ko.md) | [日本語](README.ja.md) | [简体中文](README.zh-CN.md) @@ -351,10 +351,10 @@ Create bound loggers per-invocation. Do not cache them at module level. ## Documentation -- Full docs: [yeongseon.github.io/azure-functions-logging](https://yeongseon.github.io/azure-functions-logging/) -- [Configuration reference](https://yeongseon.github.io/azure-functions-logging/configuration/) -- [Troubleshooting guide](https://yeongseon.github.io/azure-functions-logging/troubleshooting/) -- [API reference](https://yeongseon.github.io/azure-functions-logging/api/) +- Full docs: [yeongseon.github.io/azure-functions-logging-python](https://yeongseon.github.io/azure-functions-logging-python/) +- [Configuration reference](https://yeongseon.github.io/azure-functions-logging-python/configuration/) +- [Troubleshooting guide](https://yeongseon.github.io/azure-functions-logging-python/troubleshooting/) +- [API reference](https://yeongseon.github.io/azure-functions-logging-python/api/) ## Ecosystem diff --git a/README.zh-CN.md b/README.zh-CN.md index 82760a3..df526fb 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -2,12 +2,12 @@ [![PyPI](https://img.shields.io/pypi/v/azure-functions-logging.svg)](https://pypi.org/project/azure-functions-logging/) [![Python Version](https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12%20%7C%203.13%20%7C%203.14-blue)](https://pypi.org/project/azure-functions-logging/) -[![CI](https://github.com/yeongseon/azure-functions-logging/actions/workflows/ci-test.yml/badge.svg)](https://github.com/yeongseon/azure-functions-logging/actions/workflows/ci-test.yml) -[![Release](https://github.com/yeongseon/azure-functions-logging/actions/workflows/release.yml/badge.svg)](https://github.com/yeongseon/azure-functions-logging/actions/workflows/release.yml) -[![Security Scans](https://github.com/yeongseon/azure-functions-logging/actions/workflows/security.yml/badge.svg)](https://github.com/yeongseon/azure-functions-logging/actions/workflows/security.yml) -[![codecov](https://codecov.io/gh/yeongseon/azure-functions-logging/branch/main/graph/badge.svg)](https://codecov.io/gh/yeongseon/azure-functions-logging) +[![CI](https://github.com/yeongseon/azure-functions-logging-python/actions/workflows/ci-test.yml/badge.svg)](https://github.com/yeongseon/azure-functions-logging-python/actions/workflows/ci-test.yml) +[![Release](https://github.com/yeongseon/azure-functions-logging-python/actions/workflows/release.yml/badge.svg)](https://github.com/yeongseon/azure-functions-logging-python/actions/workflows/release.yml) +[![Security Scans](https://github.com/yeongseon/azure-functions-logging-python/actions/workflows/security.yml/badge.svg)](https://github.com/yeongseon/azure-functions-logging-python/actions/workflows/security.yml) +[![codecov](https://codecov.io/gh/yeongseon/azure-functions-logging-python/branch/main/graph/badge.svg)](https://codecov.io/gh/yeongseon/azure-functions-logging-python) [![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit)](https://pre-commit.com/) -[![Docs](https://img.shields.io/badge/docs-gh--pages-blue)](https://yeongseon.github.io/azure-functions-logging/) +[![Docs](https://img.shields.io/badge/docs-gh--pages-blue)](https://yeongseon.github.io/azure-functions-logging-python/) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) 其他语言: [English](README.md) | [한국어](README.ko.md) | [日本語](README.ja.md) @@ -56,7 +56,7 @@ pip install azure-functions-logging 本地开发用: ```bash -git clone https://github.com/yeongseon/azure-functions-logging.git +git clone https://github.com/yeongseon/azure-functions-logging-python.git cd azure-functions-logging pip install -e .[dev] ``` @@ -101,7 +101,7 @@ bound.info("Processing") # includes user_id + operation in every log line ## Documentation -- 完整文档: [yeongseon.github.io/azure-functions-logging](https://yeongseon.github.io/azure-functions-logging/) +- 完整文档: [yeongseon.github.io/azure-functions-logging-python](https://yeongseon.github.io/azure-functions-logging-python/) - 产品需求文档: `PRD.md` ## Ecosystem diff --git a/docs/development.md b/docs/development.md index 3cd16bc..dab69f5 100644 --- a/docs/development.md +++ b/docs/development.md @@ -13,7 +13,7 @@ This guide explains how to set up a development environment for `azure-functions 1. Clone the repository: ```bash - git clone https://github.com/yeongseon/azure-functions-logging.git + git clone https://github.com/yeongseon/azure-functions-logging-python.git cd azure-functions-logging ``` diff --git a/docs/installation.md b/docs/installation.md index eeacb5a..66058a2 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -45,7 +45,7 @@ dependencies = [ To contribute to the project or run tests locally, clone the repository and install in editable mode with development dependencies: ```bash -git clone https://github.com/yeongseon/azure-functions-logging.git +git clone https://github.com/yeongseon/azure-functions-logging-python.git cd azure-functions-logging pip install -e ".[dev]" ``` diff --git a/llms-full.txt b/llms-full.txt index 6a36b0b..37dc8b8 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -8,8 +8,8 @@ - Version: 0.4.1 - Python: >=3.10, <3.15 - License: MIT -- Docs: https://yeongseon.github.io/azure-functions-logging/ -- Repository: https://github.com/yeongseon/azure-functions-logging +- Docs: https://yeongseon.github.io/azure-functions-logging-python/ +- Repository: https://github.com/yeongseon/azure-functions-logging-python ## Installation diff --git a/llms.txt b/llms.txt index 8694b73..9991d25 100644 --- a/llms.txt +++ b/llms.txt @@ -8,8 +8,8 @@ - Version: 0.4.1 - Python: >=3.10, <3.15 - License: MIT -- Docs: https://yeongseon.github.io/azure-functions-logging/ -- Repository: https://github.com/yeongseon/azure-functions-logging +- Docs: https://yeongseon.github.io/azure-functions-logging-python/ +- Repository: https://github.com/yeongseon/azure-functions-logging-python ## What It Does @@ -68,11 +68,11 @@ def handler(req, context): ## Documentation -- [Getting Started](https://yeongseon.github.io/azure-functions-logging/getting-started/) -- [API Reference](https://yeongseon.github.io/azure-functions-logging/api/) -- [Configuration](https://yeongseon.github.io/azure-functions-logging/configuration/) -- [Examples](https://yeongseon.github.io/azure-functions-logging/examples/) -- [Troubleshooting](https://yeongseon.github.io/azure-functions-logging/troubleshooting/) +- [Getting Started](https://yeongseon.github.io/azure-functions-logging-python/getting-started/) +- [API Reference](https://yeongseon.github.io/azure-functions-logging-python/api/) +- [Configuration](https://yeongseon.github.io/azure-functions-logging-python/configuration/) +- [Examples](https://yeongseon.github.io/azure-functions-logging-python/examples/) +- [Troubleshooting](https://yeongseon.github.io/azure-functions-logging-python/troubleshooting/) ## Ecosystem diff --git a/mkdocs.yml b/mkdocs.yml index 6e89132..a89a048 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,8 +1,8 @@ site_name: Azure Functions Logging site_description: Developer-friendly logging helpers for Azure Functions Python site_author: Yeongseon Choe -site_url: https://yeongseon.github.io/azure-functions-logging/ -repo_url: https://github.com/yeongseon/azure-functions-logging +site_url: https://yeongseon.github.io/azure-functions-logging-python/ +repo_url: https://github.com/yeongseon/azure-functions-logging-python edit_uri: edit/main/docs/ theme: @@ -75,7 +75,7 @@ plugins: extra: social: - icon: fontawesome/brands/github - link: https://github.com/yeongseon/azure-functions-logging + link: https://github.com/yeongseon/azure-functions-logging-python copyright: | © 2026 Yeongseon Choe – MIT Licensed diff --git a/pyproject.toml b/pyproject.toml index f678271..b20a512 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,10 +23,10 @@ classifiers = [ ] [project.urls] -Homepage = "https://yeongseon.github.io/azure-functions-logging/" -Documentation = "https://yeongseon.github.io/azure-functions-logging/" -Repository = "https://github.com/yeongseon/azure-functions-logging" -Issues = "https://github.com/yeongseon/azure-functions-logging/issues" +Homepage = "https://yeongseon.github.io/azure-functions-logging-python/" +Documentation = "https://yeongseon.github.io/azure-functions-logging-python/" +Repository = "https://github.com/yeongseon/azure-functions-logging-python" +Issues = "https://github.com/yeongseon/azure-functions-logging-python/issues" [project.optional-dependencies] dev = [ diff --git a/src/azure_functions_logging/__init__.py b/src/azure_functions_logging/__init__.py index 9ec4362..996f8f0 100644 --- a/src/azure_functions_logging/__init__.py +++ b/src/azure_functions_logging/__init__.py @@ -22,7 +22,7 @@ "with_context", ] -__version__ = "0.5.0" +__version__ = "0.5.2" def get_logger(name: str | None = None) -> FunctionLogger: diff --git a/src/azure_functions_logging/_json_formatter.py b/src/azure_functions_logging/_json_formatter.py index 6245f02..5fb8594 100644 --- a/src/azure_functions_logging/_json_formatter.py +++ b/src/azure_functions_logging/_json_formatter.py @@ -5,6 +5,7 @@ from datetime import datetime, timezone import json import logging +from typing import Any _STANDARD_RECORD_FIELDS: set[str] = { "args", @@ -38,12 +39,30 @@ } +def _json_default(value: Any) -> str: + """Fallback serializer for ``json.dumps``. + + A logging library must never lose log records because of an unserializable + ``extra`` value (datetime, Decimal, UUID, dataclass, exception, request + objects, etc.). This callable coerces unknown values to ``str(value)`` and + returns a sentinel string if even ``str()`` raises, so the formatter always + produces a valid JSON document. + """ + try: + return str(value) + except Exception: + return f"" + + class JsonFormatter(logging.Formatter): """Structured JSON log formatter. Output is newline-delimited JSON (NDJSON), with one JSON object per log line. Context fields (invocation_id, function_name, etc.) are included when present on the LogRecord (set by ContextFilter). + + Unserializable values in ``extra`` are coerced to strings via + :func:`_json_default` rather than dropping the log record. """ def __init__(self) -> None: @@ -74,4 +93,4 @@ def format(self, record: logging.LogRecord) -> str: "extra": extra, } - return json.dumps(payload, ensure_ascii=False) + return json.dumps(payload, ensure_ascii=False, default=_json_default) diff --git a/src/azure_functions_logging/_logger.py b/src/azure_functions_logging/_logger.py index 5645a90..742ce4d 100644 --- a/src/azure_functions_logging/_logger.py +++ b/src/azure_functions_logging/_logger.py @@ -5,6 +5,55 @@ import logging from typing import Any +_RESERVED_LOG_RECORD_KEYS: frozenset[str] = frozenset( + { + "args", + "asctime", + "created", + "exc_info", + "exc_text", + "filename", + "funcName", + "levelname", + "levelno", + "lineno", + "message", + "module", + "msecs", + "msg", + "name", + "pathname", + "process", + "processName", + "relativeCreated", + "stack_info", + "taskName", + "thread", + "threadName", + } +) + + +def _sanitize_extra(extra: dict[str, Any]) -> dict[str, Any]: + """Rename keys that collide with reserved ``LogRecord`` attributes. + + ``logging.Logger._log`` raises ``KeyError`` if ``extra`` contains a key that + shadows a built-in record attribute. Rather than crash the user's code, we + prefix the offending keys with ``extra_`` so the value still reaches the + formatter under a deterministic name. + """ + if not extra: + return extra + if not any(key in _RESERVED_LOG_RECORD_KEYS for key in extra): + return extra + sanitized: dict[str, Any] = {} + for key, value in extra.items(): + if key in _RESERVED_LOG_RECORD_KEYS: + sanitized[f"extra_{key}"] = value + else: + sanitized[key] = value + return sanitized + class FunctionLogger: """Wrapper around a standard ``logging.Logger`` with context binding. @@ -65,6 +114,7 @@ def _log( extra = kwargs.pop("extra", None) or {} extra.update(kwargs) extra.update(self._context) + extra = _sanitize_extra(extra) self._logger.log( level, msg, diff --git a/tests/test_json_formatter.py b/tests/test_json_formatter.py index 4274c3b..02ba16b 100644 --- a/tests/test_json_formatter.py +++ b/tests/test_json_formatter.py @@ -1,9 +1,12 @@ from __future__ import annotations +from dataclasses import dataclass from datetime import datetime, timezone +from decimal import Decimal import json import logging import sys +import uuid from azure_functions_logging._json_formatter import JsonFormatter @@ -107,3 +110,51 @@ def test_timestamp_is_iso8601_with_timezone() -> None: assert parsed.tzinfo is not None assert parsed.utcoffset() == timezone.utc.utcoffset(parsed) + + +def test_unserializable_extra_does_not_drop_log_line() -> None: + """Issue #77: a logging library must never drop logs because of unserializable extra.""" + formatter = JsonFormatter() + record = _make_record(msg="payload") + record.when = datetime(2026, 1, 2, 3, 4, 5, tzinfo=timezone.utc) + record.amount = Decimal("1.23") + record.request_id = uuid.UUID("12345678-1234-5678-1234-567812345678") + + output = formatter.format(record) + payload = json.loads(output) + + assert payload["message"] == "payload" + assert payload["extra"]["when"] == "2026-01-02 03:04:05+00:00" + assert payload["extra"]["amount"] == "1.23" + assert payload["extra"]["request_id"] == "12345678-1234-5678-1234-567812345678" + + +def test_unserializable_extra_with_dataclass_falls_back_to_str() -> None: + @dataclass + class Order: + id: str + total: int + + formatter = JsonFormatter() + record = _make_record(msg="order") + record.order = Order(id="o-1", total=42) + + payload = json.loads(formatter.format(record)) + + assert "order" in payload["extra"] + assert "Order" in payload["extra"]["order"] + assert "o-1" in payload["extra"]["order"] + + +def test_unserializable_extra_where_str_raises_returns_sentinel() -> None: + class Hostile: + def __str__(self) -> str: + raise RuntimeError("no") + + formatter = JsonFormatter() + record = _make_record(msg="hostile") + record.bad = Hostile() + + payload = json.loads(formatter.format(record)) + + assert payload["extra"]["bad"] == "" diff --git a/tests/test_logger.py b/tests/test_logger.py index 821cef8..870dffc 100644 --- a/tests/test_logger.py +++ b/tests/test_logger.py @@ -132,3 +132,56 @@ def test_log_returns_early_when_level_disabled() -> None: logger.info("should not log", order_id="o-999") underlying.log.assert_not_called() + + +def test_reserved_logrecord_keys_in_kwargs_are_prefixed_not_raised() -> None: + """Issue #77: kwargs colliding with LogRecord reserved attrs must not crash.""" + underlying = _mock_underlying_logger() + logger = FunctionLogger(underlying) + + logger.info("hi", name="custom", message="user-supplied", levelname="INFO") + + underlying.log.assert_called_once() + _, kwargs = underlying.log.call_args + extra = kwargs["extra"] + assert extra["extra_name"] == "custom" + assert extra["extra_message"] == "user-supplied" + assert extra["extra_levelname"] == "INFO" + assert "name" not in extra + assert "message" not in extra + assert "levelname" not in extra + + +def test_reserved_keys_in_explicit_extra_arg_are_also_prefixed() -> None: + underlying = _mock_underlying_logger() + logger = FunctionLogger(underlying) + + logger.info("hi", extra={"name": "custom", "user_id": "u-1"}) + + _, kwargs = underlying.log.call_args + extra = kwargs["extra"] + assert extra["extra_name"] == "custom" + assert extra["user_id"] == "u-1" + + +def test_non_reserved_kwargs_still_pass_through_unchanged() -> None: + underlying = _mock_underlying_logger() + logger = FunctionLogger(underlying) + + logger.info("hi", order_id="o-1", region="eastus") + + _, kwargs = underlying.log.call_args + extra = kwargs["extra"] + assert extra == {"order_id": "o-1", "region": "eastus"} + + +def test_reserved_keys_via_real_stdlib_logger_does_not_raise() -> None: + """End-to-end: the previously crashing call must now succeed against real stdlib.""" + import logging as stdlib_logging + + real_logger = stdlib_logging.getLogger("test.reserved.keys.regression") + real_logger.addHandler(stdlib_logging.NullHandler()) + real_logger.setLevel(stdlib_logging.INFO) + logger = FunctionLogger(real_logger) + + logger.info("hi", name="custom", message="user-supplied") diff --git a/tests/test_public_api.py b/tests/test_public_api.py index 0cddea9..81c61d3 100644 --- a/tests/test_public_api.py +++ b/tests/test_public_api.py @@ -21,7 +21,7 @@ def test_all_exports(self) -> None: } def test_version_is_0_5_0(self) -> None: - assert azure_functions_logging.__version__ == "0.5.0" + assert azure_functions_logging.__version__ == "0.5.2" def test_version_is_string(self) -> None: assert isinstance(azure_functions_logging.__version__, str) From d2ea8c9d64f461e55586a5db339366b97b245f2f Mon Sep 17 00:00:00 2001 From: yeongseon Date: Sun, 26 Apr 2026 18:08:55 +0900 Subject: [PATCH 2/2] docs: align cd paths with renamed repository slug Six docs files still instructed users to 'cd azure-functions-logging' after cloning the renamed 'azure-functions-logging-python' repo, leaving the working directory at the parent path and breaking every subsequent command in the install/dev/contribute walkthroughs. Refs #77 --- README.ja.md | 2 +- README.ko.md | 2 +- README.zh-CN.md | 2 +- docs/contributing.md | 4 ++-- docs/development.md | 2 +- docs/installation.md | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/README.ja.md b/README.ja.md index 8771bb1..9f1161a 100644 --- a/README.ja.md +++ b/README.ja.md @@ -57,7 +57,7 @@ pip install azure-functions-logging ```bash git clone https://github.com/yeongseon/azure-functions-logging-python.git -cd azure-functions-logging +cd azure-functions-logging-python pip install -e .[dev] ``` diff --git a/README.ko.md b/README.ko.md index e1fa3ad..6d85288 100644 --- a/README.ko.md +++ b/README.ko.md @@ -57,7 +57,7 @@ pip install azure-functions-logging ```bash git clone https://github.com/yeongseon/azure-functions-logging-python.git -cd azure-functions-logging +cd azure-functions-logging-python pip install -e .[dev] ``` diff --git a/README.zh-CN.md b/README.zh-CN.md index df526fb..a043993 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -57,7 +57,7 @@ pip install azure-functions-logging ```bash git clone https://github.com/yeongseon/azure-functions-logging-python.git -cd azure-functions-logging +cd azure-functions-logging-python pip install -e .[dev] ``` diff --git a/docs/contributing.md b/docs/contributing.md index bf9ca69..fc96d4a 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -8,8 +8,8 @@ Thank you for your interest in contributing to `azure-functions-logging`. This g 2. Clone your fork: ```bash - git clone https://github.com//azure-functions-logging.git - cd azure-functions-logging + git clone https://github.com//azure-functions-logging-python.git + cd azure-functions-logging-python ``` 3. Install development dependencies: diff --git a/docs/development.md b/docs/development.md index dab69f5..005dc08 100644 --- a/docs/development.md +++ b/docs/development.md @@ -14,7 +14,7 @@ This guide explains how to set up a development environment for `azure-functions ```bash git clone https://github.com/yeongseon/azure-functions-logging-python.git - cd azure-functions-logging + cd azure-functions-logging-python ``` 2. Install the package in editable mode with development dependencies: diff --git a/docs/installation.md b/docs/installation.md index 66058a2..168e848 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -46,7 +46,7 @@ To contribute to the project or run tests locally, clone the repository and inst ```bash git clone https://github.com/yeongseon/azure-functions-logging-python.git -cd azure-functions-logging +cd azure-functions-logging-python pip install -e ".[dev]" ```