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:
2025-08-11 20:13:26 +02:00
parent 59fd3dd3b1
commit 55a330b223
3683 changed files with 2956207 additions and 16948 deletions

View File

@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace App\Framework\Metrics\Formatters;
use App\Framework\Metrics\MetricsCollection;
use App\Framework\Serializer\Json\JsonSerializer;
use App\Framework\Serializer\Json\JsonSerializerConfig;
/**
* Formats metrics as JSON for easy consumption by APIs and debugging
*/
final readonly class JsonFormatter implements MetricsFormatter
{
private JsonSerializer $serializer;
public function __construct(
?JsonSerializer $serializer = null,
private bool $prettyPrint = false
) {
$config = $this->prettyPrint
? JsonSerializerConfig::pretty()
: JsonSerializerConfig::compact();
$this->serializer = $serializer ?? new JsonSerializer($config);
}
public function format(MetricsCollection $metrics): string
{
return $this->serializer->serialize($metrics->toArray());
}
public function getContentType(): string
{
return 'application/json; charset=utf-8';
}
public function getName(): string
{
return 'json';
}
}

View File

@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace App\Framework\Metrics\Formatters;
use App\Framework\Metrics\MetricsCollection;
/**
* Interface for metric formatters
*/
interface MetricsFormatter
{
/**
* Format metrics collection to string representation
*/
public function format(MetricsCollection $metrics): string;
/**
* Get the appropriate Content-Type header for this format
*/
public function getContentType(): string;
/**
* Get the format name
*/
public function getName(): string;
}

View File

@@ -0,0 +1,126 @@
<?php
declare(strict_types=1);
namespace App\Framework\Metrics\Formatters;
use App\Framework\Metrics\Metric;
use App\Framework\Metrics\MetricsCollection;
use App\Framework\Metrics\MetricSuffix;
use App\Framework\Metrics\MetricType;
/**
* Formats metrics in OpenMetrics format
* OpenMetrics is a CNCF standard that extends Prometheus format
* @see https://openmetrics.io/
*/
final readonly class OpenMetricsFormatter implements MetricsFormatter
{
public function format(MetricsCollection $metrics): string
{
$output = [];
$processedMetrics = [];
foreach ($metrics->getMetrics() as $metricName => $metricGroup) {
// Skip if already processed
if (isset($processedMetrics[$metricName])) {
continue;
}
$firstMetric = $metricGroup[0];
$baseName = MetricSuffix::getBaseName($metricName);
// HELP line (required in OpenMetrics)
$help = $firstMetric->help ?? 'No description provided';
$output[] = sprintf('# HELP %s %s', $baseName, $help);
// TYPE line (required in OpenMetrics)
$type = $this->determineMetricType($metricName, $firstMetric->type);
$output[] = sprintf('# TYPE %s %s', $baseName, $type->value);
// UNIT line (OpenMetrics specific, optional)
if ($firstMetric->unit !== null) {
$output[] = sprintf('# UNIT %s %s', $baseName, $firstMetric->unit);
}
// Add metric values
foreach ($metricGroup as $metric) {
$output[] = $this->formatMetric($metric);
}
$processedMetrics[$metricName] = true;
// Mark related metrics as processed
if ($type === MetricType::HISTOGRAM) {
foreach (MetricSuffix::histogramSuffixes() as $suffix) {
$processedMetrics[$suffix->addTo($baseName)] = true;
}
}
}
// OpenMetrics requires EOF marker
$output[] = '# EOF';
return implode("\n", $output) . "\n";
}
private function formatMetric(Metric $metric): string
{
$labels = $metric->getFormattedLabels();
$name = $metric->name;
// OpenMetrics doesn't append unit to metric name (uses UNIT metadata instead)
$line = sprintf('%s%s %s', $name, $labels, $this->formatValue($metric->value));
// Add timestamp if present (seconds with fractional part for OpenMetrics)
if ($metric->timestamp !== null) {
$line .= ' ' . number_format($metric->timestamp->toFloat(), 3, '.', '');
}
return $line;
}
private function formatValue(float $value): string
{
// Handle special float values (OpenMetrics spec)
if (is_infinite($value)) {
return $value > 0 ? '+Inf' : '-Inf';
}
if (is_nan($value)) {
return 'NaN';
}
// Use scientific notation for very large/small numbers
if (abs($value) >= 1e10 || (abs($value) < 1e-10 && $value != 0)) {
return sprintf('%e', $value);
}
// Regular formatting
$formatted = sprintf('%.10f', $value);
$formatted = rtrim($formatted, '0');
$formatted = rtrim($formatted, '.');
return $formatted;
}
private function determineMetricType(string $name, MetricType $type): MetricType
{
if (MetricSuffix::hasHistogramSuffix($name)) {
return MetricType::HISTOGRAM;
}
return $type;
}
public function getContentType(): string
{
return 'application/openmetrics-text; version=1.0.0; charset=utf-8';
}
public function getName(): string
{
return 'openmetrics';
}
}

View File

@@ -0,0 +1,191 @@
<?php
declare(strict_types=1);
namespace App\Framework\Metrics\Formatters;
use App\Framework\Metrics\Metric;
use App\Framework\Metrics\MetricsCollection;
use App\Framework\Metrics\MetricSuffix;
use App\Framework\Metrics\MetricType;
/**
* Formats metrics in Prometheus text format
* @see https://prometheus.io/docs/instrumenting/exposition_formats/
*/
final readonly class PrometheusFormatter implements MetricsFormatter
{
public function format(MetricsCollection $metrics): string
{
$output = [];
$processedMetrics = [];
// Group metrics by base name for histogram handling
$groupedMetrics = $this->groupMetricsByBaseName($metrics);
foreach ($groupedMetrics as $baseName => $group) {
if (isset($processedMetrics[$baseName])) {
continue;
}
// Check if this is a histogram
$isHistogram = $this->isHistogramGroup($group);
if ($isHistogram) {
$output = array_merge($output, $this->formatHistogram($baseName, $group));
} else {
// Format regular metrics
foreach ($group as $metricName => $metricGroup) {
if (! isset($processedMetrics[$metricName])) {
$output = array_merge($output, $this->formatRegularMetric($metricName, $metricGroup));
$processedMetrics[$metricName] = true;
}
}
}
$processedMetrics[$baseName] = true;
}
// Add newline at the end for proper Prometheus format
return implode("\n", $output) . "\n";
}
private function groupMetricsByBaseName(MetricsCollection $metrics): array
{
$grouped = [];
foreach ($metrics->getMetrics() as $metricName => $metricGroup) {
$baseName = MetricSuffix::getBaseName($metricName);
$grouped[$baseName][$metricName] = $metricGroup;
}
return $grouped;
}
private function isHistogramGroup(array $group): bool
{
$keys = array_keys($group);
foreach ($keys as $key) {
if (MetricSuffix::hasHistogramSuffix($key)) {
return true;
}
}
return false;
}
private function formatHistogram(string $baseName, array $group): array
{
$output = [];
// Find the first metric with help text
$helpText = null;
foreach ($group as $metrics) {
if (! empty($metrics) && $metrics[0]->help !== null) {
$helpText = $metrics[0]->help;
break;
}
}
// Add HELP and TYPE for histogram
if ($helpText !== null) {
$output[] = sprintf('# HELP %s %s', $baseName, $helpText);
}
$output[] = sprintf('# TYPE %s histogram', $baseName);
// Format all histogram components
foreach ($group as $metricName => $metrics) {
foreach ($metrics as $metric) {
$output[] = $this->formatMetric($metric);
}
}
return $output;
}
private function formatRegularMetric(string $metricName, array $metricGroup): array
{
$output = [];
$firstMetric = $metricGroup[0];
// Add HELP and TYPE lines
if ($firstMetric->help !== null) {
$output[] = sprintf('# HELP %s %s', $metricName, $firstMetric->help);
}
$output[] = sprintf('# TYPE %s %s', $metricName, $firstMetric->type->value);
// Add metric values
foreach ($metricGroup as $metric) {
$output[] = $this->formatMetric($metric);
}
return $output;
}
private function formatMetric(Metric $metric): string
{
$labels = $metric->getFormattedLabels();
$name = $metric->name;
// Add unit suffix if specified (OpenMetrics convention)
if ($metric->unit !== null) {
$name .= '_' . $metric->unit;
}
$line = sprintf('%s%s %s', $name, $labels, $this->formatValue($metric->value));
// Add timestamp if present (milliseconds for Prometheus)
if ($metric->timestamp !== null) {
$timestampMs = (int) ($metric->timestamp->toFloat() * 1000);
$line .= ' ' . $timestampMs;
}
return $line;
}
private function formatValue(float $value): string
{
// Handle special float values
if (is_infinite($value)) {
return $value > 0 ? '+Inf' : '-Inf';
}
if (is_nan($value)) {
return 'NaN';
}
// Use scientific notation for very large/small numbers
if (abs($value) >= 1e10 || (abs($value) < 1e-10 && $value != 0)) {
return sprintf('%e', $value);
}
// Regular formatting, remove unnecessary zeros
$formatted = sprintf('%.10f', $value);
$formatted = rtrim($formatted, '0');
$formatted = rtrim($formatted, '.');
return $formatted;
}
private function determineMetricType(string $name, MetricType $type): MetricType
{
// Check if this is a histogram component
if (MetricSuffix::hasHistogramSuffix($name)) {
return MetricType::HISTOGRAM;
}
return $type;
}
public function getContentType(): string
{
return 'text/plain; version=0.0.4; charset=utf-8';
}
public function getName(): string
{
return 'prometheus';
}
}

