Skip to content

Commit 1a85134

Browse files
committed
Feat: Add Redis watcher
1 parent 9b97522 commit 1a85134

File tree

4 files changed

+229
-0
lines changed

4 files changed

+229
-0
lines changed

src/Instrumentation/Laravel/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: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
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 Illuminate\Support\Str;
13+
use OpenTelemetry\API\Instrumentation\CachedInstrumentation;
14+
use OpenTelemetry\API\Trace\SpanKind;
15+
use OpenTelemetry\Contrib\Instrumentation\Laravel\Watchers\Watcher;
16+
use OpenTelemetry\SemConv\TraceAttributes;
17+
use OpenTelemetry\SemConv\TraceAttributeValues;
18+
use RangeException;
19+
use RuntimeException;
20+
21+
/**
22+
* Watch the Redis Command event
23+
*
24+
* Call facade `Redis::enableEvents()` before using this watcher
25+
*/
26+
class RedisCommandWatcher extends Watcher
27+
{
28+
public function __construct(
29+
private CachedInstrumentation $instrumentation,
30+
) {
31+
}
32+
33+
/** @psalm-suppress UndefinedInterfaceMethod */
34+
public function register(Application $app): void
35+
{
36+
/** @phan-suppress-next-line PhanTypeArraySuspicious */
37+
$app['events']->listen(CommandExecuted::class, [$this, 'recordRedisCommand']);
38+
}
39+
40+
/**
41+
* Record a query.
42+
*/
43+
/** @psalm-suppress UndefinedThisPropertyFetch */
44+
public function recordRedisCommand(CommandExecuted $event): void
45+
{
46+
$nowInNs = (int)(microtime(true) * 1E9);
47+
48+
$operationName = Str::upper($event->command);
49+
50+
/** @psalm-suppress ArgumentTypeCoercion */
51+
$span = $this->instrumentation->tracer()
52+
->spanBuilder($operationName)
53+
->setSpanKind(SpanKind::KIND_CLIENT)
54+
->setStartTimestamp($this->calculateQueryStartTime($nowInNs, $event->time))
55+
->startSpan();
56+
57+
// See https://opentelemetry.io/docs/specs/semconv/database/redis/
58+
$attributes = [
59+
TraceAttributes::DB_SYSTEM => TraceAttributeValues::DB_SYSTEM_REDIS,
60+
TraceAttributes::DB_NAME => $this->fetchDbIndex($event->connection),
61+
TraceAttributes::DB_OPERATION => $operationName,
62+
TraceAttributes::DB_QUERY_TEXT => Serializer::serializeCommand($event->command, $event->parameters),
63+
TraceAttributes::SERVER_ADDRESS => $this->fetchDbHost($event->connection),
64+
];
65+
66+
/** @psalm-suppress PossiblyInvalidArgument */
67+
$span->setAttributes($attributes);
68+
$span->end($nowInNs);
69+
}
70+
71+
private function calculateQueryStartTime(int $nowInNs, float $queryTimeMs): int
72+
{
73+
return (int)($nowInNs - ($queryTimeMs * 1E6));
74+
}
75+
76+
private function fetchDbIndex(Connection $connection): int
77+
{
78+
if ($connection instanceof PhpRedisConnection) {
79+
$index = $connection->client()->getDbNum();
80+
81+
if ($index === false) {
82+
throw new RuntimeException('Cannot fetch database index.');
83+
}
84+
85+
return $index;
86+
} elseif ($connection instanceof PredisConnection) {
87+
/** @psalm-suppress PossiblyUndefinedMethod */
88+
$index = $connection->client()->getConnection()->getParameters()->database;
89+
90+
if (is_int($index)) {
91+
throw new RuntimeException('Cannot fetch database index.');
92+
}
93+
94+
return $index;
95+
} else {
96+
throw new RangeException('Unknown Redis connection instance: ' . get_class($connection));
97+
}
98+
}
99+
100+
private function fetchDbHost(Connection $connection): string
101+
{
102+
if ($connection instanceof PhpRedisConnection) {
103+
$host = $connection->client()->getHost();
104+
105+
if ($host === false) {
106+
throw new RuntimeException('Cannot fetch database host.');
107+
}
108+
109+
return $host;
110+
} elseif ($connection instanceof PredisConnection) {
111+
/** @psalm-suppress PossiblyUndefinedMethod */
112+
$host = $connection->client()->getConnection()->getParameters()->host;
113+
114+
if (is_int($host)) {
115+
throw new RuntimeException('Cannot fetch database index.');
116+
}
117+
118+
return $host;
119+
} else {
120+
throw new RangeException('Unknown Redis connection instance: ' . get_class($connection));
121+
}
122+
}
123+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
<?php
2+
3+
namespace OpenTelemetry\Contrib\Instrumentation\Laravel\Watchers\RedisCommand;
4+
5+
/**
6+
* @see https://github.com/open-telemetry/opentelemetry-js-contrib/blob/main/packages/opentelemetry-redis-common/src/index.ts
7+
*/
8+
class Serializer
9+
{
10+
/**
11+
* List of regexes and the number of arguments that should be serialized for matching commands.
12+
* For example, HSET should serialize which key and field it's operating on, but not its value.
13+
* Setting the subset to -1 will serialize all arguments.
14+
* Commands without a match will have their first argument serialized.
15+
*
16+
* Refer to https://redis.io/commands/ for the full list.
17+
*/
18+
private const SERIALIZATION_SUBSETS = [
19+
[
20+
'regex' => '/^ECHO/i',
21+
'args' => 0,
22+
],
23+
[
24+
'regex' => '/^(LPUSH|MSET|PFA|PUBLISH|RPUSH|SADD|SET|SPUBLISH|XADD|ZADD)/i',
25+
'args' => 1,
26+
],
27+
[
28+
'regex' => '/^(HSET|HMSET|LSET|LINSERT)/i',
29+
'args' => 2,
30+
],
31+
[
32+
'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',
33+
'args' => -1,
34+
],
35+
];
36+
37+
/**
38+
* Given the redis command name and arguments, return a combination of the
39+
* command name + the allowed arguments according to `SERIALIZATION_SUBSETS`.
40+
*
41+
* @param string $command The redis command name
42+
* @param array $params The redis command arguments
43+
* @return string A combination of the command name + args according to `SERIALIZATION_SUBSETS`.
44+
*/
45+
public static function serializeCommand(string $command, array $params): string
46+
{
47+
if (count($params) === 0) {
48+
return $command;
49+
}
50+
51+
$paramsToSerializeNum = 0;
52+
53+
// Find the number of arguments to serialize for the given command
54+
foreach (self::SERIALIZATION_SUBSETS as $subset) {
55+
if (preg_match($subset['regex'], $command)) {
56+
$paramsToSerializeNum = $subset['args'];
57+
break;
58+
}
59+
}
60+
61+
// Serialize the allowed number of arguments
62+
$paramsWillToSerialize = ($paramsToSerializeNum >= 0) ? array_slice($params, 0, $paramsToSerializeNum) : $params;
63+
64+
// If there are more arguments than serialized, add a placeholder
65+
if (count($params) > count($paramsWillToSerialize)) {
66+
$paramsWillToSerialize[] = '[' . (count($params) - $paramsToSerializeNum) . ' other arguments]';
67+
}
68+
69+
return $command . ' ' . implode(' ', $paramsWillToSerialize);
70+
}
71+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
namespace OpenTelemetry\Tests\Contrib\Instrumentation\Laravel\Unit\Watches\RedisCommand;
4+
5+
use OpenTelemetry\Contrib\Instrumentation\Laravel\Watchers\RedisCommand\Serializer;
6+
use PHPUnit\Framework\TestCase;
7+
8+
class SerializerTest extends TestCase
9+
{
10+
/**
11+
* @dataProvider serializeCases
12+
*/
13+
public function testSerialize($command, $params, $expected): void
14+
{
15+
$this->assertSame($expected, Serializer::serializeCommand($command, $params));
16+
}
17+
18+
public function serializeCases(): iterable
19+
{
20+
// Only serialize command
21+
yield ['ECHO', ['param1'], 'ECHO [1 other arguments]'];
22+
23+
// Only serialize 1 params
24+
yield ['SET', ['param1', 'param2'], 'SET param1 [1 other arguments]'];
25+
yield ['SET', ['param1', 'param2', 'param3'], 'SET param1 [2 other arguments]'];
26+
27+
// Only serialize 2 params
28+
yield ['HSET', ['param1', 'param2', 'param3'], 'HSET param1 param2 [1 other arguments]'];
29+
30+
// Serialize all params
31+
yield ['DEL', ['param1', 'param2', 'param3', 'param4'], 'DEL param1 param2 param3 param4'];
32+
}
33+
}

0 commit comments

Comments
 (0)