Remove default route logging in Sinatra app - ruby

I have a Sintra app with a /health/api endpoint, described below, that gets called many times a second by my load balancers. I would like to remove the default logging for only this route, or conversely print to the logs only the endpoints I care about.
get '/health/api' do
# Health Check
'I keep quiet'
end
get '/members' do
# get members data
'This request gets logged'
end

You cannot configure the default Rack::CommonLogger to print only some requests, and keep quiet on others. But you can disable the default CommonLogger and use your own with filtering capabilities instead:
require 'rubygems'
require 'rack'
require 'sinatra'
LOGGING_BLACKLIST = ['/health/api']
class FilteredCommonLogger < Rack::CommonLogger
def call(env)
if filter_log(env)
# default CommonLogger behaviour: log and move on
super
else
# pass request to next component without logging
#app.call(env)
end
end
# return true if request should be logged
def filter_log(env)
!LOGGING_BLACKLIST.include?(env['PATH_INFO'])
end
end
disable :logging
use FilteredCommonLogger
get '/members' do
# get members data
'This request gets logged'
end
get '/health/api' do
# Health Check
'I keep quiet'
end
You can obviously also write your custom logger that only logs "whitelisted" requests.

Related

Raising 500 errors deliberately in Sinatra in order to test how they are handled

I want to write an RSpec test which verifies that, should a 500 error occur in my Sinatra-powered API, the error will be caught by a Sinatra error definition and returned to the client in a JSON format. That is, rather than returning some HTML error page, it returns JSON like this to conform with the rest of the API:
{
success: "false",
response: "Internal server error"
}
However, I'm unsure how to actually trigger a 500 error in my Sinatra app in order to test this behaviour with RSpec. I can't find a way to mock Sinatra routes, so currently my best idea is this route which deliberately causes a 500. This feels like a pretty dreadful solution:
get '/api/v1/testing/internal-server-error' do
1 / 0
end
Is there a way to mock Sinatra routes so that I can have, say, /'s route handler block raise an exception, therefore triggering a 500? If not, is there some other way to deliberately cause a 500 error in my app?
When facing a situation like this, what I usually do is separate concerns, and move logic outside of the Sinatra get ... block. Then, it is easy to stub it and make it raise an error.
For example, given this server code:
# server.rb
require 'sinatra'
class SomeModel
def self.some_action
"do what you need to do"
end
end
get '/' do
SomeModel.some_action
end
You can then use this code to have the model, or any other class/function you are using to actually generate the response, raise an error, using this spec:
# spec
describe '/' do
context 'on error' do
before do
allow(SomeModel).to receive(:some_action) { raise ArgumentError }
end
it 'errors gracefully' do
get '/'
expect(last_response.status).to eq 500
end
end
end
For completeness, here is a self contained file that can be tested to demonstrate this approach by running rspec thisfile.rb:
# thisfile.rb
require 'rack/test'
require 'rspec'
require 'sinatra'
# server
class SomeModel
def self.some_action
"do what you need to do"
end
end
get '/' do
SomeModel.some_action
end
# spec_helper
ENV['APP_ENV'] = 'test'
module RSpecMixin
include Rack::Test::Methods
def app() Sinatra::Application end
end
RSpec.configure do |c|
c.include RSpecMixin
end
# spec
describe '/' do
context 'on error' do
before do
allow(SomeModel).to receive(:some_action) { raise ArgumentError }
end
it 'errors gracefully' do
get '/'
expect(last_response.status).to eq 500
end
end
end
Use the halt method:
require 'sinatra'
get '/' do
halt 500, {
success: 'false',
response: 'Internal server error'
}.to_json
end

Facebook Graph API for websites using Ruby Koala gem in Sinatra

