logtail-rack 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: cc7cc7670a2f510b98d303a3a8a9d508f12845b3f4aa122a3ba85b74811a2612
4
+ data.tar.gz: 192cff59004342160a95c620d3c454a99466e7414229fd6530c09a7a588634ad
5
+ SHA512:
6
+ metadata.gz: 352c298d34087b2465ee410376a2e1f7620617a2a739e92536675f2ad1228019c6964022cb5403935d344130c97080c5f5d705b813dd1abfbdfc07d2ce151526
7
+ data.tar.gz: 3d31454c761c8b61b54f3bf9f3d0147d886e95361532d03e92ae218b1e06a475916eefd54dc1a1256b10149b9a2d33571f7cffbc4168c562e42f4805c52385d6
@@ -0,0 +1,33 @@
1
+ name: build
2
+
3
+ on: [push, pull_request]
4
+
5
+ jobs:
6
+ test:
7
+
8
+ runs-on: ubuntu-20.04
9
+
10
+ strategy:
11
+ matrix:
12
+ ruby-version:
13
+ - 3.0.0
14
+ - 2.7.2
15
+ - 2.6.6
16
+ - 2.5.8
17
+ - 2.4.10
18
+ - 2.3.8
19
+ - 2.2.10
20
+ - jruby-9.2.14.0
21
+ - truffleruby-21.0.0
22
+
23
+ steps:
24
+ - uses: actions/checkout@v2
25
+
26
+ - name: Set up Ruby ${{ matrix.ruby-version }}
27
+ uses: ruby/setup-ruby@v1
28
+ with:
29
+ ruby-version: ${{ matrix.ruby-version }}
30
+ bundler-cache: true
31
+
32
+ - name: Run tests
33
+ run: bundle exec rspec --format documentation
data/.gitignore ADDED
@@ -0,0 +1,21 @@
1
+ /doc/
2
+ /pkg/
3
+ /spec/reports/
4
+ /tmp/
5
+
6
+ # rspec failure tracking
7
+ .rspec_status
8
+ .DS_Store
9
+ .rvmrc
10
+ .ruby-version
11
+ coverage
12
+ Gemfile.lock
13
+ *.swp
14
+ *.gem
15
+
16
+ /.bundle
17
+ /.yardoc
18
+ /doc
19
+ /log
20
+ /tmp
21
+ /pkg
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
data/LICENSE.md ADDED
@@ -0,0 +1,16 @@
1
+ # License
2
+
3
+ Copyright (c) 2021, Logtail
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,9 @@
1
+ # 🪵 Logtail Integration For Rack
2
+
3
+ [![ISC License](https://img.shields.io/badge/license-ISC-ff69b4.svg)](LICENSE.md)
4
+ [![Build Status](https://github.com/logtail/logtail-ruby-rack/workflows/build/badge.svg)](https://github.com/logtail/logtail-ruby-rack/actions?query=workflow%3Abuild)
5
+
6
+ This library integrates the [`logtail` Ruby library](https://github.com/logtail/logtail-ruby) with the [rack](https://github.com/rack/rack) framework,
7
+ turning your Rack logs into rich structured events.
8
+
9
+ * **Sign-up: [https://logtail.com](https://logtail.com)**
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
@@ -0,0 +1,16 @@
1
+ require "rack"
2
+ require "logtail"
3
+ require "logtail-rack/config"
4
+ require "logtail-rack/error_event"
5
+ require "logtail-rack/http_context"
6
+ require "logtail-rack/http_events"
7
+ require "logtail-rack/session_context"
8
+ require "logtail-rack/user_context"
9
+
10
+ module Logtail
11
+ module Integrations
12
+ module Rack
13
+ end
14
+ end
15
+ end
16
+
@@ -0,0 +1,85 @@
1
+ require "logtail"
2
+
3
+ Logtail::Config.instance.define_singleton_method(:logrageify!) do
4
+ integrations.rack.http_events.collapse_into_single_event = true
5
+ end
6
+
7
+ module Logtail
8
+ class Config
9
+ module Integrations
10
+ extend self
11
+ # Convenience module for accessing the various `Logtail::Integrations::Rack::*` classes
12
+ # through the {Logtail::Config} object. Logtail 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 Logtail classes.
16
+ #
17
+ # For example:
18
+ #
19
+ # config = Logtail::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 {Logtail::Integrations::Rack::ErrorEvent}
29
+ # middleware class specific configuration. See {Logtail::Integrations::Rack::ExceptionEvent}
30
+ # for a list of methods available.
31
+ #
32
+ # @example
33
+ # config = Logtail::Config.instance
34
+ # config.integrations.rack.error_event.enabled = false
35
+ def error_event
36
+ Logtail::Integrations::Rack::ErrorEvent
37
+ end
38
+
39
+ # Convenience method for accessing the {Logtail::Integrations::Rack::HTTPContext}
40
+ # middleware class specific configuration. See {Logtail::Integrations::Rack::HTTPContext}
41
+ # for a list of methods available.
42
+ #
43
+ # @example
44
+ # config = Logtail::Config.instance
45
+ # config.integrations.rack.http_context.enabled = false
46
+ def http_context
47
+ Logtail::Integrations::Rack::HTTPContext
48
+ end
49
+
50
+ # Convenience method for accessing the {Logtail::Integrations::Rack::HTTPEvents}
51
+ # middleware class specific configuration. See {Logtail::Integrations::Rack::HTTPEvents}
52
+ # for a list of methods available.
53
+ #
54
+ # @example
55
+ # config = Logtail::Config.instance
56
+ # config.integrations.rack.http_events.enabled = false
57
+ def http_events
58
+ Logtail::Integrations::Rack::HTTPEvents
59
+ end
60
+
61
+ # Convenience method for accessing the {Logtail::Integrations::Rack::SessionContext}
62
+ # middleware class specific configuration. See {Logtail::Integrations::Rack::SessionContext}
63
+ # for a list of methods available.
64
+ #
65
+ # @example
66
+ # config = Logtail::Config.instance
67
+ # config.integrations.rack.session_context.enabled = false
68
+ def session_context
69
+ Logtail::Integrations::Rack::SessionContext
70
+ end
71
+
72
+ # Convenience method for accessing the {Logtail::Integrations::Rack::UserContext}
73
+ # middleware class specific configuration. See {Logtail::Integrations::Rack::UserContext}
74
+ # for a list of methods available.
75
+ #
76
+ # @example
77
+ # config = Logtail::Config.instance
78
+ # config.integrations.rack.user_context.enabled = false
79
+ def user_context
80
+ Logtail::Integrations::Rack::UserContext
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,28 @@
1
+ require "logtail/config"
2
+ require "logtail/events/error"
3
+ require "logtail-rack/middleware"
4
+
5
+ module Logtail
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 "logtail/contexts/http"
2
+ require "logtail/current_context"
3
+ require "logtail-rack/middleware"
4
+ require "logtail-rack/util/request"
5
+
6
+ module Logtail
7
+ module Integrations
8
+ module Rack
9
+ # A Rack middleware that is reponsible for adding the HTTP context {Logtail::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
+
21
+ CurrentContext.add(context.to_hash)
22
+ @app.call(env)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,277 @@
1
+ require "set"
2
+
3
+ require "logtail/config"
4
+ require "logtail/contexts/http"
5
+ require "logtail/current_context"
6
+ require "logtail-rack/http_request"
7
+ require "logtail-rack/http_response"
8
+ require "logtail-rack/middleware"
9
+
10
+ module Logtail
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
+ # Logtail::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 {Logtail::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 {Logtail::Events::HTTPRequest} event would
70
+ # provide.
71
+ #
72
+ # @example
73
+ # Logtail::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 = CurrentContext.fetch(:http)
145
+ content_length = headers[CONTENT_LENGTH_KEY]
146
+ duration_ms = (Time.now - start) * 1000.0
147
+
148
+ http_response = HTTPResponse.new(
149
+ content_length: content_length,
150
+ headers: headers,
151
+ http_context: http_context,
152
+ request_id: request.request_id,
153
+ status: status,
154
+ duration_ms: duration_ms,
155
+ body_limit: self.class.http_body_limit,
156
+ headers_to_sanitize: self.class.http_header_filters,
157
+ )
158
+
159
+ {
160
+ message: http_response.message,
161
+ event: {
162
+ http_response_sent: {
163
+ body: http_response.body,
164
+ content_length: http_response.content_length,
165
+ headers_json: http_response.headers_json,
166
+ request_id: http_response.request_id,
167
+ service_name: http_response.service_name,
168
+ status: http_response.status,
169
+ duration_ms: http_response.duration_ms,
170
+ }
171
+ }
172
+ }
173
+ end
174
+
175
+ [status, headers, body]
176
+ else
177
+ start = Time.now
178
+
179
+ Config.instance.logger.info do
180
+ event_body = capture_request_body? ? request.body_content : nil
181
+ http_request = HTTPRequest.new(
182
+ body: event_body,
183
+ content_length: request.content_length,
184
+ headers: request.headers,
185
+ host: request.host,
186
+ method: request.request_method,
187
+ path: request.path,
188
+ port: request.port,
189
+ query_string: request.query_string,
190
+ request_id: request.request_id,
191
+ scheme: request.scheme,
192
+ body_limit: self.class.http_body_limit,
193
+ headers_to_sanitize: self.class.http_header_filters,
194
+ )
195
+
196
+ {
197
+ message: http_request.message,
198
+ event: {
199
+ http_request_received: {
200
+ body: http_request.body,
201
+ content_length: http_request.content_length,
202
+ headers_json: http_request.headers_json,
203
+ host: http_request.host,
204
+ method: http_request.method,
205
+ path: http_request.path,
206
+ port: http_request.port,
207
+ query_string: http_request.query_string,
208
+ request_id: http_request.request_id,
209
+ scheme: http_request.scheme,
210
+ service_name: http_request.service_name,
211
+ }
212
+ }
213
+ }
214
+ end
215
+
216
+ status, headers, body = @app.call(env)
217
+
218
+ Config.instance.logger.info do
219
+ event_body = capture_response_body? ? body : nil
220
+ content_length = headers[CONTENT_LENGTH_KEY]
221
+ duration_ms = (Time.now - start) * 1000.0
222
+
223
+ http_response = HTTPResponse.new(
224
+ body: event_body,
225
+ content_length: content_length,
226
+ headers: headers,
227
+ request_id: request.request_id,
228
+ status: status,
229
+ duration_ms: duration_ms,
230
+ body_limit: self.class.http_body_limit,
231
+ headers_to_sanitize: self.class.http_header_filters,
232
+ )
233
+
234
+ {
235
+ message: http_response.message,
236
+ event: {
237
+ http_response_sent: {
238
+ body: http_response.body,
239
+ content_length: http_response.content_length,
240
+ headers_json: http_response.headers_json,
241
+ request_id: http_response.request_id,
242
+ service_name: http_response.service_name,
243
+ status: http_response.status,
244
+ duration_ms: http_response.duration_ms,
245
+ }
246
+ }
247
+ }
248
+ end
249
+
250
+ [status, headers, body]
251
+ end
252
+ end
253
+
254
+ private
255
+ def capture_request_body?
256
+ self.class.capture_request_body?
257
+ end
258
+
259
+ def capture_response_body?
260
+ self.class.capture_response_body?
261
+ end
262
+
263
+ def collapse_into_single_event?
264
+ self.class.collapse_into_single_event?
265
+ end
266
+
267
+ def silenced?(env, request)
268
+ if !self.class.silence_request.nil?
269
+ self.class.silence_request.call(env, request)
270
+ else
271
+ false
272
+ end
273
+ end
274
+ end
275
+ end
276
+ end
277
+ end
@@ -0,0 +1,37 @@
1
+ module Logtail
2
+ module Integrations
3
+ module Rack
4
+ # The HTTP server request event tracks incoming HTTP requests to your HTTP server.
5
+ # Such as unicorn, webrick, puma, etc.
6
+ #
7
+ # @note This event should be installed automatically through integrations,
8
+ # such as the {Integrations::ActionController::LogSubscriber} integration.
9
+ class HTTPRequest
10
+ attr_reader :body, :content_length, :headers, :headers_json, :host, :method, :path, :port,
11
+ :query_string, :request_id, :scheme, :service_name
12
+
13
+ def initialize(attributes)
14
+ @body = attributes[:body]
15
+ @content_length = attributes[:content_length]
16
+ @headers = attributes[:headers]
17
+ @host = attributes[:host]
18
+ @method = attributes[:method]
19
+ @path = attributes[:path]
20
+ @port = attributes[:port]
21
+ @query_string = attributes[:query_string]
22
+ @scheme = attributes[:scheme]
23
+ @request_id = attributes[:request_id]
24
+ @service_name = attributes[:service_name]
25
+
26
+ if @headers
27
+ @headers_json = @headers.to_json
28
+ end
29
+ end
30
+
31
+ def message
32
+ 'Started %s "%s"' % [method, path]
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,54 @@
1
+ module Logtail
2
+ module Integrations
3
+ module Rack
4
+ # The HTTP server response event tracks outgoing HTTP responses that you send
5
+ # to clients.
6
+
7
+ class HTTPResponse
8
+ attr_reader :body, :content_length, :headers, :headers_json, :http_context, :request_id, :service_name,
9
+ :status, :duration_ms
10
+
11
+ def initialize(attributes)
12
+ @body = attributes[:body]
13
+ @content_length = attributes[:content_length]
14
+ @headers = attributes[:headers]
15
+ @http_context = attributes[:http_context]
16
+ @request_id = attributes[:request_id]
17
+ @service_name = attributes[:service_name]
18
+ @status = attributes[:status]
19
+ @duration_ms = attributes[:duration_ms]
20
+
21
+ if @headers
22
+ @headers_json = @headers.to_json
23
+ end
24
+ end
25
+
26
+ # Returns the human readable log message for this event.
27
+ def message
28
+ if http_context
29
+ message = "#{http_context[:method]} #{http_context[:path]} completed with " \
30
+ "#{status} #{status_description} "
31
+
32
+ if content_length
33
+ message << ", #{content_length} bytes, "
34
+ end
35
+
36
+ message << "in #{duration_ms}ms"
37
+ else
38
+ message = "Completed #{status} #{status_description} "
39
+
40
+ if content_length
41
+ message << ", #{content_length} bytes, "
42
+ end
43
+
44
+ message << "in #{duration_ms}ms"
45
+ end
46
+ end
47
+
48
+ def status_description
49
+ ::Rack::Utils::HTTP_STATUS_CODES[status]
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,28 @@
1
+ module Logtail
2
+ module Integrations
3
+ module Rack
4
+ # Base class that all Logtail 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
+ # Logtail::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 "logtail/config"
2
+ require "logtail/contexts/session"
3
+ require "logtail-rack/middleware"
4
+
5
+ module Logtail
6
+ module Integrations
7
+ module Rack
8
+ # A Rack middleware that is responsible for adding the Session context
9
+ # {Logtail::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
+ Logtail::Config.instance.debug { "Rack env session detected, using id attribute" }
28
+ session.id
29
+ elsif session.respond_to?(:[])
30
+ Logtail::Config.instance.debug { "Rack env session detected, using the session_id key" }
31
+ session["session_id"]
32
+ else
33
+ Logtail::Config.instance.debug { "Rack env session detected but could not extract id" }
34
+ nil
35
+ end
36
+ else
37
+ Logtail::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,132 @@
1
+ require "logtail/config"
2
+ require "logtail/contexts/user"
3
+ require "logtail-rack/middleware"
4
+
5
+ module Logtail
6
+ module Integrations
7
+ module Rack
8
+ # This is a Rack middleware responsible for setting the user context.
9
+ # See {Logtail::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, Logtail 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 {Logtail::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. Logtail does not automatically support your authentication strategy (see module level docs)
47
+ # 2. You need to customize your authentication beyond Logtail's defaults.
48
+ #
49
+ # @example Setting your own custom user context
50
+ # Logtail::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
+ CurrentContext.add({user: user_hash})
71
+ end
72
+
73
+ @app.call(env)
74
+ end
75
+
76
+ private
77
+ def get_user_hash(env)
78
+ # The order is relevant here. The 'warden' key can be set, but
79
+ # not return a user, in which case the user data might be in another key.
80
+ if self.class.custom_user_hash.is_a?(Proc)
81
+ Logtail::Config.instance.debug { "Obtaining user context from the custom user hash" }
82
+ self.class.custom_user_hash.call(env)
83
+ elsif env[:clearance] && env[:clearance].signed_in?
84
+ Logtail::Config.instance.debug { "Obtaining user context from the clearance user" }
85
+ user = env[:clearance].current_user
86
+ get_user_object_hash(user)
87
+ elsif env['warden'] && (user = env['warden'].user)
88
+ Logtail::Config.instance.debug { "Obtaining user context from the warden user" }
89
+ get_user_object_hash(user)
90
+ else
91
+ Logtail::Config.instance.debug { "Could not locate any user data" }
92
+ nil
93
+ end
94
+ end
95
+
96
+ def get_user_object_hash(user)
97
+ id = try_user_id(user)
98
+ name = try_user_name(user)
99
+ email = try_user_email(user)
100
+
101
+ if id || name || email
102
+ {id: id, name: name, email: email}
103
+ else
104
+ nil
105
+ end
106
+ end
107
+
108
+ def try_user_id(user)
109
+ user.respond_to?(:id) ? user.id : nil
110
+ end
111
+
112
+ def try_user_name(user)
113
+ if user.respond_to?(:name) && user.name.is_a?(String)
114
+ user.name
115
+ elsif user.respond_to?(:first_name) && user.first_name.is_a?(String) && user.respond_to?(:last_name) && user.last_name.is_a?(String)
116
+ "#{user.first_name} #{user.last_name}"
117
+ else
118
+ nil
119
+ end
120
+ end
121
+
122
+ def try_user_email(user)
123
+ if user.respond_to?(:email) && user.email.is_a?(String)
124
+ user.email
125
+ else
126
+ nil
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,63 @@
1
+ module Logtail
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 = 'HTTP_X_REQUEST_ID'.freeze
14
+
15
+ def body_content
16
+ content = body.read
17
+ body.rewind
18
+ content
19
+ end
20
+
21
+ # Returns a list of request headers. The rack env contains a lot of data, this function
22
+ # identifies those that were the actual request headers.
23
+ #
24
+ # This was extracted from: https://github.com/ruby-grape/grape/blob/91c6c78ae3d3f3ffabaf57ffc4dc35ab7cfc7b5f/lib/grape/request.rb#L30
25
+ def headers
26
+ @headers ||= begin
27
+ headers = {}
28
+
29
+ @env.each_pair do |k, v|
30
+ next unless k.is_a?(String) && k.to_s.start_with?(HTTP_PREFIX)
31
+
32
+ k = k[5..-1].
33
+ split(HTTP_HEADER_ORIGINAL_DELIMITER).
34
+ each(&:capitalize!).
35
+ join(HTTP_HEADER_NEW_DELIMITER)
36
+
37
+ headers[k] = v
38
+ end
39
+
40
+ headers
41
+ end
42
+ end
43
+
44
+ def ip
45
+ @ip ||= if @env[REMOTE_IP_KEY_NAME]
46
+ @env[REMOTE_IP_KEY_NAME].to_s || super
47
+ else
48
+ super
49
+ end
50
+ end
51
+
52
+ def referer
53
+ # Rails 3.X returns "/" for some reason
54
+ @referer ||= super == "/" ? nil : super
55
+ end
56
+
57
+ def request_id
58
+ @request_id ||= @env[REQUEST_ID_KEY_NAME1] ||
59
+ @env[REQUEST_ID_KEY_NAME2]
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,7 @@
1
+ module Logtail
2
+ module Integrations
3
+ module Rack
4
+ VERSION = "0.1.0"
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,36 @@
1
+ lib = File.expand_path("../lib", __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require "logtail-rack/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "logtail-rack"
7
+ spec.version = Logtail::Integrations::Rack::VERSION
8
+ spec.authors = ["Logtail"]
9
+ spec.email = ["hi@logtail.com"]
10
+
11
+ spec.summary = %q{Logtail integration for Rack}
12
+ spec.homepage = "https://github.com/logtail/logtail-ruby-rack"
13
+ spec.license = "ISC"
14
+
15
+ spec.required_ruby_version = '>= 2.2.10'
16
+
17
+ spec.metadata["homepage_uri"] = spec.homepage
18
+ spec.metadata["source_code_uri"] = "https://github.com/logtail/logtail-ruby-rack"
19
+ spec.metadata["changelog_uri"] = "https://github.com/logtail/logtail-ruby-rack/blob/master/README.md"
20
+
21
+ # Specify which files should be added to the gem when it is released.
22
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
23
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
24
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
25
+ end
26
+ spec.bindir = "exe"
27
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
28
+ spec.require_paths = ["lib"]
29
+
30
+ spec.add_dependency "logtail-ruby", "~> 0.1"
31
+ spec.add_runtime_dependency "rack", ">= 1.2", "< 3.0"
32
+
33
+ spec.add_development_dependency "bundler", ">= 0.0"
34
+ spec.add_development_dependency "rake", "~> 10.0"
35
+ spec.add_development_dependency "rspec", "~> 3.0"
36
+ end
metadata ADDED
@@ -0,0 +1,142 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: logtail-rack
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Logtail
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2021-02-12 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: logtail-ruby
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.1'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0.1'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rack
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '1.2'
34
+ - - "<"
35
+ - !ruby/object:Gem::Version
36
+ version: '3.0'
37
+ type: :runtime
38
+ prerelease: false
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: '1.2'
44
+ - - "<"
45
+ - !ruby/object:Gem::Version
46
+ version: '3.0'
47
+ - !ruby/object:Gem::Dependency
48
+ name: bundler
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0.0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0.0'
61
+ - !ruby/object:Gem::Dependency
62
+ name: rake
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '10.0'
68
+ type: :development
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '10.0'
75
+ - !ruby/object:Gem::Dependency
76
+ name: rspec
77
+ requirement: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '3.0'
82
+ type: :development
83
+ prerelease: false
84
+ version_requirements: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '3.0'
89
+ description:
90
+ email:
91
+ - hi@logtail.com
92
+ executables: []
93
+ extensions: []
94
+ extra_rdoc_files: []
95
+ files:
96
+ - ".github/workflows/main.yml"
97
+ - ".gitignore"
98
+ - ".rspec"
99
+ - Gemfile
100
+ - LICENSE.md
101
+ - README.md
102
+ - Rakefile
103
+ - lib/logtail-rack.rb
104
+ - lib/logtail-rack/config.rb
105
+ - lib/logtail-rack/error_event.rb
106
+ - lib/logtail-rack/http_context.rb
107
+ - lib/logtail-rack/http_events.rb
108
+ - lib/logtail-rack/http_request.rb
109
+ - lib/logtail-rack/http_response.rb
110
+ - lib/logtail-rack/middleware.rb
111
+ - lib/logtail-rack/session_context.rb
112
+ - lib/logtail-rack/user_context.rb
113
+ - lib/logtail-rack/util/request.rb
114
+ - lib/logtail-rack/version.rb
115
+ - logtail-ruby-rack.gemspec
116
+ homepage: https://github.com/logtail/logtail-ruby-rack
117
+ licenses:
118
+ - ISC
119
+ metadata:
120
+ homepage_uri: https://github.com/logtail/logtail-ruby-rack
121
+ source_code_uri: https://github.com/logtail/logtail-ruby-rack
122
+ changelog_uri: https://github.com/logtail/logtail-ruby-rack/blob/master/README.md
123
+ post_install_message:
124
+ rdoc_options: []
125
+ require_paths:
126
+ - lib
127
+ required_ruby_version: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: 2.2.10
132
+ required_rubygems_version: !ruby/object:Gem::Requirement
133
+ requirements:
134
+ - - ">="
135
+ - !ruby/object:Gem::Version
136
+ version: '0'
137
+ requirements: []
138
+ rubygems_version: 3.2.3
139
+ signing_key:
140
+ specification_version: 4
141
+ summary: Logtail integration for Rack
142
+ test_files: []