Skip to content

Commit 1f6b72f

Browse files
committed
FEATURE: Add command line command to read statistics events
1 parent ef8706e commit 1f6b72f

3 files changed

Lines changed: 181 additions & 11 deletions

File tree

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Flowpack\DecoupledContentStore\Command;
5+
6+
use Flowpack\DecoupledContentStore\Core\Domain\ValueObject\PrunnerJobId;
7+
use Flowpack\DecoupledContentStore\Core\Infrastructure\RedisStatisticsEventService;
8+
use Neos\Flow\Annotations as Flow;
9+
use Flowpack\DecoupledContentStore\Core\Domain\ValueObject\ContentReleaseIdentifier;
10+
use Flowpack\DecoupledContentStore\Core\Infrastructure\ContentReleaseLogger;
11+
use Neos\Flow\Cli\CommandController;
12+
13+
/**
14+
* Commands to read statistics events for a content release from redis
15+
*/
16+
class ContentReleaseEventsCommandController extends CommandController
17+
{
18+
#[Flow\Inject]
19+
protected RedisStatisticsEventService $redisStatisticsEventService;
20+
21+
/**
22+
* Count statistics events in a content release.
23+
*
24+
* This command will count how many statics events with given filters, grouped by the specified keys exist in a content release.
25+
*
26+
* Use --where to filter the events (e.g. "--where=event=title") and --groupBy to count events separately
27+
* (e.g. "--groupBy=additionalPayload.preset"). You can specify multiple filters and groups by separating them with
28+
* ',' (e.g "--groupBy=additionalPayloads.preset,additionalPayloads.documentId")
29+
*
30+
* Do not use "--where event=title"! Flow will remove "event=" and the filter will not be applied.
31+
*
32+
* @param string $contentReleaseIdentifier The contentReleaseIdentifier which statistics should be counted
33+
* @param string $where filter the events before counting
34+
* @param string $groupBy group the events by this value into separately counted groups
35+
* @return void
36+
*/
37+
public function countStatisticsEventCommand(string $contentReleaseIdentifier, string $where = '', string $groupBy = ''): void
38+
{
39+
$contentReleaseIdentifier = ContentReleaseIdentifier::fromString($contentReleaseIdentifier);
40+
// split every string in $where by the first '=' and use the left part as key and the right part as value
41+
$where = $where ? array_column(array_map(fn($s) => explode('=', $s, 2), explode(',', $where)), 1, 0) : [];
42+
$groupBy = $groupBy ? explode(',', $groupBy) : [];
43+
44+
$this->output("Filters: \n");
45+
if($where) {
46+
foreach ($where as $key=>$value) {
47+
$this->output(" $key = \"$value\"\n");
48+
}
49+
} else {
50+
$this->output(" None \n");
51+
}
52+
53+
$eventCounts = $this->redisStatisticsEventService->countEvents($contentReleaseIdentifier, $where, $groupBy);
54+
$this->output->outputTable($eventCounts, array_merge(['count'], $groupBy));
55+
56+
$this->output("Total: %d\n", [array_sum(array_map(fn($e) => $e['count'], $eventCounts))]);
57+
}
58+
}

Classes/Core/Infrastructure/RedisStatisticsEventOutput.php

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,15 @@
44
namespace Flowpack\DecoupledContentStore\Core\Infrastructure;
55

66
use Flowpack\DecoupledContentStore\Core\Domain\ValueObject\ContentReleaseIdentifier;
7-
use Flowpack\DecoupledContentStore\Core\RedisKeyService;
87
use Neos\Flow\Annotations as Flow;
98

109
class RedisStatisticsEventOutput implements StatisticsEventOutputInterface
1110
{
12-
13-
#[Flow\Inject]
14-
protected RedisClientManager $redisClientManager;
15-
1611
#[Flow\Inject]
17-
protected RedisKeyService $redisKeyService;
12+
protected RedisStatisticsEventService $redisStatisticsEventService;
1813

1914
public function writeEvent(ContentReleaseIdentifier $contentReleaseIdentifier, string $prefix, string $event, array $additionalPayload): void
2015
{
21-
$this->redisClientManager->getPrimaryRedis()->rPush($this->redisKeyService->getRedisKeyForPostfix($contentReleaseIdentifier, 'statisticsEvents'), json_encode([
22-
'event' => $event,
23-
'prefix' => $prefix,
24-
'additionalPayload' => $additionalPayload,
25-
]));
16+
$this->redisStatisticsEventService->addEvent($contentReleaseIdentifier, $prefix, $event, $additionalPayload);
2617
}
2718
}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Flowpack\DecoupledContentStore\Core\Infrastructure;
5+
6+
use Flowpack\DecoupledContentStore\Core\Domain\ValueObject\ContentReleaseIdentifier;
7+
use Flowpack\DecoupledContentStore\Core\RedisKeyService;
8+
use Flowpack\DecoupledContentStore\Exception;
9+
use Neos\Flow\Annotations as Flow;
10+
11+
#[Flow\Scope("singleton")]
12+
class RedisStatisticsEventService
13+
{
14+
#[Flow\Inject]
15+
protected RedisClientManager $redisClientManager;
16+
17+
#[Flow\Inject]
18+
protected RedisKeyService $redisKeyService;
19+
20+
public function addEvent(ContentReleaseIdentifier $contentReleaseIdentifier, string $prefix, string $event, array $additionalPayload): void
21+
{
22+
$this->redisClientManager->getPrimaryRedis()->rPush($this->redisKeyService->getRedisKeyForPostfix($contentReleaseIdentifier, 'statisticsEvents'), json_encode([
23+
'event' => $event,
24+
'prefix' => $prefix,
25+
'additionalPayload' => $additionalPayload,
26+
]));
27+
}
28+
29+
/**
30+
* @param ContentReleaseIdentifier $contentReleaseIdentifier
31+
* @param array<string,string> $where
32+
* @param string[] $groupBy
33+
* @return array<>
34+
* @throws Exception
35+
*/
36+
public function countEvents(
37+
ContentReleaseIdentifier $contentReleaseIdentifier,
38+
array $where,
39+
array $groupBy,
40+
): array
41+
{
42+
$redis = $this->redisClientManager->getPrimaryRedis();
43+
$key = $this->redisKeyService->getRedisKeyForPostfix($contentReleaseIdentifier, 'statisticsEvents');
44+
$chunkSize = 1000;
45+
46+
$countedEvents = [];
47+
48+
$listLength = $redis->lLen($key);
49+
for ($start = 0; $start < $listLength; $start += $chunkSize) {
50+
$events = $redis->lRange($key, $start, $start + $chunkSize - 1);
51+
52+
foreach ($events as $eventJson) {
53+
$event = $this->flatten(json_decode($eventJson, true));
54+
if($this->shouldCount($event, $where)) {
55+
$group = $this->groupValues($event, $groupBy);
56+
$eventKey = json_encode($group);
57+
if (array_key_exists($eventKey, $countedEvents)) {
58+
$countedEvents[$eventKey]['count'] += 1;
59+
} else {
60+
$countedEvents[$eventKey] = array_merge(['count' => 1], $group);
61+
}
62+
}
63+
}
64+
}
65+
// throw away the keys and sort in _reverse_ order by count
66+
usort($countedEvents, fn($a, $b) => $b['count'] - $a['count']);
67+
return $countedEvents;
68+
}
69+
70+
/**
71+
* @phpstan-type JSONArray array<string, string|JSONArray>
72+
73+
* @param JSONArray $array
74+
* @return array<string,string>
75+
*/
76+
private function flatten(array $array): array
77+
{
78+
$results = [];
79+
80+
foreach ($array as $key => $value) {
81+
if (is_array($value) && ! empty($value)) {
82+
foreach ($this->flatten($value) as $subKey => $subValue) {
83+
$results[$key . '.' . $subKey] = $subValue;
84+
}
85+
} else {
86+
$results[$key] = $value;
87+
}
88+
}
89+
90+
return $results;
91+
}
92+
93+
/**
94+
* @param array<string,string> $event
95+
* @param array<string,string> $where
96+
* @return bool
97+
*/
98+
private function shouldCount(array $event, array $where): bool
99+
{
100+
foreach ($where as $key=>$value) {
101+
if (!array_key_exists($key, $event) || $event[$key] !== $value) {
102+
return false;
103+
}
104+
}
105+
return true;
106+
}
107+
108+
/**
109+
* @param array<string,string> $event
110+
* @param string[] $groupedBy
111+
* @return array<string,string>
112+
*/
113+
private function groupValues(array $event, array $groupedBy): array
114+
{
115+
$group = [];
116+
foreach ($groupedBy as $path) {
117+
$group[$path] = $event[$path] ?? null;
118+
}
119+
return $group;
120+
}
121+
}

0 commit comments

Comments
 (0)