Merge pull request #467 from beyondcode/refactor/dashboard
[2.x] Refactoring dashboard
This commit is contained in:
commit
b174ffb6e7
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -1,265 +1,423 @@
|
||||||
<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>
|
|
||||||
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" rel="stylesheet"
|
<title>WebSockets Dashboard</title>
|
||||||
integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
|
|
||||||
<script
|
<link href="https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css" rel="stylesheet">
|
||||||
src="https://code.jquery.com/jquery-3.3.1.min.js"
|
<link href="https://cdn.jsdelivr.net/npm/vue-json-editor@1.4.2/assets/jsoneditor.min.css" rel="stylesheet">
|
||||||
integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8="
|
|
||||||
crossorigin="anonymous"></script>
|
<script src="https://js.pusher.com/7.0/pusher.min.js"></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>
|
}"
|
||||||
</select>
|
>
|
||||||
<label class="my-1 mr-2" for="app">Port:</label>
|
<div class="w-full my-6 rounded-lg bg-gray-100 p-6">
|
||||||
<input class="form-control form-control-sm mr-2" v-model="port" placeholder="Port">
|
<div class="font-semibold uppercase text-gray-700 mb-6">
|
||||||
<button v-if="! connected" type="submit" @click.prevent="connect" class="mr-2 btn btn-sm btn-primary">
|
Connect to app
|
||||||
Connect
|
</div>
|
||||||
</button>
|
|
||||||
<button v-if="connected" type="submit" @click.prevent="disconnect" class="btn btn-sm btn-danger">
|
|
||||||
Disconnect
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
<div id="status"></div>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div v-if="connected && app.statisticsEnabled">
|
|
||||||
<h4>Realtime Statistics</h4>
|
|
||||||
<div id="statisticsChart" style="width: 100%; height: 250px;"></div>
|
|
||||||
</div>
|
|
||||||
<div v-if="connected">
|
|
||||||
<h4>Event Creator</h4>
|
|
||||||
<form>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col">
|
|
||||||
<input type="text" class="form-control" v-model="form.channel" placeholder="Channel">
|
|
||||||
</div>
|
|
||||||
<div class="col">
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<h4>Events</h4>
|
|
||||||
<table id="events" class="table table-striped table-hover">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Type</th>
|
|
||||||
<th>Details</th>
|
|
||||||
<th>Time</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr v-for="log in logs.slice().reverse()">
|
|
||||||
<td><span class="badge" :class="getBadgeClass(log)">@{{ log.type }}</span></td>
|
|
||||||
<td><pre>@{{ log.details }}</pre></td>
|
|
||||||
<td>@{{ log.time }}</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<script>
|
|
||||||
new Vue({
|
|
||||||
el: '#app',
|
|
||||||
|
|
||||||
data: {
|
<div class="flex flex-row justify-between">
|
||||||
connected: false,
|
<div class="relative">
|
||||||
chart: null,
|
<select
|
||||||
pusher: null,
|
v-model="app"
|
||||||
app: null,
|
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"
|
||||||
port: {{ $port }},
|
id="grid-state"
|
||||||
apps: {!! json_encode($apps) !!},
|
>
|
||||||
form: {
|
<option
|
||||||
|
v-for="app in apps"
|
||||||
|
:value="app"
|
||||||
|
>
|
||||||
|
@{{ app.name }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<div class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-gray-700">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<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
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
v-else
|
||||||
|
@click.prevent="connect"
|
||||||
|
class="rounded-full px-3 py-2 text-white focus:outline-none"
|
||||||
|
:class="{
|
||||||
|
'bg-green-500 hover:bg-green-600': ! connecting,
|
||||||
|
'bg-gray-500 cursor-not-allowed': connecting,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<template v-if="connecting">
|
||||||
|
Connecting...
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
Connect
|
||||||
|
</template>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
<tr>
|
||||||
|
<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">
|
||||||
|
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">
|
||||||
|
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>
|
||||||
|
</thead>
|
||||||
|
<tbody class="bg-white divide-y divide-gray-200">
|
||||||
|
<tr
|
||||||
|
v-for="(log, index) in logs.slice().reverse()"
|
||||||
|
:key="index"
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
new Vue({
|
||||||
|
el: '#app',
|
||||||
|
data: {
|
||||||
|
connected: false,
|
||||||
|
connecting: false,
|
||||||
|
sendingEvent: false,
|
||||||
|
chart: null,
|
||||||
|
pusher: null,
|
||||||
|
app: null,
|
||||||
|
port: {{ config('websockets.dashboard.port') }},
|
||||||
|
apps: @json($apps),
|
||||||
|
form: {
|
||||||
|
channel: null,
|
||||||
|
event: null,
|
||||||
|
data: null,
|
||||||
|
},
|
||||||
|
logs: [],
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
this.app = this.apps[0] || null;
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
connect () {
|
||||||
|
this.connecting = true;
|
||||||
|
|
||||||
|
this.pusher = new Pusher(this.app.key, {
|
||||||
|
wsHost: this.app.host === null ? window.location.hostname : this.app.host,
|
||||||
|
wsPort: this.port === null ? 6001 : this.port,
|
||||||
|
wssPort: this.port === null ? 6001 : this.port,
|
||||||
|
wsPath: this.app.path === null ? '' : this.app.path,
|
||||||
|
disableStats: true,
|
||||||
|
authEndpoint: `${window.baseURL}/auth`,
|
||||||
|
auth: {
|
||||||
|
headers: {
|
||||||
|
'X-CSRF-Token': "{{ csrf_token() }}",
|
||||||
|
'X-App-ID': this.app.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
enabledTransports: ['ws', 'wss'],
|
||||||
|
forceTLS: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.pusher.connection.bind('state_change', states => {
|
||||||
|
this.connecting = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.pusher.connection.bind('connected', () => {
|
||||||
|
this.connected = true;
|
||||||
|
this.connecting = false;
|
||||||
|
|
||||||
|
if (this.app.statisticsEnabled) {
|
||||||
|
this.loadChart();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.pusher.connection.bind('disconnected', () => {
|
||||||
|
this.connected = false;
|
||||||
|
this.connecting = false;
|
||||||
|
this.logs = [];
|
||||||
|
});
|
||||||
|
|
||||||
|
this.pusher.connection.bind('error', event => {
|
||||||
|
if (event.error.data.code === 4100) {
|
||||||
|
this.connected = false;
|
||||||
|
this.logs = [];
|
||||||
|
|
||||||
|
throw new Error("Over capacity");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.connecting = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.subscribeToAllChannels();
|
||||||
|
this.subscribeToStatistics();
|
||||||
|
},
|
||||||
|
|
||||||
|
disconnect () {
|
||||||
|
this.pusher.disconnect();
|
||||||
|
this.connecting = false;
|
||||||
|
},
|
||||||
|
|
||||||
|
loadChart () {
|
||||||
|
axios.get(`/api/${this.app.id}/statistics`)
|
||||||
|
.then(res => {
|
||||||
|
let data = res.data;
|
||||||
|
|
||||||
|
let chartData = [
|
||||||
|
{
|
||||||
|
x: data.peak_connections.x,
|
||||||
|
y: data.peak_connections.y,
|
||||||
|
type: 'lines',
|
||||||
|
name: '# Peak Connections'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x: data.websocket_message_count.x,
|
||||||
|
y: data.websocket_message_count.y,
|
||||||
|
type: 'bar',
|
||||||
|
name: '# Websocket Messages'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x: data.api_message_count.x,
|
||||||
|
y: data.api_message_count.y,
|
||||||
|
type: 'bar',
|
||||||
|
name: '# API Messages'
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
let layout = {
|
||||||
|
margin: {
|
||||||
|
l: 50,
|
||||||
|
r: 0,
|
||||||
|
b: 50,
|
||||||
|
t: 50,
|
||||||
|
pad: 4,
|
||||||
|
},
|
||||||
|
autosize: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.chart = Plotly.newPlot('statisticsChart', chartData, layout);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
subscribeToAllChannels () {
|
||||||
|
@json($channels).forEach(channelName => this.subscribeToChannel(channelName))
|
||||||
|
},
|
||||||
|
|
||||||
|
subscribeToChannel (channel) {
|
||||||
|
this.pusher.subscribe(`{{ $logPrefix }}${channel}`)
|
||||||
|
.bind('log-message', (data) => {
|
||||||
|
this.logs.push(data);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
subscribeToStatistics () {
|
||||||
|
this.pusher.subscribe('{{ $logPrefix }}statistics')
|
||||||
|
.bind('statistics-updated', (data) => {
|
||||||
|
var update = {
|
||||||
|
x: [[data.time], [data.time], [data.time]],
|
||||||
|
y: [[data.peak_connection_count], [data.websocket_message_count], [data.api_message_count]],
|
||||||
|
};
|
||||||
|
|
||||||
|
Plotly.extendTraces('statisticsChart', update, [0, 1, 2]);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
sendEvent () {
|
||||||
|
if (! this.sendingEvent) {
|
||||||
|
this.sendingEvent = true;
|
||||||
|
|
||||||
|
let payload = {
|
||||||
|
_token: '{{ csrf_token() }}',
|
||||||
|
key: this.app.key,
|
||||||
|
secret: this.app.secret,
|
||||||
|
appId: this.app.id,
|
||||||
|
channel: this.form.channel,
|
||||||
|
event: this.form.event,
|
||||||
|
data: JSON.stringify(this.form.data),
|
||||||
|
};
|
||||||
|
|
||||||
|
axios
|
||||||
|
.post('/event', payload)
|
||||||
|
.then(() => {
|
||||||
|
this.form = {
|
||||||
channel: null,
|
channel: null,
|
||||||
event: null,
|
event: null,
|
||||||
data: null
|
data: null,
|
||||||
},
|
};
|
||||||
logs: [],
|
})
|
||||||
},
|
.catch(err => {
|
||||||
|
alert('Error sending event.');
|
||||||
mounted() {
|
})
|
||||||
this.app = this.apps[0] || null;
|
.then(() => {
|
||||||
},
|
this.sendingEvent = false;
|
||||||
|
});
|
||||||
methods: {
|
|
||||||
connect() {
|
|
||||||
this.pusher = new Pusher(this.app.key, {
|
|
||||||
wsHost: this.app.host === null ? window.location.hostname : this.app.host,
|
|
||||||
wsPort: this.port === null ? 6001 : this.port,
|
|
||||||
wssPort: this.port === null ? 6001 : this.port,
|
|
||||||
wsPath: this.app.path === null ? '' : this.app.path,
|
|
||||||
disableStats: true,
|
|
||||||
authEndpoint: '{{ url(request()->path().'/auth') }}',
|
|
||||||
auth: {
|
|
||||||
headers: {
|
|
||||||
'X-CSRF-Token': "{{ csrf_token() }}",
|
|
||||||
'X-App-ID': this.app.id
|
|
||||||
}
|
|
||||||
},
|
|
||||||
enabledTransports: ['ws', 'flash']
|
|
||||||
});
|
|
||||||
|
|
||||||
this.pusher.connection.bind('state_change', states => {
|
|
||||||
$('div#status').text("Channels current state is " + states.current);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.pusher.connection.bind('connected', () => {
|
|
||||||
this.connected = true;
|
|
||||||
|
|
||||||
this.loadChart();
|
|
||||||
});
|
|
||||||
|
|
||||||
this.pusher.connection.bind('disconnected', () => {
|
|
||||||
this.connected = false;
|
|
||||||
this.logs = [];
|
|
||||||
});
|
|
||||||
|
|
||||||
this.pusher.connection.bind('error', event => {
|
|
||||||
if (event.error.data.code === 4100) {
|
|
||||||
$('div#status').text("Maximum connection limit exceeded!");
|
|
||||||
this.connected = false;
|
|
||||||
this.logs = [];
|
|
||||||
throw new Error("Over capacity");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.subscribeToAllChannels();
|
|
||||||
|
|
||||||
this.subscribeToStatistics();
|
|
||||||
},
|
|
||||||
|
|
||||||
disconnect() {
|
|
||||||
this.pusher.disconnect();
|
|
||||||
},
|
|
||||||
|
|
||||||
loadChart() {
|
|
||||||
$.getJSON('{{ url(request()->path().'/api') }}/' + this.app.id + '/statistics', (data) => {
|
|
||||||
|
|
||||||
let chartData = [
|
|
||||||
{
|
|
||||||
x: data.peak_connections.x,
|
|
||||||
y: data.peak_connections.y,
|
|
||||||
type: 'lines',
|
|
||||||
name: '# Peak Connections'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
x: data.websocket_message_count.x,
|
|
||||||
y: data.websocket_message_count.y,
|
|
||||||
type: 'bar',
|
|
||||||
name: '# Websocket Messages'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
x: data.api_message_count.x,
|
|
||||||
y: data.api_message_count.y,
|
|
||||||
type: 'bar',
|
|
||||||
name: '# API Messages'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
let layout = {
|
|
||||||
margin: {
|
|
||||||
l: 50,
|
|
||||||
r: 0,
|
|
||||||
b: 50,
|
|
||||||
t: 50,
|
|
||||||
pad: 4
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
this.chart = Plotly.newPlot('statisticsChart', chartData, layout);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
subscribeToAllChannels() {
|
|
||||||
[
|
|
||||||
'disconnection',
|
|
||||||
'connection',
|
|
||||||
'vacated',
|
|
||||||
'occupied',
|
|
||||||
'subscribed',
|
|
||||||
'client-message',
|
|
||||||
'api-message',
|
|
||||||
'replicator-subscribed',
|
|
||||||
'replicator-unsubscribed',
|
|
||||||
].forEach(channelName => this.subscribeToChannel(channelName))
|
|
||||||
},
|
|
||||||
|
|
||||||
subscribeToChannel(channel) {
|
|
||||||
this.pusher.subscribe('{{ \BeyondCode\LaravelWebSockets\Dashboard\DashboardLogger::LOG_CHANNEL_PREFIX }}' + channel)
|
|
||||||
.bind('log-message', (data) => {
|
|
||||||
this.logs.push(data);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
subscribeToStatistics() {
|
|
||||||
this.pusher.subscribe('{{ \BeyondCode\LaravelWebSockets\Dashboard\DashboardLogger::LOG_CHANNEL_PREFIX }}statistics')
|
|
||||||
.bind('statistics-updated', (data) => {
|
|
||||||
var update = {
|
|
||||||
x: [[data.time], [data.time], [data.time]],
|
|
||||||
y: [[data.peak_connection_count], [data.websocket_message_count], [data.api_message_count]]
|
|
||||||
};
|
|
||||||
|
|
||||||
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() {
|
|
||||||
$.post('{{ url(request()->path().'/event') }}', {
|
|
||||||
_token: '{{ csrf_token() }}',
|
|
||||||
key: this.app.key,
|
|
||||||
secret: this.app.secret,
|
|
||||||
appId: this.app.id,
|
|
||||||
channel: this.form.channel,
|
|
||||||
event: this.form.event,
|
|
||||||
data: this.form.data,
|
|
||||||
}).fail(() => {
|
|
||||||
alert('Error sending event.');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
|
||||||
|
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>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ use BeyondCode\LaravelWebSockets\Statistics\Logger\StatisticsLogger as Statistic
|
||||||
use Illuminate\Support\Facades\Facade;
|
use Illuminate\Support\Facades\Facade;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @see \BeyondCode\LaravelWebSockets\Statistics\Logger\HttpStatisticsLogger
|
* @see \BeyondCode\LaravelWebSockets\Statistics\Logger\HttpStatisticsLogger
|
||||||
* @mixin \BeyondCode\LaravelWebSockets\Statistics\Logger\HttpStatisticsLogger
|
* @mixin \BeyondCode\LaravelWebSockets\Statistics\Logger\HttpStatisticsLogger
|
||||||
*/
|
*/
|
||||||
class StatisticsLogger extends Facade
|
class StatisticsLogger extends Facade
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue