Merge pull request #467 from beyondcode/refactor/dashboard

[2.x] Refactoring dashboard
This commit is contained in:
rennokki 2020-08-18 09:44:18 +03:00 committed by GitHub
commit b174ffb6e7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 720 additions and 509 deletions

View File

@ -11,5 +11,8 @@ end_of_line = lf
insert_final_newline = true insert_final_newline = true
trim_trailing_whitespace = true trim_trailing_whitespace = true
[*.blade.php]
indent_size = 2
[*.md] [*.md]
trim_trailing_whitespace = false trim_trailing_whitespace = false

View File

@ -209,9 +209,13 @@ return [
| store them into an array and then store them into the database | store them into an array and then store them into the database
| on each interval. | on each interval.
| |
| You can opt-in to avoid any statistics storage by setting the logger
| to the built-in NullLogger.
|
*/ */
'logger' => BeyondCode\LaravelWebSockets\Statistics\Logger\HttpStatisticsLogger::class, 'logger' => \BeyondCode\LaravelWebSockets\Statistics\Logger\HttpStatisticsLogger::class,
// 'logger' => \BeyondCode\LaravelWebSockets\Statistics\Logger\NullStatisticsLogger::class,
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------

View File

@ -1,166 +1,306 @@
<html> <html>
<head> <head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1"> <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<title>WebSockets Dashboard</title> <title>WebSockets Dashboard</title>
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous"> <link href="https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css" rel="stylesheet">
<script <link href="https://cdn.jsdelivr.net/npm/vue-json-editor@1.4.2/assets/jsoneditor.min.css" rel="stylesheet">
src="https://code.jquery.com/jquery-3.3.1.min.js"
integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=" <script src="https://js.pusher.com/7.0/pusher.min.js"></script>
crossorigin="anonymous"></script>
<script src="https://js.pusher.com/4.3/pusher.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue@2.5.17/dist/vue.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/vue@2.5.17/dist/vue.min.js"></script>
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script> <script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/v-jsoneditor@1.4.1/dist/v-jsoneditor.min.js"></script>
<script>
window.baseURL = '{{ url(request()->path()) }}';
axios.defaults.baseURL = baseURL;
</script>
</head> </head>
<body> <body class="px-6">
<div class="container" id="app"> <div
<div class="card col-xs-12 mt-4"> id="app"
<div class="card-header"> class="mx-auto"
<form id="connect" class="form-inline" role="form"> :class="{
<label class="my-1 mr-2" for="app">App:</label> 'max-w-xl': ! connected,
<select class="form-control form-control-sm mr-2" name="app" id="app" v-model="app"> 'max-w-6xl': connected,
<option v-for="app in apps" :value="app">@{{ app.name }}</option> }"
>
<div class="w-full my-6 rounded-lg bg-gray-100 p-6">
<div class="font-semibold uppercase text-gray-700 mb-6">
Connect to app
</div>
<div class="flex flex-row justify-between">
<div class="relative">
<select
v-model="app"
class="block appearance-none w-full bg-gray-200 border border-gray-200 text-gray-700 py-2 px-6 pr-12 rounded-lg focus:outline-none focus:bg-white focus:border-gray-500"
id="grid-state"
>
<option
v-for="app in apps"
:value="app"
>
@{{ app.name }}
</option>
</select> </select>
<label class="my-1 mr-2" for="app">Port:</label> <div class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-gray-700">
<input class="form-control form-control-sm mr-2" v-model="port" placeholder="Port"> <svg class="fill-current h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M9.293 12.95l.707.707L15.657 8l-1.414-1.414L10 10.828 5.757 6.586 4.343 8z"/></svg>
<button v-if="! connected" type="submit" @click.prevent="connect" class="mr-2 btn btn-sm btn-primary"> </div>
Connect </div>
</button> <div>
<button v-if="connected" type="submit" @click.prevent="disconnect" class="btn btn-sm btn-danger"> <button
v-if="connected"
@click.prevent="disconnect"
class="bg-red-500 hover:bg-red-600 rounded-full px-3 py-2 text-white focus:outline-none"
>
Disconnect Disconnect
</button> </button>
</form>
<div id="status"></div> <button
</div> v-else
<div class="card-body"> @click.prevent="connect"
<div v-if="connected && app.statisticsEnabled"> class="rounded-full px-3 py-2 text-white focus:outline-none"
<h4>Realtime Statistics</h4> :class="{
<div id="statisticsChart" style="width: 100%; height: 250px;"></div> 'bg-green-500 hover:bg-green-600': ! connecting,
</div> 'bg-gray-500 cursor-not-allowed': connecting,
<div v-if="connected"> }"
<h4>Event Creator</h4> >
<form> <template v-if="connecting">
<div class="row"> Connecting...
<div class="col"> </template>
<input type="text" class="form-control" v-model="form.channel" placeholder="Channel"> <template v-else>
</div> Connect
<div class="col"> </template>
<input type="text" class="form-control" v-model="form.event" placeholder="Event">
</div>
</div>
<div class="row mt-3">
<div class="col">
<div class="form-group">
<textarea placeholder="Data" v-model="form.data" class="form-control" id="data"
rows="3"></textarea>
</div>
</div>
</div>
<div class="row text-right">
<div class="col">
<button type="submit" @click.prevent="sendEvent" class="btn btn-sm btn-primary">Send event
</button> </button>
</div> </div>
</div> </div>
</form>
</div> </div>
<h4>Events</h4>
<table id="events" class="table table-striped table-hover"> <div
v-if="connected && app.statisticsEnabled"
class="w-full my-6 px-6"
>
<div class="font-semibold uppercase text-gray-700">
Live statistics
</div>
<div
id="statisticsChart"
style="width: 100%; height: 250px;"
></div>
</div>
<div
v-if="connected"
class="flex flex-col rounded-lg bg-gray-100 p-6 my-6 space-y-6"
>
<div class="font-semibold uppercase text-gray-700">
Send payload event to channel
</div>
<div class="flex flex-col space-y-6 md:flex-row md:space-x-6 md:space-y-0">
<div class="w-full md:w-1/2">
<label
class="block text-gray-700 text-sm font-bold mb-2"
for="channel"
>
Channel name
</label>
<input
v-model="form.channel"
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
id="channel"
type="text"
placeholder="ex: orders"
>
</div>
<div class="w-full md:w-1/2">
<label
class="block text-gray-700 text-sm font-bold mb-2"
for="event"
>
Event name
</label>
<input
v-model="form.event"
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
id="event"
type="text"
placeholder="ex: OrderShipped"
>
</div>
</div>
<div>
<label class="block text-gray-700 text-sm font-bold mb-2">
Payload
</label>
<v-jsoneditor
v-model="form.data"
:options="{ mode: 'code' }"
/>
</div>
<div>
<button
@click.prevent="sendEvent"
class="rounded-full px-3 py-2 text-white focus:outline-none"
:class="{
'bg-blue-500 hover:bg-blue-600': ! sendingEvent,
'bg-gray-500 cursor-not-allowed': sendingEvent,
}"
>
<template v-if="sendingEvent">
Sending...
</template>
<template v-else>
Send event
</template>
</button>
</div>
</div>
<div
v-if="connected"
class="flex flex-col my-6"
>
<div class="font-semibold uppercase text-gray-700 mb-6">
Server activity
</div>
<div class="overflow-x-auto sm:-mx-6 sm:px-6 lg:-mx-8 lg:px-8">
<div class="align-middle inline-block min-w-full shadow overflow-hidden sm:rounded-lg border-b border-gray-200">
<table class="min-w-full divide-y divide-gray-200">
<thead> <thead>
<tr> <tr>
<th>Type</th> <th class="px-6 py-3 border-b border-gray-200 bg-gray-100 text-left text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">
<th>Details</th> Type
<th>Time</th> </th>
<th class="px-6 py-3 border-b border-gray-200 bg-gray-100 text-left text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">
Details
</th>
<th class="px-6 py-3 border-b border-gray-200 bg-gray-100 text-left text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">
Time
</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody class="bg-white divide-y divide-gray-200">
<tr v-for="log in logs.slice().reverse()"> <tr
<td><span class="badge" :class="getBadgeClass(log)">@{{ log.type }}</span></td> v-for="(log, index) in logs.slice().reverse()"
<td><pre>@{{ log.details }}</pre></td> :key="index"
<td>@{{ log.time }}</td> >
<td class="px-6 py-4 whitespace-no-wrap border-b border-gray-200">
<div
:class="[getBadgeClass(log)]"
class="rounded-full px-3 py-1 inline-block text-sm"
>
@{{ log.type }}
</div>
</td>
<td class="px-6 py-4 whitespace-no-wrap border-b border-gray-200">
<pre class="text-xs">@{{ log.details }}</pre>
</td>
<td class="px-6 py-4 whitespace-no-wrap border-b border-gray-200">
@{{ log.time }}
</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
</div> </div>
</div> </div>
</div> </div>
</div>
<script> <script>
new Vue({ new Vue({
el: '#app', el: '#app',
data: { data: {
connected: false, connected: false,
connecting: false,
sendingEvent: false,
chart: null, chart: null,
pusher: null, pusher: null,
app: null, app: null,
port: {{ $port }}, port: {{ config('websockets.dashboard.port') }},
apps: {!! json_encode($apps) !!}, apps: @json($apps),
form: { form: {
channel: null, channel: null,
event: null, event: null,
data: null data: null,
}, },
logs: [], logs: [],
}, },
mounted () { mounted () {
this.app = this.apps[0] || null; this.app = this.apps[0] || null;
}, },
methods: { methods: {
connect () { connect () {
this.connecting = true;
this.pusher = new Pusher(this.app.key, { this.pusher = new Pusher(this.app.key, {
wsHost: this.app.host === null ? window.location.hostname : this.app.host, wsHost: this.app.host === null ? window.location.hostname : this.app.host,
wsPort: this.port === null ? 6001 : this.port, wsPort: this.port === null ? 6001 : this.port,
wssPort: this.port === null ? 6001 : this.port, wssPort: this.port === null ? 6001 : this.port,
wsPath: this.app.path === null ? '' : this.app.path, wsPath: this.app.path === null ? '' : this.app.path,
disableStats: true, disableStats: true,
authEndpoint: '{{ url(request()->path().'/auth') }}', authEndpoint: `${window.baseURL}/auth`,
auth: { auth: {
headers: { headers: {
'X-CSRF-Token': "{{ csrf_token() }}", 'X-CSRF-Token': "{{ csrf_token() }}",
'X-App-ID': this.app.id 'X-App-ID': this.app.id,
}
}, },
enabledTransports: ['ws', 'flash'] },
enabledTransports: ['ws', 'wss'],
forceTLS: false,
}); });
this.pusher.connection.bind('state_change', states => { this.pusher.connection.bind('state_change', states => {
$('div#status').text("Channels current state is " + states.current); this.connecting = false;
}); });
this.pusher.connection.bind('connected', () => { this.pusher.connection.bind('connected', () => {
this.connected = true; this.connected = true;
this.connecting = false;
if (this.app.statisticsEnabled) {
this.loadChart(); this.loadChart();
}
}); });
this.pusher.connection.bind('disconnected', () => { this.pusher.connection.bind('disconnected', () => {
this.connected = false; this.connected = false;
this.connecting = false;
this.logs = []; this.logs = [];
}); });
this.pusher.connection.bind('error', event => { this.pusher.connection.bind('error', event => {
if (event.error.data.code === 4100) { if (event.error.data.code === 4100) {
$('div#status').text("Maximum connection limit exceeded!");
this.connected = false; this.connected = false;
this.logs = []; this.logs = [];
throw new Error("Over capacity"); throw new Error("Over capacity");
} }
this.connecting = false;
}); });
this.subscribeToAllChannels(); this.subscribeToAllChannels();
this.subscribeToStatistics(); this.subscribeToStatistics();
}, },
disconnect () { disconnect () {
this.pusher.disconnect(); this.pusher.disconnect();
this.connecting = false;
}, },
loadChart () { loadChart () {
$.getJSON('{{ url(request()->path().'/api') }}/' + this.app.id + '/statistics', (data) => { axios.get(`/api/${this.app.id}/statistics`)
.then(res => {
let data = res.data;
let chartData = [ let chartData = [
{ {
@ -180,16 +320,18 @@
y: data.api_message_count.y, y: data.api_message_count.y,
type: 'bar', type: 'bar',
name: '# API Messages' name: '# API Messages'
} },
]; ];
let layout = { let layout = {
margin: { margin: {
l: 50, l: 50,
r: 0, r: 0,
b: 50, b: 50,
t: 50, t: 50,
pad: 4 pad: 4,
} },
autosize: true,
}; };
this.chart = Plotly.newPlot('statisticsChart', chartData, layout); this.chart = Plotly.newPlot('statisticsChart', chartData, layout);
@ -197,68 +339,84 @@
}, },
subscribeToAllChannels () { subscribeToAllChannels () {
[ @json($channels).forEach(channelName => this.subscribeToChannel(channelName))
'disconnection',
'connection',
'vacated',
'occupied',
'subscribed',
'client-message',
'api-message',
'replicator-subscribed',
'replicator-unsubscribed',
].forEach(channelName => this.subscribeToChannel(channelName))
}, },
subscribeToChannel (channel) { subscribeToChannel (channel) {
this.pusher.subscribe('{{ \BeyondCode\LaravelWebSockets\Dashboard\DashboardLogger::LOG_CHANNEL_PREFIX }}' + channel) this.pusher.subscribe(`{{ $logPrefix }}${channel}`)
.bind('log-message', (data) => { .bind('log-message', (data) => {
this.logs.push(data); this.logs.push(data);
}); });
}, },
subscribeToStatistics () { subscribeToStatistics () {
this.pusher.subscribe('{{ \BeyondCode\LaravelWebSockets\Dashboard\DashboardLogger::LOG_CHANNEL_PREFIX }}statistics') this.pusher.subscribe('{{ $logPrefix }}statistics')
.bind('statistics-updated', (data) => { .bind('statistics-updated', (data) => {
var update = { var update = {
x: [[data.time], [data.time], [data.time]], x: [[data.time], [data.time], [data.time]],
y: [[data.peak_connection_count], [data.websocket_message_count], [data.api_message_count]] y: [[data.peak_connection_count], [data.websocket_message_count], [data.api_message_count]],
}; };
Plotly.extendTraces('statisticsChart', update, [0, 1, 2]); Plotly.extendTraces('statisticsChart', update, [0, 1, 2]);
}); });
}, },
getBadgeClass(log) {
if (log.type === 'occupied' || log.type === 'connection') {
return 'badge-primary';
}
if (log.type === 'vacated') {
return 'badge-warning';
}
if (log.type === 'disconnection') {
return 'badge-error';
}
if (log.type === 'api_message') {
return 'badge-info';
}
return 'badge-secondary';
},
sendEvent () { sendEvent () {
$.post('{{ url(request()->path().'/event') }}', { if (! this.sendingEvent) {
this.sendingEvent = true;
let payload = {
_token: '{{ csrf_token() }}', _token: '{{ csrf_token() }}',
key: this.app.key, key: this.app.key,
secret: this.app.secret, secret: this.app.secret,
appId: this.app.id, appId: this.app.id,
channel: this.form.channel, channel: this.form.channel,
event: this.form.event, event: this.form.event,
data: this.form.data, data: JSON.stringify(this.form.data),
}).fail(() => { };
axios
.post('/event', payload)
.then(() => {
this.form = {
channel: null,
event: null,
data: null,
};
})
.catch(err => {
alert('Error sending event.'); alert('Error sending event.');
})
.then(() => {
this.sendingEvent = false;
}); });
} }
},
getBadgeClass (log) {
if (['connection', 'subscribed'].includes(log.type)) {
return 'bg-green-500 text-white';
} }
if (['replicator-subscribed', 'replicator-joined'].includes(log.type)) {
return 'bg-green-700 text-white';
}
if (log.type === 'vacated') {
return 'bg-orange-500 text-white';
}
if (['disconnection', 'occupied', 'replicator-unsubscribed', 'replicator-left'].includes(log.type)) {
return 'bg-red-700 text-white';
}
if (['api_message', 'replicator-message-published', 'replicator-message-received'].includes(log.type)) {
return 'bg-black text-white';
}
return 'bg-gray-700 text-white';
},
},
}); });
</script> </script>
</body> </body>

View File

@ -3,16 +3,14 @@
namespace BeyondCode\LaravelWebSockets\Dashboard; namespace BeyondCode\LaravelWebSockets\Dashboard;
use BeyondCode\LaravelWebSockets\WebSockets\Channels\ChannelManager; use BeyondCode\LaravelWebSockets\WebSockets\Channels\ChannelManager;
use Ratchet\ConnectionInterface;
use stdClass;
class DashboardLogger class DashboardLogger
{ {
const LOG_CHANNEL_PREFIX = 'private-websockets-dashboard-'; const LOG_CHANNEL_PREFIX = 'private-websockets-dashboard-';
const TYPE_DISCONNECTION = 'disconnection'; const TYPE_DISCONNECTED = 'disconnected';
const TYPE_CONNECTION = 'connection'; const TYPE_CONNECTED = 'connected';
const TYPE_VACATED = 'vacated'; const TYPE_VACATED = 'vacated';
@ -20,7 +18,7 @@ class DashboardLogger
const TYPE_SUBSCRIBED = 'subscribed'; const TYPE_SUBSCRIBED = 'subscribed';
const TYPE_CLIENT_MESSAGE = 'client-message'; const TYPE_WS_MESSAGE = 'ws-message';
const TYPE_API_MESSAGE = 'api-message'; const TYPE_API_MESSAGE = 'api-message';
@ -28,101 +26,36 @@ class DashboardLogger
const TYPE_REPLICATOR_UNSUBSCRIBED = 'replicator-unsubscribed'; const TYPE_REPLICATOR_UNSUBSCRIBED = 'replicator-unsubscribed';
public static function connection(ConnectionInterface $connection) const TYPE_REPLICATOR_JOINED_CHANNEL = 'replicator-joined';
{
/** @var \GuzzleHttp\Psr7\Request $request */
$request = $connection->httpRequest;
static::log($connection->app->id, static::TYPE_CONNECTION, [ const TYPE_REPLICATOR_LEFT_CHANNEL = 'replicator-left';
'details' => [
'origin' => "{$request->getUri()->getScheme()}://{$request->getUri()->getHost()}",
'socketId' => $connection->socketId,
],
]);
}
public static function occupied(ConnectionInterface $connection, string $channelName) const TYPE_REPLICATOR_MESSAGE_PUBLISHED = 'replicator-message-published';
{
static::log($connection->app->id, static::TYPE_OCCUPIED, [
'details' => [
'channel' => $channelName,
],
]);
}
public static function subscribed(ConnectionInterface $connection, string $channelName) const TYPE_REPLICATOR_MESSAGE_RECEIVED = 'replicator-message-received';
{
static::log($connection->app->id, static::TYPE_SUBSCRIBED, [
'details' => [
'socketId' => $connection->socketId,
'channel' => $channelName,
],
]);
}
public static function clientMessage(ConnectionInterface $connection, stdClass $payload) /**
{ * The list of all channels.
static::log($connection->app->id, static::TYPE_CLIENT_MESSAGE, [ *
'details' => [ * @var array
'socketId' => $connection->socketId, */
'channel' => $payload->channel, public static $channels = [
'event' => $payload->event, self::TYPE_DISCONNECTED,
'data' => $payload, self::TYPE_CONNECTED,
], self::TYPE_VACATED,
]); self::TYPE_OCCUPIED,
} self::TYPE_SUBSCRIBED,
self::TYPE_WS_MESSAGE,
self::TYPE_API_MESSAGE,
self::TYPE_REPLICATOR_SUBSCRIBED,
self::TYPE_REPLICATOR_UNSUBSCRIBED,
self::TYPE_REPLICATOR_JOINED_CHANNEL,
self::TYPE_REPLICATOR_LEFT_CHANNEL,
self::TYPE_REPLICATOR_MESSAGE_PUBLISHED,
self::TYPE_REPLICATOR_MESSAGE_RECEIVED,
];
public static function disconnection(ConnectionInterface $connection) public static function log($appId, string $type, array $details = [])
{
static::log($connection->app->id, static::TYPE_DISCONNECTION, [
'details' => [
'socketId' => $connection->socketId,
],
]);
}
public static function vacated(ConnectionInterface $connection, string $channelName)
{
static::log($connection->app->id, static::TYPE_VACATED, [
'details' => [
'socketId' => $connection->socketId,
'channel' => $channelName,
],
]);
}
public static function apiMessage($appId, string $channel, string $event, string $payload)
{
static::log($appId, static::TYPE_API_MESSAGE, [
'details' => [
'channel' => $connection,
'event' => $event,
'payload' => $payload,
],
]);
}
public static function replicatorSubscribed(string $appId, string $channel, string $serverId)
{
static::log($appId, static::TYPE_REPLICATOR_SUBSCRIBED, [
'details' => [
'serverId' => $serverId,
'channel' => $channel,
],
]);
}
public static function replicatorUnsubscribed(string $appId, string $channel, string $serverId)
{
static::log($appId, static::TYPE_REPLICATOR_UNSUBSCRIBED, [
'details' => [
'serverId' => $serverId,
'channel' => $channel,
],
]);
}
public static function log($appId, string $type, array $attributes = [])
{ {
$channelName = static::LOG_CHANNEL_PREFIX.$type; $channelName = static::LOG_CHANNEL_PREFIX.$type;
@ -134,7 +67,8 @@ class DashboardLogger
'data' => [ 'data' => [
'type' => $type, 'type' => $type,
'time' => strftime('%H:%M:%S'), 'time' => strftime('%H:%M:%S'),
] + $attributes, 'details' => $details,
],
]); ]);
} }
} }

