edouard-clarity 0.9.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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