Skip to content
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
3 changes: 3 additions & 0 deletions example/inertia-pingcrm-app/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@ APP_ENV=local
APP_URL=http://localhost:8000
APP_DEBUG=true

LOG_LEVEL=info

DB_CONNECTION=postgres
DB_HOST=127.0.0.1
DB_PORT=5432
DB_DATABASE=database_app_test
DB_USERNAME=app
DB_PASSWORD=secret
DB_ECHO=false

AWS_ENDPOINT=http://localhost:9000
AWS_ACCESS_KEY_ID=minioadmin
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
@dataclasses.dataclass
class LoggingConfig:
default: str = dataclasses.field(default_factory=lambda: env("LOG_CHANNEL", "stack"))
level: str = dataclasses.field(default_factory=lambda: env("LOG_LEVEL", "info"))

channels: dict = dataclasses.field(
default_factory=lambda: {
Expand Down
8 changes: 6 additions & 2 deletions fastapi_startkit/src/fastapi_startkit/logging/handler.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import logging

# Framework-only severities have no stdlib equivalent; map them to the nearest one.
_STDLIB_LEVELS = {"notice": "INFO", "alert": "CRITICAL", "emergency": "CRITICAL"}


class LoggingHandler(logging.Handler):
def emit(self, record: logging.LogRecord) -> None:
Expand All @@ -8,8 +11,9 @@ def emit(self, record: logging.LogRecord) -> None:
Logger.log(record.levelname, self.format(record))

@staticmethod
def install():
def install(level: str | None = None):
root_logger = logging.getLogger()
if not any(isinstance(h, LoggingHandler) for h in root_logger.handlers):
root_logger.addHandler(LoggingHandler())
root_logger.setLevel(logging.DEBUG)
if level:
root_logger.setLevel(_STDLIB_LEVELS.get(level.lower(), level.upper()))
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ def __init__(
self,
channel_factory: type[ChannelFactory] = ChannelFactory,
driver_factory: type[DriverFactory] = DriverFactory,
level: str | None = None,
):
self.channel_factory = channel_factory
self.driver_factory = driver_factory
self.configure_python_logging()
self.configure_python_logging(level)

def channel(self, channel: str):
channel_class = self.channel_factory.make(channel)
Expand All @@ -19,7 +20,7 @@ def channel(self, channel: str):
return channel_class()

@classmethod
def configure_python_logging(cls):
def configure_python_logging(cls, level: str | None = None):
from ..handler import LoggingHandler

LoggingHandler.install()
LoggingHandler.install(level)
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ def register(self):

self.app.bind("LogChannelFactory", ChannelFactory)
self.app.bind("LogDriverFactory", DriverFactory)
self.app.bind("LoggingManager", LoggingManager(ChannelFactory, DriverFactory))
self.app.bind("LoggingManager", LoggingManager(ChannelFactory, DriverFactory, config.get("level")))

def boot(self):
self.publishes({Path(__file__).resolve().parent.parent.joinpath("config/logging.py"): "config/logging.py"})
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from typing import Optional, Dict, Any
from pydantic.dataclasses import dataclass
from pydantic.fields import Field
from fastapi_startkit.environment.environment import env


Expand All @@ -8,6 +9,7 @@ class SQLiteConfig:
driver: str = "sqlite"
url: Optional[str] = env("DB_URL", None, cast=False)
database: str = env("DB_DATABASE", "database.sqlite")
echo: bool = Field(default_factory=lambda: env("DB_ECHO", False))
options: Optional[Dict[str, Any]] = None


Expand All @@ -23,6 +25,7 @@ class MySQLConfig:
unix_socket: str = env("DB_SOCKET", "", cast=False)
charset: str = env("DB_CHARSET", "utf8mb4")
collation: str = env("DB_COLLATION", "utf8mb4_unicode_ci")
echo: bool = Field(default_factory=lambda: env("DB_ECHO", False))
options: Optional[Dict[str, Any]] = None


Expand All @@ -37,4 +40,5 @@ class PostgresConfig:
password: str = env("DB_PASSWORD", "", cast=False)
charset: str = env("DB_CHARSET", "utf8")
sslmode: str = env("DB_SSLMODE", "prefer")
echo: bool = Field(default_factory=lambda: env("DB_ECHO", False))
options: Optional[Dict[str, Any]] = None
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ def build_url(cls, config: dict) -> str:
@classmethod
def create_engine(cls, cfg: dict) -> AsyncEngine:
url = cls.build_url(cfg)
kwargs: dict[str, Any] = {"echo": True}
kwargs: dict[str, Any] = {"echo": bool(cfg.get("echo", False))}
from fastapi_startkit.application import app

if cast("Application", app()).is_testing():
Expand Down
48 changes: 48 additions & 0 deletions fastapi_startkit/tests/logging/test_log_level.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import logging
import unittest

from fastapi_startkit.logging.config import LoggingConfig
from fastapi_startkit.logging.handler import LoggingHandler
from fastapi_startkit.logging.managers import LoggingManager


class RootLogLevelTest(unittest.TestCase):
def setUp(self):
self.root = logging.getLogger()
self._handlers = list(self.root.handlers)
self._level = self.root.level

def tearDown(self):
for handler in list(self.root.handlers):
if handler not in self._handlers:
self.root.removeHandler(handler)
self.root.setLevel(self._level)

def test_install_without_level_leaves_root_level_untouched(self):
self.root.setLevel(logging.WARNING)
LoggingHandler.install()
self.assertEqual(self.root.level, logging.WARNING)

def test_manager_applies_configured_info_level(self):
self.root.setLevel(logging.WARNING)
LoggingManager(level="info")
self.assertEqual(self.root.level, logging.INFO)

def test_configured_level_is_applied_even_when_handler_already_installed(self):
LoggingHandler.install("debug")
LoggingHandler.install("error")
self.assertEqual(self.root.level, logging.ERROR)
self.assertEqual(len([h for h in self.root.handlers if isinstance(h, LoggingHandler)]), 1)

def test_framework_only_levels_map_to_stdlib_levels(self):
LoggingHandler.install("notice")
self.assertEqual(self.root.level, logging.INFO)
LoggingHandler.install("emergency")
self.assertEqual(self.root.level, logging.CRITICAL)

def test_logging_config_defaults_to_info(self):
self.assertEqual(LoggingConfig().level, "info")


if __name__ == "__main__":
unittest.main()
15 changes: 15 additions & 0 deletions fastapi_startkit/tests/masoniteorm/config/test_db_url.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import unittest

from fastapi_startkit.exceptions.exceptions import DriverNotFound
from fastapi_startkit.masoniteorm.config.config import MySQLConfig, PostgresConfig, SQLiteConfig
from fastapi_startkit.masoniteorm.connections.factory import ConnectionFactory


Expand Down Expand Up @@ -175,3 +176,17 @@ def test_url_passthrough_bypasses_driver_validation(self):
}
url = ConnectionFactory.build_url(config)
self.assertEqual(url, "sqlite+aiosqlite:///db.sqlite3")


class TestConnectionFactoryEcho(unittest.TestCase):
def test_engine_echo_is_off_by_default(self):
engine = ConnectionFactory.create_engine({"driver": "sqlite", "database": ":memory:"})
self.assertFalse(engine.echo)

def test_engine_echo_follows_config(self):
engine = ConnectionFactory.create_engine({"driver": "sqlite", "database": ":memory:", "echo": True})
self.assertTrue(engine.echo)

def test_connection_configs_default_echo_to_false(self):
for config_class in (SQLiteConfig, MySQLConfig, PostgresConfig):
self.assertFalse(config_class().echo)
Loading