Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Added the basic discovery and zabbix update functionality.
  • Loading branch information
rkaw92 committed Apr 5, 2016
0 parents commit 767f46e
Show file tree
Hide file tree
Showing 11 changed files with 621 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitignore
@@ -0,0 +1,2 @@
node_modules
jsdoc
8 changes: 8 additions & 0 deletions LICENSE
@@ -0,0 +1,8 @@
The MIT License (MIT)
Copyright (c) 2016 greatcare software

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
86 changes: 86 additions & 0 deletions README.md
@@ -0,0 +1,86 @@
# PM2 monitoring tool for Zabbix

## What it is
This is a Node.js-powered daemon and utility that monitors a PM2 instance and sends status updates to the Zabbix server monitoring solution.

### Features
* Automatically discovers processes managed by PM2
* Reports Node.js process status, CPU usage, memory usage and restart count
* Monitors the PM2 God Daemon itself for status, resource usage and PID changes
* Provides a Zabbix item template for easy installation

### Architecture

pm2-zabbix operates in two ways: as a script called by the Agent used for item discovery, and by a stand-alone executable that runs in the background and periodically sends updates for data items using zabbix_sender. If all relevant items already exist on the server and no automatic discovery (LLD) is required, integration with the Agent via UserParameters is optional.

## Installation

### Prerequisites

This module relies on having the `zabbix_sender` binary installed, and on `/etc/zabbix/zabbix_agentd.conf` being present on the system. Both typically come with a `zabbix-agent` package for your Linux distribution (some repositories may split this into two separate packages - `zabbix-agent` and `zabbix-sender`). It has been tested with Zabbix 3.0.

### Installing

Begin by installing the module (as root) on the server that you run PM2 on:
```
# npm install -g pm2-zabbix
```

This installs a `pm2-zabbix` executable in your **$PATH**. Alternatively, you can choose a directory to your liking and perform a local install there, or clone this repository - the relevant script is `monitor.js`.

### Testing discovery

To see if the tool can detect your running PM2 processes, switch to the user that runs PM2 and then:
```
$ pm2-zabbix --discover
```

This should print a JSON with a familiar-looking list of processes such as this one:

```json
{
"data": [
{
"{#PROCESS_ID}": "index-0",
"{#PROCESS_NAME}": "index"
},
{
"{#PROCESS_ID}": "index-1",
"{#PROCESS_NAME}": "index"
}
]
}
```
(If the list is empty, inspect `pm2 l` and see if your processes are really there.)

The above is a JSON object compatible with the Zabbix LLD protocol. It tells us that two items (in our case, processes) have been discovered - two instances of the same index.js application launched with PM2. An appropriate template installed on the Zabbix server may use this information to automatically create items.

### Testing Zabbix connectivity

The asynchronous background monitoring protocol uses `zabbix_sender` to send data items to the server. By default, configuration parameters are taken from `/etc/zabbix/zabbix_agentd.conf`, including the server address and the authentication credentials. The monitoring mode can be started using:
```
$ pm2-zabbix --monitor
```
(add --debug for additional logging)

The above launches a process that connects to the current user's PM2 instance (or launches a new one if necessary) and starts sending updates in the background.


### Running the monitoring daemon

`pm2-zabbix` is just a Node.js script, which could be launched from pm2. However, this setup is not recommended, since the monitoring tool also monitors the status of the pm2 God Daemon itself. Instead, it is best to install a proper start-up script, specific for your distro's init system, and launch the daemon in parallel to pm2.

An example sysvinit script and a systemd unit file are provided in the `install/init/` directory of this repository. These most likely need to be customized for your local install - in particular, the user name will have to be changed to match the system user that you run pm2 as.

### Configuring the Zabbix Agent

