├── .prettierrc.js ├── .gitignore ├── .eslintrc ├── README.md ├── config └── default.toml ├── example └── log.txt ├── Gruntfile.js ├── package.json └── index.js /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 160, 3 | tabWidth: 4, 4 | singleQuote: true 5 | }; 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | npm-debug.log 4 | config/production.* 5 | config/development.* 6 | *v8.log 7 | package-lock.json* 8 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "indent": 0, 4 | "no-await-in-loop": 0 5 | }, 6 | "extends": ["nodemailer", "prettier"], 7 | "parserOptions": { 8 | "ecmaVersion": 2017 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # graylog-tailer 2 | 3 | Tail graylog access file and send it back to Graylog as search event 4 | 5 | See [here](http://docs.graylog.org/en/2.3/pages/securing.html#logging-user-activity) how to set up rest access logging in Graylog. 6 | -------------------------------------------------------------------------------- /config/default.toml: -------------------------------------------------------------------------------- 1 | 2 | source="./example/log.txt" 3 | 4 | [log] 5 | [log.gelf] 6 | enabled=false 7 | component="graylog" 8 | [log.gelf.options] 9 | graylogPort=12201 10 | graylogHostname="127.0.0.1" 11 | connection="lan" 12 | -------------------------------------------------------------------------------- /example/log.txt: -------------------------------------------------------------------------------- 1 | 2018-10-22 13:18:35,589 DEBUG: org.graylog2.rest.accesslog - 127.0.0.1 andris [-] "GET api/search/universal/relative?query=stored%3Ayes&range=300&limit=150&sort=timestamp%3Adesc" Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36 200 -1 2 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function (grunt) { 4 | 5 | // Project configuration. 6 | grunt.initConfig({ 7 | eslint: { 8 | all: ['Gruntfile.js', 'index.js'] 9 | } 10 | }); 11 | 12 | // Load the plugin(s) 13 | grunt.loadNpmTasks('grunt-eslint'); 14 | 15 | // Tasks 16 | grunt.registerTask('default', ['eslint']); 17 | }; 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graylog-tailer", 3 | "private": true, 4 | "version": "1.0.0", 5 | "description": "", 6 | "main": "index.js", 7 | "scripts": { 8 | "test": "grunt" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "gelf": "^2.0.1", 15 | "tail": "^2.0.0", 16 | "wild-config": "^1.3.6" 17 | }, 18 | "devDependencies": { 19 | "eslint": "^5.7.0", 20 | "eslint-config-nodemailer": "^1.2.0", 21 | "eslint-config-prettier": "^3.1.0", 22 | "grunt": "^1.0.3", 23 | "grunt-cli": "^1.3.1", 24 | "grunt-eslint": "^21.0.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* eslint no-console: 0*/ 2 | 3 | 'use strict'; 4 | 5 | const config = require('wild-config'); 6 | const Gelf = require('gelf'); 7 | const os = require('os'); 8 | const urllib = require('url'); 9 | const Tail = require('tail').Tail; 10 | 11 | const component = config.log.gelf.component || 'wildduck'; 12 | const hostname = config.log.gelf.hostname || os.hostname(); 13 | const gelf = 14 | config.log.gelf && config.log.gelf.enabled 15 | ? new Gelf(config.log.gelf.options) 16 | : { 17 | // placeholder 18 | emit: (evt, message) => console.log(JSON.stringify(message)) 19 | }; 20 | 21 | const loggelf = message => { 22 | if (typeof message === 'string') { 23 | message = { 24 | short_message: message 25 | }; 26 | } 27 | message = message || {}; 28 | 29 | if (!message.short_message || message.short_message.indexOf(component.toUpperCase()) !== 0) { 30 | message.short_message = component.toUpperCase() + ' ' + (message.short_message || ''); 31 | } 32 | 33 | message.facility = component; // facility is deprecated but set by the driver if not provided 34 | message.host = hostname; 35 | if (!message.timestamp) { 36 | message.timestamp = Date.now() / 1000; 37 | } 38 | message._component = component; 39 | Object.keys(message).forEach(key => { 40 | if (!message[key]) { 41 | delete message[key]; 42 | } 43 | }); 44 | gelf.emit('gelf.log', message); 45 | }; 46 | 47 | function parseLine(line) { 48 | let parts = line.split(/\s+/); 49 | 50 | if (!line || !parts || parts.length < 9) { 51 | return; 52 | } 53 | 54 | let message = {}; 55 | 56 | message.timestamp = 57 | new Date( 58 | parts 59 | .slice(0, 2) 60 | .join('T') 61 | .replace(/,/g, '.') + 'Z' 62 | ).getTime() / 1000; 63 | 64 | message._ip = parts[5]; 65 | message._user = parts[6]; 66 | 67 | let remainder = []; 68 | 69 | let urlParts = []; 70 | for (let i = 9; i < parts.length; i++) { 71 | let part = parts[i]; 72 | if (/"$/.test(part)) { 73 | urlParts.push(part.replace(/"$/, '')); 74 | remainder = parts.slice(i + 1); 75 | break; 76 | } else { 77 | urlParts.push(part); 78 | } 79 | } 80 | 81 | let url = urllib.parse(urlParts.join(' '), true, true); 82 | if (url.pathname === 'api/search/universal/relative' && url.query.query) { 83 | message._search = 'yes'; 84 | Object.keys(url.query).forEach(key => { 85 | let value = url.query[key]; 86 | if (key !== 'query' && !isNaN(value)) { 87 | value = Number(value); 88 | } 89 | message['_search_' + key] = value; 90 | }); 91 | } else { 92 | return; 93 | } 94 | 95 | remainder.pop(); 96 | message._response_code = Number(remainder.pop()); 97 | message._ua = remainder.join(' '); 98 | 99 | message.short_message = '[' + message._user + '] ' + message._search_query; 100 | message.full_message = line; 101 | 102 | loggelf(message); 103 | } 104 | 105 | try { 106 | const tail = new Tail(config.source); 107 | 108 | tail.on('line', data => { 109 | parseLine(data); 110 | }); 111 | 112 | tail.on('error', err => { 113 | console.error(err); 114 | process.exit(1); 115 | }); 116 | } catch (err) { 117 | console.error(err.message); 118 | process.exit(1); 119 | } 120 | 121 | console.log('Tailing for %s', config.source); 122 | --------------------------------------------------------------------------------