When implementing logging in your application, structure your log messages and parameters consistently to improve readability, searchability, and integration with monitoring tools. Consider these practices:

  1. Use the appropriate logging method for your context:
    # Correct: Inside an exception handler
    try:
        dangerous_operation()
    except Exception:
        logger.exception("Operation failed")  # Automatically includes traceback
       
    # Correct: Outside an exception handler
    if error_occurred:
        logger.error("Operation failed", exc_info=True)  # Explicitly include traceback
    
  2. Provide structured context with log messages:
    # Good: Structured logging with extra parameters
    logger.debug(
        "Query executed: %(sql)s; duration=%(duration).3f; alias=%(alias)s",
        extra={
            "duration": duration,
            "sql": sql,
            "params": params,
            "alias": connection.alias,
        }
    )
    
  3. Be mindful of performance in log formatters:
    # Performance-aware formatter
    def format(self, record):
        # Cache formatted values to avoid repeated processing
        sql = None
        formatted_sql = None
           
        if args := record.args:
            if isinstance(args, dict) and (sql := args.get("sql")):
                args["sql"] = formatted_sql = format_sql(sql)
               
        # Reuse formatted value if it's the same SQL
        if extra_sql := getattr(record, "sql", None):
            if extra_sql == sql:
                record.sql = formatted_sql
            else:
                record.sql = format_sql(extra_sql)
                   
        return super().format(record)
    

By following these practices, you’ll create logs that are more useful for debugging, monitoring, and analysis while avoiding common performance pitfalls.