. * @since 3.0.0 */ namespace Decalog\Plugin\Feature; use Prometheus\CollectorRegistry; use Prometheus\Exception\MetricNotFoundException; use Prometheus\Storage\InMemory; use Decalog\System\Option; use Decalog\System\Environment; use Decalog\Logger; use Decalog\Plugin\Feature\ClassTypes; use Decalog\System\Markdown; use Decalog\Listener\AbstractListener; /** * Main DecaLog monitor class. * * This class defines all code necessary to monitor metrics with DecaLog. * * @package Features * @author Pierre Lannoy . * @since 3.0.0 */ class DMonitor { /** * The class of the component. * * @since 3.0.0 * @var string $class Maintains the class of the component. */ protected $class = 'unknwon'; /** * The name of the component. * * @since 3.0.0 * @var string $class Maintains the name of the component. */ protected $name = 'unknown'; /** * The version of the component. * * @since 3.0.0 * @var string $version Maintains the version of the component. */ protected $version = '-'; /** * Is metrics mode active? * * @since 3.7.0 * @var boolean $active True if there is at least one running metrics logger. */ public static $active = false; /** * The metrics registry. * * @since 3.0.0 * @var array $metrics_registry Maintains the metrics definitions. */ private static $metrics_registry = []; /** * The "production" CollectorRegistry instance. * * @since 3.0.0 * @var \Prometheus\CollectorRegistry $production Maintains the internal CollectorRegistry instance. */ private static $production = null; /** * The "development" CollectorRegistry instance. * * @since 3.0.0 * @var \Prometheus\CollectorRegistry $development Maintains the internal CollectorRegistry instance. */ private static $development = null; /** * The internal logger. * * @since 3.0.0 * @var \Decalog\Logger $logger Maintains the logger. */ private static $logger = null; /** * The technical metrics labels names. * * @since 3.0.0 * @var array $label_names The names list. */ private $label_names = [ 'prod' => [ 'channel', 'environment' ], 'dev' => [ 'channel', 'environment' ], ]; /** * The metrics labels values. * * @since 3.0.0 * @var array $label_values The values list. */ private $label_values = []; /** * Is logger allowed to run. * * @since 3.0.0 * @var boolean $allowed Maintains the allowed status of the monitor. */ private $allowed = true; /** * Is the first initialization done? * * @since 3.0.0 * @var boolean $self_initialized Is the first initialization done? */ private static $self_initialized = false; /** * Initialize the class and set its properties. * * @param string $class The class identifier, must be in ClassTypes::$classes. * @param string $name Optional. The name of the component. * @param string $version Optional. The version of the component. * @since 3.0.0 */ public function __construct( $class, $name = null, $version = null ) { if ( ! isset( self::$logger ) && class_exists( '\Decalog\Logger' ) ) { self::$logger = new \Decalog\Logger( 'plugin', DECALOG_PRODUCT_NAME, DECALOG_VERSION ); } if ( ! isset( self::$logger ) && ! class_exists( '\Decalog\Logger' ) ) { self::$logger = new \Psr\Log\NullLogger(); } if ( ! Option::network_get( 'autolisteners' ) ) { $this->allowed = in_array( 'prom', Option::network_get( 'listeners' ), true ); } if ( $this->allowed ) { if ( in_array( $class, ClassTypes::$classes, true ) ) { $this->class = $class; } if ( $name && is_string( $name ) ) { $this->name = $name; } if ( $version && is_string( $version ) ) { $this->version = $version; } if ( ! isset( self::$production ) ) { self::$production = new CollectorRegistry( new InMemory(), false ); } if ( ! isset( self::$development ) ) { self::$development = new CollectorRegistry( new InMemory(), false ); } $this->label_values = [ 'prod' => [ $this->normalize_string( $this->current_channel_tag() ), $this->normalize_string( Environment::stage() ) ], 'dev' => [ $this->normalize_string( $this->current_channel_tag() ), $this->normalize_string( Environment::stage() ) ], ]; self::$logger->debug( 'A new instance of DecaLog monitor is initialized and operational.' ); } else { self::$logger->debug( 'Skipped initialization of a DecaLog monitor.' ); } if ( ! self::$self_initialized ) { self::$self_initialized = true; $this->class = 'plugin'; $this->name = DECALOG_PRODUCT_NAME; $this->version = DECALOG_VERSION; foreach ( EventTypes::$levels as $key => $level ) { $this->create_dev_counter( 'event_' . $key, 'Number of handled ' . $key . ' events per request - [count]' ); } $this->create_prod_counter( 'metric_prod', 'Number of handled `production` metrics - [count]' ); $this->create_dev_counter( 'metric_dev', 'Number of handled `development` metrics - [count]' ); add_action( 'shutdown', [ $this, 'before_close' ], AbstractListener::$monitor_priority + 1, 0 ); $this->class = $class; $this->name = $name; $this->version = $version; } } /** * Self-metrics handling.. * * @since 3.0.0 */ public function before_close() { if ( $this->allowed ) { $class = $this->class; $name = $this->name; $version = $this->version; $this->class = 'plugin'; $this->name = DECALOG_PRODUCT_NAME; $this->version = DECALOG_VERSION; foreach ( EventTypes::$levels as $key => $level ) { $this->inc_dev_counter( 'event_' . $key, DLogger::count( $key ) ); } $this->inc_prod_counter( 'metric_prod', count( self::$production->getMetricFamilySamples() ) ); $this->inc_dev_counter( 'metric_dev', count( self::$development->getMetricFamilySamples() ) ); $this->class = $class; $this->name = $name; $this->version = $version; } } /** * Get the metrics registry. * * @return array The registry; * @since 3.0.0 */ public static function registry() { $allowed = true; if ( ! Option::network_get( 'autolisteners' ) ) { $allowed = in_array( 'prom', Option::network_get( 'listeners' ), true ); } if ( $allowed ) { return self::$metrics_registry; } return []; } /** * Register the metrics. * * @param boolean $prod True if it's production profile, false if it's development profile. * @param string $type The type of the metrics (counter, gauge or histogram). * @param string $name The unique name of the metrics. * @param string $help The help string associated with this metrics. * @since 3.0.0 */ private function register( $prod, $type, $name, $help ) { $prod = $prod ? 'production' : 'development'; if ( ! array_key_exists( $this->class, self::$metrics_registry ) ) { self::$metrics_registry[ $this->class ] = []; } if ( ! array_key_exists( $prod, self::$metrics_registry[ $this->class ] ) ) { self::$metrics_registry[ $this->class ][ $prod ] = []; } if ( ! array_key_exists( $type, self::$metrics_registry[ $this->class ][ $prod ] ) ) { self::$metrics_registry[ $this->class ][ $prod ][ $type ] = []; } $idx = $this->current_namespace() . '_' . $name; if ( ! array_key_exists( $idx, self::$metrics_registry[ $this->class ][ $prod ][ $type ] ) ) { self::$metrics_registry[ $this->class ][ $prod ][ $type ][ $this->current_namespace() . '_' . $name ] = [ 'name' => $this->name, 'version' => $this->version, 'help' => $help, ]; } } /** * Normalizes the metrics name. * * @param string $name The unique name of the metrics. * @@return string The normalized name. * @since 3.0.0 */ private function normalize( $name ) { return ( preg_replace( '/[^a-zA-Z0-9_]/', '', $name ) ?? 'unknown_' . md5( (string) $name ) ); } /** * Creates the named counter, in the right profile. * * @param boolean $prod True if it's production profile, false if it's development profile. * @param string $name The unique name of the counter. * @param string $help The help string associated with this counter. * @since 3.0.0 */ private function create_counter( $prod, $name, $help ) { if ( ! $this->allowed ) { return; } if ( '' === $name ) { self::$logger->error( 'A metric must be named.' ); } $name = $this->normalize( $name ); try { $registry = ( $prod ? self::$production : self::$development ); $registry->registerCounter( $this->current_namespace(), $name, $help, $this->label_names[ ( $prod ? 'prod' : 'dev' ) ] ); $this->register( $prod, 'counter', $name, $help ); $this->init_counter( $prod, $name ); } catch ( \Throwable $e ) { self::$logger->error( $e->getMessage(), [ 'code' => $e->getCode() ] ); } } /** * Creates and sets the named gauge, in the right profile. * * @param boolean $prod True if it's production profile, false if it's development profile. * @param string $name The unique name of the gauge. * @param int|float $value The initial value to set. * @param string $help The help string associated with this gauge. * @since 3.0.0 */ private function create_gauge( $prod, $name, $value, $help ) { if ( ! $this->allowed ) { return; } if ( '' === $name ) { self::$logger->error( 'A metric must be named.' ); } $name = $this->normalize( $name ); try { $registry = ( $prod ? self::$production : self::$development ); $registry->registerGauge( $this->current_namespace(), $name, $help, $this->label_names[ ( $prod ? 'prod' : 'dev' ) ] ); $this->register( $prod, 'gauge', $name, $help ); $this->set_gauge( $prod, $name, $value ); } catch ( \Throwable $e ) { self::$logger->error( $e->getMessage(), [ 'code' => $e->getCode() ] ); } } /** * Creates the named histogram, in the right profile. * * @param boolean $prod True if it's production profile, false if it's development profile. * @param string $name The unique name of the histogram. * @param null|array $buckets The buckets. * @param string $help The help string associated with this histogram. * @since 3.0.0 */ private function create_histogram( $prod, $name, $buckets, $help ) { if ( ! $this->allowed ) { return; } if ( '' === $name ) { self::$logger->error( 'A metric must be named.' ); } $name = $this->normalize( $name ); try { $registry = ( $prod ? self::$production : self::$development ); $registry->registerHistogram( $this->current_namespace(), $name, $help, $this->label_names[ ( $prod ? 'prod' : 'dev' ) ], $buckets ); $this->register( $prod, 'histogram', $name, $help ); } catch ( \Throwable $e ) { self::$logger->error( $e->getMessage(), [ 'code' => $e->getCode() ] ); } } /** * Inits the named counter, in the right profile. * * @param boolean $prod True if it's production profile, false if it's development profile. * @param string $name The unique name of the counter. * @since 3.0.0 */ private function init_counter( $prod, $name ) { if ( ! $this->allowed ) { return; } if ( '' === $name ) { self::$logger->error( 'A metric must be named.' ); } $name = $this->normalize( $name ); try { $registry = ( $prod ? self::$production : self::$development ); $counter = $registry->getCounter( $this->current_namespace(), $name ); $counter->incBy( 0, $this->label_values[ ( $prod ? 'prod' : 'dev' ) ] ); } catch ( \Throwable $e ) { if ( $e instanceof MetricNotFoundException ) { if ( Option::network_get( 'unknown_metrics_warn', true ) ) { self::$logger->error( $e->getMessage(), [ 'code' => $e->getCode() ] ); } } else { self::$logger->error( $e->getMessage(), [ 'code' => $e->getCode() ] ); } } } /** * Sets the named gauge, in the right profile. * * @param boolean $prod True if it's production profile, false if it's development profile. * @param string $name The unique name of the gauge. * @param int|float $value The value to set. * @since 3.0.0 */ private function set_gauge( $prod, $name, $value ) { if ( ! $this->allowed ) { return; } if ( '' === $name ) { self::$logger->error( 'A metric must be named.' ); } $name = $this->normalize( $name ); try { $registry = ( $prod ? self::$production : self::$development ); $gauge = $registry->getGauge( $this->current_namespace(), $name ); $gauge->set( $value, $this->label_values[ ( $prod ? 'prod' : 'dev' ) ] ); } catch ( \Throwable $e ) { if ( $e instanceof MetricNotFoundException ) { if ( Option::network_get( 'unknown_metrics_warn', true ) ) { self::$logger->error( $e->getMessage(), [ 'code' => $e->getCode() ] ); } } else { self::$logger->error( $e->getMessage(), [ 'code' => $e->getCode() ] ); } } } /** * Increments the named counter, in the right profile. * * @param boolean $prod True if it's production profile, false if it's development profile. * @param string $name The unique name of the counter. * @param int|float $value The value of how much to increment. * @since 3.0.0 */ private function inc_counter( $prod, $name, $value ) { if ( ! $this->allowed ) { return; } if ( '' === $name ) { self::$logger->error( 'A metric must be named.' ); } $name = $this->normalize( $name ); try { $registry = ( $prod ? self::$production : self::$development ); $counter = $registry->getCounter( $this->current_namespace(), $name ); $counter->incBy( $value, $this->label_values[ ( $prod ? 'prod' : 'dev' ) ] ); } catch ( \Throwable $e ) { if ( $e instanceof MetricNotFoundException ) { if ( Option::network_get( 'unknown_metrics_warn', true ) ) { self::$logger->error( $e->getMessage(), [ 'code' => $e->getCode() ] ); } } else { self::$logger->error( $e->getMessage(), [ 'code' => $e->getCode() ] ); } } } /** * Increments the named gauge, in the right profile. * * @param boolean $prod True if it's production profile, false if it's development profile. * @param string $name The unique name of the gauge. * @param int|float $value The value of how much to increment. * @since 3.0.0 */ private function inc_gauge( $prod, $name, $value ) { if ( ! $this->allowed ) { return; } if ( '' === $name ) { self::$logger->error( 'A metric must be named.' ); } $name = $this->normalize( $name ); try { $registry = ( $prod ? self::$production : self::$development ); $gauge = $registry->getGauge( $this->current_namespace(), $name ); $gauge->incBy( $value, $this->label_values[ ( $prod ? 'prod' : 'dev' ) ] ); } catch ( \Throwable $e ) { if ( $e instanceof MetricNotFoundException ) { if ( Option::network_get( 'unknown_metrics_warn', true ) ) { self::$logger->error( $e->getMessage(), [ 'code' => $e->getCode() ] ); } } else { self::$logger->error( $e->getMessage(), [ 'code' => $e->getCode() ] ); } } } /** * Adds an observation to the named histogram, in the right profile. * * @param boolean $prod True if it's production profile, false if it's development profile. * @param string $name The unique name of the histogram. * @param int|float $value The value to add. * @since 3.0.0 */ private function observe_histogram( $prod, $name, $value ) { if ( ! $this->allowed ) { return; } if ( '' === $name ) { self::$logger->error( 'A metric must be named.' ); } $name = $this->normalize( $name ); try { $registry = ( $prod ? self::$production : self::$development ); $histogram = $registry->getHistogram( $this->current_namespace(), $name ); $histogram->observe( $value, $this->label_values[ ( $prod ? 'prod' : 'dev' ) ] ); } catch ( \Throwable $e ) { if ( $e instanceof MetricNotFoundException ) { if ( Option::network_get( 'unknown_metrics_warn', true ) ) { self::$logger->error( $e->getMessage(), [ 'code' => $e->getCode() ] ); } } else { self::$logger->error( $e->getMessage(), [ 'code' => $e->getCode() ] ); } } } /** * Create the named counter, in production profile. * * @param string $name The unique name of the counter. * @param string $help Optional. The help string associated with this counter. * @since 3.0.0 */ public function create_prod_counter( $name, $help = '' ) { $this->create_counter( true, $name, $help ); } /** * Increments the named counter, in production profile. * * @param string $name The unique name of the counter. * @param int|float $value The value of how much to increment. * @since 3.0.0 */ public function inc_prod_counter( $name, $value ) { $this->inc_counter( true, $name, $value ); } /** * Create the named counter, in development profile. * * @param string $name The unique name of the counter. * @param string $help Optional. The help string associated with this counter. * @since 3.0.0 */ public function create_dev_counter( $name, $help = '' ) { $this->create_counter( false, $name, $help ); } /** * Increments the named counter, in development profile. * * @param string $name The unique name of the counter. * @param int|float $value The value of how much to increment. * @since 3.0.0 */ public function inc_dev_counter( $name, $value ) { $this->inc_counter( false, $name, $value ); } /** * Create and set the named gauge, in production profile. * * @param string $name The unique name of the gauge. * @param int|float $value Optional. The initial value to set. * @param string $help Optional. The help string associated with this gauge. * @since 3.0.0 */ public function create_prod_gauge( $name, $value = 0, $help = '' ) { $this->create_gauge( true, $name, $value, $help ); } /** * Sets the named gauge, in production profile. * * @param string $name The unique name of the gauge. * @param int|float $value The value to set. * @since 3.0.0 */ public function set_prod_gauge( $name, $value ) { $this->set_gauge( true, $name, $value ); } /** * Increments the named gauge, in production profile. * * @param string $name The unique name of the gauge. * @param int|float $value The value of how much to increment. * @since 3.0.0 */ public function inc_prod_gauge( $name, $value ) { $this->inc_gauge( true, $name, $value ); } /** * Create and set the named gauge, in development profile. * * @param string $name The unique name of the gauge. * @param int|float $value Optional. The initial value to set. * @param string $help Optional. The help string associated with this gauge. * @since 3.0.0 */ public function create_dev_gauge( $name, $value = 0, $help = '' ) { $this->create_gauge( false, $name, $value, $help ); } /** * Sets the named gauge, in development profile. * * @param string $name The unique name of the gauge. * @param int|float $value The value to set. * @since 3.0.0 */ public function set_dev_gauge( $name, $value ) { $this->set_gauge( false, $name, $value ); } /** * Increments the named gauge, in development profile. * * @param string $name The unique name of the gauge. * @param int|float $value The value of how much to increment. * @since 3.0.0 */ public function inc_dev_gauge( $name, $value ) { $this->inc_gauge( false, $name, $value ); } /** * Creates the named histogram, in production profile. * * @param string $name The unique name of the histogram. * @param null|array $buckets Optional. The buckets. * @param string $help Optional. The help string associated with this histogram. * @since 3.0.0 */ public function create_prod_histogram( $name, $buckets = null, $help = '' ) { $this->create_histogram( true, $name, $buckets, $help ); } /** * Adds an observation to the named histogram, in production profile. * * @param string $name The unique name of the histogram. * @param int|float $value The value to add. * @since 3.0.0 */ public function observe_prod_histogram( $name, $value ) { $this->observe_histogram( true, $name, $value ); } /** * Creates the named histogram, in development profile. * * @param string $name The unique name of the histogram. * @param null|array $buckets Optional. The buckets. * @param string $help Optional. The help string associated with this histogram. * @since 3.0.0 */ public function create_dev_histogram( $name, $buckets = null, $help = '' ) { $this->create_histogram( false, $name, $buckets, $help ); } /** * Adds an observation to the named histogram, in development profile. * * @param string $name The unique name of the histogram. * @param int|float $value The value to add. * @since 3.0.0 */ public function observe_dev_histogram( $name, $value ) { $this->observe_histogram( false, $name, $value ); } /** * Get the current namespace. * * @return string The current namespace. * @since 3.0.0 */ private function current_namespace() { $class = strtolower( $this->class ); $name = strtolower( str_replace( ' ', '', $this->name ) ); // TODO : replace all non /w char return 'wordpress_' . $class . '_' . $name; } /** * Get the current channel tag. * * @return string The current channel tag. * @since 3.0.0 */ private function current_channel_tag() { return $this->channel_tag( Environment::exec_mode() ); } /** * Get the channel tag. * * @param integer $id Optional. The channel id (execution mode). * @return string The channel tag. * @since 3.0.0 */ private function channel_tag( $id = 0 ) { if ( $id >= count( ChannelTypes::$channels ) ) { $id = 0; } return strtolower( ChannelTypes::$channels[ $id ] ); } /** * Normalize a string. * * @param string $string The string. * @return string The normalized string. * @since 1.10.0+ */ private function normalize_string( $string ) { $string = str_replace( '"', '“', $string ); $string = str_replace( '\'', '`', $string ); return decalog_filter_string( $string ); } /** * Get the collector registry for production profile. * * @return \Prometheus\CollectorRegistry The production registry. * @since 3.0.0 */ public function prod_registry() { return self::$production; } /** * Get the collector registry for development profile. * * @return \Prometheus\CollectorRegistry The development registry. * @since 3.0.0 */ public function dev_registry() { return self::$development; } /** * Get the metrics definitions. * * @return array The output of the shortcode, ready to print. * @since 3.0.0 */ public static function get_metrics_definition() { $content = []; foreach ( self::$metrics_registry as $class => $class_detail ) { foreach ( $class_detail as $env => $env_detail ) { foreach ( $env_detail as $type => $type_detail ) { foreach ( $type_detail as $metrics => $detail ) { $content[ $metrics ] = [ 'class' => $class, 'profile' => $env, 'type' => $type, 'name' => $metrics, 'source' => $detail['name'], 'version' => $detail['version'], 'description' => $detail['help'], ]; } } } } return $content; } /** * Get the metrics definitions. * * @param array $attributes 'style' => 'markdown', 'html'. * 'mode' => 'raw', 'clean'. * @return string The output of the shortcode, ready to print. * @since 3.0.0 */ public static function sc_get_metrics( $attributes ) { $content = '
'; foreach ( self::$metrics_registry as $class => $class_detail ) { $content .= '


'; $content .= '

' . strtoupper( $class ) . ' CLASS

'; foreach ( $class_detail as $env => $env_detail ) { $content .= '

' . ucfirst( $env ) . ' Profile

'; foreach ( $env_detail as $type => $type_detail ) { $content .= ''; } } } $content .= '
'; return $content; } } add_shortcode( 'decalog-metrics', [ 'Decalog\Plugin\Feature\DMonitor', 'sc_get_metrics' ] );