php-mcp/src/Mod/Mcp/Services/McpMetricsService.php
2026-01-26 20:57:41 +00:00

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