Download the PHP package herdwatch-oss/monolog-ecs-formatter without Composer
On this page you can find all versions of the php package herdwatch-oss/monolog-ecs-formatter. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.
Download herdwatch-oss/monolog-ecs-formatter
More information about herdwatch-oss/monolog-ecs-formatter
Files in herdwatch-oss/monolog-ecs-formatter
Package monolog-ecs-formatter
Short Description Monolog formatter and Symfony bundle that promotes typed ECS value objects (metrics, labels, text, tags, service, user, tracing, error) to top-level ECS-aligned JSON fields for clean Elasticsearch mapping, with a default copy mode that also keeps the legacy Monolog top-level keys (channel, level_name, level, datetime) so existing dashboards keep working during migration.
License MIT
Informations about the package monolog-ecs-formatter
herdwatch-oss/monolog-ecs-formatter
A Monolog formatter and Symfony bundle that promotes typed ECS value objects from a log's context to top-level ECS-aligned JSON fields, for clean Elasticsearch mapping.
You log structured data as small typed objects — Metrics, Labels, Text, Tags, Service, User, Tracing, EcsError, or any of your own — and the formatter lifts them to the correct ECS location with key validation, per-namespace caps, and a never-drop guarantee. The JSON type of each value is fixed by the object's method signatures, so there is no guesswork or formatter-side coercion.
Installation
Register the bundle in config/bundles.php:
Configuration
Create config/packages/monolog_ecs_formatter.yaml:
Passing fields
Fields are detected by instanceof, not by array key — so the key is irrelevant. Pass them positionally (cleanest) or under any key; both work, and ordinary context entries flow through untouched:
Multiple bags of the same kind merge. EcsField objects are detected in both context and extra (context wins on a conflict) — this is how the identity processor's injected Service object gets promoted. Anything that is not an EcsField is left alone: non-field context goes under a leftover context object, and extra is emitted verbatim.
Recommended usage
Pass field objects positionally, without a key. The key is ignored for routing anyway, so a key is redundant and misleading; positional also reads cleanly and lets multiple fields merge:
Reserve string keys for the two cases where the key genuinely matters:
- the
exceptionconvention —['exception' => $e]with a raw\Throwable, which the formatter auto-promotes toerror.*; - ordinary scalar context you want to keep or interpolate —
['request_id' => $id].
Governed namespaces (bags)
| Bag | ECS field | Value typing | Cap |
|---|---|---|---|
Metrics |
metric.* |
count()/total() → int, gauge() → float, flag() → bool |
8 keys |
Labels |
labels.* |
scalars coerced to string | 8 keys |
Text |
text.* |
strings | 2 keys |
Tags |
tags |
de-duplicated keyword strings | 8 |
Keys must match /^[a-z][a-z0-9]*(_[a-z][a-z0-9]*){0,2}$/ (lower snake_case, ≤ 3 segments).
Nothing is ever dropped. A key that fails validation or exceeds the cap is demoted into the leftover context with a dotted key — e.g. Labels::create()->add('Bad Key', 'x') ends up as "context": {"labels.Bad Key": "x"}. This is identical in both modes; the mode only governs the legacy base keys (see Modes).
Building a bag from an array
When you already hold an array (config-driven logging, generic middleware), use the fromArray() factories — values still go through the bag's typing/validation:
Identity fields
| Object | ECS fields |
|---|---|
Service |
service.name, service.language (default php), and optional version, environment, node.name |
User |
user.id, name, email, domain, full_name, hash (null fields omitted) |
Tracing |
trace.id, transaction.id — for logs ↔ APM correlation |
EcsError |
error.type (class), message, stack_trace (includes throw-site file:line + previous-exception chain); error.code only when non-zero |
Standard ECS context fields
Bundled value objects for common runtime / request / host fields, so each service doesn't hand-roll them. All take named constructor arguments and omit null fields.
| Object | ECS fields |
|---|---|
Http |
http.request.{method,body.bytes,mime_type}, http.response.{status_code,body.bytes,mime_type} — request/response nest, so separate Http objects deep-merge |
Process |
process.pid, process.command_line, process.name |
Client |
client.ip, client.port |
UserAgent |
user_agent.original, user_agent.version, user_agent.device.name |
Host |
host.name, host.ip |
Event |
event.action, event.start/event.end, event.duration (nanoseconds), event.sequence (monotonic ordering number), event.outcome (the EventOutcome enum: success/failure/unknown), event.reason (short, low-cardinality reason for the outcome, e.g. timeout), event.type/event.category (the EventType/EventCategory closed-set enums, emitted as de-duplicated arrays), event.url (link to an external system to continue investigation) — merged additively onto the base event object; it cannot override event.kind/dataset/etc. |
Url |
url.full, url.scheme, url.domain, url.port, url.path, url.query, url.fragment — pass parts by name, or use Url::parse($url) to split a URL string |
Network |
network.direction (the NetworkDirection closed-set enum: inbound, outbound, ingress, egress, internal, external, unknown) — e.g. outbound on HTTP-client telemetry, inbound on request logging |
Urlrecords the URL faithfully —url.full/url.querykeep whatever you pass, including any embedded credentials or query tokens/PII. Redaction is the application's job: sanitise the URL before logging it, or strip sensitive fields in a Monolog processor.The
EventOutcome/EventType/EventCategoryenums encode the ECS 8.11 allowed-value sets — the schema this formatter targets. Newer ECS releases extend these sets additively (e.g. theapiandecs_versionin config only changes the advertisedecs.versionstring — new enum cases arrive with library updates.
Project-specific fields
Any class implementing EcsField is detected automatically — no registration, no formatter change. EcsField extends JsonSerializable; use SerializesToEcs to satisfy it (it maps jsonSerialize() to toEcs()):
Rules that keep this safe:
- A fragment targeting a governed namespace (
metric/labels/text/tags) is validated and capped like any bag, whatever produced it. - Unknown namespaces and new top-level fields pass through.
- A fragment can never overwrite the base skeleton (
@timestamp,log.level,message,ecs.version); contributions tolog/event(e.g.log.origin) are merged additively, with the base winning conflicts.
Serialisation under other handlers. Because
EcsFieldisJsonSerializable, the same object also serialises to its ECS data under a non-ECS formatter/handler (a JSON handler emits the bare fragment; a line-based one wraps it under the class name) instead of an empty{}. That path is rawtoEcs()— the validation, caps and never-drop demotion above are applied only byEcsFieldsFormatter, which pulls the objects out before normalising. If a non-ECS handler needs the governed, promoted form, point it atEcsFieldsFormattertoo.
To apply a custom field to every record, inject it from a Monolog processor (the pattern the bundled identity processor uses).
ECS base fields emitted on every record
| Field | Value |
|---|---|
@timestamp |
record datetime, ISO-8601 with microseconds |
log.level |
lowercased level name (dotted top-level key, per the ecs-logging spec) |
message |
log message |
ecs.version |
configurable; defaults to 8.11.0 (the ECS schema this formatter's fields conform to — bump it if you emit fields from a newer ECS version) |
log.logger |
channel name |
event.kind / module / dataset |
event / symfony / symfony.logs |
event.severity |
Monolog level integer |
event.created is deliberately not emitted: per ECS it is the agent's/pipeline's read time (the record's own time is @timestamp; datastore arrival is event.ingested, typically set by an ingest pipeline).
Exceptions
A \Throwable at context['exception'] (the Monolog convention) is promoted to error.* by the formatter automatically — no processor or configuration required. So existing $log->error($msg, ['exception' => $e]) call sites get error.{type,message,code,stack_trace} for free. The consumed exception is then removed from the leftover context (in both modes — it now lives, typed, under error.*). An explicit new EcsError($e) always takes precedence.
Service identity processor (service.*)
When service_name is configured, EcsIdentityProcessor is registered as a global Monolog processor and injects a Service object into every record, which the formatter promotes to service.*. Omitting service_name disables it. The processor does not handle exceptions — that is the formatter's job (see above).
The optional service_version and service_environment keys ride along on that injected Service, so every record carries service.version and service.environment (bind them to %env(APP_VERSION)% / %env(APP_ENV)% to track deploys per environment). Both require service_name — setting either without it is a configuration error. A Service set explicitly at the call site still wins (the processor only fills the gap).
Modes
The mode controls one thing only: whether the legacy Monolog top-level keys (channel, level_name, level, datetime) are emitted alongside the ECS fields. Promotion of ECS fields and the never-drop handling of un-promotable entries are identical in both modes.
copy (default)
ECS fields are promoted to top-level and the legacy top-level keys (channel, level_name, level, datetime) are kept. This is the default because the bundle is normally installed into an existing application: dashboards still querying the old keys keep working while you migrate them to the ECS fields (log.logger, log.level, event.severity, @timestamp).
move
The clean ECS-only shape: ECS fields are promoted to top-level, everything else stays under context/extra, and no legacy top-level keys are emitted. Switch to move once nothing depends on the legacy keys — the duplicated base keys in copy cost storage and can muddy your Elasticsearch mapping.
Wiring the formatter in monolog.yaml
For service-type handlers wired in services.yaml:
Test command
In dev/test environments the bundle registers a console command that emits sample records covering every field type, the governance rules, and a custom EcsField:
License
Released under the MIT License. Copyright © Herdwatch.
All versions of monolog-ecs-formatter with dependencies
monolog/monolog Version ^3.0
psr/log Version ^3.0
symfony/config Version ^7.0
symfony/console Version ^7.0
symfony/dependency-injection Version ^7.0
symfony/http-kernel Version ^7.0