logeater 0.1.1
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.
- 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
|