schoolpalm / app-logger
A Laravel package for application logging with database and file drivers.
Requires
- php: ^8.2
- illuminate/support: ^10.0|^11.0|^12.0
Requires (Dev)
- laravel/pint: ^1.13
- mockery/mockery: ^1.6
- orchestra/testbench: ^8.0|^9.0|^10.0
- pestphp/pest: ^3.8
- pestphp/pest-plugin-laravel: ^3.2
- phpunit/phpunit: ^11.0
This package is auto-updated.
Last update: 2026-07-21 20:37:24 UTC
README
A flexible, multi-driver application logging package for Laravel. Supports database and file storage, structured context, log querying, and multi-tenant path resolution.
Table of Contents
- Installation
- Configuration
- Architecture Overview
- Log Levels
- Usage
- AppContext API
- Querying Logs
- Drivers
- Path Resolvers
- Context Normalization
- API Endpoints
- HTTP Controller
- Testing
- License
Installation
composer require schoolpalm/app-logger
Publish Configuration
php artisan vendor:publish --provider="SchoolPalm\AppLogger\AppLoggerServiceProvider" --tag="config"
Publish Migrations (required for the database driver)
php artisan vendor:publish --provider="SchoolPalm\AppLogger\AppLoggerServiceProvider" --tag="app-logger-migrations"
Then run the migration:
php artisan migrate
Configuration
The package publishes a configuration file at config/app-logger.php.
<?php return [ /* |-------------------------------------------------------------------------- | Default Logging Driver |-------------------------------------------------------------------------- | | Supported drivers: database, file | */ 'driver' => env('APP_LOGGER_DRIVER', 'database'), /* |-------------------------------------------------------------------------- | Database Connection |-------------------------------------------------------------------------- | | Optional database connection name for the database driver. | Leave null to use the default connection. | */ 'database_connection' => env('APP_LOGGER_DATABASE_CONNECTION', null), /* |-------------------------------------------------------------------------- | File Storage Disks |-------------------------------------------------------------------------- | | The 'app-logger' disk is used by default for file-based logging. | */ 'disks' => [ 'app-logger' => [ 'driver' => 'local', 'root' => storage_path(), ], ], /* |-------------------------------------------------------------------------- | File Logging Settings |-------------------------------------------------------------------------- */ 'file' => [ 'disk' => 'app-logger', 'path_resolver' => \SchoolPalm\AppLogger\Path\DefaultLogPathResolver::class, ], /* |-------------------------------------------------------------------------- | Context Settings |-------------------------------------------------------------------------- */ 'context' => [ 'normalize' => true, ], /* |-------------------------------------------------------------------------- | Query Limits |-------------------------------------------------------------------------- */ 'query' => [ 'max_limit' => 1000, ], ];
Architecture Overview
┌─────────────────────────────────────────────────────────────┐
│ AppLogger Facade │
│ (Proxy to LoggerManager via 'app-logger') │
└───────────────────────────┬─────────────────────────────────┘
│
┌───────────────────────────▼─────────────────────────────────┐
│ LoggerManager │
│ ┌───────────────┐ ┌───────────────┐ ┌──────────────────┐ │
│ │ DatabaseDriver │ │ FileDriver │ │ LogRepository │ │
│ │ (write only) │ │ (write only) │ │ (read / query) │ │
│ └───────┬───────┘ └───────┬───────┘ └───────┬──────────┘ │
└──────────┼──────────────────┼──────────────────┼────────────┘
│ │ │
▼ ▼ ▼
┌──────────┐ ┌──────────────┐ ┌──────────────┐
│ app_logs │ │ JSONL files │ │ Same backends│
│ (MySQL/ │ │ via Storage │ │ (read path) │
│ Postgres)│ │ (disk) │ │ │
└──────────┘ └──────────────┘ └──────────────┘
Key Components
| Component | Description |
|---|---|
LoggerManager |
Core manager extending Laravel's Manager class. Drives both logging and querying. |
LogDriver |
Interface for writing log entries. Implemented by DatabaseLogger and FileLogger. |
LogRepository |
Interface for reading/querying log entries. Implemented by DatabaseLogRepository and FileLogRepository. |
LogPayload |
Immutable value object representing a single log entry. |
AppContext |
Immutable value object representing execution context (tenant, user, school, module, etc.). |
LogPathResolver |
Interface for resolving file storage paths (only relevant for the file driver). |
ContextNormalizer |
Converts complex context values (models, dates) into JSON-safe primitives. |
Log Levels
The package supports PSR-3-compatible log levels via LogLevel:
| Constant | Value |
|---|---|
LogLevel::EMERGENCY |
emergency |
LogLevel::ALERT |
alert |
LogLevel::CRITICAL |
critical |
LogLevel::ERROR |
error |
LogLevel::WARNING |
warning |
LogLevel::NOTICE |
notice |
LogLevel::INFO |
info |
LogLevel::DEBUG |
debug |
use SchoolPalm\AppLogger\Support\LogLevel; LogLevel::all(); // ['emergency', 'alert', 'critical', 'error', 'warning', 'notice', 'info', 'debug'] LogLevel::isValid('info'); // true LogLevel::isValid('trace'); // false
Usage
Basic Logging
Use the AppLogger facade to write logs:
use SchoolPalm\AppLogger\Facades\AppLogger; AppLogger::info('User signed in'); AppLogger::debug('Cache miss for key users:123'); AppLogger::warning('API rate limit approaching'); AppLogger::error('Payment processing failed');
Logging with Context
Pass an AppContext object to describe the execution environment (tenant, user, school, module, etc.):
use SchoolPalm\AppLogger\Facades\AppLogger; use SchoolPalm\AppLogger\Context\AppContext; $context = AppContext::make([ 'tenant_id' => 10, 'school_id' => 5, 'user_id' => 42, 'module' => 'payments', 'ip' => request()->ip(), ]); AppLogger::info('Payment processed', $context, [ 'amount' => 99.99, 'currency' => 'USD', 'invoice' => 'INV-2024-001', ]);
Using LogPayload
For full control over the log entry, construct a LogPayload directly:
use SchoolPalm\AppLogger\Support\LogPayload; use SchoolPalm\AppLogger\Support\LogLevel; use SchoolPalm\AppLogger\Context\AppContext; $payload = new LogPayload( level: LogLevel::NOTICE, message: 'Student enrollment completed', context: AppContext::make(['tenant_id' => 10]), data: ['student_id' => 100, 'course' => 'Mathematics'], channel: 'enrollments', ); AppLogger::log($payload);
LogPayload also provides static named constructors:
LogPayload::info($message, $context, $data); LogPayload::debug($message, $context, $data); LogPayload::warning($message, $context, $data); LogPayload::error($message, $context, $data); LogPayload::critical($message, $context, $data);
And immutable "with*" modifier methods:
$payload = LogPayload::info('Something happened') ->withContext(AppContext::make(['tenant_id' => 10])) ->withData(['key' => 'value']) ->withChannel('security') ->withException($exception);
Convenience Methods
The facade provides typed convenience methods:
AppLogger::info(string $message, ?AppContext $context = null, array $data = []): bool AppLogger::debug(string $message, ?AppContext $context = null, array $data = []): bool AppLogger::warning(string $message, ?AppContext $context = null, array $data = []): bool AppLogger::error(string $message, ?AppContext $context = null, array $data = [], ?Throwable $exception = null): bool AppLogger::exception(Throwable $exception, ?AppContext $context = null, array $data = []): bool
Exception Logging
Log an exception with full stack trace information:
use SchoolPalm\AppLogger\Facades\AppLogger; try { // some operation } catch (\Exception $e) { AppLogger::exception($e, $context, ['operation' => 'data-export']); }
Or use the error method with an explicit exception:
AppLogger::error('Export failed', $context, ['file' => 'export.csv'], $e);
Scoped Context with withContext
For scenarios where you want to reuse a context across multiple log calls, use the manager's withContext() method (returns a cloned instance):
use SchoolPalm\AppLogger\Facades\AppLogger; $manager = AppLogger::getFacadeRoot(); $logger = $manager->withContext( AppContext::make(['tenant_id' => 10, 'school_id' => 5]) ); $logger->info('Student created', data: ['student_id' => 100]); $logger->warning('Duplicate enrollment detected', data: ['student_id' => 101]); $logger->error('Enrollment failed', data: ['student_id' => 102]);
Note:
withContext()is available on the underlyingLoggerManagerinstance. Access it viaAppLogger::getFacadeRoot()or injectSchoolPalm\AppLogger\Loggers\LoggerManagerdirectly via dependency injection.
AppContext API
AppContext is an immutable value object that carries execution context metadata.
use SchoolPalm\AppLogger\Context\AppContext; // Create a context $context = AppContext::make(['tenant_id' => 10, 'user_id' => 42]); $empty = AppContext::empty(); // Immutable add $context = $context->with('school_id', 5); // Immutable merge $context = $context->merge(['module' => 'documents', 'ip' => '192.168.1.1']); // Read values $context->get('tenant_id'); // 10 $context->get('missing', 'n/a'); // 'n/a' $context->has('user_id'); // true $context->all(); // ['tenant_id' => 10, 'user_id' => 42, ...] // Checks $context->isEmpty(); // false $context->isNotEmpty(); // true $context->count(); // number of attributes // Convert to storage-safe array $context->toArray();
Querying Logs
DatabaseLogRepository
When using the database driver, logs can be queried through the repository:
use SchoolPalm\AppLogger\Facades\AppLogger; // Latest logs $logs = AppLogger::logs()->latest(50); // By severity level $logs = AppLogger::logs()->byLevel('error', 50); // By channel $logs = AppLogger::logs()->byChannel('security', 50); // By context key-value $logs = AppLogger::logs()->byContext('tenant_id', 10, 100); // By context key (any value) $logs = AppLogger::logs()->byContext('tenant_id'); // Search message content $logs = AppLogger::logs()->search('payment failed', 50); // Between dates use Carbon\CarbonImmutable; $logs = AppLogger::logs()->between( CarbonImmutable::parse('-7 days'), CarbonImmutable::now(), 100 ); // Find by ID $log = AppLogger::logs()->find(1); // Paginate $paginator = AppLogger::logs()->paginate(perPage: 50);
The facade's logsByContext shortcut:
$logs = AppLogger::logsByContext('tenant_id', 10, 100);
FileLogRepository
When using the file driver, the FileLogRepository provides the same interface. Logs are read from the configured file path, parsed from JSONL format, and filtered in-memory. The same logs() and logsByContext() methods work transparently.
Note: Query performance on the
filedriver degrades as the log file grows. For production systems with high log volumes, prefer thedatabasedriver.
Drivers
Database Driver
Writes log entries to the app_logs database table.
- Uses the
AppLogEloquent model. - Supports an optional database connection via the
database_connectionconfig. - The
contextanddatacolumns are stored as JSON. - Exception information is stored as a JSON string in the
exceptioncolumn.
// config/app-logger.php 'driver' => 'database', 'database_connection' => null, // uses default connection
File Driver
Writes log entries as newline-delimited JSON (JSONL) to a file on the configured storage disk.
- Each line is a JSON-encoded log payload.
- File location is determined by the
LogPathResolver. - Default path:
storage/logs/app.log.
// config/app-logger.php 'driver' => 'file', 'file' => [ 'disk' => 'app-logger', 'path_resolver' => \SchoolPalm\AppLogger\Path\DefaultLogPathResolver::class, ],
Path Resolvers
Path resolvers are only relevant for the file driver. They determine where log files are stored.
DefaultLogPathResolver
The default resolver writes all logs to a single file:
class DefaultLogPathResolver implements LogPathResolver { public function resolve(AppContext $context): string { return 'logs/app.log'; } }
Resulting path: storage/logs/app.log
TenantLogPathResolver
For multi-tenant applications, use the provided TenantLogPathResolver to isolate logs per tenant and school:
use SchoolPalm\AppLogger\Path\TenantLogPathResolver; // In AppServiceProvider or a dedicated service provider: $this->app->singleton( \SchoolPalm\AppLogger\Contracts\LogPathResolver::class, TenantLogPathResolver::class );
The resolver expects tenant_id and school_id in the AppContext:
$context = AppContext::make([ 'tenant_id' => 10, 'school_id' => 5, ]);
Resulting path: storage/tenants/10/schools/5/app.log
Custom Path Resolver
Implement the LogPathResolver contract to define your own storage strategy:
use SchoolPalm\AppLogger\Contracts\LogPathResolver; use SchoolPalm\AppLogger\Context\AppContext; class ModuleBasedPathResolver implements LogPathResolver { public function resolve(AppContext $context): string { $tenant = $context->get('tenant_id', 'system'); $module = $context->get('module', 'general'); $date = now()->format('Y/m'); return "tenants/{$tenant}/{$date}/{$module}.log"; } }
Register it in the config:
// config/app-logger.php 'file' => [ 'path_resolver' => App\Logging\ModuleBasedPathResolver::class, ],
Or bind it in a service provider:
$this->app->singleton( \SchoolPalm\AppLogger\Contracts\LogPathResolver::class, ModuleBasedPathResolver::class );
Context Normalization
Before storage, context values are normalized via ContextNormalizer to ensure JSON serialization safety:
| Input Type | Normalized Output |
|---|---|
DateTimeInterface |
ISO 8601 string (DATE_ATOM) |
Arrayable (e.g., Eloquent models) |
Recursively normalized array |
Jsonable |
Decoded JSON array |
| Arrays | Recursively normalized |
Objects with __toString() |
String cast |
| Other objects | Fully qualified class name |
Scalars (string, int, bool, null) |
Unchanged |
This normalization is enabled by default via the context.normalize config option.
API Endpoints
The package exposes RESTful HTTP endpoints for reading and writing logs.
Write a log entry
POST /api/app-logger/logs
Request body:
{
"message": "User signed in",
"level": "info",
"context": {
"user_id": 123,
"source": "api"
}
}
Response:
{
"message": "Log created successfully.",
"level": "info",
"context": {
"user_id": 123,
"source": "api"
}
}
Read log entries
GET /api/app-logger/logs?context_key=user_id&context_value=123
Returns log entries containing the given context key with the matching value.
Omit context_value to return all logs with a given context key:
GET /api/app-logger/logs?context_key=user_id
Optional query parameters:
| Parameter | Description |
|---|---|
level |
Filter by log level (e.g., info, error) |
limit |
Number of entries to return (default: 100, max: 1000) |
Response:
{
"data": [
{
"id": 1,
"level": "info",
"message": "User signed in",
"context": {
"user_id": 123,
"source": "api"
},
"created_at": "2026-07-20T15:00:00.000000Z",
"updated_at": "2026-07-20T15:00:00.000000Z"
}
]
}
HTTP Controller
The package's HTTP endpoints are typically registered through the consuming application's routes. You can define them in your routes/api.php file:
use Illuminate\Support\Facades\Route; use SchoolPalm\AppLogger\Facades\AppLogger; Route::prefix('api/app-logger')->group(function () { // Write a log Route::post('/logs', function (\Illuminate\Http\Request $request) { $validated = $request->validate([ 'message' => 'required|string', 'level' => 'required|in:emergency,alert,critical,error,warning,notice,info,debug', 'context' => 'nullable|array', ]); AppLogger::log( new \SchoolPalm\AppLogger\Support\LogPayload( level: $validated['level'], message: $validated['message'], context: \SchoolPalm\AppLogger\Context\AppContext::make( $validated['context'] ?? [] ), ) ); return response()->json($validated); }); // Read logs Route::get('/logs', function (\Illuminate\Http\Request $request) { $key = $request->query('context_key'); $value = $request->query('context_value'); $limit = min( (int) $request->query('limit', 100), config('app-logger.query.max_limit', 1000) ); $logs = AppLogger::logsByContext($key, $value, $limit); if ($request->has('level')) { $logs = $logs->filter( fn($log) => ($log['level'] ?? $log->level) === $request->query('level') )->values(); } return response()->json(['data' => $logs]); }); });
Testing
Run the package tests:
composer test
The test suite covers:
- Unit tests for
AppContextandLogPayload - Feature tests for
DatabaseLogger,FileLogger, both repositories, the facade, and theLoggerManager - Integration tests for end-to-end logging workflows
License
This package is open-sourced software licensed under the MIT license.