feat: Add runtime metrics for HTTP requests and database queries (v0.1.0)

- 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:
2026-02-02 14:24:05 +01:00
parent f1748727ce
commit a55eb2e4b3
13 changed files with 1009 additions and 18 deletions

View File

@@ -5,6 +5,25 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.1.0] - 2026-02-02
### Added
- HTTP request metrics:
- `wordpress_http_requests_total` - Counter of HTTP requests by method, status code, and endpoint
- `wordpress_http_request_duration_seconds` - Histogram of request durations
- Database query metrics:
- `wordpress_db_queries_total` - Counter of database queries by endpoint
- `wordpress_db_query_duration_seconds` - Histogram of query durations (requires SAVEQUERIES)
- RuntimeCollector class for collecting metrics during WordPress request lifecycle
- New settings options for enabling/disabling runtime metrics
- Translation files (.pot, .po, .mo) for German (Switzerland)
### Changed
- Metrics are now categorized into static metrics (users, posts, etc.) and runtime metrics (HTTP, database)
- Runtime metrics only collected when explicitly enabled and license is valid
## [0.0.2] - 2026-02-01
### Fixed

View File

@@ -14,6 +14,7 @@ This plugin provides a Prometheus `/metrics` endpoint and an extensible way to a
- Prometheus compatible authenticated `/metrics` endpoint
- Optional default metrics (users, posts, comments, plugins)
- Runtime metrics (HTTP requests, request duration, database queries)
- Dedicated plugin settings under 'Settings/Metrics' menu
- Extensible by other plugins using `wp_prometheus_collect_metrics` action hook
- License management integration
@@ -26,11 +27,11 @@ This project is proudly **"vibe-coded"** using Claude.AI - the entire codebase w
**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.
### Version 0.1.0 (Planned)
### Version 0.2.0 (Planned)
- Add request/response timing metrics
- Add HTTP status code counters
- Add database query metrics
- WooCommerce integration metrics
- Cron job metrics
- Transient cache metrics
## Technical Stack
@@ -224,7 +225,8 @@ wp-prometheus/
│ ├── License/
│ │ └── Manager.php # License management
│ ├── Metrics/
│ │ ── Collector.php # Prometheus metrics collector
│ │ ── Collector.php # Prometheus metrics collector
│ │ └── RuntimeCollector.php # Runtime metrics collector
│ ├── Installer.php # Activation/Deactivation
│ ├── Plugin.php # Main plugin class
│ └── index.php
@@ -280,6 +282,25 @@ add_action( 'wp_prometheus_collect_metrics', function( $collector ) {
## Session History
### 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

17
PLAN.md
View File

@@ -91,6 +91,8 @@ wp-prometheus/
The plugin provides the following default metrics (can be toggled in settings):
### Static Metrics
| Metric | Type | Labels | Description |
|--------|------|--------|-------------|
| wordpress_info | Gauge | version, php_version, multisite | WordPress installation info |
@@ -99,6 +101,15 @@ The plugin provides the following default metrics (can be toggled in settings):
| wordpress_comments_total | Gauge | status | Total comments by status |
| wordpress_plugins_total | Gauge | status | Total plugins (active/inactive) |
### Runtime Metrics
| Metric | Type | Labels | Description |
| ---------------------------------------- | --------- | ------------------------ | ------------------------------------- |
| wordpress_http_requests_total | Counter | method, status, endpoint | Total HTTP requests |
| wordpress_http_request_duration_seconds | Histogram | method, endpoint | Request duration distribution |
| wordpress_db_queries_total | Counter | endpoint | Total database queries |
| wordpress_db_query_duration_seconds | Histogram | endpoint | Query duration (requires SAVEQUERIES) |
## Extensibility
### Adding Custom Metrics
@@ -150,12 +161,6 @@ https://example.com/metrics/?token=your-auth-token
## Future Enhancements
### Version 0.1.0
- Request/Response timing metrics
- HTTP status code counters
- Database query metrics
### Version 0.2.0
- WooCommerce integration metrics

View File

@@ -71,6 +71,8 @@ scrape_configs:
## Default Metrics
### Static Metrics
| Metric | Type | Labels | Description |
|--------|------|--------|-------------|
| wordpress_info | Gauge | version, php_version, multisite | WordPress installation info |
@@ -79,6 +81,17 @@ scrape_configs:
| wordpress_comments_total | Gauge | status | Total comments by status |
| wordpress_plugins_total | Gauge | status | Total plugins (active/inactive) |
### Runtime Metrics (v0.1.0+)
| Metric | Type | Labels | Description |
|--------|------|--------|-------------|
| wordpress_http_requests_total | Counter | method, status, endpoint | Total HTTP requests |
| wordpress_http_request_duration_seconds | Histogram | method, endpoint | Request duration distribution |
| wordpress_db_queries_total | Counter | endpoint | Total database queries |
| wordpress_db_query_duration_seconds | Histogram | endpoint | Query duration (requires SAVEQUERIES) |
**Note:** Runtime metrics aggregate data across requests. Enable only the metrics you need to minimize performance impact.
## Extending with Custom Metrics
Add your own metrics using the `wp_prometheus_collect_metrics` action:

Binary file not shown.

View File

@@ -0,0 +1,198 @@
# German (Switzerland, formal) translation for WP Prometheus.
# Copyright (C) 2026 Marco Graetsch
# This file is distributed under the GPL v2 or later.
msgid ""
msgstr ""
"Project-Id-Version: WP Prometheus 0.1.0\n"
"Report-Msgid-Bugs-To: https://src.bundespruefstelle.ch/magdev/wp-prometheus/issues\n"
"POT-Creation-Date: 2026-02-02T00:00:00+00:00\n"
"PO-Revision-Date: 2026-02-02T00:00:00+00:00\n"
"Last-Translator: Marco Graetsch <magdev3.0@gmail.com>\n"
"Language-Team: German (Switzerland) <de_CH@li.org>\n"
"Language: de_CH\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: src/Admin/Settings.php:40
msgid "Metrics Settings"
msgstr "Metriken-Einstellungen"
#: src/Admin/Settings.php:41
msgid "Metrics"
msgstr "Metriken"
#: src/Admin/Settings.php:58
msgid "License Settings"
msgstr "Lizenz-Einstellungen"
#: src/Admin/Settings.php:65
msgid "Authentication"
msgstr "Authentifizierung"
#: src/Admin/Settings.php:73
msgid "Default Metrics"
msgstr "Standard-Metriken"
#: src/Admin/Settings.php:150
msgid "License settings saved."
msgstr "Lizenz-Einstellungen gespeichert."
#: src/Admin/Settings.php:195
msgid "License is active and valid."
msgstr "Lizenz ist aktiv und gueltig."
#: src/Admin/Settings.php:196
msgid "License is invalid."
msgstr "Lizenz ist ungueltig."
#: src/Admin/Settings.php:197
msgid "License has expired."
msgstr "Lizenz ist abgelaufen."
#: src/Admin/Settings.php:198
msgid "License has been revoked."
msgstr "Lizenz wurde widerrufen."
#: src/Admin/Settings.php:199
msgid "License is inactive."
msgstr "Lizenz ist inaktiv."
#: src/Admin/Settings.php:200
msgid "License has not been validated yet."
msgstr "Lizenz wurde noch nicht validiert."
#: src/Admin/Settings.php:201
msgid "License server is not configured."
msgstr "Lizenz-Server ist nicht konfiguriert."
#. translators: %s: Expiration date
#: src/Admin/Settings.php:214
msgid "Expires: %s"
msgstr "Laeuft ab: %s"
#. translators: %s: Time ago
#: src/Admin/Settings.php:225
msgid "Last checked: %s ago"
msgstr "Zuletzt geprueft: vor %s"
#: src/Admin/Settings.php:239
msgid "License Server URL"
msgstr "Lizenz-Server URL"
#: src/Admin/Settings.php:249
msgid "License Key"
msgstr "Lizenzschluessel"
#: src/Admin/Settings.php:259
msgid "Server Secret"
msgstr "Server-Geheimnis"
#: src/Admin/Settings.php:264
msgid "Leave empty to keep existing."
msgstr "Leer lassen, um bestehenden Wert zu behalten."
#: src/Admin/Settings.php:270
msgid "Save License Settings"
msgstr "Lizenz-Einstellungen speichern"
#: src/Admin/Settings.php:272
msgid "Validate License"
msgstr "Lizenz validieren"
#: src/Admin/Settings.php:275
msgid "Activate License"
msgstr "Lizenz aktivieren"
#: src/Admin/Settings.php:301
msgid "Configure authentication for the /metrics endpoint."
msgstr "Authentifizierung fuer den /metrics-Endpunkt konfigurieren."
#: src/Admin/Settings.php:310
msgid "Select which default metrics to expose."
msgstr "Waehlen Sie, welche Standard-Metriken bereitgestellt werden sollen."
#: src/Admin/Settings.php:324
msgid "Regenerate"
msgstr "Neu generieren"
#: src/Admin/Settings.php:327
msgid "Use this token to authenticate Prometheus scrape requests."
msgstr "Verwenden Sie diesen Token zur Authentifizierung von Prometheus-Abfragen."
#: src/Admin/Settings.php:340
msgid "WordPress Info (version, PHP version, multisite)"
msgstr "WordPress-Info (Version, PHP-Version, Multisite)"
#: src/Admin/Settings.php:341
msgid "Total Users by Role"
msgstr "Benutzer nach Rolle"
#: src/Admin/Settings.php:342
msgid "Total Posts by Type and Status"
msgstr "Beitraege nach Typ und Status"
#: src/Admin/Settings.php:343
msgid "Total Comments by Status"
msgstr "Kommentare nach Status"
#: src/Admin/Settings.php:344
msgid "Total Plugins (active/inactive)"
msgstr "Plugins (aktiv/inaktiv)"
#: src/Admin/Settings.php:345
msgid "HTTP Requests Total (by method, status, endpoint)"
msgstr "HTTP-Anfragen gesamt (nach Methode, Status, Endpunkt)"
#: src/Admin/Settings.php:346
msgid "HTTP Request Duration (histogram)"
msgstr "HTTP-Anfragedauer (Histogramm)"
#: src/Admin/Settings.php:347
msgid "Database Queries Total (by endpoint)"
msgstr "Datenbank-Abfragen gesamt (nach Endpunkt)"
#: src/Admin/Settings.php:369
msgid "Prometheus Configuration"
msgstr "Prometheus-Konfiguration"
#: src/Admin/Settings.php:370
msgid "Add the following to your prometheus.yml:"
msgstr "Fuegen Sie Folgendes zu Ihrer prometheus.yml hinzu:"
#. translators: %s: Endpoint URL
#: src/Admin/Settings.php:385
msgid "Metrics endpoint: %s"
msgstr "Metriken-Endpunkt: %s"
#: src/Admin/Settings.php:93
msgid "Auth Token"
msgstr "Auth-Token"
#: src/Admin/Settings.php:101
msgid "Enabled Metrics"
msgstr "Aktivierte Metriken"
#: src/Plugin.php:120
msgid "Settings"
msgstr "Einstellungen"
#. translators: 1: Required PHP version, 2: Current PHP version
#: wp-prometheus.php:112
msgid "WP Prometheus requires PHP version %1$s or higher. You are running PHP %2$s."
msgstr "WP Prometheus erfordert PHP-Version %1$s oder hoeher. Sie verwenden PHP %2$s."
#. translators: 1: Required WordPress version, 2: Current WordPress version
#: wp-prometheus.php:127
msgid "WP Prometheus requires WordPress version %1$s or higher. You are running WordPress %2$s."
msgstr "WP Prometheus erfordert WordPress-Version %1$s oder hoeher. Sie verwenden WordPress %2$s."
#: wp-prometheus.php:140
msgid "WP Prometheus requires Composer dependencies to be installed. Please run \"composer install\" in the plugin directory."
msgstr "WP Prometheus erfordert installierte Composer-Abhaengigkeiten. Bitte fuehren Sie \"composer install\" im Plugin-Verzeichnis aus."
#. translators: %s: Required PHP version
#: wp-prometheus.php:156
msgid "WP Prometheus requires PHP version %s or higher."
msgstr "WP Prometheus erfordert PHP-Version %s oder hoeher."

195
languages/wp-prometheus.pot Normal file
View File

@@ -0,0 +1,195 @@
# Copyright (C) 2026 Marco Graetsch
# This file is distributed under the GPL v2 or later.
msgid ""
msgstr ""
"Project-Id-Version: WP Prometheus 0.1.0\n"
"Report-Msgid-Bugs-To: https://src.bundespruefstelle.ch/magdev/wp-prometheus/issues\n"
"POT-Creation-Date: 2026-02-02T00:00:00+00:00\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"PO-Revision-Date: 2026-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
#: src/Admin/Settings.php:40
msgid "Metrics Settings"
msgstr ""
#: src/Admin/Settings.php:41
msgid "Metrics"
msgstr ""
#: src/Admin/Settings.php:58
msgid "License Settings"
msgstr ""
#: src/Admin/Settings.php:65
msgid "Authentication"
msgstr ""
#: src/Admin/Settings.php:73
msgid "Default Metrics"
msgstr ""
#: src/Admin/Settings.php:150
msgid "License settings saved."
msgstr ""
#: src/Admin/Settings.php:195
msgid "License is active and valid."
msgstr ""
#: src/Admin/Settings.php:196
msgid "License is invalid."
msgstr ""
#: src/Admin/Settings.php:197
msgid "License has expired."
msgstr ""
#: src/Admin/Settings.php:198
msgid "License has been revoked."
msgstr ""
#: src/Admin/Settings.php:199
msgid "License is inactive."
msgstr ""
#: src/Admin/Settings.php:200
msgid "License has not been validated yet."
msgstr ""
#: src/Admin/Settings.php:201
msgid "License server is not configured."
msgstr ""
#. translators: %s: Expiration date
#: src/Admin/Settings.php:214
msgid "Expires: %s"
msgstr ""
#. translators: %s: Time ago
#: src/Admin/Settings.php:225
msgid "Last checked: %s ago"
msgstr ""
#: src/Admin/Settings.php:239
msgid "License Server URL"
msgstr ""
#: src/Admin/Settings.php:249
msgid "License Key"
msgstr ""
#: src/Admin/Settings.php:259
msgid "Server Secret"
msgstr ""
#: src/Admin/Settings.php:264
msgid "Leave empty to keep existing."
msgstr ""
#: src/Admin/Settings.php:270
msgid "Save License Settings"
msgstr ""
#: src/Admin/Settings.php:272
msgid "Validate License"
msgstr ""
#: src/Admin/Settings.php:275
msgid "Activate License"
msgstr ""
#: src/Admin/Settings.php:301
msgid "Configure authentication for the /metrics endpoint."
msgstr ""
#: src/Admin/Settings.php:310
msgid "Select which default metrics to expose."
msgstr ""
#: src/Admin/Settings.php:324
msgid "Regenerate"
msgstr ""
#: src/Admin/Settings.php:327
msgid "Use this token to authenticate Prometheus scrape requests."
msgstr ""
#: src/Admin/Settings.php:340
msgid "WordPress Info (version, PHP version, multisite)"
msgstr ""
#: src/Admin/Settings.php:341
msgid "Total Users by Role"
msgstr ""
#: src/Admin/Settings.php:342
msgid "Total Posts by Type and Status"
msgstr ""
#: src/Admin/Settings.php:343
msgid "Total Comments by Status"
msgstr ""
#: src/Admin/Settings.php:344
msgid "Total Plugins (active/inactive)"
msgstr ""
#: src/Admin/Settings.php:345
msgid "HTTP Requests Total (by method, status, endpoint)"
msgstr ""
#: src/Admin/Settings.php:346
msgid "HTTP Request Duration (histogram)"
msgstr ""
#: src/Admin/Settings.php:347
msgid "Database Queries Total (by endpoint)"
msgstr ""
#: src/Admin/Settings.php:369
msgid "Prometheus Configuration"
msgstr ""
#: src/Admin/Settings.php:370
msgid "Add the following to your prometheus.yml:"
msgstr ""
#. translators: %s: Endpoint URL
#: src/Admin/Settings.php:385
msgid "Metrics endpoint: %s"
msgstr ""
#: src/Admin/Settings.php:93
msgid "Auth Token"
msgstr ""
#: src/Admin/Settings.php:101
msgid "Enabled Metrics"
msgstr ""
#: src/Plugin.php:120
msgid "Settings"
msgstr ""
#. translators: 1: Required PHP version, 2: Current PHP version
#: wp-prometheus.php:112
msgid "WP Prometheus requires PHP version %1$s or higher. You are running PHP %2$s."
msgstr ""
#. translators: 1: Required WordPress version, 2: Current WordPress version
#: wp-prometheus.php:127
msgid "WP Prometheus requires WordPress version %1$s or higher. You are running WordPress %2$s."
msgstr ""
#: wp-prometheus.php:140
msgid "WP Prometheus requires Composer dependencies to be installed. Please run \"composer install\" in the plugin directory."
msgstr ""
#. translators: %s: Required PHP version
#: wp-prometheus.php:156
msgid "WP Prometheus requires PHP version %s or higher."
msgstr ""

View File

@@ -342,6 +342,9 @@ class Settings {
'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 ) {

View File

@@ -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 ) {

View File

@@ -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.
*

View 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;
}
}

View File

@@ -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();

View File

@@ -3,7 +3,7 @@
* Plugin Name: WP Prometheus
* Plugin URI: https://src.bundespruefstelle.ch/magdev/wp-prometheus
* Description: Prometheus metrics endpoint for WordPress with extensible hooks for custom metrics.
* Version: 0.0.2
* Version: 0.1.0
* Requires at least: 6.4
* Requires PHP: 8.3
* Author: Marco Graetsch
@@ -26,7 +26,7 @@ if ( ! defined( 'ABSPATH' ) ) {
*
* @var string
*/
define( 'WP_PROMETHEUS_VERSION', '0.0.2' );
define( 'WP_PROMETHEUS_VERSION', '0.1.0' );
/**
* Plugin file path.