267 lines
8.5 KiB
PHP
267 lines
8.5 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Core\Mod\Mcp\Services;
|
|
|
|
use Core\Mod\Mcp\Models\McpToolCall;
|
|
use Core\Mod\Mcp\Models\McpToolCallStat;
|
|
use Illuminate\Support\Carbon;
|
|
use Illuminate\Support\Collection;
|
|
|
|
/**
|
|
* MCP Metrics Service - dashboard metrics for MCP tool usage.
|
|
*
|
|
* Provides overview stats, trends, performance percentiles, and activity feeds.
|
|
*/
|
|
class McpMetricsService
|
|
{
|
|
/**
|
|
* Get overview metrics for the dashboard.
|
|
*/
|
|
public function getOverview(int $days = 7): array
|
|
{
|
|
$startDate = now()->subDays($days - 1)->startOfDay();
|
|
|
|
$stats = McpToolCallStat::forDateRange($startDate, now())->get();
|
|
|
|
$totalCalls = $stats->sum('call_count');
|
|
$successCalls = $stats->sum('success_count');
|
|
$errorCalls = $stats->sum('error_count');
|
|
|
|
$successRate = $totalCalls > 0
|
|
? round(($successCalls / $totalCalls) * 100, 1)
|
|
: 0;
|
|
|
|
$avgDuration = $totalCalls > 0
|
|
? round($stats->sum('total_duration_ms') / $totalCalls, 1)
|
|
: 0;
|
|
|
|
// Compare to previous period
|
|
$previousStart = $startDate->copy()->subDays($days);
|
|
$previousStats = McpToolCallStat::forDateRange($previousStart, $startDate->copy()->subDay())->get();
|
|
$previousCalls = $previousStats->sum('call_count');
|
|
|
|
$callsTrend = $previousCalls > 0
|
|
? round((($totalCalls - $previousCalls) / $previousCalls) * 100, 1)
|
|
: 0;
|
|
|
|
return [
|
|
'total_calls' => $totalCalls,
|
|
'success_calls' => $successCalls,
|
|
'error_calls' => $errorCalls,
|
|
'success_rate' => $successRate,
|
|
'avg_duration_ms' => $avgDuration,
|
|
'calls_trend_percent' => $callsTrend,
|
|
'unique_tools' => $stats->pluck('tool_name')->unique()->count(),
|
|
'unique_servers' => $stats->pluck('server_id')->unique()->count(),
|
|
'period_days' => $days,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Get daily call trend data for charting.
|
|
*/
|
|
public function getDailyTrend(int $days = 7): Collection
|
|
{
|
|
$trend = McpToolCallStat::getDailyTrend($days);
|
|
|
|
// Fill in missing dates with zeros
|
|
$dates = collect();
|
|
for ($i = $days - 1; $i >= 0; $i--) {
|
|
$date = now()->subDays($i)->toDateString();
|
|
$existing = $trend->firstWhere('date', $date);
|
|
|
|
$dates->push([
|
|
'date' => $date,
|
|
'date_formatted' => Carbon::parse($date)->format('M j'),
|
|
'total_calls' => $existing->total_calls ?? 0,
|
|
'total_success' => $existing->total_success ?? 0,
|
|
'total_errors' => $existing->total_errors ?? 0,
|
|
'success_rate' => $existing->success_rate ?? 0,
|
|
]);
|
|
}
|
|
|
|
return $dates;
|
|
}
|
|
|
|
/**
|
|
* Get top tools by call count.
|
|
*/
|
|
public function getTopTools(int $days = 7, int $limit = 10): Collection
|
|
{
|
|
return McpToolCallStat::getTopTools($days, $limit);
|
|
}
|
|
|
|
/**
|
|
* Get server breakdown.
|
|
*/
|
|
public function getServerStats(int $days = 7): Collection
|
|
{
|
|
return McpToolCallStat::getServerStats($days);
|
|
}
|
|
|
|
/**
|
|
* Get recent tool calls for activity feed.
|
|
*/
|
|
public function getRecentCalls(int $limit = 20): Collection
|
|
{
|
|
return McpToolCall::query()
|
|
->orderByDesc('created_at')
|
|
->limit($limit)
|
|
->get()
|
|
->map(function ($call) {
|
|
return [
|
|
'id' => $call->id,
|
|
'server_id' => $call->server_id,
|
|
'tool_name' => $call->tool_name,
|
|
'success' => $call->success,
|
|
'duration' => $call->getDurationForHumans(),
|
|
'duration_ms' => $call->duration_ms,
|
|
'error_message' => $call->error_message,
|
|
'session_id' => $call->session_id,
|
|
'plan_slug' => $call->plan_slug,
|
|
'created_at' => $call->created_at->diffForHumans(),
|
|
'created_at_full' => $call->created_at->toIso8601String(),
|
|
];
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get error breakdown.
|
|
*/
|
|
public function getErrorBreakdown(int $days = 7): Collection
|
|
{
|
|
return McpToolCall::query()
|
|
->select('tool_name', 'error_code')
|
|
->selectRaw('COUNT(*) as error_count')
|
|
->where('success', false)
|
|
->where('created_at', '>=', now()->subDays($days))
|
|
->groupBy('tool_name', 'error_code')
|
|
->orderByDesc('error_count')
|
|
->limit(20)
|
|
->get();
|
|
}
|
|
|
|
/**
|
|
* Get tool performance metrics (p50, p95, p99).
|
|
*/
|
|
public function getToolPerformance(int $days = 7, int $limit = 10): Collection
|
|
{
|
|
// Get raw call data for percentile calculations
|
|
$calls = McpToolCall::query()
|
|
->select('tool_name', 'duration_ms')
|
|
->whereNotNull('duration_ms')
|
|
->where('success', true)
|
|
->where('created_at', '>=', now()->subDays($days))
|
|
->get()
|
|
->groupBy('tool_name');
|
|
|
|
$performance = collect();
|
|
|
|
foreach ($calls as $toolName => $toolCalls) {
|
|
$durations = $toolCalls->pluck('duration_ms')->sort()->values();
|
|
$count = $durations->count();
|
|
|
|
if ($count === 0) {
|
|
continue;
|
|
}
|
|
|
|
$performance->push([
|
|
'tool_name' => $toolName,
|
|
'call_count' => $count,
|
|
'min_ms' => $durations->first(),
|
|
'max_ms' => $durations->last(),
|
|
'avg_ms' => round($durations->avg(), 1),
|
|
'p50_ms' => $this->percentile($durations, 50),
|
|
'p95_ms' => $this->percentile($durations, 95),
|
|
'p99_ms' => $this->percentile($durations, 99),
|
|
]);
|
|
}
|
|
|
|
return $performance
|
|
->sortByDesc('call_count')
|
|
->take($limit)
|
|
->values();
|
|
}
|
|
|
|
/**
|
|
* Get hourly distribution for the last 24 hours.
|
|
*/
|
|
public function getHourlyDistribution(): Collection
|
|
{
|
|
$hourly = McpToolCall::query()
|
|
->selectRaw('HOUR(created_at) as hour')
|
|
->selectRaw('COUNT(*) as call_count')
|
|
->selectRaw('SUM(CASE WHEN success = 1 THEN 1 ELSE 0 END) as success_count')
|
|
->where('created_at', '>=', now()->subHours(24))
|
|
->groupBy('hour')
|
|
->orderBy('hour')
|
|
->get()
|
|
->keyBy('hour');
|
|
|
|
// Fill in missing hours
|
|
$result = collect();
|
|
for ($i = 0; $i < 24; $i++) {
|
|
$hour = str_pad((string) $i, 2, '0', STR_PAD_LEFT);
|
|
$existing = $hourly->get($i);
|
|
|
|
$result->push([
|
|
'hour' => $hour,
|
|
'hour_formatted' => Carbon::createFromTime($i)->format('ga'),
|
|
'call_count' => $existing->call_count ?? 0,
|
|
'success_count' => $existing->success_count ?? 0,
|
|
]);
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Get plan activity - which plans are using MCP tools.
|
|
*/
|
|
public function getPlanActivity(int $days = 7, int $limit = 10): Collection
|
|
{
|
|
return McpToolCall::query()
|
|
->select('plan_slug')
|
|
->selectRaw('COUNT(*) as call_count')
|
|
->selectRaw('COUNT(DISTINCT tool_name) as unique_tools')
|
|
->selectRaw('SUM(CASE WHEN success = 1 THEN 1 ELSE 0 END) as success_count')
|
|
->whereNotNull('plan_slug')
|
|
->where('created_at', '>=', now()->subDays($days))
|
|
->groupBy('plan_slug')
|
|
->orderByDesc('call_count')
|
|
->limit($limit)
|
|
->get()
|
|
->map(function ($item) {
|
|
$item->success_rate = $item->call_count > 0
|
|
? round(($item->success_count / $item->call_count) * 100, 1)
|
|
: 0;
|
|
|
|
return $item;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Calculate percentile from a sorted collection.
|
|
*/
|
|
protected function percentile(Collection $sortedValues, int $percentile): float
|
|
{
|
|
$count = $sortedValues->count();
|
|
if ($count === 0) {
|
|
return 0;
|
|
}
|
|
|
|
$index = ($percentile / 100) * ($count - 1);
|
|
$lower = (int) floor($index);
|
|
$upper = (int) ceil($index);
|
|
|
|
if ($lower === $upper) {
|
|
return $sortedValues[$lower];
|
|
}
|
|
|
|
$fraction = $index - $lower;
|
|
|
|
return round($sortedValues[$lower] + ($sortedValues[$upper] - $sortedValues[$lower]) * $fraction, 1);
|
|
}
|
|
}
|