Skip to content

Commit

Permalink
Merge pull request #1168: add LazyPipeline, improve Telemetry
Browse files Browse the repository at this point in the history
  • Loading branch information
spiralbot committed Nov 29, 2024
1 parent 4e41155 commit 4746714
Show file tree
Hide file tree
Showing 5 changed files with 173 additions and 10 deletions.
3 changes: 1 addition & 2 deletions src/CurrentRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

use Psr\Http\Message\ServerRequestInterface;
use Spiral\Core\Attribute\Scope;
use Spiral\Http\Exception\HttpException;

/**
* Provides access to the current request in the `http` scope.
Expand All @@ -17,7 +16,7 @@ final class CurrentRequest
{
private ?ServerRequestInterface $request = null;

public function set(ServerRequestInterface $request): void
public function set(?ServerRequestInterface $request): void
{
$this->request = $request;
}
Expand Down
24 changes: 19 additions & 5 deletions src/Http.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,24 +25,35 @@ final class Http implements RequestHandlerInterface
{
private ?RequestHandlerInterface $handler = null;
private readonly TracerFactoryInterface $tracerFactory;
private readonly Pipeline|LazyPipeline $pipeline;

public function __construct(
private readonly HttpConfig $config,
private readonly Pipeline $pipeline,
Pipeline|LazyPipeline $pipeline,
private readonly ResponseFactoryInterface $responseFactory,
private readonly ContainerInterface $container,
?TracerFactoryInterface $tracerFactory = null,
private readonly ?EventDispatcherInterface $dispatcher = null,
) {
foreach ($this->config->getMiddleware() as $middleware) {
$this->pipeline->pushMiddleware($this->container->get($middleware));
if ($pipeline instanceof Pipeline) {
foreach ($this->config->getMiddleware() as $middleware) {
$pipeline->pushMiddleware($this->container->get($middleware));
}
} else {
$pipeline = $pipeline->withAddedMiddleware(
...$this->config->getMiddleware()
);
}

$this->pipeline = $pipeline;
$scope = $this->container instanceof ScopeInterface ? $this->container : new Container();
$this->tracerFactory = $tracerFactory ?? new NullTracerFactory($scope);
}

public function getPipeline(): Pipeline
/**
* @internal
*/
public function getPipeline(): Pipeline|LazyPipeline
{
return $this->pipeline;
}
Expand Down Expand Up @@ -97,7 +108,10 @@ public function handle(ServerRequestInterface $request): ResponseInterface
attributes: [
'http.method' => $request->getMethod(),
'http.url' => (string) $request->getUri(),
'http.headers' => $request->getHeaders(),
'http.headers' => \array_map(
static fn (array $values): string => \implode(',', $values),
$request->getHeaders(),
),
],
scoped: true,
traceKind: TraceKind::SERVER,
Expand Down
149 changes: 149 additions & 0 deletions src/LazyPipeline.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
<?php

declare(strict_types=1);

namespace Spiral\Http;

use Psr\Container\ContainerInterface;
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Spiral\Core\Attribute\Proxy;
use Spiral\Core\Container\Autowire;
use Spiral\Http\Event\MiddlewareProcessing;
use Spiral\Http\Exception\PipelineException;
use Spiral\Telemetry\SpanInterface;
use Spiral\Telemetry\TracerInterface;

