Search by

markdoel / laravel-log-insight

markdoel

Parse Laravel logs, cluster recurring errors and get AI-powered root cause analysis and fix suggestions.

Package info

github.com/markdoel/laravel-log-insight

pkg:composer/markdoel/laravel-log-insight

Statistics

Installs: 1

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.0.0 2026-10-26 10:35 UTC

README

tests static analysis License: MIT

Laravel Log Insight reads your Laravel logs, groups recurring errors into clusters and asks an AI model for the root cause and a fix, one request per cluster.

Instead of scrolling through thousands of log lines, you get a short list: this error happened 40 times since yesterday, it starts in OrderController.php:42, and this is most likely why.

Features

  • Log parsing: reads Laravel's default Monolog format, from both single and daily log files. Multi-line messages, stack traces and previous exceptions are merged into one entry. Files are streamed, so large logs are fine.
  • Clustering: each error gets a fingerprint made of the exception class, the normalized message and the topmost application frame. IDs, UUIDs, e-mail and IP addresses, timestamps, URLs and variable path segments are normalized away, so the same bug lands in the same cluster. Each cluster tracks its count and its first and last occurrence.
  • Deploy-stable fingerprints (opt-in): identify the application frame by its method instead of its line, so a deploy that only shifts lines keeps the existing clusters and causes no new AI requests. See Fingerprint frame.
  • Idempotent runs: analyzing the same log twice never counts an entry twice.
  • Privacy first: a redaction layer removes secrets, tokens, passwords, credentials, e-mail and IP addresses before anything leaves your server. You can add your own patterns.
  • AI analysis: one queued job per new cluster calls the Anthropic Messages API through Laravel's HTTP client, with no SDK. The context is the representative stack trace plus the source code around the affected line. The answer is structured JSON: root_cause, suggested_fix, affected_files, severity, confidence.
  • Cost control:
    • A cluster that already has an analysis is never sent again.
    • Invalid answers get exactly one correction request.
    • Permanent failures are recorded and not retried automatically. They can be retried by hand from the dashboard.
  • Notifications (opt-in): one queued mail and/or Slack message per analysis at or above a minimum severity, with the root cause and the suggested fix. See Notifications.
  • Events: ClusterDetected and AnalysisCompleted, so you can attach your own notifications or ticket creation.
  • Reports: a terminal table, or JSON for scripts and CI.
  • Dashboard (opt-in): a Livewire page in your app with the cluster list, filters for time range, minimum severity and analysis status, and pagination, plus a detail page per cluster with the analysis, stack trace, a 14-day chart and the latest occurrences. Protected by a gate that only allows the local environment by default. See Dashboard.
  • Similar clusters (opt-in): embeds every new cluster with Voyage AI and finds clusters with different fingerprints that describe a similar problem, for example the same bug thrown from two controllers. Shown in log-insight:report --json, in log-insight:similar {cluster} and on the dashboard's cluster page. See Similar clusters.
  • Extensible: LogParser, Fingerprinter, Redactor, LlmClient and EmbeddingClient are interfaces bound in the container.

Requirements

  • PHP 8.2 or higher
  • Laravel 12 or 13 (Laravel 13 needs PHP 8.3+)
  • A queue worker for the AI analysis (any driver; sync works too)
  • An Anthropic API key if you want AI analysis. Parsing and clustering work without one.
  • Optionally a Voyage AI API key for similar clusters.

Installation

composer require markdoel/laravel-log-insight

php artisan vendor:publish --tag=log-insight-migrations
php artisan migrate

Optionally publish the config file:

php artisan vendor:publish --tag=log-insight-config

Add your API key to .env:

ANTHROPIC_API_KEY=sk-ant-...

Usage

Analyze the logs of the last 24 hours:

php artisan log-insight:analyze --since=24h
Option Description
--file= Analyze one file instead of the configured log directory
--since= Only entries newer than 30m, 24h, 7d, 2w or a date like 2026-01-31
--dry-run Parse and cluster, but store nothing and call no AI provider

Then look at the results:

