Created on 25 Jan 2021 ;    Modified on 25 Jan 2021

How create a minimal flask project (part 5: instance folder and log)


This is the fifth part of an article about Flask, as follows:

Objective

This is the fifth part of a Flask project to show a single page in two different versions:

  • single language;
  • two languages.

Here we speak how create an instance folder and how to log messages.

Instance folder is useful to write and to lay files that change in different instances of the same application. E.g. configuration files, data files, log files, ...

Instance folder and its contents must not be stored in version control system used to develop the application.

Methodology

First we modify our application factory function to instruct Flask to use instance folder.

After that, we are going to configure and to start the Python logging library.

Development

We modify the factory function create_app in single_page/__init__.py: calling Flask, we use the parameter instance_relative_config to tell it to refer to instance folder and the folder is relative to project (alias instance) folder.

 1  import os                                                      # +
 2 
 3  from flask import Flask
 4  from flask_babel   import Babel
 5  from flask_sitemap import Sitemap
 6 
 7  babel = Babel()
 8  sitemap = Sitemap()
 9 
10  def create_app():
11      '''create and configure the app'''
12      app = Flask(__name__, instance_relative_config=True)        # +-
13 
14      # ensure the instance folder exists
15      if not os.path.exists(app.instance_path):                   # +
16          os.makedirs(app.instance_path)
17 
18      app.config.from_mapping(
19          SECRET_KEY='leave-hope-to-enter',
20          LANGUAGES = {'en': 'english', 'it': 'italiano',},
21      )
22 
23      from .oneel import views as views1
24      app.register_blueprint(views1.oneel)
25 
26      from .twoels import views as views2
27      app.register_blueprint(views2.twoels)
28 
29      babel.init_app(app)
30      sitemap.init_app(app)
31 
32      @app.route('/sitemap')
33      def ep_sitemap():                            # endpoint for sitemap
34          return sitemap.sitemap(), 200, {'Content-Type': 'text/xml', }
35 
36      return app

Please note we check for folder existence and we create it if it isn't: no magic here!

And now, in single_page/__init__.py we add:

  • a group of configuration parameters we are going to use running the python's log library;
  • configuration parameters start to get quite a lot, so we group them in function set_config;
  • similarly we put the initializazion of the python's log library in the function set_logger.
 1  import os
 2  import logging                                           # +
 3  from   logging.handlers import RotatingFileHandler       # +
 4 
 5  # 3rd parties libs import
 6  from flask import Flask
 7  from flask_babel   import Babel
 8  from flask_sitemap import Sitemap
 9 
10  babel = Babel()
11  sitemap = Sitemap()
12 
13  def create_app():
14      '''create and configure the app'''
15      app = Flask(__name__, instance_relative_config=True)
16 
17      # ensure the instance folder exists
18      if not os.path.exists(app.instance_path):
19          os.makedirs(app.instance_path)
20 
21      set_config(app)                                      # +
22      set_logger(app)                                      # +
23 
24      from .oneel import views as views1
25      app.register_blueprint(views1.oneel)
26 
27      from .twoels import views as views2
28      app.register_blueprint(views2.twoels)
29 
30      babel.init_app(app)
31      sitemap.init_app(app)
32 
33      @app.route('/sitemap')
34      def ep_sitemap():                            # endpoint for sitemap
35          return sitemap.sitemap(), 200, {'Content-Type': 'text/xml', }
36 
37      return app
38 
39 
40  def set_config(app):                                     # +
41      '''setting configuration '''
42      app.config.from_mapping(
43          SECRET_KEY='leave-hope-to-enter',
44          LANGUAGES = {'en': 'english', 'it': 'italiano',},
45          LOG = {
46                  'DIR' : os.path.join(app.instance_path, 'logs'),
47                  'FILE': os.path.join(app.instance_path, 'logs/covid.log'),
48                  'BUFSIZE': 102400,
49                  'FILE_HANDLER_LEVEL': logging.DEBUG,
50                  'APP_LOGGER_LEVEL'  : logging.DEBUG,
51                }
52      )
53 
54 
55  def set_logger(app):                                     # +
56      '''setting logger'''
57      # ensure the log folder exists
58      if not os.path.exists(app.config['LOG']['DIR']):
59          os.mkdir(app.config['LOG']['DIR'])
60      file_handler = RotatingFileHandler(app.config['LOG']['FILE'],
61                                         maxBytes=app.config['LOG']['BUFSIZE'],
62                                         backupCount=10)
63      file_handler.setFormatter(logging.Formatter(
64          '%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]'))
65      file_handler.setLevel(app.config['LOG']['FILE_HANDLER_LEVEL'])
66      app.logger.addHandler(file_handler)
67 
68      app.logger.setLevel(app.config['LOG']['APP_LOGGER_LEVEL'])
69      app.logger.debug('Covid application starts')

