Logging in Rails with a per-request ID - ruby-on-rails

I'm looking for a quick and easy way to generate a unique per-request ID in rails that I can then use for logging across a particular request.
Any solution should ideally not make too much use of the default logging code, as I'm running the application under both jruby and ruby.

Backupify produced a great article about this: http://blog.backupify.com/2012/06/27/contextual-logging-with-log4r-and-graylog/
We wanted the request_id (that is generated by rails and available at request.uuid to be present on all messages throughout the request. In order to get it into the rack logging (the list of parameters and the timing among others), we added it to the MDC in a rack middleware.
application.rb:
config.middleware.insert_after "ActionDispatch::RequestId", "RequestIdContext"
app/controllers/request_id_context.rb: (had trouble finding it in lib for some reason)
class RequestIdContext
def initialize(app)
#app = app
end
def call(env)
Log4r::MDC.get_context.keys.each {|k| Log4r::MDC.remove(k) }
Log4r::MDC.put("pid", Process.pid)
Log4r::MDC.put("request_id", env["action_dispatch.request_id"])
#app.call(env)
end
end
If you push jobs onto delay job/resque, put the request_id into the queue. and in your worker pull it off and set into the MDC. Then you can trace the requests the whole way through

It looks like lograge (gem) automatically puts request.uuid in your logs.
They have this pattern:
bfb1bf03-8e12-456e-80f9-85afaf246c7f

This is now a feature of rails:
class WidgetsController < ApplicationController
def get
puts request.request_id
end
end

Maybe the NDC feature of log4r is usefull to you.

Related

HTTP request uuid or request start time in Ruby applications

I need to get a request uuid or time when server get a request. It's easy in Rails, but I'm working on a gem and I would like it to be more generic. So I would like it to work also with Sinatra and every other Ruby application which works in a http server.
This is another problem, it's a gem. I can't put Time.now at the beggining of my application controller. I need it to be generic, so it should work with different frameworks.
What would you propose?
You can implement a Rack middleware which you can use independently from your actual application framework (as long as it used rack, which is true for at least Rails, Sinatra, Padriono and most other Ruby web frameworks).
Rails already includes a middleware for adding a unique ID to a request of required in ActionDispatch::RequestId. Another alternative could be the rack-request-id gem.
A minimal versions of this midleware could look like this:
class RequestIdMiddleware
def initialize(app)
#app = app
end
def call(env)
env['request_id'] = env['HTTP_X_REQUEST_ID'] || SecureRandom.uuid
env['request_started_at'] = Time.now
#app.call(env)
end
end
You can then use this middleware in your config.ru or by adding this to your application.rb in Rails:
config.middleware.use RequestIdMiddleware

Better to attach a simple function as class method or a module?

I have a simple function to prevent emails from being sent to customers when testing locally:
def safe_emails emails
if Rails.env == 'production'
emails
else
emails.select{|e| ['staff#example.com', 'staff2#example.com'].include?(e) }
end
end
I want to share that function between mailers. I can see two options, a module or a class method.
Option 1: Module
class ReportMailer < ActionMailer::Base
include SafeEmailer
def daily emails
mail_to: safe_emails(emails)
end
end
Option2: Class Method
class ReportMailer < ActionMailer::Base
def daily emails
mail_to: SafeEmailer.safe_emails(emails)
end
end
The class method is a no-no according to some due to global scope, including a module with one method doesnt seem all that attractive. Monkey-patching ActionMailer to throw the method in there also seems like it could cause trouble (when Rails 4.3 introduces the safe_emails method or whatever).
I would go with module option even if it's a simple one function module. Keeping a generic function that can eventually be used by multiple classes in a module makes much more sense than defining it inside a class.
If Rails 4.3 is of your concern then you can simply replace your include MySafeEmailModule with whatever Rails 4.3 would include this function in, as compared to find and replace all calls to ReportMailer.daily_emails.
Neither – in your case, you'd need a policy object, that decides who receives email regarding the Rails.env. I'd keep that logic outside the ReportMailer.
I'd go with something like:
UserMailer.welcome_email(#user).deliver if SafeEmailer.new(#user.email).safe?
This is probably the easiest way.
Set the below configuration in your non production environments.(config/environments/.rb)
config.action_mailer.delivery_method = :smtp (default), :sendmail, :test, or :file
Have a look at letter opener gem. You could have the delivery_method set as letter_opener and have the emails open in browser instead of actually sending them in your non production environments.

How to stub in Rails development environment?

I'm looking for a reliable way to dynamically stub certain methods in my development environment. One example use case is when I need to do development that normally requires access to the Facebook Graph APIs but I don't have Internet access. I'd like to be able to stub the calls to fb_graph methods so it looks as if I'm authenticated and have profile data. Ideally I could turn the stubs on or off with a minor config change.
Any ideas? I'm assuming something like mocha can handle this?
You can use the VCR gem which will record the results of an initial HTTP request into a yml file and then use the contents of that yml file on subsequent http requests. It can then be configured to ignore the VCR logic and always make HTTP requests, if so desired:
https://www.relishapp.com/myronmarston/vcr
Mocha can certainly do it. But it feels a bit strange.
You could also do something like dependency injection.
For instance:
class User < AR::Base
def find_friends
Facebook.find_friends(facebook_id)
end
end
class Facebook
def self.find_friends(id)
# connect to Facebook here
end
end
class FakeFacebook
def self.find_friends(id)
# a fake implementation here
end
end
And inside an initializer:
if Rails.env.development?
User::Facebook = FakeFacebook
end

Change log level for single controller or action in rails

We are running a rails project behind haproxy. There is a keep-alive sent to the application every second. This is causing very noisy log files which makes it a bit of a pain to dig through and is making them unnecessarily large.
My first thought was to change the logging level for that action to debug, but someone else proposed changing the logging level in an around_filter. I am not crazy about that idea, but it could just be how I implemented it. I am open to different solutions, but the general requirements are that I can quiet those actions, but I could change the logging level if I needed to see them for whatever reason.
Another solution is to insert some Rack middleware which handles the keep-alive check before it gets to the Rails ApplicationController lifecycle.
Step 1: make some middleware which respondes to the keep-alive check. In my example the keep-alive request is a GET /health-check so it would look like:
class HealthCheckMiddleware
def initialize(app)
#app = app
end
def call(env)
if env['PATH_INFO'] == '/health-check'
return [200, {}, ['healthy']]
end
#app.call(env)
end
end
Of course, adjust this health check as necessary. Maybe you need to check other Request / CGI variables...
Step 2: make sure you insert this middleware before Rails::Rack::Logger:
config.middleware.insert_before Rails::Rack::Logger, "HealthCheckMiddleware"
Now your middleware will handle the health check and your logs have been totally by-passed.

Passing variables to config/environments/demo.rb from the Rails app

I have been struggling with a problem for the past days in a Ruby on Rails App I'm currently working on. I have different countries and for each country we use different Amazon S3 buckets. Amazon S3 key credentials are stored as constants in config/environments/environment_name.rb(ex:demo.rb) There is no way for me to determine which country we are operating from the config file. I can determine which country we are operating from the controllers,models,views,etc but not from the config file. Is there a Ruby meta programming or some other kind of magic that I'm not aware of so that I want to say if we are working on UK as a country in the app, use UK's bucket credentials or Germany as a country, use Germany's bucket credentials? I can't think of a way to pass parameters to environment files from the app itself. Thank you very much in advance for all your helps.
Rather than actually pass the configuration details to whichever S3 client you're using at launch, you should probably select the relevant credentials for each request. Your config file can define them all in a hash like so:
# config/s3.rb
S3_BUCKETS => {
:us => 'our-files-us',
:gb => 'our-files-gb',
:tz => 'special-case'
}
Then you can select the credentials on request like so (in maybe your AppController):
bucket_name = S3_BUCKETS[I18n.locale]
# pass this info to your S3 client
Make sense?
Write a little middleware if you want to keep the knowledge of the per-country configuration out of the main application.
A middleware is extremely simple. A do-nothing middleware looks like this:
class DoesNothing
def initialize(app, *args)
#app = app
end
def call(env)
#app.call(env)
end
end
Rack powers applications through chaining a series of middlewares together... each one is given a reference to #app, which is the next link in the chain, and it must invoke #call on that application. The one at the end of the chain runs the app.
So in your case, you can do some additional configuration in here.
class PerCountryConfiguration
def initialize(app)
#app = app
end
def call(env)
case env["COUNTRY"]
when "AU"
Rails.application.config.s3_buckets = { ... }
when "US"
Rails.application.config.s3_buckets = { ... }
... etc
end
#app.call(env)
end
end
There are several ways to use the middleware, but since it depends on access to the Rails environment, you'll want to do it from inside Rails. Put it in your application.rb:
config.middleware.use PerCountryConfiguration
If you want to pass additional arguments to the constructor of your middleware, just list them after the class name:
config.middleware.use PerCountryConfiguration, :some_argument
You can also mount the middleware from inside of ApplicationController, which means all of the initializers and everything will have already been executed, so it may be too far along the chain.

Resources