# WP Prometheus **Author:** Marco Graetsch **Author URL:** **Author Email:** **Repository URL:** **Issues URL:** ## Project Overview This plugin provides a Prometheus `/metrics` endpoint and an extensible way to add your own metrics in third-party plugins using hooks. It adds some default metrics like number of active accounts, number of articles, comments, and plugin status. The default metrics can be activated/deactivated in the plugin settings. ### Features - Prometheus compatible authenticated `/metrics` endpoint - Optional default metrics (users, posts, comments, plugins) - Runtime metrics (HTTP requests, request duration, database queries) - Cron job metrics (scheduled events, overdue, next run) - Transient cache metrics (total, expiring, expired) - WooCommerce integration (products, orders, revenue, customers) - Custom metric builder with admin UI (gauges with static or option-based values) - Metric export/import for backup and site migration - Grafana dashboard templates for easy visualization - Dedicated plugin settings under 'Settings/Metrics' menu - Extensible by other plugins using `wp_prometheus_collect_metrics` action hook - License management integration ### Key Fact: 100% AI-Generated This project is proudly **"vibe-coded"** using Claude.AI - the entire codebase was created through AI assistance. ## Temporary Roadmap **Note for AI Assistants:** Clean this section after the specific features are done or new releases are made. Effective changes are tracked in `CHANGELOG.md`. Do not add completed versions here - document them in the Session History section at the end of this file. *No planned features at this time.* ## Technical Stack - **Language:** PHP 8.3.x - **PHP-Standards:** PSR-4 - **Framework:** Latest WordPress Plugin API - **Styling:** Custom CSS - **Dependency Management:** Composer - **Internationalization:** WordPress i18n (.pot/.po/.mo files) - **Canonical Plugin Name:** `wp-prometheus` - **License Client:** `magdev/wc-licensed-product-client` Have a look at for a working admin integration - **Prometheus Client:** `promphp/prometheus_client_php` ### Security Best Practices - All user inputs are sanitized (integers for quantities/prices) - Nonce verification on form submissions - Output escaping in templates (`esc_attr`, `esc_html`, `esc_js`) - Direct file access prevention via `ABSPATH` check - XSS-safe DOM construction in JavaScript (no `innerHTML` with user data) - SQL injection prevention using `$wpdb->prepare()` throughout ### Translation Ready All user-facing strings use: ```php __('Text to translate', 'wp-prometheus') _e('Text to translate', 'wp-prometheus') ``` Text domain: `wp-prometheus` #### Translation Template - Base `.pot` file created: `languages/wp-prometheus.pot` - Ready for translation to any locale - All translatable strings properly marked with text domain #### Available Translations - `en_US` - English (United States) [base language - .pot template] - `de_CH` - German (Switzerland, formal) To compile translations to .mo files for production: ```bash for po in languages/*.po; do msgfmt -o "${po%.po}.mo" "$po"; done ``` ### Create releases - The `vendor/` directory MUST be included in releases (Dependencies required for runtime) - **CRITICAL**: Build `vendor/` for the MINIMUM supported PHP version, not the development version - Use `composer config platform.php 8.3.0` before building release packages - Run `composer update --no-dev --optimize-autoloader` to rebuild dependencies - **CRITICAL**: WordPress requires plugins in a subdirectory structure - Run zip from the `plugins/` parent directory, NOT from within the plugin directory - Package must extract to `wp-prometheus/` subdirectory with main file at `wp-prometheus/wp-prometheus.php` - Correct command: `cd /wp-content/plugins/ && zip -r wp-prometheus/releases/wp-prometheus-x.x.x.zip wp-prometheus ...` - Wrong: Running zip from inside the plugin directory creates files at root level - **CRITICAL**: Exclude symlinks explicitly - zip follows symlinks by default - Always use `-x "wp-prometheus/wp-core" -x "wp-prometheus/wp-core/*" -x "wp-prometheus/wp-plugins" -x "wp-prometheus/wp-plugins/*"` to exclude development symlinks - Otherwise the entire linked directory contents will be included in the package - Exclusion patterns must match the relative path structure used in zip command - Always verify the package structure with `unzip -l` before distribution - Check all files are prefixed with `wp-prometheus/` - Verify main file is at `wp-prometheus/wp-prometheus.php` - Check for duplicate entries (indicates multiple builds in same archive) - Test installation on the minimum supported PHP version before final deployment - Releases are stored in `releases/` including checksums - Track release changes in a single `CHANGELOG.md` file - Bump the version number to either bugfix release versions or on new features minor release versions - **CRITICAL**: WordPress reads version from TWO places - BOTH must be updated: 1. Plugin header comment `Version: x.x.x` - WordPress uses THIS for admin display 2. PHP constant `WP_PROMETHEUS_VERSION` (line ~28) - Used internally by the plugin - If only the constant is updated, WordPress will show the old version in Plugins list **Important Git Notes:** - Default branch while development is `dev` - Create releases from branch `main` after merging branch `dev` - Tags should use format `vX.X.X` (e.g., `v1.1.22`), start with v0.0.1 - Use annotated tags (`-a`) not lightweight tags - Commit messages should follow the established format with Claude Code attribution - `.claude/settings.local.json` changes are typically local-only (stash before rebasing) **CRITICAL - Release Workflow:** On every new version, ALWAYS execute this complete workflow: ```bash # 1. Commit changes to dev branch git add git commit -m "Description of changes (vX.X.X)" # 2. Merge dev to main git checkout main git merge dev --no-edit # 3. Create annotated tag git tag -a vX.X.X -m "Version X.X.X - Brief description" # 4. Push everything to origin git push origin dev main vX.X.X # 5. Switch back to dev for continued development git checkout dev ``` Never skip any of these steps. The release is not complete until all branches and the tag are pushed to origin. #### What Gets Released - All plugin source files - Compiled vendor dependencies - Translation files (.mo compiled from .po) - Assets (CSS, JS) - Documentation (README, CHANGELOG, etc.) #### What's Excluded - Git metadata (`.git/`) - Development files (`.vscode/`, `.claude/`, `CLAUDE.md`, `wp-core`, `wp-plugins`) - Logs and cache files - Previous releases - `composer.lock` (but `vendor/` is included) --- **For AI Assistants:** When starting a new session on this project: 1. Read this CLAUDE.md file first 2. Semantic versioning follows the `MAJOR.MINOR.BUGFIX` pattern 3. Check git log for recent changes 4. Verify you're on the `dev` branch before making changes 5. Run `composer install` if vendor/ is missing 6. Test changes before committing 7. Follow commit message format with Claude Code attribution 8. Update this session history section with learnings 9. Never commit backup files (`*.po~`, `*.bak`, etc.) - check `git status` before committing 10. Follow markdown linting rules (see below) Always refer to this document when starting work on this project. ### Markdown Linting Rules When editing CLAUDE.md or other markdown files, follow these rules to avoid linting errors: 1. **MD031 - Blank lines around fenced code blocks**: Always add a blank line before and after fenced code blocks, even when they follow list items. Example of correct format: - **Item label**: (blank line here) \`\`\`php code example \`\`\` (blank line here) 2. **MD056 - Table column count**: Table separators must have matching column counts and proper spacing. Use consistent dash lengths that match column header widths. 3. **MD009 - No trailing spaces**: Remove trailing whitespace from lines 4. **MD012 - No multiple consecutive blank lines**: Use only single blank lines between sections 5. **MD040 - Fenced code blocks should have a language specified**: Always add a language identifier to code blocks (e.g., `txt`, `bash`, `php`). For shortcode examples, use `txt`. 6. **MD032 - Lists should be surrounded by blank lines**: Add a blank line before AND after list blocks, including after bold labels like `**Attributes:**`. 7. **MD034 - Bare URLs**: Wrap URLs in angle brackets (e.g., ``) or use markdown link syntax `[text](url)`. 8. **Author section formatting**: Use a heading (`### Name`) instead of bold (`**Name**`) for the author name to maintain consistent document structure. ## Project Architecture ### Directory Structure ```txt wp-prometheus/ ├── .gitea/workflows/ │ └── release.yml # CI/CD pipeline ├── assets/ │ ├── css/ # Admin/Frontend styles │ ├── dashboards/ # Grafana dashboard templates │ │ ├── wordpress-overview.json │ │ ├── wordpress-runtime.json │ │ └── wordpress-woocommerce.json │ └── js/ │ └── admin.js # Admin JavaScript ├── languages/ # Translation files ├── lib/ │ └── wc-licensed-product-client/ # Git submodule ├── releases/ # Release packages ├── src/ │ ├── Admin/ │ │ ├── DashboardProvider.php # Grafana dashboard provider │ │ └── Settings.php # Settings page │ ├── Endpoint/ │ │ └── MetricsEndpoint.php # /metrics endpoint │ ├── License/ │ │ └── Manager.php # License management │ ├── Metrics/ │ │ ├── Collector.php # Prometheus metrics collector │ │ ├── CustomMetricBuilder.php # Custom metric CRUD │ │ ├── RuntimeCollector.php # Runtime metrics collector │ │ └── StorageFactory.php # Storage adapter factory │ ├── Installer.php # Activation/Deactivation │ ├── Plugin.php # Main plugin class │ └── index.php ├── CHANGELOG.md ├── CLAUDE.md ├── composer.json ├── index.php ├── PLAN.md ├── README.md ├── uninstall.php └── wp-prometheus.php # Plugin bootstrap ``` ### Implementation Details #### License Manager (`src/License/Manager.php`) - Integration with `SecureLicenseClient` or `LicenseClient` - Option storage for license key, server URL, server secret - License validation with domain binding - License activation with domain - Status caching (24-hour transient) - AJAX handlers for admin operations - Exception handling for all license states #### Metrics Endpoint Restriction Logic ```php // In Plugin::init_components() if ( LicenseManager::is_license_valid() ) { $this->collector = new Collector(); new MetricsEndpoint( $this->collector ); } ``` Admin settings always work; metrics endpoint requires valid license. #### Custom Metrics Extension ```php // Third-party plugins can add custom metrics add_action( 'wp_prometheus_collect_metrics', function( $collector ) { $gauge = $collector->register_gauge( 'my_custom_metric', 'Description of my metric', array( 'label1', 'label2' ) ); $gauge->set( 42, array( 'value1', 'value2' ) ); } ); ``` --- ## Session History ### 2026-02-02 - Early Mode Toggle (v0.4.2) - Added option to disable early mode for users who need extensibility - Implementation: - Added `wp_prometheus_disable_early_mode` WordPress option - Added `WP_PROMETHEUS_DISABLE_EARLY_MODE` environment variable support - Option check in `wp_prometheus_early_metrics_check()` before early interception - Environment variable accepts `1`, `true`, `yes`, `on` (case-insensitive) - Admin UI in Metrics tab: - "Early Mode" section with description of functionality - Checkbox to disable early metrics interception - Environment override notice when env var is set - Current status indicator showing early mode state - **Key Learning**: Balancing compatibility vs extensibility - Early mode fixes memory issues but disables `wp_prometheus_collect_metrics` hook - Users with custom metrics need the hook, so early mode must be optional - Default remains enabled (safe) with explicit opt-out for advanced users ### 2026-02-02 - Plugin Compatibility Fix (v0.4.1) - Fixed memory exhaustion (1GB limit) when wp-fedistream (Twig-based) plugin is active - Root cause: Infinite recursion through WordPress hook system when content filters trigger Twig rendering - Solution: Early metrics endpoint interception before full WordPress initialization - Implementation changes: - Added `wp_prometheus_early_metrics_check()` in bootstrap file (wp-prometheus.php) - Checks REQUEST_URI for `/metrics` pattern before `plugins_loaded` fires - Defines `WP_PROMETHEUS_EARLY_METRICS` constant to signal early mode - Removes content filters (`the_content`, `the_excerpt`, `get_the_excerpt`, `the_title`) - Collector skips `wp_prometheus_collect_metrics` action in early mode - Changed MetricsEndpoint from `template_redirect` to `parse_request` hook - **Key Learning**: WordPress plugin loading order and hook timing - Plugins load alphabetically, so wp-fedistream ('f') loads before wp-prometheus ('p') - `template_redirect` fires too late - after themes and Twig initialize - `parse_request` fires earlier but still after plugin files load - Earliest interception point: top-level code in plugin bootstrap file - **Key Learning**: Content filter recursion in WordPress - `get_the_excerpt()` internally triggers `apply_filters('the_content', ...)` - This creates unexpected recursion vectors when Twig templates process content - Solution: Remove all content-related filters before metrics collection - **Key Learning**: Isolating metrics collection from WordPress template system - Use `remove_all_filters()` to clear problematic filter chains - Skip extensibility hooks (`do_action`) when in isolated early mode - Exit immediately after output to prevent further WordPress processing ### 2026-02-02 - Persistent Storage (v0.4.0) - Added persistent storage support for metrics: - `StorageFactory.php` - Factory class for storage adapter instantiation - Redis storage adapter for shared metrics across multiple instances - APCu storage adapter for single-server high-performance caching - Automatic fallback to In-Memory if configured adapter fails - Added new "Storage" tab in admin settings: - Storage adapter selection (In-Memory, Redis, APCu) - Redis configuration (host, port, password, database, key prefix) - APCu configuration (key prefix) - Connection test button with detailed error messages - Added environment variable support for Docker deployments: - `WP_PROMETHEUS_STORAGE_ADAPTER` - Adapter selection - `WP_PROMETHEUS_REDIS_HOST`, `_PORT`, `_PASSWORD`, `_DATABASE`, `_PREFIX` - `WP_PROMETHEUS_APCU_PREFIX` - Environment variables take precedence over admin settings - Updated `Collector.php` to use `StorageFactory::get_adapter()` - Updated Help tab with storage backends documentation - Updated translation files with all new strings - **Key Learning**: promphp/prometheus_client_php storage adapters - Redis adapter requires options array with host, port, password, timeout - APCu adapter just needs a prefix string - Use `Redis::setPrefix()` before instantiation for custom key prefixes - **Key Learning**: Docker environment variable configuration - Use `getenv()` with explicit false check (`false !== getenv()`) - Environment variables should override WordPress options for containerized deployments ### 2026-02-02 - Custom Metrics & Dashboards (v0.3.0) - Added Custom Metric Builder with full admin UI: - `CustomMetricBuilder.php` - CRUD operations, validation, export/import - Support for static values and WordPress option-based values - Label support (max 5 labels, 50 value combinations) - Prometheus naming convention validation (`[a-zA-Z_:][a-zA-Z0-9_:]*`) - Added Grafana Dashboard Templates: - `DashboardProvider.php` - Dashboard file provider with path traversal protection - `wordpress-overview.json` - General WordPress metrics - `wordpress-runtime.json` - HTTP/DB performance metrics - `wordpress-woocommerce.json` - WooCommerce store metrics - Added export/import functionality: - JSON-based configuration export - Three import modes: skip, overwrite, rename duplicates - Version tracking in export format - Updated Settings page with new tabs: - "Custom Metrics" tab with metric form and table - "Dashboards" tab with download buttons - "Reset Runtime Metrics" button in Metrics tab - Updated `Collector.php` to integrate custom metrics - Updated translation files with all new strings - **Key Learning**: Dynamic form handling in WordPress admin - Use `wp_create_nonce()` with unique nonce names per AJAX action - Localize script with `wp_localize_script()` for nonces and AJAX URL - Always verify `current_user_can('manage_options')` in AJAX handlers - **Key Learning**: Grafana dashboard JSON format - Use `${DS_PROMETHEUS}` for data source variable - Schema version 39 for current Grafana compatibility - Panels use `gridPos` for layout positioning ### 2026-02-02 - Extended Metrics (v0.2.0) - Added WooCommerce integration metrics (only when WooCommerce is active): - `wordpress_woocommerce_products_total` - Products by status and type - `wordpress_woocommerce_orders_total` - Orders by status - `wordpress_woocommerce_revenue_total` - Revenue (all time, today, month) - `wordpress_woocommerce_customers_total` - Customers (registered, guest) - Added cron job metrics: - `wordpress_cron_events_total` - Scheduled cron events by hook (top 20) - `wordpress_cron_overdue_total` - Number of overdue cron events - `wordpress_cron_next_run_timestamp` - Unix timestamp of next scheduled cron - Added transient cache metrics: - `wordpress_transients_total` - Transients by type (total, with_expiration, persistent, expired) - Updated Settings page with new metric categories - Updated Help tab with new metrics reference - **Key Learning**: WooCommerce HPOS (High-Performance Order Storage) requires different queries - Check `OrderUtil::custom_orders_table_usage_is_enabled()` to determine storage type - HPOS uses `wc_orders` table instead of `posts` and `postmeta` - **Key Learning**: Cron event labeling requires cardinality control - Limit to top 20 hooks to prevent label explosion - Use `arsort()` to get most frequent hooks first ### 2026-02-02 - Runtime Metrics (v0.1.0) - Implemented runtime metrics collection for HTTP requests and database queries - Created `RuntimeCollector` class that hooks into WordPress request lifecycle - Added new metrics: - `wordpress_http_requests_total` - Counter by method, status, endpoint - `wordpress_http_request_duration_seconds` - Histogram of request durations - `wordpress_db_queries_total` - Counter by endpoint - `wordpress_db_query_duration_seconds` - Histogram (requires SAVEQUERIES) - Updated `Collector` class to expose stored runtime metrics - Added new settings options in admin for enabling/disabling runtime metrics - Created translation files (.pot, .po, .mo) for internationalization - **Key Learning**: With InMemory Prometheus storage, counters/histograms reset per request - Solution: Store aggregated data in WordPress options, read during metrics collection - Histograms exposed as gauge metrics following Prometheus naming conventions (`_bucket`, `_sum`, `_count`) - **Key Learning**: Endpoint normalization is important for cardinality control - Group requests into categories (admin, ajax, cron, rest-api, frontend, etc.) - Avoid high-cardinality labels like full URL paths ### 2026-02-01 - CI/CD Fixes (v0.0.2) - Fixed composer.json dependency configuration for CI compatibility - **Key Learning**: Git submodules with path repositories need explicit version aliases for CI: ```json "repositories": [ { "type": "path", "url": "lib/wc-licensed-product-client", "options": { "symlink": false, "versions": { "magdev/wc-licensed-product-client": "0.2.2" } } } ] ``` - Using `dev-main` constraints with `minimum-stability: dev` causes issues in CI - Path repository with `symlink: false` and explicit `versions` mapping works reliably - **Key Learning**: Gitea API returns "Release has no Tag" error when re-releasing existing tags - Fixed release.yml to check for and delete existing releases before creating new ones - Changed minimum-stability back to stable ### 2026-02-01 - Initial Setup (v0.0.1) - Created initial plugin structure based on wp-fedistream blueprint - Set up composer.json with promphp/prometheus_client_php and wc-licensed-product-client - Implemented core classes: Plugin, Installer, License/Manager, Metrics/Collector, Endpoint/MetricsEndpoint, Admin/Settings - Created authenticated /metrics endpoint with Bearer token support - Added default metrics: wordpress_info, users_total, posts_total, comments_total, plugins_total - Created extensibility via `wp_prometheus_collect_metrics` action hook - Set up Gitea CI/CD pipeline for automated releases - Created documentation: README.md, PLAN.md, CHANGELOG.md