We can see:

  • we use a rotating file handler: our log file will start again after 100 KByte;
  • this file will be instance/logs/covid.log;
  • our log level is logging.DEBUG (it means we'll write all messages, from the lower level up to the top);
  • the log record contains: time of record, its level, message, module and line requesting the log;
  • we need to create the instance/log folder if it does not exist.

So our directory structure become:

▼ flask_single_page
    run.py
    babel.cfg
    ▼ instance                   # + instance folder
        ▼ log                    # + next step: to store log files
    ▼ single_page
        __init__.py
        ▼ oneel
            __init__.py
            views.py
            ▼ static
                ▼ css
                    style.css
            ▼ templates
                base.html
                index.html
        ▼ twoels
            __init__.py
            views.py
            ▼ static
                ▼ css
                    style.css
            ▼ templates
                2lbase.html
                2lindex.html
    ▼ docs
    ▼ tests
    ▼ venv

To log we need a call to our application logger. E.g. if we wish track the enter in views we can modify them as follow:

 1  from datetime import datetime
 2  from flask import Blueprint, render_template, current_app       # +-
 3  from single_page import sitemap
 4 
 5  # this app will respond to srv/1l/... URLs
 6  oneel = Blueprint('oneel',
 7                    __name__,
 8                    static_folder='static',
 9                    template_folder='templates',
10                    url_prefix='/1l')
11 
12  @oneel.route('/')                 # index URLs
13  @oneel.route('/index')
14  @oneel.route('/index.html')
15  def index():
16      current_app.logger.debug('> index')                         # +
17      return render_template('index.html', title='single language title')
18 
19 
20  @sitemap.register_generator
21  def index():
22      '''generate URLs using language codes
23 
24         Note. used by flask-sitemap
25      '''
26      yield 'oneel.index', {}, datetime.now(), 'monthly', 0.7

The call to current_app.logger.debug in function index will log our entrance in the function body.

An example of log file follows:

 1  2021-01-25 11:34:12,645 DEBUG: Covid application starts [in C:\Dati\Studio\Sviluppi\flask_single_page\single_page\__init__.py:69]
 2  2021-01-25 11:42:51,267 DEBUG: Covid application starts [in C:\Dati\Studio\Sviluppi\flask_single_page\single_page\__init__.py:69]
 3  2021-01-25 11:45:23,935 DEBUG: > index [in C:\Dati\Studio\Sviluppi\flask_single_page\single_page\oneel\views.py:23]
 4  2021-01-25 11:45:38,915 DEBUG: > index [in C:\Dati\Studio\Sviluppi\flask_single_page\single_page\oneel\views.py:23]
 5  2021-01-25 11:46:08,656 DEBUG: > index [in C:\Dati\Studio\Sviluppi\flask_single_page\single_page\twoels\views.py:38]
 6  2021-01-25 11:46:15,718 ERROR: Exception on /de/2l/ [GET] [in C:\Dati\Studio\Sviluppi\flask_single_page\venv\lib\site-packages\flask\app.py:1891]
 7  Traceback (most recent call last):
 8    File "C:\Dati\Studio\Sviluppi\flask_single_page\venv\lib\site-packages\flask\app.py", line 2447, in wsgi_app
 9      response = self.full_dispatch_request()
10    File "C:\Dati\Studio\Sviluppi\flask_single_page\venv\lib\site-packages\flask\app.py", line 1952, in full_dispatch_request
11      rv = self.handle_user_exception(e)
12    File "C:\Dati\Studio\Sviluppi\flask_single_page\venv\lib\site-packages\flask\app.py", line 1821, in handle_user_exception
13      reraise(exc_type, exc_value, tb)
14    File "C:\Dati\Studio\Sviluppi\flask_single_page\venv\lib\site-packages\flask\_compat.py", line 39, in reraise
15      raise value
16    File "C:\Dati\Studio\Sviluppi\flask_single_page\venv\lib\site-packages\flask\app.py", line 1948, in full_dispatch_request
17      rv = self.preprocess_request()
18    File "C:\Dati\Studio\Sviluppi\flask_single_page\venv\lib\site-packages\flask\app.py", line 2236, in preprocess_request
19      func(request.endpoint, request.view_args)
20    File "C:\Dati\Studio\Sviluppi\flask_single_page\single_page\twoels\views.py", line 31, in pull_lang_code
21      raise ValueError(f"language {lc} is not accepted, because not in {tuple(current_app.config['LANGUAGES'].keys())}")
22  ValueError: language de is not accepted, because not in ('en', 'it')

Lines 1 and 2 show the start of the application. Lines from 3 to 5 show use of the index view in oneel and twoels apps.

From line 6 downward we have a stack trace due to an error calling url http://localhost:5000/de/2l/ because language de is not configured in our application. Note. This trace is not called explicity from our application. We raise a ValueError signal; Flask forward it also to a log error in addition to writing it to console.

Enjoy, ldfa