View File

@@ -0,0 +1,104 @@
<?php
declare(strict_types=1);
namespace App\Framework\Metrics\Formatters;
use App\Framework\Metrics\Metric;
use App\Framework\Metrics\MetricsCollection;
use App\Framework\Metrics\MetricType;
/**
* Formats metrics in StatsD format for push-based metrics
* @see https://github.com/statsd/statsd/blob/master/docs/metric_types.md
*/
final readonly class StatsDFormatter implements MetricsFormatter
{
public function __construct(
private string $prefix = '',
private string $tagSeparator = ',' // Can be ',' or ';' depending on StatsD implementation
) {
}
public function format(MetricsCollection $metrics): string
{
$lines = [];
foreach ($metrics->getAllMetrics() as $metric) {
$line = $this->formatMetric($metric);
if ($line !== null) {
$lines[] = $line;
}
}
return implode("\n", $lines);
}
private function formatMetric(Metric $metric): ?string
{
$name = $this->prefix . $metric->name;
$value = $metric->value;
// Determine StatsD metric type suffix
$typeSuffix = match($metric->type) {
MetricType::COUNTER => 'c',
MetricType::GAUGE => 'g',
MetricType::HISTOGRAM => 'h',
MetricType::SUMMARY => 'ms', // StatsD uses timing for summary-like metrics
};
// Format: metric.name:value|type|@sample_rate|#tag1:value,tag2:value
$line = sprintf('%s:%s|%s', $name, $this->formatValue($value), $typeSuffix);
// Add tags if present
if (! empty($metric->labels)) {
$line .= '|#' . $this->formatTags($metric->labels);
}
return $line;
}
private function formatValue(float $value): string
{
// StatsD doesn't support Inf or NaN
if (is_infinite($value) || is_nan($value)) {
return '0';
}
// Format based on whether it's an integer or float
if ($value == floor($value)) {
return (string) (int) $value;
}
// Keep decimal precision but remove trailing zeros
$formatted = sprintf('%.6f', $value);
$formatted = rtrim($formatted, '0');
$formatted = rtrim($formatted, '.');
return $formatted;
}
private function formatTags(array $labels): string
{
$tags = [];
foreach ($labels as $key => $value) {
// Escape special characters in StatsD tags
$escapedKey = str_replace([':', '|', '@', ','], '_', $key);
$escapedValue = str_replace([':', '|', '@', ','], '_', $value);
$tags[] = sprintf('%s:%s', $escapedKey, $escapedValue);
}
return implode($this->tagSeparator, $tags);
}
public function getContentType(): string
{
return 'text/plain; charset=utf-8';
}
public function getName(): string
{
return 'statsd';
}
}

View File

@@ -0,0 +1,115 @@
<?php
declare(strict_types=1);
namespace App\Framework\Metrics;
use App\Framework\Core\ValueObjects\Timestamp;
/**
* Represents a single metric with its metadata and value
*/
final readonly class Metric
{
/**
* @param array<string, string> $labels
*/
public function __construct(
public string $name,
public float $value,
public MetricType $type,
public ?string $help = null,
public array $labels = [],
public ?string $unit = null,
public ?Timestamp $timestamp = null
) {
}
public function withLabel(string $key, string $value): self
{
return new self(
$this->name,
$this->value,
$this->type,
$this->help,
array_merge($this->labels, [$key => $value]),
$this->unit,
$this->timestamp
);
}
public function withValue(float $value): self
{
return new self(
$this->name,
$value,
$this->type,
$this->help,
$this->labels,
$this->unit,
$this->timestamp
);
}
public function withTimestamp(Timestamp $timestamp): self
{
return new self(
$this->name,
$this->value,
$this->type,
$this->help,
$this->labels,
$this->unit,
$timestamp
);
}
/**
* Get formatted label string for Prometheus format
*/
public function getFormattedLabels(): string
{
if (empty($this->labels)) {
return '';
}
$parts = [];
foreach ($this->labels as $key => $value) {
// Escape special characters in label values
$escapedValue = str_replace(['\\', '"', "\n"], ['\\\\', '\\"', '\\n'], $value);
$parts[] = sprintf('%s="%s"', $key, $escapedValue);
}
return '{' . implode(',', $parts) . '}';
}
/**
* Convert metric to array representation
*/
public function toArray(): array
{
$data = [
'name' => $this->name,
'value' => $this->value,
'type' => $this->type->value,
];
if ($this->help !== null) {
$data['help'] = $this->help;
}
if (! empty($this->labels)) {
$data['labels'] = $this->labels;
}
if ($this->unit !== null) {
$data['unit'] = $this->unit;
}
if ($this->timestamp !== null) {
$data['timestamp'] = $this->timestamp->toIso8601();
}
return $data;
}
}

View File

@@ -0,0 +1,95 @@
<?php
declare(strict_types=1);
namespace App\Framework\Metrics;
/**
* Standard metric suffixes for complex metric types
*/
enum MetricSuffix: string
{
// Histogram suffixes
case BUCKET = '_bucket';
case SUM = '_sum';
case COUNT = '_count';
// Summary suffixes (for future use)
case QUANTILE = '_quantile';
// Info suffix (for future use)
case INFO = '_info';
/**
* Check if a metric name has this suffix
*/
public function isInName(string $name): bool
{
return str_ends_with($name, $this->value);
}
/**
* Remove this suffix from a metric name
*/
public function removeFrom(string $name): string
{
if ($this->isInName($name)) {
return substr($name, 0, -strlen($this->value));
}
return $name;
}
/**
* Add this suffix to a metric name
*/
public function addTo(string $name): string
{
if (! $this->isInName($name)) {
return $name . $this->value;
}
return $name;
}
/**
* Get all histogram-related suffixes
* @return self[]
*/
public static function histogramSuffixes(): array
{
return [
self::BUCKET,
self::SUM,
self::COUNT,
];
}
/**
* Check if name has any histogram suffix
*/
public static function hasHistogramSuffix(string $name): bool
{
foreach (self::histogramSuffixes() as $suffix) {
if ($suffix->isInName($name)) {
return true;
}
}
return false;
}
/**
* Get base name by removing any known suffix
*/
public static function getBaseName(string $name): string
{
foreach (self::cases() as $suffix) {
if ($suffix->isInName($name)) {
return $suffix->removeFrom($name);
}
}
return $name;
}
}

