Product Crafter Emilio Carrión
English
Spanish

The importance of logging

Gaining visibility in your code

Logging is one of the simplest yet most powerful tools we have to monitor and troubleshoot software. Yet, far too often, we encounter critical systems with little to no logging, leading to a total lack of visibility when incidents occur.

In this article, I want to emphasize the importance of logging and share some practical techniques to introduce logging in your code in a clean and effective way. Whether you’re working on an e-commerce system or any other kind of software, having proper logs will save you hours of investigation during postmortems and make incident resolution much smoother.

Let’s dive in!

Why You Need to Log: A Real-World Scenario

Imagine you’re investigating an incident: orders are failing, the warehouse is stuck, and you have no idea what went wrong. You dig into the code and realize there’s not a single log to guide you. You’re essentially blind.

Postmortems in these situations often lead to the same conclusion: “We should add logs here.” But by then, it’s too late. The incident has already caused damage—lost revenue, frustrated users, and unhappy clients.

Logging should never be an afterthought. It’s free, it doesn’t hurt anyone, and it makes your life (and your team’s life) significantly easier.

A Simple Use Case: Order Creation

Let’s take an example from a basic e-commerce system with a create order use case. This function receives a user and a list of products. It performs a few basic checks:

  1. If the user doesn’t exist, return an error.
  2. If no valid products are found, return an error.
  3. If everything is fine, create an order.

Here’s the base logic in pseudocode:

def create_order(user_id, products):
    user = find_user(user_id)
    if user is None:
        return None  # User does not exist

    valid_products = []
    for product in products:
        if is_valid_product(product):
            valid_products.append(product)
    
    if not valid_products:
        return None  # No products to process

    order_id = random_id()
    simulate_payment_delay()  # Simulating database delay
    
    return order_id

At first glance, this function works. But what happens if something fails? Let’s explore:

  • What if the user doesn’t exist?
  • What if the product list is empty?
  • What if an invalid product is provided?

If you don’t log these cases, you won’t know where or why the order creation failed. Let’s fix that.

Adding Simple Logs: Visibility Matters

Adding logs is straightforward. Let’s start by covering some edge cases:

1. User Not Found

If the user doesn’t exist, log that specific event:

if user is None:
    logger.info(f"User not found: {user_id}")
    return None

Now, if an incident occurs, you’ll know immediately which user ID caused the issue.

2. No Products Found

If the product list is empty, log that too:

if not valid_products:
    logger.info(f"No products found for order: {user_id}")
    return None

3. Invalid Products

If products are skipped because they’re invalid, log each occurrence:

for product in products:
    if not is_valid_product(product):
        logger.info(f"Product not found: {product}")
        continue
    valid_products.append(product)

Avoiding Log Pollution: Instrumentation Classes

A common complaint about logging is that it can clutter the code, making it harder to read. To avoid this, we can use an instrumentation class to encapsulate our logs.

Here’s an example:

class CreateOrderInstrumentation:
    def user_not_found(self, user_id):
        logger.info(f"User not found: {user_id}")

    def no_products_found(self):
        logger.info(f"No products found")

    def product_not_found(self, product):
        logger.info(f"Product not found: {product}")

Using the Instrumentation Class

Instead of logging directly in your business logic, you can call methods from the instrumentation class:

instrumentation = CreateOrderInstrumentation()

if user is None:
    instrumentation.user_not_found(user_id)
    return None

for product in products:
    if not is_valid_product(product):
        instrumentation.product_not_found(product)
        continue
    valid_products.append(product)

if not valid_products:
    instrumentation.no_products_found()
    return None

This approach has several benefits:

  1. Cleaner Code: Business logic remains focused on its purpose.
  2. Separation of Concerns: Logging and monitoring are abstracted into a separate class.
  3. Extensibility: You can add metrics or tracing logic without touching the core code.

Enriching Logs with Extra Context

Logs are more useful when they contain context. For example, you can include a trace ID to correlate all logs from a single request or flow:

import uuid

class CreateOrderInstrumentation:
    def __init__(self):
        self.trace_id = uuid.uuid4()

    def log(self, level, message):
        logger.log(level, f"{self.trace_id} - {message}")

    def user_not_found(self):
        self.log(logging.INFO, "User not found")

    def no_products_found(self):
        self.log(logging.WARNING, "No products found")

    def product_not_found(self, product):
        self.log(logging.INFO, f"Product not found: {product}")

With a trace ID, you can:

  • Correlate logs across services and systems.
  • Track the flow of a request through different parts of your code.

Final Thoughts: Build a Culture of Logging

Logging is not a luxury—it’s a necessity. It provides visibility into your systems, helps you identify problems quickly, and makes postmortems far more effective.

Here’s what you can do starting today:

  1. Log edge cases: When something unexpected happens, log it.
  2. Use instrumentation classes: Keep your code clean and focused on business logic.
  3. Add context: Use IDs and extra details to make logs more actionable.

Logging doesn’t cost anything. It’s free, it’s easy, and it will save you from disasters. So go ahead—add those logs, build that visibility, and create a culture of logging in your team.

Your future self will thank you.

If you enjoyed this article, consider sharing it with your team and subscribing to my blog for more tips on writing clean, maintainable, and production-ready code. See you in the next post!