PATCH fetch json body to Rails - ruby-on-rails

I'm PATCHing a form to Rails through fetch, but some attributes are not handled properly on the server side.
I'm running Rails 5.2.2 over Ruby ruby 2.5.1p57
When I post the data to the server, I get this console.log output in browser:
{id: 10172, weekday: 1, is_only_private: false, is_allow_forced: false, from_time: 08:00, to_time: 09:00, act_ids: [10001, 10002], customer_id: 10000, consultation_id: 10000}
But on the server side I can see this log on console:
Parameters: {"id"=>"10172", "weekday"=>1, "is_only_private"=>false, "is_allow_forced"=>false, "from_time"=>"08:00", "to_time"=>"09:00", "act_ids"=>[10001, 10002], "customer_id"=>"10000", "consultation_id"=>"10000", "timetable"=>{"id"=>"10172", "weekday"=>1, "is_only_private"=>false, "is_allow_forced"=>false, "from_time"=>"08:00", "to_time"=>"09:00"}}
act_ids disappears inside timetable attribute
My app it's hybrid, it's responding with HTML and JSON (even XML) in the same routes.
Question:
Is not it solved in this version of Rails yet?
Workaround with this
def timetable_params
my_params = params.require(:timetable).permit :weekday,
:is_only_private,
:is_allow_forced,
:from_time,
:to_time,
act_ids: []
my_params[:act_ids] ||= params[:act_ids]
my_params
end

