tibbs / scoped-logger-laravel
Fine-grained logging level management for Laravel
Fund package maintenance!
tibbsa
Installs: 39
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 1
Forks: 0
Open Issues: 0
pkg:composer/tibbs/scoped-logger-laravel
Requires
- php: ^8.3
- illuminate/contracts: ^12.11.0
- spatie/laravel-package-tools: ^1.16
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/pint: ^1.14
- nunomaduro/collision: ^8.1
- orchestra/testbench: ^10.0
- pestphp/pest: ^3.0
- pestphp/pest-plugin-arch: ^3.0
- pestphp/pest-plugin-laravel: ^3.0
- phpstan/extension-installer: ^1.3
- phpstan/phpstan-deprecation-rules: ^2.0
- phpstan/phpstan-phpunit: ^2.0
This package is auto-updated.
Last update: 2026-01-09 16:51:42 UTC
README
When troubleshooting specific issues in a Laravel application, it is often helpful to have increased logging visibility regarding a specific portion of your application. However, if you increase your log level, then you will also increase the log traffic from all other parts of the application at the same time. While logging channels offer some flexibility in separating different types of log entries, they are cumbersome and must all be pre-configured before they can be used.
This package adds the ability to define different logging levels based on the "scope" of a particular log entry.
Scopes can be explicitly defined when logging, or automatically determined based on the calling class (via its name,
a defined property, or a defined method). Logging levels for a given scope can be configured in the config/scoped-logger.php
configuration file or overridden at runtime for debugging purposes.
Features
- 🎯 Scope-based log levels - Set different log levels for different parts of your application
- 🔍 Wildcard pattern matching - Use
App\Services\*orpayment.*to match multiple scopes - 📊 Smart pattern priority - Most specific pattern wins (exact > longer > fewer wildcards)
- 🔄 Automatic scope detection - Auto-detect scope from calling class (FQCN, property, or method)
- 🚫 Scope suppression - Completely silence logs from specific scopes
- ⚠️ Unknown scope detection - Configurable handling for unconfigured scopes (exception, log warning, or ignore)
- 📺 Per-channel configurations - Different scope levels for different log channels
- 🔀 Multiple scopes - Log with multiple scopes using "most verbose wins" strategy
- 🎛️ Conditional logging - Use closures for dynamic levels based on environment, time, etc.
- 🐛 Debug mode - Detailed scope resolution info for troubleshooting
- 🪝 Laravel integration - Works seamlessly with
Logfacade andlogger()helper - ⚡ Minimal config required - Works out of the box with sensible defaults (just define your scopes!)
- ⚡ Runtime modification - Temporarily override scope levels without config changes
Requirements
- Laravel 12.11.0+
- PHP 8.3+
Installation
Install the package via composer:
composer require tibbs/scoped-logger-laravel
Publish the config file (optional):
php artisan vendor:publish --tag="scoped-logger-config"
Quick Start
Configure scopes in config/scoped-logger.php:
return [ 'default_level' => 'info', 'scopes' => [ 'payment' => 'debug', // Verbose logging for payments 'auth' => 'error', // Only errors for authentication 'reporting' => 'debug', // Detailed logs for reports 'chatty-vendor' => false, // Completely suppress ], ];
Use in your application:
// Explicit scope Log::scope('payment')->debug('Processing payment', ['amount' => 100]); // ✅ Logs because 'payment' scope allows 'debug' Log::scope('auth')->info('User logged in'); // ❌ Silently dropped because 'auth' requires 'error' or higher Log::info('General application message'); // ✅ Logs at default level
Usage
Explicit Scopes
The most straightforward way to use scoped logging:
use Illuminate\Support\Facades\Log; // Using Log facade Log::scope('payment')->debug('Payment details', $data); Log::scope('api')->info('API request', $request); // Using logger helper logger()->scope('auth')->warning('Failed login attempt'); // Chain with other methods Log::scope('reporting') ->withContext(['user_id' => 123]) ->info('Report generated');
Automatic Scope Detection
Scoped Logger can automatically detect the scope from the calling class:
namespace App\Services; class PaymentService { // Option 1: Use class FQCN as scope // Configure: 'App\Services\PaymentService' => 'debug' // Option 2: Define static property protected static string $log_scope = 'payment'; // Option 3: Define static method public static function getLogScope(): string { return 'payment'; } public function processPayment() { // Automatically uses 'payment' scope Log::debug('Processing payment...'); } }
Scope Priority
Scopes are resolved in this order:
- Explicit scope -
Log::scope('payment') - Class FQCN - If the calling class FQCN is configured as a scope
- Class property/method - Static property
$log_scopeor methodgetLogScope() - Default level - Falls back to
default_levelconfig
Pattern Matching
Use wildcard patterns to match multiple scopes with a single configuration:
'scopes' => [ // Match all service classes 'App\\Services\\*' => 'debug', // Match specific namespaces (more specific wins) 'App\\Services\\Payment\\*' => 'info', // Dot notation patterns 'payment.*' => 'debug', // Suppress all vendor logs 'vendor.*' => false, ],
Supported Wildcards:
*- Matches any characters (including none)?- Matches exactly one character
Pattern Specificity:
When multiple patterns match a scope, the most specific pattern wins:
- Exact matches (no wildcards) are most specific
- Longer patterns are more specific than shorter ones
- Fewer wildcards make patterns more specific
'scopes' => [ 'App\\*' => 'warning', // Least specific 'App\\Services\\*' => 'info', // More specific 'App\\Services\\Payment\\*' => 'debug', // Most specific 'App\\Services\\PaymentService' => 'error', // Exact (highest priority) ], Log::scope('App\\Services\\Payment\\StripeService')->debug('...'); // Uses 'App\\Services\\Payment\\*' (most specific pattern match) Log::scope('App\\Services\\PaymentService')->debug('...'); // Uses exact match 'App\\Services\\PaymentService'
Suppressing Scopes
Set a scope to false to completely suppress all logs from that scope:
'scopes' => [ 'noisy-vendor' => false, 'debug-toolbar' => false, 'vendor.*' => false, // Suppress all vendor packages ],
Log::scope('noisy-vendor')->emergency('Critical error!'); // ❌ Completely suppressed, even emergency logs
Unknown Scope Handling
By default, using an unconfigured scope throws an exception. This helps catch typos and configuration mistakes:
'scopes' => [ 'payment' => 'debug', ], Log::scope('paymnet')->info('typo!'); // ❌ Throws UnknownScopeException - helps you catch the typo
You can configure how unknown scopes are handled:
// config/scoped-logger.php 'unknown_scope_handling' => 'exception', // Default - throw exception // 'unknown_scope_handling' => 'log', // Log warning and continue with default level // 'unknown_scope_handling' => 'ignore', // Silently use default level
Handling Options:
exception(default): ThrowsUnknownScopeException- best for catching configuration errorslog: Logs a warning and processes the log with the default levelignore: Silently uses the default level
What counts as "known":
- Exact match in scopes configuration
- Matches a wildcard pattern
- Has a runtime override set
- Auto-detected scopes that return
null(use default level)
Environment Variable:
SCOPED_LOG_UNKNOWN_SCOPE=log # or 'exception', 'ignore'
Configuration
Disabling Scoped Logging
You can completely disable scoped logging by setting SCOPED_LOG_ENABLED=false in your .env file. When disabled:
- ✅ All logs pass through to the underlying Laravel logger without any filtering
- ❌ No scope-based filtering - all configured scope levels are ignored
- ❌ No scope added to context - the
scopekey won't appear in log context - ❌ No metadata added - caller metadata (file, line, class) won't be added
- ❌ No debug info added - scope resolution debug info won't be added
- ❌ No unknown scope checking - unknown scopes won't throw exceptions or log warnings
- ✅ Shared context preserved - context from
withContext()is still merged - ✅ Underlying channel level applies - Laravel's channel log level still filters
Use this when:
- You want to completely bypass scoped logging
- You're troubleshooting and want to see all logs regardless of scope configuration
- You want Laravel's default logging behavior
// .env SCOPED_LOG_ENABLED=false // Bypass all scoped logging features
Full Configuration Options
All available configuration options:
return [ // Master switch - set to false to disable scoped logging globally 'enabled' => env('SCOPED_LOG_ENABLED', true), // Default level when no scope matches 'default_level' => env('SCOPED_LOG_DEFAULT_LEVEL', 'info'), // Scope definitions (supports exact matches, wildcards, closures, and false for suppression) 'scopes' => [ 'payment' => 'debug', 'auth' => 'warning', 'App\\Services\\MailchimpApi' => 'debug', 'App\\Services\\*' => 'info', // Wildcard pattern 'vendor.*' => false, // Suppress completely 'api' => fn() => app()->environment('local') ? 'debug' : 'error', // Closure ], // How to handle unknown/unconfigured scopes (exception, log, or ignore) 'unknown_scope_handling' => env('SCOPED_LOG_UNKNOWN_SCOPE', 'exception'), // Per-channel scope configurations (override global scopes for specific channels) 'channel_scopes' => [ 'daily' => [ 'payment' => 'debug', 'api' => 'info', ], 'slack' => [ 'payment' => 'error', 'api' => 'error', ], ], // Auto-detection settings 'auto_detection' => [ 'enabled' => env('SCOPED_LOG_AUTO_DETECT', true), 'property' => 'log_scope', // Property/method name to check 'stack_depth' => 10, // How deep to traverse stack 'skip_vendor' => true, // Skip vendor classes 'skip_paths' => ['/vendor/', '/bootstrap/'], ], // List of channels that should NOT use scoped logging // By default, all channels use scoped logging (global by default) 'disabled_channels' => [ // 'slack', // 'sentry', ], // Add scope identifier to log context 'include_scope_in_context' => env('SCOPED_LOG_INCLUDE_SCOPE', true), 'scope_context_key' => 'scope', // Add caller metadata (file, line, class, function) to log context 'include_metadata' => env('SCOPED_LOG_INCLUDE_METADATA', false), // Metadata extraction settings 'metadata_skip_vendor' => true, // Skip vendor files when finding caller 'metadata_relative_paths' => true, // Show paths relative to base_path() 'metadata_base_path' => null, // Base path for relative paths (null = base_path()) // Debug mode - adds detailed scope resolution info to context (performance impact) 'debug_mode' => env('SCOPED_LOG_DEBUG', false), ];
⚠️ Important: Channel Log Levels
The underlying Laravel channel log level acts as a floor for all logged events.
If you're using scoped logging on a channel, that channel should be configured with the lowest log level you need (typically debug), otherwise the channel will filter out logs that scoped logging passes along to record.
Example Problem
// config/scoped-logger.php 'scopes' => [ 'payment' => 'debug', // You want debug logs for payments ], // config/logging.php 'channels' => [ 'daily' => [ 'driver' => 'daily', 'level' => 'warning', // ⚠️ PROBLEM: Channel filters out debug/info ], ],
Log::scope('payment')->debug('Payment processing'); // ❌ DROPPED: Scoped logger allows it, but channel level is 'warning'
Solution
Set your channel to debug level and let scoped logging handle the filtering:
// config/logging.php 'channels' => [ 'daily' => [ 'driver' => 'daily', 'level' => 'debug', // ✅ Let scoped logger control filtering ], ],
Now scoped logging has full control over what gets logged.
Why This Happens
Logging flows through two filters:
- Scoped Logger - Filters based on scope configuration
- Channel Driver - Filters based on channel
levelconfig
Both must allow the log through. The channel level is a hard floor that cannot be overridden by scoped logging.
Runtime Modification
Temporarily override scope levels at runtime without changing configuration:
// Temporarily increase logging for debugging Log::setRuntimeLevel('payment', 'debug'); Log::scope('payment')->debug('Now logging'); // ✅ Logs // Clear override Log::clearRuntimeLevel('payment'); // Temporarily suppress a noisy scope Log::setRuntimeLevel('chatty-service', false); // Clear all overrides Log::clearAllRuntimeLevels();
Runtime overrides:
- Take precedence over configured levels and pattern matches
- Persist only for the current request (in-memory)
- Perfect for temporary debugging without config changes
Conditional Logging
Use closures for dynamic log levels based on environment, time, feature flags, or any custom logic:
// config/scoped-logger.php 'scopes' => [ // Environment-based 'api' => fn() => app()->environment('local') ? 'debug' : 'error', // Time-based (verbose logging during off-peak hours) 'batch-import' => fn() => now()->hour >= 2 && now()->hour < 6 ? 'debug' : 'info', // Feature flag-based 'experimental' => fn() => config('features.verbose_experimental') ? 'debug' : 'warning', // Custom logic 'performance' => fn() => app('metrics')->isUnderLoad() ? 'error' : 'info', ],
Closures are evaluated on each log call, allowing real-time adjustments based on current conditions.
Per-Channel Scope Configurations
Configure different log levels for the same scope across different channels:
// config/scoped-logger.php 'scopes' => [ // Global defaults 'payment' => 'error', 'api' => 'warning', ], 'channel_scopes' => [ // Verbose logging on daily file 'daily' => [ 'payment' => 'debug', 'api' => 'info', ], // Errors only on Slack 'slack' => [ 'payment' => 'error', 'api' => 'error', ], ],
Channel-specific scopes override global scopes. If a scope isn't defined for a channel, it falls back to the global configuration.
Log::channel('daily')->scope('payment')->debug('Processing payment'); // ✅ Logs (daily channel allows debug for payment) Log::channel('slack')->scope('payment')->debug('Processing payment'); // ❌ Dropped (slack channel requires error level)
Multiple Scopes
Log with multiple scopes simultaneously by passing an array to scope(). The package uses a "most verbose wins" strategy:
Log::scope(['payment', 'api'])->debug('Payment via API');
Rules:
- Uses the lowest (most verbose) log level among all scopes
- If any scope is suppressed (
false), the entire log is suppressed - All scopes are included in context as a comma-separated string
// config/scoped-logger.php 'scopes' => [ 'payment' => 'debug', // Most verbose 'api' => 'error', // Least verbose ], Log::scope(['payment', 'api'])->debug('test'); // ✅ Logs because payment allows debug (most verbose wins) Log::scope(['payment', 'api'])->info('test'); // ✅ Logs because payment allows info Log::scope('api')->debug('test'); // ❌ Dropped because api requires error level
Debug Mode
Enable debug mode to see detailed scope resolution information in log context:
# .env
SCOPED_LOG_DEBUG=true
When enabled, each log entry includes:
[
'scoped_logger_debug' => [
'resolved_scope' => 'payment',
'log_level' => 'debug',
'configured_level' => 'debug',
'resolution_method' => 'explicit (scope() method)',
'runtime_override' => 'no',
'matched_pattern' => 'App\\Services\\*', // If applicable
],
]
⚠️ Warning: Debug mode adds overhead. Use only for troubleshooting scope resolution issues.
Artisan Commands
List all configured scopes
php artisan scoped-logger:list # Sort by level instead of name php artisan scoped-logger:list --sort=level # Show effective scopes for a specific channel (merges global + channel overrides) php artisan scoped-logger:list --channel=daily
Options:
--sort=name|level- Sort scopes by name (default) or level--channel=<name>- Show effective scopes for a specific channel
Default output displays:
- Global scopes table with levels and pattern indicators
- Channel-specific scope overrides (if configured)
- Total scope counts and pattern cache statistics
Channel-specific output (--channel) displays:
- Merged effective scopes (global + channel overrides)
- Source column indicating whether each scope comes from
globalorchannelconfiguration - Count of channel-specific overrides
Test scope resolution
php artisan scoped-logger:test payment # Test with a specific log level php artisan scoped-logger:test payment --level=debug # Test against a specific channel's configuration php artisan scoped-logger:test payment --channel=slack # Test against all channels that have overrides php artisan scoped-logger:test payment --all-channels # Combine options php artisan scoped-logger:test payment --level=info --all-channels
Options:
--level=<level>- Test log level to check (default: debug)--channel=<name>- Test against a specific channel's scope configuration--all-channels- Test against all channels that have overrides
Default output shows:
- What pattern (if any) matches the scope
- What log level applies
- Whether the specified test level would log or be dropped
- Whether each PSR-3 level would log or be dropped
Channel-specific output (--channel) additionally shows:
- Whether the level comes from a channel override (indicated by
(channel override))
All-channels output (--all-channels) shows:
- Detailed results for global and each channel with overrides
- Summary comparison table showing how the scope behaves across all channels
Example output for --all-channels:
Testing scope: payment
Test level: debug
Global (no channel):
Configured Level: debug
✓ Log::debug() WILL BE LOGGED
Channel: daily
Configured Level: info (channel override)
✗ Log::debug() WILL BE DROPPED
Channel: slack
Configured Level: error (channel override)
✗ Log::debug() WILL BE DROPPED
Summary:
+---------+-------+---------------+
| Channel | Level | Log::debug() |
+---------+-------+---------------+
| global | debug | logs |
| daily | info | drops |
| slack | error | drops |
+---------+-------+---------------+
Performance
If your application logs a large number of entries (thousands), some caution may be warranted because of the unavoidable overhead that this filtering process adds to each log call. In real world applications, it is likely that the overhead associated with actually logging the entry will far exceed any overhead from filtering, but for those interested in eeking out every last inch of performance, strategies you can employ to minimize the performance impact include:
- Defining explicit scopes on log calls, e.g.
logger()->scope('auth')->info('my log'), thereby eliminating the need for inspection of the calling class to determine the scope - Avoiding using closures for scope definitions, e.g.
scope => 'debug', as this incurs a performance penalty due to the need to evaluate the closure on each log call - Disable metadata extraction in production (
include_metadata=>false): This is one of the most expensive features due to stack trace walking. Only enable it for debugging purposes. - Keep
debug_modedisabled in production: This adds extra context processing overhead - Use runtime level overrides for temporary debugging instead of closures: Runtime overrides (
setRuntimeLevel()) are actually faster than the baseline explicit scope because they short-circuit other lookups. - Limit the number of wildcard patterns: While pattern matching is cached, having many patterns increases the initial match time. The difference between 3 and 50+ patterns is minimal after caching, but organizing scopes hierarchically can help.
- Set
auto_detection.enabled=>falseif you always use explicit scopes: Auto-detection adds overhead per call due to stack trace inspection and reflection. - Reduce
auto_detection.stack_depthif auto-detection is needed: The default of 10 frames is usually sufficient; deeper stacks add overhead. - Leverage early filtering: Logs that get filtered out (below threshold) or from suppressed scopes exit early and are faster than logs that pass through. This means the package has minimal impact on "noisy" debug logging in production when those scopes are set to higher levels.
- Avoid passing very large context arrays if performance is critical: While the overhead is modest, it does add up at high volumes.
Testing
composer test # standard test suite composer test-performance # performance evaluation test suite
Changelog
Please see CHANGELOG for more information on what has changed recently.
Contributing
Please see CONTRIBUTING for details.
Security Vulnerabilities
Credits
License
The MIT License (MIT). Please see License File for more information.