Initial plugin setup (v0.0.1)
Some checks failed
Create Release Package / build-release (push) Failing after 48s

- Create initial WordPress plugin structure
- Add Prometheus metrics collector with default metrics
- Implement authenticated /metrics endpoint with Bearer token
- Add license management integration
- Create admin settings page under Settings > Metrics
- Set up Gitea CI/CD pipeline for automated releases
- Add extensibility via wp_prometheus_collect_metrics hook

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-01 15:31:21 +01:00
commit 7ff87f7c8d
21 changed files with 2890 additions and 0 deletions

15
.editorconfig Normal file
View File

@@ -0,0 +1,15 @@
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true
[*]
indent_style = space
indent_size = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
indent_size = 2

View File

@@ -0,0 +1,195 @@
name: Create Release Package
on:
push:
tags:
- 'v*'
jobs:
build-release:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
submodules: recursive
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
extensions: mbstring, xml, zip, intl, gettext
tools: composer:v2
- name: Get version from tag
id: version
run: |
VERSION=${GITHUB_REF_NAME#v}
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Building version: $VERSION"
- name: Validate composer.json
run: composer validate --strict
- name: Install Composer dependencies (production)
run: |
composer config platform.php 8.3.0
composer install --no-dev --optimize-autoloader --no-interaction
- name: Install gettext
run: apt-get update && apt-get install -y gettext
- name: Compile translations
run: |
for po in languages/*.po; do
if [ -f "$po" ]; then
mo="${po%.po}.mo"
echo "Compiling $po to $mo"
msgfmt -o "$mo" "$po"
fi
done
- name: Verify plugin version matches tag
run: |
PLUGIN_VERSION=$(grep -oP "Version:\s*\K[0-9]+\.[0-9]+\.[0-9]+" wp-prometheus.php | head -1)
TAG_VERSION=${{ steps.version.outputs.version }}
if [ "$PLUGIN_VERSION" != "$TAG_VERSION" ]; then
echo "Error: Plugin version ($PLUGIN_VERSION) does not match tag version ($TAG_VERSION)"
exit 1
fi
echo "Version verified: $PLUGIN_VERSION"
- name: Create release directory
run: mkdir -p releases
- name: Build release package
run: |
VERSION=${{ steps.version.outputs.version }}
PLUGIN_NAME="wp-prometheus"
RELEASE_FILE="releases/${PLUGIN_NAME}-${VERSION}.zip"
# Move to parent directory for proper zip structure
cd ..
# Create zip with proper WordPress plugin structure
zip -r "${PLUGIN_NAME}/${RELEASE_FILE}" "${PLUGIN_NAME}" \
-x "${PLUGIN_NAME}/.git/*" \
-x "${PLUGIN_NAME}/.gitea/*" \
-x "${PLUGIN_NAME}/.github/*" \
-x "${PLUGIN_NAME}/.vscode/*" \
-x "${PLUGIN_NAME}/.claude/*" \
-x "${PLUGIN_NAME}/CLAUDE.md" \
-x "${PLUGIN_NAME}/PLAN.md" \
-x "${PLUGIN_NAME}/wp-core" \
-x "${PLUGIN_NAME}/wp-core/*" \
-x "${PLUGIN_NAME}/wp-plugins" \
-x "${PLUGIN_NAME}/wp-plugins/*" \
-x "${PLUGIN_NAME}/releases/*" \
-x "${PLUGIN_NAME}/composer.lock" \
-x "${PLUGIN_NAME}/*.log" \
-x "${PLUGIN_NAME}/.gitignore" \
-x "${PLUGIN_NAME}/.gitmodules" \
-x "${PLUGIN_NAME}/.editorconfig" \
-x "${PLUGIN_NAME}/phpcs.xml*" \
-x "${PLUGIN_NAME}/phpunit.xml*" \
-x "${PLUGIN_NAME}/tests/*" \
-x "${PLUGIN_NAME}/*.po~" \
-x "${PLUGIN_NAME}/*.bak" \
-x "*.DS_Store"
cd "${PLUGIN_NAME}"
echo "Created: ${RELEASE_FILE}"
- name: Generate checksums
run: |
VERSION=${{ steps.version.outputs.version }}
RELEASE_FILE="releases/wp-prometheus-${VERSION}.zip"
cd releases
sha256sum "wp-prometheus-${VERSION}.zip" > "wp-prometheus-${VERSION}.zip.sha256"
echo "SHA256:"
cat "wp-prometheus-${VERSION}.zip.sha256"
- name: Verify package structure
run: |
set +o pipefail
VERSION=${{ steps.version.outputs.version }}
echo "Package contents:"
unzip -l "releases/wp-prometheus-${VERSION}.zip" | head -50 || true
# Verify main file is at correct location
if unzip -l "releases/wp-prometheus-${VERSION}.zip" | grep -q "wp-prometheus/wp-prometheus.php"; then
echo "✓ Main plugin file at correct location"
else
echo "✗ Error: Main plugin file not found at wp-prometheus/wp-prometheus.php"
exit 1
fi
# Verify vendor directory is included
if unzip -l "releases/wp-prometheus-${VERSION}.zip" | grep -q "wp-prometheus/vendor/"; then
echo "✓ Vendor directory included"
else
echo "✗ Error: Vendor directory not found"
exit 1
fi
- name: Extract changelog for release notes
id: changelog
run: |
VERSION=${{ steps.version.outputs.version }}
# Extract changelog section for this version
NOTES=$(sed -n "/^## \[${VERSION}\]/,/^## \[/p" CHANGELOG.md | sed '$ d' | tail -n +2)
if [ -z "$NOTES" ]; then
NOTES="Release version ${VERSION}"
fi
# Save to file for multi-line output
echo "$NOTES" > release_notes.txt
echo "Release notes extracted"
- name: Create Gitea Release
env:
GITEA_TOKEN: ${{ secrets.SRC_GITEA_TOKEN }}
run: |
VERSION=${{ steps.version.outputs.version }}
TAG_NAME=${{ github.ref_name }}
PRERELEASE="false"
if [[ "$TAG_NAME" == *-* ]]; then
PRERELEASE="true"
fi
# Read release notes
BODY=$(cat release_notes.txt)
# Create release via Gitea API
RELEASE_RESPONSE=$(curl -s -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"tag_name\": \"${TAG_NAME}\", \"name\": \"Release ${VERSION}\", \"body\": $(echo "$BODY" | jq -Rs .), \"draft\": false, \"prerelease\": ${PRERELEASE}}" \
"${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}/releases")
RELEASE_ID=$(echo "$RELEASE_RESPONSE" | jq -r '.id')
if [ "$RELEASE_ID" == "null" ] || [ -z "$RELEASE_ID" ]; then
echo "Failed to create release:"
echo "$RELEASE_RESPONSE"
exit 1
fi
echo "Created release ID: $RELEASE_ID"
# Upload attachments
for file in "releases/wp-prometheus-${VERSION}.zip" "releases/wp-prometheus-${VERSION}.zip.sha256"; do
if [ -f "$file" ]; then
FILENAME=$(basename "$file")
echo "Uploading $FILENAME..."
curl -s -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/octet-stream" \
--data-binary "@$file" \
"${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}/releases/${RELEASE_ID}/assets?name=${FILENAME}"
echo "Uploaded $FILENAME"
fi
done
echo "Release created successfully: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/releases/tag/${TAG_NAME}"

6
.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
# For development purposes
# Linked wordpress core and plugin folder
wp-plugins
wp-core
vendor/
releases/*

3
.gitmodules vendored Normal file
View File

@@ -0,0 +1,3 @@
[submodule "lib/wc-licensed-product-client"]
path = lib/wc-licensed-product-client
url = ../wc-licensed-product-client.git

32
CHANGELOG.md Normal file
View File

@@ -0,0 +1,32 @@
# Changelog
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.0.1] - 2026-02-01
### Added
- Initial plugin structure and bootstrap
- Main plugin class with singleton pattern
- License management integration with wc-licensed-product-client
- Prometheus metrics collector with default WordPress metrics:
- `wordpress_info` - WordPress installation information
- `wordpress_users_total` - Total users by role
- `wordpress_posts_total` - Total posts by type and status
- `wordpress_comments_total` - Total comments by status
- `wordpress_plugins_total` - Total plugins by status
- Authenticated `/metrics` endpoint with Bearer token
- Admin settings page under Settings > Metrics
- Extensibility via `wp_prometheus_collect_metrics` action hook
- Gitea CI/CD pipeline for automated releases
- Comprehensive documentation (README.md, PLAN.md, CLAUDE.md)
### Security
- Bearer token authentication for metrics endpoint
- Nonce verification for all AJAX requests
- Capability checks for admin operations
- Input sanitization and output escaping

292
CLAUDE.md Normal file
View File

@@ -0,0 +1,292 @@
# WP Prometheus
**Author:** Marco Graetsch
**Author URL:** <https://src.bundespruefstelle.ch/magdev>
**Author Email:** <magdev3.0@gmail.com>
**Repository URL:** <https://src.bundespruefstelle.ch/magdev/wp-prometheus>
**Issues URL:** <https://src.bundespruefstelle.ch/magdev/wp-prometheus/issues>
## 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)
- 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.
### Version 0.1.0 (Planned)
- Add request/response timing metrics
- Add HTTP status code counters
- Add database query metrics
## 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 <https://src.bundespruefstelle.ch/magdev/wp-fedistream> for a working admin integration
- **Prometheus Client:** `promphp/prometheus_client_php` <https://github.com/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 <files>
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., `<https://example.com>`) 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
│ └── js/
│ └── admin.js # Admin JavaScript
├── languages/ # Translation files
├── lib/
│ └── wc-licensed-product-client/ # Git submodule
├── releases/ # Release packages
├── src/
│ ├── Admin/
│ │ └── Settings.php # Settings page
│ ├── Endpoint/
│ │ └── MetricsEndpoint.php # /metrics endpoint
│ ├── License/
│ │ └── Manager.php # License management
│ ├── Metrics/
│ │ └── Collector.php # Prometheus metrics collector
│ ├── 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-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

190
PLAN.md Normal file
View File

@@ -0,0 +1,190 @@
# WP Prometheus Implementation Plan
## Overview
This document outlines the implementation plan for the WP Prometheus plugin, providing a Prometheus-compatible `/metrics` endpoint for WordPress.
## Architecture
### Core Components
1. **Plugin Bootstrap** (`wp-prometheus.php`)
- WordPress plugin header
- Version constants
- PHP/WordPress version checks
- Autoloader initialization
- Activation/Deactivation hooks
2. **Plugin Class** (`src/Plugin.php`)
- Singleton pattern
- Component initialization
- Hook registration
- Text domain loading
3. **Installer** (`src/Installer.php`)
- Activation logic
- Default options setup
- Rewrite rules flushing
- Uninstallation cleanup
4. **License Manager** (`src/License/Manager.php`)
- Integration with wc-licensed-product-client
- License validation/activation
- Status caching (24-hour transient)
- AJAX handlers for admin actions
5. **Metrics Collector** (`src/Metrics/Collector.php`)
- Prometheus CollectorRegistry wrapper
- Default WordPress metrics
- Custom metric registration hooks
- Extensibility via `wp_prometheus_collect_metrics` action
6. **Metrics Endpoint** (`src/Endpoint/MetricsEndpoint.php`)
- Custom rewrite rule for `/metrics/`
- Bearer token authentication
- Prometheus text format output
- Cache control headers
7. **Admin Settings** (`src/Admin/Settings.php`)
- Settings page under Settings > Metrics
- License configuration form
- Auth token management
- Metric toggle checkboxes
### Directory Structure
```txt
wp-prometheus/
├── .gitea/workflows/
│ └── release.yml # CI/CD pipeline
├── assets/
│ ├── css/ # Admin/Frontend styles
│ └── js/
│ └── admin.js # Admin JavaScript
├── languages/ # Translation files
├── lib/
│ └── wc-licensed-product-client/ # Git submodule
├── releases/ # Release packages
├── src/
│ ├── Admin/
│ │ └── Settings.php
│ ├── Endpoint/
│ │ └── MetricsEndpoint.php
│ ├── License/
│ │ └── Manager.php
│ ├── Metrics/
│ │ └── Collector.php
│ ├── Installer.php
│ ├── Plugin.php
│ └── index.php
├── CHANGELOG.md
├── CLAUDE.md
├── composer.json
├── index.php
├── PLAN.md
├── README.md
├── uninstall.php
└── wp-prometheus.php
```
## Default Metrics
The plugin provides the following default metrics (can be toggled in settings):
| Metric | Type | Labels | Description |
|--------|------|--------|-------------|
| wordpress_info | Gauge | version, php_version, multisite | WordPress installation info |
| wordpress_users_total | Gauge | role | Total users by role |
| wordpress_posts_total | Gauge | post_type, status | Total posts by type and status |
| wordpress_comments_total | Gauge | status | Total comments by status |
| wordpress_plugins_total | Gauge | status | Total plugins (active/inactive) |
## Extensibility
### Adding Custom Metrics
Third-party plugins can add custom metrics using the `wp_prometheus_collect_metrics` action:
```php
add_action( 'wp_prometheus_collect_metrics', function( $collector ) {
// Register a custom gauge
$gauge = $collector->register_gauge(
'my_custom_metric',
'Description of my metric',
array( 'label1', 'label2' )
);
// Set the value
$gauge->set( 42, array( 'value1', 'value2' ) );
} );
```
### Available Methods
- `$collector->register_gauge( $name, $help, $labels )`
- `$collector->register_counter( $name, $help, $labels )`
- `$collector->register_histogram( $name, $help, $labels, $buckets )`
## Authentication
The `/metrics` endpoint requires authentication using a Bearer token:
```yaml
# Prometheus configuration
scrape_configs:
- job_name: 'wordpress'
static_configs:
- targets: ['example.com']
metrics_path: '/metrics/'
scheme: 'https'
authorization:
type: Bearer
credentials: 'your-auth-token'
```
Alternatively, the token can be passed as a query parameter (for testing):
```txt
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
- Cron job metrics
- Transient cache metrics
### Version 0.3.0
- Custom metric builder in admin
- Metric export/import
- Grafana dashboard templates
## Dependencies
- PHP 8.3+
- WordPress 6.4+
- Composer packages:
- `promphp/prometheus_client_php` - Prometheus client library
- `magdev/wc-licensed-product-client` - License validation
## Security Considerations
1. Auth token stored securely in WordPress options
2. Bearer token authentication for metrics endpoint
3. Admin capability check for settings
4. Nonce verification for AJAX requests
5. Input sanitization and output escaping
6. Direct file access prevention
## License
GPL v2 or later

164
README.md Normal file
View File

@@ -0,0 +1,164 @@
# WP Prometheus
A WordPress plugin that provides a Prometheus-compatible `/metrics` endpoint with extensible hooks for custom metrics.
## Features
- Prometheus-compatible authenticated `/metrics` endpoint
- Default WordPress metrics (users, posts, comments, plugins)
- Extensible by other plugins using hooks
- Settings page under Settings > Metrics
- Bearer token authentication
- License management integration
## Requirements
- PHP 8.3 or higher
- WordPress 6.4 or higher
- Composer (for development)
## Installation
### From Release Package
1. Download the latest release from the [releases page](https://src.bundespruefstelle.ch/magdev/wp-prometheus/releases)
2. Upload the zip file via Plugins > Add New > Upload Plugin
3. Activate the plugin
4. Configure settings under Settings > Metrics
### From Source
1. Clone the repository to your WordPress plugins directory:
```bash
cd wp-content/plugins/
git clone https://src.bundespruefstelle.ch/magdev/wp-prometheus.git
cd wp-prometheus
git submodule update --init --recursive
composer install
```
2. Activate the plugin in WordPress admin
## Configuration
### License
1. Go to Settings > Metrics
2. Enter your license server URL, license key, and server secret
3. Click "Save License Settings"
4. Click "Validate License" or "Activate License"
### Authentication Token
A random auth token is generated on activation. You can view it or regenerate it in Settings > Metrics.
### Prometheus Configuration
Add the following to your `prometheus.yml`:
```yaml
scrape_configs:
- job_name: 'wordpress'
static_configs:
- targets: ['your-wordpress-site.com']
metrics_path: '/metrics/'
scheme: 'https'
authorization:
type: Bearer
credentials: 'your-auth-token-from-settings'
```
## Default Metrics
| Metric | Type | Labels | Description |
|--------|------|--------|-------------|
| wordpress_info | Gauge | version, php_version, multisite | WordPress installation info |
| wordpress_users_total | Gauge | role | Total users by role |
| wordpress_posts_total | Gauge | post_type, status | Total posts by type and status |
| wordpress_comments_total | Gauge | status | Total comments by status |
| wordpress_plugins_total | Gauge | status | Total plugins (active/inactive) |
## Extending with Custom Metrics
Add your own metrics using the `wp_prometheus_collect_metrics` action:
```php
add_action( 'wp_prometheus_collect_metrics', function( $collector ) {
// Register a gauge
$gauge = $collector->register_gauge(
'my_custom_metric',
'Description of my metric',
array( 'label1', 'label2' )
);
// Set value with labels
$gauge->set( 42, array( 'value1', 'value2' ) );
} );
```
### Available Methods
```php
// Gauge (can go up and down)
$gauge = $collector->register_gauge( $name, $help, $labels );
$gauge->set( $value, $labelValues );
// Counter (only goes up)
$counter = $collector->register_counter( $name, $help, $labels );
$counter->inc( $labelValues );
$counter->incBy( $amount, $labelValues );
// Histogram (for distributions)
$histogram = $collector->register_histogram( $name, $help, $labels, $buckets );
$histogram->observe( $value, $labelValues );
```
## Development
### Build for Release
```bash
# Set PHP platform version
composer config platform.php 8.3.0
# Install production dependencies
composer install --no-dev --optimize-autoloader
# Compile translations
for po in languages/*.po; do msgfmt -o "${po%.po}.mo" "$po"; done
```
### Create Release Package
From the plugins directory (parent of wp-prometheus):
```bash
cd /wp-content/plugins/
zip -r wp-prometheus/releases/wp-prometheus-x.x.x.zip wp-prometheus \
-x "wp-prometheus/.git/*" \
-x "wp-prometheus/.gitea/*" \
-x "wp-prometheus/.claude/*" \
-x "wp-prometheus/CLAUDE.md" \
-x "wp-prometheus/wp-core" \
-x "wp-prometheus/wp-plugins" \
-x "wp-prometheus/releases/*" \
-x "wp-prometheus/composer.lock" \
-x "*.DS_Store"
```
## Author
**Marco Graetsch**
- Website: <https://src.bundespruefstelle.ch/magdev>
- Email: magdev3.0@gmail.com
## License
This plugin is licensed under the GPL v2 or later.
## Credits
- [PromPHP/prometheus_client_php](https://github.com/PromPHP/prometheus_client_php) - Prometheus client library
- Built with Claude AI assistance

100
assets/js/admin.js Normal file
View File

@@ -0,0 +1,100 @@
/**
* WP Prometheus Admin JavaScript
*
* @package WP_Prometheus
*/
(function($) {
'use strict';
$(document).ready(function() {
// Validate license button.
$('#wp-prometheus-validate-license').on('click', function(e) {
e.preventDefault();
performLicenseAction('wp_prometheus_validate_license', 'Validating...');
});
// Activate license button.
$('#wp-prometheus-activate-license').on('click', function(e) {
e.preventDefault();
performLicenseAction('wp_prometheus_activate_license', 'Activating...');
});
// Regenerate token button.
$('#wp-prometheus-regenerate-token').on('click', function(e) {
e.preventDefault();
if (confirm('Are you sure you want to regenerate the auth token? You will need to update your Prometheus configuration.')) {
var newToken = generateToken(32);
$('#wp_prometheus_auth_token').val(newToken);
}
});
/**
* Perform a license action via AJAX.
*
* @param {string} action AJAX action name.
* @param {string} message Loading message.
*/
function performLicenseAction(action, message) {
var $spinner = $('#wp-prometheus-license-spinner');
var $message = $('#wp-prometheus-license-message');
$spinner.addClass('is-active');
$message.hide();
$.ajax({
url: wpPrometheus.ajaxUrl,
type: 'POST',
data: {
action: action,
nonce: wpPrometheus.nonce
},
success: function(response) {
$spinner.removeClass('is-active');
if (response.success) {
$message
.removeClass('notice-error')
.addClass('notice notice-success')
.html('<p>' + response.data.message + '</p>')
.show();
// Reload page after successful validation/activation.
setTimeout(function() {
location.reload();
}, 1500);
} else {
$message
.removeClass('notice-success')
.addClass('notice notice-error')
.html('<p>' + (response.data.message || 'An error occurred.') + '</p>')
.show();
}
},
error: function() {
$spinner.removeClass('is-active');
$message
.removeClass('notice-success')
.addClass('notice notice-error')
.html('<p>Connection error. Please try again.</p>')
.show();
}
});
}
/**
* Generate a random token.
*
* @param {number} length Token length.
* @return {string} Generated token.
*/
function generateToken(length) {
var charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
var token = '';
for (var i = 0; i < length; i++) {
token += charset.charAt(Math.floor(Math.random() * charset.length));
}
return token;
}
});
})(jQuery);

61
composer.json Normal file
View File

@@ -0,0 +1,61 @@
{
"name": "magdev/wp-prometheus",
"description": "WordPress Prometheus metrics endpoint with extensible hooks for custom metrics",
"type": "wordpress-plugin",
"license": "GPL-2.0-or-later",
"authors": [
{
"name": "Marco Graetsch",
"email": "magdev3.0@gmail.com",
"homepage": "https://src.bundespruefstelle.ch/magdev"
}
],
"homepage": "https://src.bundespruefstelle.ch/magdev/wp-prometheus",
"support": {
"issues": "https://src.bundespruefstelle.ch/magdev/wp-prometheus/issues"
},
"repositories": [
{
"type": "path",
"url": "lib/wc-licensed-product-client"
}
],
"require": {
"php": ">=8.3",
"magdev/wc-licensed-product-client": "dev-main",
"promphp/prometheus_client_php": "^2.10"
},
"require-dev": {
"phpunit/phpunit": "^10.0",
"squizlabs/php_codesniffer": "^3.7",
"wp-coding-standards/wpcs": "^3.0",
"phpcompatibility/phpcompatibility-wp": "*"
},
"autoload": {
"psr-4": {
"Magdev\\WpPrometheus\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Magdev\\WpPrometheus\\Tests\\": "tests/"
}
},
"config": {
"allow-plugins": {
"dealerdirect/phpcodesniffer-composer-installer": true
},
"optimize-autoloader": true,
"sort-packages": true,
"platform": {
"php": "8.3.0"
}
},
"scripts": {
"phpcs": "phpcs",
"phpcbf": "phpcbf",
"test": "phpunit"
},
"minimum-stability": "dev",
"prefer-stable": true
}

11
index.php Normal file
View File

@@ -0,0 +1,11 @@
<?php
/**
* Silence is golden.
*
* @package WP_Prometheus
*/
// Prevent direct file access.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}

406
src/Admin/Settings.php Normal file
View File

@@ -0,0 +1,406 @@
<?php
/**
* Admin settings class.
*
* @package WP_Prometheus
*/
namespace Magdev\WpPrometheus\Admin;
use Magdev\WpPrometheus\License\Manager as LicenseManager;
// Prevent direct file access.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Settings class.
*
* Handles plugin settings page in the WordPress admin.
*/
class Settings {
/**
* Constructor.
*/
public function __construct() {
add_action( 'admin_menu', array( $this, 'add_settings_page' ) );
add_action( 'admin_init', array( $this, 'register_settings' ) );
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
}
/**
* Add settings page to admin menu.
*
* @return void
*/
public function add_settings_page(): void {
add_options_page(
__( 'Metrics Settings', 'wp-prometheus' ),
__( 'Metrics', 'wp-prometheus' ),
'manage_options',
'wp-prometheus',
array( $this, 'render_settings_page' )
);
}
/**
* Register plugin settings.
*
* @return void
*/
public function register_settings(): void {
// License settings section.
add_settings_section(
'wp_prometheus_license_section',
__( 'License Settings', 'wp-prometheus' ),
array( $this, 'render_license_section' ),
'wp-prometheus'
);
// Auth token section.
add_settings_section(
'wp_prometheus_auth_section',
__( 'Authentication', 'wp-prometheus' ),
array( $this, 'render_auth_section' ),
'wp-prometheus'
);
// Metrics section.
add_settings_section(
'wp_prometheus_metrics_section',
__( 'Default Metrics', 'wp-prometheus' ),
array( $this, 'render_metrics_section' ),
'wp-prometheus'
);
// Register settings.
register_setting( 'wp_prometheus_settings', 'wp_prometheus_auth_token', array(
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
) );
register_setting( 'wp_prometheus_settings', 'wp_prometheus_enabled_metrics', array(
'type' => 'array',
'sanitize_callback' => array( $this, 'sanitize_metrics' ),
) );
// Auth token field.
add_settings_field(
'wp_prometheus_auth_token',
__( 'Auth Token', 'wp-prometheus' ),
array( $this, 'render_auth_token_field' ),
'wp-prometheus',
'wp_prometheus_auth_section'
);
// Enabled metrics field.
add_settings_field(
'wp_prometheus_enabled_metrics',
__( 'Enabled Metrics', 'wp-prometheus' ),
array( $this, 'render_enabled_metrics_field' ),
'wp-prometheus',
'wp_prometheus_metrics_section'
);
}
/**
* Enqueue admin scripts.
*
* @param string $hook_suffix Current admin page.
* @return void
*/
public function enqueue_scripts( string $hook_suffix ): void {
if ( 'settings_page_wp-prometheus' !== $hook_suffix ) {
return;
}
wp_enqueue_script(
'wp-prometheus-admin',
WP_PROMETHEUS_URL . 'assets/js/admin.js',
array( 'jquery' ),
WP_PROMETHEUS_VERSION,
true
);
wp_localize_script( 'wp-prometheus-admin', 'wpPrometheus', array(
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'wp_prometheus_license_action' ),
) );
}
/**
* Render the settings page.
*
* @return void
*/
public function render_settings_page(): void {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
// Handle license settings save.
if ( isset( $_POST['wp_prometheus_license_nonce'] ) && wp_verify_nonce( sanitize_key( $_POST['wp_prometheus_license_nonce'] ), 'wp_prometheus_save_license' ) ) {
LicenseManager::save_settings( array(
'license_key' => isset( $_POST['license_key'] ) ? sanitize_text_field( wp_unslash( $_POST['license_key'] ) ) : '',
'server_url' => isset( $_POST['license_server_url'] ) ? esc_url_raw( wp_unslash( $_POST['license_server_url'] ) ) : '',
'server_secret' => isset( $_POST['license_server_secret'] ) ? sanitize_text_field( wp_unslash( $_POST['license_server_secret'] ) ) : '',
) );
echo '<div class="notice notice-success is-dismissible"><p>' . esc_html__( 'License settings saved.', 'wp-prometheus' ) . '</p></div>';
}
?>
<div class="wrap">
<h1><?php echo esc_html( get_admin_page_title() ); ?></h1>
<?php $this->render_license_form(); ?>
<form method="post" action="options.php">
<?php
settings_fields( 'wp_prometheus_settings' );
do_settings_sections( 'wp-prometheus' );
submit_button();
?>
</form>
<?php $this->render_endpoint_info(); ?>
</div>
<?php
}
/**
* Render license settings form.
*
* @return void
*/
private function render_license_form(): void {
$license_key = LicenseManager::get_license_key();
$server_url = LicenseManager::get_server_url();
$license_status = LicenseManager::get_cached_status();
$license_data = LicenseManager::get_cached_data();
$last_check = LicenseManager::get_last_check();
$status_classes = array(
'valid' => 'notice-success',
'invalid' => 'notice-error',
'expired' => 'notice-warning',
'revoked' => 'notice-error',
'inactive' => 'notice-warning',
'unchecked' => 'notice-info',
'unconfigured' => 'notice-info',
);
$status_messages = array(
'valid' => __( 'License is active and valid.', 'wp-prometheus' ),
'invalid' => __( 'License is invalid.', 'wp-prometheus' ),
'expired' => __( 'License has expired.', 'wp-prometheus' ),
'revoked' => __( 'License has been revoked.', 'wp-prometheus' ),
'inactive' => __( 'License is inactive.', 'wp-prometheus' ),
'unchecked' => __( 'License has not been validated yet.', 'wp-prometheus' ),
'unconfigured' => __( 'License server is not configured.', 'wp-prometheus' ),
);
$status_class = $status_classes[ $license_status ] ?? 'notice-info';
$status_message = $status_messages[ $license_status ] ?? __( 'Unknown status.', 'wp-prometheus' );
?>
<div class="wp-prometheus-license-status notice <?php echo esc_attr( $status_class ); ?>" style="padding: 12px;">
<strong><?php echo esc_html( $status_message ); ?></strong>
<?php if ( 'valid' === $license_status && ! empty( $license_data['expires_at'] ) ) : ?>
<br><span class="description">
<?php
printf(
/* translators: %s: Expiration date */
esc_html__( 'Expires: %s', 'wp-prometheus' ),
esc_html( $license_data['expires_at'] )
);
?>
</span>
<?php endif; ?>
<?php if ( $last_check > 0 ) : ?>
<br><span class="description">
<?php
printf(
/* translators: %s: Time ago */
esc_html__( 'Last checked: %s ago', 'wp-prometheus' ),
esc_html( human_time_diff( $last_check, time() ) )
);
?>
</span>
<?php endif; ?>
</div>
<form method="post" action="" id="wp-prometheus-license-form">
<?php wp_nonce_field( 'wp_prometheus_save_license', 'wp_prometheus_license_nonce' ); ?>
<table class="form-table">
<tr>
<th scope="row">
<label for="license_server_url"><?php esc_html_e( 'License Server URL', 'wp-prometheus' ); ?></label>
</th>
<td>
<input type="url" name="license_server_url" id="license_server_url"
value="<?php echo esc_attr( $server_url ); ?>"
class="regular-text" placeholder="https://example.com">
</td>
</tr>
<tr>
<th scope="row">
<label for="license_key"><?php esc_html_e( 'License Key', 'wp-prometheus' ); ?></label>
</th>
<td>
<input type="text" name="license_key" id="license_key"
value="<?php echo esc_attr( $license_key ); ?>"
class="regular-text" placeholder="XXXX-XXXX-XXXX-XXXX">
</td>
</tr>
<tr>
<th scope="row">
<label for="license_server_secret"><?php esc_html_e( 'Server Secret', 'wp-prometheus' ); ?></label>
</th>
<td>
<input type="password" name="license_server_secret" id="license_server_secret"
value="" class="regular-text" placeholder="<?php echo esc_attr( ! empty( LicenseManager::get_server_secret() ) ? '••••••••••••••••' : '' ); ?>">
<p class="description"><?php esc_html_e( 'Leave empty to keep existing.', 'wp-prometheus' ); ?></p>
</td>
</tr>
</table>
<p class="submit">
<?php submit_button( __( 'Save License Settings', 'wp-prometheus' ), 'primary', 'submit', false ); ?>
<button type="button" id="wp-prometheus-validate-license" class="button button-secondary" style="margin-left: 10px;">
<?php esc_html_e( 'Validate License', 'wp-prometheus' ); ?>
</button>
<button type="button" id="wp-prometheus-activate-license" class="button button-secondary" style="margin-left: 10px;">
<?php esc_html_e( 'Activate License', 'wp-prometheus' ); ?>
</button>
<span id="wp-prometheus-license-spinner" class="spinner" style="float: none;"></span>
</p>
</form>
<div id="wp-prometheus-license-message" style="display: none; margin-top: 10px;"></div>
<hr>
<?php
}
/**
* Render license section description.
*
* @return void
*/
public function render_license_section(): void {
// License section rendered separately in render_license_form().
}
/**
* Render auth section description.
*
* @return void
*/
public function render_auth_section(): void {
echo '<p>' . esc_html__( 'Configure authentication for the /metrics endpoint.', 'wp-prometheus' ) . '</p>';
}
/**
* Render metrics section description.
*
* @return void
*/
public function render_metrics_section(): void {
echo '<p>' . esc_html__( 'Select which default metrics to expose.', 'wp-prometheus' ) . '</p>';
}
/**
* Render auth token field.
*
* @return void
*/
public function render_auth_token_field(): void {
$token = get_option( 'wp_prometheus_auth_token', '' );
?>
<input type="text" name="wp_prometheus_auth_token" id="wp_prometheus_auth_token"
value="<?php echo esc_attr( $token ); ?>" class="regular-text" readonly>
<button type="button" id="wp-prometheus-regenerate-token" class="button button-secondary">
<?php esc_html_e( 'Regenerate', 'wp-prometheus' ); ?>
</button>
<p class="description">
<?php esc_html_e( 'Use this token to authenticate Prometheus scrape requests.', 'wp-prometheus' ); ?>
</p>
<?php
}
/**
* Render enabled metrics field.
*
* @return void
*/
public function render_enabled_metrics_field(): void {
$enabled = get_option( 'wp_prometheus_enabled_metrics', array() );
$metrics = array(
'wordpress_info' => __( 'WordPress Info (version, PHP version, multisite)', 'wp-prometheus' ),
'wordpress_users_total' => __( 'Total Users by Role', 'wp-prometheus' ),
'wordpress_posts_total' => __( 'Total Posts by Type and Status', 'wp-prometheus' ),
'wordpress_comments_total' => __( 'Total Comments by Status', 'wp-prometheus' ),
'wordpress_plugins_total' => __( 'Total Plugins (active/inactive)', 'wp-prometheus' ),
);
foreach ( $metrics as $key => $label ) {
?>
<label style="display: block; margin-bottom: 5px;">
<input type="checkbox" name="wp_prometheus_enabled_metrics[]"
value="<?php echo esc_attr( $key ); ?>"
<?php checked( in_array( $key, $enabled, true ) ); ?>>
<?php echo esc_html( $label ); ?>
</label>
<?php
}
}
/**
* Render endpoint info.
*
* @return void
*/
private function render_endpoint_info(): void {
$token = get_option( 'wp_prometheus_auth_token', '' );
$endpoint_url = home_url( '/metrics/' );
?>
<hr>
<h2><?php esc_html_e( 'Prometheus Configuration', 'wp-prometheus' ); ?></h2>
<p><?php esc_html_e( 'Add the following to your prometheus.yml:', 'wp-prometheus' ); ?></p>
<pre style="background: #f1f1f1; padding: 15px; overflow-x: auto;">
scrape_configs:
- job_name: 'wordpress'
static_configs:
- targets: ['<?php echo esc_html( wp_parse_url( home_url(), PHP_URL_HOST ) ); ?>']
metrics_path: '/metrics/'
scheme: '<?php echo esc_html( wp_parse_url( home_url(), PHP_URL_SCHEME ) ); ?>'
authorization:
type: Bearer
credentials: '<?php echo esc_html( $token ); ?>'</pre>
<p>
<?php
printf(
/* translators: %s: Endpoint URL */
esc_html__( 'Metrics endpoint: %s', 'wp-prometheus' ),
'<code>' . esc_url( $endpoint_url ) . '</code>'
);
?>
</p>
<?php
}
/**
* Sanitize enabled metrics.
*
* @param mixed $input Input value.
* @return array
*/
public function sanitize_metrics( $input ): array {
if ( ! is_array( $input ) ) {
return array();
}
return array_map( 'sanitize_text_field', $input );
}
}

View File

@@ -0,0 +1,148 @@
<?php
/**
* Metrics endpoint class.
*
* @package WP_Prometheus
*/
namespace Magdev\WpPrometheus\Endpoint;
use Magdev\WpPrometheus\Metrics\Collector;
// Prevent direct file access.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* MetricsEndpoint class.
*
* Provides the /metrics endpoint for Prometheus scraping.
*/
class MetricsEndpoint {
/**
* Metrics collector instance.
*
* @var Collector
*/
private Collector $collector;
/**
* Constructor.
*
* @param Collector $collector Metrics collector instance.
*/
public function __construct( Collector $collector ) {
$this->collector = $collector;
$this->init_hooks();
}
/**
* Initialize WordPress hooks.
*
* @return void
*/
private function init_hooks(): void {
add_action( 'init', array( $this, 'register_endpoint' ) );
add_action( 'template_redirect', array( $this, 'handle_request' ) );
}
/**
* Register the metrics endpoint rewrite rule.
*
* @return void
*/
public function register_endpoint(): void {
add_rewrite_rule(
'^metrics/?$',
'index.php?wp_prometheus_metrics=1',
'top'
);
add_rewrite_tag( '%wp_prometheus_metrics%', '([^&]+)' );
}
/**
* Handle the metrics endpoint request.
*
* @return void
*/
public function handle_request(): void {
if ( ! get_query_var( 'wp_prometheus_metrics' ) ) {
return;
}
// Authenticate the request.
if ( ! $this->authenticate() ) {
status_header( 401 );
header( 'WWW-Authenticate: Bearer realm="WP Prometheus Metrics"' );
header( 'Content-Type: text/plain; charset=utf-8' );
echo 'Unauthorized';
exit;
}
// Output metrics.
status_header( 200 );
header( 'Content-Type: text/plain; version=0.0.4; charset=utf-8' );
header( 'Cache-Control: no-cache, no-store, must-revalidate' );
header( 'Pragma: no-cache' );
header( 'Expires: 0' );
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Prometheus format.
echo $this->collector->render();
exit;
}
/**
* Authenticate the metrics request.
*
* @return bool
*/
private function authenticate(): bool {
$auth_token = get_option( 'wp_prometheus_auth_token', '' );
// If no token is set, deny access.
if ( empty( $auth_token ) ) {
return false;
}
// Check for Bearer token in Authorization header.
$auth_header = $this->get_authorization_header();
if ( ! empty( $auth_header ) && preg_match( '/Bearer\s+(.*)$/i', $auth_header, $matches ) ) {
return hash_equals( $auth_token, $matches[1] );
}
// Check for token in query parameter (less secure but useful for testing).
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Auth token check.
if ( isset( $_GET['token'] ) && hash_equals( $auth_token, sanitize_text_field( wp_unslash( $_GET['token'] ) ) ) ) {
return true;
}
return false;
}
/**
* Get the Authorization header from the request.
*
* @return string
*/
private function get_authorization_header(): string {
if ( isset( $_SERVER['HTTP_AUTHORIZATION'] ) ) {
return sanitize_text_field( wp_unslash( $_SERVER['HTTP_AUTHORIZATION'] ) );
}
if ( isset( $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] ) ) {
return sanitize_text_field( wp_unslash( $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] ) );
}
if ( function_exists( 'apache_request_headers' ) ) {
$headers = apache_request_headers();
if ( isset( $headers['Authorization'] ) ) {
return sanitize_text_field( $headers['Authorization'] );
}
}
return '';
}
}

