Files
fotospiel-app/app/Services/AiEditing/AiObservabilityService.php
Codex Agent 1d2242fb4d
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled
tests / ui (push) Has been cancelled
feat(ai): finalize AI magic edits epic rollout and operations
2026-02-06 22:41:51 +01:00

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,
]);
}
}