Using Lager with Cowboy

Lager is a popular logging framework for Erlang applications. To get it working with Cowboy (assuming you’re using erlang.mk), you need to add it as a dependency to your Makefile:

DEPS = cowboy lager

and then add the parse transform to the options for erlc (as shown here):

include erlang.mk
ERLC_OPTS += +'{parse_transform, lager_transform}'

(alternatively, you could add a compile header to each file that needs it). You also need to add lager to your app.src file:

    {applications, [
        kernel,
        stdlib,
        cowboy,
        lager
    ]},
]}.

so that it is started as part of your release. You can find an example repo here.

Hello world with Cowboy and Websockets

Cowboy is a web server/framework written in Erlang, that provides pretty seamless websocket support.

The “getting started” guide is very good, and I’m going to assume you’ve read it, and followed it up to the “cowboy setup” section.

We’re going to add a ws handler, rather than an http one, using the template:

make new t=cowboy_ws n=ws_handler

And wire it up in the app:

start(_Type, _Args) ->
    Dispatch = cowboy_router:compile([{'_', [
        {"/connect", ws_handler, []} 
    ]}]),
    Port = 8080,
    cowboy:start_http(my_http_listener, 100, [{port, Port}], [{env, [{dispatch, Dispatch}]}]),
    ping_pong_sup:start_link().

You could now build and run the release, and connect to a websocket by calling http://localhost:8080/connect. But it wouldn’t be very interesting… yet.

The next job is to add a web page that will call the server, in priv/static/index.html:

<html>
    <head>
        <title>Ping-Pong</title>
    </head>

    <body>
        <h1>Ping-Pong</h1>
    </body>

    <script>
var socket = new WebSocket('ws://localhost:8080/connect');
    
socket.onopen = function() {
    console.log('connected');
};
    </script>
</html>

and add a static handler to the dispatcher:

    Dispatch = cowboy_router:compile([{'_', [
        {"/", cowboy_static, {priv_file, ping_pong, "static/index.html"}},
        {"/connect", ws_handler, []} 
    ]}]),

Still not hugely exciting, but if you look in the js console in your browser you should be connected to a websocket! We can now send a message to the server:

socket.onopen = function() {
    console.log('connected');
    send('ping');
};

function send(data) {
    console.log('Sending data: ' + data);
    socket.send(data);
}

and return the standard response:

websocket_handle({text, <<"ping">>}, Req, State) ->
    Reply = {text, <<"pong">>},
    {reply, Reply, Req, State};

which we can listen for on the client:

socket.onmessage = function(ev) {
    console.log('Received data: ' + ev.data);
};

If all that went well, then we can liven things up by adding some json into the mix. First, add jiffy as a dependency (other json libraries are available!), then update the js to send some json instead:

socket.onopen = function() {
    console.log('connected');
    var msg = {type: 'ping', count: 1};
    send(JSON.stringify(msg));
};

and handle that on the server:

websocket_handle({text, Json}, Req, State) ->
    Map = jiffy:decode(Json, [return_maps]),
    Count = maps:get(<<"count">>, Map),
    Reply = #{type => <<"pong">>, count => Count + 1},
    {reply, {text, jiffy:encode(Reply)}, Req, State};

Just remember that the socket receives the json as text, you need to parse it!

socket.onmessage = function(ev) {
    console.log('Received data: ' + ev.data);
    var msg = JSON.parse(ev.data);
}

An alternative to parsing the json is to use pattern matching:

websocket_handle({text, <<"{\"type":\"ping\",\"count\":", Count:1/binary, "}">>}, Req, State) ->
    Reply = #{type => <<"pong">>, count => list_to_integer(binary_to_list(Count)) + 1}, 
    {reply, {text, jiffy:encode(Reply)}, Req, State};

but in this case, it has the disadvantage that the length of Count has to be specified.

You can find the source code for these examples here, and there’s another example in the Cowboy repo.

Using the erlang.mk gen_server template

I can never remember how to do this when the time comes, so here’s a note to future self:

make new t=gen_server n=foo_server

The generated file will be in src/foo_server.erl. You can also list all the available templates using:

make list-templates

Available templates: cowboy_http cowboy_loop cowboy_rest cowboy_ws gen_server ranch_protocol supervisor

Or just look at the source.