Skip to content

RabbitMQ 日志轮转

概述

日志轮转(Log Rotation)是日志管理的重要环节,可以防止日志文件无限增长导致磁盘空间耗尽。RabbitMQ 内置了日志轮转功能,同时也支持与系统级日志轮转工具配合使用。本文将详细介绍 RabbitMQ 日志轮转的配置方法和最佳实践。

核心知识点

日志轮转方式

方式说明适用场景
基于大小文件达到指定大小时轮转日志量稳定的场景
基于时间按固定时间间隔轮转日常运维管理
外部工具使用 logrotate 等工具复杂轮转需求

RabbitMQ 内置轮转配置

配置项说明默认值
log.file.rotation.date轮转时间
log.file.rotation.size轮转大小
log.file.rotation.count保留文件数5

轮转时间格式

$D0     - 每天午夜
$D1     - 每周一午夜
$D5     - 每周五午夜
$W0     - 每周日午夜
$M1D0   - 每月1号午夜
$Q1D0   - 每季度1号午夜

配置示例

基于大小的轮转

bash
log.file.rotation.size = 104857600
log.file.rotation.count = 10

基于时间的轮转

bash
log.file.rotation.date = $D0
log.file.rotation.count = 30

混合轮转策略

bash
log.file.rotation.date = $D0
log.file.rotation.size = 52428800
log.file.rotation.count = 20

完整配置示例

bash
log.file.level = info
log.file = /var/log/rabbitmq/rabbit.log
log.file.formatter = text
log.file.rotation.date = $D0
log.file.rotation.size = 104857600
log.file.rotation.count = 30

PHP 日志轮转管理类

php
<?php

class RabbitMQLogRotationManager
{
    private $configFile;
    private $logDir;
    
    public function __construct(
        $configFile = '/etc/rabbitmq/rabbitmq.conf',
        $logDir = '/var/log/rabbitmq'
    ) {
        $this->configFile = $configFile;
        $this->logDir = $logDir;
    }
    
    public function getRotationConfig()
    {
        $config = [
            'enabled' => true,
            'size' => null,
            'date' => null,
            'count' => 5,
        ];
        
        if (!file_exists($this->configFile)) {
            return $config;
        }
        
        $content = file_get_contents($this->configFile);
        
        if (preg_match('/log\.file\.rotation\.size\s*=\s*(\d+)/', $content, $m)) {
            $config['size'] = (int)$m[1];
        }
        
        if (preg_match('/log\.file\.rotation\.date\s*=\s*(\S+)/', $content, $m)) {
            $config['date'] = $m[1];
        }
        
        if (preg_match('/log\.file\.rotation\.count\s*=\s*(\d+)/', $content, $m)) {
            $config['count'] = (int)$m[1];
        }
        
        return $config;
    }
    
    public function setRotationBySize($sizeMB, $count = 10)
    {
        $sizeBytes = $sizeMB * 1024 * 1024;
        
        $this->updateConfig('log.file.rotation.size', $sizeBytes);
        $this->updateConfig('log.file.rotation.count', $count);
        $this->removeConfig('log.file.rotation.date');
        
        return [
            'type' => 'size',
            'size_mb' => $sizeMB,
            'count' => $count,
        ];
    }
    
    public function setRotationByDate($pattern = '$D0', $count = 30)
    {
        $this->updateConfig('log.file.rotation.date', $pattern);
        $this->updateConfig('log.file.rotation.count', $count);
        $this->removeConfig('log.file.rotation.size');
        
        return [
            'type' => 'date',
            'pattern' => $pattern,
            'count' => $count,
        ];
    }
    
    public function setRotationHybrid($pattern, $sizeMB, $count)
    {
        $sizeBytes = $sizeMB * 1024 * 1024;
        
        $this->updateConfig('log.file.rotation.date', $pattern);
        $this->updateConfig('log.file.rotation.size', $sizeBytes);
        $this->updateConfig('log.file.rotation.count', $count);
        
        return [
            'type' => 'hybrid',
            'pattern' => $pattern,
            'size_mb' => $sizeMB,
            'count' => $count,
        ];
    }
    
    public function getLogFiles()
    {
        $files = [];
        
        if (!is_dir($this->logDir)) {
            return $files;
        }
        
        $iterator = new DirectoryIterator($this->logDir);
        
        foreach ($iterator as $file) {
            if ($file->isFile() && preg_match('/\.log/', $file->getFilename())) {
                $files[] = [
                    'name' => $file->getFilename(),
                    'path' => $file->getPathname(),
                    'size' => $file->getSize(),
                    'size_human' => $this->formatBytes($file->getSize()),
                    'modified' => date('Y-m-d H:i:s', $file->getMTime()),
                    'is_rotated' => preg_match('/\.log\.\d+$/', $file->getFilename()),
                ];
            }
        }
        
        usort($files, function($a, $b) {
            return strcmp($b['modified'], $a['modified']);
        });
        
        return $files;
    }
    
