Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Log request issues #111

Merged
merged 27 commits into from
Apr 12, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
3927384
Get 'structlog' running
Dec 6, 2023
77b9f74
Get rid of _from_structlog and _record keys
Dec 6, 2023
778f340
Merge branch 'main' into logbad
Dec 20, 2023
9a7a039
Get request logs working!
Dec 22, 2023
1840932
Add a basic test of the structured logger
Jan 10, 2024
d977702
Test log output for nonexistent sequence
Jan 10, 2024
26b5608
Merge branch 'main' into logbad
Apr 10, 2024
6f576a6
Remove old logging setup code
Apr 11, 2024
0c074b9
Make Flask dependencies go missing again
Apr 11, 2024
570962d
Actually store response dump in log
Apr 11, 2024
8893a52
Start logging documentation
Apr 11, 2024
9f4048e
Update log docs and adjust log size
Apr 12, 2024
fa7c496
Add log folders to .gitignore
Apr 12, 2024
55930cd
Explain how to write logs
Apr 12, 2024
4b87020
Include logged response dump in nonexistent sequence test
Apr 12, 2024
4c49fec
Document optional stuff in abstract endpoint test
Apr 12, 2024
d886d4e
Drop unused code
Apr 12, 2024
9929fb8
Mention tests that involve the logging system
Apr 12, 2024
7393899
Explain how to manually generate a log entry
Apr 12, 2024
e952f91
Correct typo in comment
Apr 12, 2024
737eabb
Ensure that `logs` directory is always present
Apr 12, 2024
70db035
doc: Indicate how to find backscope logs on the production server
gwhitney Apr 12, 2024
bf6bb5e
doc: Remove warning that get_commit is not working.
gwhitney Apr 12, 2024
2de4b2b
Explain where the structured logger is kept
Apr 12, 2024
854304b
Merge branch 'logbad' of https://github.com/Vectornaut/backscope into…
Apr 12, 2024
aa5db13
Fix up formatting
Apr 12, 2024
b9d6b17
Improve import ordering; add final newlines
Apr 12, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
151 changes: 131 additions & 20 deletions flaskr/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,19 @@
"""

import os
from flask import Flask, current_app
import click
import logging
import structlog
from logging import StreamHandler
from logging.handlers import RotatingFileHandler
from structlog.processors import JSONRenderer, TimeStamper
from structlog.stdlib import ProcessorFormatter
from structlog.dev import ConsoleRenderer
from flask import Flask, current_app
from flask.cli import with_appcontext
from flask_cors import CORS
from flask_migrate import Migrate
from flask_sqlalchemy import SQLAlchemy
import logging
from logging.handlers import RotatingFileHandler
import sys

from dotenv import load_dotenv

gwhitney marked this conversation as resolved.
Show resolved Hide resolved
Expand All @@ -29,6 +33,76 @@ class EnvironmentException(Exception):
# Create a new sql alchemy database object
db = SQLAlchemy()

# --- Log setup utilities ---

# We definitely want the log file to include bad request warnings, so the log
# level has to be at least that low
file_log_level = logging.WARNING

# If we're in development, the console log level is either DEBUG or INFO,
# depending on whether debugging is turned on. If we're not in development,
# there's no console log, so the console log level is effectively infinite
def console_log_level(environment):
return (
(
logging.DEBUG if config[environment].DEBUG else
logging.INFO
) if config[environment].DEVELOPMENT else
float('inf')
)

def create_file_handler():
# Create a file handler, which writes to rotating files api.log, api.log.1, ..., api.log.5
handler = RotatingFileHandler('api.log', maxBytes=10**7, backupCount=5)

# Add a JSON formatter
gwhitney marked this conversation as resolved.
Show resolved Hide resolved
formatter = ProcessorFormatter(
processors = [
ProcessorFormatter.remove_processors_meta,
TimeStamper(fmt='iso', utc=False),
JSONRenderer(sort_keys=True)
]
)
handler.setFormatter(formatter)

# Set log level
handler.setLevel(file_log_level)

return handler

def create_console_handler(environment):
# Create a stream handler that writes to stderr
handler = StreamHandler()

# Add a console formatter
formatter = ProcessorFormatter(
processors = [
ProcessorFormatter.remove_processors_meta,
TimeStamper(fmt='iso', utc=False),
ConsoleRenderer()
]
)
handler.setFormatter(formatter)

# Print everything, or almost everything, in the console
handler.setLevel(console_log_level(environment))

return handler

# This factory calls up the logger that will be stored in current_app.logger
##class BackscopeLoggerFactory(structlog.stdlib.LoggerFactory):
## def __init__(self, default_name, **kwargs):
## super().__init__(**kwargs)
## self.default_name = default_name
##
## def __call__(self, name=None, *args) -> logging.Logger:
## if name is None:
## name = self.default_name
## print('factory made: ', super().__call__(name, *args))
## return super().__call__(name, *args)

# --- App creation ---

# To choose the environment, we look for settings in the following order:
# (1) Function parameter
# (2) .env
Expand All @@ -37,12 +111,57 @@ def create_app(environment=None, oeis_scheme='https', oeis_hostport='oeis.org'):
if environment is None:
# Get app type from .env if provided. Otherwise, use 'development'
environment = os.environ.get('APP_ENVIRONMENT', 'development')

# Initial app and configuration

# --- Set up logging ---
#
# To access `app.logger` before `app` is created, we take advantage of
# knowing that `app` will use the `__name__` logger. we set `app.logger` to
# the most verbose level so that each handler can do its own filtering
#
# "When you want to configure logging for your project, you should do it as
# soon as possible when the program starts. If app.logger is accessed before
# logging is configured, it will add a default handler. If possible,
# configure logging before creating the application object"
#
# https://flask.palletsprojects.com/en/2.3.x/logging/

# For explicit logging, we'll use a structured logger that feeds into the
# basic logger. The structured logger will be current_app.structlogger, and
# the basic logger will be current_app.logger
structlog.configure(
processors=[
structlog.stdlib.filter_by_level,
structlog.stdlib.ProcessorFormatter.wrap_for_formatter,
],
##logger_factory=BackscopeLoggerFactory(__name__)
logger_factory=structlog.stdlib.LoggerFactory()
)
##structlogger = structlog.get_logger(__name__)

# Create file handler
file_handler = create_file_handler()

# Set up the basic logger
min_level = min(file_log_level, console_log_level(environment))
##logging.basicConfig(
##level=min_level
##level=min(file_log_level, console_log_level(environment))
##)
basic_logger = logging.getLogger(__name__)
basic_logger.setLevel(min_level)
basic_logger.addHandler(file_handler)
if config[environment].DEVELOPMENT:
# in development, also log to the console, with higher verbosity
basic_logger.addHandler(create_console_handler(environment))

# Create app
app = Flask(__name__, instance_relative_config=True)


# Add structured logger
app.structlogger = structlog.get_logger(__name__)

# Upload config from config.py
if environment == 'development': CORS(app)
if config[environment].DEVELOPMENT: CORS(app)
if config[environment].TESTING and config[environment].SQLALCHEMY_DATABASE_URI is None:
## this is a really convoluted way of throwing an exception when you try
## to run tests without specifying the test database, but allowing the
Expand All @@ -58,28 +177,20 @@ def create_app(environment=None, oeis_scheme='https', oeis_hostport='oeis.org'):
app.config['oeis_scheme'] = oeis_scheme
app.config['oeis_hostport'] = oeis_hostport

# Logging
file_handler = RotatingFileHandler('api.log', maxBytes=10000, backupCount=1)
file_handler.setLevel(logging.INFO)
app.logger.addHandler(file_handler)
stdout = logging.StreamHandler(sys.stdout)
stdout.setLevel(logging.DEBUG)
app.logger.addHandler(stdout)

# Initialize the application
db.init_app(app)
Migrate(app, db)

# Add a command line interface to the application
app.cli.add_command(clear_database_command)

# The nscope endpoint application
from flaskr import nscope

# The executor and blueprint are specified via nscope
nscope.executor.init_app(app)
app.register_blueprint(nscope.bp)

# The primary application
return app

Expand Down
2 changes: 2 additions & 0 deletions flaskr/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,14 @@ class Config:
DOC_PASSWORD = 'password'

DEBUG = False
DEVELOPMENT = False
TESTING = False
PRODUCTION = False


class DevConfig(Config):
DEBUG = True
DEVELOPMENT = True


class TestConfig(Config):
Expand Down
7 changes: 6 additions & 1 deletion flaskr/nscope/test/abstract_endpoint_test.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import unittest
from structlog.testing import capture_logs
import sys
from flaskr import create_app, db, clear_database
import flaskr.nscope.views as views
Expand Down Expand Up @@ -59,7 +60,7 @@ def test_endpoint(self):
# "The test client makes requests to the application without running a live
# server." the `with` block runs teardown
# https://github.com/pallets/flask/issues/2949
with self.app.test_client() as client:
with self.app.test_client() as client, capture_logs() as log_output:
if self.verbose:
print(" Testing response")

Expand All @@ -80,4 +81,8 @@ def test_endpoint(self):
f"'{response_type.__name__}'"
)

# check logs
if hasattr(self, 'expected_log_output'):
self.assertEqual(log_output, self.expected_log_output)

# TO DO: test background work
28 changes: 28 additions & 0 deletions flaskr/nscope/test/test_logging.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import unittest
from flaskr import create_app, db, clear_database
from structlog.testing import capture_logs


class LoggingTest(unittest.TestCase):
katestange marked this conversation as resolved.
Show resolved Hide resolved
expected_log_output = [{'log_level': 'error', 'event': 'Error message'}]

gwhitney marked this conversation as resolved.
Show resolved Hide resolved
def setUp(self):
self.app = create_app('testing')
self.ctx = self.app.app_context()
with self.ctx:
clear_database()

def tearDown(self):
# clear database
db.session.remove()
with self.ctx:
db.drop_all()

def test_logging(self):
with capture_logs() as log_output:
self.app.structlogger.error('Error message')
self.assertEqual(log_output, self.expected_log_output)


if __name__ == "__main__":
unittest.main()
5 changes: 4 additions & 1 deletion flaskr/nscope/test/test_lookup_errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@
# https://oeis.org/wiki/A-numbers
#
class TestNonexistentSequence(abstract_endpoint_test.AbstractEndpointTest):
endpoint = "http://localhost:5000/api/get_oeis_values/A000000/12"
endpoint = 'http://localhost:5000/api/get_oeis_values/A000000/12'
expected_response = "Error: B-file for ID 'A000000' not found in OEIS."
expected_log_output = [
{'log_level': 'error', 'event': 'request issue', 'tags': ['http error']}
]


if __name__ == "__main__":
Expand Down
Loading