Setting up an additional logger in Rails – how to instantiate only once? - ruby-on-rails

In addition to the default Rails logger, I'd like to create my own one. I will be using LogstashLogger, configured with a custom URL.
I created a file lib/custom_logstash_logger.rb with:
class CustomLogstashLogger
include ActiveSupport::Configurable
config_accessor :logstash_url
def self.log(message)
# ?
end
end
And I'm configuring it via config/initializers/custom_logstash_logger.rb:
CustomLogstashLogger.config.logstash_url = ENV.fetch("LOGSTASH_URL", nil)
Now, I could go ahead and define self.log such that I always create a new logger whenever the method is called:
def self.log(message)
logger = LogStashLogger.new(uri: self.config.logstash_url)
logger.info(message)
end
But that is hardly efficient—the logger is instantiated every time the log method is called.
What would be the ideal way to instantiate the LogStashLogger already when the class is configured, and not when the logger is used? I don't want to modify application.rb or set a global variable that holds the logger—it should be a class method.

As mentioned by Aleksei Matuishkin, one can use a lazily instantiated variable:
#logger ||= LogStashLogger.new(uri: self.config.logstash_url)
This allows creating the logger when first calling log.

Related

Globally accessible function without initialisation

Is it possible in Rails to have a "simple" function that is globally accessible?
do_something
=> "Of course"
I've tried adding an instance method to the Object class, but that adds that method to everything.
Yes. Just declare a method on Main (which is the global scope in Ruby) at any point in your application:
# config/application.rb
# ...
def do_something
end
You can reference the method explicitly with ::do_something but with the way that module nesting works in ruby any call will go up the module nesting to Main anyways.
Still is a dumb idea though as the code will not be reloaded in development and pollutes the global namespace. And since Main is on the module nesting of everything just like object you're adding a do_something method to all the objects in the system.
If you want to avoid that you would have to create a lambda/proc assigned to a global/constant:
$do_something = ->{}
DO_SOMETHING = ->{}
Or just grow up and encapsulate your method in a module/class.

How to use cattr_accessor to define config in a Rails initializer

Given the following class (this is the actual class there is no other code)
class PageRepo
cattr_accessor :root_path
end
and initializer file
PageRepo.root_path = Rails.root.join("content")
When I run
rails console
PageRepo.root_path
=> PageRepo.root_path
=> #<Pathname:/Users/blah/my_rails_app/content/pages>
However when I try to access this in my rails controller the root_path value is nil. (I've inspected this with web_console.)
No other class subclasses or is a parent of the PageRepo class and I'm not setting the root_path to nil anywhere at class level or instance level after the initializer stage. I have restarted spring and my server multiple times to no avail.
Is there something I'm not aware of when it comes to either Rails initializers or cattr_accessor?
Update
I'd like to set the path like this because throughout my code I will be initialising a PageRepo instance and the path will not change. However, it may change across different environments. I.e. in development it the path will be different than that of the test and production environments. Whilst I could just do
def initialize
#root_path = ENV['ROOT_PATH']
end
I'd prefer to not force the programmer to use ENV VARS to do this and hence my attempt above.
My solution would be just using a class method
class PageRepo
self.root_path
Rails.root.join("content")
end
end
PageRepo.root_path would return #<Pathname:/Users/blah/my_rails_app/content/pages>.
You want to have #<Pathname:/Users/blah/my_rails_app/content/pages>? or this depends on the action?
I believe your initializer is not working
an interesting post, what you are trying to achieve is writing something like this in the class initializer or constructor
class PageRepo
self.initialize
self.root_path = Rails.root.join("content")
end
end
but I never saw self.initialize being used with rails. so I believe the first approach is better.

How to call methods of an external class in Ruby

I have a file under /lib with its own method.
# lib/file.rb
class File < ApplicationController
def my_method
...
end
end
However I can't reach the method through the console
ruby-1.9.2-p290 :044 > File.my_method
NoMethodError: undefined method `my_method' for File:Class
Any idea how?
my_method is an instance method of the File class. It means that you can call it only on the instance of the File class.
file = File.new
file.my_method
You can declare my_method as class method using def self.my_method syntax.
class File < ApplicationController
def self.my_method
...
end
end
File.my_method
But in class methods you can't use instance variables of the File object.
You're trying to call my_method as a class method, but you've defined it as an instance method.
You should either define it as def self.my_method, or create an instance of the controller to call it as an instance method.
In addition, you are going to run into problems for a couple of reasons - (1) Rails expects controllers to be named like FilesController, and (2) File is a class in the standard library. I would encourage you to change the class name to FilesController, and rename the file itself to files_controller.rb to prevent both issues.
Well... there are several interesting things going on with this example. The first would be that this class name is call File which is already defined in Ruby.
That is most likely why when you are in the console you didn't get an undefined class error. Since my_method is not defined on Ruby's File class, this is why you are seeing undefined method.
Now to your question. I would try naming your class something different first and trying again from lib. I believe it should be loaded by default again with the rails environment. For a version or two that functionality was taken out but I want to say it's back in. If not, just go into your config/application.rb file and look for a declaration along the lines of config.autoload_paths. Add the lib directory there and you should be good to go.
Lastly, is there a reason you want a controller in lib?

Modifying a ruby class doesn't work as expected when running Spork

I have a plain Ruby class in my Rails app that I'm reopening in a test environment. It basically looks like
class A
def get_dependency
B
end
... some other methods ...
end
And in my test environment in cucumber (in a file loaded from features/env.rb) (and a similar place for rspec) I do
class A
def get_dependency
MockedB
end
end
This works fine in normal runs, but when I have Spork running, it fails strangely. Class A's get_dependency method is overwritten properly, but all its other public methods are now missing. Any ideas?
I'm assuming this is related to load order somehow, but I didn't get any changes when I moved the require for my file out of the preload section of Spork.
This isn't a great answer, but it's a workaround. Instead of reopening the class I just modified a singleton instance. The code is basically the same, except I added an instance method on A:
class A
def instance
##instance ||= A.new
end
end
Then in my test code I modified the instance
instance = A.instance
def instance.get_dependency
MockedB
end
And I just had to ensure that my actual code was always calling A.instance instead of A.new.
One possible scenario is that A is set to get autoloaded, but when you define the override for it in your cucumber environment, you do so before it has been autoloaded; since A now exists, it will never get autoloaded.
A possible solution, which invokes the autoloader before overriding the method is this:
A.class_exec do
def get_dependency
MockedB
end
end
It will raise a ConstMissing if A cannot be autoloaded at that point (perhaps the autoloaders have not yet been set up).

Getting delayed job to log

#Here is how I have delayed job set up.
Delayed::Worker.backend = :active_record
#Delayed::Worker.logger = Rails.logger
Delayed::Worker.logger = ActiveSupport::BufferedLogger.new("log/
##{Rails.env}_delayed_jobs.log", Rails.logger.level)
Delayed::Worker.logger.auto_flushing = 1
class Delayed::Job
def logger
Delayed::Worker.logger
end
end
if JobsCommon::check_job_exists("PeriodicJob").blank?
Delayed::Job.enqueue PeriodicJob.new(), 0, 30.seconds.from_now
end
#end
#Here is my simple job.
class PeriodicJob
def perform
Rails.logger.info "Periodic job writing #{Time.now}"
Delayed::Job.enqueue PeriodicJob.new(), 0,
30.seconds.from_now
end
end
I don't see any log messages from delayed job in my rails logs or delayed job log file, the only messages I see are jobs starting/success/failure in the delayed_jobs.log file.
this is causing big problems, including detecting bugs and memory leaks in workers almost impossible! Please help!
We've gotten it to work on Rails 3/Delayed Job 2.0.3 by hacking Rails.logger itself to use a different log file (the one we want for delayed_job entries) and also setting the delayed job logger to use the exact same object:
file_handle = File.open("log/#{Rails.env}_delayed_jobs.log", (File::WRONLY | File::APPEND | File::CREAT))
# Be paranoid about syncing, part #1
file_handle.sync = true
# Be paranoid about syncing, part #2
Rails.logger.auto_flushing = true
# Hack the existing Rails.logger object to use our new file handle
Rails.logger.instance_variable_set :#log, file_handle
# Calls to Rails.logger go to the same object as Delayed::Worker.logger
Delayed::Worker.logger = Rails.logger
If the above code doesn't work, try replacing Rails.logger with RAILS_DEFAULT_LOGGER.
This may be a simple workaround but it works well enough for me:
system("echo #{your message here} >> logfile.log")
Simple but works
I have it working with the following setup in initializer:
require 'delayed/worker'
Delayed::Worker.logger = Rails.logger
module Delayed
class Worker
def say_with_flushing(text, level = Logger::INFO)
if logger
say_without_flushing(text, level)
logger.flush
end
end
alias_method_chain :say, :flushing
end
end
i simply did this:
/config/environments/development.rb
MyApp::Application.configure do
[...]
[...]
[...]
Delayed::Worker.logger = Rails.logger
end
In every next request you do the mail will be appear on the log.
NOTE: Sometimes you have to refresh the page to the mail be logged on the log. Don't forget to restart the server ;)
DelayedJob does not seem to output if there is something wrong:
1- Non-active record classes need to be required and initialized:
How:
Create a file config/initializers/load_classes_for_dj.rb
Add to it the lines:
require 'lib/libtest/delayed_test.rb'
DelayedTest
Note that if you have '#{config.root}/lib/libtest' in config.autoload_paths in config/application.rb, you do not need to do the require.
Source:
Rails Delayed Job & Library Class
2- Classes that implement the Singleton module won't work by calling:
SingletonClass.instance.delay.sayhello
In order to fix that, do the following:
class SingletonClass
include Singleton
# create a class method that does the call for you
def self.delayed_sayhello
SingletonClass.instance.sayhello
end
def sayhello
# this is how you can actually write to delayed_job.log
# https://stackoverflow.com/questions/9507765/delayed-job-not-logging
Delayed::Worker.logger.add(Logger::INFO, "hello there")
end
end
In order to call the delayed class method, do the following:
SingletonClass.delay.delayed_sayhello
Yes, I am calling .delay on a class here. Since classes in Ruby are also objects, the call is valid here and allows me to access the class method "delayed_sayhello"
3- Do not pass ActiveRecord objects or some complex objects to your call but rather pass ids, look up the objects in the database in your delayed method, and then do your work:
DO NOT DO:
DelayedClass.new.delay.do_some_processing_on_album Album.first
DO THIS INSTEAD:
DelayedClass.new.delay.do_some_processing_on_album Album.first.id
and inside DelayedClass do_some_processing_on_album , do
a = Album.find_by_id id
There was a stackoverflow post about this that I saw a while ago -- not sure which :-)
4- For completion, this is how to do mailers (do not call the deliver method):
Notifier.delay.signup(user_id)
As per 3, do not pass the user's object but rather their id, do the lookup inside the signup method.
Now once you've ensured you have followed the guidelines above, you can use:
Delayed::Worker.logger.add(Logger::INFO, "hello")
If you are still facing issues, I suggest you break down your problem:
a- Create a new class
b- Make sure you have it included and initialized as per step 1
c- Add logging from step 4 and try calling MyNewClass.new.delay to see if it works
I hope this helps you guys :-)
Don't forget to change the ActiveRecord::Base.logger
Delayed::Worker.logger = Logger.new("log/delayed_job.log", 5, 104857600)
if caller.last =~ /script\/delayed_job/ or (File.basename($0) == "rake" and ARGV[0] =~ /jobs\:work/)
ActiveRecord::Base.logger = Delayed::Worker.logger
end
More detailed answer: Have delayed_job log "puts", sql queries and jobs status

Resources