# 55. Debugging with logging Module

**1. Basic Logging Setup**

Setting up basic logging configuration.

```python
import logging

# Basic logging configuration
logging.basicConfig(level=logging.DEBUG)

# Example log statements
logging.debug("This is a debug message")
logging.info("This is an info message")
logging.warning("This is a warning message")
logging.error("This is an error message")
logging.critical("This is a critical message")
```

This sets up the logging system to log messages at the `DEBUG` level and above, and it logs messages of various severity levels.

***

**2. Logging to a File**

Logging messages to a file with a specified log level.

```python
import logging

# Logging to a file
logging.basicConfig(filename='app.log', level=logging.INFO)

logging.info("This is an info message saved to a file.")
logging.error("This is an error message saved to a file.")
```

This configuration writes log messages to `app.log` file, allowing for permanent logging.

***

**3. Customizing Log Message Format**

Customizing the format of log messages.

```python
import logging

# Custom log format
logging.basicConfig(
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
    level=logging.DEBUG
)

logging.debug("Debug message with timestamp and custom format.")
```

This sets a custom format for the log messages, including timestamps, logger name, log level, and the message.

***

**4. Logging with Rotating Log Files**

Using a rotating file handler for log files that grow too large.

```python
import logging
from logging.handlers import RotatingFileHandler

# Create a rotating file handler
handler = RotatingFileHandler('app.log', maxBytes=2000, backupCount=3)
logging.basicConfig(handlers=[handler], level=logging.DEBUG)

# Example log message
logging.debug("This message will be written to a rotating log file.")
```

This configuration creates a log file that rotates when it exceeds 2000 bytes, keeping up to 3 backups.

***

**5. Logging with Multiple Handlers**

Configuring multiple logging handlers to log to both a file and the console.

```python
import logging

# Create a custom logger
logger = logging.getLogger()

# Create handlers
file_handler = logging.FileHandler('file.log')
console_handler = logging.StreamHandler()

# Set log levels for each handler
file_handler.setLevel(logging.INFO)
console_handler.setLevel(logging.DEBUG)

# Create formatter and add it to the handlers
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
file_handler.setFormatter(formatter)
console_handler.setFormatter(formatter)

# Add handlers to the logger
logger.addHandler(file_handler)
logger.addHandler(console_handler)

# Log some messages
logger.debug("This is a debug message.")
logger.info("This is an info message.")
```

This example shows how to log to both a file and the console, with different log levels for each handler.

***

**6. Log Record Creation**

Creating custom log records with extra context.

```python
import logging

# Custom logger with extra information
logger = logging.getLogger()

# Log record with extra context
logger.warning("Warning message with custom context", extra={'user': 'John Doe', 'action': 'login'})

# Example log message with extra data
logging.basicConfig(format='%(asctime)s - %(user)s - %(action)s - %(message)s', level=logging.DEBUG)
```

This code uses the `extra` argument to add custom context (like user and action) to log records.

***

**7. Logging Exceptions**

Logging exceptions with traceback.

```python
import logging

try:
    1 / 0  # Division by zero to cause an exception
except ZeroDivisionError:
    logging.exception("An exception occurred")
```

The `logging.exception()` method logs the exception along with the traceback.

***

**8. Dynamic Log Level Adjustment**

Dynamically changing the log level at runtime.

```python
import logging

# Set initial logging level
logging.basicConfig(level=logging.DEBUG)

# Log some messages
logging.debug("This is a debug message.")
logging.info("This is an info message.")

# Change log level dynamically
logging.getLogger().setLevel(logging.ERROR)

# Log messages after changing level
logging.debug("This debug message will not be shown.")
logging.error("This error message will be shown.")
```

This demonstrates how to change the logging level dynamically at runtime.

***

**9. Creating a Custom Logging Handler**

Creating a custom log handler to log to a database or external system.

```python
import logging

class DatabaseHandler(logging.Handler):
    def emit(self, record):
        log_entry = self.format(record)
        # Save log_entry to a database or external system
        print(f"Log saved to database: {log_entry}")

# Set up custom handler
db_handler = DatabaseHandler()
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
db_handler.setFormatter(formatter)

# Add handler to logger
logging.getLogger().addHandler(db_handler)

# Log a message
logging.info("This is an info message that would be saved to a database.")
```

This creates a custom handler to log messages to an external system (like a database).

***

**10. Logging in Multithreaded Applications**

Handling logging in multithreaded applications with thread-specific loggers.

```python
import logging
import threading

# Create a logger for each thread
def worker(thread_name):
    logger = logging.getLogger(thread_name)
    logger.debug(f"Debug message from {thread_name}")

# Set up basic configuration
logging.basicConfig(level=logging.DEBUG)

# Start multiple threads
threads = []
for i in range(5):
    thread = threading.Thread(target=worker, args=(f"Thread-{i}",))
    threads.append(thread)
    thread.start()

for thread in threads:
    thread.join()
```

This snippet creates individual loggers for each thread to prevent interference in multithreaded applications.

***

These snippets demonstrate various ways to implement logging in Python for better traceability and debugging, including logging to files, customizing formats, handling exceptions, and working with multithreaded applications.
