Skip to content

Commit 02df933

Browse files
authored
feat(cache): enable auto-instrumentation for symfony cache (#942)
1 parent 8728790 commit 02df933

14 files changed

+560
-68
lines changed

phpstan-baseline.neon

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -419,3 +419,8 @@ parameters:
419419
message: "#^Parameter \\#2 \\$responses of static method Sentry\\\\SentryBundle\\\\Tracing\\\\HttpClient\\\\AbstractTraceableResponse\\:\\:stream\\(\\) expects iterable\\<Sentry\\\\SentryBundle\\\\Tracing\\\\HttpClient\\\\AbstractTraceableResponse\\>, array\\<int, stdClass\\> given\\.$#"
420420
count: 1
421421
path: tests/Tracing/HttpClient/TraceableResponseTest.php
422+
423+
-
424+
message: "#^Call to method PHPUnit\\\\Framework\\\\Assert\\:\\:assertCount\\(\\) with 1 and array\\{\\} will always evaluate to false\\.$#"
425+
count: 2
426+
path: tests/End2End/TracingCacheEnd2EndTest.php

src/Tracing/Cache/TraceableCacheAdapterForV2.php

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,6 @@ public function __construct(HubInterface $hub, AdapterInterface $decoratedAdapte
4242
*/
4343
public function get(string $key, callable $callback, ?float $beta = null, ?array &$metadata = null)
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: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,16 +38,12 @@ public function __construct(HubInterface $hub, AdapterInterface $decoratedAdapte
3838
* {@inheritdoc}
3939
*
4040
* @param mixed[] $metadata
41+
*
42+
* @return mixed
4143
*/
4244
public function get(string $key, callable $callback, ?float $beta = null, ?array &$metadata = null): mixed
4345
{
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);
46+
return $this->traceGet($key, $callback, $beta, $metadata);
5147
}
5248

5349
public function withSubNamespace(string $namespace): static

src/Tracing/Cache/TraceableCacheAdapterTrait.php

Lines changed: 152 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,27 +177,155 @@ 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+
$spanContext = SpanContext::make()
248+
->setOp('cache.get')
249+
->setOrigin('auto.cache');
250+
251+
$spanContext->setDescription(urldecode($key));
252+
253+
$getSpan = $parentSpan->startChild($spanContext);
254+
255+
try {
256+
$this->hub->setSpan($getSpan);
257+
258+
$wasMiss = false;
259+
$saveStartTimestamp = null;
260+
261+
try {
262+
$value = $this->decoratedAdapter->get($key, function (CacheItemInterface $item, &$save) use ($callback, &$wasMiss, &$saveStartTimestamp) {
263+
$wasMiss = true;
264+
265+
$result = $callback($item, $save);
266+
267+
if ($save) {
268+
$saveStartTimestamp = microtime(true);
269+
}
270+
271+
return $result;
272+
}, $beta, $metadata);
273+
} catch (\Throwable $t) {
274+
$getSpan->finish();
275+
throw $t;
276+
}
277+
278+
$now = microtime(true);
279+
280+
$getSpan->setData([
281+
'cache.hit' => !$wasMiss,
282+
'cache.item_size' => self::getCacheItemSize($value),
283+
]);
284+
285+
// If we got a timestamp here we know that we missed
286+
if (null !== $saveStartTimestamp) {
287+
$getSpan->finish($saveStartTimestamp);
288+
$saveContext = SpanContext::make()
289+
->setOp('cache.put')
290+
->setOrigin('auto.cache')
291+
->setDescription(urldecode($key));
292+
$saveSpan = $parentSpan->startChild($saveContext);
293+
$saveSpan->setStartTimestamp($saveStartTimestamp);
294+
$saveSpan->setData([
295+
'cache.item_size' => self::getCacheItemSize($value),
296+
]);
297+
$saveSpan->finish($now);
298+
} else {
299+
$getSpan->finish();
192300
}
301+
302+
return $value;
303+
} finally {
304+
// We always want to restore the previous parent span.
305+
$this->hub->setSpan($parentSpan);
193306
}
194307
}
195308

309+
/**
310+
* Calculates the size of the cached item.
311+
*
312+
* @param mixed $value
313+
*
314+
* @return int|null
315+
*/
316+
public static function getCacheItemSize($value): ?int
317+
{
318+
// We only gather the payload size for strings since this is easy to figure out
319+
// and has basically no overhead.
320+
// Getting the size of objects would be more complex, and it would potentially
321+
// introduce more overhead since we don't get the size from the current framework abstraction.
322+
if (\is_string($value)) {
323+
return \strlen($value);
324+
}
325+
326+
return null;
327+
}
328+
196329
/**
197330
* @phpstan-param \Closure(CacheItem): CacheItem $callback
198331
* @phpstan-param string $key

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: 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
/**
@@ -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
/**

0 commit comments

Comments
 (0)