kastner-clarity 0.9.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,4 @@
1
+ === 0.0.1 2009-10-31
2
+
3
+ * 1 major enhancement:
4
+ * Initial release
@@ -0,0 +1,33 @@
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/tail_command_builder.rb
12
+ lib/clarity/grep_renderer.rb
13
+ lib/clarity/process_tree.rb
14
+ lib/clarity/renderers/log_renderer.rb
15
+ lib/clarity/server.rb
16
+ lib/clarity/server/basic_auth.rb
17
+ lib/clarity/server/chunk_http.rb
18
+ lib/clarity/server/mime_types.rb
19
+ public/images/spinner_big.gif
20
+ public/javascripts/app.js
21
+ public/stylesheets/app.css
22
+ script/console
23
+ script/destroy
24
+ script/generate
25
+ test/commands/grep_command_builder_test.rb
26
+ test/commands/tail_command_builder_test.rb
27
+ test/files/logfile.log
28
+ test/test_helper.rb
29
+ test/test_string_scanner.rb
30
+ views/_header.html.erb
31
+ views/_toolbar.html.erb
32
+ views/error.html.erb
33
+ views/index.html.erb
@@ -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
+
@@ -0,0 +1,78 @@
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
+ == USAGE:
21
+
22
+ clarity --username=admin --password=secret --port=8989 /var/log
23
+
24
+ == COMMANDLINE:
25
+
26
+ Specific options:
27
+ -f, --config=FILE Config file (yml)
28
+ -p, --port=PORT Port to listen on
29
+ -b, --address=ADDRESS Address to bind to (default 0.0.0.0)
30
+ --include=MASK File mask of logs to add (default: **/*.log*)
31
+ --user=USER User to run as
32
+ Password protection:
33
+ --username=USER Enable httpauth username
34
+ --password=PASS Enable httpauth password
35
+ Misc:
36
+ -h, --help Show this message.
37
+
38
+
39
+ == SCREENSHOT:
40
+
41
+ http://img.skitch.com/20091104-je9kq1a2gfr586ia8y246bq4n8.png
42
+
43
+ == REQUIREMENTS:
44
+
45
+ * eventmachine
46
+ * eventmachine_httpserver
47
+ * json
48
+
49
+ == INSTALL:
50
+
51
+ * sudo gem install clarity
52
+
53
+ == LICENSE:
54
+
55
+ (The MIT License)
56
+
57
+ Copyright (c) 2009 Tobias Lütke
58
+
59
+ Permission is hereby granted, free of charge, to any person obtaining
60
+ a copy of this software and associated documentation files (the
61
+ 'Software'), to deal in the Software without restriction, including
62
+ without limitation the rights to use, copy, modify, merge, publish,
63
+ distribute, sublicense, and/or sell copies of the Software, and to
64
+ permit persons to whom the Software is furnished to do so, subject to
65
+ the following conditions:
66
+
67
+ The above copyright notice and this permission notice shall be
68
+ included in all copies or substantial portions of the Software.
69
+
70
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
71
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
72
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
73
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
74
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
75
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
76
+ SOFTWARE OR THE USE OR OT
77
+
78
+
@@ -0,0 +1,24 @@
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 'kastner-clarity' do
13
+ self.developer 'Tobias Lütke', 'tobi@shopify.com'
14
+ self.developer 'John Tajima', 'john@shopify.com'
15
+ self.developer 'Erik Kastner', 'kastner@gmail.com'
16
+ self.summary = 'Web interface for grep and tail -f'
17
+ self.post_install_message = 'PostInstall.txt'
18
+ self.readme_file = 'README.rdoc'
19
+ self.extra_deps = [['eventmachine','>= 0.12.10'], ['eventmachine_httpserver','>= 0.2.0'], ["json", ">= 1.0.0"]]
20
+ self.test_globs = ['test/**/*_test.rb']
21
+ end
22
+
23
+ require 'newgem/tasks'
24
+ Dir['tasks/**/*.rake'].each { |t| load t }
@@ -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
@@ -0,0 +1,23 @@
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/renderers/log_renderer'
17
+
18
+ module Clarity
19
+ VERSION = '0.9.7'
20
+
21
+ Templates = File.dirname(__FILE__) + '/../views'
22
+ Public = File.dirname(__FILE__) + '/../public'
23
+ 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,56 @@
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
+ terms.empty? ? ['gzcat filename'] : ['zgrep options -e term filename'] + ['grep options -e term'] * (terms.size-1)
43
+ end
44
+
45
+ def bzip_tools
46
+ terms.empty? ? ['bzcat filename'] : ['bzgrep options -e term filename'] + ['grep options -e term'] * (terms.size-1)
47
+ end
48
+
49
+ def default_tools
50
+ terms.empty? ? ['cat filename'] : ['grep options -e term filename'] + ['grep options -e term']* (terms.size-1)
51
+ end
52
+
53
+
54
+ class InvalidParameterError < StandardError; end
55
+
56
+ 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? && File.extname(filename) == ".log"
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
@@ -0,0 +1,36 @@
1
+ require 'uri'
2
+ require 'erb'
3
+
4
+ class LogRenderer
5
+
6
+ # Thank you to http://daringfireball.net/2009/11/liberal_regex_for_matching_urls
7
+ #
8
+ UrlParser = %r{\b(([\w-]+://?|www[.])[^\s()<>]+(?:\([\w\d]+\)|([^[:punct:]\s]|/)))}
9
+ Prefix = ""
10
+ Suffix = "<br/>\n"
11
+
12
+ def render(line = {})
13
+ # Escape
14
+ output = ERB::Util.h(line)
15
+
16
+ # Transform urls into html links
17
+ output.gsub!(UrlParser) do |match|
18
+ html_link(match)
19
+ end
20
+
21
+ # Return with formatting
22
+ "#{Prefix}#{output}#{Suffix}"
23
+ end
24
+
25
+ def finalize
26
+ '</div><hr><p id="done">Done</p></body></html>'
27
+ end
28
+
29
+ private
30
+
31
+ def html_link(url)
32
+ uri = URI.parse(url) rescue url
33
+ "<a href='#{uri}'>#{url}</a>"
34
+ end
35
+
36
+ end