Custom PSR-3 Logger

Custom logger implementing PSR-3 interface. Enables custom implementation tailored to specific requirements, handling special output destinations and processing logic not covered by existing libraries. Used in framework development and special applications.

LoggingPHPPSR-3Custom ImplementationFramework Development

Library

Custom PSR-3 Logger

Overview

Custom PSR-3 Logger is a custom logger implementation that adheres to the PSR-3 interface. It enables the creation of tailored solutions for specific requirements and handles special output destinations or processing logic that existing libraries cannot accommodate. This flexible logging solution is used in framework development or special-purpose applications. By adhering to PSR-3, it ensures compatibility with other libraries while allowing the implementation of custom features.

Details

Custom PSR-3 Logger in 2025 continues to meet demand from framework developers and projects with special requirements. By adhering to the PSR-3 interface, it ensures compatibility with other PHP libraries while providing the flexibility to implement custom logging functionality. It can handle special output destinations (custom APIs, proprietary protocols, encrypted logs) and processing logic (log filtering, transformation, aggregation) that generic libraries like Monolog cannot achieve. The design allows for complete customization according to requirements.

Key Features

  • PSR-3 Compliance: Compatibility guaranteed through standard interfaces
  • Complete Customization: Custom implementation possible according to requirements
  • Flexible Output Destinations: Support for any output destination or protocol
  • Extensibility: Ability to add custom features and filtering logic
  • Framework Integration: Seamless integration with other PHP libraries
  • Testability: Easy mocking and testing due to interface compliance

Pros and Cons

Pros

  • Ensures complete compatibility with other libraries through PSR-3 compliance
  • Flexibility to fully accommodate special requirements and output destinations
  • Easy integration with existing log processing systems
  • Ability to implement custom logging strategies during framework development
  • Performance optimization possible according to performance requirements
  • Support for enterprise-specific security requirements and compliance

Cons

  • High implementation and maintenance costs
  • Risk of insufficient testing compared to mature existing libraries
  • High learning cost for new developers
  • Development team responsibility for bugs and security issues
  • Time and resources required for feature additions and improvements
  • Burden of community support and documentation

Reference Pages

Code Examples

Installation and Basic Setup

# Install PSR-3 interface
composer require psr/log

# Create project structure
mkdir src/Logger
touch src/Logger/CustomLogger.php

# Configure autoloader (composer.json)
# "autoload": {
#     "psr-4": {
#         "MyApp\\Logger\\": "src/Logger/"
#     }
# }

composer dump-autoload

Basic Custom Logger Implementation

<?php

namespace MyApp\Logger;

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

class CustomLogger extends AbstractLogger
{
    private $logFile;
    private $minLevel;
    
    // Log level priority mapping
    private const LEVEL_PRIORITY = [
        LogLevel::EMERGENCY => 700,
        LogLevel::ALERT     => 600,
        LogLevel::CRITICAL  => 500,
        LogLevel::ERROR     => 400,
        LogLevel::WARNING   => 300,
        LogLevel::NOTICE    => 250,
        LogLevel::INFO      => 200,
        LogLevel::DEBUG     => 100,
    ];
    
    public function __construct(string $logFile = null, string $minLevel = LogLevel::DEBUG)
    {
        $this->logFile = $logFile ?: sys_get_temp_dir() . '/custom.log';
        $this->minLevel = $minLevel;
        
        // Create log directory
        $logDir = dirname($this->logFile);
        if (!is_dir($logDir)) {
            mkdir($logDir, 0755, true);
        }
    }
    
    /**
     * Implementation of PSR-3's core log method
     */
    public function log($level, $message, array $context = []): void
    {
        // Log level filtering
        if (!$this->shouldLog($level)) {
            return;
        }
        
        // Message interpolation
        $interpolatedMessage = $this->interpolate($message, $context);
        
        // Build log entry
        $logEntry = $this->buildLogEntry($level, $interpolatedMessage, $context);
        
        // Write log
        $this->writeLog($logEntry);
    }
    
