Logging Closures and non Sinatra classes - ruby

I am new to Ruby and Sinatra and trying to access logger object inside closures (EM.run do .. end).
Here is extracts from working code where logging statements with messagge "LOGGER IS NOT ACCESSIBLE HERE" give compilation error.
class Connection
def get_updates
logger.info "LOGGER IS NOT ACCESSIBLE HERE 1"
end
end
class Streamer < Sinatra::Base
def stream
logger.info "Inside stream"
EM.run do
logger.info "LOGGER IS NOT ACCESSIBLE HERE 3"
Connection.new.get_updates
EM::PeriodicTimer.new(10) do
logger.info "LOGGER IS NOT ACCESSIBLE HERE 4"
end
end
end
end
get '/' do
logger.info "loading data"
Streamer.new.stream
end
From the document I found Sinatra uses env['rack.logger']. How can we use same in non Sinatra classes like Connection and Streamer in above code?

Actually a very interesting question. As it turns out, the logger field is not actually a variable - it is a method within Sinatra::Base. So when you are executing code that is in the scope of a Sinatra::Base object, you can call logger by just typing it and it will return you the logger object.
Since it's a part of the Sinatra::Base object, it won't be visible within the scope of a Connection object.
Now we're ending the simple Ruby stuff, time for the more advanced bit.
The more interesting part is when you call it from within the EM.run do section. The reason that you can't use logger in there is because those blocks are not executed within the scope of the Streamer object, they are executed in some other scope by eventmachine itself using something like instance_eval or class_eval - this post shows a good example of how instance_eval works in a DSL like EM.
This is also why the logger method is accessible from the get '/' block - Sinatra will execute this block in the scope of an object that has a logger method using instance_eval.
In order to do what you want to do, you can try creating a local variable containing the logger:
class Streamer < Sinatra::Base
def stream
# This will create a local variable called logger that will
# save whatever is returned by the logger method
logger = self.logger
logger.info "Inside stream"
EM.run do
logger.info "This should now be accessible"
Connection.new.get_updates
EM::PeriodicTimer.new(10) do
logger.info "This should now be accessible"
end
end
end
end
This still doesn't solve the problem of logging from within your Connection class. The way to do that would be to either use a global log variable (bad idea) or pass in the logger variable when you do Connection.new (not amazing idea, but better than a global).

Related

Ruby Module Constant Set and Read

I'm developing a module that contains standard methods as well as CLI (Thor) and API (Sinatra). I've created a Submodule that includes a class that inherits Sinatra::Base:
module Monteverde
module Activity
def self.version
"1.0.1"
end
class API < Sinatra::Base
set :port, 22340
get "/version" do
Monteverde::Activity.version
end
run! if defined? Monteverde::OPERATION
end
end
end
This "Activity" is invokes from the CLI (Thor):
module Monteverde
class CLI < Thor
desc "api", "Start Activity API"
def api
Monteverde.const_set("OPERATION", "started")
Monteverde::Activity::API
end
desc "aversion", "Get Activity Version"
def aversion
puts Monteverde::Activity.version
end
end
end
If I don't add an "if" to Sinatra's run! it will run automatically and take over the rest of the methods in the module:
...
class API < Sinatra::Base
register Sinatra::DefaultRoutes
set :port, 22340
get "/version" do
Monteverde::Activity.version
end
run!
end
$ ruby monteverde.rb aversion
$ == Sinatra (v2.0.0) has taken the stage on 22340 for development with backup from Puma
$ ...
My issue is that the OPERATION constant is not recognized even though it's set right before the module is called.
Why isn't OPERATION being recognized and how can I get it to be?
Is there another more intuitive/useful way to get Sinatra not to fire when I call the module?
In your API class definition, the run! line happens as soon as you require that file. It doesn't run again if you reference the class, as you're trying to do with the last line of your def api method.
To get around this, you can move the run! command into a method, and call that from Thor:
class API < Sinatra::Base
# ... other stuff
def self.start
run! if defined? Monteverde::OPERATION
end
end
Then in the Thor file:
def api
Monteverde.const_set("OPERATION", "started")
Monteverde::Activity::API.start
end
You can deduce that run! is a class method since it's callable in the scope of the class definition (not in an instance method), and so I define a class method to call it.
It would be simpler, though, to not define def self.start, and instead just call the run! method from the Thor file directly:
def api
Monteverde::Activity::API.run!
end

Sinatra Multiple Parallel Requests Variable Behaviour

