本文目录导读:

实现PHP后台日志查看,常见有几种方案,从简单到专业,你可以根据项目规模和需求选择。
简单文件日志查看(适合小项目)
记录日志到文件
// log.php - 日志记录函数
function writeLog($message, $type = 'INFO') {
$logFile = __DIR__ . '/logs/app_' . date('Y-m-d') . '.log';
$logDir = dirname($logFile);
// 确保目录存在
if (!is_dir($logDir)) {
mkdir($logDir, 0755, true);
}
$logMessage = sprintf(
"[%s] [%s] %s - %s\n",
date('Y-m-d H:i:s'),
$type,
$_SERVER['REMOTE_ADDR'] ?? '127.0.0.1',
$message
);
file_put_contents($logFile, $logMessage, FILE_APPEND | LOCK_EX);
}
后台日志查看页面
// admin/logs.php - 日志查看页面
session_start();
// 检查管理员登录
if (!isset($_SESSION['admin_logged_in'])) {
header('Location: login.php');
exit;
}
// 获取日志文件列表
$logDir = __DIR__ . '/../logs/';
$logFiles = glob($logDir . '*.log');
rsort($logFiles); // 最新的文件在前面
// 读取指定日志文件
$currentFile = $_GET['file'] ?? ($logFiles[0] ?? '');
$logContent = '';
$lines = [];
if (file_exists($currentFile)) {
$content = file_get_contents($currentFile);
$lines = array_reverse(explode("\n", $content)); // 最新的日志在前面
}
// 搜索过滤
$search = $_GET['search'] ?? '';
if ($search && $lines) {
$lines = array_filter($lines, function($line) use ($search) {
return stripos($line, $search) !== false;
});
}
// 分页
$page = $_GET['page'] ?? 1;
$perPage = 50;
$totalLines = count($lines);
$totalPages = ceil($totalLines / $perPage);
$currentLines = array_slice($lines, ($page - 1) * $perPage, $perPage);
?>
<!DOCTYPE html>
<html>
<head>系统日志</title>
<style>
.log-container { font-family: monospace; background: #1e1e1e; color: #d4d4d4; padding: 20px; }
.log-line { padding: 2px 0; border-bottom: 1px solid #333; }
.log-line:hover { background: #2d2d2d; }
.INFO { color: #6a9955; }
.ERROR { color: #f44747; }
.WARNING { color: #dcdcaa; }
.search-form { margin-bottom: 20px; }
.pagination { margin-top: 20px; }
.file-list { margin-bottom: 20px; }
</style>
</head>
<body>
<div class="log-container">
<h1>系统日志</h1>
<!-- 文件选择 -->
<div class="file-list">
<select onchange="location.href='?file='+this.value">
<?php foreach ($logFiles as $file): ?>
<option value="<?= htmlspecialchars($file) ?>"
<?= $file === $currentFile ? 'selected' : '' ?>>
<?= basename($file) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<!-- 搜索 -->
<form class="search-form" method="GET">
<input type="hidden" name="file" value="<?= htmlspecialchars($currentFile) ?>">
<input type="text" name="search" placeholder="搜索日志..."
value="<?= htmlspecialchars($search) ?>">
<button type="submit">搜索</button>
</form>
<!-- 日志内容 -->
<div id="log-content">
<?php foreach ($currentLines as $line): ?>
<?php if (empty($line)) continue; ?>
<div class="log-line">
<?php
// 解析日志类型并添加颜色
if (preg_match('/\[(INFO|ERROR|WARNING)\]/', $line, $matches)) {
echo '<span class="' . $matches[1] . '">' .
htmlspecialchars($line) . '</span>';
} else {
echo htmlspecialchars($line);
}
?>
</div>
<?php endforeach; ?>
</div>
<!-- 分页 -->
<div class="pagination">
第 <?= $page ?> / <?= $totalPages ?> 页
<?php if ($page > 1): ?>
<a href="?file=<?= urlencode($currentFile) ?>&page=<?= $page-1 ?>&search=<?= urlencode($search) ?>">上一页</a>
<?php endif; ?>
<?php if ($page < $totalPages): ?>
<a href="?file=<?= urlencode($currentFile) ?>&page=<?= $page+1 ?>&search=<?= urlencode($search) ?>">下一页</a>
<?php endif; ?>
</div>
</div>
</body>
</html>
数据库日志(适合中型项目)
数据库表结构
CREATE TABLE `system_logs` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`level` ENUM('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL') DEFAULT 'INFO',
`message` TEXT NOT NULL,
`context` JSON DEFAULT NULL,
`ip_address` VARCHAR(45) DEFAULT NULL,
`user_agent` VARCHAR(500) DEFAULT NULL,
`request_uri` VARCHAR(500) DEFAULT NULL,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX `idx_level` (`level`),
INDEX `idx_created_at` (`created_at`),
INDEX `idx_level_created` (`level`, `created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
日志记录类
// Logger.php
class Logger {
private $db;
private static $instance = null;
private function __construct() {
$this->db = Database::getInstance(); // 你的数据库连接类
}
public static function getInstance() {
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
public function log($level, $message, array $context = []) {
$stmt = $this->db->prepare("
INSERT INTO system_logs (level, message, context, ip_address, user_agent, request_uri)
VALUES (?, ?, ?, ?, ?, ?)
");
$stmt->execute([
strtoupper($level),
$message,
json_encode($context),
$_SERVER['REMOTE_ADDR'] ?? null,
$_SERVER['HTTP_USER_AGENT'] ?? null,
$_SERVER['REQUEST_URI'] ?? null
]);
return $this->db->lastInsertId();
}
public function info($message, array $context = []) {
return $this->log('INFO', $message, $context);
}
public function error($message, array $context = []) {
return $this->log('ERROR', $message, $context);
}
public function warning($message, array $context = []) {
return $this->log('WARNING', $message, $context);
}
}
高级日志查看器
// admin/log-viewer.php
class LogViewer {
private $db;
private $perPage = 20;
public function __construct() {
$this->db = Database::getInstance();
}
public function getLogs($filters = []) {
$where = ['1=1'];
$params = [];
// 级别过滤
if (!empty($filters['level'])) {
$where[] = 'level = ?';
$params[] = strtoupper($filters['level']);
}
// 日期范围过滤
if (!empty($filters['date_from'])) {
$where[] = 'created_at >= ?';
$params[] = $filters['date_from'];
}
if (!empty($filters['date_to'])) {
$where[] = 'created_at <= ?';
$params[] = $filters['date_to'] . ' 23:59:59';
}
// 搜索关键词
if (!empty($filters['search'])) {
$where[] = '(message LIKE ? OR context LIKE ?)';
$searchTerm = '%' . $filters['search'] . '%';
$params[] = $searchTerm;
$params[] = $searchTerm;
}
$whereClause = implode(' AND ', $where);
// 计算总数
$countStmt = $this->db->prepare("SELECT COUNT(*) FROM system_logs WHERE $whereClause");
$countStmt->execute($params);
$total = $countStmt->fetchColumn();
// 分页查询
$page = max(1, $filters['page'] ?? 1);
$offset = ($page - 1) * $this->perPage;
$stmt = $this->db->prepare("
SELECT * FROM system_logs
WHERE $whereClause
ORDER BY created_at DESC
LIMIT ? OFFSET ?
");
$params[] = $this->perPage;
$params[] = $offset;
$stmt->execute($params);
$logs = $stmt->fetchAll(PDO::FETCH_ASSOC);
return [
'logs' => $logs,
'total' => $total,
'page' => $page,
'perPage' => $this->perPage,
'totalPages' => ceil($total / $this->perPage)
];
}
public function getLogStats() {
$stmt = $this->db->query("
SELECT
level,
COUNT(*) as count,
DATE(created_at) as date
FROM system_logs
WHERE created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)
GROUP BY level, DATE(created_at)
ORDER BY date DESC, level
");
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
}
// 在页面中使用
$viewer = new LogViewer();
$filters = [
'level' => $_GET['level'] ?? '',
'search' => $_GET['search'] ?? '',
'date_from' => $_GET['date_from'] ?? date('Y-m-d', strtotime('-7 days')),
'date_to' => $_GET['date_to'] ?? date('Y-m-d'),
'page' => $_GET['page'] ?? 1
];
$result = $viewer->getLogs($filters);
$stats = $viewer->getLogStats();
使用成熟的日志库(推荐)
使用 Monolog + 前端日志查看器
安装 Composer 包
composer require monolog/monolog
配置日志系统
// config/logging.php
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use Monolog\Handler\RotatingFileHandler;
use Monolog\Formatter\LineFormatter;
class Logging {
private static $logger = null;
public static function getLogger() {
if (self::$logger === null) {
$logger = new Logger('app');
// 按天轮转的日志文件
$handler = new RotatingFileHandler(
__DIR__ . '/../logs/app.log',
30, // 保留30天
Logger::DEBUG
);
// 格式化
$formatter = new LineFormatter(
"[%datetime%] %channel%.%level_name%: %message% %context% %extra%\n",
"Y-m-d H:i:s"
);
$handler->setFormatter($formatter);
$logger->pushHandler($handler);
// 可以根据环境添加不同的处理器
if (getenv('APP_ENV') === 'production') {
// 生产环境可以添加邮件通知等
}
self::$logger = $logger;
}
return self::$logger;
}
}
// 使用示例
$log = Logging::getLogger();
$log->info('用户登录', ['user_id' => 123]);
$log->error('数据库连接失败', ['error' => $e->getMessage()]);
前端日志查看器(推荐 LNAV 或 Log Viewer)
<!-- admin/logs-advanced.html -->
<!DOCTYPE html>
<html>
<head>日志查看器</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css">
<style>
.log-container {
background: #1e1e1e;
color: #d4d4d4;
font-family: 'Consolas', 'Courier New', monospace;
padding: 20px;
border-radius: 5px;
max-height: 80vh;
overflow-y: auto;
}
.log-entry {
padding: 5px;
margin: 2px 0;
cursor: pointer;
transition: background 0.2s;
}
.log-entry:hover {
background: #2d2d2d;
}
.level-INFO { color: #6a9955; }
.level-WARNING { color: #dcdcaa; }
.level-ERROR { color: #f44747; }
.level-CRITICAL { color: #ff0000; font-weight: bold; }
.stats-card {
text-align: center;
padding: 20px;
margin: 10px 0;
}
.log-detail {
background: #252526;
padding: 15px;
border-radius: 5px;
margin-top: 10px;
}
.auto-refresh {
margin: 10px 0;
}
</style>
</head>
<body>
<div class="container-fluid mt-4">
<div class="row">
<!-- 统计信息 -->
<div class="col-md-3">
<div class="card stats-card">
<div class="card-body">
<h5 class="card-title">今日日志</h5>
<p class="card-text display-4" id="todayCount">0</p>
</div>
</div>
<div class="card stats-card">
<div class="card-body">
<h5 class="card-title">错误数量</h5>
<p class="card-text display-4 text-danger" id="errorCount">0</p>
</div>
</div>
</div>
<!-- 日志列表 -->
<div class="col-md-9">
<!-- 过滤器 -->
<div class="card mb-3">
<div class="card-body">
<form id="filterForm" class="row g-3">
<div class="col-md-3">
<select class="form-select" name="level">
<option value="">所有级别</option>
<option value="INFO">INFO</option>
<option value="WARNING">WARNING</option>
<option value="ERROR">ERROR</option>
<option value="CRITICAL">CRITICAL</option>
</select>
</div>
<div class="col-md-3">
<input type="text" class="form-control" name="search" placeholder="搜索日志...">
</div>
<div class="col-md-2">
<input type="date" class="form-control" name="date_from">
</div>
<div class="col-md-2">
<input type="date" class="form-control" name="date_to">
</div>
<div class="col-md-2">
<button type="submit" class="btn btn-primary w-100">筛选</button>
</div>
</form>
<div class="auto-refresh">
<label>
<input type="checkbox" id="autoRefresh"> 自动刷新 (每10秒)
</label>
<span class="badge bg-secondary" id="lastUpdate"></span>
</div>
</div>
</div>
<!-- 日志内容 -->
<div class="log-container" id="logContainer">
<div class="text-center text-muted py-5">
加载中...
</div>
</div>
<!-- 分页 -->
<nav aria-label="日志分页">
<ul class="pagination justify-content-center" id="pagination">
</ul>
</nav>
</div>
</div>
</div>
<!-- 日志详情模态框 -->
<div class="modal fade" id="logDetailModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">日志详情</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body" id="logDetailContent">
</div>
</div>
</div>
</div>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
// AJAX 加载日志
function loadLogs(page = 1) {
const filters = {
page: page,
level: $('[name="level"]').val(),
search: $('[name="search"]').val(),
date_from: $('[name="date_from"]').val(),
date_to: $('[name="date_to"]').val()
};
$('#logContainer').html('<div class="text-center py-5"><div class="spinner-border"></div></div>');
$.get('/api/logs', filters, function(response) {
renderLogs(response);
$('#lastUpdate').text('最后更新: ' + new Date().toLocaleTimeString());
});
}
function renderLogs(response) {
const container = $('#logContainer');
container.empty();
if (response.logs.length === 0) {
container.html('<div class="text-center text-muted py-5">暂无日志</div>');
return;
}
response.logs.forEach(function(log) {
const levelClass = 'level-' + log.level;
const time = new Date(log.created_at).toLocaleString();
const detail = JSON.stringify(log.context, null, 2);
const html = `
<div class="log-entry" data-detail='${escapeHtml(detail)}'>
<span class="text-muted">[${time}]</span>
<span class="${levelClass}">[${log.level}]</span>
<span>${escapeHtml(log.message)}</span>
${log.context ? '<span class="badge bg-info">有详情</span>' : ''}
</div>
`;
container.append(html);
});
// 更新统计
$('#todayCount').text(response.todayCount || 0);
$('#errorCount').text(response.errorCount || 0);
// 更新分页
renderPagination(response);
// 绑定点击事件
$('.log-entry').click(function() {
const detail = $(this).data('detail');
$('#logDetailContent').html('<pre>' + detail + '</pre>');
$('#logDetailModal').modal('show');
});
}
function renderPagination(response) {
const pagination = $('#pagination');
pagination.empty();
if (response.totalPages <= 1) return;
// 上一页
pagination.append(`
<li class="page-item ${response.page <= 1 ? 'disabled' : ''}">
<a class="page-link" href="#" data-page="${response.page - 1}">上一页</a>
</li>
`);
// 页码
for (let i = 1; i <= response.totalPages; i++) {
pagination.append(`
<li class="page-item ${i === response.page ? 'active' : ''}">
<a class="page-link" href="#" data-page="${i}">${i}</a>
</li>
`);
}
// 下一页
pagination.append(`
<li class="page-item ${response.page >= response.totalPages ? 'disabled' : ''}">
<a class="page-link" href="#" data-page="${response.page + 1}">下一页</a>
</li>
`);
// 绑定点击事件
$('.page-link').click(function(e) {
e.preventDefault();
const page = $(this).data('page');
if (page) loadLogs(page);
});
}
function escapeHtml(text) {
if (!text) return '';
return $('<div>').text(text).html();
}
// 自动刷新
let refreshInterval;
$('#autoRefresh').change(function() {
if (this.checked) {
refreshInterval = setInterval(loadLogs, 10000);
} else {
clearInterval(refreshInterval);
}
});
// 表单提交
$('#filterForm').submit(function(e) {
e.preventDefault();
loadLogs();
});
// 初始加载
$(document).ready(function() {
loadLogs();
});
</script>
</body>
</html>
使用开源日志查看器(推荐)
使用 LogViewer(Composer 包)
composer require rap2hpoutre/laravel-log-viewer
或使用更通用的:
composer require arcanedev/log-viewer
安全注意事项
- 权限控制:确保只有管理员可以访问日志
- 路径遍历防护:严格验证文件路径
- XSS防护:始终使用
htmlspecialchars()或escapeHtml() - 日志轮转:防止日志文件过大
- 敏感信息过滤:不要记录密码等敏感信息
生产环境最佳实践
// 生产环境日志配置
class ProductionLogger {
public static function init() {
// 1. 使用日志聚合服务(如ELK Stack)
// 2. 设置日志级别(生产环境通常 INFO 及以上)
// 3. 实现日志告警(错误数超过阈值时发送通知)
// 4. 定期清理旧日志
// 5. 日志文件权限设置(644)
// 6. 使用独立的日志服务器
}
}
选择哪种方案取决于你的项目需求:
- 小型项目:方案一(文件日志)
- 中型项目:方案二(数据库日志)
- 大型项目:方案三或四(专业日志库 + 聚合服务)