Skip to content

Commit cb659fc

Browse files
chore: refactor prometheus to handle missing data (#2696)
Co-authored-by: Alex Justesen <[email protected]>
1 parent 1e9ea18 commit cb659fc

File tree

2 files changed

+110
-136
lines changed

2 files changed

+110
-136
lines changed

app/Services/PrometheusMetricsService.php

Lines changed: 52 additions & 136 deletions
Original file line numberDiff line numberDiff line change
@@ -105,142 +105,35 @@ protected function registerMetrics(CollectorRegistry $registry, Result $result):
105105
);
106106
$pingGauge->set($result->ping, $labelValues);
107107

108-
// Ping jitter
109-
$pingJitterGauge = $registry->getOrRegisterGauge(
110-
'speedtest_tracker',
111-
'ping_jitter_ms',
112-
'Ping jitter in milliseconds',
113-
$labelNames
114-
);
115-
$pingJitterGauge->set($result->ping_jitter, $labelValues);
116-
117-
// Download jitter
118-
$downloadJitterGauge = $registry->getOrRegisterGauge(
119-
'speedtest_tracker',
120-
'download_jitter_ms',
121-
'Download jitter in milliseconds',
122-
$labelNames
123-
);
124-
$downloadJitterGauge->set($result->download_jitter, $labelValues);
125-
126-
// Upload jitter
127-
$uploadJitterGauge = $registry->getOrRegisterGauge(
128-
'speedtest_tracker',
129-
'upload_jitter_ms',
130-
'Upload jitter in milliseconds',
131-
$labelNames
132-
);
133-
$uploadJitterGauge->set($result->upload_jitter, $labelValues);
134-
135-
// Packet loss
136-
$packetLossGauge = $registry->getOrRegisterGauge(
137-
'speedtest_tracker',
138-
'packet_loss_percent',
139-
'Packet loss percentage',
140-
$labelNames
141-
);
142-
$packetLossGauge->set($result->packet_loss, $labelValues);
143-
144-
// Ping latency low/high
145-
$pingLowGauge = $registry->getOrRegisterGauge(
146-
'speedtest_tracker',
147-
'ping_low_ms',
148-
'Ping low latency in milliseconds',
149-
$labelNames
150-
);
151-
$pingLowGauge->set($result->ping_low, $labelValues);
152-
153-
$pingHighGauge = $registry->getOrRegisterGauge(
154-
'speedtest_tracker',
155-
'ping_high_ms',
156-
'Ping high latency in milliseconds',
157-
$labelNames
158-
);
159-
$pingHighGauge->set($result->ping_high, $labelValues);
160-
161-
// Download latency metrics (IQM = Interquartile Mean - more reliable than average)
162-
$downloadLatencyIqmGauge = $registry->getOrRegisterGauge(
163-
'speedtest_tracker',
164-
'download_latency_iqm_ms',
165-
'Download latency interquartile mean in milliseconds',
166-
$labelNames
167-
);
168-
$downloadLatencyIqmGauge->set($result->downloadlatencyiqm, $labelValues);
169-
170-
$downloadLatencyLowGauge = $registry->getOrRegisterGauge(
171-
'speedtest_tracker',
172-
'download_latency_low_ms',
173-
'Download latency low in milliseconds',
174-
$labelNames
175-
);
176-
$downloadLatencyLowGauge->set($result->downloadlatency_low, $labelValues);
177-
178-
$downloadLatencyHighGauge = $registry->getOrRegisterGauge(
179-
'speedtest_tracker',
180-
'download_latency_high_ms',
181-
'Download latency high in milliseconds',
182-
$labelNames
183-
);
184-
$downloadLatencyHighGauge->set($result->downloadlatency_high, $labelValues);
185-
186-
// Upload latency metrics
187-
$uploadLatencyIqmGauge = $registry->getOrRegisterGauge(
188-
'speedtest_tracker',
189-
'upload_latency_iqm_ms',
190-
'Upload latency interquartile mean in milliseconds',
191-
$labelNames
192-
);
193-
$uploadLatencyIqmGauge->set($result->uploadlatencyiqm, $labelValues);
194-
195-
$uploadLatencyLowGauge = $registry->getOrRegisterGauge(
196-
'speedtest_tracker',
197-
'upload_latency_low_ms',
198-
'Upload latency low in milliseconds',
199-
$labelNames
200-
);
201-
$uploadLatencyLowGauge->set($result->uploadlatency_low, $labelValues);
202-
203-
$uploadLatencyHighGauge = $registry->getOrRegisterGauge(
204-
'speedtest_tracker',
205-
'upload_latency_high_ms',
206-
'Upload latency high in milliseconds',
207-
$labelNames
208-
);
209-
$uploadLatencyHighGauge->set($result->uploadlatency_high, $labelValues);
210-
211-
// Bytes transferred during test
212-
$downloadedBytesGauge = $registry->getOrRegisterGauge(
213-
'speedtest_tracker',
214-
'downloaded_bytes',
215-
'Total bytes downloaded during test',
216-
$labelNames
217-
);
218-
$downloadedBytesGauge->set($result->downloaded_bytes, $labelValues);
219-
220-
$uploadedBytesGauge = $registry->getOrRegisterGauge(
221-
'speedtest_tracker',
222-
'uploaded_bytes',
223-
'Total bytes uploaded during test',
224-
$labelNames
225-
);
226-
$uploadedBytesGauge->set($result->uploaded_bytes, $labelValues);
227-
228-
// Test duration
229-
$downloadElapsedGauge = $registry->getOrRegisterGauge(
230-
'speedtest_tracker',
231-
'download_elapsed_ms',
232-
'Download test duration in milliseconds',
233-
$labelNames
234-
);
235-
$downloadElapsedGauge->set($result->download_elapsed, $labelValues);
236-
237-
$uploadElapsedGauge = $registry->getOrRegisterGauge(
238-
'speedtest_tracker',
239-
'upload_elapsed_ms',
240-
'Upload test duration in milliseconds',
241-
$labelNames
242-
);
243-
$uploadElapsedGauge->set($result->upload_elapsed, $labelValues);
108+
// Jitter metrics - optional, may not be present in all test results
109+
$this->registerGaugeIfNotNull($registry, 'ping_jitter_ms', 'Ping jitter in milliseconds', $labelNames, $labelValues, $result->ping_jitter);
110+
$this->registerGaugeIfNotNull($registry, 'download_jitter_ms', 'Download jitter in milliseconds', $labelNames, $labelValues, $result->download_jitter);
111+
$this->registerGaugeIfNotNull($registry, 'upload_jitter_ms', 'Upload jitter in milliseconds', $labelNames, $labelValues, $result->upload_jitter);
112+
113+
// Packet loss - optional
114+
$this->registerGaugeIfNotNull($registry, 'packet_loss_percent', 'Packet loss percentage', $labelNames, $labelValues, $result->packet_loss);
115+
116+
// Ping latency metrics - optional
117+
$this->registerGaugeIfNotNull($registry, 'ping_low_ms', 'Ping low latency in milliseconds', $labelNames, $labelValues, $result->ping_low);
118+
$this->registerGaugeIfNotNull($registry, 'ping_high_ms', 'Ping high latency in milliseconds', $labelNames, $labelValues, $result->ping_high);
119+
120+
// Download latency metrics - optional (IQM = Interquartile Mean - more reliable than average)
121+
$this->registerGaugeIfNotNull($registry, 'download_latency_iqm_ms', 'Download latency interquartile mean in milliseconds', $labelNames, $labelValues, $result->downloadlatencyiqm);
122+
$this->registerGaugeIfNotNull($registry, 'download_latency_low_ms', 'Download latency low in milliseconds', $labelNames, $labelValues, $result->downloadlatency_low);
123+
$this->registerGaugeIfNotNull($registry, 'download_latency_high_ms', 'Download latency high in milliseconds', $labelNames, $labelValues, $result->downloadlatency_high);
124+
125+
// Upload latency metrics - optional
126+
$this->registerGaugeIfNotNull($registry, 'upload_latency_iqm_ms', 'Upload latency interquartile mean in milliseconds', $labelNames, $labelValues, $result->uploadlatencyiqm);
127+
$this->registerGaugeIfNotNull($registry, 'upload_latency_low_ms', 'Upload latency low in milliseconds', $labelNames, $labelValues, $result->uploadlatency_low);
128+
$this->registerGaugeIfNotNull($registry, 'upload_latency_high_ms', 'Upload latency high in milliseconds', $labelNames, $labelValues, $result->uploadlatency_high);
129+
130+
// Bytes transferred during test - optional
131+
$this->registerGaugeIfNotNull($registry, 'downloaded_bytes', 'Total bytes downloaded during test', $labelNames, $labelValues, $result->downloaded_bytes);
132+
$this->registerGaugeIfNotNull($registry, 'uploaded_bytes', 'Total bytes uploaded during test', $labelNames, $labelValues, $result->uploaded_bytes);
133+
134+
// Test duration - optional
135+
$this->registerGaugeIfNotNull($registry, 'download_elapsed_ms', 'Download test duration in milliseconds', $labelNames, $labelValues, $result->download_elapsed);
136+
$this->registerGaugeIfNotNull($registry, 'upload_elapsed_ms', 'Upload test duration in milliseconds', $labelNames, $labelValues, $result->upload_elapsed);
244137
}
245138

