Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,11 @@
"ramsey/uuid": "^4.9"
},
"require-dev": {
"ergebnis/composer-normalize": "^2.52",
"guzzlehttp/psr7": "^3.0",
"infection/infection": "^0.34",
"phpstan/phpstan": "^2.2",
"phpunit/phpunit": "^13.2",
"ergebnis/composer-normalize": "^2.54",
"guzzlehttp/psr7": "^3.2",
"infection/infection": "^0.35",
"phpstan/phpstan": "^2.3",
"phpunit/phpunit": "^13.4",
"slevomat/coding-standard": "^8.31",
"squizlabs/php_codesniffer": "^4.0"
},
Expand Down Expand Up @@ -76,7 +76,7 @@
"test-file": "@php ./vendor/bin/phpunit --configuration phpunit.xml --no-coverage --filter",
"tests": [
"@php -d memory_limit=2G ./vendor/bin/phpunit --configuration phpunit.xml tests",
"@php ./vendor/bin/infection --threads=max --logger-html=reports/coverage/mutation-report.html --coverage=reports/coverage"
"@php ./vendor/bin/infection --threads=max --logger-html=reports/coverage/mutation-report.html"
]
}
}
10 changes: 5 additions & 5 deletions src/CorrelationIdMiddleware.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,17 @@
*/
final readonly class CorrelationIdMiddleware implements MiddlewareInterface
{
/**
* Name of the request attribute carrying the resolved {@see CorrelationId}.
*/
public const string ATTRIBUTE_NAME = 'correlationId';

/**
* Name of the header carrying the correlation ID on requests and responses. Use it when
* propagating the identifier to outbound requests, so the name never drifts across services.
*/
public const string HEADER_NAME = 'Correlation-Id';

/**
* Name of the request attribute carrying the resolved {@see CorrelationId}.
*/
public const string ATTRIBUTE_NAME = 'correlationId';

private function __construct(
private CorrelationIdProvider $provider,
private RequestScopedCorrelationId $correlationId
Expand Down
60 changes: 30 additions & 30 deletions tests/Unit/CorrelatedLoggerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,49 +13,52 @@

final class CorrelatedLoggerTest extends TestCase
{
public function testAttachesCorrelationIdToLogContext(): void
public function testReadsTheCorrelationIdAtLogTime(): void
{
/** @Given an in-memory PSR-3 logger */
$logger = new CapturingLogger();

/** @And a correlated logger reading a fixed correlation ID */
$correlatedLogger = CorrelatedLogger::from(
logger: $logger,
correlationId: UuidCorrelationId::from(value: 'corr-123')
/** @And a middleware sharing its correlation ID with a correlated logger built before any request */
$middleware = CorrelationIdMiddleware::build();
$correlatedLogger = CorrelatedLogger::from(logger: $logger, correlationId: $middleware->correlationId());

/** @And a request carrying a Correlation-Id header handled after the logger was built */
$middleware->process(
new ServerRequest('GET', '/', ['Correlation-Id' => 'corr-late-bound']),
new CapturingHandler()
);

/** @When a log entry is emitted */
$correlatedLogger->info('something happened');
$correlatedLogger->info('charge routed');

/** @Then the recorded entry carries the correlation ID in its context */
/** @Then the recorded entry carries the correlation ID resolved after the logger was built */
$entries = $logger->loggedEntries();

self::assertCount(1, $entries);
self::assertSame(LogLevel::INFO, $entries[0]['level']);
self::assertSame('something happened', $entries[0]['message']);
self::assertSame('corr-123', $entries[0]['context']['correlation_id']);
self::assertSame('corr-late-bound', $entries[0]['context']['correlation_id']);
}

public function testPreservesExistingContextKeysWhenAttachingCorrelationId(): void
public function testAttachesCorrelationIdToLogContext(): void
{
/** @Given an in-memory PSR-3 logger */
$logger = new CapturingLogger();

/** @And a correlated logger reading a fixed correlation ID */
$correlatedLogger = CorrelatedLogger::from(
logger: $logger,
correlationId: UuidCorrelationId::from(value: 'corr-xyz')
correlationId: UuidCorrelationId::from(value: 'corr-123')
);

/** @When a log entry is emitted with additional context */
$correlatedLogger->info('order placed', ['order_id' => 42]);
/** @When a log entry is emitted */
$correlatedLogger->info('something happened');

/** @Then both the original context and the correlation ID are recorded */
/** @Then the recorded entry carries the correlation ID in its context */
$entries = $logger->loggedEntries();

self::assertCount(1, $entries);
self::assertSame(42, $entries[0]['context']['order_id']);
self::assertSame('corr-xyz', $entries[0]['context']['correlation_id']);
self::assertSame(LogLevel::INFO, $entries[0]['level']);
self::assertSame('something happened', $entries[0]['message']);
self::assertSame('corr-123', $entries[0]['context']['correlation_id']);
}

public function testOmitsCorrelationIdWhenTheValueIsEmpty(): void
Expand All @@ -81,28 +84,25 @@ public function testOmitsCorrelationIdWhenTheValueIsEmpty(): void
self::assertArrayNotHasKey('correlation_id', $entries[0]['context']);
}

public function testReadsTheCorrelationIdAtLogTime(): void
public function testPreservesExistingContextKeysWhenAttachingCorrelationId(): void
{
/** @Given an in-memory PSR-3 logger */
$logger = new CapturingLogger();

/** @And a middleware sharing its correlation ID with a correlated logger built before any request */
$middleware = CorrelationIdMiddleware::build();
$correlatedLogger = CorrelatedLogger::from(logger: $logger, correlationId: $middleware->correlationId());

/** @And a request carrying a Correlation-Id header handled after the logger was built */
$middleware->process(
new ServerRequest('GET', '/', ['Correlation-Id' => 'corr-late-bound']),
new CapturingHandler()
/** @And a correlated logger reading a fixed correlation ID */
$correlatedLogger = CorrelatedLogger::from(
logger: $logger,
correlationId: UuidCorrelationId::from(value: 'corr-xyz')
);

/** @When a log entry is emitted */
$correlatedLogger->info('charge routed');
/** @When a log entry is emitted with additional context */
$correlatedLogger->info('order placed', ['order_id' => 42]);

/** @Then the recorded entry carries the correlation ID resolved after the logger was built */
/** @Then both the original context and the correlation ID are recorded */
$entries = $logger->loggedEntries();

self::assertCount(1, $entries);
self::assertSame('corr-late-bound', $entries[0]['context']['correlation_id']);
self::assertSame(42, $entries[0]['context']['order_id']);
self::assertSame('corr-xyz', $entries[0]['context']['correlation_id']);
}
}
148 changes: 74 additions & 74 deletions tests/Unit/CorrelationIdMiddlewareTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,80 +56,6 @@ public function generate(): CorrelationId
self::assertSame($fixedValue, $response->getHeaderLine('Correlation-Id'));
}

public function testCorrelationIdStartsEmptyBeforeAnyRequestIsHandled(): void
{
/** @Given a middleware no request has gone through yet */
$middleware = CorrelationIdMiddleware::build();

/** @When the correlation ID of the request in flight is read */
$value = $middleware->correlationId()->toString();

/** @Then the value should be empty */
self::assertSame('', $value);
}

public function testCorrelationIdExposesTheValueResolvedFromTheRequestHeader(): void
{
/** @Given an existing correlation ID carried in the request header */
$existingCorrelationId = 'req-in-flight-42';

/** @And a middleware using the default provider */
$middleware = CorrelationIdMiddleware::build();

/** @When the middleware processes a request with that Correlation-Id header */
$middleware->process(
new ServerRequest('GET', '/', ['Correlation-Id' => $existingCorrelationId]),
new CapturingHandler()
);

/** @Then the correlation ID of the request in flight should expose the header value */
self::assertSame($existingCorrelationId, $middleware->correlationId()->toString());
}

public function testCorrelationIdIsAssignedBeforeTheHandlerRuns(): void
{
/** @Given a middleware using the default provider */
$middleware = CorrelationIdMiddleware::build();

/** @And a handler that records the correlation ID of the request in flight at handling time */
$handler = new readonly class ($middleware->correlationId()) implements RequestHandlerInterface {
public function __construct(private CorrelationId $correlationId)
{
}

public function handle(ServerRequestInterface $request): ResponseInterface
{
return new Response(status: 200, headers: ['Seen-By-Handler' => $this->correlationId->toString()]);
}
};

/** @When the middleware processes a request without the Correlation-Id header */
$response = $middleware->process(new ServerRequest('GET', '/'), $handler);

/** @Then the handler should have observed the correlation ID already assigned */
self::assertNotSame('', $response->getHeaderLine('Seen-By-Handler'));
self::assertSame($response->getHeaderLine('Correlation-Id'), $response->getHeaderLine('Seen-By-Handler'));
}

public function testCorrelationIdReflectsTheLatestHandledRequest(): void
{
/** @Given a middleware using the default provider */
$middleware = CorrelationIdMiddleware::build();

/** @When the middleware processes two requests carrying different Correlation-Id headers */
$middleware->process(
new ServerRequest('GET', '/first', ['Correlation-Id' => 'req-first']),
new CapturingHandler()
);
$middleware->process(
new ServerRequest('GET', '/second', ['Correlation-Id' => 'req-second']),
new CapturingHandler()
);

/** @Then the correlation ID of the request in flight should expose the latest value */
self::assertSame('req-second', $middleware->correlationId()->toString());
}

public function testPreservesExistingResponseHeaders(): void
{
/** @Given a request without the Correlation-Id header */
Expand Down Expand Up @@ -287,6 +213,31 @@ public function testGeneratesNewCorrelationIdWhenHeaderIsEmpty(): void
self::assertSame($correlationId->toString(), $response->getHeaderLine('Correlation-Id'));
}

public function testCorrelationIdIsAssignedBeforeTheHandlerRuns(): void
{
/** @Given a middleware using the default provider */
$middleware = CorrelationIdMiddleware::build();

/** @And a handler that records the correlation ID of the request in flight at handling time */
$handler = new readonly class ($middleware->correlationId()) implements RequestHandlerInterface {
public function __construct(private CorrelationId $correlationId)
{
}

public function handle(ServerRequestInterface $request): ResponseInterface
{
return new Response(status: 200, headers: ['Seen-By-Handler' => $this->correlationId->toString()]);
}
};

/** @When the middleware processes a request without the Correlation-Id header */
$response = $middleware->process(new ServerRequest('GET', '/'), $handler);

/** @Then the handler should have observed the correlation ID already assigned */
self::assertNotSame('', $response->getHeaderLine('Seen-By-Handler'));
self::assertSame($response->getHeaderLine('Correlation-Id'), $response->getHeaderLine('Seen-By-Handler'));
}

public function testCorrelationIdAttributeIsAccessibleDownstream(): void
{
/** @Given a request without the Correlation-Id header */
Expand All @@ -308,6 +259,37 @@ public function testCorrelationIdAttributeIsAccessibleDownstream(): void
self::assertSame($response->getHeaderLine('Correlation-Id'), $correlationId->toString());
}

public function testCorrelationIdReflectsTheLatestHandledRequest(): void
{
/** @Given a middleware using the default provider */
$middleware = CorrelationIdMiddleware::build();

/** @When the middleware processes two requests carrying different Correlation-Id headers */
$middleware->process(
new ServerRequest('GET', '/first', ['Correlation-Id' => 'req-first']),
new CapturingHandler()
);
$middleware->process(
new ServerRequest('GET', '/second', ['Correlation-Id' => 'req-second']),
new CapturingHandler()
);

/** @Then the correlation ID of the request in flight should expose the latest value */
self::assertSame('req-second', $middleware->correlationId()->toString());
}

public function testCorrelationIdStartsEmptyBeforeAnyRequestIsHandled(): void
{
/** @Given a middleware no request has gone through yet */
$middleware = CorrelationIdMiddleware::build();

/** @When the correlation ID of the request in flight is read */
$value = $middleware->correlationId()->toString();

/** @Then the value should be empty */
self::assertSame('', $value);
}

public function testGeneratesNewCorrelationIdWhenHeaderIsWhitespaceOnly(): void
{
/** @Given the whitespace-only value that will be returned for the Correlation-Id header */
Expand Down Expand Up @@ -354,4 +336,22 @@ public function testGeneratesNewCorrelationIdWhenHeaderIsWhitespaceOnly(): void
self::assertTrue($response->hasHeader('Correlation-Id'));
self::assertSame($correlationId->toString(), $response->getHeaderLine('Correlation-Id'));
}

public function testCorrelationIdExposesTheValueResolvedFromTheRequestHeader(): void
{
/** @Given an existing correlation ID carried in the request header */
$existingCorrelationId = 'req-in-flight-42';

/** @And a middleware using the default provider */
$middleware = CorrelationIdMiddleware::build();

/** @When the middleware processes a request with that Correlation-Id header */
$middleware->process(
new ServerRequest('GET', '/', ['Correlation-Id' => $existingCorrelationId]),
new CapturingHandler()
);

/** @Then the correlation ID of the request in flight should expose the header value */
self::assertSame($existingCorrelationId, $middleware->correlationId()->toString());
}
}