Skip to content
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

Rename and clean up logging and starlette #48

Merged
merged 2 commits into from
Jan 15, 2025
Merged

Conversation

jschlyter
Copy link
Member

@jschlyter jschlyter commented Jan 15, 2025

Summary by CodeRabbit

  • New Features

    • Upgraded logging system to use structlog for more structured and flexible logging
    • Enhanced log entry context and formatting
  • Improvements

    • Simplified logging configuration
    • Improved log handling for Uvicorn logs
  • Dependency Changes

    • Removed jsonformatter dependency
  • Refactor

    • Migrated logging setup from custom implementation to structlog
    • Reorganized logging-related code
  • Tests

    • Updated logging test cases to reflect new logging approach
    • Removed obsolete test for previous logging setup

@jschlyter jschlyter requested a review from a team as a code owner January 15, 2025 05:45
Copy link

coderabbitai bot commented Jan 15, 2025

Walkthrough

The pull request introduces a comprehensive refactoring of the logging system in the project. The changes involve migrating from a custom JSON formatter to the structlog library for structured logging. The main modifications include moving logging configuration functions from dnstapir/structlog.py to dnstapir/logging.py, removing the jsonformatter dependency, and updating the logging setup to provide more flexible log formatting and processing.

Changes

File Change Summary
dnstapir/logging.py Added drop_color_message_key and setup_logging functions for structured logging configuration
dnstapir/starlette.py Added a docstring describing Starlette middleware utilities
dnstapir/structlog.py Deleted file, with logging functions moved to logging.py
pyproject.toml Removed jsonformatter dependency
tests/test_logging.py Updated logging setup to use new setup_logging function
tests/test_structlog.py Deleted test file

Sequence Diagram

sequenceDiagram
    participant App as Application
    participant Logging as Logging Setup
    participant StructLog as Structlog
    
    App->>Logging: Call setup_logging()
    Logging->>StructLog: Configure logging processors
    Logging->>Logging: Set up log handlers
    Logging->>StructLog: Configure log formatting
    
    Note over Logging,StructLog: Logging configured with optional JSON format
Loading

Possibly related PRs

  • Add structlog #47: Changes in this PR involve the addition of the drop_color_message_key and setup_logging functions in dnstapir/structlog.py, which are directly related to the functions added in the main PR's overhaul of the logging configuration in dnstapir/logging.py.

Poem

🐰 A Logging Leap of Faith 🌟
Structlog dances, JSON takes flight,
Logging transformed with rabbit-like might!
Keys dropped, processors aligned,
A cleaner log trail now defined.
Hop, hop, hooray for structured grace! 🎉

Finishing Touches

  • 📝 Generate Docstrings (Beta)

Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR. (Beta)
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
dnstapir/logging.py (1)

23-23: Ensure compatibility with Python versions prior to 3.9

The use of built-in generic types like list[Processor] requires Python 3.9 or higher. If the project supports earlier versions (e.g., Python 3.7 or 3.8), consider importing List from the typing module.

Apply this diff for compatibility:

