8 Commits
v0.7.2 ... dev

Author SHA1 Message Date
57e1b838cc Add Grafana dashboard and wp-prometheus integration (v0.7.5)
All checks were successful
Create Release Package / build-release (push) Successful in 1m9s
- Add example Grafana dashboard with 24 panels for license metrics
- Register dashboard with wp-prometheus via hook
- Add dashboard documentation with PromQL examples and alerting rules
- Update README with monitoring section

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 11:29:14 +01:00
cfd34c9329 Add MARKETING.md to .gitignore
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 10:58:30 +01:00
fb4be7124b Update CLAUDE.md with v0.7.4 session learnings
- Removed v0.7.4 from roadmap (completed)
- Added session history for Prometheus metrics integration
- Documented new PrometheusController and metrics implementation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 10:57:01 +01:00
73ba7fb929 Add Prometheus metrics integration (v0.7.4)
All checks were successful
Create Release Package / build-release (push) Successful in 1m8s
- New Metrics settings tab with enable/disable toggle
- PrometheusController for wp_prometheus_collect_metrics hook
- License gauges: total by status, lifetime, expiring, expiring soon
- Download gauges: total downloads, active versions
- API counters: requests, rate limits, validation errors
- Metric tracking in RestApiController and UpdateController

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 10:52:50 +01:00
548b2ae8af Bump version to 0.7.3
All checks were successful
Create Release Package / build-release (push) Successful in 1m15s
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 13:53:37 +01:00
e0001c3f4e Fix API Verification Secret not visible in Docker environments
- Add ResponseSigner::getServerSecret() to check multiple sources
- Check constant, getenv(), $_ENV, and $_SERVER for server secret
- Update Plugin.php to use ResponseSigner::isSigningEnabled()
- Maintains backward compatibility with standard WordPress setups

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 13:52:57 +01:00
a879be989c Update CLAUDE.md with Docker environment variable fix session
- Documented bug fix for API Verification Secret not visible in Docker
- Added ResponseSigner::getServerSecret() method documentation
- Removed known bug from roadmap (now fixed)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 13:51:57 +01:00
40c08bf474 Update CLAUDE.md with v0.7.2 session learnings
- Document CI/CD workflow fix for handling existing releases
- Add lessons learned about Gitea releases and tag updates
- Note about not creating zip archives locally (RAM issue)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-29 22:44:08 +01:00
16 changed files with 5949 additions and 3360 deletions

3
.gitignore vendored
View File

