作者 zed

增加日志分标记写入功能。

  1 +<?php
  2 +
  3 +namespace Jiaoyin;
  4 +
  5 +class StreamLogger
  6 +{
  7 + protected string $basePath;
  8 + protected array $buffer = [];
  9 + protected int $flushSize = 50; // 缓冲条数
  10 + protected int $flushInterval = 1000; // 最长缓存时间(毫秒)
  11 + protected array $lastWrite = []; // 每个 channel 上次写盘时间戳(毫秒)
  12 +
  13 + public function __construct(string $basePath = null)
  14 + {
  15 + // 优先使用传入的
  16 + if ($basePath) {
  17 + $this->basePath = rtrim($basePath, '/');
  18 + } else {
  19 + // 没传才自动查找项目根目录
  20 + $this->basePath = $this->findProjectRoot() . '/logs';
  21 + }
  22 +
  23 + if (!is_dir($this->basePath)) {
  24 + mkdir($this->basePath, 0777, true);
  25 + }
  26 +
  27 + // 启动定时器(仅 Swoole 环境有效)
  28 + if (class_exists(\Swoole\Timer::class)) {
  29 + \Swoole\Timer::tick(1000, function () {
  30 + $this->flushByInterval();
  31 + });
  32 + }
  33 + }
  34 +
  35 + public function log($channel, $content): void
  36 + {
  37 + $time = $this->timeFormat('ms');
  38 + $msg = "[$time] " . (is_array($content)
  39 + ? json_encode($content, JSON_UNESCAPED_UNICODE)
  40 + : $content
  41 + );
  42 +
  43 + $this->buffer[$channel][] = $msg;
  44 +
  45 + // 更新最后写入时间
  46 + $this->lastWrite[$channel] = $this->nowMs();
  47 +
  48 + // 数量超限:立即 flush
  49 + if (count($this->buffer[$channel]) >= $this->flushSize) {
  50 + $this->flush($channel);
  51 + }
  52 + }
  53 +
  54 + public function error($channel, $content): void
  55 + {
  56 + $this->log($channel . '_error', $content);
  57 + }
  58 +
  59 + // 定时器触发:超过时间未写盘则 flush
  60 + protected function flushByInterval(): void
  61 + {
  62 + $now = $this->nowMs();
  63 +
  64 + foreach ($this->buffer as $channel => $list) {
  65 + if (empty($list)) continue;
  66 +
  67 + $last = $this->lastWrite[$channel] ?? 0;
  68 +
  69 + if ($now - $last >= $this->flushInterval) {
  70 + $this->flush($channel);
  71 + }
  72 + }
  73 + }
  74 +
  75 + public function flush($channel = null): void
  76 + {
  77 + if ($channel === null) {
  78 + foreach ($this->buffer as $ch => $_) {
  79 + $this->writeChannel($ch);
  80 + }
  81 + } else {
  82 + $this->writeChannel($channel);
  83 + }
  84 + }
  85 +
  86 + protected function writeChannel($channel): void
  87 + {
  88 + if (empty($this->buffer[$channel])) return;
  89 +
  90 + $file = "{$this->basePath}/{$channel}.log";
  91 +
  92 + file_put_contents($file, implode(PHP_EOL, $this->buffer[$channel]) . PHP_EOL, FILE_APPEND);
  93 +
  94 + $this->buffer[$channel] = [];
  95 + $this->lastWrite[$channel] = $this->nowMs();
  96 + }
  97 +
  98 + protected function findProjectRoot(): string
  99 + {
  100 + $dir = __DIR__;
  101 + while ($dir !== '/' && $dir !== '.') {
  102 + if (basename($dir) === 'vendor') {
  103 + return dirname($dir); // vendor 的父级 → 项目根目录
  104 + }
  105 + if (is_dir($dir . '/vendor')) {
  106 + return $dir; // 当前目录就是项目根目录
  107 + }
  108 + $dir = dirname($dir); // 继续往上
  109 + }
  110 +
  111 + return __DIR__; // 兜底
  112 + }
  113 +
  114 + protected function nowMs(): int
  115 + {
  116 + return (int)(microtime(true) * 1000);
  117 + }
  118 +
  119 + protected function timeFormat($type = 's', $format = 'Y-m-d H:i:s'): string
  120 + {
  121 + [$ms, $sec] = explode(' ', microtime());
  122 + $t = date($format, $sec);
  123 + return $type === 'ms'
  124 + ? $t . '.' . sprintf('%03d', floor($ms * 1000))
  125 + : $t;
  126 + }
  127 +}