Skip to content

Commit 842a98b

Browse files
committed
fist commit
1 parent 1ef555d commit 842a98b

File tree

7 files changed

+304
-0
lines changed

7 files changed

+304
-0
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
.idea
2+
vendor
3+
composer.lock
4+
phpunit.xml

composer.json

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"name": "alancolant/laravel-pulse-graphql",
3+
"description": "Monitor your graphql query with a Laravel pulse Card",
4+
"type": "library",
5+
"license": "MIT",
6+
"authors": [
7+
{
8+
"name": "COLANT Alan",
9+
"email": "contact@alancolant.com"
10+
}
11+
],
12+
"require": {
13+
"php": "^8.1",
14+
"illuminate/support": "*",
15+
"laravel/pulse": "^1.0.0@beta"
16+
},
17+
"autoload": {
18+
"psr-4": {
19+
"Alancolant\\PulseGraphql\\": "src/"
20+
}
21+
},
22+
"extra": {
23+
"laravel": {
24+
"providers": [
25+
"Alancolant\\PulseGraphql\\PulseGraphqlServiceProvider"
26+
]
27+
}
28+
}
29+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
<x-pulse::card :cols="$cols" :rows="$rows" :class="$class">
2+
<x-pulse::card-header name="Graphql queries usage">
3+
<x-slot:icon>
4+
<x-dynamic-component :component="'pulse::icons.sparkles'"/>
5+
</x-slot:icon>
6+
<x-slot:actions>
7+
<x-pulse::select
8+
wire:model.live="orderBy"
9+
label="Sort by"
10+
:options="[
11+
'slowest' => 'slowest',
12+
'count' => 'count',
13+
'avg' => 'average',
14+
]"
15+
@change="loading = true"
16+
/>
17+
</x-slot:actions>
18+
</x-pulse::card-header>
19+
20+
<x-pulse::scroll :expand="$expand" wire:poll.5s="">
21+
@if ($data->isEmpty())
22+
<x-pulse::no-results/>
23+
@else
24+
<x-pulse::table>
25+
<colgroup>
26+
<col width="0%"/>
27+
<col width="0%"/>
28+
<col width="100%"/>
29+
<col width="0%"/>
30+
<col width="0%"/>
31+
</colgroup>
32+
<x-pulse::thead>
33+
<tr>
34+
<x-pulse::th>Schema</x-pulse::th>
35+
<x-pulse::th>Type</x-pulse::th>
36+
<x-pulse::th>Operation</x-pulse::th>
37+
<x-pulse::th class="text-right">Count</x-pulse::th>
38+
<x-pulse::th class="text-right">Slowest</x-pulse::th>
39+
<x-pulse::th class="text-right">Average</x-pulse::th>
40+
</tr>
41+
</x-pulse::thead>
42+
<tbody>
43+
@foreach ($data as $infos)
44+
<tr wire:key="{{ $infos->schemaName.$infos->operationType.$infos->operation }}-spacer" class="h-2 first:h-0"></tr>
45+
<tr wire:key="{{ $infos->schemaName.$infos->operationType.$infos->operation }}-row">
46+
<x-pulse::td class="text-xs">
47+
{{$infos->schemaName}}
48+
</x-pulse::td>
49+
<x-pulse::td class="text-xs">
50+
@switch($infos->operationType)
51+
@case('query')
52+
<span class="text-xs font-mono px-1 border rounded font-semibold text-blue-400 dark:text-blue-300 bg-blue-50 dark:bg-blue-900 border-blue-200 dark:border-blue-700">
53+
query
54+
</span>
55+
@break
56+
@case('mutation')
57+
<span class="text-xs font-mono px-1 border rounded font-semibold text-purple-400 dark:text-purple-300 bg-purple-50 dark:bg-purple-900 border-purple-200 dark:border-purple-700">
58+
mutation
59+
</span>
60+
@break
61+
@default
62+
<span class="text-xs font-mono px-1 border rounded font-semibold text-gray-400 dark:text-gray-100 bg-gray-50 dark:bg-gray-700 border-gray-200 dark:border-gray-500">
63+
{{$infos->operationType}}
64+
</span>
65+
@endswitch
66+
</x-pulse::td>
67+
<x-pulse::td class="overflow-hidden max-w-[1px]">
68+
<code class="block text-gray-900 dark:text-gray-100 truncate"
69+
title="{{ $infos->operation }}">
70+
{{$infos->operation }}
71+
</code>
72+
</x-pulse::td>
73+
<x-pulse::td numeric class="text-gray-700 dark:text-gray-300 font-bold">
74+
{{ number_format($infos->count) }}
75+
</x-pulse::td>
76+
<x-pulse::td numeric class="text-gray-700 dark:text-gray-300">
77+
@if ($infos->slowest === null)
78+
<strong>Unknown</strong>
79+
@else
80+
<strong>{{ number_format($infos->slowest) ?: '<1' }}</strong> ms
81+
@endif
82+
</x-pulse::td>
83+
<x-pulse::td numeric class="text-gray-700 dark:text-gray-300">
84+
@if ($infos->average === null)
85+
<strong>Unknown</strong>
86+
@else
87+
<strong>{{ number_format($infos->average) ?: '<1' }}</strong> ms
88+
@endif
89+
</x-pulse::td>
90+
</tr>
91+
@endforeach
92+
</tbody>
93+
</x-pulse::table>
94+
@endif
95+
</x-pulse::scroll>
96+
</x-pulse::card>

