%-*-Mode:erlang;coding:utf-8;tab-width:4;c-basic-offset:4;indent-tabs-mode:()-*-
% ex: set ft=erlang fenc=utf-8 sts=4 ts=4 sw=4 et nomod:
%%%
%%%------------------------------------------------------------------------
%%% @doc
%%% ==CloudI Erlang Metrics==
%%% All memory values are in bytes (unless "words" is explicitly specified).
%%% @end
%%%
%%% BSD LICENSE
%%% 
%%% Copyright (c) 2015, Michael Truog <mjtruog at gmail dot com>
%%% All rights reserved.
%%% 
%%% Redistribution and use in source and binary forms, with or without
%%% modification, are permitted provided that the following conditions are met:
%%% 
%%%     * Redistributions of source code must retain the above copyright
%%%       notice, this list of conditions and the following disclaimer.
%%%     * Redistributions in binary form must reproduce the above copyright
%%%       notice, this list of conditions and the following disclaimer in
%%%       the documentation and/or other materials provided with the
%%%       distribution.
%%%     * All advertising materials mentioning features or use of this
%%%       software must display the following acknowledgment:
%%%         This product includes software developed by Michael Truog
%%%     * The name of the author may not be used to endorse or promote
%%%       products derived from this software without specific prior
%%%       written permission
%%% 
%%% THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
%%% CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
%%% INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
%%% OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
%%% DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
%%% CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
%%% SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
%%% BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
%%% SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
%%% INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
%%% WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
%%% NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
%%% OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
%%% DAMAGE.
%%%
%%% @author Michael Truog <mjtruog [at] gmail (dot) com>
%%% @copyright 2015 Michael Truog
%%% @version 1.5.1 {@date} {@time}
%%%------------------------------------------------------------------------

-module(cloudi_service_monitoring_erlang).
-author('mjtruog [at] gmail (dot) com').

%% external interface
-export([basic_update/0,
         memory_init/1,
         memory_update/1,
         system_info_init/1,
         system_info_update/1,
         statistics_init/1,
         statistics_update/1,
         process_info_init/1,
         process_info_update/1,
         port_info_init/1,
         port_info_update/1]).

%%%------------------------------------------------------------------------
%%% External interface functions
%%%------------------------------------------------------------------------

basic_update() ->
    {_, ErrorLoggerN} = erlang:process_info(whereis(error_logger),
                                            message_queue_len),
    [metric(gauge, [code, modules],
            erlang:length(code:all_loaded())),
     metric(gauge, [ets, tables],
            erlang:length(ets:all())),
     metric(gauge, [error_logger, message_queue_len], ErrorLoggerN)].

memory_init(Options) ->
    Options.

memory_update([]) ->
    {[], []};
memory_update(Options) ->
    Memory = erlang:memory(),
    MetricValues = [metric(gauge, memory_value(Option, Memory))
                    || Option <- Options],
    {MetricValues, Options}.

system_info_init(Options) ->
    [system_info_value(Option) || Option <- Options],
    Options.

system_info_update(Options) ->
    MetricValues = [metric(gauge, system_info_value(Option))
                    || Option <- Options],
    {MetricValues, Options}.

statistics_init(Options) ->
    case lists:member(scheduler_wall_time, Options) of
        true ->
            _ = erlang:system_flag(scheduler_wall_time, true);
        false ->
            ok
    end,
    [statistics_value(Option) || Option <- Options].

statistics_update(EntriesOld) ->
    statistics_update(EntriesOld, [], []).

process_info_init(Options) ->
    Options.

process_info_update([]) ->
    {[], []};
process_info_update(Options) ->
    Processes = erlang:processes(),
    MetricValues = [metric(gauge, process_info_value(Option, Processes))
                    || Option <- Options],
    {MetricValues, Options}.

port_info_init(Options) ->
    Options.

port_info_update([]) ->
    {[], []};
port_info_update(Options) ->
    Ports = erlang:ports(),
    MetricValues = [metric(gauge, port_info_value(Option, Ports))
                    || Option <- Options],
    {MetricValues, Options}.

%%%------------------------------------------------------------------------
%%% Private functions
%%%------------------------------------------------------------------------

memory_value(Option, Memory) ->
    {_, Value} = lists:keyfind(Option, 1, Memory),
    {Option, Value}.

system_info_value(Option)
    when (Option =:= dirty_cpu_schedulers) orelse
         (Option =:= dirty_cpu_schedulers_online) orelse
         (Option =:= dirty_io_schedulers) ->
    Value = try erlang:system_info(Option)
    catch
        error:badarg ->
            0
    end,
    {Option, Value};
system_info_value(Option)
    when (Option =:= logical_processors) orelse
         (Option =:= logical_processors_available) orelse
         (Option =:= logical_processors_online) ->
    Value = case erlang:system_info(Option) of
        unknown ->
            0;
        Count when is_integer(Count) ->
            Count
    end,
    {Option, Value};
