Skip to content

Commit

Permalink
Allow normalization of cache keys
Browse files Browse the repository at this point in the history
  • Loading branch information
timacdonald committed Oct 6, 2023
1 parent 4b53151 commit 6842f70
Show file tree
Hide file tree
Showing 3 changed files with 142 additions and 42 deletions.
4 changes: 1 addition & 3 deletions config/pulse.php
Original file line number Diff line number Diff line change
Expand Up @@ -209,9 +209,7 @@
*/

'cache_keys' => [
'^post:139$' => 'Post 139',
'^server:1\d{2}$' => 'Servers 100 - 199',
'^flight:.*' => 'All flights',
'/(.*)/' => '\1',
],

/*
Expand Down
22 changes: 20 additions & 2 deletions src/Recorders/CacheInteractions.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use Carbon\CarbonImmutable;
use Illuminate\Cache\Events\CacheHit;
use Illuminate\Cache\Events\CacheMissed;
use Illuminate\Config\Repository;
use Illuminate\Support\Str;
use Laravel\Pulse\Entry;
use Laravel\Pulse\Pulse;
Expand Down Expand Up @@ -34,6 +35,7 @@ class CacheInteractions
*/
public function __construct(
protected Pulse $pulse,
protected Repository $config,
) {
//
}
Expand All @@ -45,15 +47,31 @@ public function record(CacheHit|CacheMissed $event): ?Entry
{
$now = new CarbonImmutable();

if (Str::startsWith($event->key, ['illuminate:', 'laravel:pulse'])) {
if (Str::startsWith($event->key, ['illuminate:', 'laravel:pulse:'])) {
return null;
}

return new Entry($this->table, [
'date' => $now->toDateTimeString(),
'hit' => $event instanceof CacheHit,
'key' => $event->key,
'key' => fn () => $this->normalize($event->key),
'user_id' => $this->pulse->authenticatedUserIdResolver(),
]);
}

/**
* Normalize the cache key.
*/
protected function normalize(string $key): string
{
foreach ($this->config->get('pulse.cache_keys') as $pattern => $replacement) {
$normalized = preg_replace($pattern, $replacement, $key, count: $count);

if ($count > 0) {
return $normalized;
}
}

return $key;
}
}
158 changes: 121 additions & 37 deletions tests/Feature/CacheInteractionsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,60 +20,51 @@
Carbon::setTestNow('2000-01-02 03:04:05');

Cache::get('cache-key');

expect(Pulse::entries())->toHaveCount(1);
Pulse::ignore(fn () => expect(DB::table('pulse_cache_interactions')->count())->toBe(0));

Pulse::store(app(Ingest::class));

expect(Pulse::entries())->toHaveCount(0);
$cacheHits = Pulse::ignore(fn () => DB::table('pulse_cache_interactions')->get());
expect($cacheHits)->toHaveCount(1);
expect((array) $cacheHits[0])->toEqual([
$interactions = Pulse::ignore(fn () => DB::table('pulse_cache_interactions')->get());
expect($interactions)->toHaveCount(1);
expect($interactions[0])->toHaveProperties([
'date' => '2000-01-02 03:04:05',
'user_id' => null,
'key' => 'cache-key',
'hit' => 0,
]);
});

it('ignores any internal illuminate cache interactions', function () {
Carbon::setTestNow('2000-01-02 03:04:05');

it('ignores internal illuminate cache interactions', function () {
Cache::get('illuminate:');
Pulse::store(app(Ingest::class));

$cacheHits = Pulse::ignore(fn () => DB::table('pulse_cache_interactions')->get());
expect($cacheHits)->toHaveCount(0);
$interactions = Pulse::ignore(fn () => DB::table('pulse_cache_interactions')->get());
expect($interactions)->toHaveCount(0);
});

it('ignores any internal pulse cache interactions', function () {
Carbon::setTestNow('2000-01-02 03:04:05');

Cache::get('laravel:pulse');
it('ignores internal pulse cache interactions', function () {
Cache::get('laravel:pulse:foo');
Pulse::store(app(Ingest::class));

$cacheHits = Pulse::ignore(fn () => DB::table('pulse_cache_interactions')->get());
expect($cacheHits)->toHaveCount(0);
$interactions = Pulse::ignore(fn () => DB::table('pulse_cache_interactions')->get());
expect($interactions)->toHaveCount(0);
});

it('captures hits and misses', function () {
it('ingests hits', function () {
Carbon::setTestNow('2000-01-02 03:04:05');
Cache::put('hit', 123);

Cache::put('hit', 123);
Cache::get('hit');
Cache::get('miss');
Pulse::store(app(Ingest::class));

$cacheHits = Pulse::ignore(fn () => DB::table('pulse_cache_interactions')->get());
expect($cacheHits)->toHaveCount(2);
expect((array) $cacheHits[0])->toEqual([
$interactions = Pulse::ignore(fn () => DB::table('pulse_cache_interactions')->get());
expect($interactions)->toHaveCount(2);
expect($interactions[0])->toHaveProperties([
'date' => '2000-01-02 03:04:05',
'user_id' => null,
'key' => 'hit',
'hit' => 1,
]);
expect((array) $cacheHits[1])->toEqual([
expect($interactions[1])->toHaveProperties([
'date' => '2000-01-02 03:04:05',
'user_id' => null,
'key' => 'miss',
Expand All @@ -87,19 +78,19 @@
Cache::get('cache-key');
Pulse::store(app(Ingest::class));

$cacheHits = Pulse::ignore(fn () => DB::table('pulse_cache_interactions')->get());
expect($cacheHits)->toHaveCount(1);
expect($cacheHits[0]->user_id)->toBe('567');
$interactions = Pulse::ignore(fn () => DB::table('pulse_cache_interactions')->get());
expect($interactions)->toHaveCount(1);
expect($interactions[0]->user_id)->toBe('567');
});

it('captures the authenticated user if they login after the interaction', function () {
Cache::get('cache-key');
Auth::login(User::make(['id' => '567']));
Pulse::store(app(Ingest::class));

$cacheHits = Pulse::ignore(fn () => DB::table('pulse_cache_interactions')->get());
expect($cacheHits)->toHaveCount(1);
expect($cacheHits[0]->user_id)->toBe('567');
$interactions = Pulse::ignore(fn () => DB::table('pulse_cache_interactions')->get());
expect($interactions)->toHaveCount(1);
expect($interactions[0]->user_id)->toBe('567');
});

it('captures the authenticated user if they logout after the interaction', function () {
Expand All @@ -109,9 +100,9 @@
Auth::logout();
Pulse::store(app(Ingest::class));

$cacheHits = Pulse::ignore(fn () => DB::table('pulse_cache_interactions')->get());
expect($cacheHits)->toHaveCount(1);
expect($cacheHits[0]->user_id)->toBe('567');
$interactions = Pulse::ignore(fn () => DB::table('pulse_cache_interactions')->get());
expect($interactions)->toHaveCount(1);
expect($interactions[0]->user_id)->toBe('567');
});

it('does not trigger an inifite loop when retriving the authenticated user from the database', function () {
Expand Down Expand Up @@ -140,9 +131,9 @@ public function user()
Cache::get('cache-key');
Pulse::store(app(Ingest::class));

$cacheHits = Pulse::ignore(fn () => DB::table('pulse_cache_interactions')->get());
expect($cacheHits)->toHaveCount(1);
expect($cacheHits[0]->user_id)->toBe(null);
$interactions = Pulse::ignore(fn () => DB::table('pulse_cache_interactions')->get());
expect($interactions)->toHaveCount(1);
expect($interactions[0]->user_id)->toBe(null);
});

it('quietly fails if an exception is thrown while preparing the entry payload', function () {
Expand Down Expand Up @@ -178,3 +169,96 @@ public function hasUser()
expect($interactions[1]->user_id)->toBe('567');
expect($interactions[2]->user_id)->toBe('789');
});

it('stores the original keys by default', function () {
Carbon::setTestNow('2000-01-02 03:04:05');

Cache::get('users:1234:profile');
Pulse::store(app(Ingest::class));

$interactions = Pulse::ignore(fn () => DB::table('pulse_cache_interactions')->get());
expect($interactions)->toHaveCount(1);
expect((array) $interactions[0])->toEqual([
'date' => '2000-01-02 03:04:05',
'user_id' => null,
'key' => 'users:1234:profile',
'hit' => 0,
]);
});

it('can normalize cache keys', function () {
Carbon::setTestNow('2000-01-02 03:04:05');

Config::set('pulse.cache_keys', [
'/users:\d+:profile/' => 'users:{user}:profile',
]);
Cache::get('users:1234:profile');
Pulse::store(app(Ingest::class));

$interactions = Pulse::ignore(fn () => DB::table('pulse_cache_interactions')->get());
expect($interactions)->toHaveCount(1);
expect((array) $interactions[0])->toEqual([
'date' => '2000-01-02 03:04:05',
'user_id' => null,
'key' => 'users:{user}:profile',
'hit' => 0,
]);
});

it('can use back references in normalized cache keys', function () {
Carbon::setTestNow('2000-01-02 03:04:05');

Config::set('pulse.cache_keys', [
'/^([^:]+):([^:]+):baz/' => '\2:\1',
]);
Cache::get('foo:bar:baz');
Pulse::store(app(Ingest::class));

$interactions = Pulse::ignore(fn () => DB::table('pulse_cache_interactions')->get());
expect($interactions)->toHaveCount(1);
expect((array) $interactions[0])->toEqual([
'date' => '2000-01-02 03:04:05',
'user_id' => null,
'key' => 'bar:foo',
'hit' => 0,
]);
});

it('uses the original key if no matching pattern is found', function () {
Carbon::setTestNow('2000-01-02 03:04:05');

Config::set('pulse.cache_keys', [
'/\d/' => 'foo',
]);
Cache::get('actual-key');
Pulse::store(app(Ingest::class));

$interactions = Pulse::ignore(fn () => DB::table('pulse_cache_interactions')->get());
expect($interactions)->toHaveCount(1);
expect((array) $interactions[0])->toEqual([
'date' => '2000-01-02 03:04:05',
'user_id' => null,
'key' => 'actual-key',
'hit' => 0,
]);
});

it('can provide regex flags in normalization key', function () {
Carbon::setTestNow('2000-01-02 03:04:05');

Config::set('pulse.cache_keys', [
'/foo/i' => 'lowercase-key',
'/FOO/i' => 'uppercase-key',
]);
Cache::get('FOO');
Pulse::store(app(Ingest::class));

$interactions = Pulse::ignore(fn () => DB::table('pulse_cache_interactions')->get());
expect($interactions)->toHaveCount(1);
expect((array) $interactions[0])->toEqual([
'date' => '2000-01-02 03:04:05',
'user_id' => null,
'key' => 'lowercase-key',
'hit' => 0,
]);
});

0 comments on commit 6842f70

Please sign in to comment.