PSR-3 Logger Interface

PHP-FIGによるロギングライブラリの共通インターフェース標準。8つのログレベル(emergency、alert、critical、error、warning、notice、info、debug)を定義。ライブラリ間の相互運用性とコード再利用性を促進する抽象化層。

ロギングライブラリPHPPSR-3標準インターフェース相互運用性

ライブラリ

PSR-3 Logger Interface

概要

PSR-3 Logger InterfaceはPHP-FIG(PHP Framework Interop Group)により策定されたロギングライブラリの共通インターフェース標準です。8つのログレベル(emergency、alert、critical、error、warning、notice、info、debug)を定義し、異なるロギング実装間の相互運用性とコード再利用性を促進する抽象化層として機能。2025年現在、PHPエコシステムで必須の標準として完全に定着し、フレームワークとライブラリの統一的な基盤として機能しています。

詳細

PSR-3は「ライブラリが Psr\Log\LoggerInterface オブジェクトを受け取り、シンプルで汎用的な方法でログを記録できる」という目標のもと設計されました。RFC 5424に準拠した8つのログレベルメソッドと、任意のレベルを受け取るlogメソッドを提供します。メッセージプレースホルダー機能により、コンテキスト配列の値を使用した動的なログメッセージ生成が可能。実装固有のロギングライブラリに依存しないコードの作成を可能にし、デプロイ時に適切な実装を選択できる柔軟性を提供します。

主な特徴

  • 標準インターフェース: RFC 5424準拠の8レベルロギング
  • メッセージプレースホルダー: 動的コンテキスト挿入機能
  • 実装非依存: 任意のPSR-3準拠ライブラリとの互換性
  • 依存性注入対応: クリーンアーキテクチャサポート
  • NullLoggerパターン: テスト・開発時の安全なフォールバック
  • LoggerAwareTrait: 簡単なロガー統合

メリット・デメリット

メリット

  • PHPエコシステム全体での圧倒的な普及と統一性確保
  • フレームワーク・ライブラリ間の完全な相互運用性実現
  • 実装ライブラリの自由な選択と切り替えが可能
  • 依存性注入パターンによるテスタブルなコード設計
  • MonologやSymfony Loggerなど豊富な実装選択肢
  • 外部モジュールとの統合が極めて容易

デメリット

  • インターフェースのみで実際のロギング機能は別途必要
  • パフォーマンス最適化は実装ライブラリに依存
  • 高度な機能(構造化ログ等)はライブラリ固有機能に限定
  • インターフェース抽象化によるわずかなオーバーヘッド
  • 学習コストは使用する実装ライブラリの複雑さに依存
  • 型安全性はPHP言語機能の制約を受ける

参考ページ

書き方の例

インターフェース実装とセットアップ

<?php
// PSR-3パッケージのインストール
// composer require psr/log

use Psr\Log\LoggerInterface;
use Psr\Log\AbstractLogger;
use Psr\Log\LogLevel;

// 基本的なカスタムロガー実装
class SimpleLogger extends AbstractLogger
{
    public function log($level, $message, array $context = [])
    {
        $timestamp = date('Y-m-d H:i:s');
        $interpolated = $this->interpolate($message, $context);
        
        echo sprintf(
            "[%s] %s: %s\n", 
            $timestamp, 
            strtoupper($level), 
            $interpolated
        );
    }
    
    private function interpolate($message, array $context = [])
    {
        $replace = [];
        foreach ($context as $key => $val) {
            if (!is_array($val) && (!is_object($val) || method_exists($val, '__toString'))) {
                $replace['{'.$key.'}'] = $val;
            }
        }
        return strtr($message, $replace);
    }
}

// LoggerAwareTraitの使用
use Psr\Log\LoggerAwareTrait;
use Psr\Log\NullLogger;

class UserService
{
    use LoggerAwareTrait;
    
    public function __construct()
    {
        // NullLoggerをデフォルトとして設定
        $this->logger = new NullLogger();
    }
    
    public function createUser(array $userData)
    {
        $this->logger->info('ユーザー作成開始', ['data' => $userData]);
        
        try {
            // ユーザー作成処理
            $userId = $this->doCreateUser($userData);
            $this->logger->info('ユーザー作成完了', ['user_id' => $userId]);
            return $userId;
        } catch (Exception $e) {
            $this->logger->error('ユーザー作成失敗', [
                'exception' => $e,
                'user_data' => $userData
            ]);
            throw $e;
        }
    }
}

ログレベルとコンテキスト使用

<?php
// 8つのログレベルの使用例
class ApplicationLogger
{
    private LoggerInterface $logger;
    
    public function __construct(LoggerInterface $logger)
    {
        $this->logger = $logger;
    }
    
