Add translation observability logs
This commit is contained in:
parent
35c6c6f654
commit
ab68569dbe
|
|
@ -55,6 +55,25 @@ The OCI edge nginx layer handles gzip, cache HITs, HTTP/2, TLS session reuse,
|
|||
client keepalive, and upstream keepalive to the Traefik backend. CSS and
|
||||
JavaScript keep the edge's longer immutable asset policy.
|
||||
|
||||
## Translation Observability
|
||||
|
||||
`translate.php` emits one structured JSON log event for each valid translation
|
||||
request. The event name is `website_translation` and the payload does not include
|
||||
source text or translated text.
|
||||
|
||||
Useful fields:
|
||||
|
||||
- `cache_hits`
|
||||
- `cache_misses`
|
||||
- `ollama_latency_ms`
|
||||
- `failed_json_parse_count`
|
||||
- `timeout_count`
|
||||
- `status`
|
||||
- `reason`
|
||||
|
||||
In Loki, filter website logs for `"event":"website_translation"` to inspect
|
||||
cache behavior, Ollama latency, JSON parse failures, and timeout failures.
|
||||
|
||||
## Validation
|
||||
|
||||
Do not build images locally. The image is built and deployed by the Debian
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$requestStartedAt = microtime(true);
|
||||
|
||||
function translate_response(int $status, array $body): never {
|
||||
http_response_code($status);
|
||||
echo json_encode($body, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
||||
|
|
@ -18,6 +20,22 @@ function clean_translate_text(string $value, int $maxLength = 4000): string {
|
|||
return $value;
|
||||
}
|
||||
|
||||
function elapsed_ms(float $startedAt): int {
|
||||
return (int) round((microtime(true) - $startedAt) * 1000);
|
||||
}
|
||||
|
||||
function translation_log(array $fields): void {
|
||||
$payload = array_merge([
|
||||
'event' => 'website_translation',
|
||||
'timestamp' => gmdate('c'),
|
||||
], $fields);
|
||||
|
||||
$json = json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
||||
if ($json !== false) {
|
||||
error_log($json);
|
||||
}
|
||||
}
|
||||
|
||||
function redis_encode_command(array $parts): string {
|
||||
$command = '*' . count($parts) . "\r\n";
|
||||
foreach ($parts as $part) {
|
||||
|
|
@ -215,14 +233,32 @@ foreach ($texts as $index => $text) {
|
|||
$missTexts[] = $text;
|
||||
}
|
||||
|
||||
$cacheHits = count($texts) - count($missTexts);
|
||||
$cacheMisses = count($missTexts);
|
||||
$failedJsonParseCount = 0;
|
||||
$ollamaTimeoutCount = 0;
|
||||
$ollamaLatencyMs = null;
|
||||
|
||||
if (!$missTexts) {
|
||||
ksort($cleanTranslations);
|
||||
translation_log([
|
||||
'status' => 'success',
|
||||
'target_lang' => $targetLang,
|
||||
'model' => $ollamaModel,
|
||||
'text_count' => count($texts),
|
||||
'cache_hits' => $cacheHits,
|
||||
'cache_misses' => $cacheMisses,
|
||||
'ollama_latency_ms' => $ollamaLatencyMs,
|
||||
'failed_json_parse_count' => $failedJsonParseCount,
|
||||
'timeout_count' => $ollamaTimeoutCount,
|
||||
'duration_ms' => elapsed_ms($requestStartedAt),
|
||||
]);
|
||||
echo json_encode([
|
||||
'translations' => array_values($cleanTranslations),
|
||||
'model' => $ollamaModel,
|
||||
'cache' => [
|
||||
'hits' => count($texts),
|
||||
'misses' => 0,
|
||||
'hits' => $cacheHits,
|
||||
'misses' => $cacheMisses,
|
||||
],
|
||||
], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
|
|
@ -254,28 +290,95 @@ curl_setopt_array($curl, [
|
|||
CURLOPT_TIMEOUT => 130,
|
||||
]);
|
||||
|
||||
$ollamaStartedAt = microtime(true);
|
||||
$rawResponse = curl_exec($curl);
|
||||
$curlError = curl_error($curl);
|
||||
$curlErrno = curl_errno($curl);
|
||||
$statusCode = (int) curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
|
||||
$ollamaLatencyMs = elapsed_ms($ollamaStartedAt);
|
||||
curl_close($curl);
|
||||
|
||||
if ($rawResponse === false || $statusCode < 200 || $statusCode >= 300) {
|
||||
$ollamaTimeoutCount = $curlErrno === CURLE_OPERATION_TIMEDOUT ? 1 : 0;
|
||||
translation_log([
|
||||
'status' => 'error',
|
||||
'reason' => 'ollama_request_failed',
|
||||
'target_lang' => $targetLang,
|
||||
'model' => $ollamaModel,
|
||||
'text_count' => count($texts),
|
||||
'cache_hits' => $cacheHits,
|
||||
'cache_misses' => $cacheMisses,
|
||||
'ollama_latency_ms' => $ollamaLatencyMs,
|
||||
'failed_json_parse_count' => $failedJsonParseCount,
|
||||
'timeout_count' => $ollamaTimeoutCount,
|
||||
'ollama_status_code' => $statusCode,
|
||||
'curl_errno' => $curlErrno,
|
||||
'duration_ms' => elapsed_ms($requestStartedAt),
|
||||
]);
|
||||
translate_response(502, ['error' => 'Ollama request failed', 'status' => $statusCode, 'detail' => $curlError]);
|
||||
}
|
||||
|
||||
$ollamaResponse = json_decode($rawResponse, true);
|
||||
if (!is_array($ollamaResponse) || !isset($ollamaResponse['response']) || !is_string($ollamaResponse['response'])) {
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
$failedJsonParseCount++;
|
||||
}
|
||||
translation_log([
|
||||
'status' => 'error',
|
||||
'reason' => 'invalid_ollama_response',
|
||||
'target_lang' => $targetLang,
|
||||
'model' => $ollamaModel,
|
||||
'text_count' => count($texts),
|
||||
'cache_hits' => $cacheHits,
|
||||
'cache_misses' => $cacheMisses,
|
||||
'ollama_latency_ms' => $ollamaLatencyMs,
|
||||
'failed_json_parse_count' => $failedJsonParseCount,
|
||||
'timeout_count' => $ollamaTimeoutCount,
|
||||
'ollama_status_code' => $statusCode,
|
||||
'duration_ms' => elapsed_ms($requestStartedAt),
|
||||
]);
|
||||
translate_response(502, ['error' => 'Invalid Ollama response']);
|
||||
}
|
||||
|
||||
$translationJson = trim(str_replace(['```json', '```'], '', $ollamaResponse['response']));
|
||||
$translations = json_decode($translationJson, true);
|
||||
if (!is_array($translations) || count($translations) !== count($missTexts)) {
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
$failedJsonParseCount++;
|
||||
}
|
||||
translation_log([
|
||||
'status' => 'error',
|
||||
'reason' => 'unexpected_translation_response',
|
||||
'target_lang' => $targetLang,
|
||||
'model' => $ollamaModel,
|
||||
'text_count' => count($texts),
|
||||
'cache_hits' => $cacheHits,
|
||||
'cache_misses' => $cacheMisses,
|
||||
'ollama_latency_ms' => $ollamaLatencyMs,
|
||||
'failed_json_parse_count' => $failedJsonParseCount,
|
||||
'timeout_count' => $ollamaTimeoutCount,
|
||||
'ollama_status_code' => $statusCode,
|
||||
'duration_ms' => elapsed_ms($requestStartedAt),
|
||||
]);
|
||||
translate_response(502, ['error' => 'Unexpected translation response']);
|
||||
}
|
||||
|
||||
foreach ($translations as $index => $translation) {
|
||||
if (!is_string($translation) && !is_numeric($translation)) {
|
||||
translation_log([
|
||||
'status' => 'error',
|
||||
'reason' => 'unexpected_translation_item',
|
||||
'target_lang' => $targetLang,
|
||||
'model' => $ollamaModel,
|
||||
'text_count' => count($texts),
|
||||
'cache_hits' => $cacheHits,
|
||||
'cache_misses' => $cacheMisses,
|
||||
'ollama_latency_ms' => $ollamaLatencyMs,
|
||||
'failed_json_parse_count' => $failedJsonParseCount,
|
||||
'timeout_count' => $ollamaTimeoutCount,
|
||||
'ollama_status_code' => $statusCode,
|
||||
'duration_ms' => elapsed_ms($requestStartedAt),
|
||||
]);
|
||||
translate_response(502, ['error' => 'Unexpected translation item']);
|
||||
}
|
||||
$originalPosition = $missPositions[$index];
|
||||
|
|
@ -289,11 +392,24 @@ foreach ($missPositions as $position) {
|
|||
translation_cache_write($cacheWrites);
|
||||
|
||||
ksort($cleanTranslations);
|
||||
translation_log([
|
||||
'status' => 'success',
|
||||
'target_lang' => $targetLang,
|
||||
'model' => $ollamaModel,
|
||||
'text_count' => count($texts),
|
||||
'cache_hits' => $cacheHits,
|
||||
'cache_misses' => $cacheMisses,
|
||||
'ollama_latency_ms' => $ollamaLatencyMs,
|
||||
'failed_json_parse_count' => $failedJsonParseCount,
|
||||
'timeout_count' => $ollamaTimeoutCount,
|
||||
'ollama_status_code' => $statusCode,
|
||||
'duration_ms' => elapsed_ms($requestStartedAt),
|
||||
]);
|
||||
echo json_encode([
|
||||
'translations' => array_values($cleanTranslations),
|
||||
'model' => $ollamaModel,
|
||||
'cache' => [
|
||||
'hits' => count($texts) - count($missTexts),
|
||||
'misses' => count($missTexts),
|
||||
'hits' => $cacheHits,
|
||||
'misses' => $cacheMisses,
|
||||
],
|
||||
], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
||||
|
|
|
|||
Loading…
Reference in New Issue