You've already forked wp-prometheus
feat: Add runtime metrics for HTTP requests and database queries (v0.1.0)
All checks were successful
Create Release Package / build-release (push) Successful in 59s
All checks were successful
Create Release Package / build-release (push) Successful in 59s
- Add RuntimeCollector class for tracking request lifecycle metrics - Add wordpress_http_requests_total counter (method, status, endpoint) - Add wordpress_http_request_duration_seconds histogram - Add wordpress_db_queries_total counter (endpoint) - Add wordpress_db_query_duration_seconds histogram (requires SAVEQUERIES) - Update Collector to expose stored runtime metrics - Add new settings options for enabling/disabling runtime metrics - Create translation files (.pot, .po, .mo) for internationalization - Update documentation and changelog Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -337,11 +337,14 @@ class Settings {
|
||||
public function render_enabled_metrics_field(): void {
|
||||
$enabled = get_option( 'wp_prometheus_enabled_metrics', array() );
|
||||
$metrics = array(
|
||||
'wordpress_info' => __( 'WordPress Info (version, PHP version, multisite)', 'wp-prometheus' ),
|
||||
'wordpress_users_total' => __( 'Total Users by Role', 'wp-prometheus' ),
|
||||
'wordpress_posts_total' => __( 'Total Posts by Type and Status', 'wp-prometheus' ),
|
||||
'wordpress_comments_total' => __( 'Total Comments by Status', 'wp-prometheus' ),
|
||||
'wordpress_plugins_total' => __( 'Total Plugins (active/inactive)', 'wp-prometheus' ),
|
||||
'wordpress_info' => __( 'WordPress Info (version, PHP version, multisite)', 'wp-prometheus' ),
|
||||
'wordpress_users_total' => __( 'Total Users by Role', 'wp-prometheus' ),
|
||||
'wordpress_posts_total' => __( 'Total Posts by Type and Status', 'wp-prometheus' ),
|
||||
'wordpress_comments_total' => __( 'Total Comments by Status', 'wp-prometheus' ),
|
||||
'wordpress_plugins_total' => __( 'Total Plugins (active/inactive)', 'wp-prometheus' ),
|
||||
'wordpress_http_requests_total' => __( 'HTTP Requests Total (by method, status, endpoint)', 'wp-prometheus' ),
|
||||
'wordpress_http_request_duration_seconds' => __( 'HTTP Request Duration (histogram)', 'wp-prometheus' ),
|
||||
'wordpress_db_queries_total' => __( 'Database Queries Total (by endpoint)', 'wp-prometheus' ),
|
||||
);
|
||||
|
||||
foreach ( $metrics as $key => $label ) {
|
||||
|
||||
@@ -63,6 +63,7 @@ final class Installer {
|
||||
'wp_prometheus_auth_token',
|
||||
'wp_prometheus_enable_default_metrics',
|
||||
'wp_prometheus_enabled_metrics',
|
||||
'wp_prometheus_runtime_metrics',
|
||||
);
|
||||
|
||||
foreach ( $options as $option ) {
|
||||
|
||||
@@ -95,6 +95,9 @@ class Collector {
|
||||
$this->collect_plugins_total();
|
||||
}
|
||||
|
||||
// Collect runtime metrics (HTTP requests, DB queries).
|
||||
$this->collect_runtime_metrics( $enabled_metrics );
|
||||
|
||||
/**
|
||||
* Fires after default metrics are collected.
|
||||
*
|
||||
@@ -233,6 +236,153 @@ class Collector {
|
||||
$gauge->set( count( $all_plugins ) - count( $active_plugins ), array( 'inactive' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect runtime metrics from stored data.
|
||||
*
|
||||
* @param array $enabled_metrics List of enabled metrics.
|
||||
* @return void
|
||||
*/
|
||||
private function collect_runtime_metrics( array $enabled_metrics ): void {
|
||||
$runtime_collector = RuntimeCollector::get_instance();
|
||||
$stored_metrics = $runtime_collector->get_stored_metrics();
|
||||
|
||||
// HTTP requests total counter.
|
||||
if ( in_array( 'wordpress_http_requests_total', $enabled_metrics, true ) && ! empty( $stored_metrics['counters'] ) ) {
|
||||
foreach ( $stored_metrics['counters'] as $counter_data ) {
|
||||
if ( 'http_requests_total' !== $counter_data['name'] ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$counter = $this->registry->getOrRegisterCounter(
|
||||
$this->namespace,
|
||||
'http_requests_total',
|
||||
'Total number of HTTP requests',
|
||||
array( 'method', 'status', 'endpoint' )
|
||||
);
|
||||
|
||||
$counter->incBy(
|
||||
(int) $counter_data['value'],
|
||||
array(
|
||||
$counter_data['labels']['method'] ?? 'GET',
|
||||
$counter_data['labels']['status'] ?? '200',
|
||||
$counter_data['labels']['endpoint'] ?? 'unknown',
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// HTTP request duration histogram.
|
||||
if ( in_array( 'wordpress_http_request_duration_seconds', $enabled_metrics, true ) && ! empty( $stored_metrics['histograms'] ) ) {
|
||||
foreach ( $stored_metrics['histograms'] as $histogram_data ) {
|
||||
if ( 'http_request_duration_seconds' !== $histogram_data['name'] ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// For histograms, we expose as a gauge with pre-aggregated bucket counts.
|
||||
// This is a workaround since we can't directly populate histogram buckets.
|
||||
$this->expose_histogram_as_gauges(
|
||||
'http_request_duration_seconds',
|
||||
'HTTP request duration in seconds',
|
||||
$histogram_data,
|
||||
array( 'method', 'endpoint' )
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Database queries total counter.
|
||||
if ( in_array( 'wordpress_db_queries_total', $enabled_metrics, true ) && ! empty( $stored_metrics['counters'] ) ) {
|
||||
foreach ( $stored_metrics['counters'] as $counter_data ) {
|
||||
if ( 'db_queries_total' !== $counter_data['name'] ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$counter = $this->registry->getOrRegisterCounter(
|
||||
$this->namespace,
|
||||
'db_queries_total',
|
||||
'Total number of database queries',
|
||||
array( 'endpoint' )
|
||||
);
|
||||
|
||||
$counter->incBy(
|
||||
(int) $counter_data['value'],
|
||||
array(
|
||||
$counter_data['labels']['endpoint'] ?? 'unknown',
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Database query duration histogram (if SAVEQUERIES is enabled).
|
||||
if ( in_array( 'wordpress_db_queries_total', $enabled_metrics, true ) && ! empty( $stored_metrics['histograms'] ) ) {
|
||||
foreach ( $stored_metrics['histograms'] as $histogram_data ) {
|
||||
if ( 'db_query_duration_seconds' !== $histogram_data['name'] ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->expose_histogram_as_gauges(
|
||||
'db_query_duration_seconds',
|
||||
'Database query duration in seconds',
|
||||
$histogram_data,
|
||||
array( 'endpoint' )
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Expose pre-aggregated histogram data as gauge metrics.
|
||||
*
|
||||
* Since we store histogram data externally, we expose it using gauges
|
||||
* that follow Prometheus histogram naming conventions.
|
||||
*
|
||||
* @param string $name Metric name.
|
||||
* @param string $help Metric description.
|
||||
* @param array $histogram_data Stored histogram data.
|
||||
* @param array $label_names Label names.
|
||||
* @return void
|
||||
*/
|
||||
private function expose_histogram_as_gauges( string $name, string $help, array $histogram_data, array $label_names ): void {
|
||||
$label_values = array();
|
||||
foreach ( $label_names as $label_name ) {
|
||||
$label_values[] = $histogram_data['labels'][ $label_name ] ?? 'unknown';
|
||||
}
|
||||
|
||||
// Expose bucket counts.
|
||||
$bucket_gauge = $this->registry->getOrRegisterGauge(
|
||||
$this->namespace,
|
||||
$name . '_bucket',
|
||||
$help . ' (bucket)',
|
||||
array_merge( $label_names, array( 'le' ) )
|
||||
);
|
||||
|
||||
$cumulative_count = 0;
|
||||
foreach ( $histogram_data['buckets'] as $le => $count ) {
|
||||
$cumulative_count += $count;
|
||||
$bucket_gauge->set(
|
||||
$cumulative_count,
|
||||
array_merge( $label_values, array( $le ) )
|
||||
);
|
||||
}
|
||||
|
||||
// Expose sum.
|
||||
$sum_gauge = $this->registry->getOrRegisterGauge(
|
||||
$this->namespace,
|
||||
$name . '_sum',
|
||||
$help . ' (sum)',
|
||||
$label_names
|
||||
);
|
||||
$sum_gauge->set( $histogram_data['sum'], $label_values );
|
||||
|
||||
// Expose count.
|
||||
$count_gauge = $this->registry->getOrRegisterGauge(
|
||||
$this->namespace,
|
||||
$name . '_count',
|
||||
$help . ' (count)',
|
||||
$label_names
|
||||
);
|
||||
$count_gauge->set( $histogram_data['count'], $label_values );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a custom gauge metric.
|
||||
*
|
||||
|
||||
371
src/Metrics/RuntimeCollector.php
Normal file
371
src/Metrics/RuntimeCollector.php
Normal file
@@ -0,0 +1,371 @@
|
||||
<?php
|
||||
/**
|
||||
* Runtime metrics collector class.
|
||||
*
|
||||
* @package WP_Prometheus
|
||||
*/
|
||||
|
||||
namespace Magdev\WpPrometheus\Metrics;
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* RuntimeCollector class.
|
||||
*
|
||||
* Collects runtime metrics during WordPress request lifecycle.
|
||||
* Stores aggregated data for later retrieval by the Prometheus endpoint.
|
||||
*/
|
||||
class RuntimeCollector {
|
||||
|
||||
/**
|
||||
* Singleton instance.
|
||||
*
|
||||
* @var RuntimeCollector|null
|
||||
*/
|
||||
private static ?RuntimeCollector $instance = null;
|
||||
|
||||
/**
|
||||
* Request start time.
|
||||
*
|
||||
* @var float
|
||||
*/
|
||||
private float $request_start_time;
|
||||
|
||||
/**
|
||||
* Option name for stored metrics.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private const OPTION_NAME = 'wp_prometheus_runtime_metrics';
|
||||
|
||||
/**
|
||||
* Histogram buckets for request duration (in seconds).
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private const DURATION_BUCKETS = array( 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10 );
|
||||
|
||||
/**
|
||||
* Get singleton instance.
|
||||
*
|
||||
* @return RuntimeCollector
|
||||
*/
|
||||
public static function get_instance(): RuntimeCollector {
|
||||
if ( null === self::$instance ) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Private constructor.
|
||||
*/
|
||||
private function __construct() {
|
||||
$this->request_start_time = microtime( true );
|
||||
$this->init_hooks();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize WordPress hooks.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function init_hooks(): void {
|
||||
// Record metrics at the end of the request.
|
||||
add_action( 'shutdown', array( $this, 'record_request_metrics' ), 9999 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Record request metrics at shutdown.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function record_request_metrics(): void {
|
||||
// Skip metrics endpoint requests to avoid self-referential metrics.
|
||||
if ( $this->is_metrics_request() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip AJAX requests for license validation etc. from this plugin.
|
||||
if ( $this->is_plugin_ajax_request() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$enabled_metrics = get_option( 'wp_prometheus_enabled_metrics', array() );
|
||||
$metrics = $this->get_stored_metrics();
|
||||
$duration = microtime( true ) - $this->request_start_time;
|
||||
$status_code = http_response_code() ?: 200;
|
||||
$method = isset( $_SERVER['REQUEST_METHOD'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) : 'GET';
|
||||
$endpoint = $this->get_normalized_endpoint();
|
||||
|
||||
// Record HTTP request count and duration.
|
||||
if ( in_array( 'wordpress_http_requests_total', $enabled_metrics, true ) ) {
|
||||
$this->increment_counter(
|
||||
$metrics,
|
||||
'http_requests_total',
|
||||
array(
|
||||
'method' => $method,
|
||||
'status' => (string) $status_code,
|
||||
'endpoint' => $endpoint,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Record request duration histogram.
|
||||
if ( in_array( 'wordpress_http_request_duration_seconds', $enabled_metrics, true ) ) {
|
||||
$this->observe_histogram(
|
||||
$metrics,
|
||||
'http_request_duration_seconds',
|
||||
$duration,
|
||||
array(
|
||||
'method' => $method,
|
||||
'endpoint' => $endpoint,
|
||||
),
|
||||
self::DURATION_BUCKETS
|
||||
);
|
||||
}
|
||||
|
||||
// Record database query metrics.
|
||||
if ( in_array( 'wordpress_db_queries_total', $enabled_metrics, true ) ) {
|
||||
global $wpdb;
|
||||
$query_count = $wpdb->num_queries;
|
||||
|
||||
$this->increment_counter(
|
||||
$metrics,
|
||||
'db_queries_total',
|
||||
array( 'endpoint' => $endpoint ),
|
||||
$query_count
|
||||
);
|
||||
|
||||
// Track query time if SAVEQUERIES is enabled.
|
||||
if ( defined( 'SAVEQUERIES' ) && SAVEQUERIES && ! empty( $wpdb->queries ) ) {
|
||||
$total_query_time = 0;
|
||||
foreach ( $wpdb->queries as $query ) {
|
||||
$total_query_time += $query[1]; // Query time is the second element.
|
||||
}
|
||||
|
||||
$this->observe_histogram(
|
||||
$metrics,
|
||||
'db_query_duration_seconds',
|
||||
$total_query_time,
|
||||
array( 'endpoint' => $endpoint ),
|
||||
self::DURATION_BUCKETS
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$this->save_stored_metrics( $metrics );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if current request is for the metrics endpoint.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function is_metrics_request(): bool {
|
||||
$request_uri = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '';
|
||||
return strpos( $request_uri, '/metrics' ) !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if current request is a plugin AJAX request.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function is_plugin_ajax_request(): bool {
|
||||
if ( ! wp_doing_ajax() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$action = isset( $_REQUEST['action'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['action'] ) ) : '';
|
||||
return strpos( $action, 'wp_prometheus_' ) === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get normalized endpoint for labeling.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function get_normalized_endpoint(): string {
|
||||
$request_uri = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '/';
|
||||
|
||||
// Remove query string.
|
||||
$path = strtok( $request_uri, '?' );
|
||||
|
||||
// Normalize common patterns.
|
||||
if ( is_admin() ) {
|
||||
return 'admin';
|
||||
}
|
||||
|
||||
if ( wp_doing_ajax() ) {
|
||||
return 'ajax';
|
||||
}
|
||||
|
||||
if ( wp_doing_cron() ) {
|
||||
return 'cron';
|
||||
}
|
||||
|
||||
if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
|
||||
return 'rest-api';
|
||||
}
|
||||
|
||||
if ( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST ) {
|
||||
return 'xmlrpc';
|
||||
}
|
||||
|
||||
// Check for common WordPress patterns.
|
||||
if ( preg_match( '#^/wp-json/#', $path ) ) {
|
||||
return 'rest-api';
|
||||
}
|
||||
|
||||
if ( preg_match( '#^/wp-login\.php#', $path ) ) {
|
||||
return 'login';
|
||||
}
|
||||
|
||||
if ( preg_match( '#^/wp-cron\.php#', $path ) ) {
|
||||
return 'cron';
|
||||
}
|
||||
|
||||
if ( preg_match( '#^/feed/#', $path ) || preg_match( '#/feed/?$#', $path ) ) {
|
||||
return 'feed';
|
||||
}
|
||||
|
||||
// Generic frontend.
|
||||
return 'frontend';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get stored metrics from database.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_stored_metrics(): array {
|
||||
$metrics = get_option( self::OPTION_NAME, array() );
|
||||
|
||||
if ( ! is_array( $metrics ) ) {
|
||||
return array(
|
||||
'counters' => array(),
|
||||
'histograms' => array(),
|
||||
'last_reset' => time(),
|
||||
);
|
||||
}
|
||||
|
||||
return $metrics;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save stored metrics to database.
|
||||
*
|
||||
* @param array $metrics Metrics data.
|
||||
* @return void
|
||||
*/
|
||||
private function save_stored_metrics( array $metrics ): void {
|
||||
update_option( self::OPTION_NAME, $metrics, false );
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment a counter metric.
|
||||
*
|
||||
* @param array $metrics Reference to metrics array.
|
||||
* @param string $name Counter name.
|
||||
* @param array $labels Label values.
|
||||
* @param int $increment Amount to increment (default 1).
|
||||
* @return void
|
||||
*/
|
||||
private function increment_counter( array &$metrics, string $name, array $labels, int $increment = 1 ): void {
|
||||
if ( ! isset( $metrics['counters'] ) ) {
|
||||
$metrics['counters'] = array();
|
||||
}
|
||||
|
||||
$key = $this->make_label_key( $name, $labels );
|
||||
|
||||
if ( ! isset( $metrics['counters'][ $key ] ) ) {
|
||||
$metrics['counters'][ $key ] = array(
|
||||
'name' => $name,
|
||||
'labels' => $labels,
|
||||
'value' => 0,
|
||||
);
|
||||
}
|
||||
|
||||
$metrics['counters'][ $key ]['value'] += $increment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Observe a value in a histogram metric.
|
||||
*
|
||||
* @param array $metrics Reference to metrics array.
|
||||
* @param string $name Histogram name.
|
||||
* @param float $value Observed value.
|
||||
* @param array $labels Label values.
|
||||
* @param array $buckets Bucket boundaries.
|
||||
* @return void
|
||||
*/
|
||||
private function observe_histogram( array &$metrics, string $name, float $value, array $labels, array $buckets ): void {
|
||||
if ( ! isset( $metrics['histograms'] ) ) {
|
||||
$metrics['histograms'] = array();
|
||||
}
|
||||
|
||||
$key = $this->make_label_key( $name, $labels );
|
||||
|
||||
if ( ! isset( $metrics['histograms'][ $key ] ) ) {
|
||||
$bucket_counts = array();
|
||||
foreach ( $buckets as $bucket ) {
|
||||
$bucket_counts[ (string) $bucket ] = 0;
|
||||
}
|
||||
$bucket_counts['+Inf'] = 0;
|
||||
|
||||
$metrics['histograms'][ $key ] = array(
|
||||
'name' => $name,
|
||||
'labels' => $labels,
|
||||
'buckets' => $bucket_counts,
|
||||
'sum' => 0.0,
|
||||
'count' => 0,
|
||||
);
|
||||
}
|
||||
|
||||
// Increment bucket counts.
|
||||
foreach ( $buckets as $bucket ) {
|
||||
if ( $value <= $bucket ) {
|
||||
$metrics['histograms'][ $key ]['buckets'][ (string) $bucket ]++;
|
||||
}
|
||||
}
|
||||
$metrics['histograms'][ $key ]['buckets']['+Inf']++;
|
||||
|
||||
// Update sum and count.
|
||||
$metrics['histograms'][ $key ]['sum'] += $value;
|
||||
$metrics['histograms'][ $key ]['count']++;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a unique key from name and labels.
|
||||
*
|
||||
* @param string $name Metric name.
|
||||
* @param array $labels Label values.
|
||||
* @return string
|
||||
*/
|
||||
private function make_label_key( string $name, array $labels ): string {
|
||||
ksort( $labels );
|
||||
return $name . ':' . wp_json_encode( $labels );
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset stored metrics.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function reset_metrics(): void {
|
||||
delete_option( self::OPTION_NAME );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get histogram buckets.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_duration_buckets(): array {
|
||||
return self::DURATION_BUCKETS;
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ use Magdev\WpPrometheus\Admin\Settings;
|
||||
use Magdev\WpPrometheus\Endpoint\MetricsEndpoint;
|
||||
use Magdev\WpPrometheus\License\Manager as LicenseManager;
|
||||
use Magdev\WpPrometheus\Metrics\Collector;
|
||||
use Magdev\WpPrometheus\Metrics\RuntimeCollector;
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
@@ -90,6 +91,20 @@ final class Plugin {
|
||||
new Settings();
|
||||
}
|
||||
|
||||
// Initialize runtime collector for request metrics (always runs to collect data).
|
||||
// Only collect if at least one runtime metric is enabled.
|
||||
$enabled_metrics = get_option( 'wp_prometheus_enabled_metrics', array() );
|
||||
$runtime_metrics = array(
|
||||
'wordpress_http_requests_total',
|
||||
'wordpress_http_request_duration_seconds',
|
||||
'wordpress_db_queries_total',
|
||||
);
|
||||
$has_runtime_metrics = ! empty( array_intersect( $runtime_metrics, $enabled_metrics ) );
|
||||
|
||||
if ( $has_runtime_metrics && LicenseManager::is_license_valid() ) {
|
||||
RuntimeCollector::get_instance();
|
||||
}
|
||||
|
||||
// Initialize metrics endpoint (only if licensed).
|
||||
if ( LicenseManager::is_license_valid() ) {
|
||||
$this->collector = new Collector();
|
||||
|
||||
Reference in New Issue
Block a user