rack-key_value_logger 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1 @@
1
+ Gemfile.lock
data/Gemfile ADDED
@@ -0,0 +1,12 @@
1
+ source :rubygems
2
+
3
+ gem 'rake'
4
+
5
+ gemspec
6
+
7
+ group :test do
8
+ gem 'debugger'
9
+ gem 'rack'
10
+ gem 'rspec'
11
+ gem 'sinatra'
12
+ end
data/HISTORY.md ADDED
@@ -0,0 +1,4 @@
1
+ ## 0.3.1
2
+
3
+ * (feature) Add `:ignore_paths` option
4
+ * Add human-readable timestamp to each log line
data/MIT-LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ Copyright (c) 2012 Alex Sharp
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+
data/README.md ADDED
@@ -0,0 +1,56 @@
1
+ [![Build Status](https://secure.travis-ci.org/zaarly/rack-key_value_logger.png)](http://travis-ci.org/zaarly/rack-key_value_logger)
2
+
3
+ ## What
4
+
5
+ Structured, key-value logging for your rack apps. Inspired by [lograge](https://github.com/roidrage/lograge).
6
+
7
+ ## Why?
8
+
9
+ Application logs are an incredibly rich source of information. But digging out
10
+ the information can be extremely painful if your logs are not structured in an
11
+ easily parsable manner.
12
+
13
+ `Rack::KeyValueLogger` logs key-value pairs, where the key and value are
14
+ separated by a "=" character. Pairs are separated by pipe ("|") characters.
15
+ Here's an example of what a log line looks like:
16
+
17
+ ```
18
+ [1351714706 2012-10-31 20:18:26 UTC] method=GET|url=/homepage|params=page=2|user_id=123|scheme=http|user_agent=curl/7.24.0 (x86_64-apple-darwin12.0) libcurl/7.24.0 OpenSSL/0.9.8r zlib/1.2.5|remote_ip=127.0.0.1|http_version=HTTP/1.1|requested_content_type=text/html|log_source=key_value_logger|status=200|content-length=111|content_type=text/html|runtime=21.553
19
+ ```
20
+
21
+ ## Get Started
22
+
23
+ ```ruby
24
+ gem 'rack-key_value_logger'
25
+ ```
26
+
27
+ #### Sinatra
28
+
29
+ ```ruby
30
+ class MyApp < Sinatra::Base
31
+ use Rack::KeyValueLogger
32
+ end
33
+ ```
34
+
35
+ #### Rails
36
+
37
+ ```ruby
38
+ module MyApp
39
+ class Application < Rails::Application
40
+ # ...
41
+ config.middleware.use "Rack::KeyValueLogger"
42
+ end
43
+ end
44
+ ```
45
+
46
+ ## Configuration
47
+
48
+ A number of configuration options are supported when adding
49
+ `Rack::KeyValueLogger` to the middleware stack.
50
+
51
+ * `:log_failure_response_bodies` - `true` or `false`. Logs the entire response
52
+ body to the `response_body` key on 40x responses. Defaults to `false`.
53
+ * `:ignore_paths` - A regular expression of paths we should not log.
54
+ * `:logger` - A `Logger` instance. Defaults to `Logger($stdout)`.
55
+ * `:user_id` - A string key at which the current user's id is stored in
56
+ `env["rack.session"]`. Defaults to "user_id".
data/Rakefile ADDED
@@ -0,0 +1,5 @@
1
+ require 'rspec/core/rake_task'
2
+
3
+ RSpec::Core::RakeTask.new(:spec)
4
+
5
+ task :default => :spec
@@ -0,0 +1,5 @@
1
+ module Rack
2
+ class KeyValueLogger
3
+ VERSION = '0.3.1'
4
+ end
5
+ end
@@ -0,0 +1,147 @@
1
+ require 'logger'
2
+ require 'multi_json'
3
+
4
+ module Rack
5
+ class KeyValueLogger
6
+ SEPARATOR = "|"
7
+
8
+ attr_reader :msg, :logger, :opts, :ignore_paths
9
+
10
+ # @example Preventing rails assets from being logged
11
+ # use Rack::KeyValueLogger, :ignore_paths => /\/assets/
12
+ #
13
+ # @example Logging non-success response bodies
14
+ # use Rack::KeyValueLogger, :log_failure_response_bodies => true
15
+ # # NOTE: Most fields below have been omitted for brevity and replaced with "..."
16
+ # # => [1351712789 2012-10-31 19:46:29 UTC] method=GET|url=/422|params=|...|response_body=["{\"errors\"=>{\"key\"=>\"val\"}}"]|runtime=0.07
17
+ #
18
+ # @param opts
19
+ # @option opts :logger A logger instance. Defaults to logging to $stdout.
20
+ # @option opts :log_failure_response_bodies Set to `true` to log response
21
+ # bodies for non-success codes. Defaults to false.
22
+ # @option opts :user_id a string key representing the user id key.
23
+ # Defaults to 'user_id'
24
+ # @option opts :ignore_paths a regular expression indicating url paths we don't want to
25
+ # in the session hash.
26
+ def initialize(app, opts = {})
27
+ @app, @opts = app, opts
28
+ @logger = @opts[:logger] || ::Logger.new($stdout)
29
+ @opts[:log_failure_response_bodies] ||= false
30
+ @opts[:user_id] ||= 'user_id'
31
+ @ignore_paths = @opts[:ignore_paths]
32
+ end
33
+
34
+ # Logs key=value pairs of useful information about the request and
35
+ # response to the a log. We either piggy-back off the
36
+ # env['rack.logger'] or we log to $stdout.
37
+ #
38
+ # Components
39
+ # * session - session hash, json-encoded
40
+ # * accept - Accept-Encoding request header
41
+ # * user-agent - User agent string
42
+ # * request-time - in seconds since epoch
43
+ # * method - request method
44
+ # * status - response status code
45
+ # * url - the url, without query string
46
+ # * query-string - query string params
47
+ # * user-id - user's id
48
+ # * scheme - http or https
49
+ # * content-length - length in bytes of the response body
50
+ # * requested-content-type
51
+ # * content-type - Content-Type response header
52
+ # * remote-ip - User's ip address
53
+ # * runtime - Duration of request in milliseconds
54
+ # * http-version - http version of the client
55
+ # * mobile-device - the mobile device return by rack-mobile-detect
56
+ def call(env)
57
+ @msg = []
58
+ start = Time.now
59
+ request = Rack::Request.new(env)
60
+ user_id = env['rack.session'] && env['rack.session'][opts[:user_id]]
61
+ mobile_device = env['X_MOBILE_DEVICE']
62
+ url = request.path
63
+ query_string = env['QUERY_STRING']
64
+
65
+ if ignored_path?(url)
66
+ return @app.call(env)
67
+ end
68
+
69
+ # record request attributes
70
+ msg << "method=#{request.request_method}"
71
+ msg << "url=#{url}"
72
+ msg << "params=#{query_string}"
73
+ msg << "user_id=#{user_id}"
74
+ msg << "scheme=#{request.scheme}"
75
+ msg << "user_agent=#{request.user_agent}"
76
+ msg << "remote_ip=#{request.ip}"
77
+ msg << "http_version=#{env['HTTP_VERSION']}"
78
+ msg << "mobile_device=#{mobile_device}" if mobile_device
79
+ msg << "requested_content_type=#{request.content_type}"
80
+ msg << "log_source=key_value_logger"
81
+
82
+ begin
83
+ status, headers, body = @app.call(env)
84
+
85
+ record_response_attributes(status, headers, body)
86
+ rescue => e
87
+ msg << 'status=500'
88
+ raise e
89
+ ensure
90
+ record_runtime(start)
91
+
92
+ # Don't log Rack::Cascade fake 404's
93
+ flush_log unless rack_cascade_404?(headers)
94
+ end
95
+
96
+ [status, headers, body]
97
+ end
98
+
99
+ private
100
+
101
+ # Returns true if the passed in `url` argument matches the `ignore_paths`
102
+ # attribute. Return false otherwise, or if `ignore_paths` is not set.
103
+ #
104
+ # @param [String] url a url path
105
+ # @return [Boolean]
106
+ def ignored_path?(url)
107
+ return false if ignore_paths.nil?
108
+ url =~ ignore_paths
109
+ end
110
+
111
+ def record_runtime(start)
112
+ msg << "runtime=#{((Time.now - start) * 1000).round(5)}"
113
+ end
114
+
115
+ # Flush `msg` to the logger instance.
116
+ def flush_log
117
+ result = msg.join(SEPARATOR)
118
+ now = Time.now
119
+ result = "[#{now.to_i} #{now.utc}] " + result
120
+
121
+ logger.info result
122
+ end
123
+
124
+ def record_response_attributes(status, headers, body)
125
+ msg << "status=#{status}"
126
+ msg << "content-length=#{headers['Content-Length']}"
127
+ msg << "content_type=#{headers['Content-Type']}"
128
+
129
+ if status.to_s =~ /^4[0-9]{2}/ && opts[:log_failure_response_bodies]
130
+ response = Rack::Response.new(body, status, headers)
131
+ msg << "response_body=#{MultiJson.encode(response.body)}"
132
+ end
133
+ end
134
+
135
+ # Sinatra adds a "X-Cascade" header with a value of "pass" to the response.
136
+ # This makes it possible to detect whether this is a 404 worth logging,
137
+ # or just a Rack::Cascade 404.
138
+ #
139
+ # @param [Hash, nil] headers the headers hash from the response
140
+ # @return [Boolean]
141
+ def rack_cascade_404?(headers)
142
+ return false if headers.nil?
143
+ cascade_header = headers['X-Cascade']
144
+ cascade_header && cascade_header == 'pass'
145
+ end
146
+ end
147
+ end
@@ -0,0 +1 @@
1
+ require 'rack/key_value_logger'
@@ -0,0 +1,24 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "key_value_logger/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "rack-key_value_logger"
7
+ s.version = Rack::KeyValueLogger::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Alex Sharp"]
10
+ s.email = ["ajsharp@gmail.com"]
11
+ s.homepage = "https://github.com/zaarly/rack-key_value_logger"
12
+ s.summary = %q{Structured, key-value logging for your rack apps.}
13
+ s.description = %q{Structured, key-value logging for your rack apps. Inspired by lograge.}
14
+
15
+ s.rubyforge_project = s.name
16
+
17
+ s.files = `git ls-files`.split("\n")
18
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
19
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
20
+ s.require_paths = ["lib"]
21
+
22
+ s.add_dependency 'multi_json'
23
+ s.add_dependency 'rack'
24
+ end
@@ -0,0 +1,44 @@
1
+ require 'spec_helper'
2
+
3
+ module SinatraTest
4
+ describe 'ignoring rack cascade 404s' do
5
+ DRAIN = StringIO.new
6
+ LOGGER = Logger.new(DRAIN)
7
+
8
+ before do
9
+ DRAIN.truncate(0)
10
+ end
11
+ let(:drain) { DRAIN } # for shared helpers
12
+
13
+ class Base < Sinatra::Base
14
+ use Rack::KeyValueLogger, :logger => LOGGER
15
+ end
16
+
17
+ class FirstApp < Base
18
+ get('/other') { status(200) }
19
+ end
20
+
21
+ class SecondApp < Base
22
+ get('/success') { status(200) }
23
+ end
24
+
25
+ let(:app) do
26
+ Rack::Builder.app do
27
+ run Rack::Cascade.new([FirstApp, SecondApp])
28
+ end
29
+ end
30
+
31
+ before do
32
+ do_get '/success'
33
+ end
34
+
35
+ it_behaves_like 'it logs', 'status', '200'
36
+ it_behaves_like 'it logs', 'url', '/success'
37
+ it_behaves_like 'it does not log', 'status', '404'
38
+
39
+ it 'should only log one entry' do
40
+ drain.rewind
41
+ drain.lines.count.should == 1
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,124 @@
1
+ require 'spec_helper'
2
+ require 'stringio'
3
+
4
+ describe "logging non success response bodies" do
5
+ let(:logger) { Logger.new(drain) }
6
+ let(:drain) { StringIO.new }
7
+
8
+ let(:app) do
9
+ a = lambda do |env|
10
+ case env['PATH_INFO']
11
+ when '/200'
12
+ [200, default_test_headers, ['Success']]
13
+ when '/422'
14
+ [422, default_test_headers, [{'errors' => {'key' => 'val'}}]]
15
+ when '/401'
16
+ [401, default_test_headers, ['Unauthorized']]
17
+ when '/400'
18
+ [400, default_test_headers, ['Fail']]
19
+ when '/500'
20
+ raise "oh noez!"
21
+ end
22
+ end
23
+
24
+ log = logger # hold scope out of the block
25
+ Rack::Builder.app do
26
+ use Rack::KeyValueLogger, :log_failure_response_bodies => true, :logger => log
27
+ run a
28
+ end
29
+ end
30
+
31
+ it "should clear the msg attr out after each log line" do
32
+ do_get('/200')
33
+ do_get('/401')
34
+ drain.rewind
35
+ drain.read.scan('method').size.should == 2
36
+ end
37
+
38
+ context 'when the proper option is passed in' do
39
+ it "logs the response body for 401's" do
40
+ do_get('/401')
41
+ drain.should include_entry "response_body=.*Unauthorized.*"
42
+ end
43
+
44
+ it "logs the response body for 400's" do
45
+ do_get('/400')
46
+ drain.should include_entry "response_body=.*Fail.*"
47
+ end
48
+
49
+ it "logs the response body for 422's" do
50
+ do_get('/422')
51
+ drain.should include_entry 'response_body=.*errors.*'
52
+ end
53
+ end
54
+
55
+ context 'a 200 response' do
56
+ before do
57
+ do_get('/200')
58
+ end
59
+
60
+ it_behaves_like "it logs", 'status', 200
61
+
62
+ it 'does not log the response body for success endpoints' do
63
+ drain.should_not include_entry 'response_body=Unauthorized'
64
+ end
65
+ end
66
+
67
+ context 'a 400 bad request response' do
68
+ before do
69
+ do_get('/400')
70
+ end
71
+
72
+ it_behaves_like 'it logs', 'status', 400
73
+ end
74
+
75
+ context 'an unexpected 500 response' do
76
+ before do
77
+ begin
78
+ do_get('/500')
79
+ rescue => e
80
+ # raise other exceptions
81
+ raise e unless e.message == 'oh noez!'
82
+ end
83
+ end
84
+
85
+ it_behaves_like 'it logs', 'url', '/500'
86
+ it_behaves_like 'it logs', 'status', 500
87
+ end
88
+ end
89
+
90
+ describe "ignoring certain paths" do
91
+ let(:logger) { Logger.new(drain) }
92
+ let(:drain) { StringIO.new }
93
+
94
+ let(:app) do
95
+ ignore_app = lambda do |env|
96
+ case env['PATH_INFO']
97
+ when '/ignore'
98
+ [200, default_test_headers, ['ignore me!']]
99
+ when '/do-not-ignore'
100
+ [200, default_test_headers, ["don't ignore me!"]]
101
+ end
102
+ end
103
+
104
+ log = logger
105
+ Rack::Builder.app do
106
+ use Rack::KeyValueLogger, :logger => log, :ignore_paths => /^\/ignore/
107
+ run ignore_app
108
+ end
109
+ end
110
+
111
+ it 'does not log anything for ignored paths' do
112
+ do_get('/ignore')
113
+ drain.should_not include_entry "url=/ignore"
114
+ end
115
+
116
+ context 'logging non-ignored paths' do
117
+ before do
118
+ do_get '/do-not-ignore'
119
+ end
120
+
121
+ it_behaves_like 'it logs', 'status', '200'
122
+ it_behaves_like 'it logs', 'url', '/do-not-ignore'
123
+ end
124
+ end
@@ -0,0 +1,42 @@
1
+ require 'rubygems'
2
+ require 'rspec'
3
+ require 'rack'
4
+ require 'sinatra/base'
5
+
6
+ $:.push(File.expand_path(File.dirname(__FILE__)))
7
+ $:.push(File.expand_path(File.dirname(__FILE__)) + '/../lib')
8
+
9
+ require 'rack-key_value_logger'
10
+ require 'rack/key_value_logger'
11
+ require 'debugger'
12
+
13
+ RSpec.configure do |c|
14
+ def do_get(url)
15
+ Rack::MockRequest.new(app).get(url)
16
+ end
17
+
18
+ def default_test_headers
19
+ {'Content-Type' => 'text/plain'}
20
+ end
21
+ end
22
+
23
+ # @example
24
+ # $drain.should include_entry 'status=500'
25
+ RSpec::Matchers.define :include_entry do |expected|
26
+ match do |actual|
27
+ actual.rewind
28
+ !!actual.detect { |l| l =~ /#{expected}/ }
29
+ end
30
+ end
31
+
32
+ shared_examples 'it logs' do |field, value|
33
+ it "logs #{field} = #{value}" do
34
+ drain.should include_entry "#{field}=#{value}"
35
+ end
36
+ end
37
+
38
+ shared_examples 'it does not log' do |field, value|
39
+ it "does not log #{field} = #{value}" do
40
+ drain.should_not include_entry "#{field}=#{value}"
41
+ end
42
+ end
metadata ADDED
@@ -0,0 +1,94 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rack-key_value_logger
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.3.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Alex Sharp
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-11-27 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: multi_json
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: rack
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ description: Structured, key-value logging for your rack apps. Inspired by lograge.
47
+ email:
48
+ - ajsharp@gmail.com
49
+ executables: []
50
+ extensions: []
51
+ extra_rdoc_files: []
52
+ files:
53
+ - .gitignore
54
+ - Gemfile
55
+ - HISTORY.md
56
+ - MIT-LICENSE
57
+ - README.md
58
+ - Rakefile
59
+ - lib/key_value_logger/version.rb
60
+ - lib/rack-key_value_logger.rb
61
+ - lib/rack/key_value_logger.rb
62
+ - rack-key_value_logger.gemspec
63
+ - spec/integration/sinatra_spec.rb
64
+ - spec/rack/key_value_logger_spec.rb
65
+ - spec/spec_helper.rb
66
+ homepage: https://github.com/zaarly/rack-key_value_logger
67
+ licenses: []
68
+ post_install_message:
69
+ rdoc_options: []
70
+ require_paths:
71
+ - lib
72
+ required_ruby_version: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ required_rubygems_version: !ruby/object:Gem::Requirement
79
+ none: false
80
+ requirements:
81
+ - - ! '>='
82
+ - !ruby/object:Gem::Version
83
+ version: '0'
84
+ requirements: []
85
+ rubyforge_project: rack-key_value_logger
86
+ rubygems_version: 1.8.24
87
+ signing_key:
88
+ specification_version: 3
89
+ summary: Structured, key-value logging for your rack apps.
90
+ test_files:
91
+ - spec/integration/sinatra_spec.rb
92
+ - spec/rack/key_value_logger_spec.rb
93
+ - spec/spec_helper.rb
94
+ has_rdoc: