June 26th, 2008 cschneid
Thanks to the requests a few enterprising commenters, I’ve looked into logging with mod_rails and Sinatra a little closer. My last attempt was close, but not quite. If you swap out the config.ru file for this version, logging will work correctly.
require 'sinatra/lib/sinatra.rb'
require 'rubygems'
Sinatra::Application.default_options.merge!(
:run => false,
:env => :production,
:raise_errors => true
)
log = File.new("sinatra.log", "a")
STDOUT.reopen(log)
STDERR.reopen(log)
require 'test.rb'
run Sinatra.application
Some interesting things to note
- Open the file with “a” instead of “w”. Append rather than open means that restarts and the like won’t reset the log file. A fun fact about passenger: it seems that passenger restarts your application if you return a 500 response code. The append means that we don’t blow away the log file right away after that.
- raise_errors => true. This is important. When Sinatra is in production mode, it just stops throwing errors, and instead shows a simple “Server Error” page. This setting re-enables the exception throwing.
At least one more thing to look into:: Nice error pages. How can we have good error logging AND good error pages?
Posted in Programming | 5 Comments »
June 25th, 2008 cschneid
A question came up on IRC today regarding Sinatra’s redirect helper. royfreeman was trying to redirect from inside a post action. Sinatra redirected the URL ok, but the new request continued to use the POST method to request the new URL. He was trying to do this from a mobile device, and was confused when the request used a GET with his laptop’s browser.
Sinatra sends a 302 response code as a redirect. According to the spec, 302 shouldn’t change the request method, but you can see a note saying that most clients do change it. Apparently the mobile browser that person was using did things correctly (instead of the mainstream misinterpretation).
The fix for this in the spec is 2 different response codes: 303 and 307. 303 resets to GET, 307 keeps the same method.
To force sinatra to send a different response code, it’s very simple:
redirect '/', 303 # forces the 303 return code
## OR:
redirect '/', 307 # forces the 307 return code
Many thanks to Sydd for working through this, he did most of the hunting to find the specification details and to figure out what was going on.
Posted in Programming, Ruby, Sinatra | 4 Comments »
June 24th, 2008 cschneid
I wrote about how to deploy a Sinatra application on top of Passenger, with specific instructions for Dreamhost. As it turns out, the logging functionality of Sinatra didn’t work with the instructions, this is the workaround.
The technical details: The trick to this is that Sinatra’s logging uses Rack’s CommonLogger class. If you don’t tell it otherwise, it logs to whatever rack’s env[‘rack.error’] is set to, which is STDERR in this case. Sinatra doesn’t give an easy way to tell it to use a certain logger, but instead, always uses the defaults. The easiest way is to ignore that whole layer off annoyance, and just reopen STDERR and STDOUT to write to the file. That way, no change to Sinatra or your application is needed.
Just add three lines to your config.ru file:
# Add these 3 lines
log = File.new("sinatra.log", "w")
STDOUT.reopen(log)
STDERR.reopen(log)
The config.ru file in it’s entirety:
require 'sinatra/lib/sinatra.rb'
require 'rubygems'
Sinatra::Application.default_options.merge!(
:run => false,
:env => :production
)
# Add these 3 lines
log = File.new("sinatra.log", "w")
STDOUT.reopen(log)
STDERR.reopen(log)
require 'test.rb'
run Sinatra.application
Posted in Programming | 2 Comments »
June 17th, 2008 cschneid
I’ve previously written about deploying Sinatra to Dreamhost, both with FastCGI, and also Passenger (mod_rails). This howto will cover how to deploy Sinatra to a load-balanced reverse-proxy setup using Lighttpd and Thin.
1) Install Lighttpd and Thin
# Figure out lighttpd yourself, it should be handled by your
# linux distro's package manager
# For thin:
gem install thin
2) Create your rackup file - the “require ‘app’” line should require the actual sinatra app you have written.
require 'sinatra'
Sinatra::Application.default_options.merge!(
:run => false,
:env => :production
)
require 'app'
run Sinatra.application
3) Setup a config.yml - change the /path/to/my/app path to reflect reality.
---
environment: production
chdir: /path/to/my/app
address: 127.0.0.1
user: root
group: root
port: 4567
pid: /path/to/my/app/thin.pid
rackup: /path/to/my/app/config.ru
log: /path/to/my/app/thin.log
max_conns: 1024
timeout: 30
max_persistent_conns: 512
daemonize: true
4) Setup lighttpd.conf - change mydomain to reflect reality. Also make sure the first port here matches up with the port setting in config.yml.
$HTTP["host"] =~ "(www\.)?mydomain\.com" {
proxy.balance = "fair"
proxy.server = ("/" =>
(
( "host" => "127.0.0.1", "port" => 4567 ),
( "host" => "127.0.0.1", "port" => 4568 )
)
)
}
5) Start thin and your application . I have a rake script so I can just call “rake start” rather than typing this in. It’s just a few lines to do that.
thin -s 2 -C config.yml -R config.ru start
Go to mydomain.com/ and see the result! Everything should be setup now, check it out at the domain you setup in your lighttpd.conf file.
Variations - nginx - I haven’t looked into the config file syntax, but there isn’t any reason that this exact same approach wouldn’t work with nginx. Thin and the rackup file stay the same, while the nginx layer has to be configured to reverse proxy to the thin install.
Variations - More Thin instances - To add more thin instances, change the -s 2 parameter on the thin start command to be how ever many servers you want. Then be sure lighttpd proxies to all of them by adding more lines to the proxy statements. Then restart lighttpd and you’re done.
Posted in Linux, Programming, Ruby, Sinatra, System Admin | 3 Comments »
June 15th, 2008 cschneid
There are several ways of getting parameters and data into your Sinatra application.
1) URL Options
get '/post/:id' do
post_id = params[:id]
end
# Several options, period gets interpreted as a separator between the two.
get '/author/:id.:format' do
author_id = params[:id]
format = params[:format]
end
# Several options, separated by '/'
get '/author/:author_id/comments/:comment_id' do
author_id = params[:author_id]
comment_id = params[:comment_id]
end
2) URL Parameters - the classic ?foo=bar&baz=quux
# this will get the post id out of /author?id=2
get '/author' do
author_id = params[:id]
end
# Works the same way for the POST method (and DELETE and PUT of course)
post '/author' do
author_id = params[:id]
end
3) Raw Posted Data - if the POSTed content is in the “application/x-www-form-urlencoded” format, it will get parsed as above, with the params[:id] method of getting the data. But occasionally, you want the whole chunk of raw data. This is useful for images, or big chunks of xml.
post '/data' do
raw = request.env["rack.input"].read
end
Posted in Programming, Ruby, Sinatra | 11 Comments »
June 13th, 2008 cschneid
I went looking for a way to embed a short little script in my Haml template, but didn’t find much in my googling. Turns out that before Haml 2.0, to do this, it requires a custom filter. Well, now Haml 2.0 ships with a built-in javascript filter.
.before content above script
:javascript
alert("Hello World");
alert("More scripting!");
.after content after script
The script gets wrapped in <script> tags, and works as expected.
Posted in Programming | 1 Comment »
June 12th, 2008 cschneid
Cookies are a fairly simple thing to use in Sinatra, but they have a few quirks.
Lets first look at the simple use case:
require 'sinatra'
get '/' do
# Get the string representation
cookie = request.cookies["thing"]
# Set a default
cookie ||= 0
# Convert to an integer
cookie = cookie.to_i
# Do something with the value
cookie += 1
# Reset the cookie
set_cookie("thing", cookie)
# Render something
"Thing is now: #{cookie}"
end
Setting a path, expiration date, or domain gets a little more complicated - see the source code for set_cookie if you want to dig deeper.
set_cookie("thing", { :domain => myDomain,
:path => myPath,
:expires => Date.new } )
That’s the easy stuff with cookies - It can also serialize Array objects, separating them with ampersands (&), but when they come back, it doesn’t deserialize or split them in any way, it hands you the raw, encoded string for your parsing pleasure.
I’ll look into how serialization and data handling needs to occur in a later post. I’m guessing that it’s the user’s job to split and un-encode, but I haven’t verified that yet.
Posted in Programming, Ruby, Sinatra | 2 Comments »
June 9th, 2008 cschneid
After much poking and prodding it looks like Sinatra won’t work on top of JRuby Rack. It’s an unintended consequence of how everything was implemented, and it looks like some (hopefully minor) changes will have to be made to Sinatra.
Steps to get to where I am:
- Install JRuby 1.1.2
- Get tomcat - I used a 6.0.14 install I had laying around
- Install the JRuby gem “warble”, which will come with jruby-rack
- Create a new directory structure, described below
- Copy jruby-complete.jar, and jruby-rack.jar to the WEB-INF/lib directory
- Create a simple Sinatra hello world app
- Create your web.xml file as described
The directory structure
jruby-sinatra/
jruby-sinatra/WEB-INF
jruby-sinatra/WEB-INF/web.xml
jruby-sinatra/WEB-INF/hello_world.rb
jruby-sinatra/WEB-INF/sinatra # full copy of sinatra goes here
jruby-sinatra/WEB-INF/lib
jruby-sinatra/WEB-INF/lib/jruby-complete-1.1.1.jar
jruby-sinatra/WEB-INF/lib/jruby-rack-0.9.jar
My (non-working) current web.xml file.
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd">
<web-app>
<context-param>
<param-name>public.root</param-name>
<param-value>/</param-value>
</context-param>
<context-param>
<param-name>rackup</param-name>
<param-value>
$:.unshift '/Users/cschneid/servers/apache-tomcat-6.0.14/webapps/jruby-sinatra/WEB-INF/'
$:.unshift '/Users/cschneid/servers/apache-tomcat-6.0.14/webapps/jruby-sinatra/WEB-INF/sinatra/lib'
puts "====================="
puts "====================="
puts "====================="
puts "Current file is: #{__FILE__}"
puts "Dollar 0 is: #{$0}"
puts "List of load paths:"
puts $:.map {|a| puts a }
puts "====================="
puts "====================="
puts "====================="
puts "Requiring sinatra"
require "sinatra"
puts "Setting up the sinatra app options"
Sinatra::Application.default_options.merge!(
:run => false,
:env => :production
)
puts "About to require test"
require 'test'
puts "About to run sinatra"
run Sinatra.application
</param-value>
</context-param>
<filter>
<filter-name>RackFilter</filter-name>
<filter-class>org.jruby.rack.RackFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>RackFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<listener>
<listener-class>org.jruby.rack.RackServletContextListener</listener-class>
</listener>
</web-app>
The problem - the load fails on the Sinatra::Application.default_options.merge line. The first time you access default_options, it generates the hash. Every subsequent time, it just hands back the already created instance. Since this is the first time, it attempts to create the default_options structure. It gets a “root” of the directory tree from $0. And then later combines it with ‘/views’ and ‘/public’. The problem lies in the fact that we have a ruby script embedded inside the web.xml file. It doesn’t have a reasonable $0 to even set, so it doesn’t. If you ask for __FILE__, it just reports back with “<script>”. End result: you get a simple “can’t convert nil into String” exception thrown.
Next Steps - I don’t know, I think we should probably wean Sinatra off of $0 if at all possible. Or at least figure out how to set it to something reasonable so that default_options doesn’t blow up. If anybody has any ideas on how to go about this, let me know, since this doesn’t have an obvious fix that I see. Mainly, I don’t want to break functionality elsewhere (for plain ruby).
Posted in Programming, Ruby, Sinatra | 3 Comments »
June 8th, 2008 cschneid
I don’t like deployment via FastCGI that much. It’s just too unstable, and random for my tastes. So when I saw that Passenger was going to support Rack, I knew I had to get that working on Dreamhost.
Once I started going at it, it only took me a few minutes to make everything work. You can find documentation at the Passenger Github repository.
Steps:
- Setup your account to support mod_rails
- Create the directory structure
- Make a rackup file (config.ru)
- Create a very simple Sinatra application
1) Setting up the account
- Domains -> Manage Domains -> Edit (web hosting column)
- Enable “Ruby on Rails Passenger (mod_rails)”
- Add the public directory to the web directory box. So if you were using “gittr.com”, it would change to “gittr.com/public”
- Save your changes
2) Creating the directory structure
domain.com/
domain.com/tmp
domain.com/public
# a vendored version of sinatra - not necessary if you use the gem
domain.com/sinatra
3) Creating a rackup file
# This file goes in domain.com/config.ru
require 'sinatra/lib/sinatra.rb'
require 'rubygems'
Sinatra::Application.default_options.merge!(
:run => false,
:env => :production
)
require 'test.rb'
run Sinatra.application
4) A very simple Sinatra application
# this is test.rb referred to above
get '/' do
"Worked on dreamhost"
end
get '/foo/:bar' do
"You asked for foo/#{params[:bar]}"
end
And that’s all there is to it! Once it’s all setup, point your browser at your domain, and you should see a “Worked on Dreamhost” page. To restart the application after making changes, you need to run “touch tmp/restart.txt”.
Posted in Programming, Ruby, Sinatra, System Admin | 17 Comments »
June 4th, 2008 cschneid
Well, it looks like my bug report has been fixed already! Awesome response time! Anyway, it looks like we can expect Sinatra working on JRuby 1.1.3 (the next release).
Woot! Way to go JRuby guys!
Posted in Programming | 1 Comment »