forked from Github-Mirrors/canaille
feat: usedefault python logging configuration format
This commit is contained in:
parent
4edffcaa9f
commit
dc81832159
13 changed files with 171 additions and 104 deletions
|
@ -3,6 +3,11 @@ All notable changes to this project will be documented in this file.
|
|||
The format is based on `Keep a Changelog <https://keepachangelog.com/en/1.0.0/>`_,
|
||||
and this project adheres to `Semantic Versioning <https://semver.org/spec/v2.0.0.html>`_.
|
||||
|
||||
Changed
|
||||
*******
|
||||
|
||||
- Use default python logging configuration format. :issue:`188` :pr:`165`
|
||||
|
||||
[0.0.42] - 2023-12-29
|
||||
=====================
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import datetime
|
||||
from logging.config import dictConfig
|
||||
from logging.config import fileConfig
|
||||
|
||||
from flask import Flask
|
||||
from flask import request
|
||||
|
@ -25,20 +26,9 @@ def setup_sentry(app): # pragma: no cover
|
|||
|
||||
|
||||
def setup_logging(app):
|
||||
log_level = app.config.get("LOGGING", {}).get("LEVEL", "WARNING")
|
||||
if not app.config.get("LOGGING", {}).get("PATH"):
|
||||
handler = {
|
||||
"class": "logging.StreamHandler",
|
||||
"stream": "ext://flask.logging.wsgi_errors_stream",
|
||||
"formatter": "default",
|
||||
}
|
||||
else:
|
||||
handler = {
|
||||
"class": "logging.handlers.WatchedFileHandler",
|
||||
"filename": app.config["LOGGING"]["PATH"],
|
||||
"formatter": "default",
|
||||
}
|
||||
|
||||
conf = app.config.get("LOGGING")
|
||||
if conf is None:
|
||||
log_level = "DEBUG" if app.debug else "INFO"
|
||||
dictConfig(
|
||||
{
|
||||
"version": 1,
|
||||
|
@ -47,15 +37,28 @@ def setup_logging(app):
|
|||
"format": "[%(asctime)s] %(levelname)s in %(module)s: %(message)s",
|
||||
}
|
||||
},
|
||||
"handlers": {"wsgi": handler},
|
||||
"handlers": {
|
||||
"wsgi": {
|
||||
"class": "logging.StreamHandler",
|
||||
"stream": "ext://flask.logging.wsgi_errors_stream",
|
||||
"formatter": "default",
|
||||
}
|
||||
},
|
||||
"root": {"level": log_level, "handlers": ["wsgi"]},
|
||||
"loggers": {
|
||||
"faker": {"level": "WARNING"},
|
||||
"mail.log": {"level": "WARNING"},
|
||||
},
|
||||
"disable_existing_loggers": False,
|
||||
}
|
||||
)
|
||||
|
||||
elif isinstance(conf, dict):
|
||||
dictConfig(conf)
|
||||
|
||||
else:
|
||||
fileConfig(conf, disable_existing_loggers=False)
|
||||
|
||||
|
||||
def setup_jinja(app):
|
||||
app.jinja_env.filters["len"] = len
|
||||
|
|
|
@ -67,15 +67,13 @@ SECRET_KEY = "change me before you go in production"
|
|||
# Defaults to 2 days
|
||||
# INVITATION_EXPIRATION = 172800
|
||||
|
||||
[LOGGING]
|
||||
# LEVEL can be one value among:
|
||||
# DEBUG, INFO, WARNING, ERROR, CRITICAL
|
||||
# Defaults to WARNING
|
||||
# LEVEL = "WARNING"
|
||||
|
||||
# The path of the log file. If not set (the default) logs are
|
||||
# written in the standard error output.
|
||||
# PATH = ""
|
||||
# LOGGING configures the logging output:
|
||||
# - if unset, everything is logged in the standard output
|
||||
# the log level is debug if DEBUG is True, else this is INFO
|
||||
# - if this is a dictionnary, it is passed to the python dictConfig method:
|
||||
# https://docs.python.org/3/library/logging.config.html#logging.config.dictConfig
|
||||
# - if this is a string, it is passed to the python fileConfig method
|
||||
# https://docs.python.org/3/library/logging.config.html#logging.config.fileConfig
|
||||
|
||||
# [BACKENDS.SQL]
|
||||
# The SQL database connection string
|
||||
|
|
|
@ -67,16 +67,13 @@ ENABLE_REGISTRATION = true
|
|||
# Defaults to 2 days
|
||||
# INVITATION_EXPIRATION = 172800
|
||||
|
||||
[LOGGING]
|
||||
# LEVEL can be one value among:
|
||||
# DEBUG, INFO, WARNING, ERROR, CRITICAL
|
||||
# Defaults to WARNING
|
||||
# LEVEL = "WARNING"
|
||||
LEVEL = "DEBUG"
|
||||
|
||||
# The path of the log file. If not set (the default) logs are
|
||||
# written in the standard error output.
|
||||
# PATH = ""
|
||||
# LOGGING configures the logging output:
|
||||
# - if unset, everything is logged in the standard output
|
||||
# the log level is debug if DEBUG is True, else this is INFO
|
||||
# - if this is a dictionnary, it is passed to the python dictConfig method:
|
||||
# https://docs.python.org/3/library/logging.config.html#logging.config.dictConfig
|
||||
# - if this is a string, it is passed to the python fileConfig method
|
||||
# https://docs.python.org/3/library/logging.config.html#logging.config.fileConfig
|
||||
|
||||
# [BACKENDS.SQL]
|
||||
# The SQL database connection string
|
||||
|
|
|
@ -67,16 +67,13 @@ ENABLE_REGISTRATION = true
|
|||
# Defaults to 2 days
|
||||
# INVITATION_EXPIRATION = 172800
|
||||
|
||||
[LOGGING]
|
||||
# LEVEL can be one value among:
|
||||
# DEBUG, INFO, WARNING, ERROR, CRITICAL
|
||||
# Defaults to WARNING
|
||||
# LEVEL = "WARNING"
|
||||
LEVEL = "DEBUG"
|
||||
|
||||
# The path of the log file. If not set (the default) logs are
|
||||
# written in the standard error output.
|
||||
# PATH = ""
|
||||
# LOGGING configures the logging output:
|
||||
# - if unset, everything is logged in the standard output
|
||||
# the log level is debug if DEBUG is True, else this is INFO
|
||||
# - if this is a dictionnary, it is passed to the python dictConfig method:
|
||||
# https://docs.python.org/3/library/logging.config.html#logging.config.dictConfig
|
||||
# - if this is a string, it is passed to the python fileConfig method
|
||||
# https://docs.python.org/3/library/logging.config.html#logging.config.fileConfig
|
||||
|
||||
# [BACKENDS.SQL]
|
||||
# The SQL database connection string
|
||||
|
|
|
@ -67,16 +67,13 @@ ENABLE_REGISTRATION = true
|
|||
# Defaults to 2 days
|
||||
# INVITATION_EXPIRATION = 172800
|
||||
|
||||
[LOGGING]
|
||||
# LEVEL can be one value among:
|
||||
# DEBUG, INFO, WARNING, ERROR, CRITICAL
|
||||
# Defaults to WARNING
|
||||
# LEVEL = "WARNING"
|
||||
LEVEL = "DEBUG"
|
||||
|
||||
# The path of the log file. If not set (the default) logs are
|
||||
# written in the standard error output.
|
||||
# PATH = ""
|
||||
# LOGGING configures the logging output:
|
||||
# - if unset, everything is logged in the standard output
|
||||
# the log level is debug if DEBUG is True, else this is INFO
|
||||
# - if this is a dictionnary, it is passed to the python dictConfig method:
|
||||
# https://docs.python.org/3/library/logging.config.html#logging.config.dictConfig
|
||||
# - if this is a string, it is passed to the python fileConfig method
|
||||
# https://docs.python.org/3/library/logging.config.html#logging.config.fileConfig
|
||||
|
||||
[BACKENDS.SQL]
|
||||
# The SQL database connection string
|
||||
|
|
|
@ -67,16 +67,13 @@ ENABLE_REGISTRATION = true
|
|||
# Defaults to 2 days
|
||||
# INVITATION_EXPIRATION = 172800
|
||||
|
||||
[LOGGING]
|
||||
# LEVEL can be one value among:
|
||||
# DEBUG, INFO, WARNING, ERROR, CRITICAL
|
||||
# Defaults to WARNING
|
||||
# LEVEL = "WARNING"
|
||||
LEVEL = "DEBUG"
|
||||
|
||||
# The path of the log file. If not set (the default) logs are
|
||||
# written in the standard error output.
|
||||
# PATH = ""
|
||||
# LOGGING configures the logging output:
|
||||
# - if unset, everything is logged in the standard output
|
||||
# the log level is debug if DEBUG is True, else this is INFO
|
||||
# - if this is a dictionnary, it is passed to the python dictConfig method:
|
||||
# https://docs.python.org/3/library/logging.config.html#logging.config.dictConfig
|
||||
# - if this is a string, it is passed to the python fileConfig method
|
||||
# https://docs.python.org/3/library/logging.config.html#logging.config.fileConfig
|
||||
|
||||
# [BACKENDS.SQL]
|
||||
# The SQL database connection string
|
||||
|
|
|
@ -67,16 +67,13 @@ ENABLE_REGISTRATION = true
|
|||
# Defaults to 2 days
|
||||
# INVITATION_EXPIRATION = 172800
|
||||
|
||||
[LOGGING]
|
||||
# LEVEL can be one value among:
|
||||
# DEBUG, INFO, WARNING, ERROR, CRITICAL
|
||||
# Defaults to WARNING
|
||||
# LEVEL = "WARNING"
|
||||
LEVEL = "DEBUG"
|
||||
|
||||
# The path of the log file. If not set (the default) logs are
|
||||
# written in the standard error output.
|
||||
# PATH = ""
|
||||
# LOGGING configures the logging output:
|
||||
# - if unset, everything is logged in the standard output
|
||||
# the log level is debug if DEBUG is True, else this is INFO
|
||||
# - if this is a dictionnary, it is passed to the python dictConfig method:
|
||||
# https://docs.python.org/3/library/logging.config.html#logging.config.dictConfig
|
||||
# - if this is a string, it is passed to the python fileConfig method
|
||||
# https://docs.python.org/3/library/logging.config.html#logging.config.fileConfig
|
||||
|
||||
# [BACKENDS.SQL]
|
||||
# The SQL database connection string
|
||||
|
|
|
@ -67,16 +67,13 @@ ENABLE_REGISTRATION = true
|
|||
# Defaults to 2 days
|
||||
# INVITATION_EXPIRATION = 172800
|
||||
|
||||
[LOGGING]
|
||||
# LEVEL can be one value among:
|
||||
# DEBUG, INFO, WARNING, ERROR, CRITICAL
|
||||
# Defaults to WARNING
|
||||
# LEVEL = "WARNING"
|
||||
LEVEL = "DEBUG"
|
||||
|
||||
# The path of the log file. If not set (the default) logs are
|
||||
# written in the standard error output.
|
||||
# PATH = ""
|
||||
# LOGGING configures the logging output:
|
||||
# - if unset, everything is logged in the standard output
|
||||
# the log level is debug if DEBUG is True, else this is INFO
|
||||
# - if this is a dictionnary, it is passed to the python dictConfig method:
|
||||
# https://docs.python.org/3/library/logging.config.html#logging.config.dictConfig
|
||||
# - if this is a string, it is passed to the python fileConfig method
|
||||
# https://docs.python.org/3/library/logging.config.html#logging.config.fileConfig
|
||||
|
||||
[BACKENDS.SQL]
|
||||
# The SQL database connection string
|
||||
|
|
|
@ -45,12 +45,60 @@ def test_no_configuration():
|
|||
assert "No configuration file found." in str(exc)
|
||||
|
||||
|
||||
def test_logging_to_file(configuration, backend, tmp_path, smtpd, admin):
|
||||
def test_file_log_config(configuration, backend, tmp_path, smtpd, admin):
|
||||
assert len(smtpd.messages) == 0
|
||||
log_path = os.path.join(tmp_path, "canaille.log")
|
||||
log_path = os.path.join(tmp_path, "canaille-by-file.log")
|
||||
|
||||
file_content = LOGGING_CONF_FILE_CONTENT.format(log_path=log_path)
|
||||
config_file_path = tmp_path / "logging.conf"
|
||||
with open(config_file_path, "w") as fd:
|
||||
fd.write(file_content)
|
||||
|
||||
logging_configuration = {**configuration, "LOGGING": config_file_path}
|
||||
app = create_app(logging_configuration, backend=backend)
|
||||
|
||||
testclient = TestApp(app)
|
||||
with testclient.session_transaction() as sess:
|
||||
sess["user_id"] = [admin.id]
|
||||
|
||||
res = testclient.get("/admin/mail")
|
||||
res.form["email"] = "test@test.com"
|
||||
res = res.form.submit()
|
||||
|
||||
assert len(smtpd.messages) == 1
|
||||
assert "Test email from" in smtpd.messages[0].get("Subject")
|
||||
|
||||
with open(log_path) as fd:
|
||||
log_content = fd.read()
|
||||
|
||||
assert "Sending a mail to test@test.com: Test email from" in log_content
|
||||
|
||||
|
||||
def test_dict_log_config(configuration, backend, tmp_path, smtpd, admin):
|
||||
assert len(smtpd.messages) == 0
|
||||
log_path = os.path.join(tmp_path, "canaille-by-dict.log")
|
||||
logging_configuration = {
|
||||
**configuration,
|
||||
"LOGGING": {"LEVEL": "DEBUG", "PATH": log_path},
|
||||
"LOGGING": {
|
||||
"version": 1,
|
||||
"formatters": {
|
||||
"default": {
|
||||
"format": "[%(asctime)s] %(levelname)s in %(module)s: %(message)s",
|
||||
}
|
||||
},
|
||||
"handlers": {
|
||||
"wsgi": {
|
||||
"class": "logging.handlers.WatchedFileHandler",
|
||||
"filename": log_path,
|
||||
"formatter": "default",
|
||||
}
|
||||
},
|
||||
"root": {"level": "DEBUG", "handlers": ["wsgi"]},
|
||||
"loggers": {
|
||||
"faker": {"level": "WARNING"},
|
||||
},
|
||||
"disable_existing_loggers": False,
|
||||
},
|
||||
}
|
||||
app = create_app(logging_configuration, backend=backend)
|
||||
|
||||
|
@ -69,3 +117,27 @@ def test_logging_to_file(configuration, backend, tmp_path, smtpd, admin):
|
|||
log_content = fd.read()
|
||||
|
||||
assert "Sending a mail to test@test.com: Test email from" in log_content
|
||||
|
||||
|
||||
LOGGING_CONF_FILE_CONTENT = """
|
||||
[loggers]
|
||||
keys=root
|
||||
|
||||
[handlers]
|
||||
keys=wsgi
|
||||
|
||||
[formatters]
|
||||
keys=default
|
||||
|
||||
[logger_root]
|
||||
level=DEBUG
|
||||
handlers=wsgi
|
||||
|
||||
[handler_wsgi]
|
||||
class=logging.handlers.WatchedFileHandler
|
||||
args=('{log_path}',)
|
||||
formatter=default
|
||||
|
||||
[formatter_default]
|
||||
format=[%(asctime)s] %(levelname)s in %(module)s: %(message)s
|
||||
"""
|
||||
|
|
|
@ -207,7 +207,9 @@ def test_first_login_form_error(testclient, backend, smtpd):
|
|||
|
||||
res = testclient.get("/firstlogin/temp", status=200)
|
||||
res.form["csrf_token"] = "invalid"
|
||||
res = res.form.submit(name="action", value="sendmail", status=400)
|
||||
res = res.form.submit(
|
||||
name="action", value="sendmail", status=400, expect_errors=True
|
||||
)
|
||||
assert len(smtpd.messages) == 0
|
||||
u.delete()
|
||||
|
||||
|
|
|
@ -289,7 +289,12 @@ def test_password_reset_email_failed(SMTP, smtpd, testclient, backend, logged_ad
|
|||
|
||||
|
||||
def test_admin_bad_request(testclient, logged_admin):
|
||||
testclient.post("/profile/admin/settings", {"action": "foobar"}, status=400)
|
||||
res = testclient.get("/profile/admin/settings")
|
||||
testclient.post(
|
||||
"/profile/admin/settings",
|
||||
{"action": "foobar", "csrf_token": res.form["csrf_token"].value},
|
||||
status=400,
|
||||
)
|
||||
testclient.get("/profile/foobar/settings", status=404)
|
||||
|
||||
|
||||
|
|
|
@ -379,7 +379,7 @@ def test_no_jwt_bad_csrf(testclient, backend, logged_user, client):
|
|||
|
||||
form = res.form
|
||||
form["csrf_token"] = "foobar"
|
||||
res = form.submit(name="answer", value="logout", status=400)
|
||||
res = form.submit(name="answer", value="logout", status=400, expect_errors=True)
|
||||
|
||||
|
||||
def test_end_session_already_disconnected(testclient, backend, user, client, id_token):
|
||||
|
|
Loading…
Reference in a new issue