rails_api_logger 0.9.0 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4f1fb1fae010aa6cf4bdda941fd6ad2ce3ac5583faa690d23680194d2f3eb991
4
- data.tar.gz: 0dd85cd48972098af775d7a5feec4d17b8aaaa51641e9a4405244d8ccdd12644
3
+ metadata.gz: 2fda8a1c76ae8f88f8753f6a24b0e794031f2fe699cf517b2bdc0dc4a9492047
4
+ data.tar.gz: 6435707109046e2f3444290e8f1c18381a2bbd3b9558602198752c077de3f7ed
5
5
  SHA512:
6
- metadata.gz: 4d310658b9bbae0d5406a8df1d7a85e11d161db8405ad970cb422fbd9ad38d2a95c251a8ba3f6acbe30089290b61d94ed8c8cd1b0e601fbbea6f8d2a111b7ae0
7
- data.tar.gz: c3c29eaf30dbbb4548b3527ac7220193135a4ca0eb69b5f17b12e0ef88516b8aa91c1b8367dacd4a4ee5988c4722ba493c5e98a28ba7b5c61645db971c36bcdb
6
+ metadata.gz: d8660e1388c86679bc3be3004049b783e96eef9bf18c6c61fb4d885fdd9cf4f2408bb131adf35193652687a3f37b2a7411e3e7953eb919fc468ab99a165e147c
7
+ data.tar.gz: 53bbe981627e011159acaf23468f1b1bf00b6171ae9f50fc6d577d3461937a871e9955f29e391fdb38027902fd6db42a6a9b20614d6da21d50bacbf0bb2cc980
data/.gitignore CHANGED
@@ -6,6 +6,10 @@
6
6
  /pkg/
7
7
  /spec/reports/
8
8
  /tmp/
