feat: usedefault python logging configuration format

This commit is contained in:
Éloi Rivard 2024-03-15 19:55:12 +01:00
parent 4edffcaa9f
commit dc81832159
No known key found for this signature in database
GPG key ID: 7EDA204EA57DD184
13 changed files with 171 additions and 104 deletions

View file

@ -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
===================== =====================

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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
"""

View file

@ -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()

View file

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

View file

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