You've already forked wp-prometheus
Initial plugin setup (v0.0.1)
Some checks failed
Create Release Package / build-release (push) Failing after 48s
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:
15
.editorconfig
Normal file
15
.editorconfig
Normal 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
|
||||
195
.gitea/workflows/release.yml
Normal file
195
.gitea/workflows/release.yml
Normal 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
6
.gitignore
vendored
Normal 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
3
.gitmodules
vendored
Normal 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
32
CHANGELOG.md
Normal 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
292
CLAUDE.md
Normal 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
190
PLAN.md
Normal 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
164
README.md
Normal 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
100
assets/js/admin.js
Normal 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
61
composer.json
Normal 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
11
index.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
/**
|
||||
* Silence is golden.
|
||||
*
|
||||
* @package WP_Prometheus
|
||||
*/
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
1
lib/wc-licensed-product-client
Submodule
1
lib/wc-licensed-product-client
Submodule
Submodule lib/wc-licensed-product-client added at 56abe8a97c
406
src/Admin/Settings.php
Normal file
406
src/Admin/Settings.php
Normal 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 );
|
||||
}
|
||||
}
|
||||
148
src/Endpoint/MetricsEndpoint.php
Normal file
148
src/Endpoint/MetricsEndpoint.php
Normal 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
103
src/Installer.php
Normal 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
496
src/License/Manager.php
Normal 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
288
src/Metrics/Collector.php
Normal 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
150
src/Plugin.php
Normal 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
11
src/index.php
Normal 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
18
uninstall.php
Normal 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
200
wp-prometheus.php
Normal 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' );
|
||||
Reference in New Issue
Block a user