diff --git a/CHANGES.rst b/CHANGES.rst
index 1bde369c..28b6f8d7 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -3,6 +3,11 @@ All notable changes to this project will be documented in this file.
The format is based on `Keep a Changelog `_,
and this project adheres to `Semantic Versioning `_.
+Changed
+*******
+
+- Use default python logging configuration format. :issue:`188` :pr:`165`
+
[0.0.42] - 2023-12-29
=====================
diff --git a/canaille/__init__.py b/canaille/__init__.py
index 4a6c4619..204b4d61 100644
--- a/canaille/__init__.py
+++ b/canaille/__init__.py
@@ -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,36 +26,38 @@ 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,
+ "formatters": {
+ "default": {
+ "format": "[%(asctime)s] %(levelname)s in %(module)s: %(message)s",
+ }
+ },
+ "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,
+ }
+ )
- dictConfig(
- {
- "version": 1,
- "formatters": {
- "default": {
- "format": "[%(asctime)s] %(levelname)s in %(module)s: %(message)s",
- }
- },
- "handlers": {"wsgi": handler},
- "root": {"level": log_level, "handlers": ["wsgi"]},
- "loggers": {
- "faker": {"level": "WARNING"},
- },
- "disable_existing_loggers": False,
- }
- )
+ elif isinstance(conf, dict):
+ dictConfig(conf)
+
+ else:
+ fileConfig(conf, disable_existing_loggers=False)
def setup_jinja(app):
diff --git a/canaille/config.sample.toml b/canaille/config.sample.toml
index bd0f85b9..133894ad 100644
--- a/canaille/config.sample.toml
+++ b/canaille/config.sample.toml
@@ -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
diff --git a/demo/conf-docker/canaille-ldap.toml b/demo/conf-docker/canaille-ldap.toml
index 9efe8155..756a0253 100644
--- a/demo/conf-docker/canaille-ldap.toml
+++ b/demo/conf-docker/canaille-ldap.toml
@@ -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
diff --git a/demo/conf-docker/canaille-memory.toml b/demo/conf-docker/canaille-memory.toml
index 37e3ad19..fbfe4a92 100644
--- a/demo/conf-docker/canaille-memory.toml
+++ b/demo/conf-docker/canaille-memory.toml
@@ -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
diff --git a/demo/conf-docker/canaille-sql.toml b/demo/conf-docker/canaille-sql.toml
index 8620cc0b..c787f861 100644
--- a/demo/conf-docker/canaille-sql.toml
+++ b/demo/conf-docker/canaille-sql.toml
@@ -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
diff --git a/demo/conf/canaille-ldap.toml b/demo/conf/canaille-ldap.toml
index 14054f42..04cfcec9 100644
--- a/demo/conf/canaille-ldap.toml
+++ b/demo/conf/canaille-ldap.toml
@@ -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
diff --git a/demo/conf/canaille-memory.toml b/demo/conf/canaille-memory.toml
index 616f3d3d..0e6f14ec 100644
--- a/demo/conf/canaille-memory.toml
+++ b/demo/conf/canaille-memory.toml
@@ -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
diff --git a/demo/conf/canaille-sql.toml b/demo/conf/canaille-sql.toml
index 6f24bf81..3756fffd 100644
--- a/demo/conf/canaille-sql.toml
+++ b/demo/conf/canaille-sql.toml
@@ -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
diff --git a/tests/app/test_flaskutils.py b/tests/app/test_flaskutils.py
index b8563f96..b3e2d61c 100644
--- a/tests/app/test_flaskutils.py
+++ b/tests/app/test_flaskutils.py
@@ -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
+"""
diff --git a/tests/core/test_account.py b/tests/core/test_account.py
index fa7aae11..e2f83068 100644
--- a/tests/core/test_account.py
+++ b/tests/core/test_account.py
@@ -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()
diff --git a/tests/core/test_profile_settings.py b/tests/core/test_profile_settings.py
index 456c9a20..39f886c2 100644
--- a/tests/core/test_profile_settings.py
+++ b/tests/core/test_profile_settings.py
@@ -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)
diff --git a/tests/oidc/test_end_session.py b/tests/oidc/test_end_session.py
index 192e3883..5ea9e3ba 100644
--- a/tests/oidc/test_end_session.py
+++ b/tests/oidc/test_end_session.py
@@ -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):