    public function demonstrateLogLevels()
    {
        // Emergency: システム使用不可
        $this->logger->emergency('データベース接続完全停止', [
            'error_code' => 'DB_FATAL',
            'timestamp' => time()
        ]);
        
        // Alert: 即座の対応が必要
        $this->logger->alert('ディスク容量が臨界値に達成', [
            'disk_usage' => '98%',
            'available_space' => '100MB'
        ]);
        
        // Critical: 深刻な状況
        $this->logger->critical('決済処理でクリティカルエラー', [
            'payment_id' => 'pay_123456',
            'amount' => 15000,
            'currency' => 'JPY'
        ]);
        
        // Error: 実行時エラー(即座の対応不要)
        $this->logger->error('ファイルアップロード失敗', [
            'file_name' => 'document.pdf',
            'file_size' => 2048000,
            'error' => 'Permission denied'
        ]);
        
        // Warning: 例外的だが必ずしもエラーでない事象
        $this->logger->warning('API レスポンス時間が閾値超過', [
            'endpoint' => '/api/users',
            'response_time' => 5200,
            'threshold' => 3000
        ]);
        
        // Notice: 通常だが重要な事象
        $this->logger->notice('新規ユーザー登録完了', [
            'user_id' => 12345,
            'registration_method' => 'email',
            'user_ip' => '192.168.1.100'
        ]);
        
        // Info: 情報メッセージ
        $this->logger->info('ユーザー {username} がログインしました', [
            'username' => 'tanaka_taro',
            'login_time' => date('c'),
            'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? 'unknown'
        ]);
        
        // Debug: デバッグ情報
        $this->logger->debug('SQL クエリ実行', [
            'query' => 'SELECT * FROM users WHERE active = ?',
            'params' => [true],
            'execution_time' => 0.045
        ]);
    }
    
    // 汎用logメソッドの使用
    public function logWithDynamicLevel(string $level, string $message, array $context = [])
    {
        $this->logger->log($level, $message, $context);
    }
}

フレームワーク統合パターン

<?php
// Symfonyでの使用例
use Symfony\Component\DependencyInjection\ContainerInterface;
use Psr\Log\LoggerInterface;

class SymfonyIntegrationExample
{
    private LoggerInterface $logger;
    
    public function __construct(LoggerInterface $logger)
    {
        $this->logger = $logger;
    }
    
    public function processOrder(array $orderData)
    {
        $this->logger->info('注文処理開始', ['order' => $orderData]);
        
        // 注文処理ロジック
        return $this->handleOrderProcessing($orderData);
    }
}

// services.yaml での設定
/*
services:
    App\Service\OrderService:
        arguments:
            $logger: '@monolog.logger.order'
*/

// LaravelでのPSR-3使用例
use Illuminate\Support\Facades\Log;
use Psr\Log\LoggerInterface;

class LaravelIntegrationExample
{
    private LoggerInterface $logger;
    
    public function __construct()
    {
        // LaravelのLogファサードもPSR-3準拠
        $this->logger = Log::channel('application');
    }
    
    public function handlePayment(float $amount, string $currency)
    {
        $this->logger->info('決済処理開始', [
            'amount' => $amount,
            'currency' => $currency,
            'timestamp' => now()->toISOString()
        ]);
        
        try {
            $result = $this->processPayment($amount, $currency);
            $this->logger->info('決済処理成功', ['result' => $result]);
            return $result;
        } catch (PaymentException $e) {
            $this->logger->error('決済処理失敗', [
                'exception' => $e->getMessage(),
                'amount' => $amount,
                'currency' => $currency
            ]);
            throw $e;
        }
    }
}

依存性注入とテストパターン

<?php
use Psr\Log\LoggerInterface;
use Psr\Log\Test\TestLogger;
use PHPUnit\Framework\TestCase;

// プロダクションクラス
class EmailService
{
    private LoggerInterface $logger;
    private MailerInterface $mailer;
    
    public function __construct(LoggerInterface $logger, MailerInterface $mailer)
    {
        $this->logger = $logger;
        $this->mailer = $mailer;
    }
    
    public function sendEmail(string $to, string $subject, string $body): bool
    {
        $this->logger->info('メール送信開始', [
            'to' => $to,
            'subject' => $subject
        ]);
        
        try {
            $result = $this->mailer->send($to, $subject, $body);
            
            if ($result) {
                $this->logger->info('メール送信成功', ['to' => $to]);
            } else {
                $this->logger->warning('メール送信失敗', ['to' => $to]);
            }
            
            return $result;
        } catch (Exception $e) {
            $this->logger->error('メール送信エラー', [
                'to' => $to,
                'exception' => $e->getMessage()
            ]);
            return false;
        }
    }
}

