Sharpen Your Code

Using the Right Tool (And a Note on Logging)

Coding can feel quite daunting—specially with the wealth of information out there to consume. Sometimes, as I stare at the code on my screen, I feel particularly stuck or unmotivated.

Analogy

During a summer break while in high school, I took an odd job of pine tree pruning.

Yeah, you read that right.

Pine tree pruning.

It involved carrying a 16-foot pole with a serrated blade at the end of it. Then, going down a line of pine trees, cutting all branches up to that height.

It was grueling. And paid terribly. I think it was something like 60 cents a tree, or something ridiculous. A thankless task for logging companies turning trees into lumber.

After my first day, I thought I did pretty well. I earned like $20 in approximately 8 hours. (That's a little over 30 trees, for those of you that are counting.)

One of the workers there was able to prune over 120 trees a day.

I was baffled.

The Obvious Correlation

What I found out after a couple of days is that the worker who was pruning lots of trees would sharpen his blade every night.

He was able to cut down branches with one swift movement, whereas I was spending time trying to literally saw off even smaller branches.

Yes, technique was important. But so was a sharp blade.

The tools you use to perform a task can be just as important as to how you accomplish that task.

Back to Development

I haven't posted on here for a little while because I've started a new side project.

Thankfully, I've taken a lot of lessons learned building my blog and transferred it over to my new project. But I still have a lot to learn.

I can't stress enough how important it is to be comfortable in your environment.

Secondly, take advantage of all the libraries in the Python ecosystem. A quick tip on those external tools...

As you start using different libraries, try to read the documentation, get to know the API, and really spend your time with them. It will help you a ton.

For me, that has meant getting comfortable with Beanie in order to interface with my MongoDB database.

Sharpen Your Own Tools

However, not all tools have to be external.

In Python, there are already a lot of built-in tools at your disposal. But if you don't sharpen them a bit, you may be missing your full potential.

For example, as I've started my new project, I have wanted to make things a little bit tidier.

One of the ways to do that is to be more proactive with my approach to logging.

Yes, default logging could use a little sharpening.

There's a good article by Guilherme Latrova that provides really good guidance on why/how you may want to go about logging in Python. A bit from the article:

Most people don't know what to log, so they decide to log anything thinking it might be better than nothing, and end up creating just noise. Noise is a piece of information that doesn't help you or your team understand what's going on or resolving a problem.

Creating noise is an example of working with a dull blade. It will only make your life harder.

However, good logging can give you valuable context, providing you with information you need right away in order to monitor and/or course-correct when things go wrong.

Yes, this speeds up your development.

Richify Your Logger

I may have edited this heading after seeing Will's tweet.

Maybe you've heard of Rich? You know, it makes your print statements look very pretty!

That alone as a tool could really help your development.

But what's more, Rich comes with it's own handler that you can use with your logger.

For my new project, I reference a post by Hayden Kotelman titled FastAPI and Rich Tracebacks in Development.

I use a similar approach, ensuring that my tracebacks/logging are readable and pleasant—but most of all, useful.

Before I begin, take a look at what a typical traceback looks like in my project without Rich:

Ugly Traceback

Nothing out of the ordinary.

Alright, so let's begin.

First, here's the relevant packages:

import logging
import sys
from functools import lru_cache
from pathlib import Path

from myapp.config import settings  # my own config
from pydantic import BaseModel

My configuration file has a few variables that I keep handy, but I go ahead and define my logger format here.

LOGGER_FILE = Path(settings.LOGGER_FILE)  # where log is stored
DATE_FORMAT = "%d %b %Y | %H:%M:%S"
LOGGER_FORMAT = "%(asctime)s | %(message)s"

I then use a pydantic model to hold my logger configuration info.

class LoggerConfig(BaseModel):
    handlers: list
    format: str
    date_format: str | None = None
    logger_file: Path | None = None
    level: int = logging.INFO

My next piece of code is nearly identical to Kotelman's.

@lru_cache
def get_logger_config():
    """Installs RichHandler (Rich library) if not in production
    environment, or use the production log configuration.
    """

    if not settings.ENV_STATE == "prd":
        from rich.logging import RichHandler

        output_file_handler = logging.FileHandler(LOGGER_FILE)
        handler_format = logging.Formatter(
            LOGGER_FORMAT, datefmt=DATE_FORMAT)
        output_file_handler.setFormatter(handler_format)

        return LoggerConfig(
            handlers=[
                RichHandler(
                    rich_tracebacks=True,
                    tracebacks_show_locals=True,
                    show_time=False
                ),
                output_file_handler
            ],
            format=None,
            date_format=DATE_FORMAT,
            logger_file=LOGGER_FILE,
        )

    output_file_handler = logging.FileHandler(LOGGER_FILE)
    handler_format = logging.Formatter(
        LOGGER_FORMAT, datefmt=DATE_FORMAT)
    output_file_handler.setFormatter(handler_format)

    # Stdout
    stdout_handler = logging.StreamHandler(sys.stdout)
    stdout_handler.setFormatter(handler_format)

    return LoggerConfig(
        handlers=[output_file_handler, stdout_handler],
        format="%(levelname)s: %(asctime)s \t%(message)s",
        date_format="%d-%b-%y %H:%M:%S",
        logger_file=LOGGER_FILE,
    )

Wow, that's a lot of sharpening!

The articles I linked might give you more context as to what's going on here, but here's the general gist of it.

If you are not in production, the function get_logger_config imports the RichHandler and sets it in motion.

You'll note that RichHandler has a few options, but you can get a sense of how customizable it is by reviewing the documentation.

Ultimately, you set the handler to show up on your terminal through stdout_handler, and the setFormatter method allows you to pull in your new handler format.

Overriding

Logging in Python can be a bit confusing for newbies like myself, especially coming fresh into it. Each library could have its own logging configuration/handlers, and the layers could run deep.

Logging in FastAPI can be tricky because Uvicorn (the server that FastAPI runs on), has its own logging mechanism.

Ultimately, what I wanted to do was remove other logger handlers (specifically, Uvicorn), and use RichHandler instead.

def setup_rich_logger():
    """Cycles through uvicorn root loggers to
    remove handler, then runs `get_logger_config()`
    to populate the `LoggerConfig` class with Rich
    logger parameters.
    """

    # Remove all handlers from root logger
    # and proprogate to root logger.
    for name in logging.root.manager.loggerDict.keys():
        logging.getLogger(name).handlers = []
        logging.getLogger(name).propagate = True

    logger_config = get_logger_config()  # get Rich logging config

    logging.basicConfig(
        level=logger_config.level,
        format=logger_config.format,
        datefmt=logger_config.date_format,
        handlers=logger_config.handlers,
    )

Reference: Unify Python logging for a Gunicorn/Uvicorn/FastAPI application

What It Looks Like

I originally published this piece without any images to show the end result.

My bad. Still getting used to this blogging thing.

To invoke the logger in FastAPI, I call the function after declaring my application. Something like:

from fastapi import FastAPI
from app.logger import setup_rich_logger

def get_app() -> FastAPI:

    app = FastAPI()
    setup_rich_logger()

    return app

app = get_app()

For fun, I've inserted the following lines of code in the module where I instantiate my database connection:

Logging Code

Note that for my "debug" message to show, I have to change the level in my LoggerConfig class.

level: int = logging.DEBUG

Once I start up my FastAPI application. I am greeted with the following:

Starting FastAPI With Rich Logging

And when things go wrong:

Richified Traceback

...and so on...

And then one final look when things are in working order. I set the level back to INFO and removed the logging lines from above.

Richified FastAPI Application

I'm quickly able to see when my database is online, which Beanie documents are being picked up by my model finder, what are my recent articles that I've published, and whenever someone's trying to login.

It still could use a little more sharpening, but this already makes my life so much easier.

In Closing

This is hardly a peek at the toolbox available to you when coding in Python.

But I did want to point out how spending a little time up front setting up your logger could potentially save you tons of time down the road.

Don't stop there, though.

Let me know what other tools you keep sharpened to aid in your coding adventures!