logtail-rack 0.1.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: 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: []