I want to implement Facebook login for web apps. All I need is the basic public information of a user for the account creation steps.
This is what I have done:
Created a basic Facebook app with nearly no custom permissions.
Used the APP_ID and APP_SECRET in Koala to get access_token.
Everything worked perfectly, I am able to login/logout.
Just that the only information I am able to get back when I do: graph.get_object('me') is the logged in user's name and an id (It doesn't look like the default Facebook id).
Surprised whether something changed in the new API, I tested the gem in the console using the access_token from graph explorer (where all permissions are enabled by default). And I get all data using the same method call.
When I review what all the app gets while signing up; I see that the user's basic information, profile pic and other public data will be accessible to the app.
Any idea why this is so? It seems I am missing something obvious. The code is available in Github. But this is pretty much everything to it:
require 'bundler'
Bundler.require :default
Dotenv.load '.env'
require_relative './app/constants.rb'
module Banana
class App < Sinatra::Base
use Rack::Session::Cookie, secret: COOKIE_SECRET
set :public_folder, File.dirname(__FILE__) + '/bower_components'
get '/' do
if logged_in?
haml :welcome_in, layout: :layout
else
haml :log_in, layout: :layout
end
end
get '/log_out' do
session['oauth'] = nil
session['access_token'] = nil
redirect '/'
end
get '/log_in' do
session['oauth'] = Koala::Facebook::OAuth.new(APP_ID, APP_SECRET, "#{request.base_url}/call_back")
redirect session['oauth'].url_for_oauth_code()
end
get '/call_back' do
begin
session['access_token'] = session['oauth'].get_access_token(params[:code])
rescue
redirect '/?error=user_denied'
end
redirect '/'
end
get '/test' do
if logged_in?
p graph.get_object("rakeshbs")
"e"
else
redirect '/'
end
end
def logged_in?
!session['access_token'].nil?
end
def toggle_access
logged_in? ? '/log_out' : '/log_in'
end
def graph
#graph ||= Koala::Facebook::API.new(session['access_token'])
end
def errored?
!params["error"].nil?
end
def user
p graph.get_connections(:me, :photos) # This is just nil
#user ||= OpenStruct.new(
name: graph.get_object("me")["name"], # All I get here is just a hash with the name and an id!
photo: 'http://semantic-ui.com/images/avatar/small/elliot.jpg'
)
end
end
end
You should add fields parameter.
Something like this:
graph.get_object('me', { fields: 'id,first_name,last_name,gender,birthday,photos,email' })

How to test if some specific rack middleware is being used?