    /**
     * Log level filtering
     */
    private function shouldLog(string $level): bool
    {
        $currentPriority = self::LEVEL_PRIORITY[$level] ?? 0;
        $minPriority = self::LEVEL_PRIORITY[$this->minLevel] ?? 0;
        
        return $currentPriority >= $minPriority;
    }
    
    /**
     * PSR-3 compliant message interpolation
     */
    private function interpolate(string $message, array $context): string
    {
        $replacements = [];
        
        foreach ($context as $key => $val) {
            // Process only stringifiable values
            if (is_null($val) || is_scalar($val) || (is_object($val) && method_exists($val, '__toString'))) {
                $replacements['{' . $key . '}'] = $val;
            }
        }
        
        return strtr($message, $replacements);
    }
    
    /**
     * Build log entry
     */
    private function buildLogEntry(string $level, string $message, array $context): string
    {
        $timestamp = date('Y-m-d H:i:s');
        $levelUpper = strtoupper($level);
        
        // JSON format context information
        $contextJson = empty($context) ? '' : ' ' . json_encode($context, JSON_UNESCAPED_UNICODE);
        
        return "[{$timestamp}] {$levelUpper}: {$message}{$contextJson}" . PHP_EOL;
    }
    
    /**
     * Write log
     */
    private function writeLog(string $logEntry): void
    {
        file_put_contents($this->logFile, $logEntry, FILE_APPEND | LOCK_EX);
    }
}

// Usage example
use MyApp\Logger\CustomLogger;
use Psr\Log\LogLevel;

$logger = new CustomLogger('/var/log/myapp.log', LogLevel::INFO);

$logger->info('User logged in', ['user_id' => 123, 'ip' => '192.168.1.1']);
$logger->error('Database connection error', ['error' => 'Connection timeout', 'host' => 'db.example.com']);
$logger->debug('Debug information', ['query' => 'SELECT * FROM users']); // Not output (below INFO level)

Advanced Custom Logger (Multiple Output Support)

<?php

namespace MyApp\Logger;

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

class AdvancedCustomLogger extends AbstractLogger
{
    private $handlers = [];
    
    public function addHandler(LogHandlerInterface $handler): void
    {
        $this->handlers[] = $handler;
    }
    
    public function log($level, $message, array $context = []): void
    {
        $logRecord = new LogRecord($level, $message, $context);
        
        foreach ($this->handlers as $handler) {
            if ($handler->handles($logRecord)) {
                $handler->write($logRecord);
            }
        }
    }
}

// Log handler interface
interface LogHandlerInterface
{
    public function handles(LogRecord $record): bool;
    public function write(LogRecord $record): void;
}

// Log record class
class LogRecord
{
    public $level;
    public $message;
    public $context;
    public $timestamp;
    
    public function __construct(string $level, string $message, array $context = [])
    {
        $this->level = $level;
        $this->message = $message;
        $this->context = $context;
        $this->timestamp = new \DateTime();
    }
}

// File handler
class FileHandler implements LogHandlerInterface
{
    private $filePath;
    private $minLevel;
    
    public function __construct(string $filePath, string $minLevel = LogLevel::DEBUG)
    {
        $this->filePath = $filePath;
        $this->minLevel = $minLevel;
    }
    
    public function handles(LogRecord $record): bool
    {
        $levelPriority = [
            LogLevel::DEBUG => 100,
            LogLevel::INFO => 200,
            LogLevel::NOTICE => 250,
            LogLevel::WARNING => 300,
            LogLevel::ERROR => 400,
            LogLevel::CRITICAL => 500,
            LogLevel::ALERT => 600,
            LogLevel::EMERGENCY => 700,
        ];
        
        return ($levelPriority[$record->level] ?? 0) >= ($levelPriority[$this->minLevel] ?? 0);
    }
    