246139
protected function buildLabels(Result $result): array
@@ -262,4 +155,27 @@ protected function emptyMetrics(): string
262155
{
263156
return "# no data available\n";
264157
}
158+
159+
/**
160+
* Register a gauge metric only if the value is not null.
161+
* Follows Prometheus best practice of not exporting missing metrics.
162+
*/
163+
protected function registerGaugeIfNotNull(
164+
CollectorRegistry $registry,
165+
string $name,
166+
string $help,
167+
array $labelNames,
168+
array $labelValues,
169+
mixed $value
170+
): void {
171+
if ($value !== null) {
172+
$gauge = $registry->getOrRegisterGauge(
173+
'speedtest_tracker',
174+
$name,
175+
$help,
176+
$labelNames
177+
);
178+
$gauge->set($value, $labelValues);
179+
}
180+
}
265181
}

tests/Feature/MetricsEndpointTest.php

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,4 +123,62 @@
123123

124124
$response->assertSuccessful();
125125
});
126+
127+
test('handles results with missing packet loss data', function () {
128+
app(DataIntegrationSettings::class)->fill([
129+
'prometheus_enabled' => true,
130+
'prometheus_allowed_ips' => [],
131+
])->save();
132+
133+
// Create a result without packet loss data
134+
$dataWithoutPacketLoss = json_decode('{"isp": "Speedtest Communications", "ping": {"low": 17.841, "high": 24.077, "jitter": 1.878, "latency": 19.133}, "type": "result", "result": {"id": "d6fe2fb3-f4f8-4cc5-b898-7b42109e67c2", "url": "https://docs.speedtest-tracker.dev", "persisted": true}, "server": {"id": 0, "ip": "127.0.0.1", "host": "docs.speedtest-tracker.dev", "name": "Speedtest", "port": 8080, "country": "United States", "location": "New York City, NY"}, "upload": {"bytes": 124297377, "elapsed": 9628, "latency": {"iqm": 341.111, "low": 16.663, "high": 529.86, "jitter": 37.587}, "bandwidth": 113750000}, "download": {"bytes": 230789788, "elapsed": 14301, "latency": {"iqm": 104.125, "low": 23.72, "high": 269.563, "jitter": 13.447}, "bandwidth": 115625000}, "interface": {"name": "eth0", "isVpn": false, "macAddr": "00:00:00:00:00:00", "externalIp": "127.0.0.1", "internalIp": "127.0.0.1"}, "timestamp": "2024-03-01T01:00:00Z"}', true);
135+
136+
Result::factory()->create([
137+
'ping' => $dataWithoutPacketLoss['ping']['latency'],
138+
'download' => $dataWithoutPacketLoss['download']['bandwidth'],
139+
'upload' => $dataWithoutPacketLoss['upload']['bandwidth'],
140+
'data' => $dataWithoutPacketLoss,
141+
]);
142+
143+
$response = $this->get('/prometheus');
144+
145+
$response->assertSuccessful();
146+
$response->assertHeader('Content-Type', 'text/plain; version=0.0.4; charset=utf-8');
147+
// Verify packet_loss metric is not in the output when data is missing
148+
expect($response->getContent())->not->toContain('speedtest_tracker_packet_loss_percent');
149+
});
150+
151+
test('handles failed speedtests by only exporting info metric', function () {
152+
app(DataIntegrationSettings::class)->fill([
153+
'prometheus_enabled' => true,
154+
'prometheus_allowed_ips' => [],
155+
])->save();
156+
157+
// Create a failed result
158+
$failedData = json_decode('{"type": "log", "level": "error", "message": "Connection timeout", "timestamp": "2024-03-01T01:00:00Z"}', true);
159+
160+
$result = Result::factory()->create([
161+
'status' => \App\Enums\ResultStatus::Failed,
162+
'data' => $failedData,
163+
]);
164+
165+
// Cache the result ID so the Prometheus service can find it
166+
Cache::forever('prometheus:latest_result', $result->id);
167+
168+
$response = $this->get('/prometheus');
169+
170+
$response->assertSuccessful();
171+
$response->assertHeader('Content-Type', 'text/plain; version=0.0.4; charset=utf-8');
172+
173+
$content = $response->getContent();
174+
175+
// Should have the info metric (result_id)
176+
expect($content)->toContain('speedtest_tracker_result_id');
177+
178+
// Should NOT have numeric metrics for failed tests
179+
expect($content)->not->toContain('speedtest_tracker_download_bytes');
180+
expect($content)->not->toContain('speedtest_tracker_upload_bytes');
181+
expect($content)->not->toContain('speedtest_tracker_ping_ms');
182+
expect($content)->not->toContain('speedtest_tracker_packet_loss_percent');
183+
});
126184
});

0 commit comments

Comments
 (0)