I am trying to make a bit of a custom Rails logger which ultimately will log to a database. However, I don't have access to things like the request object, which I very much would like to have.
I'm currently trying to use the LogSubscriber (notification) interface to do the bulk of this; perhaps this is not the right approach. I do know I could abuse Thread.current[] but I was hoping to avoid doing that.
Here's the code I have which is as basic as I can get it for an example. This is loaded in an initializer.
module RequestLogging
class LogSubscriber < ActiveSupport::LogSubscriber
def process_action(event)
pp request # <--- does not work
pp event
end
end
RequestLogging::LogSubscriber.attach_to :action_controller
Probably you need to override process_action in ActionController::Instrumentation and then request object will be accessible like event.payload[:request]. I think you can put code somewhere in config/initializers, code example:
ActionController::Instrumentation.class_eval do
def process_action(*args)
raw_payload = {
controller: self.class.name,
action: self.action_name,
params: request.filtered_parameters,
format: request.format.try(:ref),
method: request.method,
path: (request.fullpath rescue "unknown"),
request: request,
session: session
}
ActiveSupport::Notifications.instrument("start_processing.action_controller", raw_payload.dup)
ActiveSupport::Notifications.instrument("process_action.action_controller", raw_payload) do |payload|
result = super
payload[:status] = response.status
append_info_to_payload(payload)
result
end
end
end
you can get the even.payload then pass it your own CustomLogger(formatted_log(even.payload) and then there you can define a module and save it.
You may want to customise your formatted_log function to beautify the payload accordingly.
def process_action(event)
CustomLogger.application(formattedLog(event.payload))
end
def formattedLog(payload)
# some restructuring of data.
end
Related
I have rails 5 based api app, using fast_jsonapi
and after a while I observe that all most all my actions are having one common pattern
def action_name
#some_object.perform_action_name # this returns #some_object
render json: ControllerNameSerializer.new(#some_object).to_h
end
I do not wish to write the last render line here and it should work, for that I want that the returned value by the action should be processed by any hidden responder like thing, Serializer klass can be made out looking at controller name.
Perhaps this could be achieved by adding a small middleware. However at first, I find it not a good idea/practise to go for a middleware. In middleware, we do get rendered response, we need a hook prior to that.
I would imagine like
class SomeController ...
respond_with_returned_value
def action_name
#some_object.perform_action_name # this returns #some_object
end
Any suggestions?
Note, do not worry about error/failure cases, #some_object.errors could hold them and I have a mechanism to handle that separately.
Sketched out...
class ApplicationController < ...
def respond_with_returned_value
include MyWrapperModule
end
...
end
module MyWrapperModule
def self.included(base)
base.public_instance_methods.each do |method_name|
original_method_name = "original_#{method_name}".to_sym
rename method_name -> original_method_name
define_method(method_name) { render json: send(original_method_name) }
end
end
end
Seems like there really should be some blessed way to do this - or like someone must have already done it.
I'm building an around_action for my customer_mailer class so that I don't have to wrap begin and rescue around every time I call deliver_now
class CustomerMailer < ApplicationMailer
around_action :rescue_error
def send_email(customer)
...
end
def invite_friend(customer, invitee_email)
...
end
private
def rescue_error
yield
rescue => e
msg = "Caught exception! #{e} | #{action_name}"
puts msg
raise
end
end
So in the rescue, I want to log the message with information such as which action was called, I managed to find the method action_name to show which action was called, but I couldn't find a way to retrieve the parameters that were passed into the action, any ideas?
Thanks!
Before I answer your question: would using Bugsnag or something similar work in your case? Alternatively would rescue_from Exception, with: :exception_handler work for you? (it won't allow you to reraise the exception though)
I dug into Rails source code and it seems that parameters are not stored anywhere. They are just passed as a splat to an instance method defined in your mailer class. However, there is a way to store them (without monkey-patching).
Mailers inherit from AbstractController::Base. Looking at the snippet below:
# Call the action. Override this in a subclass to modify the
# behavior around processing an action. This, and not #process,
# is the intended way to override action dispatching.
#
# Notice that the first argument is the method to be dispatched
# which is *not* necessarily the same as the action name.
def process_action(method_name, *args)
send_action(method_name, *args)
end
# Actually call the method associated with the action. Override
# this method if you wish to change how action methods are called,
# not to add additional behavior around it. For example, you would
# override #send_action if you want to inject arguments into the
# method.
alias send_action send
we can see that we can override #send_action and make it store the arguments. Add the following to your ApplicationMailer:
class ApplicationMailer < ActionMailer::Base
def send_action(method_name, *args)
#action_args = args
super
end
end
The arguments will be available as #action_args in all your mailers.
Just store the parameters with which the action has been called to an instance variable, say #params. Then these parameters will be accessible in rescue_error via #params. As per your example:
class CustomerMailer < ApplicationMailer
around_action :rescue_error
def send_email(customer)
#params = { customer: customer }
...
end
def invite_friend(customer, invitee_email)
#params = { customer: customer, invitee_email: invitee_email }
...
end
private
def rescue_error
begin
yield
rescue => e
msg = "Caught exception! #{e} | #{action_name} | #{#params.inspect}"
puts msg
raise
end
end
end
You can make the assignment to #params a bit cleaner by using hash parameters in your actions, e.g.
def invite_friend(options = {})
#params = params
...
end
Of course, this requires accessing the parameters via options, such as options[:customer] to access customer, and options[:invitee_email] to access invitee_email.
The action name have to be yielded , it depends on the way you use your rescue_error .
Define a variable in the block that will be yielded
or raise specifics errors (maybe your custom exception class )
this way you'll retrieve invormation via "e"
post an exemple use case of rescue_error.
I have a Ruby/Rails class MyBase defined as such:
module MyModule1
class MyBase < Sinatra::Base
register Sinatra::RespondTo
register Sinatra::CrossOrigin
...
I define a sub-class of MyBase and has an API someAction that can be called:
module MyModule1
module MyModule2
class MySubClass < MyModule1::MyBase
post 'someAction' do
...
end
...
There are numerous other APIs in MySubClass not shown above for brevity.
I would like to time and then log the duration of each time any API is hit. I know how to time using the STATSD rails module and I know how to log too. But where/how should I put this timing code to keep my code DRY? I want to write it only once, preferably in the base class. But I don't know how to define a base class method that wraps the subclassed method and gets called anytime the subclassed method fires.
What are you looking for is a Rack middleware.
You can inject your own like this. Thanks to this the logic for the logging time of response is taken out completely from your Sinatra application as it works on the bottom level of request (Sinatra is built on top of raw Rack application).
class MeasureResponse
def initialize(app)
#app = app
end
def call(env)
start = Time.now
status, headers, response = #app.call(env)
stop = Time.now
log_time(start, stop)
[status, headers, response]
end
private
def log_time(start, stop)
total_time = start - stop
puts "Time of request: #{total_time}" #replace with your logger
end
end
module MyModule1
class MyBase < Sinatra::MyBase
configure do
use MeasureResponse
end
post 'someAction' do
...
end
end
end
Luckily this is a simple case of writing a different post method here that wraps the block you pass to it and calls the original post. It would be slightly more code if you were trying to wrap regular def methods.
Create a new post class method and call sinatra's post within it (this goes for get, put, etc):
module MyModule1
class MyBase < Sinatra::Base
register Sinatra::RespondTo
register Sinatra::CrossOrigin
def self.post_with_logging(action_name, &block)
post action_name do
start_your_benchmarks! # start your benchmarking
block.call # this is your subclass' action block
finalize_benchmarks! # finish up
log_whatever!
end
end
end
end
Use this new post_with_logging method in your subclasses instead of the plain post method:
module MyModule1
module MyModule2
class MySubClass < MyModule1::MyBase
post_with_logging 'someAction' do
...
end
This is essentially wrapping the block and using the plain post method within. Hopefully this is pretty clear. Let me know if more explanation is necessary.
Update: in order to not have to change the subclass use super:
module MyModule1
class MyBase < Sinatra::Base
register Sinatra::RespondTo
register Sinatra::CrossOrigin
def self.post(action_name, &block)
super action_name do
start_your_benchmarks! # start your benchmarking
block.call # this is your subclass' action block
finalize_benchmarks! # finish up
log_whatever!
end
end
end
end
And use your subclasses as normal.
In Rails notifications, I am subscribing to "process_action.action_controller", and would like to add more attributes to the payload. How can I do that?
I have tried using append_info_to_payload, but this seems to do nothing.
module AppendExceptionPayload
module ControllerRuntime
extend ActiveSupport::Concern
protected
def append_info_to_payload(payload)
super
payload[:happy] = "HAPPY"
end
end
end
The subscription and above code is in a Rails engine, so this is where I make the call to add it:
require 'append_exception_payload'
module Instrument
class Engine < ::Rails::Engine
ActiveSupport.on_load :action_controller do
include AppendExceptionPayload::ControllerRuntime
end
end
end
After putting up the bounty, I found a solution myself. Rails handles this really cleanly.
Basically, the append_info_to_payload method is meant exactly for this.
So to include session information and signed_in user information I added this to my application_controller.rb:
def append_info_to_payload(payload)
super
payload[:session] = request.session_options[:id] rescue ""
payload[:user_id] = session[:user_id] rescue "unknown"
end
So i jumped in and had a look at the api for the process_action method (private) and the append_info_to_payload instance method (public) and the proccess action method seems to call append_info_to_payload in its code like so:
ActiveSupport::Notifications.instrument("process_action.action_controller", raw_payload) do |payload|
result = super
payload[:status] = response.status
append_info_to_payload(payload)
result
end
and append_info_to_payload works something like this
def append_info_to_payload(payload) #:nodoc:
payload[:view_runtime] = view_runtime
end
I can suggest trying payload[:view_runtime] instead of payload[:happy] or trying to use payload[:status]
Let me know how you get on and I will try help more, unfortunately there is really no documentation for this stuff.
I have an expensive (time-consuming) external request to another web service I need to make, and I'd like to cache it. So I attempted to use this idiom, by putting the following in the application controller:
def get_listings
cache(:get_listings!)
end
def get_listings!
return Hpricot.XML(open(xml_feed))
end
When I call get_listings! in my controller everything is cool, but when I call get_listings Rails complains that no block was given. And when I look up that method I see that it does indeed expect a block, and additionally it looks like that method is only for use in views? So I'm guessing that although it wasn't stated, that the example is just pseudocode.
So my question is, how do I cache something like this? I tried various other ways but couldn't figure it out. Thanks!
an in-code approach could look something like this:
def get_listings
#listings ||= get_listings!
end
def get_listings!
Hpricot.XML(open(xml_feed))
end
which will cache the result on a per-request basis (new controller instance per request), though you may like to look at the 'memoize' helpers as an api option.
If you want to share across requests don't save data on the class objects, as your app will not be threadsafe, unless you're good at concurrent programming & make sure the threads don't interfere with each other's data access to the shared variable.
The "rails way" to cache across requests is the Rails.cache store. Memcached gets used a lot, but you might find the file or memory stores fit your needs. It really depends on how you're deploying and whether you want to prioritise cache hits, response time, storage (RAM), or use a hosted solution e.g. a heroku addon.
As nruth suggests, Rails' built-in cache store is probably what you want.
Try:
def get_listings
Rails.cache.fetch(:listings) { get_listings! }
end
def get_listings!
Hpricot.XML(open(xml_feed))
end
fetch() retrieves the cached value for the specified key, or writes the result of the block to the cache if it doesn't exist.
By default, the Rails cache uses file store, but in a production environment, memcached is the preferred option.
See section 2 of http://guides.rubyonrails.org/caching_with_rails.html for more details.
You can use the cache_method gem:
gem install cache_method
require 'cache_method'
In your code:
def get_listings
Hpricot.XML(open(xml_feed))
end
cache_method :get_listings
You might notice I got rid of get_listings!. If you need a way to refresh the data manually, I suggest:
def refresh
clear_method_cache :get_listings
end
Here's another tidbit:
def get_listings
Hpricot.XML(open(xml_feed))
end
cache_method :get_listings, (60*60) # automatically expire cache after an hour
You can also use cachethod gem (https://github.com/reneklacan/cachethod)
gem 'cachethod'
Then it is deadly simple to cache method's result
class Dog
cache_method :some_method, expires_in: 1.minutes
def some_method arg1
..
end
end
It also supports argument level caching
There was suggested cache_method gem, though it's pretty heavy. If you need to call method without arguments, solution is very simple:
Object.class_eval do
def self.cache_method(method_name)
original_method_name = "_original_#{method_name}"
alias_method original_method_name, method_name
define_method method_name do
#cache ||= {}
#cache[method_name] = send original_method_name unless #cache.key?(method_name)
#cache[method_name]
end
end
end
then you can use it in any class:
def get_listings
Hpricot.XML(open(xml_feed))
end
cache_method :get_listings
Note - this will also cache nil, which is the only reason to use it instead of #cached_value ||=
Late to the party, but in case someone arrives here searching.
I use to carry this little module around from project to project, I find it convenient and extensible enough, without adding an extra gem. It uses the Rails.cache backend, so please use it only if you have one.
# lib/active_record/cache_method.rb
module ActiveRecord
module CacheMethod
extend ActiveSupport::Concern
module ClassMethods
# To be used with a block
def cache_method(args = {})
#caller = caller
caller_method_name = args.fetch(:method_name) { #caller[0][/`.*'/][1..-2] }
expires_in = args.fetch(:expires_in) { 24.hours }
cache_key = args.fetch(:cache_key) { "#{self.name.underscore}/methods/#{caller_method_name}" }
Rails.cache.fetch(cache_key, expires_in: expires_in) do
yield
end
end
end
# To be used with a block
def cache_method(args = {})
#caller = caller
caller_method_name = args.fetch(:method_name) { #caller[0][/`.*'/][1..-2] }
expires_in = args.fetch(:expires_in) { 24.hours }
cache_key = args.fetch(:cache_key) { "#{self.class.name.underscore}-#{id}-#{updated_at.to_i}/methods/#{caller_method_name}" }
Rails.cache.fetch(cache_key, expires_in: expires_in) do
yield
end
end
end
end
Then in an initializer:
# config/initializers/active_record.rb
require 'active_record/cache_method'
ActiveRecord::Base.send :include, ActiveRecord::CacheMethod
And then in a model:
# app/models/user.rb
class User < AR
def self.my_slow_class_method
cache_method do
# some slow things here
end
end
def this_is_also_slow(var)
custom_key_depending_on_var = ...
cache_method(key_name: custom_key_depending_on_var, expires_in: 10.seconds) do
# other slow things depending on var
end
end
end
At this point it only works with models, but can be easily generalized.
Other answers are excellent but if you want a simple hand-rolled approach you can do this. Define a method like the below one in your class...
def use_cache_if_available(method_name,&hard_way)
#cached_retvals ||= {} # or initialize in constructor
return #cached_retvals[method_name] if #cached_retvals.has_key?(method_name)
#cached_retvals[method_name] = hard_way.call
end
Thereafter, for each method you want to cache you can put wrap the method body in something like this...
def some_expensive_method(arg1, arg2, arg3)
use_cache_if_available(__method__) {
calculate_it_the_hard_way_here
}
end
One thing that this does better than the simplest method listed above is that it will cache a nil. It has the convenience that it doesn't require creating duplicate methods. Probably the gem approach is cleaner, though.
I'd like to suggest my own gem https://github.com/igorkasyanchuk/rails_cached_method
For example:
class A
def A.get_listings
....
end
end
Just call:
A.cached.get_listings