View File

@ -3,6 +3,7 @@
namespace BeyondCode\LaravelWebSockets\Dashboard\Http\Controllers; namespace BeyondCode\LaravelWebSockets\Dashboard\Http\Controllers;
use BeyondCode\LaravelWebSockets\Apps\AppManager; use BeyondCode\LaravelWebSockets\Apps\AppManager;
use BeyondCode\LaravelWebSockets\Dashboard\DashboardLogger;
use Illuminate\Http\Request; use Illuminate\Http\Request;
class ShowDashboard class ShowDashboard
@ -12,6 +13,8 @@ class ShowDashboard
return view('websockets::dashboard', [ return view('websockets::dashboard', [
'apps' => $apps->all(), 'apps' => $apps->all(),
'port' => config('websockets.dashboard.port', 6001), 'port' => config('websockets.dashboard.port', 6001),
'channels' => DashboardLogger::$channels,
'logPrefix' => DashboardLogger::LOG_CHANNEL_PREFIX,
]); ]);
} }
} }

View File

@ -21,12 +21,11 @@ class TriggerEventController extends Controller
'data' => $request->json()->get('data'), 'data' => $request->json()->get('data'),
], $request->json()->get('socket_id'), $request->appId); ], $request->json()->get('socket_id'), $request->appId);
DashboardLogger::apiMessage( DashboardLogger::log($request->appId, DashboardLogger::TYPE_API_MESSAGE, [
$request->appId, 'channel' => $channelName,
$channelName, 'event' => $request->json()->get('name'),
$request->json()->get('name'), 'payload' => $request->json()->get('data'),
$request->json()->get('data') ]);
);
StatisticsLogger::apiMessage($request->appId); StatisticsLogger::apiMessage($request->appId);
} }

