// +---------------------------------------------------------------------- declare (strict_types = 1); namespace app\log\driver; use think\App; use think\contract\LogHandlerInterface; use think\facade\Config; /** * 本地化调试输出到文件 */ class Monolog implements LogHandlerInterface { /** * 配置参数 * @var array */ protected $config = [ 'time_format' => 'c', 'json_options' => JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES, ]; /** * 日志写入接口 * @access public * @param array $log 日志信息 * @return bool */ public function save(array $log): bool { $info = []; // 日志信息封装 $time = \DateTime::createFromFormat('0.u00 U', microtime())->setTimezone(new \DateTimeZone(date_default_timezone_get()))->format($this->config['time_format']); foreach ($log as $type => $val) { $message = []; foreach ($val as $msg) { $message[] = json_encode(['time' => $time, 'type' => $type, 'msg' => $msg], $this->config['json_options']) ; } $info[$type] = $message; } if ($info) { return $this->write($info); } return true; } /** * 日志写入 * @access protected * @param array $message 日志信息 * @param string $destination 日志文件 * @return bool */ protected function write(array $message): bool { $tcp_log_url = env('TCP_LOG_URL'); $tcp_log_port = (int) env('TCP_LOG_PORT'); $project = env('PROJECT_NAME'); // 快速失败:配置缺失 if (!$tcp_log_url || !$tcp_log_port || !$project) { return true; // 或 error_log("Log config missing"); } // 1. 安全序列化并限制大小(例如最大 64KB) $maxSize = 64 * 1024; // 64KB $json = json_encode([ 'message' => $message, 'project' => $project ], $this->config['json_options'] ?? 0); if ($json === false) { error_log("Failed to encode log message: " . json_last_error_msg()); return true; } // 截断过大的日志(保留结构完整性) if (strlen($json) > $maxSize) { // 尝试只截断 message 内容(更智能) $fallbackMessage = [ 'message' => '[TRUNCATED] Log too large (' . strlen($json) . ' bytes)', 'project' => $project, 'original_size_bytes' => strlen($json) ]; $json = json_encode($fallbackMessage, $this->config['json_options'] ?? 0); } // 2. 添加换行符(Logstash tcp + json_lines 必需) $payload = $json . "\n"; // 3. 建立连接(适当延长超时) $socket = @fsockopen($tcp_log_url, $tcp_log_port, $errno, $errstr, 2); // 2秒超时 if (!$socket) { error_log("Log socket connect failed to {$tcp_log_url}:{$tcp_log_port} - {$errstr} ({$errno})"); return true; // 不影响主流程 } // 4. 发送数据(抑制 fwrite 警告,但可选记录) $bytesWritten = @fwrite($socket, $payload); $expected = strlen($payload); if ($bytesWritten === false || $bytesWritten !== $expected) { error_log("Partial or failed log send to {$tcp_log_url}:{$tcp_log_port} (sent {$bytesWritten}/{$expected} bytes)"); } // 5. 确保关闭 socket fclose($socket); return true; } }