本文目录导读:

实现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. 使用独立的日志服务器
}
}
选择哪种方案取决于你的项目需求:
- 小型项目:方案一(文件日志)
- 中型项目:方案二(数据库日志)
- 大型项目:方案三或四(专业日志库 + 聚合服务)
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播