View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace App\Framework\Metrics;
/**
* Metric types as defined by Prometheus/OpenMetrics standards
*/
enum MetricType: string
{
case COUNTER = 'counter';
case GAUGE = 'gauge';
case HISTOGRAM = 'histogram';
case SUMMARY = 'summary';
public function getDescription(): string
{
return match($this) {
self::COUNTER => 'A cumulative metric that only increases',
self::GAUGE => 'A metric that can go up and down',
self::HISTOGRAM => 'A metric that samples observations and counts them in buckets',
self::SUMMARY => 'A metric that samples observations and provides quantiles',
};
}
}

View File

@@ -0,0 +1,180 @@
<?php
declare(strict_types=1);
namespace App\Framework\Metrics;
use App\Framework\Core\ValueObjects\Timestamp;
/**
* Collection of metrics for export
*/
final class MetricsCollection
{
/**
* @var array<string, Metric[]>
*/
private array $metrics = [];
private ?Timestamp $collectedAt = null;
public function __construct()
{
$this->collectedAt = Timestamp::now();
}
public function add(Metric $metric): self
{
$this->metrics[$metric->name][] = $metric;
return $this;
}
/**
* Add a counter metric
*/
public function counter(
string $name,
float $value,
?string $help = null,
array $labels = [],
?string $unit = null
): self {
return $this->add(new Metric(
$name,
$value,
MetricType::COUNTER,
$help,
$labels,
$unit,
$this->collectedAt
));
}
/**
* Add a gauge metric
*/
public function gauge(
string $name,
float $value,
?string $help = null,
array $labels = [],
?string $unit = null
): self {
return $this->add(new Metric(
$name,
$value,
MetricType::GAUGE,
$help,
$labels,
$unit,
$this->collectedAt
));
}
/**
* Add a histogram metric with buckets
*/
public function histogram(
string $name,
array $buckets,
float $sum,
int $count,
?string $help = null,
array $labels = []
): self {
// Add bucket metrics
foreach ($buckets as $le => $value) {
$this->add(new Metric(
$name . '_bucket',
(float) $value,
MetricType::HISTOGRAM,
$help,
array_merge($labels, ['le' => (string) $le]),
null,
$this->collectedAt
));
}
// Add sum and count
$this->add(new Metric(
$name . '_sum',
$sum,
MetricType::HISTOGRAM,
$help,
$labels,
null,
$this->collectedAt
));
$this->add(new Metric(
$name . '_count',
(float) $count,
MetricType::HISTOGRAM,
$help,
$labels,
null,
$this->collectedAt
));
return $this;
}
/**
* Get all metrics
* @return array<string, Metric[]>
*/
public function getMetrics(): array
{
return $this->metrics;
}
/**
* Get all metrics as flat array
* @return Metric[]
*/
public function getAllMetrics(): array
{
$flat = [];
foreach ($this->metrics as $metricGroup) {
foreach ($metricGroup as $metric) {
$flat[] = $metric;
}
}
return $flat;
}
public function getCollectedAt(): ?Timestamp
{
return $this->collectedAt;
}
/**
* Merge another collection into this one
*/
public function merge(self $other): self
{
foreach ($other->metrics as $name => $metrics) {
foreach ($metrics as $metric) {
$this->add($metric);
}
}
return $this;
}
/**
* Convert collection to array representation
*/
public function toArray(): array
{
return [
'timestamp' => $this->collectedAt?->toIso8601(),
'metrics' => array_map(
fn (Metric $metric) => $metric->toArray(),
$this->getAllMetrics()
),
];
}
}

