macpaw/sentry-monolog-adapter
A Monolog handler that forwards log records to Sentry as events and breadcrumbs, making it easy to capture errors and context from Laravel and PHP applications. Configurable log levels and simple integration with existing logging stacks.
This is a thin adapter, not a full Sentry integration. It gives you two things: a Monolog handler that pushes records to Sentry's Hub, and a Symfony Messenger integration that keeps Sentry scope/trace context alive across async message boundaries. If you already run sentry/sentry-symfony, the Monolog side adds little; the Messenger context propagation is the actual differentiator.
Minimal path:
composer require macpaw/sentry-monolog-adapter sentry/sentry-symfonysentry/sentry-symfony it's autowired as Sentry\State\HubInterface. Standalone, call Sentry\init(['dsn' => ...]) early in the kernel boot and grab the hub via SentrySdk::getCurrentHub().# config/services.yaml
services:
MacPaw\SentryMonologAdapter\SentryHandler:
arguments:
$hub: '@Sentry\State\HubInterface'
$level: !php/const Monolog\Level::Error
$bubble: false
$tags: { app: 'checkout' }
# config/packages/monolog.yaml
monolog:
handlers:
sentry:
type: service
id: MacPaw\SentryMonologAdapter\SentryHandler
# keep your normal file/stream handlers below it
First use case: funnel ERROR+ records to Sentry so that things logged deliberately ($logger->error('payment gateway timeout', ['gateway' => 'stripe'])) show up as Sentry messages, while uncaught exceptions still arrive via Sentry's own exception listener. Trigger a test error, confirm it lands in the project, then tune the level.
Channel-scoped handlers. Don't send every channel to Sentry. Define the handler once, then reference it from specific channels:
monolog:
channels: ['payments', 'messaging']
handlers:
sentry:
type: service
id: MacPaw\SentryMonologAdapter\SentryHandler
channels: ['payments', 'messaging']
This keeps doctrine, http_client, and event noise out of your Sentry quota.
Exceptions vs. messages. The handler inspects $record->context['exception'] and routes it through captureException; otherwise it calls captureMessage with the formatted record and level mapped to a Sentry severity. The practical consequence: if you catch and re-log an exception, include the exception object in context to get a proper Sentry issue with a stack trace, not a flat message.
try {
$this->client->charge($order);
} catch (GatewayException $e) {
$this->logger->error('charge failed', ['exception' => $e, 'order_id' => $order->id]);
throw $e;
}
Messenger trace continuity. Register the package's Messenger middleware/context so a log emitted inside an async handler is correlated with the HTTP request that dispatched the message. Without it, worker logs appear as orphaned events with no trace link back to the user action:
# config/packages/messenger.yaml
framework:
messenger:
buses:
command_bus:
middleware:
- MacPaw\SentryMonologAdapter\Messenger\SentryContextMiddleware
Check the package source for the exact middleware id in your installed version — the class name has shifted across releases.
Bubbling for dual-write. bubble: false stops records from reaching later handlers, so with the snippet above nothing hits your file log. If you want both, either set bubble: true or place the Sentry handler last in the chain and leave bubbling on for the preceding handlers.
Tags for triage. Pass static tags at construction (app, service, region) and dynamic ones via $record->extra. In Sentry these become filterable facets — worth the five minutes it takes to standardise a component key across services.
sentry/sentry-symfony is installed, it registers its own Monolog handler for uncaught exceptions. Adding this handler at Error level means a thrown exception is captured twice. Either drop the level to Critical, restrict channels, or disable the bundle's handler and rely on this one.HubInterface only works when the Sentry bundle is loaded. Without it, define the service manually against SentrySdk::getCurrentHub() inside a factory, otherwise you get "cannot autowire" at cache warmup.Monolog\Logger::ERROR; Monolog 3 expects Monolog\Level::Error. Passing the wrong type produces a confusing TypeError during container build, not at runtime.ignore_exceptions still applies. Records suppressed by sentry/sentry-symfony's ignore_exceptions config won't reach Sentry even if this handler fires, because filtering happens at the Hub/Scope layer. If an error is mysteriously missing, check that config first.before_send are your throttle. This handler does no rate limiting — a log loop in a worker will hammer the DSN. Set traces_sample_rate and use before_send to drop known-noisy messages before they leave the process.$logger->error('sentry-wiring-check') from a route and from a Messenger handler, confirm both land, then check the Messenger one carries the trace id of the dispatching request.write() if you need to reshape the payload (e.g., strip PII from $record->context before it hits captureMessage). Prefer that over forking the package.AbstractProcessingHandler subclass plus a Messenger middleware is maybe 150 lines), so have that as a fallback if the project goes quiet. Pin the version and read the diff on upgrade — pre-1.0–style API changes have happened before.How can I help you explore Laravel packages today?