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

[5.X] Add logging feature #718

Merged
merged 19 commits into from
Jul 5, 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 .github/workflows/pythonapp.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
python-version: ["3.9", "3.10", "3.11"]
name: Python ${{ matrix.python-version }}
steps:
- uses: actions/checkout@v3
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,5 @@ src/masonite.egg-info/*
.vscode
build/
venv4
.python-version
*.whl
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
init:
cp .env-example .env
pip install -r requirements.txt
pip install --no-cache-dir --upgrade -r requirements.txt
pip install '.[test]'
# Create MySQL Database
# Create Postgres Database
Expand Down
910 changes: 910 additions & 0 deletions poetry.lock

Large diffs are not rendered by default.

35 changes: 35 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
[tool.poetry]
authors = ['Joe Mancuso', 'Eduardo Aguad']
description = "Masonite is a modern and developer centric Python web framework."
license = "MIT"
name = "masonite"
version = "5.0.0"

[tool.poetry.dependencies]
python = "^3.9"
inflection = ">=0.3,<0.4"
exceptionite = ">=2.2,<3.0"
pendulum = ">=2,<3"
jinja2 = "<3.1.0"
cleo = ">=0.8.1,<0.9"
hupper = ">=1.10,<1.11"
bcrypt = ">=3.2,<3.3"
whitenoise = ">=5.2,<5.3"
python-dotenv = ">=0.15,<0.16"
masonite-orm = ">=2.14,<3"
hashids = ">=1.3,<1.4"
cryptography = ">=36,<37"
tldextract = ">=2.2,<2.3"
hfilesize = ">=0.1"
dotty_dict = ">=1.3.0,<1.40"
pyjwt = ">=2.3,<2.5"
pytest = ">=7,<8"
werkzeug = ">=2,<3"
watchdog = ">=2,<3"

[tool.poetry.dev-dependencies]
pytest = "^7.0.0"

[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
8 changes: 4 additions & 4 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ argon2-cffi
bcrypt>=3.2,<3.3
black
cleo>=0.8.1,<0.9
cryptography>=3.3.1,<4.0
dotty_dict>=1.3.0<1.40
cryptography>=36,<37
dotty_dict>=1.3.0,<1.40
exceptionite>=2.0,<3
hashids>=1.3,<1.4
hfilesize>=0.1
Expand All @@ -20,7 +20,7 @@ python-dotenv>=0.15,<0.16
responses
slackblocks
tldextract>=2.2,<2.3
werkzeug>=2<3
watchdog>=2<3
werkzeug>=2,<3
watchdog>=2,<3
whitenoise>=5.2,<5.3
pyjwt>=2.3,<2.5
2 changes: 2 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,8 @@
"masonite.helpers",
"masonite.input",
"masonite.loader",
"masonite.logging",
"masonite.logging.drivers",
"masonite.mail.drivers",
"masonite.mail",
"masonite.middleware.route",
Expand Down
5 changes: 5 additions & 0 deletions src/masonite/facades/Log.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from .Facade import Facade


class Log(metaclass=Facade):
key = "logger"
24 changes: 24 additions & 0 deletions src/masonite/facades/Log.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from typing import TYPE_CHECKING, List

if TYPE_CHECKING:
from ..logging.LoggerFactory import LoggerFactory

class Log:
"""Log facade."""

def log(level: str, message: str) -> None:
"""Log a message with the given level."""
...
def debug(message: str) -> None: ...
def info(message: str) -> None: ...
def notice(message: str) -> None: ...
def warning(message: str) -> None: ...
def error(message: str) -> None: ...
def critical(message: str) -> None: ...
def alert(message: str) -> None: ...
def emergency(message: str) -> None: ...
def stack(*channels: List[str]) -> "LoggerFactory":
"""On-demand stack channels."""
...
def channel(channel: str) -> "LoggerFactory": ...
def build(driver: str, options: dict = {}) -> "LoggerFactory": ...
1 change: 1 addition & 0 deletions src/masonite/facades/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@
from .Cache import Cache
from .RateLimiter import RateLimiter
from .Broadcast import Broadcast
from .Log import Log
126 changes: 126 additions & 0 deletions src/masonite/logging/Logger.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import logging
from typing import TYPE_CHECKING, List

if TYPE_CHECKING:
from ..foundation import Application

from .LoggerFactory import LoggerFactory
from ..exceptions import InvalidConfigurationSetup


class Logger:
# 0 Emergency: system is unusable
# 1 Alert: action must be taken immediately
# 2 Critical: critical conditions
# 3 Error: error conditions
# 4 Warning: warning conditions
# 5 Notice: normal but significant condition
# 6 Informational: informational messages
# 7 Debug: debug-level messages

def __init__(self, application: "Application", options: dict = {}) -> None:
self.application = application
self.drivers = {}
self.options = options

# configure python logging module to add new levels
logging.NOTICE = 25
logging.ALERT = 60
logging.EMERGENCY = 70
new_levels = {
"notice": logging.NOTICE,
"alert": logging.ALERT,
"emergency": logging.EMERGENCY,
}
for name, levelno in new_levels.items():
logging.addLevelName(levelno, name.upper())

self.levels = {
"debug": logging.DEBUG,
"info": logging.INFO,
"notice": logging.NOTICE,
"warning": logging.WARNING,
"error": logging.ERROR,
"critical": logging.CRITICAL,
"alert": logging.ALERT,
"emergency": logging.EMERGENCY,
}

def get_default_level(self) -> str:
return self.options.get("channels.default.level")

def get_default_timezone(self) -> str:
return self.options.get("channels.default.timezone")

def get_default_format(self) -> str:
return self.options.get("channels.default.format")

def get_default_date_format(self) -> str:
return self.options.get("channels.default.date_format")

def add_driver(self, name: str, driver):
self.drivers.update({name: driver})

def set_options(self, options: dict) -> "Logger":
self.options = options
return self

def get_driver_from_channel(self, channel: str = None, options: dict = {}):
if channel is None:
channel = self.options.get("channels.default.driver")

# get driver for channel
driver_name = self.options.get(f"channels.{channel}.driver")
if not driver_name:
raise InvalidConfigurationSetup(
f"No config for channel '{channel}' in config/logging.py !"
)
return self.get_driver(
driver_name, channel, options or self.options.get(f"channels.{channel}")
)

def get_driver(self, driver: str, name: str = None, options: dict = {}):
return self.drivers[driver](self.application, name or driver, options)

def get_level_name(self, levelno: int) -> str:
for name, no in self.levels.items():
if no == levelno:
return name

def log(self, level: str, message: str) -> None:
"""Log a message with the given level."""
return LoggerFactory(self).log(level, message)

def debug(self, message: str) -> None:
return LoggerFactory(self).debug(message)

def info(self, message: str) -> None:
return LoggerFactory(self).info(message)

def notice(self, message: str) -> None:
return LoggerFactory(self).notice(message)

def warning(self, message: str) -> None:
return LoggerFactory(self).warning(message)

def error(self, message: str) -> None:
return LoggerFactory(self).error(message)

def critical(self, message: str) -> None:
return LoggerFactory(self).critical(message)

def alert(self, message: str) -> None:
return LoggerFactory(self).alert(message)

def emergency(self, message: str) -> None:
return LoggerFactory(self).emergency(message)

def stack(self, *channels: List[str]) -> "LoggerFactory":
"""On-demand stack channels."""
return LoggerFactory(self, driver="stack", options={"channels": channels})

def channel(self, channel: str) -> "LoggerFactory":
return LoggerFactory(self, channel=channel)

def build(self, driver: str, options: dict = {}) -> "LoggerFactory":
return LoggerFactory(self, driver, options=options)
6 changes: 6 additions & 0 deletions src/masonite/logging/LoggerExceptionsListener.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from ..facades import Log


class LoggerExceptionsListener:
def handle(self, exception_type: str, exception: Exception):
Log.error(f"{exception_type}: {exception}")
51 changes: 51 additions & 0 deletions src/masonite/logging/LoggerFactory.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
from typing import TYPE_CHECKING

if TYPE_CHECKING:
from .Logger import Logger


class LoggerFactory:
def __init__(
self,
logger: "Logger",
driver: str = None,
channel: str = None,
options: dict = {},
) -> None:
self.logger = logger
self.driver = driver
self.channel = channel
self.options = options

if driver and not channel:
self.selected_driver = self.logger.get_driver(driver, options=options)
# log to default configured channel or given channel
else:
self.selected_driver = self.logger.get_driver_from_channel(channel, options)

def log(self, level: str, message: str) -> None:
self.selected_driver.log(level, message)

def debug(self, message: str) -> None:
self.selected_driver.debug(message)

def info(self, message: str) -> None:
self.selected_driver.info(message)

def notice(self, message: str) -> None:
self.selected_driver.notice(message)

def warning(self, message: str) -> None:
self.selected_driver.warning(message)

def error(self, message: str) -> None:
self.selected_driver.error(message)

def critical(self, message: str) -> None:
self.selected_driver.critical(message)

def alert(self, message: str) -> None:
self.selected_driver.alert(message)

def emergency(self, message: str) -> None:
self.selected_driver.emergency(message)
34 changes: 34 additions & 0 deletions src/masonite/logging/LoggingProvider.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from ..providers import Provider
from ..facades import Config
from .Logger import Logger
from .drivers import (
TerminalDriver,
DailyFileDriver,
SingleFileDriver,
StackDriver,
SysLogDriver,
SlackDriver,
)
from .LoggerExceptionsListener import LoggerExceptionsListener


class LoggingProvider(Provider):
def __init__(self, application):
self.application = application

def register(self):
logger = Logger(self.application).set_options(Config.get("logging"))
logger.add_driver("terminal", TerminalDriver)
logger.add_driver("daily", DailyFileDriver)
logger.add_driver("single", SingleFileDriver)
logger.add_driver("stack", StackDriver)
logger.add_driver("syslog", SysLogDriver)
logger.add_driver("slack", SlackDriver)
self.application.bind("logger", logger)

self.application.make("event").listen(
"masonite.exception.*", [LoggerExceptionsListener]
)

def boot(self):
pass
1 change: 1 addition & 0 deletions src/masonite/logging/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .LoggingProvider import LoggingProvider
Loading
Loading