Skip to content
18 changes: 15 additions & 3 deletions lib/logtail-rails/logger.rb
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,15 @@ def self.create_default_logger(source_token, options = {})
options[:ingesting_port] ||= options[:telemetry_port] if options[:telemetry_port].present?
options[:ingesting_scheme] ||= options[:telemetry_scheme] if options[:telemetry_scheme].present?

if ENV['LOGTAIL_SKIP_LOGS'].blank? && !Rails.env.test?
io_device = Logtail::LogDevices::HTTP.new(source_token, options)
else
if ENV['LOGTAIL_SKIP_LOGS'].present? || Rails.env.test?
io_device = STDOUT
elsif source_token.blank?
# Don't break booting the app without the token, e.g. for `assets:precompile` in a Docker build
@blank_source_token = true
io_device = STDOUT
else
io_device = Logtail::LogDevices::HTTP.new(source_token, options)
@better_stack_logger_created = true
end

logger = self.create_logger(io_device)
Expand All @@ -87,5 +92,12 @@ def self.create_default_logger(source_token, options = {})

logger
end

class << self
# What {create_default_logger} did, Logtail::Frameworks::Rails::Railtie warns about it once the app has booted
#
# @private
attr_reader :blank_source_token, :better_stack_logger_created
end
end
end
25 changes: 25 additions & 0 deletions lib/logtail-rails/railtie.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,31 @@ class Railtie < ::Rails::Railtie
config.app_middleware.use middleware_class
end
end

# Warns about problems with the logger create_default_logger created, unless the integration is turned off.
# Registered here, so that it runs after the app's own after_initialize blocks, which may still broadcast to it.
initializer(:logtail_logger_check, after: :load_config_initializers) do |app|
app.config.after_initialize do
next unless Integrations::Rails.enabled?

if Logtail::Logger.blank_source_token
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.")
next
end
next unless Logtail::Logger.better_stack_logger_created

# A `config.logger = ...` line after config/application.rb, like the one generated in
# config/environments/production.rb, replaces the Better Stack logger without any error
loggers = ::Rails.logger.respond_to?(:broadcasts) ? ::Rails.logger.broadcasts : [::Rails.logger]
next if loggers.any? { |logger| logger.is_a?(Logtail::Logger) }

# Before Rails 7.1, broadcasting extends the logger with an anonymous module, which hides the target
extended_modules = ::Rails.logger.singleton_class.included_modules - ::Rails.logger.class.included_modules
next if extended_modules.any? { |mod| mod.name.nil? }

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.")
end
end
end
end
end
Expand Down
108 changes: 108 additions & 0 deletions spec/logtail-rails/boot_warnings_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
require "spec_helper"
require "open3"
require "rbconfig"
require "tmpdir"

RSpec.describe Logtail::Frameworks::Rails::Railtie do
describe "boot warnings" do
let(:blank_token_warning) { "Logtail: the source token passed to Logtail::Logger.create_default_logger is blank" }
let(:logger_warning) { "Logtail: Rails.logger isn't the Better Stack logger" }

def ingesting_options
'ingesting_host: "127.0.0.1", ingesting_port: 9, ingesting_scheme: "http"'
end

# Booting an app patches Rails classes for the whole process, so each case boots its own production app in a
# new Ruby process. Its config/application.rb creates the logger as in the setup docs.
def boot_app(source_token, environment_file = "", after_boot = "")
Dir.mktmpdir do |root|
FileUtils.mkdir_p(File.join(root, "config", "environments"))
File.write(File.join(root, "config", "environments", "production.rb"), environment_file)
File.write(File.join(root, "boot.rb"), <<-RUBY)
require "logger" # Rails < 7.1 doesn't require it before ActiveSupport needs it
require "rails"
require "action_controller/railtie"
require "logtail-rails"
class BootWarningsSpecApp < Rails::Application
config.root = #{root.inspect}
config.eager_load = false
config.secret_key_base = "1e05af2b349457936a41427e63450937"
config.logger = Logtail::Logger.create_default_logger(#{source_token.inspect}, #{ingesting_options})
end
BootWarningsSpecApp.initialize!
#{after_boot}
RUBY

Open3.capture3({ "RAILS_ENV" => "production" }, RbConfig.ruby, "-rbundler/setup", File.join(root, "boot.rb"))
end
end

it "boots with a missing source token, logs to STDOUT and warns once" do
stdout, stderr, status = boot_app(nil, "", 'Rails.logger.info("Logged after boot")')

expect(status).to be_success, stderr
expect(stderr.scan(blank_token_warning).length).to eq(1)
expect(stderr.scan(/^Logtail: /).length).to eq(1)
expect(stdout).to include("Logged after boot")
end

it "boots with an empty source token and warns once" do
_stdout, stderr, status = boot_app("")

expect(status).to be_success, stderr
expect(stderr.scan(blank_token_warning).length).to eq(1)
expect(stderr.scan(/^Logtail: /).length).to eq(1)
end

# E.g. in development, where config/application.rb still creates the logger, but without the token
it "doesn't warn about a missing source token when the integration is turned off" do
_stdout, stderr, status = boot_app(nil, "Logtail::Integrations::Rails.enabled = false\n")

expect(status).to be_success, stderr
expect(stderr).not_to match(/^Logtail: /)
end

it "warns once when config.logger is set again after config/application.rb" do
_stdout, stderr, status = boot_app("source-token", <<-RUBY)
Rails.application.configure do
config.logger = ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new(STDOUT))
end
RUBY

expect(status).to be_success, stderr
expect(stderr.scan(logger_warning).length).to eq(1)
expect(stderr.scan(/^Logtail: /).length).to eq(1)
end

it "doesn't warn when Rails.logger broadcasts to the Better Stack logger" do
# ActiveSupport::BroadcastLogger replaced ActiveSupport::Logger.broadcast in Rails 7.1
environment_file = if ::ActiveSupport::Logger.respond_to?(:broadcast)
<<-RUBY
Rails.application.configure do
config.logger = ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new(STDOUT))
config.logger.extend(ActiveSupport::Logger.broadcast(Logtail::Logger.create_default_logger("source-token", #{ingesting_options})))
end
RUBY
else
<<-RUBY
Rails.application.configure do
config.logger = ActiveSupport::BroadcastLogger.new(ActiveSupport::Logger.new(STDOUT), Logtail::Logger.create_default_logger("source-token", #{ingesting_options}))
end
RUBY
end

_stdout, stderr, status = boot_app("source-token", environment_file)

expect(status).to be_success, stderr
expect(stderr).not_to match(/^Logtail: /)
end

it "doesn't warn with the documented setup" do
_stdout, stderr, status = boot_app("source-token")

expect(status).to be_success, stderr
expect(stderr).not_to match(/^Logtail: /)
end
end
end
Loading