Skip to content

Commit e30ee96

Browse files
Merge pull request #377 from JLG-WOCFR-DEV/codex/continue-developpement-prioritaire-3x42eu
Add remote request telemetry instrumentation
2 parents 36671ba + d0f2f0f commit e30ee96

File tree

5 files changed

+299
-5
lines changed

5 files changed

+299
-5
lines changed

docs/ameliorations-et-tests.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
- **`blc_update_image_scan_status`** – La structure de statut n'expose ni `job_id` ni journal des transitions et ne notifie aucun hook dédié lors d'un changement d'état, ce qui complique l'audit et l'alignement avec les processus ITIL utilisés par les suites professionnelles. Harmoniser cette fonction avec `blc_update_link_scan_status()` (machine à états, journalisation) permettrait de tracer finement les scans d'images. 【F:liens-morts-detector-jlg/includes/blc-scanner.php†L867-L934】【F:liens-morts-detector-jlg/includes/blc-scanner.php†L557-L666】
88
- **`blc_update_image_scan_status`** – La structure de statut n'expose ni `job_id` ni journal des transitions et ne notifie aucun hook dédié lors d'un changement d'état, ce qui complique l'audit et l'alignement avec les processus ITIL utilisés par les suites professionnelles. Harmoniser cette fonction avec `blc_update_link_scan_status()` (machine à états, journalisation) permettrait de tracer finement les scans d'images. 【F:liens-morts-detector-jlg/includes/blc-scanner.php†L867-L934】【F:liens-morts-detector-jlg/includes/blc-scanner.php†L557-L666】
99
- **`blc_schedule_manual_image_scan`** – Contrairement au parcours des liens, la planification d'images ne retournait aucun identifiant de job ni tentative associée, empêchant de tracer les relances et de synchroniser l'interface avec le scan lancé. Une instrumentation équivalente est attendue côté solutions pro. 【F:liens-morts-detector-jlg/includes/blc-admin-pages.php†L1280-L1358】
10-
- **`JLG\BrokenLinks\Scanner\RemoteRequestClient`** – Le client gère désormais la rotation des User-Agent, le backoff exponentiel et le respect de `Retry-After`, mais reste mono-endpoint : aucun support de pool de proxys, de répartition géographique ou de métriques exportables n'est prévu. Les solutions pro offrent souvent une abstraction réseau capable de basculer entre plusieurs sorties et de publier des temps de réponse détaillés. 【F:liens-morts-detector-jlg/includes/Scanner/RemoteRequestClient.php†L17-L168】
10+
- **`JLG\BrokenLinks\Scanner\RemoteRequestClient`** – Le client gère désormais la rotation des User-Agent, le backoff exponentiel et le respect de `Retry-After`, mais reste mono-endpoint : aucun support de pool de proxys, de répartition géographique ou de métriques exportables n'est prévu. Les solutions pro offrent souvent une abstraction réseau capable de basculer entre plusieurs sorties et de publier des temps de réponse détaillés. 【F:liens-morts-detector-jlg/includes/Scanner/RemoteRequestClient.php†L17-L168】 ✅ Les tentatives sont maintenant journalisées via des métriques structurées (`blc_remote_request_metrics`) persistées dans l'historique. 【F:liens-morts-detector-jlg/includes/Scanner/RemoteRequestClient.php†L17-L356】【F:liens-morts-detector-jlg/includes/blc-scanner.php†L720-L768】
1111
- **`blc_schedule_automated_report_generation`** – La vérification d'un événement déjà planifié repose sur `wp_next_scheduled()` avec le contexte complet, mais `blc_normalize_report_context()` y injecte un `completed_at` recalculé à chaque appel. Les arguments diffèrent donc entre deux déclenchements successifs et la déduplication échoue, ce qui multiplie les jobs concurrents et peut saturer le stockage des exports. Stabiliser les métadonnées utilisées pour la comparaison (ou introduire un identifiant persistant) est nécessaire pour garantir l'idempotence. ✅ La normalisation accepte désormais un drapeau `stabilize_completed_at` qui réutilise `ended_at` ou `0` pour éviter les doublons lors de la planification. 【F:liens-morts-detector-jlg/includes/blc-reporting.php†L88-L166】
1212
## Tests de débogage ajoutés
1313

