timber-rack 1.0.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.
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: []