91 lines
3.3 KiB
PHP
91 lines
3.3 KiB
PHP
<?php
|
|
|
|
namespace App\Services\AiEditing;
|
|
|
|
use App\Models\AiEditRequest;
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
class AiObservabilityService
|
|
{
|
|
public function recordTerminalOutcome(
|
|
AiEditRequest $request,
|
|
string $status,
|
|
?int $durationMs = null,
|
|
bool $moderationBlocked = false,
|
|
string $stage = 'job'
|
|
): void {
|
|
$bucket = now()->format('YmdH');
|
|
$prefix = sprintf('ai-editing:obs:tenant:%d:event:%d:hour:%s', $request->tenant_id, $request->event_id, $bucket);
|
|
|
|
Cache::add($prefix.':total', 0, now()->addHours(8));
|
|
$total = (int) Cache::increment($prefix.':total');
|
|
|
|
if ($status === AiEditRequest::STATUS_SUCCEEDED) {
|
|
Cache::add($prefix.':succeeded', 0, now()->addHours(8));
|
|
Cache::increment($prefix.':succeeded');
|
|
} elseif ($status === AiEditRequest::STATUS_BLOCKED) {
|
|
Cache::add($prefix.':blocked', 0, now()->addHours(8));
|
|
Cache::increment($prefix.':blocked');
|
|
} elseif ($status === AiEditRequest::STATUS_FAILED) {
|
|
Cache::add($prefix.':failed', 0, now()->addHours(8));
|
|
Cache::increment($prefix.':failed');
|
|
}
|
|
|
|
if ($moderationBlocked) {
|
|
Cache::add($prefix.':moderation_blocked', 0, now()->addHours(8));
|
|
Cache::increment($prefix.':moderation_blocked');
|
|
}
|
|
|
|
if (is_int($durationMs) && $durationMs > 0) {
|
|
Cache::add($prefix.':duration_total_ms', 0, now()->addHours(8));
|
|
Cache::increment($prefix.':duration_total_ms', $durationMs);
|
|
|
|
$latencyWarningMs = max(500, (int) config('ai-editing.observability.latency_warning_ms', 15000));
|
|
if ($durationMs >= $latencyWarningMs) {
|
|
Log::warning('AI provider latency warning', [
|
|
'tenant_id' => $request->tenant_id,
|
|
'event_id' => $request->event_id,
|
|
'request_id' => $request->id,
|
|
'duration_ms' => $durationMs,
|
|
'threshold_ms' => $latencyWarningMs,
|
|
'stage' => $stage,
|
|
]);
|
|
}
|
|
}
|
|
|
|
$this->checkFailureRateAlert($request, $prefix, $total, $stage);
|
|
}
|
|
|
|
private function checkFailureRateAlert(AiEditRequest $request, string $prefix, int $total, string $stage): void
|
|
{
|
|
$minSamples = max(1, (int) config('ai-editing.observability.failure_rate_min_samples', 10));
|
|
if ($total < $minSamples) {
|
|
return;
|
|
}
|
|
|
|
$failed = (int) (Cache::get($prefix.':failed', 0) ?: 0);
|
|
$threshold = (float) config('ai-editing.observability.failure_rate_alert_threshold', 0.35);
|
|
$failureRate = $total > 0 ? ($failed / $total) : 0.0;
|
|
|
|
if ($failureRate < $threshold) {
|
|
return;
|
|
}
|
|
|
|
$cooldownKey = $prefix.':failure_rate_alert';
|
|
if (! Cache::add($cooldownKey, 1, now()->addMinutes(30))) {
|
|
return;
|
|
}
|
|
|
|
Log::warning('AI failure-rate alert threshold reached', [
|
|
'tenant_id' => $request->tenant_id,
|
|
'event_id' => $request->event_id,
|
|
'failure_rate' => round($failureRate, 5),
|
|
'failed' => $failed,
|
|
'total' => $total,
|
|
'threshold' => $threshold,
|
|
'stage' => $stage,
|
|
]);
|
|
}
|
|
}
|