-
Notifications
You must be signed in to change notification settings - Fork 18
feat: support structured logs #416
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
21 commits
Select commit
Hold shift + click to select a range
ccad74f
feat: support structured logging
adubovik a3d6273
feat: simplify logging
adubovik 0b71f6f
feat: make logging instrumentation unconditional once tracing in gene…
adubovik 791f383
Merge branch 'development' into feat/support-structured-logs
adubovik 8b40f81
chore: add doc logging
adubovik 59f4b69
feat: add logging configuration for client
adubovik ecd1e93
fix: simplify log config
adubovik ae2a9bd
feat: defer to already installed root handler
adubovik b0cbade
feat: remove log_level
adubovik 920c27f
feat: auto-add otel* log record fields
adubovik f5c48d6
feat: move code around
adubovik 96299b0
chore: simplify doc and comments
adubovik af47fa0
chore: document OTEL_PYTHON_LOG_CORRELATION
adubovik 8bc61e4
feat: add deprecation warning
adubovik eaee79f
feat: minor fixes
adubovik ba6fc0a
chore: accept fix from github bot
adubovik 4544657
feat: extract LogConfig with typed env-var overrides
adubovik 2763aec
chore: fix package version
adubovik b5c46a0
feat: support third-party loggers
adubovik 20ec117
feat: replace warning with a logger warning
adubovik 80d34f8
feat: review fix
adubovik File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,12 @@ | ||
| from aidial_sdk.application import DIALApp | ||
| from aidial_sdk.exceptions import HTTPException | ||
| from aidial_sdk.utils.log_config import LogConfig, configure_root_logger | ||
| from aidial_sdk.utils.logging import logger | ||
|
|
||
| __all__ = ["DIALApp", "HTTPException", "logger"] | ||
| __all__ = [ | ||
| "DIALApp", | ||
| "HTTPException", | ||
| "LogConfig", | ||
| "configure_root_logger", | ||
| "logger", | ||
| ] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| from aidial_sdk.utils.logging import logger | ||
|
|
||
| _LOGGING_DOC = ( | ||
| "https://github.com/epam/ai-dial-sdk/blob/development/docs/logging.md" | ||
| ) | ||
|
|
||
|
|
||
| def warn_otel_log_correlation() -> None: | ||
| logger.warning( | ||
| "OTEL_PYTHON_LOG_CORRELATION=true is not the recommended way to add " | ||
| "trace context to console logs: it double-logs SDK records and cannot " | ||
| "produce JSON. Use DIAL_SDK_TEXT_LOG_FORMAT (with otel* placeholders) " | ||
| f"instead — see {_LOGGING_DOC}." | ||
| ) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| import json | ||
| import logging | ||
| from collections import defaultdict | ||
|
|
||
|
|
||
| class JsonLogFormatter(logging.Formatter): | ||
| """Render a record through a JSON template whose string leaves are | ||
| `%`-format strings, then `json.dumps` the result (escaping every value). | ||
|
|
||
| Any ``otel*`` field on the record (injected by the OTel logging instrumentor | ||
| when tracing is on) is auto-added to the output, unless the template already | ||
| references it under some key (e.g. ``{"trace_id": "%(otelTraceID)s"}``).""" | ||
|
|
||
| def __init__(self, template: dict, datefmt: str | None = None) -> None: | ||
| super().__init__(datefmt=datefmt) | ||
| self._template = template | ||
| template_str = json.dumps(template) | ||
| self._uses_time = "%(asctime)" in template_str | ||
| self._template_str = template_str | ||
|
|
||
| def _interpolate(self, node: object, fields: dict) -> object: | ||
|
adubovik marked this conversation as resolved.
Dismissed
|
||
| match node: | ||
| case str(): | ||
| return node % fields | ||
| case dict(): | ||
| return { | ||
| k: self._interpolate(v, fields) for k, v in node.items() | ||
| } | ||
| case list(): | ||
| return [self._interpolate(v, fields) for v in node] | ||
| case _: | ||
| return node | ||
|
|
||
|
adubovik marked this conversation as resolved.
|
||
| def format(self, record: logging.LogRecord) -> str: | ||
| record.message = record.getMessage() | ||
| if self._uses_time: | ||
| record.asctime = self.formatTime(record, self.datefmt) | ||
|
|
||
| # Expose the traceback via %(exc_text)s, escaped like any other value. | ||
| if record.exc_info and not record.exc_text: | ||
| record.exc_text = self.formatException(record.exc_info) | ||
|
|
||
| # defaultdict(str): a missing field renders as "" instead of raising. | ||
| rendered = self._interpolate( | ||
| self._template, defaultdict(str, record.__dict__) | ||
| ) | ||
|
|
||
| if isinstance(rendered, dict): | ||
| for key, value in record.__dict__.items(): | ||
| if ( | ||
| key.startswith("otel") | ||
| and f"%({key})" not in self._template_str | ||
| ): | ||
| rendered.setdefault(key, value) | ||
|
|
||
| return json.dumps(rendered, ensure_ascii=False, default=str) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,34 +1,104 @@ | ||
| import logging | ||
| import os | ||
| import sys | ||
| from typing import Literal | ||
|
|
||
| from aidial_sdk._pydantic._compat import BaseModel | ||
|
|
||
| DIAL_SDK_LOG = os.environ.get("DIAL_SDK_LOG", "WARNING").upper() | ||
|
|
||
|
|
||
| class LogConfig(BaseModel): | ||
| """Logging configuration to be set for the server""" | ||
|
|
||
| version: int = 1 | ||
| disable_existing_loggers: bool = False | ||
| formatters: dict = { | ||
| "default": { | ||
| "()": "uvicorn.logging.DefaultFormatter", | ||
| "fmt": "%(levelprefix)s | %(asctime)s | %(name)s | %(process)d | %(message)s", | ||
| "datefmt": "%Y-%m-%d %H:%M:%S", | ||
| "use_colors": True, | ||
| }, | ||
| } | ||
| handlers: dict = { | ||
| "default": { | ||
| "formatter": "default", | ||
| "class": "logging.StreamHandler", | ||
| "stream": "ext://sys.stderr", | ||
| }, | ||
| } | ||
| loggers: dict = { | ||
| "aidial_sdk": {"handlers": ["default"], "level": DIAL_SDK_LOG}, | ||
| "uvicorn": { | ||
| "handlers": ["default"], | ||
| "propagate": False, | ||
| }, | ||
| } | ||
| from uvicorn.logging import DefaultFormatter | ||
|
|
||
| from aidial_sdk.telemetry.types import OTEL_PYTHON_LOG_CORRELATION | ||
| from aidial_sdk.utils._json_log_formatter import JsonLogFormatter | ||
| from aidial_sdk.utils.env import env_json_dict | ||
|
|
||
| _DATEFMT = "%Y-%m-%d %H:%M:%S" | ||
|
|
||
| _DEFAULT_TEXT_FORMAT = ( | ||
| "%(levelprefix)s | %(asctime)s | %(name)s | %(process)d | %(message)s" | ||
| ) | ||
| _DEFAULT_JSON_FORMAT = { | ||
| "level": "%(levelname)s", | ||
| "time": "%(asctime)s", | ||
| "logger": "%(name)s", | ||
| "process": "%(process)d", | ||
| "message": "%(message)s", | ||
| } | ||
|
|
||
|
|
||
| class LogConfig: | ||
| level: str | ||
| formatter: logging.Formatter | ||
|
|
||
| def __init__( | ||
| self, | ||
| *, | ||
| level: str | None = None, | ||
| log_format: Literal["text", "json"] | None = None, | ||
| text_format: str | None = None, | ||
| json_format: dict | None = None, | ||
| ) -> None: | ||
| self.level = ( | ||
| level or os.environ.get("DIAL_SDK_LOG", "WARNING") | ||
| ).upper() | ||
| resolved_format = ( | ||
| log_format or os.environ.get("DIAL_SDK_LOG_FORMAT", "text") | ||
| ).lower() | ||
|
|
||
| if resolved_format == "json": | ||
| json_format = json_format or env_json_dict( | ||
| "DIAL_SDK_JSON_LOG_FORMAT", _DEFAULT_JSON_FORMAT | ||
| ) | ||
| self.formatter = JsonLogFormatter( | ||
| template=json_format, datefmt=_DATEFMT | ||
| ) | ||
| else: | ||
| text_format = text_format or os.getenv( | ||
| "DIAL_SDK_TEXT_LOG_FORMAT", _DEFAULT_TEXT_FORMAT | ||
| ) | ||
| self.formatter = DefaultFormatter( | ||
| fmt=text_format, datefmt=_DATEFMT, use_colors=True | ||
| ) | ||
|
|
||
|
|
||
| def configure_root_logger(config: LogConfig | None = None) -> None: | ||
| """Route all logging through a single console handler on the root logger, | ||
| using the SDK's format, so the application's own loggers get it too. Pass a | ||
| ``LogConfig`` to override the ``DIAL_SDK_LOG*`` env vars. | ||
| """ | ||
|
|
||
| config = config or LogConfig() | ||
| root = logging.getLogger() | ||
|
|
||
| # OTEL_PYTHON_LOG_CORRELATION installs OTEL's own root console format; defer | ||
| # to it. Otherwise take over — drop competing stderr handlers (e.g. the | ||
| # basicConfig one from ANTHROPIC_LOG) and install ours. | ||
| if not OTEL_PYTHON_LOG_CORRELATION: | ||
| for h in root.handlers[:]: | ||
| if isinstance(h, logging.StreamHandler) and h.stream is sys.stderr: | ||
| root.removeHandler(h) | ||
| handler = logging.StreamHandler(sys.stderr) | ||
| handler.setFormatter(config.formatter) | ||
| root.addHandler(handler) | ||
|
|
||
| for name in ("aidial_sdk", "uvicorn", "uvicorn.access", "uvicorn.error"): | ||
| child = logging.getLogger(name) | ||
| child.handlers = [] | ||
| child.propagate = True | ||
|
|
||
| logging.getLogger("aidial_sdk").setLevel(config.level) | ||
|
|
||
|
|
||
| def configure_sdk_logger() -> None: | ||
| """Configure only the SDK's own loggers (``aidial_sdk``, ``uvicorn``), called | ||
| once when ``DIALApp`` is imported. To format your own loggers the same way, | ||
| call ``configure_root_logger()``.""" | ||
|
|
||
| config = LogConfig() | ||
| handler = logging.StreamHandler(sys.stderr) | ||
| handler.setFormatter(config.formatter) | ||
|
|
||
| aidial_sdk = logging.getLogger("aidial_sdk") | ||
| aidial_sdk.handlers = [handler] | ||
| aidial_sdk.setLevel(config.level) | ||
|
|
||
| uvicorn = logging.getLogger("uvicorn") | ||
| uvicorn.handlers = [handler] | ||
| uvicorn.propagate = False |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.