@@ -26,6 +26,7 @@
2626
> `blc_update_image_scan_status` conserve maintenant un journal d'audit (transitions valides/invalides), persiste l'historique des jobs et propage `job_id`, `attempt` et `scheduled_at` pour l'observabilité, offrant un suivi équivalent à celui des scans de liens. 【F:liens-morts-detector-jlg/includes/blc-scanner.php†L1109-L1285】【F:tests/LinkScanStatusTest.php†L294-L372】
2727
> `blc_schedule_manual_image_scan` génère un identifiant de job déterministe, retente la planification en cas d'échec, journalise le contexte et renvoie les métadonnées nécessaires à l'interface et aux appels AJAX. 【F:liens-morts-detector-jlg/includes/blc-admin-pages.php†L1280-L1366】【F:liens-morts-detector-jlg/includes/blc-admin-pages.php†L3698-L3771】
2828
> `blc_perform_image_check` journalise désormais la durée, l'état et la progression du lot traité, enregistre un historique `blc_image_scan_metrics_history` persistant et expose le hook `blc_image_scan_metrics` pour la télémétrie externe. 【F:liens-morts-detector-jlg/includes/blc-scanner.php†L742-L779】【F:liens-morts-detector-jlg/includes/blc-scanner.php†L4668-L4744】
29+
> `JLG\BrokenLinks\Scanner\RemoteRequestClient` publie des métriques de requêtes HTTP (durée, code de réponse, retries) via `blc_remote_request_metrics` et conserve les 100 derniers échantillons pour l'observabilité. 【F:liens-morts-detector-jlg/includes/Scanner/RemoteRequestClient.php†L216-L356】【F:liens-morts-detector-jlg/includes/blc-scanner.php†L720-L768】
2930
3031
## Tests manuels recommandés
3132

liens-morts-detector-jlg/includes/Scanner/RemoteRequestClient.php

Lines changed: 160 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -94,14 +94,30 @@ protected function requestWithRetries($method, $url, array $args)
9494
$this->enforceRateLimit();
9595

9696
$requestArgs = $this->prepareRequestArguments($args, $attempt);
97+
$requestStartedAt = microtime(true);
9798
$lastResponse = $this->dispatchRequest($method, $url, $requestArgs);
99+
$durationMs = (int) round((microtime(true) - $requestStartedAt) * 1000);
98100

99-
if (!$this->shouldRetry($lastResponse, $attempt, $attempts)) {
101+
$retryAfter = $this->getRetryAfterDelay($lastResponse);
102+
$willRetry = $this->shouldRetry($lastResponse, $attempt, $attempts);
103+
104+
$this->recordRequestMetrics(
105+
$method,
106+
$url,
107+
$requestArgs,
108+
$attempt,
109+
$attempts,
110+
$durationMs,
111+
$lastResponse,
112+
$willRetry,
113+
$retryAfter
114+
);
115+
116+
if (!$willRetry) {
100117
return $lastResponse;
101118
}
102119

103120
$delay = min($maxDelayMs, $delayMs * (2 ** ($attempt - 1)));
104-
$retryAfter = $this->getRetryAfterDelay($lastResponse);
105121
if ($retryAfter !== null) {
106122
$delay = max($delay, $retryAfter);
107123
}
@@ -279,7 +295,7 @@ private function getRetryAfterDelay($response)
279295
}
280296

281297
$header = \wp_remote_retrieve_header($response, 'retry-after');
282-
if ($header === '') {
298+
if (!is_string($header) || $header === '') {
283299
return null;
284300
}
285301

@@ -315,6 +331,147 @@ private function pickUserAgent($attempt)
315331
return $this->userAgents[$index];
316332
}
317333

