90 lines
3.1 KiB
PHP
90 lines
3.1 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace app\adminapi\service;
|
|
|
|
use think\facade\Log;
|
|
|
|
/** A request-local, patient-free timing record for the diagnosis assistant SSE path. */
|
|
final class AssistantSseTiming
|
|
{
|
|
private const PHASES = ['prepare', 'sse_start', 'first_delta', 'upstream_done', 'sse_done'];
|
|
|
|
private const NUMBER_FIELDS = [
|
|
'duration_ms', 'aggregate_ms', 'compaction_ms', 'retry_ms',
|
|
'source_bytes', 'query_bytes', 'attachment_count',
|
|
'delta_bytes', 'delta_count', 'first_delta_ms',
|
|
];
|
|
|
|
private const BOOLEAN_FIELDS = ['compaction_attempted', 'retry_attempted', 'first_delta_observed'];
|
|
|
|
private const ERROR_CODES = [
|
|
'AI_ASSISTANT_FAILED', 'UPSTREAM_UNAVAILABLE', 'UPSTREAM_REJECTED',
|
|
'INCOMPLETE_RESPONSE', 'EMPTY_RESPONSE', 'UPSTREAM_TIMEOUT',
|
|
'CONFIG_DISABLED', 'INVALID_PROFILE', 'CONFIG_MISSING', 'CONFIG_INVALID',
|
|
'CURL_UNAVAILABLE', 'SKILL_UNAVAILABLE', 'CLIENT_DISCONNECTED',
|
|
'STREAM_DELIVERY_FAILED', 'REQUEST_BUILD_FAILED', 'CURL_INIT_FAILED',
|
|
'UPSTREAM_BUSY', 'INVALID_RESPONSE',
|
|
];
|
|
|
|
private string $traceId;
|
|
|
|
public function __construct()
|
|
{
|
|
$this->traceId = bin2hex(random_bytes(16));
|
|
}
|
|
|
|
public function id(): string
|
|
{
|
|
return $this->traceId;
|
|
}
|
|
|
|
public static function elapsedMs(int $startedAt): int
|
|
{
|
|
return max(0, (int) floor((hrtime(true) - $startedAt) / 1000000));
|
|
}
|
|
|
|
/**
|
|
* Only a fixed set of numeric and boolean metadata reaches the log. In particular,
|
|
* arbitrary prompt text, patient identifiers, URLs and credentials are discarded.
|
|
*
|
|
* @param array<string,mixed> $metrics
|
|
* @return array<string,int|bool|string>
|
|
*/
|
|
public function event(string $phase, array $metrics = []): array
|
|
{
|
|
$event = [
|
|
'trace_id' => $this->traceId,
|
|
'phase' => in_array($phase, self::PHASES, true) ? $phase : 'upstream_done',
|
|
];
|
|
foreach (self::NUMBER_FIELDS as $field) {
|
|
if (isset($metrics[$field]) && is_int($metrics[$field])) {
|
|
$event[$field] = max(0, $metrics[$field]);
|
|
}
|
|
}
|
|
foreach (self::BOOLEAN_FIELDS as $field) {
|
|
if (isset($metrics[$field]) && is_bool($metrics[$field])) {
|
|
$event[$field] = $metrics[$field];
|
|
}
|
|
}
|
|
if (isset($metrics['status']) && in_array($metrics['status'], ['ok', 'error', 'exception', 'disconnected'], true)) {
|
|
$event['status'] = $metrics['status'];
|
|
}
|
|
if (isset($metrics['error_code']) && in_array($metrics['error_code'], self::ERROR_CODES, true)) {
|
|
$event['error_code'] = $metrics['error_code'];
|
|
}
|
|
return $event;
|
|
}
|
|
|
|
/** @param array<string,mixed> $metrics */
|
|
public function log(string $phase, array $metrics = []): void
|
|
{
|
|
try {
|
|
Log::info('diagnosis ai assistant timing ' . json_encode($this->event($phase, $metrics)));
|
|
} catch (\Throwable $ignored) {
|
|
// Telemetry must never interrupt the clinical request.
|
|
}
|
|
}
|
|
}
|