feat: CI/CD pipeline setup complete - Ansible playbooks updated, secrets configured, workflow ready

This commit is contained in:
2025-10-31 01:39:24 +01:00
parent 55c04e4fd0
commit e26eb2aa12
601 changed files with 44184 additions and 32477 deletions

View File

@@ -0,0 +1,231 @@
<?php
declare(strict_types=1);
namespace App\Framework\ErrorReporting;
use App\Framework\Config\Environment;
use App\Framework\Config\EnvironmentType;
use App\Framework\Config\EnvKey;
/**
* Error Reporting Configuration that adapts to environment
*
* Provides type-safe configuration for error reporting system
* with environment-specific defaults (dev/staging/production).
*
* Features:
* - Environment-based factory methods
* - Type-safe configuration properties
* - Framework-compliant readonly Value Object
* - Sensible defaults per environment
*/
final readonly class ErrorReportingConfig
{
/**
* @param bool $enabled Enable/disable error reporting globally
* @param bool $asyncProcessing Process error reports asynchronously via queue
* @param string[] $filterLevels Only report these log levels (empty = all levels)
* @param string[] $excludedExceptionTypes Exception types to exclude from reporting
* @param bool $captureRequestContext Capture HTTP request context
* @param bool $captureUserContext Capture user/session context
* @param bool $captureStackTraces Include full stack traces in reports
* @param int $maxStackTraceDepth Maximum stack trace depth
* @param bool $sanitizeSensitiveData Sanitize passwords, tokens, etc.
* @param int $samplingRate Sampling rate for high-volume errors (1-100, 100 = all)
* @param int $maxReportsPerMinute Rate limit for error reports
* @param bool $enableAnalytics Enable error analytics and anomaly detection
* @param int $analyticsRetentionDays Days to retain error reports for analytics
*/
public function __construct(
public bool $enabled = true,
public bool $asyncProcessing = true,
public array $filterLevels = [],
public array $excludedExceptionTypes = [],
public bool $captureRequestContext = true,
public bool $captureUserContext = true,
public bool $captureStackTraces = true,
public int $maxStackTraceDepth = 20,
public bool $sanitizeSensitiveData = true,
public int $samplingRate = 100,
public int $maxReportsPerMinute = 60,
public bool $enableAnalytics = true,
public int $analyticsRetentionDays = 30
) {
// Validation
if ($samplingRate < 1 || $samplingRate > 100) {
throw new \InvalidArgumentException('Sampling rate must be between 1 and 100');
}
if ($maxStackTraceDepth < 1) {
throw new \InvalidArgumentException('Max stack trace depth must be at least 1');
}
if ($analyticsRetentionDays < 1) {
throw new \InvalidArgumentException('Analytics retention days must be at least 1');
}
}
/**
* Create configuration from environment
*/
public static function fromEnvironment(Environment $env): self
{
$appEnv = $env->getString(EnvKey::APP_ENV, 'production');
$environmentType = match (strtolower($appEnv)) {
'production', 'prod' => EnvironmentType::PROD,
'staging', 'stage' => EnvironmentType::STAGING,
'development', 'dev', 'local' => EnvironmentType::DEV,
default => EnvironmentType::PROD
};
return self::forEnvironment($environmentType, $env);
}
/**
* Create environment-specific configuration
*/
public static function forEnvironment(EnvironmentType $environment, Environment $env): self
{
return match ($environment) {
EnvironmentType::PROD => self::production($env),
EnvironmentType::STAGING => self::staging($env),
EnvironmentType::DEV => self::development($env),
};
}
/**
* Production configuration - strict, sanitized, sampled
*/
private static function production(Environment $env): self
{
$filterLevels = $env->getString('ERROR_REPORTING_FILTER_LEVELS', '');
$excludedTypes = $env->getString('ERROR_REPORTING_EXCLUDED_TYPES', '');
// Production default: only error, critical, alert, emergency
$defaultFilterLevels = ['error', 'critical', 'alert', 'emergency'];
return new self(
enabled: $env->getBool('ERROR_REPORTING_ENABLED', true),
asyncProcessing: $env->getBool('ERROR_REPORTING_ASYNC', true),
filterLevels: $filterLevels !== ''
? explode(',', $filterLevels)
: $defaultFilterLevels,
excludedExceptionTypes: $excludedTypes !== ''
? explode(',', $excludedTypes)
: [],
captureRequestContext: $env->getBool('ERROR_REPORTING_CAPTURE_REQUEST', true),
captureUserContext: $env->getBool('ERROR_REPORTING_CAPTURE_USER', true),
captureStackTraces: $env->getBool('ERROR_REPORTING_CAPTURE_STACK_TRACES', true),
maxStackTraceDepth: $env->getInt('ERROR_REPORTING_MAX_STACK_DEPTH', 15),
sanitizeSensitiveData: $env->getBool('ERROR_REPORTING_SANITIZE', true),
samplingRate: $env->getInt('ERROR_REPORTING_SAMPLING_RATE', 100),
maxReportsPerMinute: $env->getInt('ERROR_REPORTING_MAX_PER_MINUTE', 30),
enableAnalytics: $env->getBool('ERROR_REPORTING_ANALYTICS', true),
analyticsRetentionDays: $env->getInt('ERROR_REPORTING_RETENTION_DAYS', 30)
);
}
/**
* Staging configuration - more verbose, less restrictive
*/
private static function staging(Environment $env): self
{
$filterLevels = $env->getString('ERROR_REPORTING_FILTER_LEVELS', '');
$excludedTypes = $env->getString('ERROR_REPORTING_EXCLUDED_TYPES', '');
// Staging default: warning and above
$defaultFilterLevels = ['warning', 'error', 'critical', 'alert', 'emergency'];
return new self(
enabled: $env->getBool('ERROR_REPORTING_ENABLED', true),
asyncProcessing: $env->getBool('ERROR_REPORTING_ASYNC', true),
filterLevels: $filterLevels !== ''
? explode(',', $filterLevels)
: $defaultFilterLevels,
excludedExceptionTypes: $excludedTypes !== ''
? explode(',', $excludedTypes)
: [],
captureRequestContext: $env->getBool('ERROR_REPORTING_CAPTURE_REQUEST', true),
captureUserContext: $env->getBool('ERROR_REPORTING_CAPTURE_USER', true),
captureStackTraces: $env->getBool('ERROR_REPORTING_CAPTURE_STACK_TRACES', true),
maxStackTraceDepth: $env->getInt('ERROR_REPORTING_MAX_STACK_DEPTH', 20),
sanitizeSensitiveData: $env->getBool('ERROR_REPORTING_SANITIZE', true),
samplingRate: $env->getInt('ERROR_REPORTING_SAMPLING_RATE', 100),
maxReportsPerMinute: $env->getInt('ERROR_REPORTING_MAX_PER_MINUTE', 60),
enableAnalytics: $env->getBool('ERROR_REPORTING_ANALYTICS', true),
analyticsRetentionDays: $env->getInt('ERROR_REPORTING_RETENTION_DAYS', 14)
);
}
/**
* Development configuration - verbose, everything enabled
*/
private static function development(Environment $env): self
{
$filterLevels = $env->getString('ERROR_REPORTING_FILTER_LEVELS', '');
$excludedTypes = $env->getString('ERROR_REPORTING_EXCLUDED_TYPES', '');
return new self(
enabled: $env->getBool('ERROR_REPORTING_ENABLED', true),
asyncProcessing: $env->getBool('ERROR_REPORTING_ASYNC', false), // Sync in dev for debugging
filterLevels: $filterLevels !== '' ? explode(',', $filterLevels) : [], // All levels
excludedExceptionTypes: $excludedTypes !== '' ? explode(',', $excludedTypes) : [],
captureRequestContext: $env->getBool('ERROR_REPORTING_CAPTURE_REQUEST', true),
captureUserContext: $env->getBool('ERROR_REPORTING_CAPTURE_USER', true),
captureStackTraces: $env->getBool('ERROR_REPORTING_CAPTURE_STACK_TRACES', true),
maxStackTraceDepth: $env->getInt('ERROR_REPORTING_MAX_STACK_DEPTH', 30), // Deep traces
sanitizeSensitiveData: $env->getBool('ERROR_REPORTING_SANITIZE', false), // See real data
samplingRate: $env->getInt('ERROR_REPORTING_SAMPLING_RATE', 100), // All errors
maxReportsPerMinute: $env->getInt('ERROR_REPORTING_MAX_PER_MINUTE', 1000), // No limit
enableAnalytics: $env->getBool('ERROR_REPORTING_ANALYTICS', true),
analyticsRetentionDays: $env->getInt('ERROR_REPORTING_RETENTION_DAYS', 7)
);
}
/**
* Check if error level should be reported
*/
public function shouldReportLevel(string $level): bool
{
// Empty filter = all levels
if (empty($this->filterLevels)) {
return true;
}
return in_array(strtolower($level), array_map('strtolower', $this->filterLevels), true);
}
/**
* Check if exception type should be reported
*/
public function shouldReportException(\Throwable $exception): bool
{
if (empty($this->excludedExceptionTypes)) {
return true;
}
$exceptionClass = get_class($exception);
foreach ($this->excludedExceptionTypes as $excludedType) {
if ($exceptionClass === $excludedType || is_subclass_of($exceptionClass, $excludedType)) {
return false;
}
}
return true;
}
/**
* Check if error should be sampled (for high-volume scenarios)
*/
public function shouldSample(): bool
{
if ($this->samplingRate === 100) {
return true;
}
return random_int(1, 100) <= $this->samplingRate;
}
}

View File

@@ -17,6 +17,7 @@ use App\Framework\ErrorReporting\Storage\ErrorReportStorageInterface;
use App\Framework\Http\Session\Session;
use App\Framework\Logging\Logger;
use App\Framework\Queue\Queue;
use App\Framework\ErrorReporting\ErrorReportingConfig;
/**
* Initializer for Error Reporting system
@@ -27,17 +28,15 @@ final readonly class ErrorReportingInitializer
public function initialize(Container $container): void
{
$env = $container->get(Environment::class);
$enabled = $env->getBool('ERROR_REPORTING_ENABLED', true);
// Create configuration from environment
$config = ErrorReportingConfig::fromEnvironment($env);
// Register config as singleton
$container->singleton(ErrorReportingConfig::class, $config);
// Storage
$container->bind(ErrorReportStorageInterface::class, function (Container $container) use ($enabled) {
if (! $enabled) {
// Return storage even if disabled (might be used for queries)
return new DatabaseErrorReportStorage(
connection: $container->get(ConnectionInterface::class)
);
}
$container->bind(ErrorReportStorageInterface::class, function (Container $container) {
return new DatabaseErrorReportStorage(
connection: $container->get(ConnectionInterface::class)
);
@@ -52,106 +51,28 @@ final readonly class ErrorReportingInitializer
});
// Error Reporter Interface - bind to concrete or Null implementation
$container->bind(ErrorReporterInterface::class, function (Container $container) use ($enabled) {
if (! $enabled) {
$container->bind(ErrorReporterInterface::class, function (Container $container) use ($config) {
if (!$config->enabled) {
return new NullErrorReporter();
}
$env = $container->get(Environment::class);
$processors = [];
$filters = [];
// Add built-in processors
if ($container->has(RequestContextProcessor::class)) {
$processors[] = $container->get(RequestContextProcessor::class);
}
if ($container->has(UserContextProcessor::class)) {
$processors[] = $container->get(UserContextProcessor::class);
}
// Add environment-based filters
$filterLevels = $env->getString('ERROR_REPORTING_FILTER_LEVELS', '');
if ($filterLevels !== '') {
$allowedLevels = explode(',', $filterLevels);
$filters[] = function (ErrorReport $report) use ($allowedLevels) {
return in_array($report->level, $allowedLevels);
};
}
// Add environment filter for production
$appEnv = $env->getString('APP_ENV', 'production');
if ($appEnv === 'production') {
$filters[] = function (ErrorReport $report) {
// Don't report debug/info in production
return ! in_array($report->level, ['debug', 'info']);
};
}
return new ErrorReporter(
storage: $container->get(ErrorReportStorageInterface::class),
clock: $container->get(Clock::class),
logger: $container->has(Logger::class) ? $container->get(Logger::class) : null,
queue: $container->has(Queue::class) ? $container->get(Queue::class) : null,
asyncProcessing: $env->getBool('ERROR_REPORTING_ASYNC', true),
processors: $processors,
filters: $filters
);
return $this->createErrorReporter($container, $config);
});
// Error Reporter (concrete class) - delegate to interface
$container->bind(ErrorReporter::class, function (Container $container) use ($enabled) {
if (! $enabled) {
$container->bind(ErrorReporter::class, function (Container $container) use ($config) {
if (!$config->enabled) {
throw new \RuntimeException('ErrorReporter is disabled. Use ErrorReporterInterface instead.');
}
$env = $container->get(Environment::class);
$processors = [];
$filters = [];
// Add built-in processors
if ($container->has(RequestContextProcessor::class)) {
$processors[] = $container->get(RequestContextProcessor::class);
}
if ($container->has(UserContextProcessor::class)) {
$processors[] = $container->get(UserContextProcessor::class);
}
// Add environment-based filters
$filterLevels = $env->getString('ERROR_REPORTING_FILTER_LEVELS', '');
if ($filterLevels !== '') {
$allowedLevels = explode(',', $filterLevels);
$filters[] = function (ErrorReport $report) use ($allowedLevels) {
return in_array($report->level, $allowedLevels);
};
}
// Add environment filter for production
$appEnv = $env->getString('APP_ENV', 'production');
if ($appEnv === 'production') {
$filters[] = function (ErrorReport $report) {
// Don't report debug/info in production
return ! in_array($report->level, ['debug', 'info']);
};
}
return new ErrorReporter(
storage: $container->get(ErrorReportStorageInterface::class),
clock: $container->get(Clock::class),
logger: $container->has(Logger::class) ? $container->get(Logger::class) : null,
queue: $container->has(Queue::class) ? $container->get(Queue::class) : null,
asyncProcessing: $env->getBool('ERROR_REPORTING_ASYNC', true),
processors: $processors,
filters: $filters
);
return $this->createErrorReporter($container, $config);
});
// Middleware
$container->bind(ErrorReportingMiddleware::class, function (Container $container) use ($enabled) {
$container->bind(ErrorReportingMiddleware::class, function (Container $container) use ($config) {
return new ErrorReportingMiddleware(
reporter: $container->get(ErrorReporter::class),
enabled: $enabled
enabled: $config->enabled
);
});
@@ -166,4 +87,46 @@ final readonly class ErrorReportingInitializer
return new UserContextProcessor(null);
});
}
/**
* Create ErrorReporter instance with processors and filters from config
*/
private function createErrorReporter(Container $container, ErrorReportingConfig $config): ErrorReporter
{
$processors = [];
$filters = [];
// Add built-in processors based on config
if ($config->captureRequestContext && $container->has(RequestContextProcessor::class)) {
$processors[] = $container->get(RequestContextProcessor::class);
}
if ($config->captureUserContext && $container->has(UserContextProcessor::class)) {
$processors[] = $container->get(UserContextProcessor::class);
}
// Add level-based filter from config
if (!empty($config->filterLevels)) {
$filters[] = function (ErrorReport $report) use ($config) {
return $config->shouldReportLevel($report->level);
};
}
// Add sampling filter
if ($config->samplingRate < 100) {
$filters[] = function (ErrorReport $report) use ($config) {
return $config->shouldSample();
};
}
return new ErrorReporter(
storage: $container->get(ErrorReportStorageInterface::class),
clock: $container->get(Clock::class),
logger: $container->has(Logger::class) ? $container->get(Logger::class) : null,
queue: $container->has(Queue::class) ? $container->get(Queue::class) : null,
asyncProcessing: $config->asyncProcessing,
processors: $processors,
filters: $filters
);
}
}

View File

@@ -91,8 +91,8 @@ final readonly class DatabaseErrorReportStorage implements ErrorReportStorageInt
public function findRecent(int $limit = 100, int $offset = 0): array
{
$sql = '
SELECT * FROM error_reports
ORDER BY timestamp DESC
SELECT * FROM error_reports
ORDER BY timestamp DESC
LIMIT ? OFFSET ?
';
@@ -178,10 +178,10 @@ final readonly class DatabaseErrorReportStorage implements ErrorReportStorageInt
// Top errors by fingerprint
$topErrorsSql = '
SELECT fingerprint, exception, file, line, COUNT(*) as count, MAX(timestamp) as last_seen
FROM error_reports
WHERE timestamp BETWEEN ? AND ?
GROUP BY fingerprint, exception, file, line
ORDER BY count DESC
FROM error_reports
WHERE timestamp BETWEEN ? AND ?
GROUP BY fingerprint, exception, file, line
ORDER BY count DESC
LIMIT 10
';
$topErrorsResults = $this->connection->query($topErrorsSql, [$from->format('Y-m-d H:i:s'), $to->format('Y-m-d H:i:s')]);
@@ -240,7 +240,7 @@ final readonly class DatabaseErrorReportStorage implements ErrorReportStorageInt
$sql = "
SELECT DATE_FORMAT(timestamp, '{$groupFormat}') as period, COUNT(*) as count
FROM error_reports
FROM error_reports
WHERE timestamp BETWEEN ? AND ?
GROUP BY DATE_FORMAT(timestamp, '{$groupFormat}')
ORDER BY period
@@ -263,7 +263,7 @@ final readonly class DatabaseErrorReportStorage implements ErrorReportStorageInt
{
$sql = '
SELECT fingerprint, exception, message, file, line, COUNT(*) as count, MAX(timestamp) as last_seen
FROM error_reports
FROM error_reports
WHERE timestamp BETWEEN ? AND ?
GROUP BY fingerprint, exception, message, file, line
ORDER BY count DESC
@@ -291,9 +291,9 @@ final readonly class DatabaseErrorReportStorage implements ErrorReportStorageInt
public function findByFingerprint(string $fingerprint, int $limit = 100): array
{
$sql = '
SELECT * FROM error_reports
WHERE fingerprint = ?
ORDER BY timestamp DESC
SELECT * FROM error_reports
WHERE fingerprint = ?
ORDER BY timestamp DESC
LIMIT ?
';
@@ -330,9 +330,9 @@ final readonly class DatabaseErrorReportStorage implements ErrorReportStorageInt
// Table size
$sizeSql = "
SELECT
SELECT
ROUND(((data_length + index_length) / 1024 / 1024), 2) as size_mb
FROM information_schema.tables
FROM information_schema.tables
WHERE table_schema = DATABASE() AND table_name = 'error_reports'
";
$sizeResult = $this->connection->query($sizeSql);