Skip to content

Commit 1a9ff70

Browse files
committed
feat(cache): enable auto-instrumentation for symfony cache
1 parent 9b169cc commit 1a9ff70

13 files changed

+498
-63
lines changed

src/Tracing/Cache/TraceableCacheAdapterForV2.php

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,8 @@ public function __construct(HubInterface $hub, AdapterInterface $decoratedAdapte
4040
*
4141
* @return mixed
4242
*/
43-
public function get(string $key, callable $callback, ?float $beta = null, ?array &$metadata = null)
43+
public function get(string $key, callable $callback, ?float $beta = null, ?array &$metadata = null): mixed
4444
{
45-
return $this->traceFunction('cache.get_item', function () use ($key, $callback, $beta, &$metadata) {
46-
if (!$this->decoratedAdapter instanceof CacheInterface) {
47-
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));
48-
}
49-
50-
return $this->decoratedAdapter->get($key, $callback, $beta, $metadata);
51-
}, $key);
45+
return $this->traceGet($key, $callback, $beta, $metadata);
5246
}
5347
}

src/Tracing/Cache/TraceableCacheAdapterForV3.php

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,6 @@ public function __construct(HubInterface $hub, AdapterInterface $decoratedAdapte
4040
*/
4141
public function get(string $key, callable $callback, ?float $beta = null, ?array &$metadata = null): mixed
4242
{
43-
return $this->traceFunction('cache.get_item', function () use ($key, $callback, $beta, &$metadata) {
44-
if (!$this->decoratedAdapter instanceof CacheInterface) {
45-
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));
46-
}
47-
48-
return $this->decoratedAdapter->get($key, $callback, $beta, $metadata);
49-
}, $key);
43+
return $this->traceGet($key, $callback, $beta, $metadata);
5044
}
5145
}

src/Tracing/Cache/TraceableCacheAdapterForV3WithNamespace.php

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,7 @@ public function __construct(HubInterface $hub, AdapterInterface $decoratedAdapte
4141
*/
4242
public function get(string $key, callable $callback, ?float $beta = null, ?array &$metadata = null): mixed
4343
{
44-
return $this->traceFunction('cache.get_item', function () use ($key, $callback, $beta, &$metadata) {
45-
if (!$this->decoratedAdapter instanceof CacheInterface) {
46-
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));
47-
}
48-
49-
return $this->decoratedAdapter->get($key, $callback, $beta, $metadata);
50-
}, $key);
44+
return $this->traceGet($key, $callback, $beta, $metadata);
5145
}
5246

5347
public function withSubNamespace(string $namespace): static

src/Tracing/Cache/TraceableCacheAdapterTrait.php

Lines changed: 142 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace Sentry\SentryBundle\Tracing\Cache;
66