php artisan log-insight:report
php artisan log-insight:report --since=7d --severity=high
php artisan log-insight:report --limit=50 --json > report.json
Option Description
--limit=10 Number of clusters to show
--since= Only clusters that occurred since then
--severity= Only analyzed clusters with at least this severity: low, medium, high, critical
--json Print the complete report, including suggested fixes, as JSON. With similar clusters enabled, every cluster also gets a similar list.

Find clusters similar to cluster 3 (needs similar clusters):

php artisan log-insight:similar 3
php artisan log-insight:similar 3 --threshold=0.8 --limit=10 --json
Option Description
--limit= Number of similar clusters to show (default LOG_INSIGHT_SIMILARITY_LIMIT, 5)
--threshold= Minimum cosine similarity from 0 to 1 (default LOG_INSIGHT_SIMILARITY_THRESHOLD, 0.85)
--json Print the result as JSON

Running it on a schedule

Analyze new entries every hour, in routes/console.php:

use Illuminate\Support\Facades\Schedule;

Schedule::command('log-insight:analyze --since=2h')->hourly()->withoutOverlapping();

The overlapping time window is intentional and harmless: entries are never counted twice. New clusters are queued for analysis automatically, so make sure a queue worker is running.

Example output

log-insight:analyze:

   INFO  Parsed 8 entries (4 with exception) from 1 file.

   INFO  Found 5 clusters (5 new).

+-------+------------------------------------+-----------------------------------------------------------------+---------------------------------------------+-----+
| Count | Error                              | Message                                                         | Location                                    | New |
+-------+------------------------------------+-----------------------------------------------------------------+---------------------------------------------+-----+
| 2     | ModelNotFoundException             | No query results for model [App\Models\Order] {n}               | app/Http/Controllers/OrderController.php:42 | yes |
| 1     | ERROR                              | Import failed for row {n} Expected column "sku" to be presen... | -                                           | yes |
| 1     | TypeError                          | App\Services\InvoiceRenderer::render(): Argument #{n} ($invo... | app/Services/InvoiceRenderer.php:19         | yes |
| 1     | UniqueConstraintViolationException | SQLSTATE[{n}]: Integrity constraint violation: {n} Duplicate... | app/Services/RegistrationService.php:31     | yes |
| 1     | ERROR                              | Payment gateway returned an unexpected response                 | -                                           | yes |
+-------+------------------------------------+-----------------------------------------------------------------+---------------------------------------------+-----+

   INFO  Queued 5 clusters for AI analysis.

log-insight:report after the analysis (example data; cluster 2 is still waiting for its analysis):

   INFO  Top 3 of 5 clusters, most frequent first.

+----+-------+------------------------+-------------------------------------------------------+---------------------------------------------+--------------+----------------+-----------------------------------------------------------------+
| ID | Count | Error                  | Message                                               | Location                                    | Last seen    | Severity       | Root cause                                                      |
+----+-------+------------------------+-------------------------------------------------------+---------------------------------------------+--------------+----------------+-----------------------------------------------------------------+
| 1  | 2     | ModelNotFoundException | No query results for model [App\Models\Order] {n}     | app/Http/Controllers/OrderController.php:42 | 21 hours ago | medium (82%)   | OrderController::show() uses findOrFail() for orders that cu... |
| 2  | 1     | ERROR                  | Import failed for row {n} Expected column "sku" to... | -                                           | 22 hours ago | pending        | -                                                               |
| 3  | 1     | TypeError              | App\Services\InvoiceRenderer::render(): Argument #... | app/Services/InvoiceRenderer.php:19         | 23 hours ago | critical (90%) | SendInvoiceJob runs for invoices whose customer was deleted,... |
+----+-------+------------------------+-------------------------------------------------------+---------------------------------------------+--------------+----------------+-----------------------------------------------------------------+
  Use --json for the full analysis including suggested fixes.

One cluster from log-insight:report --json (example data, shortened):

{
    "id": 3,
    "type": "exception",
    "level": "critical",
    "exception_class": "TypeError",
    "message": "App\\Services\\InvoiceRenderer::render(): Argument #{n} ($invoice) must be of type App\\Models\\Invoice, null given, called in /var/www/html/app/Jobs/SendInvoiceJob.php on line {n}",
    "location": "app/Services/InvoiceRenderer.php:19",
    "occurrences": 1,
    "first_seen_at": "2026-10-25T11:05:30+00:00",
    "last_seen_at": "2026-10-25T11:05:30+00:00",
    "status": "analyzed",
    "analysis": {
        "severity": "critical",
        "confidence": 0.9,
        "root_cause": "SendInvoiceJob runs for invoices whose customer was deleted, so the invoice lookup returns null before it reaches the renderer.",
        "suggested_fix": "Load the invoice with findOrFail() in SendInvoiceJob::handle() and delete the job for missing invoices.",
        "affected_files": [
            "app/Jobs/SendInvoiceJob.php"
        ],
        "provider": "anthropic",
        "model": "claude-opus-5-5",
        "analyzed_at": "2026-10-25T11:06:02+00:00"
    },
    "analysis_error": null
}

The full report also contains filters, total, and per cluster the fingerprint, the original sample_message, file and line.

Configuration

All options live in config/log-insight.php and can be set via .env:

Variable Default Description
ANTHROPIC_API_KEY – API key. Without it, clusters are stored but not analyzed.
LOG_INSIGHT_MODEL claude-opus-5-5 Model for the analysis. claude-sonnet-5-5 or claude-haiku-5-5 cost less.
LOG_INSIGHT_EFFORT high Thinking depth: low, medium, high, xhigh, max. Lower is cheaper.
LOG_INSIGHT_MAX_TOKENS 16000 Output limit per request, including the model's thinking.
LOG_INSIGHT_TIMEOUT 300 HTTP timeout in seconds.
LOG_INSIGHT_FALLBACKS true Let the API retry a declined request on Anthropic's recommended fallback model. Disable for models without server-side fallback support.
LOG_INSIGHT_ANALYSIS_ENABLED true Set to false to only parse and cluster.
LOG_INSIGHT_MIN_LEVEL error Only entries at or above this level are clustered.
LOG_INSIGHT_FINGERPRINT_FRAME line How the application frame enters the fingerprint: line (path.php:42) or method (path.php::Class::method). See Fingerprint frame.
LOG_INSIGHT_CONTEXT_LINES 15 Source lines sent before and after the affected line.
LOG_INSIGHT_MAX_TRACE_LINES 60 Stack trace lines sent per cluster.
LOG_INSIGHT_REDACT_TRACE_ARGUMENTS true Replace string arguments in stack frames.
LOG_INSIGHT_LOG_PATH storage/logs Directory with the log files.
LOG_INSIGHT_LOG_PATTERN laravel*.log File pattern, matches single and daily logs.
LOG_INSIGHT_BASE_PATH base_path() Project root, used to resolve paths and read source excerpts.
LOG_INSIGHT_QUEUE_CONNECTION / LOG_INSIGHT_QUEUE default Queue connection and name for AnalyzeClusterJob.
LOG_INSIGHT_DB_CONNECTION default Database connection for the log_insight_* tables.
LOG_INSIGHT_NOTIFICATIONS_ENABLED false Send a notification for severe analyses. See Notifications.
LOG_INSIGHT_NOTIFICATIONS_MIN_SEVERITY critical Lowest severity that notifies: low, medium, high, critical.
LOG_INSIGHT_NOTIFICATIONS_MAIL_TO – Comma-separated mail recipients. Empty sends no mail.
LOG_INSIGHT_SLACK_WEBHOOK_URL – Slack incoming webhook URL. Needs laravel/slack-notification-channel.
LOG_INSIGHT_SLACK_CHANNEL – Slack channel such as #errors, used with your Slack bot token when no webhook URL is set.
LOG_INSIGHT_DASHBOARD_ENABLED false Register the dashboard routes (cluster list and cluster detail page). Needs livewire/livewire. See Dashboard.
LOG_INSIGHT_DASHBOARD_PATH log-insight URL path of the dashboard. Cluster pages live at {path}/clusters/{id}.
dashboard.middleware (config only) ['web'] Middleware for the dashboard routes. The viewLogInsight gate is always added.
LOG_INSIGHT_SIMILARITY_ENABLED false Embed new clusters and show similar clusters. Needs VOYAGE_API_KEY and the separate migration. See Similar clusters.
VOYAGE_API_KEY – Voyage AI API key. Without it, similar clusters stay off.
LOG_INSIGHT_EMBEDDING_MODEL voyage-4 Embedding model. voyage-4-large is more accurate, voyage-4-lite cheaper. Only vectors of the same model are compared.
LOG_INSIGHT_SIMILARITY_THRESHOLD 0.85 Minimum cosine similarity (greater than 0, at most 1) for a cluster to count as similar.
LOG_INSIGHT_SIMILARITY_LIMIT 5 Similar clusters shown per cluster.
LOG_INSIGHT_VOYAGE_BASE_URL https://ai.mongodb.com Embeddings API base URL. Use https://api.voyageai.com for keys from the Voyage AI platform.
LOG_INSIGHT_EMBEDDING_TIMEOUT 30 HTTP timeout in seconds for embedding requests.

