ias/newrelic-logger-bundle
A Symfony bundle that integrates New Relic logging with your application's Monolog setup, forwarding log records to New Relic for centralized visibility and analysis. Simplifies configuring a New Relic log handler so you can correlate application events with observability data without custom wiring.
Do not add this to composer.json. ias/newrelic-logger-bundle is a Symfony bundle (Bundle class + DI extension + compiler pass), it is archived, and it has 0 stars / 0 dependents. In a Laravel app it will be inert: no package discovery, no config/logging.php driver, no service provider. Pulling it in drags symfony/http-kernel and symfony/dependency-injection along for the sake of one Monolog handler.
Use it the only way it has value: as a ~200-line reference implementation of the New Relic Logs ingestion contract.
Minimal steps:
git clone https://github.com/theias/newrelic-logger-bundle into a scratch directory — do not composer require.AbstractProcessingHandler subclass that formats a record and POSTs it to https://log-api.newrelic.com/log/v1 (EU: https://log-api.eu.newrelic.com/log/v1) with a license key.DependencyInjection/Configuration.php next. That is where the real answers live: endpoint/region, API key, minimum log level, batching, and which enrichment attributes get stamped on each record (service.name, host.name, trace.id, span.id).message, timestamp, attributes (flat key/value map), plus per-record metadata. New Relic rejects on shape, not on intent.First use case: stand up a read-only spike — a Laravel custom logging channel that ships a single test record to New Relic Logs and confirms it is queryable in the UI, correlated with an APM trace. One to two days of work end to end, including tests, and it is throwaway by design.
Before you write a line of handler code, check the two supported paths that probably remove the need for the handler entirely:
stderr/stdout with a JSON formatter in a Stderr channel, and let the New Relic PHP agent forward. Zero custom code, vendor-supported, batching and backpressure included.newrelic/monolog-enricher adds trace.id/span.id correlation to records; pair it with agent forwarding.Only build a bespoke HTTP sender if agent installation is genuinely impossible (locked-down containers, shared hosting, serverless).
Start with a plain channel in config/logging.php that emits structured JSON. Most teams stop here once the agent is forwarding:
'newrelic' => [
'driver' => 'monolog',
'handler' => Monolog\Handler\StreamHandler::class,
'with' => ['stream' => 'php://stderr'],
'formatter' => Monolog\Formatter\JsonFormatter::class,
'level' => env('LOG_LEVEL', 'info'),
],
If you truly must ship directly, register a custom driver with Log::extend() rather than subclassing anything from the repo:
// app/Providers/LoggingServiceProvider.php
Log::extend('newrelic', function ($app, array $config) {
$logger = new Monolog\Logger($config['name'] ?? 'newrelic');
$http = new Monolog\Handler\NewRelicLogsHandler(
apiKey: config('services.newrelic.license_key'),
endpoint: config('services.newrelic.logs_endpoint'), // region-specific
client: $app->make(GuzzleHttp\ClientInterface::class), // reuse what you ship
);
// Always compose a guaranteed local fallback.
$fallback = (new Monolog\Handler\StreamHandler(storage_path('logs/newrelic-fallback.log')))
->setLevel(Monolog\Logger::ERROR);
$logger->pushHandler(new Monolog\Handler\WhatFailureGroupHandler([$http, $fallback]));
return $logger;
});
A synchronous HTTP call per log record couples request latency and request reliability to a third-party SaaS. Wrap in a BufferHandler and flush on close/reset, or use a FingersCrossedHandler so you only ship a buffer when something actually goes wrong:
$buffer = new Monolog\Handler\BufferHandler($http, $bufferLimit = 50, $level, true, true);
Cap the buffer. An unbounded in-process buffer is a memory leak with extra steps, and it silently loses everything on deploy/restart.
This is the whole point of the integration: logs that carry trace.id / span.id turn log search into a debugging workflow. Do it in a processor, not in the handler, so it applies to every channel:
$logger->pushProcessor(function (LogRecord $record) {
$record->extra['service.name'] = config('app.name');
$record->extra['host.name'] = gethostname();
if ($traceId = newrelic_get_trace_metadata()['trace_id'] ?? null) {
$record->extra['trace.id'] = $traceId;
}
return $record;
});
Ingest volume maps directly to the bill, and every attribute you ship leaves the perimeter. Put level filtering and PII redaction in a processor that runs before the transport handler, and keep the redaction rules in-repo under review — an abandoned library gives you nowhere to land that policy.
StreamHandler fallback.AbstractProcessingHandler::write() now receives a LogRecord value object, and the record array is gone. Every ported line must be validated against Monolog 3 explicitly.log-api.newrelic.com vs log-api.eu.newrelic.com — send EU-account logs to the US endpoint and they vanish. This is the single most common "it's logging but nothing shows up" bug.composer require gets you an inert package. There is no service provider, no facade, no config/logging.php integration — that is on you.WhatFailureGroupHandler or equivalent, and a circuit breaker that degrades to local logging on 429/5xx rather than propagating. A log shipper that can 500 a checkout is worse than no log shipper.message and flatten/drop attributes above a size threshold..env committed to the repo. Ingest keys are scoped to the account and let anyone write to your log stream.message, timestamp, service.name, attributes, trace IDs — in a contract test. Do not verify by watching a dashboard.Buffer/FingersCrossed/WhatFailureGroup). There is no supported extension point in the bundle itself — it is a closed, unmaintained unit.newrelic/monolog-enricher). You keep the pattern and drop the dependency.How can I help you explore Laravel packages today?