@vladonv/pm2-postgres 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -0
- package/app.js +67 -0
- package/lib/actions.js +36 -0
- package/lib/clientFactory.js +30 -0
- package/lib/stats/refreshBackendMetrics.js +19 -0
- package/lib/stats/refreshIndexCount.js +13 -0
- package/lib/stats/refreshLockCount.js +21 -0
- package/lib/stats/refreshTableCount.js +13 -0
- package/lib/stats/refreshTablesSize.js +14 -0
- package/lib/stats/refreshTransactionMetrics.js +33 -0
- package/lib/stats/refreshVersion.js +17 -0
- package/lib/stats.js +103 -0
- package/package.json +35 -0
- package/pm2-postgres.jpg +0 -0
package/README.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# pm2-postgres
|
|
2
|
+
PostgreSQL module for Keymetrics
|
|
3
|
+
|
|
4
|
+

|
|
5
|
+
|
|
6
|
+
## Description
|
|
7
|
+
|
|
8
|
+
PM2 module to monitor key PostgreSQL server metrics:
|
|
9
|
+
|
|
10
|
+
* Tables / Indexes Count
|
|
11
|
+
* Backends Active / Idle
|
|
12
|
+
* Exclusive / Access Share Locks
|
|
13
|
+
* Total Tables Size
|
|
14
|
+
* Transactions Committed / Rollback
|
|
15
|
+
* Tuples Fetched / Updated / Inserted / Deleted
|
|
16
|
+
|
|
17
|
+
## Requirements
|
|
18
|
+
|
|
19
|
+
This module requires a PostgreSQL install (v9.3+, including SCRAM-SHA-256 auth on PostgreSQL 10-17).
|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
$ npm install pm2 -g
|
|
25
|
+
|
|
26
|
+
$ pm2 install pm2-postgres
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Config
|
|
30
|
+
|
|
31
|
+
The default connection details are :
|
|
32
|
+
"hostname": "localhost"
|
|
33
|
+
"port": 5432
|
|
34
|
+
"username": "guest"
|
|
35
|
+
"password": "guest"
|
|
36
|
+
"database": "postgres"
|
|
37
|
+
|
|
38
|
+
To modify the config values you can use the commands:
|
|
39
|
+
```bash
|
|
40
|
+
$ pm2 set pm2-postgres:hostname localhost
|
|
41
|
+
$ pm2 set pm2-postgres:port 5432
|
|
42
|
+
$ pm2 set pm2-postgres:username guest
|
|
43
|
+
$ pm2 set pm2-postgres:password guest
|
|
44
|
+
$ pm2 set pm2-postgres:database postgres
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Uninstall
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
$ pm2 uninstall pm2-postgres
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
# License
|
|
54
|
+
|
|
55
|
+
MIT
|
package/app.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
var fs = require('fs');
|
|
2
|
+
var path = require('path');
|
|
3
|
+
var pmx = require('pmx');
|
|
4
|
+
var pgClientFactory = require('./lib/clientFactory.js');
|
|
5
|
+
var pgStats = require('./lib/stats.js');
|
|
6
|
+
var pgActions = require('./lib/actions.js');
|
|
7
|
+
|
|
8
|
+
// Debian/Ubuntu's postgresql-common names pidfiles "<version>-main.pid"
|
|
9
|
+
// (e.g. "17-main.pid", "9.6-main.pid"). Scanning the directory instead of
|
|
10
|
+
// hardcoding a version list keeps this working for future major versions.
|
|
11
|
+
function detectPostgresPidPaths() {
|
|
12
|
+
var pgRunDir = '/var/run/postgresql';
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
return fs.readdirSync(pgRunDir)
|
|
16
|
+
.filter(function (name) { return /^\d+(\.\d+)?-main\.pid$/.test(name); })
|
|
17
|
+
.sort(function (a, b) { return parseFloat(b) - parseFloat(a); })
|
|
18
|
+
.map(function (name) { return path.join(pgRunDir, name); });
|
|
19
|
+
} catch (e) {
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
pmx.initModule({
|
|
25
|
+
|
|
26
|
+
pid: pmx.resolvePidPaths(detectPostgresPidPaths()),
|
|
27
|
+
|
|
28
|
+
// Options related to the display style on Keymetrics
|
|
29
|
+
widget: {
|
|
30
|
+
|
|
31
|
+
// Logo displayed
|
|
32
|
+
logo: 'http://www.inquidia.com/sites/default/files/postgresql_logo%5B1%5D.png',
|
|
33
|
+
|
|
34
|
+
// Module colors
|
|
35
|
+
// 0 = main element
|
|
36
|
+
// 1 = secondary
|
|
37
|
+
// 2 = main border
|
|
38
|
+
// 3 = secondary border
|
|
39
|
+
theme: ['#60798c', '#326892', '#ffffff', '#807C7C'],
|
|
40
|
+
|
|
41
|
+
// Section to show / hide
|
|
42
|
+
el: {
|
|
43
|
+
probes: true,
|
|
44
|
+
actions: true
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
// Main block to show / hide
|
|
48
|
+
block: {
|
|
49
|
+
actions: true,
|
|
50
|
+
issues: true,
|
|
51
|
+
meta: true,
|
|
52
|
+
|
|
53
|
+
// Custom metrics to put in BIG
|
|
54
|
+
main_probes: ['Tables', 'Indexes','Total Tables Size','Backends Active','Exclusive Locks']
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
}, function (err, conf) {
|
|
60
|
+
var pgClient = pgClientFactory.build(conf);
|
|
61
|
+
|
|
62
|
+
// Init metrics refresh loop
|
|
63
|
+
pgStats.init(pgClient);
|
|
64
|
+
|
|
65
|
+
// Init actions
|
|
66
|
+
pgActions.init(pgClient);
|
|
67
|
+
});
|
package/lib/actions.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
var pmx = require('pmx');
|
|
2
|
+
|
|
3
|
+
function initActions(pgClient) {
|
|
4
|
+
|
|
5
|
+
// List DBs
|
|
6
|
+
pmx.action('List DBs', function (reply) {
|
|
7
|
+
var queryString = "SELECT datname FROM pg_database WHERE datistemplate = false;"
|
|
8
|
+
pgClient.query(queryString, function (err, results) {
|
|
9
|
+
if (err) {
|
|
10
|
+
return reply(err);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
reply(results.rows.map(function (row) { return row.datname; }));
|
|
14
|
+
})
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
// Show Settings
|
|
18
|
+
pmx.action('Show Settings', function (reply) {
|
|
19
|
+
var queryString = "SHOW ALL;";
|
|
20
|
+
pgClient.query(queryString, function (err, results) {
|
|
21
|
+
if (err) {
|
|
22
|
+
return reply(err);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
reply(results.rows);
|
|
26
|
+
})
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function init(pgClient) {
|
|
33
|
+
initActions(pgClient);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
module.exports.init = init;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
var pg = require('pg');
|
|
2
|
+
var pmx = require('pmx');
|
|
3
|
+
|
|
4
|
+
function build(conf) {
|
|
5
|
+
var pgClient = {};
|
|
6
|
+
|
|
7
|
+
var connectionString = "postgres://" + conf.username + ":" + conf.password + "@" + conf.hostname + ":" + conf.port + "/" + conf.database;
|
|
8
|
+
|
|
9
|
+
var pool = new pg.Pool({ connectionString: connectionString });
|
|
10
|
+
|
|
11
|
+
pool.on('error', function (err) {
|
|
12
|
+
// Errors on idle clients (e.g. connection dropped) must not crash the process.
|
|
13
|
+
pmx.notify("Postgres pool error: " + err);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
pgClient.query = function (queryString, cb) {
|
|
17
|
+
pool.query(queryString, function (err, result) {
|
|
18
|
+
if (err) {
|
|
19
|
+
pmx.notify("Couldn't query postgres: " + err);
|
|
20
|
+
return cb(err);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return cb(null, result);
|
|
24
|
+
});
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
return pgClient;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
module.exports.build = build;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
var pmx = require('pmx');
|
|
2
|
+
|
|
3
|
+
module.exports = function refreshBackendMetrics(metrics, pgClient) {
|
|
4
|
+
var queryString = "SELECT count(*) - ( SELECT count(*) FROM pg_stat_activity WHERE"
|
|
5
|
+
+ " state = 'idle' ) AS backends_active, ( SELECT count(*) FROM"
|
|
6
|
+
+ " pg_stat_activity WHERE state = 'idle' ) AS backends_idle"
|
|
7
|
+
+ " FROM pg_stat_activity;";
|
|
8
|
+
pgClient.query(queryString, function (err, results) {
|
|
9
|
+
if (err) {
|
|
10
|
+
return pmx.notify("Backend Query Error: " + err);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Backends Active
|
|
14
|
+
metrics.backendsActive.set(results.rows[0].backends_active);
|
|
15
|
+
|
|
16
|
+
// Backends Idle
|
|
17
|
+
metrics.backendsIdle.set(results.rows[0].backends_idle);
|
|
18
|
+
});
|
|
19
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
var pmx = require('pmx');
|
|
2
|
+
|
|
3
|
+
module.exports = function refreshIndexCount(metrics, pgClient) {
|
|
4
|
+
var queryString = "SELECT count(1) as indexes FROM pg_class WHERE relkind = 'i';";
|
|
5
|
+
pgClient.query(queryString, function (err, results) {
|
|
6
|
+
if (err) {
|
|
7
|
+
return pmx.notify("Index Query Error: " + err);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// # of Indexes
|
|
11
|
+
metrics.indexCount.set(results.rows[0].indexes);
|
|
12
|
+
});
|
|
13
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
var pmx = require('pmx');
|
|
2
|
+
|
|
3
|
+
function findLockCount(rows, mode) {
|
|
4
|
+
var row = rows.find(function (row) { return row.mode === mode; });
|
|
5
|
+
return row ? row.count : 'N/A';
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
module.exports = function refreshLockCount(metrics, pgClient) {
|
|
9
|
+
var queryString = "SELECT mode, count(mode) AS count FROM pg_locks GROUP BY mode ORDER BY mode;";
|
|
10
|
+
pgClient.query(queryString, function (err, results) {
|
|
11
|
+
if (err) {
|
|
12
|
+
return pmx.notify("Lock Query Error: " + err);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// # of Access Share Locks
|
|
16
|
+
metrics.accessShareLockCount.set(findLockCount(results.rows, 'AccessShareLock'));
|
|
17
|
+
|
|
18
|
+
// # of Exclusive Locks
|
|
19
|
+
metrics.exclusiveLockCount.set(findLockCount(results.rows, 'ExclusiveLock'));
|
|
20
|
+
});
|
|
21
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
var pmx = require('pmx');
|
|
2
|
+
|
|
3
|
+
module.exports = function refreshTableCount(metrics, pgClient) {
|
|
4
|
+
var queryString = "SELECT count(1) as relations FROM pg_class WHERE relkind IN ('r', 't');";
|
|
5
|
+
pgClient.query(queryString, function (err, results) {
|
|
6
|
+
if (err) {
|
|
7
|
+
return pmx.notify("Table Query Error: " + err);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// # of Tables
|
|
11
|
+
metrics.tableCount.set(results.rows[0].relations);
|
|
12
|
+
});
|
|
13
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
var pmx = require('pmx');
|
|
2
|
+
var humanize = require('humanize');
|
|
3
|
+
|
|
4
|
+
module.exports = function refreshTablesSize(metrics, pgClient) {
|
|
5
|
+
var queryString = "SELECT ((sum(relpages)* 8) * 1024) AS size_relations FROM pg_class WHERE relkind IN ('r', 't');";
|
|
6
|
+
pgClient.query(queryString, function (err, results) {
|
|
7
|
+
if (err) {
|
|
8
|
+
return pmx.notify("Table Size Query Error: " + err);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Total Tables Size
|
|
12
|
+
metrics.tablesSize.set(humanize.filesize(results.rows[0].size_relations));
|
|
13
|
+
});
|
|
14
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
var pmx = require('pmx');
|
|
2
|
+
|
|
3
|
+
module.exports = function refreshTransactionMetrics(metrics, pgClient) {
|
|
4
|
+
var queryString = "SELECT sum(xact_commit) AS transactions_committed,"
|
|
5
|
+
+ " sum(xact_rollback) AS transactions_rollback, sum(blks_read) AS blocks_read,"
|
|
6
|
+
+ " sum(blks_hit) AS blocks_hit, sum(tup_returned) AS tuples_returned,"
|
|
7
|
+
+ " sum(tup_fetched) AS tuples_fetched, sum(tup_inserted) AS tuples_inserted,"
|
|
8
|
+
+ " sum(tup_updated) AS tuples_updated, sum(tup_deleted) AS tuples_deleted"
|
|
9
|
+
+ " FROM pg_stat_database;";
|
|
10
|
+
pgClient.query(queryString, function (err, results) {
|
|
11
|
+
if (err) {
|
|
12
|
+
return pmx.notify("Transaction Query Error: " + err);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Transactions Committed
|
|
16
|
+
metrics.transactionsCommitted.set(results.rows[0].transactions_committed);
|
|
17
|
+
|
|
18
|
+
// Transactions Rollback
|
|
19
|
+
metrics.transactionsRollback.set(results.rows[0].transactions_rollback);
|
|
20
|
+
|
|
21
|
+
// Tuples Fetched
|
|
22
|
+
metrics.tuplesFetched.set(results.rows[0].tuples_fetched);
|
|
23
|
+
|
|
24
|
+
// Tuples Inserted
|
|
25
|
+
metrics.tuplesInserted.set(results.rows[0].tuples_inserted);
|
|
26
|
+
|
|
27
|
+
// Tuples Updated
|
|
28
|
+
metrics.tuplesUpdated.set(results.rows[0].tuples_updated);
|
|
29
|
+
|
|
30
|
+
// Tuples Deleted
|
|
31
|
+
metrics.tuplesDeleted.set(results.rows[0].tuples_deleted);
|
|
32
|
+
});
|
|
33
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
var pmx = require('pmx');
|
|
2
|
+
|
|
3
|
+
module.exports = function refreshVersion(metrics, pgClient) {
|
|
4
|
+
var queryString = "SELECT version();";
|
|
5
|
+
pgClient.query(queryString, function (err, results) {
|
|
6
|
+
if (err) {
|
|
7
|
+
return pmx.notify("Version Query Error: " + err);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// # of Indexes
|
|
11
|
+
var fullVersion = results.rows[0].version;
|
|
12
|
+
var match = fullVersion.match(/^PostgreSQL (\d+(?:\.\d+)*)/);
|
|
13
|
+
if (match) {
|
|
14
|
+
metrics.version.set(match[1]);
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
};
|
package/lib/stats.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
var pmx = require('pmx');
|
|
2
|
+
|
|
3
|
+
var refreshTableCount = require('./stats/refreshTableCount');
|
|
4
|
+
var refreshIndexCount = require('./stats/refreshIndexCount');
|
|
5
|
+
var refreshTransactionMetrics = require('./stats/refreshTransactionMetrics');
|
|
6
|
+
var refreshTablesSize = require('./stats/refreshTablesSize');
|
|
7
|
+
var refreshBackendMetrics = require('./stats/refreshBackendMetrics');
|
|
8
|
+
var refreshLockCount = require('./stats/refreshLockCount');
|
|
9
|
+
var refreshVersion = require('./stats/refreshVersion');
|
|
10
|
+
|
|
11
|
+
var metrics = {};
|
|
12
|
+
var REFRESH_RATE = 10000; // ms
|
|
13
|
+
var probe = pmx.probe();
|
|
14
|
+
|
|
15
|
+
// Init metrics with default values
|
|
16
|
+
function initMetrics() {
|
|
17
|
+
metrics.version = probe.metric({
|
|
18
|
+
name: 'PostgreSQL Version',
|
|
19
|
+
value: 'N/A'
|
|
20
|
+
});
|
|
21
|
+
metrics.tableCount = probe.metric({
|
|
22
|
+
name: 'Tables',
|
|
23
|
+
value: 'N/A'
|
|
24
|
+
});
|
|
25
|
+
metrics.indexCount = probe.metric({
|
|
26
|
+
name: 'Indexes',
|
|
27
|
+
value: 'N/A'
|
|
28
|
+
});
|
|
29
|
+
metrics.transactionsCommitted = probe.metric({
|
|
30
|
+
name: 'Transactions Committed',
|
|
31
|
+
value: 'N/A'
|
|
32
|
+
});
|
|
33
|
+
metrics.transactionsRollback = probe.metric({
|
|
34
|
+
name: 'Transactions Rollback',
|
|
35
|
+
value: 'N/A'
|
|
36
|
+
});
|
|
37
|
+
metrics.tuplesFetched = probe.metric({
|
|
38
|
+
name: 'Tuples Fetched',
|
|
39
|
+
value: 'N/A'
|
|
40
|
+
});
|
|
41
|
+
metrics.tuplesInserted = probe.metric({
|
|
42
|
+
name: 'Tuples Inserted',
|
|
43
|
+
value: 'N/A'
|
|
44
|
+
});
|
|
45
|
+
metrics.tuplesUpdated = probe.metric({
|
|
46
|
+
name: 'Tuples Updated',
|
|
47
|
+
value: 'N/A'
|
|
48
|
+
});
|
|
49
|
+
metrics.tuplesDeleted = probe.metric({
|
|
50
|
+
name: 'Tuples Deleted',
|
|
51
|
+
value: 'N/A'
|
|
52
|
+
});
|
|
53
|
+
metrics.tablesSize = probe.metric({
|
|
54
|
+
name: 'Total Tables Size',
|
|
55
|
+
value: 'N/A'
|
|
56
|
+
});
|
|
57
|
+
metrics.backendsActive = probe.metric({
|
|
58
|
+
name: 'Backends Active',
|
|
59
|
+
value: 'N/A'
|
|
60
|
+
});
|
|
61
|
+
metrics.backendsIdle = probe.metric({
|
|
62
|
+
name: 'Backends Idle',
|
|
63
|
+
value: 'N/A',
|
|
64
|
+
alert: {
|
|
65
|
+
mode: 'threshold-avg',
|
|
66
|
+
value: 8,
|
|
67
|
+
msg: 'Too many Idle Backends',
|
|
68
|
+
cmp: ">"
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
metrics.accessShareLockCount = probe.metric({
|
|
72
|
+
name: 'Access Share Locks',
|
|
73
|
+
value: 'N/A'
|
|
74
|
+
});
|
|
75
|
+
metrics.exclusiveLockCount = probe.metric({
|
|
76
|
+
name: 'Exclusive Locks',
|
|
77
|
+
value: 'N/A',
|
|
78
|
+
alert: {
|
|
79
|
+
mode: 'threshold-avg',
|
|
80
|
+
value: 500,
|
|
81
|
+
msg: 'Too many Exclusive Locks',
|
|
82
|
+
cmp: ">"
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Refresh metrics
|
|
88
|
+
function refreshMetrics(pgClient) {
|
|
89
|
+
refreshTableCount(metrics, pgClient);
|
|
90
|
+
refreshIndexCount(metrics, pgClient);
|
|
91
|
+
refreshTransactionMetrics(metrics, pgClient);
|
|
92
|
+
refreshTablesSize(metrics, pgClient);
|
|
93
|
+
refreshBackendMetrics(metrics, pgClient);
|
|
94
|
+
refreshLockCount(metrics, pgClient);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function init(pgClient) {
|
|
98
|
+
initMetrics();
|
|
99
|
+
setInterval(refreshMetrics.bind(this, pgClient), REFRESH_RATE);
|
|
100
|
+
refreshVersion(metrics,pgClient);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
module.exports.init = init;
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vladonv/pm2-postgres",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "PM2 PostgreSQL Module (fork of pm2-hive/pm2-postgres, updated for PostgreSQL 10-18 / SCRAM auth support)",
|
|
5
|
+
"main": "app.js",
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "public"
|
|
8
|
+
},
|
|
9
|
+
"dependencies": {
|
|
10
|
+
"humanize": "0.0.9",
|
|
11
|
+
"pg": "^8.23.0",
|
|
12
|
+
"pmx": ">=0.5.5"
|
|
13
|
+
},
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/vladonv/pm2-postgres.git"
|
|
17
|
+
},
|
|
18
|
+
"config": {
|
|
19
|
+
"hostname": "localhost",
|
|
20
|
+
"port": 5432,
|
|
21
|
+
"username": "guest",
|
|
22
|
+
"password": "guest",
|
|
23
|
+
"database": "postgres"
|
|
24
|
+
},
|
|
25
|
+
"apps": [
|
|
26
|
+
{
|
|
27
|
+
"merge_logs": true,
|
|
28
|
+
"max_memory_restart": "200M",
|
|
29
|
+
"script": "app.js"
|
|
30
|
+
}
|
|
31
|
+
],
|
|
32
|
+
"author": "Keymetrics Inc. (original), vladonv (fork maintainer)",
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"devDependencies": {}
|
|
35
|
+
}
|
package/pm2-postgres.jpg
ADDED
|
Binary file
|