timber-rack 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 842b8f0648df453c036c272d6a4c6acb14b5871f93d510e5c0ff13d695099558
4
+ data.tar.gz: ac7015817b3f3cd58e2fbb2ae06439af33ad958c44beb9fa6bed381a68b44f9d
5
+ SHA512:
6
+ metadata.gz: f90f921a1ef9f09dccdd32d3ab0d00439b35726de39713d1d69fad28be6dde80c9815d088111beae17d3cc1b0c71d1752fa80c4475036a18401fa0bf705f8a2f
7
+ data.tar.gz: ef5185ce6ce4e908c2521effd3a3a4790ec949e11fcfa1eb03d8ce863e216fd770f8308e1e226862c8cb2515235ed0333c972832d246f0d3985935d72e7402b3
data/.gitignore ADDED
@@ -0,0 +1,14 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ # rspec failure tracking
11
+ .rspec_status
12
+
13
+ # Ignore gemset
14
+ /.gem
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.travis.yml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ sudo: false
3
+ language: ruby
4
+ cache: bundler
5
+ rvm:
6
+ - 2.5.1
7
+ before_install: gem install bundler -v 1.17.3
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ source "https://rubygems.org"
2
+
3
+ # TODO: REMOVE ME
4
+ gem 'timber', git: 'https://github.com/timberio/timber-ruby.git', branch: '3.0'
5
+
6
+ # Specify your gem's dependencies in timber-ruby-rack.gemspec
7
+ gemspec
data/LICENSE.md ADDED
@@ -0,0 +1,16 @@
1
+ # License
2
+
3
+ Copyright (c) 2016, Timber Technologies, Inc.
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any purpose
6
+ with or without fee is hereby granted, provided that the above copyright notice
7
+ and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
10
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
11
+ FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
12
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
13
+ OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
14
+ TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
15
+ THIS SOFTWARE.
16
+
data/README.md ADDED
@@ -0,0 +1,12 @@
1
+ # 🌲 Timber Integration For Rack
2
+
3
+ [![ISC License](https://img.shields.io/badge/license-ISC-ff69b4.svg)](LICENSE.md)
4
+ [![Yard Docs](http://img.shields.io/badge/yard-docs-blue.svg)](http://www.rubydoc.info/github/timberio/timber-ruby-rack)
5
+ [![Build Status](https://travis-ci.org/timberio/timber-ruby-rack.svg?branch=master)](https://travis-ci.org/timberio/timber-ruby-rack)
6
+ [![Code Climate](https://codeclimate.com/github/timberio/timber-ruby-rack/badges/gpa.svg)](https://codeclimate.com/github/timberio/timber-ruby-rack)
7
+
8
+ This library integrates the [`timber` Ruby library](https://github.com/timberio/timber-ruby) with the [Rack](https://github.com/rack/rack) framework,
9
+ turning your Rack logs into rich structured events.
10
+
11
+ * **Sign-up: [https://app.timber.io](https://app.timber.io)**
12
+ * **Documentation: [https://docs.timber.io/setup/languages/ruby/integrations/rack](https://docs.timber.io/setup/languages/ruby/integrations/rack)**
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "timber-rack"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,16 @@
1
+ require "rack"
2
+ require "timber"
3
+ require "timber-rack/config"
4
+ require "timber-rack/error_event"
5
+ require "timber-rack/http_context"
6
+ require "timber-rack/http_events"
7
+ require "timber-rack/session_context"
8
+ require "timber-rack/user_context"
9
+
10
+ module Timber
11
+ module Integrations
12
+ module Rack
13
+ end
14
+ end
15
+ end
16
+
@@ -0,0 +1,85 @@
1
+ require "timber"
2
+
3
+ Timber::Config.instance.define_singleton_method(:logrageify!) do
4
+ integrations.rack.http_events.collapse_into_single_event = true
5
+ end
6
+
7
+ module Timber
8
+ class Config
9
+ module Integrations
10
+ extend self
11
+ # Convenience module for accessing the various `Timber::Integrations::Rack::*` classes
12
+ # through the {Timber::Config} object. Timber couples configuration with the class
13
+ # responsibls for implementing it. This provides for a tighter design, but also
14
+ # requires the user to understand and access the various classes. This module aims
15
+ # to provide a simple ruby-like configuration interface for internal Timber classes.
16
+ #
17
+ # For example:
18
+ #
19
+ # config = Timber::Config.instance
20
+ # config.integrations.rack.http_events.enabled = false
21
+ def rack
22
+ Rack
23
+ end
24
+
25
+ module Rack
26
+ extend self
27
+
28
+ # Convenience method for accessing the {Timber::Integrations::Rack::ErrorEvent}
29
+ # middleware class specific configuration. See {Timber::Integrations::Rack::ExceptionEvent}
30
+ # for a list of methods available.
31
+ #
32
+ # @example
33
+ # config = Timber::Config.instance
34
+ # config.integrations.rack.error_event.enabled = false
35
+ def error_event
36
+ Timber::Integrations::Rack::ErrorEvent
37
+ end
38
+
39
+ # Convenience method for accessing the {Timber::Integrations::Rack::HTTPContext}
40
+ # middleware class specific configuration. See {Timber::Integrations::Rack::HTTPContext}
41
+ # for a list of methods available.
42
+ #
43
+ # @example
44
+ # config = Timber::Config.instance
45
+ # config.integrations.rack.http_context.enabled = false
46
+ def http_context
47
+ Timber::Integrations::Rack::HTTPContext
48
+ end
49
+
50
+ # Convenience method for accessing the {Timber::Integrations::Rack::HTTPEvents}
51
+ # middleware class specific configuration. See {Timber::Integrations::Rack::HTTPEvents}
52
+ # for a list of methods available.
53
+ #
54
+ # @example
55
+ # config = Timber::Config.instance
56
+ # config.integrations.rack.http_events.enabled = false
57
+ def http_events
58
+ Timber::Integrations::Rack::HTTPEvents
59
+ end
60
+
61
+ # Convenience method for accessing the {Timber::Integrations::Rack::SessionContext}
62
+ # middleware class specific configuration. See {Timber::Integrations::Rack::SessionContext}
63
+ # for a list of methods available.
64
+ #
65
+ # @example
66
+ # config = Timber::Config.instance
67
+ # config.integrations.rack.session_context.enabled = false
68
+ def session_context
69
+ Timber::Integrations::Rack::SessionContext
70
+ end
71
+
72
+ # Convenience method for accessing the {Timber::Integrations::Rack::UserContext}
73
+ # middleware class specific configuration. See {Timber::Integrations::Rack::UserContext}
74
+ # for a list of methods available.
75
+ #
76
+ # @example
77
+ # config = Timber::Config.instance
78
+ # config.integrations.rack.user_context.enabled = false
79
+ def user_context
80
+ Timber::Integrations::Rack::UserContext
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,28 @@
1
+ require "timber/config"
2
+ require "timber/events/error"
3
+ require "timber-rack/middleware"
4
+
5
+ module Timber
6
+ module Integrations
7
+ module Rack
8
+ # A Rack middleware that is reponsible for capturing exception and error events
9
+ class ErrorEvent < Middleware
10
+ def call(env)
11
+ begin
12
+ status, headers, body = @app.call(env)
13
+ rescue Exception => exception
14
+ Config.instance.logger.fatal do
15
+ Events::Error.new(
16
+ name: exception.class.name,
17
+ error_message: exception.message,
18
+ backtrace: exception.backtrace
19
+ )
20
+ end
21
+
22
+ raise exception
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,27 @@
1
+ require "timber/contexts/http"
2
+ require "timber/current_context"
3
+ require "timber-rack/middleware"
4
+ require "timber-rack/util/request"
5
+
6
+ module Timber
7
+ module Integrations
8
+ module Rack
9
+ # A Rack middleware that is reponsible for adding the HTTP context {Timber::Contexts::HTTP}.
10
+ class HTTPContext < Middleware
11
+ def call(env)
12
+ request = Util::Request.new(env)
13
+ context = Contexts::HTTP.new(
14
+ host: request.host,
15
+ method: request.request_method,
16
+ path: request.path,
17
+ remote_addr: request.ip,
18
+ request_id: request.request_id
19
+ )
20
+ CurrentContext.with(context) do
21
+ @app.call(env)
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,278 @@
1
+ require "set"
2
+
3
+ require "timber/config"
4
+ require "timber/contexts/http"
5
+ require "timber/current_context"
6
+ require "timber-rack/http_request"
7
+ require "timber-rack/http_response"
8
+ require "timber-rack/middleware"
9
+
10
+ module Timber
11
+ module Integrations
12
+ module Rack
13
+ # A Rack middleware that is reponsible for capturing and logging HTTP server requests and
14
+ # response events. The {Events::HTTPRequest} and {Events::HTTPResponse} events
15
+ # respectively.
16
+ class HTTPEvents < Middleware
17
+ class << self
18
+ # Allows you to capture the HTTP request body, default is off (false).
19
+ #
20
+ # Capturing HTTP bodies can be extremely helpful when debugging issues,
21
+ # but please proceed with caution:
22
+ #
23
+ # 1. Capturing HTTP bodies can use quite a bit of data (this can be mitigated, see below)
24
+ #
25
+ # If you opt to capture bodies, you can also truncate the size to reduce the data
26
+ # captured. See {Events::HTTPRequest}.
27
+ #
28
+ # @example
29
+ # Timber::Integrations::Rack::HTTPEvents.capture_request_body = true
30
+ def capture_request_body=(value)
31
+ @capture_request_body = value
32
+ end
33
+
34
+ # Accessor method for {#capture_request_body=}
35
+ def capture_request_body?
36
+ @capture_request_body == true
37
+ end
38
+
39
+ # Just like {#capture_request_body=} but for the {Events::HTTPResponse} event.
40
+ # Please see {#capture_request_body=} for more details. The documentation there also
41
+ # applies here.
42
+ def capture_response_body=(value)
43
+ @capture_response_body = value
44
+ end
45
+
46
+ # Accessor method for {#capture_response_body=}
47
+ def capture_response_body?
48
+ @capture_response_body == true
49
+ end
50
+
51
+ # Collapse both the HTTP request and response events into a single log line event.
52
+ # While we don't recommend this, it can help to reduce log volume if desired.
53
+ # The reason we don't recommend this, is because the logging service you use should
54
+ # not be so expensive that you need to strip out useful logs. It should also provide
55
+ # the tools necessary to properly search your logs and reduce noise. Such as viewing
56
+ # logs for a specific request.
57
+ #
58
+ # To provide an example. This setting turns this:
59
+ #
60
+ # Started GET "/" for 127.0.0.1 at 2012-03-10 14:28:14 +0100
61
+ # Completed 200 OK in 79ms (Views: 78.8ms | ActiveRecord: 0.0ms)
62
+ #
63
+ # Into this:
64
+ #
65
+ # Get "/" sent 200 OK in 79ms
66
+ #
67
+ # The single event is still a {Timber::Events::HTTPResponse} event. Because
68
+ # we capture HTTP context, you still get the HTTP details, but you will not get
69
+ # all of the request details that the {Timber::Events::HTTPRequest} event would
70
+ # provide.
71
+ #
72
+ # @example
73
+ # Timber::Integrations::Rack::HTTPEvents.collapse_into_single_event = true
74
+ def collapse_into_single_event=(value)
75
+ @collapse_into_single_event = value
76
+ end
77
+
78
+ # Accessor method for {#collapse_into_single_event=}.
79
+ def collapse_into_single_event?
80
+ @collapse_into_single_event == true
81
+ end
82
+
83
+ # This setting allows you to silence requests based on any conditions you desire.
84
+ # We require a block because it gives you complete control over how you want to
85
+ # silence requests. The first parameter being the traditional Rack env hash, the
86
+ # second being a [Rack Request](http://www.rubydoc.info/gems/rack/Rack/Request) object.
87
+ #
88
+ # @example
89
+ # Integrations::Rack::HTTPEvents.silence_request = lambda do |rack_env, rack_request|
90
+ # rack_request.path == "/_health"
91
+ # end
92
+ def silence_request=(proc)
93
+ if proc && !proc.is_a?(Proc)
94
+ raise ArgumentError.new("The value passed to #silence_request must be a Proc")
95
+ end
96
+
97
+ @silence_request = proc
98
+ end
99
+
100
+ # Accessor method for {#silence_request=}
101
+ def silence_request
102
+ @silence_request
103
+ end
104
+
105
+ def http_body_limit=(value)
106
+ @http_body_limit = value
107
+ end
108
+
109
+ # Accessor method for {#http_body_limit=}
110
+ def http_body_limit
111
+ @http_body_limit
112
+ end
113
+
114
+ def http_header_filters=(value)
115
+ @http_header_filters = value
116
+ end
117
+
118
+ # Accessor method for {#http_header_filters=}
119
+ def http_header_filters
120
+ @http_header_filters
121
+ end
122
+ end
123
+
124
+ CONTENT_LENGTH_KEY = 'Content-Length'.freeze
125
+
126
+ def call(env)
127
+ request = Util::Request.new(env)
128
+
129
+ if silenced?(env, request)
130
+ if Config.instance.logger.respond_to?(:silence)
131
+ Config.instance.logger.silence do
132
+ @app.call(env)
133
+ end
134
+ else
135
+ @app.call(env)
136
+ end
137
+
138
+ elsif collapse_into_single_event?
139
+ start = Time.now
140
+
141
+ status, headers, body = @app.call(env)
142
+
143
+ Config.instance.logger.info do
144
+ http_context_key = Contexts::HTTP.keyspace
145
+ http_context = CurrentContext.fetch(http_context_key)
146
+ content_length = headers[CONTENT_LENGTH_KEY]
147
+ duration_ms = (Time.now - start) * 1000.0
148
+
149
+ http_response = HTTPResponse.new(
150
+ content_length: content_length,
151
+ headers: headers,
152
+ http_context: http_context,
153
+ request_id: request.request_id,
154
+ status: status,
155
+ duration_ms: duration_ms,
156
+ body_limit: self.class.http_body_limit,
157
+ headers_to_sanitize: self.class.http_header_filters,
158
+ )
159
+
160
+ {
161
+ message: http_response.message,
162
+ event: {
163
+ http_response_sent: {
164
+ body: http_response.body,
165
+ content_length: http_response.content_length,
166
+ headers_json: http_response.headers_json,
167
+ request_id: http_response.request_id,
168
+ service_name: http_response.service_name,
169
+ status: http_response.status,
170
+ duration_ms: http_response.duration_ms,
171
+ }
172
+ }
173
+ }
174
+ end
175
+
176
+ [status, headers, body]
177
+ else
178
+ start = Time.now
179
+
180
+ Config.instance.logger.info do
181
+ event_body = capture_request_body? ? request.body_content : nil
182
+ http_request = HTTPRequest.new(
183
+ body: event_body,
184
+ content_length: request.content_length,
185
+ headers: request.headers,
186
+ host: request.host,
187
+ method: request.request_method,
188
+ path: request.path,
189
+ port: request.port,
190
+ query_string: request.query_string,
191
+ request_id: request.request_id,
192
+ scheme: request.scheme,
193
+ body_limit: self.class.http_body_limit,
194
+ headers_to_sanitize: self.class.http_header_filters,
195
+ )
196
+
197
+ {
198
+ message: http_request.message,
199
+ event: {
200
+ http_request_received: {
201
+ body: http_request.body,
202
+ content_length: http_request.content_length,
203
+ headers_json: http_request.headers_json,
204
+ host: http_request.host,
205
+ method: http_request.method,
206
+ path: http_request.path,
207
+ port: http_request.port,
208
+ query_string: http_request.query_string,
209
+ request_id: http_request.request_id,
210
+ scheme: http_request.scheme,
211
+ service_name: http_request.service_name,
212
+ }
213
+ }
214
+ }
215
+ end
216
+
217
+ status, headers, body = @app.call(env)
218
+
219
+ Config.instance.logger.info do
220
+ event_body = capture_response_body? ? body : nil
221
+ content_length = headers[CONTENT_LENGTH_KEY]
222
+ duration_ms = (Time.now - start) * 1000.0
223
+
224
+ http_response = HTTPResponse.new(
225
+ body: event_body,
226
+ content_length: content_length,
227
+ headers: headers,
228
+ request_id: request.request_id,
229
+ status: status,
230
+ duration_ms: duration_ms,
231
+ body_limit: self.class.http_body_limit,
232
+ headers_to_sanitize: self.class.http_header_filters,
233
+ )
234
+
235
+ {
236
+ message: http_response.message,
237
+ event: {
238
+ http_response_sent: {
239
+ body: http_response.body,
240
+ content_length: http_response.content_length,
241
+ headers_json: http_response.headers_json,
242
+ request_id: http_response.request_id,
243
+ service_name: http_response.service_name,
244
+ status: http_response.status,
245
+ duration_ms: http_response.duration_ms,
246
+ }
247
+ }
248
+ }
249
+ end
250
+
251
+ [status, headers, body]
252
+ end
253
+ end
254
+
255
+ private
256
+ def capture_request_body?
257
+ self.class.capture_request_body?
258
+ end
259
+
260
+ def capture_response_body?
261
+ self.class.capture_response_body?
262
+ end
263
+
264
+ def collapse_into_single_event?
265
+ self.class.collapse_into_single_event?
266
+ end
267
+
268
+ def silenced?(env, request)
269
+ if !self.class.silence_request.nil?
270
+ self.class.silence_request.call(env, request)
271
+ else
272
+ false
273
+ end
274
+ end
275
+ end
276
+ end
277
+ end
278
+ end
@@ -0,0 +1,51 @@
1
+ require "timber/util"
2
+
3
+ module Timber
4
+ module Integrations
5
+ module Rack
6
+ # The HTTP server request event tracks incoming HTTP requests to your HTTP server.
7
+ # Such as unicorn, webrick, puma, etc.
8
+ #
9
+ # @note This event should be installed automatically through integrations,
10
+ # such as the {Integrations::ActionController::LogSubscriber} integration.
11
+ class HTTPRequest
12
+ BODY_MAX_BYTES = 8192.freeze
13
+ HEADERS_JSON_MAX_BYTES = 8192.freeze
14
+ HEADERS_TO_SANITIZE = ['authorization', 'x-amz-security-token'].freeze
15
+ HOST_MAX_BYTES = 256.freeze
16
+ METHOD_MAX_BYTES = 20.freeze
17
+ PATH_MAX_BYTES = 2048.freeze
18
+ QUERY_STRING_MAX_BYTES = 2048.freeze
19
+ REQUEST_ID_MAX_BYTES = 256.freeze
20
+ SCHEME_MAX_BYTES = 20.freeze
21
+ SERVICE_NAME_MAX_BYTES = 256.freeze
22
+
23
+ attr_reader :body, :content_length, :headers, :headers_json, :host, :method, :path, :port,
24
+ :query_string, :request_id, :scheme, :service_name
25
+
26
+ def initialize(attributes)
27
+ normalizer = Util::AttributeNormalizer.new(attributes)
28
+ body_limit = attributes.delete(:body_limit) || BODY_MAX_BYTES
29
+ headers_to_sanitize = HEADERS_TO_SANITIZE + (attributes.delete(:headers_to_sanitize) || [])
30
+
31
+ @body = normalizer.fetch(:body, :string, :limit => body_limit)
32
+ @content_length = normalizer.fetch(:content_length, :integer)
33
+ @headers= normalizer.fetch(:headers, :hash, :sanitize => headers_to_sanitize)
34
+ @headers_json = @headers.to_json.byteslice(0, HEADERS_JSON_MAX_BYTES)
35
+ @host = normalizer.fetch(:host, :string, :limit => HOST_MAX_BYTES)
36
+ @method = normalizer.fetch!(:method, :string, :upcase => true, :limit => METHOD_MAX_BYTES)
37
+ @path = normalizer.fetch(:path, :string, :limit => PATH_MAX_BYTES)
38
+ @port = normalizer.fetch(:port, :integer)
39
+ @query_string = normalizer.fetch(:query_string, :string, :limit => QUERY_STRING_MAX_BYTES)
40
+ @scheme = normalizer.fetch(:scheme, :string, :limit => SCHEME_MAX_BYTES)
41
+ @request_id = normalizer.fetch(:request_id, :string, :limit => REQUEST_ID_MAX_BYTES)
42
+ @service_name = normalizer.fetch(:service_name, :string, :limit => SERVICE_NAME_MAX_BYTES)
43
+ end
44
+
45
+ def message
46
+ 'Started %s "%s"' % [method, path]
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,63 @@
1
+ require "timber/util"
2
+
3
+ module Timber
4
+ module Integrations
5
+ module Rack
6
+ # The HTTP server response event tracks outgoing HTTP responses that you send
7
+ # to clients.
8
+
9
+ class HTTPResponse
10
+ BODY_MAX_BYTES = 8192.freeze
11
+ HEADERS_JSON_MAX_BYTES = 256.freeze
12
+ HEADERS_TO_SANITIZE = ['authorization', 'x-amz-security-token'].freeze
13
+ REQUEST_ID_MAX_BYTES = 256.freeze
14
+ SERVICE_NAME_MAX_BYTES = 256.freeze
15
+
16
+ attr_reader :body, :content_length, :headers, :headers_json, :http_context, :request_id, :service_name,
17
+ :status, :duration_ms
18
+
19
+ def initialize(attributes)
20
+ normalizer = Util::AttributeNormalizer.new(attributes)
21
+ body_limit = attributes.delete(:body_limit) || BODY_MAX_BYTES
22
+ headers_to_sanitize = HEADERS_TO_SANITIZE + (attributes.delete(:headers_to_sanitize) || [])
23
+
24
+ @body = normalizer.fetch(:body, :string, :limit => body_limit)
25
+ @content_length = normalizer.fetch(:content_length, :integer)
26
+ @headers = normalizer.fetch(:headers, :hash, :sanitize => headers_to_sanitize)
27
+ @headers_json = @headers.to_json.byteslice(0, HEADERS_JSON_MAX_BYTES)
28
+ @http_context = attributes[:http_context]
29
+ @request_id = normalizer.fetch(:request_id, :string, :limit => REQUEST_ID_MAX_BYTES)
30
+ @service_name = normalizer.fetch(:service_name, :string, :limit => SERVICE_NAME_MAX_BYTES)
31
+ @status = normalizer.fetch!(:status, :integer)
32
+ @duration_ms = normalizer.fetch!(:duration_ms, :float, :precision => 6)
33
+ end
34
+
35
+ # Returns the human readable log message for this event.
36
+ def message
37
+ if http_context
38
+ message = "#{http_context[:method]} #{http_context[:path]} completed with " \
39
+ "#{status} #{status_description} "
40
+
41
+ if content_length
42
+ message << ", #{content_length} bytes, "
43
+ end
44
+
45
+ message << "in #{duration_ms}ms"
46
+ else
47
+ message = "Completed #{status} #{status_description} "
48
+
49
+ if content_length
50
+ message << ", #{content_length} bytes, "
51
+ end
52
+
53
+ message << "in #{duration_ms}ms"
54
+ end
55
+ end
56
+
57
+ def status_description
58
+ ::Rack::Utils::HTTP_STATUS_CODES[status]
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,28 @@
1
+ module Timber
2
+ module Integrations
3
+ module Rack
4
+ # Base class that all Timber Rack middlewares extend. See the class level methods for
5
+ # configuration options.
6
+ class Middleware
7
+ class << self
8
+ # Easily enable / disable specific middlewares.
9
+ #
10
+ # @example
11
+ # Timber::Integrations::Rack::UserContext.enabled = false
12
+ def enabled=(value)
13
+ @enabled = value
14
+ end
15
+
16
+ # Accessor method for {#enabled=}.
17
+ def enabled?
18
+ @enabled != false
19
+ end
20
+ end
21
+
22
+ def initialize(app)
23
+ @app = app
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,47 @@
1
+ require "timber/config"
2
+ require "timber/contexts/session"
3
+ require "timber-rack/middleware"
4
+
5
+ module Timber
6
+ module Integrations
7
+ module Rack
8
+ # A Rack middleware that is responsible for adding the Session context
9
+ # {Timber::Contexts::Session}.
10
+ class SessionContext < Middleware
11
+ def call(env)
12
+ id = get_session_id(env)
13
+ if id
14
+ context = Contexts::Session.new(id: id)
15
+ CurrentContext.with(context) do
16
+ @app.call(env)
17
+ end
18
+ else
19
+ @app.call(env)
20
+ end
21
+ end
22
+
23
+ private
24
+ def get_session_id(env)
25
+ if session = env['rack.session']
26
+ if session.respond_to?(:id)
27
+ Timber::Config.instance.debug { "Rack env session detected, using id attribute" }
28
+ session.id
29
+ elsif session.respond_to?(:[])
30
+ Timber::Config.instance.debug { "Rack env session detected, using the session_id key" }
31
+ session["session_id"]
32
+ else
33
+ Timber::Config.instance.debug { "Rack env session detected but could not extract id" }
34
+ nil
35
+ end
36
+ else
37
+ Timber::Config.instance.debug { "No session data could be detected, skipping" }
38
+
39
+ nil
40
+ end
41
+ rescue Exception => e
42
+ nil
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,135 @@
1
+ require "timber/config"
2
+ require "timber/contexts/user"
3
+ require "timber-rack/middleware"
4
+
5
+ module Timber
6
+ module Integrations
7
+ module Rack
8
+ # This is a Rack middleware responsible for setting the user context.
9
+ # See {Timber::Contexts::User} for more information on the user context.
10
+ #
11
+ # ## Why a Rack middleware?
12
+ #
13
+ # We use a Rack middleware because we want to set the user context as early as
14
+ # possible, and before the initial incoming request log line:
15
+ #
16
+ # Started GET /welcome
17
+ #
18
+ # The above log line is logged in a request middleware, before it reaches
19
+ # the controller.
20
+ #
21
+ # If, for example, we set the user context in a controller, the log line above
22
+ # will not have the user context attached. This is because it is logged before
23
+ # the controller is executed. This is not ideal, and it's why we take a middleware
24
+ # approach here. If for some reason you cannot identify the user at the middleware
25
+ # level then setting it in the controller is perfectly fine, just be aware of the
26
+ # above downside.
27
+ #
28
+ # ## Authentication frameworks automatically detected:
29
+ #
30
+ # If you use any of the following authentication frameworks, Timber will
31
+ # automatically set the user context for you.
32
+ #
33
+ # * Devise, or any Warden based authentication strategy
34
+ # * Clearance
35
+ #
36
+ # Or, you can use your own custom authentication, see the {.custom_user_context}
37
+ # class method for more details.
38
+ #
39
+ # @note This middleware is automatically inserted for frameworks we support.
40
+ # Such as Rails. See {Timber::Frameworks} for a comprehensive list.
41
+ class UserContext < Middleware
42
+ class << self
43
+ # The custom user context allows you to hook in and set your own custom
44
+ # user context. This is used in situations where either:
45
+ #
46
+ # 1. Timber does not automatically support your authentication strategy (see module level docs)
47
+ # 2. You need to customize your authentication beyond Timber's defaults.
48
+ #
49
+ # @example Setting your own custom user context
50
+ # Timber::Integrations::Rack::UserContext.custom_user_hash = lambda do |rack_env|
51
+ # rack_env['my_custom_key'].user
52
+ # end
53
+ def custom_user_hash=(proc)
54
+ if proc && !proc.is_a?(Proc)
55
+ raise ArgumentError.new("The value passed to #custom_user_hash must be a Proc")
56
+ end
57
+
58
+ @custom_user_hash = proc
59
+ end
60
+
61
+ # Accessor method for {#custom_user_hash=}.
62
+ def custom_user_hash
63
+ @custom_user_hash
64
+ end
65
+ end
66
+
67
+ def call(env)
68
+ user_hash = get_user_hash(env)
69
+ if user_hash
70
+ context = Contexts::User.new(user_hash)
71
+ CurrentContext.with(context) do
72
+ @app.call(env)
73
+ end
74
+ else
75
+ @app.call(env)
76
+ end
77
+ end
78
+
79
+ private
80
+ def get_user_hash(env)
81
+ # The order is relevant here. The 'warden' key can be set, but
82
+ # not return a user, in which case the user data might be in another key.
83
+ if self.class.custom_user_hash.is_a?(Proc)
84
+ Timber::Config.instance.debug { "Obtaining user context from the custom user hash" }
85
+ self.class.custom_user_hash.call(env)
86
+ elsif env[:clearance] && env[:clearance].signed_in?
87
+ Timber::Config.instance.debug { "Obtaining user context from the clearance user" }
88
+ user = env[:clearance].current_user
89
+ get_user_object_hash(user)
90
+ elsif env['warden'] && (user = env['warden'].user)
91
+ Timber::Config.instance.debug { "Obtaining user context from the warden user" }
92
+ get_user_object_hash(user)
93
+ else
94
+ Timber::Config.instance.debug { "Could not locate any user data" }
95
+ nil
96
+ end
97
+ end
98
+
99
+ def get_user_object_hash(user)
100
+ id = try_user_id(user)
101
+ name = try_user_name(user)
102
+ email = try_user_email(user)
103
+
104
+ if id || name || email
105
+ {id: id, name: name, email: email}
106
+ else
107
+ nil
108
+ end
109
+ end
110
+
111
+ def try_user_id(user)
112
+ user.respond_to?(:id) ? user.id : nil
113
+ end
114
+
115
+ def try_user_name(user)
116
+ if user.respond_to?(:name) && user.name.is_a?(String)
117
+ user.name
118
+ elsif user.respond_to?(:first_name) && user.first_name.is_a?(String) && user.respond_to?(:last_name) && user.last_name.is_a?(String)
119
+ "#{user.first_name} #{user.last_name}"
120
+ else
121
+ nil
122
+ end
123
+ end
124
+
125
+ def try_user_email(user)
126
+ if user.respond_to?(:email) && user.email.is_a?(String)
127
+ user.email
128
+ else
129
+ nil
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,65 @@
1
+ module Timber
2
+ module Util
3
+ # @private
4
+ class Request < ::Rack::Request
5
+ # We store strings as constants since they are reused on a per request basis.
6
+ # This avoids string allocations.
7
+ HTTP_HEADER_ORIGINAL_DELIMITER = '_'.freeze
8
+ HTTP_HEADER_NEW_DELIMITER = '_'.freeze
9
+ HTTP_PREFIX = 'HTTP_'.freeze
10
+
11
+ REMOTE_IP_KEY_NAME = 'action_dispatch.remote_ip'.freeze
12
+ REQUEST_ID_KEY_NAME1 = 'action_dispatch.request_id'.freeze
13
+ REQUEST_ID_KEY_NAME2 = 'X-Request-ID'.freeze
14
+ REQUEST_ID_KEY_NAME3 = 'X-Request-Id'.freeze
15
+
16
+ def body_content
17
+ content = body.read
18
+ body.rewind
19
+ content
20
+ end
21
+
22
+ # Returns a list of request headers. The rack env contains a lot of data, this function
23
+ # identifies those that were the actual request headers.
24
+ #
25
+ # This was extracted from: https://github.com/ruby-grape/grape/blob/91c6c78ae3d3f3ffabaf57ffc4dc35ab7cfc7b5f/lib/grape/request.rb#L30
26
+ def headers
27
+ @headers ||= begin
28
+ headers = {}
29
+
30
+ @env.each_pair do |k, v|
31
+ next unless k.is_a?(String) && k.to_s.start_with?(HTTP_PREFIX)
32
+
33
+ k = k[5..-1].
34
+ split(HTTP_HEADER_ORIGINAL_DELIMITER).
35
+ each(&:capitalize!).
36
+ join(HTTP_HEADER_NEW_DELIMITER)
37
+
38
+ headers[k] = v
39
+ end
40
+
41
+ headers
42
+ end
43
+ end
44
+
45
+ def ip
46
+ @ip ||= if @env[REMOTE_IP_KEY_NAME]
47
+ @env[REMOTE_IP_KEY_NAME].to_s || super
48
+ else
49
+ super
50
+ end
51
+ end
52
+
53
+ def referer
54
+ # Rails 3.X returns "/" for some reason
55
+ @referer ||= super == "/" ? nil : super
56
+ end
57
+
58
+ def request_id
59
+ @request_id ||= @env[REQUEST_ID_KEY_NAME1] ||
60
+ @env[REQUEST_ID_KEY_NAME2] ||
61
+ @env[REQUEST_ID_KEY_NAME3]
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,7 @@
1
+ module Timber
2
+ module Integrations
3
+ module Rack
4
+ VERSION = "1.0.0"
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,42 @@
1
+
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "timber-rack/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "timber-rack"
8
+ spec.version = Timber::Integrations::Rack::VERSION
9
+ spec.authors = ["Timber Technologies, Inc."]
10
+ spec.email = ["hi@timber.io"]
11
+
12
+ spec.summary = %q{Timber for Ruby is a drop in replacement for your Ruby logger that unobtrusively augments your logs with rich metadata and context making them easier to search, use, and read.}
13
+ spec.homepage = "https://docs.timber.io/languages/ruby/"
14
+ spec.license = "ISC"
15
+
16
+ spec.required_ruby_version = '>= 1.9.0'
17
+
18
+ if spec.respond_to?(:metadata)
19
+ spec.metadata["homepage_uri"] = spec.homepage
20
+ spec.metadata["source_code_uri"] = "https://github.com/timberio/timber-ruby-rack"
21
+ spec.metadata["changelog_uri"] = "https://github.com/timberio/timber-ruby-rack/blob/master/README.md"
22
+ else
23
+ raise "RubyGems 2.0 or newer is required to protect against " \
24
+ "public gem pushes."
25
+ end
26
+
27
+ # Specify which files should be added to the gem when it is released.
28
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
29
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
30
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
31
+ end
32
+ spec.bindir = "exe"
33
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
34
+ spec.require_paths = ["lib"]
35
+
36
+ # spec.add_dependency "timber", "3.0.0.alpha.0"
37
+ spec.add_runtime_dependency "rack", ">= 1.2", "< 3.0"
38
+
39
+ spec.add_development_dependency "bundler", ">= 0.0"
40
+ spec.add_development_dependency "rake", "~> 10.0"
41
+ spec.add_development_dependency "rspec", "~> 3.0"
42
+ end
metadata ADDED
@@ -0,0 +1,132 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: timber-rack
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Timber Technologies, Inc.
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2019-03-08 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rack
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '1.2'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '3.0'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: '1.2'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '3.0'
33
+ - !ruby/object:Gem::Dependency
34
+ name: bundler
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0.0'
40
+ type: :development
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0.0'
47
+ - !ruby/object:Gem::Dependency
48
+ name: rake
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '10.0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '10.0'
61
+ - !ruby/object:Gem::Dependency
62
+ name: rspec
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '3.0'
68
+ type: :development
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '3.0'
75
+ description:
76
+ email:
77
+ - hi@timber.io
78
+ executables: []
79
+ extensions: []
80
+ extra_rdoc_files: []
81
+ files:
82
+ - ".gitignore"
83
+ - ".rspec"
84
+ - ".travis.yml"
85
+ - Gemfile
86
+ - LICENSE.md
87
+ - README.md
88
+ - Rakefile
89
+ - bin/console
90
+ - bin/setup
91
+ - lib/timber-rack.rb
92
+ - lib/timber-rack/config.rb
93
+ - lib/timber-rack/error_event.rb
94
+ - lib/timber-rack/http_context.rb
95
+ - lib/timber-rack/http_events.rb
96
+ - lib/timber-rack/http_request.rb
97
+ - lib/timber-rack/http_response.rb
98
+ - lib/timber-rack/middleware.rb
99
+ - lib/timber-rack/session_context.rb
100
+ - lib/timber-rack/user_context.rb
101
+ - lib/timber-rack/util/request.rb
102
+ - lib/timber-rack/version.rb
103
+ - timber-rack.gemspec
104
+ homepage: https://docs.timber.io/languages/ruby/
105
+ licenses:
106
+ - ISC
107
+ metadata:
108
+ homepage_uri: https://docs.timber.io/languages/ruby/
109
+ source_code_uri: https://github.com/timberio/timber-ruby-rack
110
+ changelog_uri: https://github.com/timberio/timber-ruby-rack/blob/master/README.md
111
+ post_install_message:
112
+ rdoc_options: []
113
+ require_paths:
114
+ - lib
115
+ required_ruby_version: !ruby/object:Gem::Requirement
116
+ requirements:
117
+ - - ">="
118
+ - !ruby/object:Gem::Version
119
+ version: 1.9.0
120
+ required_rubygems_version: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ requirements: []
126
+ rubygems_version: 3.0.3
127
+ signing_key:
128
+ specification_version: 4
129
+ summary: Timber for Ruby is a drop in replacement for your Ruby logger that unobtrusively
130
+ augments your logs with rich metadata and context making them easier to search,
131
+ use, and read.
132
+ test_files: []