Files
ulthon_admin/extend/think/log/DebugLogToolkit.php

251 lines
7.8 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
declare(strict_types=1);
namespace think\log;
use think\facade\App;
/**
* debug_log 行构建与 JSONL 追加写共享工具(纯静态、无状态).
*
* 行格式契约锚点FIELDS 常量是 T3异步驱动/T4同步驱动修复/T5Reader/T6导入任务
* 四方共享的字段契约,签名与语义变更必须四方同步。
*
* 与 think\log\driver\DebugMysql 的关系:
* - DebugMysql.save() 的行构建逻辑L89-136迁移至此共享
* - DebugMysql L89 把 app('http') 放在 CLI 判断之前是 bug 示范——
* 本类 buildRow 必须先判 App::runningInConsole()CLI 下禁止触碰 http/request。
*
* 落盘格式JSONL每行一个 JSON 对象UTF-8目标
* {runtimePath}log/{Ymd}.jsonl —— date('Ymd') 每次调用现算(长驻 CLI 进程跨天滚动)。
*
* 自动加载composer.json psr-0 fallback "": "extend/"(与 think\log\driver\DebugMysql 同机制)。
*/
class DebugLogToolkit
{
/**
* 行字段契约(顺序即 CSV 表头顺序,与 ul_debug_log 表字段一一对应).
*
* T5 Reader 侧将引用此常量校验 JSONL 行完整性;旧 CSV 文件表头 = 同序数组。
*/
public const FIELDS = [
'level',
'content',
'create_time',
'create_time_title',
'uid',
'app_name',
'controller_name',
'action_name',
];
/**
* 构建 debug_log 行8 字段,键序与 FIELDS 一致).
*
* CLI 判断顺序铁律App::runningInConsole() 在前——CLI 下 app_name='cli'、
* controller/action 空串,禁止触碰 app('http')/request()Web 分支三者各自
* 独立 try-catch 兜底空串(任一容器依赖缺失不拖垮整行写入)。
*
* @param string $level 日志级别LogRecord->type
* @param string $content 日志内容LogRecord->message调用侧保证已字符串化
*
* @return array 键值与 FIELDS 完全一致的关联数组
*/
public static function buildRow(string $level, string $content): array
{
$controllerName = '';
$actionName = '';
if (App::runningInConsole()) {
$appName = 'cli';
} else {
$appName = self::fetchAppName();
$controllerName = self::fetchRequestSegment('controller');
$actionName = self::fetchRequestSegment('action');
}
$createTime = time();
return [
'level' => $level,
'content' => $content,
'create_time' => $createTime,
'create_time_title' => date('Y-m-d H:i:s', $createTime),
'uid' => self::resolveUid(),
'app_name' => $appName,
'controller_name' => $controllerName,
'action_name' => $actionName,
];
}
/**
* 行数组 → 单物理行 JSON 字符串(不含换行符).
*
* 编码前逐字段做 UTF-8 清洗(非法字节替换为 '?');清洗后 json_encode 仍失败
* 则整体降级为 '[encoding-error]' 占位行——禁止让 json_encode 返回 false
* 写出空行毒化 JSONL 文件。
*
* @return string 非空单行 JSON任何异常路径都返回占位行永不抛出、永不为空串
*/
public static function encodeRow(array $row): string
{
try {
$clean = [];
foreach ($row as $key => $value) {
$clean[$key] = is_string($value)
? mb_convert_encoding($value, 'UTF-8', 'UTF-8')
: $value;
}
$json = json_encode($clean, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);
return $json === false || $json === '' ? '[encoding-error]' : $json;
} catch (\Throwable $th) {
return '[encoding-error]';
}
}
/**
* 追加写入一行到 JSONL 文件(目标 {runtimePath}log/{Ymd}.jsonl.
*
* - date('Ymd') 每次调用现算,禁止缓存——长驻 CLI 进程跨天滚动文件
* - 行尾保证单个 "\n"传入行缺尾换行自动补齐fwrite 后显式校验写入字节数
* - 全方法 catch \Throwable 返回 false绝不抛异常日志通道写入失败不能中断业务
*
* @param string $line 单行内容(建议为 encodeRow 输出;空行拒绝写入)
*/
public static function appendLine(string $line): bool
{
try {
if (trim($line) === '') {
return false;
}
if (substr($line, -1) !== "\n") {
$line .= "\n";
}
$dir = App::getRuntimePath() . 'log/';
if (!is_dir($dir) && !mkdir($dir, 0777, true) && !is_dir($dir)) {
return false;
}
$file = $dir . date('Ymd') . '.jsonl';
$handle = fopen($file, 'a');
if ($handle === false) {
return false;
}
if (!flock($handle, LOCK_EX)) {
fclose($handle);
return false;
}
$written = fwrite($handle, $line);
if ($written === false || $written !== strlen($line)) {
flock($handle, LOCK_UN);
fclose($handle);
return false;
}
flock($handle, LOCK_UN);
fclose($handle);
return true;
} catch (\Throwable $th) {
return false;
}
}
/**
* 解析单行 JSONL 为行数组.
*
* @return array|null 正常行返回关联数组;坏 JSON / 非数组 / FIELDS 键不全返回 null
*/
public static function decodeLine(string $line): ?array
{
try {
$decoded = json_decode($line, true, 512, JSON_THROW_ON_ERROR);
} catch (\Throwable $th) {
return null;
}
if (!is_array($decoded)) {
return null;
}
foreach (self::FIELDS as $field) {
if (!array_key_exists($field, $decoded)) {
return null;
}
}
return $decoded;
}
/**
* 判断 CSV 首行是否为旧格式 debug_log CSV 的表头行.
*
* 旧 CSVDebugMysql.saveByFile 产物,{ymd}.csv首行为 8 列字段名,
* 导入侧跳过表头不当作数据行入库。
*
* @param array $fields fgetcsv 读出的首行
*/
public static function isCsvHeader(array $fields): bool
{
return isset($fields[0]) && $fields[0] === 'level' && count($fields) === 8;
}
/**
* 解析请求 uid双常量兼容public/index.php:11 定义的是历史拼写 REUQEST_UID保留不改.
*
* @return string REUQEST_UID > REQUEST_UID > uniqid() 兜底
*/
protected static function resolveUid(): string
{
if (defined('REUQEST_UID')) {
return (string) REUQEST_UID;
}
if (defined('REQUEST_UID')) {
return (string) REQUEST_UID;
}
return uniqid();
}
/**
* Web 分支取 app_name独立兜底app('http') 缺失/异常返回空串).
*/
protected static function fetchAppName(): string
{
try {
$name = app('http')->getName();
return is_string($name) && $name !== '' ? $name : '';
} catch (\Throwable $th) {
return '';
}
}
/**
* Web 分支取 request 的 controller/action 段(独立兜底返回空串).
*
* @param string $segment 'controller' 或 'action'
*/
protected static function fetchRequestSegment(string $segment): string
{
try {
$value = request()->{$segment}();
return is_string($value) && $value !== '' ? $value : '';
} catch (\Throwable $th) {
return '';
}
}
}