Skip to content

Commit d0ba7ab

Browse files
authored
Feat: Add Redis watcher (#293)
1 parent 785e797 commit d0ba7ab

File tree

4 files changed

+216
-0
lines changed

4 files changed

+216
-0
lines changed

src/Hooks/Illuminate/Foundation/Application.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
use OpenTelemetry\Contrib\Instrumentation\Laravel\Watchers\ExceptionWatcher;
1414
use OpenTelemetry\Contrib\Instrumentation\Laravel\Watchers\LogWatcher;
1515
use OpenTelemetry\Contrib\Instrumentation\Laravel\Watchers\QueryWatcher;
16+
use OpenTelemetry\Contrib\Instrumentation\Laravel\Watchers\RedisCommand\RedisCommandWatcher;
1617
use OpenTelemetry\Contrib\Instrumentation\Laravel\Watchers\Watcher;
1718
use function OpenTelemetry\Instrumentation\hook;
1819
use Throwable;
@@ -32,6 +33,7 @@ public function instrument(): void
3233
$this->registerWatchers($application, new ExceptionWatcher());
3334
$this->registerWatchers($application, new LogWatcher($this->instrumentation));
3435
$this->registerWatchers($application, new QueryWatcher($this->instrumentation));
36+
$this->registerWatchers($application, new RedisCommandWatcher($this->instrumentation));
3537
},
3638
);
3739
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace OpenTelemetry\Contrib\Instrumentation\Laravel\Watchers\RedisCommand;
6+
7+
use Illuminate\Contracts\Foundation\Application;
8+
use Illuminate\Redis\Connections\Connection;
9+
use Illuminate\Redis\Connections\PhpRedisConnection;
10+
use Illuminate\Redis\Connections\PredisConnection;
11+
use Illuminate\Redis\Events\CommandExecuted;
12+
use OpenTelemetry\API\Instrumentation\CachedInstrumentation;
13+
use OpenTelemetry\API\Trace\SpanKind;
14+
use OpenTelemetry\Contrib\Instrumentation\Laravel\Watchers\Watcher;
15+
use OpenTelemetry\SemConv\TraceAttributes;
16+
use OpenTelemetry\SemConv\TraceAttributeValues;
17+
use Throwable;
18+
19+
/**
20+
* Watch the Redis Command event
21+
*
22+
* Call facade `Redis::enableEvents()` before using this watcher
23+
*/
24+
class RedisCommandWatcher extends Watcher
25+
{
26+
public function __construct(
27+
private CachedInstrumentation $instrumentation,
28+
) {
29+
}
30+
31+
/** @psalm-suppress UndefinedInterfaceMethod */
32+
public function register(Application $app): void
33+
{
34+
/** @phan-suppress-next-line PhanTypeArraySuspicious */
35+
$app['events']->listen(CommandExecuted::class, [$this, 'recordRedisCommand']);
36+
}
37+
38+
/**
39+
* Record a Redis command.
40+
*/
41+
/** @psalm-suppress UndefinedThisPropertyFetch */
42+
public function recordRedisCommand(CommandExecuted $event): void
43+
{
44+
$nowInNs = (int) (microtime(true) * 1E9);
45+
46+
$operationName = strtoupper($event->command);
47+
48+
/** @psalm-suppress ArgumentTypeCoercion */
49+
$span = $this->instrumentation->tracer()
50+
->spanBuilder($operationName)
51+
->setSpanKind(SpanKind::KIND_CLIENT)
52+
->setStartTimestamp($this->calculateQueryStartTime($nowInNs, $event->time))
53+
->startSpan();
54+
55+
// See https://opentelemetry.io/docs/specs/semconv/database/redis/
56+
$attributes = [
57+
TraceAttributes::DB_SYSTEM => TraceAttributeValues::DB_SYSTEM_REDIS,
58+
TraceAttributes::DB_NAME => $this->fetchDbIndex($event->connection),
59+
TraceAttributes::DB_OPERATION => $operationName,
60+
TraceAttributes::DB_STATEMENT => Serializer::serializeCommand($event->command, $event->parameters),
61+
TraceAttributes::SERVER_ADDRESS => $this->fetchDbHost($event->connection),
62+
];
63+
64+
/** @psalm-suppress PossiblyInvalidArgument */
65+
$span->setAttributes($attributes);
66+
$span->end($nowInNs);
67+
}
68+
69+
private function calculateQueryStartTime(int $nowInNs, float $queryTimeMs): int
70+
{
71+
return (int) ($nowInNs - ($queryTimeMs * 1E6));
72+
}
73+
74+
private function fetchDbIndex(Connection $connection): ?int
75+
{
76+
try {
77+
if ($connection instanceof PhpRedisConnection) {
78+
return $connection->client()->getDbNum();
79+
} elseif ($connection instanceof PredisConnection) {
80+
/** @psalm-suppress PossiblyUndefinedMethod */
81+
return $connection->client()->getConnection()->getParameters()->database;
82+
}
83+
84+
return null;
85+
} catch (Throwable $e) {
86+
return null;
87+
}
88+
}
89+
90+
private function fetchDbHost(Connection $connection): ?string
91+
{
92+
try {
93+
if ($connection instanceof PhpRedisConnection) {
94+
return $connection->client()->getHost();
95+
} elseif ($connection instanceof PredisConnection) {
96+
/** @psalm-suppress PossiblyUndefinedMethod */
97+
return $connection->client()->getConnection()->getParameters()->host;
98+
}
99+
100+
return null;
101+
} catch (Throwable $e) {
102+
return null;
103+
}
104+
}
105+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace OpenTelemetry\Contrib\Instrumentation\Laravel\Watchers\RedisCommand;
6+
7+
/**
8+
* @see https://github.com/open-telemetry/opentelemetry-js-contrib/blob/main/packages/opentelemetry-redis-common/src/index.ts
9+
*/
10+
class Serializer
11+
{
12+
/**
13+
* List of regexes and the number of arguments that should be serialized for matching commands.
14+
* For example, HSET should serialize which key and field it's operating on, but not its value.
15+
* Setting the subset to -1 will serialize all arguments.
16+
* Commands without a match will have their first argument serialized.
17+
*
18+
* Refer to https://redis.io/commands/ for the full list.
19+
*/
20+
private const SERIALIZATION_SUBSETS = [
21+
[
22+
'regex' => '/^ECHO/i',
23+
'args' => 0,
24+
],
25+
[
26+
'regex' => '/^(LPUSH|MSET|PFA|PUBLISH|RPUSH|SADD|SET|SPUBLISH|XADD|ZADD)/i',
27+
'args' => 1,
28+
],
29+
[
30+
'regex' => '/^(HSET|HMSET|LSET|LINSERT)/i',
31+
'args' => 2,
32+
],
33+
[
34+
'regex' => '/^(ACL|BIT|B[LRZ]|CLIENT|CLUSTER|CONFIG|COMMAND|DECR|DEL|EVAL|EX|FUNCTION|GEO|GET|HINCR|HMGET|HSCAN|INCR|L[TRLM]|MEMORY|P[EFISTU]|RPOP|S[CDIMORSU]|XACK|X[CDGILPRT]|Z[CDILMPRS])/i',
35+
'args' => -1,
36+
],
37+
];
38+
39+
/**
40+
* Given the redis command name and arguments, return a combination of the
41+
* command name + the allowed arguments according to `SERIALIZATION_SUBSETS`.
42+
*
43+
* @param string $command The redis command name
44+
* @param array $params The redis command arguments
45+
* @return string A combination of the command name + args according to `SERIALIZATION_SUBSETS`.
46+
*/
47+
public static function serializeCommand(string $command, array $params): string
48+
{
49+
if (count($params) === 0) {
50+
return $command;
51+
}
52+
53+
$paramsToSerializeNum = 0;
54+
55+
// Find the number of arguments to serialize for the given command
56+
foreach (self::SERIALIZATION_SUBSETS as $subset) {
57+
if (preg_match($subset['regex'], $command)) {
58+
$paramsToSerializeNum = $subset['args'];
59+
60+
break;
61+
}
62+
}
63+
64+
// Serialize the allowed number of arguments
65+
$paramsToSerialize = ($paramsToSerializeNum >= 0) ? array_slice($params, 0, $paramsToSerializeNum) : $params;
66+
67+
// If there are more arguments than serialized, add a placeholder
68+
if (count($params) > count($paramsToSerialize)) {
69+
$paramsToSerialize[] = '[' . (count($params) - $paramsToSerializeNum) . ' other arguments]';
70+
}
71+
72+
return $command . ' ' . implode(' ', $paramsToSerialize);
73+
}
74+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace OpenTelemetry\Tests\Contrib\Instrumentation\Laravel\Unit\Watches\RedisCommand;
6+
7+
use OpenTelemetry\Contrib\Instrumentation\Laravel\Watchers\RedisCommand\Serializer;
8+
use PHPUnit\Framework\TestCase;
9+
10+
class SerializerTest extends TestCase
11+
{
12+
/**
13+
* @dataProvider serializeCases
14+
*/
15+
public function testSerialize($command, $params, $expected): void
16+
{
17+
$this->assertSame($expected, Serializer::serializeCommand($command, $params));
18+
}
19+
20+
public function serializeCases(): iterable
21+
{
22+
// Only serialize command
23+
yield ['ECHO', ['param1'], 'ECHO [1 other arguments]'];
24+
25+
// Only serialize 1 params
26+
yield ['SET', ['param1', 'param2'], 'SET param1 [1 other arguments]'];
27+
yield ['SET', ['param1', 'param2', 'param3'], 'SET param1 [2 other arguments]'];
28+
29+
// Only serialize 2 params
30+
yield ['HSET', ['param1', 'param2', 'param3'], 'HSET param1 param2 [1 other arguments]'];
31+
32+
// Serialize all params
33+
yield ['DEL', ['param1', 'param2', 'param3', 'param4'], 'DEL param1 param2 param3 param4'];
34+
}
35+
}

0 commit comments

Comments
 (0)