DebugLogger.php•7.08 kB
<?php
declare(strict_types=1);
namespace OpenFGA\MCP;
use function assert;
use function date;
use function dirname;
use function file_put_contents;
use function function_exists;
use function getmypid;
use function is_array;
use function is_dir;
use function is_string;
use function json_encode;
use function mkdir;
final class DebugLogger
{
private static ?string $logDir = null;
private static ?string $logFile = null;
/**
* @param array<string, mixed>|null $context
* @param string $error
* @param ?string $id
*/
public static function logError(string $error, ?string $id = null, ?array $context = null): void
{
if (! self::isDebugEnabled()) {
return;
}
self::initializePaths();
self::ensureLogDirectory();
$entry = [
'timestamp' => date('Y-m-d H:i:s'),
'type' => 'ERROR',
'id' => $id,
'error' => $error,
'context' => $context,
];
self::writeLog($entry);
}
/**
* @param array<string, mixed> $params
* @param string $method
* @param ?string $id
*/
public static function logRequest(string $method, array $params, ?string $id = null): void
{
if (! self::isDebugEnabled()) {
return;
}
self::initializePaths();
self::ensureLogDirectory();
$entry = [
'timestamp' => date('Y-m-d H:i:s'),
'type' => 'REQUEST',
'id' => $id,
'method' => $method,
'params' => $params,
];
self::writeLog($entry);
}
/**
* @param array<string, mixed> $response
* @param ?string $id
*/
public static function logResponse(array $response, ?string $id = null): void
{
if (! self::isDebugEnabled()) {
return;
}
self::initializePaths();
self::ensureLogDirectory();
$entry = [
'timestamp' => date('Y-m-d H:i:s'),
'type' => 'RESPONSE',
'id' => $id,
'response' => $response,
];
self::writeLog($entry);
}
/**
* @param array<string, mixed> $context
* @param string $event
*/
public static function logServerLifecycle(string $event, array $context = []): void
{
if (! self::isDebugEnabled()) {
return;
}
self::initializePaths();
self::ensureLogDirectory();
$entry = [
'timestamp' => date('Y-m-d H:i:s'),
'type' => 'SERVER_LIFECYCLE',
'event' => $event,
'context' => $context,
'pid' => getmypid(),
];
self::writeLog($entry);
}
/**
* @param array<int, mixed> $arguments
* @param string $toolName
* @param mixed $result
* @param ?string $id
*/
public static function logToolCall(string $toolName, array $arguments, mixed $result, ?string $id = null): void
{
if (! self::isDebugEnabled()) {
return;
}
self::initializePaths();
self::ensureLogDirectory();
$entry = [
'timestamp' => date('Y-m-d H:i:s'),
'type' => 'TOOL_CALL',
'id' => $id,
'tool' => $toolName,
'arguments' => $arguments,
'result' => is_array($result) ? $result : ['value' => $result],
];
self::writeLog($entry);
}
private static function ensureLogDirectory(): void
{
assert(null !== self::$logDir, 'Log directory path must be initialized');
assert(null !== self::$logFile, 'Log file path must be initialized');
$logDir = self::$logDir; // For Psalm null-safety
$logFile = self::$logFile; // For Psalm null-safety
if (! is_dir($logDir) && ! mkdir($logDir, 0o755, true)) {
// If we can't create the directory, log to stderr as fallback
$inTestEnvironment = function_exists('test') || function_exists('it') || class_exists('PHPUnit\Framework\TestCase');
if (! $inTestEnvironment) {
error_log('[MCP DEBUG] Failed to create log directory: ' . $logDir);
}
return;
}
// On first run, log where we're writing to help with debugging
static $logged = false;
if (! $logged) {
$inTestEnvironment = function_exists('test') || function_exists('it') || class_exists('PHPUnit\Framework\TestCase');
if (! $inTestEnvironment) {
error_log('[MCP DEBUG] Logging to: ' . $logFile);
}
$logged = true;
}
}
/**
* @param array<string, mixed> $entry
*/
/**
* Get the project root directory by finding composer.json.
*/
private static function getProjectRoot(): string
{
static $projectRoot = null;
if (null === $projectRoot) {
// Start from the current file's directory and walk up to find composer.json
$dir = __DIR__;
while ($dir !== dirname($dir)) {
if (file_exists($dir . '/composer.json')) {
$projectRoot = $dir;
break;
}
$dir = dirname($dir);
}
// Fallback to the directory above src if composer.json not found
if (null === $projectRoot) {
$projectRoot = dirname(__DIR__);
}
}
// PHPStan doesn't realize $projectRoot is guaranteed to be set above
assert(is_string($projectRoot));
return $projectRoot;
}
private static function initializePaths(): void
{
if (null === self::$logDir) {
self::$logDir = self::getProjectRoot() . '/logs';
self::$logFile = self::$logDir . '/mcp-debug.log';
}
}
private static function isDebugEnabled(): bool
{
// Default to true for debugging
return getConfiguredBool('OPENFGA_MCP_DEBUG', true);
}
/**
* @param array<string, mixed> $entry
*/
private static function writeLog(array $entry): void
{
$jsonString = json_encode($entry, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
if (false === $jsonString) {
$jsonString = '{"error": "Failed to encode log entry"}';
}
$logLine = $jsonString . "\n";
assert(null !== self::$logFile, 'Log file path must be initialized');
$logFile = self::$logFile;
$result = file_put_contents($logFile, $logLine, FILE_APPEND | LOCK_EX);
if (false === $result) {
$inTestEnvironment = function_exists('test') || function_exists('it') || class_exists('PHPUnit\Framework\TestCase');
if (! $inTestEnvironment) {
error_log('[MCP DEBUG] Failed to write to log file: ' . $logFile);
}
}
}
}