103
src/Installer.php Normal file
View File

@@ -0,0 +1,103 @@
<?php
/**
* Plugin installer class.
*
* @package WP_Prometheus
*/
namespace Magdev\WpPrometheus;
// Prevent direct file access.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Installer class.
*
* Handles plugin activation, deactivation, and uninstallation.
*/
final class Installer {
/**
* Plugin activation.
*
* @return void
*/
public static function activate(): void {
// Set default options.
self::set_default_options();
// Flush rewrite rules for the metrics endpoint.
flush_rewrite_rules();
// Store activation time for reference.
update_option( 'wp_prometheus_activated', time() );
}
/**
* Plugin deactivation.
*
* @return void
*/
public static function deactivate(): void {
// Flush rewrite rules.
flush_rewrite_rules();
}
/**
* Plugin uninstallation.
*
* @return void
*/
public static function uninstall(): void {
// Remove all plugin options.
$options = array(
'wp_prometheus_activated',
'wp_prometheus_license_key',
'wp_prometheus_license_server_url',
'wp_prometheus_license_server_secret',
'wp_prometheus_license_status',
'wp_prometheus_license_data',
'wp_prometheus_license_last_check',
'wp_prometheus_auth_token',
'wp_prometheus_enable_default_metrics',
'wp_prometheus_enabled_metrics',
);
foreach ( $options as $option ) {
delete_option( $option );
}
// Remove transients.
delete_transient( 'wp_prometheus_license_check' );
}
/**
* Set default plugin options.
*
* @return void
*/
private static function set_default_options(): void {
// Generate a random auth token if not set.
if ( ! get_option( 'wp_prometheus_auth_token' ) ) {
update_option( 'wp_prometheus_auth_token', wp_generate_password( 32, false ) );
}
// Enable default metrics by default.
if ( false === get_option( 'wp_prometheus_enable_default_metrics' ) ) {
update_option( 'wp_prometheus_enable_default_metrics', 1 );
}
// Default enabled metrics.
if ( false === get_option( 'wp_prometheus_enabled_metrics' ) ) {
update_option( 'wp_prometheus_enabled_metrics', array(
'wordpress_info',
'wordpress_users_total',
'wordpress_posts_total',
'wordpress_comments_total',
'wordpress_plugins_total',
) );
}
}
}

