From df3b8a7ec2b208b2f09f8290f0bea58431fce6fd Mon Sep 17 00:00:00 2001 From: magdev Date: Mon, 2 Feb 2026 20:46:22 +0100 Subject: [PATCH] feat: Add Prometheus metrics integration - Add includes/Prometheus/Integration.php with metrics collection - Expose content, engagement, user, WooCommerce, and ActivityPub metrics - Add settings toggle in Integrations tab - Requires WP Prometheus plugin to be active - Bump version to 0.5.0 Co-Authored-By: Claude Opus 4.5 --- CHANGELOG.md | 13 + README.md | 14 +- includes/Installer.php | 1 + includes/Plugin.php | 39 ++- includes/Prometheus/Integration.php | 422 ++++++++++++++++++++++++++++ includes/Prometheus/index.php | 1 + wp-fedistream.php | 4 +- 7 files changed, 477 insertions(+), 17 deletions(-) create mode 100644 includes/Prometheus/Integration.php create mode 100644 includes/Prometheus/index.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 947a533..f344ebe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.5.0] - 2026-02-02 + +### Added + +- **Prometheus Metrics Integration** - Expose FediStream metrics for monitoring + - Content metrics: `fedistream_content_total` (by type/status), `fedistream_genres_total`, `fedistream_moods_total` + - Engagement metrics: `fedistream_plays_total`, `fedistream_plays_today`, `fedistream_favorites_total`, `fedistream_local_follows_total`, `fedistream_listening_history_entries` + - User metrics: `fedistream_users_with_library`, `fedistream_users_following_artists`, `fedistream_notifications_total`, `fedistream_notifications_pending` + - WooCommerce metrics (conditional): `fedistream_purchases_total`, `fedistream_customers_total`, `fedistream_products_total` + - ActivityPub metrics (conditional): `fedistream_activitypub_followers_total`, `fedistream_activitypub_followers_by_artist`, `fedistream_activitypub_reactions_total` + - New setting in Integrations tab to enable/disable Prometheus metrics + - Requires WP Prometheus plugin to be active + ## [0.4.9] - 2026-02-02 ### Changed diff --git a/README.md b/README.md index dc1298a..1afd375 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Stream music over ActivityPub - Build your own music streaming platform for Musicians and Labels. -[![Version](https://img.shields.io/badge/version-0.4.9-blue.svg)](CHANGELOG.md) +[![Version](https://img.shields.io/badge/version-0.5.0-blue.svg)](CHANGELOG.md) [![PHP](https://img.shields.io/badge/PHP-%3E%3D8.3-purple.svg)](https://php.net) [![WordPress](https://img.shields.io/badge/WordPress-%3E%3D6.4-blue.svg)](https://wordpress.org) [![License](https://img.shields.io/badge/license-GPL--2.0%2B-green.svg)](https://www.gnu.org/licenses/gpl-2.0.html) @@ -39,18 +39,6 @@ WP FediStream is a WordPress plugin that enables musicians, bands, and labels to - [ActivityPub Plugin](https://wordpress.org/plugins/activitypub/) - For Fediverse integration - [WooCommerce](https://woocommerce.com/) 10.0+ - For selling music -## Known Issues - -### Plugin Incompatibilities - -The following plugins are known to cause issues when used together with WP FediStream: - -| Plugin | Issue | Status | -|---------------|-------------------------------------------------------------------|---------------------| -| WP Prometheus | Memory exhaustion (PHP Fatal error) when viewing FediStream pages | Under investigation | - -If you encounter memory issues or PHP fatal errors, try deactivating these plugins temporarily to isolate the problem. - ## License Key WP FediStream requires a valid license key for frontend functionality (player, shortcodes, ActivityPub). The admin dashboard works without a license, allowing you to configure the plugin before activation. diff --git a/includes/Installer.php b/includes/Installer.php index 9be9892..fa7130a 100644 --- a/includes/Installer.php +++ b/includes/Installer.php @@ -347,6 +347,7 @@ class Installer { $defaults = array( 'wp_fedistream_enable_activitypub' => 1, 'wp_fedistream_enable_woocommerce' => 0, + 'wp_fedistream_enable_prometheus' => 0, 'wp_fedistream_audio_formats' => array( 'mp3', 'wav', 'flac', 'ogg' ), 'wp_fedistream_max_upload_size' => 50, // MB 'wp_fedistream_default_license' => 'all-rights-reserved', diff --git a/includes/Plugin.php b/includes/Plugin.php index 4b95bba..6e392d1 100644 --- a/includes/Plugin.php +++ b/includes/Plugin.php @@ -16,6 +16,7 @@ use WP_FediStream\Frontend\TemplateLoader; use WP_FediStream\Frontend\Widgets; use WP_FediStream\PostTypes\Artist; use WP_FediStream\WooCommerce\Integration as WooCommerceIntegration; +use WP_FediStream\Prometheus\Integration as PrometheusIntegration; use WP_FediStream\WooCommerce\DigitalDelivery; use WP_FediStream\WooCommerce\StreamingAccess; use WP_FediStream\PostTypes\Album; @@ -207,6 +208,11 @@ final class Plugin { new StreamingAccess(); } + // Initialize Prometheus integration. + if ( get_option( 'wp_fedistream_enable_prometheus', 0 ) && $this->is_prometheus_active() ) { + new PrometheusIntegration(); + } + // Initialize user library and notifications. new UserLibrary(); new LibraryPage(); @@ -448,6 +454,7 @@ final class Plugin { // Get current settings. $enable_activitypub = get_option( 'wp_fedistream_enable_activitypub', 1 ); $enable_woocommerce = get_option( 'wp_fedistream_enable_woocommerce', 0 ); + $enable_prometheus = get_option( 'wp_fedistream_enable_prometheus', 0 ); $max_upload_size = get_option( 'wp_fedistream_max_upload_size', 50 ); $default_license = get_option( 'wp_fedistream_default_license', 'all-rights-reserved' ); @@ -486,7 +493,7 @@ final class Plugin { $this->render_settings_tab( $max_upload_size, $default_license ); break; case 'integrations': - $this->render_integrations_tab( $enable_activitypub, $enable_woocommerce ); + $this->render_integrations_tab( $enable_activitypub, $enable_woocommerce, $enable_prometheus ); break; } ?> @@ -525,6 +532,7 @@ final class Plugin { case 'integrations': update_option( 'wp_fedistream_enable_activitypub', isset( $_POST['enable_activitypub'] ) ? 1 : 0 ); update_option( 'wp_fedistream_enable_woocommerce', isset( $_POST['enable_woocommerce'] ) ? 1 : 0 ); + update_option( 'wp_fedistream_enable_prometheus', isset( $_POST['enable_prometheus'] ) ? 1 : 0 ); echo '

