naoray/laravel-github-monolog
Laravel package that connects Monolog to GitHub, letting your application automatically create GitHub issues from logged errors and exceptions. Useful for tracking production errors directly in your repository's issue tracker with minimal setup.
Install the handler and publish its config:
composer require naoray/laravel-github-monolog
php artisan vendor:publish --provider="Naoray\LaravelGithubMonolog\ServiceProvider"
Set the minimum viable environment:
LOG_CHANNEL=stack
LOG_STACK=single,github
LOG_GITHUB_TOKEN=ghp_xxx # fine-grained PAT with Issues: Read & Write
LOG_GITHUB_REPOSITORY=org/repo
LOG_GITHUB_LEVEL=error
Wire the channel into a stack so logs still land locally even when the GitHub API is unavailable:
// config/logging.php
'stack' => [
'driver' => 'stack',
'channels' => ['single', 'github'],
'ignore_exceptions' => false,
],
Where to look first:
config/github-monolog.php — token/repo, level, labels, and the signature generator (the thing that decides whether an exception opens a new issue or comments on an existing one).<!-- Signature: {signature} --> marker (see Gotchas).First use case: point the channel at a scratch repository, LOG_GITHUB_LEVEL=debug, then php artisan tinker and throw a known exception a few times. You should get exactly one issue with multiple comments. If you get N issues, your signature generator is too specific — that is the primary daily-use behaviour to verify before shipping.
Stack it, don't replace it. Use stack with single/daily + github. The GitHub channel is your triage/alerting surface; the file channel is your forensic record and your fallback when the API is rate-limited or the token expires.
Tune level per channel. error for GitHub, debug locally. Set LOG_GITHUB_LEVEL per environment so staging exercises the pipeline without spamming the production repo.
Treat the signature generator as your grouping policy. This is the core operational lever:
The default generator hashes the error into a stable signature; the practical move is to extend/bind your own and normalise volatile fragments out of the message before hashing, while deliberately keeping structural context (e.g. tenant class, not tenant ID) if you want per-segment issues.
Queued jobs changed behaviour in 3.11 — build around it. Errors thrown inside jobs are now grouped by job class + queue, not by the route that dispatched them. That is usually a big noise reduction for apps that dispatch the same job from many endpoints. If you relied on route-level granularity, encode your discriminator explicitly into the signature rather than relying on the old behaviour, which no longer exists.
Use labels as your triage automation hook. Set labels in config (e.g. bug, auto-reported, needs-triage) and let GitHub Actions, Projects, or on-call rotations take it from there. This is what turns the package from "an issue tracker" into a lightweight incident workflow.
Attach context that survives the jump. Push app version, environment, request ID, and queue name into the issue body via Monolog context processors on the channel. Without it, every issue body looks identical and you cannot tell a v2.4 regression from a v2.5 one.
Write against the API, not the network. In tests, Http::fake(['api.github.com/*' => Http::response([], 201)]) and assert on the recorded request payload — issue title, labels, and whether it hit POST /issues versus POST /issues/{n}/comments. That assertion is the cheapest regression test you can have for signature-generator changes.
Guard the channel by environment. Disable in local/testing via the logging config (or a token-less channel) so a stray report() on a laptop doesn't file issues on the shared board.
Duplicate issues after the 3.11 upgrade are expected, and only partly auto-healed. v3.11.0's new grouping opened fresh issues for job errors that previously had route-keyed threads. 3.11.1 adds a one-time lookup for the issue v3.10 would have used (default signature generator only) and comments there instead. Practically:
The compat lookup is removed in the next major. Do not build tooling that depends on legacy issue resolution; verify your custom generator and archive/close legacy threads before v4.
Do not strip the signature marker from the comment stub. Comments on existing issues now embed <!-- Signature: {signature} -->, which is how subsequent occurrences match an open issue. If you published the stub and rewrote it as friendly prose, you will silently re-introduce duplicates. Keep that HTML comment exactly as-is — it is invisible in rendered markdown but load-bearing.
Silent failure is the default failure mode. The handler talks to the network from inside your error path. Wrap the channel so a GitHub outage, a revoked token, or a rate-limit 403 never escalates into a 500. Check ignore_exceptions on the stack and consider WhatFailureGroupHandler semantics for anything non-critical.
Never route the handler's own failures back into itself. Logging a 401 from the GitHub channel through the same channel is an infinite issue-spam loop. Exclude api.github.com / GuzzleHttp exceptions from this handler explicitly.
Rate limits and issue flood are real. Authenticated PATs get ~5,000 requests/hour, and every issue/comment costs at least one. A single bad loop can burn the hourly budget and open thousands of issues. Mitigate: sane level (error, not debug), a coarser signature for high-volume job classes, and your own throttle (cache-key per signature) ahead of Log::channel('github').
Job grouping collapses tenants. Grouping by job class + queue means a job failing for 100 customers becomes one issue with 100 comments. That is usually right; if you need per-tenant visibility, put the tenant class into the custom signature — but budget for issue count accordingly.
Scrub before you ship. Issue bodies and stack traces go to GitHub, where they persist and, on public repos, are public and indexable. Strip credentials, request payloads, PII, and customer identifiers via a context processor rather than hoping exceptions are tidy. Also beware GitHub markdown: leading @ in a username or email auto-mentions real accounts; backtick-escape log lines.
Token hygiene. Use a dedicated bot account or machine user so issue attribution isn't muddled with a human's account, and scope the fine-grained PAT to Issues only on the single target repo.
When it misbehaves, debug in this order: (1) is the channel actually in the stack for that environment, (2) does the token have Issues write on the repo, (3) what does your signature hash resolve to for the failing exception — log it locally, (4) did you publish and then modify the comment stub, (5) is something swallowing handler exceptions. The signature value is the answer to almost every "duplicate issue" report.
How can I help you explore Laravel packages today?