├── single_file_app_pattern ├── requirements.txt ├── app.py └── flask_logs.py ├── app_factory_pattern ├── requirements.txt ├── app.py └── log_demo_factory │ ├── public │ ├── __init__.py │ └── views.py │ ├── __init__.py │ ├── extensions.py │ ├── settings.py │ ├── app.py │ └── flask_logs.py ├── README.md └── .gitignore /single_file_app_pattern/requirements.txt: -------------------------------------------------------------------------------- 1 | Flask>=1.0.2 2 | -------------------------------------------------------------------------------- /app_factory_pattern/requirements.txt: -------------------------------------------------------------------------------- 1 | Flask>=1.0.2 2 | environs -------------------------------------------------------------------------------- /app_factory_pattern/app.py: -------------------------------------------------------------------------------- 1 | from log_demo_factory.app import create_app 2 | 3 | app = create_app() 4 | -------------------------------------------------------------------------------- /app_factory_pattern/log_demo_factory/public/__init__.py: -------------------------------------------------------------------------------- 1 | """Public views""" 2 | from . import views 3 | -------------------------------------------------------------------------------- /app_factory_pattern/log_demo_factory/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ Nothing to see here """ 3 | -------------------------------------------------------------------------------- /app_factory_pattern/log_demo_factory/extensions.py: -------------------------------------------------------------------------------- 1 | from log_demo_factory.flask_logs import LogSetup 2 | 3 | logs = LogSetup() 4 | -------------------------------------------------------------------------------- /app_factory_pattern/log_demo_factory/public/views.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, current_app 2 | 3 | bp = Blueprint("public", __name__) 4 | 5 | 6 | @bp.route("/") 7 | def hello_world(): 8 | current_app.logger.info("I'm in a glass case of emotion!") 9 | current_app.logger.warning("") 10 | current_app.logger.error("The lightsabers are _NOT_ helping.") 11 | 12 | return "Hello World!" 13 | -------------------------------------------------------------------------------- /app_factory_pattern/log_demo_factory/settings.py: -------------------------------------------------------------------------------- 1 | from environs import Env, EnvError 2 | 3 | env = Env() 4 | env.read_env() 5 | 6 | # Logging Setup 7 | LOG_TYPE = env.str("LOG_TYPE", "stream") # Default is a Stream handler 8 | LOG_LEVEL = env.str("LOG_LEVEL", "INFO") 9 | 10 | # File Logging Setup 11 | LOG_DIR = env.str("LOG_DIR", "/data/logs") 12 | APP_LOG_NAME = env.str("APP_LOG_NAME", "app.log") 13 | WWW_LOG_NAME = env.str("WWW_LOG_NAME", "www.log") 14 | LOG_MAX_BYTES = env.int("LOG_MAX_BYTES", 100_000_000) # 100MB in bytes 15 | LOG_COPIES = env.int("LOG_COPIES", 5) 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flask Logging Demo 2 | 3 | Supporting code for the Flask Logging demo: 4 | 5 | `git clone .git && cd logging_demo && python3 -m venv venv` 6 | 7 | ## Single Application File Example: 8 | ``` 9 | 1. source venv/bin/activate 10 | 2. cd single_file_app_pattern 11 | 3. pip install -r requirements.txt 12 | 4. flask run 13 | 5. curl http://localhost:5000/ 14 | ``` 15 | 16 | ## Application Factory Pattern Example: 17 | This pattern supports configuration via .env file as well as ENV_VAR 18 | ``` 19 | 1. source venv/bin/activate 20 | 2. cd app_factory_pattern 21 | 3. pip install -r requirements.txt 22 | 4. flask run 23 | 5. curl http://localhost:5000 24 | ``` 25 | -------------------------------------------------------------------------------- /app_factory_pattern/log_demo_factory/app.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from datetime import datetime as dt 3 | 4 | from flask import Flask, request 5 | 6 | from . import public 7 | from .extensions import logs 8 | 9 | 10 | def create_app(config_object="log_demo_factory.settings"): 11 | """Main app factory, runs all the other sections""" 12 | app = Flask(__name__.split(".")[0]) 13 | app.config.from_object(config_object) 14 | register_extensions(app) 15 | register_blueprints(app) 16 | 17 | @app.after_request 18 | def after_request(response): 19 | """ Logging after every request. """ 20 | logger = logging.getLogger("app.access") 21 | logger.info( 22 | "%s [%s] %s %s %s %s %s %s %s", 23 | request.remote_addr, 24 | dt.utcnow().strftime("%d/%b/%Y:%H:%M:%S.%f")[:-3], 25 | request.method, 26 | request.path, 27 | request.scheme, 28 | response.status, 29 | response.content_length, 30 | request.referrer, 31 | request.user_agent, 32 | ) 33 | return response 34 | 35 | return app 36 | 37 | 38 | def register_extensions(app): 39 | logs.init_app(app) 40 | return None 41 | 42 | 43 | def register_blueprints(app): 44 | app.register_blueprint(public.views.bp) 45 | return None 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *.cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # Jupyter Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # SageMath parsed files 79 | *.sage.py 80 | 81 | # dotenv 82 | .env 83 | 84 | # virtualenv 85 | .venv 86 | venv/ 87 | ENV/ 88 | 89 | # Spyder project settings 90 | .spyderproject 91 | .spyproject 92 | 93 | # Rope project settings 94 | .ropeproject 95 | 96 | # mkdocs documentation 97 | /site 98 | 99 | # mypy 100 | .mypy_cache/ 101 | 102 | # My adds 103 | migrations/ 104 | .DS_Store 105 | .idea/ 106 | .vscode/ 107 | *.crt 108 | *.key 109 | -------------------------------------------------------------------------------- /single_file_app_pattern/app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, current_app, request 2 | from flask_logs import LogSetup 3 | from datetime import datetime as dt 4 | import logging 5 | import os 6 | 7 | app = Flask(__name__) 8 | 9 | # Logging Setup - This would usually be stuffed into a settings module 10 | # Default output is a Stream (stdout) handler, also try out "watched" and "file" 11 | app.config["LOG_TYPE"] = os.environ.get("LOG_TYPE", "stream") 12 | app.config["LOG_LEVEL"] = os.environ.get("LOG_LEVEL", "INFO") 13 | 14 | # File Logging Setup 15 | # app.config['LOG_DIR'] = os.environ.get("LOG_DIR", "./") 16 | # app.config['APP_LOG_NAME'] = os.environ.get("APP_LOG_NAME", "app.log") 17 | # app.config['WWW_LOG_NAME'] = os.environ.get("WWW_LOG_NAME", "www.log") 18 | # app.config['LOG_MAX_BYTES'] = os.environ.get("LOG_MAX_BYTES", 100_000_000) # 100MB in bytes 19 | # app.config['LOG_COPIES'] = os.environ.get("LOG_COPIES", 5) 20 | 21 | logs = LogSetup() 22 | logs.init_app(app) 23 | 24 | 25 | @app.after_request 26 | def after_request(response): 27 | """ Logging after every request. """ 28 | logger = logging.getLogger("app.access") 29 | logger.info( 30 | "%s [%s] %s %s %s %s %s %s %s", 31 | request.remote_addr, 32 | dt.utcnow().strftime("%d/%b/%Y:%H:%M:%S.%f")[:-3], 33 | request.method, 34 | request.path, 35 | request.scheme, 36 | response.status, 37 | response.content_length, 38 | request.referrer, 39 | request.user_agent, 40 | ) 41 | return response 42 | 43 | 44 | @app.route("/") 45 | def hello_world(): 46 | current_app.logger.info("Hello There") 47 | current_app.logger.error("") 48 | current_app.logger.warning("General Kenobi") 49 | return "Hello World!" 50 | 51 | 52 | if __name__ == "__main__": 53 | app.run() 54 | -------------------------------------------------------------------------------- /single_file_app_pattern/flask_logs.py: -------------------------------------------------------------------------------- 1 | from logging.config import dictConfig 2 | 3 | """ 4 | We have options in python for stdout (streamhandling) and file logging 5 | File logging has options for a Rotating file based on size or time (daily) 6 | or a watched file, which supports logrotate style rotation 7 | Most of the changes happen in the handlers, lets define a few standards 8 | """ 9 | 10 | 11 | class LogSetup(object): 12 | def __init__(self, app=None, **kwargs): 13 | if app is not None: 14 | self.init_app(app, **kwargs) 15 | 16 | def init_app(self, app): 17 | log_type = app.config["LOG_TYPE"] 18 | logging_level = app.config["LOG_LEVEL"] 19 | if log_type != "stream": 20 | try: 21 | log_directory = app.config["LOG_DIR"] 22 | app_log_file_name = app.config["APP_LOG_NAME"] 23 | access_log_file_name = app.config["WWW_LOG_NAME"] 24 | except KeyError as e: 25 | exit(code="{} is a required parameter for log_type '{}'".format(e, log_type)) 26 | app_log = "/".join([log_directory, app_log_file_name]) 27 | www_log = "/".join([log_directory, access_log_file_name]) 28 | 29 | if log_type == "stream": 30 | logging_policy = "logging.StreamHandler" 31 | elif log_type == "watched": 32 | logging_policy = "logging.handlers.WatchedFileHandler" 33 | else: 34 | log_max_bytes = app.config["LOG_MAX_BYTES"] 35 | log_copies = app.config["LOG_COPIES"] 36 | logging_policy = "logging.handlers.RotatingFileHandler" 37 | 38 | std_format = { 39 | "formatters": { 40 | "default": { 41 | "format": "[%(asctime)s.%(msecs)03d] %(levelname)s %(name)s:%(funcName)s: %(message)s", 42 | "datefmt": "%d/%b/%Y:%H:%M:%S", 43 | }, 44 | "access": {"format": "%(message)s"}, 45 | } 46 | } 47 | std_logger = { 48 | "loggers": { 49 | "": {"level": logging_level, "handlers": ["default"], "propagate": True}, 50 | "app.access": { 51 | "level": logging_level, 52 | "handlers": ["access_logs"], 53 | "propagate": False, 54 | }, 55 | "root": {"level": logging_level, "handlers": ["default"]}, 56 | } 57 | } 58 | if log_type == "stream": 59 | logging_handler = { 60 | "handlers": { 61 | "default": { 62 | "level": logging_level, 63 | "formatter": "default", 64 | "class": logging_policy, 65 | }, 66 | "access_logs": { 67 | "level": logging_level, 68 | "class": logging_policy, 69 | "formatter": "access", 70 | }, 71 | } 72 | } 73 | elif log_type == "watched": 74 | logging_handler = { 75 | "handlers": { 76 | "default": { 77 | "level": logging_level, 78 | "class": logging_policy, 79 | "filename": app_log, 80 | "formatter": "default", 81 | "delay": True, 82 | }, 83 | "access_logs": { 84 | "level": logging_level, 85 | "class": logging_policy, 86 | "filename": www_log, 87 | "formatter": "access", 88 | "delay": True, 89 | }, 90 | } 91 | } 92 | else: 93 | logging_handler = { 94 | "handlers": { 95 | "default": { 96 | "level": logging_level, 97 | "class": logging_policy, 98 | "filename": app_log, 99 | "backupCount": log_copies, 100 | "maxBytes": log_max_bytes, 101 | "formatter": "default", 102 | "delay": True, 103 | }, 104 | "access_logs": { 105 | "level": logging_level, 106 | "class": logging_policy, 107 | "filename": www_log, 108 | "backupCount": log_copies, 109 | "maxBytes": log_max_bytes, 110 | "formatter": "access", 111 | "delay": True, 112 | }, 113 | } 114 | } 115 | 116 | log_config = { 117 | "version": 1, 118 | "formatters": std_format["formatters"], 119 | "loggers": std_logger["loggers"], 120 | "handlers": logging_handler["handlers"], 121 | } 122 | dictConfig(log_config) 123 | -------------------------------------------------------------------------------- /app_factory_pattern/log_demo_factory/flask_logs.py: -------------------------------------------------------------------------------- 1 | from logging.config import dictConfig 2 | 3 | """ 4 | We have options in python for stdout (streamhandling) and file logging 5 | File logging has options for a Rotating file based on size or time (daily) 6 | or a watched file, which supports logrotate style rotation 7 | Most of the changes happen in the handlers, lets define a few standards 8 | """ 9 | 10 | 11 | class LogSetup(object): 12 | def __init__(self, app=None, **kwargs): 13 | if app is not None: 14 | self.init_app(app, **kwargs) 15 | 16 | def init_app(self, app): 17 | log_type = app.config["LOG_TYPE"] 18 | logging_level = app.config["LOG_LEVEL"] 19 | if log_type != "stream": 20 | try: 21 | log_directory = app.config["LOG_DIR"] 22 | app_log_file_name = app.config["APP_LOG_NAME"] 23 | access_log_file_name = app.config["WWW_LOG_NAME"] 24 | except KeyError as e: 25 | exit(code="{} is a required parameter for log_type '{}'".format(e, log_type)) 26 | app_log = "/".join([log_directory, app_log_file_name]) 27 | www_log = "/".join([log_directory, access_log_file_name]) 28 | 29 | if log_type == "stream": 30 | logging_policy = "logging.StreamHandler" 31 | elif log_type == "watched": 32 | logging_policy = "logging.handlers.WatchedFileHandler" 33 | else: 34 | log_max_bytes = app.config["LOG_MAX_BYTES"] 35 | log_copies = app.config["LOG_COPIES"] 36 | logging_policy = "logging.handlers.RotatingFileHandler" 37 | 38 | std_format = { 39 | "formatters": { 40 | "default": { 41 | "format": "[%(asctime)s.%(msecs)03d] %(levelname)s %(name)s:%(funcName)s: %(message)s", 42 | "datefmt": "%d/%b/%Y:%H:%M:%S", 43 | }, 44 | "access": {"format": "%(message)s"}, 45 | } 46 | } 47 | std_logger = { 48 | "loggers": { 49 | "": {"level": logging_level, "handlers": ["default"], "propagate": True}, 50 | "app.access": { 51 | "level": logging_level, 52 | "handlers": ["access_logs"], 53 | "propagate": False, 54 | }, 55 | "root": {"level": logging_level, "handlers": ["default"]}, 56 | } 57 | } 58 | if log_type == "stream": 59 | logging_handler = { 60 | "handlers": { 61 | "default": { 62 | "level": logging_level, 63 | "formatter": "default", 64 | "class": logging_policy, 65 | }, 66 | "access_logs": { 67 | "level": logging_level, 68 | "class": logging_policy, 69 | "formatter": "access", 70 | }, 71 | } 72 | } 73 | elif log_type == "watched": 74 | logging_handler = { 75 | "handlers": { 76 | "default": { 77 | "level": logging_level, 78 | "class": logging_policy, 79 | "filename": app_log, 80 | "formatter": "default", 81 | "delay": True, 82 | }, 83 | "access_logs": { 84 | "level": logging_level, 85 | "class": logging_policy, 86 | "filename": www_log, 87 | "formatter": "access", 88 | "delay": True, 89 | }, 90 | } 91 | } 92 | else: 93 | logging_handler = { 94 | "handlers": { 95 | "default": { 96 | "level": logging_level, 97 | "class": logging_policy, 98 | "filename": app_log, 99 | "backupCount": log_copies, 100 | "maxBytes": log_max_bytes, 101 | "formatter": "default", 102 | "delay": True, 103 | }, 104 | "access_logs": { 105 | "level": logging_level, 106 | "class": logging_policy, 107 | "filename": www_log, 108 | "backupCount": log_copies, 109 | "maxBytes": log_max_bytes, 110 | "formatter": "access", 111 | "delay": True, 112 | }, 113 | } 114 | } 115 | 116 | log_config = { 117 | "version": 1, 118 | "formatters": std_format["formatters"], 119 | "loggers": std_logger["loggers"], 120 | "handlers": logging_handler["handlers"], 121 | } 122 | dictConfig(log_config) 123 | 124 | --------------------------------------------------------------------------------