View File

@ -78,7 +78,7 @@ class LocalClient implements ReplicationInterface
*/ */
public function joinChannel(string $appId, string $channel, string $socketId, string $data) public function joinChannel(string $appId, string $channel, string $socketId, string $data)
{ {
$this->channelData["$appId:$channel"][$socketId] = $data; $this->channelData["{$appId}:{$channel}"][$socketId] = $data;
} }
/** /**
@ -92,10 +92,10 @@ class LocalClient implements ReplicationInterface
*/ */
public function leaveChannel(string $appId, string $channel, string $socketId) public function leaveChannel(string $appId, string $channel, string $socketId)
{ {
unset($this->channelData["$appId:$channel"][$socketId]); unset($this->channelData["{$appId}:{$channel}"][$socketId]);
if (empty($this->channelData["$appId:$channel"])) { if (empty($this->channelData["{$appId}:{$channel}"])) {
unset($this->channelData["$appId:$channel"]); unset($this->channelData["{$appId}:{$channel}"]);
} }
} }
@ -108,7 +108,7 @@ class LocalClient implements ReplicationInterface
*/ */
public function channelMembers(string $appId, string $channel): PromiseInterface public function channelMembers(string $appId, string $channel): PromiseInterface
{ {
$members = $this->channelData["$appId:$channel"] ?? []; $members = $this->channelData["{$appId}:{$channel}"] ?? [];
$members = array_map(function ($user) { $members = array_map(function ($user) {
return json_decode($user); return json_decode($user);
@ -130,8 +130,8 @@ class LocalClient implements ReplicationInterface
// Count the number of users per channel // Count the number of users per channel
foreach ($channelNames as $channel) { foreach ($channelNames as $channel) {
$results[$channel] = isset($this->channelData["$appId:$channel"]) $results[$channel] = isset($this->channelData["{$appId}:{$channel}"])
? count($this->channelData["$appId:$channel"]) ? count($this->channelData["{$appId}:{$channel}"])
: 0; : 0;
} }

View File

@ -12,7 +12,7 @@ use React\EventLoop\LoopInterface;
use React\Promise\PromiseInterface; use React\Promise\PromiseInterface;
use stdClass; use stdClass;
class RedisClient implements ReplicationInterface class RedisClient extends LocalClient
{ {
/** /**
* The running loop. * The running loop.
@ -89,6 +89,175 @@ class RedisClient implements ReplicationInterface
return $this; return $this;
} }
/**
* Publish a message to a channel on behalf of a websocket user.
*
* @param string $appId
* @param string $channel
* @param stdClass $payload
* @return bool
*/
public function publish(string $appId, string $channel, stdClass $payload): bool
{
$payload->appId = $appId;
$payload->serverId = $this->getServerId();
$payload = json_encode($payload);
$this->publishClient->__call('publish', ["{$appId}:{$channel}", $payload]);
DashboardLogger::log($appId, DashboardLogger::TYPE_REPLICATOR_MESSAGE_PUBLISHED, [
'channel' => $channel,
'serverId' => $this->getServerId(),
'payload' => $payload,
'pubsub' => "{$appId}:{$channel}",
]);
return true;
}
/**
* Subscribe to a channel on behalf of websocket user.
*
* @param string $appId
* @param string $channel
* @return bool
*/
public function subscribe(string $appId, string $channel): bool
{
if (! isset($this->subscribedChannels["{$appId}:{$channel}"])) {
// We're not subscribed to the channel yet, subscribe and set the count to 1
$this->subscribeClient->__call('subscribe', ["{$appId}:{$channel}"]);
$this->subscribedChannels["{$appId}:{$channel}"] = 1;
} else {
// Increment the subscribe count if we've already subscribed
$this->subscribedChannels["{$appId}:{$channel}"]++;
}
DashboardLogger::log($appId, DashboardLogger::TYPE_REPLICATOR_SUBSCRIBED, [
'channel' => $channel,
'serverId' => $this->getServerId(),
'pubsub' => "{$appId}:{$channel}",
]);
return true;
}
/**
* Unsubscribe from a channel on behalf of a websocket user.
*
* @param string $appId
* @param string $channel
* @return bool
*/
public function unsubscribe(string $appId, string $channel): bool
{
if (! isset($this->subscribedChannels["{$appId}:{$channel}"])) {
return false;
}
// Decrement the subscription count for this channel
$this->subscribedChannels["{$appId}:{$channel}"]--;
// If we no longer have subscriptions to that channel, unsubscribe
if ($this->subscribedChannels["{$appId}:{$channel}"] < 1) {
$this->subscribeClient->__call('unsubscribe', ["{$appId}:{$channel}"]);
unset($this->subscribedChannels["{$appId}:{$channel}"]);
}
DashboardLogger::log($appId, DashboardLogger::TYPE_REPLICATOR_UNSUBSCRIBED, [
'channel' => $channel,
'serverId' => $this->getServerId(),
'pubsub' => "{$appId}:{$channel}",
]);
return true;
}
/**
* Add a member to a channel. To be called when they have
* subscribed to the channel.
*
* @param string $appId
* @param string $channel
* @param string $socketId
* @param string $data
* @return void
*/
public function joinChannel(string $appId, string $channel, string $socketId, string $data)
{
$this->publishClient->__call('hset', ["{$appId}:{$channel}", $socketId, $data]);
DashboardLogger::log($appId, DashboardLogger::TYPE_REPLICATOR_JOINED_CHANNEL, [
'channel' => $channel,
'serverId' => $this->getServerId(),
'socketId' => $socketId,
'data' => $data,
'pubsub' => "{$appId}:{$channel}",
]);
}
/**
* Remove a member from the channel. To be called when they have
* unsubscribed from the channel.
*
* @param string $appId
* @param string $channel
* @param string $socketId
* @return void
*/
public function leaveChannel(string $appId, string $channel, string $socketId)
{
$this->publishClient->__call('hdel', ["{$appId}:{$channel}", $socketId]);
DashboardLogger::log($appId, DashboardLogger::TYPE_REPLICATOR_LEFT_CHANNEL, [
'channel' => $channel,
'serverId' => $this->getServerId(),
'socketId' => $socketId,
'pubsub' => "{$appId}:{$channel}",
]);
}
/**
* Retrieve the full information about the members in a presence channel.
*
* @param string $appId
* @param string $channel
* @return PromiseInterface
*/
public function channelMembers(string $appId, string $channel): PromiseInterface
{
return $this->publishClient->__call('hgetall', ["{$appId}:{$channel}"])
->then(function ($members) {
// The data is expected as objects, so we need to JSON decode
return array_map(function ($user) {
return json_decode($user);
}, $members);
});
}
/**
* Get the amount of users subscribed for each presence channel.
*
* @param string $appId
* @param array $channelNames
* @return PromiseInterface
*/
public function channelMemberCounts(string $appId, array $channelNames): PromiseInterface
{
$this->publishClient->__call('multi', []);
foreach ($channelNames as $channel) {
$this->publishClient->__call('hlen', ["{$appId}:{$channel}"]);
}
return $this->publishClient->__call('exec', [])
->then(function ($data) use ($channelNames) {
return array_combine($channelNames, $data);
});
}
/** /**
* Handle a message received from Redis on a specific channel. * Handle a message received from Redis on a specific channel.
* *
@ -101,7 +270,7 @@ class RedisClient implements ReplicationInterface
$payload = json_decode($payload); $payload = json_decode($payload);
// Ignore messages sent by ourselves. // Ignore messages sent by ourselves.
if (isset($payload->serverId) && $this->serverId === $payload->serverId) { if (isset($payload->serverId) && $this->getServerId() === $payload->serverId) {
return; return;
} }
@ -125,6 +294,7 @@ class RedisClient implements ReplicationInterface
} }
$socket = $payload->socket ?? null; $socket = $payload->socket ?? null;
$serverId = $payload->serverId ?? null;
// Remove fields intended for internal use from the payload. // Remove fields intended for internal use from the payload.
unset($payload->socket); unset($payload->socket);
@ -133,143 +303,15 @@ class RedisClient implements ReplicationInterface
// Push the message out to connected websocket clients. // Push the message out to connected websocket clients.
$channel->broadcastToEveryoneExcept($payload, $socket, $appId, false); $channel->broadcastToEveryoneExcept($payload, $socket, $appId, false);
}
/** DashboardLogger::log($appId, DashboardLogger::TYPE_REPLICATOR_MESSAGE_RECEIVED, [
* Subscribe to a channel on behalf of websocket user. 'channel' => $channel->getChannelName(),
* 'redisChannel' => $redisChannel,
* @param string $appId 'serverId' => $this->getServer(),
* @param string $channel 'incomingServerId' => $serverId,
* @return bool 'incomingSocketId' => $socket,
*/ 'payload' => $payload,
public function subscribe(string $appId, string $channel): bool ]);
{
if (! isset($this->subscribedChannels["$appId:$channel"])) {
// We're not subscribed to the channel yet, subscribe and set the count to 1
$this->subscribeClient->__call('subscribe', ["$appId:$channel"]);
$this->subscribedChannels["$appId:$channel"] = 1;
} else {
// Increment the subscribe count if we've already subscribed
$this->subscribedChannels["$appId:$channel"]++;
}
DashboardLogger::replicatorSubscribed($appId, $channel, $this->serverId);
return true;
}
/**
* Unsubscribe from a channel on behalf of a websocket user.
*
* @param string $appId
* @param string $channel
* @return bool
*/
public function unsubscribe(string $appId, string $channel): bool
{
if (! isset($this->subscribedChannels["$appId:$channel"])) {
return false;
}
// Decrement the subscription count for this channel
$this->subscribedChannels["$appId:$channel"]--;
// If we no longer have subscriptions to that channel, unsubscribe
if ($this->subscribedChannels["$appId:$channel"] < 1) {
$this->subscribeClient->__call('unsubscribe', ["$appId:$channel"]);
unset($this->subscribedChannels["$appId:$channel"]);
}
DashboardLogger::replicatorUnsubscribed($appId, $channel, $this->serverId);
return true;
}
/**
* Publish a message to a channel on behalf of a websocket user.
*
* @param string $appId
* @param string $channel
* @param stdClass $payload
* @return bool
*/
public function publish(string $appId, string $channel, stdClass $payload): bool
{
$payload->appId = $appId;
$payload->serverId = $this->serverId;
$this->publishClient->__call('publish', ["$appId:$channel", json_encode($payload)]);
return true;
}
/**
* Add a member to a channel. To be called when they have
* subscribed to the channel.
*
* @param string $appId
* @param string $channel
* @param string $socketId
* @param string $data
* @return void
*/
public function joinChannel(string $appId, string $channel, string $socketId, string $data)
{
$this->publishClient->__call('hset', ["$appId:$channel", $socketId, $data]);
}
/**
* Remove a member from the channel. To be called when they have
* unsubscribed from the channel.
*
* @param string $appId
* @param string $channel
* @param string $socketId
* @return void
*/
public function leaveChannel(string $appId, string $channel, string $socketId)
{
$this->publishClient->__call('hdel', ["$appId:$channel", $socketId]);
}
/**
* Retrieve the full information about the members in a presence channel.
*
* @param string $appId
* @param string $channel
* @return PromiseInterface
*/
public function channelMembers(string $appId, string $channel): PromiseInterface
{
return $this->publishClient->__call('hgetall', ["$appId:$channel"])
->then(function ($members) {
// The data is expected as objects, so we need to JSON decode
return array_map(function ($user) {
return json_decode($user);
}, $members);
});
}
/**
* Get the amount of users subscribed for each presence channel.
*
* @param string $appId
* @param array $channelNames
* @return PromiseInterface
*/
public function channelMemberCounts(string $appId, array $channelNames): PromiseInterface
{
$this->publishClient->__call('multi', []);
foreach ($channelNames as $channel) {
$this->publishClient->__call('hlen', ["$appId:$channel"]);
}
return $this->publishClient->__call('exec', [])
->then(function ($data) use ($channelNames) {
return array_combine($channelNames, $data);
});
} }
/** /**

View File

@ -0,0 +1,47 @@
<?php
namespace BeyondCode\LaravelWebSockets\Statistics\Logger;
use BeyondCode\LaravelWebSockets\WebSockets\Channels\ChannelManager;
use Clue\React\Buzz\Browser;
use Ratchet\ConnectionInterface;
class NullStatisticsLogger implements StatisticsLogger
{
/** @var \BeyondCode\LaravelWebSockets\WebSockets\Channels\ChannelManager */
protected $channelManager;
/** @var \Clue\React\Buzz\Browser */
protected $browser;
public function __construct(ChannelManager $channelManager, Browser $browser)
{
$this->channelManager = $channelManager;
$this->browser = $browser;
}
public function webSocketMessage(ConnectionInterface $connection)
{
//
}
public function apiMessage($appId)
{
//
}
public function connection(ConnectionInterface $connection)
{
//
}
public function disconnection(ConnectionInterface $connection)
{
//
}
public function save()
{
//
}
}

View File

@ -82,7 +82,10 @@ class Channel
$this->replicator->unsubscribe($connection->app->id, $this->channelName); $this->replicator->unsubscribe($connection->app->id, $this->channelName);
if (! $this->hasConnections()) { if (! $this->hasConnections()) {
DashboardLogger::vacated($connection, $this->channelName); DashboardLogger::log($connection->app->id, DashboardLogger::TYPE_VACATED, [
'socketId' => $connection->socketId,
'channel' => $this->channelName,
]);
} }
} }
@ -93,10 +96,15 @@ class Channel
$this->subscribedConnections[$connection->socketId] = $connection; $this->subscribedConnections[$connection->socketId] = $connection;
if (! $hadConnectionsPreviously) { if (! $hadConnectionsPreviously) {
DashboardLogger::occupied($connection, $this->channelName); DashboardLogger::log($connection->app->id, DashboardLogger::TYPE_OCCUPIED, [
'channel' => $this->channelName,
]);
} }
DashboardLogger::subscribed($connection, $this->channelName); DashboardLogger::log($connection->app->id, DashboardLogger::TYPE_SUBSCRIBED, [
'socketId' => $connection->socketId,
'channel' => $this->channelName,
]);
} }
public function broadcast($payload) public function broadcast($payload)

