StreamLogger.php
4.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
<?php
namespace Jiaoyin;
class StreamLogger
{
protected string $basePath;
protected array $buffer = [];
protected int $flushSize = 50; // 缓冲条数
protected int $flushInterval = 1000; // 最长缓存时间(毫秒)
protected array $lastWrite = []; // 每个 channel 上次写盘时间戳(毫秒)
protected bool $timerCreated = false; // 是否已创建定时器
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);
}
// 自动延迟启动(防止主程序部分服务未完成初始化。)
$this->bootTickSafely();
}
protected function bootTickSafely()
{
// 如果已经创建,避免重复
if (!empty($this->timerCreated)) {
return;
}
$this->timerCreated = true;
// 如果已经在协程环境 → 直接创建 tick
if (\Swoole\Coroutine::getCid() > 0) {
$this->startTick();
return;
}
// 不在协程 → 用 defer 延迟到协程环境创建后
\Swoole\Coroutine::defer(function () {
$this->startTick();
});
}
protected function startTick()
{
\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;
}
}