I am fairly new to ruby and would like to understand how class instance variables behave in case of multiple parallel requests.
I have a method inside my controller class which is called everytime for each request for a specific operation (create in this case)
class DeployProvision
def self.create(data)
raise "Input JSON not received." unless data
# $logger.info input_data.inspect
failure = false
response_result = ""
response_status = "200"
#validator = SchemaValidate.new
validation = #validator.validate_create_workflow(data.to_json)
end
end
This method is called as (DeployProvision.create(data))
I am a little confused on how #validator class instance variable behaves when multiple requests come. Is it shared among multiple requests. Is it a good idea to declare this as class instance variable instead of a local variable ?
I am working on an existing code base and would like to understand the intent of creating #validator as a class instance variable instead of local variable.
You can write ultra-simple script like this:
require 'sinatra'
class Foo
def self.bar
#test = Time.now
puts #test
end
end
get '/' do
Foo.bar
end
and you'll see it does nothing, because with every call, you're creating new instance of Time(SchemaValidate in your code).
If you used memoization and had something like #validator ||= SchemaValidate.new you would have one instance of SchemaValidate stored between requests.
I don't think that'd change anything in terms of performance and I don't have idea why would anyone do something like that.
You can have some fun with ultra-simple scripts with sinatra to test how it behaves.
Good luck with this code!

How to create and call parameterised page-object classes

I am using page-factory visit, on methods to call page-object classes from spec file in ruby. I would like know how to parameterise page-object classes, passing parameters from spec file using page factory methods.
I want to log all steps information in page-object class. To do this, I created a log in spec file using the logger gem. I need to pass the log object as input parameter to page classes to capture data. Here is the code I am using to do this.
spec file that calling page class:
require './lib/pages/Test_page'
file="logs/uniusecase_#{#ncs_server['build_no']}_#{#ncs_server['test_type']}_#{time}.log"
$log=Logger.new(file)
describe 'testcase-1',:sanity do
visit Testpage, using_params: {logger: $log} do |page|
end
end
page-object class:
class Testpage
include PageObject
log = "<%=params[:logger]%>"
def goto
log ("test msg-1")
end
def testmethod()
log("test msg -2")
end
end
I am getting "NameError: undefined local variable or method `log' error message while execution. Could somebody help me in doing this?
The :using_params are stored in the class' #merged_params variable. You can get this variable by doing:
self.class.instance_variable_get(:#merged_params)
So your method would look something like:
def goto
logger = self.class.instance_variable_get(:#merged_params)[:logger]
logger("test msg-1")
end
However, if you are defining the logger in a global variable, it will already be available to the page object class (ie you do not need to pass it along). In other words, you could simply do:
def goto
$log ("test msg-1")
end

Share state between test classes and tested classes

I'm experimenting with RSpec.
Since I don't like mocks, I would like to emulate a console print using a StringIO object.
So, I want to test that the Logger class writes Welcome to the console. To do so, my idea was to override the puts method used inside Logger from within the spec file, so that nothing actually changes when using Logger elsewhere.
Here's some code:
describe Logger do
Logger.class_eval do
def puts(*args)
???.puts(*args)
end
end
it 'says "Welcome"' do
end
Doing this way, I need to share some StringIO object (which would go where the question marks are now) between the Logger class and the test class.
I found out that when I'm inside RSpec tests, self is an instance of Class. What I thought initially was to do something like this:
Class.class_eval do
attr_accessor :my_io
#my_io = StringIO.new
end
and then replace ??? with Class.my_io.
When I do this, a thousand bells ring in my head telling me it's not a clean way to do this.
What can I do?
PS: I still don't get this:
a = StringIO.new
a.print('a')
a.string # => "a"
a.read # => "" ??? WHY???
a.readlines # => [] ???
Still: StringIO.new('hello').readlines # => ["hello"]
To respond to your last concern, StringIO simulates file behavior. When you write/print to it, the input cursor is positioned after the last thing you wrote. If you write something and want to read it back, you need to reposition yourself (e.g. with rewind, seek, etc.), per http://ruby-doc.org/stdlib-1.9.3/libdoc/stringio/rdoc/StringIO.html
In contrast, StringIO.new('hello') establishes hello as the initial contents of the string while leaving in the position at 0. In any event, the string method just returns the contents, independent of position.
It's not clear why you have an issue with the test double mechanism in RSpec.
That said, your approach for sharing a method works, although:
The fact that self is an anonymous class within RSpec's describe is not really relevant
Instead of using an instance method of Class, you can define your own class and associated class method and "share" that instead, as in the following:
class Foo
def self.bar(arg)
puts(arg)
end
end
describe "Sharing stringio" do
Foo.class_eval do
def self.puts(*args)
MyStringIO.my_io.print(*args)
end
end
class MyStringIO
#my_io = StringIO.new
def self.my_io ; #my_io ; end
end
it 'says "Welcome"' do
Foo.bar("Welcome")
expect(MyStringIO.my_io.string).to eql "Welcome"
end
end
Logger already allows the output device to be specified on construction, so you can easily pass in your StringIO directly without having to redefine anything:
require 'logger'
describe Logger do
let(:my_io) { StringIO.new }
let(:log) { Logger.new(my_io) }
it 'says welcome' do
log.error('Welcome')
expect(my_io.string).to include('ERROR -- : Welcome')
end
end
As other posters have mentioned, it's unclear whether you're intending to test Logger or some code that uses it. In the case of the latter, consider injecting the logger into the client code.
The answers to this SO question also show several ways to share a common Logger between clients.

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