View File

@ -38,7 +38,12 @@ class PusherClientMessage implements PusherMessage
return; return;
} }
DashboardLogger::clientMessage($this->connection, $this->payload); DashboardLogger::log($this->connection->app->id, DashboardLogger::TYPE_WS_MESSAGE, [
'socketId' => $this->connection->socketId,
'channel' => $this->payload->channel,
'event' => $this->payload->event,
'data' => $this->payload,
]);
$channel = $this->channelManager->find($this->connection->app->id, $this->payload->channel); $channel = $this->channelManager->find($this->connection->app->id, $this->payload->channel);

View File

@ -48,7 +48,9 @@ class WebSocketHandler implements MessageComponentInterface
{ {
$this->channelManager->removeFromAllChannels($connection); $this->channelManager->removeFromAllChannels($connection);
DashboardLogger::disconnection($connection); DashboardLogger::log($connection->app->id, DashboardLogger::TYPE_DISCONNECTED, [
'socketId' => $connection->socketId,
]);
StatisticsLogger::disconnection($connection); StatisticsLogger::disconnection($connection);
} }
@ -106,7 +108,13 @@ class WebSocketHandler implements MessageComponentInterface
]), ]),
])); ]));
DashboardLogger::connection($connection); /** @var \GuzzleHttp\Psr7\Request $request */
$request = $connection->httpRequest;
DashboardLogger::log($connection->app->id, DashboardLogger::TYPE_CONNECTED, [
'origin' => "{$request->getUri()->getScheme()}://{$request->getUri()->getHost()}",
'socketId' => $connection->socketId,
]);
StatisticsLogger::connection($connection); StatisticsLogger::connection($connection);