// テストクラス
class EmailServiceTest extends TestCase
{
    public function testSuccessfulEmailSending()
    {
        // TestLoggerを使用してログ出力をテスト
        $logger = new TestLogger();
        $mailer = $this->createMock(MailerInterface::class);
        $mailer->method('send')->willReturn(true);
        
        $emailService = new EmailService($logger, $mailer);
        
        $result = $emailService->sendEmail(
            '[email protected]',
            'テストメール',
            'これはテストです'
        );
        
        $this->assertTrue($result);
        
        // ログ出力の検証
        $this->assertTrue($logger->hasInfo('メール送信開始'));
        $this->assertTrue($logger->hasInfo('メール送信成功'));
        
        // ログコンテキストの検証
        $records = $logger->records;
        $this->assertEquals('[email protected]', $records[0]['context']['to']);
        $this->assertEquals('テストメール', $records[0]['context']['subject']);
    }
    
    public function testEmailSendingWithException()
    {
        $logger = new TestLogger();
        $mailer = $this->createMock(MailerInterface::class);
        $mailer->method('send')->willThrowException(new Exception('SMTP エラー'));
        
        $emailService = new EmailService($logger, $mailer);
        
        $result = $emailService->sendEmail('[email protected]', 'テスト', 'テスト');
        
        $this->assertFalse($result);
        $this->assertTrue($logger->hasError('メール送信エラー'));
    }
}

Monolog実装との組み合わせ

<?php
// Monologを使ったPSR-3実装例
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use Monolog\Handler\RotatingFileHandler;
use Monolog\Processor\IntrospectionProcessor;
use Monolog\Formatter\JsonFormatter;

class MonologIntegrationExample
{
    public static function createApplicationLogger(): LoggerInterface
    {
        $logger = new Logger('application');
        
        // コンソール出力ハンドラー
        $consoleHandler = new StreamHandler('php://stdout', Logger::DEBUG);
        $logger->pushHandler($consoleHandler);
        
        // ローテーションファイルハンドラー
        $fileHandler = new RotatingFileHandler(
            '/var/log/application.log',
            30, // 30日間保持
            Logger::INFO
        );
        $fileHandler->setFormatter(new JsonFormatter());
        $logger->pushHandler($fileHandler);
        
        // プロセッサー追加(ファイル名・行番号情報)
        $logger->pushProcessor(new IntrospectionProcessor());
        
        return $logger;
    }
    
    public static function createTestLogger(): LoggerInterface
    {
        // テスト環境用のシンプルなロガー
        $logger = new Logger('test');
        $handler = new StreamHandler('/tmp/test.log', Logger::DEBUG);
        $logger->pushHandler($handler);
        
        return $logger;
    }
}

// 使用例
$logger = MonologIntegrationExample::createApplicationLogger();
$userService = new UserService();
$userService->setLogger($logger);

// PSR-3インターフェースを通じて使用
$logger->info('アプリケーション起動', [
    'version' => '1.0.0',
    'environment' => 'production',
    'memory_usage' => memory_get_usage(true)
]);

高度なコンテキスト処理

<?php
// 高度なコンテキスト処理とプレースホルダー
class AdvancedLoggingExample
{
    private LoggerInterface $logger;
    
    public function __construct(LoggerInterface $logger)
    {
        $this->logger = $logger;
    }
    
    public function processComplexOperation(array $data)
    {
        $operationId = uniqid('op_');
        $startTime = microtime(true);
        
        // 開始ログ(構造化コンテキスト)
        $this->logger->info('複雑な処理を開始: {operation_id}', [
            'operation_id' => $operationId,
            'input_data_count' => count($data),
            'start_time' => $startTime,
            'memory_before' => memory_get_usage(),
            'process_id' => getmypid()
        ]);
        
        try {
            foreach ($data as $index => $item) {
                $this->processItem($item, $operationId, $index);
            }
            
            $endTime = microtime(true);
            $duration = $endTime - $startTime;
            
            // 成功ログ(パフォーマンス情報含む)
            $this->logger->info('処理完了: {operation_id}', [
                'operation_id' => $operationId,
                'duration_seconds' => round($duration, 3),
                'items_processed' => count($data),
                'memory_peak' => memory_get_peak_usage(),
                'memory_after' => memory_get_usage()
            ]);
            
        } catch (Exception $e) {
            $this->logger->error('処理中にエラーが発生: {operation_id}', [
                'operation_id' => $operationId,
                'exception_class' => get_class($e),
                'exception_message' => $e->getMessage(),
                'exception_file' => $e->getFile(),
                'exception_line' => $e->getLine(),
                'stack_trace' => $e->getTraceAsString()
            ]);
            throw $e;
        }
    }
    
    private function processItem($item, string $operationId, int $index)
    {
        $this->logger->debug('アイテム処理: {operation_id}[{index}]', [
            'operation_id' => $operationId,
            'index' => $index,
            'item_type' => gettype($item),
            'item_size' => is_string($item) ? strlen($item) : null
        ]);
        
        // アイテム処理ロジック
        if (is_array($item) && !empty($item['error'])) {
            $this->logger->warning('問題のあるアイテム: {operation_id}[{index}]', [
                'operation_id' => $operationId,
                'index' => $index,
                'error_details' => $item['error']
            ]);
        }
    }
}