system_info_value(Option) ->
    Value = erlang:system_info(Option),
    true = is_integer(Value),
    {Option, Value}.

statistics_value(context_switches) ->
    {ContextSwitches, _} = erlang:statistics(context_switches),
    {context_switches, ContextSwitches};
statistics_value(garbage_collection) ->
    GC = {_GCs, _WordsReclaimed, _} = erlang:statistics(garbage_collection),
    {garbage_collection, GC};
statistics_value(io) ->
    {{input, TotalBytesIn},
     {output, TotalBytesOut}} = erlang:statistics(io),
    {io, {TotalBytesIn, TotalBytesOut}};
statistics_value(scheduler_wall_time) ->
    {scheduler_wall_time, lists:sort(erlang:statistics(scheduler_wall_time))};
statistics_value(reductions) ->
    {TotalReductions, _} = erlang:statistics(reductions),
    {reductions, TotalReductions};
statistics_value(Option) ->
    Value = erlang:statistics(Option),
    true = is_integer(Value),
    {Option, Value}.

statistics_value_update(context_switches, ContextSwitchesOld) ->
    EntryNew = {_, ContextSwitchesNew} = statistics_value(context_switches),
    {[metric(spiral, [context_switches],
             ContextSwitchesNew - ContextSwitchesOld)],
     EntryNew};
statistics_value_update(garbage_collection,
                        {GCsOld, WordsReclaimedOld, _}) ->
    EntryNew = {_, ValueNew} = statistics_value(garbage_collection),
    {GCsNew, WordsReclaimedNew, _} = ValueNew,
    {[metric(spiral, [garbage_collection, count],
             GCsNew - GCsOld),
      metric(spiral, [garbage_collection, words_reclaimed],
             WordsReclaimedNew - WordsReclaimedOld)],
     EntryNew};
statistics_value_update(io, {TotalBytesInOld, TotalBytesOutOld}) ->
    EntryNew = {_, ValueNew} = statistics_value(io),
    {TotalBytesInNew, TotalBytesOutNew} = ValueNew,
    {[metric(spiral, [io, input], TotalBytesInNew - TotalBytesInOld),
      metric(spiral, [io, output], TotalBytesOutNew - TotalBytesOutOld)],
     EntryNew};
statistics_value_update(scheduler_wall_time, SchedulersOld) ->
    EntryNew = {_, SchedulersNew} = statistics_value(scheduler_wall_time),
    MetricValues = lists:foldl(fun({SchedulerId,
                                    ActiveTimeOld, TotalTimeOld}, L) ->
        case lists:keyfind(SchedulerId, 1, SchedulersNew) of
            {_, ActiveTimeNew, TotalTimeNew} ->
                [metric(histogram,
                        [scheduler_wall_time,
                         erlang:integer_to_list(SchedulerId), active],
                        ActiveTimeNew - ActiveTimeOld),
                 metric(histogram,
                        [scheduler_wall_time,
                         erlang:integer_to_list(SchedulerId), total],
                        TotalTimeNew - TotalTimeOld) | L];
            false ->
                L
        end
    end, [], SchedulersOld),
    {MetricValues, EntryNew};
statistics_value_update(reductions, TotalReductionsOld) ->
    EntryNew = {_, TotalReductionsNew} = statistics_value(reductions),
    {[metric(spiral, [reductions],
             TotalReductionsNew - TotalReductionsOld)], EntryNew};
statistics_value_update(Option, _) ->
    EntryNew = statistics_value(Option),
    {[metric(gauge, EntryNew)], EntryNew}.

statistics_update([], EntriesNew, MetricValues) ->
    {MetricValues, EntriesNew};
statistics_update([{Option, ValueOld} | EntriesOld],
                  EntriesNew, MetricValues) ->
    {MetricValueL, EntryNew} = statistics_value_update(Option, ValueOld),
    statistics_update(EntriesOld,
                      [EntryNew | EntriesNew], MetricValueL ++ MetricValues).

process_info_value(Option, Processes)
    when Option =:= message_queue_len ->
    % if more options are added, provide them as
    % a list to erlang:process_info/2
    TotalCount = lists:foldl(fun(Pid, I) ->
        case erlang:process_info(Pid, Option) of
            {Option, Count} when is_integer(Count) ->
                I + Count;
            undefined ->
                I
        end
    end, 0, Processes),
    {Option, TotalCount}.

port_info_value(Option, Ports)
    when (Option =:= memory) orelse
         (Option =:= queue_size) ->
    TotalCount = lists:foldl(fun(Port, I) ->
        case erlang:port_info(Port, Option) of
            {Option, Count} when is_integer(Count) ->
                I + Count;
            undefined ->
                I
        end
    end, 0, Ports),
    {Option, TotalCount}.

metric(gauge, {Option, Value})
    when is_atom(Option) ->
    {gauge, [Option], Value}.

metric(Type, [_ | _] = Name, Value)
    when (Type =:= gauge) orelse
         (Type =:= spiral) orelse
         (Type =:= histogram) ->
    {Type, Name, Value}.

