feat: CI/CD pipeline setup complete - Ansible playbooks updated, secrets configured, workflow ready
This commit is contained in:
231
src/Framework/ErrorReporting/ErrorReportingConfig.php
Normal file
231
src/Framework/ErrorReporting/ErrorReportingConfig.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user