' . esc_html__( 'Integration settings saved.', 'wp-fedistream' ) . '

'; break; } @@ -719,9 +727,10 @@ final class Plugin { * * @param int $enable_activitypub Whether ActivityPub is enabled. * @param int $enable_woocommerce Whether WooCommerce integration is enabled. + * @param int $enable_prometheus Whether Prometheus metrics are enabled. * @return void */ - private function render_integrations_tab( int $enable_activitypub, int $enable_woocommerce ): void { + private function render_integrations_tab( int $enable_activitypub, int $enable_woocommerce, int $enable_prometheus ): void { ?>
@@ -760,6 +769,23 @@ final class Plugin { + + + + + is_prometheus_active() ) : ?> +

+ + +

+ +

+ + + @@ -933,4 +959,13 @@ final class Plugin { public function is_activitypub_active(): bool { return class_exists( 'Activitypub\Activitypub' ); } + + /** + * Check if WP Prometheus plugin is active. + * + * @return bool + */ + public function is_prometheus_active(): bool { + return defined( 'WP_PROMETHEUS_VERSION' ) || class_exists( 'WP_Prometheus\Plugin' ); + } } diff --git a/includes/Prometheus/Integration.php b/includes/Prometheus/Integration.php new file mode 100644 index 0000000..7fa9a68 --- /dev/null +++ b/includes/Prometheus/Integration.php @@ -0,0 +1,422 @@ +check_prometheus(); + + // If plugins_loaded hasn't fully completed, hook init at priority 20. + // Otherwise, run init directly. + if ( ! did_action( 'plugins_loaded' ) || doing_action( 'plugins_loaded' ) ) { + add_action( 'plugins_loaded', array( $this, 'init' ), 20 ); + } else { + $this->init(); + } + } + + /** + * Check if WP Prometheus is active. + * + * @return void + */ + public function check_prometheus(): void { + $this->prometheus_active = defined( 'WP_PROMETHEUS_VERSION' ) + || class_exists( 'WP_Prometheus\Plugin' ); + } + + /** + * Initialize Prometheus integration. + * + * @return void + */ + public function init(): void { + if ( ! $this->prometheus_active ) { + return; + } + + // Register metrics collector. + add_action( 'wp_prometheus_collect_metrics', array( $this, 'collect_metrics' ) ); + } + + /** + * Check if Prometheus is active. + * + * @return bool + */ + public function is_active(): bool { + return $this->prometheus_active; + } + + /** + * Collect FediStream metrics. + * + * @param object $collector The Prometheus collector. + * @return void + */ + public function collect_metrics( $collector ): void { + // Register and collect all metric categories. + $this->collect_content_metrics( $collector ); + $this->collect_engagement_metrics( $collector ); + $this->collect_user_metrics( $collector ); + + if ( $this->is_woocommerce_enabled() ) { + $this->collect_woocommerce_metrics( $collector ); + } + + if ( $this->is_activitypub_enabled() ) { + $this->collect_activitypub_metrics( $collector ); + } + } + + /** + * Collect content metrics. + * + * @param object $collector The Prometheus collector. + * @return void + */ + private function collect_content_metrics( $collector ): void { + // Content count by type and status. + $content_gauge = $collector->register_gauge( + 'fedistream_content_total', + 'Total FediStream content count', + array( 'type', 'status' ) + ); + + $post_types = array( + 'fedistream_artist', + 'fedistream_album', + 'fedistream_track', + 'fedistream_playlist', + ); + + foreach ( $post_types as $post_type ) { + $type_label = str_replace( 'fedistream_', '', $post_type ); + $counts = wp_count_posts( $post_type ); + + foreach ( array( 'publish', 'draft', 'pending' ) as $status ) { + $count = isset( $counts->$status ) ? (int) $counts->$status : 0; + $content_gauge->set( $count, array( $type_label, $status ) ); + } + } + + // Genre count. + $genres_gauge = $collector->register_gauge( + 'fedistream_genres_total', + 'Total number of genres', + array() + ); + $genre_count = wp_count_terms( array( 'taxonomy' => 'fedistream_genre' ) ); + $genres_gauge->set( is_wp_error( $genre_count ) ? 0 : (int) $genre_count, array() ); + + // Mood count. + $moods_gauge = $collector->register_gauge( + 'fedistream_moods_total', + 'Total number of moods', + array() + ); + $mood_count = wp_count_terms( array( 'taxonomy' => 'fedistream_mood' ) ); + $moods_gauge->set( is_wp_error( $mood_count ) ? 0 : (int) $mood_count, array() ); + } + + /** + * Collect engagement metrics. + * + * @param object $collector The Prometheus collector. + * @return void + */ + private function collect_engagement_metrics( $collector ): void { + global $wpdb; + + $plays_table = $wpdb->prefix . 'fedistream_plays'; + $favorites_table = $wpdb->prefix . 'fedistream_favorites'; + $follows_table = $wpdb->prefix . 'fedistream_user_follows'; + $history_table = $wpdb->prefix . 'fedistream_listening_history'; + + // Total plays. + $plays_gauge = $collector->register_gauge( + 'fedistream_plays_total', + 'Total track plays', + array() + ); + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching + $total_plays = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$plays_table}" ); + $plays_gauge->set( $total_plays, array() ); + + // Plays today. + $plays_today_gauge = $collector->register_gauge( + 'fedistream_plays_today', + 'Track plays today', + array() + ); + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching + $plays_today = (int) $wpdb->get_var( + "SELECT COUNT(*) FROM {$plays_table} WHERE DATE(played_at) = CURDATE()" + ); + $plays_today_gauge->set( $plays_today, array() ); + + // Favorites by type. + $favorites_gauge = $collector->register_gauge( + 'fedistream_favorites_total', + 'Total favorites by content type', + array( 'type' ) + ); + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching + $favorites_by_type = $wpdb->get_results( + "SELECT content_type, COUNT(*) as count FROM {$favorites_table} GROUP BY content_type" + ); + foreach ( $favorites_by_type as $row ) { + $favorites_gauge->set( (int) $row->count, array( $row->content_type ) ); + } + + // Local follows. + $follows_gauge = $collector->register_gauge( + 'fedistream_local_follows_total', + 'Total local artist follows', + array() + ); + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching + $total_follows = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$follows_table}" ); + $follows_gauge->set( $total_follows, array() ); + + // Listening history entries. + $history_gauge = $collector->register_gauge( + 'fedistream_listening_history_entries', + 'Total listening history entries', + array() + ); + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching + $history_count = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$history_table}" ); + $history_gauge->set( $history_count, array() ); + } + + /** + * Collect user metrics. + * + * @param object $collector The Prometheus collector. + * @return void + */ + private function collect_user_metrics( $collector ): void { + global $wpdb; + + $favorites_table = $wpdb->prefix . 'fedistream_favorites'; + $follows_table = $wpdb->prefix . 'fedistream_user_follows'; + $notifications_table = $wpdb->prefix . 'fedistream_notifications'; + + // Users with library (favorites). + $users_library_gauge = $collector->register_gauge( + 'fedistream_users_with_library', + 'Users who have favorites', + array() + ); + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching + $users_with_library = (int) $wpdb->get_var( + "SELECT COUNT(DISTINCT user_id) FROM {$favorites_table}" + ); + $users_library_gauge->set( $users_with_library, array() ); + + // Users following artists. + $users_following_gauge = $collector->register_gauge( + 'fedistream_users_following_artists', + 'Users following at least one artist', + array() + ); + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching + $users_following = (int) $wpdb->get_var( + "SELECT COUNT(DISTINCT user_id) FROM {$follows_table}" + ); + $users_following_gauge->set( $users_following, array() ); + + // Notifications by type and status. + $notifications_gauge = $collector->register_gauge( + 'fedistream_notifications_total', + 'Notifications by type and status', + array( 'type', 'status' ) + ); + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching + $notifications = $wpdb->get_results( + "SELECT type, is_read, COUNT(*) as count FROM {$notifications_table} GROUP BY type, is_read" + ); + foreach ( $notifications as $row ) { + $status = $row->is_read ? 'read' : 'unread'; + $notifications_gauge->set( (int) $row->count, array( $row->type, $status ) ); + } + + // Pending notifications (simple gauge). + $pending_gauge = $collector->register_gauge( + 'fedistream_notifications_pending', + 'Total unread notifications', + array() + ); + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching + $pending = (int) $wpdb->get_var( + "SELECT COUNT(*) FROM {$notifications_table} WHERE is_read = 0" + ); + $pending_gauge->set( $pending, array() ); + } + + /** + * Collect WooCommerce metrics. + * + * @param object $collector The Prometheus collector. + * @return void + */ + private function collect_woocommerce_metrics( $collector ): void { + global $wpdb; + + $purchases_table = $wpdb->prefix . 'fedistream_purchases'; + + // Purchases by type. + $purchases_gauge = $collector->register_gauge( + 'fedistream_purchases_total', + 'Total purchases by content type', + array( 'type' ) + ); + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching + $purchases = $wpdb->get_results( + "SELECT content_type, COUNT(*) as count FROM {$purchases_table} GROUP BY content_type" + ); + foreach ( $purchases as $row ) { + $purchases_gauge->set( (int) $row->count, array( $row->content_type ) ); + } + + // Unique customers. + $customers_gauge = $collector->register_gauge( + 'fedistream_customers_total', + 'Unique customers with purchases', + array() + ); + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching + $customers = (int) $wpdb->get_var( + "SELECT COUNT(DISTINCT user_id) FROM {$purchases_table}" + ); + $customers_gauge->set( $customers, array() ); + + // WooCommerce products count (FediStream types). + if ( function_exists( 'wc_get_products' ) ) { + $products_gauge = $collector->register_gauge( + 'fedistream_products_total', + 'FediStream products in WooCommerce', + array( 'type' ) + ); + + foreach ( array( 'fedistream_album', 'fedistream_track' ) as $type ) { + $products = wc_get_products( + array( + 'type' => $type, + 'limit' => -1, + 'return' => 'ids', + ) + ); + $products_gauge->set( count( $products ), array( $type ) ); + } + } + } + + /** + * Collect ActivityPub metrics. + * + * @param object $collector The Prometheus collector. + * @return void + */ + private function collect_activitypub_metrics( $collector ): void { + global $wpdb; + + $followers_table = $wpdb->prefix . 'fedistream_followers'; + $reactions_table = $wpdb->prefix . 'fedistream_reactions'; + + // Total ActivityPub followers. + $followers_gauge = $collector->register_gauge( + 'fedistream_activitypub_followers_total', + 'Total ActivityPub followers across all artists', + array() + ); + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching + $total_followers = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$followers_table}" ); + $followers_gauge->set( $total_followers, array() ); + + // Followers by artist (top 10 to avoid cardinality explosion). + $followers_by_artist_gauge = $collector->register_gauge( + 'fedistream_activitypub_followers_by_artist', + 'Followers per artist', + array( 'artist_id', 'artist_name' ) + ); + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching + $followers_by_artist = $wpdb->get_results( + "SELECT artist_id, COUNT(*) as count FROM {$followers_table} GROUP BY artist_id ORDER BY count DESC LIMIT 10" + ); + foreach ( $followers_by_artist as $row ) { + $artist = get_post( $row->artist_id ); + $artist_name = $artist ? $artist->post_title : 'Unknown'; + $followers_by_artist_gauge->set( (int) $row->count, array( $row->artist_id, $artist_name ) ); + } + + // Reactions by type (if table exists). + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching + $table_exists = $wpdb->get_var( + $wpdb->prepare( 'SHOW TABLES LIKE %s', $reactions_table ) + ); + + if ( $table_exists ) { + $reactions_gauge = $collector->register_gauge( + 'fedistream_activitypub_reactions_total', + 'Fediverse reactions by type', + array( 'type' ) + ); + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching + $reactions = $wpdb->get_results( + "SELECT reaction_type, COUNT(*) as count FROM {$reactions_table} GROUP BY reaction_type" + ); + foreach ( $reactions as $row ) { + $reactions_gauge->set( (int) $row->count, array( $row->reaction_type ) ); + } + } + } + + /** + * Check if WooCommerce integration is enabled. + * + * @return bool + */ + private function is_woocommerce_enabled(): bool { + return get_option( 'wp_fedistream_enable_woocommerce', 0 ) + && class_exists( 'WooCommerce' ); + } + + /** + * Check if ActivityPub integration is enabled. + * + * @return bool + */ + private function is_activitypub_enabled(): bool { + return (bool) get_option( 'wp_fedistream_enable_activitypub', 1 ); + } +} diff --git a/includes/Prometheus/index.php b/includes/Prometheus/index.php new file mode 100644 index 0000000..49d255d --- /dev/null +++ b/includes/Prometheus/index.php @@ -0,0 +1 @@ +