Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

added lib/bootstrap.rb #2635

Merged
merged 4 commits into from
May 23, 2024
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
3 changes: 3 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1250,6 +1250,7 @@ Style/MethodCallWithArgsParentheses:
- add_development_dependency
- catch
- debug
- exit
- expect
- fail
- gem
Expand All @@ -1260,6 +1261,7 @@ Style/MethodCallWithArgsParentheses:
- pp
- raise
- require
- require_relative
- skip
- sleep
- source
Expand All @@ -1268,6 +1270,7 @@ Style/MethodCallWithArgsParentheses:
- throw
- use
- warn
- warn_and_exit
AllowedPatterns: [^assert, ^refute]

Style/MethodCallWithoutArgsParentheses:
Expand Down
105 changes: 105 additions & 0 deletions lib/bootstrap.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# This file is distributed under New Relic's license terms.
# See https://github.com/newrelic/newrelic-ruby-agent/blob/main/LICENSE for complete details.
# frozen_string_literal: true

# This file is designed to bootstrap a `Bundler.require`-based Ruby app (such as
# a Ruby on Rails app) so the app can be instrumented and observed by the
# New Relic Ruby agent without the agent being added to the app as a dependency.
# NOTE: introducing the agent into your application via bootstrap is in beta.
# Use at your own risk.
#
# Given a production-ready Ruby app that optionally has a pre-packaged "frozen"
# or "deployment"–gem bundle, the New Relic Ruby agent can be introduced
# to the app without modifying the app and keeping all of the app's content
# read-only.
#
# Prerequisites:
# - Ruby (tested v2.4+)
# - Bundler (included with Ruby, tested v1.17+)
#
# Instructions:
# - First, make sure the New Relic Ruby agent exists on disk. For these
# instructions, we'll assume the agent exists at `/newrelic`.
# - The agent can be downloaded as the "newrelic_rpm" gem from RubyGems.org
# and unpacked with "gem unpack"
# - The agent can be cloned from the New Relic public GitHub repo:
# https://github.com/newrelic/newrelic-ruby-agent
# - Next, use the "RUBYOPT" environment variable to require ("-r") this
# file (note that the ".rb" extension is dropped):
# ```
# export RUBYOPT="-r /newrelic/lib/bootstrap"
# ```
# - Add your New Relic license key as an environment variable.
# ```
# export NEW_RELIC_LICENSE_KEY=1a2b3c4d5e67f8g9h0i
# ```
# - Launch an existing Ruby app as usual. For a Ruby on Rails app, this might
# involve running `bin/rails server`.
# - In the Ruby app's directory, look for and inspect
# `log/newrelic_agent.log`. If this file exists and there are no "WARN" or
# "ERROR" entries within it, then the agent was successfully introduced to
# the Ruby application.

module NRBundlerPatch
NR_AGENT_GEM = 'newrelic_rpm'

def require(*_groups)
super

require_newrelic
end

def require_newrelic
lib = File.dirname(__FILE__)
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
Kernel.require NR_AGENT_GEM
end
end

class NRBundlerPatcher
BUNDLER = 'bundler'
RUBYOPT = 'RUBYOPT'

def self.patch
check_for_require
check_for_rubyopt
check_for_bundler
Bundler::Runtime.prepend(NRBundlerPatch)
end

private

def self.check_for_require
warn_and_exit "#{__FILE__} is meant to be required, not invoked directly" if $PROGRAM_NAME == __FILE__
end
Comment on lines +72 to +74
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice!


def self.check_for_rubyopt
unless ENV[RUBYOPT].to_s.match?("-r #{__FILE__.rpartition('.').first}")
warn_and_exit "#{__FILE__} is meant to be required via the RUBYOPT env var"
end
end

def self.check_for_bundler
require_bundler

warn_and_exit 'Required Ruby Bundler class Bundler::Runtime not defined!' unless defined?(Bundler::Runtime)

unless Bundler::Runtime.method_defined?(:require)
warn_and_exit "The active Ruby Bundler instance doesn't offer Bundler::Runtime#require"
end
end

def self.require_bundler
require BUNDLER
rescue LoadError => e
warn_and_exit "Required Ruby library '#{BUNDLER}' could not be required - #{e}"
end

def self.warn_and_exit(msg)
warn "New Relic entrypoint at #{__FILE__} encountered an issue:\n\t#{msg}"

exit 1
end
end

NRBundlerPatcher.patch
134 changes: 134 additions & 0 deletions test/new_relic/bootstrap_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
# This file is distributed under New Relic's license terms.
# See https://github.com/newrelic/newrelic-ruby-agent/blob/main/LICENSE for complete details.
# frozen_string_literal: true

require_relative '../test_helper'

class NewRelicBootstrapTest < Minitest::Test
class PhonyBundler
DEFAULT_GEM_NAME = 'tadpole'

def require(*_groups)
Kernel.require DEFAULT_GEM_NAME
end
end

def setup
require_bootstrap
monkeypatch_phony_bundler
end

def test_the_overall_prepend_based_monkeypatch
# Make sure that newrelic_rpm is required as a result of the patching
required_gems = []
Kernel.stub :require, proc { |gem| required_gems << gem } do
PhonyBundler.new.require
end

assert_equal 2, required_gems.size, "Expected 2 gems to be required, saw #{required_gems.size}."
assert_equal [PhonyBundler::DEFAULT_GEM_NAME, 'newrelic_rpm'], required_gems,
"Expected to see 'newrelic_rpm' required. Only saw #{required_gems}"
end

def test_check_for_require
opn = $PROGRAM_NAME

$PROGRAM_NAME = bootstrap_file
msg = ''
NRBundlerPatcher.stub :warn_and_exit, proc { |m| msg = m } do
NRBundlerPatcher.check_for_require
end

assert_match(/meant to be required, not invoked/, msg,
'Expected check_for_require to complain when bootstrap is invoked directly')
ensure
$PROGRAM_NAME = opn
end

def test_check_for_rubyopt
oro = ENV.fetch('RUBYOPT', nil)

ENV['RUBYOPT'] = "-r #{bootstrap_file}"

refute NRBundlerPatcher.check_for_rubyopt
ensure
ENV['RUBYOPT'] = oro if oro
end

def test_check_for_bundler_class_not_defined
oruntime = Bundler.send(:const_get, :Runtime)

NRBundlerPatcher.stub :require_bundler, nil do
Bundler.send(:remove_const, :Runtime)

msg = ''
NRBundlerPatcher.stub :warn_and_exit, proc { |m| msg = m; Bundler.send(:const_set, :Runtime, oruntime) } do
NRBundlerPatcher.check_for_bundler
end

assert_match(/class Bundler::Runtime not defined!/, msg,
'Expected check_for_bundler to complain if Bundler::Runtime is not defined')
end
ensure
Bundler.send(:const_set, :Runtime, oruntime)
end

def test_check_for_bundler_method_not_defined
skip_unless_minitest5_or_above

NRBundlerPatcher.stub :require_bundler, nil do
Bundler::Runtime.stub :method_defined?, false, [:require] do
msg = ''
NRBundlerPatcher.stub :warn_and_exit, proc { |m| msg = m } do
NRBundlerPatcher.check_for_bundler
end

assert_match(/doesn't offer Bundler::Runtime#require/, msg,
'Expected check_for_bundler to complain if Bundler::Runtime#require is not defined')
end
end
end

def test_require_bundler
skip_unless_minitest5_or_above

NRBundlerPatcher.stub :require, proc { |_gem| raise LoadError }, ['bundler'] do
msg = ''
NRBundlerPatcher.stub :warn_and_exit, proc { |m| msg = m } do
NRBundlerPatcher.check_for_bundler
end

assert_match(/could not be required/, msg,
'Expected require_bundler to complain if Bundler could not be required')
end
end

private

# Load the bootstrap file and anticipate the `warn` and `exit` calls
# with assertions
def require_bootstrap
assert_raises SystemExit do
assert_output(/New Relic entrypoint/) do
require_relative '../../lib/bootstrap'
end
end
end

# Have the patcher patch our phony Bundler instead of the real one
def monkeypatch_phony_bundler
NRBundlerPatcher.stub :check_for_require, nil do
NRBundlerPatcher.stub :check_for_rubyopt, nil do
NRBundlerPatcher.stub :check_for_bundler, nil do
Bundler::Runtime.stub :prepend, proc { |mod| PhonyBundler.prepend(mod) } do
NRBundlerPatcher.patch
end
end
end
end
end

def bootstrap_file
@bootstrap_file ||= File.expand_path('../../../lib/bootstrap.rb', __FILE__)
end
end
Loading