Streaming using WebSockets and Sproutcore

This is a quick brain dump on how I the Chililog Workbench Stream page works.

I don’t profess to be a Sproutcore guru so if you know of a better way to do what I’ve done, please let me know!

The code is located here: https://github.com/chililog/chililog-server/tree/master/src/main/sc2

Stream.html Page

The stream page is just a standard HTML page.  No tricky server side code here.  I’ve used Sproutcore V2 to do all the UI rendering.

The main action happens with this bit of html:

<script type="text/javascript">

<div class="container">

<div class="title">

<h1>Stream</h1>

Watch real time log entries as they come in. Select your repository and click Start.</div>

<div id="streamContent" class="clearfix">

<form class="alert-message block-message info form-stacked">

<fieldset>
        {{view App.RepositoryField id="repositoryField" }}
        {{view App.SeverityField id="severityField" }}
        {{view App.SourceField id="sourceField" }}
        {{view App.HostField id="hostField" }}
        {{view App.ActionButton id="actionButton" class="btn primary"}}&nbsp;&nbsp;&nbsp;
        {{view App.ClearButton id="clearButton" class="btn" isVisibleBinding="App.pageController.showActionButton2" }}

<div class="clearfix"></div>

</fieldset>

    </form>

    {{view App.ErrorMessage id="errorMessage"}}

<div id="results" style="display: none;">

<div class="heading ui-corner-all">Log Entries</div>

</div>

    {{#view id="bottombar" class="clearfix"}}

<div style="float: right;">
        {{view App.ClearButton id="clearButton2" class="btn" isVisibleBinding="App.pageController.showActionButton2" }}
        &nbsp;&nbsp;&nbsp;
        {{view App.ActionButton id="actionButton2" class="btn primary" isVisibleBinding="App.pageController.showActionButton2" }}</div>

<div style="float: left;">
        {{view App.TestMessageButton id="testMessageButton" class="btn" isVisibleBinding="App.pageController.isStreaming" }}</div>

    {{/view}}</div>

  ...
</script>

Points to note:

  • The form #streamContent > form renders the streaming criteria.
  • #results is where incoming log entries will be rendered.
  • #bottombar holds the buttons that appear at the bottom of the page.
  • The “code behind” specific to this html file is stream.js.
  • The CSS and styling mainly comes from Twitter Bootstrap

Views

Fields

I’ve got views named like App.RepositoryField and App.SourceField.

I’ve taken a field to be a label, a data entry control and optional help.

To stop repetition, I found it easy to define a generic field views (in app_views.js) :

  • App.FieldView – label and data control on the same line
  • App.StackedFieldView - label on top of data control.

I then inherit and customise these generic fields as so:

/**
 * @class
 * Source field
 */
App.SourceField = App.StackedFieldView.extend({
  label: '_stream.source'.loc(),

  DataView : App.TextBoxView.extend(App.CriteriaFieldDataMixin, {
    classNames: 'medium'.w(),
    valueBinding: 'App.pageController.source',
    name: 'source',
    disabledBinding: SC.Binding.from('App.pageController.isStreaming').oneWay().bool()
  })
});

Simple Bindings

I’ve set my fields and other views to bind to values in the App.pageController (see below).

To bind a property’s value, just append the word “Binding” to the property name and set the value of the property to the name of the object containing the desired value.

In the above example, valueBinding  is set to App.pageController.source. This means that every time the value of App.pageController.source changes, the value property my text box will be set to new value.  The browser will then render the new value.

Complex Bindings

Sometimes, you need more than a simple 1-1 mapping.  Sproutcore supports binding transformation.

In the above example, disableBinding is set to SC.Binding.from(‘App.pageController.isStreaming’).oneWay().bool().

This means :

  • the property disable will be set to the contents of App.pageController.isStreaming.  
  • oneWay() flags that App.pageController.isStreaming NOT be set to the value of disabled.
  • bool() transforms the data content into a true/false.

For more on Sproutcore transformation, see http://wiki.sproutcore.com/w/page/12412963/Runtime-Bindings.

I know it is old documentation. I’ve tried to submit this to the guides but my pull request has not been accepted as yet.

Controllers

I’ve tried to make sure that views and business logic do not directly communicate with each other.  I feel this separation of concern makes the app easier to maintain.

They communicate via App.pageController and App.statechart.

  • Business logic changes properties in the App.pageController  and views react to that
  • Views updates  App.pageController  with data and triggers business logic by sending actions to App.statechart.
/**
 * @class
 * Mediates between state charts and views
 */
App.pageController = SC.Object.create({
  /**
   * Selected item of the repository field
   *
   * @type App.RepositoryStatusRecord
   */
  repository: null,

  /**
   * Selected item of the severity field
   *
   * @type SC.Object
   */
  severity: null,

  /**
   * Value of the source field
   *
   * @type String
   */
  source: '',

  /**
   * Value of the host field
   *
   * @type String
   */
  host: '',

  /**
   * Error message to display
   *
   * @type String
   */
  errorMessage: '',

  /**
   * Indicates if we are currently streaming or not
   *
   * @type Boolean
   */
  isStreaming: NO,

  /**
   * Maximum number of log entries displayed. If this is exceeded, the earliest entries are deleted
   *
   * @type int
   */
  maxRowsToDisplay: 1000,

  /**
   * Options for displaying in the repository dropdown
   *
   * @type SC.ArrayProxy of SC.RepositoryStatusRecord
   */
  repositoryOptions: SC.ArrayProxy.create(),

  /**
   * Options for displaying in the severity dropdown
   *
   * @type Array of SC.Object
   */
  severityOptions: [
    SC.Object.create({label: '_repositoryEntryRecord.Severity.Emergency'.loc(), value: '0'}),
    SC.Object.create({label: '_repositoryEntryRecord.Severity.Action'.loc(), value: '1'}),
    SC.Object.create({label: '_repositoryEntryRecord.Severity.Critical'.loc(), value: '2'}),
    SC.Object.create({label: '_repositoryEntryRecord.Severity.Error'.loc(), value: '3'}),
    SC.Object.create({label: '_repositoryEntryRecord.Severity.Warning'.loc(), value: '4'}),
    SC.Object.create({label: '_repositoryEntryRecord.Severity.Notice'.loc(), value: '5'}),
    SC.Object.create({label: '_repositoryEntryRecord.Severity.Information'.loc(), value: '6'}),
    SC.Object.create({label: '_repositoryEntryRecord.Severity.Debug'.loc(), value: '7', selected: YES })
  ],

  /**
   * Flag to indicate if we want the bottom bar to show or not
   *
   * @type Boolean
   */
  showActionButton2: NO,

  /**
   * Writes a log entry to the results area
   * @param {Object} logEntry data to write to page
   * @param {Boolean} doSeverityCheck YES to perform severity check to decide if log entry is to be dispalyed or not
   */
  writeLogEntry: function (logEntry, doSeverityCheck) {
   ...
  }
});

State Chart

If you are developing an app using SC2, please consider using a State Chart.  I really helps order you thinking and your logic.

App.statechart = SC.Statechart.create({

  rootState: SC.State.extend({

    initialSubstate: 'notStreaming',

    /**
     * Prompt the user for criteria
     */
    notStreaming: SC.State.extend({
      enterState: function() {
      },

      exitState: function() {
      },

      doStart: function() {
        App.pageController.set('errorMessage', '');
        this.gotoState('streaming');
      }
    }),

    /**
     * Startup web socket and wait for incoming messages
     */
    streaming: SC.State.extend({
      enterState: function() {
        App.pageController.set('isStreaming', YES);
        App.streamingController.startStreaming();
      },

      /**
       * Stop streaming
       */
      doStop: function() {
        App.pageController.set('errorMessage', '');
        this.gotoState('notStreaming');
      },

      exitState: function() {
        App.streamingController.stopStreaming();
        App.pageController.set('isStreaming', NO);
      }
    })
  })
});

This is a simple state chart.

  1. Start state is notStreaming.
  2. If the you clicks Start button, it sends a doStart action to the state chart.
  3. This triggers the doStart() under notStreaming to execute. This moves the state to streaming.
  4. While streaming, if the user clicks Stop, it sends a doStop action to the state chart.
  5. This triggers the doStop() under streaming to execute. This moves the state back to notStreaming.

During the transitions, the isStreaming property in the App.pageController is updated.
This will trigger our views to enable/disable (like App.SourceField as detailed above) as well as change label (like App.ActionButton which changes between start and stop).

Receiving Log Entries

When the Start button is clicked, we start receiving log entries.

For simplicity, I’ve put the code in its own controller: App.streamingController.

App.streamingController = SC.Object.create({
  /**
   * Current Websocket being used to talk to the server
   *
   * @type WebSocket
   */
  webSocket: null,

  /**
   * Make web socket connection and wait for log entries to come down
   */
  startStreaming: function() {
    try {
      $('#results').css('display', 'block');
      var webSocket = new App.WebSocket('ws://' + document.domain + ':61615/websocket');

      webSocket.onopen = function () {
        SC.Logger.log('Socket opening');

        var request = {
          MessageType: 'SubscriptionRequest',
          MessageID: new Date().getTime() + '',
          RepositoryName: App.pageController.getPath('repository.name'),
          Source: App.pageController.getPath('source'),
          Host: App.pageController.getPath('host'),
          Severity: App.pageController.getPath('severity.value'),
          Username: App.sessionEngine.getPath('loggedInUser.username'),
          Password: 'token:' + App.sessionEngine.get('authenticationToken')
        };
        var requestJSON = JSON.stringify(request);
        webSocket.send(requestJSON);

        App.pageController.writeLogEntry({
          Timestamp: '',
          Source: 'workbench',
          Host: 'local',
          Severity: '6',
          Message: 'Started.  Waiting for messages ...'
        }, false);
      };

      webSocket.onmessage = function (evt) {
        SC.Logger.log('Socket received message: ' + evt.data);
        try {
          var response = JSON.parse(evt.data);
          if (!response.Success) {
            // Turn our error into a message for display
            response.LogEntry = {
              Timestamp: '',
              Source: 'Chililog',
              Host: 'Chililog',
              Severity: '3',
              Message: response.ErrorMessage
            };
          }
          App.pageController.writeLogEntry(response.LogEntry, true);
        }
        catch (exception) {
          SC.Logger.log('Error parsing log entry. ' + exception);
        }
      };

      webSocket.onclose = function (evt) {
        SC.Logger.log('Socket close');
        App.pageController.writeLogEntry({
          Timestamp: '',
          Source: 'workbench',
          Host: 'local',
          Severity: '6',
          Message: 'Stopped'
        }, false);
      };

      webSocket.onerror = function (evt) {
        SC.Logger.log('Socket error: ' + evt.data);
      };

      App.streamingController.set('webSocket', webSocket);
    } catch (exception) {
      App.pageController.errorMessage = exception;
      this.gotoState('notStreaming');
    }
  },

  /**
   * Stop streaming
   */
  stopStreaming: function() {
    try {
      App.streamingController.get('webSocket').close();
    } catch (exception) {
      SC.Logger.log('Error closing web socket: ' + exception);
    }
  }
});

When we start streaming, App.streamingController.startStreaming() is called. This establishes a web socket connect with the server.

When log entries are received, it uses App.pageController.writeLogEntry() to render it.  I’ve written directly to the DOM using jQuery rather than use a DataStore because:

  • I found that the data store performed very slowly compared to writing directly to the DOM when there is a large number of log entries to display
  • Because log entries are displayed as read only, I do not need CRUD functions of SC.Record and SC.DataStore
When set stop streaming, App.streamingController.stopStreaming() is called.

Sending Log Entries

To send test log entries, I setup another web socket.

I put this code local to the Send Test Message button.

App.TestMessageButton = App.ButtonView.extend({
  label: '_stream.test'.loc(),

  click: function() {
    this.set('disabled', YES);

    var username = App.sessionEngine.getPath('loggedInUser.username');
    var scDate = SC.DateTime.create({ timezone: 0});
    var ts = scDate.toFormattedString('%Y-%m-%dT%H:%M:%S.%sZ');
    var request = {
      MessageType: 'PublicationRequest',
      MessageID: new Date().getTime() + '',
      RepositoryName: App.pageController.getPath('repository.name'),
      Username: App.sessionEngine.getPath('loggedInUser.username'),
      Password: 'token:' + App.sessionEngine.get('authenticationToken'),
      LogEntries: [
        { Timestamp: ts, Source: 'workbench', Host: 'local', Severity: '7', Message: 'Test DEBUG message sent from browser with all sorts of funny characters !@#$%^&*()_+{}[]:";\'<>,.?/ ' + navigator.userAgent},
        { Timestamp: ts, Source: 'workbench', Host: 'local', Severity: '4', Message: 'Test WARNING message with a timestamp and a very long example of a java class path org.chililog.server.pubsub.websocket.AVeryLongClassName. The time is now ' + new Date() },
        { Timestamp: ts, Source: 'workbench', Host: 'local', Severity: '3', Message: 'Test ERROR message sent by ' + username}
      ]
    };

    try {
      var me = this;
      var webSocket = new App.WebSocket('ws://' + document.domain + ':61615/websocket');

      webSocket.onopen = function () {
        SC.Logger.log('Test Socket opening');
        var requestJSON = JSON.stringify(request);

        // Sent it
        webSocket.send(requestJSON);
      };

      webSocket.onmessage = function (evt) {
        SC.Logger.log('Socket received message: ' + evt.data);
        try {
          var response = JSON.parse(evt.data);
          if (!response.Success) {
            response.LogEntry = { Timestamp: '', Source: 'Chililog', Host: 'Chililog', Severity: '3', Message: response.ErrorMessage };
            App.pageController.writeLogEntry(response.LogEntry, true);
          }

          // Close after we get a response so that button is enabled and the user can send another message
          webSocket.close();
        }
        catch (exception) {
          SC.Logger.log('Error parsing log entry. ' + exception);
        }
      };

      webSocket.onclose = function (evt) {
        SC.Logger.log('Test Socket close');
        me.set('disabled', NO); //cannot use this because it is the websocket
      };

      webSocket.onerror = function (evt) {
        SC.Logger.log('Test Socket error: ' + evt.data);
        var logEntry = { Timestamp: '', Source: 'Chililog', Host: 'Chililog', Severity: '3', Message: evt.data };
        App.pageController.writeLogEntry(logEntry, true);
      };
    } catch (exception) {
      SC.Logger.log('Test Socket error: ' + evt.data);
      var logEntry = { Timestamp: '', Source: 'Chililog', Host: 'Chililog', Severity: '3', Message: exception };
      App.pageController.writeLogEntry(logEntry, true);
    }

    return;
  }
});

Remote Javascript Logging with Chililog

Step by Step Tutorial Video

In case you don’t have the time, here’s a video I made of this demo. DYI instructions with links to the live demo site are below the video.

Remote JavaScript Logging with Chililog from chililog on Vimeo.

Live Demo DYI Instructions

1. Start Chililog Workbench

The Chililog workbench allows you view incoming log entries in real time as well as to search for historical log entries.
You will need to use Safari 5+, Chrome 13+ or Firefox 7+ because they support web sockets.

  1. Open the workbench (http://demo.chililog.com/workbench/index.html) in a new window.
  2. Login as sandpitworkbench and password sandpit.
  3. Click Stream
  4. Click Start.

2. Start TODO JavaScript application

I have used the Sproutcore 2 sample TO DO app and injected Chililog javascript logger into it.

  1. Open the TO DO app (http://jsdemo.chililog.org/examples/sproutcore2/) in another new window.
  2. Place your browser windows side-by-side so you can see both the Chililog Workbench and the TO DO app at the same time.

3. Streaming

  1. In the TO DO app, type your to do tasks and click ENTER.
  2. As you click ENTER for each task, you will see the log entries appear in the Chililog Workbench.
  3. Try getting a friend to start the TO DO app on another computer and type in some tasks. You will be able to see their tasks streamed to the Chililog Workbench on your desktop browser. Chililog aggregates all log entries.

4.Searching

  1. In the Chililog Workbench, click Search in the top menu bar.
  2. Select a time period of 30 minutes and click Search.
  3. You will see all the log entries that have been sent to Chililog in the past 30 minutes.
  4. Double click on an entry to view more details.

Getting the Source Code

Free Evaluation Account

For those who don’t have the time to download and install chililog server for evaluation, I can setup an account for you on our demo server.

Just leave a reply to this post and I’ll email you back the details.

At long last … a chililog web site

Here it is … http://www.chililog.org.

What do you think?

Working with Sproutcore 2

I stated in an earlier post that the SC1 version of Chililog workbench sucked because:

  1. Table Views were not working
  2. My implemented mixed web app and mobile app concepts. The hybrid did not feel right.

So here’s a summary of my experience migrating a SC1 app to SC2 Beta 3.  Hope it helps anyone on the same boat…

Note

As per the core team’s recommendation, only use SC2 for web apps.  For mobile and desktop apps, stick to SC1 at this point in time.

I am assuming the reader has a good understanding of SC1.  If you are a new Sproutcore user, I’m planning future posts on SC2 that will be more like a tutorial.

Build Tools

The first noticeable difference is the build tools.

In SC1, we had:

  • sc-gen to generate projects and files
  • sc-server for use in development to serve our assets to a browser, and
  • sc-build to compile and deploy.

In SC2, this is no longer the case.

  • SC2 comes has a starter kit that contains barebones HTML, css and javascript to get you started.
  • However, the starter kit only contains the core sproutcore package.  To be useful, you really need to include other packages like state chart, data store and datetime.  I manually downloaded these Javascript files and combined then in the correct order before putting them into my project.  What a pain … then I found out about Browser Package Manager (BPM)
  • BPM is a build tool that downloads the javascript packages you specify, combines them in the correct order and then minifies them for you.  If you wish, you can write your app as a BPM package and use BPM as your build tool.  I’ve yet to use BPM due to a lack of time … I’m planning to wait until SC2 Release Candidate release before giving it a go.

Views

The view layer is totally different to SC1.

In SC1, we had views like SC.ButtonView and SC.ListView.  I declared and layout the login page controls like so:

/**
 * login_page.js
 */
Chililog.loginPage = SC.Page.design({

  loginPane:  SC.MainPane.design({
    layout: { top: 0, left: 0, bottom: 0, right: 0 },
    childViews: 'boxView'.w(),

    boxView: SC.View.design({
      layout: { width: 300, height: 280, centerX: 0, centerY: -100 },
      classNames: ['login-box'],
      childViews: 'title line username password loginButton loadingImage'.w(),

      title: SC.LabelView.design({
        layout: { top: 20, left: 20, right: 20, height: 30 },
        controlSize: SC.LARGE_CONTROL_SIZE,
        tagName: 'h1',
        value: 'Chililog Workbench Login'
      }),

      line: SC.LabelView.design({
        layout: { top: 50, left: 20, right: 20, height: 2 },
        tagName: 'hr'
      }),

      username: SC.View.design({
        layout: {top: 70, left: 20, right: 20, height: 50 },
        childViews: 'label field'.w(),

        label: SC.LabelView.design({
          layout: { top: 0, left: 0, right: 0, height: 19 },
          value: '_loginPane.Username',
          localize: YES
        }),

        field: SC.TextFieldView.design({
          layout: { top: 20, left: 0, right: 0, height: 25 },
          isEnabledBinding: SC.Binding.from('Chililog.loginViewController.isLoggingIn').oneWay().not(),
          valueBinding: 'Chililog.loginViewController.username'
        })
      }),

      password: SC.View.design({
        layout: {top: 130, left: 20, right: 20, height: 50 },
        childViews: 'label field'.w(),

        label: SC.LabelView.design({
          layout: { top: 0, left: 0, right: 0, height: 19 },
          value: '_loginPane.Password',
          localize: YES
        }),

        field: SC.TextFieldView.design({
          layout: { top: 20, left: 0, right: 0, height: 25 },
          isPassword: YES,
          isEnabledBinding: SC.Binding.from('Chililog.loginViewController.isLoggingIn').oneWay().not(),
          valueBinding: 'Chililog.loginViewController.password'
        })
      }),

      loginButton: SC.ButtonView.design({
        layout: {top: 230, width: 100, height: 35, centerX: 0 },
        title: '_loginPane.Login',
        localize: YES,
        isDefault: YES,
        controlSize: SC.HUGE_CONTROL_SIZE,
        isEnabledBinding: SC.Binding.from('Chililog.loginViewController.isLoggingIn').oneWay().not(),
        target: 'Chililog.loginViewController',
        action: 'login'
      }),

      loadingImage: Chililog.ImageView.design({
        layout: { top: 235, right: 50, width: 16, height: 16 },
        value: sc_static('images/working'),
        isVisibleBinding: SC.Binding.from('Chililog.loginViewController.isLoggingIn').oneWay().bool(),
        useImageQueue: NO
      })
    })  //boxView
  })  //loginPane
});</pre>

In SC2, we are now able to declare the views separately to laying them out.  Laying out the views is done in simple HTML using handlebar templates (much like SC.TemplateView in SC1).  Views can be declered “inline” within the handlebar template or separately in another file.  I’ve typically declared the views in another to keep my layout HTML template as simple as possible.

<!-- Login.html -->
<!doctype html>
<!--[if lt IE 7 ]><html lang="en" class="no-js ie6"><![endif]-->
<!--[if IE 7 ]><html lang="en" class="no-js ie7"> <![endif]-->
<!--[if IE 8 ]><html lang="en" class="no-js ie8"> <![endif]-->
<!--[if IE 9 ]><html lang="en" class="no-js ie9"> <![endif]-->
<!--[if (gt IE 9)|!(IE)]><!--><html lang="en"> <!--<![endif]-->
<head>
  <!-- START COMMON HEAD BLOCK -->
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">

  <title>chililog Workbench</title>

  <meta name="viewport" content="width=device-width, initial-scale=1.0">

  <link rel="shortcut icon" href="/favicon.ico">
  <link rel="stylesheet" type="text/css" href="css/bootstrap-1.3.0.css">
  <link rel="stylesheet" type="text/css" href="css/chililog.css">
  <link rel="stylesheet" type="text/css" href="css/default/jquery-ui.css" />
  <link rel="stylesheet" type="text/css" href="css/default/theme.css">

  <!--[if lt IE 9]>
  <script src="http://html5shiv.googlecode.com/svn/trunk/html5.js"></script>
  <![endif]-->
  <!-- END COMMON HEAD BLOCK -->
</head>
<body class="login">

<div class="titleBar">
  chililog workbench login
</div>
<div class="contentBar">
  <script type="text/x-handlebars">
    {{#view App.ErrorMessage class="alert-message block-message error" }}
      {{message}}
    {{/view}}

    <div class="form-stacked">
      {{#view App.UsernameField id="usernameField" class="clearfix" }}
        <label for="usernameData">{{label}}</label>
        <div class="input">{{view Data id="usernameData" class="xlarge"}}</div>
      {{/view}}

      {{#view App.PasswordField id="passwordField" class="clearfix"}}
        <label for="passwordData">{{label}}</label>
        <div class="input">{{view Data id="passwordData" class="xlarge"}}</div>
      {{/view}}

      {{#view App.RememberMeField id="rememberMeField" class="clearfix"}}
        {{view Data id="rememberMeData"}}
      {{/view}}
    </div>

    <div class="buttonBar">
      {{view App.LoginButton class="btn primary"}}&nbsp;&nbsp;&nbsp;{{view App.WorkingImage}}
    </div>
  </script>
</div>

<!-- START COMMON SCRIPT BLOCK -->
<script type="text/javascript" src="js/libs/jquery-1.6.2.min.js"></script>
<script type="text/javascript" src="js/libs/jquery-ui-1.8.16.custom.min.js"></script>
<script type="text/javascript" src="js/libs/sproutcore-2.0.beta.3.js"></script>
<script type="text/javascript" src="js/libs/sproutcore-datetime.js"></script>
<script type="text/javascript" src="js/libs/sproutcore-utils.js"></script>
<script type="text/javascript" src="js/libs/sproutcore-statechart.js"></script>
<script type="text/javascript" src="js/libs/sproutcore-datastore.js"></script>
<script type="text/javascript" src="js/app.js"></script>
<script type="text/javascript" src="js/app_strings.js"></script>
<script type="text/javascript" src="js/app_views.js"></script>
<script type="text/javascript" src="js/app_datastore.js"></script>
<script type="text/javascript" src="js/app_engine.js"></script>
<!-- END COMMON SCRIPT BLOCK -->

<script type="text/javascript" src="js/login_page.js"></script>

</body>
</html>

The views are declared in login_page.js.

/**
 * login_page.js
 */
App.FieldDataMixin = {

  // Login when ENTER clicked
  insertNewline: function() {
    App.statechart.sendAction('doLogin');
    return;
  }
};

App.UsernameField = SC.View.extend({
  label: '_login.username'.loc(),

  Data : App.TextBoxView.extend(App.FieldDataMixin, {
    valueBinding: 'App.pageController.username',
    name: 'username',
    tabindex: '1',
    disabledBinding: SC.Binding.from('App.pageController.isLoggingIn').oneWay().bool(),
    didInsertElement: function() {
      this.$().focus();
    }
  })
});

App.PasswordField = SC.View.extend({
  label: '_login.password'.loc(),

  Data : App.TextBoxView.extend(App.FieldDataMixin, {
    valueBinding: 'App.pageController.password',
    type: 'password',
    name: 'password',
    tabindex: '1',
    disabledBinding: SC.Binding.from('App.pageController.isLoggingIn').oneWay().bool()
  })
});

Controllers

In SC1, I used SC.ArrayController as the controller for a list of records (e.g. a search or listing page) and SC.ObjectController for single records (e.g. a details or properties page).

In SC2, I use SC.ArrayProxy as the controller for a list of records and SC.Object for a controller for a single record or a page.

Data Store

It has been a while but from memory, I did not have to change anything.  My SC1 data store code just worked in SC2. :-)

AJAX and JSON

In SC1, there was the AJAX package with SC.Request and SC.Response.  This is how you used it.

    var postData = {
      'Username': username,
      'Password': password,
      'ExpiryType': 'Absolute',
      'ExpirySeconds': Chililog.AUTHENTICATION_TOKEN_EXPIRY_SECONDS
    };
    var url = '/api/Authentication';
    var request = SC.Request.postUrl(url).async(isAsync).json(YES);
    var params = { rememberMe: rememberMe, callbackTarget: callbackTarget, callbackFunction: callbackFunction, callbackParams: callbackParams };
    request.notify(this, 'endLogin', params).send(postData);

In SC2, SC.Request and SC.Response have not been ported.  I don’t think they will be ported because similar functionality is available in jQuery (upon which Sproutcore is dependent and must be included in your SC2 app).  I used the $.ajax jQuery API to execute an HTTP request. Also, to parse/serialize JSON, I used the in built javascript functions JSON.parse() and JSON.stringify() respectively (well … I believe they are in built into must modern browsers) .

    var postData = {
      'Username': username,
      'Password': password,
      'ExpiryType': 'Absolute',
      'ExpirySeconds': rememberMe ? App.AUTHENTICATION_REMEMBER_ME_TOKEN_EXPIRY_SECONDS : App.AUTHENTICATION_SESSION_TOKEN_EXPIRY_SECONDS
    };

    // Call server
    var url = '/api/Authentication';
    var context = { rememberMe: rememberMe, postData: postData,
      callbackTarget: callbackTarget, callbackFunction: callbackFunction, callbackParams: callbackParams };

    $.ajax({
      type: 'POST',
      url: url,
      data: JSON.stringify(postData),
      dataType: 'json',
      contentType: 'application/json; charset=utf-8',
      context: context,
      error: this._ajaxError,
      success: this._endLogin
    });

State Chart

The state chart API also has not changed much.  The only change that comes to mind is sendEvent() has been renamed to sendAction().

However, because the views in my SC2 web app were very different to my views in the previous SC1 app, I found that I had to re-write my state charts.

Localisation

In SC1, I would create a strings file using sc-gen language appname.fr fr and then put my strings in there.

SC.stringsFor('fr', {
     "_appTile": "Todos 0.1a",
     "_appButtonAddTask": "Ajouter une tâche",
     "_appSummaryCounter0": "tâche",
     "_appSummaryCounter1": "sélectionnés"
});
// Use localization
var localizedString = "_appTitle".loc();

To see the localization, navigate to http://localhost:4020/appname/fr.

In SC2, SC.STRINGS is defined as an empty object.  The loc() function is still present and now looks up SC.STRINGS.

All I need to do is  just set SC.STRINGS in a javascript file after sproutcore.js.  In Chililog, I called the file app_strings.js.  The idea would be to load different strings file for different languages.

SC.STRINGS = {
     "_appTile": "Todos 0.1a",
     "_appButtonAddTask": "Ajouter une tâche",
     "_appSummaryCounter0": "tâche",
     "_appSummaryCounter1": "sélectionnés"
};
// Use localization
var localizedString = "_appTitle".loc();

SC Error

SC.Error is no longer an SC.Object in SC2. It is just a standard Javascript object in SC2.

Also, shortcuts such as SC.$error and SC.ok are no longer used.  Rather, errors are instantiated as follows:

    throw new SC.Error('%@ - Unable to find template "%@".'.fmt(this, templateName));

SC Logger

SC.Logger‘s functionality has been reduced to just window.console in SC2:

  • SC.Logger.debug()
  • SC.Logger.info()
  • SC.Logger.warn()
  • SC.Logger.error()

SC1 had grouping and severity filtering; as well as benchmarking via SC.Benchmark.

I’m guessing that SC2 is still in development and logging features will make a comeback.  If not, we can always expand Chililog javascript logger to do so.

SC Date Time

No changes :-)

Sproutcore V1 vs V2

Why use Sproutcore at all?

The Chililog workbench is the user interface for viewing log entries and configuring Chililog.

I could have easily implemented the Chililog workbench in traditional a web framework – ASP, ASP.NET, JSP, Rails, Faces, Struts, etc.

However, these are server side frameworks.  HTML is dynamically generated on the web server and sent to the browser for rendering.  Hence, every time an action is performed, the page needs to refresh from the server.

I don’t mind page refreshes when browsing content – for example, reading the news.  However, in an app, it really annoys me.  For example, if the app needs to perform a page refresh after adding a record, adding one record may not be so troublesome. However, if I need to add 10 records quickly, page refreshes really slow me down.

A few years ago, we all started using AJAX to overcome page refreshes.  Calling the server in the background makes a lot of sense.

Then, we saw javascript performance dramatically improve when FireFox, Safari and Chrome muscled in on Internet Explorer’s market share.

Now, with HTML5 features such as local storage rolling out in browsers, it seems logical to me that finally there is enough functions and capacity in browsers for me to implement a FAT Javascript client.

I wanted to just code static HTML pages and have all the UI rendering and much of the business logic performed in the browser.  The server will be used just to serve up static files and to store data.

Finally, my browser app can have the same performance as a desktop app written in Java Swing or .NET.

Sproutcore V1

In Jan 2011, after looking around, I decided on Sproutcore V1 because:

  1. Simplicity – it is just Javascript, CSS and HTML. Other technologies like Cappuccino or Google Web Toolkit required me to learn another language which is then compiled into Javascript.  Why not use Javascript in the first place?
  2. Bindings – To be able to bind data to the browser’s DOM is a huge plus.  Once bindings are setup, changes in data automatically changes the DOM.  For example, I may want to display the name of the logged in user in 3 places on the HTML page.  Previously, when updating the username, I have to remember to write code to update the user’s name in those 3 places. With bindings, I declare bindings from the user’s name to the 3 paces in the DOM.  Then when I update the user’s name, the 3 places in the DOM automatically updates.
  3. Datastore – This caches server side data locally.  It supports a large amount of data as well as SQL like syntax to query and sort it.  This means that I can save on AJAX calls back to the server and hence make my app run even faster.
  4. State Charts – I think this feature sets Sproutcore part form other frameworks.  It helps you structure your UI in a systematic and logical way.

Why switch to V2?

I mentioned in an earlier post that I switched from SC1 to SC2.

There were two main reasons for that decision:

1. SC1 TableView implementation performed poorly for me.

When I started with SC1, I used the SC.TableView that came with the framework to display log entries.  I blogged about SC.TableView here.

However, SC.TableView was end of life and was to be replaced with endash’s TableView.

I tried using endash’s table view but still had problems.

  • Firstly, it was slow to load when I had many log entries.  The more log entries I had, the slower it got.
  • Secondly, when transitioning between different views, it did not render properly.  I had cells randomly being blanked out.

I fixed what I could but I could not fix the above two issues.

2. My App did not feel right

Yehuda Katz (see his talk at 32:38) puts it best – “people quickly expect app”.

Looking back, my UI was a hybrid between a web app and a mobile app.

I had mobile app features like transitions from left to right for master/details.  At the same time, I had web app features like opening new browser tabs.  It just did not feel right.

It came to me (a bit late) that the workbench could be a web app.  Most users of the workbench will be on a desktop/laptop/slate/tablet computer that have large screens.   Using the workbench on a mobile phone did not make sense.   Reading log entries on a tiny screen is way too hard.

In the future, I may build a mobile app for Chililog but that will not be the workbench.  Its will focus on notifications rather than search, analysis and configuration.

Sproutcore V2

Just when I was about to move away from Sproutcore, the team released SC2.  The initial target for SC2 was for web apps – just what I needed to build.

The features I found most useful were:

  1. Templating using handlebars
  2. Bindings – improved with bindings straight into templates
  3. Datastore & State Charts – still there

My Likes

  1. Light weight and modular
  2. No build tools required
  3. Easier to use with other frameworks – e.g. JQuery UI
  4. Templating – this is easier that declaring your views in javascript

Don’t Use It If …

  1. If you are a mobile app developer, stick with SC1.  SC2 does not have all the mobile bells and whistles of SC1.  I am sure this will come but it is not here today.

Replacing SC.TableView

So what did I use to replace SC.TableView?

I directly wrote to the DOM.  If found this to be the fastest way when displaying a large number of records – see the Stream and Search page.

I used templated collection bindings for displaying a small number of records – see the user and repository admin pages.  I have less lines of code to write and because there are only a small number of records, performance is not an issue.

Right Move?

Looking back, I feel that I’ve made the right move.  The workbench app feels right.  Here’s a live demo.

I will be blogging about how I’ve used SC2 in the coming weeks.

Chili what?

That’s right … Chililog.  Why?

  1. I like chili. The spicier the better.
  2. Sounds like chili (hot) dog which I also like to eat.
  3. The domain name was not taken.

Introducing Chililog …

December 2010

I’m one of these sad people for whom coding is both a hobby and a profession.

The day job is going great, but the hobby side has been lacking – having kids tends to have that effect.  So, as a New Year’s resolution, I promise myself  I am going to do some coding in new technologies.

Around the same time, I had a problem at the day job.  Our data centre provider claimed that we had exceeded by bandwidth allowance.  To find out what was causing our unusually high bandwidth usage, I had to aggregate logs from the firewall, load balancer, web server and app servers.

Wow – what a pain in the arse!

The logs were in different formats, the timestamps were in different timezones and I had to wait 1 week before I got access to the firewall logs.

I tried to find open source software but I could not find one that did everything I needed in one package.

It was at this moment when the idea for Chililog was born.

January 2011

OK – it is time to pick the technology.

Ever since dBase III, I’ve only used SQL databases like SQL Server, Oracle, MySQL, DB2, etc.  Time to try something new.  I’ve been reading about NoSQL so I decided to give mongoDB a try.

If we start writing a huge amount of log entries into mongoDB, sooner to later, we will hit the limits of the hardware.  Need to pick a queuing mechanism.  I can queue log entries and then write them into mongoDB at a controlled rate.  I picked HornetQ because it seems to be the fastest.  It also supports pubsub which means we can stream data to subscribers – i.e. real time viewing.

Now for the user interface.  Again, I wanted to try something new.  I’ve been reading about HTML5 so it is time to try it out.   I think is quite funny what’s old is new again.  HTML5 is bringing back good ol’ fat client-server.

  • Instead of a fat client using Java Swing or Visual Basic, we now use Javascript and download everything to the browser.
  • Instead of a COM/DCOM or CORBA/SOM application server, we use a AJAX JSON and a REST API web server
  • Finally, I can get rid of the slow page refreshes.

Decide to use Sproutcore because it is just HTML and  Javascript.  I can’t get simpler than that.  There is no server side dynamic pages – every UI component executes on the client side in the browser.

In order to serve the Sproutcore HTML pages, I decided to use Netty as a web server since it is already used by HornetQ.  I also used Netty to implement a JSON REST API to which my Sproutcore app can communicate.

May 2011

Managed to hookup mongoDB and HornetQ.

My long lost Java skills are coming back.

July 2011

For a couple of months, I’ve been using Sproutcore V1.  I finished the UI but I am not happy about it.

The TableView for Sproutcore V1 performed very slowing when adding records after the initial load; as well as when switching between views.  I’ve also found a few rendering issues which I’ve not been expert enough to fix.

On top of that, my app did not feel right.  Yehuda Katz (see his talk at 32:38) puts it best – “people quickly expect app”.  Looking back, my UI was a hybrid between a web app and a mobile app.  I had mobile app features like transitions from left to right for master/details.  At the same time, I had web app features like opening new browser tabs.  It just did not feel right.

I decide to throw it out.  2 months wasted.  Decide to start on a web style app using Sprotucore V2.

August 2011

UI re-write done.  Much better.  Submitted it for Sproutcore demo app competition but did not win :-(

I must admit, the deadline for the competition came sooner than I wanted.  Although I finished the UI, it looked like crap.

I start theming the UI using Twitter Bootstrap.

I also started testing with different publishers – C#, Java and JavaScript.

Lastly, I added WebSocket support.  This means that we can now stream data from and to the browser.  Found this really useful to stream JavaScript console logs to the server; and also to stream incoming log entries to browsers for viewing:

  • I can use this for remote support. If a user rings up with a problem, I can see the log entries generated from their browser.  I can trace what is happening as they are using my app.  This helps because most users are bad at communicating what they are doing and at re-creating the problem.
  • Because these entries are streamed to my browser, I don’t have to hit the refresh button when viewing incoming log entries

October 2011

After some initial testing, time for an initial release – hence this blog post.

The initial reason I started Chililog is to learn some new technologies.  I’ve open sourced Chililog to share what I’ve learnt with the open source community that has helped me in Chililog’s development.

What Next?

I’m going to start blogging about how Sproutcore, HornetQ and Netty is used in Chililog.  Hope you find what I’ve learnt about these technologies useful.

I also want to continue working on Chililog.  I’ve only completed 2/3 of the planned functionality.  I want to work on log monitoring and log entry parsing.  Chililog will become much more useful with monitoring – it will tell you when there is a problem.

If you want to help out with Chililog, please let me know.  All help is appreciated.