9
+ /spec/dummy/log
10
+ /spec/dummy/tmp
11
+ /spec/dummy/db/*.sqlite3*
12
+ .env
9
13
 
10
14
  # rspec failure tracking
11
15
  .rspec_status
@@ -21,12 +21,63 @@ blocks:
21
21
  - cache restore
22
22
  - bundle config set path 'vendor/bundle'
23
23
  - bundle install -j 4
24
- - sem-service start postgres 14 --username=semaphore
24
+
25
25
  - cache store
26
26
  jobs:
27
- - name: tests
27
+ - name: linter
28
28
  commands:
29
29
  - bundle exec standardrb
30
+ - name: tests sqlite separate db
31
+ env_vars:
32
+ - name: TARGET_DB
33
+ value: sqlite
34
+ - name: RAILS_ENV
35
+ value: test
36
+ commands:
37
+ - bundle exec rspec
38
+ - name: tests postgres separate db
39
+ env_vars:
40
+ - name: TARGET_DB
41
+ value: postgres
42
+ - name: RAILS_ENV
43
+ value: test
44
+ commands:
45
+ - sem-service start postgres 14
46
+ - bin/rails db:create db:schema:load
47
+ - bundle exec rspec
48
+ - name: tests sqlite same db
49
+ env_vars:
50
+ - name: SAME_DB
51
+ value: "true"
52
+ - name: TARGET_DB
53
+ value: sqlite
54
+ - name: RAILS_ENV
55
+ value: test
56
+ commands:
57
+ - bundle exec rspec
58
+ - name: tests postgres same db
59
+ env_vars:
60
+ - name: SAME_DB
61
+ value: "true"
62
+ - name: TARGET_DB
63
+ value: postgres
64
+ - name: RAILS_ENV
65
+ value: test
66
+ commands:
67
+ - sem-service start postgres 14
68
+ - bin/rails db:create db:schema:load
69
+ - bundle exec rspec
70
+ - name: tests postgres same target
71
+ env_vars:
72
+ - name: SAME_TARGET
73
+ value: "true"
74
+ - name: TARGET_DB
75
+ value: postgres
76
+ - name: RAILS_ENV
77
+ value: test
78
+ commands:
79
+ - sem-service start postgres 14
80
+ - bin/rails db:create db:schema:load
30
81
  - bundle exec rspec
31
82
  promotions:
32
83
  - name: main
data/CHANGELOG.md CHANGED
@@ -1,41 +1,90 @@
1
+ # 0.10.0
2
+
3
+ **BREAKING CHANGES**
4
+
5
+ This version contains many breaking changes. Consider this when upgrading:
6
+
7
+ * Replace calls to `RailsApiLogger.new` with `RailsApiLogger::Logger.new`. More in general the logger has been renamed.
8
+ * `InboundRequestLog` has been renamed to `RailsApiLogger::InboundRequestLog`. Table name did not change.
9
+ * `OutboundRequestLog` has been renamed to `RailsApiLogger::OutboundRequestLog`. Table name did not change.
10
+ * If you had `has_many :inbound_request_logs` or `has_many :outbound_request_logs` defined, this will break. There's
11
+ now [three methods](app/models/rails_api_logger/loggable.rb) you can use on your model.
12
+ * `InboundRequestsLoggerMiddleware` has been renamed to `RailsApiLogger::Middleware`
13
+
14
+ > Do the changes above and then continue with the following steps if you want to connect rails_api_logger to a different
15
+ database:
16
+
17
+ * Specify a database called `api_logger`. [Check here](spec/dummy/config/database.yml) for an example.
18
+ * Check that everything still works (also in production!) with the new configuration.
19
+ * Add the migrations in the right folder to create again the necessary tables. Run the migrations that will generate a
20
+ new schema file.
21
+ * Release and do the same in production. Adapt your build/release steps if you need.
22
+
23
+ * Add the following line into `production.rb`:
24
+ `config.rails_api_logger.connects_to = { database: { writing: :api_logger } }` if you want to point to a new database.
25
+
26
+ > If you are not on SQLite you can point also `api_logger` database to the current database you have, so you benefit from
27
+ isolated transactions but don't need to create a new database or migrate data.
28
+
29
+ ### List of changes in this version:
30
+
31
+ * Namespace correctly. Renamed all classes
32
+ * Added tests with a dummy app
33
+ * Use a separate database connection configuration to isolate transactions
34
+ * I acknowledge that there might be issues on mysql. I don't use it so I won't fix them, but PR are welcome.
35
+ * Added `host_regexp` option to the middleware.
36
+
1
37
  # 0.9.0
2
- * Add option skip_request_body to skip the request body. Use this option when you don't want to persist the request body. `[Skipped]` will be persisted instead.
38
+
39
+ * Add option skip_request_body to skip the request body. Use this option when you don't want to persist the request
40
+ body. `[Skipped]` will be persisted instead.
3
41
  * Add option skip_request_body_regexp to skip logging the body of requests matching a regexp.
4
42
  * Renamed the option skip_body into skip_response_body. This is a breaking change!
5
43
  * Renamed the option skip_body_regexp into skip_response_body_regexp. This is a breaking change!
6
44
 
7
45
  # 0.8.1
46
+
8
47
  * Fix Rails 7.1 warnings.
9
48
 
10
49
  # 0.8.0
11
- * Add option skip_body to skip the body for request responses. Use this option when you don't want to persist the response body. `[Skipped]` will be persisted instead. This is not a breaking change.
50
+
51
+ * Add option skip_body to skip the body for request responses. Use this option when you don't want to persist the
52
+ response body. `[Skipped]` will be persisted instead. This is not a breaking change.
12
53
 
13
54
  # 0.7.0
55
+
14
56
  * Fix an issue in the middleware where the request body was not read correctly if there were encoding issues.
15
57
  * Improved documentation about outboud request logging.
16
58
  * Add option skip_body_regexp to skip logging the body of requests matching a regexp.
17
59
 
18
60
  # 0.6.3
61
+
19
62
  * Fix the CHANGELOG path in gemspec.
20
63
 
21
64
  # 0.6.2
65
+
22
66
  * Fixes Zeitwerk warning.
23
67
 
24
68
  # 0.6.1
69
+
25
70
  * Fixes the loading of concern into controllers.
26
71
 
27
72
  # 0.6.0
73
+
28
74
  * Fixes an important concurrency issue by removing instance variables in the rack middleware.
29
75
 
30
76
  # 0.5.0
77
+
31
78
  * Started using Zeitwerk.
32
79
  * Removed RailsAdmin specific code.
33
80
  * Improved RailsApiLogger class.
34
81
 
35
82
  # 0.4.1
83
+
36
84
  * Fixed the `.failed` scope.
37
85
 
38
86
  # 0.4.0
87
+
39
88
  * Added `started_at`, `ended_at` and `duration` methods.
40
89
 
41
90
  Migrate your tables with:
@@ -47,12 +96,14 @@ add_column :outbound_request_logs, :started_at, :timestamp
47
96
  add_column :outbound_request_logs, :ended_at, :timestamp
48
97
  ```
49
98
 
50
-
51
99
  # 0.3.0
100
+
52
101
  * Added `formatted_request_body` and `formatted_response_body` methods.
53
102
 
54
103
  # 0.2.0
104
+
55
105
  * Switch to a middleware solution.
56
106
 
57
107
  # 0.1.0
108
+
58
109
  * Initial release.
data/README.md CHANGED
@@ -1,23 +1,23 @@
1
1
  # Rails API Logger
2
2
 
3
- The simplest way to log API requests of your Rails application in your database.
3
+ The simplest way to log API requests in your database.
4
4
 
5
5
  The Rails API logger gem introduces a set of tools to log and debug API requests.
6
+
6
7
  It works on two sides:
7
8
 
8
9
  * **Inbound requests**: API exposed by your application
9
- * **Outbound requests**: API invoked by your application
10
+ * **Outbound requests**: API invoked by your application
10
11
 
11
- This gem has been extracted from various Renuo projects, where we implemented this
12
- technique multiple times successfully.
12
+ This gem has been extracted from various [Renuo](https://www.renuo.ch) projects.
13
13
 
14
14
  This gem creates two database tables to log the following information:
15
15
 
16
16
  * **path** the path/url invoked
17
17
  * **method** the method used to invoke the API (get, post, put, etc...)
18
18
  * **request_body** what was included in the request body
19
- * **response_body** what was included in the response body
20
- * **response_code** the HTTP response code of the request
19
+ * **response_body** what was included in the response body
20
+ * **response_code** the HTTP response code of the request
21
21
  * **started_at** when the request started
22
22
  * **ended_at** when the request finished
23
23
 
@@ -33,14 +33,31 @@ And then execute:
33
33
 
34
34
  ```bash
35
35
  bundle install
36
- spring stop # if it's running. otherwise it does not see the new generator
37
- bundle exec rails generate rails_api_logger:install
38
- bundle exec rails db:migrate
36
+ bin/rails g rails_api_logger:install
37
+ bin/rails db:migrate
39
38
  ```
40
39
 
41
40
  This will generate two tables `inbound_request_logs` and `outbound_request_logs`.
42
41
  These tables will contain the logs.
43
42
 
43
+ ## Ensure logging of data
44
+
45
+ RailsApiLogger can use a separate database, to ensure that the logs are written in the database even if a
46
+ surrounding database transaction is rolled back.
47
+
48
+ Make sure to add the following in your `config/environments/production.rb`:
49
+
50
+ ```ruby
51
+ config.rails_api_logger.connects_to = { database: { writing: :api_logger } }
52
+ ```
53
+
54
+ and [configure a new database](spec/dummy/config/database.yml) accordingly.
55
+
56
+ > ⚠️ If you skip this step, rails_api_logger will use your primary database but a rollback will also rollback the
57
+ > writing of logs
58
+ > If you are not on SQLite you can point also `api_logger` to the same database! By doing so you can use a single
59
+ > database but still guarantee the writing of logs in an isolated transaction.
60
+
44
61
  ## Log Outbound Requests
45
62
 
46
63
  Given an outbound request in the following format:
@@ -59,7 +76,7 @@ uri = URI('http://example.com/some_path?query=string')
59
76
  http = Net::HTTP.start(uri.host, uri.port)
60
77
  request = Net::HTTP::Get.new(uri)
61
78
 
62
- log = OutboundRequestLog.from_request(request)
79
+ log = RailsApiLogger::OutboundRequestLog.from_request(request)
63
80
 
64
81
  response = http.request(request)
65
82
 
@@ -76,7 +93,7 @@ request = Net::HTTP::Post.new(uri)
76
93
  request.body = { answer: 42 }.to_json
77
94
  request.content_type = 'application/json'
78
95
 
79
- response = RailsApiLogger.new.call(nil, request) do
96
+ response = RailsApiLogger::Logger.new.call(nil, request) do
80
97
  Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(request) }
81
98
  end
82
99
  ```
@@ -86,25 +103,7 @@ This will guarantee that the log is always persisted, even in case of errors.
86
103
  ### Database Transactions Caveats
87
104
 
88
105
  If you log your outbound requests inside of parent app transactions, your logs will not be persisted if
89
- the transaction is rolled-back. You can circumvent that by opening another database connection
90
- to the same (or another database if you're into that stuff) when logging.
91
-
92
- ```
93
- # config/initializers/outbound_request_log_patch.rb
94
-
95
- module OutboundRequestLogTransactionPatch
96
- extend ActiveSupport::Concern
97
-
98
- included do
99
- connects_to database: { writing: :primary, reading: :primary }
100
- end
101
- end
102
-
103
- OutboundRequestLog.include(OutboundRequestLogTransactionPatch)
104
- ```
105
-
106
- You can also log the request in a separate thread to provoke the checkout of a separate database connection.
107
- Have a look at [this example here](https://github.com/renuo/rails_api_logger/blob/28d4ced88fea5a5f4fd72f5a1db42ad4734eb547/spec/outbound_request_log_spec.rb#L28-L30).
106
+ the transaction is rolled-back. Use a separate database to prevent this.
108
107
 
109
108
  ## Log Inbound Requests
110
109
 
@@ -112,36 +111,41 @@ If you are exposing some API you might be interested in logging the requests you
112
111
  You can do so by adding this middleware in `config/application.rb`
113
112
 
114
113
  ```ruby
115
- config.middleware.insert_before Rails::Rack::Logger, InboundRequestsLoggerMiddleware
114
+ config.middleware.insert_before Rails::Rack::Logger, RailsApiLogger::Middleware
116
115
  ```
117
116
 
118
117
  this will by default only log requests that have an impact in your system (POST, PUT, and PATCH calls).
119
118
  If you want to log all requests (also GET ones) use
120
119
 
121
120
  ```ruby
122
- config.middleware.insert_before Rails::Rack::Logger, InboundRequestsLoggerMiddleware, only_state_change: false
121
+ config.middleware.insert_before Rails::Rack::Logger, RailsApiLogger::Middleware, only_state_change: false
123
122
  ```
124
123
 
125
124
  If you want to log only requests on a certain path, you can pass a regular expression:
126
125
 
127
126
  ```ruby
128
- config.middleware.insert_before Rails::Rack::Logger, InboundRequestsLoggerMiddleware, path_regexp: /api/
127
+ config.middleware.insert_before Rails::Rack::Logger, RailsApiLogger::Middleware, path_regexp: /api/
129
128
  ```
130
129
 
131
- If you want to skip logging the response or request body of certain requests, you can pass a regular expression:
130
+ If you want to log only requests on a certain host, you can also use a regular expression:
132
131
 
133
132
  ```ruby
134
- config.middleware.insert_before Rails::Rack::Logger, InboundRequestsLoggerMiddleware,
135
- skip_request_body_regexp: /api/books/,
136
- skip_response_body_regexp: /api/letters/
133
+ config.middleware.insert_before Rails::Rack::Logger, RailsApiLogger::Middleware, host_regexp: /api.example.com/
137
134
  ```
138
135
 
136
+ If you want to skip logging the response or request body of certain requests, you can pass a regular expression:
137
+
138
+ ```ruby
139
+ config.middleware.insert_before Rails::Rack::Logger, RailsApiLogger::Middleware,
140
+ skip_request_body_regexp: /api\/books/,
141
+ skip_response_body_regexp: /api\/letters/
142
+ ```
139
143
 
140
144
  In the implementation of your API, you can call any time `attach_inbound_request_loggable(model)`
141
145
  to attach an already persisted model to the log record.
142
146
 
143
-
144
147
  For example:
148
+
145
149
  ```ruby
146
150
 
147
151
  def create
@@ -158,18 +162,21 @@ end
158
162
  in the User model you can define:
159
163
 
160
164
  ```ruby
161
- has_many :inbound_request_logs, inverse_of: :loggable, dependent: :destroy, as: :loggable
165
+ has_many_inbound_request_logs
162
166
  ```
163
167
 
164
- to be able to access the logs attached to the model.
168
+ to be able to access the inbound logs attached to the model.
169
+
170
+ You also have `has_many_outbound_request_logs` and `has_many_request_logs` that includes both.
165
171
 
166
172
  ## RailsAdmin integration
167
173
 
168
174
  We provide here some code samples to integrate the models in [RailsAdmin](https://github.com/sferik/rails_admin).
169
175
 
170
- This configuration will give you some nice views, and searches to work with the logs efficiently.
176
+ This configuration will give you some nice views, and searches to work with the logs efficiently.
177
+
171
178
  ```ruby
172
- %w[InboundRequestLog OutboundRequestLog].each do |logging_model|
179
+ %w[RailsApiLogger::InboundRequestLog RailsApiLogger::OutboundRequestLog].each do |logging_model|
173
180
  config.model logging_model do
174
181
  list do
175
182
  filters %i[method path response_code request_body response_body created_at]
@@ -204,24 +211,22 @@ This configuration will give you some nice views, and searches to work with the
204
211
  end
205
212
  ```
206
213
 
207
-
208
214
  ## Development
209
215
 
210
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
216
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can
217
+ also run `bin/console` for an interactive prompt that will allow you to experiment.
211
218
 
212
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
219
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the
220
+ version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version,
221
+ push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
213
222
 
214
223
  ## Contributing
215
224
 
216
- Bug reports and pull requests are welcome on GitHub at https://github.com/renuo/rails_api_logger.
217
- This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to
218
- the [code of conduct](https://github.com/renuo/rails_api_logger/blob/main/CODE_OF_CONDUCT.md).
225
+ Bug reports and pull requests are welcome on GitHub at https://github.com/renuo/rails_api_logger.
226
+ This project is intended to be a safe, welcoming space for collaboration.
227
+
228
+ Try to be a decent human being while interacting with other people.
219
229
 
220
230
  ## License
221
231
 
222
232
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
223
-
224
- ## Code of Conduct
225
-
226
- Everyone interacting in the RailsApiLogger project's codebases, issue trackers, chat rooms and mailing lists is
227
- expected to follow the [code of conduct](https://github.com/renuo/rails_api_logger/blob/main/CODE_OF_CONDUCT.md).
data/Rakefile CHANGED
@@ -1,6 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/setup"
1
4
  require "bundler/gem_tasks"
2
- require "rspec/core/rake_task"
5
+ require "rake/testtask"
3
6
 
4
- RSpec::Core::RakeTask.new(:spec)
7
+ APP_RAKEFILE = File.expand_path("spec/dummy/Rakefile", __dir__)
8
+ load "rails/tasks/engine.rake"
9
+ load "rails/tasks/statistics.rake"
5
10
 
6
11
  task default: :spec
@@ -0,0 +1,80 @@
1
+ module RailsApiLogger
2
+ class Middleware
3
+ attr_accessor :only_state_change, :host_regexp, :path_regexp, :skip_request_body_regexp, :skip_response_body_regexp
4
+
5
+ def initialize(app, only_state_change: true,
6
+ host_regexp: /.*/,
7
+ path_regexp: /.*/,
8
+ skip_request_body_regexp: nil,
9
+ skip_response_body_regexp: nil)
10
+ @app = app
11
+ self.only_state_change = only_state_change
12
+ self.host_regexp = host_regexp
13
+ self.path_regexp = path_regexp
14
+ self.skip_request_body_regexp = skip_request_body_regexp
15
+ self.skip_response_body_regexp = skip_response_body_regexp
16
+ end
17
+
18
+ def call(env)
19
+ request = ActionDispatch::Request.new(env)
20
+ logging = log?(env, request)
21
+ if logging
22
+ env["INBOUND_REQUEST_LOG"] = InboundRequestLog.from_request(request, skip_request_body: skip_request_body?(env))
23
+ request.body.rewind if request.body.respond_to?(:read)
24
+ end
25
+ 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))
38
+ end
39
+ end
40
+ [status, headers, body]
41
+ end
42
+
43
+ private
44
+
45
+ def skip_request_body?(env)
46
+ skip_request_body_regexp && env["PATH_INFO"] =~ skip_request_body_regexp
47
+ end
48
+
49
+ def skip_response_body?(env)
50
+ skip_response_body_regexp && env["PATH_INFO"] =~ skip_response_body_regexp
51
+ end
52
+
53
+ def log?(env, request)
54
+ # The HTTP_HOST header is preferred to the SERVER_NAME header per the Rack spec: https://github.com/rack/rack/blob/main/SPEC.rdoc#label-The+Environment
55
+ host = env["HTTP_HOST"] || env["SERVER_NAME"]
56
+ path = env["PATH_INFO"]
57
+ (host =~ host_regexp) &&
58
+ (path =~ path_regexp) &&
59
+ (!only_state_change || request_with_state_change?(request))
60
+ end
61
+
62
+ def parsed_body(body)
63
+ return unless body.present?
64
+
65
+ if body.respond_to?(:to_ary)
66
+ JSON.parse(body.to_ary[0])
67
+ elsif body.respond_to?(:body)
68
+ JSON.parse(body.body)
69
+ else
70
+ body
71
+ end
72
+ rescue JSON::ParserError, ArgumentError
73
+ body
74
+ end
75
+
76
+ def request_with_state_change?(request)
77
+ request.post? || request.put? || request.patch? || request.delete?
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,5 @@
1
+ module RailsApiLogger
2
+ class InboundRequestLog < RequestLog
3
+ self.table_name = "inbound_request_logs"
4
+ end
5
+ end
@@ -0,0 +1,27 @@
1
+ module RailsApiLogger
2
+ module Loggable
3
+ def self.included(base)
4
+ base.extend ClassMethods
5
+ end
6
+
7
+ # :nodoc:
8
+ module ClassMethods
9
+ def has_many_outbound_request_logs
10
+ has_many :outbound_request_logs, -> { order(:created_at) },
11
+ class_name: "RailsApiLogger::OutboundRequestLog",
12
+ inverse_of: :loggable, dependent: :destroy, as: :loggable
13
+ end
14
+
15
+ def has_many_inbound_request_logs
16
+ has_many :inbound_request_logs, -> { order(:created_at) },
17
+ class_name: "RailsApiLogger::InboundRequestLog",
18
+ inverse_of: :loggable, dependent: :destroy, as: :loggable
19
+ end
20
+
21
+ def has_many_request_logs
22
+ has_many_inbound_request_logs
23
+ has_many_outbound_request_logs
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,22 @@
1
+ module RailsApiLogger
2
+ class Logger
3
+ def initialize(loggable = nil, skip_request_body: false, skip_response_body: false)
4
+ @loggable = loggable
5
+ @skip_request_body = skip_request_body
6
+ @skip_response_body = skip_response_body
7
+ end
8
+
9
+ def call(url, request)
10
+ log = OutboundRequestLog.from_request(request, loggable: @loggable, skip_request_body: @skip_request_body)
11
+ yield.tap do |response|
12
+ log.from_response(response, skip_response_body: @skip_response_body)
13
+ end
14
+ rescue => e
15
+ log.response_body = {error: e.message} if log
16
+ raise
17
+ ensure
18
+ log.ended_at = Time.current
19
+ log.save!
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,5 @@
1
+ module RailsApiLogger
2
+ class OutboundRequestLog < RequestLog
3
+ self.table_name = "outbound_request_logs"
4
+ end
5
+ end
@@ -0,0 +1,80 @@
1
+ module RailsApiLogger
2
+ class RequestLog < ActiveRecord::Base
3
+ self.abstract_class = true
4
+
5
+ connects_to(**RailsApiLogger.connects_to) if RailsApiLogger.connects_to
6
+
7
+ serialize :request_body, coder: JSON
8
+ serialize :response_body, coder: JSON
9
+
10
+ belongs_to :loggable, optional: true, polymorphic: true
11
+
12
+ scope :failed, -> { where(response_code: 400..599).or(where.not(ended_at: nil).where(response_code: nil)) }
13
+
14
+ validates :method, presence: true
15
+ validates :path, presence: true
16
+
17
+ def self.from_request(request, loggable: nil, skip_request_body: false)
18
+ if skip_request_body
19
+ body = "[Skipped]"
20
+ else
21
+ request_body = (request.body.respond_to?(:read) ? request.body.read : request.body)
22
+ body = request_body&.dup&.force_encoding("UTF-8")
23
+ begin
24
+ body = JSON.parse(body) if body.present?
25
+ rescue JSON::ParserError
26
+ body
27
+ end
28
+ end
29
+ create(path: request.path, request_body: body, method: request.method, started_at: Time.current, loggable: loggable)
30
+ end
31
+
32
+ def from_response(response, skip_response_body: false)
33
+ self.response_code = response.code
34
+ self.response_body = skip_response_body ? "[Skipped]" : manipulate_body(response.body)
35
+ self
36
+ end
37
+
38
+ def formatted_request_body
39
+ formatted_body(request_body)
40
+ end
41
+
42
+ def formatted_response_body
43
+ formatted_body(response_body)
44
+ end
45
+
46
+ def formatted_body(body)
47
+ if body.is_a?(String) && body.blank?
48
+ ""
49
+ elsif body.is_a?(Hash)
50
+ JSON.pretty_generate(body)
51
+ else
52
+ xml = Nokogiri::XML(body)
53
+ if xml.errors.any?
54
+ body
55
+ else
56
+ xml.to_xml(indent: 2)
57
+ end
58
+ end
59
+ rescue
60
+ body
61
+ end
62
+
63
+ def duration
64
+ return if started_at.nil? || ended_at.nil?
65
+ ended_at - started_at
66
+ end
67
+
68
+ private
69
+
70
+ def manipulate_body(body)
71
+ body_duplicate = body&.dup&.force_encoding("UTF-8")
72
+ begin
73
+ body_duplicate = JSON.parse(body_duplicate) if body_duplicate.present?
74
+ rescue JSON::ParserError
75
+ body_duplicate
76
+ end
77
+ body_duplicate
78
+ end
79
+ end
80
+ end
data/bin/rails ADDED
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # This command will automatically be run when you run "rails" with Rails gems
5
+ # installed from the root of your application.
6
+
7
+ ENGINE_ROOT = File.expand_path('..', __dir__)
8
+ ENGINE_PATH = File.expand_path('../lib/rails_api_logger/engine', __dir__)
9
+ APP_PATH = File.expand_path('../spec/dummy/config/application', __dir__)
10
+
11
+ # Set up gems listed in the Gemfile.
12
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
13
+ require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
14
+
15
+ require 'rails/all'
16
+ require 'rails/engine/commands'
@@ -3,7 +3,7 @@
3
3
  require "rails/generators/base"
