Proxy blog

Let's create a Rails app to stream live logs

#Technic 04/06/2014

A company asked me to demonstrate my skills for Rails, BackboneJS, MarionetteJS and data live stream. I decided to get a try with javascript EventSource and ActionController::Live. You can take a look at the sources here. It provides an Interface to watch and filter app logs, live!

I share in this post various things I learned around this experimentation.


Browser to server with EventSource

EventSource is a new HTML5 feature that provides one-way (server to client) event streaming. It’s really simple technology, easy to implement and use. Basically you open a connection from the browser with the EventSource object and the server send JSON message through this continuous connection.

Here an exemple with my start_streaming function (see logs.coffee view):

start_streaming: ()->
  #skip if source already open
  return false if this.source

  this.source = new EventSource RL.relPath("logs/#{this.model.get('name')}")

  me = this
  this.source.addEventListener 'error', (e)->
    if this.readyState == EventSource.CLOSED
      me.logError 'Connection lost...'
    else
      me.logError 'Connection error... Auto reconnect'

  this.source.addEventListener 'logs.all', (e) =>
    me.logError 'Reconnected' if this.error
    this.error = false
    markdown = $.parseJSON(e.data)
    matching = 'match' if !!this.filter && markdown.indexOf(this.filter) != -1
    me.logHtml "<pre class='prettyprint'><code class='#{matching}'>#{markdown}</code></pre>"
  true

More details about this technology:

Browser compatibility

Like always, Internet Explorer still do not implement it. Any way, you have consider that only most recent version of Firefox and Chrome implement it. During this experimentation I found this article that propose two solutions that provides fallbacks in case of missing EventSource:

I haven’t tried any of them yet, but soon … may be …

Rails ActionController::Live

EventSource is half of the way. Server side should handle the connection. Here come to the rescue ActionController::Live. It’s a module you can drop in any controller to turn it into a potential streaming endpoint.

Here a basic exemple with message formatting for an EventSource listener:

class StreamingController < ActionController::Base
  include ActionController::Live

  def stream
      logger.info "Stream start"
      response.headers['Content-Type'] = 'text/event-stream'

      0..100.each do |i|
         send_event "Hello for the #{i} time"
      end
      logger.info "Stream end"
  end

  protected
  def send_event message
       unless message.blank?
            response.stream.write "event: new_message\n"
            response.stream.write "data: #{message.to_json}\n\n"
       end
  end
end

You can take a look at this Rails casts (pro) or at my LogsController if you want to see how I connect it with the logs tail.

Read continuously a file in Ruby

Now we have a continuous connection from the Browser to the server, let’s connect the logs files. We need to read the log file and when reaching the end, wait for more!

It’s something that I made in C, back to university, but I never did it with Ruby. It tried couple of technics but the easiest way came out through file-tail gem.

Some basic usage:

File.open(filename) do |log|
  log.extend(File::Tail)
  log.interval = 10
  log.backward(10)
  log.tail { |line| puts line }
end

Backbone HAML templates

Now we get through the core technical feature, it’s time for classic UI.

I love HAML and I don’t want to use anything else even for backbone. I found another gem to fix this haml coffee assets. It allow to write HAML templates containing coffee script instead of ruby.

#cart
  %h2 Cart
  - if @cart.length is 0
    %p.empty Your cart is empty
  - else
    %ul
      - for item in @cart
        %li
          .item
            = item.name
            %a{ :href => "/cart/item/remove/#{ item.id }" } Remove Item

HAML is not the fastest templating solution in term of performances (may be the opposite). But HAML readability and typing speed is everything to me.

Templates files are stored into app/assets/templates or app/assets/templates/live_logs in my case for nested reason with .hamlc extension. I guess precompiling assets will turn them into html files with javascript, resolving performance issue.

Backbone JST issue with nested path

Because I used a rails engine, assets are nested under the engine path live_logs. JST loading system is a bit weak and get lost with it. Here some Coffee Script to fix this.

# Let's define a JST fixer that:
# * Base on current path
# * Remove root '/'
# * ensure that both '/a_path' and '/a_path/' works

RL.JST = (path)->
  id = window.location.pathname
  id = id.substring(1) if id[0] == '/'
  id = id + '/' unless id[id.length - 1] == '/'
  id = id + path
  JST[id]

And here we go, let’s replace default JST call by our app function

class RL.Tab extends Marionette.ItemView
  #template: JST['tab']
  template: RL.JST 'tab'

jQuery matcher :contains

The last feature I implemented was a log filter. I discovered a jQuery feature that I didn’t know: the contains selector. With those simple lines, I apply a match class to highlight the content:

this.ui.container.find("code").removeClass 'match'
this.ui.container.find("code:contains(#{textToFind})").addClass 'match'

And done

Really fast explained tiny things today. If you want more details, fork a version of my repository. I think I will get through tiny projects like this one more often, really refreshing. If you find something useful or have question on this short article or Live logs Rails engine, send me your feedback.