From 25f377fa386f9225469be1ff8d207452c14b4cb1 Mon Sep 17 00:00:00 2001 From: aubes <3941035+aubes@users.noreply.github.com> Date: Thu, 23 Apr 2026 09:05:55 +0200 Subject: [PATCH 1/2] doc: Add symfony logs and advanced logs examples --- README.md | 2 + docs/advanced-example.md | 162 ++++++++++++++++++++++++++++++++++ docs/processors/auto-label.md | 2 +- docs/symfony-logs.md | 58 ++++++++++++ 4 files changed, 223 insertions(+), 1 deletion(-) create mode 100644 docs/advanced-example.md create mode 100644 docs/symfony-logs.md diff --git a/README.md b/README.md index 645d454..55dba70 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,8 @@ ecs_logging: ## Documentation - [Configuration reference](docs/configuration-reference.md) +- [Logging a Symfony app in ECS format](docs/symfony-logs.md) +- [Advanced example](docs/advanced-example.md) - Processors - [ServiceProcessor](docs/processors/service.md) - [ErrorProcessor](docs/processors/error.md) diff --git a/docs/advanced-example.md b/docs/advanced-example.md new file mode 100644 index 0000000..39655cb --- /dev/null +++ b/docs/advanced-example.md @@ -0,0 +1,162 @@ +# Advanced example + +This example shows a Symfony application logging an error during a payment process, with every processor enabled (service, error, user, host, tracing, http_request, auto_label) and OpenTelemetry auto-instrumentation for distributed tracing. + +## Prerequisites + +- **Symfony 6.4+** with Monolog +- **`symfony/security-bundle`** installed (for `UserProcessor`) +- **[`open-telemetry/opentelemetry-auto-symfony`](https://github.com/opentelemetry-php/contrib-auto-symfony)** installed with `OTEL_PHP_PSR3_MODE=inject`, which injects flat `trace_id`/`span_id` keys into Monolog context. The `TracingProcessor` in `opentelemetry` mode reads these keys and maps them to ECS fields. + +## Configuration + +### `config/packages/monolog.yaml` + +```yaml +monolog: + handlers: + app: + type: stream + path: "%kernel.logs_dir%/%kernel.environment%.log" + level: info + channels: ["app"] + formatter: 'monolog.formatter.ecs' + main: + type: stream + path: "%kernel.logs_dir%/%kernel.environment%.log" + level: warning + channels: ["!event", "!app"] + formatter: 'monolog.formatter.ecs' +``` + +### `config/packages/ecs_logging.yaml` + +```yaml +ecs_logging: + monolog: + handlers: ['app', 'main'] + + tags: ['env:prod', 'region:eu-west-1'] + + processor: + service: + enabled: true + name: 'my-app' + version: '%env(string:APP_VERSION)%' + type: 'payments' + + error: + enabled: true + map_exception_key: true # also process context['exception'] + + user: + enabled: true + + host: + enabled: true + + tracing: + enabled: true + mode: 'opentelemetry' + + http_request: + enabled: true + include_client_ip: true + + auto_label: + enabled: true + mode: 'bundle' + move_to_labels: true + include_extra: true # process Monolog extras too + non_scalar_strategy: json # encode arrays/objects instead of dropping +``` + +## Triggering a log + +```php +// src/Service/PaymentService.php +namespace App\Service; + +use Psr\Log\LoggerInterface; + +class PaymentService +{ + public function __construct(private readonly LoggerInterface $logger) {} + + public function process(string $orderId): void + { + try { + // ... payment processing logic + throw new \RuntimeException('Card declined by issuer'); + } catch (\Throwable $e) { + $this->logger->error('Payment failed', [ + 'error' => $e, + 'order_id' => 'ORD-9876', + ]); + + throw $e; + } + } +} +``` + +## Generated log + +```json +{ + "@timestamp": "2026-03-20T14:32:01.000000+00:00", + "message": "Payment failed", + "ecs.version": "9.3.0", + "log": { + "logger": "app", + "level": "error" + }, + "service": { + "name": "my-app", + "version": "1.5.2", + "type": "payments" + }, + "error": { + "type": "RuntimeException", + "message": "Card declined by issuer", + "code": 0, + "stack_trace": "RuntimeException: Card declined by issuer in /app/src/Service/PaymentService.php:17\nStack trace:\n#0 /app/src/Controller/CheckoutController.php(34): App\\Service\\PaymentService->process('ORD-9876')\n#1 ..." + }, + "trace": { + "id": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4" + }, + "transaction": { + "id": "f6e5d4c3b2a1f6e5" + }, + "span": { + "id": "9f8e7d6c5b4a3210" + }, + "user": { + "name": "john.doe" + }, + "host": { + "name": "web-01.example.com", + "ip": ["203.0.113.10"], + "architecture": "x86_64" + }, + "url": { + "path": "/api/checkout", + "scheme": "https", + "domain": "example.com" + }, + "http": { + "request": { + "method": "POST", + "mime_type": "application/json" + }, + "version": "2" + }, + "client": { + "ip": "198.51.100.42" + }, + "labels": { + "order_id": "ORD-9876" + }, + "tags": ["env:prod", "region:eu-west-1"] +} +``` diff --git a/docs/processors/auto-label.md b/docs/processors/auto-label.md index 58fa9c4..72611df 100644 --- a/docs/processors/auto-label.md +++ b/docs/processors/auto-label.md @@ -32,7 +32,7 @@ The `mode` option defines which context keys are **kept as-is** (the ECS whiteli | `full` | All ECS top-level field sets (8.x and 9.x) | | `custom` | Only the keys listed in `fields` | -Bundle-internal keys (`tracing`, `span`) are always protected regardless of mode. +Always-protected keys (`tracing`, `span`) are preserved regardless of mode. ```yaml auto_label: diff --git a/docs/symfony-logs.md b/docs/symfony-logs.md new file mode 100644 index 0000000..9b977d5 --- /dev/null +++ b/docs/symfony-logs.md @@ -0,0 +1,58 @@ +# Logging a Symfony app in ECS format + +Starting point for capturing Symfony's framework logs (routing, security, Doctrine, messenger, uncaught exceptions…) in ECS format without dropping information from the Monolog context. + +This is a working baseline, not a finished recipe: adapt handler paths, log levels, channel filters and enabled processors to your own routing / retention / privacy constraints. + +## Configuration + +### `config/packages/monolog.yaml` + +```yaml +monolog: + handlers: + ecs: + type: stream + path: "%kernel.logs_dir%/ecs.log" + level: info + formatter: 'monolog.formatter.ecs' +``` + +### `config/packages/ecs_logging.yaml` + +```yaml +ecs_logging: + monolog: + handlers: ['ecs'] + + processor: + error: + enabled: true + map_exception_key: true # capture context['exception'] from ErrorListener + + auto_label: + enabled: true + mode: 'bundle' + move_to_labels: true # keep non-ECS keys in labels instead of dropping + include_extra: true # process Monolog extras too + non_scalar_strategy: json # encode arrays/objects as JSON strings +``` + +Add `service`, `host`, `http_request`, `user`, `tracing`… as needed - see the [advanced example](advanced-example.md). + +## How context is preserved + +| Source | Destination | +|---|---| +| `context['exception']` (framework) or `context['error']` (manual) | `error.type` / `error.message` / `error.code` / `error.stack_trace` | +| Scalar context keys (`route`, `firewall_name`, `message_id`…) | `labels.{key}` | +| Array / object context values | `labels.{key}` as JSON string | +| Monolog `extra` | `labels.{key}` | +| Native ECS keys in context (`user`, `http`, `url`…) | promoted to top-level | + +## Trade-offs + +- **Message keeps its placeholders** (`Matched route "{route}"`) - the value goes to `labels.route`. You lose the ready-to-read string, you gain a queryable field. +- **Raw `\Throwable` is replaced** by `error.*` fields. Downstream processors that expected the original object won't find it. Full trace remains in `error.stack_trace`. +- **JSON-encoded labels are opaque in Kibana** - with `non_scalar_strategy: json`, arrays/objects are preserved as strings, but not as structured fields. Prefer flat scalar context keys when you need to filter on them. +- **`error.code` is emitted as integer** (inherited from `elastic/ecs-logging-php`). ECS types it as `keyword`; Elasticsearch usually coerces on ingest. From 414acc0e5af8d0e7c65d700df04859aaa40437e1 Mon Sep 17 00:00:00 2001 From: aubes <3941035+aubes@users.noreply.github.com> Date: Thu, 23 Apr 2026 22:29:23 +0200 Subject: [PATCH 2/2] docs: fix OpenTelemetry package attribution in tracing and advanced example --- docs/advanced-example.md | 2 +- docs/processors/tracing.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/advanced-example.md b/docs/advanced-example.md index 39655cb..9836a0b 100644 --- a/docs/advanced-example.md +++ b/docs/advanced-example.md @@ -6,7 +6,7 @@ This example shows a Symfony application logging an error during a payment proce - **Symfony 6.4+** with Monolog - **`symfony/security-bundle`** installed (for `UserProcessor`) -- **[`open-telemetry/opentelemetry-auto-symfony`](https://github.com/opentelemetry-php/contrib-auto-symfony)** installed with `OTEL_PHP_PSR3_MODE=inject`, which injects flat `trace_id`/`span_id` keys into Monolog context. The `TracingProcessor` in `opentelemetry` mode reads these keys and maps them to ECS fields. +- **[`open-telemetry/opentelemetry-auto-psr3`](https://github.com/opentelemetry-php/contrib-auto-psr3)** installed with `OTEL_PHP_PSR3_MODE=inject`, which injects flat `trace_id`/`span_id` keys into Monolog context. Typically paired with **[`open-telemetry/opentelemetry-auto-symfony`](https://github.com/opentelemetry-php/contrib-auto-symfony)**, which creates the spans. The `TracingProcessor` in `opentelemetry` mode reads these keys and maps them to ECS fields. ## Configuration diff --git a/docs/processors/tracing.md b/docs/processors/tracing.md index 9059114..017968d 100644 --- a/docs/processors/tracing.md +++ b/docs/processors/tracing.md @@ -64,7 +64,7 @@ ECS output: In `opentelemetry` mode, the processor reads flat `trace_id`, `span_id`, and `trace_flags` keys from the log context (injected by the OpenTelemetry Monolog handler) and maps them to ECS fields. The `field_name` option is ignored. This mode is designed to work with: -- [`open-telemetry/opentelemetry-auto-symfony`](https://github.com/opentelemetry-php/contrib-auto-symfony) with `OTEL_PHP_PSR3_MODE=inject` +- [`open-telemetry/opentelemetry-auto-psr3`](https://github.com/opentelemetry-php/contrib-auto-psr3) with `OTEL_PHP_PSR3_MODE=inject` (typically paired with [`open-telemetry/opentelemetry-auto-symfony`](https://github.com/opentelemetry-php/contrib-auto-symfony), which creates the spans) - Any OpenTelemetry setup that injects flat tracing keys into Monolog context ### Configuration