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/>`_,
|
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>`_.
|
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
|
[0.0.42] - 2023-12-29
|
||||||
=====================
|
=====================
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import datetime
|
import datetime
|
||||||
from logging.config import dictConfig
|
from logging.config import dictConfig
|
||||||
|
from logging.config import fileConfig
|
||||||
|
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
from flask import request
|
from flask import request
|
||||||
|
@ -25,20 +26,9 @@ def setup_sentry(app): # pragma: no cover
|
||||||
|
|
||||||
|
|
||||||
def setup_logging(app):
|
def setup_logging(app):
|
||||||
log_level = app.config.get("LOGGING", {}).get("LEVEL", "WARNING")
|
conf = app.config.get("LOGGING")
|
||||||
if not app.config.get("LOGGING", {}).get("PATH"):
|
if conf is None:
|
||||||
handler = {
|
log_level = "DEBUG" if app.debug else "INFO"
|
||||||
"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",
|
|
||||||
}
|
|
||||||
|
|
||||||
dictConfig(
|
dictConfig(
|
||||||
{
|
{
|
||||||
"version": 1,
|
"version": 1,
|
||||||
|
@ -47,15 +37,28 @@ def setup_logging(app):
|
||||||
"format": "[%(asctime)s] %(levelname)s in %(module)s: %(message)s",
|
"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"]},
|
"root": {"level": log_level, "handlers": ["wsgi"]},
|
||||||
"loggers": {
|
"loggers": {
|
||||||
"faker": {"level": "WARNING"},
|
"faker": {"level": "WARNING"},
|
||||||
|
"mail.log": {"level": "WARNING"},
|
||||||
},
|
},
|
||||||
"disable_existing_loggers": False,
|
"disable_existing_loggers": False,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
elif isinstance(conf, dict):
|
||||||
|
dictConfig(conf)
|
||||||
|
|
||||||
|
else:
|
||||||
|
fileConfig(conf, disable_existing_loggers=False)
|
||||||
|
|
||||||
|
|
||||||
def setup_jinja(app):
|
def setup_jinja(app):
|
||||||
app.jinja_env.filters["len"] = len
|
app.jinja_env.filters["len"] = len
|
||||||
|
|
|
@ -67,15 +67,13 @@ SECRET_KEY = "change me before you go in production"
|
||||||
# Defaults to 2 days
|
# Defaults to 2 days
|
||||||
# INVITATION_EXPIRATION = 172800
|
# INVITATION_EXPIRATION = 172800
|
||||||
|
|
||||||
[LOGGING]
|
# LOGGING configures the logging output:
|
||||||
# LEVEL can be one value among:
|
# - if unset, everything is logged in the standard output
|
||||||
# DEBUG, INFO, WARNING, ERROR, CRITICAL
|
# the log level is debug if DEBUG is True, else this is INFO
|
||||||
# Defaults to WARNING
|
# - if this is a dictionnary, it is passed to the python dictConfig method:
|
||||||
# LEVEL = "WARNING"
|
# 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
|
||||||
# The path of the log file. If not set (the default) logs are
|
# https://docs.python.org/3/library/logging.config.html#logging.config.fileConfig
|
||||||
# written in the standard error output.
|
|
||||||
# PATH = ""
|
|
||||||
|
|
||||||
# [BACKENDS.SQL]
|
# [BACKENDS.SQL]
|
||||||
# The SQL database connection string
|
# The SQL database connection string
|
||||||
|
|
|
@ -67,16 +67,13 @@ ENABLE_REGISTRATION = true
|
||||||
# Defaults to 2 days
|
# Defaults to 2 days
|
||||||
# INVITATION_EXPIRATION = 172800
|
# INVITATION_EXPIRATION = 172800
|
||||||
|
|
||||||
[LOGGING]
|
# LOGGING configures the logging output:
|
||||||
# LEVEL can be one value among:
|
# - if unset, everything is logged in the standard output
|
||||||
# DEBUG, INFO, WARNING, ERROR, CRITICAL
|
# the log level is debug if DEBUG is True, else this is INFO
|
||||||
# Defaults to WARNING
|
# - if this is a dictionnary, it is passed to the python dictConfig method:
|
||||||
# LEVEL = "WARNING"
|
# https://docs.python.org/3/library/logging.config.html#logging.config.dictConfig
|
||||||
LEVEL = "DEBUG"
|
# - 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
|
||||||
# The path of the log file. If not set (the default) logs are
|
|
||||||
# written in the standard error output.
|
|
||||||
# PATH = ""
|
|
||||||
|
|
||||||
# [BACKENDS.SQL]
|
# [BACKENDS.SQL]
|
||||||
# The SQL database connection string
|
# The SQL database connection string
|
||||||
|
|
|
@ -67,16 +67,13 @@ ENABLE_REGISTRATION = true
|
||||||
# Defaults to 2 days
|
# Defaults to 2 days
|
||||||
# INVITATION_EXPIRATION = 172800
|
# INVITATION_EXPIRATION = 172800
|
||||||
|
|
||||||
[LOGGING]
|
# LOGGING configures the logging output:
|
||||||
# LEVEL can be one value among:
|
# - if unset, everything is logged in the standard output
|
||||||
# DEBUG, INFO, WARNING, ERROR, CRITICAL
|
# the log level is debug if DEBUG is True, else this is INFO
|
||||||
# Defaults to WARNING
|
# - if this is a dictionnary, it is passed to the python dictConfig method:
|
||||||
# LEVEL = "WARNING"
|
# https://docs.python.org/3/library/logging.config.html#logging.config.dictConfig
|
||||||
LEVEL = "DEBUG"
|
# - 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
|
||||||
# The path of the log file. If not set (the default) logs are
|
|
||||||
# written in the standard error output.
|
|
||||||
# PATH = ""
|
|
||||||
|
|
||||||
# [BACKENDS.SQL]
|
# [BACKENDS.SQL]
|
||||||
# The SQL database connection string
|
# The SQL database connection string
|
||||||
|
|
|
@ -67,16 +67,13 @@ ENABLE_REGISTRATION = true
|
||||||
# Defaults to 2 days
|
# Defaults to 2 days
|
||||||
# INVITATION_EXPIRATION = 172800
|
# INVITATION_EXPIRATION = 172800
|
||||||
|
|
||||||
[LOGGING]
|
# LOGGING configures the logging output:
|
||||||
# LEVEL can be one value among:
|
# - if unset, everything is logged in the standard output
|
||||||
# DEBUG, INFO, WARNING, ERROR, CRITICAL
|
# the log level is debug if DEBUG is True, else this is INFO
|
||||||
# Defaults to WARNING
|
# - if this is a dictionnary, it is passed to the python dictConfig method:
|
||||||
# LEVEL = "WARNING"
|
# https://docs.python.org/3/library/logging.config.html#logging.config.dictConfig
|
||||||
LEVEL = "DEBUG"
|
# - 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
|
||||||
# The path of the log file. If not set (the default) logs are
|
|
||||||
# written in the standard error output.
|
|
||||||
# PATH = ""
|
|
||||||
|
|
||||||
[BACKENDS.SQL]
|
[BACKENDS.SQL]
|
||||||
# The SQL database connection string
|
# The SQL database connection string
|
||||||
|
|
|
@ -67,16 +67,13 @@ ENABLE_REGISTRATION = true
|
||||||
# Defaults to 2 days
|
# Defaults to 2 days
|
||||||
# INVITATION_EXPIRATION = 172800
|
# INVITATION_EXPIRATION = 172800
|
||||||
|
|
||||||
[LOGGING]
|
# LOGGING configures the logging output:
|
||||||
# LEVEL can be one value among:
|
# - if unset, everything is logged in the standard output
|
||||||
# DEBUG, INFO, WARNING, ERROR, CRITICAL
|
# the log level is debug if DEBUG is True, else this is INFO
|
||||||
# Defaults to WARNING
|
# - if this is a dictionnary, it is passed to the python dictConfig method:
|
||||||
# LEVEL = "WARNING"
|
# https://docs.python.org/3/library/logging.config.html#logging.config.dictConfig
|
||||||
LEVEL = "DEBUG"
|
# - 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
|
||||||
# The path of the log file. If not set (the default) logs are
|
|
||||||
# written in the standard error output.
|
|
||||||
# PATH = ""
|
|
||||||
|
|
||||||
# [BACKENDS.SQL]
|
# [BACKENDS.SQL]
|
||||||
# The SQL database connection string
|
# The SQL database connection string
|
||||||
|
|
|
@ -67,16 +67,13 @@ ENABLE_REGISTRATION = true
|
||||||
# Defaults to 2 days
|
# Defaults to 2 days
|
||||||
# INVITATION_EXPIRATION = 172800
|
# INVITATION_EXPIRATION = 172800
|
||||||
|
|
||||||
[LOGGING]
|
# LOGGING configures the logging output:
|
||||||
# LEVEL can be one value among:
|
# - if unset, everything is logged in the standard output
|
||||||
# DEBUG, INFO, WARNING, ERROR, CRITICAL
|
# the log level is debug if DEBUG is True, else this is INFO
|
||||||
# Defaults to WARNING
|
# - if this is a dictionnary, it is passed to the python dictConfig method:
|
||||||
# LEVEL = "WARNING"
|
# https://docs.python.org/3/library/logging.config.html#logging.config.dictConfig
|
||||||
LEVEL = "DEBUG"
|
# - 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
|
||||||
# The path of the log file. If not set (the default) logs are
|
|
||||||
# written in the standard error output.
|
|
||||||
# PATH = ""
|
|
||||||
|
|
||||||
# [BACKENDS.SQL]
|
# [BACKENDS.SQL]
|
||||||
# The SQL database connection string
|
# The SQL database connection string
|
||||||
|
|
|
@ -67,16 +67,13 @@ ENABLE_REGISTRATION = true
|
||||||
# Defaults to 2 days
|
# Defaults to 2 days
|
||||||
# INVITATION_EXPIRATION = 172800
|
# INVITATION_EXPIRATION = 172800
|
||||||
|
|
||||||
[LOGGING]
|
# LOGGING configures the logging output:
|
||||||
# LEVEL can be one value among:
|
# - if unset, everything is logged in the standard output
|
||||||
# DEBUG, INFO, WARNING, ERROR, CRITICAL
|
# the log level is debug if DEBUG is True, else this is INFO
|
||||||
# Defaults to WARNING
|
# - if this is a dictionnary, it is passed to the python dictConfig method:
|
||||||
# LEVEL = "WARNING"
|
# https://docs.python.org/3/library/logging.config.html#logging.config.dictConfig
|
||||||
LEVEL = "DEBUG"
|
# - 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
|
||||||
# The path of the log file. If not set (the default) logs are
|
|
||||||
# written in the standard error output.
|
|
||||||
# PATH = ""
|
|
||||||
|
|
||||||
[BACKENDS.SQL]
|
[BACKENDS.SQL]
|
||||||
# The SQL database connection string
|
# The SQL database connection string
|
||||||
|
|
|
@ -45,12 +45,60 @@ def test_no_configuration():
|
||||||
assert "No configuration file found." in str(exc)
|
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
|
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 = {
|
logging_configuration = {
|
||||||
**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)
|
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()
|
log_content = fd.read()
|
||||||
|
|
||||||
assert "Sending a mail to test@test.com: Test email from" in log_content
|
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 = testclient.get("/firstlogin/temp", status=200)
|
||||||
res.form["csrf_token"] = "invalid"
|
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
|
assert len(smtpd.messages) == 0
|
||||||
u.delete()
|
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):
|
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)
|
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 = res.form
|
||||||
form["csrf_token"] = "foobar"
|
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):
|
def test_end_session_already_disconnected(testclient, backend, user, client, id_token):
|
||||||
|
|
Loading…
Reference in a new issue