logtail-rack 0.2.7 → 0.2.9
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 +4 -4
- data/Gemfile +2 -0
- data/lib/logtail-rack/error_event.rb +1 -1
- data/lib/logtail-rack/http_context.rb +22 -10
- data/lib/logtail-rack/http_events.rb +164 -17
- data/lib/logtail-rack/http_request.rb +3 -1
- data/lib/logtail-rack/http_response.rb +3 -1
- data/lib/logtail-rack/middleware.rb +14 -0
- data/lib/logtail-rack/session_context.rb +5 -1
- data/lib/logtail-rack/user_context.rb +3 -0
- data/lib/logtail-rack/util/request.rb +4 -0
- data/lib/logtail-rack/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4df8d3a1e2937eb55804fc33386d4fff4225cb262e3aee05730dc8d34cb0ea3f
|
|
4
|
+
data.tar.gz: 36d88de7aeffcc49e185f2dc9149e4fc7811f113ee46e65c22d5410bbca539da
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7e0e826ff231eaffa61f7a25846321cb9e39966f62d223f9f9d7bf290b80faacd504fa6230b2a314ab54f0bee4d6ac923d1b2431af81c64f47400b9eb0280fc3
|
|
7
|
+
data.tar.gz: 5ad21673f3fa6afbd3f14f839637a5fafa89ba6f7d4c87eda2461b8a58ac62de72e5fb80328a93a62e8f60d94cf14912d700597494063b5dffbe8774c74240d8
|
data/Gemfile
CHANGED
|
@@ -5,3 +5,5 @@ gemspec
|
|
|
5
5
|
gem "base64" if RUBY_VERSION >= "3.4.0"
|
|
6
6
|
# No longer a default gem on Ruby 4.0 and TruffleRuby 40, and logtail 0.1.17 requires it undeclared
|
|
7
7
|
gem "logger"
|
|
8
|
+
# Rack 3 moved the session middlewares the tests use into this gem; its 1.x releases for older Racks are empty
|
|
9
|
+
gem "rack-session"
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
require "logtail/config"
|
|
1
2
|
require "logtail/contexts/http"
|
|
2
3
|
require "logtail/current_context"
|
|
3
4
|
require "logtail-rack/middleware"
|
|
@@ -10,19 +11,30 @@ module Logtail
|
|
|
10
11
|
# A Rack middleware that is reponsible for adding the HTTP context {Logtail::Contexts::HTTP}.
|
|
11
12
|
class HTTPContext < Middleware
|
|
12
13
|
def call(env)
|
|
13
|
-
|
|
14
|
-
context
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
request_id: request.request_id
|
|
20
|
-
)
|
|
21
|
-
|
|
22
|
-
CurrentContext.with(context.to_hash) do
|
|
14
|
+
context = get_http_context(env)
|
|
15
|
+
if context
|
|
16
|
+
CurrentContext.with(context) do
|
|
17
|
+
@app.call(env)
|
|
18
|
+
end
|
|
19
|
+
else
|
|
23
20
|
@app.call(env)
|
|
24
21
|
end
|
|
25
22
|
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
def get_http_context(env)
|
|
26
|
+
request = Util::Request.new(env)
|
|
27
|
+
Contexts::HTTP.new(
|
|
28
|
+
host: Util::Encoding.force_utf8_encoding(request.host),
|
|
29
|
+
method: Util::Encoding.force_utf8_encoding(request.request_method),
|
|
30
|
+
path: request.path,
|
|
31
|
+
remote_addr: Util::Encoding.force_utf8_encoding(request.ip),
|
|
32
|
+
request_id: request.request_id
|
|
33
|
+
).to_hash
|
|
34
|
+
rescue StandardError => e
|
|
35
|
+
Logtail::Config.instance.debug { "Could not build the HTTP context: #{e.inspect}\n\n#{e.backtrace}" }
|
|
36
|
+
nil
|
|
37
|
+
end
|
|
26
38
|
end
|
|
27
39
|
end
|
|
28
40
|
end
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require "set"
|
|
2
|
+
require "uri"
|
|
2
3
|
|
|
3
4
|
require "logtail/config"
|
|
4
5
|
require "logtail/contexts/http"
|
|
@@ -15,7 +16,9 @@ module Logtail
|
|
|
15
16
|
# response events. The {Events::HTTPRequest} and {Events::HTTPResponse} events
|
|
16
17
|
# respectively.
|
|
17
18
|
class HTTPEvents < Middleware
|
|
19
|
+
DEFAULT_STATUS_FOR_EXCEPTION = ->(_exception) { 500 }
|
|
18
20
|
DEFAULT_HTTP_HEADER_FILTERS = ["Authorization", "Proxy-Authorization", "Cookie", "Set-Cookie"].freeze
|
|
21
|
+
DEFAULT_QUERY_STRING_FILTERS = %w[passw secret token _key crypt salt certificate otp ssn cvv cvc].freeze
|
|
19
22
|
|
|
20
23
|
class << self
|
|
21
24
|
# Allows you to capture the HTTP request body, default is off (false).
|
|
@@ -105,6 +108,29 @@ module Logtail
|
|
|
105
108
|
@silence_request
|
|
106
109
|
end
|
|
107
110
|
|
|
111
|
+
# When the app raises instead of returning a response, the response event is still
|
|
112
|
+
# logged, and the exception is re-raised unchanged. This setting resolves the HTTP
|
|
113
|
+
# status of that event: a callable that receives the exception and returns an Integer
|
|
114
|
+
# status. The default, {DEFAULT_STATUS_FOR_EXCEPTION}, returns 500, and so does a
|
|
115
|
+
# callable that raises itself.
|
|
116
|
+
#
|
|
117
|
+
# @example
|
|
118
|
+
# Logtail::Integrations::Rack::HTTPEvents.status_for_exception = lambda do |exception|
|
|
119
|
+
# exception.is_a?(MyApp::NotFound) ? 404 : 500
|
|
120
|
+
# end
|
|
121
|
+
def status_for_exception=(callable)
|
|
122
|
+
if !callable.respond_to?(:call)
|
|
123
|
+
raise ArgumentError.new("The value passed to #status_for_exception must respond to #call")
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
@status_for_exception = callable
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Accessor method for {#status_for_exception=}
|
|
130
|
+
def status_for_exception
|
|
131
|
+
@status_for_exception
|
|
132
|
+
end
|
|
133
|
+
|
|
108
134
|
# Filter sensitive HTTP headers (such as "Authorization: Bearer secret_token")
|
|
109
135
|
#
|
|
110
136
|
# Filtered HTTP header values will be sent to Better Stack as "[FILTERED]"
|
|
@@ -123,14 +149,60 @@ module Logtail
|
|
|
123
149
|
@http_header_filters
|
|
124
150
|
end
|
|
125
151
|
|
|
152
|
+
# Filter sensitive query string parameters (such as "?token=secret_token")
|
|
153
|
+
#
|
|
154
|
+
# Filtered values will be sent to Better Stack as "[FILTERED]", in the query string of
|
|
155
|
+
# the request event and in the query of the URLs in the Referer and Location headers.
|
|
156
|
+
# Like Rails' filter_parameters, a String or Symbol filters every parameter whose
|
|
157
|
+
# URL-decoded name contains it, ignoring case, and a Regexp every name it matches.
|
|
158
|
+
#
|
|
159
|
+
# {DEFAULT_QUERY_STRING_FILTERS} are filtered out of the box. Setting this replaces
|
|
160
|
+
# the whole list, pass an empty list to log query strings unfiltered.
|
|
161
|
+
#
|
|
162
|
+
# @example
|
|
163
|
+
# Logtail::Integrations::Rack::HTTPEvents.query_string_filters = Logtail::Integrations::Rack::HTTPEvents::DEFAULT_QUERY_STRING_FILTERS + ["code", /\Aapi_/]
|
|
164
|
+
def query_string_filters=(value)
|
|
165
|
+
strings = value.select { |filter| filter.is_a?(String) || filter.is_a?(Symbol) }
|
|
166
|
+
regexps = value.select { |filter| filter.is_a?(Regexp) }
|
|
167
|
+
if !strings.empty?
|
|
168
|
+
regexps << Regexp.new(strings.map { |filter| Regexp.escape(filter.to_s) }.join("|"), Regexp::IGNORECASE)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
@query_string_filters = value
|
|
172
|
+
@query_string_filter_regexps = regexps
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Accessor method for {#query_string_filters=}
|
|
176
|
+
def query_string_filters
|
|
177
|
+
@query_string_filters
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Whether {#query_string_filters} filter the parameter with this name from a query string
|
|
181
|
+
def filter_query_string_parameter?(name)
|
|
182
|
+
begin
|
|
183
|
+
name = URI.decode_www_form_component(name)
|
|
184
|
+
rescue ArgumentError
|
|
185
|
+
# Invalid %-encoding, match the name as it is
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
name = name.scrub
|
|
189
|
+
@query_string_filter_regexps.any? { |regexp| regexp =~ name }
|
|
190
|
+
end
|
|
191
|
+
|
|
126
192
|
def normalize_header_name(name)
|
|
127
193
|
name.to_s.downcase.gsub("-", "_")
|
|
128
194
|
end
|
|
129
195
|
end
|
|
130
196
|
|
|
197
|
+
self.status_for_exception = DEFAULT_STATUS_FOR_EXCEPTION
|
|
131
198
|
self.http_header_filters = DEFAULT_HTTP_HEADER_FILTERS
|
|
199
|
+
self.query_string_filters = DEFAULT_QUERY_STRING_FILTERS
|
|
132
200
|
|
|
133
|
-
CONTENT_LENGTH_KEY = '
|
|
201
|
+
CONTENT_LENGTH_KEY = 'content-length'.freeze
|
|
202
|
+
# A query string parameter name and its value, up to the next separator
|
|
203
|
+
QUERY_STRING_PARAMETER = /([^&;=]+)=([^&;]+)/
|
|
204
|
+
URL_QUERY = /\?([^#]*)/
|
|
205
|
+
URL_HEADERS = ["referer", "location"].freeze
|
|
134
206
|
|
|
135
207
|
def call(env)
|
|
136
208
|
request = Util::Request.new(env)
|
|
@@ -145,14 +217,14 @@ module Logtail
|
|
|
145
217
|
end
|
|
146
218
|
|
|
147
219
|
elsif collapse_into_single_event?
|
|
148
|
-
request_start =
|
|
149
|
-
status, headers, body =
|
|
150
|
-
request_end =
|
|
220
|
+
request_start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
221
|
+
status, headers, body = call_app(env, request, request_start)
|
|
222
|
+
request_end = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
151
223
|
|
|
152
224
|
Config.instance.logger.info do
|
|
153
|
-
http_context = CurrentContext.fetch(:http)
|
|
154
|
-
content_length =
|
|
155
|
-
duration_ms = (request_end - request_start) * 1000.0
|
|
225
|
+
http_context = CurrentContext.fetch(:http, nil)
|
|
226
|
+
content_length = response_content_length(headers)
|
|
227
|
+
duration_ms = ((request_end - request_start) * 1000.0).round(1)
|
|
156
228
|
|
|
157
229
|
http_response = HTTPResponse.new(
|
|
158
230
|
content_length: content_length,
|
|
@@ -177,7 +249,7 @@ module Logtail
|
|
|
177
249
|
}
|
|
178
250
|
}
|
|
179
251
|
}
|
|
180
|
-
end
|
|
252
|
+
end rescue logging_failed($!)
|
|
181
253
|
|
|
182
254
|
[status, headers, body]
|
|
183
255
|
else
|
|
@@ -191,7 +263,7 @@ module Logtail
|
|
|
191
263
|
method: request.request_method,
|
|
192
264
|
path: request.path,
|
|
193
265
|
port: request.port,
|
|
194
|
-
query_string: Util::Encoding.force_utf8_encoding(request.query_string),
|
|
266
|
+
query_string: filter_query_string(Util::Encoding.force_utf8_encoding(request.query_string)),
|
|
195
267
|
request_id: request.request_id,
|
|
196
268
|
scheme: Util::Encoding.force_utf8_encoding(request.scheme),
|
|
197
269
|
)
|
|
@@ -214,16 +286,17 @@ module Logtail
|
|
|
214
286
|
}
|
|
215
287
|
}
|
|
216
288
|
}
|
|
217
|
-
end
|
|
289
|
+
end rescue logging_failed($!)
|
|
218
290
|
|
|
219
|
-
request_start =
|
|
220
|
-
status, headers, body =
|
|
221
|
-
request_end =
|
|
291
|
+
request_start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
292
|
+
status, headers, body = call_app(env, request, request_start)
|
|
293
|
+
request_end = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
222
294
|
|
|
223
295
|
Config.instance.logger.info do
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
296
|
+
# Only an Array body can be read twice, other bodies may stream to the server once
|
|
297
|
+
event_body = capture_response_body? && body.is_a?(Array) ? body.join : nil
|
|
298
|
+
content_length = response_content_length(headers)
|
|
299
|
+
duration_ms = ((request_end - request_start) * 1000.0).round(1)
|
|
227
300
|
|
|
228
301
|
http_response = HTTPResponse.new(
|
|
229
302
|
body: event_body,
|
|
@@ -248,7 +321,7 @@ module Logtail
|
|
|
248
321
|
}
|
|
249
322
|
}
|
|
250
323
|
}
|
|
251
|
-
end
|
|
324
|
+
end rescue logging_failed($!)
|
|
252
325
|
|
|
253
326
|
[status, headers, body]
|
|
254
327
|
end
|
|
@@ -275,14 +348,88 @@ module Logtail
|
|
|
275
348
|
end
|
|
276
349
|
end
|
|
277
350
|
|
|
351
|
+
# Calls the app. When it raises instead of returning a response, logs the response
|
|
352
|
+
# event with the status from {.status_for_exception} and re-raises the exception.
|
|
353
|
+
def call_app(env, request, request_start)
|
|
354
|
+
@app.call(env)
|
|
355
|
+
rescue Exception => exception
|
|
356
|
+
log_exception_response(request, exception, request_start)
|
|
357
|
+
raise exception
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
# Never raises, so that the exception of the app is the one that propagates.
|
|
361
|
+
def log_exception_response(request, exception, request_start)
|
|
362
|
+
request_end = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
363
|
+
|
|
364
|
+
Config.instance.logger.info do
|
|
365
|
+
duration_ms = ((request_end - request_start) * 1000.0).round(1)
|
|
366
|
+
|
|
367
|
+
http_response = HTTPResponse.new(
|
|
368
|
+
http_context: collapse_into_single_event? ? CurrentContext.fetch(:http, nil) : nil,
|
|
369
|
+
request_id: request.request_id,
|
|
370
|
+
status: status_for_exception(exception),
|
|
371
|
+
duration_ms: duration_ms,
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
{
|
|
375
|
+
message: http_response.message,
|
|
376
|
+
event: {
|
|
377
|
+
http_response_sent: {
|
|
378
|
+
body: http_response.body,
|
|
379
|
+
content_length: http_response.content_length,
|
|
380
|
+
headers_json: http_response.headers_json,
|
|
381
|
+
request_id: http_response.request_id,
|
|
382
|
+
service_name: http_response.service_name,
|
|
383
|
+
status: http_response.status,
|
|
384
|
+
duration_ms: http_response.duration_ms,
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
end
|
|
389
|
+
rescue Exception => e
|
|
390
|
+
Config.instance.debug { "Failed to log the response of a request that raised #{exception.class}: #{e.class}: #{e.message}" }
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
def status_for_exception(exception)
|
|
394
|
+
self.class.status_for_exception.call(exception)
|
|
395
|
+
rescue Exception => e
|
|
396
|
+
Config.instance.debug { "status_for_exception raised #{e.class}: #{e.message}, logging status 500" }
|
|
397
|
+
500
|
|
398
|
+
end
|
|
399
|
+
|
|
278
400
|
def filter_http_headers(headers)
|
|
279
401
|
headers.map do |name, value|
|
|
280
402
|
normalized_name = self.class.normalize_header_name(name)
|
|
281
403
|
is_filtered = self.class.http_header_filters.include?(normalized_name)
|
|
404
|
+
value = filter_url_query(value) if URL_HEADERS.include?(normalized_name)
|
|
282
405
|
[name, is_filtered ? "[FILTERED]" : value]
|
|
283
406
|
end.to_h
|
|
284
407
|
end
|
|
285
408
|
|
|
409
|
+
# Replaces the value of every parameter that {.query_string_filters} match with
|
|
410
|
+
# "[FILTERED]", leaving the rest of the query string unchanged.
|
|
411
|
+
def filter_query_string(query_string)
|
|
412
|
+
return query_string if !query_string.is_a?(String) || self.class.query_string_filters.empty?
|
|
413
|
+
|
|
414
|
+
query_string.gsub(QUERY_STRING_PARAMETER) do |parameter|
|
|
415
|
+
name = Regexp.last_match(1)
|
|
416
|
+
self.class.filter_query_string_parameter?(name) ? "#{name}=[FILTERED]" : parameter
|
|
417
|
+
end
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
# Filters the query of the URL in a Referer or Location header.
|
|
421
|
+
def filter_url_query(url)
|
|
422
|
+
return url if !url.is_a?(String) || !url.include?("?") || self.class.query_string_filters.empty?
|
|
423
|
+
|
|
424
|
+
Util::Encoding.force_utf8_encoding(url).sub(URL_QUERY) { "?#{filter_query_string(Regexp.last_match(1))}" }
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
# Rack 3 applications return "content-length", older ones usually "Content-Length".
|
|
428
|
+
def response_content_length(headers)
|
|
429
|
+
_name, value = headers.find { |name, _value| name.to_s.downcase == CONTENT_LENGTH_KEY }
|
|
430
|
+
safe_to_i(value)
|
|
431
|
+
end
|
|
432
|
+
|
|
286
433
|
def safe_to_i(val)
|
|
287
434
|
val.nil? ? nil : val.to_i
|
|
288
435
|
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
1
3
|
require "logtail-rack/util/encoding"
|
|
2
4
|
|
|
3
5
|
module Logtail
|
|
@@ -26,7 +28,7 @@ module Logtail
|
|
|
26
28
|
@service_name = attributes[:service_name]
|
|
27
29
|
|
|
28
30
|
if @headers
|
|
29
|
-
@headers_json = Util::Encoding.force_utf8_encoding(@headers)
|
|
31
|
+
@headers_json = ::JSON.generate(Util::Encoding.force_utf8_encoding(@headers))
|
|
30
32
|
end
|
|
31
33
|
end
|
|
32
34
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
1
3
|
require "logtail-rack/util/encoding"
|
|
2
4
|
|
|
3
5
|
module Logtail
|
|
@@ -21,7 +23,7 @@ module Logtail
|
|
|
21
23
|
@duration_ms = attributes[:duration_ms]
|
|
22
24
|
|
|
23
25
|
if @headers
|
|
24
|
-
@headers_json = Util::Encoding.force_utf8_encoding(@headers)
|
|
26
|
+
@headers_json = ::JSON.generate(Util::Encoding.force_utf8_encoding(@headers))
|
|
25
27
|
end
|
|
26
28
|
end
|
|
27
29
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
require "logtail/config"
|
|
2
|
+
|
|
1
3
|
module Logtail
|
|
2
4
|
module Integrations
|
|
3
5
|
module Rack
|
|
@@ -22,6 +24,18 @@ module Logtail
|
|
|
22
24
|
def initialize(app)
|
|
23
25
|
@app = app
|
|
24
26
|
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
# Logging must never fail the request, so an error raised while building or writing an event
|
|
30
|
+
# only goes to the debug logger. The middlewares call the logger themselves, so that the
|
|
31
|
+
# runtime context of the line points to them, and rescue the error with this method:
|
|
32
|
+
#
|
|
33
|
+
# Config.instance.logger.info do
|
|
34
|
+
# ...
|
|
35
|
+
# end rescue logging_failed($!)
|
|
36
|
+
def logging_failed(error)
|
|
37
|
+
Config.instance.debug { "#{self.class.name} could not log an event: #{error.inspect}\n\n#{error.backtrace}" }
|
|
38
|
+
end
|
|
25
39
|
end
|
|
26
40
|
end
|
|
27
41
|
end
|
|
@@ -25,7 +25,10 @@ module Logtail
|
|
|
25
25
|
if session = env['rack.session']
|
|
26
26
|
if session.respond_to?(:id)
|
|
27
27
|
Logtail::Config.instance.debug { "Rack env session detected, using id attribute" }
|
|
28
|
-
session.id
|
|
28
|
+
id = session.id
|
|
29
|
+
# Since Rack 1.6.12 and 2.0.8 a Rack::Session::SessionId, which MessagePack can't encode. Its public id
|
|
30
|
+
# is the session cookie of server-side stores, the private id a hash of it that can't be used as one.
|
|
31
|
+
id.respond_to?(:private_id) ? id.private_id : id
|
|
29
32
|
elsif session.respond_to?(:[])
|
|
30
33
|
Logtail::Config.instance.debug { "Rack env session detected, using the session_id key" }
|
|
31
34
|
session["session_id"]
|
|
@@ -39,6 +42,7 @@ module Logtail
|
|
|
39
42
|
nil
|
|
40
43
|
end
|
|
41
44
|
rescue Exception => e
|
|
45
|
+
Logtail::Config.instance.debug { "Could not obtain the session id: #{e.inspect}\n\n#{e.backtrace}" }
|
|
42
46
|
nil
|
|
43
47
|
end
|
|
44
48
|
end
|
|
@@ -93,6 +93,9 @@ module Logtail
|
|
|
93
93
|
Logtail::Config.instance.debug { "Could not locate any user data" }
|
|
94
94
|
nil
|
|
95
95
|
end
|
|
96
|
+
rescue StandardError => e
|
|
97
|
+
Logtail::Config.instance.debug { "Could not obtain the user context: #{e.inspect}\n\n#{e.backtrace}" }
|
|
98
|
+
nil
|
|
96
99
|
end
|
|
97
100
|
|
|
98
101
|
def get_user_object_hash(user)
|
|
@@ -13,6 +13,10 @@ module Logtail
|
|
|
13
13
|
REQUEST_ID_KEY_NAME2 = 'HTTP_X_REQUEST_ID'.freeze
|
|
14
14
|
|
|
15
15
|
def body_content
|
|
16
|
+
# Rack 3 allows a missing or non-rewindable input, and reading one that can't be rewound takes the body
|
|
17
|
+
# from the app
|
|
18
|
+
return nil unless body.respond_to?(:rewind)
|
|
19
|
+
|
|
16
20
|
content = body.read
|
|
17
21
|
body.rewind
|
|
18
22
|
content
|
data/lib/logtail-rack/version.rb
CHANGED