    public function write(LogRecord $record): void
    {
        $formatted = sprintf(
            "[%s] %s: %s %s\n",
            $record->timestamp->format('Y-m-d H:i:s'),
            strtoupper($record->level),
            $record->message,
            empty($record->context) ? '' : json_encode($record->context)
        );
        
        file_put_contents($this->filePath, $formatted, FILE_APPEND | LOCK_EX);
    }
}

// Email handler
class EmailHandler implements LogHandlerInterface
{
    private $to;
    private $subject;
    private $triggerLevel;
    
    public function __construct(string $to, string $subject = 'Application Error', string $triggerLevel = LogLevel::ERROR)
    {
        $this->to = $to;
        $this->subject = $subject;
        $this->triggerLevel = $triggerLevel;
    }
    
    public function handles(LogRecord $record): bool
    {
        $levelPriority = [
            LogLevel::ERROR => 400,
            LogLevel::CRITICAL => 500,
            LogLevel::ALERT => 600,
            LogLevel::EMERGENCY => 700,
        ];
        
        return isset($levelPriority[$record->level]) && 
               $levelPriority[$record->level] >= ($levelPriority[$this->triggerLevel] ?? 400);
    }
    
    public function write(LogRecord $record): void
    {
        $body = sprintf(
            "Level: %s\nTime: %s\nMessage: %s\nContext: %s",
            strtoupper($record->level),
            $record->timestamp->format('Y-m-d H:i:s'),
            $record->message,
            json_encode($record->context, JSON_PRETTY_PRINT)
        );
        
        mail($this->to, $this->subject, $body);
    }
}

// Usage example
$logger = new AdvancedCustomLogger();
$logger->addHandler(new FileHandler('/var/log/app.log', LogLevel::INFO));
$logger->addHandler(new EmailHandler('[email protected]', 'Critical Error', LogLevel::CRITICAL));

$logger->info('Application started');
$logger->error('Processing error occurred', ['error_code' => 500]);
$logger->critical('Database not responding', ['host' => 'db.example.com']); // Email will be sent

Database Logger Implementation

<?php

namespace MyApp\Logger;

use Psr\Log\AbstractLogger;
use PDO;

class DatabaseLogger extends AbstractLogger
{
    private $pdo;
    private $tableName;
    
    public function __construct(PDO $pdo, string $tableName = 'application_logs')
    {
        $this->pdo = $pdo;
        $this->tableName = $tableName;
        $this->createTableIfNotExists();
    }
    
    public function log($level, $message, array $context = []): void
    {
        $sql = "INSERT INTO {$this->tableName} (level, message, context, created_at) VALUES (?, ?, ?, ?)";
        $stmt = $this->pdo->prepare($sql);
        
        $stmt->execute([
            $level,
            $message,
            json_encode($context),
            date('Y-m-d H:i:s')
        ]);
    }
    
    private function createTableIfNotExists(): void
    {
        $sql = "
            CREATE TABLE IF NOT EXISTS {$this->tableName} (
                id INT AUTO_INCREMENT PRIMARY KEY,
                level VARCHAR(20) NOT NULL,
                message TEXT NOT NULL,
                context JSON,
                created_at DATETIME NOT NULL,
                INDEX idx_level (level),
                INDEX idx_created_at (created_at)
            )
        ";
        
        $this->pdo->exec($sql);
    }
    
    /**
     * Log search functionality
     */
    public function searchLogs(array $criteria = []): array
    {
        $where = [];
        $params = [];
        
        if (isset($criteria['level'])) {
            $where[] = 'level = ?';
            $params[] = $criteria['level'];
        }
        
        if (isset($criteria['from_date'])) {
            $where[] = 'created_at >= ?';
            $params[] = $criteria['from_date'];
        }
        
        if (isset($criteria['to_date'])) {
            $where[] = 'created_at <= ?';
            $params[] = $criteria['to_date'];
        }
        
        if (isset($criteria['message_contains'])) {
            $where[] = 'message LIKE ?';
            $params[] = '%' . $criteria['message_contains'] . '%';
        }
        
        $whereClause = empty($where) ? '' : 'WHERE ' . implode(' AND ', $where);
        $sql = "SELECT * FROM {$this->tableName} {$whereClause} ORDER BY created_at DESC LIMIT 1000";
        
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute($params);
        
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }
}

// Usage example
$pdo = new PDO('mysql:host=localhost;dbname=myapp', 'username', 'password');
$logger = new DatabaseLogger($pdo);

$logger->info('User registration', ['user_id' => 456, 'email' => '[email protected]']);
$logger->error('Payment processing failed', ['order_id' => 789, 'amount' => 1000]);

// Log search
$errorLogs = $logger->searchLogs([
    'level' => 'error',
    'from_date' => '2025-01-01',
    'to_date' => '2025-01-31'
]);

API Integration Logger Implementation

<?php

namespace MyApp\Logger;

use Psr\Log\AbstractLogger;

class ApiLogger extends AbstractLogger
{
    private $apiEndpoint;
    private $apiKey;
    private $batchSize;
    private $logBuffer = [];
    
    public function __construct(string $apiEndpoint, string $apiKey, int $batchSize = 10)
    {
        $this->apiEndpoint = $apiEndpoint;
        $this->apiKey = $apiKey;
        $this->batchSize = $batchSize;
        
        // Flush buffer on shutdown
        register_shutdown_function([$this, 'flush']);
    }
    
    public function log($level, $message, array $context = []): void
    {
        $this->logBuffer[] = [
            'level' => $level,
            'message' => $message,
            'context' => $context,
            'timestamp' => date('c'), // ISO 8601 format
            'server' => $_SERVER['SERVER_NAME'] ?? 'unknown',
            'request_id' => uniqid()
        ];
        
        if (count($this->logBuffer) >= $this->batchSize) {
            $this->flush();
        }
    }
    
    public function flush(): void
    {
        if (empty($this->logBuffer)) {
            return;
        }
        
        $payload = [
            'logs' => $this->logBuffer,
            'application' => 'MyApp',
            'version' => '1.0.0'
        ];
        
        $this->sendToApi($payload);
        $this->logBuffer = [];
    }
    
    private function sendToApi(array $payload): void
    {
        $ch = curl_init();
        
        curl_setopt_array($ch, [
            CURLOPT_URL => $this->apiEndpoint,
            CURLOPT_POST => true,
            CURLOPT_POSTFIELDS => json_encode($payload),
            CURLOPT_HTTPHEADER => [
                'Content-Type: application/json',
                'Authorization: Bearer ' . $this->apiKey,
                'User-Agent: CustomLogger/1.0'
            ],
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT => 5,
            CURLOPT_CONNECTTIMEOUT => 3
        ]);
        
        $response = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        
        if ($httpCode !== 200) {
            // Emergency log for API send failure
            error_log("Failed to send logs to API: HTTP {$httpCode}");
        }
        
        curl_close($ch);
    }
}

// Encrypted logger implementation
class EncryptedFileLogger extends AbstractLogger
{
    private $filePath;
    private $encryptionKey;
    
    public function __construct(string $filePath, string $encryptionKey)
    {
        $this->filePath = $filePath;
        $this->encryptionKey = $encryptionKey;
    }
    
    public function log($level, $message, array $context = []): void
    {
        $logData = [
            'level' => $level,
            'message' => $message,
            'context' => $context,
            'timestamp' => time()
        ];
        
        $encrypted = $this->encrypt(json_encode($logData));
        file_put_contents($this->filePath, $encrypted . "\n", FILE_APPEND | LOCK_EX);
    }
    
    private function encrypt(string $data): string
    {
        $iv = random_bytes(16);
        $encrypted = openssl_encrypt($data, 'AES-256-CBC', $this->encryptionKey, 0, $iv);
        return base64_encode($iv . $encrypted);
    }
    