The application directories that count for the "topmost application frame" are set in fingerprint.app_paths (default app/, src/, routes/, database/). Vendor code never counts.

Fingerprint frame

By default the topmost application frame is identified by file and line, e.g. app/Http/Controllers/OrderController.php:42. That is precise, but a deploy that only adds or removes lines above the failing call moves the line number, so the same bug starts a new cluster and is sent to the AI provider again.

With LOG_INSIGHT_FINGERPRINT_FRAME=method the frame is identified by the method that contains it instead:

app/Http/Controllers/OrderController.php::App\Http\Controllers\OrderController::show

The method is read from the stack trace: the function that contains frame #k's line is the call shown in frame #k+1, and for the throw location it is the call of frame #0. Closure names from PHP 8.4+ and anonymous classes are reduced to {closure} and class@anonymous, because their names contain line numbers too. When the method cannot be determined (for example the frame is the last one before {main}), the line form is used as a fallback.

Trade-offs:

  • Coarser clusters. Two different errors of the same exception class and message that are thrown from different lines of the same method end up in one cluster. With line they stay separate.
  • Fewer duplicate clusters and AI requests. Line shifts from unrelated changes no longer split a cluster. Renaming or moving the method still does.
  • Location column. Reports show the method form as the location. The source excerpt for the AI analysis is still taken around the affected line, which is looked up in the cluster's most recent stack trace.
  • Switching modes starts new clusters. The frame is part of the fingerprint hash, so errors seen after a switch, in either direction, get new clusters and a new analysis. Existing clusters and analyses stay in the database but no longer receive occurrences, except for errors where method falls back to the line form. Switch once, ideally right after a release, and expect a one-off round of AI requests.

line stays the default, and its fingerprints are identical to those of earlier versions.

Notifications

Log Insight can tell your team about severe clusters as soon as their analysis is stored. It is off by default:

LOG_INSIGHT_NOTIFICATIONS_ENABLED=true
LOG_INSIGHT_NOTIFICATIONS_MIN_SEVERITY=high
LOG_INSIGHT_NOTIFICATIONS_MAIL_TO=ops@example.com,dev@example.com
LOG_INSIGHT_SLACK_WEBHOOK_URL=https://hooks.slack.com/services/...

Every analysis at or above min_severity sends exactly one queued notification to all configured routes. It contains the error class, the normalized message, the location, the occurrence count, severity and confidence, the root cause, the suggested fix and the log-insight:report command to run for details. Analyses below the threshold, and analyses without any configured route, send nothing.

  • Mail uses Laravel's built-in mail channel and your application's default mailer.

  • Slack needs the optional Slack channel package:

    composer require laravel/slack-notification-channel

    Set LOG_INSIGHT_SLACK_WEBHOOK_URL to post to an incoming webhook, or LOG_INSIGHT_SLACK_CHANNEL to post as your Slack app, using the bot token in services.slack.notifications.bot_user_oauth_token (see the Laravel docs). The webhook URL wins when both are set. If a Slack route is configured but the package is not installed, Log Insight logs a warning that names the missing package and still sends the mail.

Notifications are queued on the same connection and queue as AnalyzeClusterJob (LOG_INSIGHT_QUEUE_CONNECTION / LOG_INSIGHT_QUEUE). The listener is only registered when notifications are enabled, so a disabled installation does no extra work. Changing LOG_INSIGHT_NOTIFICATIONS_ENABLED needs a fresh boot, for example php artisan config:cache and a queue worker restart.

An invalid min_severity throws an exception as soon as the next analysis completes, so a typo never silences the notifications without you noticing. The analysis itself is stored before that.

Dashboard

Log Insight ships a small dashboard for the browser: the stored clusters, most frequent first, with their count, error, message, location, last occurrence, analysis status and severity. You can filter by time range (last hour, 24 hours, 7 days, 30 days or all time; the last 7 days by default), minimum severity and status (analyzed, pending, failed), and page through the results 25 at a time. The filters and the page are kept in the URL, so a filtered view can be bookmarked or shared.

Click a message to open the cluster's detail page at /log-insight/clusters/{id}. It shows:

  • the cluster facts: error, normalized and latest message, occurrence count, level, first and last occurrence, location, the file and line it was thrown in and the fingerprint,
  • the latest analysis: root cause, suggested fix, affected files, severity, confidence, provider and model,
  • for a failed analysis, the recorded failure reason,
  • the occurrences per day for the last 14 days (today included, in app time) as a small inline SVG bar chart, with a table view,
  • the stack trace of the most recent occurrence,
  • the 20 most recent occurrences with time, level, channel and message.

Retrying a failed analysis

When an analysis failed permanently, for example because the provider rejected the request, the cluster is not sent again automatically. Its detail page shows the failure reason and a Retry analysis button. Retrying clears analysis_failed_at and analysis_error and queues a new AnalyzeClusterJob on the configured queue connection and queue, so a queue worker has to run. The button only exists for failed clusters: a cluster that has an analysis is never sent again, and pending clusters are queued by log-insight:analyze as before. With analysis disabled or without an API key the page explains why the button is missing. Clicking twice, or in two tabs, queues the cluster once.

It needs Livewire 4, which is an optional dependency, and it is off by default:

composer require livewire/livewire
LOG_INSIGHT_DASHBOARD_ENABLED=true
# LOG_INSIGHT_DASHBOARD_PATH=log-insight

Then open /log-insight. If the dashboard is enabled but Livewire is not installed, the page answers with 503 and the composer require command instead of an error. Nothing else in the package depends on Livewire.

The route uses the web middleware group by default. To change it, publish the config and edit dashboard.middleware, for example ['web', 'auth'].

Authorization

Access is controlled by the viewLogInsight gate, like Horizon and Telescope. Out of the box it only allows requests while APP_ENV is local, so the dashboard is closed in production until you decide who may see it. Define the gate yourself, usually in App\Providers\AppServiceProvider::boot():

use App\Models\User;
use Illuminate\Support\Facades\Gate;

public function boot(): void
{
    Gate::define('viewLogInsight', function (User $user) {
        return in_array($user->email, [
            'admin@example.com',
        ]);
    });
}

Your definition replaces the default one, also in the local environment. A gate whose first parameter is not nullable never allows guests, so add auth to dashboard.middleware if guests should be redirected to the login page instead of getting a 403. The gate is checked on the page request and on every Livewire request the page makes, so filtering, paging and retrying a failed analysis are protected too. Anyone who can see the dashboard can retry a failed analysis.

Customizing the views

The dashboard uses Blade views with their own CSS: no build step, no Tailwind, nothing to compile. It follows the operating system's light or dark mode. To change the markup, publish the views:

php artisan vendor:publish --tag=log-insight-views

They are copied to resources/views/vendor/log-insight. The styles live in partials/styles.blade.php.

Similar clusters

Fingerprints are exact on purpose: a different exception class, message or application frame starts a new cluster. That keeps clusters clean, but the same underlying bug can show up as several clusters, for example a missing relation that throws in OrderController and in InvoiceController, or a message that changed wording in a deploy. Similar clusters find those groups with text embeddings from Voyage AI. They are off by default.

  1. Publish and run the separate migration. It adds one table, log_insight_cluster_embeddings, and leaves the existing tables alone:

    php artisan vendor:publish --tag=log-insight-similarity-migrations
    php artisan migrate
  2. Enable the feature and add your key:

    LOG_INSIGHT_SIMILARITY_ENABLED=true
    VOYAGE_API_KEY=...

    Voyage AI model API keys are created in MongoDB Atlas under AI Model APIs and work with the default base URL. If you have a key from the Voyage AI platform instead, also set LOG_INSIGHT_VOYAGE_BASE_URL=https://api.voyageai.com.

  3. Embed the clusters you already have, once:

    php artisan log-insight:embed

From then on, log-insight:analyze queues one EmbedClusterJob per new cluster, on the same queue connection and queue as AnalyzeClusterJob. Each job sends one short text and stores one vector per cluster and model. Clusters that are embedded already are never sent again.

What is embedded: the exception class (or the level, for plain log messages), the normalized message and the application frame, for example:

Illuminate\Database\Eloquent\ModelNotFoundException
No query results for model [App\Models\Order] {n}
at app/Http/Controllers/OrderController.php:24

The text passes the same redaction layer as everything else. No stack trace, no source code and no raw message is sent.

How similarity is computed: the cosine similarity of two vectors, from 0 (unrelated) to 1 (the same). A cluster counts as similar when it reaches LOG_INSIGHT_SIMILARITY_THRESHOLD (0.85 by default). The comparison runs in PHP over the stored vectors, so no vector database is needed.

Where you see it:

  • log-insight:similar {cluster} lists the most similar clusters with their similarity, count, error, message, location and analysis status.
  • log-insight:report --json adds similar to every cluster: a list of id, similarity, exception_class, level, message, location, occurrences, last_seen_at and status, or null while the cluster has no embedding yet. The table output and the JSON without the feature stay exactly as before.
  • The dashboard's cluster page shows a Similar clusters section that links to each similar cluster.

Trade-offs:

  • Tune the threshold. Similarity depends on your messages. Short, generic messages score high with each other. Start with the default, look at log-insight:similar for a few clusters you know, and adjust.
  • Changing the model starts over. Vectors of different models cannot be compared, so after changing LOG_INSIGHT_EMBEDDING_MODEL run log-insight:embed again. The old vectors stay in the table until you delete them.
  • Scale. Every lookup reads all vectors of the configured model, in chunks of 500. That is fast for thousands of clusters. A JSON report computes the similar clusters of all its rows in one pass.
  • Cost. One small request per new cluster, a few dozen tokens each.

Custom redaction patterns

'redaction' => [
    'trace_arguments' => true,
    'patterns' => [
        '/\bORD-\d{6}\b/' => '[ORDER_ID]',
        '/customer #\d+/i' => 'customer #[ID]',
    ],
],

Custom patterns run after the built-in rules. An invalid regular expression fails loudly as soon as the redactor is resolved, so a typo never turns redaction off without you noticing.

Architecture

flowchart LR
    subgraph analyze ["log-insight:analyze"]
        direction LR
        Files[("Log files")] --> Locator[LogFileLocator]
        Locator --> Parser["LogParser<br/>(MonologLogParser)"]
        Parser --> Fingerprinter["Fingerprinter<br/>(DefaultFingerprinter)"]
        Fingerprinter --> Aggregator[ClusterAggregator]
        Aggregator --> Repository[ClusterRepository]
    end

    Repository --> DB[("log_insight_*<br/>tables")]
    Repository -. "ClusterDetected" .-> Listeners([Your listeners])
    Repository --> Scheduler[AnalysisScheduler]
    Scheduler --> Queue[["AnalyzeClusterJob<br/>(queue)"]]

    subgraph job ["AnalyzeClusterJob"]
        direction LR
        Context[ClusterContextBuilder] --> Redactor["Redactor<br/>(PatternRedactor)"]
        Redactor --> Llm["LlmClient<br/>(AnthropicClient)"]
        Llm --> Validator[AnalysisResponseParser]
    end

    Queue --> Context
    Source[("Source code")] --> Context
    Llm <--> API{{"Anthropic<br/>Messages API"}}
    Validator --> DB
    Validator -. "AnalysisCompleted" .-> Listeners

    DB --> Report["log-insight:report<br/>dashboard<br/>(ClusterReport)"]
