Add translation observability logs

This commit is contained in:
juvdiaz 2026-06-10 10:41:46 -06:00
parent 35c6c6f654
commit ab68569dbe
2 changed files with 139 additions and 4 deletions

View File

@ -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

View File

@ -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);