I’m developing a smoke tests app in Go that tests a number of services (Redis, RabbitMQ, Single Sign-On, etc) that are offered in the marketplace of a CloudFoundry installation at one of our customers. These tests produce simple JSON output that signals what went wrong. Now the customer has asked for a dashboard so the entire organization can check on the health of the platform.

I took some time to come up with a good enough design for this and decided on the following:

  • The smoke tests app (Golang) pushes its results to RabbitMQ
  • An ASP.NET Core app listens to smoke test results and keeps track of state (the results themselves and when they were received)
  • A single page written in Elm that receives status updates via SignalR (web sockets)

Since I have never written anything in Elm and my knowledge of SignalR is a little outdated, I decided to start very simple: a SignalR hub that increments an int every five seconds and sends it to all clients. The number that’s received by each client is used to update an Elm view model. In the real world, the int will become the JSON document describing the results of the smoke tests and we build a nice view for it, you get the idea.

All source code for this post can be found here.

The server side of things

First of all, what do things look like on the server and how do we build the application? It will be an ASP.NET Core app so we start with:

dotnet new web
dotnet add package Microsoft.AspNetCore.SignalR -v 1.0.0-alpha2-final

We create an empty ASP.NET Core website and add the latest version of SignalR. Next we need to configure SignalR in our Startup class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace SmokeTestsDashboardServer
{
    public class Startup
    {
        // This method gets called by the runtime. Use this method to add services to the container.
        // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddSignalR();
            services.AddSingleton<IHostedService, CounterHostedService>();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseDefaultFiles();
            app.UseStaticFiles();
            app.UseSignalR(routes =>
            {
                routes.MapHub<SmokeHub>("smoke");
            });
        }
    }
}

The code speaks for itself, I guess. We add SignalR dependencies to the services collection and configure a hub called SmokeHub which can be reached from the client via the route /smoke.

On line 15 you can see I add a IHostedService implementation: CounterHostedService. A hosted service is an object with a start and a stop method that is managed by the host. This means that when ASP.NET Core starts, it calls the hosted service start method and when ASP.NET Core (gracefully) shuts down, it calls the stop method. In our case, we use it to start a very simple scheduler that increments an integer every five seconds and sends it to all SignalR clients. Here are two posts on implementing your own IHostedService.

The client side of things

First of all, we need the SignalR client library. You can get it via npm. I added it in the wwwroot/js/lib folder.

Now let’s take a look at the Elm code.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
port module Main exposing (..)

import Html exposing (Html, div, button, text, program)


-- MODEL
type alias Model = Int

init : ( Model, Cmd Msg )
init = ( 1, Cmd.none )


-- MESSAGES
type Msg = Counter Int


-- VIEW
view : Model -> Html Msg
view model = div [] [ text (toString model) ]


-- UPDATE
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        Counter count -> ( count, Cmd.none )


-- SUBSCRIPTIONS
port updates : (Int -> msg) -> Sub msg

subscriptions : Model -> Sub Msg
subscriptions model = updates Counter


-- MAIN
main : Program Never Model Msg
main =
    program
        { init = init
        , view = view
        , update = update
        , subscriptions = subscriptions
        }

Let’s dissect the code:

  • Line 6: we have a model, which is an Int that we initialize to 1
  • Line 13: we have one message type, which is a counter of int
  • Line 17: our view takes our model and returns some very simple html, showing the model value
  • Line 22: when we receive an update, we simply return the count
  • Line 29: we subscribe to counter updates

The question is, where do we receive counter updates from? Elm is a pure functional language. This means that the output of every function in Elm depends only on its arguments, regardless of global and/or local state. Direct communication with Javascript from Elm would break this so that is not allowed. So all interop with the outside world is done through ports.

If we check the Elm code again, you see at line 1 we declare our module with the keyword port. On line 30 we declare a port that listens to counter updates from Javascript. So now we can plug it all together in our index.html file:

wwwroot/index.html view raw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<html>
<head>
    <script type="text/javascript" src="js/lib/signalr-client-1.0.0-alpha2-final.js"></script>
</head>
<body>
    <div id="main"></div>
    <script type="text/javascript" src="js/main.js"></script>
    <script>
        var node = document.getElementById('main');
        var app = Elm.Main.embed(node);

        const logger = new signalR.ConsoleLogger(signalR.LogLevel.Information);
        const smokeHub = new signalR.HttpConnection(`http://${document.location.host}/smoke`, { logger: logger });
        const smokeConn = new signalR.HubConnection(smokeHub, logger);

        smokeConn.onClosed = e => {
            console.log('Connection closed');
        };

        smokeConn.on('send', data => {
            console.log(data);
            app.ports.updates.send(data);
        });

        smokeConn.start().then(() => smokeConn.invoke('send', 42));

    </script>
</body>
</html>

Most of the code speaks for itself. On line 22 we invoke the port in our Elm app to pass the updated counter to Elm. Line 25 is a simple test to assure that we can also send message from the client to the SignalR hub.

For completeness’ sake, here is the code for the SmokeHub:

lib/SmokeHub.cs view raw
1
2
3
4
5
6
7
8
9
10
11
12
13
using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR;

namespace SmokeTestsDashboardServer
{
    public class SmokeHub : Hub
    {
        public Task Send(int counter)
        {
            return Clients.All.InvokeAsync("Send", counter);
        }
    }
}

Note that the Send method is called by JavaScript clients. It is not the same as the Send that is called when notifying all clients of a counter update.