mirror of
https://github.com/alexjustesen/speedtest-tracker.git
synced 2026-06-23 04:30:09 +00:00
feat: Add Prometheus (#2440)
Co-authored-by: Alex Justesen <alexjustesen@users.noreply.github.com>
This commit is contained in:
@@ -7,15 +7,18 @@ use App\Jobs\Influxdb\v2\TestConnectionJob;
|
|||||||
use App\Settings\DataIntegrationSettings;
|
use App\Settings\DataIntegrationSettings;
|
||||||
use Filament\Actions\Action;
|
use Filament\Actions\Action;
|
||||||
use Filament\Forms\Components\Checkbox;
|
use Filament\Forms\Components\Checkbox;
|
||||||
|
use Filament\Forms\Components\TagsInput;
|
||||||
use Filament\Forms\Components\TextInput;
|
use Filament\Forms\Components\TextInput;
|
||||||
use Filament\Forms\Components\Toggle;
|
use Filament\Forms\Components\Toggle;
|
||||||
use Filament\Notifications\Notification;
|
use Filament\Notifications\Notification;
|
||||||
use Filament\Pages\SettingsPage;
|
use Filament\Pages\SettingsPage;
|
||||||
use Filament\Schemas\Components\Actions;
|
use Filament\Schemas\Components\Actions;
|
||||||
use Filament\Schemas\Components\Grid;
|
use Filament\Schemas\Components\Grid;
|
||||||
use Filament\Schemas\Components\Section;
|
use Filament\Schemas\Components\Tabs;
|
||||||
|
use Filament\Schemas\Components\Tabs\Tab;
|
||||||
use Filament\Schemas\Components\Utilities\Get;
|
use Filament\Schemas\Components\Utilities\Get;
|
||||||
use Filament\Schemas\Schema;
|
use Filament\Schemas\Schema;
|
||||||
|
use Filament\Support\Icons\Heroicon;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
|
||||||
class DataIntegration extends SettingsPage
|
class DataIntegration extends SettingsPage
|
||||||
@@ -52,16 +55,14 @@ class DataIntegration extends SettingsPage
|
|||||||
{
|
{
|
||||||
return $schema
|
return $schema
|
||||||
->components([
|
->components([
|
||||||
Grid::make([
|
Tabs::make()
|
||||||
'default' => 1,
|
|
||||||
'md' => 3,
|
|
||||||
])
|
|
||||||
->schema([
|
->schema([
|
||||||
Section::make(__('settings/data_integration.influxdb_v2'))
|
Tab::make(__('settings/data_integration.influxdb_v2'))
|
||||||
->description(__('settings/data_integration.influxdb_v2_description'))
|
->icon(Heroicon::OutlinedCircleStack)
|
||||||
->schema([
|
->schema([
|
||||||
Toggle::make('influxdb_v2_enabled')
|
Toggle::make('influxdb_v2_enabled')
|
||||||
->label(__('settings/data_integration.influxdb_v2_enabled'))
|
->label(__('settings/data_integration.influxdb_v2_enabled'))
|
||||||
|
->helpertext(__('settings/data_integration.influxdb_v2_description'))
|
||||||
->reactive()
|
->reactive()
|
||||||
->columnSpanFull(),
|
->columnSpanFull(),
|
||||||
Grid::make(['default' => 1, 'md' => 3])
|
Grid::make(['default' => 1, 'md' => 3])
|
||||||
@@ -127,7 +128,26 @@ class DataIntegration extends SettingsPage
|
|||||||
]),
|
]),
|
||||||
]),
|
]),
|
||||||
])
|
])
|
||||||
->compact()
|
->columnSpanFull(),
|
||||||
|
Tab::make(__('settings/data_integration.prometheus'))
|
||||||
|
->icon(Heroicon::OutlinedChartBar)
|
||||||
|
->schema([
|
||||||
|
Toggle::make('prometheus_enabled')
|
||||||
|
->label(__('settings/data_integration.prometheus_enabled'))
|
||||||
|
->helperText(__('settings/data_integration.influxdb_v2_description'))
|
||||||
|
->reactive()
|
||||||
|
->columnSpanFull(),
|
||||||
|
Grid::make(['default' => 1, 'md' => 3])
|
||||||
|
->hidden(fn (Get $get) => $get('prometheus_enabled') !== true)
|
||||||
|
->schema([
|
||||||
|
TagsInput::make('prometheus_allowed_ips')
|
||||||
|
->label(__('settings/data_integration.prometheus_allowed_ips'))
|
||||||
|
->helperText(__('settings/data_integration.prometheus_allowed_ips_helper'))
|
||||||
|
->placeholder('192.168.1.100')
|
||||||
|
->splitKeys(['Tab', ',', ' '])
|
||||||
|
->columnSpanFull(),
|
||||||
|
]),
|
||||||
|
])
|
||||||
->columnSpanFull(),
|
->columnSpanFull(),
|
||||||
])
|
])
|
||||||
->columnSpanFull(),
|
->columnSpanFull(),
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Services\PrometheusMetricsService;
|
||||||
|
use App\Settings\DataIntegrationSettings;
|
||||||
|
use Illuminate\Http\Response;
|
||||||
|
|
||||||
|
class MetricsController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
protected PrometheusMetricsService $metricsService,
|
||||||
|
protected DataIntegrationSettings $settings
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function __invoke(): Response
|
||||||
|
{
|
||||||
|
if (! $this->settings->prometheus_enabled) {
|
||||||
|
abort(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$metrics = $this->metricsService->generateMetrics();
|
||||||
|
|
||||||
|
return response($metrics, 200, [
|
||||||
|
'Content-Type' => 'text/plain; version=0.0.4; charset=utf-8',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use App\Helpers\Network;
|
||||||
|
use App\Settings\DataIntegrationSettings;
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
|
class PrometheusAllowedIpMiddleware
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
protected DataIntegrationSettings $settings
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle an incoming request.
|
||||||
|
*
|
||||||
|
* @param Closure(Request):Response $next
|
||||||
|
*/
|
||||||
|
public function handle(Request $request, Closure $next): Response
|
||||||
|
{
|
||||||
|
if (blank($this->settings->prometheus_allowed_ips)) {
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
|
||||||
|
$clientIp = $request->ip();
|
||||||
|
$allowedIps = $this->settings->prometheus_allowed_ips;
|
||||||
|
|
||||||
|
foreach ($allowedIps as $allowedIp) {
|
||||||
|
if (str_contains($allowedIp, '/')) {
|
||||||
|
if (Network::ipInRange($clientIp, $allowedIp)) {
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
} elseif ($clientIp === $allowedIp) {
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abort(403);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ use App\Events\SpeedtestCompleted;
|
|||||||
use App\Events\SpeedtestFailed;
|
use App\Events\SpeedtestFailed;
|
||||||
use App\Jobs\Influxdb\v2\WriteResult;
|
use App\Jobs\Influxdb\v2\WriteResult;
|
||||||
use App\Settings\DataIntegrationSettings;
|
use App\Settings\DataIntegrationSettings;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
|
||||||
class ProcessSpeedtestDataIntegrations
|
class ProcessSpeedtestDataIntegrations
|
||||||
{
|
{
|
||||||
@@ -24,5 +25,9 @@ class ProcessSpeedtestDataIntegrations
|
|||||||
if ($this->settings->influxdb_v2_enabled) {
|
if ($this->settings->influxdb_v2_enabled) {
|
||||||
WriteResult::dispatch($event->result);
|
WriteResult::dispatch($event->result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($this->settings->prometheus_enabled) {
|
||||||
|
Cache::forever('prometheus:latest_result', $event->result->id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,249 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Models\Result;
|
||||||
|
use App\Settings\DataIntegrationSettings;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
use Prometheus\CollectorRegistry;
|
||||||
|
use Prometheus\RenderTextFormat;
|
||||||
|
use Prometheus\Storage\InMemory;
|
||||||
|
|
||||||
|
class PrometheusMetricsService
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
protected DataIntegrationSettings $settings
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function generateMetrics(): string
|
||||||
|
{
|
||||||
|
$registry = new CollectorRegistry(new InMemory);
|
||||||
|
|
||||||
|
$resultId = Cache::get('prometheus:latest_result');
|
||||||
|
|
||||||
|
if (! $resultId) {
|
||||||
|
return $this->emptyMetrics();
|
||||||
|
}
|
||||||
|
|
||||||
|
$lastResult = Result::find($resultId);
|
||||||
|
|
||||||
|
if (! $lastResult) {
|
||||||
|
return $this->emptyMetrics();
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->registerMetrics($registry, $lastResult);
|
||||||
|
|
||||||
|
$renderer = new RenderTextFormat;
|
||||||
|
|
||||||
|
return $renderer->render($registry->getMetricFamilySamples());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function registerMetrics(CollectorRegistry $registry, Result $result): void
|
||||||
|
{
|
||||||
|
$labels = $this->buildLabels($result);
|
||||||
|
$labelNames = array_keys($labels);
|
||||||
|
$labelValues = array_values($labels);
|
||||||
|
|
||||||
|
// Download speed in bytes
|
||||||
|
$downloadBytesGauge = $registry->getOrRegisterGauge(
|
||||||
|
'speedtest_tracker',
|
||||||
|
'download_bytes',
|
||||||
|
'Download speed in bytes per second',
|
||||||
|
$labelNames
|
||||||
|
);
|
||||||
|
$downloadBytesGauge->set($result->download, $labelValues);
|
||||||
|
|
||||||
|
// Upload speed in bytes
|
||||||
|
$uploadBytesGauge = $registry->getOrRegisterGauge(
|
||||||
|
'speedtest_tracker',
|
||||||
|
'upload_bytes',
|
||||||
|
'Upload speed in bytes per second',
|
||||||
|
$labelNames
|
||||||
|
);
|
||||||
|
$uploadBytesGauge->set($result->upload, $labelValues);
|
||||||
|
|
||||||
|
// Download speed in bits per second
|
||||||
|
$downloadBitsGauge = $registry->getOrRegisterGauge(
|
||||||
|
'speedtest_tracker',
|
||||||
|
'download_bits',
|
||||||
|
'Download speed in bits per second',
|
||||||
|
$labelNames
|
||||||
|
);
|
||||||
|
$downloadBitsGauge->set(toBits($result->download), $labelValues);
|
||||||
|
|
||||||
|
// Upload speed in bits per second
|
||||||
|
$uploadBitsGauge = $registry->getOrRegisterGauge(
|
||||||
|
'speedtest_tracker',
|
||||||
|
'upload_bits',
|
||||||
|
'Upload speed in bits per second',
|
||||||
|
$labelNames
|
||||||
|
);
|
||||||
|
$uploadBitsGauge->set(toBits($result->upload), $labelValues);
|
||||||
|
|
||||||
|
// Ping latency in milliseconds
|
||||||
|
$pingGauge = $registry->getOrRegisterGauge(
|
||||||
|
'speedtest_tracker',
|
||||||
|
'ping_ms',
|
||||||
|
'Ping latency in milliseconds',
|
||||||
|
$labelNames
|
||||||
|
);
|
||||||
|
$pingGauge->set($result->ping, $labelValues);
|
||||||
|
|
||||||
|
// Ping jitter
|
||||||
|
$pingJitterGauge = $registry->getOrRegisterGauge(
|
||||||
|
'speedtest_tracker',
|
||||||
|
'ping_jitter_ms',
|
||||||
|
'Ping jitter in milliseconds',
|
||||||
|
$labelNames
|
||||||
|
);
|
||||||
|
$pingJitterGauge->set($result->ping_jitter ?? 0, $labelValues);
|
||||||
|
|
||||||
|
// Download jitter
|
||||||
|
$downloadJitterGauge = $registry->getOrRegisterGauge(
|
||||||
|
'speedtest_tracker',
|
||||||
|
'download_jitter_ms',
|
||||||
|
'Download jitter in milliseconds',
|
||||||
|
$labelNames
|
||||||
|
);
|
||||||
|
$downloadJitterGauge->set($result->download_jitter ?? 0, $labelValues);
|
||||||
|
|
||||||
|
// Upload jitter
|
||||||
|
$uploadJitterGauge = $registry->getOrRegisterGauge(
|
||||||
|
'speedtest_tracker',
|
||||||
|
'upload_jitter_ms',
|
||||||
|
'Upload jitter in milliseconds',
|
||||||
|
$labelNames
|
||||||
|
);
|
||||||
|
$uploadJitterGauge->set($result->upload_jitter ?? 0, $labelValues);
|
||||||
|
|
||||||
|
// Packet loss
|
||||||
|
$packetLossGauge = $registry->getOrRegisterGauge(
|
||||||
|
'speedtest_tracker',
|
||||||
|
'packet_loss_percent',
|
||||||
|
'Packet loss percentage',
|
||||||
|
$labelNames
|
||||||
|
);
|
||||||
|
$packetLossGauge->set($result->packet_loss ?? 0, $labelValues);
|
||||||
|
|
||||||
|
// Ping latency low/high
|
||||||
|
$pingLowGauge = $registry->getOrRegisterGauge(
|
||||||
|
'speedtest_tracker',
|
||||||
|
'ping_low_ms',
|
||||||
|
'Ping low latency in milliseconds',
|
||||||
|
$labelNames
|
||||||
|
);
|
||||||
|
$pingLowGauge->set($result->ping_low ?? 0, $labelValues);
|
||||||
|
|
||||||
|
$pingHighGauge = $registry->getOrRegisterGauge(
|
||||||
|
'speedtest_tracker',
|
||||||
|
'ping_high_ms',
|
||||||
|
'Ping high latency in milliseconds',
|
||||||
|
$labelNames
|
||||||
|
);
|
||||||
|
$pingHighGauge->set($result->ping_high ?? 0, $labelValues);
|
||||||
|
|
||||||
|
// Download latency metrics (IQM = Interquartile Mean - more reliable than average)
|
||||||
|
$downloadLatencyIqmGauge = $registry->getOrRegisterGauge(
|
||||||
|
'speedtest_tracker',
|
||||||
|
'download_latency_iqm_ms',
|
||||||
|
'Download latency interquartile mean in milliseconds',
|
||||||
|
$labelNames
|
||||||
|
);
|
||||||
|
$downloadLatencyIqmGauge->set($result->downloadlatencyiqm ?? 0, $labelValues);
|
||||||
|
|
||||||
|
$downloadLatencyLowGauge = $registry->getOrRegisterGauge(
|
||||||
|
'speedtest_tracker',
|
||||||
|
'download_latency_low_ms',
|
||||||
|
'Download latency low in milliseconds',
|
||||||
|
$labelNames
|
||||||
|
);
|
||||||
|
$downloadLatencyLowGauge->set($result->downloadlatency_low ?? 0, $labelValues);
|
||||||
|
|
||||||
|
$downloadLatencyHighGauge = $registry->getOrRegisterGauge(
|
||||||
|
'speedtest_tracker',
|
||||||
|
'download_latency_high_ms',
|
||||||
|
'Download latency high in milliseconds',
|
||||||
|
$labelNames
|
||||||
|
);
|
||||||
|
$downloadLatencyHighGauge->set($result->downloadlatency_high ?? 0, $labelValues);
|
||||||
|
|
||||||
|
// Upload latency metrics
|
||||||
|
$uploadLatencyIqmGauge = $registry->getOrRegisterGauge(
|
||||||
|
'speedtest_tracker',
|
||||||
|
'upload_latency_iqm_ms',
|
||||||
|
'Upload latency interquartile mean in milliseconds',
|
||||||
|
$labelNames
|
||||||
|
);
|
||||||
|
$uploadLatencyIqmGauge->set($result->uploadlatencyiqm ?? 0, $labelValues);
|
||||||
|
|
||||||
|
$uploadLatencyLowGauge = $registry->getOrRegisterGauge(
|
||||||
|
'speedtest_tracker',
|
||||||
|
'upload_latency_low_ms',
|
||||||
|
'Upload latency low in milliseconds',
|
||||||
|
$labelNames
|
||||||
|
);
|
||||||
|
$uploadLatencyLowGauge->set($result->uploadlatency_low ?? 0, $labelValues);
|
||||||
|
|
||||||
|
$uploadLatencyHighGauge = $registry->getOrRegisterGauge(
|
||||||
|
'speedtest_tracker',
|
||||||
|
'upload_latency_high_ms',
|
||||||
|
'Upload latency high in milliseconds',
|
||||||
|
$labelNames
|
||||||
|
);
|
||||||
|
$uploadLatencyHighGauge->set($result->uploadlatency_high ?? 0, $labelValues);
|
||||||
|
|
||||||
|
// Bytes transferred during test
|
||||||
|
$downloadedBytesGauge = $registry->getOrRegisterGauge(
|
||||||
|
'speedtest_tracker',
|
||||||
|
'downloaded_bytes',
|
||||||
|
'Total bytes downloaded during test',
|
||||||
|
$labelNames
|
||||||
|
);
|
||||||
|
$downloadedBytesGauge->set($result->downloaded_bytes ?? 0, $labelValues);
|
||||||
|
|
||||||
|
$uploadedBytesGauge = $registry->getOrRegisterGauge(
|
||||||
|
'speedtest_tracker',
|
||||||
|
'uploaded_bytes',
|
||||||
|
'Total bytes uploaded during test',
|
||||||
|
$labelNames
|
||||||
|
);
|
||||||
|
$uploadedBytesGauge->set($result->uploaded_bytes ?? 0, $labelValues);
|
||||||
|
|
||||||
|
// Test duration
|
||||||
|
$downloadElapsedGauge = $registry->getOrRegisterGauge(
|
||||||
|
'speedtest_tracker',
|
||||||
|
'download_elapsed_ms',
|
||||||
|
'Download test duration in milliseconds',
|
||||||
|
$labelNames
|
||||||
|
);
|
||||||
|
$downloadElapsedGauge->set($result->download_elapsed ?? 0, $labelValues);
|
||||||
|
|
||||||
|
$uploadElapsedGauge = $registry->getOrRegisterGauge(
|
||||||
|
'speedtest_tracker',
|
||||||
|
'upload_elapsed_ms',
|
||||||
|
'Upload test duration in milliseconds',
|
||||||
|
$labelNames
|
||||||
|
);
|
||||||
|
$uploadElapsedGauge->set($result->upload_elapsed ?? 0, $labelValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function buildLabels(Result $result): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'server_id' => (string) ($result->server_id ?? ''),
|
||||||
|
'server_name' => $result->server_name ?? '',
|
||||||
|
'server_country' => $result->server_country ?? '',
|
||||||
|
'server_location' => $result->server_location ?? '',
|
||||||
|
'isp' => $result->isp ?? '',
|
||||||
|
'scheduled' => $result->scheduled ? 'true' : 'false',
|
||||||
|
'healthy' => $result->healthy ? 'true' : 'false',
|
||||||
|
'status' => $result->status->value,
|
||||||
|
'app_name' => config('app.name', 'Speedtest Tracker'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function emptyMetrics(): string
|
||||||
|
{
|
||||||
|
return "# no data available\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,6 +18,10 @@ class DataIntegrationSettings extends Settings
|
|||||||
|
|
||||||
public bool $influxdb_v2_verify_ssl;
|
public bool $influxdb_v2_verify_ssl;
|
||||||
|
|
||||||
|
public bool $prometheus_enabled;
|
||||||
|
|
||||||
|
public array $prometheus_allowed_ips = [];
|
||||||
|
|
||||||
public static function group(): string
|
public static function group(): string
|
||||||
{
|
{
|
||||||
return 'dataintegration';
|
return 'dataintegration';
|
||||||
|
|||||||
@@ -29,6 +29,7 @@
|
|||||||
"livewire/livewire": "^3.6.4",
|
"livewire/livewire": "^3.6.4",
|
||||||
"lorisleiva/laravel-actions": "^2.9.1",
|
"lorisleiva/laravel-actions": "^2.9.1",
|
||||||
"maennchen/zipstream-php": "^2.4",
|
"maennchen/zipstream-php": "^2.4",
|
||||||
|
"promphp/prometheus_client_php": "^2.14",
|
||||||
"saloonphp/laravel-plugin": "^3.0",
|
"saloonphp/laravel-plugin": "^3.0",
|
||||||
"secondnetwork/blade-tabler-icons": "^3.35.0",
|
"secondnetwork/blade-tabler-icons": "^3.35.0",
|
||||||
"spatie/laravel-json-api-paginate": "^1.16.3",
|
"spatie/laravel-json-api-paginate": "^1.16.3",
|
||||||
|
|||||||
Generated
+68
@@ -5462,6 +5462,74 @@
|
|||||||
},
|
},
|
||||||
"time": "2025-09-19T23:02:26+00:00"
|
"time": "2025-09-19T23:02:26+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "promphp/prometheus_client_php",
|
||||||
|
"version": "v2.14.1",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/PromPHP/prometheus_client_php.git",
|
||||||
|
"reference": "a283aea8269287dc35313a0055480d950c59ac1f"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/PromPHP/prometheus_client_php/zipball/a283aea8269287dc35313a0055480d950c59ac1f",
|
||||||
|
"reference": "a283aea8269287dc35313a0055480d950c59ac1f",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"ext-json": "*",
|
||||||
|
"php": "^7.4|^8.0"
|
||||||
|
},
|
||||||
|
"replace": {
|
||||||
|
"endclothing/prometheus_client_php": "*",
|
||||||
|
"jimdo/prometheus_client_php": "*",
|
||||||
|
"lkaemmerling/prometheus_client_php": "*"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"guzzlehttp/guzzle": "^6.3|^7.0",
|
||||||
|
"phpstan/extension-installer": "^1.0",
|
||||||
|
"phpstan/phpstan": "^1.5.4",
|
||||||
|
"phpstan/phpstan-phpunit": "^1.1.0",
|
||||||
|
"phpstan/phpstan-strict-rules": "^1.1.0",
|
||||||
|
"phpunit/phpunit": "^9.4",
|
||||||
|
"squizlabs/php_codesniffer": "^3.6",
|
||||||
|
"symfony/polyfill-apcu": "^1.6"
|
||||||
|
},
|
||||||
|
"suggest": {
|
||||||
|
"ext-apc": "Required if using APCu.",
|
||||||
|
"ext-pdo": "Required if using PDO.",
|
||||||
|
"ext-redis": "Required if using Redis.",
|
||||||
|
"promphp/prometheus_push_gateway_php": "An easy client for using Prometheus PushGateway.",
|
||||||
|
"symfony/polyfill-apcu": "Required if you use APCu on PHP8.0+"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-master": "1.0-dev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Prometheus\\": "src/Prometheus/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"Apache-2.0"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Lukas Kämmerling",
|
||||||
|
"email": "kontakt@lukas-kaemmerling.de"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Prometheus instrumentation library for PHP applications.",
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/PromPHP/prometheus_client_php/issues",
|
||||||
|
"source": "https://github.com/PromPHP/prometheus_client_php/tree/v2.14.1"
|
||||||
|
},
|
||||||
|
"time": "2025-04-14T07:59:43+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "psr/clock",
|
"name": "psr/clock",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Spatie\LaravelSettings\Migrations\SettingsMigration;
|
||||||
|
|
||||||
|
class CreatePrometheusSettings extends SettingsMigration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
$this->migrator->add('dataintegration.prometheus_enabled', false);
|
||||||
|
$this->migrator->add('dataintegration.prometheus_allowed_ips', []);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -33,6 +33,13 @@ return [
|
|||||||
'influxdb_bulk_write_success' => 'Finished bulk data load to Influxdb.',
|
'influxdb_bulk_write_success' => 'Finished bulk data load to Influxdb.',
|
||||||
'influxdb_bulk_write_success_body' => 'Data has been sent to InfluxDB, check if the data was received.',
|
'influxdb_bulk_write_success_body' => 'Data has been sent to InfluxDB, check if the data was received.',
|
||||||
|
|
||||||
|
// Prometheus
|
||||||
|
'prometheus' => 'Prometheus',
|
||||||
|
'prometheus_enabled' => 'Enable',
|
||||||
|
'prometheus_enabled_helper_text' => 'When enabled, metrics for each new speedtest will be available at the /prometheus endpoint.',
|
||||||
|
'prometheus_allowed_ips' => 'Allowed IP Addresses',
|
||||||
|
'prometheus_allowed_ips_helper' => 'List of IP addresses or CIDR ranges (e.g., 192.168.1.0/24) allowed to access the metrics endpoint. Leave empty to allow all IPs.',
|
||||||
|
|
||||||
// Common labels
|
// Common labels
|
||||||
'org' => 'Org',
|
'org' => 'Org',
|
||||||
'bucket' => 'Bucket',
|
'bucket' => 'Bucket',
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use App\Http\Controllers\HomeController;
|
use App\Http\Controllers\HomeController;
|
||||||
|
use App\Http\Controllers\MetricsController;
|
||||||
|
use App\Http\Middleware\PrometheusAllowedIpMiddleware;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -18,6 +20,10 @@ Route::get('/', HomeController::class)
|
|||||||
->middleware(['getting-started', 'public-dashboard'])
|
->middleware(['getting-started', 'public-dashboard'])
|
||||||
->name('home');
|
->name('home');
|
||||||
|
|
||||||
|
Route::get('/prometheus', MetricsController::class)
|
||||||
|
->middleware(PrometheusAllowedIpMiddleware::class)
|
||||||
|
->name('prometheus');
|
||||||
|
|
||||||
Route::view('/getting-started', 'getting-started')
|
Route::view('/getting-started', 'getting-started')
|
||||||
->name('getting-started');
|
->name('getting-started');
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\Result;
|
||||||
|
use App\Settings\DataIntegrationSettings;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
Cache::flush();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('metrics endpoint', function () {
|
||||||
|
test('returns 404 when prometheus is disabled', function () {
|
||||||
|
app(DataIntegrationSettings::class)->fill(['prometheus_enabled' => false])->save();
|
||||||
|
|
||||||
|
$response = $this->get('/prometheus');
|
||||||
|
|
||||||
|
$response->assertNotFound();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns metrics when prometheus is enabled and no IP restrictions', function () {
|
||||||
|
app(DataIntegrationSettings::class)->fill([
|
||||||
|
'prometheus_enabled' => true,
|
||||||
|
'prometheus_allowed_ips' => [],
|
||||||
|
])->save();
|
||||||
|
|
||||||
|
Result::factory()->create();
|
||||||
|
|
||||||
|
$response = $this->get('/prometheus');
|
||||||
|
|
||||||
|
$response->assertSuccessful();
|
||||||
|
$response->assertHeader('Content-Type', 'text/plain; version=0.0.4; charset=utf-8');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns 403 when IP is not in allowed list', function () {
|
||||||
|
app(DataIntegrationSettings::class)->fill([
|
||||||
|
'prometheus_enabled' => true,
|
||||||
|
'prometheus_allowed_ips' => ['192.168.1.100', '10.0.0.1'],
|
||||||
|
])->save();
|
||||||
|
|
||||||
|
$response = $this->get('/prometheus', [
|
||||||
|
'REMOTE_ADDR' => '192.168.1.50',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertForbidden();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns metrics when IP is in allowed list', function () {
|
||||||
|
app(DataIntegrationSettings::class)->fill([
|
||||||
|
'prometheus_enabled' => true,
|
||||||
|
'prometheus_allowed_ips' => ['192.168.1.100', '10.0.0.1'],
|
||||||
|
])->save();
|
||||||
|
|
||||||
|
Result::factory()->create();
|
||||||
|
|
||||||
|
$response = $this->get('/prometheus', [
|
||||||
|
'REMOTE_ADDR' => '192.168.1.100',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertSuccessful();
|
||||||
|
$response->assertHeader('Content-Type', 'text/plain; version=0.0.4; charset=utf-8');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('allows access with empty array', function () {
|
||||||
|
app(DataIntegrationSettings::class)->fill([
|
||||||
|
'prometheus_enabled' => true,
|
||||||
|
'prometheus_allowed_ips' => [],
|
||||||
|
])->save();
|
||||||
|
|
||||||
|
Result::factory()->create();
|
||||||
|
|
||||||
|
$response = $this->get('/prometheus', [
|
||||||
|
'REMOTE_ADDR' => '10.0.0.1',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertSuccessful();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('allows access when IP is in CIDR range', function () {
|
||||||
|
app(DataIntegrationSettings::class)->fill([
|
||||||
|
'prometheus_enabled' => true,
|
||||||
|
'prometheus_allowed_ips' => ['192.168.1.0/24'],
|
||||||
|
])->save();
|
||||||
|
|
||||||
|
Result::factory()->create();
|
||||||
|
|
||||||
|
$response = $this->get('/prometheus', [
|
||||||
|
'REMOTE_ADDR' => '192.168.1.150',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertSuccessful();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('denies access when IP is not in CIDR range', function () {
|
||||||
|
app(DataIntegrationSettings::class)->fill([
|
||||||
|
'prometheus_enabled' => true,
|
||||||
|
'prometheus_allowed_ips' => ['192.168.1.0/24'],
|
||||||
|
])->save();
|
||||||
|
|
||||||
|
$response = $this->get('/prometheus', [
|
||||||
|
'REMOTE_ADDR' => '192.168.2.1',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertForbidden();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('supports mixed IP addresses and CIDR ranges', function () {
|
||||||
|
app(DataIntegrationSettings::class)->fill([
|
||||||
|
'prometheus_enabled' => true,
|
||||||
|
'prometheus_allowed_ips' => ['10.0.0.1', '192.168.1.0/24'],
|
||||||
|
])->save();
|
||||||
|
|
||||||
|
Result::factory()->create();
|
||||||
|
|
||||||
|
$response = $this->get('/prometheus', [
|
||||||
|
'REMOTE_ADDR' => '192.168.1.50',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertSuccessful();
|
||||||
|
|
||||||
|
$response = $this->get('/prometheus', [
|
||||||
|
'REMOTE_ADDR' => '10.0.0.1',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertSuccessful();
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user