@@ -4,3 +4,6 @@ wp-plugins
wp-core wp-core
vendor/ vendor/
releases/* releases/*
# Marketing texts (not part of plugin distribution)
MARKETING.md

View File

@@ -7,6 +7,69 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
## [0.7.5] - 2026-02-03
### Added
- **Grafana Dashboard**: Example dashboard for license metrics monitoring
- 24 panels organized into 4 sections: License Overview, Downloads & Versions, API Metrics, Errors & Rate Limiting
- Template variables for data source and instance filtering
- Includes example Prometheus alerting rules
- **WP Prometheus Dashboard Integration**: Dashboard automatically registered with wp-prometheus
- Appears in Settings > WP Prometheus > Dashboards when metrics are enabled
- Uses `wp_prometheus_register_dashboards` hook for seamless integration
- Documentation for Grafana dashboard installation and PromQL query examples
### New Files
- `docs/grafana-dashboard.json` - Complete Grafana dashboard with 24 panels
- `docs/grafana-dashboard.md` - Installation and usage documentation
### Changed
- Updated README with "Monitoring with Prometheus & Grafana" section
## [0.7.4] - 2026-02-03
### Added
- **Prometheus Metrics Integration**: Expose license and API metrics for monitoring
- New "Metrics" settings tab with enable/disable toggle
- License gauges: total by status, lifetime, expiring, expiring soon
- Download gauges: total downloads, active versions count
- API counters: requests by endpoint/result, rate limit exceeded events, validation errors by type
- Requires [WP Prometheus](https://src.bundespruefstelle.ch/magdev/wp-prometheus) plugin
### New Files
- `src/Metrics/PrometheusController.php` - Prometheus metrics collection and registration
### Technical Details
- Hooks into `wp_prometheus_collect_metrics` action for metric collection
- API counters stored persistently in WordPress options (`wclp_prometheus_counters`)
- Static methods for incrementing counters from API controllers
- Metrics only collected when enabled in settings
## [0.7.3] - 2026-02-01
### Fixed
- **Docker Environment Support:** API Verification Secret now visible on customer licenses page in Docker environments
- Added `ResponseSigner::getServerSecret()` method to check multiple sources for server secret
- Checks PHP constant, `getenv()`, `$_ENV`, and `$_SERVER` in priority order
- Maintains full backward compatibility with standard WordPress installations
### Changed
- Updated `Plugin.php` to use `ResponseSigner::isSigningEnabled()` instead of direct constant check
### Technical Details
- Root cause: Docker WordPress setups using `wp-config-docker.php` with `getenv_docker()` don't always define PHP constants
- The environment variable was accessible but the constant wasn't being created
- New `getServerSecret()` method centralizes all server secret retrieval logic
## [0.7.2] - 2026-01-29 ## [0.7.2] - 2026-01-29
### Added ### Added

124
CLAUDE.md
View File

@@ -32,7 +32,9 @@ This project is proudly **"vibe-coded"** using Claude.AI - the entire codebase w
**Note for AI Assistants:** Clean this section after the specific features are done or new releases are made. Effective changes are tracked in `CHANGELOG.md`. Do not add completed versions here - document them in the Session History section at the end of this file. **Note for AI Assistants:** Clean this section after the specific features are done or new releases are made. Effective changes are tracked in `CHANGELOG.md`. Do not add completed versions here - document them in the Session History section at the end of this file.
No pending roadmap items. ### Known Bugs
None currently tracked.
## Technical Stack ## Technical Stack
@@ -1945,3 +1947,123 @@ composer install
- Automatically created by Gitea Actions CI/CD pipeline - Automatically created by Gitea Actions CI/CD pipeline
- Release package: 881 KiB with SHA256 checksum - Release package: 881 KiB with SHA256 checksum
- First automated release - all future releases will use this workflow - First automated release - all future releases will use this workflow
**Additional fixes (same session):**
- Updated README.md with Auto-Updates section and Development section
- Fixed CI/CD workflow to handle existing releases (delete before recreate)
- When updating a tag, the workflow now checks for existing releases and deletes them first
**Lessons learned:**
- Gitea releases persist even when their tag is deleted - must delete release via API
- Composer `symlink: false` doesn't always work - CI must manually replace symlinks with `cp -r`
- Never create zip archives locally on this machine (fills up RAM indefinitely)
- Gitea API endpoint for releases by tag: `GET /api/v1/repos/{owner}/{repo}/releases/tags/{tag}`
### 2026-02-01 - Bug Fix: API Verification Secret Not Visible
**Overview:**
Fixed the "API Verification Secret" (customer secret) not appearing on the customer account licenses page in Docker environments.
**Root Cause:**
The `WC_LICENSE_SERVER_SECRET` constant was not being defined even though the environment variable was set. In Docker WordPress setups using `wp-config-docker.php`, the `getenv_docker()` function retrieves values from environment variables, but the constant wasn't being created properly. The plugin was only checking for the PHP constant, not the environment variable directly.
**Fix:**
Added `ResponseSigner::getServerSecret()` static method that checks multiple sources for the server secret:
1. `WC_LICENSE_SERVER_SECRET` constant (standard WordPress configuration)
2. `getenv('WC_LICENSE_SERVER_SECRET')` (Docker environments)
3. `$_ENV['WC_LICENSE_SERVER_SECRET']` (some PHP configurations)
4. `$_SERVER['WC_LICENSE_SERVER_SECRET']` (fallback)
**Modified files:**
- `src/Api/ResponseSigner.php` - Added `getServerSecret()` method, updated `isSigningEnabled()` and `getCustomerSecretForLicense()` to use it
- `src/Plugin.php` - Updated to use `ResponseSigner::isSigningEnabled()` instead of direct constant check
**Technical notes:**
- The fix maintains backward compatibility with standard WordPress installations using constants
- Docker environments can now use environment variables directly without needing the constant to be defined
- All three methods (`isSigningEnabled()`, `getCustomerSecretForLicense()`, and constructor) now use the centralized `getServerSecret()` method
### 2026-02-03 - Version 0.7.4 - Prometheus Metrics Integration
**Overview:**
Added Prometheus metrics integration to expose license and API metrics for monitoring. Requires the WP Prometheus plugin.
**New files:**
- `src/Metrics/PrometheusController.php` - Prometheus metrics collection controller
**Implemented:**
- New "Metrics" settings section with enable/disable toggle
- Hooks into `wp_prometheus_collect_metrics` action for metric collection
- License gauges using existing `LicenseManager::getStatistics()`:
- `wclp_licenses_total{status}` - License counts by status
- `wclp_licenses_lifetime_total` - Lifetime licenses count
- `wclp_licenses_expiring_total` - Expiring licenses count
- `wclp_licenses_expiring_soon` - Licenses expiring within 30 days
- Download gauges using existing `VersionManager::getDownloadStatistics()`:
- `wclp_downloads_total` - Total downloads
- `wclp_versions_active_total` - Active product versions
- API counters (stored in WordPress options for persistence):
- `wclp_api_requests_total{endpoint,result}` - API requests by endpoint and result
- `wclp_rate_limit_exceeded_total{endpoint}` - Rate limit exceeded events
- `wclp_validation_errors_total{error_type}` - Validation errors by type
**Modified files:**
- `src/Admin/SettingsController.php` - Added 'metrics' section with settings
- `src/Api/RestApiController.php` - Added metric tracking for API requests
- `src/Api/UpdateController.php` - Added metric tracking for update-check requests
- `src/Plugin.php` - Initialize PrometheusController
**Technical notes:**
- Metrics are only collected when enabled via settings toggle
- Static methods allow increment from API controllers without dependency injection
- Counter values persist across requests via `wclp_prometheus_counters` option
- Gauges query database on each metric collection (uses existing statistics methods)
### 2026-02-03 - Grafana Dashboard & WP Prometheus Integration
**Overview:**
Added example Grafana dashboard JSON and integrated with wp-prometheus dashboard registration system.
**New files:**
- `docs/grafana-dashboard.json` - Complete Grafana dashboard with 24 panels
- `docs/grafana-dashboard.md` - Installation and usage documentation
**Dashboard panels:**
- License Overview: Total, Active, Lifetime, Expiring Soon, Expired, Revoked stats + pie chart + time series
- Downloads & Versions: Total downloads, active versions, download trends
- API Metrics: Request rates by endpoint/result, pie chart breakdown, top requests table
- Errors & Rate Limiting: Rate limit events, validation errors by type over time
**WP Prometheus integration:**
- Dashboard automatically registered via `wp_prometheus_register_dashboards` hook
- Appears in Settings > WP Prometheus > Dashboards tab when metrics are enabled
- Uses file-based registration with metadata (title, description, icon, plugin attribution)
**Modified files:**
- `src/Metrics/PrometheusController.php` - Added `registerDashboard()` method and hook registration
- `docs/grafana-dashboard.md` - Added wp-prometheus installation option as recommended method
- `README.md` - Added "Monitoring with Prometheus & Grafana" section linking to dashboard docs
**Technical notes:**
- Dashboard registration only occurs when metrics are enabled (same condition as metric collection)
- Uses `dashicons-admin-network` icon for dashboard list
- File path uses `WC_LICENSED_PRODUCT_PLUGIN_DIR` constant for reliable path resolution

View File

@@ -393,6 +393,38 @@ The plugin sends automatic email notifications (configurable via WooCommerce > S
- **Expiration Warning (1 day)**: Urgent reminder sent 1 day before expiration - **Expiration Warning (1 day)**: Urgent reminder sent 1 day before expiration
- **License Expired**: Notification when a license auto-expires - **License Expired**: Notification when a license auto-expires
## Monitoring with Prometheus & Grafana
The plugin integrates with [wp-prometheus](https://src.bundespruefstelle.ch/magdev/wp-prometheus) to expose metrics for monitoring and alerting.
### Enable Metrics
1. Install and configure the wp-prometheus plugin
2. Go to WooCommerce > Settings > Licensed Products > Metrics
3. Enable "Prometheus Metrics"
### Available Metrics
**Gauges:**
- `wclp_licenses_total{status}` - License counts by status
- `wclp_licenses_lifetime_total` - Lifetime licenses
- `wclp_licenses_expiring_soon` - Licenses expiring within 30 days
- `wclp_downloads_total` - Total file downloads
- `wclp_versions_active_total` - Active product versions
**Counters:**
- `wclp_api_requests_total{endpoint,result}` - API requests by endpoint and result
- `wclp_rate_limit_exceeded_total{endpoint}` - Rate limit events
- `wclp_validation_errors_total{error_type}` - Validation errors by type
### Grafana Dashboard
An example Grafana dashboard is included at [docs/grafana-dashboard.json](docs/grafana-dashboard.json).
See [docs/grafana-dashboard.md](docs/grafana-dashboard.md) for installation instructions, panel descriptions, and alerting examples.
## Changelog ## Changelog
See [CHANGELOG.md](CHANGELOG.md) for version history and changes. See [CHANGELOG.md](CHANGELOG.md) for version history and changes.

1748
docs/grafana-dashboard.json Normal file

File diff suppressed because it is too large Load Diff

219
docs/grafana-dashboard.md Normal file
View File

@@ -0,0 +1,219 @@
# Grafana Dashboard for WC Licensed Product
This dashboard provides comprehensive monitoring for the WC Licensed Product plugin using Prometheus metrics exposed via the [wp-prometheus](https://src.bundespruefstelle.ch/magdev/wp-prometheus) plugin.
## Prerequisites
1. **WP Prometheus Plugin** - Install and configure [wp-prometheus](https://src.bundespruefstelle.ch/magdev/wp-prometheus) on your WordPress site
2. **Prometheus** - Configure Prometheus to scrape your WordPress metrics endpoint
3. **Grafana** - Grafana 9.0+ with Prometheus data source configured
4. **Enable Metrics** - In WordPress admin: WooCommerce > Settings > Licensed Products > Metrics > Enable Prometheus Metrics
## Installation
### Option 1: Via WP Prometheus Settings (Recommended)
When metrics are enabled, the dashboard is automatically registered with wp-prometheus:
1. Go to **Settings > WP Prometheus** in WordPress admin
2. Navigate to the **Dashboards** tab
3. Find "WC Licensed Product - License Metrics" in the list
4. Click **Download JSON** to get the dashboard file
5. Import the downloaded file into Grafana
### Option 2: Manual Import
1. Open Grafana and navigate to **Dashboards > Import**
2. Upload the `grafana-dashboard.json` file or paste its contents
3. Select your Prometheus data source
4. Click **Import**
### Configure Variables
The dashboard includes two template variables:
- **datasource** - Select your Prometheus data source
- **instance** - Filter by WordPress instance (useful for multi-site monitoring)
## Dashboard Panels
### License Overview
| Panel | Description |
| --- | --- |
| Total Licenses | Total count of all licenses |
| Active Licenses | Licenses with `status=active` |
| Lifetime Licenses | Licenses without expiration date |
| Expiring Soon (30d) | Licenses expiring within 30 days |
| Expired Licenses | Licenses with `status=expired` |
| Revoked Licenses | Licenses with `status=revoked` |
| Licenses by Status | Pie chart showing distribution |
| License Status Over Time | Time series of license counts |
### Downloads & Versions
| Panel | Description |
| --- | --- |
| Total Downloads | Cumulative download count |
| Active Product Versions | Number of active versions |
| Downloads Over Time | Download trend graph |
| Downloads (Selected Range) | Downloads in selected time range |
### API Metrics
| Panel | Description |
| --- | --- |
| API Requests (5m intervals) | Stacked bar chart by endpoint/result |
| Requests by Endpoint | Donut chart of endpoint distribution |
| Top API Requests | Table of most frequent requests |
### Errors & Rate Limiting
| Panel | Description |
| --- | --- |
| Rate Limit Events (Total) | Total HTTP 429 responses |
| Validation Errors (Total) | Total validation failures |
| Validation Errors Over Time | Error trend by type |
| Validation Errors by Type | Pie chart breakdown |
| Rate Limit Events by Endpoint | Rate limits per endpoint |
## Metrics Reference
### Gauges (current values)
```promql
# Licenses by status
wclp_licenses_total{status="active|expired|revoked|inactive"}
# Lifetime licenses (no expiration)
wclp_licenses_lifetime_total
# Licenses with expiration date
wclp_licenses_expiring_total
# Licenses expiring within 30 days
wclp_licenses_expiring_soon
# Total downloads
wclp_downloads_total
# Active product versions
wclp_versions_active_total
```
### Counters (cumulative)
```promql
# API requests by endpoint and result
wclp_api_requests_total{endpoint="validate|status|activate|update-check", result="success|error"}
# Rate limit exceeded events
wclp_rate_limit_exceeded_total{endpoint="validate|status|activate|update-check"}
# Validation errors by type
wclp_validation_errors_total{error_type="license_not_found|domain_mismatch|license_expired|license_revoked|..."}
```
## Example Prometheus Queries
### Success Rate
```promql
sum(rate(wclp_api_requests_total{result="success"}[5m])) /
sum(rate(wclp_api_requests_total[5m])) * 100
```
### Error Rate by Endpoint
```promql
sum by (endpoint) (rate(wclp_api_requests_total{result="error"}[5m]))
```
### License Churn (new activations)
```promql
increase(wclp_licenses_total{status="active"}[1d])
```
### Top Validation Errors
```promql
topk(5, sum by (error_type) (wclp_validation_errors_total))
```
## Alerting Examples
Add these alerts to your Prometheus alerting rules:
```yaml
groups:
- name: wc-licensed-product
rules:
# High rate limit events
- alert: HighRateLimitEvents
expr: increase(wclp_rate_limit_exceeded_total[5m]) > 10
for: 5m
labels:
severity: warning
annotations:
summary: "High rate limiting on {{ $labels.endpoint }}"
# Many expiring licenses
- alert: LicensesExpiringSoon
expr: wclp_licenses_expiring_soon > 20
for: 1h
labels:
severity: info
annotations:
summary: "{{ $value }} licenses expiring within 30 days"
# API error rate
- alert: HighAPIErrorRate
expr: |
sum(rate(wclp_api_requests_total{result="error"}[5m])) /
sum(rate(wclp_api_requests_total[5m])) > 0.1
for: 10m
labels:
severity: warning
annotations:
summary: "API error rate above 10%"
```
## Prometheus Configuration
Add to your `prometheus.yml`:
```yaml
scrape_configs:
- job_name: 'wordpress'
metrics_path: '/metrics'
scheme: https
bearer_token: 'YOUR_WP_PROMETHEUS_TOKEN'
static_configs:
- targets: ['your-wordpress-site.com']
```
## Troubleshooting
### No data showing
1. Verify wp-prometheus is installed and configured
2. Check that metrics are enabled in WC Licensed Product settings
3. Confirm Prometheus can reach your WordPress metrics endpoint
4. Check the data source selection in Grafana
### Missing metrics
Some metrics only appear after relevant actions occur:
- `wclp_api_requests_total` - After API requests
- `wclp_rate_limit_exceeded_total` - After rate limit events
- `wclp_validation_errors_total` - After validation errors
### Counter resets
Counters persist in WordPress options and survive restarts. To reset:
```php
\Jeremias\WcLicensedProduct\Metrics\PrometheusController::resetCounters();
```

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -65,6 +65,7 @@ final class SettingsController
'auto-updates' => __('Auto-Updates', 'wc-licensed-product'), 'auto-updates' => __('Auto-Updates', 'wc-licensed-product'),
'defaults' => __('Default Settings', 'wc-licensed-product'), 'defaults' => __('Default Settings', 'wc-licensed-product'),
'notifications' => __('Notifications', 'wc-licensed-product'), 'notifications' => __('Notifications', 'wc-licensed-product'),
'metrics' => __('Metrics', 'wc-licensed-product'),
]; ];
} }
@@ -116,6 +117,7 @@ final class SettingsController
'auto-updates' => $this->getAutoUpdatesSettings(), 'auto-updates' => $this->getAutoUpdatesSettings(),
'defaults' => $this->getDefaultsSettings(), 'defaults' => $this->getDefaultsSettings(),
'notifications' => $this->getNotificationsSettings(), 'notifications' => $this->getNotificationsSettings(),
'metrics' => $this->getMetricsSettings(),
default => $this->getPluginLicenseSettings(), default => $this->getPluginLicenseSettings(),
}; };
} }
@@ -314,6 +316,32 @@ final class SettingsController
]; ];
} }
/**
* Get metrics settings
*/
private function getMetricsSettings(): array
{
return [
'metrics_section_title' => [
'name' => __('Prometheus Metrics', 'wc-licensed-product'),
'type' => 'title',
'desc' => __('Expose license and API metrics for Prometheus monitoring. Requires the WP Prometheus plugin to be installed and active.', 'wc-licensed-product'),
'id' => 'wc_licensed_product_section_metrics',
],
'metrics_enabled' => [
'name' => __('Enable Prometheus Metrics', 'wc-licensed-product'),
'type' => 'checkbox',
'desc' => __('Expose license statistics, API usage, and download metrics via Prometheus.', 'wc-licensed-product'),
'id' => 'wc_licensed_product_metrics_enabled',
'default' => 'no',
],
'metrics_section_end' => [
'type' => 'sectionend',
'id' => 'wc_licensed_product_section_metrics_end',
],
];
}
/** /**
* Render settings tab content * Render settings tab content
*/ */
@@ -575,4 +603,12 @@ final class SettingsController
wp_send_json_error(['message' => $error]); wp_send_json_error(['message' => $error]);
} }
} }
/**
* Check if Prometheus metrics are enabled
*/
public static function isMetricsEnabled(): bool
{
return get_option('wc_licensed_product_metrics_enabled', 'no') === 'yes';
}
} }

