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

Add logger #316

Merged
merged 2 commits into from
Oct 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion makefile
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,4 @@ kill-container: ## Stop the running docker container.

.PHONY: run-local
run-local: ## Run the app locally.
uvicorn svc.main:app --port 5002 --reload
uv run uvicorn svc.main:app --port 5002 --reload
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ max-complexity = 10
#################################################

[tool.pytest.ini_options]
addopts = "--strict-markers --maxfail 1 --cov src tests/ --no-header"
addopts = "--strict-markers --maxfail 1 --cov svc tests/ --no-header"
markers = """
integration: mark a test as an integration test.
"""
Expand Down
8 changes: 2 additions & 6 deletions svc/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
import logging
import sys
from svc.core.logger import configure_logger

# Set default logging value to debug
logging.basicConfig(level=logging.INFO)

logging.info("Running fastapi-nano with Python %s", sys.version)
configure_logger()
31 changes: 31 additions & 0 deletions svc/core/logger.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import logging


def configure_logger() -> None:
"""Configure a custom logger."""

# Create a logger
logger = logging.getLogger("fnano")
logger.setLevel(logging.INFO)

# Create a handler (console output in this case)
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)

# Create a formatter and set it to the handler
formatter = logging.Formatter(
"%(asctime)s - %(name)s - %(levelname)s - %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
console_handler.setFormatter(formatter)

# Add the handler to the logger
if not logger.hasHandlers():
logger.addHandler(console_handler)

# Disable propagation to avoid log duplication via uvicorn
logger.propagate = False

# Disable passlib logger
# See: <https://github.com/pyca/bcrypt/issues/684>
logging.getLogger("passlib").setLevel(logging.ERROR)
34 changes: 21 additions & 13 deletions svc/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,24 @@
from svc.core import auth
from svc.routes import views

app = FastAPI()

# Set all CORS enabled origins
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)

app.include_router(auth.router)
app.include_router(views.router)

def create_app() -> FastAPI:
"""Create a FastAPI application."""

app = FastAPI()

# Set all CORS enabled origins
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)

app.include_router(auth.router)
app.include_router(views.router)
return app


app = create_app()
11 changes: 9 additions & 2 deletions svc/routes/views.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
from __future__ import annotations

import logging

from fastapi import APIRouter, Depends

from svc.apis.api_a.mainmod import main_func as main_func_a
from svc.apis.api_b.mainmod import main_func as main_func_b
from svc.core.auth import get_current_user

router = APIRouter()
logger = logging.getLogger(__name__)


@router.get("/")
Expand All @@ -22,12 +25,16 @@ async def view_a(
num: int,
auth: Depends = Depends(get_current_user),
) -> dict[str, int]:
return main_func_a(num)
result = main_func_a(num)
logger.info(f"API A: {result}")
return result


@router.get("/api_b/{num}", tags=["api_b"])
async def view_b(
num: int,
auth: Depends = Depends(get_current_user),
) -> dict[str, int]:
return main_func_b(num)
result = main_func_b(num)
logger.info(f"API B: {result}")
return result
78 changes: 78 additions & 0 deletions svc/tests/test_logger.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
"""Test suite for the logger module."""

import logging
from collections.abc import Iterator
from io import StringIO

import pytest
from _pytest.logging import LogCaptureFixture # for caplog fixture typing

from svc.core.logger import configure_logger


@pytest.fixture
def canned_logger() -> Iterator[logging.Logger]:
"""Fixture to configure the logger for tests with propagate set to True."""
logger = logging.getLogger("fnano")

# Clear any handlers attached to the logger from previous tests
logger.handlers = []

# Call the function that configures the logger
configure_logger()

# Set logger propagation to True for tests
original_propagate = logger.propagate
logger.propagate = True

# Yield the logger for use in tests
yield logger

# Reset the logger propagate to the original value after test
logger.propagate = original_propagate


def test_configure_logger(
canned_logger: logging.Logger, caplog: LogCaptureFixture
) -> None:
# Test if logger is configured properly
with caplog.at_level(logging.INFO, logger="fnano"):
canned_logger.info("Test log message")

# Check if the log was captured and formatted correctly
assert len(caplog.records) == 1
assert caplog.records[0].levelname == "INFO"
assert caplog.records[0].message == "Test log message"

# Check if the time format in the log is correct
log_time = caplog.records[0].asctime
assert isinstance(log_time, str)
assert len(log_time) == 19 # "YYYY-MM-DD HH:MM:SS" is 19 characters long


def test_log_output_format(canned_logger: logging.Logger) -> None:
# Set up a StringIO stream to capture log output
stream = StringIO()
handler = logging.StreamHandler(stream)
handler.setLevel(logging.INFO)

formatter = logging.Formatter(
"%(asctime)s - %(name)s - %(levelname)s - %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
handler.setFormatter(formatter)

canned_logger.handlers = [] # Remove any pre-existing handlers
canned_logger.addHandler(handler)
canned_logger.setLevel(logging.INFO)

# Log a message and capture the output
canned_logger.info("Test log message")

# Flush the handler and get the output
handler.flush()
log_output = stream.getvalue()

# Check the log output format
assert "fnano - INFO - Test log message" in log_output
assert log_output.startswith("20") # The log should start with a year like "2024"
Loading