|
|
|
<?php
|
|
|
|
|
|
|
|
namespace Jiaoyin;
|
|
|
|
|
|
|
|
class StreamLogger
|
|
|
|
{
|
|
|
|
protected string $basePath;
|
|
|
|
protected array $buffer = [];
|
|
|
|
protected int $flushSize = 50; // 缓冲条数
|
|
|
|
protected int $flushInterval = 1000; // 最长缓存时间(毫秒)
|
|
|
|
protected array $lastWrite = []; // 每个 channel 上次写盘时间戳(毫秒)
|
|
|
|
|
|
|
|
public function __construct(string $basePath = null)
|
|
|
|
{
|
|
|
|
// 优先使用传入的
|
|
|
|
if ($basePath) {
|
|
|
|
$this->basePath = rtrim($basePath, '/');
|
|
|
|
} else {
|
|
|
|
// 没传才自动查找项目根目录
|
|
|
|
$this->basePath = $this->findProjectRoot() . '/logs';
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!is_dir($this->basePath)) {
|
|
|
|
mkdir($this->basePath, 0777, true);
|
|
|
|
}
|
|
|
|
|
|
|
|
// 启动定时器(仅 Swoole 环境有效)
|
|
|
|
if (class_exists(\Swoole\Timer::class)) {
|
|
|
|
\Swoole\Timer::tick(1000, function () {
|
|
|
|
$this->flushByInterval();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public function log($channel, $content): void
|
|
|
|
{
|
|
|
|
$time = $this->timeFormat('ms');
|
|
|
|
$msg = "[$time] " . (is_array($content)
|
|
|
|
? json_encode($content, JSON_UNESCAPED_UNICODE)
|
|
|
|
: $content
|
|
|
|
);
|
|
|
|
|
|
|
|
$this->buffer[$channel][] = $msg;
|
|
|
|
|
|
|
|
// 更新最后写入时间
|
|
|
|
$this->lastWrite[$channel] = $this->nowMs();
|
|
|
|
|
|
|
|
// 数量超限:立即 flush
|
|
|
|
if (count($this->buffer[$channel]) >= $this->flushSize) {
|
|
|
|
$this->flush($channel);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public function error($channel, $content): void
|
|
|
|
{
|
|
|
|
$this->log($channel . '_error', $content);
|
|
|
|
}
|
|
|
|
|
|
|
|
// 定时器触发:超过时间未写盘则 flush
|
|
|
|
protected function flushByInterval(): void
|
|
|
|
{
|
|
|
|
$now = $this->nowMs();
|
|
|
|
|
|
|
|
foreach ($this->buffer as $channel => $list) {
|
|
|
|
if (empty($list)) continue;
|
|
|
|
|
|
|
|
$last = $this->lastWrite[$channel] ?? 0;
|
|
|
|
|
|
|
|
if ($now - $last >= $this->flushInterval) {
|
|
|
|
$this->flush($channel);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public function flush($channel = null): void
|
|
|
|
{
|
|
|
|
if ($channel === null) {
|
|
|
|
foreach ($this->buffer as $ch => $_) {
|
|
|
|
$this->writeChannel($ch);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
$this->writeChannel($channel);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
protected function writeChannel($channel): void
|
|
|
|
{
|
|
|
|
if (empty($this->buffer[$channel])) return;
|
|
|
|
|
|
|
|
$file = "{$this->basePath}/{$channel}.log";
|
|
|
|
|
|
|
|
file_put_contents($file, implode(PHP_EOL, $this->buffer[$channel]) . PHP_EOL, FILE_APPEND);
|
|
|
|
|
|
|
|
$this->buffer[$channel] = [];
|
|
|
|
$this->lastWrite[$channel] = $this->nowMs();
|
|
|
|
}
|
|
|
|
|
|
|
|
protected function findProjectRoot(): string
|
|
|
|
{
|
|
|
|
$dir = __DIR__;
|
|
|
|
while ($dir !== '/' && $dir !== '.') {
|
|
|
|
if (basename($dir) === 'vendor') {
|
|
|
|
return dirname($dir); // vendor 的父级 → 项目根目录
|
|
|
|
}
|
|
|
|
if (is_dir($dir . '/vendor')) {
|
|
|
|
return $dir; // 当前目录就是项目根目录
|
|
|
|
}
|
|
|
|
$dir = dirname($dir); // 继续往上
|
|
|
|
}
|
|
|
|
|
|
|
|
return __DIR__; // 兜底
|
|
|
|
}
|
|
|
|
|
|
|
|
protected function nowMs(): int
|
|
|
|
{
|
|
|
|
return (int)(microtime(true) * 1000);
|
|
|
|
}
|
|
|
|
|
|
|
|
protected function timeFormat($type = 's', $format = 'Y-m-d H:i:s'): string
|
|
|
|
{
|
|
|
|
[$ms, $sec] = explode(' ', microtime());
|
|
|
|
$t = date($format, $sec);
|
|
|
|
return $type === 'ms'
|
|
|
|
? $t . '.' . sprintf('%03d', floor($ms * 1000))
|
|
|
|
: $t;
|
|
|
|
}
|
|
|
|
} |
...
|
...
|
|