Skip to main content
Version: Next

Logging System

This guide covers the centralized logging system architecture, programmatic usage, and best practices for developers contributing to the Slack MCP Server project.

Architecture Overview

The logging system is split into two separate configurations:

Source Code Logging (slack_mcp/logging/)

For production application logging with features like:

  • Console and file logging
  • Automatic log rotation
  • Hierarchical configuration
  • Environment variable support
  • Case-insensitive log levels

Test Logging (test/logging/)

For test execution logging with:

  • Clean output (WARNING level by default)
  • Verbose mode for debugging
  • Simple format for test readability
  • External library silencing

Programmatic Usage

Basic Setup

from slack_mcp.logging import setup_logging
import logging

# Basic setup (console only)
setup_logging(level="INFO")

# Get logger for your module
logger = logging.getLogger(__name__)

# Use logger
logger.info("Application started")
logger.debug("Debug information")
logger.error("Error occurred")

With File Logging

from slack_mcp.logging import setup_logging

# Setup with file logging
setup_logging(
level="DEBUG",
log_file="/path/to/app.log"
)

Full Custom Configuration

from slack_mcp.logging import setup_logging

setup_logging(
level="INFO",
log_file="/var/log/slack-mcp.log",
log_format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
date_format="%Y-%m-%d %H:%M:%S"
)

CLI Integration

The logging system provides helper functions for argparse integration:

import argparse
from slack_mcp.logging import add_logging_arguments, setup_logging_from_args

# Create parser
parser = argparse.ArgumentParser()

# Add your arguments
parser.add_argument("--port", type=int, default=8000)

# Add logging arguments (--log-level, --log-file, --log-dir, --log-format)
parser = add_logging_arguments(parser)

# Parse arguments
args = parser.parse_args()

# Setup logging from arguments
setup_logging_from_args(args)

Module-Level Logging

Best Practice

Always use __name__ for module-level loggers:

import logging

# At module level
logger = logging.getLogger(__name__)

def my_function():
logger.info("Function called")
logger.debug("Processing data...")

This creates a hierarchical logger structure:

  • slack_mcp.mcp.server → Logs from MCP server
  • slack_mcp.webhook.server → Logs from webhook server
  • slack_mcp.client.manager → Logs from client manager

Logger Hierarchy

The logging configuration automatically manages logger hierarchy:

# Root logger
logging.getLogger() # Catches everything

# Package logger
logging.getLogger("slack_mcp") # All slack_mcp logs

# Module logger
logging.getLogger("slack_mcp.mcp.server") # Specific module logs

Available Functions and Constants

Functions

setup_logging(level, log_file, log_format, date_format)

Configure application logging.

Parameters:

  • level (str): Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
  • log_file (str, optional): Path to log file
  • log_format (str, optional): Custom log format
  • date_format (str, optional): Custom date format
setup_logging(
level="INFO",
log_file="app.log",
log_format="%(asctime)s %(message)s"
)

setup_logging_from_args(args)

Setup logging from parsed command-line arguments.

Parameters:

  • args: Namespace from argparse with logging attributes
args = parser.parse_args()
setup_logging_from_args(args)

add_logging_arguments(parser)

Add logging arguments to argparse parser.

Parameters:

  • parser: ArgumentParser instance

Returns: Modified parser with logging arguments

Added Arguments:

  • --log-level: Case-insensitive log level
  • --log-file: Log file path
  • --log-dir: Log directory
  • --log-format: Custom log format
parser = add_logging_arguments(parser)

get_logging_config(level, log_file, log_format, date_format)

Get logging configuration dictionary for logging.config.dictConfig().

from slack_mcp.logging import get_logging_config
import logging.config

config = get_logging_config(level="DEBUG", log_file="app.log")
logging.config.dictConfig(config)

get_log_file_path(log_dir, log_file)

Get absolute log file path from directory and filename.

from slack_mcp.logging import get_log_file_path

path = get_log_file_path(
log_dir="/var/log/slack-mcp",
log_file="server.log"
)
# Returns: /var/log/slack-mcp/server.log

Constants

from slack_mcp.logging import (
DEFAULT_LOG_FORMAT,
DEFAULT_DATE_FORMAT,
DEFAULT_LEVEL,
LOG_LEVELS,
DEFAULT_LOG_DIR,
DEFAULT_LOG_FILE,
)

# Default values
DEFAULT_LOG_FORMAT = "%(asctime)s [%(levelname)8s] %(name)s: %(message)s"
DEFAULT_DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
DEFAULT_LEVEL = "INFO"
DEFAULT_LOG_DIR = "logs"
DEFAULT_LOG_FILE = "slack_mcp.log"
LOG_LEVELS = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]

Test Logging

Setup Test Logging

from test.logging import setup_test_logging

# Basic test logging (WARNING level to reduce noise)
setup_test_logging()

# Verbose test logging (INFO level for debugging)
setup_test_logging(verbose=True)

# Custom log level
setup_test_logging(level="DEBUG")

Get Test Logger

from test.logging import get_test_logger

logger = get_test_logger(__name__)
logger.info("Test started")

Pytest Configuration

Control test logging with pytest flags:

# Verbose test output
pytest -v --log-cli-level=DEBUG

# With environment variable
TEST_LOG_LEVEL=DEBUG pytest -v

Log File Configuration

Rotation Settings

Automatic rotation is configured with:

{
"class": "logging.handlers.RotatingFileHandler",
"filename": log_file_path,
"maxBytes": 10 * 1024 * 1024, # 10MB
"backupCount": 5,
"formatter": "default",
}

