Monitoring : Part 3

In the previous post, Monitoring : Part 2 we looked at how Linux exposes host metrics and in particular how the Prometheus agent, called node_exporter, reads the Linux host metrics. In this post we’re going to circle back to the monitor project mentioned in Monitoring : Part 1, and add some basic Prometheus metrics, using Erlang, so that we can explore them in Grafana.

Dependencies

You need to have the following installed to make use of the demo:

  • git
  • docker
  • docker-compose
  • make

Getting Started

Clone the monitor repo.

mkdir temp
cd temp
git clone https://github.com/toddg/emitter

Start the docker components.

cd emitter
make start

Verify everything started OK.

docker ps

You should see the following components running:

CONTAINER ID        IMAGE                     COMMAND                  CREATED             STATUS              PORTS                    NAMES
02dc8ccb5218        toddg/erlang-wavegen      "/bin/bash -l -c /bu…"   18 hours ago        Up 18 hours         0.0.0.0:4444->4444/tcp   monitor_wavegen_1
d8bb17caa1d0        grafana/grafana:6.2.4     "/run.sh"                18 hours ago        Up 18 hours         0.0.0.0:3000->3000/tcp   monitor_grafana_1
1715763b4326        prom/prometheus:v2.10.0   "/bin/prometheus --c…"   18 hours ago        Up 18 hours         0.0.0.0:9090->9090/tcp   monitor_prometheus_1

Emit Metrics

It’s trivial to write a metrics emitter in Erlang. Here’s why, the receive ... after pattern for responding to messages. The snippet below is a simple timer function that invokes a method F on a data blob D if no message is received after Delay milliseconds:

%%--------------------------------------------------------------------
%% timer calls itself every Delay milliseconds and invokes function F on 
%% Data D
%%--------------------------------------------------------------------
timer(Delay, F, D) ->
    receive cancel  -> 
                void
    after Delay     -> 
              D1 = F(D),
              timer(Delay, F, D1)
    end.

The helper timer function above is invoked by another helper function tick. tick spawns an Erlang process using timer. The timer method in the spawned process waits for an event that never comes, times out, and invokes the function F in theafter stanza. Here’s tick:

%%--------------------------------------------------------------------
%% tick calls itself every N milliseconds and invokes function F on 
%% Data D; returns a cancel function C
%%--------------------------------------------------------------------
tick(N, F, D) ->
    Pid = spawn(fun() -> timer(N, F, D) end),
    fun() -> Pid ! cancel end.

Now that we can invoke methods via a timer, let’s wire in some methods to emit Prometheus metrics. First, we add a helper function to emit data from the various emitter functions. To start with, let’s verify that the timer works, by printing to STDOUT:

%%--------------------------------------------------------------------
%% emit : side-effect for the function F
%%--------------------------------------------------------------------
emit(Name, Value) -> 
    io:format("emit: [~p]~p~n", [Name, Value]).

Let’s wire the emit function into the following functions that actually do something:

  • flatline
  • incrementer
  • flipflop
  • sinewave

Here’s what these methods look like:

%%--------------------------------------------------------------------
%% flat line : emits the value 1 for a flat line shape
%%--------------------------------------------------------------------
flatline(_) -> emit("flatline", 1), ok.

%%--------------------------------------------------------------------
%% incrementer : always increments value
%%--------------------------------------------------------------------
incrementer(V) -> 
    V1 = V + 1,
    emit("incrementer", V1),
    V1.

%%--------------------------------------------------------------------
%% flip flop : emits the value 1 and then the value 0
%%--------------------------------------------------------------------
flipflop(0) -> emit("flipflop", 1), 1;
flipflop(1) -> emit("flipflop", 0), 0;
flipflop(_) -> emit("flipflop", 0), 0.

%%--------------------------------------------------------------------
%% sine wave
%%--------------------------------------------------------------------
sinewave(V) ->
    sinewave(V, 1).
sinewave(V, Delta) ->
    emit("sinewave", math:sin(V)),
    V1 = V + Delta,
    V1.

The last step is start these emitter methods in main:

%%====================================================================
%% API functions
%%====================================================================

%% escript Entry point
main(Args) ->
    io:format("Args: ~p~n", [Args]),
    CancelFuns = [tick(10, F, 0) || F <- [fun flatline/1, fun flipflop/1, fun sinewave/1, fun incrementer/1]],
    %% this never get's called
    receive
        {ok} -> ok
    end,
    %% cancel all the timers
    [F() || F <- CancelFuns],
    erlang:halt(0).

One of the beauties of Erlang is list comprehensions. Here, we map the tick function across the list of emitter funcitons, thereby spawning an Erlang process for each of them:

    CancelFuns = [tick(10, F, 0) || F <- [fun flatline/1, fun flipflop/1, fun sinewave/1, fun incrementer/1]],

Let’s run this and see what the output looks like:

make start
cd monitor && docker-compose logs -f --tail="all" | grep wavegen

The first line invokes the start task in the Makefile which invokes the docker-compose up. The second line tails the output of the wavegen container:

wavegen_1     | emit: ["sinewave"]-0.7391806966492228
wavegen_1     | emit: ["incrementer"]63
wavegen_1     | emit: ["flatline"]1
wavegen_1     | emit: ["flipflop"]0
wavegen_1     | emit: ["sinewave"]0.16735570030280691
wavegen_1     | emit: ["incrementer"]64
wavegen_1     | emit: ["flatline"]1
wavegen_1     | emit: ["flipflop"]1
wavegen_1     | emit: ["sinewave"]0.9200260381967906
wavegen_1     | emit: ["incrementer"]65
wavegen_1     | emit: ["flatline"]1
wavegen_1     | emit: ["flipflop"]0

The key takeaways are:

  • flatline always emits : 1
  • flipflop alternates between 0 and 1
  • incrementer is monotonically increasing by 1
  • sinewave is taking the sin() of a number that is increasing by 1 every step

Now that we have verified that he emit function is getting called repeatedly, let’s wire in the prometheus metrics by replacing the print statement with a function to emit metrics:

%%--------------------------------------------------------------------
%% emit : side-effect for the function F
%%--------------------------------------------------------------------
emit(Name, Value) -> 
    multimetric(Name, Value).


%%--------------------------------------------------------------------
%% multimetric : emit all the metrics since we're just trying to
%% sort out how all this works. this way we can see stuff in the
%% grafana dashboard
%%--------------------------------------------------------------------
multimetric(Label, Value) ->
    %% counter for times the method has been invoked
    prometheus_counter:inc(mycounter, [Label], 1),
    % set a gauge to the current value
    prometheus_gauge:set(myguage, [Label], Value),
    % set the summary to the current value
    prometheus_summary:observe(mysummary, [Label], Value),
    % set the histogram to the current value
    prometheus_histogram:observe(myhist, [Label], Value).

Of course the metrics have to be registered, and the proper services started, etc. See the source for further details.

Grafana

Now that we are emitting known metrics, let’s see what they look like in Grafana, coming up in the next post: Monitoring : Part 4.