    public function decrypt(string $encryptedData): string
    {
        $data = base64_decode($encryptedData);
        $iv = substr($data, 0, 16);
        $encrypted = substr($data, 16);
        return openssl_decrypt($encrypted, 'AES-256-CBC', $this->encryptionKey, 0, $iv);
    }
    
    /**
     * Read and decrypt log file
     */
    public function readLogs(): array
    {
        if (!file_exists($this->filePath)) {
            return [];
        }
        
        $lines = file($this->filePath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
        $logs = [];
        
        foreach ($lines as $line) {
            try {
                $decrypted = $this->decrypt($line);
                $logs[] = json_decode($decrypted, true);
            } catch (Exception $e) {
                // Skip lines that fail to decrypt
                continue;
            }
        }
        
        return $logs;
    }
}

// Usage example
$apiLogger = new ApiLogger('https://logs.example.com/api/v1/logs', 'your-api-key');
$encryptedLogger = new EncryptedFileLogger('/secure/logs/app.enc', 'your-secret-key');

$apiLogger->info('API send test');
$encryptedLogger->critical('Sensitive data error', ['sensitive_data' => 'hidden']);

// Manual buffer flush
$apiLogger->flush();

// Read encrypted logs
$decryptedLogs = $encryptedLogger->readLogs();

Testable Logger Design

<?php

namespace MyApp\Logger;

use Psr\Log\AbstractLogger;

class TestableLogger extends AbstractLogger
{
    private $logEntries = [];
    private $isTestMode = false;
    
    public function __construct(bool $testMode = false)
    {
        $this->isTestMode = $testMode;
    }
    
    public function log($level, $message, array $context = []): void
    {
        $entry = [
            'level' => $level,
            'message' => $message,
            'context' => $context,
            'timestamp' => microtime(true)
        ];
        
        if ($this->isTestMode) {
            $this->logEntries[] = $entry;
        } else {
            // Actual log processing in production
            $this->writeToActualLog($entry);
        }
    }
    
    private function writeToActualLog(array $entry): void
    {
        // Actual log output processing
        $formatted = sprintf(
            "[%s] %s: %s\n",
            date('Y-m-d H:i:s', (int)$entry['timestamp']),
            strtoupper($entry['level']),
            $entry['message']
        );
        
        error_log($formatted);
    }
    
    // Test methods
    public function getLogEntries(): array
    {
        return $this->logEntries;
    }
    
    public function hasLogEntry(string $level, string $message): bool
    {
        foreach ($this->logEntries as $entry) {
            if ($entry['level'] === $level && $entry['message'] === $message) {
                return true;
            }
        }
        return false;
    }
    
    public function getLogEntriesByLevel(string $level): array
    {
        return array_filter($this->logEntries, function($entry) use ($level) {
            return $entry['level'] === $level;
        });
    }
    
    public function clearLogs(): void
    {
        $this->logEntries = [];
    }
}

// PHPUnit test example
class TestableLoggerTest extends \PHPUnit\Framework\TestCase
{
    private $logger;
    
    protected function setUp(): void
    {
        $this->logger = new TestableLogger(true); // Create in test mode
    }
    
    public function testLogsInfoMessage()
    {
        $this->logger->info('Test message', ['user_id' => 123]);
        
        $this->assertTrue($this->logger->hasLogEntry('info', 'Test message'));
        
        $infoLogs = $this->logger->getLogEntriesByLevel('info');
        $this->assertCount(1, $infoLogs);
        $this->assertEquals(['user_id' => 123], $infoLogs[0]['context']);
    }
    
    public function testMultipleLevels()
    {
        $this->logger->debug('Debug');
        $this->logger->info('Info');
        $this->logger->error('Error');
        
        $this->assertCount(3, $this->logger->getLogEntries());
        $this->assertCount(1, $this->logger->getLogEntriesByLevel('error'));
    }
}