For the monitoring server to know what processes exist on the PM2 host, it needs to perform [Low-Level Discovery](https://www.zabbix.com/documentation/3.0/manual/discovery/low_level_discovery). A special data item is appointed that the Zabbix Agent will query. On the target host, the item must be defined as a `UserParameter`. An example configuration file that accomplishes this is provided in the `install/zabbix-agent/` directory - install it as `/etc/zabbix/zabbix_agentd.d/pm2-zabbix.conf`.

### Configuring the Zabbix Server

A template needs to be installed (and assigned to a host) that tells Zabbix of the possible items to monitor, and establishes a default set of triggers and discovery rules for dynamically finding processes.

The default template file can be found in `install/zabbix-server/` - upload it via the Zabbix management web UI and assign it to the hosts that you intend to be monitoring PM2 on. Appropriate keys will be created automatically.

## License
MIT - see the `LICENSE` file.
4 changes: 4 additions & 0 deletions index.js
@@ -0,0 +1,4 @@
module.exports.PM2Tracker = require('./lib/PM2Tracker');
module.exports.PM2ZabbixMonitor = require('./lib/PM2ZabbixMonitor');
module.exports.ZabbixDataProvider = require('./lib/ZabbixDataProvider');
module.exports.ProcessState = require('./lib/ProcessState');
190 changes: 190 additions & 0 deletions lib/PM2Tracker.js
@@ -0,0 +1,190 @@
var when = require('when');
var nodefn = require('when/node');
var pm2 = nodefn.liftAll(require('pm2'));
var fs = nodefn.liftAll(require('fs'));
var pidusage = nodefn.liftAll(require('pidusage'));
var EventEmitter = require('events').EventEmitter;
var ProcessState = require('./ProcessState');

/**
* PM2Tracker is a component that connects to the PM2 bus on its own, loads a process list,
* and allows for tracking process state changes within that list when new processes are started
* or existing ones are stopped. It is also possible to check the status of the PM2 daemon itself.
* The tracker is constructed as inactive and must be started manually using start().
* @constructor
* @extends EventEmitter
*/
function PM2Tracker() {
/**
* The PM2 bus object used for communicating with the process manager daemon.
* @type {Object}
*/
this._bus = null;
/**
* A map of processes, indexed by process ID made out of the process name and pm_id.
* @type {Object.<string,ProcessState>}
*/
this._processes = {};

EventEmitter.call(this);
}
PM2Tracker.prototype = Object.create(EventEmitter.prototype);

/**
* Synthesize a textual identifier (processID) from a process info object obtained from PM2.
* Returned IDs should coincide with PM2-generated values (for example, logs are typically stored in
* <processName>-<index>.log files by PM2).
* @static
* @param {Object} processObject - The process description. Usually obtained as an element of the process list from PM2.
* @returns {string} The synthetic processID.
*/
PM2Tracker.getProcessID = function getProcessID(processObject) {
return processObject.name + '-' + processObject.pm_id;
};

/**
* Take a process list and turn it into a map of ProcessState objects, keyed by the synthetic processID.
* @static
* @param {Object[]} processList - A process list, as obtained from pm2.list() (of PM2 API).
* @returns {Object.<string,ProcessState>} A map of process states, keyed by processID.
*/
PM2Tracker.generateProcessMap = function generateProcessMap(processList) {
var processes = {};
processList.forEach(function(processEntry) {
var processID = PM2Tracker.getProcessID(processEntry);
processes[processID] = new ProcessState({
name: processEntry.name,
status: processEntry.pm2_env.status,
resources: processEntry.monit,
restarts: processEntry.pm2_env.restart_time
});
});

return processes;
};

/**
* Load a process list into the tracker. This updates the cached process map.
* @param {Object[]} processList - The process list returned by pm2.list().
*/
PM2Tracker.prototype._loadProcessList = function _loadProcessList(processList) {
this._processes = PM2Tracker.generateProcessMap(processList);
};


/**
* React to a process:event from the PM2 bus. This re-emits the event as "processStateChanged"
* if the process status has changed (e.g. from "online" to "stopping").
* @param {Object} event - The event to react to.
*/
PM2Tracker.prototype._handleProcessEvent = function _handleProcessEvent(event) {
var processID = PM2Tracker.getProcessID(event.process);
var oldState = this._processes[processID];
var newState = new ProcessState(event.process);
this._processes[processID] = newState;

if (!oldState || !oldState.equals(newState)) {
this.emit('processStateChanged', { processID: processID, oldState: oldState, newState: newState });
}
};

/**
* Start the tracker. This initializes the connection to the PM2 bus and begins listening to events.
* The cached process list is initially populated, and is kept updated.
* @returns {Promise} A promise that fulfills when the connection has been established, appropriate listeners installed, and the process list cache populated.
*/
PM2Tracker.prototype.start = function start() {
var self = this;

return pm2.connect().then(function() {
return pm2.launchBus();
}).then(function(bus) {
self._bus = bus;
bus.on('process:event', function(event) {
self._handleProcessEvent(event);
});

return pm2.list();
}).then(function(processList) {
self._loadProcessList(processList);
});
};

/**
* Disconnect from the PM2 bus and stop listening to process events.
*/
PM2Tracker.prototype.stop = function stop() {
return pm2.disconnect();
};

/**
* Get the cached process map that includes updated process statuses.
* Note that only statuses (online, stopped, etc. - PM2-specific) are supposed to be current -
* other data, such as resource usage numbers, will most likely be stale.
* For up-to-date stats on resources, use getProcessMap().
* @returns {Object.<string,ProcessState>}
*/
PM2Tracker.prototype.getCachedProcessMap = function getCachedProcessMap() {
return this._processes;
};

/**
* Get an up-to-date process map, including momentary resource usage.
* This does not internally update the cache, which is maintained separately.
* @returns {Promise.<Object.<string,ProcessState>>}
*/
PM2Tracker.prototype.getProcessMap = function getProcessMap() {
return pm2.list().then(function(processList) {
return PM2Tracker.generateProcessMap(processList);
});
};

/**
* Get an up-to-date state of the PM2 manager daemon (PM2 process itself).
* Note that the process' name will always be "PM2" and the restart count is set to zero.
* The PID to query is taken from the PM2 pidfile, located in ~/$PM2_HOME (typically, ".pm2").
* Thus, if the user is running a non-default PM2 instance they wish to monitor, it is necessary
* to override the PM2_HOME environment variable for both pm2 and this class.
* @returns {Promise.<ProcessState>}
*/
PM2Tracker.prototype.getPM2State = function getPM2State() {
// First, figure out out where to look for the PM2 pid file.
var PM2_HOME = process.env.HOME + '/' + (process.env.PM2_HOME || '.pm2');
var PM2PID;
// Then, read the pid.
return fs.readFile(PM2_HOME + '/pm2.pid', 'utf-8').then(function(pidfileContent) {
PM2PID = Number(pidfileContent.trim());
// We have got the PID, so now, we need to look at the process.
return pidusage.stat(PM2PID).catch(function(error) {
// The process may very well not exist, in which case, we are going to get a "null" or an error, which we need to turn into a synthetic "offline" status.
if (error && error.code === 'ENOENT') {
return null;
}
throw error;
});
}).then(function(usageInfo) {
// If the process exists, we can report it as online and produce its usage stats.
if (usageInfo) {
return new ProcessState({
name: 'PM2',
status: 'online',
resources: usageInfo,
restarts: 0,
pid: PM2PID
});
}
else {
// Otherwise, it must be offline, and, as such, it consumes zero of everything.
return new ProcessState({
name: 'PM2',
status: 'offline',
resources: { cpu: 0, memory: 0 },
restarts: 0,
// Since the PID is not current anyway, we send a zero to signal that fact.
pid: 0
});
}
});
};

module.exports = PM2Tracker;

0 comments on commit 767f46e

Please sign in to comment.