496
src/License/Manager.php Normal file
View File

@@ -0,0 +1,496 @@
<?php
/**
* License management class.
*
* @package WP_Prometheus
*/
namespace Magdev\WpPrometheus\License;
use Magdev\WcLicensedProductClient\SecureLicenseClient;
use Magdev\WcLicensedProductClient\Dto\LicenseState;
use Magdev\WcLicensedProductClient\Exception\LicenseException;
use Magdev\WcLicensedProductClient\Exception\LicenseNotFoundException;
use Magdev\WcLicensedProductClient\Exception\LicenseExpiredException;
use Magdev\WcLicensedProductClient\Exception\LicenseRevokedException;
use Magdev\WcLicensedProductClient\Exception\LicenseInactiveException;
use Magdev\WcLicensedProductClient\Exception\DomainMismatchException;
use Magdev\WcLicensedProductClient\Exception\MaxActivationsReachedException;
use Magdev\WcLicensedProductClient\Exception\RateLimitExceededException;
use Magdev\WcLicensedProductClient\Security\SignatureException;
use Symfony\Component\HttpClient\HttpClient;
// Prevent direct file access.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* License Manager class.
*
* Handles license validation, activation, and status checking.
*/
final class Manager {
/**
* Option names for license settings.
*/
public const OPTION_LICENSE_KEY = 'wp_prometheus_license_key';
public const OPTION_SERVER_URL = 'wp_prometheus_license_server_url';
public const OPTION_SERVER_SECRET = 'wp_prometheus_license_server_secret';
public const OPTION_LICENSE_STATUS = 'wp_prometheus_license_status';
public const OPTION_LICENSE_DATA = 'wp_prometheus_license_data';
public const OPTION_LAST_CHECK = 'wp_prometheus_license_last_check';
/**
* Transient name for caching license validation.
*/
private const TRANSIENT_LICENSE_CHECK = 'wp_prometheus_license_check';
/**
* Cache TTL in seconds (24 hours).
*/
private const CACHE_TTL = 86400;
/**
* Singleton instance.
*
* @var Manager|null
*/
private static ?Manager $instance = null;
/**
* License client instance.
*
* @var SecureLicenseClient|null
*/
private ?SecureLicenseClient $client = null;
/**
* Get singleton instance.
*
* @return Manager
*/
public static function get_instance(): Manager {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Private constructor.
*/
private function __construct() {
$this->init_hooks();
}
/**
* Initialize WordPress hooks.
*
* @return void
*/
private function init_hooks(): void {
add_action( 'wp_ajax_wp_prometheus_validate_license', array( $this, 'ajax_validate_license' ) );
add_action( 'wp_ajax_wp_prometheus_activate_license', array( $this, 'ajax_activate_license' ) );
add_action( 'wp_ajax_wp_prometheus_deactivate_license', array( $this, 'ajax_deactivate_license' ) );
add_action( 'wp_ajax_wp_prometheus_check_license_status', array( $this, 'ajax_check_status' ) );
}
/**
* Initialize the license client.
*
* @return bool True if client was initialized successfully.
*/
private function init_client(): bool {
if ( null !== $this->client ) {
return true;
}
$server_url = self::get_server_url();
$server_secret = self::get_server_secret();
if ( empty( $server_url ) || empty( $server_secret ) ) {
return false;
}
try {
$this->client = new SecureLicenseClient(
httpClient: HttpClient::create(),
baseUrl: $server_url,
serverSecret: $server_secret,
);
return true;
} catch ( \Throwable $e ) {
return false;
}
}
/**
* Validate the current license.
*
* @return array{success: bool, message: string, data?: array}
*/
public function validate(): array {
if ( ! $this->init_client() ) {
return array(
'success' => false,
'message' => __( 'License server configuration is incomplete.', 'wp-prometheus' ),
);
}
$license_key = self::get_license_key();
if ( empty( $license_key ) ) {
return array(
'success' => false,
'message' => __( 'No license key provided.', 'wp-prometheus' ),
);
}
$domain = wp_parse_url( home_url(), PHP_URL_HOST );
try {
$result = $this->client->validate( $license_key, $domain );
$this->update_cached_status( 'valid', array(
'product_id' => $result->productId,
'expires_at' => $result->expiresAt?->format( 'c' ),
'version_id' => $result->versionId,
) );
return array(
'success' => true,
'message' => __( 'License validated successfully.', 'wp-prometheus' ),
'data' => array(
'status' => 'valid',
'product_id' => $result->productId,
'expires_at' => $result->expiresAt?->format( 'Y-m-d' ),
'lifetime' => $result->isLifetime(),
),
);
} catch ( LicenseNotFoundException $e ) {
$this->update_cached_status( 'invalid' );
return array(
'success' => false,
'message' => __( 'License key not found.', 'wp-prometheus' ),
);
} catch ( LicenseExpiredException $e ) {
$this->update_cached_status( 'expired' );
return array(
'success' => false,
'message' => __( 'Your license has expired.', 'wp-prometheus' ),
);
} catch ( LicenseRevokedException $e ) {
$this->update_cached_status( 'revoked' );
return array(
'success' => false,
'message' => __( 'Your license has been revoked.', 'wp-prometheus' ),
);
} catch ( LicenseInactiveException $e ) {
$this->update_cached_status( 'inactive' );
return array(
'success' => false,
'message' => __( 'License is inactive. Please activate it first.', 'wp-prometheus' ),
);
} catch ( DomainMismatchException $e ) {
$this->update_cached_status( 'invalid' );
return array(
'success' => false,
'message' => __( 'This license is not activated for this domain.', 'wp-prometheus' ),
);
} catch ( SignatureException $e ) {
return array(
'success' => false,
'message' => __( 'License verification failed. Please check your server secret.', 'wp-prometheus' ),
);
} catch ( RateLimitExceededException $e ) {
return array(
'success' => false,
'message' => __( 'Too many requests. Please try again later.', 'wp-prometheus' ),
);
} catch ( LicenseException $e ) {
return array(
'success' => false,
'message' => sprintf(
/* translators: %s: Error message */
__( 'License validation failed: %s', 'wp-prometheus' ),
$e->getMessage()
),
);
} catch ( \Throwable $e ) {
return array(
'success' => false,
'message' => __( 'Unable to verify license. Please try again later.', 'wp-prometheus' ),
);
}
}
/**
* Activate the license for this domain.
*
* @return array{success: bool, message: string, data?: array}
*/
public function activate(): array {
if ( ! $this->init_client() ) {
return array(
'success' => false,
'message' => __( 'License server configuration is incomplete.', 'wp-prometheus' ),
);
}
$license_key = self::get_license_key();
if ( empty( $license_key ) ) {
return array(
'success' => false,
'message' => __( 'No license key provided.', 'wp-prometheus' ),
);
}
$domain = wp_parse_url( home_url(), PHP_URL_HOST );
try {
$result = $this->client->activate( $license_key, $domain );
if ( $result->success ) {
return $this->validate();
}
return array(
'success' => false,
'message' => $result->message,
);
} catch ( MaxActivationsReachedException $e ) {
return array(
'success' => false,
'message' => __( 'Maximum activations reached. Please deactivate another site.', 'wp-prometheus' ),
);
} catch ( LicenseNotFoundException $e ) {
return array(
'success' => false,
'message' => __( 'License key not found.', 'wp-prometheus' ),
);
} catch ( LicenseExpiredException $e ) {
return array(
'success' => false,
'message' => __( 'Your license has expired.', 'wp-prometheus' ),
);
} catch ( SignatureException $e ) {
return array(
'success' => false,
'message' => __( 'License verification failed. Please check your server secret.', 'wp-prometheus' ),
);
} catch ( LicenseException $e ) {
return array(
'success' => false,
'message' => sprintf(
/* translators: %s: Error message */
__( 'License activation failed: %s', 'wp-prometheus' ),
$e->getMessage()
),
);
} catch ( \Throwable $e ) {
return array(
'success' => false,
'message' => __( 'Unable to activate license. Please try again later.', 'wp-prometheus' ),
);
}
}
/**
* Check if the license is currently valid.
*
* @return bool
*/
public static function is_license_valid(): bool {
$status = get_option( self::OPTION_LICENSE_STATUS, 'unchecked' );
return 'valid' === $status;
}
/**
* Get the license key.
*
* @return string
*/
public static function get_license_key(): string {
return get_option( self::OPTION_LICENSE_KEY, '' );
}
/**
* Get the license server URL.
*
* @return string
*/
public static function get_server_url(): string {
return get_option( self::OPTION_SERVER_URL, '' );
}
/**
* Get the server secret.
*
* @return string
*/
public static function get_server_secret(): string {
return get_option( self::OPTION_SERVER_SECRET, '' );
}
/**
* Get cached license status.
*
* @return string
*/
public static function get_cached_status(): string {
return get_option( self::OPTION_LICENSE_STATUS, 'unchecked' );
}
/**
* Get cached license data.
*
* @return array
*/
public static function get_cached_data(): array {
return get_option( self::OPTION_LICENSE_DATA, array() );
}
/**
* Get last check timestamp.
*
* @return int
*/
public static function get_last_check(): int {
return (int) get_option( self::OPTION_LAST_CHECK, 0 );
}
/**
* Save license settings.
*
* @param array $data Settings data.
* @return bool
*/
public static function save_settings( array $data ): bool {
if ( isset( $data['license_key'] ) ) {
update_option( self::OPTION_LICENSE_KEY, sanitize_text_field( $data['license_key'] ) );
}
if ( isset( $data['server_url'] ) ) {
update_option( self::OPTION_SERVER_URL, esc_url_raw( $data['server_url'] ) );
}
if ( isset( $data['server_secret'] ) ) {
$secret = sanitize_text_field( $data['server_secret'] );
if ( ! empty( $secret ) ) {
update_option( self::OPTION_SERVER_SECRET, $secret );
}
}
update_option( self::OPTION_LICENSE_STATUS, 'unchecked' );
delete_transient( self::TRANSIENT_LICENSE_CHECK );
return true;
}
/**
* Update cached license status.
*
* @param string $status Status value.
* @param array $data Additional data.
* @return void
*/
private function update_cached_status( string $status, array $data = array() ): void {
update_option( self::OPTION_LICENSE_STATUS, $status );
update_option( self::OPTION_LICENSE_DATA, $data );
update_option( self::OPTION_LAST_CHECK, time() );
}
/**
* AJAX handler: Validate license.
*
* @return void
*/
public function ajax_validate_license(): void {
check_ajax_referer( 'wp_prometheus_license_action', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( array(
'message' => __( 'You do not have permission to perform this action.', 'wp-prometheus' ),
) );
}
$result = $this->validate();
if ( $result['success'] ) {
wp_send_json_success( $result );
} else {
wp_send_json_error( $result );
}
}
/**
* AJAX handler: Activate license.
*
* @return void
*/
public function ajax_activate_license(): void {
check_ajax_referer( 'wp_prometheus_license_action', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( array(
'message' => __( 'You do not have permission to perform this action.', 'wp-prometheus' ),
) );
}
$result = $this->activate();
if ( $result['success'] ) {
wp_send_json_success( $result );
} else {
wp_send_json_error( $result );
}
}
/**
* AJAX handler: Deactivate license.
*
* @return void
*/
public function ajax_deactivate_license(): void {
check_ajax_referer( 'wp_prometheus_license_action', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( array(
'message' => __( 'You do not have permission to perform this action.', 'wp-prometheus' ),
) );
}
update_option( self::OPTION_LICENSE_STATUS, 'unchecked' );
update_option( self::OPTION_LICENSE_DATA, array() );
delete_transient( self::TRANSIENT_LICENSE_CHECK );
wp_send_json_success( array(
'success' => true,
'message' => __( 'License deactivated.', 'wp-prometheus' ),
) );
}
/**
* AJAX handler: Check license status.
*
* @return void
*/
public function ajax_check_status(): void {
check_ajax_referer( 'wp_prometheus_license_action', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( array(
'message' => __( 'You do not have permission to perform this action.', 'wp-prometheus' ),
) );
}
$result = $this->validate();
if ( $result['success'] ) {
wp_send_json_success( $result );
} else {
wp_send_json_error( $result );
}
}
}

288
src/Metrics/Collector.php Normal file
View File

@@ -0,0 +1,288 @@
<?php
/**
* Metrics collector class.
*
* @package WP_Prometheus
*/
namespace Magdev\WpPrometheus\Metrics;
use Prometheus\CollectorRegistry;
use Prometheus\Storage\InMemory;
use Prometheus\RenderTextFormat;
// Prevent direct file access.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Collector class.
*
* Collects and manages Prometheus metrics.
*/
class Collector {
/**
* Prometheus collector registry.
*
* @var CollectorRegistry
*/
private CollectorRegistry $registry;
/**
* Metric namespace.
*
* @var string
*/
private string $namespace = 'wordpress';
/**
* Constructor.
*/
public function __construct() {
$this->registry = new CollectorRegistry( new InMemory() );
}
/**
* Get the collector registry.
*
* @return CollectorRegistry
*/
public function get_registry(): CollectorRegistry {
return $this->registry;
}
/**
* Get the metric namespace.
*
* @return string
*/
public function get_namespace(): string {
return $this->namespace;
}
/**
* Collect all enabled metrics.
*
* @return void
*/
public function collect(): void {
$enabled_metrics = get_option( 'wp_prometheus_enabled_metrics', array() );
// Always collect WordPress info.
if ( in_array( 'wordpress_info', $enabled_metrics, true ) ) {
$this->collect_wordpress_info();
}
// Collect user metrics.
if ( in_array( 'wordpress_users_total', $enabled_metrics, true ) ) {
$this->collect_users_total();
}
// Collect posts metrics.
if ( in_array( 'wordpress_posts_total', $enabled_metrics, true ) ) {
$this->collect_posts_total();
}
// Collect comments metrics.
if ( in_array( 'wordpress_comments_total', $enabled_metrics, true ) ) {
$this->collect_comments_total();
}
// Collect plugins metrics.
if ( in_array( 'wordpress_plugins_total', $enabled_metrics, true ) ) {
$this->collect_plugins_total();
}
/**
* Fires after default metrics are collected.
*
* @param Collector $collector The metrics collector instance.
*/
do_action( 'wp_prometheus_collect_metrics', $this );
}
/**
* Render metrics in Prometheus text format.
*
* @return string
*/
public function render(): string {
$this->collect();
$renderer = new RenderTextFormat();
return $renderer->render( $this->registry->getMetricFamilySamples() );
}
/**
* Collect WordPress info metric.
*
* @return void
*/
private function collect_wordpress_info(): void {
$gauge = $this->registry->getOrRegisterGauge(
$this->namespace,
'info',
'WordPress installation information',
array( 'version', 'php_version', 'multisite' )
);
$gauge->set(
1,
array(
get_bloginfo( 'version' ),
PHP_VERSION,
is_multisite() ? 'yes' : 'no',
)
);
}
/**
* Collect total users metric.
*
* @return void
*/
private function collect_users_total(): void {
$gauge = $this->registry->getOrRegisterGauge(
$this->namespace,
'users_total',
'Total number of WordPress users',
array( 'role' )
);
$user_count = count_users();
foreach ( $user_count['avail_roles'] as $role => $count ) {
$gauge->set( $count, array( $role ) );
}
}
/**
* Collect total posts metric.
*
* @return void
*/
private function collect_posts_total(): void {
$gauge = $this->registry->getOrRegisterGauge(
$this->namespace,
'posts_total',
'Total number of posts by type and status',
array( 'post_type', 'status' )
);
$post_types = get_post_types( array( 'public' => true ) );
foreach ( $post_types as $post_type ) {
$counts = wp_count_posts( $post_type );
foreach ( get_object_vars( $counts ) as $status => $count ) {
if ( $count > 0 ) {
$gauge->set( (int) $count, array( $post_type, $status ) );
}
}
}
}
/**
* Collect total comments metric.
*
* @return void
*/
private function collect_comments_total(): void {
$gauge = $this->registry->getOrRegisterGauge(
$this->namespace,
'comments_total',
'Total number of comments by status',
array( 'status' )
);
$comments = wp_count_comments();
$statuses = array(
'approved' => $comments->approved,
'moderated' => $comments->moderated,
'spam' => $comments->spam,
'trash' => $comments->trash,
'total_comments' => $comments->total_comments,
);
foreach ( $statuses as $status => $count ) {
$gauge->set( (int) $count, array( $status ) );
}
}
/**
* Collect total plugins metric.
*
* @return void
*/
private function collect_plugins_total(): void {
if ( ! function_exists( 'get_plugins' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
$gauge = $this->registry->getOrRegisterGauge(
$this->namespace,
'plugins_total',
'Total number of plugins by status',
array( 'status' )
);
$all_plugins = get_plugins();
$active_plugins = get_option( 'active_plugins', array() );
$gauge->set( count( $all_plugins ), array( 'installed' ) );
$gauge->set( count( $active_plugins ), array( 'active' ) );
$gauge->set( count( $all_plugins ) - count( $active_plugins ), array( 'inactive' ) );
}
/**
* Register a custom gauge metric.
*
* @param string $name Metric name.
* @param string $help Metric description.
* @param array $labels Label names.
* @return \Prometheus\Gauge
*/
public function register_gauge( string $name, string $help, array $labels = array() ): \Prometheus\Gauge {
return $this->registry->getOrRegisterGauge(
$this->namespace,
$name,
$help,
$labels
);
}
/**
* Register a custom counter metric.
*
* @param string $name Metric name.
* @param string $help Metric description.
* @param array $labels Label names.
* @return \Prometheus\Counter
*/
public function register_counter( string $name, string $help, array $labels = array() ): \Prometheus\Counter {
return $this->registry->getOrRegisterCounter(
$this->namespace,
$name,
$help,
$labels
);
}
/**
* Register a custom histogram metric.
*
* @param string $name Metric name.
* @param string $help Metric description.
* @param array $labels Label names.
* @param array|null $buckets Histogram buckets.
* @return \Prometheus\Histogram
*/
public function register_histogram( string $name, string $help, array $labels = array(), ?array $buckets = null ): \Prometheus\Histogram {
return $this->registry->getOrRegisterHistogram(
$this->namespace,
$name,
$help,
$labels,
$buckets
);
}
}

150
src/Plugin.php Normal file
View File

@@ -0,0 +1,150 @@
<?php
/**
* Main plugin class.
*
* @package WP_Prometheus
*/
namespace Magdev\WpPrometheus;
use Magdev\WpPrometheus\Admin\Settings;
use Magdev\WpPrometheus\Endpoint\MetricsEndpoint;
use Magdev\WpPrometheus\License\Manager as LicenseManager;
use Magdev\WpPrometheus\Metrics\Collector;
// Prevent direct file access.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Plugin singleton class.
*
* Initializes and manages all plugin components.
*/
final class Plugin {
/**
* Singleton instance.
*
* @var Plugin|null
*/
private static ?Plugin $instance = null;
/**
* Metrics collector instance.
*
* @var Collector|null
*/
private ?Collector $collector = null;
/**
* Get singleton instance.
*
* @return Plugin
*/
public static function get_instance(): Plugin {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Private constructor to enforce singleton pattern.
*/
private function __construct() {
$this->init_components();
$this->init_hooks();
$this->load_textdomain();
}
/**
* Prevent cloning.
*
* @return void
*/
private function __clone() {}
/**
* Prevent unserialization.
*
* @throws \Exception Always throws to prevent unserialization.
* @return void
*/
public function __wakeup(): void {
throw new \Exception( 'Cannot unserialize singleton' );
}
/**
* Initialize plugin components.
*
* @return void
*/
private function init_components(): void {
// Initialize license manager.
LicenseManager::get_instance();
// Initialize admin settings (always needed).
if ( is_admin() ) {
new Settings();
}
// Initialize metrics endpoint (only if licensed).
if ( LicenseManager::is_license_valid() ) {
$this->collector = new Collector();
new MetricsEndpoint( $this->collector );
}
}
/**
* Initialize WordPress hooks.
*
* @return void
*/
private function init_hooks(): void {
// Add settings link to plugins page.
add_filter( 'plugin_action_links_' . WP_PROMETHEUS_BASENAME, array( $this, 'add_plugin_action_links' ) );
}
/**
* Add action links to the plugins page.
*
* @param array $links Existing action links.
* @return array Modified action links.
*/
public function add_plugin_action_links( array $links ): array {
$settings_link = sprintf(
'<a href="%s">%s</a>',
esc_url( admin_url( 'options-general.php?page=wp-prometheus' ) ),
esc_html__( 'Settings', 'wp-prometheus' )
);
// Add our link at the beginning.
array_unshift( $links, $settings_link );
return $links;
}
/**
* Load plugin textdomain.
*
* @return void
*/
private function load_textdomain(): void {
load_plugin_textdomain(
'wp-prometheus',
false,
dirname( WP_PROMETHEUS_BASENAME ) . '/languages'
);
}
/**
* Get the metrics collector.
*
* @return Collector|null
*/
public function get_collector(): ?Collector {
return $this->collector;
}
}

11
src/index.php Normal file
View File

@@ -0,0 +1,11 @@
<?php
/**
* Silence is golden.
*
* @package WP_Prometheus
*/
// Prevent direct file access.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}

18
uninstall.php Normal file
View File

@@ -0,0 +1,18 @@
<?php
/**
* Uninstall WP Prometheus plugin.
*
* @package WP_Prometheus
*/
// If uninstall.php is not called by WordPress, die.
if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {
die;
}
// Load the autoloader if it exists.
$autoloader = __DIR__ . '/vendor/autoload.php';
if ( file_exists( $autoloader ) ) {
require_once $autoloader;
\Magdev\WpPrometheus\Installer::uninstall();
}

200
wp-prometheus.php Normal file
View File

@@ -0,0 +1,200 @@
<?php
/**
* 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.1
* Requires at least: 6.4
* Requires PHP: 8.3
* Author: Marco Graetsch
* Author URI: https://src.bundespruefstelle.ch/magdev
* License: GPL v2 or later
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
* Text Domain: wp-prometheus
* Domain Path: /languages
*
* @package WP_Prometheus
*/
// Prevent direct file access.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Plugin version.
*
* @var string
*/
define( 'WP_PROMETHEUS_VERSION', '0.0.1' );
/**
* Plugin file path.
*
* @var string
*/
define( 'WP_PROMETHEUS_FILE', __FILE__ );
/**
* Plugin directory path.
*
* @var string
*/
define( 'WP_PROMETHEUS_PATH', plugin_dir_path( __FILE__ ) );
/**
* Plugin directory URL.
*
* @var string
*/
define( 'WP_PROMETHEUS_URL', plugin_dir_url( __FILE__ ) );
/**
* Plugin basename.
*
* @var string
*/
define( 'WP_PROMETHEUS_BASENAME', plugin_basename( __FILE__ ) );
/**
* Minimum WordPress version required.
*
* @var string
*/
define( 'WP_PROMETHEUS_MIN_WP_VERSION', '6.4' );
/**
* Minimum PHP version required.
*
* @var string
*/
define( 'WP_PROMETHEUS_MIN_PHP_VERSION', '8.3' );
/**
* Check requirements and bootstrap the plugin.
*
* @return void
*/
function wp_prometheus_init(): void {
// Check PHP version.
if ( version_compare( PHP_VERSION, WP_PROMETHEUS_MIN_PHP_VERSION, '<' ) ) {
add_action( 'admin_notices', 'wp_prometheus_php_version_notice' );
return;
}
// Check WordPress version.
if ( version_compare( get_bloginfo( 'version' ), WP_PROMETHEUS_MIN_WP_VERSION, '<' ) ) {
add_action( 'admin_notices', 'wp_prometheus_wp_version_notice' );
return;
}
// Check if Composer autoloader exists.
$autoloader = WP_PROMETHEUS_PATH . 'vendor/autoload.php';
if ( ! file_exists( $autoloader ) ) {
add_action( 'admin_notices', 'wp_prometheus_autoloader_notice' );
return;
}
require_once $autoloader;
// Initialize the plugin.
\Magdev\WpPrometheus\Plugin::get_instance();
}
/**
* Display PHP version notice.
*
* @return void
*/
function wp_prometheus_php_version_notice(): void {
$message = sprintf(
/* translators: 1: Required PHP version, 2: Current PHP version */
__( 'WP Prometheus requires PHP version %1$s or higher. You are running PHP %2$s.', 'wp-prometheus' ),
WP_PROMETHEUS_MIN_PHP_VERSION,
PHP_VERSION
);
printf( '<div class="notice notice-error"><p>%s</p></div>', esc_html( $message ) );
}
/**
* Display WordPress version notice.
*
* @return void
*/
function wp_prometheus_wp_version_notice(): void {
$message = sprintf(
/* translators: 1: Required WordPress version, 2: Current WordPress version */
__( 'WP Prometheus requires WordPress version %1$s or higher. You are running WordPress %2$s.', 'wp-prometheus' ),
WP_PROMETHEUS_MIN_WP_VERSION,
get_bloginfo( 'version' )
);
printf( '<div class="notice notice-error"><p>%s</p></div>', esc_html( $message ) );
}
/**
* Display autoloader notice.
*
* @return void
*/
function wp_prometheus_autoloader_notice(): void {
$message = __( 'WP Prometheus requires Composer dependencies to be installed. Please run "composer install" in the plugin directory.', 'wp-prometheus' );
printf( '<div class="notice notice-error"><p>%s</p></div>', esc_html( $message ) );
}
/**
* Plugin activation hook.
*
* @return void
*/
function wp_prometheus_activate(): void {
// Check requirements before activation.
if ( version_compare( PHP_VERSION, WP_PROMETHEUS_MIN_PHP_VERSION, '<' ) ) {
deactivate_plugins( WP_PROMETHEUS_BASENAME );
wp_die(
sprintf(
/* translators: %s: Required PHP version */
esc_html__( 'WP Prometheus requires PHP version %s or higher.', 'wp-prometheus' ),
WP_PROMETHEUS_MIN_PHP_VERSION
)
);
}
$autoloader = WP_PROMETHEUS_PATH . 'vendor/autoload.php';
if ( file_exists( $autoloader ) ) {
require_once $autoloader;
\Magdev\WpPrometheus\Installer::activate();
}
}
/**
* Plugin deactivation hook.
*
* @return void
*/
function wp_prometheus_deactivate(): void {
$autoloader = WP_PROMETHEUS_PATH . 'vendor/autoload.php';
if ( file_exists( $autoloader ) ) {
require_once $autoloader;
\Magdev\WpPrometheus\Installer::deactivate();
}
}
/**
* Plugin uninstall hook.
*
* @return void
*/
function wp_prometheus_uninstall(): void {
$autoloader = WP_PROMETHEUS_PATH . 'vendor/autoload.php';
if ( file_exists( $autoloader ) ) {
require_once $autoloader;
\Magdev\WpPrometheus\Installer::uninstall();
}
}
// Register activation/deactivation hooks.
register_activation_hook( __FILE__, 'wp_prometheus_activate' );
register_deactivation_hook( __FILE__, 'wp_prometheus_deactivate' );
// Initialize plugin after all plugins are loaded.
add_action( 'plugins_loaded', 'wp_prometheus_init' );