kastner-clarity 0.9.7

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.
@@ -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