A flexible, multi-driver application logging package for Laravel. Supports database and file storage, structured context, log querying, and multi-tenant path resolution.
- Installation
- Configuration
- Architecture Overview
- Log Levels
- Usage
- AppContext API
- Querying Logs
- Drivers
- Path Resolvers
- Context Normalization
- API Endpoints
- HTTP Controller
- Testing
- License
composer require schoolpalm/app-loggerphp artisan vendor:publish --provider="SchoolPalm\AppLogger\AppLoggerServiceProvider" --tag="config"php artisan vendor:publish --provider="SchoolPalm\AppLogger\AppLoggerServiceProvider" --tag="app-logger-migrations"Then run the migration:
php artisan migrateThe 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,
],
];┌─────────────────────────────────────────────────────────────┐
│ 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) │ │ │
└──────────┘ └──────────────┘ └──────────────┘
| 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. |
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'); // falseUse 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');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',
]);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);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 = []): boolLog 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);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 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();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);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.
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 connectionWrites 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 are only relevant for the file driver. They determine where log files are stored.
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
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
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
);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.
The package exposes RESTful HTTP endpoints for reading and writing logs.
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"
}
}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"
}
]
}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]);
});
});Run the package tests:
composer testThe 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
This package is open-sourced software licensed under the MIT license.