Enable Discovery debug logging for production troubleshooting
- Add DISCOVERY_LOG_LEVEL=debug - Add DISCOVERY_SHOW_PROGRESS=true - Temporary changes for debugging InitializerProcessor fixes on production
This commit is contained in:
@@ -0,0 +1,316 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Analytics\Storage;
|
||||
|
||||
use App\Framework\Filesystem\AtomicStorage;
|
||||
use App\Framework\Filesystem\Exceptions\FilePermissionException;
|
||||
use App\Framework\Filesystem\Serializer;
|
||||
use App\Framework\Filesystem\Storage;
|
||||
use App\Framework\Performance\Contracts\PerformanceCollectorInterface;
|
||||
|
||||
/**
|
||||
* Analytics-Storage basierend auf dem Performance-System
|
||||
*
|
||||
* Nutzt das Performance-Framework für effiziente Datenspeicherung
|
||||
* und -aggregation von Analytics-Daten.
|
||||
*/
|
||||
final class PerformanceBasedAnalyticsStorage implements AnalyticsStorage
|
||||
{
|
||||
/** @var array<string, array> */
|
||||
private array $aggregatedData = [];
|
||||
|
||||
/** @var array<array> */
|
||||
private array $rawDataBuffer = [];
|
||||
|
||||
private int $bufferSize = 1000;
|
||||
|
||||
public function __construct(
|
||||
private PerformanceCollectorInterface $performanceCollector,
|
||||
private Storage|AtomicStorage $storage,
|
||||
private Serializer $serializer,
|
||||
private string $dataPath = '/tmp/analytics'
|
||||
) {
|
||||
if (! $this->storage->exists($this->dataPath)) {
|
||||
$this->storage->createDirectory($this->dataPath);
|
||||
}
|
||||
}
|
||||
|
||||
public function storeAggregated(string $period, array $data): void
|
||||
{
|
||||
error_log("Analytics: storeAggregated called with period={$period}, data=" . json_encode($data));
|
||||
|
||||
$periodKey = $this->getPeriodKey($period);
|
||||
|
||||
if (! isset($this->aggregatedData[$periodKey])) {
|
||||
$this->aggregatedData[$periodKey] = [];
|
||||
}
|
||||
|
||||
// Merge new data with existing data
|
||||
foreach ($data as $key => $value) {
|
||||
if (isset($this->aggregatedData[$periodKey][$key])) {
|
||||
if (is_numeric($value)) {
|
||||
$this->aggregatedData[$periodKey][$key] += $value;
|
||||
} else {
|
||||
$this->aggregatedData[$periodKey][$key] = $value;
|
||||
}
|
||||
} else {
|
||||
$this->aggregatedData[$periodKey][$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
// Persist to file every 10 updates (for better responsiveness in development)
|
||||
// Also persist immediately for small amounts to ensure data is not lost
|
||||
if (count($this->aggregatedData) % 10 === 0 || count($this->aggregatedData) <= 5) {
|
||||
$this->persistAggregatedData();
|
||||
}
|
||||
}
|
||||
|
||||
public function storeRawData(array $data, float $samplingRate = 0.1): void
|
||||
{
|
||||
error_log("Analytics: storeRawData called with data=" . json_encode($data) . ", samplingRate={$samplingRate}");
|
||||
|
||||
// Apply sampling
|
||||
if ($samplingRate < 1.0 && (random_int(1, 100) / 100) > $samplingRate) {
|
||||
error_log("Analytics: Data filtered out by sampling");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->rawDataBuffer[] = array_merge($data, [
|
||||
'timestamp' => time(),
|
||||
'sampled' => $samplingRate < 1.0,
|
||||
]);
|
||||
|
||||
// Flush buffer when full
|
||||
if (count($this->rawDataBuffer) >= $this->bufferSize) {
|
||||
$this->flushRawDataBuffer();
|
||||
}
|
||||
}
|
||||
|
||||
public function getAggregated(string $startDate, string $endDate, string $period = 'hour'): array
|
||||
{
|
||||
$this->loadAggregatedData();
|
||||
|
||||
$result = [];
|
||||
$start = strtotime($startDate . ' 00:00:00');
|
||||
$end = strtotime($endDate . ' 23:59:59');
|
||||
|
||||
foreach ($this->aggregatedData as $periodKey => $data) {
|
||||
$timestamp = $this->parseTimestampFromPeriodKey($periodKey, $period);
|
||||
|
||||
if ($timestamp >= $start && $timestamp <= $end) {
|
||||
$result[$periodKey] = $data;
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function getTopList(string $metric, string $startDate, string $endDate, int $limit = 10): array
|
||||
{
|
||||
$aggregated = $this->getAggregated($startDate, $endDate);
|
||||
$topList = [];
|
||||
|
||||
foreach ($aggregated as $period => $data) {
|
||||
if (isset($data[$metric])) {
|
||||
$value = $data[$metric];
|
||||
|
||||
if (is_array($value)) {
|
||||
foreach ($value as $key => $count) {
|
||||
if (! isset($topList[$key])) {
|
||||
$topList[$key] = 0;
|
||||
}
|
||||
$topList[$key] += $count;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
arsort($topList);
|
||||
|
||||
return array_slice($topList, 0, $limit, true);
|
||||
}
|
||||
|
||||
public function getTimeSeries(string $metric, string $startDate, string $endDate, string $interval = 'hour'): array
|
||||
{
|
||||
$aggregated = $this->getAggregated($startDate, $endDate, $interval);
|
||||
$timeSeries = [];
|
||||
|
||||
foreach ($aggregated as $period => $data) {
|
||||
$timestamp = $this->parseTimestampFromPeriodKey($period, $interval);
|
||||
$value = $data[$metric] ?? 0;
|
||||
|
||||
if (is_array($value)) {
|
||||
$value = array_sum($value);
|
||||
}
|
||||
|
||||
$timeSeries[] = [
|
||||
'timestamp' => $timestamp,
|
||||
'value' => $value,
|
||||
'period' => $period,
|
||||
];
|
||||
}
|
||||
|
||||
// Sort by timestamp
|
||||
usort($timeSeries, fn ($a, $b) => $a['timestamp'] <=> $b['timestamp']);
|
||||
|
||||
return $timeSeries;
|
||||
}
|
||||
|
||||
public function cleanup(int $retentionDays): int
|
||||
{
|
||||
$cutoffTime = time() - ($retentionDays * 24 * 3600);
|
||||
$removed = 0;
|
||||
|
||||
// Clean aggregated data
|
||||
foreach ($this->aggregatedData as $periodKey => $data) {
|
||||
$timestamp = $this->parseTimestampFromPeriodKey($periodKey, 'hour');
|
||||
|
||||
if ($timestamp < $cutoffTime) {
|
||||
unset($this->aggregatedData[$periodKey]);
|
||||
$removed++;
|
||||
}
|
||||
}
|
||||
|
||||
// Clean raw data files
|
||||
$rawDataFiles = $this->storage->listDirectory($this->dataPath);
|
||||
foreach ($rawDataFiles as $file) {
|
||||
if (str_starts_with(basename($file), 'raw_') &&
|
||||
str_ends_with($file, '.json') &&
|
||||
$this->storage->lastModified($file) < $cutoffTime) {
|
||||
$this->storage->delete($file);
|
||||
$removed++;
|
||||
}
|
||||
}
|
||||
|
||||
$this->persistAggregatedData();
|
||||
|
||||
return $removed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert einen Period-Key für Aggregation
|
||||
*/
|
||||
private function getPeriodKey(string $period): string
|
||||
{
|
||||
$timestamp = time();
|
||||
|
||||
return match ($period) {
|
||||
'minute' => date('Y-m-d_H:i', $timestamp),
|
||||
'hour' => date('Y-m-d_H', $timestamp),
|
||||
'day' => date('Y-m-d', $timestamp),
|
||||
'week' => date('Y-W', $timestamp),
|
||||
'month' => date('Y-m', $timestamp),
|
||||
default => date('Y-m-d_H', $timestamp)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parst Timestamp aus Period-Key
|
||||
*/
|
||||
private function parseTimestampFromPeriodKey(string $periodKey, string $period): int
|
||||
{
|
||||
$timestamp = match ($period) {
|
||||
'minute' => strtotime($periodKey . ':00'),
|
||||
'hour' => strtotime($periodKey . ':00:00'),
|
||||
'day' => strtotime($periodKey . ' 00:00:00'),
|
||||
'week' => strtotime($periodKey . '-1'), // Monday of week
|
||||
'month' => strtotime($periodKey . '-01 00:00:00'),
|
||||
default => strtotime($periodKey . ':00:00')
|
||||
};
|
||||
|
||||
// Handle invalid date formats
|
||||
if ($timestamp === false) {
|
||||
// Return current timestamp as fallback
|
||||
return time();
|
||||
}
|
||||
|
||||
return $timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Persistiert aggregierte Daten
|
||||
*/
|
||||
private function persistAggregatedData(): void
|
||||
{
|
||||
error_log("Analytics: persistAggregatedData called - persisting " . count($this->aggregatedData) . " items");
|
||||
$filename = $this->dataPath . '/aggregated_' . date('Y-m-d') . '.' . $this->serializer->getFileExtension();
|
||||
$content = $this->serializer->serialize($this->aggregatedData);
|
||||
|
||||
try {
|
||||
if ($this->storage instanceof AtomicStorage) {
|
||||
$this->storage->putAtomic($filename, $content);
|
||||
} else {
|
||||
$this->storage->put($filename, $content);
|
||||
}
|
||||
} catch (FilePermissionException $e) {
|
||||
error_log("Analytics: Failed to persist aggregated data due to permissions: " . $e->getMessage());
|
||||
// Continue gracefully - data will remain in memory for this request
|
||||
} catch (\Exception $e) {
|
||||
error_log("Analytics: Failed to persist aggregated data: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lädt aggregierte Daten
|
||||
*/
|
||||
private function loadAggregatedData(): void
|
||||
{
|
||||
if (! empty($this->aggregatedData)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$files = $this->storage->listDirectory($this->dataPath);
|
||||
foreach ($files as $file) {
|
||||
if (str_starts_with(basename($file), 'aggregated_') &&
|
||||
str_ends_with($file, '.' . $this->serializer->getFileExtension()) &&
|
||||
$this->storage->exists($file)) {
|
||||
$content = $this->storage->get($file);
|
||||
$data = $this->serializer->deserialize($content);
|
||||
if (is_array($data)) {
|
||||
$this->aggregatedData = array_merge($this->aggregatedData, $data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Leert den Raw-Data Buffer
|
||||
*/
|
||||
private function flushRawDataBuffer(): void
|
||||
{
|
||||
if (empty($this->rawDataBuffer)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$filename = $this->dataPath . '/raw_' . date('Y-m-d_H-i-s') . '_' . uniqid() . '.' . $this->serializer->getFileExtension();
|
||||
$content = $this->serializer->serialize($this->rawDataBuffer);
|
||||
|
||||
try {
|
||||
if ($this->storage instanceof AtomicStorage) {
|
||||
$this->storage->putAtomic($filename, $content);
|
||||
} else {
|
||||
$this->storage->put($filename, $content);
|
||||
}
|
||||
$this->rawDataBuffer = [];
|
||||
} catch (FilePermissionException $e) {
|
||||
error_log("Analytics: Failed to flush raw data buffer due to permissions: " . $e->getMessage());
|
||||
// Keep data in buffer for potential retry later
|
||||
} catch (\Exception $e) {
|
||||
error_log("Analytics: Failed to flush raw data buffer: " . $e->getMessage());
|
||||
// Clear buffer to prevent memory buildup
|
||||
$this->rawDataBuffer = [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Destructor um Buffer zu leeren
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
$this->flushRawDataBuffer();
|
||||
$this->persistAggregatedData();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user