334+
/**
335+
* Build and dispatch request metrics for observability.
336+
*
337+
* @param string $method
338+
* @param string $url
339+
* @param array<string, mixed> $args
340+
* @param int $attempt
341+
* @param int $maxAttempts
342+
* @param int $durationMs
343+
* @param array|WP_Error $response
344+
* @param bool $willRetry
345+
* @param int|null $retryAfterMs
346+
*
347+
* @return void
348+
*/
349+
private function recordRequestMetrics(
350+
$method,
351+
$url,
352+
array $args,
353+
$attempt,
354+
$maxAttempts,
355+
$durationMs,
356+
$response,
357+
$willRetry,
358+
$retryAfterMs
359+
) {
360+
$metrics = $this->createRequestMetricsPayload(
361+
$method,
362+
$url,
363+
$args,
364+
$attempt,
365+
$maxAttempts,
366+
$durationMs,
367+
$response,
368+
$willRetry,
369+
$retryAfterMs
370+
);
371+
372+
if (function_exists('\\blc_record_remote_request_metrics')) {
373+
\blc_record_remote_request_metrics($metrics);
374+
}
375+
376+
if (function_exists('\\do_action')) {
377+
\do_action('blc_remote_request_metrics', $metrics, $response, $args);
378+
}
379+
}
380+
381+
/**
382+
* Assemble the metrics payload describing the request attempt.
383+
*
384+
* @param string $method
385+
* @param string $url
386+
* @param array<string, mixed> $args
387+
* @param int $attempt
388+
* @param int $maxAttempts
389+
* @param int $durationMs
390+
* @param array|WP_Error $response
391+
* @param bool $willRetry
392+
* @param int|null $retryAfterMs
393+
*
394+
* @return array<string, mixed>
395+
*/
396+
private function createRequestMetricsPayload(
397+
$method,
398+
$url,
399+
array $args,
400+
$attempt,
401+
$maxAttempts,
402+
$durationMs,
403+
$response,
404+
$willRetry,
405+
$retryAfterMs
406+
) {
407+
$parsedUrl = function_exists('wp_parse_url') ? \wp_parse_url($url) : parse_url($url);
408+
409+
$host = '';
410+
if (is_array($parsedUrl) && isset($parsedUrl['host'])) {
411+
$host = (string) $parsedUrl['host'];
412+
if (function_exists('blc_normalize_remote_host')) {
413+
$normalizedHost = \blc_normalize_remote_host($host);
414+
if ($normalizedHost !== '') {
415+
$host = $normalizedHost;
416+
}
417+
} else {
418+
$host = strtolower($host);
419+
}
420+
}
421+
422+
$path = '/';
423+
if (is_array($parsedUrl) && isset($parsedUrl['path'])) {
424+
$candidatePath = (string) $parsedUrl['path'];
425+
if ($candidatePath !== '') {
426+
$path = $candidatePath;
427+
}
428+
}
429+
430+
$methodLabel = strtoupper((string) $method);
431+
$timestamp = time();
432+
$responseCode = 0;
433+
$success = false;
434+
435+
$errorCode = '';
436+
$errorMessage = '';
437+
438+
if ($response instanceof WP_Error) {
439+
$errorCode = (string) $response->get_error_code();
440+
$errorMessage = $response->get_error_message();
441+
} else {
442+
$responseCode = (int) \wp_remote_retrieve_response_code($response);
443+
if (!$willRetry && $responseCode >= 200 && $responseCode < 400) {
444+
$success = true;
445+
}
446+
}
447+
448+
$metrics = [
449+
'method' => $methodLabel,
450+
'url' => (string) $url,
451+
'host' => $host,
452+
'path' => $path,
453+
'attempt' => (int) $attempt,
454+
'max_attempts' => (int) $maxAttempts,
455+
'duration_ms' => max(0, (int) $durationMs),
456+
'timestamp' => (int) $timestamp,
457+
'response_code' => $responseCode,
458+
'success' => $success,
459+
'will_retry' => (bool) $willRetry,
460+
'retry_after_ms' => ($retryAfterMs !== null) ? max(0, (int) $retryAfterMs) : null,
461+
];
462+
463+
if ($errorCode !== '' || $errorMessage !== '') {
464+
$metrics['error_code'] = $errorCode;
465+
$metrics['error_message'] = $errorMessage;
466+
}
467+
468+
if (isset($args['user-agent']) && is_string($args['user-agent'])) {
469+
$metrics['user_agent'] = trim($args['user-agent']);
470+
}
471+
472+
return $metrics;
473+
}
474+
318475
/**
319476
* Provide a default pool of user agent strings inspired by common browsers.
320477
*

liens-morts-detector-jlg/includes/blc-scanner.php

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -766,6 +766,39 @@ function blc_record_image_scan_metrics(array $metrics) {
766766
}
767767
}
768768

769+
if (!function_exists('blc_get_remote_request_metrics_history')) {
770+
/**
771+
* Retrieve the history of remote request metrics collected during scans.
772+
*
773+
* @return array<int, array<string, mixed>>
774+
*/
775+
function blc_get_remote_request_metrics_history() {
776+
$history = get_option('blc_remote_request_metrics_history', []);
777+
778+
return is_array($history) ? $history : [];
779+
}
780+
}
781+
782+
if (!function_exists('blc_record_remote_request_metrics')) {
783+
/**
784+
* Persist telemetry about a remote HTTP request performed during scanning.
785+
*
786+
* @param array<string, mixed> $metrics
787+
*
788+
* @return void
789+
*/
790+
function blc_record_remote_request_metrics(array $metrics) {
791+
$history = blc_get_remote_request_metrics_history();
792+
793+
array_unshift($history, $metrics);
794+
if (count($history) > 100) {
795+
$history = array_slice($history, 0, 100);
796+
}
797+
798+
update_option('blc_remote_request_metrics_history', $history, false);
799+
}
800+
}
801+
769802
if (!function_exists('blc_get_image_scan_metrics_history')) {
770803
/**
771804
* Retrieve the persisted metrics history for image scans.

patchwork.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
"dns_get_record",
66
"gethostbynamel",
77
"error_log",
8-
"file_exists"
8+
"file_exists",
9+
"microtime",
10+
"usleep"
911
]
1012
}

tests/Scanner/RemoteRequestClientTest.php

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,4 +102,105 @@ public function test_request_replaces_empty_user_agent_with_pool_value(): void
102102
$this->assertSame('Pool-Agent/2.0', $capturedArgs['headers']['USER-AGENT']);
103103
$this->assertSame('Pool-Agent/2.0', $capturedArgs['user-agent']);
104104
}
105+
106+
public function test_request_records_metrics_and_triggers_hook(): void
107+
{
108+
$metricsLog = [];
109+
$hookLog = [];
110+
111+
Functions\when('blc_record_remote_request_metrics')->alias(function ($metrics) use (&$metricsLog) {
112+
$metricsLog[] = $metrics;
113+
});
114+
115+
Functions\when('do_action')->alias(function ($hook, ...$args) use (&$hookLog) {
116+
if ($hook === 'blc_remote_request_metrics') {
117+
$hookLog[] = $args;
118+
}
119+
});
120+
121+
Functions\when('time')->justReturn(1700000000);
122+
123+
$microtimeSequence = [1000.0, 1000.05, 1000.1, 1000.15, 1000.2, 1000.25, 1000.3, 1000.35];
124+
Functions\when('microtime')->alias(function ($as_float = false) use (&$microtimeSequence) {
125+
$value = array_shift($microtimeSequence);
126+
if ($value === null) {
127+
$value = 1000.4;
128+
}
129+
130+
if ($as_float) {
131+
return $value;
132+
}
133+
134+
$integer = (int) $value;
135+
$fraction = $value - $integer;
136+
137+
return sprintf('%0.8f %d', $fraction, $integer);
138+
});
139+
140+
Functions\when('usleep')->alias(static function ($milliseconds) {
141+
return true;
142+
});
143+
144+
$responses = [
145+
['response' => ['code' => 429], 'headers' => ['retry-after' => '3']],
146+
['response' => ['code' => 200], 'headers' => []],
147+
];
148+
149+
Functions\when('wp_safe_remote_get')->alias(static function ($url, $args) use (&$responses) {
150+
return array_shift($responses);
151+
});
152+
153+
Functions\when('wp_remote_retrieve_response_code')->alias(static function ($response) {
154+
return isset($response['response']['code']) ? (int) $response['response']['code'] : 0;
155+
});
156+
157+
Functions\when('wp_remote_retrieve_header')->alias(static function ($response, $header) {
158+
$header = strtolower((string) $header);
159+
if (isset($response['headers'][$header])) {
160+
return $response['headers'][$header];
161+
}
162+
163+
return '';
164+
});
165+
166+
$client = new RemoteRequestClient([], [
167+
'max_attempts' => 2,
168+
'initial_delay_ms' => 10,
169+
'max_delay_ms' => 20,
170+
], []);
171+
172+
$result = $client->get('https://example.com/resource', []);
173+
174+
$this->assertSame(['response' => ['code' => 200], 'headers' => []], $result);
175+
$this->assertCount(2, $metricsLog);
176+
$this->assertCount(2, $hookLog);
177+
178+
$first = $metricsLog[0];
179+
$this->assertSame('GET', $first['method']);
180+
$this->assertSame('https://example.com/resource', $first['url']);
181+
$this->assertSame('example.com', $first['host']);
182+
$this->assertSame('/resource', $first['path']);
183+
$this->assertSame(1, $first['attempt']);
184+
$this->assertSame(2, $first['max_attempts']);
185+
$this->assertSame(429, $first['response_code']);
186+
$this->assertFalse($first['success']);
187+
$this->assertTrue($first['will_retry']);
188+
$this->assertSame(3000, $first['retry_after_ms']);
189+
$this->assertSame(1700000000, $first['timestamp']);
190+
$this->assertSame(
191+
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0 Safari/537.36',
192+
$first['user_agent']
193+
);
194+
195+
$second = $metricsLog[1];
196+
$this->assertSame(2, $second['attempt']);
197+
$this->assertSame(2, $second['max_attempts']);
198+
$this->assertSame(200, $second['response_code']);
199+
$this->assertTrue($second['success']);
200+
$this->assertFalse($second['will_retry']);
201+
$this->assertNull($second['retry_after_ms']);
202+
203+
$this->assertSame($metricsLog[0], $hookLog[0][0]);
204+
$this->assertSame($metricsLog[1], $hookLog[1][0]);
205+
}
105206
}

0 commit comments

Comments
 (0)