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):