Probably you permitted it as permit(..., :to_time, :act_ids), but array should be permitted via act_ids: [], which you already have in your timetable_params code
On clean application this works correctly:
#!/usr/bin/env ruby
# frozen_string_literal: true
require "bundler/inline"
gemfile(!!ENV['INSTALL']) do
source "https://rubygems.org"
gem 'rails', '5.2.2'
end
require "action_controller/railtie"
class TestApp < Rails::Application
config.root = __dir__
config.eager_load = false
config.session_store :cookie_store, key: "cookie_store_key"
secrets.secret_key_base = "secret_key_base"
config.logger = Logger.new($stdout)
Rails.logger = config.logger
end
TestApp.initialize!
TestApp.routes.draw{ resources :timetables, only: :update }
class TimetablesController < ActionController::Base
include Rails.application.routes.url_helpers
wrap_parameters format: [:json, :xml]
def update
render json: timetable_params
end
def timetable_params
params.require(:timetable).permit(:weekday,
:is_only_private,
:is_allow_forced,
:from_time,
:to_time,
act_ids: [])
end
end
require "minitest/autorun"
require "rack/test"
class BugTest < Minitest::Test
include Rack::Test::Methods
def test_returns_success
payload = {
"id"=>"10172", "weekday"=>1, "is_only_private"=>false, "is_allow_forced"=>false,
"from_time"=>"08:00", "to_time"=>"09:00",
"act_ids"=>[10001, 10002],
"customer_id"=>"10000", "consultation_id"=>"10000"
}
patch "/timetables/10172.json", payload.to_json, { 'CONTENT_TYPE' => 'application/json' }
assert last_response.ok?
puts "resp body: #{last_response.body}"
resp = JSON.parse(last_response.body)
assert_includes(resp.keys, "act_ids")
end
private def app
Rails.application
end
end
produces
Run options: --seed 62182
# Running:
I, [2019-06-14T16:04:02.967561 #44749] INFO -- : Started PATCH "/timetables/10172.json" for 127.0.0.1 at 2019-06-14 16:04:02 +0300
I, [2019-06-14T16:04:02.971039 #44749] INFO -- : Processing by TimetablesController#update as JSON
I, [2019-06-14T16:04:02.971161 #44749] INFO -- : Parameters: {"id"=>"10172", "weekday"=>1, "is_only_private"=>false, "is_allow_forced"=>false, "from_time"=>"08:00", "to_time"=>"09:00", "act_ids"=>[10001, 10002], "customer_id"=>"10000", "consultation_id"=>"10000", "timetable"=>{"id"=>"10172", "weekday"=>1, "is_only_private"=>false, "is_allow_forced"=>false, "from_time"=>"08:00", "to_time"=>"09:00", "act_ids"=>[10001, 10002], "customer_id"=>"10000", "consultation_id"=>"10000"}}
D, [2019-06-14T16:04:02.971850 #44749] DEBUG -- : Unpermitted parameters: :id, :customer_id, :consultation_id
I, [2019-06-14T16:04:02.972484 #44749] INFO -- : Completed 200 OK in 1ms (Views: 0.4ms)
resp body: {"weekday":1,"is_only_private":false,"is_allow_forced":false,"from_time":"08:00","to_time":"09:00","act_ids":[10001,10002]}
.
Finished in 0.020124s, 49.6919 runs/s, 149.0757 assertions/s.
1 runs, 3 assertions, 0 failures, 0 errors, 0 skips

Related

WARN: NameError: uninitialized constant DeliveryMethods when moving code to ActiveJob

I have code to send invites to members of my website and it can be sent via email, real-time notification or One Signal. The code works great in development until I move the invite to an ActiveJob to be processed in the background using Sidekiq and Redis. I am doing this only for when a maintainer of an organization uploads a CSV file of contacts to invite to their organization. (Thus the background job as some clients wish to invite around 10,000+ people which would bog the system if done within the controller.)
If I move the task to an ActiveJob, I get this error in the Sidekiq output:
WARN: NameError: uninitialized constant DeliveryMethods
I thought that this was because I didn't put a require statement in the ActiveJob, so I added this to the top of the ActiveJob:
require 'application_notification'
But, I get the same error message.
I am at a loss. Any help would be greatly appreciated. Here are code snippets. Please let me know if you need anything else.
Versions
Ruby:'3.0.2'
Rails: 7.0.0.alpha
gem 'rails', :github => 'rails/rails', :branch => 'main'
Redis: '~> 4.1.3'
Sidekiq: '6.0.7'
Result Output
# Terminal Output
Started POST "/import_wizard/organization/1" for ::1 at 2021-08-10 16:47:30 -0700
Processing by InvitationsController#invite_imports as JS
Parameters: {"authenticity_token"=>"--REDACTED--", "invitable_type"=>"organization", "invitable_id"=>"1"}
Member Load (1.1ms) SELECT "members".* FROM "members" WHERE "members"."id" = $1 ORDER BY "members"."id" ASC LIMIT $2 [["id", 1], ["LIMIT", 1]]
↳ app/controllers/concerns/cookies_concern.rb:171:in `load_cookies'
Organization Load (1.3ms) SELECT "organizations".* FROM "organizations" WHERE "organizations"."id" = $1 LIMIT $2 [["id", 1], ["LIMIT", 1]]
↳ app/controllers/invitations_controller.rb:216:in `set_invitable'
ImportResult Load (0.8ms) SELECT "import_results".* FROM "import_results" WHERE "import_results"."invitable_id" = $1 AND "import_results"."status" = $2 LIMIT $3 [["invitable_id", 1], ["status", 1], ["LIMIT", 1]]
↳ app/controllers/invitations_controller.rb:261:in `set_imports_to_invite'
ImportRecord Load (0.7ms) SELECT "import_records".* FROM "import_records" WHERE "import_records"."import_result_id" = $1 AND "import_records"."status" = $2 [["import_result_id", 32], ["status", "ready"]]
↳ app/controllers/invitations_controller.rb:155:in `invite_imports'
[ActiveJob] Enqueued InviteImportedMembersJob (Job ID: 69355585-cfef-4f1f-bf90-eae0f24d5f98) to Sidekiq(imports) with arguments:
#<GlobalID:0x00007fbeba81d0a0 #uri=#<URI::GID gid://prayer-nook/Organization/1>>,
[#<GlobalID:0x00007fbeba81c6a0 #uri=#<URI::GID gid://prayer-nook/ImportRecord/309>>,
#<GlobalID:0x00007fbeba817d08 #uri=#<URI::GID gid://prayer-nook/ImportRecord/310>>,
#<GlobalID:0x00007fbeba817470 #uri=#<URI::GID gid://prayer-nook/ImportRecord/311>>,
#<GlobalID:0x00007fbeba816d68 #uri=#<URI::GID gid://prayer-nook/ImportRecord/312>>,
#<GlobalID:0x00007fbeba816250 #uri=#<URI::GID gid://prayer-nook/ImportRecord/313>>,
#<GlobalID:0x00007fbeba8157b0 #uri=#<URI::GID gid://prayer-nook/ImportRecord/318>>,
#<GlobalID:0x00007fbeba814a40 #uri=#<URI::GID gid://prayer-nook/ImportRecord/319>>],
#<GlobalID:0x00007fbeb9a5f198 #uri=#<URI::GID gid://prayer-nook/Member/1>>
Rendering invitations/invite_imports.js.erb
Rendered invitations/invite_imports.js.erb (Duration: 0.1ms | Allocations: 10)
Completed 200 OK in 317ms (Views: 3.4ms | ActiveRecord: 95.8ms | Allocations: 57969)
Controller Action
The commented out line for invite_imports_task is a method I made within the controller with the exact same code that runs in the ActiveJob, but works. So, I know that the code works, its just moving to an ActiveJob that is now causing the issue.
# InvitationsController#invite_imports
# app/controllers/invitations_controller.rb
def invite_imports
set_invitable
set_imports_to_invite
#import_step = 4
imports_to_invite_array = []
#imports_to_invite.each do |record|
imports_to_invite_array << record
end
InviteImportedMembersJob.perform_later(#invitable, imports_to_invite_array, #authenticated_member)
# invite_imports_task(#invitable, imports_to_invite_array, #authenticated_member)
respond_to do |format|
format.js
end
end
Active Job
# app/jobs/invite_imported_members_job.rb
class InviteImportedMembersJob < ApplicationJob
require 'application_notification'
queue_as :imports
def perform(invitable, imports_to_invite, sender)
set_import_result(invitable)
imported_emails = imports_to_invite.map {|member| member[:email]}
member_list = Member.where(email: imported_emails)
member_email_list = member_list.pluck(:email)
non_member_email_list = imported_emails - member_email_list
sent_invites = []
error_in_sending_invites = []
member_list.each do |member|
invitation = Invitation.new(invitable: invitable, sender: sender, recipient:member)
if invitation.save
invitable.invited_members << member
sent_invites << member.email
else
error_in_sending_invites << member.email
end
end
non_member_email_list.each do |member|
InvitationMailer.with(recipient_email: member, sender: sender).app_invitation.deliver_later
waitlist = InvitationWaitlist.create(email: member, invitable: invitable, sender: sender)
# in this case the member variable is only an email address
if waitlist.save
sent_invites << member
else
error_in_sending_invites << member
end
end
update_import_records(invitable, sent_invites, error_in_sending_invites)
update_import_result
create_cue_notification(invitable)
end
private
def set_import_result(invitable)
#import_result = ImportResult.find_by(invitable:invitable, status: 'waiting')
end
def update_import_records(invitable, sent_invites, error_in_sending_invites)
if sent_invites.count > 0
ImportRecord.where(import_result_id:#import_result.id, email: sent_invites).update_all(status:'sent')
end
if error_in_sending_invites.count > 0
ImportRecord.where(import_result_id:#import_result.id, email: error_in_sending_invites).update_all(status:'error_in_sending')
end
end
def update_import_result
#import_result.completed!
end
def create_cue_notification(invitable)
hide_old_cues(invitable)
CueService.new(#import_result, set_cue_recipients(invitable), false).call!
end
def hide_old_cues(invitable)
Cue.where(cueable: #import_result).update(status:'hidden')
end
def set_cue_recipients(invitable)
if invitable.is_a?(Organization)
return invitable.maintainers
elsif invitable.is_a?(Group)
return invitable.owner
else
return nil
end
end
end
Application Notification
# app/notifications/application_notification.rb
class ApplicationNotification < Noticed::Base
deliver_by :database, format: :format_for_database
deliver_by :action_cable, channel: 'NotificationsChannel', format: :format_for_action_cable
deliver_by :one_signal, class: "DeliveryMethods::OneSignal", format: :format_for_one_signal
def format_for_database
{
type: self.class.name,
params: params
}
end
end
DeliveryMethod::OneSignal
# app/notifications/delivery_methods/one_signal.rb
class DeliveryMethods::OneSignal < Noticed::DeliveryMethods::Base
def deliver
return unless app_id.present? && one_signal_url.present? && player_id.present?
params = {"app_id" => app_id,
"contents" => {"en" => message},
"headings" => {"en" => "Prayer Nook"},
"include_player_ids" => [player_id],
"data" => data
}
uri = URI.parse(one_signal_url)
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
request = Net::HTTP::Post.new(uri.path,'Content-Type' => 'application/json;charset=utf-8')
request.body = params.as_json.to_json
response = http.request(request)
puts "OneSignal response: #{response.body}"
end
private
def app_id
ENV['ONE_SIGNAL_APP_ID']
end
def one_signal_url
ENV['ONE_SIGNAL_API_URL']
end
def player_id
recipient.site_profile.one_signal_id
end
def message
if (method = options[:format])
notification.send(method)[:message]
else
"Message from Prayer Nook"
end
end
def data
if (method = options[:format])
notification.send(method)[:data]
else
{ }
end
end
end
From the Invitation Model
## app/models/invitation.rb
def send_notifications
if self.invitable_type == 'Group'
GroupInvitationNotification.with(invitation: self, group: self.invitable, sender: self.sender).deliver_later(self.recipient)
elsif self.invitable_type == 'Organization'
OrgInvitationNotification.with(invitation: self, organization: self.invitable, sender: self.sender).deliver_later(self.recipient)
end
end
OrgInvitationNotification
# app/notifications/org_invitation_notification.rb
class OrgInvitationNotification < ApplicationNotification
# this class inherits other delivery methods from ApplicationNotification: database, action_cable, and one_signal
deliver_by :email, mailer: "InvitationMailer", method: :org_invitation, if: :immediate_email_notifications?
# required params
param :invitation
param :organization
param :sender
# helper methods to make rendering easier.
def format_for_action_cable
html = ApplicationController.render(
partial: 'notifications/toast',
locals: { header: "You've been invited",
message: message,
link_path: invitation_path(params[:invitation])
}
)
params.merge(html: html)
end
def format_for_one_signal
{
message: message,
data: { page: 'invitation', id: params[:invitation].id }
}
end
def immediate_email_notifications?
recipient.site_profile.invitations_email_notifications == 'immediately'
end
def message
t(".message", sender: params[:sender].full_name, org_name: params[:organization].name)
end
def url
invitation_url(params[:invitation])
end
end
Update
New code block per #LamPhan's comments:
# From config/application.rb
class Application < Rails::Application
# Initialize configuration defaults for originally generated Rails version.
config.load_defaults 6.0
config.active_job.queue_adapter = :sidekiq
config.active_record.encryption.support_unencrypted_data = true
config.active_record.legacy_connection_handling = false
# Settings in config/environments/* take precedence over those specified here.
# Application configuration can go into files in config/initializers
# -- all .rb files in that directory are automatically loaded after loading
# the framework and any gems in your application.
config.generators do |g|
g.test_framework :rspec,
fixtures: false,
view_specs: false,
helper_specs: false,
routing_specs: false
end
config.autoloader = :classic
end
According to comments, you're using :classic loader and your project're running on Rails 7.0.
Base on this comment (of the creator of sidekiq): Sidekiq does not work with the classic autoloader in Rails 6 at all.
so you should use :zeitwerk loader.

failed rspec when upgrade ruby version from 1.9.3 to 2.0.0

I'm upgrading ruby version from 1.9.3 to 2.0.0, however, when version ruby 2.0.0, it's failed as below:
undefined method `ancestors' for #<Delayed::Backend::ActiveRecord::Job:0x00557c667a4c10>
Here my test case:
it 'responds successfully when params is valid and allow_reply_all of bbs_message equal 1' do
request.env['Access-Token'] = 'token_key'
delayed_job = XMPPBbsChatroomSync.delay(queue: 'xmpp_request')
xmpp_bbs_chatroom_sync = class_double(XMPPBbsChatroomSync).as_stubbed_const(delay: true)
expect(delayed_job).to receive(:delete_users).with(params_queue).and_return(true)
expect(xmpp_bbs_chatroom_sync).to receive(:delay)
.with(queue: 'xmpp_request').and_return(delayed_job)
expect do
get :get, type: 'delete', access_token: 'token_key', bbs_message_id: '2'
bbs_message_support.reload
end.to change(bbs_message_support, :is_deleted).from(false).to(true)
expect(response.status).to eq 200
expect(response.content_type).to eq 'application/json'
expect(response.body).to eq msg_success.to_json
end
it is failed in code line: expect(delayed_job).to receive(:delete_users).with(params_queue).and_return(true)
In ruby version 1.9.3, it's still pass, so anyone help me ??
thanks for your attention.
here my code in XMPPBbsChatroomSync
require "net/http"
require "uri"
require "octopus_http_helper"
class XMPPBbsChatroomSync
#http_helper = OctopusHttpHelper.new(XMPP_ADDRESS)
#port = XMPP_PORT
#timeout = 10 # second
#jid = XMPP_ADDRESS_FOR_JID
#path = XMPP_BBS_CHATROOM_SYNC_PATH
#servicename_path = XMPP_BBS_CHATROOM_SYNC_SERVICENAME_PATH
#basic_authorize = XMPP_REST_API_BASIC_AUTH
class << self
def delete_users(params)
response = #http_helper.until_success { |address|
params[:room_ids].each { |room_id|
params[:user_ids].each { |user_id|
http = Net::HTTP.new(address, #port)
request = Net::HTTP::Delete.new("#{#path}/#{room_id}/owners/#{user_id}##{#jid}?servicename=muc", 'Authorization' => #basic_authorize)
unless response.kind_of? Net::HTTPSuccess
raise 'DeleteUserFromChatroomError'
end
}
}
}
end
end
end

How to get XML doc from downloaded zip file in rails

I have used Typhoeus to stream a zip file to memory, then am iterating through each file to extract the XML doc. To read the XML file I used Nokogiri, but am getting an error, Errno::ENOENT: No such file or directory # rb_sysopen - my_xml_doc.xml.
I looked up the error and saw that ruby is most likely running the script in the wrong directory. I am a little confused, do I need to save the XML doc to memory first before I can read it as well?
Here is my code to clarify further:
Controller
def index
url = "http://feed.omgili.com/5Rh5AMTrc4Pv/mainstream/posts/"
html_response = Typhoeus.get(url)
doc = Nokogiri::HTML(html_response.response_body)
path_array = []
doc.search("a").each do |value|
path_array << value.content if value.content.include?(".zip")
end
path_array.each do |zip_link|
download_file = File.open zip_link, "wb"
request = Typhoeus::Request.new("#{url}#{zip_link}")
binding.pry
request.on_headers do |response|
if response.code != 200
raise "Request failed"
end
end
request.on_body do |chunk|
download_file.write(chunk)
end
request.run
Zip::File.open(download_file) do |zipfile|
zipfile.each do |file|
binding.pry
doc = Nokogiri::XML(File.read(file.name))
end
end
end
end
file
=> #<Zip::Entry:0x007ff88998373
#comment="",
#comment_length=0,
#compressed_size=49626,
#compression_method=8,
#crc=20393847,
#dirty=false,
#external_file_attributes=0,
#extra={},
#extra_length=0,
#filepath=nil,
#follow_symlinks=false,
#fstype=0,
#ftype=:file,
#gp_flags=2056,
#header_signature=009890,
#internal_file_attributes=0,
#last_mod_date=18769,
#last_mod_time=32626,
#local_header_offset=0,
#local_header_size=nil,
#name="my_xml_doc.xml",
#name_length=36,
#restore_ownership=false,
#restore_permissions=false,
#restore_times=true,
#size=138793,
#time=2016-10-17 15:59:36 -0400,
#unix_gid=nil,
#unix_perms=nil,
#unix_uid=nil,
#version=20,
#version_needed_to_extract=20,
#zipfile="some_zip_file.zip">
This is the solution I came up with:
Gems:
gem 'typhoeus'
gem 'rubyzip'
gem 'redis', '~>3.2'
Controller:
def xml_to_redis_list(url)
html_response = Typhoeus.get(url)
doc = Nokogiri::HTML(html_response.response_body)
#redis = Redis.new
path_array = []
doc.search("a").each do |value|
path_array << value.content if value.content.include?(".zip")
end
path_array.each do |zip_link|
download_file = File.open zip_link, "wb"
request = Typhoeus::Request.new("#{url}#{zip_link}")
request.on_headers do |response|
if response.code != 200
raise "Request failed"
end
end
request.on_body do |chunk|
download_file.write(chunk)
end
request.run
while download_file.size == 0
sleep 1
end
zip_download = Zip::File.open(download_file.path)
Zip::File.open("#{Rails.root}/#{zip_download.name}") do |zip_file|
zip_file.each do |file|
xml_string = zip_file.read(file.name)
check_if_xml_duplicate(xml_string)
#redis.rpush("NEWS_XML", xml_string)
end
end
File.delete("#{Rails.root}/#{zip_link}")
end
end
def check_if_xml_duplicate(xml_string)
#redis.lrem("NEWS_XML", -1, xml_string)
end

HTTParty Google maps directions between origin and destination.

My goal: http://maps.googleapis.com/maps/api/directions/json?origin=montreal&destination=toronto&sensor=false
My class:
class GoogleMap
include HTTParty
base_uri 'http://maps.googleapis.com'
attr_accessor :origin, :destination
def initialize(service, page)
#options = { query: {origin: origin, destination: destination} }
end
def directions
self.class.get("/maps/api/directions/json", #options)
end
end
Currently when I run this on the console:
irb(main):001:0> g = GoogleMap.new("montreal", "toronto")
=> #<GoogleMap:0x007fcaeeb88538 #options={:query=>{:origin=>nil, :destination=>nil}}>
irb(main):002:0> g.directions
=> #<HTTParty::Response:0x7fcaeeb60b00 parsed_response={"error_message"=>"Invalid request. Missing the 'origin' parameter.", "routes"=>[]...
Problem is: {:query=>{:origin=>nil, :destination=>nil}} origin and destination are nil.
I would like to know how I would achieve:
irb(main):001:0> g = GoogleMap.new("montreal", "toronto")
=> #<GoogleMap:0x007fcaeeb88538 #options={:query=>{:origin=>montreal, :destination=>toronto}}
And then when I run:
g.directions I get output of http://maps.googleapis.com/maps/api/directions/json?origin=montreal&destination=toronto&sensor=false
Thank you in advance.
I think you might want to change your
def initialize(service, page)
to
def initialize(origin, destination)
Or you can do g.origin = "montreal" and g.destination = "toronto" before you call g.directions

Contextual Logging with Log4r

Here's how some of my existing logging code with Log4r is working. As you can see in the WorkerX::a_method, any time that I log a message I want the class name and the calling method to be included (I don't want all the caller history or any other noise, which was my purpose behind LgrHelper).
class WorkerX
include LgrHelper
def initialize(args = {})
#logger = Lgr.new({:debug => args[:debug], :logger_type => 'WorkerX'})
end
def a_method
error_msg("some error went down here")
# This prints out: "WorkerX::a_method - some error went down here"
end
end
class Lgr
require 'log4r'
include Log4r
def initialize(args = {}) # args: debug boolean, logger type
#debug = args[:debug]
#logger_type = args[:logger_type]
#logger = Log4r::Logger.new(#logger_type)
format = Log4r::PatternFormatter.new(:pattern => "%l:\t%d - %m")
outputter = Log4r::StdoutOutputter.new('console', :formatter => format)
#logger.outputters = outputter
if #debug then
#logger.level = DEBUG
else
#logger.level = INFO
end
end
def debug(msg)
#logger.debug(msg)
end
def info(msg)
#logger.info(msg)
end
def warn(msg)
#logger.warn(msg)
end
def error(msg)
#logger.error(msg)
end
def level
#logger.level
end
end
module LgrHelper
# This module should only be included in a class that has a #logger instance variable, obviously.
protected
def info_msg(msg)
#logger.info(log_intro_msg(self.method_caller_name) + msg)
end
def debug_msg(msg)
#logger.debug(log_intro_msg(self.method_caller_name) + msg)
end
def warn_msg(msg)
#logger.warn(log_intro_msg(self.method_caller_name) + msg)
end
def error_msg(msg)
#logger.error(log_intro_msg(self.method_caller_name) + msg)
end
def log_intro_msg(method)
msg = class_name
msg += '::'
msg += method
msg += ' - '
msg
end
def class_name
self.class.name
end
def method_caller_name
if /`(.*)'/.match(caller[1]) then # caller.first
$1
else
nil
end
end
end
I really don't like this approach. I'd rather just use the existing #logger instance variable to print the message and be smart enough to know the context. How can this, or similar simpler approach, be done?
My environment is Rails 2.3.11 (for now!).
After posting my answer using extend, (see "EDIT", below), I thought I'd try using set_trace_func to keep a sort of stack trace like in the discussion I posted to. Here is my final solution; the set_trace_proc call would be put in an initializer or similar.
#!/usr/bin/env ruby
# Keep track of the classes that invoke each "call" event
# and the method they called as an array of arrays.
# The array is in the format: [calling_class, called_method]
set_trace_func proc { |event, file, line, id, bind, klass|
if event == "call"
Thread.current[:callstack] ||= []
Thread.current[:callstack].push [klass, id]
elsif event == "return"
Thread.current[:callstack].pop
end
}
class Lgr
require 'log4r'
include Log4r
def initialize(args = {}) # args: debug boolean, logger type
#debug = args[:debug]
#logger_type = args[:logger_type]
#logger = Log4r::Logger.new(#logger_type)
format = Log4r::PatternFormatter.new(:pattern => "%l:\t%d - %m")
outputter = Log4r::StdoutOutputter.new('console', :formatter => format)
#logger.outputters = outputter
if #debug then
#logger.level = DEBUG
else
#logger.level = INFO
end
end
def debug(msg)
#logger.debug(msg)
end
def info(msg)
#logger.info(msg)
end
def warn(msg)
#logger.warn(msg)
end
def error(msg)
#logger.error(msg)
end
def level
#logger.level
end
def invoker
Thread.current[:callstack] ||= []
( Thread.current[:callstack][-2] || ['Kernel', 'main'] )
end
end
class CallingMethodLogger < Lgr
[:info, :debug, :warn, :error].each do |meth|
define_method(meth) { |msg| super("#{invoker[0]}::#{invoker[1]} - #{msg}") }
end
end
class WorkerX
def initialize(args = {})
#logger = CallingMethodLogger.new({:debug => args[:debug], :logger_type => 'WorkerX'})
end
def a_method
#logger.error("some error went down here")
# This prints out: "WorkerX::a_method - some error went down here"
end
end
w = WorkerX.new
w.a_method
I don't know how much, if any, the calls to the proc will affect the performance of an application; if it ends up being a concern, perhaps something not as intelligent about the calling class (like my old answer, below) will work better.
[EDIT: What follows is my old answer, referenced above.]
How about using extend? Here's a quick-and-dirty script I put together from your code to test it out; I had to reorder things to avoid errors, but the code is the same with the exception of LgrHelper (which I renamed CallingMethodLogger) and the second line of WorkerX's initializer:
#!/usr/bin/env ruby
module CallingMethodLogger
def info(msg)
super("#{#logger_type}::#{method_caller_name} - " + msg)
end
def debug(msg)
super("#{#logger_type}::#{method_caller_name} - " + msg)
end
def warn(msg)
super("#{#logger_type}::#{method_caller_name} - " + msg)
end
def error(msg)
super("#{#logger_type}::#{method_caller_name} - " + msg)
end
def method_caller_name
if /`(.*)'/.match(caller[1]) then # caller.first
$1
else
nil
end
end
end
class Lgr
require 'log4r'
include Log4r
def initialize(args = {}) # args: debug boolean, logger type
#debug = args[:debug]
#logger_type = args[:logger_type]
#logger = Log4r::Logger.new(#logger_type)
format = Log4r::PatternFormatter.new(:pattern => "%l:\t%d - %m")
outputter = Log4r::StdoutOutputter.new('console', :formatter => format)
#logger.outputters = outputter
if #debug then
#logger.level = DEBUG
else
#logger.level = INFO
end
end
def debug(msg)
#logger.debug(msg)
end
def info(msg)
#logger.info(msg)
end
def warn(msg)
#logger.warn(msg)
end
def error(msg)
#logger.error(msg)
end
def level
#logger.level
end
end
class WorkerX
def initialize(args = {})
#logger = Lgr.new({:debug => args[:debug], :logger_type => 'WorkerX'})
#logger.extend CallingMethodLogger
end
def a_method
#logger.error("some error went down here")
# This prints out: "WorkerX::a_method - some error went down here"
end
end
w = WorkerX.new
w.a_method
The output is:
ERROR: 2011-07-24 20:01:40 - WorkerX::a_method - some error went down here
The downside is, via this method, the caller's class name isn't automatically figured out; it's explicit based on the #logger_type passed into the Lgr instance. However, you may be able to use another method to get the actual name of the class--perhaps something like the call_stack gem or using Kernel#set_trace_func--see this thread.

Resources