Skip to content

Commit fb4e0dd

Browse files
author
Brian Crowell
committed
Initial commit
0 parents  commit fb4e0dd

28 files changed

+8397
-0
lines changed

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
/node_modules
2+
/logs
3+
config.json

LICENSE

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
Copyright 2016–7 Anonymous
2+
Copyright 2018 Brian Crowell
3+
4+
Permission to use, copy, modify, and/or distribute this software for any
5+
purpose with or without fee is hereby granted, provided that the above
6+
copyright notice and this permission notice appear in all copies.
7+
8+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
9+
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
10+
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
11+
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
12+
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
13+
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
14+
PERFORMANCE OF THIS SOFTWARE.

README.md

+82
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
# node-log-forwarder
2+
3+
Node.js-based log parsing, forwarding, and notifications.
4+
5+
This is a server that provides the functionality of products like
6+
Logstash and Fluentd—it accepts log data from remote sources,
7+
parses them, processes them, and forwards them to notification or
8+
database systems such as Slack, IRC, or Elasticsearch.
9+
10+
But unlike Logstash or Fluentd...
11+
12+
**Processing log entries is done in user-provided scripts written in JavaScript.** Make the processing logic as complex as you need to, and take advantage of any Node.js-compatible library.
13+
14+
A syslog receiver that sends fully-attributed structured syslog to Elasticsearch and messages to IRC channels might look like this:
15+
16+
```javascript
17+
const d3 = require('d3');
18+
const dateFormat = d3.timeFormat('%Y.%m.%d');
19+
20+
function preprocess(ctx, line) {
21+
return {
22+
reportingIp: ctx.meta.remoteAddress,
23+
receivingPort: ctx.meta.localPort,
24+
receivedTime: ctx.meta.receiveTime,
25+
eventTime: ctx.meta.receiveTime,
26+
message: (line instanceof Buffer) ? line.toString('latin1') : line,
27+
tag: ['raw'],
28+
};
29+
}
30+
31+
function process(ctx, msg) {
32+
ctx.sendElasticsearch('raw-syslog-' + dateFormat(msg.eventTime), 'raw-syslog');
33+
ctx.sendIrc('#syslog');
34+
35+
if(msg.message.startsWith('ERROR')) {
36+
msg.tag.push('error');
37+
ctx.sendIrc('#syslog_errors');
38+
}
39+
40+
return { log: msg };
41+
}
42+
```
43+
44+
**Almost any configuration or script change takes effect immediately** without restarting, closing sockets, losing connections, or losing messages. Alter the script above and save it and instantly see the effects of your changes.
45+
46+
It also features very fast startup, and it spreads messages across multiple worker processes for increased throughput on multiprocessor systems.
47+
48+
## Provided inputs
49+
50+
Built-in support for receiving:
51+
52+
* Netflow V9
53+
* UDP (such as syslog)
54+
* Line-based TCP (such as syslog, Bunyan, or custom log formats)
55+
56+
## Provided outputs
57+
58+
Built-in support for sending formatted results to:
59+
60+
* Local files, with filenames supplied by the user script. Use this to structure your log files in directories by source IP, date, both, or whatever other naming scheme you like.
61+
* Elasticsearch, with support for bulk uploads and throughput statistics by worker
62+
* Slack, with full custom formatting
63+
* IRC
64+
* SMTP
65+
66+
# Setup
67+
68+
TODO, but read [config.json.example](./config.json.example)
69+
70+
# Writing a user script
71+
72+
TODO, but see the [sample user scripts](./sample-user-scripts)
73+
74+
# Writing an input module
75+
76+
TODO, but see the [built-in input modules](./inputs)
77+
78+
# Contact, acknowledgements
79+
80+
Written by Brian Crowell, with special thanks to the organization that supported this project, who has asked to remain anonymous.
81+
82+
Please do get in touch if you use this project, I would love to hear about it!

config.json.example

+105
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
// This is a sample configuration file for the forwarder. Copy
2+
// this to config.json to create your configuration file.
3+
//
4+
// Settings marked "(dynamic)" can be changed while the program
5+
// is running and will take effect when the file is saved.
6+
// Settings marked "(static)" should not be changed while the
7+
// program is running.
8+
9+
{
10+
// The name for this instance, used in logging statistics (dynamic)
11+
"name": "test-logger",
12+
13+
// Map of input names to modules (dynamic)
14+
//
15+
// Inputs can be added or removed at run-time.
16+
"inputs": {
17+
18+
// Name of the input (static). Changes to the config are matched back
19+
// to the input module by name, so don't change this while the
20+
// forwarder is running.
21+
"bunyan": {
22+
23+
// Path to the module that provides the input (static)
24+
//
25+
// Different modules will have different options. This input uses
26+
// tcp-lines, which accepts TCP connections, splits input by newlines,
27+
// and provides each message to the given user script.
28+
"module": "./inputs/tcp-lines.js",
29+
30+
// Path to the user script (dynamic)
31+
//
32+
// This script decides how each message is processed. Changes to this
33+
// path or to the script are reflected as soon as they are seen.
34+
// For tcp-lines, see sample scripts msvistalog.js, bunyan.js, or cylance.js.
35+
"script": "sample-user-scripts/bunyan.js",
36+
37+
// Host to bind the socket to (optional, dynamic); if unspecified, binds
38+
// to all interfaces
39+
// "host": "localhost"
40+
41+
// Port to bind the socket to (dynamic)
42+
"port": 5022
43+
},
44+
"syslog": {
45+
// Same as above, but for a UDP input.
46+
"module": "./inputs/udp.js",
47+
"script": "sample-user-scripts/syslog.js",
48+
49+
// Optional "host"
50+
// "host": "localhost"
51+
52+
"port": 5144
53+
}
54+
},
55+
56+
// Number of workers spawned (dynamic).
57+
//
58+
// Each worker can use up to one CPU core, so set this to the
59+
// number of cores available on your system. It can be adjusted
60+
// at run time.
61+
"workerCount": 2,
62+
63+
// Log file writing
64+
"file": {
65+
// The base path to which log files will be written; paths outside this are not allowed
66+
"basePath": "/home/bcrowell/software/node-log-forwarder/logs"
67+
},
68+
69+
// SMTP support (optional, dynamic)
70+
"mail": {
71+
// Transport options, either a URL or an object (dynamic)
72+
// See https://www.npmjs.com/package/nodemailer#set-up-smtp
73+
"transportOptions": "smtp://my.mail.server"
74+
},
75+
76+
// IRC support (optional, dynamic)
77+
"irc": {
78+
"server": "myirc.company.net",
79+
"nick": "logbot",
80+
81+
// Options for the IRC library; see https://node-irc.readthedocs.org/en/latest/API.html#client
82+
"options": {
83+
// These are the defaults we use
84+
// "port": 6667,
85+
// "encoding": "utf8",
86+
// "autoRejoin": true
87+
//"debug": true
88+
}
89+
},
90+
91+
// Elasticsearch support (optional, dynamic)
92+
"elasticsearch": {
93+
"clientSettings": {
94+
"hosts": [
95+
"http://10.0.0.1:9200",
96+
"http://10.0.0.2:9200"
97+
],
98+
"apiVersion": "5.0",
99+
"sniffOnStart": true,
100+
"sniffInterval": 60000,
101+
"sniffOnConnectionFault": true
102+
},
103+
"bulkSizeMegabytes": 10
104+
}
105+
}

index.js

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
'use strict';
2+
3+
const config = require('./lib/config.js');
4+
const cluster = require('cluster');
5+
6+
const log = config.log;
7+
8+
if(cluster.isMaster) {
9+
log.info({messageId: 'master/started'}, 'Starting up master process.');
10+
11+
cluster.on('fork', worker => {
12+
checkRunningWorkers();
13+
});
14+
15+
cluster.on('disconnect', worker => {
16+
log.warn({messageId: 'master/worker-disconnected'}, `Worker #${worker.id} (PID ${worker.process.pid}) has disconnected.`);
17+
checkRunningWorkers();
18+
});
19+
20+
cluster.on('exit', worker => {
21+
log.warn({messageId: 'master/worker-exited'}, `Worker #${worker.id} (PID ${worker.process.pid}) has exited.`);
22+
checkRunningWorkers();
23+
});
24+
25+
function checkRunningWorkers() {
26+
const targetWorkerCount = config.config.workerCount || 0;
27+
const workerIds = Object.keys(cluster.workers);
28+
29+
if(workerIds.length > targetWorkerCount) {
30+
log.warn({messageId: 'master/too-many-workers'}, `Killing worker to meet worker count.`);
31+
cluster.workers[workerIds[0]].kill();
32+
}
33+
else if(workerIds.length < targetWorkerCount) {
34+
cluster.fork();
35+
}
36+
}
37+
38+
config.on('change', newConfig => {
39+
checkRunningWorkers();
40+
});
41+
}
42+
else {
43+
log.info({messageId: 'worker/started'}, `Starting up worker process ${cluster.worker.id}.`);
44+
}
45+

0 commit comments

Comments
 (0)