To be more particular, I'm talking about sentry-raven and sinatra here. I saw examples testing sinatra applications, or middlewares. But I didn't see ones testing if some particular middleware is present. Or should I be testing behavior, not configuration (or how should I call it)?
The important thing (I'd say) is the behaviour, but if you wish to check for middleware there are 2 ways I'd suggest after taking a delve into the Sinatra source (there are possibly much easier/better ways):
The env
In the Sinatra source there's a method that uses the env to check if a middleware is already present:
# Behaves exactly like Rack::CommonLogger with the notable exception that it does nothing,
# if another CommonLogger is already in the middleware chain.
class CommonLogger < Rack::CommonLogger
def call(env)
env['sinatra.commonlogger'] ? #app.call(env) : super
end
You could do the same thing in a route, e.g.
get "/env-keys" do
env.keys.inspect
end
It'll only show you the middleware if it's inserted something in env hash, e.g.
class MyBad
def initialize app, options={}
#app = app
#options = options
end
def call env
#app.call env.merge("mybad" => "I'm sorry!")
end
end
output:
["SERVER_SOFTWARE", "SERVER_NAME", "rack.input", "rack.version", "rack.errors", "rack.multithread", "rack.multiprocess", "rack.run_once", "REQUEST_METHOD", "REQUEST_PATH", "PATH_INFO", "REQUEST_URI", "HTTP_VERSION", "HTTP_HOST", "HTTP_CONNECTION", "HTTP_CACHE_CONTROL", "HTTP_ACCEPT", "HTTP_USER_AGENT", "HTTP_DNT", "HTTP_ACCEPT_ENCODING", "HTTP_ACCEPT_LANGUAGE", "GATEWAY_INTERFACE", "SERVER_PORT", "QUERY_STRING", "SERVER_PROTOCOL", "rack.url_scheme", "SCRIPT_NAME", "REMOTE_ADDR", "async.callback", "async.close", "rack.logger", "mybad", "rack.request.query_string", "rack.request.query_hash", "sinatra.route"]
It's near the end of that list.
The middleware method
There's also a method called middleware in Sinatra::Base:
# Middleware used in this class and all superclasses.
def middleware
if superclass.respond_to?(:middleware)
superclass.middleware + #middleware
else
#middleware
end
end
Call it in the class definition of a modular app and you can get the middlewares in an array:
require 'sinatra/base'
class AnExample < Sinatra::Base
use MyBad
warn "self.middleware = #{self.middleware}"
output:
self.middleware = [[MyBad, [], nil]]
There may be a way to get it from Sinatra::Application, but I haven't looked.
With help from ruby-raven guys, we've got this:
ENV['RACK_ENV'] = 'test'
# the app: start
require 'sinatra'
require 'sentry-raven'
Raven.configure(true) do |config|
config.dsn = '...'
end
use Raven::Rack
get '/' do
'Hello, world!'
end
# the app: end
require 'rspec'
require 'rack/test'
Raven.configure do |config|
logger = ::Logger.new(STDOUT)
logger.level = ::Logger::WARN
config.logger = logger
end
describe 'app' do
include Rack::Test::Methods
def app
#app || Sinatra::Application
end
class TestRavenError < StandardError; end
it 'sends errors to sentry' do
#app = Class.new Sinatra::Application do
get '/' do
raise TestRavenError
end
end
allow(Raven.client).to receive(:send).and_return(true)
begin
get '/'
rescue TestRavenError
end
expect(Raven.client).to have_received(:send)
end
end
Or if raven sending requests is in the way (when tests fail because of raven sending requests, and not because of the underlying error), one can disable them:
Raven.configure(true) do |config|
config.should_send = Proc.new { false }
end
And mock Raven.send_or_skip instead:
...
allow(Raven).to receive(:send_or_skip).and_return(true)
begin
get '/'
rescue TestRavenError
end
expect(Raven).to have_received(:send_or_skip)
...

How do I do a really simple Sinatra LDAP authentication?

I looked at the Sinatra docs and they only seem to reference HTTP authentication. I'm looking for a really simple way to control access to routes based on a user being authorised/authenticated via an LDAP server.
I've already built a class that does the LDAP bit and returns an LDAP object if the user has successfully authenticated and nil if they haven't:
>>DirectoryUser.authenticate('user', 'password')
#<DirectoryUser:0x007ffb589a2328>
I can use this to determine if they've successfully authenticated or not.
As a next step I want to splice this into a simple Sinatra app that provides a form to collect the LDAP user and password:
require 'directoryUser'
require 'sinatra'
enable :sessions
get '/form' do
username = params[:username]
password = params[:password]
haml :form
end
Then I want to only allow routes if the 'DirectoryUser' object exists:
get '/protected' do # Only if DirectoryUser object exists
"This route is protected"
end
get '/unprotected' do
"This route is unprotected"
end
I've spent hours trying to find an answer to this but so far and can't seem to find anything that works for me.
I'd probably go with something like this:
require 'directoryUser'
require 'sinatra'
enable :sessions
helpers do
def authorize!
redirect(to('/login')) unless session[:user_id]
end
end
get '/login' do
haml :login # with the login form
end
post '/login' do
user = DirectoryUser.authenticate(params[:username], params[:password])
if user
session[:user_id] = user.id
# Or: session[:logged_in] = true, depending on your needs.
redirect to('/protected')
else
redirect to('/login')
end
end
get '/protected' do
authorize!
'This route is protected'
end
get '/unprotected' do
'This route is unprotected'
end

Logging in Sinatra?

I'm having trouble figuring out how to log messages with Sinatra. I'm not looking to log requests, but rather custom messages at certain points in my app. For example, when fetching a URL I would like to log "Fetching #{url}".
Here's what I'd like:
The ability to specify log levels (ex: logger.info("Fetching #{url}"))
In development and testing environments, the messages would be written to the console.
In production, only write out messages matching the current log level.
I'm guessing this can easily be done in config.ru, but I'm not 100% sure which setting I want to enable, and if I have to manually create a Logger object myself (and furthermore, which class of Logger to use: Logger, Rack::Logger, or Rack::CommonLogger).
(I know there are similar questions on StackOverflow, but none seem to directly answer my question. If you can point me to an existing question, I will mark this one as a duplicate).
Sinatra 1.3 will ship with such a logger object, exactly usable as above. You can use edge Sinatra as described in "The Bleeding Edge". Won't be that long until we'll release 1.3, I guess.
To use it with Sinatra 1.2, do something like this:
require 'sinatra'
use Rack::Logger
helpers do
def logger
request.logger
end
end
I personally log in Sinatra via:
require 'sinatra'
require 'sequel'
require 'logger'
class MyApp < Sinatra::Application
configure :production do
set :haml, { :ugly=>true }
set :clean_trace, true
Dir.mkdir('logs') unless File.exist?('logs')
$logger = Logger.new('logs/common.log','weekly')
$logger.level = Logger::WARN
# Spit stdout and stderr to a file during production
# in case something goes wrong
$stdout.reopen("logs/output.log", "w")
$stdout.sync = true
$stderr.reopen($stdout)
end
configure :development do
$logger = Logger.new(STDOUT)
end
end
# Log all DB commands that take more than 0.2s
DB = Sequel.postgres 'mydb', user:'dbuser', password:'dbpass', host:'localhost'
DB << "SET CLIENT_ENCODING TO 'UTF8';"
DB.loggers << $logger if $logger
DB.log_warn_duration = 0.2
If you are using something like unicorn logging or other middleware that tails IO streams, you can easily set up a logger to STDOUT or STDERR
# unicorn.rb
stderr_path "#{app_root}/shared/log/unicorn.stderr.log"
stdout_path "#{app_root}/shared/log/unicorn.stdout.log"
# sinatra_app.rb
set :logger, Logger.new(STDOUT) # STDOUT & STDERR is captured by unicorn
logger.info('some info') # also accessible as App.settings.logger
this allows you to intercept messages at application scope, rather than just having access to logger as request helper
Here's another solution:
module MySinatraAppLogger
extend ActiveSupport::Concern
class << self
def logger_instance
#logger_instance ||= ::Logger.new(log_file).tap do |logger|
::Logger.class_eval { alias :write :'<<' }
logger.level = ::Logger::INFO
end
end
def log_file
#log_file ||= File.new("#{MySinatraApp.settings.root}/log/#{MySinatraApp.settings.environment}.log", 'a+').tap do |log_file|
log_file.sync = true
end
end
end
included do
configure do
enable :logging
use Rack::CommonLogger, MySinatraAppLogger.logger_instance
end
before { env["rack.errors"] = MySinatraAppLogger.log_file }
end
def logger
MySinatraAppLogger.logger_instance
end
end
class MySinatraApp < Sinatra::Base
include MySinatraAppLogger
get '/' do
logger.info params.inspect
end
end
Of course, you can do it without ActiveSupport::Concern by putting the configure and before blocks straight into MySinatraApp, but what I like about this approach is that it's very clean--all logging configuration is totally abstracted out of the main app class.
It's also very easy to spot where you can change it. For instance, the SO asked about making it log to console in development. It's pretty obvious here that all you need to do is a little if-then logic in the log_file method.

Resources