StreamLogger.php 3.5 KB
<?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;
    }
}