Logging
This guide covers Glueful's comprehensive logging system, including framework vs application logging boundaries, the LogManager, channels, performance monitoring, and production best practices.
Overview
Glueful provides a comprehensive logging system built on Monolog with advanced features designed for production environments:
Key Features
- PSR-3 Compliant: Standard logging interface with all log levels
- Clear Separation of Concerns: Framework infrastructure vs application business logic
- Advanced Performance Monitoring: Built-in timing, memory tracking, and performance analysis
- N+1 Query Detection: Automatic detection of N+1 query problems with recommendations
- Multiple Channels:
app,api,framework,error,debug, plusauthandsecurityfor authentication and security events - Database Logging: Structured storage in
activity_logs, with per-channel retention - Intelligent Sampling: Configurable sampling rates for high-volume environments
- Security-First: Automatic sanitization of sensitive data
- Production Ready: File rotation, cleanup, and memory management
Architecture
The logging system separates concerns between:
- Framework Logging: Performance metrics, protocol errors, system health
- Application Logging: Business logic, user actions, custom events
- Query Logging: Database performance, slow queries, N+1 detection
- Request Logging: HTTP requests, responses, middleware processing
Framework vs Application Logging
Glueful follows industry best practices for framework logging boundaries, ensuring clear separation of concerns between framework infrastructure and application business logic.
✅ Framework Automatically Logs (Infrastructure Concerns)
The framework handles these logging concerns automatically:
- Unhandled Exceptions & Fatal Errors - Framework-level failures and PHP errors
- HTTP Protocol Errors - Malformed requests, routing failures, invalid JSON
- Framework Lifecycle Events - Startup, shutdown, configuration loading
- HTTP Auth Failures - Missing headers, malformed JWT tokens (protocol level)
- Slow Query Detection - Configurable performance monitoring
- HTTP Client Infrastructure Failures - Connection timeouts, DNS issues, server errors
- API Deprecation Warnings - Framework-managed endpoint versioning
🔧 Application Should Log (Business Concerns)
Your application code should handle these logging scenarios via events and custom logging:
- Business Authentication Logic - User login attempts, permission checks
- Business External Service Failures - Payment processing, email delivery status
- User Behavior Tracking - Custom analytics and business metrics
- Custom Validation - Business rule violations and domain-specific errors
- CRUD Operations - User data changes, audit trails
- Business State Changes - Order status updates, user role changes
Using Framework Events for Application Logging
The framework emits events that your application can listen to for business logging:
Security Events
use Glueful\Events\Auth\RateLimitExceededEvent;
use Glueful\Events\Http\HttpAuthFailureEvent;
use Psr\Log\LoggerInterface;
class SecurityLoggingListener
{
public function __construct(private LoggerInterface $logger) {}
public function onRateLimitExceeded(RateLimitExceededEvent $event): void
{
$this->logger->warning('Rate limit violation detected', [
'ip_address' => $event->getClientIp(),
'rule' => $event->getRule(),
'current_count' => $event->getCurrentCount(),
'limit' => $event->getLimit(),
'window_seconds' => $event->getWindowSeconds(),
'timestamp' => date('c')
]);
if ($event->isSevereViolation()) {
// blacklistIp(...); notifySecurity(...);
}
}
public function onHttpAuthFailure(HttpAuthFailureEvent $event): void
{
$this->logger->info('Authentication attempt failed', [
'reason' => $event->reason,
'ip_address' => $event->request->getClientIp(),
'endpoint' => $event->request->getPathInfo(),
'user_agent' => $event->request->headers->get('User-Agent'),
'timestamp' => date('c')
]);
}
}
Query Events
use Glueful\Events\Database\QueryExecutedEvent;
use Psr\Log\LoggerInterface;
class QueryLoggingListener
{
public function __construct(private LoggerInterface $logger) {}
public function onQueryExecuted(QueryExecutedEvent $event): void
{
// Log writes that touch business-critical tables
if ($event->isModifying() && preg_match('/\b(orders|payments)\b/i', $event->getSql()) === 1) {
$this->logger->info('Business critical write', [
'type' => $event->getQueryType(),
'execution_time' => $event->getExecutionTime(),
'connection' => $event->getConnectionName(),
]);
}
}
}
The event exposes getSql(), getBindings(), getExecutionTime() (seconds), getConnectionName(), getFullQuery(), getQueryType(), isModifying() and isSlow($threshold). Extra details are in the event metadata: getMetadata('tables') (table names parsed from the SQL), getMetadata('purpose') and, for a failed query, getMetadata('error').
The event fires whenever at least one listener is registered, in debug mode or not (since 1.91; before, it fired only in debug mode). With no listener, nothing is built. Tag a query with withPurpose() on the query builder and the purpose reaches the event and the query log:
$rows = db($context)->table('orders')
->withPurpose('nightly revenue report')
->where('status', 'paid')
->get();
// listener: $event->getMetadata('purpose') === 'nightly revenue report'
A purpose set on a clone() of a builder doesn't tag the original's queries.
Request Context Logging
Use the framework's contextual logging for consistent request correlation:
// In a controller (LoggerInterface injected in the constructor)
public function store(Request $request): Response
{
$data = json_decode($request->getContent(), true) ?? [];
$this->logger->info('Creating order', [
'type' => 'application',
'customer_uuid' => $data['customer_uuid'] ?? null,
'request_id' => request_id(),
// Set by the request_logging middleware when it runs on this route
'correlation_id' => $request->attributes->get('correlation_id'),
]);
$order = $this->orders->create($data);
$this->logger->info('Order created', [
'type' => 'application',
'order_uuid' => $order['uuid'],
'request_id' => request_id(),
]);
return Response::created($order, 'Order created');
}
Which Logger to Use
Psr\Log\LoggerInterface(inject it, orapp($context, LoggerInterface::class)): the container's PSR-3 logger, a Monolog logger on theframeworkchannel writing tologging.channels.framework.path(defaultstorage/logs/framework.log) atFRAMEWORK_LOG_LEVEL. It's aNullLoggerwhenAPP_ENV=testingorFRAMEWORK_LOGGING_ENABLED=false. Use it for code that only needs PSR-3.Glueful\Logging\LogManager: the framework's richer logger, with channels, timers, batching, sampling and database logging. It isn't registered in the container; get the shared instance withLogManager::getInstance(). It writes daily-rotatedapp-YYYY-MM-DD.log,error-*.loganddebug-*.logfiles inlogging.paths.log_directory.
LogManager Core Features
Basic Usage
use Glueful\Logging\LogManager;
// Shared instance
$logger = LogManager::getInstance();
// Basic logging with all PSR-3 levels
$logger->emergency('System is unusable');
$logger->alert('Action must be taken immediately');
$logger->critical('Critical conditions');
$logger->error('Error conditions', ['error' => $exception->getMessage()]);
$logger->warning('Warning conditions');
$logger->notice('Normal but significant condition');
$logger->info('Informational messages');
$logger->debug('Debug-level messages');
// Contextual logging
$logger->info('User login', [
'user_id' => 123,
'ip_address' => $request->getClientIp(),
'user_agent' => $request->headers->get('User-Agent')
]);
Advanced Features
Performance Timing
// Time operations
$timerId = $logger->startTimer('database_operation');
// Perform your operation
$results = $this->performDatabaseOperation();
// End timing (automatically logs duration)
$duration = $logger->endTimer($timerId);
Memory Monitoring
// Current memory usage as a formatted string
$memoryUsage = $logger->getMemoryUsage();
// Add performance metrics to every record's context
$logger->configure(['include_performance_metrics' => true]);
$logger->info('Processing complete');
Level Floor and Sampling
LogManager starts at LOG_LEVEL (logging.application.level, defaulting to the profile's app level: debug in development, info in staging, warning in production and testing) and drops records below it. An unknown level name falls back to debug. Raise or lower the floor at runtime:
$logger->setMinimumLevelByName('warning'); // or configure(['minimum_level' => 'warning'])
$logger->setSamplingRate(0.1); // keep ~10% of records
Batch Logging
// Configure batch mode for high-volume logging
$logger->configure([
'batch_mode' => true,
'batch_size' => 100,
]);
// Or explicitly
$logger->setBatchMode(true, 100);
// Manual flush when needed
$logger->flush();
Channel-Based Logging
Glueful uses multiple channels to organize different types of logs:
Available Channels
- app - General application logging (the default)
- api - API request/response logging
- framework - Framework internals and performance
- error - Error and exception logging
- debug - Development and debugging information
- auth - Logins, logouts and authentication failures (written by the framework's activity-logging subscriber)
- security - Security violations and rate-limit breaches
auth and security records are kept longest in the database (180 to 365 days by profile, see logging.retention.channels).
Using Channels
// Channel-specific logging
$logger->channel('api')->info('API request processed', $context);
$logger->channel('error')->error('Database connection failed', $context);
$logger->channel('debug')->debug('Cache miss', ['key' => $cacheKey]);
// Switch channels
$apiLogger = $logger->channel('api');
$apiLogger->info('Request started');
$apiLogger->info('Request completed');
Note: channel names label records; writing to separate files per channel beyond the defaults requires configuring additional handlers.
Uncaught exceptions are labelled by the exception Handler: your Handler::mapChannel() entries win (most specific class first), then typed database exceptions get database, framework-origin ones framework, and the built-in defaults the rest. See Handler Hooks.
Database Logging
Store logs in the database for structured querying and analysis.
Setup
Database logging writes to the activity_logs table by default (Glueful\Logging\DatabaseLogHandler). Turn it on for LogManager with LOG_TO_DB=true (logging.application.log_to_db), or at runtime:
Usage
use Glueful\Logging\LogManager;
$logger = LogManager::getInstance();
$logger->configureDatabaseLogging(true, [
'min_level' => 'info',
// 'table' => 'activity_logs', // the default
]);
// Logs are automatically stored in the database
$logger->info('User action', [
'user_id' => 123,
'action' => 'profile_update',
'ip_address' => $request->getClientIp()
]);
Pruning Database Logs
use Glueful\Logging\DatabaseLogPruner;
// Keep logs for up to 60 days and 500k records (table defaults to activity_logs)
$pruner = new DatabaseLogPruner(maxAgeInDays: 60, maxRecords: 500_000, context: $context);
$result = $pruner->prune();
// Per-channel retention from logging.retention.channels
$deleted = $pruner->pruneByChannel();
Query Performance Logging
The database layer logs queries through Glueful\Database\QueryLogger.
- Slow queries:
config/logging.php→framework.slow_queries(LOG_SLOW_QUERIES,SLOW_QUERY_THRESHOLDin ms, default 200) controls slow-query logging.config/database.php→logging(DB_LOGGING,DB_SLOW_MS) controls the query log itself. - N+1 detection: configure it with
DB_LAZY_LOADING_MODE(off,warn,strict,auto), which drives the ORM's lazy-loading detector. See Advanced → Performance. - Profiling:
php glueful db:profileandQueryProfilerServicecover one-off analysis; see Query Analysis and Profiling.
use Glueful\Database\QueryLogger;
$queryLogger = new QueryLogger($logger, $context);
$queryLogger->configure(enableDebug: true, enableTiming: true);
$stats = $queryLogger->getStatistics();
$log = $queryLogger->getQueryLog();
HTTP Request Logging
Comprehensive HTTP request and response logging via middleware.
Features
- Complete Request Lifecycle: From request start to response completion
- Performance Monitoring: Request timing and slow request detection
- Error Correlation: Request ID tracking across logs
- Context Injection: Automatic request context in all logs
Setup
Use the built-in route middleware alias request_logging (provided by the DI container):
// Request and response, with the middleware's defaults
$router->get('/orders', [OrderController::class, 'index'])
->middleware(['request_logging']);
// mode, headers, bodies, level, slow threshold (ms)
$router->post('/payments', [PaymentController::class, 'process'])
->middleware(['request_logging:both,true,false,info,500']);
// Empty positions keep the default: response only, with bodies
$router->post('/uploads', [UploadController::class, 'store'])
->middleware(['request_logging:response,,true']);
The underlying class is Glueful\Routing\Middleware\RequestResponseLoggingMiddleware. Its positional params are mode (request | response | both, default both), headers (default true), bodies (default false), level (default info) and slow threshold in milliseconds (default 2000). Booleans accept true/false/1/0; an empty or unrecognised value keeps the default. Before 1.91 only the mode and level took effect from a route string. It sets a correlation_id request attribute for downstream code.
Features
// Automatic logging includes:
// - Request method, URL, headers
// - Request body (sanitized)
// - Response status, headers
// - Execution time
// - Memory usage
// - User context (if authenticated)
// Example log output:
// [INFO] HTTP Request: POST /api/users
// Context: {
// "request_id": "req_abc123",
// "method": "POST",
// "url": "/api/users",
// "user_id": 123,
// "ip_address": "192.168.1.100",
// "duration_ms": 45.2,
// "memory_usage_mb": 2.3,
// "status_code": 201
// }
Performance Monitoring
Built-in Performance Tracking
// Execution time tracking
$timer = $logger->startTimer('complex_calculation');
$result = $this->performComplexCalculation();
$elapsedMs = $logger->endTimer($timer);
// Request correlation in logs via processors/helpers
$logger->info('Processing started', [
'request_id' => function_exists('request_id') ? request_id() : null,
]);
Performance Alerts
Slow request detection is part of the request_logging middleware: requests slower than its threshold (default 2000 ms, or the 5th route param, e.g. request_logging:both,,,,500) are logged as slow. Slow query thresholds are configured in config/logging.php under framework.slow_queries.
Configuration
Framework Logging Settings
Framework logging is configured in config/logging.php:
'framework' => [
'enabled' => env('FRAMEWORK_LOGGING_ENABLED', true),
'level' => env('FRAMEWORK_LOG_LEVEL', 'info'),
'channel' => env('FRAMEWORK_LOG_CHANNEL', 'framework'),
// Feature-specific toggles
'log_exceptions' => env('LOG_FRAMEWORK_EXCEPTIONS', true),
'log_deprecations' => env('LOG_FRAMEWORK_DEPRECATIONS', true),
'log_lifecycle' => env('LOG_FRAMEWORK_LIFECYCLE', true),
'log_protocol_errors' => env('LOG_FRAMEWORK_PROTOCOL_ERRORS', true),
// Performance monitoring (optional)
'slow_requests' => [
'enabled' => env('LOG_SLOW_REQUESTS', true),
'threshold_ms' => env('SLOW_REQUEST_THRESHOLD', 1000),
],
'slow_queries' => [
'enabled' => env('LOG_SLOW_QUERIES', true),
'threshold_ms' => env('SLOW_QUERY_THRESHOLD', 200),
],
'http_client' => [
'log_failures' => env('LOG_HTTP_CLIENT_FAILURES', true),
'slow_threshold_ms' => env('HTTP_CLIENT_SLOW_THRESHOLD', 5000)
]
],
'application' => [
'default_channel' => env('LOG_CHANNEL', 'app'),
'level' => env('LOG_LEVEL', $profile['app_level']),
'log_to_file' => env('LOG_TO_FILE', $profile['log_to_file']),
'log_to_db' => env('LOG_TO_DB', $profile['log_to_db']),
],
'rotation' => [
'days' => env('LOG_ROTATION_DAYS', 30),
'strategy' => env('LOG_ROTATION_STRATEGY', 'daily'), // daily, weekly, monthly, size
'max_size' => env('LOG_MAX_SIZE', '100M'),
],
'retention' => [
'default' => env('LOG_RETENTION_DAYS', $profile['retention_default']),
'channels' => [
'auth' => env('LOG_RETENTION_AUTH_DAYS', $profile['retention']['auth']),
'security' => env('LOG_RETENTION_SECURITY_DAYS', $profile['retention']['security']),
// ... debug, api, app, framework, error
],
],
Defaults come from a profile chosen by APP_ENV (override with LOG_PROFILE): development, staging, production or testing, each setting the framework and app levels, file/DB logging, and retention. LOG_FILE_PATH moves the log directory. LOG_SENSITIVE_PATHS (comma-separated route templates such as /checkout/pay/{token}) redacts credential-bearing path segments in every log.
Environment Variables
# Framework Logging (Infrastructure/Protocol concerns)
FRAMEWORK_LOGGING_ENABLED=true
FRAMEWORK_LOG_LEVEL=info
LOG_FRAMEWORK_EXCEPTIONS=true
LOG_FRAMEWORK_DEPRECATIONS=true
LOG_FRAMEWORK_LIFECYCLE=true
LOG_FRAMEWORK_PROTOCOL_ERRORS=true
# Framework Performance Monitoring
LOG_SLOW_REQUESTS=true
SLOW_REQUEST_THRESHOLD=1000
LOG_SLOW_QUERIES=true
SLOW_QUERY_THRESHOLD=200
LOG_HTTP_CLIENT_FAILURES=true
HTTP_CLIENT_SLOW_THRESHOLD=5000
# Application logging
LOG_PROFILE=production # default follows APP_ENV
LOG_CHANNEL=app
LOG_LEVEL=info
LOG_TO_FILE=true
LOG_TO_DB=false
LOG_FILE_PATH=/var/log/myapp/
LOG_ROTATION_DAYS=30
LOG_RETENTION_DAYS=90
LOG_RETENTION_AUTH_DAYS=365
LOG_SENSITIVE_PATHS=/checkout/pay/{token}
LOG_LEVEL and LOG_CHANNEL populate logging.application.level / default_channel. LogManager uses LOG_LEVEL as its minimum level (records below it are dropped); setMinimumLevelByName() changes it at runtime. Sampling is set in code (setSamplingRate()), not by an env variable.
Deprecation Management
API versions are deprecated in config/api.php → versioning.deprecated, keyed by version:
'deprecated' => [
'1' => [
'sunset' => '2026-12-31',
'message' => 'Please migrate to API v2',
'alternative' => '/v2',
],
],
Or from the CLI: php glueful api:version:deprecate 1 --sunset=2026-12-31 --message="Migrate to v2" --alternative=/v2 (--dry-run to preview). Responses for a deprecated version carry Deprecation, Sunset and Warning headers (toggle them under versioning.headers).
Log Maintenance
Automatic Cleanup
For database logs, use the DatabaseLogPruner to prune old records by age and quantity. Files are rotated daily by the rotating handlers.
use Glueful\Logging\DatabaseLogPruner;
$pruner = new DatabaseLogPruner(maxAgeInDays: 30, maxRecords: 1_000_000);
$result = $pruner->prune();
The default config/schedule.php runs a log_cleanup job (Glueful\Queue\Jobs\LogCleanupJob) that trims logs by LOG_RETENTION_DAYS. If your app's config/schedule.php sets its own jobs list, it replaces the framework's, so include log_cleanup in it.
Production Best Practices
Framework Logging (Production)
FRAMEWORK_LOGGING_ENABLED=true
FRAMEWORK_LOG_LEVEL=error # Only log errors in production
LOG_FRAMEWORK_EXCEPTIONS=true
LOG_FRAMEWORK_DEPRECATIONS=true
LOG_FRAMEWORK_LIFECYCLE=false # Reduce noise
LOG_FRAMEWORK_PROTOCOL_ERRORS=true
# Performance monitoring (adjust thresholds for production)
SLOW_REQUEST_THRESHOLD=2000 # 2 seconds
SLOW_QUERY_THRESHOLD=500 # 500ms
Application Logging (Production)
LOG_LEVEL=error # Application should log errors/warnings
LOG_TO_FILE=true
LOG_TO_DB=false # Consider impact on performance
LOG_ROTATION_DAYS=30
High-Volume Environments
// Configure for production (LogManager)
$logger->configure([
'sampling_rate' => 0.1, // Log only 10% of entries
'batch_mode' => true, // Batch writes for performance
'batch_size' => 100, // Write 100 entries at once
'minimum_level' => 'warning', // Only log warnings and above
]);
Security Configuration
Sensitive keys in log context (e.g., password, token, key, secret, auth, credential) are automatically sanitized.
Monitoring and Alerting
Framework Logs to Monitor
- High frequency of HTTP protocol errors (potential attacks)
- Unhandled exceptions (framework stability)
- Slow query/request patterns (performance issues)
- Deprecation usage (migration planning)
Application Logs to Monitor
- Authentication failure patterns (security)
- Business critical operation failures (reliability)
- User behavior anomalies (fraud detection)
- External service failures (integration health)
Troubleshooting
Common Issues
No framework logs appearing
- Check
FRAMEWORK_LOGGING_ENABLED=true - Verify log file permissions
- Check framework log level configuration
Too many framework logs
- Increase
FRAMEWORK_LOG_LEVEL(debug → info → warning → error) - Disable specific features (lifecycle, deprecations)
- Adjust performance thresholds
Missing application context
- Ensure event listeners are registered
- Use contextual logger in controllers
- Verify user context is available after authentication
High Memory Usage
// Check memory configuration
$logger->configure([
'batch_size' => 50 // Smaller batches
]);
Slow Logging Performance
// Optimize for speed
$logger->configure([
'sampling_rate' => 0.05, // Log only 5%
'minimum_level' => 'error', // Only errors
'batch_mode' => true
]);
Log File Locations
- Framework (injected
LoggerInterface):storage/logs/framework.log(logging.channels.framework.path) - Application (
LogManager):storage/logs/app-YYYY-MM-DD.log - Debug:
storage/logs/debug-YYYY-MM-DD.log - Errors:
storage/logs/error-YYYY-MM-DD.log
The directory is logging.paths.log_directory (LOG_FILE_PATH).
Best Practices
✅ Do
- Use events for business logging
- Leverage contextual logging for request correlation
- Log user actions and business state changes
- Configure appropriate log levels for each environment
- Use structured logging with consistent field names
❌ Don't
- Log business logic in framework middleware
- Duplicate framework logging in application code
- Log sensitive information (passwords, tokens, PII)
- Create custom logging for framework concerns
- Mix business and infrastructure logging