Behavior:

  • Rotates at 10MB
  • Keeps 5 backup files
  • Oldest file deleted on rotation

Custom Handlers

Add custom handlers to the logging configuration:

import logging
from slack_mcp.logging import get_logging_config

# Get base config
config = get_logging_config(level="INFO")

# Add custom handler
config["handlers"]["custom"] = {
"class": "logging.StreamHandler",
"formatter": "default",
"stream": "ext://sys.stderr",
}

# Apply configuration
logging.config.dictConfig(config)

Case-Insensitive Log Levels

The logging system uses type=str.upper in argparse to automatically convert log level input:

log_group.add_argument(
"--log-level",
dest="log_level",
type=str.upper, # Converts to uppercase automatically
default=os.getenv("LOG_LEVEL", DEFAULT_LEVEL),
choices=LOG_LEVELS,
help=f"Set the logging level (default: {DEFAULT_LEVEL})",
)

This allows users to type:

  • --log-level debug → Converted to DEBUG
  • --log-level INFO → Stays INFO
  • --log-level Warning → Converted to WARNING

Migration Guide

From Old logging.basicConfig()

Old Code:

import logging
logging.basicConfig(level=logging.INFO)

New Code:

from slack_mcp.logging import setup_logging
setup_logging(level="INFO")

From Manual CLI Parsing

Old Code:

args = parse_args()
logging.basicConfig(level=args.log_level.upper())

New Code:

from slack_mcp.logging import setup_logging_from_args
args = parse_args()
setup_logging_from_args(args)

External Library Management

The logging system automatically reduces noise from external libraries:

"loggers": {
"uvicorn": {
"handlers": ["console"],
"level": "ERROR",
"propagate": False,
},
"httpx": {
"handlers": ["console"],
"level": "ERROR",
"propagate": False,
},
"asyncio": {
"handlers": ["console"],
"level": "WARNING",
"propagate": False,
},
}

This keeps logs clean while still capturing critical errors.

Best Practices

1. Always Use __name__

# Good
logger = logging.getLogger(__name__)

# Bad - hardcoded name
logger = logging.getLogger("my_module")

2. Log at Appropriate Levels

# DEBUG - Detailed diagnostic info
logger.debug(f"Processing request with params: {params}")

# INFO - General information
logger.info("Server started successfully")

# WARNING - Potentially problematic
logger.warning("Rate limit approaching threshold")

# ERROR - Serious problems
logger.error(f"Failed to connect: {error}")

# CRITICAL - Fatal problems
logger.critical("Database connection lost, shutting down")

3. Use Lazy Formatting

# Good - only formats if logged
logger.debug("Processing %s items", len(items))

# Bad - always formats
logger.debug(f"Processing {len(items)} items")

4. Include Context

# Good - includes context
logger.error(f"Failed to process message {message_id}: {error}")

# Bad - missing context
logger.error("Processing failed")

5. Don't Log Sensitive Data

# Bad - logs sensitive token
logger.info(f"Using token: {slack_token}")

# Good - masks sensitive data
logger.info(f"Using token: {slack_token[-4:]}")

6. Test Logging Configuration

Write tests for logging behavior:

def test_logging_setup(caplog):
"""Test that logging is configured correctly."""
from slack_mcp.logging import setup_logging

setup_logging(level="DEBUG")

logger = logging.getLogger("test_module")
logger.debug("Test message")

assert "Test message" in caplog.text

Environment-Specific Configuration

Development

from slack_mcp.logging import setup_logging

if os.getenv("ENV") == "development":
setup_logging(level="DEBUG")
else:
setup_logging(
level="INFO",
log_file="/var/log/slack-mcp/production.log"
)

Testing

# In test fixtures
@pytest.fixture(autouse=True)
def setup_test_logs():
"""Setup logging for tests."""
from test.logging import setup_test_logging
setup_test_logging(verbose=False)

Production

from slack_mcp.logging import setup_logging

setup_logging(
level=os.getenv("LOG_LEVEL", "INFO"),
log_file=os.getenv("LOG_FILE", "/var/log/slack-mcp/server.log"),
)

Troubleshooting

Logs Not Appearing

  1. Check logger name: Ensure using __name__
  2. Verify log level: Message level must be >= configured level
  3. Check configuration: Ensure setup_logging() was called

Duplicate Logs

If seeing duplicate log messages:

# Ensure propagate=False in logger config
"loggers": {
"slack_mcp": {
"handlers": ["console"],
"level": level,
"propagate": False, # Prevents duplicate logs
}
}

Performance Issues

High log volume impacting performance:

  1. Increase log level (INFO → WARNING)
  2. Disable DEBUG logging in hot code paths
  3. Use lazy formatting
  4. Consider async logging for high-throughput

Advanced Usage

Custom Formatter

import logging
from slack_mcp.logging import setup_logging

# Custom formatter class
class CustomFormatter(logging.Formatter):
def format(self, record):
# Add custom logic
return super().format(record)

# Apply after setup
setup_logging(level="INFO")
handler = logging.getLogger().handlers[0]
handler.setFormatter(CustomFormatter("%(message)s"))

Structured Logging

For JSON logging:

import json
import logging

class JSONFormatter(logging.Formatter):
def format(self, record):
return json.dumps({
"timestamp": self.formatTime(record),
"level": record.levelname,
"logger": record.name,
"message": record.getMessage(),
})

Context Logging

Add context to all logs:

import logging

logger = logging.getLogger(__name__)

# Add context to logger
logger = logging.LoggerAdapter(logger, {"request_id": "abc123"})
logger.info("Processing request")
# Output: Processing request [request_id=abc123]