mirror of https://github.com/mthenw/frontail.git
* Added configuration to fix #108 * Conflicting option * Updated README * Fix for web sockets behind nginx * make url path working without nginx * update readmepull/121/head
parent
9be6486c92
commit
35f7e0e80a
43
README.md
43
README.md
|
@ -1,6 +1,6 @@
|
|||
# frontail – streaming logs to the browser
|
||||
|
||||
```frontail``` is a Node.js application for streaming logs to the browser. It's a `tail -F` with UI.
|
||||
`frontail` is a Node.js application for streaming logs to the browser. It's a `tail -F` with UI.
|
||||
|
||||

|
||||
|
||||
|
@ -9,9 +9,9 @@
|
|||
|
||||
## Quick start
|
||||
|
||||
- `npm i frontail -g` or download a binary file from [Releases](https://github.com/mthenw/frontail/releases) page
|
||||
- `frontail /var/log/syslog`
|
||||
- visit [http://127.0.0.1:9001](http://127.0.0.1:9001)
|
||||
* `npm i frontail -g` or download a binary file from [Releases](https://github.com/mthenw/frontail/releases) page
|
||||
* `frontail /var/log/syslog`
|
||||
* visit [http://127.0.0.1:9001](http://127.0.0.1:9001)
|
||||
|
||||
## Features
|
||||
|
||||
|
@ -21,7 +21,7 @@
|
|||
* number of unread logs in favicon
|
||||
* themes (default, dark)
|
||||
* [highlighting](#highlighting)
|
||||
* search (```Tab``` to focus, ```Esc``` to clear)
|
||||
* search (`Tab` to focus, `Esc` to clear)
|
||||
* tailing [multiple files](#tailing-multiple-files) and [stdin](#stdin)
|
||||
* basic authentication
|
||||
|
||||
|
@ -51,10 +51,12 @@
|
|||
-c, --certificate <cert.pem> Certificate for HTTPS, option works only along with -k option
|
||||
--pid-path <path> if run as daemon file that will store the process id, default /var/run/frontail.pid
|
||||
--log-path <path> if run as daemon file that will be used as a log, default /dev/null
|
||||
--url-path <path> URL path for the browser application, default /
|
||||
--ui-hide-topbar hide topbar (log file name and search box)
|
||||
--ui-no-indent don't indent log lines
|
||||
--ui-highlight highlight words or lines if defined string found in logs, default preset
|
||||
--ui-highlight-preset <path> custom preset for highlighting (see ./preset/default.json)
|
||||
--path <path> prefix path for the running application, default /
|
||||
|
||||
Web interface runs on **http://127.0.0.1:[port]**.
|
||||
|
||||
|
@ -70,7 +72,7 @@ Use `-` for streaming stdin:
|
|||
|
||||
### Highlighting
|
||||
|
||||
```--ui-highlight``` option turns on highlighting in UI. By default preset from ```./preset/default.json``` is used:
|
||||
`--ui-highlight` option turns on highlighting in UI. By default preset from `./preset/default.json` is used:
|
||||
|
||||
```
|
||||
{
|
||||
|
@ -85,4 +87,31 @@ Use `-` for streaming stdin:
|
|||
|
||||
which means that every "err" string will be in red and every line containing "err" will be bolded.
|
||||
|
||||
*New presets are very welcome. If you don't like default or you would like to share yours, please create PR with json file.*
|
||||
_New presets are very welcome. If you don't like default or you would like to share yours, please create PR with json file._
|
||||
|
||||
### Running behind nginx
|
||||
|
||||
Using the `--url-path` option `frontail` can run behind nginx with the example configuration
|
||||
|
||||
Using `frontail` with `--url-path /frontail`
|
||||
|
||||
```
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
server {
|
||||
listen 8080;
|
||||
server_name localhost;
|
||||
|
||||
location /frontail {
|
||||
proxy_pass http://127.0.0.1:9001/frontail;
|
||||
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
15
index.js
15
index.js
|
@ -4,7 +4,7 @@ const connect = require('connect');
|
|||
const cookieParser = require('cookie');
|
||||
const crypto = require('crypto');
|
||||
const path = require('path');
|
||||
const socketio = require('socket.io');
|
||||
const SocketIO = require('socket.io');
|
||||
const tail = require('./lib/tail');
|
||||
const connectBuilder = require('./lib/connect_builder');
|
||||
const program = require('./lib/options_parser');
|
||||
|
@ -30,7 +30,11 @@ const doSecure = !!(program.key && program.certificate);
|
|||
const sessionSecret = String(+new Date()) + Math.random();
|
||||
const sessionKey = 'sid';
|
||||
const files = program.args.join(' ');
|
||||
const filesNamespace = crypto.createHash('md5').update(files).digest('hex');
|
||||
const filesNamespace = crypto
|
||||
.createHash('md5')
|
||||
.update(files)
|
||||
.digest('hex');
|
||||
const urlPath = program.urlPath.replace(/\/$/, ''); // remove trailing slash
|
||||
|
||||
if (program.daemonize) {
|
||||
daemonize(__filename, program, {
|
||||
|
@ -41,7 +45,7 @@ if (program.daemonize) {
|
|||
/**
|
||||
* HTTP(s) server setup
|
||||
*/
|
||||
const appBuilder = connectBuilder();
|
||||
const appBuilder = connectBuilder(urlPath);
|
||||
if (doAuthorization) {
|
||||
appBuilder.session(sessionSecret, sessionKey);
|
||||
appBuilder.authorize(program.user, program.password);
|
||||
|
@ -63,9 +67,8 @@ if (program.daemonize) {
|
|||
/**
|
||||
* socket.io setup
|
||||
*/
|
||||
const io = socketio.listen(server, {
|
||||
log: false,
|
||||
});
|
||||
const io = new SocketIO({ path: path.join(urlPath, '/socket.io') });
|
||||
io.attach(server);
|
||||
|
||||
if (doAuthorization) {
|
||||
io.use((socket, next) => {
|
||||
|
|
|
@ -3,14 +3,18 @@
|
|||
const connect = require('connect');
|
||||
const fs = require('fs');
|
||||
|
||||
function ConnectBuilder() {
|
||||
function ConnectBuilder(urlPath) {
|
||||
this.app = connect();
|
||||
this.urlPath = urlPath;
|
||||
}
|
||||
|
||||
ConnectBuilder.prototype.authorize = function authorize(user, pass) {
|
||||
this.app.use(
|
||||
this.urlPath,
|
||||
connect.basicAuth(
|
||||
(incomingUser, incomingPass) => user === incomingUser && pass === incomingPass));
|
||||
(incomingUser, incomingPass) => user === incomingUser && pass === incomingPass
|
||||
)
|
||||
);
|
||||
|
||||
return this;
|
||||
};
|
||||
|
@ -22,7 +26,7 @@ ConnectBuilder.prototype.build = function build() {
|
|||
ConnectBuilder.prototype.index = function index(path, files, filesNamespace, themeOpt) {
|
||||
const theme = themeOpt || 'default';
|
||||
|
||||
this.app.use((req, res) => {
|
||||
this.app.use(this.urlPath, (req, res) => {
|
||||
fs.readFile(path, (err, data) => {
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'text/html',
|
||||
|
@ -32,7 +36,8 @@ ConnectBuilder.prototype.index = function index(path, files, filesNamespace, the
|
|||
.toString('utf-8')
|
||||
.replace(/__TITLE__/g, files)
|
||||
.replace(/__THEME__/g, theme)
|
||||
.replace(/__NAMESPACE__/g, filesNamespace),
|
||||
.replace(/__NAMESPACE__/g, filesNamespace)
|
||||
.replace(/__PATH__/g, this.urlPath),
|
||||
'utf-8'
|
||||
);
|
||||
});
|
||||
|
@ -42,8 +47,9 @@ ConnectBuilder.prototype.index = function index(path, files, filesNamespace, the
|
|||
};
|
||||
|
||||
ConnectBuilder.prototype.session = function session(secret, key) {
|
||||
this.app.use(connect.cookieParser());
|
||||
this.app.use(this.urlPath, connect.cookieParser());
|
||||
this.app.use(
|
||||
this.urlPath,
|
||||
connect.session({
|
||||
secret,
|
||||
key,
|
||||
|
@ -53,8 +59,8 @@ ConnectBuilder.prototype.session = function session(secret, key) {
|
|||
};
|
||||
|
||||
ConnectBuilder.prototype.static = function staticf(path) {
|
||||
this.app.use(connect.static(path));
|
||||
this.app.use(this.urlPath, connect.static(path));
|
||||
return this;
|
||||
};
|
||||
|
||||
module.exports = () => new ConnectBuilder();
|
||||
module.exports = urlPath => new ConnectBuilder(urlPath);
|
||||
|
|
|
@ -14,11 +14,16 @@ module.exports = (script, params, opts) => {
|
|||
const logFile = fs.openSync(params.logPath, 'a');
|
||||
|
||||
let args = [
|
||||
'-h', params.host,
|
||||
'-p', params.port,
|
||||
'-n', params.number,
|
||||
'-l', params.lines,
|
||||
'-t', params.theme,
|
||||
'-h',
|
||||
params.host,
|
||||
'-p',
|
||||
params.port,
|
||||
'-n',
|
||||
params.number,
|
||||
'-l',
|
||||
params.lines,
|
||||
'-t',
|
||||
params.theme,
|
||||
];
|
||||
|
||||
if (options.doAuthorization) {
|
||||
|
@ -33,6 +38,10 @@ module.exports = (script, params, opts) => {
|
|||
args.push('--ui-hide-topbar');
|
||||
}
|
||||
|
||||
if (params.urlPath) {
|
||||
args.push('--url-path', params.urlPath);
|
||||
}
|
||||
|
||||
if (!params.uiIndent) {
|
||||
args.push('--ui-no-indent');
|
||||
}
|
||||
|
|
|
@ -45,6 +45,7 @@ program
|
|||
String,
|
||||
'/dev/null'
|
||||
)
|
||||
.option('--url-path <path>', 'URL path for the browser application, default /', String, '/')
|
||||
.option('--ui-hide-topbar', 'hide topbar (log file name and search box)')
|
||||
.option('--ui-no-indent', "don't indent log lines")
|
||||
.option(
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "frontail",
|
||||
"version": "4.1.0",
|
||||
"version": "4.1.1",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
|
|
@ -6,7 +6,7 @@ const path = require('path');
|
|||
|
||||
describe('connectBuilder', () => {
|
||||
it('should build connect app', () => {
|
||||
connectBuilder()
|
||||
connectBuilder('/')
|
||||
.build()
|
||||
.should.have.property('use');
|
||||
connectBuilder()
|
||||
|
@ -15,7 +15,7 @@ describe('connectBuilder', () => {
|
|||
});
|
||||
|
||||
it('should build app requiring authorized user', (done) => {
|
||||
const app = connectBuilder()
|
||||
const app = connectBuilder('/')
|
||||
.authorize('user', 'pass')
|
||||
.build();
|
||||
|
||||
|
@ -26,7 +26,7 @@ describe('connectBuilder', () => {
|
|||
});
|
||||
|
||||
it('should build app allowing user to login', (done) => {
|
||||
const app = connectBuilder()
|
||||
const app = connectBuilder('/')
|
||||
.authorize('user', 'pass')
|
||||
.build();
|
||||
app.use((req, res) => {
|
||||
|
@ -40,7 +40,7 @@ describe('connectBuilder', () => {
|
|||
});
|
||||
|
||||
it('should build app that setup session', (done) => {
|
||||
const app = connectBuilder()
|
||||
const app = connectBuilder('/')
|
||||
.session('secret', 'sessionkey')
|
||||
.build();
|
||||
app.use((req, res) => {
|
||||
|
@ -53,7 +53,7 @@ describe('connectBuilder', () => {
|
|||
});
|
||||
|
||||
it('should build app that serve static files', (done) => {
|
||||
const app = connectBuilder()
|
||||
const app = connectBuilder('/')
|
||||
.static(path.join(__dirname, 'fixtures'))
|
||||
.build();
|
||||
|
||||
|
@ -63,7 +63,7 @@ describe('connectBuilder', () => {
|
|||
});
|
||||
|
||||
it('should build app that serve index file', (done) => {
|
||||
const app = connectBuilder()
|
||||
const app = connectBuilder('/')
|
||||
.index(path.join(__dirname, 'fixtures/index'), '/testfile')
|
||||
.build();
|
||||
|
||||
|
@ -73,8 +73,19 @@ describe('connectBuilder', () => {
|
|||
.expect('Content-Type', 'text/html', done);
|
||||
});
|
||||
|
||||
it('should build app that serve index file on specified path', (done) => {
|
||||
const app = connectBuilder('/test')
|
||||
.index(path.join(__dirname, 'fixtures/index'), '/testfile')
|
||||
.build();
|
||||
|
||||
request(app)
|
||||
.get('/test')
|
||||
.expect(200)
|
||||
.expect('Content-Type', 'text/html', done);
|
||||
});
|
||||
|
||||
it('should build app that replace index title', (done) => {
|
||||
const app = connectBuilder()
|
||||
const app = connectBuilder('/')
|
||||
.index(path.join(__dirname, 'fixtures/index_with_title'), '/testfile')
|
||||
.build();
|
||||
|
||||
|
@ -84,7 +95,7 @@ describe('connectBuilder', () => {
|
|||
});
|
||||
|
||||
it('should build app that sets socket.io namespace based on files', (done) => {
|
||||
const app = connectBuilder()
|
||||
const app = connectBuilder('/')
|
||||
.index(path.join(__dirname, 'fixtures/index_with_ns'), '/testfile', 'ns', 'dark')
|
||||
.build();
|
||||
|
||||
|
@ -94,7 +105,7 @@ describe('connectBuilder', () => {
|
|||
});
|
||||
|
||||
it('should build app that sets theme', (done) => {
|
||||
const app = connectBuilder()
|
||||
const app = connectBuilder('/')
|
||||
.index(path.join(__dirname, '/fixtures/index_with_theme'), '/testfile', 'ns', 'dark')
|
||||
.build();
|
||||
|
||||
|
@ -107,7 +118,7 @@ describe('connectBuilder', () => {
|
|||
});
|
||||
|
||||
it('should build app that sets default theme', (done) => {
|
||||
const app = connectBuilder()
|
||||
const app = connectBuilder('/')
|
||||
.index(path.join(__dirname, '/fixtures/index_with_theme'), '/testfile')
|
||||
.build();
|
||||
|
||||
|
|
|
@ -107,6 +107,14 @@ describe('daemonize', () => {
|
|||
daemon.daemon.lastCall.args[1].should.containDeep(['-k', 'key.file', '-c', 'cert.file']);
|
||||
});
|
||||
|
||||
it('with url-path option', () => {
|
||||
optionsParser.parse(['node', '/path/to/frontail', '--url-path', '/test']);
|
||||
|
||||
daemonize('script', optionsParser);
|
||||
|
||||
daemon.daemon.lastCall.args[1].should.containDeep(['--url-path', '/test']);
|
||||
});
|
||||
|
||||
it('with hide-topbar option', () => {
|
||||
optionsParser.parse(['node', '/path/to/frontail', '--ui-hide-topbar']);
|
||||
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>tail -F __TITLE__</title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<link rel="stylesheet" type="text/css" href="/styles/__THEME__.css">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<link rel="stylesheet" type="text/css" href="__PATH__/styles/__THEME__.css">
|
||||
<link rel="icon" href="__PATH__/favicon.ico">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<nav class="topbar navbar navbar-inverse navbar-fixed-top" role="navigation">
|
||||
<div class="container-fluid">
|
||||
|
@ -20,12 +22,14 @@
|
|||
|
||||
<pre class="log"></pre>
|
||||
|
||||
<script src="/socket.io/socket.io.js"></script>
|
||||
<script src="/tinycon.min.js"></script>
|
||||
<script src="/ansi_up.js"></script>
|
||||
<script src="/app.js"></script>
|
||||
<script src="__PATH__/socket.io/socket.io.js"></script>
|
||||
<script src="__PATH__/tinycon.min.js"></script>
|
||||
<script src="__PATH__/ansi_up.js"></script>
|
||||
<script src="__PATH__/app.js"></script>
|
||||
<script type="text/javascript">
|
||||
var socket = new io.connect('/' + '__NAMESPACE__');
|
||||
var socket = new io.connect('/__NAMESPACE__', {
|
||||
path: '__PATH__/socket.io'
|
||||
});
|
||||
|
||||
window.load = App.init({
|
||||
socket: socket,
|
||||
|
@ -34,6 +38,7 @@
|
|||
topbar: document.getElementsByClassName('topbar')[0],
|
||||
body: document.getElementsByTagName('body')[0]
|
||||
});
|
||||
</script>
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
</html>
|
Loading…
Reference in New Issue