Skip to content

Repository files navigation

App Logger

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

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 underlying LoggerManager instance. Access it via AppLogger::getFacadeRoot() or inject SchoolPalm\AppLogger\Loggers\LoggerManager directly 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 file driver degrades as the log file grows. For production systems with high log volumes, prefer the database driver.


Drivers

Database Driver

Writes log entries to the app_logs database table.

  • Uses the AppLog Eloquent model.
  • Supports an optional database connection via the database_connection config.
  • The context and data columns are stored as JSON.
  • Exception information is stored as a JSON string in the exception column.
// 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 AppContext and LogPayload
  • Feature tests for DatabaseLogger, FileLogger, both repositories, the facade, and the LoggerManager
  • Integration tests for end-to-end logging workflows

License

This package is open-sourced software licensed under the MIT license.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages