clarity 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +4 -0
- data/Manifest.txt +37 -0
- data/PostInstall.txt +8 -0
- data/README.rdoc +56 -0
- data/Rakefile +27 -0
- data/bin/clarity +10 -0
- data/config/config.yml.sample +7 -0
- data/lib/clarity.rb +20 -0
- data/lib/clarity/cli.rb +80 -0
- data/lib/clarity/commands/command_builder.rb +56 -0
- data/lib/clarity/commands/tail_command_builder.rb +27 -0
- data/lib/clarity/parsers/hostname_parser.rb +43 -0
- data/lib/clarity/parsers/shop_parser.rb +48 -0
- data/lib/clarity/parsers/time_parser.rb +93 -0
- data/lib/clarity/renderers/log_renderer.rb +47 -0
- data/lib/clarity/server.rb +143 -0
- data/lib/clarity/server/basic_auth.rb +24 -0
- data/lib/clarity/server/chunk_http.rb +57 -0
- data/lib/clarity/server/mime_types.rb +23 -0
- data/public/images/spinner_big.gif +0 -0
- data/public/javascripts/app.js +154 -0
- data/public/stylesheets/app.css +54 -0
- data/script/console +10 -0
- data/script/destroy +14 -0
- data/script/generate +14 -0
- data/test/commands/command_builder_test.rb +100 -0
- data/test/commands/tail_command_builder_test.rb +43 -0
- data/test/files/logfile.log +17 -0
- data/test/parsers/hostname_parser_test.rb +40 -0
- data/test/parsers/shop_parser_test.rb +54 -0
- data/test/parsers/time_parser_test.rb +84 -0
- data/test/test_helper.rb +3 -0
- data/test/test_string_scanner.rb +17 -0
- data/views/_header.html.erb +5 -0
- data/views/_toolbar.html.erb +89 -0
- data/views/error.html.erb +11 -0
- data/views/index.html.erb +9 -0
- metadata +134 -0
data/History.txt
ADDED
data/Manifest.txt
ADDED
@@ -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
|
data/PostInstall.txt
ADDED
data/README.rdoc
ADDED
@@ -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
|
+
|
data/Rakefile
ADDED
@@ -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]
|
data/bin/clarity
ADDED
data/lib/clarity.rb
ADDED
@@ -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
|
data/lib/clarity/cli.rb
ADDED
@@ -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
|