clarity 0.9.0

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,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