Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Log Parser Laravel Package

kassner/log-parser

kassner/log-parser turns Laravel, Apache, Nginx, and other log files into structured data. Define patterns, stream large files line by line, and extract dates, levels, messages, and context for analysis, reporting, or dashboards. Fast and memory-efficient in any PHP or Laravel app.

Deep Wiki
Context7

Getting Started

Install with Composer — the package has no Laravel service provider and no published config, so there is nothing to register:

composer require kassner/log-parser

Verify the resolved version and PHP constraint in composer.json; the project has been quiet since mid-2023.

Where to look first

  1. vendor/kassner/log-parser/src/LogParser.php — the only class that matters. Read the $patterns map (placeholder → regex) and the field-name map that turns %h into host, %t into stamp, etc. That table is the API.
  2. The README format table — it documents which Apache/Nginx placeholders are supported (%h, %l, %u, %t, %r, %>s, %b, %{Referer}i, %{User-Agent}i, %D, %T, …).
  3. src/FormatException.php — the only exception you will routinely catch.

Smallest useful call

use Kassner\LogParser\LogParser;

$parser = new LogParser();
$parser->setFormat('%h %l %u %t "%r" %>s %b');

$entry = $parser->parse(
    '127.0.0.1 - frank [10/Oct/2000:13:55:36 -0700] "GET /apache_pb.gif HTTP/1.0" 200 2326'
);

// stdClass: host, logname, user, stamp (DateTime), request, status, ...

First real use case — an Artisan command that streams today's Nginx access log into a table:

// app/Console/Commands/IngestAccessLog.php
$parser = new LogParser();
$parser->setFormat(config('logs.format'));
$parser->setTimezone(new DateTimeZone(config('app.timezone')));

$handle = fopen(storage_path('logs/access.log'), 'r');
$batch = [];

while (($line = fgets($handle)) !== false) {
    try {
        $row = $parser->parse(rtrim($line, "\r\n"));
    } catch (FormatException $e) {
        continue; // skip malformed lines, count them
    }

    $batch[] = [
        'host'       => $row->host,
        'request'    => $row->request,
        'status'     => (int) $row->status,
        'bytes'      => (int) $row->responseBytes,
        'stamp'      => $row->stamp,
    ];

    if (count($batch) >= 500) {
        RequestLog::insert($batch);
        $batch = [];
    }
}

Then a dashboard or alert on top of that table. That is 90% of day-to-day use of this library in a Laravel app.

Implementation Patterns

Config-driven format. The format string is the single most fragile part of the integration. Put it in config/logs.php and allow an env override per server (Apache combined vs. Nginx with $request_time):

// config/logs.php
return [
    'format' => env('LOG_FORMAT', '%h %l %u %t "%r" %>s %b "%{Referer}i" "%{User-Agent}i"'),
    'paths'  => ['nginx' => storage_path('logs/nginx-access.log')],
];

Singleton binding. The parser is cheap to instantiate but the format conversion is wasted work per line. Bind one configured instance:

// app/Providers/AppServiceProvider.php
$this->app->singleton(LogParser::class, function () {
    $p = new LogParser();
    $p->setFormat(config('logs.format'));
    $p->setTimezone(new DateTimeZone(config('app.timezone')));
    return $p;
});

Inject it into commands, jobs, and parsers.

Streaming, never file(). Use fopen/fgets or a generator so a 2 GB log doesn't blow memory_limit. Wrap it in a reusable iterator:

function parsedLines(LogParser $parser, string $path): Generator {
    $h = fopen($path, 'r');
    while (($line = fgets($h)) !== false) {
        try { yield $parser->parse(rtrim($line, "\r\n")); }
        catch (FormatException) { continue; }
    }
    fclose($h);
}

Chunked writes, not per-row inserts. Accumulate into arrays of ~500–1000 rows and Model::insert(); the parser is fast, the DB round-trips are not.

Queue big imports. For monthly archives or rotated .log.1 files, dispatch a ParseLogFile job per file and let a worker do the streaming. Pass the file path, not the contents.

Reuse parsed rows for multiple concerns. One ingestion pass feeds several features: traffic analytics (request path aggregated), error dashboards (status >= 500), security rules (repeated 401/403 from one host in a window), and bot detection via userAgent.

Tolerant format fallback. If you serve both Apache and Nginx, try the combined format first, then a second parser with the Nginx format, and cache which one succeeded per file:

foreach ($candidateParsers as $p) {
    try { $entry = $p->parse($line); break; }
    catch (FormatException) {}
}

Eloquent casting. $casts = ['stamp' => 'datetime', 'status' => 'integer', 'bytes' => 'integer']; — the parser hands you a DateTime, so store it directly rather than stringifying, and normalize the - byte value to 0 on the way in.

Testing. Keep a handful of real log lines as fixtures in tests/Fixtures/ and assert against the parsed object; log formats drift when ops teams change server configs, and a fixture test catches that before ingestion silently drops everything.

Gotchas and Tips

FormatException is not exceptional — it is normal. Any line that doesn't match the regex (truncated write, a stray log line from another vhost, a restart mid-line) throws. Always catch it inside the loop and increment a counter; if the counter spikes, your format is wrong, not your logs.

Quoted fields with embedded quotes break naive formats. %r and %{User-Agent}i are wrapped in " in the format string; a user agent or referer containing an escaped quote can fail the match. Test with real traffic, not curl output.

%b vs %B differ. %b emits - for zero bytes (CLF), %B emits 0. If your format says %b, cast defensively: (int) $row->responseBytes will silently become 0, which is fine — but don't assume the property is numeric.

Timezone on %t. The timestamp carries its own offset (-0700). Call setTimezone() if you want the parsed DateTime normalized to app time, otherwise you will store mixed-offset rows. Do it once at construction.

Field names are not the placeholders. %h → host, %t → stamp, %b → a bytes field, %{User-Agent}i → camel-cased. Don't guess — var_dump() one parsed entry in tinker before writing migrations or mappers, or read the map in LogParser.php.

Set the format before parsing, once. setFormat() recompiles the internal regex; call it in the constructor path (service provider / factory), never inside the per-line loop.

Regex escaping in the format string. Literal characters in your format (brackets around %t, quotes, pipes) are interpolated into a regex. A format copied from an exotic LogFormat directive with regex metacharacters can produce surprising matches — validate against a real line first.

No gzip support. access.log.gz must be decompressed first (gzopen/gzread, or shell out to zcat). The library only sees strings.

IPv6 and %a. %h gives the client IP or hostname; if you log %a (remote IP) the placeholder must appear in your format string or the field simply won't be populated — the parser only returns fields your format declares.

Performance. Expect regex-per-line throughput in the tens-to-low-hundreds of thousands of lines per second on PHP 8. That is fine for a nightly import of a few million lines; if you need more, parse in parallel chunks via queued jobs rather than micro-optimizing the parser.

Maintenance risk. Last release was July 2023 and the API surface is small but frozen. Pin the major version in composer.json, and keep the parser behind your own thin AccessLogParser service class so a future replacement (or a fork) touches one file. That service is also the right extension point: subclass LogParser there if you need a custom placeholder or field mapping.

No Laravel integration means no guardrails. There is no published config, no facade, no health check. Everything — path discovery, rotation handling, offset tracking so you don't re-ingest the same lines — is yours to build. Store a last_ingested_offset/inode per file rather than re-reading the whole log each run.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
skyraptor/chatter
edlugz/sasapay
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource