Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Logging Bundle Laravel Package

keboola/logging-bundle

A Symfony bundle that adds Keboola-specific logging to your application, wiring Monolog handlers, processors, and configuration so logs are structured, consistent, and ready for Keboola's logging infrastructure. Drop it in, configure a few options, and get production-ready logging without boilerplat...

View on GitHub
Deep Wiki
Context7

Getting Started

Know what you are installing first. keboola/logging-bundle is a Symfony bundle, not a Laravel package. It ships a DependencyInjection\KeboolaLoggingExtension that Symfony's kernel autoloads — Laravel has no equivalent hook, so composer require alone will change nothing in a Laravel app. The entire library is three classes; the part you actually want is the Monolog processor that stamps extra.dd.trace_id / extra.dd.span_id onto every record when the ddtrace PHP extension is loaded.

2.0.0 is the release that finally installs on modern Laravel. 1.x was Monolog 2 only, so Composer would refuse to resolve it alongside Laravel 10/11/12 (monolog/monolog:^3.0). 2.0.0 switches to Monolog 3.x and adds Symfony 7.x support. That removes the dependency blocker — but it also means every processor snippet written for 1.x is now wrong (see Gotchas). Pin explicitly:

composer require keboola/logging-bundle:^2.0
composer show monolog/monolog   # must be 3.x; if it says 2.x you took the wrong path

Two viable paths, pick one up front:

  1. Lift the processor into app/Logging/ (recommended for most Laravel apps — it is ~20 lines and removes a symfony/http-kernel dependency you will never boot). Copy it from the 2.0.0 source, not from a blog post or your old vendored copy.
  2. Install the package and reference only the processor class, accepting the dead weight.

