Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ Experience SQL-compatible structured log management based on ClickHouse. [Learn

[Getting started ⇗](https://betterstack.com/docs/logs/ruby-and-rails/#logging-from-rails)

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.

## Need help?
Please let us know at [hello@betterstack.com](mailto:hello@betterstack.com). We're happy to help!

Expand Down
19 changes: 19 additions & 0 deletions lib/logtail-rails/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,22 @@
integrations.active_record.silence = true
integrations.rack.http_events.collapse_into_single_event = true
end

module Logtail
class Config
# Turns the Rails integration on or off, it's on by default. Set it in `config/application.rb`
# or `config/environments/*.rb`, see {Logtail::Frameworks::Rails::Railtie} for what it turns off.
#
# @example Keep the standard Rails logging in development
# # config/environments/development.rb
# config.logtail.enabled = false
def enabled=(value)
@enabled = value
end

# Accessor method for {#enabled=}
def enabled?
@enabled != false
end
end
end
20 changes: 20 additions & 0 deletions lib/logtail-rails/railtie.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,26 @@ class Railtie < ::Rails::Railtie
Logtail::Config.instance.logger = Proc.new { ::Rails.logger }
end

# `config.logtail.enabled = false` turns the integration off. The environment files are loaded
# by now, and Rails hasn't set up Rails.logger from config.logger yet.
initializer(:logtail_enabled, after: :load_environment_config, before: :initialize_logger) do |app|
next if app.config.logtail.enabled?

# Before the :logtail initializer below picks the middlewares
Integrations::Rails.enabled = false

# The setup docs create the logger in config/application.rb, i.e. in every environment. Log to the
# default log file instead, with a Logtail::Logger so that `logger.info("message", key: value)` works.
if app.config.logger.is_a?(Logtail::Logger)
log_file = app.paths["log"].first
FileUtils.mkdir_p(File.dirname(log_file))
# Opened like Rails opens it: Logger only writes its "# Logfile created" header into files it creates
file = File.open(log_file, "a")
file.binmode
app.config.logger = Logtail::Logger.create_logger(file)
end
end

# Must be loaded after initializers so that we respect any Logtail configuration set
initializer(:logtail, before: :build_middleware_stack, after: :load_config_initializers) do
Integrations::Rails.integrate!
Expand Down
76 changes: 76 additions & 0 deletions spec/logtail-rails/enabled_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
require "spec_helper"
require "json"
require "open3"
require "rbconfig"
require "tmpdir"

RSpec.describe Logtail::Frameworks::Rails::Railtie do
describe "config.logtail.enabled" do
# Booting an app patches Rails classes for the whole process, so each case boots its own app in a new
# Ruby process. Its config/application.rb creates the logger as in the setup docs.
def boot_app(rails_env, environment_file)
Dir.mktmpdir do |root|
FileUtils.mkdir_p(File.join(root, "config", "environments"))
File.write(File.join(root, "config", "environments", "#{rails_env}.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 EnabledSpecApp < Rails::Application
config.root = #{root.inspect}
config.eager_load = false
config.secret_key_base = "1e05af2b349457936a41427e63450937"
config.logger = Logtail::Logger.create_default_logger("source-token", ingesting_host: "127.0.0.1", ingesting_port: 9, ingesting_scheme: "http")
end
EnabledSpecApp.initialize!
Rails.logger.info("Structured log line", key: "value")
log_file = File.join(#{root.inspect}, "log", "#{rails_env}.log")
File.write(File.join(#{root.inspect}, "result.json"), JSON.generate(
"middlewares" => Rails.application.middleware.map(&:inspect).grep(/Logtail/),
"debug_exceptions_patched" => ActionDispatch::DebugExceptions.include?(Logtail::Integrations::ActionDispatch::DebugExceptions::InstanceMethods),
"event_log_subscriber_enabled" => Logtail::Integrations::Rails::EventLogSubscriber.enabled,
"logtail_logger" => Rails.logger.is_a?(Logtail::Logger),
"log_file" => (File.read(log_file) if File.exist?(log_file))
))
RUBY

_stdout, stderr, status = Open3.capture3({ "RAILS_ENV" => rails_env }, RbConfig.ruby, "-rbundler/setup", File.join(root, "boot.rb"))
raise "The app failed to boot:\n#{stderr}" unless status.success?

JSON.parse(File.read(File.join(root, "result.json")))
end
end

it "turns the integration off and logs to log/<env>.log when set to false in an environment file" do
result = boot_app("development", "Rails.application.configure { config.logtail.enabled = false }\n")

expect(result["middlewares"]).to eq([])
expect(result["debug_exceptions_patched"]).to eq(false)
expect(result["event_log_subscriber_enabled"]).to eq(false)
expect(result["logtail_logger"]).to eq(true)
expect(result["log_file"]).to include("Structured log line")
# Rails opens the log file itself, so Logger doesn't write its "# Logfile created on …" header
expect(result["log_file"]).not_to start_with("# Logfile created")
end

it "replaces the STDOUT logger create_default_logger returns in the test environment" do
result = boot_app("test", "Rails.application.configure { config.logtail.enabled = false }\n")

expect(result["middlewares"]).to eq([])
expect(result["log_file"]).to include("Structured log line")
expect(result["log_file"]).not_to start_with("# Logfile created")
end

it "keeps the integration on by default" do
result = boot_app("test", "")

expect(result["middlewares"]).to include("Logtail::Integrations::Rack::HTTPEvents")
expect(result["debug_exceptions_patched"]).to eq(true)
expect(result["log_file"]).to be_nil
end
end
end
4 changes: 2 additions & 2 deletions spec/logtail-rails/http_events_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ def method_for_action(action_name)

expect(lines[0]).to include("Started GET \\\"/rack_http\\\"")
expect(lines[1]).to include("Processing by RackHttpController#index as HTML")
expect(lines[2]).to include("Completed 200 OK in 0.0ms")
expect(lines[2]).to match(/Completed 200 OK in \d+\.\d+ms/)
end

context "with the route silenced" do
Expand Down Expand Up @@ -87,7 +87,7 @@ def method_for_action(action_name)
expect(lines.length).to eq(2)

expect(lines[0]).to include("Processing by RackHttpController#index as HTML")
expect(lines[1]).to include("GET /rack_http completed with 200 OK in 0.0ms")
expect(lines[1]).to match(/GET \/rack_http completed with 200 OK in \d+\.\d+ms/)
end
end
end
Expand Down
2 changes: 1 addition & 1 deletion spec/logtail-rails/rack_logger_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ def method_for_action(action_name)
expect(lines.length).to eq(3)
expect(lines[0]).to include("Started GET \\\"/rails_rack_logger\\\"")
expect(lines[1]).to include("Processing by RailsRackLoggerController#index as HTML")
expect(lines[2]).to include("Completed 200 OK in 0.0ms")
expect(lines[2]).to match(/Completed 200 OK in \d+\.\d+ms/)
ensure
Logtail::Integrations::Rails::EventLogSubscriber.enabled = original_enabled
end
Expand Down
Loading