diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index da675be7..862ceaf8 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -424,3 +424,8 @@ parameters: message: "#^Parameter \\#2 \\$responses of static method Sentry\\\\SentryBundle\\\\Tracing\\\\HttpClient\\\\AbstractTraceableResponse\\:\\:stream\\(\\) expects iterable\\, array\\ given\\.$#" count: 1 path: tests/Tracing/HttpClient/TraceableResponseTest.php + + - + message: "#^Call to method PHPUnit\\\\Framework\\\\Assert\\:\\:assertCount\\(\\) with 1 and array\\{\\} will always evaluate to false\\.$#" + count: 2 + path: tests/End2End/TracingCacheEnd2EndTest.php diff --git a/src/Tracing/Cache/TraceableCacheAdapterForV2.php b/src/Tracing/Cache/TraceableCacheAdapterForV2.php index e8ed9330..c4fa4d5e 100644 --- a/src/Tracing/Cache/TraceableCacheAdapterForV2.php +++ b/src/Tracing/Cache/TraceableCacheAdapterForV2.php @@ -42,12 +42,6 @@ public function __construct(HubInterface $hub, AdapterInterface $decoratedAdapte */ public function get(string $key, callable $callback, ?float $beta = null, ?array &$metadata = null) { - return $this->traceFunction('cache.get_item', function () use ($key, $callback, $beta, &$metadata) { - if (!$this->decoratedAdapter instanceof CacheInterface) { - throw new \BadMethodCallException(\sprintf('The %s::get() method is not supported because the decorated adapter does not implement the "%s" interface.', self::class, CacheInterface::class)); - } - - return $this->decoratedAdapter->get($key, $callback, $beta, $metadata); - }, $key); + return $this->traceGet($key, $callback, $beta, $metadata); } } diff --git a/src/Tracing/Cache/TraceableCacheAdapterForV3.php b/src/Tracing/Cache/TraceableCacheAdapterForV3.php index ef276620..096fa2d3 100644 --- a/src/Tracing/Cache/TraceableCacheAdapterForV3.php +++ b/src/Tracing/Cache/TraceableCacheAdapterForV3.php @@ -40,12 +40,6 @@ public function __construct(HubInterface $hub, AdapterInterface $decoratedAdapte */ public function get(string $key, callable $callback, ?float $beta = null, ?array &$metadata = null): mixed { - return $this->traceFunction('cache.get_item', function () use ($key, $callback, $beta, &$metadata) { - if (!$this->decoratedAdapter instanceof CacheInterface) { - throw new \BadMethodCallException(\sprintf('The %s::get() method is not supported because the decorated adapter does not implement the "%s" interface.', self::class, CacheInterface::class)); - } - - return $this->decoratedAdapter->get($key, $callback, $beta, $metadata); - }, $key); + return $this->traceGet($key, $callback, $beta, $metadata); } } diff --git a/src/Tracing/Cache/TraceableCacheAdapterForV3WithNamespace.php b/src/Tracing/Cache/TraceableCacheAdapterForV3WithNamespace.php index 7b53c286..843d8d2a 100644 --- a/src/Tracing/Cache/TraceableCacheAdapterForV3WithNamespace.php +++ b/src/Tracing/Cache/TraceableCacheAdapterForV3WithNamespace.php @@ -38,16 +38,12 @@ public function __construct(HubInterface $hub, AdapterInterface $decoratedAdapte * {@inheritdoc} * * @param mixed[] $metadata + * + * @return mixed */ public function get(string $key, callable $callback, ?float $beta = null, ?array &$metadata = null): mixed { - return $this->traceFunction('cache.get_item', function () use ($key, $callback, $beta, &$metadata) { - if (!$this->decoratedAdapter instanceof CacheInterface) { - throw new \BadMethodCallException(\sprintf('The %s::get() method is not supported because the decorated adapter does not implement the "%s" interface.', self::class, CacheInterface::class)); - } - - return $this->decoratedAdapter->get($key, $callback, $beta, $metadata); - }, $key); + return $this->traceGet($key, $callback, $beta, $metadata); } public function withSubNamespace(string $namespace): static diff --git a/src/Tracing/Cache/TraceableCacheAdapterTrait.php b/src/Tracing/Cache/TraceableCacheAdapterTrait.php index b2af9a59..6e7274f0 100644 --- a/src/Tracing/Cache/TraceableCacheAdapterTrait.php +++ b/src/Tracing/Cache/TraceableCacheAdapterTrait.php @@ -5,6 +5,7 @@ namespace Sentry\SentryBundle\Tracing\Cache; use Psr\Cache\CacheItemInterface; +use Psr\Cache\InvalidArgumentException; use Sentry\State\HubInterface; use Sentry\Tracing\SpanContext; use Symfony\Component\Cache\Adapter\AdapterInterface; @@ -38,7 +39,7 @@ trait TraceableCacheAdapterTrait */ public function getItem($key): CacheItem { - return $this->traceFunction('cache.get_item', function () use ($key): CacheItem { + return $this->traceFunction('cache.get', function () use ($key): CacheItem { return $this->decoratedAdapter->getItem($key); }, $key); } @@ -48,7 +49,7 @@ public function getItem($key): CacheItem */ public function getItems(array $keys = []): iterable { - return $this->traceFunction('cache.get_items', function () use ($keys): iterable { + return $this->traceFunction('cache.get', function () use ($keys): iterable { return $this->decoratedAdapter->getItems($keys); }); } @@ -58,7 +59,7 @@ public function getItems(array $keys = []): iterable */ public function clear(string $prefix = ''): bool { - return $this->traceFunction('cache.clear', function () use ($prefix): bool { + return $this->traceFunction('cache.flush', function () use ($prefix): bool { return $this->decoratedAdapter->clear($prefix); }, $prefix); } @@ -68,7 +69,7 @@ public function clear(string $prefix = ''): bool */ public function delete(string $key): bool { - return $this->traceFunction('cache.delete_item', function () use ($key): bool { + return $this->traceFunction('cache.remove', function () use ($key): bool { if (!$this->decoratedAdapter instanceof CacheInterface) { throw new \BadMethodCallException(\sprintf('The %s::delete() method is not supported because the decorated adapter does not implement the "%s" interface.', self::class, CacheInterface::class)); } @@ -92,7 +93,7 @@ public function hasItem($key): bool */ public function deleteItem($key): bool { - return $this->traceFunction('cache.delete_item', function () use ($key): bool { + return $this->traceFunction('cache.remove', function () use ($key): bool { return $this->decoratedAdapter->deleteItem($key); }, $key); } @@ -102,7 +103,7 @@ public function deleteItem($key): bool */ public function deleteItems(array $keys): bool { - return $this->traceFunction('cache.delete_items', function () use ($keys): bool { + return $this->traceFunction('cache.remove', function () use ($keys): bool { return $this->decoratedAdapter->deleteItems($keys); }); } @@ -112,7 +113,7 @@ public function deleteItems(array $keys): bool */ public function save(CacheItemInterface $item): bool { - return $this->traceFunction('cache.save', function () use ($item): bool { + return $this->traceFunction('cache.put', function () use ($item): bool { return $this->decoratedAdapter->save($item); }); } @@ -122,7 +123,7 @@ public function save(CacheItemInterface $item): bool */ public function saveDeferred(CacheItemInterface $item): bool { - return $this->traceFunction('cache.save_deferred', function () use ($item): bool { + return $this->traceFunction('cache.put', function () use ($item): bool { return $this->decoratedAdapter->saveDeferred($item); }); } @@ -162,6 +163,10 @@ public function reset(): void } /** + * Traces a symfony operation and creating one span in the process. + * + * If you want to trace a get operation with callback, use {@see self::traceGet()} instead. + * * @phpstan-template TResult * * @phpstan-param \Closure(): TResult $callback @@ -172,27 +177,155 @@ private function traceFunction(string $spanOperation, \Closure $callback, ?strin { $span = $this->hub->getSpan(); - if (null !== $span) { - $spanContext = SpanContext::make() - ->setOp($spanOperation) - ->setOrigin('auto.cache'); + // Exit early if we have no span. + if (null === $span) { + return $callback(); + } - if (null !== $spanDescription) { - $spanContext->setDescription(urldecode($spanDescription)); - } + $spanContext = SpanContext::make() + ->setOp($spanOperation) + ->setOrigin('auto.cache'); - $span = $span->startChild($spanContext); + if (null !== $spanDescription) { + $spanContext->setDescription(urldecode($spanDescription)); } + $span = $span->startChild($spanContext); + try { - return $callback(); + $result = $callback(); + + // Necessary for static analysis. Otherwise, the TResult type is assumed to be CacheItemInterface. + if (!$result instanceof CacheItemInterface) { + return $result; + } + + $data = ['cache.hit' => $result->isHit()]; + if ($result->isHit()) { + $data['cache.item_size'] = static::getCacheItemSize($result->get()); + } + $span->setData($data); + + return $result; } finally { - if (null !== $span) { - $span->finish(); + $span->finish(); + } + } + + /** + * Traces a Symfony Cache get() call with a get and optional put span. + * + * Produces 2 spans in case of a cache miss: + * 1. 'cache.get' span + * 2. 'cache.put' span + * + * If the callback uses code with sentry traces, those traces will be available in the trace explorer. + * + * Use this method if you want to instrument {@see CacheInterface::get()}. + * + * @param string $key + * @param callable $callback + * @param float|null $beta + * @param array|null $metadata + * + * @return mixed + * + * @throws InvalidArgumentException + */ + private function traceGet(string $key, callable $callback, ?float $beta = null, ?array &$metadata = null) + { + if (!$this->decoratedAdapter instanceof CacheInterface) { + throw new \BadMethodCallException(\sprintf('The %s::get() method is not supported because the decorated adapter does not implement the "%s" interface.', self::class, CacheInterface::class)); + } + $parentSpan = $this->hub->getSpan(); + + // If we don't have a parent span we can just forward it. + if (null === $parentSpan) { + return $this->decoratedAdapter->get($key, $callback, $beta, $metadata); + } + + $spanContext = SpanContext::make() + ->setOp('cache.get') + ->setOrigin('auto.cache'); + + $spanContext->setDescription(urldecode($key)); + + $getSpan = $parentSpan->startChild($spanContext); + + try { + $this->hub->setSpan($getSpan); + + $wasMiss = false; + $saveStartTimestamp = null; + + try { + $value = $this->decoratedAdapter->get($key, function (CacheItemInterface $item, &$save) use ($callback, &$wasMiss, &$saveStartTimestamp) { + $wasMiss = true; + + $result = $callback($item, $save); + + if ($save) { + $saveStartTimestamp = microtime(true); + } + + return $result; + }, $beta, $metadata); + } catch (\Throwable $t) { + $getSpan->finish(); + throw $t; + } + + $now = microtime(true); + + $getSpan->setData([ + 'cache.hit' => !$wasMiss, + 'cache.item_size' => self::getCacheItemSize($value), + ]); + + // If we got a timestamp here we know that we missed + if (null !== $saveStartTimestamp) { + $getSpan->finish($saveStartTimestamp); + $saveContext = SpanContext::make() + ->setOp('cache.put') + ->setOrigin('auto.cache') + ->setDescription(urldecode($key)); + $saveSpan = $parentSpan->startChild($saveContext); + $saveSpan->setStartTimestamp($saveStartTimestamp); + $saveSpan->setData([ + 'cache.item_size' => self::getCacheItemSize($value), + ]); + $saveSpan->finish($now); + } else { + $getSpan->finish(); } + + return $value; + } finally { + // We always want to restore the previous parent span. + $this->hub->setSpan($parentSpan); } } + /** + * Calculates the size of the cached item. + * + * @param mixed $value + * + * @return int|null + */ + public static function getCacheItemSize($value): ?int + { + // We only gather the payload size for strings since this is easy to figure out + // and has basically no overhead. + // Getting the size of objects would be more complex, and it would potentially + // introduce more overhead since we don't get the size from the current framework abstraction. + if (\is_string($value)) { + return \strlen($value); + } + + return null; + } + /** * @phpstan-param \Closure(CacheItem): CacheItem $callback * @phpstan-param string $key diff --git a/src/Tracing/Cache/TraceableTagAwareCacheAdapterForV2.php b/src/Tracing/Cache/TraceableTagAwareCacheAdapterForV2.php index 62477cc2..3928a3e3 100644 --- a/src/Tracing/Cache/TraceableTagAwareCacheAdapterForV2.php +++ b/src/Tracing/Cache/TraceableTagAwareCacheAdapterForV2.php @@ -8,7 +8,6 @@ use Symfony\Component\Cache\Adapter\TagAwareAdapterInterface; use Symfony\Component\Cache\PruneableInterface; use Symfony\Component\Cache\ResettableInterface; -use Symfony\Contracts\Cache\CacheInterface; use Symfony\Contracts\Cache\TagAwareCacheInterface; /** @@ -43,13 +42,7 @@ public function __construct(HubInterface $hub, TagAwareAdapterInterface $decorat */ public function get(string $key, callable $callback, ?float $beta = null, ?array &$metadata = null) { - return $this->traceFunction('cache.get_item', function () use ($key, $callback, $beta, &$metadata) { - if (!$this->decoratedAdapter instanceof CacheInterface) { - throw new \BadMethodCallException(\sprintf('The %s::get() method is not supported because the decorated adapter does not implement the "%s" interface.', self::class, CacheInterface::class)); - } - - return $this->decoratedAdapter->get($key, $callback, $beta, $metadata); - }, $key); + return $this->traceGet($key, $callback, $beta, $metadata); } /** diff --git a/src/Tracing/Cache/TraceableTagAwareCacheAdapterForV3.php b/src/Tracing/Cache/TraceableTagAwareCacheAdapterForV3.php index cc5cc7b4..7e524624 100644 --- a/src/Tracing/Cache/TraceableTagAwareCacheAdapterForV3.php +++ b/src/Tracing/Cache/TraceableTagAwareCacheAdapterForV3.php @@ -8,7 +8,6 @@ use Symfony\Component\Cache\Adapter\TagAwareAdapterInterface; use Symfony\Component\Cache\PruneableInterface; use Symfony\Component\Cache\ResettableInterface; -use Symfony\Contracts\Cache\CacheInterface; use Symfony\Contracts\Cache\TagAwareCacheInterface; /** @@ -41,13 +40,7 @@ public function __construct(HubInterface $hub, TagAwareAdapterInterface $decorat */ public function get(string $key, callable $callback, ?float $beta = null, ?array &$metadata = null): mixed { - return $this->traceFunction('cache.get_item', function () use ($key, $callback, $beta, &$metadata) { - if (!$this->decoratedAdapter instanceof CacheInterface) { - throw new \BadMethodCallException(\sprintf('The %s::get() method is not supported because the decorated adapter does not implement the "%s" interface.', self::class, CacheInterface::class)); - } - - return $this->decoratedAdapter->get($key, $callback, $beta, $metadata); - }, $key); + return $this->traceGet($key, $callback, $beta, $metadata); } /** diff --git a/tests/End2End/App/Controller/PsrTracingCacheController.php b/tests/End2End/App/Controller/PsrTracingCacheController.php new file mode 100644 index 00000000..1726f116 --- /dev/null +++ b/tests/End2End/App/Controller/PsrTracingCacheController.php @@ -0,0 +1,40 @@ +adapter = $adapter; + } + + public function testPopulateString() + { + $item = $this->adapter->getItem('foo'); + if (!$item->isHit()) { + $item->set('example'); + $this->adapter->save($item); + } + + return new Response(); + } + + public function testDelete() + { + $this->adapter->deleteItem('foo'); + + return new Response(); + } +} diff --git a/tests/End2End/App/Controller/TracingCacheController.php b/tests/End2End/App/Controller/TracingCacheController.php new file mode 100644 index 00000000..04fff580 --- /dev/null +++ b/tests/End2End/App/Controller/TracingCacheController.php @@ -0,0 +1,73 @@ +cache = $cache; + $this->connection = $connection; + } + + public function populateCacheWithString() + { + $this->cache->get('example', function () { + return 'example-string'; + }); + + return new Response(); + } + + public function populateCacheWithInteger() + { + $this->cache->get('numeric', function () { + return 1234; + }); + + return new Response(); + } + + public function deleteCache() + { + $this->cache->delete('example'); + + return new Response(); + } + + public function getWithDbTrace() + { + $this->cache->get('fetched-value', function () { + $this->connection->executeQuery('SELECT 1'); + + return 'value'; + }); + + return new Response(); + } + + public function crashInCallback() + { + $this->cache->get('crash', function () { + throw new \RuntimeException('crash in callback'); + }); + + return new Response(); + } +} diff --git a/tests/End2End/App/config.yml b/tests/End2End/App/config.yml index c5d951a9..af12f69e 100644 --- a/tests/End2End/App/config.yml +++ b/tests/End2End/App/config.yml @@ -20,6 +20,9 @@ framework: annotations: false php_errors: log: true + # Use FS cache for testing + cache: + app: cache.adapter.filesystem services: test.hub: diff --git a/tests/End2End/App/routing.yml b/tests/End2End/App/routing.yml index e4494d1e..b7b1f6d6 100644 --- a/tests/End2End/App/routing.yml +++ b/tests/End2End/App/routing.yml @@ -42,6 +42,34 @@ tracing_ignored_transaction: path: /tracing/ignored-transaction defaults: { _controller: 'Sentry\SentryBundle\Tests\End2End\App\Controller\TracingController::ignoredTransaction' } +tracing_cache_populate_string: + path: /tracing/cache/populate-string + defaults: { _controller: 'Sentry\SentryBundle\Tests\End2End\App\Controller\TracingCacheController::populateCacheWithString' } + +tracing_cache_populate_integer: + path: /tracing/cache/populate-integer + defaults: { _controller: 'Sentry\SentryBundle\Tests\End2End\App\Controller\TracingCacheController::populateCacheWithInteger' } + +tracing_cache_delete: + path: /tracing/cache/delete + defaults: { _controller: 'Sentry\SentryBundle\Tests\End2End\App\Controller\TracingCacheController::deleteCache' } + +tracing_cache_crash: + path: /tracing/cache/crash + defaults: { _controller: 'Sentry\SentryBundle\Tests\End2End\App\Controller\TracingCacheController::crashInCallback' } + +tracing_cache_populate_string_with_db: + path: /tracing/cache/populate-string-with-db + defaults: { _controller: 'Sentry\SentryBundle\Tests\End2End\App\Controller\TracingCacheController::getWithDbTrace' } + +psr_tracing_cache_populate_string: + path: /tracing/cache/psr/populate-string + defaults: { _controller: 'Sentry\SentryBundle\Tests\End2End\App\Controller\PsrTracingCacheController::testPopulateString' } + +psr_tracing_cache_delete: + path: /tracing/cache/psr/delete + defaults: { _controller: 'Sentry\SentryBundle\Tests\End2End\App\Controller\PsrTracingCacheController::testDelete' } + just_logging: path: /just-logging defaults: { _controller: 'Sentry\SentryBundle\Tests\End2End\App\Controller\LoggingController::justLogging' } diff --git a/tests/End2End/App/tracing.yml b/tests/End2End/App/tracing.yml index 2ba7eceb..f54e14e5 100644 --- a/tests/End2End/App/tracing.yml +++ b/tests/End2End/App/tracing.yml @@ -10,4 +10,16 @@ services: $hub: '@Sentry\State\HubInterface' $connection: '@?doctrine.dbal.default_connection' tags: - - controller.service_arguments + - { name: controller.service_arguments } + + Sentry\SentryBundle\Tests\End2End\App\Controller\TracingCacheController: + arguments: + $cache: '@cache.app' + $connection: '@?doctrine.dbal.default_connection' + tags: + - { name: controller.service_arguments } + + Sentry\SentryBundle\Tests\End2End\App\Controller\PsrTracingCacheController: + autowire: true + tags: + - { name: controller.service_arguments } diff --git a/tests/End2End/TracingCacheEnd2EndTest.php b/tests/End2End/TracingCacheEnd2EndTest.php new file mode 100644 index 00000000..064abf98 --- /dev/null +++ b/tests/End2End/TracingCacheEnd2EndTest.php @@ -0,0 +1,226 @@ + false]); + + $client->request('GET', '/tracing/cache/populate-string'); + $this->assertSame(200, $client->getResponse()->getStatusCode()); + + $this->assertCount(1, StubTransport::$events); + $event = StubTransport::$events[0]; + $this->assertCount(2, $event->getSpans()); + + $getSpan = $event->getSpans()[0]; + $this->assertEquals('cache.get', $getSpan->getOp()); + $this->assertEquals(14, $getSpan->getData('cache.item_size')); + $this->assertFalse($getSpan->getData('cache.hit')); + + $putSpan = $event->getSpans()[1]; + $this->assertEquals('cache.put', $putSpan->getOp()); + $this->assertEquals(14, $putSpan->getData('cache.item_size')); + $this->assertNull($putSpan->getData('cache.hit')); + + // assert that put is a sibling span to get + $this->assertNotEquals($getSpan->getSpanId(), $putSpan->getParentSpanId()); + $this->assertEquals($getSpan->getParentSpanId(), $putSpan->getParentSpanId()); + } + + public function testCacheHit(): void + { + $client = static::createClient(['debug' => false]); + + // Populate the cache by having a cache miss + $client->request('GET', '/tracing/cache/populate-string'); + $this->assertSame(200, $client->getResponse()->getStatusCode()); + + // Reset transport so we only get events for cache HITs + StubTransport::$events = []; + + $client->request('GET', '/tracing/cache/populate-string'); + $this->assertSame(200, $client->getResponse()->getStatusCode()); + + $this->assertCount(1, StubTransport::$events); + + $event = StubTransport::$events[0]; + $this->assertCount(1, $event->getSpans()); + + $span = $event->getSpans()[0]; + $this->assertEquals('cache.get', $span->getOp()); + $this->assertEquals(14, $span->getData('cache.item_size')); + $this->assertTrue($span->getData('cache.hit')); + } + + public function testNonStringItemSize(): void + { + $client = static::createClient(['debug' => false]); + + $client->request('GET', '/tracing/cache/populate-integer'); + $this->assertSame(200, $client->getResponse()->getStatusCode()); + $this->assertCount(1, StubTransport::$events); + + $event = StubTransport::$events[0]; + $this->assertCount(2, $event->getSpans()); + + $span = $event->getSpans()[0]; + $this->assertEquals('cache.get', $span->getOp()); + $this->assertNull($span->getData('cache.item_size')); + } + + public function testDeleteCacheSpan(): void + { + $client = static::createClient(['debug' => false]); + + $client->request('GET', '/tracing/cache/delete'); + $this->assertSame(200, $client->getResponse()->getStatusCode()); + + $this->assertCount(1, StubTransport::$events); + $event = StubTransport::$events[0]; + + $this->assertCount(1, $event->getSpans()); + + $span = $event->getSpans()[0]; + $this->assertEquals('cache.remove', $span->getOp()); + $this->assertNull($span->getData('cache.item_size')); + } + + public function testGetWithDbSpan(): void + { + if (!class_exists(Connection::class)) { + $this->markTestSkipped('Skipped if doctrine is not available'); + } + $client = static::createClient(['debug' => false]); + + $client->request('GET', '/tracing/cache/populate-string-with-db'); + $this->assertSame(200, $client->getResponse()->getStatusCode()); + + $this->assertCount(1, StubTransport::$events); + $event = StubTransport::$events[0]; + + $this->assertCount(3, $event->getSpans()); + $getSpan = $event->getSpans()[0]; + $this->assertEquals('cache.get', $getSpan->getOp()); + + $dbSpan = $event->getSpans()[1]; + $this->assertEquals('db.sql.query', $dbSpan->getOp()); + + $putSpan = $event->getSpans()[2]; + $this->assertEquals('cache.put', $putSpan->getOp()); + + // assert that the DB call is a child span of the get operation + $this->assertEquals($getSpan->getSpanId(), $dbSpan->getParentSpanId()); + + // assert that get and put are siblings + $this->assertEquals($getSpan->getParentSpanId(), $putSpan->getParentSpanId()); + } + + public function testCrashInCallback(): void + { + $client = static::createClient(['debug' => false]); + + $client->request('GET', '/tracing/cache/crash'); + $this->assertSame(500, $client->getResponse()->getStatusCode()); + + $this->assertCount(2, StubTransport::$events); + + // This is the exception from the callback + $event = StubTransport::$events[0]; + $this->assertCount(1, $event->getExceptions()); + $this->assertSame('crash in callback', $event->getExceptions()[0]->getValue()); + $this->assertCount(0, $event->getSpans()); + + $spansEvent = StubTransport::$events[1]; + $this->assertCount(2, $spansEvent->getSpans()); + + $getSpan = $spansEvent->getSpans()[0]; + $this->assertEquals('cache.get', $getSpan->getOp()); + + // This is the span for rendering the error page (I think) + $span = $spansEvent->getSpans()[1]; + $this->assertEquals('http.server', $span->getOp()); + $this->assertEquals('auto.http.server', $span->getOrigin()); + } + + public function testPsrCachePopulateString(): void + { + $client = static::createClient(['debug' => false]); + + $client->request('GET', '/tracing/cache/psr/populate-string'); + $this->assertSame(200, $client->getResponse()->getStatusCode()); + + $this->assertCount(1, StubTransport::$events); + $event = StubTransport::$events[0]; + + // PSR caches will only have two spans, one for the get and one for put + $this->assertCount(2, $event->getSpans()); + + $getSpan = $event->getSpans()[0]; + $this->assertEquals('cache.get', $getSpan->getOp()); + + $putSpan = $event->getSpans()[1]; + $this->assertEquals('cache.put', $putSpan->getOp()); + } + + public function testPsrCacheHit(): void + { + $client = static::createClient(['debug' => false]); + + $client->request('GET', '/tracing/cache/psr/populate-string'); + $this->assertSame(200, $client->getResponse()->getStatusCode()); + + // This populates the cache and is tested in a different test + $this->assertCount(1, StubTransport::$events); + $this->assertCount(2, StubTransport::$events[0]->getSpans()); + StubTransport::$events = []; + + $client->request('PUT', '/tracing/cache/psr/populate-string'); + $this->assertSame(200, $client->getResponse()->getStatusCode()); + + $this->assertCount(1, StubTransport::$events); + $event = StubTransport::$events[0]; + + $this->assertCount(1, $event->getSpans()); + $span = $event->getSpans()[0]; + $this->assertEquals('cache.get', $span->getOp()); + $this->assertTrue($span->getData('cache.hit')); + } + + public function testPsrCacheDelete(): void + { + $client = static::createClient(['debug' => false]); + + $client->request('GET', '/tracing/cache/psr/delete'); + $this->assertSame(200, $client->getResponse()->getStatusCode()); + + $this->assertCount(1, StubTransport::$events); + $event = StubTransport::$events[0]; + + $this->assertCount(1, $event->getSpans()); + $span = $event->getSpans()[0]; + $this->assertEquals('cache.remove', $span->getOp()); + $this->assertNull($span->getData('cache.item_size')); + } +} diff --git a/tests/Tracing/Cache/AbstractTraceableCacheAdapterTest.php b/tests/Tracing/Cache/AbstractTraceableCacheAdapterTest.php index 4b49194f..589c4610 100644 --- a/tests/Tracing/Cache/AbstractTraceableCacheAdapterTest.php +++ b/tests/Tracing/Cache/AbstractTraceableCacheAdapterTest.php @@ -63,7 +63,7 @@ public function testGetItem(): void $spans = $transaction->getSpanRecorder()->getSpans(); $this->assertCount(2, $spans); - $this->assertSame('cache.get_item', $spans[1]->getOp()); + $this->assertSame('cache.get', $spans[1]->getOp()); $this->assertSame('foo', $spans[1]->getDescription()); $this->assertNotNull($spans[1]->getEndTimestamp()); } @@ -92,7 +92,7 @@ public function testGetItems(): void $spans = $transaction->getSpanRecorder()->getSpans(); $this->assertCount(2, $spans); - $this->assertSame('cache.get_items', $spans[1]->getOp()); + $this->assertSame('cache.get', $spans[1]->getOp()); $this->assertNotNull($spans[1]->getEndTimestamp()); } @@ -119,14 +119,15 @@ public function testClear(): void $spans = $transaction->getSpanRecorder()->getSpans(); $this->assertCount(2, $spans); - $this->assertSame('cache.clear', $spans[1]->getOp()); + $this->assertSame('cache.flush', $spans[1]->getOp()); $this->assertSame('foo', $spans[1]->getDescription()); $this->assertNotNull($spans[1]->getEndTimestamp()); } public function testGet(): void { - $callback = static function () {}; + $callback = static function () { + }; $metadata = []; $transaction = new Transaction(new TransactionContext(), $this->hub); $transaction->initSpanRecorder(); @@ -149,7 +150,7 @@ public function testGet(): void $spans = $transaction->getSpanRecorder()->getSpans(); $this->assertCount(2, $spans); - $this->assertSame('cache.get_item', $spans[1]->getOp()); + $this->assertSame('cache.get', $spans[1]->getOp()); $this->assertSame('foo', $spans[1]->getDescription()); $this->assertNotNull($spans[1]->getEndTimestamp()); } @@ -161,7 +162,8 @@ public function testGetThrowsExceptionIfDecoratedAdapterDoesNotImplementTheCache $this->expectException(\BadMethodCallException::class); $this->expectExceptionMessage(\sprintf('The %s::get() method is not supported because the decorated adapter does not implement the "Symfony\\Contracts\\Cache\\CacheInterface" interface.', \get_class($adapter))); - $adapter->get('foo', static function () {}); + $adapter->get('foo', static function () { + }); } public function testDelete(): void @@ -187,7 +189,7 @@ public function testDelete(): void $spans = $transaction->getSpanRecorder()->getSpans(); $this->assertCount(2, $spans); - $this->assertSame('cache.delete_item', $spans[1]->getOp()); + $this->assertSame('cache.remove', $spans[1]->getOp()); $this->assertSame('foo', $spans[1]->getDescription()); $this->assertNotNull($spans[1]->getEndTimestamp()); } @@ -253,7 +255,7 @@ public function testDeleteItem(): void $spans = $transaction->getSpanRecorder()->getSpans(); $this->assertCount(2, $spans); - $this->assertSame('cache.delete_item', $spans[1]->getOp()); + $this->assertSame('cache.remove', $spans[1]->getOp()); $this->assertSame('foo', $spans[1]->getDescription()); $this->assertNotNull($spans[1]->getEndTimestamp()); } @@ -281,7 +283,7 @@ public function testDeleteItems(): void $spans = $transaction->getSpanRecorder()->getSpans(); $this->assertCount(2, $spans); - $this->assertSame('cache.delete_items', $spans[1]->getOp()); + $this->assertSame('cache.remove', $spans[1]->getOp()); $this->assertNotNull($spans[1]->getEndTimestamp()); } @@ -309,7 +311,7 @@ public function testSave(): void $spans = $transaction->getSpanRecorder()->getSpans(); $this->assertCount(2, $spans); - $this->assertSame('cache.save', $spans[1]->getOp()); + $this->assertSame('cache.put', $spans[1]->getOp()); $this->assertNotNull($spans[1]->getEndTimestamp()); } @@ -337,7 +339,7 @@ public function testSaveDeferred(): void $spans = $transaction->getSpanRecorder()->getSpans(); $this->assertCount(2, $spans); - $this->assertSame('cache.save_deferred', $spans[1]->getOp()); + $this->assertSame('cache.put', $spans[1]->getOp()); $this->assertNotNull($spans[1]->getEndTimestamp()); }