rack-key_value_logger 0.3.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.
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: