markdoel / laravel-log-insight
Parse Laravel logs, cluster recurring errors and get AI-powered root cause analysis and fix suggestions.
Requires
- php: ^8.2
- illuminate/console: ^12.0|^13.0
- illuminate/contracts: ^12.0|^13.0
- illuminate/database: ^12.0|^13.0
- illuminate/http: ^12.0|^13.0
- illuminate/queue: ^12.0|^13.0
- illuminate/support: ^12.0|^13.0
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/pint: ^1.20
- orchestra/testbench: ^10.0|^11.0
- phpunit/phpunit: ^11.5|^12.5
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-11-03 09:40:07 UTC
README
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
singleanddailylog 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:
ClusterDetectedandAnalysisCompleted, 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, inlog-insight:similar {cluster}and on the dashboard's cluster page. See Similar clusters. - Extensible:
LogParser,Fingerprinter,Redactor,LlmClientandEmbeddingClientare 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;
syncworks 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
linethey 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
methodfalls 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
mailchannel and your application's default mailer. -
Slack needs the optional Slack channel package:
composer require laravel/slack-notification-channel
Set
LOG_INSIGHT_SLACK_WEBHOOK_URLto post to an incoming webhook, orLOG_INSIGHT_SLACK_CHANNELto post as your Slack app, using the bot token inservices.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.
-
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
-
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. -
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 --jsonaddssimilarto every cluster: a list ofid,similarity,exception_class,level,message,location,occurrences,last_seen_atandstatus, ornullwhile 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:similarfor 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_MODELrunlog-insight:embedagain. 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.
AnalyzeCommanddelegates toLogAnalyzer,ReportCommandtoClusterReport(andSimilarClustersfor--json),SimilarCommandtoSimilarClusters,EmbedCommandtoEmbeddingScheduler. The dashboard'sClusterListcomponent usesClusterReporttoo,ClusterPageusesClusterDetailReportandAnalysisScheduler. -
Everything that leaves the server passes the
Redactor. That is the only way fromClusterContextBuildertoLlmClient, and fromClusterEmbeddertoEmbeddingClient. -
Similar clusters hang off the
ClusterDetectedevent: theQueueClusterEmbeddinglistener is only registered when the feature is enabled, and queues anEmbedClusterJob. To use another embedding provider, implementEmbeddingClientand bind it the same way asLlmClient. -
Contracts in
src/Contractscan be rebound in the container. To use another AI provider, implementLlmClientand 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
Authorizationheaders 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_keyorsecret, 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=falseand 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.