PHPackages                             amashukov/tracing-bundle - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Logging &amp; Monitoring](/categories/logging)
4. /
5. amashukov/tracing-bundle

ActiveSymfony-bundle[Logging &amp; Monitoring](/categories/logging)

amashukov/tracing-bundle
========================

Symfony 7 bundle — UUIDv7 X-Request-Id propagation FE -&gt; BE -&gt; Monolog logs with a Messenger sync -&gt; queue -&gt; worker bridge.

v0.2.0(1mo ago)0410↑49.4%MITPHPPHP &gt;=8.3CI failing

Since May 29Pushed 1mo agoCompare

[ Source](https://github.com/AndreyMashukov/tracing-bundle)[ Packagist](https://packagist.org/packages/amashukov/tracing-bundle)[ Docs](https://github.com/AndreyMashukov/tracing-bundle)[ RSS](/packages/amashukov-tracing-bundle/feed)WikiDiscussions main Synced 1w ago

READMEChangelogDependencies (38)Versions (3)Used By (0)

amashukov/tracing-bundle
========================

[](#amashukovtracing-bundle)

A Symfony 7 bundle that stamps every inbound HTTP request with a UUIDv7 `X-Request-Id`, mirrors it onto every Monolog log record's `extra.request_id`, echoes it back on the response, and rides through Symfony Messenger so a worker logging a handler ends up with the same id the originating HTTP request held.

[![CI](https://camo.githubusercontent.com/469dc5fd56644a694ea6775e58085ee989496328ee93eabdfda2cd089a211488/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f416e647265794d617368756b6f762f74726163696e672d62756e646c652f63692e796d6c3f6272616e63683d6d61696e266c6162656c3d4349)](https://github.com/AndreyMashukov/tracing-bundle/actions)[![PHPStan L8](https://camo.githubusercontent.com/b1896ec161a7a5947bc0ba286c2054b043558c1efdbd494159851a30c6c9df41/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f416e647265794d617368756b6f762f74726163696e672d62756e646c652f7374616e2e796d6c3f6272616e63683d6d61696e266c6162656c3d5048505374616e2532304c38)](https://github.com/AndreyMashukov/tracing-bundle/actions)[![Latest Version](https://camo.githubusercontent.com/53701d7fd83eddc3b348172e25840d4eb395db07ed4630c99d84df22c76a9221/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f616d617368756b6f762f74726163696e672d62756e646c65)](https://packagist.org/packages/amashukov/tracing-bundle)[![Downloads](https://camo.githubusercontent.com/f9b2404911235b71f451afb5c534850e45f202430c3b3c88b5223406748f93c9/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f616d617368756b6f762f74726163696e672d62756e646c65)](https://packagist.org/packages/amashukov/tracing-bundle)[![PHP](https://camo.githubusercontent.com/d8d7514d18a51230ecfd0ff7b2834a9f2115735b4ba2492cc253df76622cdffd/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f646570656e64656e63792d762f616d617368756b6f762f74726163696e672d62756e646c652f706870)](https://packagist.org/packages/amashukov/tracing-bundle)[![License](https://camo.githubusercontent.com/3da1fe2853d8e6b72375ba1cf66d4026081b25139a22bf46a961394b561ed80b/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f6c2f616d617368756b6f762f74726163696e672d62756e646c65)](LICENSE)[![Stars](https://camo.githubusercontent.com/dc70b03a0e201257339c66281c894b31d4ca79f9bd35fb77a2f6acf54cc0e15c/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f73746172732f416e647265794d617368756b6f762f74726163696e672d62756e646c653f7374796c653d736f6369616c)](https://github.com/AndreyMashukov/tracing-bundle)

`amashukov/tracing-bundle` is a **vendor-extractable Symfony 7 bundle for end-to-end `request_id` propagation**. Doing this once at the right layer beats bolting it on per-call: the bundle ships the kernel listener, the resolver, the Monolog processor, the Messenger stamp, the consume-side context restorer, validation at the trust boundary, and the CLI fallback so every console run / cron / worker logs `{"request_id":"..."}` instead of an empty extra block. Drop it into a Symfony 7 project, get the same id flowing through the FE fetch, the controller, every log line on every channel, the response header, and the queue-bound worker that picks the message up minutes later — with zero `App\*` namespace coupling.

```
FE fetch ─┐                                       ┌─► response with X-Request-Id
          │                                       │
          ▼                                       │
   ┌──────────────────────────────────────────────┴───┐
   │  Symfony kernel                                  │
   │  ├─ RequestIdListener   (kernel.request  +256)   │
   │  ├─ RequestIdResolver   (DI service)             │
   │  ├─ RequestIdProcessor  (monolog.processor)      │
   │  └─ ...handlers...                               │
   └──────────────┬───────────────────────────────────┘
                  │ $bus->dispatch($message)
                  ▼
            envelope + RequestIdStamp ─► queue ─► worker
                                                  │
                                                  ▼
                                  RequestIdMessengerMiddleware
                                  └─► WorkerRequestIdContext
                                       └─► RequestIdResolver
                                            └─► same UUIDv7 in handler logs

```

Features
--------

[](#features)

- **Per-request UUIDv7** — generated via `symfony/uid` when the incoming `X-Request-Id` is missing or malformed; valid inbound ids are lowercased and forwarded unchanged.
- **Listener at the kernel boundary** — `kernel.request` (priority `256`) writes the id onto the `Request` attribute; `kernel.response` (priority `-256`) mirrors it onto the response header. Main-request only; sub-requests inherit the parent id naturally.
- **Validation at the trust boundary** — incoming header rejected when length ≠ 36 or it contains anything outside `[a-f0-9-]`. Prevents log-injection (SQL fragments, control characters, oversized payloads) from contaminating log files and aggregator search.
- **Monolog processor on every channel** — `extra.request_id` attached to every `LogRecord` (`app`, `doctrine`, `security`, `messenger`, ...) with pre-existing extras preserved.
- **Messenger sync → queue → worker bridge** — dispatch-side middleware attaches a `RequestIdStamp` to outbound envelopes; consume-side restores the id into `WorkerRequestIdContext` before the handler runs, clears in `finally` so message N+1 starts clean.
- **CLI fallback** — every console / cron / non-Messenger worker run logs `{"request_id":"cli"}` instead of an empty extra block — log-aggregator queries stay consistent regardless of execution mode.
- **`final readonly` services** — narrow contracts, immutable wiring, autowired by default.
- **Zero `App\*` coupling** — bundle depends only on `monolog/monolog` + `symfony/*`. Drop into any project without renaming.

Installation
------------

[](#installation)

```
composer require amashukov/tracing-bundle
```

Symfony Flex registers the bundle automatically. If you don't run Flex, add it manually:

```
// config/bundles.php
use Amashukov\TracingBundle\TracingBundle;

return [
    TracingBundle::class => ['all' => true],
];
```

Requirements
------------

[](#requirements)

- PHP **8.3+** (UUIDv7 needs `symfony/uid` ≥ 7.0)
- `monolog/monolog` ^3.0
- `symfony/*` ^7.0 (`config`, `dependency-injection`, `event-dispatcher`, `http-foundation`, `http-kernel`, `uid`, `yaml`)
- `symfony/messenger` ^7.0 — soft `suggest`. The middleware class only loads when Messenger is installed; non-Messenger projects pay zero overhead.

Usage
-----

[](#usage)

After install — no further config. Every request automatically:

1. Receives a `request_id` attribute on the `Request`.
2. Logs `extra.request_id` in every Monolog record.
3. Mirrors `X-Request-Id` on the response.

### Reading the id inside a service

[](#reading-the-id-inside-a-service)

```
use Amashukov\TracingBundle\Http\RequestIdResolverInterface;

final readonly class MyService
{
    public function __construct(private RequestIdResolverInterface $requestId) {}

    public function someMethod(): void
    {
        $current = $this->requestId->current();   // UUIDv7 string or 'cli'
    }
}
```

### Browser side (Nuxt 3 / 4 plugin)

[](#browser-side-nuxt-3--4-plugin)

```
const requestId = crypto.randomUUID();

const api = $fetch.create({
  onRequest: ({ options }) => {
    options.headers = { ...options.headers, 'X-Request-Id': requestId };
  },
});
```

### Playwright per-test echo

[](#playwright-per-test-echo)

```
export const test = base.extend({
  testRequestId: [async ({ browser: _ }, use, testInfo) => {
    const requestId = crypto.randomUUID();
    process.stdout.write(`[TEST-REQ-ID] ${testInfo.title} -> ${requestId}\n`);
    await use(requestId);
  }, { auto: true }],

  page: async ({ context, page, testRequestId }, use) => {
    await context.setExtraHTTPHeaders({ 'X-Request-Id': testRequestId });
    await use(page);
  },
});
```

Then debug any failing run:

```
grep "" backend/var/log/app.log
```

Every BE event scoped to that one test, no cross-spec noise.

### CORS allow header

[](#cors-allow-header)

If the FE talks to a different origin, allow the header on both directions:

```
# RoadRunner .rr.yaml — or your CORS layer of choice
http:
  middleware:
    - headers
  headers:
    cors:
      allowed_headers: "...,X-Request-Id"
      exposed_headers: "...,X-Request-Id"
```

Messenger integration (sync → queue → worker)
---------------------------------------------

[](#messenger-integration-sync--queue--worker)

When a Messenger message crosses the sync → queue boundary, the worker process has no `RequestStack`. The bundle's middleware closes that gap.

Dispatch side (HTTP request handler):

```
$bus->dispatch(new MyMessage(...));
// Envelope leaves the dispatcher with [RequestIdStamp('01923e1c-...')] attached.
```

Consume side (worker process):

```
- Worker pulls the message; ReceivedStamp lands on the envelope.
- Middleware sees ReceivedStamp + RequestIdStamp -> WorkerContext::setRequestId(...).
- Handler runs; any Monolog log inside it gets extra.request_id == '01923e1c-...'.
- Middleware's finally clause clears the context so message N+1 starts fresh.

```

Per W3C Trace Context spec *Non-HTTP Protocol Support* and the Symfony Messenger official middleware pattern (`$envelope->last(ReceivedStamp::class)` discriminates dispatch vs consume).

Class catalogue
---------------

[](#class-catalogue)

ClassWhat it does`Http\RequestIdListener``kernel.request` (priority 256) reads `X-Request-Id`, validates as 36-char hex UUID, generates **UUIDv7** via `symfony/uid` when missing or malformed. `kernel.response` (priority -256) mirrors the id onto the response header. Main-request only.`Http\RequestIdResolverInterface`Narrow contract `current(): string`. Services depend on the interface; the bundle wires the alias.`Http\RequestIdResolver``final readonly` implementation. Reads `request_id` off the main request first, falls back to `WorkerRequestIdContext` (when running inside Messenger), then to the `cli` constant.`Monolog\RequestIdProcessor`Tagged `monolog.processor`. Attaches `extra.request_id` to every `LogRecord` on every channel. Pre-existing `extra` keys preserved.`Messenger\RequestIdStamp`Immutable `StampInterface` value object carrying one string (the originating request's id).`Messenger\RequestIdMessengerMiddleware`Dual-path Messenger middleware. **Dispatch**: attaches `new RequestIdStamp($resolver->current())` to the envelope if not already present. **Consume** (`ReceivedStamp` present): reads the stamp, writes the id to `WorkerRequestIdContext` before calling the next middleware, clears in `finally`.`Messenger\WorkerRequestIdContext`Single-cell mutable state holder for the worker's current message id. Read by `RequestIdResolver::current()` when there is no HTTP request.Validation
----------

[](#validation)

Incoming `X-Request-Id` is rejected when:

- length ≠ 36 chars
- contains anything outside `[a-f0-9-]`

Rejected → bundle generates a fresh UUIDv7.

CLI fallback
------------

[](#cli-fallback)

In CLI context (no `Request` on `RequestStack`, no `WorkerRequestIdContext` value set), the resolver returns the literal `cli`. Every console command / one-shot cron task logs `{"request_id":"cli"}` instead of an empty extra block — log-aggregator queries stay consistent regardless of execution mode.

Trace Context (W3C `traceparent`)
---------------------------------

[](#trace-context-w3c-traceparent)

This bundle deliberately implements the `X-Request-Id` header pattern only (Heroku / Cloudflare CF-Ray style). For W3C Trace Context (`traceparent` / OpenTelemetry alignment) pair this bundle with the official `open-telemetry/opentelemetry-php-instrumentation-symfony` — the two are complementary, not alternatives.

Testing
-------

[](#testing)

```
composer install
composer test
composer cs
composer stan
composer rector
```

Suite covers: valid UUIDv7 accept, mixed-case header lowercased, five invalid-header regen cases (missing, too short, too long, wrong charset, SQL-injection-looking string), response header mirror, sub-request skip, subscribed-events shape, CLI fallback (no request / no attribute / non-string attribute), `extra.request_id` attach with pre-existing extras preserved, Messenger stamp value semantics, worker-context set/get/clear, middleware dispatch path, middleware consume path, middleware `finally` clears between messages.

License
-------

[](#license)

MIT — see [LICENSE](LICENSE).

Author
------

[](#author)

[Andrei Mashukov](https://github.com/AndreyMashukov) — `a.mashukoff@gmail.com`

###  Health Score

41

—

FairBetter than 87% of packages

Maintenance90

Actively maintained with recent releases

Popularity18

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity40

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~7 days

Total

2

Last Release

50d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/4b3e86ca79a82515b23e0239b1d3616c747e59b64c40e169d538d5c759c9bbc4?d=identicon)[Andrey Mashukov](/maintainers/Andrey%20Mashukov)

---

Top Contributors

[![AndreyMashukov](https://avatars.githubusercontent.com/u/24682497?v=4)](https://github.com/AndreyMashukov "AndreyMashukov (4 commits)")

---

Tags

symfonytracingmonologSymfony Bundleobservabilityrequest iduuidv7

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan, Rector

Code StylePHP CS Fixer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/amashukov-tracing-bundle/health.svg)

```
[![Health](https://phpackages.com/badges/amashukov-tracing-bundle/health.svg)](https://phpackages.com/packages/amashukov-tracing-bundle)
```

###  Alternatives

[easycorp/easyadmin-bundle

Admin generator for Symfony applications

4.3k17.9M400](/packages/easycorp-easyadmin-bundle)[flow-php/flow

PHP ETL - Extract Transform Load - Data processing framework

85036.3k](/packages/flow-php-flow)[open-dxp/opendxp

Content &amp; Product Management Framework (CMS/PIM)

9421.6k64](/packages/open-dxp-opendxp)[pimcore/pimcore

Content &amp; Product Management Framework (CMS/PIM/E-Commerce)

3.8k3.8M511](/packages/pimcore-pimcore)[symfony/security-bundle

Provides a tight integration of the Security component into the Symfony full-stack framework

2.5k185.6M2.4k](/packages/symfony-security-bundle)[shopware/core

Shopware platform is the core for all Shopware ecommerce products.

585.6M600](/packages/shopware-core)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
