Skip to content

Commit

Permalink
V0.13.0 (#135)
Browse files Browse the repository at this point in the history
* add labels to main left navigation

* node -> member legacy renaming

* add advisor rules for #132

* basics of memory feed statistics for cluster members

* add utilization stats into diagnostic package

* basic OOM warning detection

* human data sizes

* first commit of alerting infrastructure

* successful manager commands trigger alerts

* clean up "About" panel per Hugo's feedback

* update toast dependency

* put Halin version in the header proper

* add query for listing indexes

* toast look & feel updates

* fixed bug where certain system commands were over-applied

* do not return promises because they hang modal opening

* add port to member label when it is non-standard

* 90% heap utilization is the trigger

* testrig: neo4j 4.0.2

* v0.13.0 release notes
  • Loading branch information
moxious authored Mar 20, 2020
1 parent d086c5d commit 3a77b9b
Show file tree
Hide file tree
Showing 39 changed files with 472 additions and 142 deletions.
3 changes: 2 additions & 1 deletion create-neo4j4.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ docker stop neo4j-empty

PASSWORD=admin
CWD=`pwd`
NEO4J=neo4j:4.0.0-enterprise
#NEO4J=neo4j:4.0.2-enterprise
NEO4J=neo4j:4.0.2

docker run -d --name neo4j-empty --rm \
-p 127.0.0.1:7474:7474 \
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "halin",
"description": "Halin helps you monitor and improve your Neo4j graph",
"version": "0.12.2",
"version": "0.13.0",
"neo4jDesktop": {
"apiVersion": "^1.2.0"
},
Expand Down Expand Up @@ -39,7 +39,7 @@
"react-sortable-tree": "^2.2.0",
"react-table": "^6.8.6",
"react-timeseries-charts": "^0.16.1",
"react-toastify": "^5.4.1",
"react-toastify": "^5.5.0",
"ringjs": "^0.0.1",
"semantic-ui-css": "^2.4.1",
"semantic-ui-react": "^0.88.2",
Expand Down
9 changes: 9 additions & 0 deletions release-notes.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# Halin Release Notes

## 0.13.0 Limited Alerting

- Added alerts for certain memory conditions, such as heap re-allocation and imminent out-of-memory errors (OOMs)
- Fixes #132: Halin will warn you when you are approaching the need for high_limit storage.
- Members with non-standard ports will now have their port displayed in
their label. This helps separate members running on the same host.
- Main nav menu is labeled for help with navigation
- Thanks to Hugo for several points of feedback on this release.

## 0.12.2 Patch Release

- Fixes #127: Neo4j 3.5 standalone members appear properly, not as LEADER
Expand Down
1 change: 0 additions & 1 deletion src/api/HalinContext.js
Original file line number Diff line number Diff line change
Expand Up @@ -392,7 +392,6 @@ export default class HalinContext {
this.getClusterManager().addEvent({
type: 'halin',
message: 'Halin monitoring started',
address: 'all members',
});
report('Initialization complete');
return this;
Expand Down
20 changes: 17 additions & 3 deletions src/api/cluster/ClusterManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ export default class ClusterManager {
_.set(data, 'date', moment.utc().toISOString());
_.set(data, 'payload', event.payload || null);
_.set(data, 'id', uuid.v4());
_.set(data, 'address', data.event || 'all members');
this.eventLog.push(data);

// Notify listeners.
Expand Down Expand Up @@ -166,6 +167,7 @@ export default class ClusterManager {
.then(result => {
this.addEvent({
type: 'adduser',
alert: true,
message: `Added user "${username}"`,
payload: username,
});
Expand All @@ -188,6 +190,7 @@ export default class ClusterManager {
type: 'deleteuser',
message: `Deleted user "${username}"`,
payload: username,
alert: true,
});
return result;
})
Expand All @@ -205,6 +208,7 @@ export default class ClusterManager {
type: 'addrole',
message: `Created role "${role}"`,
payload: role,
alert: true,
});
return result;
});
Expand All @@ -222,20 +226,21 @@ export default class ClusterManager {
type: 'deleterole',
message: `Deleted role "${role}"`,
payload: role,
alert: true,
});
return result;
});
}

/** Specific to a particular node */
addNodeRole(node, username, role) {
sentry.info('ADD ROLE', { username, role });
sentry.info('ADD ROLE', { username, role, addr: node.getBoltAddress() });
return node.run('call dbms.security.addRoleToUser($role, $username)', { username, role }, neo4j.SYSTEM_DB);
}

/** Specific to a particular node */
removeNodeRole(node, username, role) {
sentry.info('REMOVE ROLE', { username, role });
sentry.info('REMOVE ROLE', { username, role, addr: node.getBoltAddress() });
return node.run('call dbms.security.removeRoleFromUser($role, $username)', { username, role }, neo4j.SYSTEM_DB);
}

Expand Down Expand Up @@ -335,14 +340,19 @@ export default class ClusterManager {
});
};

