├── .gitignore ├── package.json ├── Cakefile ├── webnull.coffee ├── README.md └── webnull.js /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | node_modules 3 | 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webnull", 3 | "version": "0.0.4", 4 | "author" : "Dotan Nahum", 5 | "description" : "web/null eats your HTTP. a useful tool to assist with stress, performance and experiment testing.", 6 | "homepage" : "https://github.com/jondot/webnull", 7 | "preferGlobal" : true, 8 | "bin":{ "webnull" : "webnull.js" }, 9 | "dependencies": { 10 | "connect": ">=1.0.0", 11 | "commander": ">=0.3.2" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Cakefile: -------------------------------------------------------------------------------- 1 | {spawn} = require 'child_process' 2 | path = require 'path' 3 | fs = require 'fs' 4 | 5 | CS_SRC = path.join __dirname, 'webnull.coffee' 6 | JS_EXE = path.join __dirname, 'webnull.js' 7 | 8 | die = (message) -> 9 | console.error "ERROR: #{message}" 10 | process.exit 1 11 | 12 | compile = (callback) -> 13 | coffee = spawn 'coffee', ['-c', CS_SRC] 14 | coffee.stderr.on 'data', (data) -> console.error data.toString() 15 | coffee.stdout.on 'data', (data) -> console.info data.toString() 16 | coffee.on 'exit', (code) -> callback?() if code is 0 17 | 18 | addShebang = (script) -> 19 | fs.writeFileSync script, "#!/usr/bin/env node\n#{fs.readFileSync script}" 20 | 21 | task 'build', 'Build JavaScript executable', -> 22 | invoke 'validate' 23 | compile -> addShebang JS_EXE 24 | 25 | task 'validate', 'Validate package.json', -> 26 | try 27 | JSON.parse fs.readFileSync('package.json') 28 | catch error 29 | die "package.json: invalid JSON: #{error.message}" 30 | 31 | 32 | -------------------------------------------------------------------------------- /webnull.coffee: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env coffee 2 | 3 | connect = require 'connect' 4 | fs = require 'fs' 5 | sys = require 'sys' 6 | program = require 'commander' 7 | 8 | version = '0.0.4' 9 | program 10 | .version(version) 11 | .option('-d, --debug', 'Show when flush happens.', false) 12 | .option('-c, --canned-response [file]', 'Existing file name to read a response from.') 13 | .option('-o, --output [file]', 'File name to output to.', 'webnull.log') 14 | .option('-i, --interval [seconds]', 'Flush interval.', 10) 15 | .option('-p, --port [number]', 'Port to listen on.', 4000) 16 | .parse(process.argv) 17 | 18 | console.log("== web/null v#{version}. I eat your HTTP. ==") 19 | console.log("* Running in debug mode.") if program.debug? 20 | console.log("* Listening on port #{program.port}.") 21 | console.log("* Flushing to #{program.output} every #{program.interval} seconds.") 22 | 23 | canned_response = '' 24 | if program.cannedResponse? 25 | canned_response = fs.readFileSync(program.cannedResponse, 'utf-8') 26 | console.log("* Response will be #{canned_response.length} bytes read from #{program.cannedResponse}.") 27 | 28 | 29 | 30 | 31 | fd = fs.openSync(program.output, 'a', 0644) 32 | process.addListener "exit", ()->fs.close(fd) 33 | 34 | 35 | s = if program.debug then connect.createServer(connect.logger()) else connect.createServer() 36 | s.listen(program.port) 37 | 38 | 39 | data = 40 | hits: 0 41 | size: 0 42 | 43 | total_data = 44 | hits: 0 45 | size: 0 46 | 47 | 48 | 49 | write = (time, t_req_count, t_data_size, req_count, data_size, req_sec, data_avg)-> 50 | console.log "#{time}\t#{t_req_count} req(total)\t#{t_data_size} bytes(total)\t#{req_count} reqs\t#{data_size} bytes\t#{req_sec} req(s)\t#{data_avg} bytes(avg)" if program.debug? 51 | fs.writeSync(fd, "#{time},#{t_req_count},#{t_data_size},#{req_count},#{data_size},#{req_sec},#{data_avg}\n", null, "utf-8") 52 | 53 | 54 | 55 | flush =()-> 56 | req_sec = data.hits/program.interval 57 | data_avg = if data.hits == 0 then 0 else data.size/data.hits 58 | total_data.hits += data.hits 59 | total_data.size += data.size 60 | write(Math.round(new Date().getTime()/1000.0), total_data.hits, total_data.size, data.hits, data.size, req_sec, data_avg) 61 | data = {hits:0, size:0} 62 | 63 | 64 | 65 | null_resp = (req, res)-> 66 | data.hits += 1 67 | req.on 'data', (chunk)-> 68 | data.size += chunk.length 69 | req.on 'end', ()-> 70 | res.end(canned_response) 71 | 72 | s.use '/', null_resp 73 | 74 | 75 | 76 | setInterval flush, program.interval * 1000 77 | 78 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # web/null 2 | 3 | web/null is `/dev/null` for the Web. It silently agrees with and eats up any request 4 | being `VERB`d to it, and keeps statistics of it. 5 | 6 | web/null is great to use as a diagnostics end-socket of any distributed system you 7 | have, that works against another system sitting at an HTTP endpoint. 8 | 9 | Replace any service with it, in order to have a real peek at what 10 | your other services are doing. 11 | 12 | web/null is extremely useful (and being used) for getting stats data during stress 13 | testing a complex system. 14 | 15 | ## Quick Start 16 | 17 | via `npm`. It is preferrable (but not must) to run globally (sudo, -g) 18 | 19 | $ sudo npm install webnull -g 20 | $ webnull 21 | == web/null v0.0.4. I eat your HTTP. == 22 | * Listening on port 4000. 23 | * Flushing to webnull.log every 10 seconds. 24 | 25 | 26 | ## Via Cloning 27 | 28 | Alternatively, if you'd like to clone / run and tweak, clone this repository and run 29 | 30 | $ npm install 31 | 32 | You should then be able to run 33 | 34 | $ node webnull 35 | == web/null v0.0.4. I eat your HTTP. == 36 | * Listening on port 4000. 37 | * Flushing to webnull.log every 10 seconds. 38 | 39 | 40 | ## Output 41 | 42 | And now just experiment. Here is apachebench 43 | 44 | $ ab -n 10000 -c 10 "http://localhost:4000/" 45 | 46 | Sample output, human-readable (debug) 47 | 48 | ... 49 | 127.0.0.1 - - [Wed, 09 Nov 2011 13:35:36 GMT] "GET / HTTP/1.1" 200 - "-" "Mozilla/5.0 (X11; Linux i686) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.835.202 Safari/535.1" 50 | 127.0.0.1 - - [Wed, 09 Nov 2011 13:35:36 GMT] "GET /favicon.ico HTTP/1.1" 200 - "-" "Mozilla/5.0 (X11; Linux i686) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.835.202 Safari/535.1" 51 | 1320845743 51 req(total) 0 bytes(total) 51 reqs 0 bytes 5.1 req(s) 0 bytes(avg) 52 | 53 | Sample output, CSV (webnull.log) 54 | 55 | ... 56 | 1320845743,51,0,51,0,5.1,0 57 | 58 | ## Doing more 59 | 60 | Here's how help looks like: 61 | 62 | $ node webnull --help 63 | Usage: webnull [options] 64 | 65 | Options: 66 | 67 | -h, --help output usage information 68 | -V, --version output the version number 69 | -d, --debug Show when flush happens. 70 | -c, --canned-response [file] Existing file name to read a response from. 71 | -o, --output [file] File name to output to. 72 | -i, --interval [seconds] Flush interval. 73 | -p, --port [number] Port to listen on. 74 | 75 | ## Contributing 76 | 77 | Fork, implement, add tests, pull request, get my everlasting thanks and a respectable place here :). 78 | 79 | 80 | ## Copyright 81 | 82 | Copyright (c) 2011 [Dotan Nahum](http://gplus.to/dotan) [@jondot](http://twitter.com/jondot). See MIT-LICENSE for further details. 83 | 84 | -------------------------------------------------------------------------------- /webnull.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | (function() { 3 | var canned_response, connect, data, fd, flush, fs, null_resp, program, s, sys, total_data, version, write; 4 | connect = require('connect'); 5 | fs = require('fs'); 6 | sys = require('sys'); 7 | program = require('commander'); 8 | version = '0.0.4'; 9 | program.version(version).option('-d, --debug', 'Show when flush happens.', false).option('-c, --canned-response [file]', 'Existing file name to read a response from.').option('-o, --output [file]', 'File name to output to.', 'webnull.log').option('-i, --interval [seconds]', 'Flush interval.', 10).option('-p, --port [number]', 'Port to listen on.', 4000).parse(process.argv); 10 | console.log("== web/null v" + version + ". I eat your HTTP. =="); 11 | if (program.debug != null) { 12 | console.log("* Running in debug mode."); 13 | } 14 | console.log("* Listening on port " + program.port + "."); 15 | console.log("* Flushing to " + program.output + " every " + program.interval + " seconds."); 16 | canned_response = ''; 17 | if (program.cannedResponse != null) { 18 | canned_response = fs.readFileSync(program.cannedResponse, 'utf-8'); 19 | console.log("* Response will be " + canned_response.length + " bytes read from " + program.cannedResponse + "."); 20 | } 21 | fd = fs.openSync(program.output, 'a', 0644); 22 | process.addListener("exit", function() { 23 | return fs.close(fd); 24 | }); 25 | s = program.debug ? connect.createServer(connect.logger()) : connect.createServer(); 26 | s.listen(program.port); 27 | data = { 28 | hits: 0, 29 | size: 0 30 | }; 31 | total_data = { 32 | hits: 0, 33 | size: 0 34 | }; 35 | write = function(time, t_req_count, t_data_size, req_count, data_size, req_sec, data_avg) { 36 | if (program.debug != null) { 37 | console.log("" + time + "\t" + t_req_count + " req(total)\t" + t_data_size + " bytes(total)\t" + req_count + " reqs\t" + data_size + " bytes\t" + req_sec + " req(s)\t" + data_avg + " bytes(avg)"); 38 | } 39 | return fs.writeSync(fd, "" + time + "," + t_req_count + "," + t_data_size + "," + req_count + "," + data_size + "," + req_sec + "," + data_avg + "\n", null, "utf-8"); 40 | }; 41 | flush = function() { 42 | var data_avg, req_sec; 43 | req_sec = data.hits / program.interval; 44 | data_avg = data.hits === 0 ? 0 : data.size / data.hits; 45 | total_data.hits += data.hits; 46 | total_data.size += data.size; 47 | write(Math.round(new Date().getTime() / 1000.0), total_data.hits, total_data.size, data.hits, data.size, req_sec, data_avg); 48 | return data = { 49 | hits: 0, 50 | size: 0 51 | }; 52 | }; 53 | null_resp = function(req, res) { 54 | data.hits += 1; 55 | req.on('data', function(chunk) { 56 | return data.size += chunk.length; 57 | }); 58 | return req.on('end', function() { 59 | return res.end(canned_response); 60 | }); 61 | }; 62 | s.use('/', null_resp); 63 | setInterval(flush, program.interval * 1000); 64 | }).call(this); 65 | --------------------------------------------------------------------------------