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:
43
src/Framework/Metrics/Formatters/JsonFormatter.php
Normal file
43
src/Framework/Metrics/Formatters/JsonFormatter.php
Normal 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';
|
||||
}
|
||||
}
|
||||
28
src/Framework/Metrics/Formatters/MetricsFormatter.php
Normal file
28
src/Framework/Metrics/Formatters/MetricsFormatter.php
Normal 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;
|
||||
}
|
||||
126
src/Framework/Metrics/Formatters/OpenMetricsFormatter.php
Normal file
126
src/Framework/Metrics/Formatters/OpenMetricsFormatter.php
Normal 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';
|
||||
}
|
||||
}
|
||||
191
src/Framework/Metrics/Formatters/PrometheusFormatter.php
Normal file
191
src/Framework/Metrics/Formatters/PrometheusFormatter.php
Normal 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';
|
||||
}
|
||||
}
|
||||
104
src/Framework/Metrics/Formatters/StatsDFormatter.php
Normal file
104
src/Framework/Metrics/Formatters/StatsDFormatter.php
Normal 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';
|
||||
}
|
||||
}
|
||||
115
src/Framework/Metrics/Metric.php
Normal file
115
src/Framework/Metrics/Metric.php
Normal 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;
|
||||
}
|
||||
}
|
||||
95
src/Framework/Metrics/MetricSuffix.php
Normal file
95
src/Framework/Metrics/MetricSuffix.php
Normal 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;
|
||||
}
|
||||
}
|
||||
26
src/Framework/Metrics/MetricType.php
Normal file
26
src/Framework/Metrics/MetricType.php
Normal 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',
|
||||
};
|
||||
}
|
||||
}
|
||||
180
src/Framework/Metrics/MetricsCollection.php
Normal file
180
src/Framework/Metrics/MetricsCollection.php
Normal 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()
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
156
src/Framework/Metrics/MetricsCollector.php
Normal file
156
src/Framework/Metrics/MetricsCollector.php
Normal 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']
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user