Issue
Has anyone figured out how to stop the following Dash log messages? They clutter up my logs, and on a busy website, make it almost impossible to see actual, useful log messages when errors occur.
[17/Nov/2023:16:28:10 +0000] "POST /dash/_dash-update-component HTTP/1.1" 204 0 "https://example.com/" "Mozilla/5.0 (iPhone; CPU iPhone OS 16_6_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1"
I've tried the suggestion here by the creator of Dash. He says to try the following, but it doesn't do anything for me:
import logging
logging.getLogger('werkzeug').setLevel(logging.ERROR)
Here's a fuller example from this link if you want to try it:
import logging
from dash import Dash
from flask import Flask
logging.getLogger('werkzeug').setLevel(logging.ERROR)
URL_BASE_PATHNAME = '/'+'example/'
server = Flask(__name__)
app = Dash(name=__name__, server=server,
url_base_pathname=URL_BASE_PATHNAME)
if __name__ == "__main__":
app.run()
Here's more like what mine looks like in production with Docker Swarm:
import logging
import os
import time
from datetime import datetime, timezone
from logging.handlers import RotatingFileHandler
from pathlib import Path
from typing import List
from dotenv import load_dotenv
from flask import Flask, current_app, redirect, render_template, request, url_for
from flask.globals import _request_ctx_stack
from flask.logging import default_handler
from flask_assets import Environment
from flask_bcrypt import Bcrypt
from flask_bootstrap import Bootstrap
from flask_caching import Cache
from flask_flatpages import FlatPages
from flask_htmlmin import HTMLMIN as HTMLMin
from flask_login import LoginManager, current_user
from flask_mail import Mail
from flask_migrate import Migrate
from flask_sqlalchemy import SQLAlchemy
from werkzeug.exceptions import NotFound
from werkzeug.middleware.proxy_fix import ProxyFix
from app import databases
from app.assets import compile_assets
# Dictionary pointing to classes of configs
from app.config import (
INSTANCE_PATH,
PROJECT_FOLDER,
ROLE_ID_CUSTOMER_ADMIN,
ROLE_ID_IJACK_ADMIN,
ROLE_ID_IJACK_SERVICE,
STATIC_FOLDER,
TEMPLATE_FOLDER,
app_config,
)
from app.dash_setup import register_dashapps
from app.utils import error_send_email_w_details
# Ensure the .env file doesn't contain copies of the variables in the .flaskenv file, or it'll get confusing...
load_dotenv(PROJECT_FOLDER, override=True)
# Set log level globally so other modules can import it
log_level = None
db = SQLAlchemy()
login_manager = LoginManager()
bcrypt = Bcrypt()
cache = Cache()
mail = Mail()
pages = FlatPages()
assets_env = Environment()
# noqa: C901
def create_app(config_name=None):
"""Factory function that creates the Flask app"""
app = Flask(
__name__,
instance_path=INSTANCE_PATH,
static_folder=STATIC_FOLDER,
template_folder=TEMPLATE_FOLDER,
static_url_path="/static",
)
# Import the config class from config.py (defaults to 'development' if not in the .env file)
if config_name is None:
config_name = os.getenv("FLASK_CONFIG", "development")
config_obj = app_config[config_name]
app.config.from_object(config_obj)
app.config["SECRET_KEY"] = os.getenv("SECRET_KEY")
# Set up logging
global log_level
log_level = app.config.get("LOG_LEVEL", logging.INFO)
app.logger.setLevel(log_level)
# The default handler is a StreamHandler that writes to sys.stderr at DEBUG level.
default_handler.setLevel(log_level)
# Change default log format
log_format = (
"[%(asctime)s] %(levelname)s: %(name)s: %(module)s: %(funcName)s: %(message)s"
)
default_handler.setFormatter(logging.Formatter(log_format))
# Stop the useless 'dash-component-update' logging? (Unfortunately this doesn't seem to work...)
# https://community.plotly.com/t/prevent-post-dash-update-component-http-1-1-messages/11132
# https://community.plotly.com/t/suppressing-component-update-output-message-in-the-terminal/7613
logging.getLogger("werkzeug").setLevel(logging.ERROR)
# NOTE != means running the Flask application through Gunicorn in my workflow.
if __name__ != "__main__" and not app.debug and app.env != "development":
# Add a FileHandler to the Flask logger
Path("logs").mkdir(exist_ok=True)
file_handler = RotatingFileHandler(
"logs/myijack.log", maxBytes=10240, backupCount=10
)
file_handler.setLevel(logging.ERROR)
file_handler.setFormatter(logging.Formatter(log_format))
app.logger.addHandler(file_handler)
app.logger.error(
"Just testing Gunicorn logging in Docker Swarm service container ✅..."
)
app.logger.info("myijack.com startup now...")
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_port=1)
# Initialize extensions
Bootstrap(app)
db.init_app(app) # SQLAlchemy
databases.init_app(app) # other custom database functions
cache.init_app(
app, config=config_obj.cache_config
) # Simple if dev, otherwise Redis for test/prod
login_manager.init_app(app)
Migrate(app, db)
mail.init_app(app)
bcrypt.init_app(app)
pages.init_app(app)
# By default, when a user attempts to access a login_required view without being logged in,
# Flask-Login will flash a message and redirect them to the log in view.
# (If the login view is not set, it will abort with a 401 error.)
login_manager.login_view = "auth.login"
# login_manager.login_message = "You must be logged in to access this page."
# Register blueprints
if ALL0_DASH1_FLASK2_ADMIN3 in (0, 2):
pass
app.logger.debug("Importing blueprint views...")
from app.auth.oauth import azure_bp, github_bp, google_bp
from app.auth.views import auth as auth_bp
from app.dashapp.views import dash_bp
from app.home.views import home as home_bp
from app.pwa import pwa_bp
app.logger.debug("Registering blueprint views...")
app.register_blueprint(auth_bp)
app.register_blueprint(home_bp)
app.register_blueprint(pwa_bp)
app.register_blueprint(dash_bp)
app.register_blueprint(github_bp, url_prefix="/login")
app.register_blueprint(azure_bp, url_prefix="/login")
app.register_blueprint(google_bp, url_prefix="/login")
# Register API for saving Flask-Admin views' metadata via JavaScript AJAX
from app.api import api
api.init_app(app)
if ALL0_DASH1_FLASK2_ADMIN3 in (0, 3):
pass
# Setup Flask-Admin site
app.logger.debug("Importing Flask-Admin views...")
from app.flask_admin.views_admin import admin_views
from app.flask_admin.views_admin_cust import admin_cust_views
app.logger.debug("Adding Flask-Admin views...")
admin_views(app, db)
admin_cust_views(app, db)
with app.app_context():
# Flask-Assets must come before the Dash app so it
# can first render the {% assets %} blocks
assets_env.init_app(app)
compile_assets(assets_env, app)
# HTMLMin must come after Dash for some reason...
# app.logger.debug("Registering HTMLMin...")
app.config["MINIFY_HTML"] = True
HTMLMin(
app,
remove_comments=True,
remove_empty_space=True,
# This one can cause a bug...
# disable_css_min=False,
)
return app, dash_app
The issue has been on Github since 2018 and apparently it's closed/fixed, but not for me...
I'm using the following pyproject.toml
in production:
[tool.poetry.dependencies]
python = ">=3.8,<3.12"
dash = {extras = ["compress"], version = "^2.11.1"}
scikit-learn = "1.1.3"
pandas = "^1.5.3"
flask-login = "^0.5.0"
keras = "^2.4.3"
joblib = "^1.2.0"
boto3 = "^1.26.12"
click = "^8.1.3"
dash-bootstrap-components = "^1.4.2"
dash-table = "^5.0.0"
flask-caching = "2.0.1"
flask-migrate = "^2.5.3"
flask-sqlalchemy = "^2.4.4"
flask-testing = "^0.8.0"
gevent = "^22.10.2"
greenlet = "^2.0.1"
gunicorn = "^20.0.4"
python-dotenv = "^0.19.2"
python-dateutil = "^2.8.1"
requests = "^2.24.0"
email_validator = "^1.1.1"
flask-redis = "^0.4.0"
numexpr = "^2.7.1"
flask-mail = "^0.9.1"
python-jose = "^3.3.0"
sqlalchemy = "^1.3"
Flask-FlatPages = "^0.7.2"
flask-bootstrap4 = "^4.0.2"
colour = "^0.1.5"
tenacity = "^6.3.1"
psycopg2-binary = "^2.8.6"
twilio = "^6.54.0"
openpyxl = "^3.0.7"
phonenumbers = "^8.12.29"
celery = "^5.1.2"
flower = "^1.0.0"
Flask-Assets = "^2.0"
webassets = "^2.0"
cssmin = "^0.2.0"
rjsmin = "^1.2.0"
Flask-HTMLmin = "^2.2.0"
ipinfo = "^4.2.1"
dash-mantine-components = "^0.12.1"
Flask = "^2.1.2"
Flask-Bcrypt = "^1.0.1"
Werkzeug = "2.0.3"
Flask-WTF = "^1.0.1"
flask-restx = "^0.5.1"
flask-admin-plus = "^1.6.18"
Pillow = "^9.2.0"
multidict = "^6.0.2"
gcld3 = "^3.0.13"
plotly = "^5.14.1"
flask-dance = "^7.0.0"
blinker = "^1.6.2"
[build-system]
requires = ["poetry>=0.12"]
build-backend = "poetry.masonry.api"
Here's my gunicorn.conf.py
:
# -*- encoding: utf-8 -*-
bind = "0.0.0.0:5005"
# The Access log file to write to, same as --access-logfile
# Using default "-" makes gunicorn log to stdout - perfect for Docker
accesslog = "-"
# Same as --log-file or --error-logfile. Default "-" goes to stderr for Docker.
errorlog = "-"
# We overwrite the below loglevel in __init__.py
# loglevel = "info"
# Redirect stdout/stderr to specified file in errorlog
capture_output = True
enable_stdio_inheritance = True
# gevent setup
# workers = 4 # 4 threads (2 per CPU)
# threads = 2 # 2 CPUs
# Typically Docker handles the number of workers, not Gunicorn
workers = 1
threads = 2
worker_class = "gevent"
# The maximum number of simultaneous clients.
# This setting only affects the Eventlet and Gevent worker types.
worker_connections = 20
# Timeout in seconds (default is 30)
timeout = 30
# Directory to use for the worker heartbeat temporary file.
# Use an in-memory filesystem to avoid hanging.
# In AWS an EBS root instance volume may sometimes hang for half a minute
# and during this time Gunicorn workers may completely block.
# https://docs.gunicorn.org/en/stable/faq.html#blocking-os-fchmod
worker_tmp_dir = "/dev/shm"
Here's the Dockerfile I'm using in production:
# Builder stage ############################################################################
# Build args available during build, but not when container runs.
# They can have default values, and can be passed in at build time.
ARG ENVIRONMENT=production
FROM python:3.8.15-slim-buster AS builder
ARG POETRY_VERSION=1.2.2
# Use Docker BuildKit for better caching and faster builds
ARG DOCKER_BUILDKIT=1
ARG BUILDKIT_INLINE_CACHE=1
# Enable BuildKit for Docker-Compose
ARG COMPOSE_DOCKER_CLI_BUILD=1
# Python package installation stuff
ARG PIP_NO_CACHE_DIR=1
ARG PIP_DISABLE_PIP_VERSION_CHECK=1
ARG PIP_DEFAULT_TIMEOUT=100
# Don't write .pyc bytecode
ENV PYTHONDONTWRITEBYTECODE=1
# Don't buffer stdout. Write it immediately to the Docker log
ENV PYTHONUNBUFFERED=1
ENV PYTHONFAULTHANDLER=1
ENV PYTHONHASHSEED=random
# Tell apt-get we're never going to be able to give manual feedback:
ENV DEBIAN_FRONTEND=noninteractive
WORKDIR /project
RUN apt-get update && \
apt-get install -y --no-install-recommends gcc redis-server libpq-dev sass \
g++ protobuf-compiler libprotobuf-dev && \
# Clean up
apt-get autoremove -y && \
apt-get clean -y && \
rm -rf /var/lib/apt/lists/*
# The following only runs in the "builder" build stage of this multi-stage build.
RUN pip3 install "poetry==$POETRY_VERSION" && \
# Use a virtual environment for easy transfer of builder packages
python -m venv /venv && \
/venv/bin/pip install --upgrade pip wheel
# Poetry exports the requirements to stdout in a "requirements.txt" file format,
# and pip installs them in the /venv virtual environment. We need to copy in both
# pyproject.toml AND poetry.lock for this to work!
COPY pyproject.toml poetry.lock ./
RUN poetry config virtualenvs.create false && \
poetry export --no-interaction --no-ansi --without-hashes --format requirements.txt \
$(test "$ENVIRONMENT" != "production" && echo "--with dev") \
| /venv/bin/pip install -r /dev/stdin
# Make sure our packages are in the PATH
ENV PATH="/project/node_modules/.bin:$PATH"
ENV PATH="/venv/bin:$PATH"
COPY wsgi.py gunicorn.conf.py .env .flaskenv entrypoint.sh postcss.config.js ./
COPY assets assets
COPY app app
RUN echo "Building flask assets..." && \
# Flask assets "clean" command may fail, in which case just run "build"
flask assets clean || true && \
flask assets build
# Final stage of multi-stage build ############################################################
FROM python:3.8.15-slim-buster as production
# For setting up the non-root user in the container
ARG USERNAME=user
ARG USER_UID=1000
ARG USER_GID=$USER_UID
# Use Docker BuildKit for better caching and faster builds
ARG DOCKER_BUILDKIT=1
ARG BUILDKIT_INLINE_CACHE=1
# Enable BuildKit for Docker-Compose
ARG COMPOSE_DOCKER_CLI_BUILD=1
# Don't write .pyc bytecode
ENV PYTHONDONTWRITEBYTECODE=1
# Don't buffer stdout. Write it immediately to the Docker log
ENV PYTHONUNBUFFERED=1
ENV PYTHONFAULTHANDLER=1
ENV PYTHONHASHSEED=random
# Tell apt-get we're never going to be able to give manual feedback:
ENV DEBIAN_FRONTEND=noninteractive
# Add a new non-root user and change ownership of the workdir
RUN addgroup --gid $USER_GID --system $USERNAME && \
adduser --no-create-home --shell /bin/false --disabled-password --uid $USER_UID --system --group $USERNAME && \
# Get curl and netcat for Docker healthcheck
apt-get update && \
apt-get -y --no-install-recommends install nano curl netcat g++ && \
apt-get clean && \
# Delete index files we don't need anymore:
rm -rf /var/lib/apt/lists/*
WORKDIR /project
# Make the logs directory writable by the non-root user
RUN mkdir -p /project/logs && \
chown -R $USER_UID:$USER_GID /project/logs
# Copy in files and change ownership to the non-root user
COPY --chown=$USER_UID:$USER_GID --from=builder /venv /venv
# COPY --chown=$USER_UID:$USER_GID --from=builder /node_modules /node_modules
COPY --chown=$USER_UID:$USER_GID --from=builder /project/assets assets
COPY --chown=$USER_UID:$USER_GID app app
COPY --chown=$USER_UID:$USER_GID tests tests
COPY --chown=$USER_UID:$USER_GID wsgi.py gunicorn.conf.py .env .flaskenv entrypoint.sh ./
# Set the user so nobody can run as root on the Docker host (security)
USER $USERNAME
# Just a reminder of which port is needed in gunicorn.conf.py (in-container, in production)
# EXPOSE 5005
# Make sure we use the virtualenv
ENV PATH="/venv/bin:$PATH"
RUN echo PATH = $PATH
CMD ["/bin/bash", "/project/entrypoint.sh"]
My entrypoint.sh
file starts everything as follows:
#!/bin/bash
# Enable exit on non 0
set -euo pipefail
# Finally, start the Gunicorn app server for the Flask app.
# All config options are in the gunicorn.conf.py file.
echo "Starting Gunicorn with gunicorn.conf.py configuration..."
gunicorn --config /project/gunicorn.conf.py wsgi:app
Here's the wsgi.py
file to which the above entrypoint.sh
is referring:
print("Starting: importing app and packages...")
try:
from app import cli, create_app, db
except Exception as err:
print(f"ERROR: {err}")
print("ERROR: Unable to import cli, create_app, and db from app. Exiting...")
exit(1)
print("Creating app...")
try:
app, _ = create_app()
cli.register(app)
except Exception as err:
print(f"ERROR: {err}")
print("ERROR: Unable to create app. Exiting...")
exit(1)
print("App is ready ✅")
UPDATE Nov 20, 2023:
I've added the following to the code, and it still outputs the useless Dash POST /dash/_dash-update-component
logs...
gunicorn_logger = logging.getLogger("gunicorn.error")
gunicorn_logger.setLevel(logging.ERROR)
dash_logger = logging.getLogger("dash")
dash_logger.setLevel(logging.ERROR)
Solution
I finally solved the problem, I think. As @EricLavault suggested, it was a Gunicorn problem, not a Dash/Flask logging problem. What I was seeing were the "access logs", which looked like this:
[17/Nov/2023:16:28:10 +0000] "POST /dash/_dash-update-component HTTP/1.1" 204 0 "https://example.com/" "Mozilla/5.0 (iPhone; CPU iPhone OS 16_6_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1"
To remove the "access logs" from my Docker log output in production, I just changed my gunicorn.conf.py
settings to the following, where accesslog = None
is the key line:
accesslog = None
errorlog = "-"
loglevel = "error"
Answered By - Sean McCarthy
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.