+from typing import List
-shared_processors: list[Processor] = [
+shared_processors: List[Processor] = [
     structlog.contextvars.merge_contextvars,
tests/test_logging.py (2)

9-9: Adjust logging statement to follow structlog best practices

When using structlog, it's recommended to pass variables as keyword arguments rather than using positional string formatting. The current use may not format the message as expected.

Consider revising the logging statement:

-    logger.warning("Hello %s", "world", foo="bar")
+    logger.warning("Hello world", foo="bar")

Or, to include dynamic content:

-    logger.warning("Hello %s", "world", foo="bar")
+    logger.warning(f"Hello {'world'}", foo="bar")

5-9: Add assertions to the test function to validate logging

The test_logging function currently lacks assertions. Adding assertions ensures that the logging configuration behaves as expected and helps catch issues early.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0696975 and dcc18a4.

⛔ Files ignored due to path filters (1)
  • poetry.lock is excluded by !**/*.lock
📒 Files selected for processing (6)
  • dnstapir/logging.py (1 hunks)
  • dnstapir/starlette.py (1 hunks)
  • dnstapir/structlog.py (0 hunks)
  • pyproject.toml (0 hunks)
  • tests/test_logging.py (1 hunks)
  • tests/test_structlog.py (0 hunks)
💤 Files with no reviewable changes (3)
  • pyproject.toml
  • tests/test_structlog.py
  • dnstapir/structlog.py
✅ Files skipped from review due to trivial changes (1)
  • dnstapir/starlette.py
🔇 Additional comments (1)
dnstapir/logging.py (1)

69-76: Verify the logging behavior for Uvicorn loggers

The configuration modifies handlers and propagation for Uvicorn loggers. Ensure that this setup captures logs as intended without suppressing important log messages.

Would you like assistance in verifying that Uvicorn logs are correctly captured and no critical logs are missing?

return event_dict


def setup_logging(json_logs: bool = False, log_level: str = "INFO") -> None:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Validate the log_level parameter to prevent invalid log levels

The log_level parameter in setup_logging is not validated before use. Passing an invalid log level could lead to runtime errors. Consider validating it against the allowed log levels to ensure robustness.

Apply this diff to add validation:

 def setup_logging(json_logs: bool = False, log_level: str = "INFO") -> None:
+    valid_log_levels = ["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG", "NOTSET"]
+    if log_level.upper() not in valid_log_levels:
+        raise ValueError(f"Invalid log level: {log_level}")
     timestamper = structlog.processors.TimeStamper(fmt="iso")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def setup_logging(json_logs: bool = False, log_level: str = "INFO") -> None:
def setup_logging(json_logs: bool = False, log_level: str = "INFO") -> None:
valid_log_levels = ["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG", "NOTSET"]
if log_level.upper() not in valid_log_levels:
raise ValueError(f"Invalid log level: {log_level}")
timestamper = structlog.processors.TimeStamper(fmt="iso")

@jschlyter jschlyter merged commit 01a2386 into main Jan 15, 2025
8 of 9 checks passed
@jschlyter jschlyter deleted the cleanup_logging branch January 15, 2025 07:18
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (5)
dnstapir/logging.py (5)

1-3: Enhance module documentation.

The docstring should include more details about the module's purpose, functionality, and usage examples.

 """
-structlog helpers based on code from https://wazaari.dev/blog/fastapi-integration
+Structured logging configuration for dnstapir.
+
+This module provides utilities for setting up structured logging using the structlog library.
+It includes functionality for handling both JSON and console logging formats, with special
+handling for Uvicorn logs.
+
+Based on code from: https://wazaari.dev/blog/fastapi-structlog-integration
+
+Example:
+    >>> from dnstapir.logging import setup_logging
+    >>> setup_logging(json_logs=True, log_level="INFO")
 """

10-10: Consider using Enum for log levels.

Using an Enum for log levels would provide better type safety and IDE support.

-VALID_LOG_LEVELS = set(["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"])
+from enum import Enum
+
+class LogLevel(str, Enum):
+    CRITICAL = "CRITICAL"
+    ERROR = "ERROR"
+    WARNING = "WARNING"
+    INFO = "INFO"
+    DEBUG = "DEBUG"
+
+VALID_LOG_LEVELS = set(level.value for level in LogLevel)

22-24: Improve function documentation.

The docstring should include parameter descriptions, return type, and a note about global state modification.

-    """Set up logging"""
+    """
+    Configure structured logging for the application.
+
+    This function modifies the global logging state by reconfiguring the root logger
+    and Uvicorn loggers.
+
+    Args:
+        json_logs (bool): If True, use JSON formatter. Otherwise, use console formatter.
+        log_level (str): The logging level to use. Must be one of CRITICAL, ERROR,
+            WARNING, INFO, or DEBUG.
+
+    Raises:
+        ValueError: If the provided log_level is invalid.
+    """

30-39: Consider moving shared processors to a module-level constant.

The shared processors list could be defined at the module level to improve readability and reusability.

+DEFAULT_PROCESSORS: list[Processor] = [
+    structlog.contextvars.merge_contextvars,
+    structlog.stdlib.add_logger_name,
+    structlog.stdlib.add_log_level,
+    structlog.stdlib.PositionalArgumentsFormatter(),
+    structlog.stdlib.ExtraAdder(),
+    drop_color_message_key,
+    structlog.processors.TimeStamper(fmt="iso"),
+    structlog.processors.StackInfoRenderer(),
+]
+
 def setup_logging(json_logs: bool = False, log_level: str = "INFO") -> None:
     # ...
-    shared_processors: list[Processor] = [
-        structlog.contextvars.merge_contextvars,
-        structlog.stdlib.add_logger_name,
-        structlog.stdlib.add_log_level,
-        structlog.stdlib.PositionalArgumentsFormatter(),
-        structlog.stdlib.ExtraAdder(),
-        drop_color_message_key,
-        timestamper,
-        structlog.processors.StackInfoRenderer(),
-    ]
+    shared_processors = DEFAULT_PROCESSORS.copy()

22-83: Consider breaking down the setup_logging function.

The function is quite long and handles multiple concerns. Consider splitting it into smaller, focused functions:

  1. Validation
  2. Processor configuration
  3. Root logger setup
  4. Uvicorn logger setup
+def _validate_log_level(log_level: str) -> None:
+    if log_level.upper() not in VALID_LOG_LEVELS:
+        raise ValueError(f"Invalid log level: {log_level}")
+
+def _configure_processors(json_logs: bool) -> tuple[list[Processor], structlog.stdlib.ProcessorFormatter]:
+    shared_processors = DEFAULT_PROCESSORS.copy()
+    if json_logs:
+        shared_processors.append(structlog.processors.format_exc_info)
+
+    structlog.configure(
+        processors=shared_processors + [structlog.stdlib.ProcessorFormatter.wrap_for_formatter],
+        logger_factory=structlog.stdlib.LoggerFactory(),
+        cache_logger_on_first_use=True,
+    )
+
+    log_renderer = structlog.processors.JSONRenderer() if json_logs else structlog.dev.ConsoleRenderer()
+    return shared_processors, structlog.stdlib.ProcessorFormatter(
+        foreign_pre_chain=shared_processors,
+        processors=[
+            structlog.stdlib.ProcessorFormatter.remove_processors_meta,
+            log_renderer,
+        ],
+    )
+
+def _setup_root_logger(formatter: structlog.stdlib.ProcessorFormatter, log_level: str) -> None:
+    handler = logging.StreamHandler()
+    handler.setFormatter(formatter)
+    root_logger = logging.getLogger()
+    root_logger.addHandler(handler)
+    root_logger.setLevel(log_level.upper())
+
+def _setup_uvicorn_loggers() -> None:
+    for _log in ["uvicorn", "uvicorn.error"]:
+        logging.getLogger(_log).handlers.clear()
+        logging.getLogger(_log).propagate = True
+    logging.getLogger("uvicorn.access").handlers.clear()
+    logging.getLogger("uvicorn.access").propagate = False
+
 def setup_logging(json_logs: bool = False, log_level: str = "INFO") -> None:
-    # ... current implementation ...
+    _validate_log_level(log_level)
+    shared_processors, formatter = _configure_processors(json_logs)
+    _setup_root_logger(formatter, log_level)
+    _setup_uvicorn_loggers()
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between dcc18a4 and fa3bdc3.

📒 Files selected for processing (1)
  • dnstapir/logging.py (1 hunks)
🔇 Additional comments (1)
dnstapir/logging.py (1)

13-19: LGTM! Well-documented utility function.

The function is well-typed, properly documented, and correctly handles the removal of duplicate color messages from Uvicorn logs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant