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:
41
src/Framework/Analytics/Storage/AnalyticsStorage.php
Normal file
41
src/Framework/Analytics/Storage/AnalyticsStorage.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Analytics\Storage;
|
||||
|
||||
/**
|
||||
* Interface für Analytics-Datenspeicherung
|
||||
*/
|
||||
interface AnalyticsStorage
|
||||
{
|
||||
/**
|
||||
* Speichert aggregierte Analytics-Daten
|
||||
*/
|
||||
public function storeAggregated(string $period, array $data): void;
|
||||
|
||||
/**
|
||||
* Speichert Rohdaten (mit Sampling)
|
||||
*/
|
||||
public function storeRawData(array $data, float $samplingRate = 0.1): void;
|
||||
|
||||
/**
|
||||
* Ruft aggregierte Daten ab
|
||||
*/
|
||||
public function getAggregated(string $startDate, string $endDate, string $period = 'hour'): array;
|
||||
|
||||
/**
|
||||
* Ruft Top-Listen ab (z.B. meistbesuchte Seiten)
|
||||
*/
|
||||
public function getTopList(string $metric, string $startDate, string $endDate, int $limit = 10): array;
|
||||
|
||||
/**
|
||||
* Ruft Zeitreihen-Daten ab
|
||||
*/
|
||||
public function getTimeSeries(string $metric, string $startDate, string $endDate, string $interval = 'hour'): array;
|
||||
|
||||
/**
|
||||
* Bereinigt alte Daten
|
||||
*/
|
||||
public function cleanup(int $retentionDays): int;
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Analytics\Storage;
|
||||
|
||||
interface AnalyticsStorageInterface
|
||||
{
|
||||
public function store(array $eventData): void;
|
||||
|
||||
public function query(array $filters = []): array;
|
||||
|
||||
public function aggregate(string $metric, array $filters = []): array;
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Analytics\Storage;
|
||||
|
||||
final class FileAnalyticsStorage implements AnalyticsStorageInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly string $storagePath
|
||||
) {
|
||||
if (!is_dir($this->storagePath)) {
|
||||
mkdir($this->storagePath, 0755, true);
|
||||
}
|
||||
}
|
||||
|
||||
public function store(array $eventData): void
|
||||
{
|
||||
$date = date('Y-m-d');
|
||||
$filename = $this->storagePath . "/analytics-{$date}.jsonl";
|
||||
|
||||
$line = json_encode($eventData) . "\n";
|
||||
file_put_contents($filename, $line, FILE_APPEND | LOCK_EX);
|
||||
}
|
||||
|
||||
public function query(array $filters = []): array
|
||||
{
|
||||
// Einfache Implementation - kann erweitert werden
|
||||
$files = glob($this->storagePath . '/analytics-*.jsonl');
|
||||
$events = [];
|
||||
|
||||
foreach ($files as $file) {
|
||||
$lines = file($file, FILE_IGNORE_NEW_LINES);
|
||||
foreach ($lines as $line) {
|
||||
$event = json_decode($line, true);
|
||||
if ($this->matchesFilters($event, $filters)) {
|
||||
$events[] = $event;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $events;
|
||||
}
|
||||
|
||||
public function aggregate(string $metric, array $filters = []): array
|
||||
{
|
||||
$events = $this->query($filters);
|
||||
|
||||
// Einfache Aggregation - kann erweitert werden
|
||||
$aggregated = [];
|
||||
foreach ($events as $event) {
|
||||
$key = $event['type'] ?? 'unknown';
|
||||
$aggregated[$key] = ($aggregated[$key] ?? 0) + 1;
|
||||
}
|
||||
|
||||
return $aggregated;
|
||||
}
|
||||
|
||||
private function matchesFilters(array $event, array $filters): bool
|
||||
{
|
||||
foreach ($filters as $key => $value) {
|
||||
if (!isset($event[$key]) || $event[$key] !== $value) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -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