rails_api_logger 0.11.1 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0c60034742e7231e4a31d581dc71a67eb3826aa4702e0ff284c62ca74d477720
4
- data.tar.gz: fb275befe7ec034fbe2cb00c55916c1836887d1a39b7195433fa7587e2692be9
3
+ metadata.gz: 94143c1b37ced487b63c0f994a1ce48077f2e2dbd65b525430f7015670b6faf0
4
+ data.tar.gz: d387bed67f8534081603c3ac48d26afcf2ae2f19be190088146389e97958cefd
5
5
  SHA512:
6
- metadata.gz: 89ae643a486c5dda07badee17bfd01a226dda46ffdcc8d68638ea4ba36f3403439adb11cc1ee8b2cd5f91e59595ee34ebf199607d3d2a5a8569bafc39e3610f2
7
- data.tar.gz: da1059a513f47d89fee5cc48ed1431c8b9f0c685fc55e09931378b0b4a880440ed936bebc6133399da1cfa652f98dda6acb1f37a53f644baa13cf2e9581fdf21
6
+ metadata.gz: af01668c07dd98ff8a959a858241a3a910c0fbec4a181795b231cd00bf99f791629060f81e528e79ce664b81780c33b64f7f5216a60c2c47d3963a9e5b9c3453
7
+ data.tar.gz: 99a2c9c802084adca6687b91fb76c21c0708883443913619528e132630723c6db8353f45b5cd46f99a490b57ad0cca5a6db53acb1c722ea86a8c24301b3c29a8
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 3.2.9
1
+ 3.2.11
@@ -20,6 +20,7 @@ blocks:
20
20
  - checkout --use-cache
21
21
  - cache restore
22
22
  - bundle config set path 'vendor/bundle'
23
+ - bundle config set clean 'true'
23
24
  - bundle install -j 4
24
25
 
25
26
  - cache store
@@ -58,7 +59,7 @@ blocks:
58
59
  - sem-service start postgres 14
59
60
  - bin/rails db:create db:migrate
60
61
  - bundle exec rspec
61
- - name: tests postgres same db and separate target | rails 8.0
62
+ - name: tests postgres same db | rails 8.0
62
63
  env_vars:
63
64
  - name: SAME_DB
64
65
  value: "true"
@@ -70,7 +71,7 @@ blocks:
70
71
  - sem-service start postgres 14
71
72
  - bin/rails db:create db:migrate
72
73
  - bundle exec rspec
73
- - name: tests postgres separate db and separate target | rails 6.1
74
+ - name: tests postgres separate db and separate target | rails 8.1
74
75
  env_vars:
75
76
  - name: SAME_DB
76
77
  value: "false"
@@ -81,57 +82,49 @@ blocks:
81
82
  - name: RAILS_ENV
82
83
  value: test
83
84
  - name: BUNDLE_GEMFILE
84
- value: /home/semaphore/rails_api_logger/gemfiles/rails_6.1.gemfile
85
+ value: /home/semaphore/rails_api_logger/gemfiles/rails_8.1.gemfile
85
86
  commands:
86
87
  - sem-service start postgres 14
87
88
  - bin/rails db:create db:migrate
88
89
  - bundle exec rspec
89
- - name: tests postgres separate db and separate target | rails 7.0
90
+ - name: tests mysql separate db and separate target | rails 8.0
90
91
  env_vars:
91
92
  - name: SAME_DB
92
93
  value: "false"
93
94
  - name: SAME_TARGET
94
95
  value: "false"
95
96
  - name: TARGET_DB
96
- value: postgres
97
+ value: mysql
97
98
  - name: RAILS_ENV
98
99
  value: test
99
- - name: BUNDLE_GEMFILE
100
- value: /home/semaphore/rails_api_logger/gemfiles/rails_7.0.gemfile
101
100
  commands:
102
- - sem-service start postgres 14
101
+ - sem-service start mysql 8.0
103
102
  - bin/rails db:create db:migrate
