edouard-clarity 0.9.9

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt ADDED
@@ -0,0 +1,4 @@
1
+ === 0.0.1 2009-10-31
2
+
3
+ * 1 major enhancement:
4
+ * Initial release
data/Manifest.txt ADDED
@@ -0,0 +1,34 @@
1
+ History.txt
2
+ Manifest.txt
3
+ PostInstall.txt
4
+ README.rdoc
5
+ Rakefile
6
+ bin/clarity
7
+ config/config.yml.sample
8
+ lib/clarity.rb
9
+ lib/clarity/cli.rb
10
+ lib/clarity/commands/grep_command_builder.rb
11
+ lib/clarity/commands/hostname_command_builder.rb
12
+ lib/clarity/commands/tail_command_builder.rb
13
+ lib/clarity/grep_renderer.rb
14
+ lib/clarity/process_tree.rb
15
+ lib/clarity/renderers/log_renderer.rb
16
+ lib/clarity/server.rb
17
+ lib/clarity/server/basic_auth.rb
18
+ lib/clarity/server/chunk_http.rb
19
+ lib/clarity/server/mime_types.rb
20
+ public/images/spinner_big.gif
21
+ public/javascripts/app.js
22
+ public/stylesheets/app.css
23
+ script/console
24
+ script/destroy
25
+ script/generate
26
+ test/commands/grep_command_builder_test.rb
27
+ test/commands/tail_command_builder_test.rb
28
+ test/files/logfile.log
29
+ test/test_helper.rb
30
+ test/test_string_scanner.rb
31
+ views/_header.html.erb
32
+ views/_toolbar.html.erb
33
+ views/error.html.erb
34
+ views/index.html.erb
data/PostInstall.txt ADDED
@@ -0,0 +1,8 @@
1
+
2
+ For more information on clarity, see http://github/tobi/clarity
3
+
4
+ You can try clarity by running:
5
+ clarity -p 3000 /var/log
6
+
7
+
8
+
data/README.rdoc ADDED
@@ -0,0 +1,85 @@
1
+ = Clarity
2
+
3
+ * http://github.com/tobi/clarity
4
+
5
+ == DESCRIPTION:
6
+
7
+ Clarity - a log search tool
8
+ By John Tajima & Tobi Lütke
9
+
10
+ Clarity is a Splunk like web interface for your server log files. It supports
11
+ searching (using grep) as well as trailing log files in realtime. It has been written
12
+ using the event based architecture based on EventMachine and so allows real-time search
13
+ of very large log files. If you hit the browser Stop button it will also kill
14
+ the grep / tail utility.
15
+
16
+ We wrote Clarity to allow our support staff to use a simple interface to look
17
+ through the various log files in our server farm. The application was such a
18
+ big success internally that we decided to release it as open source.
19
+
20
+ == SECURITY:
21
+
22
+ *Warning*: Clarity takes parameters from URLs and runs them in the shell.
23
+ This is essentially the most insecure thing imaginable. You have to make absolutley sure
24
+ that clarity isn't reachable by the outside world. At the very least use --username and
25
+ --password to put some protection on it.
26
+
27
+ == USAGE:
28
+
29
+ clarity --username=admin --password=secret --port=8989 /var/log
30
+
31
+ == COMMANDLINE:
32
+
33
+ Specific options:
34
+ -f, --config=FILE Config file (yml)
35
+ -p, --port=PORT Port to listen on
36
+ -b, --address=ADDRESS Address to bind to (default 0.0.0.0)
37
+ --include=MASK File mask of logs to add (default: **/*.log*)
38
+ --user=USER User to run as
39
+ Password protection:
40
+ --username=USER Enable httpauth username
41
+ --password=PASS Enable httpauth password
42
+ Misc:
43
+ -h, --help Show this message.
44
+
45
+
46
+ == SCREENSHOT:
47
+
48
+ http://img.skitch.com/20091104-je9kq1a2gfr586ia8y246bq4n8.png
49
+
50
+ == REQUIREMENTS:
51
+
52
+ * eventmachine
53
+ * eventmachine_httpserver
54
+ * json
55
+
56
+ == INSTALL:
57
+
58
+ * sudo gem install clarity
59
+
60
+ == LICENSE:
61
+
62
+ (The MIT License)
63
+
64
+ Copyright (c) 2009 Tobias Lütke
65
+
66
+ Permission is hereby granted, free of charge, to any person obtaining
67
+ a copy of this software and associated documentation files (the
68
+ 'Software'), to deal in the Software without restriction, including
69
+ without limitation the rights to use, copy, modify, merge, publish,
70
+ distribute, sublicense, and/or sell copies of the Software, and to
71
+ permit persons to whom the Software is furnished to do so, subject to
72
+ the following conditions:
73
+
74
+ The above copyright notice and this permission notice shall be
75
+ included in all copies or substantial portions of the Software.
76
+
77
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
78
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
79
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
80
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
81
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
82
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
83
+ SOFTWARE OR THE USE OR OT
84
+
85
+
data/Rakefile ADDED
@@ -0,0 +1,23 @@
1
+ $LOAD_PATH.unshift File.join(File.dirname(__FILE__), "lib")
2
+
3
+ require 'rubygems'
4
+ gem 'hoe', '>= 2.1.0'
5
+ gem 'newgem'
6
+
7
+ require 'hoe'
8
+ require 'clarity'
9
+
10
+ Hoe.plugin :newgem
11
+
12
+ $hoe = Hoe.spec 'edouard-clarity' do
13
+ self.developer 'Tobias Lütke', 'tobi@shopify.com'
14
+ self.developer 'John Tajima', 'john@shopify.com'
15
+ self.summary = 'Web interface for grep and tail -f'
16
+ self.post_install_message = 'PostInstall.txt'
17
+ self.readme_file = 'README.rdoc'
18
+ self.extra_deps = [['eventmachine','>= 0.12.10'], ['eventmachine_httpserver','>= 0.2.0'], ["json", ">= 1.0.0"]]
19
+ self.test_globs = ['test/**/*_test.rb']
20
+ end
21
+
22
+ require 'newgem/tasks'
23
+ Dir['tasks/**/*.rake'].each { |t| load t }
data/bin/clarity ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # Created on 2009-10-31.
4
+ # Copyright (c) 2009. All rights reserved.
5
+
6
+ require 'rubygems'
7
+ require File.expand_path(File.dirname(__FILE__) + "/../lib/clarity")
8
+ require "clarity/cli"
9
+
10
+ Clarity::CLI.execute(STDOUT, ARGV)
@@ -0,0 +1,7 @@
1
+ # sample config file. Copy to config.yml since we are ignoring config.yml in .gitignore
2
+
3
+ log_files:
4
+ - /var/log/apps/user.log.*
5
+ - /var/log/mail.*
6
+
7
+ port: 8080
data/lib/clarity.rb ADDED
@@ -0,0 +1,24 @@
1
+ $:.unshift(File.dirname(__FILE__)) unless $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
2
+
3
+ begin
4
+ require File.join(File.dirname(__FILE__), *%w[.. vendor gems environment])
5
+ rescue LoadError
6
+ end
7
+
8
+ require 'eventmachine'
9
+ require 'evma_httpserver'
10
+ require 'json'
11
+ require 'yaml'
12
+ require 'base64'
13
+ require 'clarity/server'
14
+ require 'clarity/commands/grep_command_builder'
15
+ require 'clarity/commands/tail_command_builder'
16
+ require 'clarity/commands/hostname_command_builder'
17
+ require 'clarity/renderers/log_renderer'
18
+
19
+ module Clarity
20
+ VERSION = '0.9.9'
21
+
22
+ Templates = File.dirname(__FILE__) + '/../views'
23
+ Public = File.dirname(__FILE__) + '/../public'
24
+ end
@@ -0,0 +1,94 @@
1
+ require 'optparse'
2
+
3
+ module Clarity
4
+ class CLI
5
+ def self.execute(stdout, arguments=[])
6
+
7
+ options = {
8
+ :username => nil,
9
+ :password => nil,
10
+ :log_files => nil,
11
+ :port => 8080,
12
+ :address => "0.0.0.0",
13
+ :user => nil,
14
+ :group => nil,
15
+ :relative_root => nil
16
+ }
17
+
18
+ mandatory_options = %w( )
19
+
20
+ ARGV.options do |opts|
21
+ opts.banner = "Usage: #{File.basename($PROGRAM_NAME)} [options] directory"
22
+
23
+ opts.separator " "
24
+ opts.separator "Specific options:"
25
+
26
+ opts.on( "-f", "--config=FILE", String, "Config file (yml)" ) do |opt|
27
+ config = YAML.load_file( opt )
28
+ config.keys.each do |key|
29
+ options[key.to_sym] = config[key]
30
+ end
31
+ end
32
+
33
+ opts.on( "-p", "--port=PORT", Integer, "Port to listen on" ) do |opt|
34
+ options[:port] = opt
35
+ end
36
+
37
+ opts.on( "-b", "--address=ADDRESS", String, "Address to bind to (default 0.0.0.0)" ) do |opt|
38
+ options[:address] = opt
39
+ end
40
+
41
+ opts.on( "-r", "--relative=ROOT", String, "Run under a relative root" ) do |opt|
42
+ options[:relative_root] = opt
43
+ end
44
+
45
+ opts.on( "--include=MASK", String, "File mask of logs to add (default: **/*.log*)" ) do |opt|
46
+ options[:log_files] ||= []
47
+ options[:log_files] << opt
48
+ end
49
+
50
+ opts.on( "--user=USER", String, "User to run as" ) do |opt|
51
+ options[:user] = opt
52
+ end
53
+
54
+ opts.separator " "
55
+ opts.separator "Password protection:"
56
+
57
+ opts.on( "--username=USER", String, "Enable httpauth username" ) do |opt|
58
+ options[:username] = opt
59
+ end
60
+
61
+ opts.on( "--password=PASS", String, "Enable httpauth password" ) do |opt|
62
+ options[:password] = opt
63
+ end
64
+
65
+ opts.separator " "
66
+ opts.separator "Misc:"
67
+
68
+ opts.on( "-h", "--help", "Show this message." ) do
69
+ puts opts
70
+ exit
71
+ end
72
+
73
+ opts.separator " "
74
+
75
+ begin
76
+ opts.parse!(arguments)
77
+
78
+ options[:log_files] ||= ['**/*.log*']
79
+
80
+ if arguments.first
81
+ Dir.chdir(arguments.first)
82
+
83
+ ::Clarity::Server.run(options)
84
+
85
+ else
86
+ puts opts
87
+ exit(1)
88
+ end
89
+ end
90
+ end
91
+
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,57 @@
1
+ class GrepCommandBuilder
2
+
3
+ # parameter names
4
+ TermParameters = ['term1', 'term2', 'term3']
5
+ FileParameter = 'file'
6
+
7
+ attr_accessor :params
8
+ attr_reader :terms, :filename, :options
9
+
10
+ def initialize(params)
11
+ @params = params
12
+ @filename = params.fetch(FileParameter)
13
+ @terms = TermParameters.map {|term| params.fetch(term, nil) }.compact.reject {|term| term.empty? }
14
+ @options = ""
15
+ valid?
16
+ end
17
+
18
+ def valid?
19
+ raise InvalidParameterError, "Log file parameter not supplied" unless filename && !filename.empty?
20
+ true
21
+ end
22
+
23
+ def command
24
+ results = []
25
+ exec_functions.each_with_index do |cmd, index|
26
+ results << cmd.gsub('filename', filename.to_s).gsub('options', options.to_s).gsub('term', terms[index].to_s)
27
+ end
28
+ %[sh -c '#{results.join(" | ")}']
29
+ end
30
+
31
+
32
+ def exec_functions
33
+ case File.extname(filename)
34
+ when '.gz' then gzip_tools
35
+ when '.bz2' then bzip_tools
36
+ else default_tools
37
+ end
38
+ end
39
+
40
+
41
+ def gzip_tools
42
+ cat_tool = (ENV["PATH"].split(":").find{|d| File.exists?(File.join(d, "gzcat"))} ? "zcat" : "gzcat")
43
+ terms.empty? ? ["#{cat_tool} filename"] : ['zgrep options -e term filename'] + ['grep options -e term'] * (terms.size-1)
44
+ end
45
+
46
+ def bzip_tools
47
+ terms.empty? ? ['bzcat filename'] : ['bzgrep options -e term filename'] + ['grep options -e term'] * (terms.size-1)
48
+ end
49
+
50
+ def default_tools
51
+ terms.empty? ? ['cat filename'] : ['grep options -e term filename'] + ['grep options -e term']* (terms.size-1)
52
+ end
53
+
54
+
55
+ class InvalidParameterError < StandardError; end
56
+
57
+ end
@@ -0,0 +1,7 @@
1
+ class HostnameCommandBuilder
2
+
3
+ def self.command
4
+ `hostname`
5
+ end
6
+
7
+ end
@@ -0,0 +1,27 @@
1
+ #
2
+ # Handles tailing of log files
3
+ #
4
+ class TailCommandBuilder < GrepCommandBuilder
5
+
6
+ def valid?
7
+ raise InvalidParameterError, "Log file parameter not supplied or invalid log file" unless filename && !filename.empty?
8
+ true
9
+ end
10
+
11
+ def command
12
+ results = []
13
+ exec_functions.each_with_index do |cmd, index|
14
+ if index == 0
15
+ results << cmd.gsub('filename', filename.to_s)
16
+ else
17
+ results << cmd.gsub('filename', filename.to_s).gsub('options', options.to_s).gsub('term', terms[index-1].to_s)
18
+ end
19
+ end
20
+ %[sh -c '#{results.join(" | ")}']
21
+ end
22
+
23
+
24
+ def default_tools
25
+ terms.empty? ? ['tail -n 250 -f filename'] : ['tail -n 250 -f filename'] + ['grep options -e term'] * (terms.size)
26
+ end
27
+ end
@@ -0,0 +1,37 @@
1
+ module Clarity
2
+ module GrepRenderer
3
+ attr_accessor :response
4
+ attr_writer :renderer
5
+
6
+ def renderer
7
+ @renderer ||= LogRenderer.new
8
+ end
9
+
10
+ # once download is complete, send it to client
11
+ def receive_data(data)
12
+ @buffer ||= StringScanner.new("")
13
+ @buffer << data
14
+
15
+ while line = @buffer.scan_until(/\n/)
16
+ response.chunk renderer.render(line)
17
+ flush
18
+ end
19
+ end
20
+
21
+ def flush
22
+ response.send_chunks
23
+ end
24
+
25
+ def close
26
+ ProcessTree.kill(get_status.pid)
27
+ end
28
+
29
+ def unbind
30
+ response.chunk renderer.finalize
31
+ response.chunk ''
32
+ close
33
+ flush
34
+ puts 'Done'
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,23 @@
1
+ module ProcessTree
2
+
3
+ def self.kill(ppid)
4
+ return if ppid.nil?
5
+ all_pids = [ppid] + child_pids_of(ppid).flatten.uniq.compact
6
+ all_pids.each do |pid|
7
+ Process.kill('TERM',pid.to_i) rescue nil
8
+ end
9
+ end
10
+
11
+ def self.child_pids_of(ppid)
12
+ out = `ps -opid,ppid | grep #{ppid.to_s}`
13
+ ids = out.split("\n").map {|line| $1 if line =~ /^\s*([0-9]+)\s.*/ }.compact
14
+ ids.delete(ppid.to_s)
15
+ if ids.empty?
16
+ ids
17
+ else
18
+ ids << ids.map {|id| child_pids_of(id) }
19
+ end
20
+ end
21
+
22
+
23
+ end