/**
* Pipeline used to pass request and response thought the chain of middleware.
* This kind of pipeline creates middleware on the fly.
*/
final class LazyPipeline implements RequestHandlerInterface, MiddlewareInterface
{
/**
* Set of middleware to be applied for every request.
*
* @var list<MiddlewareInterface|Autowire|string>
*/
protected array $middleware = [];
private ?RequestHandlerInterface $handler = null;
private int $position = 0;
/**
* Trace span for the current pipeline run.
*/
private ?SpanInterface $span = null;

public function __construct(
#[Proxy] private readonly ContainerInterface $container,
private readonly ?EventDispatcherInterface $dispatcher = null,
) {
}

/**
* Add middleware to the pipeline.
*
* @param MiddlewareInterface ...$middleware List of middleware or its definition.
*/
public function withAddedMiddleware(MiddlewareInterface|Autowire|string ...$middleware): self
{
$pipeline = clone $this;
$pipeline->middleware = \array_merge($pipeline->middleware, $middleware);
return $pipeline;
}

/**
* Replace middleware in the pipeline.
*
* @param MiddlewareInterface ...$middleware List of middleware or its definition.
*/
public function withMiddleware(MiddlewareInterface|Autowire|string ...$middleware): self
{
$pipeline = clone $this;
$pipeline->middleware = $middleware;
return $pipeline;
}

/**
* Configures pipeline with target endpoint.
*
* @throws PipelineException
*/
public function withHandler(RequestHandlerInterface $handler): self
{
$pipeline = clone $this;
$pipeline->handler = $handler;
return $pipeline;
}

public function process(Request $request, RequestHandlerInterface $handler): Response
{
return $this->withHandler($handler)->handle($request);
}

public function handle(Request $request): Response
{
$this->handler === null and throw new PipelineException('Unable to run pipeline, no handler given.');

/** @var CurrentRequest $currentRequest */
$currentRequest = $this->container->get(CurrentRequest::class);

$previousRequest = $currentRequest->get();
$currentRequest->set($request);
try {
// There is no middleware to process, let's pass the request to the handler
if (!\array_key_exists($this->position, $this->middleware)) {
return $this->handler->handle($request);
}

$middleware = $this->resolveMiddleware($this->position);
$this->dispatcher?->dispatch(new MiddlewareProcessing($request, $middleware));

$span = $this->span;

$middlewareTitle = \is_string($this->middleware[$this->position])
&& $this->middleware[$this->position] !== $middleware::class
? \sprintf('%s=%s', $this->middleware[$this->position], $middleware::class)
: $middleware::class;
// Init a tracing span when the pipeline starts
if ($span === null) {
/** @var TracerInterface $tracer */
$tracer = $this->container->get(TracerInterface::class);
return $tracer->trace(
name: 'HTTP Pipeline',
callback: function (SpanInterface $span) use ($request, $middleware, $middlewareTitle): Response {
$span->setAttribute('http.middleware', [$middlewareTitle]);
return $middleware->process($request, $this->next($span));
},
scoped: true,
);
}

$middlewares = $span->getAttribute('http.middleware') ?? [];
$middlewares[] = $middlewareTitle;
$span->setAttribute('http.middleware', $middlewares);

return $middleware->process($request, $this->next($span));
} finally {
$currentRequest->set($previousRequest);
}
}

private function next(SpanInterface $span): self
{
$pipeline = clone $this;
++$pipeline->position;
$pipeline->span = $span;
return $pipeline;
}

private function resolveMiddleware(int $position): MiddlewareInterface
{
$middleware = $this->middleware[$position];
return $middleware instanceof MiddlewareInterface
? $middleware
: $this->container->get($middleware);
}
}
3 changes: 2 additions & 1 deletion src/Pipeline.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

/**
* Pipeline used to pass request and response thought the chain of middleware.
* @deprecated Will be removed in v4.0. Use {@see LazyPipeline} instead.
*/
final class Pipeline implements RequestHandlerInterface, MiddlewareInterface
{
Expand All @@ -31,7 +32,7 @@ final class Pipeline implements RequestHandlerInterface, MiddlewareInterface
private ?RequestHandlerInterface $handler = null;

public function __construct(
#[Proxy] private readonly ScopeInterface $scope,
#[Proxy] ScopeInterface $scope,
private readonly ?EventDispatcherInterface $dispatcher = null,
?TracerInterface $tracer = null
) {
Expand Down
4 changes: 2 additions & 2 deletions tests/HttpTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@ public function testTraceAttributesAreSet(): void
[
'http.method' => 'GET',
'http.url' => 'http://example.org/path',
'http.headers' => ['Host' => ['example.org'], 'foo' => ['bar']],
'http.headers' => ['Host' => 'example.org', 'foo' => 'bar'],
],
true,
TraceKind::SERVER,
Expand All @@ -293,7 +293,7 @@ function ($name, $callback, $attributes, $scoped, $traceKind) {
self::assertSame($attributes, [
'http.method' => 'GET',
'http.url' => 'http://example.org/path',
'http.headers' => ['Host' => ['example.org'], 'foo' => ['bar']],
'http.headers' => ['Host' => 'example.org', 'foo' => 'bar'],
]);
return $this->container
->get(TracerInterface::class)
Expand Down

0 comments on commit 4746714

Please sign in to comment.