4
4
  require "rails/generators/migration"
5
5
 
6
- class RailsApiLogger
6
+ module RailsApiLogger
7
7
  module Generators
8
8
  class InstallGenerator < Rails::Generators::Base
9
9
  include Rails::Generators::Migration
@@ -0,0 +1,25 @@
1
+ module RailsApiLogger
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace RailsApiLogger
4
+
5
+ config.generators do |g|
6
+ g.test_framework :rspec
7
+ end
8
+
9
+ config.rails_api_logger = ActiveSupport::OrderedOptions.new
10
+
11
+ initializer "rails_api_logger.config" do
12
+ config.rails_api_logger.each do |name, value|
13
+ RailsApiLogger.public_send(:"#{name}=", value)
14
+ end
15
+ end
16
+
17
+ ActiveSupport.on_load(:action_controller) do
18
+ include InboundRequestsLogger
19
+ end
20
+
21
+ ActiveSupport.on_load(:active_record) do
22
+ include RailsApiLogger::Loggable
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsApiLogger
4
+ VERSION = "0.10.0"
5
+ end
@@ -1,35 +1,17 @@
1
- require "active_record"
1
+ require "rails_api_logger/version"
2
+ require "rails_api_logger/engine"
3
+ require_relative "../app/models/rails_api_logger/loggable"
4
+ require_relative "../app/middlewares/rails_api_logger/middleware"
5
+
6
+ require "rails"
2
7
  require "nokogiri"