    public function getDiskUsage()
    {
        $files = $this->getLogFiles();
        $totalSize = 0;
        $rotatedSize = 0;
        $currentSize = 0;
        
        foreach ($files as $file) {
            $totalSize += $file['size'];
            if ($file['is_rotated']) {
                $rotatedSize += $file['size'];
            } else {
                $currentSize += $file['size'];
            }
        }
        
        return [
            'total_files' => count($files),
            'rotated_files' => count(array_filter($files, fn($f) => $f['is_rotated'])),
            'current_files' => count(array_filter($files, fn($f) => !$f['is_rotated'])),
            'total_size' => $totalSize,
            'total_size_human' => $this->formatBytes($totalSize),
            'rotated_size' => $rotatedSize,
            'rotated_size_human' => $this->formatBytes($rotatedSize),
            'current_size' => $currentSize,
            'current_size_human' => $this->formatBytes($currentSize),
        ];
    }
    
    public function cleanupOldLogs($keepCount = null)
    {
        $config = $this->getRotationConfig();
        $keepCount = $keepCount ?: $config['count'];
        
        $files = $this->getLogFiles();
        $rotatedFiles = array_filter($files, fn($f) => $f['is_rotated']);
        
        usort($rotatedFiles, function($a, $b) {
            return strcmp($b['modified'], $a['modified']);
        });
        
        $deleted = [];
        $toDelete = array_slice($rotatedFiles, $keepCount);
        
        foreach ($toDelete as $file) {
            if (unlink($file['path'])) {
                $deleted[] = $file['name'];
            }
        }
        
        return [
            'deleted_count' => count($deleted),
            'deleted_files' => $deleted,
            'kept_count' => $keepCount,
        ];
    }
    
    public function forceRotate()
    {
        $mainLogFile = $this->findMainLogFile();
        
        if (!$mainLogFile || !file_exists($mainLogFile)) {
            return [
                'success' => false,
                'message' => 'Main log file not found',
            ];
        }
        
        $timestamp = date('YmdHis');
        $rotatedFile = $mainLogFile . '.' . $timestamp;
        
        if (rename($mainLogFile, $rotatedFile)) {
            touch($mainLogFile);
            
            return [
                'success' => true,
                'original' => $mainLogFile,
                'rotated' => $rotatedFile,
            ];
        }
        
        return [
            'success' => false,
            'message' => 'Failed to rotate log file',
        ];
    }
    
    public function compressOldLogs($daysOld = 7)
    {
        $files = $this->getLogFiles();
        $compressed = [];
        $threshold = time() - ($daysOld * 86400);
        
        foreach ($files as $file) {
            if ($file['is_rotated'] && !preg_match('/\.gz$/', $file['name'])) {
                $mtime = filemtime($file['path']);
                
                if ($mtime < $threshold) {
                    $gzFile = $file['path'] . '.gz';
                    
                    if ($this->compressFile($file['path'], $gzFile)) {
                        unlink($file['path']);
                        $compressed[] = $file['name'];
                    }
                }
            }
        }
        
        return [
            'compressed_count' => count($compressed),
            'compressed_files' => $compressed,
        ];
    }
    
    public function generateRotationReport()
    {
        $config = $this->getRotationConfig();
        $usage = $this->getDiskUsage();
        $files = $this->getLogFiles();
        
        $report = "RabbitMQ 日志轮转报告\n";
        $report .= str_repeat("=", 50) . "\n\n";
        
        $report .= "轮转配置:\n";
        $report .= "- 保留文件数: {$config['count']}\n";
        
        if ($config['size']) {
            $report .= "- 大小限制: " . $this->formatBytes($config['size']) . "\n";
        }
        if ($config['date']) {
            $report .= "- 时间模式: {$config['date']}\n";
        }
        
        $report .= "\n磁盘使用:\n";
        $report .= "- 总文件数: {$usage['total_files']}\n";
        $report .= "- 总大小: {$usage['total_size_human']}\n";
        $report .= "- 当前日志: {$usage['current_size_human']}\n";
        $report .= "- 轮转日志: {$usage['rotated_size_human']}\n";
        
        $report .= "\n文件列表:\n";
        foreach (array_slice($files, 0, 10) as $file) {
            $report .= sprintf(
                "- %-30s %10s  %s\n",
                $file['name'],
                $file['size_human'],
                $file['modified']
            );
        }
        
        return $report;
    }
    
    private function findMainLogFile()
    {
        if (file_exists($this->configFile)) {
            $content = file_get_contents($this->configFile);
            if (preg_match('/^log\.file\s*=\s*(.+)$/m', $content, $m)) {
                return trim($m[1]);
            }
        }
        
        return $this->logDir . '/rabbit.log';
    }
    
    private function updateConfig($key, $value)
    {
        $content = file_exists($this->configFile) 
            ? file_get_contents($this->configFile) 
            : '';
        
        $pattern = '/^' . preg_quote($key, '/') . '\s*=.*$/m';
        $replacement = "{$key} = {$value}";
        
        if (preg_match($pattern, $content)) {
            $content = preg_replace($pattern, $replacement, $content);
        } else {
            $content = rtrim($content) . "\n{$replacement}\n";
        }
        
        $this->ensureConfigDir();
        file_put_contents($this->configFile, $content);
    }
    
    private function removeConfig($key)
    {
        if (!file_exists($this->configFile)) {
            return;
        }
        
        $content = file_get_contents($this->configFile);
        $pattern = '/^' . preg_quote($key, '/') . '\s*=.*\n?/m';
        $content = preg_replace($pattern, '', $content);
        
        file_put_contents($this->configFile, $content);
    }
    
    private function ensureConfigDir()
    {
        $dir = dirname($this->configFile);
        if (!is_dir($dir)) {
            mkdir($dir, 0755, true);
        }
    }
    
    private function formatBytes($bytes)
    {
        $units = ['B', 'KB', 'MB', 'GB', 'TB'];
        $i = 0;
        
        while ($bytes >= 1024 && $i < count($units) - 1) {
            $bytes /= 1024;
            $i++;
        }
        
        return round($bytes, 2) . ' ' . $units[$i];
    }
    
    private function compressFile($source, $destination)
    {
        $fp = fopen($source, 'rb');
        $gz = gzopen($destination, 'wb9');
        
        if (!$fp || !$gz) {
            return false;
        }
        
        while (!feof($fp)) {
            gzwrite($gz, fread($fp, 8192));
        }
        
        fclose($fp);
        gzclose($gz);
        
        return true;
    }
}

实际应用场景

场景一:使用 logrotate 管理日志

bash
/etc/logrotate.d/rabbitmq-server

/var/log/rabbitmq/*.log {
    daily
    missingok
    rotate 30
    compress
    delaycompress
    notifempty
    create 0640 rabbitmq rabbitmq
    sharedscripts
    postrotate
        rabbitmqctl eval 'logger:set_primary_config(overwrite, true).' > /dev/null
    endscript
}

场景二:Docker 环境日志轮转

yaml
version: '3.8'

services:
  rabbitmq:
    image: rabbitmq:3.12-management
    logging:
      driver: "json-file"
      options:
        max-size: "100m"
        max-file: "5"
    volumes:
      - rabbitmq_log:/var/log/rabbitmq

volumes:
  rabbitmq_log:

场景三:Kubernetes 日志轮转

yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: rabbitmq-config
data:
  rabbitmq.conf: |
    log.file.rotation.date = $D0
    log.file.rotation.count = 7
    log.file.rotation.size = 52428800
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: rabbitmq
spec:
  template:
    spec:
      containers:
        - name: rabbitmq
          image: rabbitmq:3.12-management
          volumeMounts:
            - name: config
              mountPath: /etc/rabbitmq
            - name: logs
              mountPath: /var/log/rabbitmq
      volumes:
        - name: config
          configMap:
            name: rabbitmq-config
        - name: logs
          emptyDir: {}

常见问题与解决方案

问题一:日志文件未轮转

现象:日志文件持续增长,没有轮转。

解决方案

bash
log.file.rotation.date = $D0
log.file.rotation.count = 10

或使用 logrotate:

bash
logrotate -f /etc/logrotate.d/rabbitmq-server

问题二:轮转后日志丢失

现象:轮转后新日志没有写入。

解决方案

bash
postrotate
    rabbitmqctl eval 'logger:set_primary_config(overwrite, true).' > /dev/null
endscript

问题三:磁盘空间不足

现象:日志占用过多磁盘空间。

解决方案

php
$manager->setRotationBySize(50, 10);
$manager->cleanupOldLogs(10);
$manager->compressOldLogs(3);

最佳实践

1. 轮转策略选择

场景推荐策略
小型部署每日轮转,保留 7 天
中型部署每日轮转,保留 30 天
大型部署大小+时间混合,保留 30 天

2. 配置建议

bash
log.file.rotation.date = $D0
log.file.rotation.size = 104857600
log.file.rotation.count = 30

3. 监控告警

php
$usage = $manager->getDiskUsage();
if ($usage['total_size'] > 1073741824) {
    trigger_alert('Log disk usage exceeds 1GB');
}

相关链接