File size: 5,194 Bytes
8e0dd55
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ddbf24a
8e0dd55
 
ddbf24a
 
8e0dd55
ddbf24a
 
 
8e0dd55
 
ddbf24a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8e0dd55
113f6a0
 
 
8e0dd55
ddbf24a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8e0dd55
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ddbf24a
 
 
 
8e0dd55
 
 
 
ddbf24a
 
 
 
 
 
 
 
 
 
 
 
8e0dd55
 
ddbf24a
8e0dd55
 
 
ddbf24a
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
import logging
import os
from pathlib import Path
from logging.handlers import RotatingFileHandler


class IgnoreLogChangeDetectedFilter(logging.Filter):
    def filter(self, record: logging.LogRecord):
        return "Detected file change in" not in record.getMessage()


def setup_logging(format: str = None):
    """
    Configure logging for the application with log rotation.

    Environment variables:
        LOG_LEVEL: Log level (default: INFO)
        LOG_FILE_PATH: Path to log file (default: logs/application.log)
        LOG_MAX_SIZE: Max size in MB before rotating (default: 10MB)
        LOG_BACKUP_COUNT: Number of backup files to keep (default: 5)
        DISABLE_FILE_LOGGING: Set to "true" to disable file logging (default: false)

    Ensures log directory exists, prevents path traversal, and configures
    both rotating file and console handlers. Falls back to console-only logging
    if file logging fails due to permissions.
    """
    # Check if file logging should be disabled
    disable_file_logging = os.environ.get("DISABLE_FILE_LOGGING", "false").lower() == "true"
    
    # Determine log directory and default file path
    base_dir = Path(__file__).parent
    
    # In containerized environments, use /tmp for logs if the default location isn't writable
    try:
        log_dir = base_dir / "logs"
        log_dir.mkdir(parents=True, exist_ok=True)
        default_log_file = log_dir / "application.log"
    except (PermissionError, OSError):
        # Fall back to /tmp for containerized environments
        log_dir = Path("/tmp/deepwiki-logs")
        try:
            log_dir.mkdir(parents=True, exist_ok=True)
            default_log_file = log_dir / "application.log"
        except (PermissionError, OSError):
            # If even /tmp fails, disable file logging
            disable_file_logging = True
            default_log_file = None

    # Get log level from environment - default to WARNING to reduce noise in production
    log_level_str = os.environ.get("LOG_LEVEL", "WARNING").upper()
    log_level = getattr(logging, log_level_str, logging.WARNING)

    # Get log file path (only if file logging is enabled)
    resolved_path = None
    if not disable_file_logging and default_log_file:
        log_file_path = Path(os.environ.get("LOG_FILE_PATH", str(default_log_file)))

        # Secure path check: must be inside logs/ directory
        log_dir_resolved = log_dir.resolve()
        resolved_path = log_file_path.resolve()
        if not str(resolved_path).startswith(str(log_dir_resolved) + os.sep):
            print(f"Warning: LOG_FILE_PATH '{log_file_path}' is outside the trusted log directory '{log_dir_resolved}'. Falling back to console logging.")
            disable_file_logging = True
            resolved_path = None

        # Ensure parent directories exist
        if resolved_path:
            try:
                resolved_path.parent.mkdir(parents=True, exist_ok=True)
            except (PermissionError, OSError):
                print("Warning: Cannot create log directory. Falling back to console logging.")
                disable_file_logging = True
                resolved_path = None

    # Get max log file size (default: 10MB)
    try:
        max_mb = int(os.environ.get("LOG_MAX_SIZE", 10))  # 10MB default
        max_bytes = max_mb * 1024 * 1024
    except (TypeError, ValueError):
        max_bytes = 10 * 1024 * 1024  # fallback to 10MB on error

    # Get backup count (default: 5)
    try:
        backup_count = int(os.environ.get("LOG_BACKUP_COUNT", 5))
    except ValueError:
        backup_count = 5

    # Configure format
    log_format = format or "%(asctime)s - %(levelname)s - %(name)s - %(filename)s:%(lineno)d - %(message)s"

    # Create handlers list
    handlers = []
    
    # Create console handler (always present)
    console_handler = logging.StreamHandler()
    formatter = logging.Formatter(log_format)
    console_handler.setFormatter(formatter)
    console_handler.addFilter(IgnoreLogChangeDetectedFilter())
    handlers.append(console_handler)

    # Create file handler only if file logging is enabled and path is available
    file_handler = None
    if not disable_file_logging and resolved_path:
        try:
            file_handler = RotatingFileHandler(resolved_path, maxBytes=max_bytes, backupCount=backup_count, encoding="utf-8")
            file_handler.setFormatter(formatter)
            file_handler.addFilter(IgnoreLogChangeDetectedFilter())
            handlers.append(file_handler)
        except (PermissionError, OSError) as e:
            print(f"Warning: Cannot create file handler: {e}. Using console logging only.")

    # Apply logging configuration
    logging.basicConfig(level=log_level, handlers=handlers, force=True)

    # Log configuration info
    logger = logging.getLogger(__name__)
    if file_handler and resolved_path:
        logger.debug(
            f"Logging configured: level={log_level_str}, "
            f"file={resolved_path}, max_size={max_bytes} bytes, "
            f"backup_count={backup_count}"
        )
    else:
        logger.debug(f"Logging configured: level={log_level_str}, console only (file logging disabled)")