logeater 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/.irbrc +2 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +47 -0
- data/Rakefile +12 -0
- data/bin/logeater +39 -0
- data/db/config.yml +13 -0
- data/db/migrate/20150110151439_create_requests.rb +31 -0
- data/db/migrate/20150122021627_change_requests_params_to_json.rb +9 -0
- data/db/schema.rb +43 -0
- data/lib/logeater/params_parser.rb +76 -0
- data/lib/logeater/parser.rb +161 -0
- data/lib/logeater/parser_errors.rb +34 -0
- data/lib/logeater/reader.rb +117 -0
- data/lib/logeater/request.rb +14 -0
- data/lib/logeater/version.rb +3 -0
- data/lib/logeater.rb +13 -0
- data/logeater.gemspec +38 -0
- data/test/data/single_request.gz +0 -0
- data/test/data/single_request.log +9 -0
- data/test/integration/logeater_test.rb +80 -0
- data/test/test_helper.rb +28 -0
- data/test/unit/params_parser_test.rb +96 -0
- data/test/unit/parser_test.rb +188 -0
- metadata +292 -0
data/logeater.gemspec
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require "logeater/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "logeater"
|
8
|
+
spec.version = Logeater::VERSION
|
9
|
+
spec.authors = ["Bob Lail"]
|
10
|
+
spec.email = ["bob.lail@cph.org"]
|
11
|
+
spec.summary = %q{Parses log files and imports them into a database}
|
12
|
+
spec.description = %q{Parses log files and imports them into a database}
|
13
|
+
spec.homepage = ""
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_dependency "activerecord", "~> 3.2.21"
|
22
|
+
spec.add_dependency "activesupport"
|
23
|
+
spec.add_dependency "pg"
|
24
|
+
spec.add_dependency "standalone_migrations"
|
25
|
+
spec.add_dependency "addressable"
|
26
|
+
spec.add_dependency "ruby-progressbar"
|
27
|
+
spec.add_dependency "activerecord-import", "~> 0.3.1"
|
28
|
+
spec.add_dependency "activerecord-postgres-json"
|
29
|
+
|
30
|
+
spec.add_development_dependency "bundler", "~> 1.7"
|
31
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
32
|
+
spec.add_development_dependency "rails", ">= 3.1.0", "< 5.0.0"
|
33
|
+
spec.add_development_dependency "pry"
|
34
|
+
spec.add_development_dependency "shoulda-context"
|
35
|
+
spec.add_development_dependency "turn"
|
36
|
+
spec.add_development_dependency "database_cleaner"
|
37
|
+
|
38
|
+
end
|
Binary file
|
@@ -0,0 +1,9 @@
|
|
1
|
+
I, [2015-01-10T15:18:12.064392 #2354] INFO -- : [livingsaviorco] [0fc5154a-c288-4bad-9c7a-de3d7e7d2496] Started GET "/people/1035826228?refresh_page=true" for 71.218.222.249 at 2015-01-10 15:18:12 +0000
|
2
|
+
I, [2015-01-10T15:18:12.067034 #2354] INFO -- : [livingsaviorco] [0fc5154a-c288-4bad-9c7a-de3d7e7d2496] Processing by PeopleController#show as JS
|
3
|
+
I, [2015-01-10T15:18:12.067134 #2354] INFO -- : [livingsaviorco] [0fc5154a-c288-4bad-9c7a-de3d7e7d2496] Parameters: {"refresh_page"=>"true", "id"=>"1035826228"}
|
4
|
+
I, [2015-01-10T15:18:12.156216 #2354] INFO -- : [livingsaviorco] [0fc5154a-c288-4bad-9c7a-de3d7e7d2496] Rendered people/_name.html.erb (2.3ms)
|
5
|
+
I, [2015-01-10T15:18:12.159557 #2354] INFO -- : [livingsaviorco] [0fc5154a-c288-4bad-9c7a-de3d7e7d2496] Rendered people/_status.html.erb (3.1ms)
|
6
|
+
I, [2015-01-10T15:18:12.177934 #2354] INFO -- : [livingsaviorco] [0fc5154a-c288-4bad-9c7a-de3d7e7d2496] Rendered people/_household.html.erb (18.2ms)
|
7
|
+
I, [2015-01-10T15:18:12.184299 #2354] INFO -- : [livingsaviorco] [0fc5154a-c288-4bad-9c7a-de3d7e7d2496] Rendered person_tags/_tag.html.erb (1.1ms)
|
8
|
+
I, [2015-01-10T15:18:12.262438 #2354] INFO -- : [livingsaviorco] [0fc5154a-c288-4bad-9c7a-de3d7e7d2496] [gzip] Compress reponse by 82.5 KB (87.6%) (2.6ms)
|
9
|
+
I, [2015-01-10T15:18:12.262903 #2354] INFO -- : [livingsaviorco] [0fc5154a-c288-4bad-9c7a-de3d7e7d2496] Completed 200 OK in 196ms (Views: 0.1ms | ActiveRecord: 50.0ms)
|
@@ -0,0 +1,80 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
class LogeaterTest < ActiveSupport::TestCase
|
4
|
+
attr_reader :logfile
|
5
|
+
|
6
|
+
|
7
|
+
context "Given the log of a single request, it" do
|
8
|
+
setup do
|
9
|
+
@logfile = File.expand_path("../../data/single_request.log", __FILE__)
|
10
|
+
end
|
11
|
+
|
12
|
+
should "identify the name of the logfile" do
|
13
|
+
assert_equal "single_request.log", reader.filename
|
14
|
+
end
|
15
|
+
|
16
|
+
should "create an entry in the database" do
|
17
|
+
assert_difference "Logeater::Request.count", +1 do
|
18
|
+
reader.import
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
should "set all the attributes" do
|
23
|
+
reader.import
|
24
|
+
request = Logeater::Request.first
|
25
|
+
|
26
|
+
params = {"refresh_page" => "true", "id" => "1035826228"}
|
27
|
+
assert_equal "test", request.app
|
28
|
+
assert_equal "single_request.log", request.logfile
|
29
|
+
assert_equal "0fc5154a-c288-4bad-9c7a-de3d7e7d2496", request.uuid
|
30
|
+
assert_equal "livingsaviorco", request.subdomain
|
31
|
+
assert_equal Time.new(2015, 1, 10, 15, 18, BigDecimal.new("12.064392")), request.started_at
|
32
|
+
assert_equal Time.new(2015, 1, 10, 15, 18, BigDecimal.new("12.262903")), request.completed_at
|
33
|
+
assert_equal 196, request.duration
|
34
|
+
assert_equal "GET", request.http_method
|
35
|
+
assert_equal "/people/1035826228", request.path
|
36
|
+
assert_equal params, request.params
|
37
|
+
assert_equal "people", request.controller
|
38
|
+
assert_equal "show", request.action
|
39
|
+
assert_equal "71.218.222.249", request.remote_ip
|
40
|
+
assert_equal "JS", request.format
|
41
|
+
assert_equal 200, request.http_status
|
42
|
+
assert_equal "OK", request.http_response
|
43
|
+
end
|
44
|
+
|
45
|
+
should "erase any entries that had already been imported with that app and filename" do
|
46
|
+
Logeater::Request.create!(app: app, logfile: "single_request.log", uuid: "1")
|
47
|
+
Logeater::Request.create!(app: app, logfile: "single_request.log", uuid: "2")
|
48
|
+
Logeater::Request.create!(app: app, logfile: "single_request.log", uuid: "3")
|
49
|
+
|
50
|
+
assert_difference "Logeater::Request.count", -2 do
|
51
|
+
reader.reimport
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
|
57
|
+
context "Given a gzipped logfile, it" do
|
58
|
+
setup do
|
59
|
+
@logfile = File.expand_path("../../data/single_request.gz", __FILE__)
|
60
|
+
end
|
61
|
+
|
62
|
+
should "create an entry in the database" do
|
63
|
+
assert_difference "Logeater::Request.count", +1 do
|
64
|
+
reader.import
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
def app
|
73
|
+
"test"
|
74
|
+
end
|
75
|
+
|
76
|
+
def reader
|
77
|
+
Logeater::Reader.new(app, logfile)
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
ENV["RAILS_ENV"] = "test"
|
2
|
+
require "rubygems"
|
3
|
+
require "rails"
|
4
|
+
require "rails/test_help"
|
5
|
+
require "pry"
|
6
|
+
require "database_cleaner"
|
7
|
+
require "shoulda/context"
|
8
|
+
require "turn"
|
9
|
+
require "logeater"
|
10
|
+
|
11
|
+
load File.expand_path("../../db/schema.rb", __FILE__)
|
12
|
+
|
13
|
+
DatabaseCleaner.strategy = :truncation
|
14
|
+
DatabaseCleaner.clean
|
15
|
+
|
16
|
+
DatabaseCleaner.strategy = :transaction
|
17
|
+
|
18
|
+
class ActiveSupport::TestCase
|
19
|
+
|
20
|
+
setup do
|
21
|
+
DatabaseCleaner.start
|
22
|
+
end
|
23
|
+
|
24
|
+
teardown do
|
25
|
+
DatabaseCleaner.clean
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
class ParamsParserTest < ActiveSupport::TestCase
|
4
|
+
|
5
|
+
|
6
|
+
context "Given a simple hash, it" do
|
7
|
+
should "parse it" do
|
8
|
+
assert_parses '{"utf8"=>"✓"}' => {"utf8"=>"✓"}
|
9
|
+
end
|
10
|
+
|
11
|
+
should "handle integers" do
|
12
|
+
assert_parses '{"person_id"=>10}' => {"person_id"=>10}
|
13
|
+
end
|
14
|
+
|
15
|
+
should "handle floats" do
|
16
|
+
assert_parses '{"person_id"=>10.56}' => {"person_id"=>10.56}
|
17
|
+
end
|
18
|
+
|
19
|
+
should "handle booleans" do
|
20
|
+
assert_parses '{"visible"=>true}' => {"visible"=>true}
|
21
|
+
end
|
22
|
+
|
23
|
+
should "handle nil" do
|
24
|
+
assert_parses '{"visible"=>nil}' => {"visible"=>nil}
|
25
|
+
end
|
26
|
+
|
27
|
+
should "handle arrays" do
|
28
|
+
assert_parses '{"ids"=>[1, 4]}' => {"ids"=>[1,4]}
|
29
|
+
end
|
30
|
+
|
31
|
+
|
32
|
+
|
33
|
+
should "handle empty strings" do
|
34
|
+
assert_parses '{"visible"=>""}' => {"visible"=>""}
|
35
|
+
end
|
36
|
+
|
37
|
+
should "handle empty arrays" do
|
38
|
+
assert_parses '{"array"=>[]}' => {"array"=>[]}
|
39
|
+
end
|
40
|
+
|
41
|
+
should "handle empty hashes" do
|
42
|
+
assert_parses '{"hash"=>{}}' => {"hash"=>{}}
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
|
47
|
+
context "Given a hash with more than one key, it" do
|
48
|
+
should "parse it" do
|
49
|
+
assert_parses '{"utf8"=>"✓", "authenticity_token"=>"kDM07..."}' => {"utf8"=>"✓", "authenticity_token"=>"kDM07..."}
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
|
54
|
+
context "Given a hash with a nested hash, it" do
|
55
|
+
should "handle nested hashes" do
|
56
|
+
assert_parses '{"person"=>{"name"=>"Tim"}}' => {"person"=>{"name"=>"Tim"}}
|
57
|
+
end
|
58
|
+
|
59
|
+
should "handle arrays of nested hashes" do
|
60
|
+
assert_parses '{"people"=>[{"id"=>1},{"id"=>2}]}' => {"people"=>[{"id"=>1},{"id"=>2}]}
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
|
65
|
+
context "Given a hash with a serialized Ruby object, it" do
|
66
|
+
should "parse it" do
|
67
|
+
assert_parses '{"tempfile"=>#<Tempfile:/tmp/RackMultipart20141213-1847-1c8fpzw>}' => {"tempfile"=>"Tempfile"}
|
68
|
+
end
|
69
|
+
|
70
|
+
should "ignore the object's ivars" do
|
71
|
+
assert_parses '{"photo"=>#<ActionDispatch::Http::UploadedFile:0x007f7c685318a8 @tempfile=#<Tempfile:/tmp/RackMultipart20141213-1847-1c8fpzw>, @original_filename="Martin-008.JPG", @content_type="image/jpeg", @headers="Content-Disposition: form-data; name=\"person[photo]\"; filename=\"Martin-008.JPG\"\\r\\nContent-Type: image/jpeg\\r\\n\">}' => {"photo"=>"ActionDispatch::Http::UploadedFile"}
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
|
76
|
+
context "Given an invalid input, it" do
|
77
|
+
should "raise MalformedParameters" do
|
78
|
+
assert_raises Logeater::Parser::MalformedParameters do
|
79
|
+
parse! '{"nope"=>}'
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
def assert_parses(params)
|
88
|
+
value, result = params.to_a[0]
|
89
|
+
assert_equal result, parse!(value)
|
90
|
+
end
|
91
|
+
|
92
|
+
def parse!(value)
|
93
|
+
Logeater::ParamsParser.new(value).parse!
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
@@ -0,0 +1,188 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
class ParserTest < ActiveSupport::TestCase
|
4
|
+
attr_reader :line
|
5
|
+
|
6
|
+
|
7
|
+
|
8
|
+
context "given a line that doesn't start with the generic log prefix, it" do
|
9
|
+
setup do
|
10
|
+
@line = "\n"
|
11
|
+
end
|
12
|
+
|
13
|
+
should "raise UnmatchedLine" do
|
14
|
+
assert_raises Logeater::Parser::UnmatchedLine do
|
15
|
+
parse!
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
|
21
|
+
|
22
|
+
context "given any generic log line, it" do
|
23
|
+
setup do
|
24
|
+
@line = "I, [2015-01-10T15:18:05.850839 #18070] INFO -- : the message\n"
|
25
|
+
end
|
26
|
+
|
27
|
+
should "identify the line as :generic" do
|
28
|
+
assert_parses type: :generic
|
29
|
+
end
|
30
|
+
|
31
|
+
should "identify the log level" do
|
32
|
+
assert_parses log_level: "INFO"
|
33
|
+
end
|
34
|
+
|
35
|
+
should "identify the time, including milliseconds" do
|
36
|
+
assert_parses timestamp: Time.new(2015, 1, 10, 15, 18, BigDecimal.new("5.850839"))
|
37
|
+
end
|
38
|
+
|
39
|
+
should "identify the remainder of the log message" do
|
40
|
+
assert_parses message: "the message"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
|
45
|
+
|
46
|
+
context "given a log line for a Rails request, it" do
|
47
|
+
setup do
|
48
|
+
@line = "I, [2015-01-10T15:18:05.850839 #18070] INFO -- : [livingsaviorco] [2d89d962-57c4-47c9-a9e9-6a16a5f22a12] [gzip] Compress reponse by 42.2 KB (83.3%) (1.4ms)"
|
49
|
+
end
|
50
|
+
|
51
|
+
should "identify the line as :generic" do
|
52
|
+
assert_parses type: :request_line
|
53
|
+
end
|
54
|
+
|
55
|
+
should "identify the subdomain" do
|
56
|
+
assert_parses subdomain: "livingsaviorco"
|
57
|
+
end
|
58
|
+
|
59
|
+
should "identify the request's ID" do
|
60
|
+
assert_parses uuid: "2d89d962-57c4-47c9-a9e9-6a16a5f22a12"
|
61
|
+
end
|
62
|
+
|
63
|
+
should "identify the remainder of the log message" do
|
64
|
+
assert_parses message: "[gzip] Compress reponse by 42.2 KB (83.3%) (1.4ms)"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
|
69
|
+
|
70
|
+
context "given the \"Started\" line, it" do
|
71
|
+
setup do
|
72
|
+
@line = "I, [2015-01-10T15:18:12.064392 #2354] INFO -- : [livingsaviorco] [0fc5154a-c288-4bad-9c7a-de3d7e7d2496] Started GET \"/people/1035826228?refresh_page=true\" for 71.218.222.249 at 2015-01-10 15:18:12 +0000"
|
73
|
+
end
|
74
|
+
|
75
|
+
should "identify the line as :request_started" do
|
76
|
+
assert_parses type: :request_started
|
77
|
+
end
|
78
|
+
|
79
|
+
should "identify the HTTP method" do
|
80
|
+
assert_parses http_method: "GET"
|
81
|
+
end
|
82
|
+
|
83
|
+
should "identify the path (without params)" do
|
84
|
+
assert_parses path: "/people/1035826228"
|
85
|
+
end
|
86
|
+
|
87
|
+
should "identify the remote client's IP address" do
|
88
|
+
assert_parses remote_ip: "71.218.222.249"
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
|
93
|
+
|
94
|
+
context "given the \"Processing by\" line, it" do
|
95
|
+
setup do
|
96
|
+
@line = "I, [2015-01-10T15:18:12.067034 #2354] INFO -- : [livingsaviorco] [0fc5154a-c288-4bad-9c7a-de3d7e7d2496] Processing by Api::V1::PeopleController#show as JS"
|
97
|
+
end
|
98
|
+
|
99
|
+
should "identify the line as :request_controller" do
|
100
|
+
assert_parses type: :request_controller
|
101
|
+
end
|
102
|
+
|
103
|
+
should "identify the controller and action" do
|
104
|
+
assert_parses controller: "api/v1/people", action: "show"
|
105
|
+
end
|
106
|
+
|
107
|
+
should "identify the format requested" do
|
108
|
+
assert_parses format: "JS"
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
|
113
|
+
|
114
|
+
context "given the \"Parameters\" line, it" do
|
115
|
+
setup do
|
116
|
+
@line = "I, [2015-01-10T15:18:12.067134 #2354] INFO -- : [livingsaviorco] [0fc5154a-c288-4bad-9c7a-de3d7e7d2496] Parameters: {\"refresh_page\"=>\"true\", \"id\"=>\"1035826228\"}"
|
117
|
+
end
|
118
|
+
|
119
|
+
should "identify the line as :request_params" do
|
120
|
+
assert_parses type: :request_params
|
121
|
+
end
|
122
|
+
|
123
|
+
should "identify the params" do
|
124
|
+
assert_parses params: {"refresh_page" => "true", "id" => "1035826228"}
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
context "when the \"Parameters\" line contains invalid syntax, it" do
|
129
|
+
setup do
|
130
|
+
@line = "I, [2015-01-10T15:18:12.067134 #2354] INFO -- : [livingsaviorco] [0fc5154a-c288-4bad-9c7a-de3d7e7d2496] Parameters: {\"refresh_page\"=>}"
|
131
|
+
end
|
132
|
+
|
133
|
+
should "return the params unparsed" do
|
134
|
+
assert_parses params: "{\"refresh_page\"=>}"
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
|
139
|
+
|
140
|
+
context "given the \"Completed\" line, it" do
|
141
|
+
setup do
|
142
|
+
@line = "I, [2015-01-10T15:18:12.262903 #2354] INFO -- : [livingsaviorco] [0fc5154a-c288-4bad-9c7a-de3d7e7d2496] Completed 401 Unauthorized in 2ms"
|
143
|
+
end
|
144
|
+
|
145
|
+
should "identify the line as :request_completed" do
|
146
|
+
assert_parses type: :request_completed
|
147
|
+
end
|
148
|
+
|
149
|
+
should "identify the HTTP response" do
|
150
|
+
assert_parses http_status: 401, http_response: "Unauthorized"
|
151
|
+
end
|
152
|
+
|
153
|
+
should "identify the duration of the request" do
|
154
|
+
assert_parses duration: 2
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
context "when the \"Completed\" line contains a breakdown of times, it" do
|
159
|
+
setup do
|
160
|
+
@line = "I, [2015-01-10T15:18:12.262903 #2354] INFO -- : [livingsaviorco] [0fc5154a-c288-4bad-9c7a-de3d7e7d2496] Completed 200 OK in 196ms (Views: 0.1ms | ActiveRecord: 50.0ms)"
|
161
|
+
end
|
162
|
+
|
163
|
+
should "identify the duration of the request" do
|
164
|
+
assert_parses duration: 196
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
|
169
|
+
|
170
|
+
private
|
171
|
+
|
172
|
+
def assert_parses(expectations)
|
173
|
+
results = parse!
|
174
|
+
|
175
|
+
expectations.each do |key, value|
|
176
|
+
assert_equal value, results[key]
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
def parse!
|
181
|
+
parser.parse!(line)
|
182
|
+
end
|
183
|
+
|
184
|
+
def parser
|
185
|
+
Logeater::Parser.new
|
186
|
+
end
|
187
|
+
|
188
|
+
end
|