Minimal steps

  1. Verify the extension exists in the SAPI that actually runs your app, not just your CLI:
    php -m | grep ddtrace          # php-fpm / octane container
    php artisan about              # confirm the same PHP binary Laravel uses
    
    No extension → everything below is a no-op, by design.
  2. Read the source. It is the documentation:
    ls vendor/keboola/logging-bundle/src
    cat vendor/keboola/logging-bundle/src/DependencyInjection/KeboolaLoggingExtension.php
    cat vendor/keboola/logging-bundle/src/Monolog/*.php   # note the LogRecord signature
    
    The load() method is the whole integration story: function_exists('DDTrace\current_context') → register the processor tagged monolog.processor.
  3. Wire it into the channel you ship to Datadog via a tap:
    // config/logging.php
    'datadog' => [
        'driver' => 'daily',
        'path'   => storage_path('logs/datadog.log'),
        'tap'    => [App\Logging\DatadogTraceTap::class],
    ],
    
    // app/Logging/DatadogTraceTap.php
    namespace App\Logging;
    
    use Keboola\LoggingBundle\Monolog\DataDogContextProcessor; // confirm FQCN under src/Monolog
    
    final class DatadogTraceTap
    {
        public function __invoke($logger): void
        {
            if (! function_exists('DDTrace\current_context')) {
                return; // mirrors the bundle's own guard
            }
    
            $logger->pushProcessor(new DataDogContextProcessor());
        }
    }
    
    The tap itself is unchanged by 2.0.0 — LogManager still hands you the Monolog Logger. The processor is what changed.
  4. Prove it works: hit a route inside an active span and dump one record. extra.dd.trace_id must be non-zero and must equal the trace id Datadog shows for that request. If you get a TypeError mentioning Monolog\LogRecord, you are running a 1.x-era processor on a Monolog 3 stack — see Gotchas.

First use case: a 500 in production. You find the log line, take dd.trace_id, jump straight to the trace in Datadog, and see the failing span — no correlation-id plumbing, no X-Request-Id middleware glue.

Implementation Patterns

Pattern 1 — Symfony-side reuse (shared codebases). In any app that boots a Symfony kernel this is still genuinely zero-config: no config/packages/*.yaml, no env vars, no registration. The function_exists check at container-compile time means it is safe to require in environments without ddtrace (local, CI, other services). 2.0.0 only widens the matrix (symfony/* ^6.4|^7.0); nothing to do beyond composer require — but if your shared codebase was on Symfony 6.0–6.3, this is a version bump, not a drop-in.

Pattern 2 — Channel-scoped tap in Laravel (preferred). Push the processor onto only the Datadog channel. Keeps your single/stack channel output free of dd.* noise for local tail -f and for non-Datadog log shippers. The tap key is resolved by LogManager per channel, so this composes cleanly with daily + json formatters. This is the pattern least affected by the Monolog 2→3 jump: the tap's signature never changed, only the processor it instantiates.

Pattern 3 — Conditional decorator instead of editing the bundle. If you need extra attributes (e.g. dd.service, dd.version, or a tenant id), write your own processor that calls the bundle's processor first, then amends the record. Monolog 3 makes records immutable, so this is a rewrite, not a copy-paste, from any 1.x example:

use Monolog\LogRecord;

final class EnrichedDatadogProcessor
{
    public function __construct(private readonly DataDogContextProcessor $inner) {}

    public function __invoke(LogRecord $record): LogRecord
    {
        $record = ($this->inner)($record);

        $extra = $record->extra;
        $extra['dd']['service'] ??= config('app.name');

        return $record->with(extra: $extra); // returns a clone; do not mutate
    }
}

Processors run in push order and each receives the previous one's output — chain them, don't fork the class. Note the return type: LogRecord in, LogRecord out. The old array $record): array contract is gone.

Pattern 4 — Queues are the hard part; plan for it explicitly. A ddtrace trace does not survive the queue boundary for free. Processors are evaluated per-record against current context, so a worker that pops a job without an active span emits zero/garbage ids. Carry the originating trace id in the job payload and re-stamp it:

// dispatching side
MyJob::dispatch()->withTracing(DDTrace\current_context()['trace_id'] ?? null);

Then in a JobProcessing listener, push a tiny processor that writes that value into extra.dd.trace_id (keep the exact key shape, and return $record->with(extra: $extra) under Monolog 3). Combine with Log::withContext() for human-readable fields — but remember context is not extra, and Datadog correlation reads the dd.* attributes, not the message string.

Pattern 5 — Request-scoped enrichment, not processor-scoped. For user/tenant/route data use Log::withContext([...]) in middleware. Let the bundle's processor own only dd.trace_id / dd.span_id. Two processors writing the same keys is the fastest way to get silently wrong correlation.

Testing pattern. Copy the approach from tests/datadogStubs.php: define a namespaced stub DDTrace\current_context() in your bootstrap before any log assertion, return a fixed ['trace_id' => 123, 'span_id' => 456], log once, and assert the JSON payload. The function is called at record-handling time, so you can define the stub conditionally in the test itself. Keep both branches covered — "extension present" and "extension absent" (the latter is what every other environment runs). If you assert on the processor directly rather than on emitted JSON, assert against Monolog\LogRecord now, not an array.

Gotchas and Tips

1.x processors are a TypeError on Monolog 3 — and this is the migration. This is the one that bites. Under Monolog 3, pushProcessor() invokes your callable with a LogRecord, not an array. Any vendored or copied processor typed __invoke(array $record): array throws on the first log write, which in a Laravel app means a 500 from a log call. If you lifted the processor into app/Logging before this release, audit it now:

grep -rn "array \$record" app/Logging
grep -rn "ProcessorInterface" app/  # Monolog 2 and 3 both define it, differently

Fix by retyping to LogRecord and returning $record->with(extra: ...). There is no shim; ProcessorInterface is a different interface in 3.x (Monolog 3 also dropped the adapter classes, but this bundle never used them).

The repository is a read-only mirror. Development happens in keboola/platform-libraries under libs/logging-bundle/. CI force-pushes the split back to keboola/logging-bundle on every green build — any commit or PR you open there is overwritten. If you need a fix, composer require won't help; open the PR against the monorepo with a fix(logging-bundle): … message. For a 1-star, 0-dependent internal library that just made a breaking major bump for a two-line release note, the pragmatic answer is often: vendor the ~20 lines and move on — and since you're vendoring, you're now insulated from exactly this kind of churn.

Pin ^2.0 and check your other Monolog-touching packages. A major Monolog jump is ecosystem-wide: log formatters, datadog/dd-trace-adjacent integrations, custom handlers, and any package that implements Monolog\Processor\ProcessorInterface must all be Monolog 3-ready. composer update keboola/logging-bundle alone is not the operation — composer why monolog/monolog first, and expect to bump siblings in the same commit.

"I installed it and nothing happened" is the expected outcome in Laravel. No service provider, no config publish, no autoload of the extension. That is not a bug in your setup. What did change in 2.0.0 is that Composer will now stop refusing to install it.

The key names are load-bearing and fail silently. extra.dd.trace_id and extra.dd.span_id are the exact shape Datadog's log/trace correlation expects. Rename them "for readability," or nest them under extra.datadog.*, and nothing errors — correlation just stops working. Treat them as a wire format. The Monolog major bump did not change this contract; do not assume the upgrade rewrote it.

extra nesting vs. Datadog's root attributes. The processor writes into Monolog's extra array. Datadog's reserved attributes are dd.trace_id / dd.span_id at the log-record root. Depending on your formatter (JsonFormatter nests extra as a sub-object) the ids may land at extra.dd.trace_id in the emitted JSON. Check an actual line:

tail -n 1 storage/logs/datadog.log | jq '.extra.dd // .dd'

If they are nested, add a Datadog log-processing pipeline remap or a formatter that flattens extra to the root. Budget an hour for this the first time. Worth re-running this check after the Monolog 3 upgrade — formatter behaviour and extra handling are exactly the sort of thing that shifts in a major.

Zero/garbage ids when there is no active span. current_context() outside a span returns nothing meaningful. Emitting dd.trace_id: 0 on every line is noise that pollutes your Datadog facets. Guard in your wrapper: drop the keys when the trace id is falsy.

64-bit ids and JSON. Trace ids exceed JavaScript's safe integer range; some log pipelines and JSON consumers mangle them into floats, and correlation then fails without an error. If you cannot correlate, stringify the ids in your formatter and retest before touching anything else.

The function_exists guard is a namespaced string. Any package in the process that happens to declare DDTrace\current_context (a stub in a dev dependency, a polyfill) satisfies it — including on production. That is exactly how the bundle's own functional test exercises the "extension present" branch, so it's a real, reachable failure mode. composer why DDTrace-style checks are worth a minute in incident triage.

Long-lived workers (Octane, queue daemons). The processor reads context at record-handling time, so it doesn't cache — good. But context itself can bleed across requests in a Swoole/Octane worker; reset it per request, and verify ddtrace compatibility with your Octane version. Do not "optimize" by resolving the ids once in a singleton.

Still no php/ext-ddtrace constraint declared. Composer will not stop you from installing this on an unsupported PHP version or without the extension. Note the matrix moved with 2.0.0: Monolog 3 requires PHP ≥ 8.1 and Symfony 7 requires ≥ 8.2, so CI now proves a narrower floor than 1.x did — Laravel 9-era PHP 8.0 stacks are out. Verify the constraint list yourself (composer show keboola/logging-bundle) rather than trusting the readme; the release note mentions neither PHP versions nor the extension.

Debugging checklist, in order. (0) composer show monolog/monolog — 3.x means LogRecord-typed processors only. (1) php -m | grep ddtrace in the runtime container. (2) DD_TRACE_ENABLED not set to false. (3) Is a span actually active — wrap the test in a manual span and log inside it. (4) Inspect one raw record's extra array. (5) Only then look at the Datadog pipeline. Steps 0–4 catch nearly everything, and step 0 is new: it will catch the upgrade breakage in seconds.

Extension point to remember: the bundle deliberately has none — no config tree, no compiler pass, no interface. Which means the correct place to customize is your own processor wrapper in App\Logging, leaving the vendor class untouched. That also makes swapping it out trivial when the mirror/versioning finally annoys you enough — and after a major bump whose entire changelog is two bullets, that threshold gets lower.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle