本文目录导读:

我来介绍几种 PHP 远程日志的实现方式:
使用 UDP 发送日志(推荐)
服务端 (日志收集器)
// log_server.php
$socket = stream_socket_server("udp://0.0.0.0:9999", $errno, $errstr, STREAM_SERVER_BIND);
if (!$socket) {
die("$errstr ($errno)");
}
while (true) {
$pkt = stream_socket_recvfrom($socket, 65535, 0, $peer);
$logData = json_decode($pkt, true);
// 写入日志文件
$logLine = sprintf(
"[%s] [%s] %s - %s\n",
$logData['timestamp'],
$logData['level'],
$logData['message'],
json_encode($logData['context'] ?? [])
);
file_put_contents('/var/log/remote_app.log', $logLine, FILE_APPEND);
echo "Received from: $peer\n";
}
客户端 (应用端)
// log_client.php
class RemoteLogger {
private $host;
private $port;
private $appName;
public function __construct($host, $port, $appName = 'app') {
$this->host = $host;
$this->port = $port;
$this->appName = $appName;
}
public function log($level, $message, array $context = []) {
$data = [
'timestamp' => date('Y-m-d H:i:s'),
'app' => $this->appName,
'level' => $level,
'message' => $message,
'context' => $context,
'server_ip' => $_SERVER['SERVER_ADDR'] ?? 'cli',
'request_id' => uniqid()
];
$socket = stream_socket_client("udp://{$this->host}:{$this->port}",
$errno, $errstr, 3);
if ($socket) {
stream_socket_sendto($socket, json_encode($data), 0,
"{$this->host}:{$this->port}");
fclose($socket);
}
}
}
// 使用示例
$logger = new RemoteLogger('192.168.1.100', 9999, 'myapp');
$logger->log('ERROR', '数据库连接失败', [
'db_host' => 'localhost',
'error_code' => 1045
]);
$logger->log('INFO', '用户登录成功', ['user_id' => 123]);
使用 HTTP API 方式
服务端 (REST API)
// log_api.php
<?php
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
exit;
}
$input = json_decode(file_get_contents('php://input'), true);
if (!$input || !isset($input['message'])) {
http_response_code(400);
echo json_encode(['error' => 'Invalid request']);
exit;
}
$logEntry = sprintf(
"[%s] [%s] %s | App: %s | IP: %s | %s\n",
date('Y-m-d H:i:s'),
$input['level'] ?? 'INFO',
$input['message'],
$input['app'] ?? 'unknown',
$_SERVER['REMOTE_ADDR'],
json_encode($input['context'] ?? [])
);
file_put_contents('/var/log/remote_api.log', $logEntry, FILE_APPEND | LOCK_EX);
echo json_encode(['status' => 'ok']);
客户端
// http_logger.php
class HttpRemoteLogger {
private $endpoint;
private $apiKey;
public function __construct($endpoint, $apiKey = null) {
$this->endpoint = $endpoint;
$this->apiKey = $apiKey;
}
public function log($level, $message, array $context = []) {
$data = [
'level' => $level,
'message' => $message,
'context' => $context,
'app' => 'myapp',
'timestamp' => microtime(true)
];
$ch = curl_init($this->endpoint);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($data),
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
$this->apiKey ? "X-API-Key: {$this->apiKey}" : ''
],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 3,
CURLOPT_CONNECTTIMEOUT => 2
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
return $httpCode === 200;
}
}
// 使用
$logger = new HttpRemoteLogger('http://log-server.example.com/log.php', 'my-secret-key');
$logger->log('WARNING', '磁盘空间不足', ['free_space' => '500MB']);
使用 Redis 作为中间件
// 发送端
class RedisRemoteLogger {
private $redis;
private $listKey;
public function __construct($redisHost, $redisPort, $listKey = 'app_logs') {
$this->redis = new Redis();
$this->redis->connect($redisHost, $redisPort);
$this->listKey = $listKey;
}
public function log($level, $message, array $context = []) {
$logEntry = [
'timestamp' => microtime(true),
'level' => $level,
'message' => $message,
'context' => $context,
'hostname' => gethostname(),
'pid' => getmypid()
];
$this->redis->rPush($this->listKey, json_encode($logEntry));
// 可选:设置过期时间,防止日志堆积
$this->redis->expire($this->listKey, 86400); // 24小时过期
}
}
// 接收端 - 可以单独进程消费
class LogConsumer {
private $redis;
private $listKey;
public function __construct($redisHost, $redisPort, $listKey = 'app_logs') {
$this->redis = new Redis();
$this->redis->connect($redisHost, $redisPort);
$this->listKey = $listKey;
}
public function consume() {
while (true) {
// 阻塞读取,等待新日志
$log = $this->redis->blPop($this->listKey, 10);
if ($log) {
$logData = json_decode($log[1], true);
$this->writeToFile($logData);
$this->sendToElasticsearch($logData); // 可选
}
}
}
private function writeToFile($data) {
$line = sprintf(
"[%s] [%s] %s [%s] %s\n",
date('Y-m-d H:i:s', $data['timestamp']),
$data['level'],
$data['hostname'],
$data['message'],
json_encode($data['context'])
);
file_put_contents('/var/log/redis_logs.log', $line, FILE_APPEND);
}
}
使用成熟日志库
Monolog + Socket Handler
<?php
require 'vendor/autoload.php';
use Monolog\Logger;
use Monolog\Handler\SocketHandler;
use Monolog\Formatter\JsonFormatter;
// 创建日志通道
$log = new Logger('myapp');
// 创建Socket处理器
$handler = new SocketHandler("tcp://192.168.1.100:9090");
$handler->setFormatter(new JsonFormatter());
$log->pushHandler($handler);
// 使用
$log->error('数据库错误', [
'sql' => 'SELECT * FROM users',
'error_code' => 1054
]);
$log->info('用户操作', ['action' => 'login', 'user_id' => 456]);
完整的最佳实践示例
<?php
// RemoteLogger.php - 完整的远程日志类
class RemoteLogger {
private static $instance = null;
private $config;
private $buffer = [];
private $bufferSize = 10;
public function __construct(array $config = []) {
$this->config = array_merge([
'driver' => 'udp', // udp, http, redis
'host' => '127.0.0.1',
'port' => 9999,
'app_name' => 'default',
'buffer_size' => 10,
'async' => true
], $config);
$this->bufferSize = $this->config['buffer_size'];
if ($this->config['async']) {
register_shutdown_function([$this, 'flush']);
}
}
public static function getInstance(array $config = []) {
if (self::$instance === null) {
self::$instance = new self($config);
}
return self::$instance;
}
public function log($level, $message, array $context = []) {
$entry = [
'timestamp' => date('Y-m-d\TH:i:s.u'),
'level' => strtoupper($level),
'message' => $message,
'app' => $this->config['app_name'],
'server' => [
'ip' => $_SERVER['SERVER_ADDR'] ?? gethostbyname(gethostname()),
'hostname' => gethostname(),
'pid' => getmypid()
],
'context' => $context,
'trace' => debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3)
];
if ($this->config['async']) {
$this->buffer[] = $entry;
if (count($this->buffer) >= $this->bufferSize) {
$this->flush();
}
} else {
$this->send($entry);
}
return $this;
}
public function flush() {
if (empty($this->buffer)) {
return;
}
$entries = $this->buffer;
$this->buffer = [];
// 批量发送
foreach (array_chunk($entries, 5) as $batch) {
$this->sendBatch($batch);
}
}
private function send(array $entry) {
switch ($this->config['driver']) {
case 'udp':
$this->sendUdp(json_encode($entry));
break;
case 'http':
$this->sendHttp([$entry]);
break;
case 'redis':
$this->sendRedis(json_encode($entry));
break;
}
}
private function sendBatch(array $entries) {
$data = json_encode($entries);
switch ($this->config['driver']) {
case 'udp':
// UDP 适合发送单个小包
foreach ($entries as $entry) {
$this->sendUdp(json_encode($entry));
}
break;
case 'http':
$this->sendHttp($entries);
break;
case 'redis':
foreach ($entries as $entry) {
$this->sendRedis(json_encode($entry));
}
break;
}
}
private function sendUdp($data) {
$socket = @stream_socket_client(
"udp://{$this->config['host']}:{$this->config['port']}",
$errno, $errstr, 2
);
if ($socket) {
@stream_socket_sendto($socket, $data, 0,
"{$this->config['host']}:{$this->config['port']}");
@fclose($socket);
}
}
private function sendHttp(array $entries) {
$ch = curl_init("http://{$this->config['host']}:{$this->config['port']}/log");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode(['logs' => $entries]),
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 1,
CURLOPT_CONNECTTIMEOUT => 1
]);
curl_exec($ch);
curl_close($ch);
}
private function sendRedis($data) {
try {
$redis = new Redis();
$redis->connect($this->config['host'], $this->config['port'] ?? 6379, 1);
$redis->rPush('app_logs:' . $this->config['app_name'], $data);
$redis->close();
} catch (\Exception $e) {
// Redis 连接失败,静默处理
}
}
// 便捷方法
public function debug($message, array $context = []) {
return $this->log('debug', $message, $context);
}
public function info($message, array $context = []) {
return $this->log('info', $message, $context);
}
public function warning($message, array $context = []) {
return $this->log('warning', $message, $context);
}
public function error($message, array $context = []) {
return $this->log('error', $message, $context);
}
public function critical($message, array $context = []) {
return $this->log('critical', $message, $context);
}
}
// 全局函数
function remote_log($level, $message, array $context = []) {
return RemoteLogger::getInstance()->log($level, $message, $context);
}
// 使用示例
// 初始化
$logger = RemoteLogger::getInstance([
'driver' => 'udp',
'host' => '192.168.1.100',
'port' => 9999,
'app_name' => 'ecommerce',
'async' => true
]);
// 记录日志
$logger->info('用户注册成功', ['user_id' => 12345, 'email' => 'user@example.com']);
$logger->error('支付失败', ['order_id' => 'ORD-2024-001', 'amount' => 99.99]);
// 或者使用全局函数
remote_log('alert', '服务器负载过高', ['cpu' => '95%', 'memory' => '80%']);
使用建议
- UDP方式:高性能,但不保证送达,适合大量日志
- HTTP方式:可靠性高,但性能稍差,适合重要日志
- Redis方式:平衡性能和可靠性,适合中等规模
- 建议使用异步发送,避免影响主业务
- 建议使用缓冲机制,减少网络请求次数
- 记录关键上下文,便于问题排查
选择哪种方式取决于你的具体需求:性能要求、可靠性要求、日志量等。