opbeat 2.0.0 → 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +4 -3
- data/.travis.yml +19 -28
- data/.yardopts +3 -0
- data/Gemfile +4 -2
- data/HISTORY.md +3 -0
- data/LICENSE +7 -196
- data/README.md +96 -177
- data/Rakefile +19 -13
- data/gemfiles/Gemfile.base +28 -0
- data/gemfiles/Gemfile.rails-3.2.x +3 -0
- data/gemfiles/Gemfile.rails-4.0.x +3 -0
- data/gemfiles/Gemfile.rails-4.1.x +3 -0
- data/gemfiles/Gemfile.rails-4.2.x +3 -0
- data/lib/opbeat.rb +113 -93
- data/lib/opbeat/capistrano.rb +3 -4
- data/lib/opbeat/client.rb +243 -82
- data/lib/opbeat/configuration.rb +51 -64
- data/lib/opbeat/data_builders.rb +16 -0
- data/lib/opbeat/data_builders/error.rb +27 -0
- data/lib/opbeat/data_builders/transactions.rb +85 -0
- data/lib/opbeat/error.rb +1 -2
- data/lib/opbeat/error_message.rb +71 -0
- data/lib/opbeat/error_message/exception.rb +12 -0
- data/lib/opbeat/error_message/http.rb +62 -0
- data/lib/opbeat/error_message/stacktrace.rb +75 -0
- data/lib/opbeat/error_message/user.rb +23 -0
- data/lib/opbeat/filter.rb +53 -43
- data/lib/opbeat/http_client.rb +141 -0
- data/lib/opbeat/injections.rb +83 -0
- data/lib/opbeat/injections/json.rb +19 -0
- data/lib/opbeat/injections/net_http.rb +43 -0
- data/lib/opbeat/injections/redis.rb +23 -0
- data/lib/opbeat/injections/sequel.rb +32 -0
- data/lib/opbeat/injections/sinatra.rb +56 -0
- data/lib/opbeat/{capistrano → integration}/capistrano2.rb +6 -6
- data/lib/opbeat/{capistrano → integration}/capistrano3.rb +3 -3
- data/lib/opbeat/{integrations → integration}/delayed_job.rb +6 -11
- data/lib/opbeat/integration/rails/inject_exceptions_catcher.rb +23 -0
- data/lib/opbeat/integration/railtie.rb +53 -0
- data/lib/opbeat/integration/resque.rb +16 -0
- data/lib/opbeat/integration/sidekiq.rb +38 -0
- data/lib/opbeat/line_cache.rb +21 -0
- data/lib/opbeat/logging.rb +37 -0
- data/lib/opbeat/middleware.rb +59 -0
- data/lib/opbeat/normalizers.rb +65 -0
- data/lib/opbeat/normalizers/action_controller.rb +21 -0
- data/lib/opbeat/normalizers/action_view.rb +71 -0
- data/lib/opbeat/normalizers/active_record.rb +41 -0
- data/lib/opbeat/sql_summarizer.rb +27 -0
- data/lib/opbeat/subscriber.rb +80 -0
- data/lib/opbeat/tasks.rb +20 -18
- data/lib/opbeat/trace.rb +47 -0
- data/lib/opbeat/trace_helpers.rb +29 -0
- data/lib/opbeat/transaction.rb +99 -0
- data/lib/opbeat/util.rb +26 -0
- data/lib/opbeat/util/constantize.rb +54 -0
- data/lib/opbeat/util/inspector.rb +75 -0
- data/lib/opbeat/version.rb +1 -1
- data/lib/opbeat/worker.rb +55 -0
- data/opbeat.gemspec +6 -14
- data/spec/opbeat/client_spec.rb +216 -29
- data/spec/opbeat/configuration_spec.rb +34 -38
- data/spec/opbeat/data_builders/error_spec.rb +43 -0
- data/spec/opbeat/data_builders/transactions_spec.rb +51 -0
- data/spec/opbeat/error_message/exception_spec.rb +22 -0
- data/spec/opbeat/error_message/http_spec.rb +65 -0
- data/spec/opbeat/error_message/stacktrace_spec.rb +56 -0
- data/spec/opbeat/error_message/user_spec.rb +28 -0
- data/spec/opbeat/error_message_spec.rb +78 -0
- data/spec/opbeat/filter_spec.rb +21 -99
- data/spec/opbeat/http_client_spec.rb +64 -0
- data/spec/opbeat/injections/net_http_spec.rb +37 -0
- data/spec/opbeat/injections/sequel_spec.rb +33 -0
- data/spec/opbeat/injections/sinatra_spec.rb +13 -0
- data/spec/opbeat/injections_spec.rb +49 -0
- data/spec/opbeat/integration/delayed_job_spec.rb +35 -0
- data/spec/opbeat/integration/json_spec.rb +41 -0
- data/spec/opbeat/integration/rails_spec.rb +88 -0
- data/spec/opbeat/integration/redis_spec.rb +20 -0
- data/spec/opbeat/integration/resque_spec.rb +42 -0
- data/spec/opbeat/integration/sidekiq_spec.rb +40 -0
- data/spec/opbeat/integration/sinatra_spec.rb +66 -0
- data/spec/opbeat/line_cache_spec.rb +38 -0
- data/spec/opbeat/logging_spec.rb +47 -0
- data/spec/opbeat/middleware_spec.rb +32 -0
- data/spec/opbeat/normalizers/action_controller_spec.rb +32 -0
- data/spec/opbeat/normalizers/action_view_spec.rb +77 -0
- data/spec/opbeat/normalizers/active_record_spec.rb +70 -0
- data/spec/opbeat/normalizers_spec.rb +16 -0
- data/spec/opbeat/sql_summarizer_spec.rb +6 -0
- data/spec/opbeat/subscriber_spec.rb +83 -0
- data/spec/opbeat/trace_spec.rb +43 -0
- data/spec/opbeat/transaction_spec.rb +98 -0
- data/spec/opbeat/util/inspector_spec.rb +40 -0
- data/spec/opbeat/util_spec.rb +20 -0
- data/spec/opbeat/worker_spec.rb +54 -0
- data/spec/opbeat_spec.rb +49 -0
- data/spec/spec_helper.rb +79 -6
- metadata +89 -149
- data/Makefile +0 -3
- data/gemfiles/rails30.gemfile +0 -9
- data/gemfiles/rails31.gemfile +0 -9
- data/gemfiles/rails32.gemfile +0 -9
- data/gemfiles/rails40.gemfile +0 -9
- data/gemfiles/rails41.gemfile +0 -9
- data/gemfiles/rails42.gemfile +0 -9
- data/gemfiles/ruby192_rails31.gemfile +0 -10
- data/gemfiles/ruby192_rails32.gemfile +0 -10
- data/gemfiles/sidekiq31.gemfile +0 -11
- data/lib/opbeat/better_attr_accessor.rb +0 -44
- data/lib/opbeat/event.rb +0 -223
- data/lib/opbeat/integrations/resque.rb +0 -22
- data/lib/opbeat/integrations/sidekiq.rb +0 -32
- data/lib/opbeat/interfaces.rb +0 -35
- data/lib/opbeat/interfaces/exception.rb +0 -16
- data/lib/opbeat/interfaces/http.rb +0 -57
- data/lib/opbeat/interfaces/message.rb +0 -19
- data/lib/opbeat/interfaces/stack_trace.rb +0 -50
- data/lib/opbeat/linecache.rb +0 -25
- data/lib/opbeat/logger.rb +0 -21
- data/lib/opbeat/rack.rb +0 -46
- data/lib/opbeat/rails/middleware/debug_exceptions_catcher.rb +0 -22
- data/lib/opbeat/railtie.rb +0 -26
- data/spec/opbeat/better_attr_accessor_spec.rb +0 -99
- data/spec/opbeat/event_spec.rb +0 -138
- data/spec/opbeat/integrations/delayed_job_spec.rb +0 -38
- data/spec/opbeat/logger_spec.rb +0 -55
- data/spec/opbeat/opbeat_spec.rb +0 -64
- data/spec/opbeat/rack_spec.rb +0 -117
@@ -0,0 +1,32 @@
|
|
1
|
+
module Opbeat
|
2
|
+
module Injections
|
3
|
+
module Sequel
|
4
|
+
class Injector
|
5
|
+
KIND = 'db.sequel.sql'.freeze
|
6
|
+
|
7
|
+
def self.sql_parser
|
8
|
+
@sql_parser ||= SqlSummarizer.new(nil)
|
9
|
+
end
|
10
|
+
|
11
|
+
def install
|
12
|
+
require 'sequel/database/logging'
|
13
|
+
|
14
|
+
::Sequel::Database.class_eval do
|
15
|
+
alias log_yield_without_opb log_yield
|
16
|
+
|
17
|
+
def log_yield sql, args = nil, &block
|
18
|
+
log_yield_without_opb(sql, *args) do
|
19
|
+
sig = Opbeat::Injections::Sequel::Injector.sql_parser.signature_for(sql)
|
20
|
+
Opbeat.trace(sig, KIND, sql: sql) do
|
21
|
+
block.call
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
register 'Sequel', 'sequel', Sequel::Injector.new
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module Opbeat
|
2
|
+
module Injections
|
3
|
+
module Sinatra
|
4
|
+
class Injector
|
5
|
+
def install
|
6
|
+
::Sinatra::Base.class_eval do
|
7
|
+
alias dispatch_without_opb! dispatch!
|
8
|
+
alias compile_template_with_opb compile_template
|
9
|
+
|
10
|
+
def dispatch!(*args, &block)
|
11
|
+
dispatch_without_opb!(*args, &block).tap do
|
12
|
+
if route = env['sinatra.route']
|
13
|
+
Opbeat.transaction(nil).endpoint = route
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def compile_template engine, data, opts, *args, &block
|
19
|
+
case data
|
20
|
+
when Symbol
|
21
|
+
opts[:__opbeat_template_sig] = data.to_s
|
22
|
+
else
|
23
|
+
opts[:__opbeat_template_sig] = "Inline #{engine}"
|
24
|
+
end
|
25
|
+
|
26
|
+
compile_template_with_opb(engine, data, opts, *args, &block)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
module Tilt
|
34
|
+
class Injector
|
35
|
+
KIND = 'template.view'
|
36
|
+
|
37
|
+
def install
|
38
|
+
::Tilt::Template.class_eval do
|
39
|
+
alias render_without_opb render
|
40
|
+
|
41
|
+
def render(*args, &block)
|
42
|
+
sig = options[:__opbeat_template_sig] || 'Uknown template'.freeze
|
43
|
+
|
44
|
+
Opbeat.trace sig, KIND do
|
45
|
+
render_without_opb(*args, &block)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
register 'Sinatra::Base', 'sinatra/base', Sinatra::Injector.new
|
54
|
+
register 'Tilt::Template', 'tilt/template', Tilt::Injector.new
|
55
|
+
end
|
56
|
+
end
|
@@ -1,6 +1,5 @@
|
|
1
|
-
require 'capistrano'
|
2
|
-
|
3
1
|
module Opbeat
|
2
|
+
# @api private
|
4
3
|
module Capistrano
|
5
4
|
def self.load_into(configuration)
|
6
5
|
|
@@ -17,13 +16,13 @@ module Opbeat
|
|
17
16
|
puts "Skipping Opbeat deployment notification because scm is not git."
|
18
17
|
next
|
19
18
|
end
|
20
|
-
|
19
|
+
|
21
20
|
branches = capture("cd #{current_release}; /usr/bin/env git branch --contains #{current_revision}").split
|
22
21
|
if branches.length == 1
|
23
22
|
branch = branch[0].sub("* ")
|
24
23
|
else
|
25
24
|
branch = nil
|
26
|
-
end
|
25
|
+
end
|
27
26
|
|
28
27
|
notify_command = "cd #{current_release}; REV=#{current_revision} "
|
29
28
|
notify_command << "BRANCH=#{branch} " if branch
|
@@ -32,9 +31,9 @@ module Opbeat
|
|
32
31
|
notify_command << "RAILS_ENV=#{rails_env} "
|
33
32
|
|
34
33
|
executable = fetch(:rake, 'bundle exec rake ')
|
35
|
-
notify_command << "#{executable} opbeat:
|
34
|
+
notify_command << "#{executable} opbeat:release"
|
36
35
|
capture notify_command, :once => true
|
37
|
-
|
36
|
+
|
38
37
|
end
|
39
38
|
end
|
40
39
|
end
|
@@ -45,3 +44,4 @@ end
|
|
45
44
|
if Capistrano::Configuration.instance
|
46
45
|
Opbeat::Capistrano.load_into(Capistrano::Configuration.instance)
|
47
46
|
end
|
47
|
+
|
@@ -1,11 +1,11 @@
|
|
1
1
|
namespace :opbeat do
|
2
|
-
desc "Notifies Opbeat of new
|
2
|
+
desc "Notifies Opbeat of new releases"
|
3
3
|
task :notify do
|
4
4
|
on roles(:app) do
|
5
5
|
|
6
6
|
scm = fetch(:scm)
|
7
7
|
if scm.to_s != "git"
|
8
|
-
info "Skipping Opbeat
|
8
|
+
info "Skipping Opbeat release because scm is not git."
|
9
9
|
next
|
10
10
|
end
|
11
11
|
|
@@ -14,7 +14,7 @@ namespace :opbeat do
|
|
14
14
|
|
15
15
|
within release_path do
|
16
16
|
with rails_env: fetch(:rails_env), rev: rev, branch: branch do
|
17
|
-
capture :rake, 'opbeat:
|
17
|
+
capture :rake, 'opbeat:release'
|
18
18
|
end
|
19
19
|
end
|
20
20
|
end
|
@@ -3,23 +3,19 @@ begin
|
|
3
3
|
rescue LoadError
|
4
4
|
end
|
5
5
|
|
6
|
-
# Based on the Sentry equivalent.
|
7
6
|
if defined?(Delayed)
|
8
|
-
|
9
7
|
module Delayed
|
10
8
|
module Plugins
|
11
|
-
class Opbeat <
|
9
|
+
class Opbeat < Delayed::Plugin
|
12
10
|
callbacks do |lifecycle|
|
13
11
|
lifecycle.around(:invoke_job) do |job, *args, &block|
|
14
12
|
begin
|
15
|
-
# Forward the call to the next callback in the callback chain
|
16
13
|
block.call(job, *args)
|
17
|
-
|
14
|
+
rescue ::Opbeat::Error
|
15
|
+
raise # don't report Opbeat errors
|
18
16
|
rescue Exception => exception
|
19
|
-
|
20
|
-
|
21
|
-
# Make sure we propagate the failure!
|
22
|
-
raise exception
|
17
|
+
::Opbeat.report exception
|
18
|
+
raise
|
23
19
|
end
|
24
20
|
end
|
25
21
|
end
|
@@ -27,6 +23,5 @@ if defined?(Delayed)
|
|
27
23
|
end
|
28
24
|
end
|
29
25
|
|
30
|
-
# Register DelayedJob Opbeat plugin
|
31
26
|
Delayed::Worker.plugins << Delayed::Plugins::Opbeat
|
32
|
-
end
|
27
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Opbeat
|
2
|
+
module Integration
|
3
|
+
module Rails
|
4
|
+
module InjectExceptionsCatcher
|
5
|
+
def self.included(cls)
|
6
|
+
cls.send(:alias_method_chain, :render_exception, :opbeat)
|
7
|
+
end
|
8
|
+
|
9
|
+
def render_exception_with_opbeat(env, exception)
|
10
|
+
begin
|
11
|
+
Opbeat.report(exception, rack_env: env) if Opbeat.started?
|
12
|
+
rescue
|
13
|
+
::Rails::logger.error "** [Opbeat] Error capturing or sending exception #{$!}"
|
14
|
+
::Rails::logger.debug $!.backtrace.join("\n")
|
15
|
+
end
|
16
|
+
|
17
|
+
render_exception_without_opbeat(env, exception)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'opbeat'
|
2
|
+
require 'rails'
|
3
|
+
|
4
|
+
module Opbeat
|
5
|
+
class Railtie < Rails::Railtie
|
6
|
+
|
7
|
+
config.opbeat = ActiveSupport::OrderedOptions.new
|
8
|
+
# bootstrap options with the defaults
|
9
|
+
Configuration::DEFAULTS.each { |k,v| config.opbeat[k] = v }
|
10
|
+
|
11
|
+
initializer "opbeat.configure" do |app|
|
12
|
+
config = Configuration.new app.config.opbeat do |conf|
|
13
|
+
conf.logger = Rails.logger
|
14
|
+
conf.view_paths = app.config.paths['app/views'].existent
|
15
|
+
end
|
16
|
+
|
17
|
+
if config.enabled_environments.include?(Rails.env)
|
18
|
+
if Opbeat.start!(config)
|
19
|
+
app.config.middleware.insert 0, Middleware
|
20
|
+
Rails.logger.info "** [Opbeat] Client running"
|
21
|
+
else
|
22
|
+
# :nocov:
|
23
|
+
Rails.logger.info "** [Opbeat] Failed to start"
|
24
|
+
# :nocov:
|
25
|
+
end
|
26
|
+
else
|
27
|
+
# :nocov:
|
28
|
+
Rails.logger.info "** [Opbeat] Disabled in #{Rails.env} environment"
|
29
|
+
# :nocov:
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
config.after_initialize do
|
34
|
+
# :nocov:
|
35
|
+
require 'opbeat/integration/rails/inject_exceptions_catcher'
|
36
|
+
if defined?(ActionDispatch::DebugExceptions)
|
37
|
+
ActionDispatch::DebugExceptions.send(
|
38
|
+
:include, Opbeat::Integration::Rails::InjectExceptionsCatcher)
|
39
|
+
elsif defined?(::ActionDispatch::ShowExceptions)
|
40
|
+
::ActionDispatch::ShowExceptions.send(
|
41
|
+
:include, Opbeat::Integration::Rails::InjectExceptionsCatcher)
|
42
|
+
end
|
43
|
+
# :nocov:
|
44
|
+
end
|
45
|
+
|
46
|
+
rake_tasks do
|
47
|
+
# :nocov:
|
48
|
+
require 'opbeat/tasks'
|
49
|
+
# :nocov:
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
begin
|
2
|
+
require 'sidekiq'
|
3
|
+
rescue LoadError
|
4
|
+
end
|
5
|
+
|
6
|
+
if defined? Sidekiq
|
7
|
+
module Opbeat
|
8
|
+
module Integration
|
9
|
+
class Sidekiq
|
10
|
+
def call worker, msg, queue
|
11
|
+
begin
|
12
|
+
yield
|
13
|
+
rescue Exception => exception
|
14
|
+
if [Interrupt, SystemExit, SignalException].include? exception.class
|
15
|
+
raise exception
|
16
|
+
end
|
17
|
+
|
18
|
+
Opbeat.report exception
|
19
|
+
|
20
|
+
raise
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
Sidekiq.configure_server do |config|
|
28
|
+
if Sidekiq::VERSION.to_i < 3
|
29
|
+
config.server_middleware do |chain|
|
30
|
+
chain.add Opbeat::Integration::Sidekiq
|
31
|
+
end
|
32
|
+
else
|
33
|
+
config.error_handlers << lambda do |exception|
|
34
|
+
Opbeat.report exception
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Opbeat
|
2
|
+
# @api private
|
3
|
+
class LineCache
|
4
|
+
|
5
|
+
CACHE = {}
|
6
|
+
|
7
|
+
def self.all path
|
8
|
+
CACHE[path] ||= begin
|
9
|
+
File.readlines(path)
|
10
|
+
rescue
|
11
|
+
[]
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.find path, line
|
16
|
+
return nil if line < 1
|
17
|
+
all(path)[line - 1]
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Opbeat
|
2
|
+
# @api private
|
3
|
+
module Logging
|
4
|
+
PREFIX = "** [Opbeat] ".freeze
|
5
|
+
|
6
|
+
def debug *args, &block
|
7
|
+
config.logger.debug(log_message(*args, &block)) if has_logger?
|
8
|
+
end
|
9
|
+
|
10
|
+
def info *args, &block
|
11
|
+
config.logger.info(log_message(*args, &block)) if has_logger?
|
12
|
+
end
|
13
|
+
|
14
|
+
def warn *args, &block
|
15
|
+
config.logger.warn(log_message(*args, &block)) if has_logger?
|
16
|
+
end
|
17
|
+
|
18
|
+
def error *args, &block
|
19
|
+
config.logger.error(log_message(*args, &block)) if has_logger?
|
20
|
+
end
|
21
|
+
|
22
|
+
def fatal *args, &block
|
23
|
+
config.logger.fatal(log_message(*args, &block)) if has_logger?
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def has_logger?
|
29
|
+
respond_to?(:config) && config && config.logger
|
30
|
+
end
|
31
|
+
|
32
|
+
def log_message *args, &block
|
33
|
+
msg = block_given? && block.call || args.first
|
34
|
+
"#{PREFIX}#{msg}"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module Opbeat
|
2
|
+
class Middleware
|
3
|
+
def initialize app
|
4
|
+
@app = app
|
5
|
+
end
|
6
|
+
|
7
|
+
def call env
|
8
|
+
begin
|
9
|
+
transaction = Opbeat.transaction "Rack", "app.rack.request"
|
10
|
+
resp = @app.call env
|
11
|
+
resp[2] = BodyProxy.new(resp[2]) { transaction.submit(resp[0]) } if transaction
|
12
|
+
rescue Error
|
13
|
+
raise # Don't report Opbeat errors
|
14
|
+
rescue Exception => e
|
15
|
+
Opbeat.report e, rack_env: env
|
16
|
+
transaction.submit(500) if transaction
|
17
|
+
raise
|
18
|
+
ensure
|
19
|
+
transaction.release if transaction
|
20
|
+
end
|
21
|
+
|
22
|
+
if error = env['rack.exception'] || env['sinatra.error']
|
23
|
+
Opbeat.report error, rack_env: env
|
24
|
+
end
|
25
|
+
|
26
|
+
resp
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class BodyProxy
|
31
|
+
def initialize body, &block
|
32
|
+
@body, @block, @closed = body, block, false
|
33
|
+
end
|
34
|
+
|
35
|
+
def respond_to? *args
|
36
|
+
super || @body.respond_to?(*args)
|
37
|
+
end
|
38
|
+
|
39
|
+
def close
|
40
|
+
return if closed?
|
41
|
+
|
42
|
+
@closed = true
|
43
|
+
|
44
|
+
begin
|
45
|
+
@body.close if @body.respond_to?(:close)
|
46
|
+
ensure
|
47
|
+
@block.call
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def closed?
|
52
|
+
@closed
|
53
|
+
end
|
54
|
+
|
55
|
+
def method_missing *args, &block
|
56
|
+
@body.__send__(*args, &block)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|