smartness / laravel-logging
Structured JSON logging for Laravel with bounded normalization, redaction and request/job correlation
Requires
- php: ^8.2
- ext-json: *
- illuminate/container: ^11.0 || ^12.0 || ^13.0
- illuminate/contracts: ^11.0 || ^12.0 || ^13.0
- illuminate/http: ^11.0 || ^12.0 || ^13.0
- illuminate/log: ^11.0 || ^12.0 || ^13.0
- illuminate/support: ^11.0 || ^12.0 || ^13.0
- monolog/monolog: ^3.0
- psr/log: ^3.0
Requires (Dev)
- laravel/octane: ^2.17
- orchestra/testbench: ^9.0 || ^10.0 || ^11.0
- phpunit/phpunit: ^10.5 || ^11.0 || ^12.0
Suggests
- open-telemetry/api: Attach the currently active trace and span IDs
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-12 11:05:00 UTC
README
General-purpose structured JSON logging for Laravel 11, 12 and 13 using the native Laravel LogManager and Monolog 3. Requires PHP 8.2 or later (8.3 for Laravel 13). Use it in HTTP APIs, web applications, Artisan commands and queue workers, without application-specific models, services or infrastructure.
Each event is a single JSON line (NDJSON), suitable for Grafana Loki or other collectors that ingest JSON logs. The package writes to your existing Laravel stream channels; it does not send logs to a backend or require Loki credentials.
Installation is opt-in: installing the package alone does not change channels,
replace the log service or enable collection of request bodies.
Install
Install from Packagist:
composer require smartness/laravel-logging:^0.1
No custom Composer repository or GitHub credentials are needed.
Use version 0.1.4 or later for Octane lifecycle support, in addition to the redaction and stack-routing corrections in 0.1.3.
Laravel discovers the service provider automatically. Optionally publish config:
php artisan vendor:publish --tag=structured-logging-config
Enable the formatter and identify the service:
STRUCTURED_LOGGING_ENABLED=true STRUCTURED_LOGGING_SERVICE=my-service
Select stream-backed leaf channels in config/structured-logging.php:
'channels' => ['single', 'daily', 'stdout', 'stderr', 'payments'],
Use ['*'] to select all configured stream-backed channels. The package supports
Laravel's single and daily drivers, and monolog channels backed by
StreamHandler subclasses. Existing paths and level thresholds remain in effect.
Stacks inherit the configured leaf handlers; null, Slack and other transports
retain their native behavior and are not converted to JSON by this package.
Publishing configuration does not automatically add an absent channel.
Clear/rebuild Laravel's configuration cache and restart persistent workers when changing configuration. Enable in staging first and check the collector and existing dashboards before production rollout.
Configure for your application
All package options live in config/structured-logging.php. Application-specific
channel definitions and routing stay in Laravel's config/logging.php.
| Option | Default | Purpose |
|---|---|---|
enabled |
false |
Opt in with STRUCTURED_LOGGING_ENABLED=true |
service |
APP_NAME, or laravel |
Identify the application; override with STRUCTURED_LOGGING_SERVICE |
environment |
APP_ENV, or production |
Identify the deployment environment |
channels |
single, daily, stdout, stderr |
Select existing stream channels, or use ['*'] |
request_context |
true |
Add HTTP request correlation and response headers |
job_context |
true |
Add correlation through Laravel queue events |
trace_context |
true |
Include an active OpenTelemetry span when available |
fallback_path |
php://stderr |
Fallback stream; set to null to propagate primary write failures |
Channels that do not exist are skipped. For container logging, define stdout
in config/logging.php if your application does not already have it:
'stdout' => [ 'driver' => 'monolog', 'handler' => \Monolog\Handler\StreamHandler::class, 'with' => ['stream' => 'php://stdout'], 'level' => env('LOG_LEVEL', 'debug'), ],
Then set LOG_CHANNEL=stdout. File-based applications can keep their existing
single or daily channel. HTTP, queue and trace enrichment can each be disabled
independently; CLI logging does not require an HTTP request or a queue connection.
When enabled, the package validates its configuration at application boot, before
Laravel resolves logging channels. Invalid bounds, option types or redaction modes
stop startup with an explicit error. This prevents a bad formatter configuration
from silently selecting Laravel's plaintext emergency logger. Keep package options
fixed after boot; rebuild config:cache and restart workers when changing them.
Errors in unrelated Laravel channel definitions or other packages' taps remain
subject to Laravel's own emergency logging behavior.
Use Laravel logging
use Illuminate\Support\Facades\Log; Log::info('Import completed', ['batch_id' => 123]); Log::channel('payments')->warning('Payment retry scheduled', ['attempt' => 2]); Log::error('Payment failed', ['exception' => $exception]); Log::withContext(['tenant_id' => 456])->info('Import started');
Use context for application-specific identifiers and timer durations. There is no
replacement facade, timer log level or mutable channel selector. Native Laravel
context APIs retain their semantics. Octane's standard listeners clear native
withContext() and shareContext() state between operations. Other persistent
worker integrations must manage the lifecycle of application-added context.
JSON contract
{"schema_version":1,"timestamp":"2026-10-12T10:30:00.000000Z","level":"info","message":"Import completed","service":"my-service","environment":"production","component":"stdout","process_id":42,"hostname":"app-worker","context":{"batch_id":123},"job_id":"job-456"}
- Timestamps are converted to UTC; one event occupies exactly one physical line.
componentis the configured leaf channel. A stack writes to each eligible destination and preserves each destination's component and threshold.- HTTP logs receive one validated/generated request ID per request, reused in
X-Request-Idon successful middleware responses. The ID is stored on the request object. Method, route template and elapsed time are optional metadata. - Job logs include job ID, queue, connection and attempt. Context resets on completion, failure and worker loop boundaries; nested synchronous jobs restore their parent. Exceptions reported after job events retain correlation through a weak map keyed by the exception object, cleared at the next queue loop. Synchronous jobs use their payload UUID when the queue driver has no job ID. Jobs executed inside an HTTP request also retain that request's correlation.
- If
open-telemetry/apiis installed and an active valid span exists,trace_idandspan_idare included. The package never creates spans or exporters. Bind your ownContext\TraceContextimplementation for another tracing system. - Request bodies, concrete URLs, query strings, headers, user lookups and CLI
argument lists are not collected automatically. Existing extra fields from
other processors are preserved and normalized under
extra.
schema_version identifies the envelope contract. Optional enrichment may add
fields. Consumers should tolerate unknown fields and absent optional metadata.
Very large events contain explicit truncation markers; the final minimal fallback
can omit optional fields to honor max_line_bytes.
Redaction and bounds
Known credential keys are replaced completely with [REDACTED]. Matching ignores
case, underscores and hyphens but requires a complete key, so api_key_id and
shipping remain useful. Configure sensitive_fields for your application's
additional credential names. By default, the list extends the built-in fields,
including when an older published config overrides the list. For example:
'sensitive_fields' => ['business_secret', 'integration_credentials'], 'sensitive_fields_mode' => 'extend',
Set sensitive_fields_mode to replace only when deliberately replacing the
entire built-in list. In that mode you must include every key you need to protect.
Case, underscores and hyphens are ignored for both structured keys and textual
assignments. Quoted values support escaped quotes and newlines; an unclosed quoted
secret consumes the remainder of the string so a partial value cannot leak.
Arrays, plain objects, nested JSON strings, exception messages and known key/value and Bearer/Basic patterns in text are covered.
This is not a detector for arbitrary secrets in free text, custom encodings or unknown field names. Callers must avoid logging credentials. Malformed JSON-like strings and oversized strings are omitted instead of retaining unsafe fragments. Domain object serializers/getters are never invoked: arbitrary objects are represented by their type. Pass an explicit array of safe fields when needed. Exception traces omit arguments and objects, and include bounded previous chains. Redaction applies to this package's outputs; Laravel events and unrelated handlers may still receive the original context.
Defaults: 32 KiB per encoded line (including newline), 16 KiB per context, 8 KiB per string, depth 8, 1,000 items per normalization and 30 exception trace frames. Oversized contexts are replaced with a marker. Invalid UTF-8 is substituted; cycles, resources and non-finite numbers have safe representations. Configure budgets to match your collector's limits. Truncation bounds traversal and output, not the memory already allocated by the caller before logging.
Delivery and worker lifecycle
Monolog writes each record immediately. Failed primary stream writes fall back
to php://stderr with the same formatter and level threshold. Set fallback_path
to another stream or null to retain the primary handler's exception behavior.
If both streams fail, Monolog's fallback group drops the event without recursively
logging the failure. Delivery is best effort; process termination or collector
failures require infrastructure-level monitoring.
The package retains Laravel's native manager and handler close/reset APIs, including compatibility with workers that forget channels before forking. Queue integrations must emit Laravel job lifecycle events to receive job metadata.
Laravel Octane
Octane integration activates automatically when the enabled package receives Octane's lifecycle events. Octane remains optional; installing this package does not install Octane or change the application's server.
Processors resolve the current container for every record instead of retaining
the worker's original container. Each HTTP request, task and tick gets its own
job context, including when a service provider warmed the logger before the first
request. Failed operations are discarded with their sandbox. HTTP correlation
stays available during exception reporting and request termination; tasks, ticks
and the base worker do not inherit the last HTTP request. Normal Laravel routes
also return X-Request-Id on exception responses. Octane's Octane::route()
shortcuts bypass Laravel middleware and must add response headers themselves.
Keep Octane's default preparation/reset listeners in config/octane.php, including
FlushLogContext and FlushMonologState. They clear Laravel/Monolog context that
the application or other packages add. Configure package options once at boot;
rebuild the configuration cache and reload workers after configuration changes.
An OpenTelemetry SDK must independently manage its active span's lifecycle.
For container deployments with Octane, use a php://stderr stream channel:
STRUCTURED_LOGGING_ENABLED=true STRUCTURED_LOGGING_SERVICE=my-service LOG_CHANNEL=stderr
Laravel's skeleton already defines stderr. If yours does not:
'stderr' => [ 'driver' => 'monolog', 'handler' => \Monolog\Handler\StreamHandler::class, 'with' => ['stream' => 'php://stderr'], 'level' => env('LOG_LEVEL', 'info'), ],
For FrankenPHP's Artisan launcher, explicitly select the native log output:
php artisan octane:frankenphp --host=0.0.0.0 --log-level=warn
That launcher consumes the child process's stdout without forwarding application
records; its default console renderer can also reformat stderr. The explicit
--log-level preserves raw stderr lines. RoadRunner reserves worker stdout for its
transport protocol, so use
stderr there too. Server/access/startup logs are separate
from the package's application JSON schema; configure the collector accordingly.
The package preserves configured destinations and does not silently reroute channels.
Validation covers Octane 2.x's real Worker lifecycle on Laravel 11–13 and a real
FrankenPHP server on Laravel 12/13, with two independent containers, sequential and
concurrent clients, nested jobs, error recovery and worker reload. The common
lifecycle is used by Octane's server adapters; Swoole and RoadRunner server binaries
are not part of the integration matrix. Arbitrary user-created coroutines/fibers
sharing mutable Laravel services are outside these guarantees.
Kubernetes and multiple replicas
Every pod writes independently to its container's stdout/stderr (stderr for Octane
as above). There is no package-level buffer, shared log file, distributed lock or
direct Loki client. Increasing replicas does not require a shared logger service.
Use the same service and environment across replicas of the same application;
hostname and process_id help identify the emitting instance inside each record.
A process ID alone is not globally unique.
The collector should decode the container runtime's Docker/CRI envelope before parsing application JSON and attach cluster, namespace, workload, pod and container identity from Kubernetes discovery. Keep a small set of stable index labels such as service, environment, cluster and namespace. Keep ephemeral pod names, request IDs, trace IDs, job IDs and user IDs in the body or structured metadata to avoid excessive Loki stream cardinality. JSON fields do not become index labels automatically.
Generated request IDs use 128 random bits; they do not depend on a per-pod counter.
For a request spanning services, the ingress/HTTP client must propagate a validated
X-Request-Id, or your tracing instrumentation must propagate trace context.
The package does not automatically propagate context into outgoing HTTP requests
or asynchronous job payloads. It only reads an active trace and the current job.
Container collection, retries, backpressure, retention and draining during pod termination belong to the runtime/collector. Immediate writes reduce the package's shutdown exposure but do not guarantee delivery after SIGKILL or a collector outage. Validate your actual Kubernetes collector and existing dashboards before rollout; the container smoke test is not a live-cluster certification.
Adopt in an existing Laravel application
If another library replaces Laravel's log binding, restore the native
Illuminate\Log\LogManager before enabling this package. Non-native managers
are rejected with an explicit configuration error. Keep any application-specific
migration switches and rollback procedure in the consuming application.
Select the stream channels to convert and retain their existing levels and destinations. Validate timestamp parsing, component names, context fields, alerts and byte volume with your collector before rollout. The output schema is defined above; this package does not translate an application's previous log schema.
To disable the formatter, set STRUCTURED_LOGGING_ENABLED=false, rebuild the
configuration cache and restart persistent workers. Laravel resumes the normal
formatting configured for each channel. Restoring a previous custom logger, if
present, is a separate application configuration change.
Development
composer install
composer test
composer validate --strict
Tests use Orchestra Testbench with real Laravel/Monolog channel routing and temporary streams. No database, Redis or Loki service is required. CI runs a PHP 8.2–8.5 / Laravel 11–13 compatibility matrix, respecting each framework's PHP requirements, plus minimum-dependency coverage. Laravel 11 compatibility does not imply upstream security support; only its isolated compatibility jobs permit installation of dependencies with advisories. Consumer security policies are unchanged.
The application smoke tests install the package into a fresh Laravel application, publish and cache its configuration, and check CLI, synchronous queue jobs and successive HTTP requests. Pass a new disposable directory:
bash tests/smoke/install.sh /tmp/logging-app 13 bash tests/smoke/loki.sh /tmp/logging-app bash tests/octane/run.sh /tmp/logging-app
The second command requires Docker Compose and starts isolated Alloy and Loki
containers, then removes them. It verifies real file collection, JSON queries,
original timestamps, redaction and low-cardinality labels. The Alloy configuration
in tests/smoke/alloy.config is an example for raw JSON log files. Container stdout
collection additionally needs the appropriate Docker/CRI decoding stage. Adapt
endpoints, authentication and discovery to your infrastructure before staging.
The third command modifies that disposable application, installs Octane, and tests
two FrankenPHP containers using stderr and cached configuration. It removes the
containers and test image afterward. Neither smoke test accesses a production
collector deployment.
References: Laravel logging, Laravel packages, and Loki labels, Octane lifecycle, and Kubernetes logging. Keep request IDs, trace IDs and user IDs in the log body or structured metadata; use stable, low-cardinality stream labels in the collector.