104
103
  - bundle exec rspec
105
- - name: tests postgres separate db and separate target | rails 7.1
104
+ - name: tests mysql separate db and same target | rails 8.0
106
105
  env_vars:
107
106
  - name: SAME_DB
108
107
  value: "false"
109
108
  - name: SAME_TARGET
110
- value: "false"
109
+ value: "true"
111
110
  - name: TARGET_DB
112
- value: postgres
111
+ value: mysql
113
112
  - name: RAILS_ENV
114
113
  value: test
115
- - name: BUNDLE_GEMFILE
116
- value: /home/semaphore/rails_api_logger/gemfiles/rails_7.1.gemfile
117
114
  commands:
118
- - sem-service start postgres 14
115
+ - sem-service start mysql 8.0
119
116
  - bin/rails db:create db:migrate
120
117
  - bundle exec rspec
121
- - name: tests postgres separate db and separate target | rails 7.2
118
+ - name: tests mysql same db | rails 8.0
122
119
  env_vars:
123
120
  - name: SAME_DB
124
- value: "false"
125
- - name: SAME_TARGET
126
- value: "false"
121
+ value: "true"
127
122
  - name: TARGET_DB
128
- value: postgres
123
+ value: mysql
129
124
  - name: RAILS_ENV
130
125
  value: test
131
- - name: BUNDLE_GEMFILE
132
- value: /home/semaphore/rails_api_logger/gemfiles/rails_7.2.gemfile
133
126
  commands:
134
- - sem-service start postgres 14
127
+ - sem-service start mysql 8.0
135
128
  - bin/rails db:create db:migrate
136
129
  - bundle exec rspec
137
130
  - name: tests sqlite separate db and separate target | rails 8.0
data/Appraisals CHANGED
@@ -1,40 +1,15 @@
1
- appraise "rails-6.1" do
2
- gem "concurrent-ruby", "1.3.4"
3
- gem "rails", "~> 6.1.0"
4
- gem "rspec-rails", "~> 6.1.0"
5
- gem "sqlite3", "~> 1.7.3"
6
- gem "ammeter", "~> 1.1"
7
- gem "webmock", "~> 3.25.0"
8
- end
9
-
10
- appraise "rails-7.0" do
11
- gem "concurrent-ruby", "1.3.4"
12
- gem "rails", "~> 7.0.0"
13
- gem "rspec-rails", "~> 7.1.0"
14
- gem "sqlite3", "~> 1.7.3"
15
- gem "ammeter", "~> 1.1"
16
- gem "webmock", "~> 3.25.0"
17
- end
18
-
19
- appraise "rails-7.1" do
20
- gem "rails", "~> 7.1.0"
21
- gem "rspec-rails", "~> 7.1.0"
22
- gem "sqlite3", "~> 1.7.3"
23
- gem "ammeter", "~> 1.1"
24
- gem "webmock", "~> 3.25.0"
25
- end
26
-
27
- appraise "rails-7.2" do
28
- gem "rails", "~> 7.2.0"
1
+ appraise "rails-8.0" do
2
+ gem "rails", "~> 8.0.0"
29
3
  gem "rspec-rails", "~> 7.1.0"
30
- gem "sqlite3", "~> 1.7.3"
4
+ gem "sqlite3", "~> 2.1.0"
31
5
  gem "ammeter", "~> 1.1"
6
+ gem "puma"
32
7
  gem "webmock", "~> 3.25.0"
33
8
  end
34
9
 
35
- appraise "rails-8.0" do
36
- gem "rails", "~> 8.0.0"
37
- gem "rspec-rails", "~> 7.1.0"
10
+ appraise "rails-8.1" do
11
+ gem "rails", "~> 8.1.0"
12
+ gem "rspec-rails", "~> 8.0"
38
13
  gem "sqlite3", "~> 2.1.0"
39
14
  gem "ammeter", "~> 1.1"
40
15
  gem "puma"
data/CHANGELOG.md CHANGED
@@ -1,10 +1,46 @@
1
+ # 1.0.0
2
+
3
+ * **Breaking:** Drop support for Rails < 8.0 and Ruby < 3.2.
4
+ * Logging never breaks the host app anymore. When the log can't be written (the logging database is down, a
5
+ validation fails, a column is too small...), the error is reported to `Rails.error` with source
6
+ `rails_api_logger` and the request goes on without a log. Before, the app answered 500, an outbound request was
7
+ not sent, or a successful outbound request raised. The errors of your own requests are still raised as before.
8
+ * Fix a crash (500 in the host app) when a request body is not valid UTF-8, for example a file upload
9
+ ([#6](https://github.com/renuo/rails_api_logger/issues/6)). Request and response bodies that are not valid UTF-8
10
+ are now stored as `"[Binary: <size> bytes]"`. Before, such a response body was stored as `nil`.
11
+ * The install migration creates `started_at` and `ended_at` as `datetime` instead of `timestamp`. On MySQL 8,
12
+ `timestamp` columns have no fractional seconds, so the duration of a request was rounded to whole seconds.
13
+ Existing MySQL apps can migrate their columns with:
14
+
15
+ ```ruby
16
+ %i[inbound_request_logs outbound_request_logs].each do |table|
17
+ change_column table, :started_at, :datetime, precision: 6
18
+ change_column table, :ended_at, :datetime, precision: 6
19
+ end
20
+ ```
21
+
22
+ # 0.12.0
23
+
24
+ * Add `client_reference` column to both `inbound_request_logs` and `outbound_request_logs`. This is a free-form
25
+ indexed string you can set on a log entry to correlate it with any external identifier (similar in spirit to
26
+ `loggable`, but without a polymorphic association).
27
+ * For outbound requests, pass it to the logger: `RailsApiLogger::Logger.new(client_reference: "abc-123")`.
28
+ * For inbound requests, call `attach_inbound_request_client_reference("abc-123")` from your controller.
29
+
30
+ To upgrade an existing app, run:
31
+
32
+ ```
33
+ bin/rails g rails_api_logger:upgrade_client_reference
34
+ bin/rails db:migrate
35
+ ```
36
+
1
37
  # 0.11.1
2
38
 
3
39
  * Remove deprecated "require_dependency"
4
40
 
5
41
  # 0.11.0
6
42
 
7
- * Fix stack trace to deep in webrick
43
+ * Fix stack trace too deep in webrick
8
44
  * Fixes for Rails < 7.1
9
45
  * Support custom primary key types in migration template
10
46
 
data/README.md CHANGED
@@ -23,7 +23,7 @@ This gem creates two database tables to log the following information:
23
23
 
24
24
  ## Installation
25
25
 
26
- Add this line to your application's Gemfile:
26
+ Requires Rails 8.0 or newer. Add this line to your application's Gemfile:
27
27
 
28
28
  ```ruby
29
29
  gem 'rails_api_logger'
@@ -105,6 +105,11 @@ end
105
105
 
106
106
  This will guarantee that the log is always persisted, even in case of errors.
107
107
 
108
+ ### Errors while logging
109
+
110
+ Logging never breaks your app. If a log can't be written, for example because the logging database is down,
111
+ the error is reported to `Rails.error` with source `rails_api_logger` and the request goes on without a log.
112
+
108
113
  ### Database Transactions Caveats
109
114
 
110
115
  If you log your outbound requests inside of parent app transactions, your logs will not be persisted if
@@ -146,6 +151,9 @@ config.middleware.insert_before Rails::Rack::Logger, RailsApiLogger::Middleware,
146
151
  skip_response_body_regexp: /api\/letters/
147
152
  ```
148
153
 
154
+ Bodies that are not valid UTF-8, such as file uploads, images or PDFs, are not stored.
155
+ The log keeps only their size, for example `"[Binary: 2048 bytes]"`.
156
+
149
157
  In the implementation of your API, you can call any time `attach_inbound_request_loggable(model)`
150
158
  to attach an already persisted model to the log record.
151
159
 
@@ -174,6 +182,29 @@ to be able to access the inbound logs attached to the model.
174
182
 
175
183
  You also have `has_many_outbound_request_logs` and `has_many_request_logs` that includes both.
176
184
 
185
+ ### Client reference
186
+
187
+ Both `inbound_request_logs` and `outbound_request_logs` have a `client_reference` column: a free-form indexed
188
+ string you can set on a log entry to correlate it with any external identifier (e.g. a request id coming from an
189
+ upstream system, an idempotency key, a tenant slug). It is independent from the `loggable` polymorphic association.
190
+
191
+ For inbound requests, attach it from your controller:
192
+
193
+ ```ruby
194
+ def create
195
+ attach_inbound_request_client_reference(request.headers["X-Request-Id"])
196
+ # ...
197
+ end
198
+ ```
199
+
200
+ For outbound requests, pass it when instantiating the logger:
201
+
202
+ ```ruby
203
+ RailsApiLogger::Logger.new(client_reference: "ref-abc").call(uri, request) do
204
+ http.request(request)
205
+ end
206
+ ```
207
+
177
208
  ## RailsAdmin integration
178
209
 
179
210
  We provide here some code samples to integrate the models in [RailsAdmin](https://github.com/sferik/rails_admin).
@@ -227,13 +258,13 @@ end
227
258
 
228
259
  ## Development
229
260
 
230
- We use Appraisals to un on different Rails versions. This is a run example:
261
+ We use Appraisals to run on different Rails versions. This is a run example:
231
262
 
232
263
  ```bash
233
264
  export SAME_TARGET=false
234
265
  export SAME_DB=false
235
266
  export TARGET_DB=postgres
236
- export BUNDLE_GEMFILE=/Users/alessandrorodi/RenuoWorkspace/rails_api_logger/gemfiles/rails_6.1.gemfile
267
+ export BUNDLE_GEMFILE=/Users/alessandrorodi/RenuoWorkspace/rails_api_logger/gemfiles/rails_8.0.gemfile
237
268
  bundle exec rails db:create db:migrate
238
269
  bundle exec rspec
239
270
  ```
@@ -5,6 +5,12 @@ module InboundRequestsLogger
5
5
 
6
6
  def attach_inbound_request_loggable(loggable)
7
7
  return unless request.env["INBOUND_REQUEST_LOG"].present?
8
- request.env["INBOUND_REQUEST_LOG"].update(loggable: loggable) if loggable&.persisted?
8
+ return unless loggable&.persisted?
9
+ RailsApiLogger.safely { request.env["INBOUND_REQUEST_LOG"].update(loggable: loggable) }
10
+ end
11
+
12
+ def attach_inbound_request_client_reference(client_reference)
13
+ return unless request.env["INBOUND_REQUEST_LOG"].present?
14
+ RailsApiLogger.safely { request.env["INBOUND_REQUEST_LOG"].update(client_reference: client_reference) }
9
15
  end
10
16
  end
@@ -19,22 +19,26 @@ module RailsApiLogger
19
19
  request = ActionDispatch::Request.new(env)
20
20
  logging = log?(env, request)
21
21
  if logging
22
- env["INBOUND_REQUEST_LOG"] = InboundRequestLog.from_request(request, skip_request_body: skip_request_body?(env))
22
+ env["INBOUND_REQUEST_LOG"] = RailsApiLogger.safely do
23
+ InboundRequestLog.from_request(request, skip_request_body: skip_request_body?(env))
24
+ end
23
25
  request.body.rewind if request.body.respond_to?(:rewind)
24
26
  end
25
27
  status, headers, body = @app.call(env)
26
- if logging
27
- updates = {response_code: status, ended_at: Time.current}
28
- updates[:response_body] = if skip_response_body?(env)
29
- "[Skipped]"
30
- else
31
- parsed_body(body)
32
- end
33
- # this usually works. let's be optimistic.
34
- begin
35
- env["INBOUND_REQUEST_LOG"].update_columns(updates)
36
- rescue JSON::GeneratorError => _e # this can be raised by activerecord if the string is not UTF-8.
37
- env["INBOUND_REQUEST_LOG"].update_columns(updates.except(:response_body))
28
+ if logging && env["INBOUND_REQUEST_LOG"]
29
+ RailsApiLogger.safely do
30
+ updates = {response_code: status, ended_at: Time.current}
31
+ updates[:response_body] = if skip_response_body?(env)
32
+ "[Skipped]"
33
+ else
34
+ parsed_body(body)
35
+ end
36
+ # this usually works. let's be optimistic.
37
+ begin
38
+ env["INBOUND_REQUEST_LOG"].update_columns(updates)
39
+ rescue JSON::GeneratorError => _e # this can be raised by activerecord if the string is not UTF-8.
40
+ env["INBOUND_REQUEST_LOG"].update_columns(updates.except(:response_body))
41
+ end
38
42
  end
39
43
  end
40
44
  [status, headers, body]
@@ -60,7 +64,6 @@ module RailsApiLogger
60
64
  end
61
65
 
62
66
  def parsed_body(body)
63
- return unless body.present?
64
67
  parsable_body = if body.respond_to?(:to_ary)
65
68
  body.to_ary[0]
66
69
  elsif body.respond_to?(:body)
@@ -69,9 +72,7 @@ module RailsApiLogger
69
72
  body
70
73
  end
71
74
 
72
- JSON.parse(parsable_body)
73
- rescue JSON::ParserError, ArgumentError
74
- parsable_body
75
+ RequestLog.parse_body(parsable_body).presence
75
76
  end
76
77
 
77
78
  def request_with_state_change?(request)
@@ -1,22 +1,30 @@
1
1
  module RailsApiLogger
2
2
  class Logger
3
- def initialize(loggable = nil, skip_request_body: false, skip_response_body: false)
3
+ def initialize(loggable = nil, client_reference: nil, skip_request_body: false, skip_response_body: false)
4
4
  @loggable = loggable
5
+ @client_reference = client_reference
5
6
  @skip_request_body = skip_request_body
6
7
  @skip_response_body = skip_response_body
7
8
  end
8
9
 
9
10
  def call(url, request)
10
- log = OutboundRequestLog.from_request(request, loggable: @loggable, skip_request_body: @skip_request_body)
11
+ log = RailsApiLogger.safely do
12
+ OutboundRequestLog.from_request(request, loggable: @loggable, client_reference: @client_reference,
13
+ skip_request_body: @skip_request_body)
14
+ end
11
15
  yield.tap do |response|
12
- log.from_response(response, skip_response_body: @skip_response_body)
16
+ RailsApiLogger.safely { log&.from_response(response, skip_response_body: @skip_response_body) }
13
17
  end
14
18
  rescue => e
15
19
  log.response_body = {error: e.message} if log
16
20
  raise
17
21
  ensure
18
- log.ended_at = Time.current
19
- log.save!
22
+ if log
23
+ RailsApiLogger.safely do
24
+ log.ended_at = Time.current
25
+ log.save!
26
+ end
27
+ end
20
28
  end
21
29
  end
22
30
  end
@@ -4,13 +4,8 @@ module RailsApiLogger
4
4
 
5
5
  connects_to(**RailsApiLogger.connects_to) if RailsApiLogger.connects_to
6
6
 
7
- if Gem::Version.new(Rails.version) >= Gem::Version.new("7.1")
8
- serialize :request_body, coder: JSON
9
- serialize :response_body, coder: JSON
10
- else
11
- serialize :request_body, JSON
12
- serialize :response_body, JSON
13
- end
7
+ serialize :request_body, coder: JSON
8
+ serialize :response_body, coder: JSON
14
9
 
15
10
  belongs_to :loggable, optional: true, polymorphic: true
16
11
 
@@ -19,19 +14,29 @@ module RailsApiLogger
19
14
  validates :method, presence: true
20
15
  validates :path, presence: true
21
16
 
22
- def self.from_request(request, loggable: nil, skip_request_body: false)
17
+ def self.from_request(request, loggable: nil, client_reference: nil, skip_request_body: false)
23
18
  if skip_request_body
24
19
  body = "[Skipped]"
25
20
  else
26
21
  request_body = (request.body.respond_to?(:read) ? request.body.read : request.body)
27
- body = request_body&.dup&.force_encoding("UTF-8")
28
- begin
29
- body = JSON.parse(body) if body.present?
30
- rescue JSON::ParserError
31
- body
32
- end
22
+ body = parse_body(request_body)
33
23
  end
34
- create(path: request.path, request_body: body, method: request.method, started_at: Time.current, loggable: loggable)
24
+ create(path: request.path, request_body: body, method: request.method, started_at: Time.current,
25
+ loggable: loggable, client_reference: client_reference)
26
+ end
27
+
28
+ # Bodies that are not valid UTF-8 (file uploads, images, PDFs...) can't be stored as text,
29
+ # so only their size is logged.
30
+ def self.parse_body(body)
31
+ return body unless body.is_a?(String)
32
+
33
+ body = body.dup.force_encoding("UTF-8")
34
+ return "[Binary: #{body.bytesize} bytes]" unless body.valid_encoding?
35
+ return body if body.blank?
36
+
37
+ JSON.parse(body)
38
+ rescue JSON::ParserError
39
+ body
35
40
  end
36
41
 
37
42
  def from_response(response, skip_response_body: false)
@@ -73,13 +78,7 @@ module RailsApiLogger
73
78
  private
74
79
 
75
80
  def manipulate_body(body)
76
- body_duplicate = body&.dup&.force_encoding("UTF-8")
77
- begin
78
- body_duplicate = JSON.parse(body_duplicate) if body_duplicate.present?
79
- rescue JSON::ParserError
80
- body_duplicate
81
- end
82
- body_duplicate
81
+ self.class.parse_body(body)
83
82
  end
84
83
  end
85
84
  end
@@ -1,11 +1,11 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- rails_api_logger (0.11.0)
5
- activejob (>= 6.0)
6
- activerecord (>= 6.0)
4
+ rails_api_logger (1.0.0)
5
+ activejob (>= 8.0)
6
+ activerecord (>= 8.0)
7
7
  nokogiri
8
- railties (>= 6.0)
8
+ railties (>= 8.0)
9
9
  zeitwerk (>= 2.0.0)
10
10
 
11
11
  GEM
@@ -263,7 +263,7 @@ GEM
263
263
  unicode-display_width (3.1.4)
264
264
  unicode-emoji (~> 4.0, >= 4.0.4)
265
265
  unicode-emoji (4.0.4)
266
- uri (1.0.3)
266
+ uri (1.1.2)
267
267
  useragent (0.16.11)
268
268
  webmock (3.25.1)
269
269
  addressable (>= 2.8.0)
@@ -295,4 +295,4 @@ DEPENDENCIES
295
295
  webmock (~> 3.25.0)
296
296
 
297
297
  BUNDLED WITH
298
- 2.5.4
298
+ 4.0.20
@@ -2,10 +2,11 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- gem "rails", "~> 7.1.0"
6
- gem "rspec-rails", "~> 7.1.0"
7
- gem "sqlite3", "~> 1.7.3"
5
+ gem "rails", "~> 8.1.0"
6
+ gem "rspec-rails", "~> 8.0"
7
+ gem "sqlite3", "~> 2.1.0"
8
8
  gem "ammeter", "~> 1.1"
9
+ gem "puma"
9
10
  gem "webmock", "~> 3.25.0"
10
11
 
11
12
  gemspec path: "../"