logtail-rails 0.2.15 → 0.3.1

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.
@@ -9,23 +9,23 @@ You can download the example project from GitHub directly or you can clone it to
9
9
  bundle install
10
10
  ```
11
11
 
12
- This will install all dependencies listed in the `Gemfile.lock` file.
12
+ This will install all dependencies listed in the `Gemfile`. The example runs on Rails 7.1 and newer.
13
13
 
14
- Then replace `<SOURCE_TOKEN>` and `<INGESTING_HOST>` in `config/application.rb` with your actual source token and ingesting host which you can find by going to [Sources](https://telemetry.betterstack.com/team/0/sources) -> Configure in Better Stack.
14
+ `config/application.rb` reads your source token and ingesting host from the environment. You can find them by going to [Sources](https://telemetry.betterstack.com/team/0/sources) -> Configure in Better Stack.
15
15
 
16
16
  ```ruby
17
17
  config.logger = Logtail::Logger.create_default_logger(
18
- "<YOUR_ACTUAL_SOURCE_TOKEN>",
19
- ingesting_host: "in.logs.betterstack.com",
18
+ ENV["BETTER_STACK_SOURCE_TOKEN"],
19
+ ingesting_host: ENV["BETTER_STACK_INGESTING_HOST"],
20
20
  )
21
21
  ```
22
22
 
23
23
  ## Run the example project
24
24
 
25
- To run the example application, run the following command:
25
+ To run the example application, run the following command with your source token and ingesting host:
26
26
 
27
27
  ```bash
28
- rails server
28
+ BETTER_STACK_SOURCE_TOKEN=<source_token> BETTER_STACK_INGESTING_HOST=<ingesting_host> bin/rails server
29
29
  ```
30
30
 
31
31
  This will open a local server [127.0.0.1:3000.](http://127.0.0.1:3000/) On the main page, click the "Let's go!" button to generate test logs.
@@ -37,7 +37,9 @@ All done!
37
37
  Log into your Logtail account to check your logs.
38
38
  ```
39
39
 
40
- This will create a total of 6 different logs, each corresponding to a different log level and one with additional structured data. You can review these logs in Logtail.
40
+ This will create a log for each log level, one with tags and one with additional structured data, and on Rails 8.1 and newer two events reported with `Rails.event`, next to the request, controller and template logs of the Rails integration. Click "Raise an exception" on the main page to see how an exception raised in a controller action is logged. You can review these logs in Better Stack.
41
+
42
+ Stop the server with Ctrl-C. The logger sends what it still holds before the server exits.
41
43
 
42
44
  # Logging
43
45
 
@@ -185,12 +187,53 @@ This will generate the following JSON output:
185
187
  }
186
188
  ```
187
189
 
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.
190
+ 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.
191
+
192
+ ### Context for every log line of a request
193
+
194
+ 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.
195
+
196
+ 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:
197
+
198
+ ```ruby
199
+ # config/initializers/logtail.rb
200
+ Logtail::Integrations::Rack::UserContext.custom_user_hash = lambda do |rack_env|
201
+ user_id = rack_env["rack.session"]["user_id"]
202
+ user = User.find_by(id: user_id) if user_id
203
+ user && { id: user.id, email: user.email }
204
+ end
205
+ ```
206
+
207
+ 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"]`.
189
208
 
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:
209
+ 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:
191
210
 
192
211
  ```ruby
193
- class ApplicationController < ActionController::Base
212
+ # config/initializers/logtail.rb
213
+ class LogtailTenantContext
214
+ def initialize(app)
215
+ @app = app
216
+ end
217
+
218
+ def call(env)
219
+ tenant = env["HTTP_X_TENANT"]
220
+ return @app.call(env) unless tenant
221
+
222
+ Logtail.with_context(tenant: { name: tenant }) { @app.call(env) }
223
+ end
224
+ end
225
+
226
+ Rails.application.config.middleware.insert_before Logtail::Integrations::Rack::HTTPEvents, LogtailTenantContext
227
+ ```
228
+
229
+ 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`.
230
+
231
+ ### Context from a controller
232
+
233
+ 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:
234
+
235
+ ```ruby
236
+ class ApplicationController < ActionController::Base # or ActionController::API
194
237
  around_action :with_logtail_context
195
238
 
196
239
  private
@@ -202,7 +245,7 @@ class ApplicationController < ActionController::Base
202
245
  yield
203
246
  end
204
247
  end
205
-
248
+
206
249
  def user_context
207
250
  Logtail::Contexts::User.new(
208
251
  id: current_user.id,
@@ -38,13 +38,20 @@ class ExampleController < ApplicationController
38
38
  )
39
39
 
40
40
  # Since Rails 8.1, you can also use the Rails.event to send events with structured data to Better Stack.
41
- Rails.event.notify("My first event", user_id: 123, email: "user@example.com")
42
-
43
- # You can add context to all events
44
- Rails.event.set_context(request_id: "abc123", shop_id: 456)
45
- # And tags specific events
46
- Rails.event.tagged("api") do
47
- Rails.event.notify("My tagged event with additional context", user_id: 123, email: "user@example.com")
41
+ if Rails.respond_to?(:event)
42
+ Rails.event.notify("My first event", user_id: 123, email: "user@example.com")
43
+
44
+ # You can add context to all events
45
+ Rails.event.set_context(request_id: "abc123", shop_id: 456)
46
+ # And tags specific events
47
+ Rails.event.tagged("api") do
48
+ Rails.event.notify("My tagged event with additional context", user_id: 123, email: "user@example.com")
49
+ end
48
50
  end
49
51
  end
52
+
53
+ # Exceptions raised in an action are sent to Better Stack too, with their class, message and backtrace
54
+ def raise_error
55
+ raise ArgumentError, "Something went wrong, this action raised an exception!"
56
+ end
50
57
  end
@@ -1,3 +1,5 @@
1
1
  <h1>Hello from Logtail</h1>
2
2
  <p>Click the button below to send some example logs</p>
3
- <%= button_to "Let's go!", send_logs_path, method: :get %>
3
+ <%= button_to "Let's go!", send_logs_path, method: :get %>
4
+ <p>Or raise an exception in a controller action</p>
5
+ <%= button_to "Raise an exception", raise_error_path, method: :get %>
@@ -8,8 +8,8 @@ Bundler.require(*Rails.groups)
8
8
 
9
9
  module ExampleProject
10
10
  class Application < Rails::Application
11
- # Initialize configuration defaults for originally generated Rails version.
12
- config.load_defaults 8.1
11
+ # Initialize configuration defaults of Rails 7.1, the oldest Rails this example runs on.
12
+ config.load_defaults 7.1
13
13
 
14
14
  # Configuration for the application, engines, and railties goes here.
15
15
  #
@@ -19,9 +19,10 @@ module ExampleProject
19
19
  # config.time_zone = "Central Time (US & Canada)"
20
20
  # config.eager_load_paths << Rails.root.join("extras")
21
21
 
22
+ # Send the logs to your Better Stack source, its token and ingesting host are read from the environment
22
23
  config.logger = Logtail::Logger.create_default_logger(
23
- "<SOURCE_TOKEN>",
24
- ingesting_host: "<INGESTING_HOST>",
24
+ ENV["BETTER_STACK_SOURCE_TOKEN"],
25
+ ingesting_host: ENV["BETTER_STACK_INGESTING_HOST"],
25
26
  )
26
27
  end
27
28
  end
@@ -1,6 +1,7 @@
1
1
  Rails.application.routes.draw do
2
2
  # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html
3
3
  get '/send_logs', to: 'example#send_logs', as: 'send_logs'
4
+ get '/raise_error', to: 'example#raise_error', as: 'raise_error'
4
5
 
5
6
  # Defines the root path route ("/")
6
7
  root 'example#index'
@@ -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.1"
5
5
  end
6
6
  end
7
7
  end
data/lib/logtail-rails.rb CHANGED
@@ -2,6 +2,10 @@ require "logtail-rails/overrides"
2
2
 
3
3
  require "logtail"
4
4
 
5
+ # Rails' own files rely on Active Support being loaded first, and on Rails main (8.2.0.alpha) rails/railtie
6
+ # fails without it: "uninitialized constant ActiveSupport::DescendantsTracker::WeakSet". It comes after logtail,
7
+ # which loads Ruby's logger: older Active Support releases need that with concurrent-ruby 1.3.5 or newer.
8
+ require "active_support"
5
9
  require "rails/railtie"
6
10
  require "active_record"
7
11
  require "rack"
@@ -50,6 +54,7 @@ module Logtail
50
54
  ActionView.integrate!
51
55
  ActiveRecord.integrate!
52
56
  RackLogger.integrate!
57
+ filter_parameters_in_urls(::Rails.application.config.filter_parameters)
53
58
  end
54
59
 
55
60
  def self.enabled=(value)
@@ -71,6 +76,16 @@ module Logtail
71
76
  @middlewares ||= [Logtail::Integrations::Rack::HTTPContext, SessionContext, Logtail::Integrations::Rack::UserContext,
72
77
  Logtail::Integrations::Rack::HTTPEvents, Logtail::Integrations::Rails::ErrorEvent].select(&:enabled?)
73
78
  end
79
+
80
+ # Filters the app's filter_parameters from the query strings and the Referer and Location
81
+ # URLs that Rack::HTTPEvents logs, unless the app customised its query_string_filters.
82
+ # Procs are left out: they rewrite values, while query string filters match names.
83
+ def self.filter_parameters_in_urls(filter_parameters)
84
+ http_events = Logtail::Integrations::Rack::HTTPEvents
85
+ return if http_events.query_string_filters != http_events::DEFAULT_QUERY_STRING_FILTERS
86
+
87
+ http_events.query_string_filters = filter_parameters.reject { |filter| filter.is_a?(Proc) }
88
+ end
74
89
  end
75
90
  end
76
91
  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.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Better Stack
@@ -237,8 +237,16 @@ executables: []
237
237
  extensions: []
238
238
  extra_rdoc_files: []
239
239
  files:
240
+ - ".github/actions/cleanup-sources/action.yml"
241
+ - ".github/actions/cleanup-sources/run.sh"
242
+ - ".github/actions/create-source/action.yml"
243
+ - ".github/actions/create-source/run.sh"
244
+ - ".github/actions/verify-logs/action.yml"
245
+ - ".github/actions/verify-logs/run.sh"
246
+ - ".github/workflows/e2e.yml"
240
247
  - ".github/workflows/main.yml"
241
248
  - ".github/workflows/no-closing-keywords.yml"
249
+ - ".github/workflows/notify_linear_failure.yml"
242
250
  - ".github/workflows/release.yml"
243
251
  - ".gitignore"
244
252
  - ".rspec"
@@ -246,6 +254,10 @@ files:
246
254
  - LICENSE.md
247
255
  - README.md
248
256
  - Rakefile
257
+ - e2e/example.sh
258
+ - e2e/expected.json
259
+ - e2e/installed.rb
260
+ - e2e/plan.sh
249
261
  - example-project/.gitattributes
250
262
  - example-project/.gitignore
251
263
  - example-project/Gemfile
@@ -336,7 +348,6 @@ files:
336
348
  - gemfiles/rails-7.2.gemfile
337
349
  - gemfiles/rails-8.0.gemfile
338
350
  - gemfiles/rails-8.1.gemfile
339
- - gemfiles/rails-edge.gemfile
340
351
  - lib/logtail-rails.rb
341
352
  - lib/logtail-rails/action_controller.rb
342
353
  - lib/logtail-rails/action_controller/log_subscriber.rb
@@ -1,13 +0,0 @@
1
- source 'https://rubygems.org'
2
-
3
- gem 'rails', github: 'rails/rails', branch: 'main'
4
- gem 'sidekiq', '>= 7.3.0', require: false if RUBY_VERSION >= '2.7.0'
5
-
6
- gem 'logtail'
7
- gem 'logtail-rack'
8
-
9
- if RUBY_PLATFORM == "java"
10
- gem 'mime-types', '2.6.2'
11
- end
12
-
13
- gemspec :path => '../'