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:
- If the user doesn’t exist, return an error.
- If no valid products are found, return an error.
- 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:
- Cleaner Code: Business logic remains focused on its purpose.
- Separation of Concerns: Logging and monitoring are abstracted into a separate class.
- 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:
- Log edge cases: When something unexpected happens, log it.
- Use instrumentation classes: Keep your code clean and focused on business logic.
- 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!