Added federation ping, loopback and dashboard (#14007)
* Added federation ping event to check if setup works * Update en.i18n.json * Added ping process and status tracker * Added new ping system, also a federation dashboard * Line wrap fixes * Fix migrationpull/14265/head^2
parent
85530cfa28
commit
e02187278b
@ -0,0 +1,141 @@ |
||||
.status { |
||||
flex: 0 0 auto; |
||||
|
||||
width: 6px; |
||||
height: 6px; |
||||
margin: 0 7px; |
||||
|
||||
border-radius: 50%; |
||||
} |
||||
|
||||
.status.stable { |
||||
background-color: #2de0a5; |
||||
} |
||||
|
||||
.status.unstable { |
||||
background-color: #ffd21f; |
||||
} |
||||
|
||||
.status.failing { |
||||
background-color: #f5455c; |
||||
} |
||||
|
||||
.frame { |
||||
display: flex; |
||||
flex-direction: row; |
||||
} |
||||
|
||||
.group { |
||||
display: flex; |
||||
flex-direction: row; |
||||
flex: 100%; |
||||
|
||||
max-width: 100%; |
||||
margin: 10px; |
||||
|
||||
border-width: 1px; |
||||
align-items: center; |
||||
justify-content: center; |
||||
} |
||||
|
||||
.group.left { |
||||
justify-content: flex-start; |
||||
} |
||||
|
||||
.group.wrap { |
||||
flex-wrap: wrap; |
||||
} |
||||
|
||||
.overview-column { |
||||
flex: 100%; |
||||
|
||||
min-height: 20px; |
||||
margin: 15px 0; |
||||
} |
||||
|
||||
.overview-column.small { |
||||
max-width: 20%; |
||||
} |
||||
|
||||
.group .overview-column:not(:last-child) { |
||||
border-right: 1px solid #e9e9e9; |
||||
} |
||||
|
||||
.group .overview-column:nth-child(5n) { |
||||
border-right: 0; |
||||
} |
||||
|
||||
.overview-pill { |
||||
display: flex; |
||||
|
||||
width: 100%; |
||||
padding: 0 10px; |
||||
|
||||
user-select: text; |
||||
text-align: center; |
||||
align-items: center; |
||||
} |
||||
|
||||
.overview-item { |
||||
width: 100%; |
||||
|
||||
user-select: text; |
||||
text-align: center; |
||||
} |
||||
|
||||
.overview-item > .title { |
||||
display: inline-block; |
||||
|
||||
margin-top: 8px; |
||||
|
||||
text-transform: uppercase; |
||||
|
||||
color: #9ea2a8; |
||||
|
||||
font-size: 0.875rem; |
||||
font-weight: 300; |
||||
} |
||||
|
||||
.overview-item > .value { |
||||
display: inline-block; |
||||
|
||||
width: 100%; |
||||
|
||||
text-transform: capitalize; |
||||
|
||||
color: #383838; |
||||
|
||||
font-size: 1.75rem; |
||||
font-weight: 400; |
||||
line-height: 1; |
||||
} |
||||
|
||||
@media screen and (max-width: 925px) { |
||||
.overview-item > .title { |
||||
font-size: 0.5rem; |
||||
} |
||||
|
||||
.overview-item > .value { |
||||
font-size: 1rem; |
||||
} |
||||
} |
||||
|
||||
@media screen and (max-width: 800px) { |
||||
.overview-item > .title { |
||||
font-size: 0.875rem; |
||||
} |
||||
|
||||
.overview-item > .value { |
||||
font-size: 1.75rem; |
||||
} |
||||
} |
||||
|
||||
@media screen and (max-width: 600px) { |
||||
.overview-item > .title { |
||||
font-size: 0.5rem; |
||||
} |
||||
|
||||
.overview-item > .value { |
||||
font-size: 1rem; |
||||
} |
||||
} |
@ -0,0 +1,33 @@ |
||||
<template name="dashboard"> |
||||
<div class="main-content-flex"> |
||||
<section class="page-container page-list flex-tab-main-content"> |
||||
{{> header sectionName="Federation_Dashboard"}} |
||||
<div class="content"> |
||||
<div class="section"> |
||||
<div class="section-content"> |
||||
<div class="group border-component-color"> |
||||
{{#each federationOverviewData}} |
||||
<div class="overview-column"> |
||||
<div class="overview-item"> |
||||
<span class="value">{{value}}</span> |
||||
<span class="title">{{_ title}}</span> |
||||
</div> |
||||
</div> |
||||
{{/each}} |
||||
</div> |
||||
<div class="group left wrap border-component-color"> |
||||
{{#each federationPeerStatuses}} |
||||
<div class="overview-column small"> |
||||
<div class="overview-pill" title="{{status}} - {{statusAt}}"> |
||||
<div class="status {{status}}"></div> |
||||
<span class="title">{{peer}}</span> |
||||
</div> |
||||
</div> |
||||
{{/each}} |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</section> |
||||
</div> |
||||
</template> |
@ -0,0 +1,92 @@ |
||||
import { Meteor } from 'meteor/meteor'; |
||||
import { Tracker } from 'meteor/tracker'; |
||||
import { Template } from 'meteor/templating'; |
||||
import { ReactiveVar } from 'meteor/reactive-var'; |
||||
|
||||
import { BlazeLayout } from 'meteor/kadira:blaze-layout'; |
||||
import { FlowRouter } from 'meteor/kadira:flow-router'; |
||||
import { AdminBox } from '../../../ui-utils'; |
||||
import { hasRole } from '../../../authorization'; |
||||
|
||||
import './dashboard.html'; |
||||
import './dashboard.css'; |
||||
|
||||
// Template controller
|
||||
let templateInstance; // current template instance/context
|
||||
|
||||
// Methods
|
||||
const updateOverviewData = () => { |
||||
Meteor.call('federation:getOverviewData', (error, result) => { |
||||
if (error) { |
||||
console.log(error); |
||||
|
||||
return; |
||||
// return handleError(error);
|
||||
} |
||||
|
||||
const { data } = result; |
||||
|
||||
templateInstance.federationOverviewData.set(data); |
||||
}); |
||||
}; |
||||
|
||||
const updatePeerStatuses = () => { |
||||
Meteor.call('federation:getPeerStatuses', (error, result) => { |
||||
if (error) { |
||||
console.log(error); |
||||
|
||||
return; |
||||
// return handleError(error);
|
||||
} |
||||
|
||||
const { data } = result; |
||||
|
||||
templateInstance.federationPeerStatuses.set(data); |
||||
}); |
||||
}; |
||||
|
||||
const updateData = () => { |
||||
updateOverviewData(); |
||||
updatePeerStatuses(); |
||||
}; |
||||
|
||||
Template.dashboard.helpers({ |
||||
federationOverviewData() { |
||||
return templateInstance.federationOverviewData.get(); |
||||
}, |
||||
federationPeerStatuses() { |
||||
return templateInstance.federationPeerStatuses.get(); |
||||
}, |
||||
}); |
||||
|
||||
// Events
|
||||
Template.dashboard.onCreated(function() { |
||||
templateInstance = Template.instance(); |
||||
|
||||
this.federationOverviewData = new ReactiveVar(); |
||||
this.federationPeerStatuses = new ReactiveVar(); |
||||
}); |
||||
|
||||
Template.dashboard.onRendered(() => { |
||||
Tracker.autorun(updateData); |
||||
|
||||
setInterval(updateData, 10000); |
||||
}); |
||||
|
||||
// Route setup
|
||||
|
||||
FlowRouter.route('/admin/federation-dashboard', { |
||||
name: 'federation-dashboard', |
||||
action() { |
||||
BlazeLayout.render('main', { center: 'dashboard', old: true }); |
||||
}, |
||||
}); |
||||
|
||||
AdminBox.addOption({ |
||||
icon: 'discover', |
||||
href: 'admin/federation-dashboard', |
||||
i18nLabel: 'Federation Dashboard', |
||||
permissionGranted() { |
||||
return hasRole(Meteor.userId(), 'admin'); |
||||
}, |
||||
}); |
@ -1,23 +1,2 @@ |
||||
import { MessageTypes } from '../../ui-utils/client'; |
||||
|
||||
// Register message types
|
||||
MessageTypes.registerType({ |
||||
id: 'rejected-message-by-peer', |
||||
system: true, |
||||
message: 'This_message_was_rejected_by__peer__peer', |
||||
data(message) { |
||||
return { |
||||
peer: message.peer, |
||||
}; |
||||
}, |
||||
}); |
||||
MessageTypes.registerType({ |
||||
id: 'peer-does-not-exist', |
||||
system: true, |
||||
message: 'The_peer__peer__does_not_exist', |
||||
data(message) { |
||||
return { |
||||
peer: message.peer, |
||||
}; |
||||
}, |
||||
}); |
||||
import './messageTypes'; |
||||
import './admin/dashboard'; |
||||
|
@ -0,0 +1,23 @@ |
||||
import { MessageTypes } from '../../ui-utils/client'; |
||||
|
||||
// Register message types
|
||||
MessageTypes.registerType({ |
||||
id: 'rejected-message-by-peer', |
||||
system: true, |
||||
message: 'This_message_was_rejected_by__peer__peer', |
||||
data(message) { |
||||
return { |
||||
peer: message.peer, |
||||
}; |
||||
}, |
||||
}); |
||||
MessageTypes.registerType({ |
||||
id: 'peer-does-not-exist', |
||||
system: true, |
||||
message: 'The_peer__peer__does_not_exist', |
||||
data(message) { |
||||
return { |
||||
peer: message.peer, |
||||
}; |
||||
}, |
||||
}); |
@ -0,0 +1,38 @@ |
||||
import { Meteor } from 'meteor/meteor'; |
||||
import { logger } from './logger'; |
||||
|
||||
import { FederationPeers } from '../../models'; |
||||
|
||||
import { ping } from './methods/ping'; |
||||
|
||||
import moment from 'moment'; |
||||
|
||||
export class PeerPinger { |
||||
constructor() { |
||||
this.config = { |
||||
pingInterval: 5000, |
||||
}; |
||||
|
||||
this.peers = []; |
||||
} |
||||
|
||||
log(message) { |
||||
logger.pinger.info(message); |
||||
} |
||||
|
||||
start() { |
||||
this.pingAllPeers(); |
||||
} |
||||
|
||||
pingAllPeers() { |
||||
const lastSeenAt = moment().subtract(10, 'm').toDate(); |
||||
|
||||
const peers = FederationPeers.find({ $or: [{ last_seen_at: null }, { last_seen_at: { $lte: lastSeenAt } }] }).fetch(); |
||||
|
||||
const pingResults = ping(peers.map((p) => p.peer)); |
||||
|
||||
FederationPeers.updateStatuses(pingResults); |
||||
|
||||
Meteor.setTimeout(this.pingAllPeers.bind(this), this.config.pingInterval); |
||||
} |
||||
} |
@ -0,0 +1,69 @@ |
||||
import { Meteor } from 'meteor/meteor'; |
||||
import moment from 'moment'; |
||||
|
||||
import { FederationEvents, FederationPeers, Users } from '../../../models'; |
||||
import { Federation } from '..'; |
||||
|
||||
export function federationGetOverviewData() { |
||||
if (!Meteor.userId()) { |
||||
throw new Meteor.Error('not-authorized'); |
||||
} |
||||
|
||||
const numberOfEvents = FederationEvents.find({ t: { $ne: 'png' } }).count(); |
||||
const numberOfFederatedUsers = Users.find({ federation: { $exists: true }, 'federation.peer': { $ne: Federation.localIdentifier } }).count(); |
||||
const numberOfActivePeers = FederationPeers.find({ active: true, peer: { $ne: Federation.localIdentifier } }).count(); |
||||
const numberOfInactivePeers = FederationPeers.find({ active: false, peer: { $ne: Federation.localIdentifier } }).count(); |
||||
|
||||
return { |
||||
data: [{ |
||||
title: 'Number_of_events', |
||||
value: numberOfEvents, |
||||
}, { |
||||
title: 'Number_of_federated_users', |
||||
value: numberOfFederatedUsers, |
||||
}, { |
||||
title: 'Number_of_active_peers', |
||||
value: numberOfActivePeers, |
||||
}, { |
||||
title: 'Number_of_inactive_peers', |
||||
value: numberOfInactivePeers, |
||||
}], |
||||
}; |
||||
} |
||||
|
||||
export function federationGetPeerStatuses() { |
||||
if (!Meteor.userId()) { |
||||
throw new Meteor.Error('not-authorized'); |
||||
} |
||||
|
||||
const peers = FederationPeers.find({ peer: { $ne: Federation.localIdentifier } }).fetch(); |
||||
|
||||
const peerStatuses = []; |
||||
|
||||
const stabilityLimit = moment().subtract(5, 'days'); |
||||
|
||||
for (const { peer, active, last_seen_at: lastSeenAt, last_failure_at: lastFailureAt } of peers) { |
||||
let status = 'failing'; |
||||
|
||||
if (active && lastFailureAt && moment(lastFailureAt).isAfter(stabilityLimit)) { |
||||
status = 'unstable'; |
||||
} else if (active) { |
||||
status = 'stable'; |
||||
} |
||||
|
||||
peerStatuses.push({ |
||||
peer, |
||||
status, |
||||
statusAt: active ? lastSeenAt : lastFailureAt, |
||||
}); |
||||
} |
||||
|
||||
return { |
||||
data: peerStatuses, |
||||
}; |
||||
} |
||||
|
||||
Meteor.methods({ |
||||
'federation:getOverviewData': federationGetOverviewData, |
||||
'federation:getPeerStatuses': federationGetPeerStatuses, |
||||
}); |
@ -0,0 +1,53 @@ |
||||
import { Meteor } from 'meteor/meteor'; |
||||
import { FederationEvents } from '../../../models'; |
||||
import { settings } from '../../../settings'; |
||||
import { delay } from '../PeerHTTP/utils'; |
||||
|
||||
export function ping(peers, timeToWait = 5000) { |
||||
// Create the ping events
|
||||
const pingEvents = FederationEvents.ping(peers); |
||||
|
||||
// Make sure timeToWait is at least one second
|
||||
timeToWait = timeToWait < 1000 ? 1000 : timeToWait; |
||||
|
||||
const results = {}; |
||||
|
||||
while (timeToWait > 0) { |
||||
timeToWait -= 500; |
||||
delay(500); |
||||
|
||||
for (const { _id: pingEventId } of pingEvents) { |
||||
// Get the ping event
|
||||
const pingEvent = FederationEvents.findOne({ _id: pingEventId }); |
||||
|
||||
if (!pingEvent.fulfilled && !pingEvent.error) { continue; } |
||||
|
||||
// If there is an error or the event is fulfilled, it means it is already handled.
|
||||
// Given that, fulfilled will be true if everything went well, or false if there was an error;
|
||||
results[pingEvent.peer] = pingEvent.fulfilled; |
||||
} |
||||
|
||||
// If we already have all the results, break
|
||||
if (Object.keys(results).length === peers.length) { |
||||
break; |
||||
} |
||||
} |
||||
|
||||
return results; |
||||
} |
||||
|
||||
Meteor.methods({ |
||||
FEDERATION_Test_Setup() { |
||||
const localPeer = settings.get('FEDERATION_Domain'); |
||||
|
||||
const results = ping([localPeer]); |
||||
|
||||
if (!results[localPeer]) { |
||||
throw new Meteor.Error('FEDERATION_Test_Setup_Error'); |
||||
} |
||||
|
||||
return { |
||||
message: 'FEDERATION_Test_Setup_Success', |
||||
}; |
||||
}, |
||||
}); |
@ -0,0 +1,52 @@ |
||||
import { Meteor } from 'meteor/meteor'; |
||||
|
||||
import { Base } from './_Base'; |
||||
import { Users } from '..'; |
||||
|
||||
class FederationPeersModel extends Base { |
||||
constructor() { |
||||
super('federation_peers'); |
||||
} |
||||
|
||||
refreshPeers() { |
||||
const collectionObj = this.model.rawCollection(); |
||||
const findAndModify = Meteor.wrapAsync(collectionObj.findAndModify, collectionObj); |
||||
|
||||
const users = Users.find({ federation: { $exists: true } }, { fields: { federation: 1 } }).fetch(); |
||||
|
||||
const peers = [...new Set(users.map((u) => u.federation.peer))]; |
||||
|
||||
for (const peer of peers) { |
||||
findAndModify({ peer }, [], { |
||||
$setOnInsert: { |
||||
active: false, |
||||
peer, |
||||
last_seen_at: null, |
||||
last_failure_at: null, |
||||
}, |
||||
}, { upsert: true }); |
||||
} |
||||
|
||||
this.remove({ peer: { $nin: peers } }); |
||||
} |
||||
|
||||
updateStatuses(seenPeers) { |
||||
for (const peer of Object.keys(seenPeers)) { |
||||
const seen = seenPeers[peer]; |
||||
|
||||
const updateQuery = {}; |
||||
|
||||
if (seen) { |
||||
updateQuery.active = true; |
||||
updateQuery.last_seen_at = new Date(); |
||||
} else { |
||||
updateQuery.active = false; |
||||
updateQuery.last_failure_at = new Date(); |
||||
} |
||||
|
||||
this.update({ peer }, { $set: updateQuery }); |
||||
} |
||||
} |
||||
} |
||||
|
||||
export const FederationPeers = new FederationPeersModel(); |
@ -0,0 +1,21 @@ |
||||
import { Migrations } from '../../../app/migrations/server'; |
||||
import { Users, FederationPeers } from '../../../app/models/server'; |
||||
|
||||
Migrations.add({ |
||||
version: 143, |
||||
up() { |
||||
const users = Users.find({ federation: { $exists: true } }, { fields: { federation: 1 } }).fetch(); |
||||
|
||||
let peers = [...new Set(users.map((u) => u.federation.peer))]; |
||||
|
||||
peers = peers.map((peer) => ({ |
||||
active: false, |
||||
peer, |
||||
last_seen_at: null, |
||||
})); |
||||
|
||||
if (peers.length) { |
||||
FederationPeers.model.rawCollection().insertMany(peers); |
||||
} |
||||
}, |
||||
}); |
Loading…
Reference in new issue