logtail-rails 0.2.15 → 0.3.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: 69e0f9b99939d4b4ab4c3b5b185f09fcc824120618371f8881e10b671b49a218
4
- data.tar.gz: 47c66f5323fbab6dd1b1fe2e559cce6097c13c540cd5ce7b5c2b8cb857a65b4b
3
+ metadata.gz: 2c5828f2141c9b70b0f47591b7dcf8855a648c0297477637fd7029cfd5e39a8d
4
+ data.tar.gz: 0a88a29844cdbbc76a20a827f0575fc3f3aa4befe32dde404315bb55a733cfa2
5
5
  SHA512:
6
- metadata.gz: 3378d7cf4c494bc061759ca956239472c8ad9d1f4066d47cba0e0ef7c975ea50e0be2ecc91ec9a778a9d00d78e3f79e8347092505b5a4b8a03de03c902e7fa67
7
- data.tar.gz: 6b873bad4830cfe29f52958d5b9493e0c3b0c7ddb634eabe3f6719860722e947548f710ac9b99aecd0e997105a3a1350933df4e85436889b9c0e3db08296a5f5
6
+ metadata.gz: 9238c31e5e7945f99fdc3acae8a7006f6f0568e3e2269469a0498da0d5010718d2a3dc884bfd64cc471be0c7a54283e32ff327f8fa1dd11d70b76d1baf84dc2c
7
+ data.tar.gz: 76f5185abcdc9efafbe564d65a543cb924dcfb75326686645f346a7e30201b053cad5380701f8abe3d0ed01db85f0a54b631522dca449bf4aba2164967f6d840
data/README.md CHANGED
@@ -14,7 +14,7 @@ Experience SQL-compatible structured log management based on ClickHouse. [Learn
14
14
 
15
15
  [Getting started ⇗](https://betterstack.com/docs/logs/ruby-and-rails/#logging-from-rails)
16
16
 
17
- To turn the integration off in an environment, e.g. development or test, set `config.logtail.enabled = false` in `config/environments/<environment>.rb`. Rails then logs to `log/<environment>.log` as usual, even if `config/application.rb` creates the Better Stack logger.
17
+ [Example project](example-project/README.md): logging, structured data and [adding context to every log line of a request](example-project/README.md#context).
18
18
 
19
19
  ## Need help?
20
20
  Please let us know at [hello@betterstack.com](mailto:hello@betterstack.com). We're happy to help!
@@ -185,12 +185,53 @@ This will generate the following JSON output:
185
185
  }
186
186
  ```
187
187
 
188
- We will automatically add the information about the current user to each log if you're using Ruby on Rails and the Devise gem.
188
+ We will automatically add the information about the current user to each log if you're using Ruby on Rails with Devise (or any other Warden-based authentication) or with Clearance.
189
189
 
190
- If you're not using Devise or you want to log some additional information for every request your Rails app handles, you can easily implement this using Rails' `around_action` in your application controller. A simple implementation could look like this:
190
+ ### Context for every log line of a request
191
+
192
+ The request and response lines (`Started GET "/" for 127.0.0.1` and `Completed 200 OK in 12.3ms`) are logged by a Rack middleware, before the request reaches your controller. Context set from inside a controller is therefore attached to the logs from the controller action on, but not to these two lines. To attach context to every log line of a request, set it in a Rack middleware instead.
193
+
194
+ The user context from Devise or Clearance is set this way, by the `Logtail::Integrations::Rack::UserContext` middleware. If you authenticate differently, give it a lambda that finds the user in the Rack environment and returns a hash, or `nil` when nobody is signed in:
191
195
 
192
196
  ```ruby
193
- class ApplicationController < ActionController::Base
197
+ # config/initializers/logtail.rb
198
+ Logtail::Integrations::Rack::UserContext.custom_user_hash = lambda do |rack_env|
199
+ user_id = rack_env["rack.session"]["user_id"]
200
+ user = User.find_by(id: user_id) if user_id
201
+ user && { id: user.id, email: user.email }
202
+ end
203
+ ```
204
+
205
+ The hash is logged as `context.user` on every log line of the request, including the request and response lines. The lambda runs on every request, so keep it cheap. It can read anything from the Rack environment, for example an API token from `rack_env["HTTP_AUTHORIZATION"]`.
206
+
207
+ For any other per-request context (a tenant, an API client, a GraphQL operation name), write your own middleware that wraps the request in `Logtail.with_context` and insert it before the middleware that logs the request and response lines:
208
+
209
+ ```ruby
210
+ # config/initializers/logtail.rb
211
+ class LogtailTenantContext
212
+ def initialize(app)
213
+ @app = app
214
+ end
215
+
216
+ def call(env)
217
+ tenant = env["HTTP_X_TENANT"]
218
+ return @app.call(env) unless tenant
219
+
220
+ Logtail.with_context(tenant: { name: tenant }) { @app.call(env) }
221
+ end
222
+ end
223
+
224
+ Rails.application.config.middleware.insert_before Logtail::Integrations::Rack::HTTPEvents, LogtailTenantContext
225
+ ```
226
+
227
+ Inserting it before `Logtail::Integrations::Rack::HTTPEvents` also places it after your authentication middleware (such as Warden), so the session and the signed-in user are already available in `env`.
228
+
229
+ ### Context from a controller
230
+
231
+ If the user is only known once your controller runs (for example, you authenticate from a token in a `before_action`), you can still set the context with Rails' `around_action`. It covers the logs from the controller action and everything it calls, but not the request and response lines, which are logged before the action runs. A simple implementation could look like this:
232
+
233
+ ```ruby
234
+ class ApplicationController < ActionController::Base # or ActionController::API
194
235
  around_action :with_logtail_context
195
236
 
196
237
  private
@@ -202,7 +243,7 @@ class ApplicationController < ActionController::Base
202
243
  yield
203
244
  end
204
245
  end
205
-
246
+
206
247
  def user_context
207
248
  Logtail::Contexts::User.new(
208
249
  id: current_user.id,
@@ -18,9 +18,11 @@ module Logtail
18
18
  # We determine this when the app loads to avoid the overhead on a per request basis.
19
19
  EXCEPTION_WRAPPER_TAKES_CLEANER = defined?(::ActionDispatch::ExceptionWrapper) &&
20
20
  !::ActionDispatch::ExceptionWrapper.instance_methods.include?(:env)
21
- # config.action_dispatch.log_rescued_responses (Rails 7.0+), as ActionDispatch::DebugExceptions
22
- # reads it. This gem silences its logging and logs the exception here instead.
21
+ # config.action_dispatch.log_rescued_responses (Rails 7.0+) and
22
+ # config.action_dispatch.debug_exception_log_level (Rails 7.1+), as ActionDispatch::DebugExceptions
23
+ # reads them. This gem silences its logging and logs the exception here instead.
23
24
  LOG_RESCUED_RESPONSES_KEY = "action_dispatch.log_rescued_responses".freeze
25
+ DEBUG_EXCEPTION_LOG_LEVEL_KEY = "action_dispatch.debug_exception_log_level".freeze
24
26
 
25
27
  def call(env)
26
28
  begin
@@ -38,7 +40,7 @@ module Logtail
38
40
  def log_exception(env, exception)
39
41
  return if !log_exception?(env, exception)
40
42
 
41
- Config.instance.logger.fatal do
43
+ Config.instance.logger.add(env[DEBUG_EXCEPTION_LOG_LEVEL_KEY] || ::Logger::FATAL) do
42
44
  backtrace = extract_backtrace(env, exception)
43
45
  Events::Error.new(
44
46
  name: exception.class.name,
@@ -65,10 +65,15 @@ module Logtail
65
65
  options[:ingesting_port] ||= options[:telemetry_port] if options[:telemetry_port].present?
66
66
  options[:ingesting_scheme] ||= options[:telemetry_scheme] if options[:telemetry_scheme].present?
67
67
 
68
- if ENV['LOGTAIL_SKIP_LOGS'].blank? && !Rails.env.test?
69
- io_device = Logtail::LogDevices::HTTP.new(source_token, options)
70
- else
68
+ if ENV['LOGTAIL_SKIP_LOGS'].present? || Rails.env.test?
71
69
  io_device = STDOUT
70
+ elsif source_token.blank?
71
+ # Don't break booting the app without the token, e.g. for `assets:precompile` in a Docker build
72
+ @blank_source_token = true
73
+ io_device = STDOUT
74
+ else
75
+ io_device = Logtail::LogDevices::HTTP.new(source_token, options)
76
+ @better_stack_logger_created = true
72
77
  end
73
78
 
74
79
  logger = self.create_logger(io_device)
@@ -87,5 +92,12 @@ module Logtail
87
92
 
88
93
  logger
89
94
  end
95
+
96
+ class << self
97
+ # What {create_default_logger} did, Logtail::Frameworks::Rails::Railtie warns about it once the app has booted
98
+ #
99
+ # @private
100
+ attr_reader :blank_source_token, :better_stack_logger_created
101
+ end
90
102
  end
91
103
  end
@@ -43,6 +43,31 @@ module Logtail
43
43
  config.app_middleware.use middleware_class
44
44
  end
45
45
  end
46
+
47
+ # Warns about problems with the logger create_default_logger created, unless the integration is turned off.
48
+ # Registered here, so that it runs after the app's own after_initialize blocks, which may still broadcast to it.
49
+ initializer(:logtail_logger_check, after: :load_config_initializers) do |app|
50
+ app.config.after_initialize do
51
+ next unless Integrations::Rails.enabled?
52
+
53
+ if Logtail::Logger.blank_source_token
54
+ Kernel.warn("Logtail: the source token passed to Logtail::Logger.create_default_logger is blank, logging to STDOUT instead of sending logs to Better Stack.")
55
+ next
56
+ end
57
+ next unless Logtail::Logger.better_stack_logger_created
58
+
59
+ # A `config.logger = ...` line after config/application.rb, like the one generated in
60
+ # config/environments/production.rb, replaces the Better Stack logger without any error
61
+ loggers = ::Rails.logger.respond_to?(:broadcasts) ? ::Rails.logger.broadcasts : [::Rails.logger]
62
+ next if loggers.any? { |logger| logger.is_a?(Logtail::Logger) }
63
+
64
+ # Before Rails 7.1, broadcasting extends the logger with an anonymous module, which hides the target
65
+ extended_modules = ::Rails.logger.singleton_class.included_modules - ::Rails.logger.class.included_modules
66
+ next if extended_modules.any? { |mod| mod.name.nil? }
67
+
68
+ Kernel.warn("Logtail: Rails.logger isn't the Better Stack logger created by Logtail::Logger.create_default_logger, and doesn't broadcast to it, so your logs don't reach Better Stack. Most likely config.logger is set again later, e.g. in config/environments/production.rb.")
69
+ end
70
+ end
46
71
  end
47
72
  end
48
73
  end
@@ -1,7 +1,7 @@
1
1
  module Logtail
2
2
  module Integrations
3
3
  module Rails
4
- VERSION = "0.2.15"
4
+ VERSION = "0.3.0"
5
5
  end
6
6
  end
7
7
  end
data/lib/logtail-rails.rb CHANGED
@@ -50,6 +50,7 @@ module Logtail
50
50
  ActionView.integrate!
51
51
  ActiveRecord.integrate!
52
52
  RackLogger.integrate!
53
+ filter_parameters_in_urls(::Rails.application.config.filter_parameters)
53
54
  end
54
55
 
55
56
  def self.enabled=(value)
@@ -71,6 +72,16 @@ module Logtail
71
72
  @middlewares ||= [Logtail::Integrations::Rack::HTTPContext, SessionContext, Logtail::Integrations::Rack::UserContext,
72
73
  Logtail::Integrations::Rack::HTTPEvents, Logtail::Integrations::Rails::ErrorEvent].select(&:enabled?)
73
74
  end
75
+
76
+ # Filters the app's filter_parameters from the query strings and the Referer and Location
77
+ # URLs that Rack::HTTPEvents logs, unless the app customised its query_string_filters.
78
+ # Procs are left out: they rewrite values, while query string filters match names.
79
+ def self.filter_parameters_in_urls(filter_parameters)
80
+ http_events = Logtail::Integrations::Rack::HTTPEvents
81
+ return if http_events.query_string_filters != http_events::DEFAULT_QUERY_STRING_FILTERS
82
+
83
+ http_events.query_string_filters = filter_parameters.reject { |filter| filter.is_a?(Proc) }
84
+ end
74
85
  end
75
86
  end
76
87
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: logtail-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.15
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Better Stack