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
|
client keepalive, and upstream keepalive to the Traefik backend. CSS and
|
||||||
JavaScript keep the edge's longer immutable asset policy.
|
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
|
## Validation
|
||||||
|
|
||||||
Do not build images locally. The image is built and deployed by the Debian
|
Do not build images locally. The image is built and deployed by the Debian
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
$requestStartedAt = microtime(true);
|
||||||
|
|
||||||
function translate_response(int $status, array $body): never {
|
function translate_response(int $status, array $body): never {
|
||||||
http_response_code($status);
|
http_response_code($status);
|
||||||
echo json_encode($body, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
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;
|
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 {
|
function redis_encode_command(array $parts): string {
|
||||||
$command = '*' . count($parts) . "\r\n";
|
$command = '*' . count($parts) . "\r\n";
|
||||||
foreach ($parts as $part) {
|
foreach ($parts as $part) {
|
||||||
|
|
@ -215,14 +233,32 @@ foreach ($texts as $index => $text) {
|
||||||
$missTexts[] = $text;
|
$missTexts[] = $text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$cacheHits = count($texts) - count($missTexts);
|
||||||
|
$cacheMisses = count($missTexts);
|
||||||
|
$failedJsonParseCount = 0;
|
||||||
|
$ollamaTimeoutCount = 0;
|
||||||
|
$ollamaLatencyMs = null;
|
||||||
|
|
||||||
if (!$missTexts) {
|
if (!$missTexts) {
|
||||||
ksort($cleanTranslations);
|
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([
|
echo json_encode([
|
||||||
'translations' => array_values($cleanTranslations),
|
'translations' => array_values($cleanTranslations),
|
||||||
'model' => $ollamaModel,
|
'model' => $ollamaModel,
|
||||||
'cache' => [
|
'cache' => [
|
||||||
'hits' => count($texts),
|
'hits' => $cacheHits,
|
||||||
'misses' => 0,
|
'misses' => $cacheMisses,
|
||||||
],
|
],
|
||||||
], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
||||||
exit;
|
exit;
|
||||||
|
|
@ -254,28 +290,95 @@ curl_setopt_array($curl, [
|
||||||
CURLOPT_TIMEOUT => 130,
|
CURLOPT_TIMEOUT => 130,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$ollamaStartedAt = microtime(true);
|
||||||
$rawResponse = curl_exec($curl);
|
$rawResponse = curl_exec($curl);
|
||||||
$curlError = curl_error($curl);
|
$curlError = curl_error($curl);
|
||||||
|
$curlErrno = curl_errno($curl);
|
||||||
$statusCode = (int) curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
|
$statusCode = (int) curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
|
||||||
|
$ollamaLatencyMs = elapsed_ms($ollamaStartedAt);
|
||||||
curl_close($curl);
|
curl_close($curl);
|
||||||
|
|
||||||
if ($rawResponse === false || $statusCode < 200 || $statusCode >= 300) {
|
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]);
|
translate_response(502, ['error' => 'Ollama request failed', 'status' => $statusCode, 'detail' => $curlError]);
|
||||||
}
|
}
|
||||||
|
|
||||||
$ollamaResponse = json_decode($rawResponse, true);
|
$ollamaResponse = json_decode($rawResponse, true);
|
||||||
if (!is_array($ollamaResponse) || !isset($ollamaResponse['response']) || !is_string($ollamaResponse['response'])) {
|
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']);
|
translate_response(502, ['error' => 'Invalid Ollama response']);
|
||||||
}
|
}
|
||||||
|
|
||||||
$translationJson = trim(str_replace(['```json', '```'], '', $ollamaResponse['response']));
|
$translationJson = trim(str_replace(['```json', '```'], '', $ollamaResponse['response']));
|
||||||
$translations = json_decode($translationJson, true);
|
$translations = json_decode($translationJson, true);
|
||||||
if (!is_array($translations) || count($translations) !== count($missTexts)) {
|
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']);
|
translate_response(502, ['error' => 'Unexpected translation response']);
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($translations as $index => $translation) {
|
foreach ($translations as $index => $translation) {
|
||||||
if (!is_string($translation) && !is_numeric($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']);
|
translate_response(502, ['error' => 'Unexpected translation item']);
|
||||||
}
|
}
|
||||||
$originalPosition = $missPositions[$index];
|
$originalPosition = $missPositions[$index];
|
||||||
|
|
@ -289,11 +392,24 @@ foreach ($missPositions as $position) {
|
||||||
translation_cache_write($cacheWrites);
|
translation_cache_write($cacheWrites);
|
||||||
|
|
||||||
ksort($cleanTranslations);
|
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([
|
echo json_encode([
|
||||||
'translations' => array_values($cleanTranslations),
|
'translations' => array_values($cleanTranslations),
|
||||||
'model' => $ollamaModel,
|
'model' => $ollamaModel,
|
||||||
'cache' => [
|
'cache' => [
|
||||||
'hits' => count($texts) - count($missTexts),
|
'hits' => $cacheHits,
|
||||||
'misses' => count($missTexts),
|
'misses' => $cacheMisses,
|
||||||
],
|
],
|
||||||
], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue