Files
sam-manage/app/Services/FlowTester/ApiLogCapturer.php
hskwon fe10cae06c feat: [flow-tester] API 로그 캡처 및 UI 개선
- ApiLogCapturer 추가: 플로우 실행 중 API 로그 캡처
- resolveBaseUrl() 추가: .env 환경변수 기반 baseUrl 지원
- 실행 상세 페이지: 스텝별 접기/펼치기 기능 (성공=접힘, 실패=펼침)
- JSON 가이드 및 예제 플로우 최신화
- AI 프롬프트 템플릿 업데이트
- bindExpectVariables() 추가: expect jsonPath 값에 변수 바인딩 적용
- areNumericEqual() 추가: 숫자 타입 유연 비교 ("2" == 2)
2025-12-04 15:57:56 +09:00

171 lines
4.9 KiB
PHP

<?php
namespace App\Services\FlowTester;
/**
* API 로그 캡처 서비스
*
* API 서버의 laravel.log에서 Request/Response 로그를 캡처합니다.
* 플로우 실행 시작/종료 시점의 파일 오프셋을 이용하여
* 해당 실행 중에 발생한 로그만 추출합니다.
*/
class ApiLogCapturer
{
private string $logPath;
private int $startOffset = 0;
public function __construct()
{
// API 로그 파일 경로 (Docker/로컬 환경 모두 지원)
$this->logPath = $this->resolveLogPath();
}
/**
* 캡처 시작 (현재 로그 파일 위치 기록)
*/
public function start(): void
{
if (file_exists($this->logPath)) {
$this->startOffset = filesize($this->logPath);
} else {
$this->startOffset = 0;
}
}
/**
* 캡처 종료 및 로그 추출
*
* @return array 파싱된 API 로그 배열
*/
public function capture(): array
{
if (! file_exists($this->logPath)) {
return [];
}
$currentSize = filesize($this->logPath);
// 새로운 로그가 없으면 빈 배열 반환
if ($currentSize <= $this->startOffset) {
return [];
}
// 새로 추가된 로그 읽기
$handle = fopen($this->logPath, 'r');
if (! $handle) {
return [];
}
fseek($handle, $this->startOffset);
$newLogs = fread($handle, $currentSize - $this->startOffset);
fclose($handle);
// Request/Response 로그만 파싱
return $this->parseLogs($newLogs);
}
/**
* API 로그 파일 경로 결정
*/
private function resolveLogPath(): string
{
// 1. 환경 변수로 지정된 경로
$envPath = env('API_LOG_PATH');
if ($envPath && file_exists($envPath)) {
return $envPath;
}
// 2. 로컬 개발 환경 (상대 경로)
$localPath = base_path('../api/storage/logs/laravel.log');
if (file_exists($localPath)) {
return $localPath;
}
// 3. Docker 환경 (절대 경로)
$dockerPath = '/var/www/api/storage/logs/laravel.log';
if (file_exists($dockerPath)) {
return $dockerPath;
}
// 4. 기본값 (로컬 경로)
return $localPath;
}
/**
* 로그 텍스트 파싱
*
* Laravel 로그 형식:
* [2025-12-04 12:30:53] local.INFO: API Request {...}
* [2025-12-04 12:30:53] local.INFO: API Response {...}
*/
private function parseLogs(string $rawLogs): array
{
$logs = [];
$pattern = '/\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\] \w+\.\w+: (API Request|API Response) (\{.+?\})(?=\s*\[\d{4}|\s*$)/s';
if (preg_match_all($pattern, $rawLogs, $matches, PREG_SET_ORDER)) {
foreach ($matches as $match) {
$timestamp = $match[1];
$type = $match[2] === 'API Request' ? 'request' : 'response';
$jsonStr = $match[3];
// JSON 파싱
$data = json_decode($jsonStr, true);
if ($data === null) {
continue;
}
$log = [
'timestamp' => $timestamp,
'type' => $type,
];
if ($type === 'request') {
$log['method'] = $data['method'] ?? '';
$log['uri'] = $data['uri'] ?? '';
$log['input'] = $data['input'] ?? [];
$log['ip'] = $data['ip'] ?? '';
} else {
$log['uri'] = $data['uri'] ?? '';
$log['status'] = $data['status'] ?? 0;
// content는 JSON 문자열이므로 파싱
$content = $data['content'] ?? '';
if (is_string($content)) {
$parsedContent = json_decode($content, true);
if ($parsedContent !== null) {
$log['success'] = $parsedContent['success'] ?? null;
$log['message'] = $parsedContent['message'] ?? '';
// 에러 정보가 있으면 포함
if (isset($parsedContent['error'])) {
$log['error'] = $parsedContent['error'];
}
}
}
}
$logs[] = $log;
}
}
return $logs;
}
/**
* 로그 파일 경로 반환 (디버깅용)
*/
public function getLogPath(): string
{
return $this->logPath;
}
/**
* 로그 파일 존재 여부 확인
*/
public function isAvailable(): bool
{
return file_exists($this->logPath) && is_readable($this->logPath);
}
}