View File

@@ -26,9 +26,7 @@ final class ResponseSigner
public function __construct() public function __construct()
{ {
$this->serverSecret = defined('WC_LICENSE_SERVER_SECRET') $this->serverSecret = self::getServerSecret();
? WC_LICENSE_SERVER_SECRET
: '';
} }
/** /**
@@ -185,7 +183,7 @@ final class ResponseSigner
*/ */
public static function getCustomerSecretForLicense(string $licenseKey): ?string public static function getCustomerSecretForLicense(string $licenseKey): ?string
{ {
$serverSecret = defined('WC_LICENSE_SERVER_SECRET') ? WC_LICENSE_SERVER_SECRET : ''; $serverSecret = self::getServerSecret();
if (empty($serverSecret)) { if (empty($serverSecret)) {
return null; return null;
@@ -201,6 +199,40 @@ final class ResponseSigner
*/ */
public static function isSigningEnabled(): bool public static function isSigningEnabled(): bool
{ {
return defined('WC_LICENSE_SERVER_SECRET') && !empty(WC_LICENSE_SERVER_SECRET); return !empty(self::getServerSecret());
}
/**
* Get the server secret from constant or environment variable
*
* Checks in order:
* 1. WC_LICENSE_SERVER_SECRET constant (preferred)
* 2. WC_LICENSE_SERVER_SECRET environment variable (Docker fallback)
*
* @return string The server secret, or empty string if not configured
*/
public static function getServerSecret(): string
{
// First check the constant (standard WordPress configuration)
if (defined('WC_LICENSE_SERVER_SECRET') && !empty(WC_LICENSE_SERVER_SECRET)) {
return WC_LICENSE_SERVER_SECRET;
}
// Fallback to environment variable (Docker environments)
$envSecret = getenv('WC_LICENSE_SERVER_SECRET');
if ($envSecret !== false && !empty($envSecret)) {
return $envSecret;
}
// Also check $_ENV and $_SERVER (some PHP configurations)
if (!empty($_ENV['WC_LICENSE_SERVER_SECRET'])) {
return $_ENV['WC_LICENSE_SERVER_SECRET'];
}
if (!empty($_SERVER['WC_LICENSE_SERVER_SECRET'])) {
return $_SERVER['WC_LICENSE_SERVER_SECRET'];
}
return '';
} }
} }

View File

@@ -10,6 +10,7 @@ declare(strict_types=1);
namespace Jeremias\WcLicensedProduct\Api; namespace Jeremias\WcLicensedProduct\Api;
use Jeremias\WcLicensedProduct\License\LicenseManager; use Jeremias\WcLicensedProduct\License\LicenseManager;
use Jeremias\WcLicensedProduct\Metrics\PrometheusController;
use WP_REST_Request; use WP_REST_Request;
use WP_REST_Response; use WP_REST_Response;
use WP_REST_Server; use WP_REST_Server;
@@ -108,6 +109,10 @@ final class RestApiController
'retry_after' => $retryAfter, 'retry_after' => $retryAfter,
], 429); ], 429);
$response->header('Retry-After', (string) $retryAfter); $response->header('Retry-After', (string) $retryAfter);
// Track rate limit event for metrics
PrometheusController::incrementRateLimitExceeded('api');
return $response; return $response;
} }
@@ -209,6 +214,16 @@ final class RestApiController
$statusCode = $this->getStatusCodeForResult($result); $statusCode = $this->getStatusCodeForResult($result);
// Track metrics
if ($result['valid']) {
PrometheusController::incrementApiRequest('validate', 'success');
} else {
PrometheusController::incrementApiRequest('validate', 'error');
if (!empty($result['error'])) {
PrometheusController::incrementValidationError($result['error']);
}
}
return new WP_REST_Response($result, $statusCode); return new WP_REST_Response($result, $statusCode);
} }
@@ -247,6 +262,9 @@ final class RestApiController
$license = $this->licenseManager->getLicenseByKey($licenseKey); $license = $this->licenseManager->getLicenseByKey($licenseKey);
if (!$license) { if (!$license) {
PrometheusController::incrementApiRequest('status', 'error');
PrometheusController::incrementValidationError('license_not_found');
return new WP_REST_Response([ return new WP_REST_Response([
'valid' => false, 'valid' => false,
'error' => 'license_not_found', 'error' => 'license_not_found',
@@ -254,6 +272,8 @@ final class RestApiController
], 404); ], 404);
} }
PrometheusController::incrementApiRequest('status', 'success');
return new WP_REST_Response([ return new WP_REST_Response([
'valid' => $license->isValid(), 'valid' => $license->isValid(),
'status' => $license->getStatus(), 'status' => $license->getStatus(),
@@ -280,6 +300,9 @@ final class RestApiController
$license = $this->licenseManager->getLicenseByKey($licenseKey); $license = $this->licenseManager->getLicenseByKey($licenseKey);
if (!$license) { if (!$license) {
PrometheusController::incrementApiRequest('activate', 'error');
PrometheusController::incrementValidationError('license_not_found');
return new WP_REST_Response([ return new WP_REST_Response([
'success' => false, 'success' => false,
'error' => 'license_not_found', 'error' => 'license_not_found',
@@ -288,6 +311,9 @@ final class RestApiController
} }
if (!$license->isValid()) { if (!$license->isValid()) {
PrometheusController::incrementApiRequest('activate', 'error');
PrometheusController::incrementValidationError('license_invalid');
return new WP_REST_Response([ return new WP_REST_Response([
'success' => false, 'success' => false,
'error' => 'license_invalid', 'error' => 'license_invalid',
@@ -299,6 +325,8 @@ final class RestApiController
// Check if already activated on this domain // Check if already activated on this domain
if ($license->getDomain() === $normalizedDomain) { if ($license->getDomain() === $normalizedDomain) {
PrometheusController::incrementApiRequest('activate', 'success');
return new WP_REST_Response([ return new WP_REST_Response([
'success' => true, 'success' => true,
'message' => __('License is already activated for this domain.', 'wc-licensed-product'), 'message' => __('License is already activated for this domain.', 'wc-licensed-product'),
@@ -307,6 +335,9 @@ final class RestApiController
// Check if can activate on another domain // Check if can activate on another domain
if (!$license->canActivate()) { if (!$license->canActivate()) {
PrometheusController::incrementApiRequest('activate', 'error');
PrometheusController::incrementValidationError('max_activations_reached');
return new WP_REST_Response([ return new WP_REST_Response([
'success' => false, 'success' => false,
'error' => 'max_activations_reached', 'error' => 'max_activations_reached',
@@ -318,6 +349,9 @@ final class RestApiController
$success = $this->licenseManager->updateLicenseDomain($license->getId(), $domain); $success = $this->licenseManager->updateLicenseDomain($license->getId(), $domain);
if (!$success) { if (!$success) {
PrometheusController::incrementApiRequest('activate', 'error');
PrometheusController::incrementValidationError('activation_failed');
return new WP_REST_Response([ return new WP_REST_Response([
'success' => false, 'success' => false,
'error' => 'activation_failed', 'error' => 'activation_failed',
@@ -325,6 +359,8 @@ final class RestApiController
], 500); ], 500);
} }
PrometheusController::incrementApiRequest('activate', 'success');
return new WP_REST_Response([ return new WP_REST_Response([
'success' => true, 'success' => true,
'message' => __('License activated successfully.', 'wc-licensed-product'), 'message' => __('License activated successfully.', 'wc-licensed-product'),

View File

@@ -12,6 +12,7 @@ declare(strict_types=1);
namespace Jeremias\WcLicensedProduct\Api; namespace Jeremias\WcLicensedProduct\Api;
use Jeremias\WcLicensedProduct\License\LicenseManager; use Jeremias\WcLicensedProduct\License\LicenseManager;
use Jeremias\WcLicensedProduct\Metrics\PrometheusController;
use Jeremias\WcLicensedProduct\Product\VersionManager; use Jeremias\WcLicensedProduct\Product\VersionManager;
use Jeremias\WcLicensedProduct\Product\ProductVersion; use Jeremias\WcLicensedProduct\Product\ProductVersion;
use WP_REST_Request; use WP_REST_Request;
@@ -113,6 +114,10 @@ final class UpdateController
'retry_after' => $retryAfter, 'retry_after' => $retryAfter,
], 429); ], 429);
$response->header('Retry-After', (string) $retryAfter); $response->header('Retry-After', (string) $retryAfter);
// Track rate limit event for metrics
PrometheusController::incrementRateLimitExceeded('update-check');
return $response; return $response;
} }
@@ -179,10 +184,14 @@ final class UpdateController
$validationResult = $this->licenseManager->validateLicense($licenseKey, $domain); $validationResult = $this->licenseManager->validateLicense($licenseKey, $domain);
if (!$validationResult['valid']) { if (!$validationResult['valid']) {
$errorType = $validationResult['error'] ?? 'license_invalid';
PrometheusController::incrementApiRequest('update-check', 'error');
PrometheusController::incrementValidationError($errorType);
return new WP_REST_Response([ return new WP_REST_Response([
'success' => false, 'success' => false,
'update_available' => false, 'update_available' => false,
'error' => $validationResult['error'] ?? 'license_invalid', 'error' => $errorType,
'message' => $validationResult['message'] ?? __('License validation failed.', 'wc-licensed-product'), 'message' => $validationResult['message'] ?? __('License validation failed.', 'wc-licensed-product'),
], $validationResult['error'] === 'license_not_found' ? 404 : 403); ], $validationResult['error'] === 'license_not_found' ? 404 : 403);
} }
@@ -190,6 +199,9 @@ final class UpdateController
// Get license to access product ID // Get license to access product ID
$license = $this->licenseManager->getLicenseByKey($licenseKey); $license = $this->licenseManager->getLicenseByKey($licenseKey);
if (!$license) { if (!$license) {
PrometheusController::incrementApiRequest('update-check', 'error');
PrometheusController::incrementValidationError('license_not_found');
return new WP_REST_Response([ return new WP_REST_Response([
'success' => false, 'success' => false,
'update_available' => false, 'update_available' => false,
@@ -202,6 +214,9 @@ final class UpdateController
$product = wc_get_product($productId); $product = wc_get_product($productId);
if (!$product) { if (!$product) {
PrometheusController::incrementApiRequest('update-check', 'error');
PrometheusController::incrementValidationError('product_not_found');
return new WP_REST_Response([ return new WP_REST_Response([
'success' => false, 'success' => false,
'update_available' => false, 'update_available' => false,
@@ -214,6 +229,8 @@ final class UpdateController
$latestVersion = $this->getLatestVersionForLicense($license); $latestVersion = $this->getLatestVersionForLicense($license);
if (!$latestVersion) { if (!$latestVersion) {
PrometheusController::incrementApiRequest('update-check', 'success');
return new WP_REST_Response([ return new WP_REST_Response([
'success' => true, 'success' => true,
'update_available' => false, 'update_available' => false,
@@ -230,6 +247,8 @@ final class UpdateController
// Build response // Build response
$response = $this->buildUpdateResponse($product, $latestVersion, $license, $updateAvailable); $response = $this->buildUpdateResponse($product, $latestVersion, $license, $updateAvailable);
PrometheusController::incrementApiRequest('update-check', 'success');
return new WP_REST_Response($response); return new WP_REST_Response($response);
} }

View File

@@ -0,0 +1,282 @@
<?php
/**
* Prometheus Metrics Controller
*
* @package Jeremias\WcLicensedProduct\Metrics
*/
declare(strict_types=1);
namespace Jeremias\WcLicensedProduct\Metrics;
use Jeremias\WcLicensedProduct\Admin\SettingsController;
use Jeremias\WcLicensedProduct\License\LicenseManager;
use Jeremias\WcLicensedProduct\Product\VersionManager;
/**
* Exposes license and API metrics for Prometheus monitoring
*/
final class PrometheusController
{
/**
* Option name for storing API counters
*/
private const COUNTERS_OPTION = 'wclp_prometheus_counters';
private LicenseManager $licenseManager;
private VersionManager $versionManager;
public function __construct(LicenseManager $licenseManager, VersionManager $versionManager)
{
$this->licenseManager = $licenseManager;
$this->versionManager = $versionManager;
}
/**
* Register hooks for Prometheus metrics collection
*/
public function register(): void
{
// Only register if metrics are enabled
if (!SettingsController::isMetricsEnabled()) {
return;
}
add_action('wp_prometheus_collect_metrics', [$this, 'collectMetrics']);
add_action('wp_prometheus_register_dashboards', [$this, 'registerDashboard']);
}
/**
* Register Grafana dashboard with wp-prometheus
*
* @param object $provider The dashboard provider object
*/
public function registerDashboard(object $provider): void
{
$dashboardFile = WC_LICENSED_PRODUCT_PLUGIN_DIR . 'docs/grafana-dashboard.json';
if (!file_exists($dashboardFile)) {
return;
}
$provider->register_dashboard('wc-licensed-product', [
'title' => __('WC Licensed Product - License Metrics', 'wc-licensed-product'),
'description' => __('Monitor license status, downloads, API usage, and validation errors.', 'wc-licensed-product'),
'icon' => 'dashicons-admin-network',
'file' => $dashboardFile,
'plugin' => 'WC Licensed Product',
]);
}
/**
* Collect and register all metrics
*
* @param object $collector The Prometheus collector object
*/
public function collectMetrics(object $collector): void
{
$this->collectLicenseMetrics($collector);
$this->collectDownloadMetrics($collector);
$this->collectApiMetrics($collector);
}
/**
* Collect license-related metrics
*/
private function collectLicenseMetrics(object $collector): void
{
$stats = $this->licenseManager->getStatistics();
// License count by status (gauge)
$licensesByStatus = $collector->register_gauge(
'wclp_licenses_total',
'Total number of licenses by status',
['status']
);
foreach ($stats['by_status'] as $status => $count) {
$licensesByStatus->set($count, [$status]);
}
// Lifetime licenses (gauge)
$lifetimeLicenses = $collector->register_gauge(
'wclp_licenses_lifetime_total',
'Total number of lifetime licenses'
);
$lifetimeLicenses->set($stats['lifetime']);
// Expiring licenses (gauge)
$expiringLicenses = $collector->register_gauge(
'wclp_licenses_expiring_total',
'Total number of licenses with expiration date'
);
$expiringLicenses->set($stats['expiring']);
// Licenses expiring soon - next 30 days (gauge)
$expiringSoon = $collector->register_gauge(
'wclp_licenses_expiring_soon',
'Licenses expiring within 30 days'
);
$expiringSoon->set($stats['expiring_soon']);
}
/**
* Collect download-related metrics
*/
private function collectDownloadMetrics(object $collector): void
{
$stats = $this->versionManager->getDownloadStatistics();
// Total downloads (gauge)
$totalDownloads = $collector->register_gauge(
'wclp_downloads_total',
'Total number of file downloads'
);
$totalDownloads->set($stats['total']);
// Active versions count (gauge)
$activeVersions = $collector->register_gauge(
'wclp_versions_active_total',
'Total number of active product versions'
);
$activeVersions->set($this->countActiveVersions());
}
/**
* Collect API-related metrics (counters)
*/
private function collectApiMetrics(object $collector): void
{
$counters = $this->getCounters();
// API requests by endpoint and result (counter)
$apiRequests = $collector->register_counter(
'wclp_api_requests_total',
'Total API requests by endpoint and result',
['endpoint', 'result']
);
foreach ($counters['api_requests'] ?? [] as $key => $count) {
[$endpoint, $result] = explode(':', $key);
$apiRequests->incBy($count, [$endpoint, $result]);
}
// Rate limit exceeded events (counter)
$rateLimitExceeded = $collector->register_counter(
'wclp_rate_limit_exceeded_total',
'Total rate limit exceeded events by endpoint',
['endpoint']
);
foreach ($counters['rate_limit'] ?? [] as $endpoint => $count) {
$rateLimitExceeded->incBy($count, [$endpoint]);
}
// Validation errors by type (counter)
$validationErrors = $collector->register_counter(
'wclp_validation_errors_total',
'Total validation errors by error type',
['error_type']
);
foreach ($counters['validation_errors'] ?? [] as $errorType => $count) {
$validationErrors->incBy($count, [$errorType]);
}
}
/**
* Count active product versions
*/
private function countActiveVersions(): int
{
global $wpdb;
$tableName = \Jeremias\WcLicensedProduct\Installer::getVersionsTable();
return (int) $wpdb->get_var(
"SELECT COUNT(*) FROM {$tableName} WHERE is_active = 1"
);
}
/**
* Get stored counters
*/
private function getCounters(): array
{
$counters = get_option(self::COUNTERS_OPTION, []);
return is_array($counters) ? $counters : [];
}
/**
* Increment an API request counter
*
* @param string $endpoint The API endpoint (validate, status, activate, update-check)
* @param string $result The result (success or error)
*/
public static function incrementApiRequest(string $endpoint, string $result): void
{
if (!SettingsController::isMetricsEnabled()) {
return;
}
$counters = get_option(self::COUNTERS_OPTION, []);
if (!is_array($counters)) {
$counters = [];
}
$key = "{$endpoint}:{$result}";
$counters['api_requests'][$key] = ($counters['api_requests'][$key] ?? 0) + 1;
update_option(self::COUNTERS_OPTION, $counters, false);
}
/**
* Increment rate limit exceeded counter
*
* @param string $endpoint The API endpoint
*/
public static function incrementRateLimitExceeded(string $endpoint): void
{
if (!SettingsController::isMetricsEnabled()) {
return;
}
$counters = get_option(self::COUNTERS_OPTION, []);
if (!is_array($counters)) {
$counters = [];
}
$counters['rate_limit'][$endpoint] = ($counters['rate_limit'][$endpoint] ?? 0) + 1;
update_option(self::COUNTERS_OPTION, $counters, false);
}
/**
* Increment validation error counter
*
* @param string $errorType The error type (license_not_found, domain_mismatch, etc.)
*/
public static function incrementValidationError(string $errorType): void
{
if (!SettingsController::isMetricsEnabled()) {
return;
}
$counters = get_option(self::COUNTERS_OPTION, []);
if (!is_array($counters)) {
$counters = [];
}
$counters['validation_errors'][$errorType] = ($counters['validation_errors'][$errorType] ?? 0) + 1;
update_option(self::COUNTERS_OPTION, $counters, false);
}
/**
* Reset all counters (useful for testing or maintenance)
*/
public static function resetCounters(): void
{
delete_option(self::COUNTERS_OPTION);
}
}

View File

@@ -26,6 +26,7 @@ use Jeremias\WcLicensedProduct\Frontend\AccountController;
use Jeremias\WcLicensedProduct\Frontend\DownloadController; use Jeremias\WcLicensedProduct\Frontend\DownloadController;
use Jeremias\WcLicensedProduct\License\LicenseManager; use Jeremias\WcLicensedProduct\License\LicenseManager;
use Jeremias\WcLicensedProduct\License\PluginLicenseChecker; use Jeremias\WcLicensedProduct\License\PluginLicenseChecker;
use Jeremias\WcLicensedProduct\Metrics\PrometheusController;
use Jeremias\WcLicensedProduct\Product\LicensedProductType; use Jeremias\WcLicensedProduct\Product\LicensedProductType;
use Jeremias\WcLicensedProduct\Product\VersionManager; use Jeremias\WcLicensedProduct\Product\VersionManager;
use Jeremias\WcLicensedProduct\Update\PluginUpdateChecker; use Jeremias\WcLicensedProduct\Update\PluginUpdateChecker;
@@ -147,7 +148,7 @@ final class Plugin
new LicenseEmailController($this->licenseManager); new LicenseEmailController($this->licenseManager);
// Initialize response signing if server secret is configured // Initialize response signing if server secret is configured
if (defined('WC_LICENSE_SERVER_SECRET') && WC_LICENSE_SERVER_SECRET !== '') { if (ResponseSigner::isSigningEnabled()) {
(new ResponseSigner())->register(); (new ResponseSigner())->register();
} }
@@ -171,6 +172,9 @@ final class Plugin
if (!empty($serverUrl) && !$licenseChecker->isSelfLicensing()) { if (!empty($serverUrl) && !$licenseChecker->isSelfLicensing()) {
PluginUpdateChecker::getInstance()->register(); PluginUpdateChecker::getInstance()->register();
} }
// Initialize Prometheus metrics if enabled
(new PrometheusController($this->licenseManager, $this->versionManager))->register();
} }
/** /**

View File

@@ -3,7 +3,7 @@
* Plugin Name: WooCommerce Licensed Product * Plugin Name: WooCommerce Licensed Product
* Plugin URI: https://src.bundespruefstelle.ch/magdev/wc-licensed-product * Plugin URI: https://src.bundespruefstelle.ch/magdev/wc-licensed-product
* Description: WooCommerce plugin to sell software products using license keys with domain-based validation. * Description: WooCommerce plugin to sell software products using license keys with domain-based validation.
* Version: 0.7.2 * Version: 0.7.5
* Author: Marco Graetsch * Author: Marco Graetsch
* Author URI: https://src.bundespruefstelle.ch/magdev * Author URI: https://src.bundespruefstelle.ch/magdev
* License: GPL-2.0-or-later * License: GPL-2.0-or-later
@@ -28,7 +28,7 @@ if (!defined('ABSPATH')) {
} }
// Plugin constants // Plugin constants
define('WC_LICENSED_PRODUCT_VERSION', '0.7.2'); define('WC_LICENSED_PRODUCT_VERSION', '0.7.5');
define('WC_LICENSED_PRODUCT_PLUGIN_FILE', __FILE__); define('WC_LICENSED_PRODUCT_PLUGIN_FILE', __FILE__);
define('WC_LICENSED_PRODUCT_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('WC_LICENSED_PRODUCT_PLUGIN_DIR', plugin_dir_path(__FILE__));
define('WC_LICENSED_PRODUCT_PLUGIN_URL', plugin_dir_url(__FILE__)); define('WC_LICENSED_PRODUCT_PLUGIN_URL', plugin_dir_url(__FILE__));