Loading
  • Commands only call services. AnalyzeCommand delegates to LogAnalyzer, ReportCommand to ClusterReport (and SimilarClusters for --json), SimilarCommand to SimilarClusters, EmbedCommand to EmbeddingScheduler. The dashboard's ClusterList component uses ClusterReport too, ClusterPage uses ClusterDetailReport and AnalysisScheduler.

  • Everything that leaves the server passes the Redactor. That is the only way from ClusterContextBuilder to LlmClient, and from ClusterEmbedder to EmbeddingClient.

  • Similar clusters hang off the ClusterDetected event: the QueueClusterEmbedding listener is only registered when the feature is enabled, and queues an EmbedClusterJob. To use another embedding provider, implement EmbeddingClient and bind it the same way as LlmClient.

  • Contracts in src/Contracts can be rebound in the container. To use another AI provider, implement LlmClient and bind it:

    $this->app->singleton(\MarkusDoell\LogInsight\Contracts\LlmClient::class, MyProviderClient::class);

Reacting to events

For mail and Slack, the built-in notifications are usually enough. For anything else, listen to the events:

use Illuminate\Support\Facades\Event;
use MarkusDoell\LogInsight\Ai\Severity;
use MarkusDoell\LogInsight\Events\AnalysisCompleted;

Event::listen(function (AnalysisCompleted $event) {
    if ($event->analysis->severity === Severity::Critical) {
        // notify the on-call channel, open a ticket, ...
    }
});

Privacy and data protection

Log Insight sends data to a third-party AI provider. Please read this before enabling the AI analysis in production.

What is sent, per new cluster, exactly once:

  • the exception class or log level, a representative error message and the occurrence count with first and last timestamp
  • the representative stack trace (by default at most 60 lines)
  • up to 31 lines of your application source code around the affected line (15 before, 15 after)

What is removed first: every payload passes the redaction layer:

  • private keys and JWTs
  • Authorization headers and bearer tokens
  • common API key formats (AWS, GitHub, Slack, Stripe, Anthropic, OpenAI, Google, Laravel APP_KEY)
  • credentials in connection URLs
  • values of secret keys such as password, token, api_key or secret, in query strings, JSON, PHP arrays, env/YAML style and SQL
  • e-mail addresses and IPv4/IPv6 addresses
  • by default, all string arguments in stack frames

Similar clusters: when enabled, Voyage AI receives, once per cluster and model, the exception class or level, the normalized message and the application frame, redacted. No stack trace, source code or raw message is sent. Check the MongoDB/Voyage AI terms before you send production data.

Notifications: when enabled, mail and Slack notifications contain the normalized message, the location, the root cause and the suggested fix. They pass the same redaction layer first, and no stack trace or source code is included.

What is never sent: vendor source code, files outside your project root (paths from the log are resolved safely, so ../ tricks don't work), and clusters that already have an analysis.

Limits you should know:

  • Redaction is pattern based. A secret or personal datum that follows no recognizable pattern, for example a customer name in free text, can get through. Add custom patterns for data specific to your domain.
  • Source code is sent as is, apart from redaction. If your code must not leave your infrastructure, set LOG_INSIGHT_ANALYSIS_ENABLED=false and use clustering and reports only.
  • The local database stores the original, unredacted messages and stack traces, just like your log files do.
  • How the provider handles the data is governed by its terms. Check the Anthropic commercial terms and privacy documentation and your data processing agreement before you send production data.

Log content is treated as untrusted input: the system prompt tells the model to analyze it as data and never follow instructions inside it.

Testing

composer test      # PHPUnit
composer analyse   # PHPStan (Larastan, level max)
composer lint      # Pint

The test suite never contacts a real AI or embedding provider. Every request is faked with Http::fake(), and stray requests fail the test. CI runs the suite on PHP 8.2–8.5 with Laravel 12 and 13, against SQLite, MySQL and Postgres.

How this was built

I built this package with Claude Code as a pair programmer, the same way I work day to day. I defined the scope, architecture and quality bar (interfaces, redaction before any API call, Larastan at max level, a test suite that never hits a real provider), reviewed every phase and steered the design decisions. Claude Code wrote most of the code under that direction.

Changelog

See CHANGELOG.md.

License

The MIT License (MIT). See LICENSE.md.