const allPromises = this.ctx.members().map(node => {
const membersToRunAgainst = this.ctx.supportsSystemGraph() ?
[this.ctx.getSystemDBWriter()] :
this.ctx.members();

const allPromises = membersToRunAgainst.map(node => {
return gatherRoles(node)
.then(rolesHere => determineDifferences(rolesHere, node))
.then(roleChanges => applyChanges(roleChanges, node))
.then(() => {
this.addEvent({
type: 'roleassoc',
message: `Associated "${username}" to roles ${roles.map(r => `"${r}"`).join(', ')}`,
alert: true,
payload: { username, roles },
});
})
Expand Down Expand Up @@ -442,6 +452,7 @@ export default class ClusterManager {
.then(() => this.ctx.getDatabaseSet().refresh(this.ctx))
.then(() => this.addEvent({
type: 'database',
alert: true,
message: `Stopped database ${db.name}`,
payload: db,
}));
Expand All @@ -458,6 +469,7 @@ export default class ClusterManager {
.then(() => this.ctx.getDatabaseSet().refresh(this.ctx))
.then(() => this.addEvent({
type: 'database',
alert: true,
message: `Started database ${db.name}`,
payload: db,
}));
Expand All @@ -474,6 +486,7 @@ export default class ClusterManager {
.then(() => this.ctx.getDatabaseSet().refresh(this.ctx))
.then(() => this.addEvent({
type: 'database',
alert: true,
message: `Dropped database ${db.name}`,
}));
}
Expand All @@ -495,6 +508,7 @@ export default class ClusterManager {
.then(() => this.ctx.getDatabaseSet().refresh(this.ctx))
.then(() => this.addEvent({
type: 'database',
alert: true,
message: `Created database ${name}`,
}));
}
Expand Down
19 changes: 16 additions & 3 deletions src/api/cluster/ClusterMember.js
Original file line number Diff line number Diff line change
Expand Up @@ -216,20 +216,33 @@ export default class ClusterMember {
return this.boltAddress;
}

getPort() {
const parsed = Parser.parse(this.getBoltAddress());
return parsed.port || 7687;
}

getAddress() {
const parsed = Parser.parse(this.getBoltAddress());
return parsed.host;
}

getLabel() {
const addr = this.getAddress() || 'NO_ADDRESS';
const parts = Parser.parse(this.getBoltAddress());

const addr = parts.host || 'NO_ADDRESS';
parts.port = parts.port || '7687';

// Add the port on only if it's non-standard. This also helps
// disambiguate when localhost is used 3x.
const port = parts.port === '7687' ? '' : ':' + parts.port;

if (addr.match(/^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$/)) {
// IP address
return addr;
return addr + port;
}

// Return the first portion of the hostname.
return addr.split('.')[0];
return addr.split('.')[0] + port;
}

protocols() {
Expand Down
3 changes: 2 additions & 1 deletion src/api/cluster/ClusterMember.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import _ from 'lodash';

describe('ClusterMember', function () {
const host = 'foo-host';
const label = 'foo-host:7777'; // Non-standard port gets label
const boltAddress = `bolt://${host}:7777`;
const httpAddress = `http://${host}:8888`;
const entry = {
Expand Down Expand Up @@ -78,7 +79,7 @@ describe('ClusterMember', function () {
});

it('knows its bolt address', () => expect(c.getBoltAddress()).toEqual(boltAddress));
it('knows how to label by host', () => expect(c.getLabel()).toEqual(host));
it('knows how to label by host', () => expect(c.getLabel()).toEqual(label));
it('knows how to extract protocols', () => {
const prots = c.protocols();
expect(prots).toContain('http');
Expand Down
92 changes: 91 additions & 1 deletion src/api/cluster/ClusterMemberSet.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import ClusterMember from './ClusterMember';
import errors from '../driver/errors';
import sentry from '../sentry';
import uuid from 'uuid';
import util from '../data/util';

const REFRESH_INTERVAL = 5000;

Expand All @@ -18,10 +19,18 @@ export default class ClusterMemberSet {
this.clusterMembers = [];
this.clustered = false;
this.timeout = null;
this.memFeeds = {};

// Stores utilization statistics about each member
this.stats = {};
}

members() { return this.clusterMembers; }

getStats() {
return this.stats;
}

shutdown() {
if (this.timeout) { clearTimeout(this.timeout); }

Expand Down Expand Up @@ -82,6 +91,83 @@ export default class ClusterMemberSet {
return this.timeout;
}

updateStats(halin, addr, data) {
const stats = _.merge({ lastUpdated: new Date() }, _.clone(data));

const prevHeap = _.get(this.stats[addr], 'heapCommitted');
const nowHeap = _.get(data, 'heapCommitted');

stats.pctUsed = (data.heapUsed || 0) / (data.heapCommitted || -1);
stats.pctFree = 1 - stats.pctUsed;

if (prevHeap && nowHeap !== prevHeap) {
const a = util.humanDataSize(prevHeap);
const b = util.humanDataSize(nowHeap);

halin.getClusterManager().addEvent({
type: 'memory',
alert: true,
address: addr,
message: `Heap allocation changed on ${addr} from ${a} to ${b}`,
payload: {
old: _.clone(this.stats[addr]),
new: _.clone(stats),
},
});
}

if (stats.pctFree <= 0.1) {
halin.getClusterManager().addEvent({
type: 'memory',
alert: true,
error: true,
address: addr,
message: `Heap is >= 90% utilization on ${addr}`,
payload: _.clone(stats),
});
} else if (stats.pctFree <= 0.02) {
halin.getClusterManager().addEvent({
type: 'memory',
alert: true,
error: true,
address: addr,
message: `Heap is >= 98% utilization on ${addr}`,
payload: _.clone(stats),
});
}

this.stats[addr] = stats;
return stats;
}

getMemoryFeed(halin, clusterMember) {
const addr = clusterMember.getBoltAddress();

if (this.memFeeds[addr]) {
return Promise.resolve(this.memFeeds[addr]);
}

const memFeed = halin.getDataFeed(_.merge({
node: clusterMember,
}, queryLibrary.JMX_MEMORY_STATS));

return new Promise((resolve, reject) => {
const onMemData = (newData /* , dataFeed */) => {
const data = _.get(newData, 'data[0]');
return this.updateStats(halin, addr, data);
};

const onError = (err, dataFeed) => {
sentry.fine('ClusterMemberSet: failed to get mem data', addr, err);
reject(err, dataFeed);
};

memFeed.addListener(onMemData);
memFeed.onError = onError;
return resolve(memFeed);
});
}

/**
* Ping a cluster node with a trivial query, just to keep connections
* alive and verify it's still listening. This forces driver creation
Expand Down Expand Up @@ -182,7 +268,11 @@ export default class ClusterMemberSet {
// SETUP ACTIONS FOR ANY NEW MEMBER
const driver = halin.driverFor(member.getBoltAddress());
member.setDriver(driver);
const setup = this.ping(halin, member)

const setup = Promise.all([
this.ping(halin, member),
this.getMemoryFeed(halin, member),
])
.then(() => member.checkComponents());

promises.push(setup);
Expand Down
39 changes: 39 additions & 0 deletions src/api/data/queries/dbms/listIndexes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import HalinQuery from '../HalinQuery';

export default new HalinQuery({
description: 'Lists database indexes',
query: `
CALL db.indexes()
YIELD id, name, state, populationPercent, uniqueness, type,
entityType, labelsOrTypes, properties, provider
RETURN id, name, state, populationPercent, uniqueness, type,
entityType, labelsOrTypes, properties, provider
ORDER BY name asc;
`,
columns: [
{ Header: 'ID', accessor: 'id' },
{ Header: 'Name', accessor: 'name', },
{ Header: 'State', accessor: 'state' },
{ Header: 'Population', accessor: 'populationPercent' },
{ Header: 'Uniqueness', accessor: 'uniqueness' },
{ Header: 'Type', accessor: 'type' },
{ Header: 'Entity', accessor: 'entityType' },
{ Header: 'Labels/Types', accessor: 'labelsOrTypes' },
{ Header: 'Properties', accessor: 'properties' },
{ Header: 'Provider', accessor: 'provider' },
],
exampleResult: [
{
id: 1,
name: 'index_70bffdb',
state: 'ONLINE',
populationPercent: 100.0,
uniqueness: 'NONUNIQUE',
type: 'BTREE',
entityType: 'NODE',
labelsOrTypes: ['Label'],
properties: ['name'],
provider: 'native-btree-1.0',
},
],
});
2 changes: 2 additions & 0 deletions src/api/data/queries/query-library.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import DBMS_GET_MAX_HEAP from './dbms/getMaxHeap';
import DBMS_FUNCTIONS from './dbms/functions';
import DBMS_PROCEDURES from './dbms/procedures';
import DBMS_LIST_CONFIG from './dbms/listConfig';
import DBMS_LIST_INDEXES from './dbms/listIndexes';
import DBMS_SECURITY_USER_ROLES from './dbms/security/userRoles';
import DBMS_LIST_TRANSACTIONS from './dbms/listTransactions';
import DBMS_LIST_CONNECTIONS from './dbms/listConnections';
Expand Down Expand Up @@ -104,6 +105,7 @@ const allQueries = {
DBMS_FUNCTIONS,
DBMS_PROCEDURES,
DBMS_LIST_CONFIG,
DBMS_LIST_INDEXES,
DBMS_SECURITY_USER_ROLES,
DBMS_LIST_TRANSACTIONS,
DBMS_LIST_CONNECTIONS,
Expand Down
Loading

0 comments on commit 3a77b9b

Please sign in to comment.