8
+
3
9
  require "zeitwerk"
4
10
 
5
11
  loader = Zeitwerk::Loader.for_gem
6
- loader.collapse("#{__dir__}/rails_api_logger")
7
12
  loader.ignore("#{__dir__}/generators")
8
13
  loader.setup
9
14
 
10
- class RailsApiLogger
11
- class Error < StandardError; end
12
-
13
- def initialize(loggable = nil, skip_request_body: false, skip_response_body: false)
14
- @loggable = loggable
15
- @skip_request_body = skip_request_body
16
- @skip_response_body = skip_response_body
17
- end
18
-
19
- def call(url, request)
20
- log = OutboundRequestLog.from_request(request, loggable: @loggable, skip_request_body: @skip_request_body)
21
- yield.tap do |response|
22
- log.from_response(response, skip_response_body: @skip_response_body)
23
- end
24
- rescue => e
25
- log.response_body = {error: e.message}
26
- raise
27
- ensure
28
- log.ended_at = Time.current
29
- log.save!
30
- end
31
- end
32
-
33
- ActiveSupport.on_load(:action_controller) do
34
- include InboundRequestsLogger
15
+ module RailsApiLogger
16
+ mattr_accessor :connects_to
35
17
  end
@@ -1,6 +1,8 @@
1
+ require_relative "lib/rails_api_logger/version"
2
+
1
3
  Gem::Specification.new do |spec|
2
4
  spec.name = "rails_api_logger"
3
- spec.version = "0.9.0"
5
+ spec.version = RailsApiLogger::VERSION
4
6
  spec.authors = ["Alessandro Rodi"]
5
7
  spec.email = ["alessandro.rodi@renuo.ch"]
6
8
 
@@ -23,16 +25,19 @@ Gem::Specification.new do |spec|
23
25
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
24
26
  spec.require_paths = ["lib"]
25
27
 
26
- spec.add_dependency "railties", ">= 4.1.0"
27
- spec.add_dependency "activerecord", ">= 4.1.0"
28
- spec.add_dependency "actionpack", ">= 4.1.0"
28
+ rails_version = ">= 7.1"
29
+ spec.add_dependency "activerecord", rails_version
30
+ spec.add_dependency "activejob", rails_version
31
+ spec.add_dependency "railties", rails_version
29
32
  spec.add_dependency "nokogiri"
30
33
  spec.add_dependency "zeitwerk", ">= 2.0.0"
31
34
 
32
- spec.add_development_dependency "sqlite3", "~> 1.4.0"
35
+ spec.add_development_dependency "sqlite3", "~> 2.1.0"
33
36
  spec.add_development_dependency "pg", "~> 1.5.4"
37
+ spec.add_development_dependency "mysql2", "~> 0.5.6"
34
38
  spec.add_development_dependency "standard", "~> 1.31"
35
39
  spec.add_development_dependency "rake", "~> 12.0"
36
40
  spec.add_development_dependency "rspec", "~> 3.0"
41
+ spec.add_development_dependency "rspec-rails", "~> 7.1.0"
37
42
  spec.add_development_dependency "rack"
38
43
  end
metadata CHANGED
@@ -1,57 +1,57 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails_api_logger
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.0
4
+ version: 0.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alessandro Rodi
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-09-12 00:00:00.000000000 Z
11
+ date: 2024-11-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: railties
14
+ name: activerecord
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: 4.1.0
19
+ version: '7.1'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: 4.1.0
26
+ version: '7.1'
27
27
  - !ruby/object:Gem::Dependency
28
- name: activerecord
28
+ name: activejob
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: 4.1.0
33
+ version: '7.1'
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
- version: 4.1.0
40
+ version: '7.1'
41
41
  - !ruby/object:Gem::Dependency
42
- name: actionpack
42
+ name: railties
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
45
  - - ">="
46
46
  - !ruby/object:Gem::Version
47
- version: 4.1.0
47
+ version: '7.1'
48
48
  type: :runtime
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
- version: 4.1.0
54
+ version: '7.1'
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: nokogiri
57
57
  requirement: !ruby/object:Gem::Requirement
@@ -86,14 +86,14 @@ dependencies:
86
86
  requirements:
87
87
  - - "~>"
88
88
  - !ruby/object:Gem::Version
89
- version: 1.4.0
89
+ version: 2.1.0
90
90
  type: :development
91
91
  prerelease: false
92
92
  version_requirements: !ruby/object:Gem::Requirement
93
93
  requirements:
94
94
  - - "~>"
95
95
  - !ruby/object:Gem::Version
96
- version: 1.4.0
96
+ version: 2.1.0
97
97
  - !ruby/object:Gem::Dependency
98
98
  name: pg
99
99
  requirement: !ruby/object:Gem::Requirement
@@ -108,6 +108,20 @@ dependencies:
108
108
  - - "~>"
109
109
  - !ruby/object:Gem::Version
110
110
  version: 1.5.4
111
+ - !ruby/object:Gem::Dependency
112
+ name: mysql2
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: 0.5.6
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: 0.5.6
111
125
  - !ruby/object:Gem::Dependency
112
126
  name: standard
113
127
  requirement: !ruby/object:Gem::Requirement
@@ -150,6 +164,20 @@ dependencies:
150
164
  - - "~>"
151
165
  - !ruby/object:Gem::Version
152
166
  version: '3.0'
167
+ - !ruby/object:Gem::Dependency
168
+ name: rspec-rails
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - "~>"
172
+ - !ruby/object:Gem::Version
173
+ version: 7.1.0
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - "~>"
179
+ - !ruby/object:Gem::Version
180
+ version: 7.1.0
153
181
  - !ruby/object:Gem::Dependency
154
182
  name: rack
155
183
  requirement: !ruby/object:Gem::Requirement
@@ -177,21 +205,25 @@ files:
177
205
  - ".semaphore/main-deploy.yml"
178
206
  - ".semaphore/semaphore.yml"
179
207
  - CHANGELOG.md
180
- - CODE_OF_CONDUCT.md
181
208
  - Gemfile
182
209
  - LICENSE.txt
183
210
  - README.md
184
211
  - Rakefile
212
+ - app/controllers/inbound_requests_logger.rb
213
+ - app/middlewares/rails_api_logger/middleware.rb
214
+ - app/models/rails_api_logger/inbound_request_log.rb
215
+ - app/models/rails_api_logger/loggable.rb
216
+ - app/models/rails_api_logger/logger.rb
217
+ - app/models/rails_api_logger/outbound_request_log.rb
218
+ - app/models/rails_api_logger/request_log.rb
185
219
  - bin/console
220
+ - bin/rails
186
221
  - bin/setup
187
222
  - lib/generators/rails_api_logger/install_generator.rb
188
223
  - lib/generators/templates/create_rails_api_logger_table.rb.tt
189
224
  - lib/rails_api_logger.rb
190
- - lib/rails_api_logger/inbound_request_log.rb
191
- - lib/rails_api_logger/inbound_requests_logger.rb
192
- - lib/rails_api_logger/inbound_requests_logger_middleware.rb
193
- - lib/rails_api_logger/outbound_request_log.rb
194
- - lib/rails_api_logger/request_log.rb
225
+ - lib/rails_api_logger/engine.rb
226
+ - lib/rails_api_logger/version.rb
195
227
  - rails_api_logger.gemspec
196
228
  homepage: https://github.com/renuo/rails_api_logger
197
229
  licenses:
data/CODE_OF_CONDUCT.md DELETED
@@ -1,74 +0,0 @@
1
- # Contributor Covenant Code of Conduct
2
-
3
- ## Our Pledge
4
-
5
- In the interest of fostering an open and welcoming environment, we as
6
- contributors and maintainers pledge to making participation in our project and
7
- our community a harassment-free experience for everyone, regardless of age, body
8
- size, disability, ethnicity, gender identity and expression, level of experience,
9
- nationality, personal appearance, race, religion, or sexual identity and
10
- orientation.
11
-
12
- ## Our Standards
13
-
14
- Examples of behavior that contributes to creating a positive environment
15
- include:
16
-
17
- * Using welcoming and inclusive language
18
- * Being respectful of differing viewpoints and experiences
19
- * Gracefully accepting constructive criticism
20
- * Focusing on what is best for the community
21
- * Showing empathy towards other community members
22
-
23
- Examples of unacceptable behavior by participants include:
24
-
25
- * The use of sexualized language or imagery and unwelcome sexual attention or
26
- advances
27
- * Trolling, insulting/derogatory comments, and personal or political attacks
28
- * Public or private harassment
29
- * Publishing others' private information, such as a physical or electronic
30
- address, without explicit permission
31
- * Other conduct which could reasonably be considered inappropriate in a
32
- professional setting
33
-
34
- ## Our Responsibilities
35
-
36
- Project maintainers are responsible for clarifying the standards of acceptable
37
- behavior and are expected to take appropriate and fair corrective action in
38
- response to any instances of unacceptable behavior.
39
-
40
- Project maintainers have the right and responsibility to remove, edit, or
41
- reject comments, commits, code, wiki edits, issues, and other contributions
42
- that are not aligned to this Code of Conduct, or to ban temporarily or
43
- permanently any contributor for other behaviors that they deem inappropriate,
44
- threatening, offensive, or harmful.
45
-
46
- ## Scope
47
-
48
- This Code of Conduct applies both within project spaces and in public spaces
49
- when an individual is representing the project or its community. Examples of
50
- representing a project or community include using an official project e-mail
51
- address, posting via an official social media account, or acting as an appointed
52
- representative at an online or offline event. Representation of a project may be
53
- further defined and clarified by project maintainers.
54
-
55
- ## Enforcement
56
-
57
- Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
- reported by contacting the project team at alessandro.rodi@renuo.ch. All
59
- complaints will be reviewed and investigated and will result in a response that
60
- is deemed necessary and appropriate to the circumstances. The project team is
61
- obligated to maintain confidentiality with regard to the reporter of an incident.
62
- Further details of specific enforcement policies may be posted separately.
63
-
64
- Project maintainers who do not follow or enforce the Code of Conduct in good
65
- faith may face temporary or permanent repercussions as determined by other
66
- members of the project's leadership.
67
-
68
- ## Attribution
69
-
70
- This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71
- available at [https://contributor-covenant.org/version/1/4][version]
72
-
73
- [homepage]: https://contributor-covenant.org
74
- [version]: https://contributor-covenant.org/version/1/4/
@@ -1,2 +0,0 @@
1
- class InboundRequestLog < RequestLog
2
- end
@@ -1,71 +0,0 @@
1
- class InboundRequestsLoggerMiddleware
2
- attr_accessor :only_state_change, :path_regexp, :skip_request_body_regexp, :skip_response_body_regexp
3
-
4
- def initialize(app, only_state_change: true,
5
- path_regexp: /.*/,
6
- skip_request_body_regexp: nil,
7
- skip_response_body_regexp: nil)
8
- @app = app
9
- self.only_state_change = only_state_change
10
- self.path_regexp = path_regexp
11
- self.skip_request_body_regexp = skip_request_body_regexp
12
- self.skip_response_body_regexp = skip_response_body_regexp
13
- end
14
-
15
- def call(env)
16
- request = ActionDispatch::Request.new(env)
17
- logging = log?(env, request)
18
- if logging
19
- env["INBOUND_REQUEST_LOG"] = InboundRequestLog.from_request(request, skip_request_body: skip_request_body?(env))
20
- request.body.rewind
21
- end
22
- status, headers, body = @app.call(env)
23
- if logging
24
- updates = {response_code: status, ended_at: Time.current}
25
- updates[:response_body] = if skip_response_body?(env)
26
- "[Skipped]"
27
- else
28
- parsed_body(body)
29
- end
30
- # this usually works. let's be optimistic.
31
- begin
32
- env["INBOUND_REQUEST_LOG"].update_columns(updates)
33
- rescue JSON::GeneratorError => _e # this can be raised by activerecord if the string is not UTF-8.
34
- env["INBOUND_REQUEST_LOG"].update_columns(updates.except(:response_body))
35
- end
36
- end
37
- [status, headers, body]
38
- end
39
-
40
- private
41
-
42
- def skip_request_body?(env)
43
- skip_request_body_regexp && env["PATH_INFO"] =~ skip_request_body_regexp
44
- end
45
-
46
- def skip_response_body?(env)
47
- skip_response_body_regexp && env["PATH_INFO"] =~ skip_response_body_regexp
48
- end
49
-
50
- def log?(env, request)
51
- env["PATH_INFO"] =~ path_regexp && (!only_state_change || request_with_state_change?(request))
52
- end
53
-
54
- def parsed_body(body)
55
- return unless body.present?
56
-
57
- if body.respond_to?(:to_ary)
58
- JSON.parse(body.to_ary[0])
59
- elsif body.respond_to?(:body)
60
- JSON.parse(body.body)
61
- else
62
- body
63
- end
64
- rescue JSON::ParserError, ArgumentError
65
- body
66
- end
67
-
68
- def request_with_state_change?(request)
69
- request.post? || request.put? || request.patch? || request.delete?
70
- end
71
- end
@@ -1,2 +0,0 @@
1
- class OutboundRequestLog < RequestLog
2
- end
@@ -1,76 +0,0 @@
1
- class RequestLog < ActiveRecord::Base
2
- self.abstract_class = true
3
-
4
- serialize :request_body, coder: JSON
5
- serialize :response_body, coder: JSON
6
-
7
- belongs_to :loggable, optional: true, polymorphic: true
8
-
9
- scope :failed, -> { where(response_code: 400..599).or(where.not(ended_at: nil).where(response_code: nil)) }
10
-
11
- validates :method, presence: true
12
- validates :path, presence: true
13
-
14
- def self.from_request(request, loggable: nil, skip_request_body: false)
15
- if skip_request_body
16
- body = "[Skipped]"
17
- else
18
- request_body = (request.body.respond_to?(:read) ? request.body.read : request.body)
19
- body = request_body&.dup&.force_encoding("UTF-8")
20
- begin
21
- body = JSON.parse(body) if body.present?
22
- rescue JSON::ParserError
23
- body
24
- end
25
- end
26
- create(path: request.path, request_body: body, method: request.method, started_at: Time.current, loggable: loggable)
27
- end
28
-
29
- def from_response(response, skip_response_body: false)
30
- self.response_code = response.code
31
- self.response_body = skip_response_body ? "[Skipped]" : manipulate_body(response.body)
32
- self
33
- end
34
-
35
- def formatted_request_body
36
- formatted_body(request_body)
37
- end
38
-
39
- def formatted_response_body
40
- formatted_body(response_body)
41
- end
42
-
43
- def formatted_body(body)
44
- if body.is_a?(String) && body.blank?
45
- ""
46
- elsif body.is_a?(Hash)
47
- JSON.pretty_generate(body)
48
- else
49
- xml = Nokogiri::XML(body)
50
- if xml.errors.any?
51
- body
52
- else
53
- xml.to_xml(indent: 2)
54
- end
55
- end
56
- rescue
57
- body
58
- end
59
-
60
- def duration
61
- return if started_at.nil? || ended_at.nil?
62
- ended_at - started_at
63
- end
64
-
65
- private
66
-
67
- def manipulate_body(body)
68
- body_duplicate = body&.dup&.force_encoding("UTF-8")
69
- begin
70
- body_duplicate = JSON.parse(body_duplicate) if body_duplicate.present?
71
- rescue JSON::ParserError
72
- body_duplicate
73
- end
74
- body_duplicate
75
- end
76
- end