lograge 0.0.6 → 0.1.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.
data/Gemfile CHANGED
@@ -2,3 +2,8 @@ source "http://rubygems.org"
2
2
 
3
3
  # Specify your gem's dependencies in lograge.gemspec
4
4
  gemspec
5
+
6
+ group :test do
7
+ gem 'actionpack'
8
+ gem 'logstash-event'
9
+ end
data/README.md CHANGED
@@ -34,7 +34,7 @@ Completed 200 OK in 79ms (Views: 78.8ms | ActiveRecord: 0.0ms)
34
34
  you get a single line with all the important information, like this:
35
35
 
36
36
  ```
37
- GET /jobs/833552.json format=json action=jobs#show status=200 duration=58.33 view=40.43 db=15.26
37
+ method=GET path=/jobs/833552.json format=json controller=jobs action=show status=200 duration=58.33 view=40.43 db=15.26
38
38
  ```
39
39
 
40
40
  The second line is easy to grasp with a single glance and still includes all the
@@ -70,11 +70,29 @@ MyApp::Application.configure do
70
70
  # if it's a lambda then it must return a hash
71
71
  config.lograge.custom_options = lambda do |event|
72
72
  # capture some specific timing values you are interested in
73
- {:name => "value", :name => "%2f" % float}
73
+ {:name => "value", :timing => some_float.round(2)}
74
74
  end
75
75
  end
76
76
  ```
77
77
 
78
+ Lograge supports multiple output formats. The most common is the default
79
+ lograge format described above. Alternatively, you can also generate JSON
80
+ logs in the json_event format used by [Logstash](http://logstash.net/).
81
+
82
+ ```
83
+ # config/environments/production.rb
84
+ MyApp::Application.configure do
85
+ config.lograge.log_format = :logstash
86
+ end
87
+ ```
88
+
89
+ *Note:* When using the logstash output, you need to add the additional gem
90
+ `logstash-event`. You can simply add it to your Gemfile like this
91
+
92
+ ```ruby
93
+ gem "logstash-event"
94
+ ```
95
+
78
96
  Done.
79
97
 
80
98
  **Internals**
@@ -143,6 +161,9 @@ that fits in with the general spirit of the log output generated by Lograge.
143
161
 
144
162
  **Changes**
145
163
 
164
+ * Add support for Logstash events (Holger Just, http://github.com/meineerde)
165
+ * Fix for Rails 3.2.9
166
+ * Use keys everywhere (Curt Michols, http://github.com/asenchi)
146
167
  * Add `custom_options` to allow adding custom key-value pairs at runtime (Adam
147
168
  Cooper, https://github.com/adamcooper)
148
169
 
@@ -24,6 +24,14 @@ module Lograge
24
24
  end
25
25
  end
26
26
 
27
+ # The emitted log format
28
+ #
29
+ # Currently supported formats are>
30
+ # - :lograge - The custom tense lograge format
31
+ # - :logstash - JSON formatted as a Logstash Event.
32
+ mattr_accessor :log_format
33
+ self.log_format = :lograge
34
+
27
35
  def self.remove_existing_log_subscriptions
28
36
  ActiveSupport::LogSubscriber.log_subscribers.each do |subscriber|
29
37
  case subscriber
@@ -52,6 +60,16 @@ module Lograge
52
60
  Lograge.remove_existing_log_subscriptions
53
61
  Lograge::RequestLogSubscriber.attach_to :action_controller
54
62
  Lograge.custom_options = app.config.lograge.custom_options
63
+ Lograge.log_format = app.config.lograge.log_format || :lograge
64
+ case Lograge.log_format.to_s
65
+ when "logstash"
66
+ begin
67
+ require "logstash-event"
68
+ rescue LoadError
69
+ puts "You need to install the logstash-event gem to use the logstash output."
70
+ raise
71
+ end
72
+ end
55
73
  end
56
74
  end
57
75
 
@@ -5,12 +5,42 @@ module Lograge
5
5
  class RequestLogSubscriber < ActiveSupport::LogSubscriber
6
6
  def process_action(event)
7
7
  payload = event.payload
8
- message = "#{payload[:method]} #{payload[:path]} format=#{payload[:format]} action=#{payload[:params]['controller']}##{payload[:params]['action']}"
9
- message << extract_status(payload)
10
- message << runtimes(event)
11
- message << location(event)
12
- message << custom_options(event)
13
- logger.info(message)
8
+
9
+ data = extract_request(payload)
10
+ data.merge! extract_status(payload)
11
+ data.merge! runtimes(event)
12
+ data.merge! location(event)
13
+ data.merge! custom_options(event)
14
+
15
+ logger.info send(:"process_action_#{Lograge.log_format}", data)
16
+ end
17
+
18
+ LOGRAGE_FIELDS = [
19
+ :method, :path, :format, :controller, :action, :status, :error,
20
+ :duration, :view, :db, :location
21
+ ]
22
+ def process_action_lograge(data)
23
+ fields = LOGRAGE_FIELDS
24
+ fields += (data.keys - LOGRAGE_FIELDS)
25
+
26
+ event = fields.inject([]) do |message, key|
27
+ next message unless data.has_key?(key)
28
+ # Exactly preserve the previous output
29
+ # Parsing this can be ambigious if the error messages contains
30
+ # a single quote
31
+ data[key] = "'#{data[key]}'" if key == :error
32
+ # Ensure that we always have exactly two decimals
33
+ data[key] = "%.2f" % data[key] if data[key].is_a? Numeric
34
+
35
+ message << "#{key}=#{data[key]}"
36
+ message
37
+ end
38
+ event.join(" ")
39
+ end
40
+
41
+ def process_action_logstash(data)
42
+ event = LogStash::Event.new("@fields" => data)
43
+ event.to_json
14
44
  end
15
45
 
16
46
  def redirect_to(event)
@@ -19,41 +49,56 @@ module Lograge
19
49
 
20
50
  private
21
51
 
52
+ def extract_request(payload)
53
+ {
54
+ :method => payload[:method],
55
+ :path => payload[:path],
56
+ :format => extract_format(payload),
57
+ :controller => payload[:params]['controller'],
58
+ :action => payload[:params]['action']
59
+ }
60
+ end
61
+
62
+ def extract_format(payload)
63
+ if ::ActionPack::VERSION::MAJOR == 3 && ::ActionPack::VERSION::MINOR == 0
64
+ payload[:formats].first
65
+ else
66
+ payload[:format]
67
+ end
68
+ end
69
+
22
70
  def extract_status(payload)
23
71
  if payload[:status]
24
- " status=#{payload[:status]}"
72
+ { :status => payload[:status].to_i }
25
73
  elsif payload[:exception]
26
74
  exception, message = payload[:exception]
27
- " status=500 error='#{exception}:#{message}'"
75
+ { :status => 500, :error => "#{exception}:#{message}" }
28
76
  else
29
- " status=0"
77
+ { :status => 0 }
30
78
  end
31
79
  end
32
80
 
33
81
  def custom_options(event)
34
- message = ""
35
- (Lograge.custom_options(event) || {}).each do |name, value|
36
- message << " #{name}=#{value}"
37
- end
38
- message
82
+ Lograge.custom_options(event) || {}
39
83
  end
40
84
 
41
85
  def runtimes(event)
42
- message = ""
43
- {:duration => event.duration,
44
- :view => event.payload[:view_runtime],
45
- :db => event.payload[:db_runtime]}.each do |name, runtime|
46
- message << " #{name}=%.2f" % runtime if runtime
86
+ {
87
+ :duration => event.duration,
88
+ :view => event.payload[:view_runtime],
89
+ :db => event.payload[:db_runtime]
90
+ }.inject({}) do |runtimes, (name, runtime)|
91
+ runtimes[name] = runtime.to_f.round(2) if runtime
92
+ runtimes
47
93
  end
48
- message
49
94
  end
50
95
 
51
96
  def location(event)
52
97
  if location = Thread.current[:lograge_location]
53
98
  Thread.current[:lograge_location] = nil
54
- " location=#{location}"
99
+ { :location => location }
55
100
  else
56
- ""
101
+ {}
57
102
  end
58
103
  end
59
104
  end
@@ -9,7 +9,8 @@ module Rails
9
9
  # Started GET / for 192.168.2.1...
10
10
  class Logger
11
11
  # Overwrites Rails 3.2 code that logs new requests
12
- def call_app(env)
12
+ def call_app(*args)
13
+ env = args.last
13
14
  @app.call(env)
14
15
  ensure
15
16
  ActiveSupport::LogSubscriber.flush_all!
@@ -1,3 +1,3 @@
1
1
  module Lograge
2
- VERSION = "0.0.6"
2
+ VERSION = "0.1.0"
3
3
  end
@@ -35,7 +35,11 @@ describe Lograge::RequestLogSubscriber do
35
35
  )
36
36
  }
37
37
 
38
- describe "when processing an action" do
38
+ describe "when processing an action with lograge output" do
39
+ before do
40
+ Lograge::log_format = :lograge
41
+ end
42
+
39
43
  it "should include the URL in the log output" do
40
44
  subscriber.process_action(event)
41
45
  log_output.string.should include('/home')
@@ -43,7 +47,7 @@ describe Lograge::RequestLogSubscriber do
43
47
 
44
48
  it "should start the log line with the HTTP method" do
45
49
  subscriber.process_action(event)
46
- log_output.string.starts_with?('GET').should == true
50
+ log_output.string.starts_with?('method=GET').should == true
47
51
  end
48
52
 
49
53
  it "should include the status code" do
@@ -53,7 +57,7 @@ describe Lograge::RequestLogSubscriber do
53
57
 
54
58
  it "should include the controller and action" do
55
59
  subscriber.process_action(event)
56
- log_output.string.should include('action=home#index')
60
+ log_output.string.should include('controller=home action=index')
57
61
  end
58
62
 
59
63
  it "should include the duration" do
@@ -108,7 +112,90 @@ describe Lograge::RequestLogSubscriber do
108
112
  end
109
113
  end
110
114
 
111
- describe "with custom_options configured" do
115
+ describe "when processing an action with logstash output" do
116
+ before do
117
+ require 'logstash-event'
118
+ Lograge::log_format = :logstash
119
+ end
120
+
121
+ it "should include the URL in the log output" do
122
+ subscriber.process_action(event)
123
+ log_output.string.should include('/home')
124
+ end
125
+
126
+ it "should start include the HTTP method" do
127
+ subscriber.process_action(event)
128
+ log_output.string.should include('"method":"GET"')
129
+ end
130
+
131
+ it "should include the status code" do
132
+ subscriber.process_action(event)
133
+ log_output.string.should include('"status":200')
134
+ end
135
+
136
+ it "should include the controller and action" do
137
+ subscriber.process_action(event)
138
+ log_output.string.should include('"controller":"home"')
139
+ log_output.string.should include('"action":"index"')
140
+ end
141
+
142
+ it "should include the duration" do
143
+ subscriber.process_action(event)
144
+ log_output.string.should =~ /"duration":\d+\.\d{0,2}/
145
+ end
146
+
147
+ it "should include the view rendering time" do
148
+ subscriber.process_action(event)
149
+ log_output.string.should =~ /"view":0.01/
150
+ end
151
+
152
+ it "should include the database rendering time" do
153
+ subscriber.process_action(event)
154
+ log_output.string.should =~ /"db":0.02/
155
+ end
156
+
157
+ it "should add a 500 status when an exception occurred" do
158
+ event.payload[:status] = nil
159
+ event.payload[:exception] = ['AbstractController::ActionNotFound', 'Route not found']
160
+ subscriber.process_action(event)
161
+ log_output.string.should =~ /"status":500/
162
+ log_output.string.should =~ /"error":"AbstractController::ActionNotFound:Route not found"/
163
+ end
164
+
165
+ it "should return an unknown status when no status or exception is found" do
166
+ event.payload[:status] = nil
167
+ event.payload[:exception] = nil
168
+ subscriber.process_action(event)
169
+ log_output.string.should =~ /"status":0/
170
+ end
171
+
172
+ describe "with a redirect" do
173
+ before do
174
+ Thread.current[:lograge_location] = "http://www.example.com"
175
+ end
176
+
177
+ it "should add the location to the log line" do
178
+ subscriber.process_action(event)
179
+ log_output.string.should =~ %r{"location":"http://www.example.com"}
180
+ end
181
+
182
+ it "should remove the thread local variable" do
183
+ subscriber.process_action(event)
184
+ Thread.current[:lograge_location].should == nil
185
+ end
186
+ end
187
+
188
+ it "should not include a location by default" do
189
+ subscriber.process_action(event)
190
+ log_output.string.should_not =~ /"location":/
191
+ end
192
+ end
193
+
194
+ describe "with custom_options configured for lograge output" do
195
+ before do
196
+ Lograge::log_format = :lograge
197
+ end
198
+
112
199
  it "should combine the hash properly for the output" do
113
200
  Lograge.custom_options = {:data => "value"}
114
201
  subscriber.process_action(event)
@@ -126,6 +213,28 @@ describe Lograge::RequestLogSubscriber do
126
213
  end
127
214
  end
128
215
 
216
+ describe "with custom_options configured for logstash output" do
217
+ before do
218
+ Lograge::log_format = :logstash
219
+ end
220
+
221
+ it "should combine the hash properly for the output" do
222
+ Lograge.custom_options = {:data => "value"}
223
+ subscriber.process_action(event)
224
+ log_output.string.should =~ /"data":"value"/
225
+ end
226
+ it "should combine the output of a lambda properly" do
227
+ Lograge.custom_options = lambda {|event| {:data => "value"}}
228
+ subscriber.process_action(event)
229
+ log_output.string.should =~ /"data":"value"/
230
+ end
231
+ it "should work if the method returns nil" do
232
+ Lograge.custom_options = lambda {|event| nil}
233
+ subscriber.process_action(event)
234
+ log_output.string.should be_present
235
+ end
236
+ end
237
+
129
238
  describe "when processing a redirect" do
130
239
  it "should store the location in a thread local variable" do
131
240
  subscriber.redirect_to(redirect)
@@ -4,6 +4,8 @@
4
4
  # loaded once.
5
5
  #
6
6
  # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
7
+ require 'action_pack'
8
+
7
9
  RSpec.configure do |config|
8
10
  config.treat_symbols_as_metadata_keys_with_true_values = true
9
11
  config.run_all_when_everything_filtered = true
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lograge
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.6
4
+ version: 0.1.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-07-24 00:00:00.000000000 Z
12
+ date: 2012-11-13 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rspec
16
- requirement: &70325289194340 !ruby/object:Gem::Requirement
16
+ requirement: !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ! '>='
@@ -21,10 +21,15 @@ dependencies:
21
21
  version: '0'
22
22
  type: :development
23
23
  prerelease: false
24
- version_requirements: *70325289194340
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
25
30
  - !ruby/object:Gem::Dependency
26
31
  name: guard-rspec
27
- requirement: &70325289193820 !ruby/object:Gem::Requirement
32
+ requirement: !ruby/object:Gem::Requirement
28
33
  none: false
29
34
  requirements:
30
35
  - - ! '>='
@@ -32,10 +37,15 @@ dependencies:
32
37
  version: '0'
33
38
  type: :development
34
39
  prerelease: false
35
- version_requirements: *70325289193820
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
36
46
  - !ruby/object:Gem::Dependency
37
47
  name: activesupport
38
- requirement: &70325289193340 !ruby/object:Gem::Requirement
48
+ requirement: !ruby/object:Gem::Requirement
39
49
  none: false
40
50
  requirements:
41
51
  - - ! '>='
@@ -43,10 +53,15 @@ dependencies:
43
53
  version: '0'
44
54
  type: :runtime
45
55
  prerelease: false
46
- version_requirements: *70325289193340
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
47
62
  - !ruby/object:Gem::Dependency
48
63
  name: actionpack
49
- requirement: &70325289192680 !ruby/object:Gem::Requirement
64
+ requirement: !ruby/object:Gem::Requirement
50
65
  none: false
51
66
  requirements:
52
67
  - - ! '>='
@@ -54,7 +69,12 @@ dependencies:
54
69
  version: '0'
55
70
  type: :runtime
56
71
  prerelease: false
57
- version_requirements: *70325289192680
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
58
78
  description: Tame Rails' multi-line logging into a single line per request
59
79
  email:
60
80
  - meyer@paperplanes.de
@@ -98,7 +118,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
98
118
  version: '0'
99
119
  requirements: []
100
120
  rubyforge_project: lograge
101
- rubygems_version: 1.8.11
121
+ rubygems_version: 1.8.23
102
122
  signing_key:
103
123
  specification_version: 3
104
124
  summary: Tame Rails' multi-line logging into a single line per request