src/Livewire/Graphql.php

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
namespace Alancolant\PulseGraphql\Livewire;
4+
5+
use Illuminate\Support\Facades\View;
6+
use Laravel\Pulse\Livewire\Card;
7+
use Livewire\Attributes\Lazy;
8+
use Livewire\Attributes\Url;
9+
10+
class Graphql extends Card
11+
{
12+
#[Url(as: 'graphql')]
13+
public string $orderBy = 'slowest';
14+
15+
#[Lazy]
16+
public function render()
17+
{
18+
[$data, $time, $runAt] = $this->remember(
19+
fn() => $this->aggregate(
20+
'graphql_request',
21+
['max', 'count', 'avg'],
22+
match ($this->orderBy) {
23+
'count' => 'count',
24+
'avg' => 'avg',
25+
default => 'max',
26+
})->map(function ($row) {
27+
[$schemaName, $operationType, $operation] = json_decode($row->key, flags: JSON_THROW_ON_ERROR);
28+
return (object)[
29+
'schemaName' => $schemaName,
30+
'operationType' => $operationType,
31+
'operation' => $operation,
32+
'count' => $row->count,
33+
'slowest' => $row->max,
34+
'average' => $row->avg,
35+
];
36+
}),
37+
$this->orderBy,
38+
);
39+
return View::make('graphql::livewire.graphql', [
40+
'time' => $time,
41+
'runAt' => $runAt,
42+
'data' => $data,
43+
]);
44+
}
45+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
namespace Alancolant\PulseGraphql;
4+
5+
use Alancolant\PulseGraphql\Livewire\Graphql;
6+
use Illuminate\Foundation\Application;
7+
use Illuminate\Support\ServiceProvider;
8+
use Livewire\LivewireManager;
9+
10+
class PulseGraphqlServiceProvider extends ServiceProvider
11+
{
12+
public function boot(): void
13+
{
14+
$this->loadViewsFrom(__DIR__ . '/../resources/views', 'graphql');
15+
16+
$this->callAfterResolving('livewire', function (LivewireManager $livewire, Application $app) {
17+
$livewire->component('pulse.graphql', Graphql::class);
18+
});
19+
}
20+
}

src/Recorders/GraphqlRecorder.php

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
<?php
2+
3+
namespace Alancolant\PulseGraphql\Recorders;
4+
5+
use Alancolant\PulseGraphql\ResolverMiddlewares\SendEventToPulseMiddleware;
6+
use GraphQL\Language\Printer;
7+
use GraphQL\Type\Definition\ResolveInfo;
8+
use Illuminate\Config\Repository;
9+
use Illuminate\Contracts\Foundation\Application;
10+
use Illuminate\Support\Arr;
11+
use Illuminate\Support\Carbon;
12+
use Illuminate\Support\Facades\Config;
13+
use Illuminate\Support\Facades\Route;
14+
use Laragraph\Utils\RequestParser;
15+
use Laravel\Pulse\Concerns\ConfiguresAfterResolving;
16+
use Laravel\Pulse\Pulse;
17+
use Rebing\GraphQL\GraphQL;
18+
19+
class GraphqlRecorder
20+
{
21+
22+
use ConfiguresAfterResolving;
23+
24+
/**
25+
* Create a new recorder instance.
26+
*/
27+
public function __construct(
28+
protected Pulse $pulse,
29+
protected Repository $config,
30+
protected RequestParser $parser
31+
)
32+
{
33+
//
34+
}
35+
36+
/**
37+
* Register the recorder.
38+
*/
39+
public function register(callable $record, Application $app): void
40+
{
41+
$this->afterResolving(
42+
$app, GraphQL::class,
43+
function () use (&$record) {
44+
\Rebing\GraphQL\Support\Facades\GraphQL::appendGlobalResolverMiddleware(new SendEventToPulseMiddleware($record));
45+
}
46+
);
47+
}
48+
49+
public function record(Carbon $startedAt, array $args, $context, ResolveInfo $info): void
50+
{
51+
52+
if (!in_array($info->parentType, ["Query", "Mutation"])) {
53+
return;
54+
}
55+
56+
$schemaName = Arr::first(Route::current()->parameters()) ?? Config::get('graphql.default_schema', 'default');
57+
58+
$operationType = $info->operation->operation;
59+
$query = $info->fieldName;
60+
61+
// $operation = $info->operation->name->value ?? null;
62+
// $fields = array_keys(Arr::dot($info->getFieldSelection(PHP_INT_MAX)));
63+
// $vars = $this->formatVariableDefinitions($info->operation->variableDefinitions);
64+
65+
66+
$duration = ($startedAt->diffInMilliseconds());
67+
$this->pulse->record(
68+
type: 'graphql_request',
69+
key: json_encode([
70+
$schemaName, $operationType, $query
71+
], flags: JSON_THROW_ON_ERROR),
72+
value: $duration,
73+
timestamp: $startedAt,
74+
)->max()->avg()->count();
75+
}
76+
77+
private function formatVariableDefinitions(?iterable $variableDefinitions = []): array
78+
{
79+
return collect($variableDefinitions)
80+
->map(function ($def) {
81+
return Printer::doPrint($def);
82+
})
83+
->toArray();
84+
}
85+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
namespace Alancolant\PulseGraphql\ResolverMiddlewares;
4+
5+
use Closure;
6+
use GraphQL\Type\Definition\ResolveInfo;
7+
use Rebing\GraphQL\Support\Middleware;
8+
9+
class SendEventToPulseMiddleware extends Middleware
10+
{
11+
public function __construct(public Closure $record)
12+
{
13+
}
14+
15+
/**
16+
* @inheritDoc
17+
*/
18+
public function handle($root, array $args, $context, ResolveInfo $info, Closure $next)
19+
{
20+
$startedAt = now();
21+
$result = $next($root, $args, $context, $info);
22+
($this->record)($startedAt, $args, $context, $info);
23+
return $result;
24+
}
25+
}

0 commit comments

Comments
 (0)