CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/pypi-asgi-correlation-id

Middleware correlating project logs to individual requests

Pending

Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

Overview
Eval results
Files

logging.mddocs/

Logging Integration

Log filters that automatically attach correlation IDs to log records, enabling seamless log correlation without manual ID injection. These filters integrate with Python's standard logging framework to automatically include tracking identifiers in all log output.

Capabilities

Correlation ID Filter

Logging filter that automatically attaches the current correlation ID to log records, making it available for formatting and output.

class CorrelationIdFilter(logging.Filter):
    """
    Logging filter to attach correlation IDs to log records.
    
    Parameters:
    - name: Filter name (default: '')
    - uuid_length: Optional length to trim UUID to (default: None)
    - default_value: Default value when no correlation ID exists (default: None)
    """
    def __init__(
        self,
        name: str = '',
        uuid_length: Optional[int] = None,
        default_value: Optional[str] = None
    ): ...
    
    def filter(self, record: LogRecord) -> bool:
        """
        Attach a correlation ID to the log record.
        
        Adds 'correlation_id' attribute to the log record containing:
        - Current correlation ID from context variable
        - Trimmed to uuid_length if specified
        - default_value if no correlation ID exists
        
        Parameters:
        - record: Log record to modify
        
        Returns:
        - bool: Always True (never filters out records)
        """

The filter adds a correlation_id attribute to every log record, which can be used in log formatters:

# In log format string
'%(levelname)s [%(correlation_id)s] %(name)s %(message)s'

# Results in output like:
# INFO [a1b2c3d4] myapp.views Processing user request
# ERROR [a1b2c3d4] myapp.models Database connection failed

Celery Tracing IDs Filter

Logging filter that attaches both parent and current Celery task IDs to log records, enabling hierarchical task tracing.

class CeleryTracingIdsFilter(logging.Filter):
    """
    Logging filter to attach Celery tracing IDs to log records.
    
    Parameters:
    - name: Filter name (default: '')
    - uuid_length: Optional length to trim UUIDs to (default: None)  
    - default_value: Default value when no IDs exist (default: None)
    """
    def __init__(
        self,
        name: str = '',
        uuid_length: Optional[int] = None,
        default_value: Optional[str] = None
    ): ...
    
    def filter(self, record: LogRecord) -> bool:
        """
        Append parent and current ID to the log record.
        
        Adds two attributes to the log record:
        - 'celery_parent_id': ID of the process that spawned current task
        - 'celery_current_id': Unique ID for the current task process
        
        Both IDs are trimmed to uuid_length if specified, and use
        default_value when the respective context variable is None.
        
        Parameters:
        - record: Log record to modify
        
        Returns:
        - bool: Always True (never filters out records)
        """

This filter enables detailed task tracing in log output:

# In log format string  
'%(levelname)s [%(celery_current_id)s|%(celery_parent_id)s] %(name)s %(message)s'

# Results in output like:
# INFO [task-abc123|req-xyz789] tasks.email Sending notification email
# ERROR [task-def456|task-abc123] tasks.data Task failed processing batch

Usage Examples

Basic Logging Setup

import logging
from asgi_correlation_id import CorrelationIdFilter

# Configure logging with correlation ID
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s %(levelname)s [%(correlation_id)s] %(name)s %(message)s'
)

# Add filter to root logger
correlation_filter = CorrelationIdFilter()
logging.getLogger().addFilter(correlation_filter)

# Use logging normally - correlation ID is automatically included
logger = logging.getLogger(__name__)
logger.info("This will include the correlation ID")

Advanced Logging Configuration

LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'formatters': {
        'detailed': {
            'format': '%(asctime)s %(levelname)s [%(correlation_id)s] %(name)s:%(lineno)d %(message)s',
            'datefmt': '%Y-%m-%d %H:%M:%S'
        },
        'simple': {
            'format': '%(levelname)s [%(correlation_id)s] %(message)s'
        },
    },
    'filters': {
        'correlation_id': {
            '()': 'asgi_correlation_id.CorrelationIdFilter',
            'uuid_length': 8,  # Trim to 8 characters
            'default_value': 'no-id'
        },
    },
    'handlers': {
        'console': {
            'class': 'logging.StreamHandler',
            'formatter': 'detailed',
            'filters': ['correlation_id'],
        },
        'file': {
            'class': 'logging.FileHandler',
            'filename': 'app.log',
            'formatter': 'simple',
            'filters': ['correlation_id'],
        },
    },
    'loggers': {
        'myapp': {
            'handlers': ['console', 'file'],
            'level': 'INFO',
            'propagate': False,
        },
    },
}

import logging.config
logging.config.dictConfig(LOGGING)

Celery Logging Setup

import logging
from asgi_correlation_id import CeleryTracingIdsFilter

# Configure Celery task logging
celery_filter = CeleryTracingIdsFilter(uuid_length=8, default_value='none')

# Add to Celery logger
celery_logger = logging.getLogger('celery')
celery_logger.addFilter(celery_filter)

# Configure format to include both IDs
logging.basicConfig(
    format='%(asctime)s [%(celery_current_id)s|%(celery_parent_id)s] %(name)s %(message)s'
)

Combined HTTP and Celery Logging

import logging
from asgi_correlation_id import CorrelationIdFilter, CeleryTracingIdsFilter

# Create filters
http_filter = CorrelationIdFilter(uuid_length=8, default_value='no-req')
celery_filter = CeleryTracingIdsFilter(uuid_length=8, default_value='no-task')

# Configure different loggers for different contexts
http_logger = logging.getLogger('webapp')
http_logger.addFilter(http_filter)

celery_logger = logging.getLogger('tasks')  
celery_logger.addFilter(celery_filter)

# Set up formatters for each context
logging.basicConfig(
    format='%(asctime)s %(name)s %(message)s'
)

# HTTP logs will show: 2023-01-01 12:00:00 webapp [a1b2c3d4] Processing request
# Celery logs will show: 2023-01-01 12:00:00 tasks [task-123|req-456] Running background job

Filter Behavior

UUID Length Trimming

When uuid_length is specified:

  • UUIDs longer than the specified length are truncated
  • UUIDs shorter than the specified length remain unchanged
  • None values are handled gracefully
# With uuid_length=8
filter = CorrelationIdFilter(uuid_length=8)

# "a1b2c3d4-e5f6-7890-abcd-ef1234567890" becomes "a1b2c3d4"
# "short" remains "short"
# None remains None (or uses default_value)

Default Values

The default_value parameter provides fallback text when no correlation ID is available:

filter = CorrelationIdFilter(default_value='no-correlation-id')

# When correlation_id.get() returns None:
# Log shows: INFO [no-correlation-id] myapp Processing request

Types

from logging import Filter, LogRecord
from typing import TYPE_CHECKING, Optional

if TYPE_CHECKING:
    from logging import LogRecord

# Internal utility function
def _trim_string(string: Optional[str], string_length: Optional[int]) -> Optional[str]:
    """Trim string to specified length if provided."""

Install with Tessl CLI

npx tessl i tessl/pypi-asgi-correlation-id

docs

celery-extension.md

context.md

index.md

logging.md

middleware.md

sentry-extension.md

tile.json