├── .gitignore ├── rebar ├── cover.spec ├── screenshot--2015-10-05--18.41.30.jpg ├── include ├── beam_stats_msg_statsd_gauge.hrl ├── beam_stats_msg_graphite.hrl ├── beam_stats_ets_table.hrl ├── beam_stats_process_ancestry.hrl ├── beam_stats_processes.hrl ├── beam_stats_process.hrl └── beam_stats.hrl ├── src ├── beam_stats_logging.hrl ├── beam_stats_ets.erl ├── beam_stats.app.src ├── beam_stats_app.erl ├── beam_stats_config.erl ├── beam_stats_source.erl ├── beam_stats_sup.erl ├── beam_stats.erl ├── beam_stats_msg_statsd_gauge.erl ├── beam_stats_ets_table.erl ├── beam_stats_sup_consumers.erl ├── beam_stats_processes.erl ├── beam_stats_consumer_graphite.erl ├── beam_stats_delta.erl ├── beam_stats_producer.erl ├── beam_stats_consumer_csv.erl ├── beam_stats_consumer_statsd.erl ├── beam_stats_consumer.erl ├── beam_stats_process.erl └── beam_stats_msg_graphite.erl ├── rebar.config ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md └── test └── beam_stats_consumer_statsd_SUITE.erl /.gitignore: -------------------------------------------------------------------------------- 1 | deps/ 2 | ebin/ 3 | logs/ 4 | test/*.beam 5 | -------------------------------------------------------------------------------- /rebar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xandkar/beam_stats/HEAD/rebar -------------------------------------------------------------------------------- /cover.spec: -------------------------------------------------------------------------------- 1 | %%% vim: set filetype=erlang: 2 | {incl_app , beam_stats, details}. 3 | -------------------------------------------------------------------------------- /screenshot--2015-10-05--18.41.30.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xandkar/beam_stats/HEAD/screenshot--2015-10-05--18.41.30.jpg -------------------------------------------------------------------------------- /include/beam_stats_msg_statsd_gauge.hrl: -------------------------------------------------------------------------------- 1 | -record(beam_stats_msg_statsd_gauge, 2 | { name :: iolist() 3 | , value :: non_neg_integer() 4 | }). 5 | -------------------------------------------------------------------------------- /src/beam_stats_logging.hrl: -------------------------------------------------------------------------------- 1 | -define(log_error(Format) , error_logger:error_msg(Format)). 2 | -define(log_error(Format, Args), error_logger:error_msg(Format, Args)). 3 | -------------------------------------------------------------------------------- /include/beam_stats_msg_graphite.hrl: -------------------------------------------------------------------------------- 1 | -record(beam_stats_msg_graphite, 2 | { path :: [binary()] 3 | , value :: integer() 4 | , timestamp :: erlang:timestamp() 5 | }). 6 | -------------------------------------------------------------------------------- /include/beam_stats_ets_table.hrl: -------------------------------------------------------------------------------- 1 | -record(beam_stats_ets_table, 2 | { id :: beam_stats_ets_table:id() 3 | , name :: atom() 4 | , size :: non_neg_integer() 5 | , memory :: non_neg_integer() 6 | }). 7 | -------------------------------------------------------------------------------- /include/beam_stats_process_ancestry.hrl: -------------------------------------------------------------------------------- 1 | -record(beam_stats_process_ancestry, 2 | { raw_initial_call :: mfa() 3 | , otp_initial_call :: hope_option:t(mfa()) 4 | , otp_ancestors :: hope_option:t([atom() | pid()]) 5 | }). 6 | -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | { deps 2 | , [ {hope, ".*", {git, "https://github.com/xandkar/hope.git" , {tag, "4.0.0"}}} 3 | , {meck, ".*", {git, "https://github.com/eproxus/meck.git" , {tag, "0.8.13"}}} 4 | ] 5 | }. 6 | 7 | {cover_enabled, true}. 8 | 9 | {clean_files, ["test/*.beam"]}. 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | notifications: 2 | recipients: 3 | - siraaj@khandkar.net 4 | 5 | language: 6 | erlang 7 | 8 | otp_release: 9 | - 22.1 10 | - 22.0 11 | - 21.3 12 | - 21.2 13 | - 21.1 14 | - 21.0 15 | - 20.3 16 | - 19.3 17 | - 18.3 18 | - 18.1 19 | - 18.0 20 | - 17.5 21 | - 17.0 22 | 23 | script: 24 | "make travis_ci" 25 | -------------------------------------------------------------------------------- /src/beam_stats_ets.erl: -------------------------------------------------------------------------------- 1 | -module(beam_stats_ets). 2 | 3 | -export_type( 4 | [ t/0 5 | ]). 6 | 7 | -export( 8 | [ collect/0 9 | ]). 10 | 11 | -type t() :: 12 | [beam_stats_ets_table:t()]. 13 | 14 | -spec collect() -> 15 | t(). 16 | collect() -> 17 | TableIDs = beam_stats_source:ets_all(), 18 | [Tbl || {some, Tbl} <- [beam_stats_ets_table:of_id(I) || I <- TableIDs]]. 19 | -------------------------------------------------------------------------------- /src/beam_stats.app.src: -------------------------------------------------------------------------------- 1 | {application, beam_stats, 2 | [ 3 | {description, "Periodic VM stats production and consumption."}, 4 | {vsn, "1.0.3"}, 5 | {registered, []}, 6 | {applications, 7 | [ kernel 8 | , stdlib 9 | , hope 10 | ]}, 11 | 12 | {mod, { beam_stats_app, []}}, 13 | 14 | {env, 15 | [ {production_interval , 30000} 16 | , {consumers, []} 17 | ]} 18 | 19 | ]}. 20 | -------------------------------------------------------------------------------- /src/beam_stats_app.erl: -------------------------------------------------------------------------------- 1 | -module(beam_stats_app). 2 | 3 | -behaviour(application). 4 | 5 | %% Application callbacks 6 | -export([start/2, stop/1]). 7 | 8 | %% =================================================================== 9 | %% Application callbacks 10 | %% =================================================================== 11 | 12 | start(_StartType, _StartArgs) -> 13 | DeltasServer = beam_stats_delta:start(), 14 | beam_stats_sup:start_link(DeltasServer). 15 | 16 | stop(_State) -> 17 | ok. 18 | -------------------------------------------------------------------------------- /include/beam_stats_processes.hrl: -------------------------------------------------------------------------------- 1 | -record(beam_stats_processes, 2 | { individual_stats :: [beam_stats_process:t()] 3 | , count_all :: non_neg_integer() 4 | , count_exiting :: non_neg_integer() 5 | , count_garbage_collecting :: non_neg_integer() 6 | , count_registered :: non_neg_integer() 7 | , count_runnable :: non_neg_integer() 8 | , count_running :: non_neg_integer() 9 | , count_suspended :: non_neg_integer() 10 | , count_waiting :: non_neg_integer() 11 | }). 12 | -------------------------------------------------------------------------------- /include/beam_stats_process.hrl: -------------------------------------------------------------------------------- 1 | -record(beam_stats_process, 2 | { pid :: pid() 3 | , registered_name :: hope_option:t(atom()) 4 | , ancestry :: beam_stats_process:ancestry() 5 | , status :: beam_stats_process:status() 6 | , memory :: non_neg_integer() 7 | , total_heap_size :: non_neg_integer() 8 | , stack_size :: non_neg_integer() 9 | , message_queue_len :: non_neg_integer() 10 | , reductions :: non_neg_integer() 11 | }). 12 | -------------------------------------------------------------------------------- /include/beam_stats.hrl: -------------------------------------------------------------------------------- 1 | -record(beam_stats, 2 | { timestamp :: erlang:timestamp() 3 | , node_id :: atom() 4 | , memory :: [{atom(), non_neg_integer()}] 5 | , io_bytes_in :: non_neg_integer() 6 | , io_bytes_out :: non_neg_integer() 7 | , context_switches :: non_neg_integer() 8 | , reductions :: non_neg_integer() 9 | , run_queue :: non_neg_integer() 10 | , ets :: beam_stats_ets:t() 11 | , processes :: beam_stats_processes:t() 12 | 13 | %, statistics :: [{atom() , term()}] 14 | %, system :: [{atom() , term()}] 15 | %, port :: [{atom() , term()}] 16 | %, dets :: [{atom() , term()}] 17 | }). 18 | -------------------------------------------------------------------------------- /src/beam_stats_config.erl: -------------------------------------------------------------------------------- 1 | -module(beam_stats_config). 2 | 3 | -export( 4 | [ production_interval/0 5 | , consumers/0 6 | ]). 7 | 8 | -define(APP, beam_stats). 9 | 10 | %% ============================================================================ 11 | %% API 12 | %% ============================================================================ 13 | 14 | -spec production_interval() -> 15 | non_neg_integer(). 16 | production_interval() -> 17 | get_env(production_interval). 18 | 19 | -spec consumers() -> 20 | [{ConsumerModule :: module(), ConsumerDefinedOptionsData :: term()}]. 21 | consumers() -> 22 | get_env(consumers). 23 | 24 | %% ============================================================================ 25 | %% Internal 26 | %% ============================================================================ 27 | 28 | -spec get_env(atom()) -> 29 | term(). 30 | get_env(Key) -> 31 | {ok, Value} = application:get_env(?APP, Key), 32 | Value. 33 | -------------------------------------------------------------------------------- /src/beam_stats_source.erl: -------------------------------------------------------------------------------- 1 | -module(beam_stats_source). 2 | 3 | -export( 4 | [ erlang_is_process_alive/1 5 | , erlang_memory/0 6 | , erlang_node/0 7 | , erlang_process_info/2 8 | , erlang_processes/0 9 | , erlang_registered/0 10 | , erlang_statistics/1 11 | , erlang_system_info/1 12 | , ets_all/0 13 | , ets_info/2 14 | , os_timestamp/0 15 | ]). 16 | 17 | erlang_is_process_alive(Pid) -> 18 | erlang:is_process_alive(Pid). 19 | 20 | erlang_memory() -> 21 | erlang:memory(). 22 | 23 | erlang_node() -> 24 | erlang:node(). 25 | 26 | erlang_process_info(Pid, Key) -> 27 | erlang:process_info(Pid, Key). 28 | 29 | erlang_processes() -> 30 | erlang:processes(). 31 | 32 | erlang_registered() -> 33 | erlang:registered(). 34 | 35 | erlang_statistics(Key) -> 36 | erlang:statistics(Key). 37 | 38 | erlang_system_info(Key) -> 39 | erlang:system_info(Key). 40 | 41 | ets_all() -> 42 | ets:all(). 43 | 44 | ets_info(Table, Key) -> 45 | ets:info(Table, Key). 46 | 47 | os_timestamp() -> 48 | os:timestamp(). 49 | -------------------------------------------------------------------------------- /src/beam_stats_sup.erl: -------------------------------------------------------------------------------- 1 | -module(beam_stats_sup). 2 | 3 | -behaviour(supervisor). 4 | 5 | %% API 6 | -export([start_link/1]). 7 | 8 | %% Supervisor callbacks 9 | -export([init/1]). 10 | 11 | %% Helper macro for declaring children of supervisor 12 | -define(CHILD(Type, Module, Args), 13 | {Module, {Module, start_link, Args}, permanent, 5000, Type, [Module]}). 14 | 15 | %% =================================================================== 16 | %% API functions 17 | %% =================================================================== 18 | 19 | start_link(DeltasServer) -> 20 | supervisor:start_link({local, ?MODULE}, ?MODULE, DeltasServer). 21 | 22 | %% =================================================================== 23 | %% Supervisor callbacks 24 | %% =================================================================== 25 | 26 | init(DeltasServer) -> 27 | Children = 28 | [ ?CHILD(worker , beam_stats_producer , [DeltasServer]) 29 | , ?CHILD(supervisor , beam_stats_sup_consumers, []) 30 | ], 31 | SupFlags = {one_for_one, 5, 10}, 32 | {ok, {SupFlags, Children}}. 33 | -------------------------------------------------------------------------------- /src/beam_stats.erl: -------------------------------------------------------------------------------- 1 | -module(beam_stats). 2 | 3 | -include("include/beam_stats.hrl"). 4 | 5 | -export_type( 6 | [ t/0 7 | ]). 8 | 9 | -export( 10 | [ collect/1 11 | ]). 12 | 13 | -define(T, #?MODULE). 14 | 15 | -type t() :: 16 | ?T{}. 17 | 18 | -spec collect(beam_stats_delta:t()) -> 19 | t(). 20 | collect(DeltasServer) -> 21 | { {io_bytes_in , DeltaOfIOBytesIn} 22 | , {io_bytes_out , DeltaOfIOBytesOut} 23 | } = beam_stats_delta:of_io(DeltasServer), 24 | ?T 25 | { timestamp = beam_stats_source:os_timestamp() 26 | , node_id = beam_stats_source:erlang_node() 27 | , memory = beam_stats_source:erlang_memory() 28 | , io_bytes_in = DeltaOfIOBytesIn 29 | , io_bytes_out = DeltaOfIOBytesOut 30 | , context_switches = beam_stats_delta:of_context_switches(DeltasServer) 31 | , reductions = beam_stats_delta:of_reductions(DeltasServer) 32 | , run_queue = beam_stats_source:erlang_statistics(run_queue) 33 | , ets = beam_stats_ets:collect() 34 | , processes = beam_stats_processes:collect(DeltasServer) 35 | }. 36 | -------------------------------------------------------------------------------- /src/beam_stats_msg_statsd_gauge.erl: -------------------------------------------------------------------------------- 1 | -module(beam_stats_msg_statsd_gauge). 2 | 3 | -include("include/beam_stats_msg_graphite.hrl"). 4 | -include("include/beam_stats_msg_statsd_gauge.hrl"). 5 | 6 | -export_type( 7 | [ t/0 8 | ]). 9 | 10 | -export( 11 | [ of_msg_graphite/1 12 | , to_iolist/1 13 | ]). 14 | 15 | -define(T, #?MODULE). 16 | 17 | -type t() :: 18 | ?T{}. 19 | 20 | -spec of_msg_graphite(beam_stats_msg_graphite:t()) -> 21 | t(). 22 | of_msg_graphite( 23 | #beam_stats_msg_graphite 24 | { path = Path 25 | , value = Value 26 | , timestamp = _Timestamp 27 | } 28 | ) -> 29 | PathIOList = beam_stats_msg_graphite:path_to_iolist(Path), 30 | cons(PathIOList, Value). 31 | 32 | -spec cons(iolist(), non_neg_integer()) -> 33 | t(). 34 | cons(Name, Value) -> 35 | ?T 36 | { name = Name 37 | , value = Value 38 | }. 39 | 40 | -spec to_iolist(t()) -> 41 | iolist(). 42 | to_iolist( 43 | ?T 44 | { name = Name 45 | , value = Value 46 | } 47 | ) when Value >= 0 -> 48 | ValueBin = integer_to_binary(Value), 49 | [Name, <<":">>, ValueBin, <<"|g\n">>]. 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Siraaj Khandkar 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | REBAR := ./rebar 2 | 3 | .PHONY: \ 4 | all \ 5 | clean_all \ 6 | clean_app \ 7 | compile_all \ 8 | compile_app \ 9 | deps \ 10 | deps_get \ 11 | deps_update \ 12 | dialyze \ 13 | dialyzer_plt_build \ 14 | test \ 15 | travis_ci 16 | 17 | all: \ 18 | travis_ci \ 19 | dialyze 20 | 21 | travis_ci: \ 22 | clean_all \ 23 | deps_get \ 24 | compile_all \ 25 | test 26 | 27 | deps_get: 28 | @$(REBAR) get-deps 29 | 30 | deps_update: 31 | @$(REBAR) update-deps 32 | 33 | deps: \ 34 | deps_get \ 35 | deps_update 36 | 37 | compile_all: 38 | $(REBAR) compile skip_deps=false 39 | 40 | compile_app: 41 | $(REBAR) compile skip_deps=true 42 | 43 | clean_all: 44 | $(REBAR) clean skip_deps=false 45 | 46 | clean_app: 47 | $(REBAR) clean skip_deps=true 48 | 49 | dialyze: 50 | @dialyzer $(shell \ 51 | find . -name '*.beam' \ 52 | | grep -v deps/meck/ \ 53 | ) 54 | 55 | 56 | dialyzer_plt_build: 57 | @dialyzer \ 58 | --build_plt \ 59 | --apps $(shell ls $(shell \ 60 | erl -eval 'io:format(code:lib_dir()), init:stop().' -noshell) \ 61 | | grep -v interface \ 62 | | sed -e 's/-[0-9.]*//' \ 63 | ) 64 | 65 | test: 66 | @$(REBAR) ct skip_deps=true --verbose=1 67 | -------------------------------------------------------------------------------- /src/beam_stats_ets_table.erl: -------------------------------------------------------------------------------- 1 | -module(beam_stats_ets_table). 2 | 3 | -include("include/beam_stats_ets_table.hrl"). 4 | 5 | -export_type( 6 | [ t/0 7 | , id/0 8 | ]). 9 | 10 | -export( 11 | [ of_id/1 12 | ]). 13 | 14 | -type id() :: 15 | atom() 16 | | ets:tid() 17 | % integer() is just a workaround, to let us mock ets:tid(), which is 18 | % opaque, but represented as an integer, without Dialyzer complaining. 19 | | integer() 20 | . 21 | 22 | -type t() :: 23 | #?MODULE{}. 24 | 25 | -spec of_id(id()) -> 26 | hope_opt:t(t()). 27 | of_id(ID) -> 28 | Name = beam_stats_source:ets_info(ID, name), 29 | Size = beam_stats_source:ets_info(ID, size), 30 | NumberOfWords = beam_stats_source:ets_info(ID, memory), 31 | case lists:member(undefined, [Name, Size, NumberOfWords]) of 32 | true -> % Table went away. 33 | none; 34 | false -> 35 | WordSize = beam_stats_source:erlang_system_info(wordsize), 36 | NumberOfBytes = NumberOfWords * WordSize, 37 | T = 38 | #?MODULE 39 | { id = ID 40 | , name = Name 41 | , size = Size 42 | , memory = NumberOfBytes 43 | }, 44 | {some, T} 45 | end. 46 | -------------------------------------------------------------------------------- /src/beam_stats_sup_consumers.erl: -------------------------------------------------------------------------------- 1 | -module(beam_stats_sup_consumers). 2 | 3 | -behaviour(supervisor). 4 | 5 | %% API 6 | -export( 7 | [ start_link/0 8 | , start_child/2 9 | ]). 10 | 11 | %% Supervisor callbacks 12 | -export([init/1]). 13 | 14 | %% Helper macro for declaring children of supervisor 15 | -define(CHILD(Type, Module, Args), 16 | {make_ref(), {Module, start_link, Args}, permanent, 5000, Type, [Module]}). 17 | 18 | %% =================================================================== 19 | %% API 20 | %% =================================================================== 21 | 22 | start_link() -> 23 | supervisor:start_link({local, ?MODULE}, ?MODULE, []). 24 | 25 | start_child(Module, Options) -> 26 | Child = ?CHILD(worker, beam_stats_consumer, [Module, Options]), 27 | supervisor:start_child(?MODULE, Child). 28 | 29 | %% =================================================================== 30 | %% Supervisor callbacks 31 | %% =================================================================== 32 | 33 | init([]) -> 34 | Consumers = beam_stats_config:consumers(), 35 | ConsumerSpecToChild = 36 | fun ({Module, Options}) -> 37 | ?CHILD(worker, beam_stats_consumer, [Module, Options]) 38 | end, 39 | Children = lists:map(ConsumerSpecToChild, Consumers), 40 | SupFlags = {one_for_one, 5, 10}, 41 | {ok, {SupFlags, Children}}. 42 | -------------------------------------------------------------------------------- /src/beam_stats_processes.erl: -------------------------------------------------------------------------------- 1 | -module(beam_stats_processes). 2 | 3 | -include("include/beam_stats_process.hrl"). 4 | -include("include/beam_stats_processes.hrl"). 5 | 6 | -export_type( 7 | [ t/0 8 | ]). 9 | 10 | -export( 11 | [ collect/1 12 | , collect_and_print/1 13 | , print/1 14 | ]). 15 | 16 | -define(T, #?MODULE). 17 | 18 | -type t() :: 19 | ?T{}. 20 | 21 | -spec collect(beam_stats_delta:t()) -> 22 | t(). 23 | collect(DeltasServer) -> 24 | Pids = beam_stats_source:erlang_processes(), 25 | PsOpts = [beam_stats_process:of_pid(P, DeltasServer) || P <- Pids], 26 | Ps = [P || {some, P} <- PsOpts], 27 | ?T 28 | { individual_stats 29 | = Ps 30 | , count_all 31 | = length(Ps) 32 | , count_exiting 33 | = length([P || P <- Ps, P#beam_stats_process.status =:= exiting]) 34 | , count_garbage_collecting 35 | = length([P || P <- Ps, P#beam_stats_process.status =:= garbage_collecting]) 36 | , count_registered 37 | = length(beam_stats_source:erlang_registered()) 38 | , count_runnable 39 | = length([P || P <- Ps, P#beam_stats_process.status =:= runnable]) 40 | , count_running 41 | = length([P || P <- Ps, P#beam_stats_process.status =:= running]) 42 | , count_suspended 43 | = length([P || P <- Ps, P#beam_stats_process.status =:= suspended]) 44 | , count_waiting 45 | = length([P || P <- Ps, P#beam_stats_process.status =:= waiting]) 46 | }. 47 | 48 | -spec collect_and_print(beam_stats_delta:t()) -> 49 | ok. 50 | collect_and_print(DeltasServer) -> 51 | print(collect(DeltasServer)). 52 | 53 | -spec print(t()) -> 54 | ok. 55 | print( 56 | ?T 57 | { individual_stats = PerProcessStats 58 | , count_all = CountAll 59 | , count_exiting = CountExiting 60 | , count_garbage_collecting = CountGarbageCollecting 61 | , count_registered = CountRegistered 62 | , count_runnable = CountRunnable 63 | , count_running = CountRunning 64 | , count_suspended = CountSuspended 65 | , count_waiting = CountWaiting 66 | } 67 | ) -> 68 | PerProcessStatsSorted = lists:sort( 69 | fun (#beam_stats_process{memory=A}, #beam_stats_process{memory=B}) -> 70 | % From lowest to highest, so that the largest appears the end of 71 | % the output and be easier to see (without scrolling back): 72 | A < B 73 | end, 74 | PerProcessStats 75 | ), 76 | lists:foreach(fun beam_stats_process:print/1, PerProcessStatsSorted), 77 | io:format("==================================================~n"), 78 | io:format( 79 | "CountAll : ~b~n" 80 | "CountExiting : ~b~n" 81 | "CountGarbageCollecting : ~b~n" 82 | "CountRegistered : ~b~n" 83 | "CountRunnable : ~b~n" 84 | "CountRunning : ~b~n" 85 | "CountSuspended : ~b~n" 86 | "CountWaiting : ~b~n" 87 | "~n", 88 | [ CountAll 89 | , CountExiting 90 | , CountGarbageCollecting 91 | , CountRegistered 92 | , CountRunnable 93 | , CountRunning 94 | , CountSuspended 95 | , CountWaiting 96 | ] 97 | ). 98 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/xandkar/beam_stats.svg?branch=master)](https://travis-ci.org/xandkar/beam_stats) 2 | 3 | beam_stats 4 | ========== 5 | 6 | Periodically collects and pushes VM metrics to arbitrary consumer processes, 7 | which, in-turn, can do whatever they want with the given data (such as 8 | serialize and forward to some time series storage). Includes, off by default, 9 | example implementations of consumers for: 10 | 11 | - StatsD (`beam_stats_consumer_statsd`) 12 | - Graphite (`beam_stats_consumer_graphite`) 13 | - CSV file (`beam_stats_consumer_csv`) 14 | 15 | Essentially like `folsomite`, but different. Different in the following ways: 16 | 17 | - More-general: consumers other than graphite can be defined 18 | - More-focused: only concerned with VM metrics, while `folsomite` ships off 19 | _everything_ from `folsom` (in addition to VM metrics) 20 | - Easier-(for me!)-to-reason-about implementation: 21 | + Well-defined metrics-to-binary conversions, as opposed to the 22 | nearly-arbitrary term-to-string conversions used in `folsomite` 23 | + Spec'd, tested and Dialyzed 24 | - More detailed stats: 25 | - **per-process**. As much process ancestry is collected as possible, then 26 | anonymous processes are aggregated to their youngest-known, named 27 | predecessor - this aggregation keeps the useful breadcrumbs, while 28 | reducing the number of unique names from exploding, which 29 | **avoids the associated problems**: 30 | 1. not very useful when there're lots of short-lived processes 31 | 2. exploading disk space usage in Whisper 32 | - per-ETS-table 33 | - and more ... see records defined in `include` directory 34 | 35 | For an example of using pre-process stats to track-down memory leaks, here's a 36 | screenshot of the SSL connection process memory usage growth drop after upgrade 37 | from 17.5 to 18.1 (back in 2015): 38 | ![SSL memory leak going away](screenshot--2015-10-05--18.41.30.jpg) 39 | 40 | ### Adding consumers 41 | 42 | #### At app config time 43 | 44 | ```erlang 45 | {env, 46 | [ {production_interval , 30000} 47 | , {consumers, 48 | [ {beam_stats_consumer_statsd, 49 | [ {consumption_interval , 60000} 50 | , {dst_host , "localhost"} 51 | , {dst_port , 8125} 52 | , {src_port , 8124} 53 | , {num_msgs_per_packet , 10} 54 | 55 | % If you want to name your node something other than what 56 | % erlang:node() returns: 57 | , {static_node_name , <<"unicorn_at_rainbow">>} 58 | ]} 59 | , {beam_stats_consumer_graphite, 60 | [ {consumption_interval , 60000} 61 | , {host , "localhost"} 62 | , {port , 2003} 63 | , {timeout , 5000} 64 | ]} 65 | , {beam_stats_consumer_csv, 66 | [ {consumption_interval , 60000} 67 | , {path , "beam_stats.csv"} 68 | ]} 69 | , {some_custom_consumer_module, 70 | [ {some_custom_option_a, "abc"} 71 | , {some_custom_option_b, 123} 72 | ]} 73 | 74 | ]} 75 | ]} 76 | ``` 77 | 78 | #### Dynamically 79 | 80 | ```erlang 81 | beam_stats_consumer:add(consumer_module, ConsumerOptions). 82 | ``` 83 | 84 | ### Removing consumers 85 | 86 | Not yet implemented. 87 | -------------------------------------------------------------------------------- /src/beam_stats_consumer_graphite.erl: -------------------------------------------------------------------------------- 1 | -module(beam_stats_consumer_graphite). 2 | 3 | -include("include/beam_stats.hrl"). 4 | -include("beam_stats_logging.hrl"). 5 | 6 | -behaviour(beam_stats_consumer). 7 | 8 | -export_type( 9 | [ option/0 10 | ]). 11 | 12 | -export( 13 | [ init/1 14 | , consume/2 15 | , terminate/1 16 | ]). 17 | 18 | -type option() :: 19 | {consumption_interval , non_neg_integer()} 20 | | {host , inet:ip_address() | inet:hostname()} 21 | | {port , inet:port_number()} 22 | | {timeout , timeout()} 23 | . 24 | 25 | -record(state, 26 | { sock = none :: hope_option:t(Socket :: port()) 27 | , host :: inet:ip_address() | inet:hostname() 28 | , port :: inet:port_number() 29 | , timeout :: timeout() 30 | }). 31 | 32 | -type state() :: 33 | #state{}. 34 | 35 | -define(DEFAULT_HOST , "localhost"). 36 | -define(DEFAULT_PORT , 2003). 37 | -define(DEFAULT_TIMEOUT , 5000). 38 | 39 | -spec init([option()]) -> 40 | {non_neg_integer(), state()}. 41 | init(Options) -> 42 | Get = fun (Key, Default) -> hope_kv_list:get(Options, Key, Default) end, 43 | ConsumptionInterval = Get(consumption_interval, 60000), 44 | State = #state 45 | { sock = none 46 | , host = Get(host , ?DEFAULT_HOST) 47 | , port = Get(port , ?DEFAULT_PORT) 48 | , timeout = Get(timeout , ?DEFAULT_TIMEOUT) 49 | }, 50 | {ConsumptionInterval, State}. 51 | 52 | -spec consume(beam_stats_consumer:queue(), state()) -> 53 | state(). 54 | consume(Q, #state{}=State1) -> 55 | Payload = beam_stats_queue_to_iolists(Q), 56 | State2 = try_to_connect_if_no_socket(State1), 57 | try_to_send(State2, Payload). 58 | 59 | -spec terminate(state()) -> 60 | {}. 61 | terminate(#state{sock=SockOpt}) -> 62 | hope_option:iter(SockOpt, fun gen_tcp:close/1). 63 | 64 | %% ============================================================================ 65 | 66 | -spec try_to_send(state(), iolist()) -> 67 | state(). 68 | try_to_send(#state{sock=none}=State, _) -> 69 | ?log_error("Sending failed. No socket in state."), 70 | % TODO: Maybe schedule retry? 71 | State; 72 | try_to_send(#state{sock={some, Sock}}=State, Payload) -> 73 | case gen_tcp:send(Sock, Payload) 74 | of ok -> 75 | State 76 | ; {error, _}=Error -> 77 | ?log_error("gen_tcp:send(~p, Payload) -> ~p", [Sock, Error]), 78 | % TODO: Maybe schedule retry? 79 | ok = gen_tcp:close(Sock), 80 | State#state{sock=none} 81 | end. 82 | 83 | -spec try_to_connect_if_no_socket(state()) -> 84 | state(). 85 | try_to_connect_if_no_socket(#state{sock={some, _}}=State) -> 86 | State; 87 | try_to_connect_if_no_socket( 88 | #state 89 | { sock = none 90 | , host = Host 91 | , port = Port 92 | , timeout = Timeout 93 | }=State 94 | ) -> 95 | Options = [binary, {active, false}], 96 | case gen_tcp:connect(Host, Port, Options, Timeout) 97 | of {ok, Sock} -> 98 | State#state{sock = {some, Sock}} 99 | ; {error, _}=Error -> 100 | ?log_error( 101 | "gen_tcp:connect(~p, ~p, ~p, ~p) -> ~p", 102 | [Host, Port, Options, Timeout, Error] 103 | ), 104 | State#state{sock = none} 105 | end. 106 | 107 | -spec beam_stats_queue_to_iolists(beam_stats_consumer:queue()) -> 108 | [iolist()]. 109 | beam_stats_queue_to_iolists(Q) -> 110 | [beam_stats_to_iolist(B) || B <- queue:to_list(Q)]. 111 | 112 | -spec beam_stats_to_iolist(beam_stats:t()) -> 113 | [iolist()]. 114 | beam_stats_to_iolist(#beam_stats{}=BeamStats) -> 115 | Msgs = beam_stats_msg_graphite:of_beam_stats(BeamStats), 116 | lists:map(fun beam_stats_msg_graphite:to_iolist/1, Msgs). 117 | -------------------------------------------------------------------------------- /src/beam_stats_delta.erl: -------------------------------------------------------------------------------- 1 | -module(beam_stats_delta). 2 | 3 | -export_type( 4 | [ t/0 5 | ]). 6 | 7 | -export( 8 | [ start/0 9 | , stop/1 10 | , gc/1 11 | , of_context_switches/1 12 | , of_io/1 13 | , of_reductions/1 14 | , of_process_info_reductions/2 15 | ]). 16 | 17 | -record(?MODULE, 18 | { erlang_statistics :: ets:tid() 19 | , erlang_process_info_reductions :: ets:tid() 20 | }). 21 | 22 | -define(T, #?MODULE). 23 | 24 | -opaque t() :: 25 | ?T{}. 26 | 27 | -spec start() -> 28 | t(). 29 | start() -> 30 | Options = 31 | [ set 32 | , public 33 | ], 34 | ?T 35 | { erlang_statistics = 36 | ets:new(beam_stats_delta_erlang_statistics, Options) 37 | , erlang_process_info_reductions = 38 | ets:new(beam_stats_delta_erlang_process_info_reductions, Options) 39 | }. 40 | 41 | -spec stop(t()) -> 42 | {}. 43 | stop(?T 44 | { erlang_statistics = TidErlangStatistics 45 | , erlang_process_info_reductions = TidErlangProcessInfoReductions 46 | } 47 | ) -> 48 | true = ets:delete(TidErlangStatistics), 49 | true = ets:delete(TidErlangProcessInfoReductions), 50 | {}. 51 | 52 | -spec gc(t()) -> 53 | {}. 54 | gc(?T{erlang_process_info_reductions=Table}=T) -> 55 | case ets:first(Table) 56 | of '$end_of_table' -> 57 | {} 58 | ; FirstPid when is_pid(FirstPid) -> 59 | gc(T, FirstPid) 60 | end. 61 | 62 | -spec gc(t(), pid()) -> 63 | {}. 64 | gc(?T{erlang_process_info_reductions=Table}=T, Pid) -> 65 | Next = ets:next(Table, Pid), 66 | case beam_stats_source:erlang_is_process_alive(Pid) 67 | of true -> true 68 | ; false -> ets:delete(Table, Pid) 69 | end, 70 | case Next 71 | of '$end_of_table' -> 72 | {} 73 | ; NextPid when is_pid(NextPid) -> 74 | gc(T, NextPid) 75 | end. 76 | 77 | -spec of_context_switches(t()) -> 78 | non_neg_integer(). 79 | of_context_switches(?T{erlang_statistics=Table}) -> 80 | Key = context_switches, 81 | {Current, 0} = beam_stats_source:erlang_statistics(Key), 82 | delta(Table, Key, Current). 83 | 84 | -spec of_io(t()) -> 85 | { {io_bytes_in , non_neg_integer()} 86 | , {io_bytes_out , non_neg_integer()} 87 | }. 88 | of_io(?T{erlang_statistics=Table}) -> 89 | Key = io, 90 | { {input , CurrentIn} 91 | , {output , CurrentOut} 92 | } = beam_stats_source:erlang_statistics(Key), 93 | DeltaIn = delta(Table, io_bytes_in , CurrentIn), 94 | DeltaOut = delta(Table, io_bytes_out, CurrentOut), 95 | { {io_bytes_in , DeltaIn} 96 | , {io_bytes_out , DeltaOut} 97 | }. 98 | 99 | % We can get between-calls-delta directly from erlang:statistics(reductions), 100 | % but then if some other process also calls it - we'll get incorrect data on 101 | % the next call. 102 | % Managing deltas ourselves here, will at least reduce the possible callers to 103 | % only those with knowledge of our table ID. 104 | -spec of_reductions(t()) -> 105 | non_neg_integer(). 106 | of_reductions(?T{erlang_statistics=Table}) -> 107 | Key = reductions, 108 | {Current, _} = beam_stats_source:erlang_statistics(Key), 109 | delta(Table, Key, Current). 110 | 111 | -spec of_process_info_reductions(t(), pid()) -> 112 | hope_option:t(non_neg_integer()). 113 | of_process_info_reductions(?T{erlang_process_info_reductions=Table}, Pid) -> 114 | case beam_stats_source:erlang_process_info(Pid, reductions) 115 | of undefined -> 116 | none 117 | ; {reductions, Current} -> 118 | Delta = delta(Table, Pid, Current), 119 | {some, Delta} 120 | end. 121 | 122 | -spec delta(ets:tid(), Key, non_neg_integer()) -> 123 | non_neg_integer() 124 | when Key :: atom() | pid(). 125 | delta(Table, Key, CurrentTotal) -> 126 | PreviousTotalOpt = find(Table, Key), 127 | PreviousTotal = hope_option:get(PreviousTotalOpt, 0), 128 | save(Table, Key, CurrentTotal), 129 | CurrentTotal - PreviousTotal. 130 | 131 | -spec find(ets:tid(), term()) -> 132 | hope_option:t(term()). 133 | find(Table, K) -> 134 | case ets:lookup(Table, K) 135 | of [] -> none 136 | ; [{K, V}] -> {some, V} 137 | end. 138 | 139 | -spec save(ets:tid(), term(), term()) -> 140 | {}. 141 | save(Table, K, V) -> 142 | true = ets:insert(Table, {K, V}), 143 | {}. 144 | -------------------------------------------------------------------------------- /src/beam_stats_producer.erl: -------------------------------------------------------------------------------- 1 | -module(beam_stats_producer). 2 | 3 | -behaviour(gen_server). 4 | 5 | %% API 6 | -export( 7 | [ start_link/1 8 | , subscribe/1 9 | , unsubscribe/1 10 | 11 | % For testing 12 | , sync_produce_consume/0 13 | ]). 14 | 15 | %% gen_server callbacks 16 | -export( 17 | [ init/1 18 | , handle_call/3 19 | , handle_cast/2 20 | , handle_info/2 21 | , terminate/2 22 | , code_change/3 23 | ]). 24 | 25 | %% ============================================================================ 26 | %% Internal data 27 | %% ============================================================================ 28 | 29 | -define(PRODUCE_SYNC , produce_sync). 30 | -define(PRODUCE_ASYNC , produce_async). 31 | 32 | -record(state, 33 | { consumers = ordsets:new() :: ordsets:ordset(pid()) 34 | , deltas_server :: beam_stats_delta:t() 35 | }). 36 | 37 | -type state() :: 38 | #state{}. 39 | 40 | %% ============================================================================ 41 | %% API 42 | %% ============================================================================ 43 | 44 | start_link(DeltasServer) -> 45 | gen_server:start_link({local, ?MODULE}, ?MODULE, DeltasServer, []). 46 | 47 | -spec subscribe(pid()) -> 48 | ok. 49 | subscribe(PID) -> 50 | gen_server:cast(?MODULE, {subscribe, PID}). 51 | 52 | -spec unsubscribe(pid()) -> 53 | ok. 54 | unsubscribe(PID) -> 55 | gen_server:cast(?MODULE, {unsubscribe, PID}). 56 | 57 | -spec sync_produce_consume() -> 58 | {}. 59 | sync_produce_consume() -> 60 | {} = gen_server:call(?MODULE, ?PRODUCE_SYNC). 61 | 62 | %% ============================================================================ 63 | %% gen_server callbacks (unused) 64 | %% ============================================================================ 65 | 66 | code_change(_OldVsn, State, _Extra) -> 67 | {ok, State}. 68 | 69 | terminate(_Reason, _State) -> 70 | ok. 71 | 72 | %% ============================================================================ 73 | %% gen_server callbacks 74 | %% ============================================================================ 75 | 76 | init(DeltasServer) -> 77 | ok = schedule_first_production(), 78 | Consumers = ordsets:new(), 79 | {ok, #state{consumers=Consumers, deltas_server=DeltasServer}}. 80 | 81 | handle_cast({subscribe, PID}, #state{consumers=Consumers1}=State) -> 82 | Consumers2 = ordsets:add_element(PID, Consumers1), 83 | {noreply, State#state{consumers=Consumers2}}; 84 | 85 | handle_cast({unsubscribe, PID}, #state{consumers=Consumers1}=State) -> 86 | Consumers2 = ordsets:del_element(PID, Consumers1), 87 | {noreply, State#state{consumers=Consumers2}}. 88 | 89 | handle_call(?PRODUCE_SYNC, _From, State1) -> 90 | State2 = produce_sync(State1), 91 | {reply, {}, State2}. 92 | 93 | handle_info(?PRODUCE_ASYNC, #state{}=State1) -> 94 | State2 = produce_async(State1), 95 | ok = schedule_next_production(), 96 | {noreply, State2}. 97 | 98 | %% ============================================================================ 99 | %% Private 100 | %% ============================================================================ 101 | 102 | -spec produce_sync(state()) -> 103 | state(). 104 | produce_sync(#state{}=State) -> 105 | produce(State, fun beam_stats_consumer:consume_sync/2). 106 | 107 | -spec produce_async(state()) -> 108 | state(). 109 | produce_async(#state{}=State) -> 110 | produce(State, fun beam_stats_consumer:consume_async/2). 111 | 112 | -spec produce(state(), fun((pid(), term()) -> ok)) -> 113 | state(). 114 | produce( 115 | #state 116 | { consumers = ConsumersSet 117 | , deltas_server = DeltasServer 118 | }=State, 119 | MsgSendFun 120 | ) -> 121 | Stats = beam_stats:collect(DeltasServer), 122 | ConsumersList = ordsets:to_list(ConsumersSet), 123 | Send = fun (Consumer) -> MsgSendFun(Consumer, Stats) end, 124 | ok = lists:foreach(Send, ConsumersList), 125 | beam_stats_delta:gc(DeltasServer), 126 | State. 127 | 128 | -spec schedule_first_production() -> 129 | ok. 130 | schedule_first_production() -> 131 | _ = self() ! ?PRODUCE_ASYNC, 132 | ok. 133 | 134 | -spec schedule_next_production() -> 135 | ok. 136 | schedule_next_production() -> 137 | ProductionInterval = beam_stats_config:production_interval(), 138 | _ = erlang:send_after(ProductionInterval, self(), ?PRODUCE_ASYNC), 139 | ok. 140 | -------------------------------------------------------------------------------- /src/beam_stats_consumer_csv.erl: -------------------------------------------------------------------------------- 1 | -module(beam_stats_consumer_csv). 2 | 3 | -include("include/beam_stats.hrl"). 4 | -include("beam_stats_logging.hrl"). 5 | 6 | -behaviour(beam_stats_consumer). 7 | 8 | -export_type( 9 | [ option/0 10 | ]). 11 | 12 | -export( 13 | [ init/1 14 | , consume/2 15 | , terminate/1 16 | ]). 17 | 18 | -type option() :: 19 | {consumption_interval , non_neg_integer()} 20 | | {path , file:filename()} 21 | . 22 | 23 | -record(state, 24 | { path :: file:filename() 25 | , file = none :: hope_option:t(file:io_device()) 26 | }). 27 | 28 | -type state() :: 29 | #state{}. 30 | 31 | -spec init([option()]) -> 32 | {non_neg_integer(), state()}. 33 | init(Options) -> 34 | ConsumptionInterval = hope_kv_list:get(Options, consumption_interval, 60000), 35 | {some, Path} = hope_kv_list:get(Options, path), 36 | State = #state 37 | { path = Path 38 | , file = none 39 | }, 40 | {ConsumptionInterval, State}. 41 | 42 | -spec consume(beam_stats_consumer:queue(), state()) -> 43 | state(). 44 | consume(Q, #state{}=State1) -> 45 | Payload = beam_stats_queue_to_binary(Q), 46 | State2 = try_to_open_if_no_file(State1), 47 | try_to_write(State2, Payload). 48 | 49 | -spec terminate(state()) -> 50 | {}. 51 | terminate(#state{file=FileOpt}) -> 52 | hope_option:iter(FileOpt, fun file:close/1). 53 | 54 | %% ============================================================================ 55 | 56 | -spec try_to_write(state(), binary()) -> 57 | state(). 58 | try_to_write(#state{file=none, path=Path}=State, _) -> 59 | ?log_error("Writing to file (~p) failed: no file in state.", [Path]), 60 | State; 61 | try_to_write(#state{file={some, File}}=State, Payload) -> 62 | case file:write(File, Payload) 63 | of ok -> 64 | State 65 | ; {error, _}=Error -> 66 | ?log_error("file:write(~p, ~p) -> ~p", [File, Payload, Error]), 67 | % TODO: Maybe schedule retry? 68 | ok = file:close(File), 69 | State#state{file=none} 70 | end. 71 | 72 | -spec try_to_open_if_no_file(state()) -> 73 | state(). 74 | try_to_open_if_no_file(#state{file={some, _}}=State) -> 75 | State; 76 | try_to_open_if_no_file(#state{file=none, path=Path}=State) -> 77 | Options = [append], 78 | case file:open(Path, Options) 79 | of {ok, File} -> 80 | State#state{file = {some, File}} 81 | ; {error, _}=Error -> 82 | ?log_error("file:open(~p, ~p) -> ~p", [Path, Options, Error]), 83 | State#state{file = none} 84 | end. 85 | 86 | -spec beam_stats_queue_to_binary(beam_stats_consumer:queue()) -> 87 | binary(). 88 | beam_stats_queue_to_binary(BEAMStatsQ) -> 89 | Bins = [beam_stats_to_bin(B) || B <- queue:to_list(BEAMStatsQ)], 90 | iolist_to_binary(Bins). 91 | 92 | 93 | -spec beam_stats_to_bin(beam_stats:t()) -> 94 | binary(). 95 | beam_stats_to_bin(#beam_stats 96 | { timestamp = Timestamp 97 | , node_id = NodeID 98 | , memory = Memory 99 | } 100 | ) -> 101 | <> = timestamp_to_bin(Timestamp), 102 | <> = node_id_to_bin(NodeID), 103 | MemoryPairToBin = make_pair_to_bin(NodeIDBin, TimestampBin, <<"memory">>), 104 | MemoryBinPairs = lists:map(fun atom_int_to_bin_bin/1, Memory), 105 | MemoryBins = lists:map(MemoryPairToBin, MemoryBinPairs), 106 | AllBins = 107 | [ MemoryBins 108 | ], 109 | iolist_to_binary(AllBins). 110 | 111 | -spec timestamp_to_bin(erlang:timestamp()) -> 112 | binary(). 113 | timestamp_to_bin(Timestamp) -> 114 | TimestampFloat = timestamp_to_float(Timestamp), 115 | {{Year, Month, Day}, {Hour, Min, Sec}} = calendar:now_to_local_time(Timestamp), 116 | SecondsFloat = Sec + (TimestampFloat - trunc(TimestampFloat)), 117 | Fmt2Digits = "~2.10.0b", 118 | FmtDate = string:join(["~b" , Fmt2Digits, Fmt2Digits], "-"), 119 | FmtTime = string:join([Fmt2Digits, Fmt2Digits, "~9..0f" ], ":"), 120 | Separator = " ", 121 | Fmt = FmtDate ++ Separator ++ FmtTime, 122 | IOList = io_lib:format(Fmt, [Year, Month, Day, Hour, Min, SecondsFloat]), 123 | iolist_to_binary(IOList). 124 | 125 | -spec timestamp_to_float(erlang:timestamp()) -> 126 | float(). 127 | timestamp_to_float({ComponentMega, ComponentWhole, ComponentMicro}) -> 128 | OneMillion = 1000000, 129 | TotalWholeSeconds = ComponentMega * OneMillion + ComponentWhole, 130 | TotalMicroSeconds = (TotalWholeSeconds * OneMillion) + ComponentMicro, 131 | TotalMicroSeconds / OneMillion. 132 | 133 | -spec make_pair_to_bin(binary(), binary(), binary()) -> 134 | fun(({binary(), binary()}) -> binary()). 135 | make_pair_to_bin(<>, <>, <>) -> 136 | fun ({<>, <>}) -> 137 | << TimestampBin/binary 138 | , "|" 139 | , NodeID/binary 140 | , "|" 141 | , Type/binary 142 | , "|" 143 | , K/binary 144 | , "|" 145 | , V/binary 146 | , "\n" 147 | >> 148 | end. 149 | 150 | -spec node_id_to_bin(node()) -> 151 | binary(). 152 | node_id_to_bin(NodeID) -> 153 | atom_to_binary(NodeID, utf8). 154 | 155 | -spec atom_int_to_bin_bin({atom(), integer()}) -> 156 | {binary(), binary()}. 157 | atom_int_to_bin_bin({K, V}) -> 158 | {atom_to_binary(K, latin1), integer_to_binary(V)}. 159 | -------------------------------------------------------------------------------- /src/beam_stats_consumer_statsd.erl: -------------------------------------------------------------------------------- 1 | -module(beam_stats_consumer_statsd). 2 | 3 | -include("include/beam_stats.hrl"). 4 | -include("beam_stats_logging.hrl"). 5 | 6 | -behaviour(beam_stats_consumer). 7 | 8 | -export_type( 9 | [ option/0 10 | ]). 11 | 12 | %% Consumer interface 13 | -export( 14 | [ init/1 15 | , consume/2 16 | , terminate/1 17 | ]). 18 | 19 | -type option() :: 20 | {consumption_interval , non_neg_integer()} 21 | | {dst_host , inet:ip_address() | inet:hostname()} 22 | | {dst_port , inet:port_number()} 23 | | {src_port , inet:port_number()} 24 | | {num_msgs_per_packet , non_neg_integer()} 25 | | {static_node_name , binary()} 26 | . 27 | 28 | -define(DEFAULT_DST_HOST, "localhost"). 29 | -define(DEFAULT_DST_PORT, 8125). 30 | -define(DEFAULT_SRC_PORT, 8124). 31 | 32 | -record(state, 33 | { sock :: hope_option:t(gen_udp:socket()) 34 | , dst_host :: inet:ip_address() | inet:hostname() 35 | , dst_port :: inet:port_number() 36 | , src_port :: inet:port_number() 37 | , num_msgs_per_packet :: non_neg_integer() 38 | , static_node_name :: hope_option:t(binary()) 39 | }). 40 | 41 | -type state() :: 42 | #state{}. 43 | 44 | %% ============================================================================ 45 | %% Consumer implementation 46 | %% ============================================================================ 47 | 48 | -spec init([option()]) -> 49 | {non_neg_integer(), state()}. 50 | init(Options) -> 51 | ConsumptionInterval = hope_kv_list:get(Options, consumption_interval, 60000), 52 | DstHost = hope_kv_list:get(Options, dst_host, ?DEFAULT_DST_HOST), 53 | DstPort = hope_kv_list:get(Options, dst_port, ?DEFAULT_DST_PORT), 54 | SrcPort = hope_kv_list:get(Options, src_port, ?DEFAULT_SRC_PORT), 55 | NumMsgsPerPacket = hope_kv_list:get(Options, num_msgs_per_packet, 10), 56 | StaticNodeNameOpt = hope_kv_list:get(Options, static_node_name), 57 | State = #state 58 | { sock = none 59 | , dst_host = DstHost 60 | , dst_port = DstPort 61 | , src_port = SrcPort 62 | , num_msgs_per_packet = NumMsgsPerPacket 63 | , static_node_name = StaticNodeNameOpt 64 | }, 65 | {ConsumptionInterval, State}. 66 | 67 | -spec consume(beam_stats_consumer:queue(), state()) -> 68 | state(). 69 | consume( 70 | Q, 71 | #state 72 | { num_msgs_per_packet = NumMsgsPerPacket 73 | , static_node_name = StaticNodeNameOpt 74 | }=State 75 | ) -> 76 | Packets = beam_stats_queue_to_packets(Q, NumMsgsPerPacket, StaticNodeNameOpt), 77 | lists:foldl(fun try_to_connect_and_send/2, State, Packets). 78 | 79 | -spec terminate(state()) -> 80 | {}. 81 | terminate(#state{sock=SockOpt}) -> 82 | hope_option:iter(SockOpt, fun gen_udp:close/1). 83 | 84 | %% ============================================================================ 85 | %% Transport 86 | %% ============================================================================ 87 | 88 | -spec try_to_connect_and_send(iolist(), state()) -> 89 | state(). 90 | try_to_connect_and_send(Payload, #state{}=State1) -> 91 | State2 = try_to_connect_if_no_socket(State1), 92 | try_to_send(State2, Payload). 93 | 94 | -spec try_to_send(state(), iolist()) -> 95 | state(). 96 | try_to_send(#state{sock=none}=State, _) -> 97 | ?log_error("Sending failed. No socket in state."), 98 | % TODO: Maybe schedule retry? 99 | State; 100 | try_to_send( 101 | #state 102 | { sock = {some, Sock} 103 | , dst_host = DstHost 104 | , dst_port = DstPort 105 | }=State, 106 | Payload 107 | ) -> 108 | case gen_udp:send(Sock, DstHost, DstPort, Payload) 109 | of ok -> 110 | State 111 | ; {error, _}=Error -> 112 | ?log_error( 113 | "gen_udp:send(~p, ~p, ~p, Payload) -> ~p", 114 | [Sock, DstHost, DstPort, Error] 115 | ), 116 | % TODO: Do something with unsent messages? 117 | ok = gen_udp:close(Sock), 118 | State#state{sock=none} 119 | end. 120 | 121 | -spec try_to_connect_if_no_socket(state()) -> 122 | state(). 123 | try_to_connect_if_no_socket(#state{sock={some, _}}=State) -> 124 | State; 125 | try_to_connect_if_no_socket(#state{sock=none, src_port=SrcPort}=State) -> 126 | case gen_udp:open(SrcPort) 127 | of {ok, Sock} -> 128 | State#state{sock = {some, Sock}} 129 | ; {error, _}=Error -> 130 | ?log_error("gen_udp:open(~p) -> ~p", [SrcPort, Error]), 131 | State#state{sock = none} 132 | end. 133 | 134 | %% ============================================================================ 135 | %% Serialization 136 | %% ============================================================================ 137 | 138 | -spec beam_stats_queue_to_packets( 139 | beam_stats_consumer:queue(), 140 | non_neg_integer(), 141 | hope_option:t(binary()) 142 | ) -> 143 | [iolist()]. 144 | beam_stats_queue_to_packets(Q, NumMsgsPerPacket, StaticNodeNameOpt) -> 145 | MsgIOLists = lists:append([beam_stats_to_iolists(B, StaticNodeNameOpt) || B <- queue:to_list(Q)]), 146 | hope_list:divide(MsgIOLists, NumMsgsPerPacket). 147 | 148 | -spec beam_stats_to_iolists(beam_stats:t(), hope_option:t(binary())) -> 149 | [iolist()]. 150 | beam_stats_to_iolists(#beam_stats{node_id=NodeID}=BeamStats, StaticNodeNameOpt) -> 151 | NodeIDBinDefault = beam_stats_msg_graphite:node_id_to_bin(NodeID), 152 | NodeIDBin = hope_option:get(StaticNodeNameOpt, NodeIDBinDefault), 153 | MsgsGraphite = beam_stats_msg_graphite:of_beam_stats(BeamStats, NodeIDBin), 154 | MsgsStatsD = 155 | lists:map(fun beam_stats_msg_statsd_gauge:of_msg_graphite/1, MsgsGraphite), 156 | lists:map(fun beam_stats_msg_statsd_gauge:to_iolist/1, MsgsStatsD). 157 | -------------------------------------------------------------------------------- /src/beam_stats_consumer.erl: -------------------------------------------------------------------------------- 1 | -module(beam_stats_consumer). 2 | 3 | -include("include/beam_stats.hrl"). 4 | 5 | -behaviour(gen_server). 6 | 7 | -export_type( 8 | [ queue/0 9 | ]). 10 | 11 | %% Public API 12 | -export( 13 | [ add/2 14 | , consume_sync/2 15 | , consume_async/2 16 | ]). 17 | 18 | %% Internal API 19 | -export( 20 | [ start_link/2 21 | ]). 22 | 23 | %% gen_server callbacks 24 | -export( 25 | [ init/1 26 | , handle_call/3 27 | , handle_cast/2 28 | , handle_info/2 29 | , terminate/2 30 | , code_change/3 31 | ]). 32 | 33 | -type queue() :: 34 | queue:queue(beam_stats:t()). 35 | 36 | %% ============================================================================ 37 | %% Consumer interface 38 | %% ============================================================================ 39 | 40 | -callback init(Options :: term()) -> 41 | {ConsumptionInterval :: non_neg_integer(), State :: term()}. 42 | 43 | -callback consume(queue(), State) -> 44 | State. 45 | 46 | -callback terminate(State :: term()) -> 47 | {}. 48 | 49 | %% ============================================================================ 50 | %% Internal data 51 | %% ============================================================================ 52 | 53 | -define(FLUSH , flush). 54 | -define(CONSUME_SYNC , consume_sync). 55 | -define(CONSUME_ASYNC , consume_async). 56 | 57 | -record(state, 58 | { consumer_module :: module() 59 | , consumer_state :: term() 60 | , consumption_interval :: non_neg_integer() 61 | , beam_stats_queue :: queue() 62 | }). 63 | 64 | -type state() :: 65 | #state{}. 66 | 67 | %% ============================================================================ 68 | %% Public API 69 | %% ============================================================================ 70 | 71 | -spec add(module(), term()) -> 72 | supervisor:startchild_ret(). 73 | add(ConsumerModule, ConsumerOptions) -> 74 | beam_stats_sup_consumers:start_child(ConsumerModule, ConsumerOptions). 75 | 76 | -spec consume_sync(pid(), beam_stats:t()) -> 77 | {}. 78 | consume_sync(PID, #beam_stats{}=BEAMStats) -> 79 | {} = gen_server:call(PID, {?CONSUME_SYNC, BEAMStats}). 80 | 81 | -spec consume_async(pid(), beam_stats:t()) -> 82 | {}. 83 | consume_async(PID, #beam_stats{}=BEAMStats) -> 84 | ok = gen_server:cast(PID, {?CONSUME_ASYNC, BEAMStats}), 85 | {}. 86 | 87 | %% ============================================================================ 88 | %% Internal API 89 | %% ============================================================================ 90 | 91 | start_link(ConsumerModule, ConsumerOptions) -> 92 | RegisteredName = ConsumerModule, 93 | GenServerModule = ?MODULE, 94 | GenServerOpts = [], 95 | InitArgs = [ConsumerModule, ConsumerOptions], 96 | gen_server:start_link({local, RegisteredName}, GenServerModule, InitArgs, GenServerOpts). 97 | 98 | %% ============================================================================ 99 | %% gen_server callbacks (unused) 100 | %% ============================================================================ 101 | 102 | code_change(_OldVsn, State, _Extra) -> 103 | {ok, State}. 104 | 105 | %% ============================================================================ 106 | %% gen_server callbacks 107 | %% ============================================================================ 108 | 109 | init([ConsumerModule, ConsumerOptions]) -> 110 | {ConsumptionInterval, ConsumerState} = ConsumerModule:init(ConsumerOptions), 111 | State = #state 112 | { consumer_module = ConsumerModule 113 | , consumer_state = ConsumerState 114 | , consumption_interval = ConsumptionInterval 115 | , beam_stats_queue = queue:new() 116 | }, 117 | ok = beam_stats_producer:subscribe(self()), 118 | ok = schedule_first_flush(), 119 | {ok, State}. 120 | 121 | terminate(_Reason, _State) -> 122 | ok = beam_stats_producer:unsubscribe(self()). 123 | 124 | handle_call({?CONSUME_SYNC, #beam_stats{}=BEAMStats}, _, #state{}=State1) -> 125 | State2 = consume_one(BEAMStats, State1), 126 | {reply, {}, State2}. 127 | 128 | handle_cast({?CONSUME_ASYNC, #beam_stats{}=BEAMStats}, #state{}=State1) -> 129 | State2 = enqueue(BEAMStats, State1), 130 | {noreply, State2}. 131 | 132 | handle_info(?FLUSH, #state{consumption_interval=ConsumptionInterval}=State1) -> 133 | State2 = consume_all_queued(State1), 134 | ok = schedule_next_flush(ConsumptionInterval), 135 | {noreply, State2}. 136 | 137 | %% ============================================================================ 138 | %% Internal 139 | %% ============================================================================ 140 | 141 | -spec consume_one(beam_stats:t(), state()) -> 142 | state(). 143 | consume_one(#beam_stats{}=BEAMStats, #state{}=State1) -> 144 | Q = queue:in(BEAMStats, queue:new()), 145 | consume(Q, State1). 146 | 147 | -spec consume_all_queued(state()) -> 148 | state(). 149 | consume_all_queued(#state{beam_stats_queue=Q}=State1) -> 150 | State2 = consume(Q, State1), 151 | State2#state{beam_stats_queue = queue:new()}. 152 | 153 | -spec consume(queue(), state()) -> 154 | state(). 155 | consume( 156 | Q, 157 | #state 158 | { consumer_module = ConsumerModule 159 | , consumer_state = ConsumerState 160 | }=State 161 | ) -> 162 | State#state{consumer_state = ConsumerModule:consume(Q, ConsumerState)}. 163 | 164 | -spec enqueue(beam_stats:t(), state()) -> 165 | state(). 166 | enqueue(#beam_stats{}=BEAMStats, #state{beam_stats_queue=Q}=State) -> 167 | State#state{beam_stats_queue = queue:in(BEAMStats, Q)}. 168 | 169 | -spec schedule_first_flush() -> 170 | ok. 171 | schedule_first_flush() -> 172 | _ = self() ! ?FLUSH, 173 | ok. 174 | 175 | -spec schedule_next_flush(non_neg_integer()) -> 176 | ok. 177 | schedule_next_flush(Time) -> 178 | _ = erlang:send_after(Time, self(), ?FLUSH), 179 | ok. 180 | -------------------------------------------------------------------------------- /src/beam_stats_process.erl: -------------------------------------------------------------------------------- 1 | -module(beam_stats_process). 2 | 3 | -include("include/beam_stats_process_ancestry.hrl"). 4 | -include("include/beam_stats_process.hrl"). 5 | 6 | -export_type( 7 | [ t/0 8 | , status/0 9 | , ancestry/0 10 | , best_known_origin/0 11 | ]). 12 | 13 | -export( 14 | [ of_pid/2 15 | , get_best_known_origin/1 16 | , print/1 17 | ]). 18 | 19 | -type status() :: 20 | exiting 21 | | garbage_collecting 22 | | runnable 23 | | running 24 | | suspended 25 | | waiting 26 | . 27 | 28 | -type ancestry() :: 29 | #beam_stats_process_ancestry{}. 30 | 31 | -type best_known_origin() :: 32 | {registered_name , atom()} 33 | | {ancestry , ancestry()} 34 | . 35 | 36 | -define(T, #?MODULE). 37 | 38 | -type t() :: 39 | ?T{}. 40 | 41 | %% ============================================================================ 42 | %% Public API 43 | %% ============================================================================ 44 | 45 | -spec of_pid(pid(), beam_stats_delta:t()) -> 46 | none % when process is dead 47 | | {some, t()} % when process is alive 48 | . 49 | of_pid(Pid, DeltasServer) -> 50 | try 51 | Dict = pid_info_exn(Pid, dictionary), 52 | Ancestry = 53 | #beam_stats_process_ancestry 54 | { raw_initial_call = pid_info_exn(Pid, initial_call) 55 | , otp_initial_call = hope_kv_list:get(Dict, '$initial_call') 56 | , otp_ancestors = hope_kv_list:get(Dict, '$ancestors') 57 | }, 58 | T = 59 | ?T 60 | { pid = Pid 61 | , registered_name = pid_info_opt(Pid, registered_name) 62 | , ancestry = Ancestry 63 | , status = pid_info_exn(Pid, status) 64 | , memory = pid_info_exn(Pid, memory) 65 | , total_heap_size = pid_info_exn(Pid, total_heap_size) 66 | , stack_size = pid_info_exn(Pid, stack_size) 67 | , message_queue_len = pid_info_exn(Pid, message_queue_len) 68 | , reductions = pid_info_reductions(Pid, DeltasServer) 69 | }, 70 | {some, T} 71 | catch throw:{process_dead, _} -> 72 | none 73 | end. 74 | 75 | -spec print(t()) -> 76 | ok. 77 | print( 78 | ?T 79 | { pid = Pid 80 | , registered_name = RegisteredNameOpt 81 | , ancestry = #beam_stats_process_ancestry 82 | { raw_initial_call = InitialCallRaw 83 | , otp_initial_call = InitialCallOTPOpt 84 | , otp_ancestors = AncestorsOpt 85 | } 86 | , status = Status 87 | , memory = Memory 88 | , total_heap_size = TotalHeapSize 89 | , stack_size = StackSize 90 | , message_queue_len = MsgQueueLen 91 | , reductions = Reductions 92 | }=T 93 | ) -> 94 | BestKnownOrigin = get_best_known_origin(T), 95 | io:format("--------------------------------------------------~n"), 96 | io:format( 97 | "Pid : ~p~n" 98 | "BestKnownOrigin : ~p~n" 99 | "RegisteredNameOpt : ~p~n" 100 | "InitialCallRaw : ~p~n" 101 | "InitialCallOTPOpt : ~p~n" 102 | "AncestorsOpt : ~p~n" 103 | "Status : ~p~n" 104 | "Memory : ~p~n" 105 | "TotalHeapSize : ~p~n" 106 | "StackSize : ~p~n" 107 | "MsgQueueLen : ~p~n" 108 | "Reductions : ~p~n" 109 | "~n", 110 | [ Pid 111 | , BestKnownOrigin 112 | , RegisteredNameOpt 113 | , InitialCallRaw 114 | , InitialCallOTPOpt 115 | , AncestorsOpt 116 | , Status 117 | , Memory 118 | , TotalHeapSize 119 | , StackSize 120 | , MsgQueueLen 121 | , Reductions 122 | ] 123 | ). 124 | 125 | %% ============================================================================ 126 | %% Private helpers 127 | %% ============================================================================ 128 | 129 | -spec pid_info_reductions(pid(), beam_stats_delta:t()) -> 130 | non_neg_integer(). 131 | pid_info_reductions(Pid, DeltasServer) -> 132 | case beam_stats_delta:of_process_info_reductions(DeltasServer, Pid) 133 | of {some, Reductions} -> 134 | Reductions 135 | ; none -> 136 | throw({process_dead, Pid}) 137 | end. 138 | 139 | pid_info_exn(Pid, Key) -> 140 | {some, Value} = pid_info_opt(Pid, Key), 141 | Value. 142 | 143 | pid_info_opt(Pid, Key) -> 144 | case {Key, beam_stats_source:erlang_process_info(Pid, Key)} 145 | of {registered_name, []} -> none 146 | ; {_ , {Key, Value}} -> {some, Value} 147 | ; {_ , undefined} -> throw({process_dead, Pid}) 148 | end. 149 | 150 | %% ============================================================================ 151 | %% Process code origin approximation or naming the anonymous processes. 152 | %% 153 | %% At runtime, given a PID, how precisely can we identify the origin of the 154 | %% code it is running? 155 | %% 156 | %% We have these data points: 157 | %% 158 | %% - Sometimes | registered name (if so, we're done) 159 | %% - Sometimes | ancestor PIDs or registered names 160 | %% - Always | initial_call (can be too generic, such as erlang:apply) 161 | %% - Always | current_function (can be too far down the stack) 162 | %% - Always | current_location (can be too far down the stack) 163 | %% - Potentially | application tree, but maybe expensive to compute, need to check 164 | %% ============================================================================ 165 | 166 | -define(TAG(Tag), fun (X) -> {Tag, X} end). 167 | 168 | -spec get_best_known_origin(t()) -> 169 | best_known_origin(). 170 | get_best_known_origin(?T{registered_name={some, RegisteredName}}) -> 171 | {registered_name, RegisteredName}; 172 | get_best_known_origin(?T{registered_name=none, ancestry=Ancestry}) -> 173 | {ancestry, Ancestry}. 174 | -------------------------------------------------------------------------------- /src/beam_stats_msg_graphite.erl: -------------------------------------------------------------------------------- 1 | -module(beam_stats_msg_graphite). 2 | 3 | -include("include/beam_stats.hrl"). 4 | -include("include/beam_stats_ets_table.hrl"). 5 | -include("include/beam_stats_msg_graphite.hrl"). 6 | -include("include/beam_stats_process.hrl"). 7 | -include("include/beam_stats_process_ancestry.hrl"). 8 | -include("include/beam_stats_processes.hrl"). 9 | 10 | -export_type( 11 | [ t/0 12 | ]). 13 | 14 | -export( 15 | [ of_beam_stats/1 16 | , of_beam_stats/2 17 | , to_iolist/1 18 | , path_to_iolist/1 19 | , node_id_to_bin/1 20 | ]). 21 | 22 | -define(SCHEMA_VERSION, <<"beam_stats_v0">>). 23 | -define(T, #?MODULE). 24 | 25 | -type t() :: 26 | ?T{}. 27 | 28 | %% ============================================================================ 29 | %% API 30 | %% ============================================================================ 31 | 32 | -spec of_beam_stats(beam_stats:t()) -> 33 | [t()]. 34 | of_beam_stats(#beam_stats{node_id=NodeID}=BeamStats) -> 35 | NodeIDBin = node_id_to_bin(NodeID), 36 | of_beam_stats(BeamStats, NodeIDBin). 37 | 38 | -spec of_beam_stats(beam_stats:t(), binary()) -> 39 | [t()]. 40 | of_beam_stats(#beam_stats 41 | { timestamp = Timestamp 42 | , node_id = _ 43 | , memory = Memory 44 | , io_bytes_in = IOBytesIn 45 | , io_bytes_out = IOBytesOut 46 | , context_switches = ContextSwitches 47 | , reductions = Reductions 48 | , run_queue = RunQueue 49 | , ets = ETS 50 | , processes = Processes 51 | }, 52 | <> 53 | ) -> 54 | Ts = Timestamp, 55 | N = NodeID, 56 | Msgs = 57 | [ cons([N, <<"io">> , <<"bytes_in">> ], IOBytesIn , Ts) 58 | , cons([N, <<"io">> , <<"bytes_out">>], IOBytesOut , Ts) 59 | , cons([N, <<"context_switches">> ], ContextSwitches, Ts) 60 | , cons([N, <<"reductions">> ], Reductions , Ts) 61 | , cons([N, <<"run_queue">> ], RunQueue , Ts) 62 | | of_memory(Memory, NodeID, Ts) 63 | ] 64 | ++ of_ets(ETS, NodeID, Ts) 65 | ++ of_processes(Processes, NodeID, Ts), 66 | lists:map(fun path_prefix_schema_version/1, Msgs). 67 | 68 | -spec to_iolist(t()) -> 69 | iolist(). 70 | to_iolist( 71 | ?T 72 | { path = Path 73 | , value = Value 74 | , timestamp = Timestamp 75 | } 76 | ) -> 77 | PathIOList = path_to_iolist(Path), 78 | ValueBin = integer_to_binary(Value), 79 | TimestampInt = timestamp_to_integer(Timestamp), 80 | TimestampBin = integer_to_binary(TimestampInt), 81 | [PathIOList, <<" ">>, ValueBin, <<" ">>, TimestampBin, <<"\n">>]. 82 | 83 | -spec path_to_iolist([binary()]) -> 84 | iolist(). 85 | path_to_iolist(Path) -> 86 | list_interleave(Path, <<".">>). 87 | 88 | -spec node_id_to_bin(node()) -> 89 | binary(). 90 | node_id_to_bin(NodeID) -> 91 | NodeIDBin = atom_to_binary(NodeID, utf8), 92 | re:replace(NodeIDBin, "[\@\.]", "_", [global, {return, binary}]). 93 | 94 | %% ============================================================================ 95 | %% Helpers 96 | %% ============================================================================ 97 | 98 | -spec path_prefix_schema_version(t()) -> 99 | t(). 100 | path_prefix_schema_version(?T{}=T) -> 101 | path_prefix(T, ?SCHEMA_VERSION). 102 | 103 | -spec path_prefix(t(), binary()) -> 104 | t(). 105 | path_prefix(?T{path=Path}=T, <>) -> 106 | T?T{path = [Prefix | Path]}. 107 | 108 | -spec list_interleave([A], A) -> 109 | [A]. 110 | list_interleave([], _) -> 111 | []; 112 | list_interleave([X], _) -> 113 | [X]; 114 | list_interleave([X|Xs], Sep) -> 115 | [X, Sep | list_interleave(Xs, Sep)]. 116 | 117 | -spec timestamp_to_integer(erlang:timestamp()) -> 118 | non_neg_integer(). 119 | timestamp_to_integer({Megaseconds, Seconds, _}) -> 120 | Megaseconds * 1000000 + Seconds. 121 | 122 | -spec of_memory([{atom(), non_neg_integer()}], binary(), erlang:timestamp()) -> 123 | [t()]. 124 | of_memory(Memory, <>, Timestamp) -> 125 | ComponentToMessage = 126 | fun ({Key, Value}) -> 127 | KeyBin = atom_to_binary(Key, latin1), 128 | cons([NodeID, <<"memory">>, KeyBin], Value, Timestamp) 129 | end, 130 | lists:map(ComponentToMessage, Memory). 131 | 132 | -spec of_ets(beam_stats_ets:t(), binary(), erlang:timestamp()) -> 133 | [t()]. 134 | of_ets(PerTableStats, <>, Timestamp) -> 135 | OfEtsTable = fun (Table) -> of_ets_table(Table, NodeID, Timestamp) end, 136 | MsgsNested = lists:map(OfEtsTable, PerTableStats), 137 | MsgsFlattened = lists:append(MsgsNested), 138 | aggregate_by_path(MsgsFlattened, Timestamp). 139 | 140 | -spec of_ets_table(beam_stats_ets_table:t(), binary(), erlang:timestamp()) -> 141 | [t()]. 142 | of_ets_table(#beam_stats_ets_table 143 | { id = ID 144 | , name = Name 145 | , size = Size 146 | , memory = Memory 147 | }, 148 | <>, 149 | Timestamp 150 | ) -> 151 | IDType = 152 | case ID =:= Name 153 | of true -> <<"NAMED">> 154 | ; false -> <<"TID">> 155 | end, 156 | NameBin = atom_to_binary(Name, latin1), 157 | NameAndID = [NameBin, IDType], 158 | [ cons([NodeID, <<"ets_table">>, <<"size">> | NameAndID], Size , Timestamp) 159 | , cons([NodeID, <<"ets_table">>, <<"memory">> | NameAndID], Memory, Timestamp) 160 | ]. 161 | 162 | -spec of_processes(beam_stats_processes:t(), binary(), erlang:timestamp()) -> 163 | [t()]. 164 | of_processes( 165 | #beam_stats_processes 166 | { individual_stats = Processes 167 | , count_all = CountAll 168 | , count_exiting = CountExiting 169 | , count_garbage_collecting = CountGarbageCollecting 170 | , count_registered = CountRegistered 171 | , count_runnable = CountRunnable 172 | , count_running = CountRunning 173 | , count_suspended = CountSuspended 174 | , count_waiting = CountWaiting 175 | }, 176 | <>, 177 | Timestamp 178 | ) -> 179 | OfProcess = fun (P) -> of_process(P, NodeID, Timestamp) end, 180 | PerProcessMsgsNested = lists:map(OfProcess, Processes), 181 | PerProcessMsgsFlattened = lists:append(PerProcessMsgsNested), 182 | PerProcessMsgsAggregates = aggregate_by_path(PerProcessMsgsFlattened, Timestamp), 183 | Ts = Timestamp, 184 | N = NodeID, 185 | [ cons([N, <<"processes_count_all">> ], CountAll , Ts) 186 | , cons([N, <<"processes_count_exiting">> ], CountExiting , Ts) 187 | , cons([N, <<"processes_count_garbage_collecting">>], CountGarbageCollecting, Ts) 188 | , cons([N, <<"processes_count_registered">> ], CountRegistered , Ts) 189 | , cons([N, <<"processes_count_runnable">> ], CountRunnable , Ts) 190 | , cons([N, <<"processes_count_running">> ], CountRunning , Ts) 191 | , cons([N, <<"processes_count_suspended">> ], CountSuspended , Ts) 192 | , cons([N, <<"processes_count_waiting">> ], CountWaiting , Ts) 193 | | PerProcessMsgsAggregates 194 | ]. 195 | 196 | -spec of_process(beam_stats_process:t(), binary(), erlang:timestamp()) -> 197 | [t()]. 198 | of_process( 199 | #beam_stats_process 200 | { pid = _ 201 | , memory = Memory 202 | , total_heap_size = TotalHeapSize 203 | , stack_size = StackSize 204 | , message_queue_len = MsgQueueLen 205 | , reductions = Reductions 206 | }=Process, 207 | <>, 208 | Timestamp 209 | ) -> 210 | Origin = beam_stats_process:get_best_known_origin(Process), 211 | OriginBin = proc_origin_to_bin(Origin), 212 | Ts = Timestamp, 213 | N = NodeID, 214 | [ cons([N, <<"process_memory">> , OriginBin], Memory , Ts) 215 | , cons([N, <<"process_total_heap_size">> , OriginBin], TotalHeapSize , Ts) 216 | , cons([N, <<"process_stack_size">> , OriginBin], StackSize , Ts) 217 | , cons([N, <<"process_message_queue_len">> , OriginBin], MsgQueueLen , Ts) 218 | , cons([N, <<"process_reductions">> , OriginBin], Reductions , Ts) 219 | ]. 220 | 221 | -spec aggregate_by_path([t()], erlang:timestamp()) -> 222 | [t()]. 223 | aggregate_by_path(Msgs, Timestamp) -> 224 | Aggregate = 225 | fun (?T{path=K, value=V}, ValsByPath) -> 226 | dict:update_counter(K, V, ValsByPath) 227 | end, 228 | ValsByPathDict = lists:foldl(Aggregate, dict:new(), Msgs), 229 | ValsByPathList = dict:to_list(ValsByPathDict), 230 | [cons(Path, Value, Timestamp) || {Path, Value} <- ValsByPathList]. 231 | 232 | -spec proc_origin_to_bin(beam_stats_process:best_known_origin()) -> 233 | binary(). 234 | proc_origin_to_bin({registered_name, Name}) -> 235 | NameBin = atom_to_binary(Name, utf8), 236 | <<"named--", NameBin/binary>>; 237 | proc_origin_to_bin({ancestry, Ancestry}) -> 238 | #beam_stats_process_ancestry 239 | { raw_initial_call = InitCallRaw 240 | , otp_initial_call = InitCallOTPOpt 241 | , otp_ancestors = AncestorsOpt 242 | } = Ancestry, 243 | Blank = <<"NONE">>, 244 | InitCallOTPBinOpt = hope_option:map(InitCallOTPOpt , fun mfa_to_bin/1), 245 | InitCallOTPBin = hope_option:get(InitCallOTPBinOpt, Blank), 246 | AncestorsBinOpt = hope_option:map(AncestorsOpt , fun ancestors_to_bin/1), 247 | AncestorsBin = hope_option:get(AncestorsBinOpt , Blank), 248 | InitCallRawBin = mfa_to_bin(InitCallRaw), 249 | << "spawned-via--" 250 | , InitCallRawBin/binary 251 | , "--" 252 | , InitCallOTPBin/binary 253 | , "--" 254 | , AncestorsBin/binary 255 | >>. 256 | 257 | ancestors_to_bin([]) -> 258 | <<>>; 259 | ancestors_to_bin([A | Ancestors]) -> 260 | ABin = ancestor_to_bin(A), 261 | case ancestors_to_bin(Ancestors) 262 | of <<>> -> 263 | ABin 264 | ; <> -> 265 | <> 266 | end. 267 | 268 | ancestor_to_bin(A) when is_atom(A) -> 269 | atom_to_binary(A, utf8); 270 | ancestor_to_bin(A) when is_pid(A) -> 271 | <<"PID">>. 272 | 273 | -spec mfa_to_bin(mfa()) -> 274 | binary(). 275 | mfa_to_bin({Module, Function, Arity}) -> 276 | ModuleBin = atom_to_binary(Module , utf8), 277 | FunctionBin = atom_to_binary(Function, utf8), 278 | ArityBin = erlang:integer_to_binary(Arity), 279 | <>. 280 | 281 | -spec cons([binary()], integer(), erlang:timestamp()) -> 282 | t(). 283 | cons(Path, Value, Timestamp) -> 284 | ?T 285 | { path = Path 286 | , value = Value 287 | , timestamp = Timestamp 288 | }. 289 | -------------------------------------------------------------------------------- /test/beam_stats_consumer_statsd_SUITE.erl: -------------------------------------------------------------------------------- 1 | -module(beam_stats_consumer_statsd_SUITE). 2 | 3 | -include_lib("beam_stats/include/beam_stats.hrl"). 4 | -include_lib("beam_stats/include/beam_stats_ets_table.hrl"). 5 | -include_lib("beam_stats/include/beam_stats_process.hrl"). 6 | -include_lib("beam_stats/include/beam_stats_process_ancestry.hrl"). 7 | -include_lib("beam_stats/include/beam_stats_processes.hrl"). 8 | 9 | -export( 10 | [ all/0 11 | , groups/0 12 | ]). 13 | 14 | %% Test cases 15 | -export( 16 | [ t_full_cycle/1 17 | , t_deltas_gc/1 18 | ]). 19 | 20 | -define(GROUP, beam_stats_consumer_statsd). 21 | 22 | %% ============================================================================ 23 | %% Common Test callbacks 24 | %% ============================================================================ 25 | 26 | all() -> 27 | [{group, ?GROUP}]. 28 | 29 | groups() -> 30 | Tests = 31 | [ t_full_cycle 32 | , t_deltas_gc 33 | ], 34 | Properties = [], 35 | [{?GROUP, Properties, Tests}]. 36 | 37 | %% ============================================================================ 38 | %% Test cases 39 | %% ============================================================================ 40 | 41 | t_deltas_gc(_Cfg) -> 42 | Pid1 = list_to_pid("<0.101.0>"), 43 | Pid2 = list_to_pid("<0.102.0>"), 44 | Pid3 = list_to_pid("<0.103.0>"), 45 | meck:new(beam_stats_source), 46 | meck:expect(beam_stats_source, erlang_process_info, 47 | fun (P, reductions) when P == Pid1 -> {reductions, 1} 48 | ; (P, reductions) when P == Pid2 -> {reductions, 2} 49 | ; (P, reductions) when P == Pid3 -> {reductions, 3} 50 | end 51 | ), 52 | DeltasServer = beam_stats_delta:start(), 53 | {some, 1} = beam_stats_delta:of_process_info_reductions(DeltasServer, Pid1), 54 | {some, 2} = beam_stats_delta:of_process_info_reductions(DeltasServer, Pid2), 55 | {some, 3} = beam_stats_delta:of_process_info_reductions(DeltasServer, Pid3), 56 | {some, 0} = beam_stats_delta:of_process_info_reductions(DeltasServer, Pid1), 57 | {some, 0} = beam_stats_delta:of_process_info_reductions(DeltasServer, Pid2), 58 | {some, 0} = beam_stats_delta:of_process_info_reductions(DeltasServer, Pid3), 59 | meck:expect(beam_stats_source, erlang_is_process_alive, fun (_) -> false end), 60 | {} = beam_stats_delta:gc(DeltasServer), 61 | {some, 1} = beam_stats_delta:of_process_info_reductions(DeltasServer, Pid1), 62 | {some, 2} = beam_stats_delta:of_process_info_reductions(DeltasServer, Pid2), 63 | {some, 3} = beam_stats_delta:of_process_info_reductions(DeltasServer, Pid3), 64 | {} = beam_stats_delta:stop(DeltasServer), 65 | meck:unload(beam_stats_source). 66 | 67 | t_full_cycle(_Cfg) -> 68 | meck:new(beam_stats_source), 69 | _BEAMStatsExpected = meck_expect_beam_stats(), 70 | 71 | {ok,[hope,beam_stats]} = application:ensure_all_started(beam_stats), 72 | ct:log("beam_stats started~n"), 73 | ServerPort = 8125, 74 | {ok, ServerSocket} = gen_udp:open(ServerPort, [binary, {active, false}]), 75 | ct:log("UDP server started started~n"), 76 | {ok, _} = beam_stats_consumer:add(beam_stats_consumer_statsd, 77 | [ {consumption_interval , 60000} 78 | , {dst_host , "localhost"} 79 | , {dst_port , ServerPort} 80 | , {src_port , 8124} 81 | , {num_msgs_per_packet , 10} 82 | ] 83 | ), 84 | ct:log("consumer added~n"), 85 | _ = meck_expect_beam_stats( 86 | % Double the original values, so that deltas will equal originals after 87 | % 1 update of new beam_stats_state:t() 88 | [ {io_bytes_in , 6} 89 | , {io_bytes_out , 14} 90 | , {context_switches , 10} 91 | , {reductions , 18} 92 | ] 93 | ), 94 | ct:log("meck_expect_beam_stats ok~n"), 95 | {} = beam_stats_producer:sync_produce_consume(), 96 | ct:log("produced and consumed~n"), 97 | ok = application:stop(beam_stats), 98 | ct:log("beam_stats stopped~n"), 99 | 100 | ResultOfReceive1 = gen_udp:recv(ServerSocket, 0), 101 | ResultOfReceive2 = gen_udp:recv(ServerSocket, 0), 102 | ResultOfReceive3 = gen_udp:recv(ServerSocket, 0), 103 | ResultOfReceive4 = gen_udp:recv(ServerSocket, 0), 104 | ok = gen_udp:close(ServerSocket), 105 | {ok, {_, _, PacketReceived1}} = ResultOfReceive1, 106 | {ok, {_, _, PacketReceived2}} = ResultOfReceive2, 107 | {ok, {_, _, PacketReceived3}} = ResultOfReceive3, 108 | {ok, {_, _, PacketReceived4}} = ResultOfReceive4, 109 | ct:log("PacketReceived1: ~n~s~n", [PacketReceived1]), 110 | ct:log("PacketReceived2: ~n~s~n", [PacketReceived2]), 111 | ct:log("PacketReceived3: ~n~s~n", [PacketReceived3]), 112 | ct:log("PacketReceived4: ~n~s~n", [PacketReceived4]), 113 | PacketsCombined = 114 | << PacketReceived1/binary 115 | , PacketReceived2/binary 116 | , PacketReceived3/binary 117 | , PacketReceived4/binary 118 | >>, 119 | ct:log("PacketsCombined: ~n~s~n", [PacketsCombined]), 120 | MsgsExpected = 121 | [ <<"beam_stats_v0.node_foo_host_bar.io.bytes_in:3|g">> 122 | , <<"beam_stats_v0.node_foo_host_bar.io.bytes_out:7|g">> 123 | , <<"beam_stats_v0.node_foo_host_bar.context_switches:5|g">> 124 | , <<"beam_stats_v0.node_foo_host_bar.reductions:9|g">> 125 | , <<"beam_stats_v0.node_foo_host_bar.run_queue:17|g">> 126 | , <<"beam_stats_v0.node_foo_host_bar.memory.mem_type_foo:1|g">> 127 | , <<"beam_stats_v0.node_foo_host_bar.memory.mem_type_bar:2|g">> 128 | , <<"beam_stats_v0.node_foo_host_bar.memory.mem_type_baz:3|g">> 129 | , <<"beam_stats_v0.node_foo_host_bar.ets_table.size.foo.NAMED:5|g">> 130 | , <<"beam_stats_v0.node_foo_host_bar.ets_table.memory.foo.NAMED:40|g">> 131 | , <<"beam_stats_v0.node_foo_host_bar.ets_table.size.bar.TID:16|g">> 132 | , <<"beam_stats_v0.node_foo_host_bar.ets_table.memory.bar.TID:128|g">> 133 | 134 | % Processes totals 135 | , <<"beam_stats_v0.node_foo_host_bar.processes_count_all:4|g">> 136 | , <<"beam_stats_v0.node_foo_host_bar.processes_count_exiting:0|g">> 137 | , <<"beam_stats_v0.node_foo_host_bar.processes_count_garbage_collecting:0|g">> 138 | , <<"beam_stats_v0.node_foo_host_bar.processes_count_registered:1|g">> 139 | , <<"beam_stats_v0.node_foo_host_bar.processes_count_runnable:0|g">> 140 | , <<"beam_stats_v0.node_foo_host_bar.processes_count_running:3|g">> 141 | , <<"beam_stats_v0.node_foo_host_bar.processes_count_suspended:0|g">> 142 | , <<"beam_stats_v0.node_foo_host_bar.processes_count_waiting:1|g">> 143 | 144 | % Process 1 145 | , <<"beam_stats_v0.node_foo_host_bar.process_memory.named--reg_name_foo:15|g">> 146 | , <<"beam_stats_v0.node_foo_host_bar.process_total_heap_size.named--reg_name_foo:25|g">> 147 | , <<"beam_stats_v0.node_foo_host_bar.process_stack_size.named--reg_name_foo:10|g">> 148 | , <<"beam_stats_v0.node_foo_host_bar.process_message_queue_len.named--reg_name_foo:0|g">> 149 | , <<"beam_stats_v0.node_foo_host_bar.process_reductions.named--reg_name_foo:0|g">> 150 | 151 | % Process 2 152 | , <<"beam_stats_v0.node_foo_host_bar.process_memory.spawned-via--bar_mod-bar_fun-1--NONE--NONE:25|g">> 153 | , <<"beam_stats_v0.node_foo_host_bar.process_total_heap_size.spawned-via--bar_mod-bar_fun-1--NONE--NONE:35|g">> 154 | , <<"beam_stats_v0.node_foo_host_bar.process_stack_size.spawned-via--bar_mod-bar_fun-1--NONE--NONE:40|g">> 155 | , <<"beam_stats_v0.node_foo_host_bar.process_message_queue_len.spawned-via--bar_mod-bar_fun-1--NONE--NONE:5|g">> 156 | , <<"beam_stats_v0.node_foo_host_bar.process_reductions.spawned-via--bar_mod-bar_fun-1--NONE--NONE:0|g">> 157 | 158 | % Process 3 and 4, aggregated by origin 159 | , <<"beam_stats_v0.node_foo_host_bar.process_memory.spawned-via--baz_mod-baz_fun-3--baz_otp_mod-baz_otp_fun-2--PID-PID:30|g">> 160 | , <<"beam_stats_v0.node_foo_host_bar.process_total_heap_size.spawned-via--baz_mod-baz_fun-3--baz_otp_mod-baz_otp_fun-2--PID-PID:45|g">> 161 | , <<"beam_stats_v0.node_foo_host_bar.process_stack_size.spawned-via--baz_mod-baz_fun-3--baz_otp_mod-baz_otp_fun-2--PID-PID:55|g">> 162 | , <<"beam_stats_v0.node_foo_host_bar.process_message_queue_len.spawned-via--baz_mod-baz_fun-3--baz_otp_mod-baz_otp_fun-2--PID-PID:1|g">> 163 | , <<"beam_stats_v0.node_foo_host_bar.process_reductions.spawned-via--baz_mod-baz_fun-3--baz_otp_mod-baz_otp_fun-2--PID-PID:0|g">> 164 | ], 165 | MsgsReceived = binary:split(PacketsCombined, <<"\n">>, [global, trim]), 166 | RemoveExpectedFromReceived = 167 | fun (Expected, Received) -> 168 | ct:log( 169 | "Looking for expected msg ~p in remaining received ~p~n", 170 | [Expected, Received] 171 | ), 172 | true = lists:member(Expected, Received), 173 | Received -- [Expected] 174 | end, 175 | MsgsRemaining = lists:foldl(RemoveExpectedFromReceived, MsgsReceived, MsgsExpected), 176 | ct:log("MsgsRemaining: ~p", [MsgsRemaining]), 177 | [] = MsgsRemaining, 178 | meck:unload(beam_stats_source). 179 | 180 | meck_expect_beam_stats() -> 181 | meck_expect_beam_stats([]). 182 | 183 | meck_expect_beam_stats(Overrides) -> 184 | IOBytesIn = hope_kv_list:get(Overrides, io_bytes_in , 3), 185 | IOBytesOut = hope_kv_list:get(Overrides, io_bytes_out, 7), 186 | ContextSwitches = hope_kv_list:get(Overrides, context_switches, 5), 187 | Reductions = hope_kv_list:get(Overrides, reductions, 9), 188 | Pid0 = list_to_pid("<0.0.0>"), 189 | Pid1 = list_to_pid("<0.1.0>"), 190 | Pid2 = list_to_pid("<0.2.0>"), 191 | Pid3 = list_to_pid("<0.3.0>"), 192 | Pid4 = list_to_pid("<0.4.0>"), 193 | Process1 = 194 | #beam_stats_process 195 | { pid = Pid1 196 | , registered_name = {some, reg_name_foo} 197 | , ancestry = 198 | #beam_stats_process_ancestry 199 | { raw_initial_call = {foo_mod, foo_fun, 2} 200 | , otp_initial_call = none 201 | , otp_ancestors = none 202 | } 203 | , status = running 204 | , memory = 15 205 | , total_heap_size = 25 206 | , stack_size = 10 207 | , message_queue_len = 0 208 | }, 209 | Process2 = 210 | #beam_stats_process 211 | { pid = Pid2 212 | , registered_name = none 213 | , ancestry = 214 | #beam_stats_process_ancestry 215 | { raw_initial_call = {bar_mod, bar_fun, 1} 216 | , otp_initial_call = none 217 | , otp_ancestors = none 218 | } 219 | , status = running 220 | , memory = 25 221 | , total_heap_size = 35 222 | , stack_size = 40 223 | , message_queue_len = 5 224 | }, 225 | Process3 = 226 | #beam_stats_process 227 | { pid = Pid3 228 | , registered_name = none 229 | , ancestry = 230 | #beam_stats_process_ancestry 231 | { raw_initial_call = {baz_mod, baz_fun, 3} 232 | , otp_initial_call = {some, {baz_otp_mod, baz_otp_fun, 2}} 233 | , otp_ancestors = {some, [Pid0, Pid1]} 234 | } 235 | , status = running 236 | , memory = 25 237 | , total_heap_size = 35 238 | , stack_size = 40 239 | , message_queue_len = 1 240 | }, 241 | Process4 = 242 | #beam_stats_process 243 | { pid = Pid4 244 | , registered_name = none 245 | , ancestry = 246 | #beam_stats_process_ancestry 247 | { raw_initial_call = {baz_mod, baz_fun, 3} 248 | , otp_initial_call = {some, {baz_otp_mod, baz_otp_fun, 2}} 249 | , otp_ancestors = {some, [Pid0, Pid1]} 250 | } 251 | , status = waiting 252 | , memory = 5 253 | , total_heap_size = 10 254 | , stack_size = 15 255 | , message_queue_len = 0 256 | }, 257 | Processes = 258 | #beam_stats_processes 259 | { individual_stats = 260 | [ Process1 261 | , Process2 262 | , Process3 263 | , Process4 264 | ] 265 | , count_all = 4 266 | , count_exiting = 0 267 | , count_garbage_collecting = 0 268 | , count_registered = 1 269 | , count_runnable = 0 270 | , count_running = 3 271 | , count_suspended = 0 272 | , count_waiting = 1 273 | }, 274 | ETSTableStatsFoo = 275 | #beam_stats_ets_table 276 | { id = foo 277 | , name = foo 278 | , size = 5 279 | , memory = 40 280 | }, 281 | ETSTableStatsBarA = 282 | #beam_stats_ets_table 283 | { id = 37 284 | , name = bar 285 | , size = 8 286 | , memory = 64 287 | }, 288 | ETSTableStatsBarB = 289 | #beam_stats_ets_table 290 | { id = 38 291 | , name = bar 292 | , size = 8 293 | , memory = 64 294 | }, 295 | meck:expect(beam_stats_source, erlang_is_process_alive, 296 | fun (_) -> true end), 297 | meck:expect(beam_stats_source, erlang_memory, 298 | fun () -> [{mem_type_foo, 1}, {mem_type_bar, 2}, {mem_type_baz, 3}] end), 299 | meck:expect(beam_stats_source, erlang_node, 300 | fun () -> 'node_foo@host_bar' end), 301 | meck:expect(beam_stats_source, erlang_registered, 302 | fun () -> [reg_name_foo] end), 303 | meck:expect(beam_stats_source, erlang_statistics, 304 | fun (io ) -> {{input, IOBytesIn}, {output, IOBytesOut}} 305 | ; (context_switches) -> {ContextSwitches, 0} 306 | ; (reductions ) -> {Reductions, undefined} % 2nd element is unused 307 | ; (run_queue ) -> 17 308 | end 309 | ), 310 | meck:expect(beam_stats_source, ets_all, 311 | fun () -> [foo, 37, 38] end), 312 | meck:expect(beam_stats_source, erlang_system_info, 313 | fun (wordsize) -> 8 end), 314 | meck:expect(beam_stats_source, ets_info, 315 | fun (foo, memory) -> 5 316 | ; (foo, name ) -> foo 317 | ; (foo, size ) -> 5 318 | 319 | ; (37 , memory) -> 8 320 | ; (37 , name ) -> bar 321 | ; (37 , size ) -> 8 322 | 323 | ; (38 , memory) -> 8 324 | ; (38 , name ) -> bar 325 | ; (38 , size ) -> 8 326 | end 327 | ), 328 | meck:expect(beam_stats_source, erlang_processes, 329 | fun () -> [Pid1, Pid2, Pid3, Pid4] end), 330 | meck:expect(beam_stats_source, os_timestamp, 331 | fun () -> {1, 2, 3} end), 332 | meck:expect(beam_stats_source, erlang_process_info, 333 | fun (P, K) when P == Pid1 -> 334 | case K 335 | of dictionary -> {K, []} 336 | ; initial_call -> {K, {foo_mod, foo_fun, 2}} 337 | ; registered_name -> {K, reg_name_foo} 338 | ; status -> {K, running} 339 | ; memory -> {K, 15} 340 | ; total_heap_size -> {K, 25} 341 | ; stack_size -> {K, 10} 342 | ; message_queue_len -> {K, 0} 343 | ; reductions -> {K, 1} 344 | end 345 | ; (P, K) when P == Pid2 -> 346 | case K 347 | of dictionary -> {K, []} 348 | ; initial_call -> {K, {bar_mod, bar_fun, 1}} 349 | ; registered_name -> [] 350 | ; status -> {K, running} 351 | ; memory -> {K, 25} 352 | ; total_heap_size -> {K, 35} 353 | ; stack_size -> {K, 40} 354 | ; message_queue_len -> {K, 5} 355 | ; reductions -> {K, 2} 356 | end 357 | ; (P, K) when P == Pid3 -> 358 | Dict = 359 | [ {'$initial_call', {baz_otp_mod, baz_otp_fun, 2}} 360 | , {'$ancestors' , [Pid0, Pid1]} 361 | ], 362 | case K 363 | of dictionary -> {K, Dict} 364 | ; initial_call -> {K, {baz_mod, baz_fun, 3}} 365 | ; registered_name -> [] 366 | ; status -> {K, running} 367 | ; memory -> {K, 25} 368 | ; total_heap_size -> {K, 35} 369 | ; stack_size -> {K, 40} 370 | ; message_queue_len -> {K, 1} 371 | ; reductions -> {K, 3} 372 | end 373 | ; (P, K) when P == Pid4 -> 374 | Dict = 375 | [ {'$initial_call', {baz_otp_mod, baz_otp_fun, 2}} 376 | , {'$ancestors' , [Pid0, Pid1]} 377 | ], 378 | case K 379 | of dictionary -> {K, Dict} 380 | ; initial_call -> {K, {baz_mod, baz_fun, 3}} 381 | ; registered_name -> [] 382 | ; status -> {K, waiting} 383 | ; memory -> {K, 5} 384 | ; total_heap_size -> {K, 10} 385 | ; stack_size -> {K, 15} 386 | ; message_queue_len -> {K, 0} 387 | ; reductions -> {K, 4} 388 | end 389 | end 390 | ), 391 | #beam_stats 392 | { timestamp = {1, 2, 3} 393 | , node_id = 'node_foo@host_bar' 394 | , memory = [{mem_type_foo, 1}, {mem_type_bar, 2}, {mem_type_baz, 3}] 395 | , io_bytes_in = IOBytesIn 396 | , io_bytes_out = IOBytesOut 397 | , context_switches = ContextSwitches 398 | , reductions = 9 399 | , run_queue = 17 400 | , ets = [ETSTableStatsFoo, ETSTableStatsBarA, ETSTableStatsBarB] 401 | , processes = Processes 402 | }. 403 | --------------------------------------------------------------------------------