clarity 0.9.0

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,37 @@
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/command_builder.rb
11
+ lib/clarity/commands/tail_command_builder.rb
12
+ lib/clarity/parsers/hostname_parser.rb
13
+ lib/clarity/parsers/shop_parser.rb
14
+ lib/clarity/parsers/time_parser.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/command_builder_test.rb
27
+ test/commands/tail_command_builder_test.rb
28
+ test/files/logfile.log
29
+ test/parsers/hostname_parser_test.rb
30
+ test/parsers/shop_parser_test.rb
31
+ test/parsers/time_parser_test.rb
32
+ test/test_helper.rb
33
+ test/test_string_scanner.rb
34
+ views/_header.html.erb
35
+ views/_toolbar.html.erb
36
+ views/error.html.erb
37
+ 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,56 @@
1
+ = Clarity
2
+
3
+ * http://github.com/#{github_username}/#{project_name}
4
+
5
+ == DESCRIPTION:
6
+
7
+ Clarity - a log search tool
8
+ By John Tajima & Tobi Lütke
9
+
10
+ Clarity is an eventmachine-based web application that is used at Shopify to
11
+ search log files on production servers.
12
+
13
+ We wrote Clarity to allow authorized users to use a simple interface to look
14
+ through the various log files in our server farm, without requiring access to
15
+ production servers.
16
+
17
+ Clarity requires eventmachine and eventmachine/evma_httpserver.
18
+
19
+ sudo gem install eventmachine eventmachine_httpserver
20
+
21
+
22
+ == REQUIREMENTS:
23
+
24
+ * eventmachine
25
+ * eventmachine_httpserver
26
+
27
+ == INSTALL:
28
+
29
+ * sudo gem install clarity
30
+
31
+ == LICENSE:
32
+
33
+ (The MIT License)
34
+
35
+ Copyright (c) 2009 Tobias Lütke
36
+
37
+ Permission is hereby granted, free of charge, to any person obtaining
38
+ a copy of this software and associated documentation files (the
39
+ 'Software'), to deal in the Software without restriction, including
40
+ without limitation the rights to use, copy, modify, merge, publish,
41
+ distribute, sublicense, and/or sell copies of the Software, and to
42
+ permit persons to whom the Software is furnished to do so, subject to
43
+ the following conditions:
44
+
45
+ The above copyright notice and this permission notice shall be
46
+ included in all copies or substantial portions of the Software.
47
+
48
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
49
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
50
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
51
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
52
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
53
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
54
+ SOFTWARE OR THE USE OR OT
55
+
56
+
@@ -0,0 +1,27 @@
1
+ require 'rubygems'
2
+ gem 'hoe', '>= 2.1.0'
3
+ require 'hoe'
4
+ require 'fileutils'
5
+ require './lib/clarity'
6
+
7
+ Hoe.plugin :newgem
8
+ # Hoe.plugin :website
9
+ # Hoe.plugin :cucumberfeatures
10
+
11
+ # Generate all the Rake tasks
12
+ # Run 'rake -T' to see list of generated tasks (from gem root directory)
13
+ $hoe = Hoe.spec 'clarity' do
14
+ self.developer 'Tobias Lütke', 'tobi@shopify.com'
15
+ self.developer 'John Tajima', 'john@shopify.com'
16
+ self.post_install_message = 'PostInstall.txt' # TODO remove if post-install message not required
17
+ # self.rubyforge_name = self.name # TODO this is default value
18
+
19
+ self.extra_deps = [['eventmachine','>= 0.12.10'], ['eventmachine_httpserver','>= 0.2.0']]
20
+ end
21
+
22
+ require 'newgem/tasks'
23
+ Dir['tasks/**/*.rake'].each { |t| load t }
24
+
25
+ # TODO - want other tests/tasks run by default? Add them to the list
26
+ # remove_task :default
27
+ # task :default => [:spec, :features]
@@ -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,20 @@
1
+ $:.unshift(File.dirname(__FILE__)) unless $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
2
+
3
+ require 'eventmachine'
4
+ require 'evma_httpserver'
5
+ require 'yaml'
6
+ require 'base64'
7
+ require 'clarity/server'
8
+ require 'clarity/commands/command_builder'
9
+ require 'clarity/commands/tail_command_builder'
10
+ require 'clarity/parsers/time_parser'
11
+ require 'clarity/parsers/hostname_parser'
12
+ require 'clarity/parsers/shop_parser'
13
+ require 'clarity/renderers/log_renderer'
14
+
15
+ module Clarity
16
+ VERSION = '0.9.0'
17
+
18
+ Templates = File.dirname(__FILE__) + '/../views'
19
+ Public = File.dirname(__FILE__) + '/../public'
20
+ end
@@ -0,0 +1,80 @@
1
+ require 'optparse'
2
+ #require File.dirname(__FILE__) + '/../clarity'
3
+
4
+
5
+ module Clarity
6
+ class CLI
7
+ def self.execute(stdout, arguments=[])
8
+
9
+ options = {
10
+ :username => nil,
11
+ :password => nil,
12
+ :log_files => ['**/*.log*'],
13
+ :port => 8080,
14
+ :address => "0.0.0.0"
15
+ }
16
+
17
+ mandatory_options = %w( )
18
+
19
+ ARGV.options do |opts|
20
+ opts.banner = "Usage: #{File.basename($PROGRAM_NAME)} [options] directory"
21
+
22
+ opts.separator " "
23
+ opts.separator "Specific options:"
24
+
25
+ opts.on( "-f", "--config=FILE", String, "Config file (yml)" ) do |opt|
26
+ options.update YAML.load_file( opt )
27
+ end
28
+
29
+ opts.on( "-p", "--port=PORT", Integer, "Port to listen on" ) do |opt|
30
+ options[:port] = opt
31
+ end
32
+
33
+ opts.on( "-b", "--address=ADDRESS", String, "Address to bind to (default 0.0.0.0)" ) do |opt|
34
+ options[:address] = opt
35
+ end
36
+
37
+ opts.on( "--include=MASK", String, "File mask of logs to add (default: **/*.log*)" ) do |opt|
38
+ options[:log_files] ||= []
39
+ options[:log_files] += opt
40
+ end
41
+
42
+ opts.separator " "
43
+ opts.separator "Password protection:"
44
+
45
+ opts.on( "--username=USER", String, "Enable httpauth username" ) do |opt|
46
+ options[:username] = opt
47
+ end
48
+
49
+ opts.on( "--password=PASS", String, "Enable httpauth password" ) do |opt|
50
+ options[:password] = opt
51
+ end
52
+
53
+ opts.separator " "
54
+ opts.separator "Misc:"
55
+
56
+ opts.on( "-h", "--help", "Show this message." ) do
57
+ puts opts
58
+ exit
59
+ end
60
+
61
+ opts.separator " "
62
+
63
+ begin
64
+ opts.parse!(arguments)
65
+
66
+ if arguments.first
67
+ Dir.chdir(arguments.first)
68
+ end
69
+
70
+ ::Clarity::Server.run(options)
71
+
72
+ #rescue
73
+ # puts opts
74
+ # exit
75
+ end
76
+ end
77
+
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,56 @@
1
+ class CommandBuilder
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 < CommandBuilder
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,43 @@
1
+
2
+ class HostnameParser
3
+
4
+ # given a string in format:
5
+ #
6
+ # app3 rails.shopify[9855]: [wadedemt.myshopify.com] Processing ShopController#products (for 192.168.1.230 at 2009-07-24 14:58:21) [GET]
7
+ # 129.123.2.1 rails.shopify[9855]: [wadedemt.myshopify.com] Processing ShopController#products (for 192.168.1.230 at 2009-07-24 14:58:21) [GET]
8
+ #
9
+ # strips out the hostname/IP and appname
10
+ #
11
+ # result => [wadedemt.myshopify.com] Processing ShopController#products (for 192.168.1.230 at 2009-07-24 14:58:21) [GET]
12
+
13
+
14
+ LineRegexp = /^([\w-]+|\d+\.\d+\.\d+\.\d+)\s([^:]*):\s*(.*)/
15
+
16
+ attr_accessor :elements, :next_parser
17
+
18
+ def initialize(next_renderer = nil)
19
+ @next_renderer = next_renderer
20
+ end
21
+
22
+ def parse(line, elements = {})
23
+ @elements = elements
24
+ # parse line into elements and put into element
25
+ next_line = parse_line(line)
26
+ if @next_renderer && next_line
27
+ @elements = @next_renderer.parse(next_line, @elements)
28
+ end
29
+ @elements
30
+ end
31
+
32
+ # parse line and break into pieces
33
+ def parse_line(line)
34
+ results = LineRegexp.match(line)
35
+ if results
36
+ @elements[:line] = results[-1]
37
+ results[-1] # remaining line
38
+ else
39
+ @elements[:line] = line
40
+ line
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,48 @@
1
+ class ShopParser
2
+
3
+ # given a string in format:
4
+ #
5
+ # [wadedemt.myshopify.com] Processing ShopController#products (for 192.168.1.230 at 2009-07-24 14:58:21) [GET]
6
+ #
7
+ # strips out the shop name
8
+ #
9
+ # result => :shop => wadedemt.myshopify.com
10
+ # :line => Processing ShopController#products (for 192.168.1.230 at 2009-07-24 14:58:21) [GET]
11
+
12
+
13
+ LineRegexp = /^\s*\[([a-zA-Z0-9\-.]+)\]\s*(.*)/
14
+
15
+ attr_accessor :elements
16
+
17
+ def initialize(next_renderer = nil)
18
+ @next_renderer = next_renderer
19
+ end
20
+
21
+ def parse(line, elements = {})
22
+ @elements = elements
23
+ # parse line into elements and put into element
24
+ next_line = parse_line(line)
25
+ if @next_renderer && next_line
26
+ @elements = @next_renderer.parse(next_line, @elements)
27
+ end
28
+ @elements
29
+ end
30
+
31
+ # parse line and break into pieces
32
+ def parse_line(line)
33
+ results = LineRegexp.match(line)
34
+ if results
35
+ if results[1] =~ /\./
36
+ @elements[:shop] = results[1]
37
+ @elements[:line] = results[-1]
38
+ results[-1]
39
+ else
40
+ @elements[:line] = line
41
+ line
42
+ end
43
+ else
44
+ @elements[:line] = line
45
+ line
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,93 @@
1
+
2
+ class TimeParser
3
+
4
+ # strips out timestamp and if start/end times are defined, will reject lines that don't fall within proper time periods
5
+ #
6
+ # entry format:
7
+ # Jul 24 14:58:21 app3 rails.shopify[9855]: [wadedemt.myshopify.com] Processing ShopController#products (for 192.168.1.230 at 2009-07-24 14:58:21) [GET]
8
+ #
9
+ # params = {
10
+ # 'sh' => start hour
11
+ # 'sm' => start minute
12
+ # 'ss' => start second
13
+ # 'eh' => end hour
14
+ # 'em' => end minute
15
+ # 'es' => end second
16
+ # }
17
+ #
18
+ # if 'sh' is defined, reject any lines where timestamp is earlier than start time
19
+ # if 'eh' is defined, reject any lines where timestamp is later than end time
20
+ # if 'sh' && 'eh' is defined, reject any lines where timestamp is not between start time and end time
21
+
22
+ LineRegexp = /^(\w+\s+\d+\s\d\d:\d\d:\d\d)\s(.*)/
23
+
24
+ attr_accessor :elements, :params
25
+
26
+ def initialize(next_renderer = nil, params = {})
27
+ @next_renderer = next_renderer
28
+ @params = params
29
+ end
30
+
31
+ def parse(line, elements = {})
32
+ @elements = elements
33
+ next_line = parse_line(line)
34
+
35
+ # reject line if we filter by time
36
+ if check_time?
37
+ if !start_time_valid? || !end_time_valid?
38
+ # reject this entry
39
+ @elements = {}
40
+ return @elements
41
+ end
42
+ else
43
+ if @next_renderer && next_line
44
+ @elements = @next_renderer.parse(next_line, @elements)
45
+ end
46
+ end
47
+ @elements
48
+ end
49
+
50
+ def check_time?
51
+ (params['sh'] && !params['sh'].empty?) || (params['eh'] && !params['eh'].empty?)
52
+ end
53
+
54
+ # check if current line's time is >= start time, if it was set
55
+ def start_time_valid?
56
+ line_time = parse_time_from_string(@elements[:timestamp])
57
+ start_time = Time.utc(line_time.year, line_time.month, line_time.day, params.fetch('sh',0).to_i, params.fetch('sm', 0).to_i, params.fetch('ss', 0).to_i )
58
+ line_time >= start_time ? true : false
59
+ rescue Exception => e
60
+ puts "Error! #{e}"
61
+ end
62
+
63
+ def end_time_valid?
64
+ line_time = parse_time_from_string(@elements[:timestamp])
65
+ end_time = Time.utc(line_time.year, line_time.month, line_time.day, params.fetch('eh',23).to_i, params.fetch('em', 59).to_i, params.fetch('es', 59).to_i )
66
+ line_time <= end_time ? true : false
67
+ rescue Exception => e
68
+ puts "Error! #{e}"
69
+ end
70
+
71
+ def parse_time_from_string(text)
72
+ # Jul 24 14:58:21
73
+ time = nil
74
+ if text =~ /(\w+)\s+(\d+)\s+(\d+):(\d+):(\d+)/
75
+ time = Time.utc(Time.now.year, $1, $2, $3, $4, $5)
76
+ end
77
+ time
78
+ end
79
+
80
+ # parse line and break into pieces
81
+ def parse_line(line)
82
+ results = LineRegexp.match(line)
83
+ if results
84
+ @elements[:timestamp] = results[1]
85
+ @elements[:line] = results[-1]
86
+ results[-1] # remaining line
87
+ else
88
+ @elements[:line] = line
89
+ line
90
+ end
91
+ end
92
+
93
+ end