77
use Psr\Cache\CacheItemInterface;
8+
use Psr\Cache\InvalidArgumentException;
89
use Sentry\State\HubInterface;
910
use Sentry\Tracing\SpanContext;
1011
use Symfony\Component\Cache\Adapter\AdapterInterface;
@@ -38,7 +39,7 @@ trait TraceableCacheAdapterTrait
3839
*/
3940
public function getItem($key): CacheItem
4041
{
41-
return $this->traceFunction('cache.get_item', function () use ($key): CacheItem {
42+
return $this->traceFunction('cache.get', function () use ($key): CacheItem {
4243
return $this->decoratedAdapter->getItem($key);
4344
}, $key);
4445
}
@@ -48,7 +49,7 @@ public function getItem($key): CacheItem
4849
*/
4950
public function getItems(array $keys = []): iterable
5051
{
51-
return $this->traceFunction('cache.get_items', function () use ($keys): iterable {
52+
return $this->traceFunction('cache.get', function () use ($keys): iterable {
5253
return $this->decoratedAdapter->getItems($keys);
5354
});
5455
}
@@ -58,7 +59,7 @@ public function getItems(array $keys = []): iterable
5859
*/
5960
public function clear(string $prefix = ''): bool
6061
{
61-
return $this->traceFunction('cache.clear', function () use ($prefix): bool {
62+
return $this->traceFunction('cache.flush', function () use ($prefix): bool {
6263
return $this->decoratedAdapter->clear($prefix);
6364
}, $prefix);
6465
}
@@ -68,7 +69,7 @@ public function clear(string $prefix = ''): bool
6869
*/
6970
public function delete(string $key): bool
7071
{
71-
return $this->traceFunction('cache.delete_item', function () use ($key): bool {
72+
return $this->traceFunction('cache.remove', function () use ($key): bool {
7273
if (!$this->decoratedAdapter instanceof CacheInterface) {
7374
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));
7475
}
@@ -92,7 +93,7 @@ public function hasItem($key): bool
9293
*/
9394
public function deleteItem($key): bool
9495
{
95-
return $this->traceFunction('cache.delete_item', function () use ($key): bool {
96+
return $this->traceFunction('cache.remove', function () use ($key): bool {
9697
return $this->decoratedAdapter->deleteItem($key);
9798
}, $key);
9899
}
@@ -102,7 +103,7 @@ public function deleteItem($key): bool
102103
*/
103104
public function deleteItems(array $keys): bool
104105
{
105-
return $this->traceFunction('cache.delete_items', function () use ($keys): bool {
106+
return $this->traceFunction('cache.remove', function () use ($keys): bool {
106107
return $this->decoratedAdapter->deleteItems($keys);
107108
});
108109
}
@@ -112,7 +113,7 @@ public function deleteItems(array $keys): bool
112113
*/
113114
public function save(CacheItemInterface $item): bool
114115
{
115-
return $this->traceFunction('cache.save', function () use ($item): bool {
116+
return $this->traceFunction('cache.put', function () use ($item): bool {
116117
return $this->decoratedAdapter->save($item);
117118
});
118119
}
@@ -122,7 +123,7 @@ public function save(CacheItemInterface $item): bool
122123
*/
123124
public function saveDeferred(CacheItemInterface $item): bool
124125
{
125-
return $this->traceFunction('cache.save_deferred', function () use ($item): bool {
126+
return $this->traceFunction('cache.put', function () use ($item): bool {
126127
return $this->decoratedAdapter->saveDeferred($item);
127128
});
128129
}
@@ -162,6 +163,10 @@ public function reset(): void
162163
}
163164

164165
/**
166+
* Traces a symfony operation and creating one span in the process.
167+
*
168+
* If you want to trace a get operation with callback, use {@see self::traceGet()} instead.
169+
*
165170
* @phpstan-template TResult
166171
*
167172
* @phpstan-param \Closure(): TResult $callback
@@ -172,25 +177,143 @@ private function traceFunction(string $spanOperation, \Closure $callback, ?strin
172177
{
173178
$span = $this->hub->getSpan();
174179

175-
if (null !== $span) {
176-
$spanContext = SpanContext::make()
177-
->setOp($spanOperation)
178-
->setOrigin('auto.cache');
180+
// Exit early if we have no span.
181+
if (null === $span) {
182+
return $callback();
183+
}
179184

180-
if (null !== $spanDescription) {
181-
$spanContext->setDescription(urldecode($spanDescription));
182-
}
185+
$spanContext = SpanContext::make()
186+
->setOp($spanOperation)
187+
->setOrigin('auto.cache');
183188

184-
$span = $span->startChild($spanContext);
189+
if (null !== $spanDescription) {
190+
$spanContext->setDescription(urldecode($spanDescription));
185191
}
186192

193+
$span = $span->startChild($spanContext);
194+
187195
try {
188-
return $callback();
196+
$result = $callback();
197+
198+
// Necessary for static analysis. Otherwise, the TResult type is assumed to be CacheItemInterface.
199+
if (!$result instanceof CacheItemInterface) {
200+
return $result;
201+
}
202+
203+
$data = ['cache.hit' => $result->isHit()];
204+
if ($result->isHit()) {
205+
$data['cache.item_size'] = static::getCacheItemSize($result->get());
206+
}
207+
$span->setData($data);
208+
209+
return $result;
189210
} finally {
190-
if (null !== $span) {
191-
$span->finish();
211+
$span->finish();
212+
}
213+
}
214+
215+
/**
216+
* Traces a Symfony Cache get() call with a get and optional put span.
217+
*
218+
* Produces 2 spans in case of a cache miss:
219+
* 1. 'cache.get' span
220+
* 2. 'cache.put' span
221+
*
222+
* If the callback uses code with sentry traces, those traces will be available in the trace explorer.
223+
*
224+
* Use this method if you want to instrument {@see CacheInterface::get()}.
225+
*
226+
* @param string $key
227+
* @param callable $callback
228+
* @param float|null $beta
229+
* @param array<int|string,mixed>|null $metadata
230+
*
231+
* @return mixed
232+
*
233+
* @throws InvalidArgumentException
234+
*/
235+
private function traceGet(string $key, callable $callback, ?float $beta = null, ?array &$metadata = null)
236+
{
237+
if (!$this->decoratedAdapter instanceof CacheInterface) {
238+
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));
239+
}
240+
$parentSpan = $this->hub->getSpan();
241+
242+
// If we don't have a parent span we can just forward it.
243+
if (null === $parentSpan) {
244+
return $this->decoratedAdapter->get($key, $callback, $beta, $metadata);
245+
}
246+
247+
try {
248+
$spanContext = SpanContext::make()
249+
->setOp('cache.get')
250+
->setOrigin('auto.cache');
251+
252+
$spanContext->setDescription(urldecode($key));
253+
254+
$getSpan = $parentSpan->startChild($spanContext);
255+
$this->hub->setSpan($getSpan);
256+
257+
$wasMiss = false;
258+
$saveStartTimestamp = null;
259+
260+
$value = $this->decoratedAdapter->get($key, function (CacheItemInterface $item, &$save) use ($callback, &$wasMiss, &$saveStartTimestamp) {
261+
$wasMiss = true;
262+
263+
$result = $callback($item, $save);
264+
265+
if ($save) {
266+
$saveStartTimestamp = microtime(true);
267+
}
268+
269+
return $result;
270+
}, $beta, $metadata);
271+
272+
$now = microtime(true);
273+
274+
$getSpan->setData([
275+
'cache.hit' => !$wasMiss,
276+
'cache.item_size' => self::getCacheItemSize($value),
277+
]);
278+
279+
// If we got a timestamp here we know that we missed
280+
if (null !== $saveStartTimestamp) {
281+
$getSpan->finish($saveStartTimestamp);
282+
$saveContext = SpanContext::make()
283+
->setOp('cache.put')
284+
->setOrigin('auto.cache')
285+
->setDescription(urldecode($key));
286+
$saveSpan = $parentSpan->startChild($saveContext);
287+
$saveSpan->setStartTimestamp($saveStartTimestamp);
288+
$saveSpan->setData([
289+
'cache.item_size' => self::getCacheItemSize($value),
290+
]);
291+
$saveSpan->finish($now);
292+
} else {
293+
$getSpan->finish();
192294
}
295+
} finally {
296+
// We always want to restore the previous parent span.
297+
$this->hub->setSpan($parentSpan);
193298
}
299+
300+
return $value;
301+
}
302+
303+
/**
304+
* Calculates the size of the cached item.
305+
*
306+
* @param mixed $value
307+
*
308+
* @return int|null
309+
*/
310+
public static function getCacheItemSize($value): ?int
311+
{
312+
if (\is_string($value)) {
313+
return \strlen($value);
314+
}
315+
316+
return null;
194317
}
195318

196319
/**

src/Tracing/Cache/TraceableTagAwareCacheAdapterForV2.php

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
use Symfony\Component\Cache\Adapter\TagAwareAdapterInterface;
99
use Symfony\Component\Cache\PruneableInterface;
1010
use Symfony\Component\Cache\ResettableInterface;
11-
use Symfony\Contracts\Cache\CacheInterface;
1211
use Symfony\Contracts\Cache\TagAwareCacheInterface;
1312

1413
/**
@@ -43,13 +42,7 @@ public function __construct(HubInterface $hub, TagAwareAdapterInterface $decorat
4342
*/
4443
public function get(string $key, callable $callback, ?float $beta = null, ?array &$metadata = null)
4544
{
46-
return $this->traceFunction('cache.get_item', function () use ($key, $callback, $beta, &$metadata) {
47-
if (!$this->decoratedAdapter instanceof CacheInterface) {
48-
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));
49-
}
50-
51-
return $this->decoratedAdapter->get($key, $callback, $beta, $metadata);
52-
}, $key);
45+
return $this->traceGet($key, $callback, $beta, $metadata);
5346
}
5447

5548
/**

src/Tracing/Cache/TraceableTagAwareCacheAdapterForV3.php

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
use Symfony\Component\Cache\Adapter\TagAwareAdapterInterface;
99
use Symfony\Component\Cache\PruneableInterface;
1010
use Symfony\Component\Cache\ResettableInterface;
11-
use Symfony\Contracts\Cache\CacheInterface;
1211
use Symfony\Contracts\Cache\TagAwareCacheInterface;
1312

1413
/**
@@ -25,7 +24,7 @@ final class TraceableTagAwareCacheAdapterForV3 implements TagAwareAdapterInterfa
2524
use TraceableCacheAdapterTrait;
2625

2726
/**
28-
* @param HubInterface $hub The current hub
27+
* @param HubInterface $hub The current hub
2928
* @param TagAwareAdapterInterface $decoratedAdapter The decorated cache adapter
3029
*/
3130
public function __construct(HubInterface $hub, TagAwareAdapterInterface $decoratedAdapter)
@@ -41,13 +40,7 @@ public function __construct(HubInterface $hub, TagAwareAdapterInterface $decorat
4140
*/
4241
public function get(string $key, callable $callback, ?float $beta = null, ?array &$metadata = null): mixed
4342
{
44-
return $this->traceFunction('cache.get_item', function () use ($key, $callback, $beta, &$metadata) {
45-
if (!$this->decoratedAdapter instanceof CacheInterface) {
46-
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));
47-
}
48-
49-
return $this->decoratedAdapter->get($key, $callback, $beta, $metadata);
50-
}, $key);
43+
return $this->traceGet($key, $callback, $beta, $metadata);
5144
}
5245

5346
/**
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Sentry\SentryBundle\Tests\End2End\App\Controller;
6+
7+
use Psr\Cache\CacheItemPoolInterface;
8+
use Symfony\Component\Cache\Adapter\AdapterInterface;
9+
use Symfony\Component\HttpFoundation\Response;
10+
11+
class PsrTracingCacheController
12+
{
13+
/**
14+
* @var AdapterInterface
15+
*/
16+
private $adapter;
17+
18+
public function __construct(CacheItemPoolInterface $adapter)
19+
{
20+
$this->adapter = $adapter;
21+
}
22+
23+
public function testPopulateString()
24+
{
25+
$item = $this->adapter->getItem('foo');
26+
if (!$item->isHit()) {
27+
$item->set('example');
28+
$this->adapter->save($item);
29+
}
30+
31+
return new Response();
32+
}
33+
34+
public function testDelete()
35+
{
36+
$this->adapter->deleteItem('foo');
37+
38+
return new Response();
39+
}
40+
}

0 commit comments

Comments
 (0)