View File

@@ -0,0 +1,156 @@
<?php
declare(strict_types=1);
namespace App\Framework\Metrics;
use App\Framework\Cache\Cache;
use App\Framework\Database\Connection;
use App\Framework\Http\MiddlewareMetricsCollector;
use App\Framework\Performance\MemoryMonitor;
use App\Framework\Performance\PerformanceCollector;
/**
* Collects metrics from various framework components
*/
final readonly class MetricsCollector
{
public function __construct(
private ?Cache $cache = null,
private ?Connection $database = null,
private ?PerformanceCollector $performance = null,
private ?MemoryMonitor $memory = null,
private ?MiddlewareMetricsCollector $middleware = null
) {
}
/**
* Collect all available metrics
*/
public function collect(): MetricsCollection
{
$collection = new MetricsCollection();
// System metrics
$this->collectSystemMetrics($collection);
// Cache metrics
if ($this->cache !== null) {
$this->collectCacheMetrics($collection);
}
// Database metrics
if ($this->database !== null) {
$this->collectDatabaseMetrics($collection);
}
// Performance metrics
if ($this->performance !== null) {
$this->collectPerformanceMetrics($collection);
}
// Memory metrics
if ($this->memory !== null) {
$this->collectMemoryMetrics($collection);
}
// HTTP/Middleware metrics
if ($this->middleware !== null) {
$this->collectMiddlewareMetrics($collection);
}
return $collection;
}
private function collectSystemMetrics(MetricsCollection $collection): void
{
// PHP version info
$collection->gauge(
'php_info',
1,
'PHP version information',
['version' => PHP_VERSION, 'sapi' => PHP_SAPI]
);
// Process metrics
$collection->gauge(
'process_open_fds',
(float) count(get_resources()),
'Number of open file descriptors'
);
// Uptime (approximation based on request time)
if (isset($_SERVER['REQUEST_TIME_FLOAT'])) {
$uptime = microtime(true) - $_SERVER['REQUEST_TIME_FLOAT'];
$collection->gauge(
'process_uptime_seconds',
$uptime,
'Time since process started'
);
}
}
private function collectCacheMetrics(MetricsCollection $collection): void
{
// This would need integration with your cache implementation
// Example metrics:
$collection->counter(
'cache_hits_total',
0, // Get from cache stats
'Total number of cache hits'
);
$collection->counter(
'cache_misses_total',
0, // Get from cache stats
'Total number of cache misses'
);
}
private function collectDatabaseMetrics(MetricsCollection $collection): void
{
// Database connection metrics
$collection->gauge(
'database_connections_active',
1, // Would get from connection pool
'Number of active database connections'
);
}
private function collectPerformanceMetrics(MetricsCollection $collection): void
{
// Would integrate with PerformanceCollector
$collection->gauge(
'http_request_duration_seconds',
microtime(true) - ($_SERVER['REQUEST_TIME_FLOAT'] ?? microtime(true)),
'HTTP request duration'
);
}
private function collectMemoryMetrics(MetricsCollection $collection): void
{
$collection->gauge(
'process_memory_bytes',
(float) memory_get_usage(true),
'Current memory usage in bytes'
);
$collection->gauge(
'process_memory_peak_bytes',
(float) memory_get_peak_usage(true),
'Peak memory usage in bytes'
);
}
private function collectMiddlewareMetrics(MetricsCollection $collection): void
{
// Would integrate with MiddlewareMetricsCollector
// Example: Request count by method and status
$collection->counter(
'http_requests_total',
1,
'Total HTTP requests',
['method' => $_SERVER['REQUEST_METHOD'] ?? 'unknown']
);
}
}