├── .github └── workflows │ └── erlang.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── bin └── rebar3 ├── doc ├── edoc-info ├── statsderl.md ├── statsderl_app.md ├── statsderl_pool.md ├── statsderl_protocol.md ├── statsderl_server.md ├── statsderl_sup.md ├── statsderl_transform.md ├── statsderl_udp.md └── statsderl_utils.md ├── include └── statsderl.hrl ├── rebar.config ├── rebar.config.script ├── rebar.lock ├── src ├── statsderl.app.src ├── statsderl.erl ├── statsderl_app.erl ├── statsderl_client.erl ├── statsderl_protocol.erl ├── statsderl_sample.erl ├── statsderl_sup.erl ├── statsderl_transform.erl └── statsderl_utils.erl └── test ├── statsderl_profile.erl ├── statsderl_tests.erl ├── statsderl_transform_tests.erl └── statsderl_utils_tests.erl /.github/workflows/erlang.yml: -------------------------------------------------------------------------------- 1 | name: Erlang CI 2 | on: [push] 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | strategy: 7 | matrix: 8 | erlang: [21, 22, 23, 24, 25] 9 | 10 | container: 11 | image: erlang:${{ matrix.erlang }} 12 | 13 | steps: 14 | - uses: actions/checkout@v1 15 | - name: Compile 16 | run: make compile 17 | - name: Run xref 18 | run: make xref 19 | - name: Run eunit 20 | run: make eunit 21 | - name: Run dialyzer 22 | run: make dialyzer 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | doc/README.md 2 | erl_crash.dump 3 | fprofx.* 4 | _build 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2011-2020 Louis-Philippe Gauthier 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 | CACHEGRIND=qcachegrind 2 | REBAR3=$(shell which rebar3) 3 | ifeq ($(REBAR3),) 4 | REBAR3=./bin/rebar3 5 | endif 6 | 7 | all: compile 8 | 9 | clean: 10 | @echo "Running rebar3 clean..." 11 | @$(REBAR3) clean -a 12 | 13 | compile: 14 | @echo "Running rebar3 compile..." 15 | @$(REBAR3) as compile compile 16 | 17 | dialyzer: 18 | @echo "Running rebar3 dialyze..." 19 | @$(REBAR3) dialyzer 20 | 21 | edoc: 22 | @echo "Running rebar3 edoc..." 23 | @$(REBAR3) as edoc edoc 24 | 25 | eunit: 26 | @echo "Running rebar3 eunit..." 27 | @$(REBAR3) do eunit -cv, cover -v 28 | 29 | profile: 30 | @echo "Profiling..." 31 | @$(REBAR3) as test compile 32 | @erl -noshell \ 33 | -pa _build/test/lib/*/ebin \ 34 | -pa _build/test/lib/*/test \ 35 | -eval 'statsderl_profile:fprofx()' \ 36 | -eval 'init:stop()' 37 | @_build/test/lib/fprofx/erlgrindx -p fprofx.analysis 38 | @$(CACHEGRIND) fprofx.cgrind 39 | 40 | test: xref eunit dialyzer 41 | 42 | xref: 43 | @echo "Running rebar3 xref..." 44 | @$(REBAR3) xref 45 | 46 | .PHONY: clean compile dialyzer edoc eunit profile xref 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # statsderl 2 | 3 | High Performance Erlang StatsD Client 4 | 5 | ![Build Status](https://github.com/lpgauth/shackle/workflows/Erlang%20CI/badge.svg) 6 | 7 | ### Requirements 8 | 9 | * Erlang 16.0 + 10 | 11 | ### Features 12 | 13 | * Performance optimized 14 | * Parse transform 15 | 16 | ## API 17 | Function Index 18 | 19 | #### Environment variables: 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 |
NameTypeDefaultDescription
base_keybase_key()undefinedkey prefix
hostnameinet:ip_address() | inet:hostname() | binary(){127,0,0,1}server hostname
portinet:port_number()8125server port
pool_sizepos_integer()4pool size
53 | 54 | #### base_key options: 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 |
NameSourceDescriptionExample
hostnameinet:gethostname/0use the hostname"h033"
nameerl -nameuse the long node name"nonode@nohost"
snameerl -snameuse the short node name"nonode"
82 | 83 | ## Parse Transform 84 | 85 | `statsderl_transform` is used to pre-compute (at compile time) the scaled sample rate and `StatsD` packet. 86 | 87 | ```erlang 88 | -compile({parse_transform, statsderl_transform}). 89 | ``` 90 | ## Examples 91 | 92 | ```erlang 93 | 1> statsderl_app:start(). 94 | ok. 95 | 96 | 2> statsderl:counter(["test", $., "counter"], 1, 0.23). 97 | ok. 98 | 99 | 3> statsderl:decrement("test.decrement", 1, 0.5). 100 | ok. 101 | 102 | 4> statsderl:gauge([<<"test">>, $., "gauge"], 333, 1.0). 103 | ok. 104 | 105 | 5> statsderl:gauge_decrement([<<"test.gauge_decrement">>], 15, 0.001). 106 | ok. 107 | 108 | 6> statsderl:gauge_increment(<<"test.gauge_increment">>, 32, 1). 109 | ok. 110 | 111 | 7> statsderl:increment(<<"test.increment">>, 1, 1). 112 | ok. 113 | 114 | 8> statsderl:timing("test.timing", 5, 0.5). 115 | ok. 116 | 117 | 9> statsderl:timing_fun(<<"test.timing_fun">>, fun() -> timer:sleep(100) end, 0.5). 118 | ok. 119 | 120 | 10> Timestamp = os:timestamp(). 121 | {1448,591778,258983} 122 | 123 | 11> statsderl:timing_now("test.timing_now", Timestamp, 0.15). 124 | ok. 125 | 126 | 12> statsderl_app:stop(). 127 | ok. 128 | ``` 129 | 130 | ## Tests 131 | 132 | ```makefile 133 | make test 134 | ``` 135 | 136 | ## License 137 | 138 | ```license 139 | The MIT License (MIT) 140 | 141 | Copyright (c) 2011-2020 Louis-Philippe Gauthier 142 | 143 | Permission is hereby granted, free of charge, to any person obtaining a copy 144 | of this software and associated documentation files (the "Software"), to deal 145 | in the Software without restriction, including without limitation the rights 146 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 147 | copies of the Software, and to permit persons to whom the Software is 148 | furnished to do so, subject to the following conditions: 149 | 150 | The above copyright notice and this permission notice shall be included in all 151 | copies or substantial portions of the Software. 152 | 153 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 154 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 155 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 156 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 157 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 158 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 159 | SOFTWARE. 160 | ``` 161 | -------------------------------------------------------------------------------- /bin/rebar3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lpgauth/statsderl/50722505b991ca6da3d220c5006c23c6837a0a32/bin/rebar3 -------------------------------------------------------------------------------- /doc/edoc-info: -------------------------------------------------------------------------------- 1 | %% encoding: UTF-8 2 | {application,statsderl}. 3 | {modules,[statsderl,statsderl_app,statsderl_pool,statsderl_protocol, 4 | statsderl_server,statsderl_sup,statsderl_transform,statsderl_utils]}. 5 | -------------------------------------------------------------------------------- /doc/statsderl.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Module statsderl # 4 | * [Data Types](#types) 5 | * [Function Index](#index) 6 | * [Function Details](#functions) 7 | 8 | 9 | 10 | ## Data Types ## 11 | 12 | 13 | 14 | 15 | ### key() ### 16 | 17 | 18 |

 19 | key() = iodata()
 20 | 
21 | 22 | 23 | 24 | 25 | ### sample_rate() ### 26 | 27 | 28 |

 29 | sample_rate() = number()
 30 | 
31 | 32 | 33 | 34 | 35 | ### value() ### 36 | 37 | 38 |

 39 | value() = number()
 40 | 
41 | 42 | 43 | 44 | ## Function Index ## 45 | 46 | 47 |
counter/3
decrement/3
gauge/3
gauge_decrement/3
gauge_increment/3
increment/3
timing/3
timing_fun/3
timing_now/3
timing_now_us/3
48 | 49 | 50 | 51 | 52 | ## Function Details ## 53 | 54 | 55 | 56 | ### counter/3 ### 57 | 58 |

 59 | counter(Key::key(), Value::value(), Rate::sample_rate()) -> ok
 60 | 
61 |
62 | 63 | 64 | 65 | ### decrement/3 ### 66 | 67 |

 68 | decrement(Key::key(), Value::value(), Rate::sample_rate()) -> ok
 69 | 
70 |
71 | 72 | 73 | 74 | ### gauge/3 ### 75 | 76 |

 77 | gauge(Key::key(), Value::value(), Rate::sample_rate()) -> ok
 78 | 
79 |
80 | 81 | 82 | 83 | ### gauge_decrement/3 ### 84 | 85 |

 86 | gauge_decrement(Key::key(), Value::value(), Rate::sample_rate()) -> ok
 87 | 
88 |
89 | 90 | 91 | 92 | ### gauge_increment/3 ### 93 | 94 |

 95 | gauge_increment(Key::key(), Value::value(), Rate::sample_rate()) -> ok
 96 | 
97 |
98 | 99 | 100 | 101 | ### increment/3 ### 102 | 103 |

104 | increment(Key::key(), Value::value(), Rate::sample_rate()) -> ok
105 | 
106 |
107 | 108 | 109 | 110 | ### timing/3 ### 111 | 112 |

113 | timing(Key::key(), Value::value(), Rate::sample_rate()) -> ok
114 | 
115 |
116 | 117 | 118 | 119 | ### timing_fun/3 ### 120 | 121 |

122 | timing_fun(Key::key(), Fun::function(), Rate::sample_rate()) -> any()
123 | 
124 |
125 | 126 | 127 | 128 | ### timing_now/3 ### 129 | 130 |

131 | timing_now(Key::key(), Timestamp::erlang:timestamp(), Rate::sample_rate()) -> ok
132 | 
133 |
134 | 135 | 136 | 137 | ### timing_now_us/3 ### 138 | 139 |

140 | timing_now_us(Key::key(), Timestamp::erlang:timestamp(), Rate::sample_rate()) -> ok
141 | 
142 |
143 | 144 | -------------------------------------------------------------------------------- /doc/statsderl_app.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Module statsderl_app # 4 | * [Function Index](#index) 5 | * [Function Details](#functions) 6 | 7 | __Behaviours:__ [`application`](application.md). 8 | 9 | 10 | 11 | ## Function Index ## 12 | 13 | 14 |
start/0
start/2
stop/0
stop/1
15 | 16 | 17 | 18 | 19 | ## Function Details ## 20 | 21 | 22 | 23 | ### start/0 ### 24 | 25 |

26 | start() -> {ok, [atom()]} | {error, term()}
27 | 
28 |
29 | 30 | 31 | 32 | ### start/2 ### 33 | 34 |

35 | start(StartType::application:start_type(), StartArgs::term()) -> {ok, pid()}
36 | 
37 |
38 | 39 | 40 | 41 | ### stop/0 ### 42 | 43 |

44 | stop() -> ok | {error, {not_started, statsderl}}
45 | 
46 |
47 | 48 | 49 | 50 | ### stop/1 ### 51 | 52 |

53 | stop(State::term()) -> ok
54 | 
55 |
56 | 57 | -------------------------------------------------------------------------------- /doc/statsderl_pool.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Module statsderl_pool # 4 | * [Data Types](#types) 5 | * [Function Index](#index) 6 | * [Function Details](#functions) 7 | 8 | 9 | 10 | ## Data Types ## 11 | 12 | 13 | 14 | 15 | ### key() ### 16 | 17 | 18 |

 19 | key() = iodata()
 20 | 
21 | 22 | 23 | 24 | 25 | ### operation() ### 26 | 27 | 28 |

 29 | operation() = {cast, iodata()} | {counter, key(), value(), sample_rate()} | {gauge, key(), value()} | {gauge_decrement, key(), value()} | {gauge_increment, key(), value()} | {timing, key(), value()} | {timing_now, key(), erlang:timestamp()} | {timing_now_us, key(), erlang:timestamp()}
 30 | 
31 | 32 | 33 | 34 | 35 | ### pool_size() ### 36 | 37 | 38 |

 39 | pool_size() = pos_integer()
 40 | 
41 | 42 | 43 | 44 | 45 | ### sample_rate() ### 46 | 47 | 48 |

 49 | sample_rate() = number()
 50 | 
51 | 52 | 53 | 54 | 55 | ### value() ### 56 | 57 | 58 |

 59 | value() = number()
 60 | 
61 | 62 | 63 | 64 | ## Function Index ## 65 | 66 | 67 |
init/0
sample/2
sample_scaled/2
server_name/1
size/0
68 | 69 | 70 | 71 | 72 | ## Function Details ## 73 | 74 | 75 | 76 | ### init/0 ### 77 | 78 |

 79 | init() -> ok
 80 | 
81 |
82 | 83 | 84 | 85 | ### sample/2 ### 86 | 87 |

 88 | sample(Rate::sample_rate(), Operation::operation()) -> ok
 89 | 
90 |
91 | 92 | 93 | 94 | ### sample_scaled/2 ### 95 | 96 |

 97 | sample_scaled(RateInt::non_neg_integer(), Operation::operation()) -> ok
 98 | 
99 |
100 | 101 | 102 | 103 | ### server_name/1 ### 104 | 105 |

106 | server_name(N::pos_integer()) -> atom()
107 | 
108 |
109 | 110 | 111 | 112 | ### size/0 ### 113 | 114 |

115 | size() -> pool_size()
116 | 
117 |
118 | 119 | -------------------------------------------------------------------------------- /doc/statsderl_protocol.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Module statsderl_protocol # 4 | * [Data Types](#types) 5 | * [Function Index](#index) 6 | * [Function Details](#functions) 7 | 8 | 9 | 10 | ## Data Types ## 11 | 12 | 13 | 14 | 15 | ### key() ### 16 | 17 | 18 |

19 | key() = iodata()
20 | 
21 | 22 | 23 | 24 | 25 | ### operation() ### 26 | 27 | 28 |

29 | operation() = {cast, iodata()} | {counter, key(), value(), sample_rate()} | {gauge, key(), value()} | {gauge_decrement, key(), value()} | {gauge_increment, key(), value()} | {timing, key(), value()} | {timing_now, key(), erlang:timestamp()} | {timing_now_us, key(), erlang:timestamp()}
30 | 
31 | 32 | 33 | 34 | 35 | ### sample_rate() ### 36 | 37 | 38 |

39 | sample_rate() = number()
40 | 
41 | 42 | 43 | 44 | 45 | ### value() ### 46 | 47 | 48 |

49 | value() = number()
50 | 
51 | 52 | 53 | 54 | ## Function Index ## 55 | 56 | 57 |
encode/1
58 | 59 | 60 | 61 | 62 | ## Function Details ## 63 | 64 | 65 | 66 | ### encode/1 ### 67 | 68 |

69 | encode(X1::operation()) -> iodata()
70 | 
71 |
72 | 73 | -------------------------------------------------------------------------------- /doc/statsderl_server.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Module statsderl_server # 4 | * [Function Index](#index) 5 | * [Function Details](#functions) 6 | 7 | __Behaviours:__ [`metal`](metal.md). 8 | 9 | 10 | 11 | ## Function Index ## 12 | 13 | 14 |
handle_msg/2
init/3
start_link/1
terminate/2
15 | 16 | 17 | 18 | 19 | ## Function Details ## 20 | 21 | 22 | 23 | ### handle_msg/2 ### 24 | 25 |

26 | handle_msg(X1::term(), State::term()) -> {ok, term()}
27 | 
28 |
29 | 30 | 31 | 32 | ### init/3 ### 33 | 34 |

35 | init(Name::atom(), Parent::pid(), Opts::term()) -> {ok, term()} | {stop, atom()}
36 | 
37 |
38 | 39 | 40 | 41 | ### start_link/1 ### 42 | 43 |

44 | start_link(Name::atom()) -> {ok, pid()}
45 | 
46 |
47 | 48 | 49 | 50 | ### terminate/2 ### 51 | 52 |

53 | terminate(Reason::term(), State::term()) -> ok
54 | 
55 |
56 | 57 | -------------------------------------------------------------------------------- /doc/statsderl_sup.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Module statsderl_sup # 4 | * [Function Index](#index) 5 | * [Function Details](#functions) 6 | 7 | __Behaviours:__ [`supervisor`](supervisor.md). 8 | 9 | 10 | 11 | ## Function Index ## 12 | 13 | 14 |
init/1
start_link/0
15 | 16 | 17 | 18 | 19 | ## Function Details ## 20 | 21 | 22 | 23 | ### init/1 ### 24 | 25 |

26 | init(Args::[]) -> {ok, {{one_for_one, 5, 10}, [supervisor:child_spec()]}}
27 | 
28 |
29 | 30 | 31 | 32 | ### start_link/0 ### 33 | 34 |

35 | start_link() -> {ok, pid()}
36 | 
37 |
38 | 39 | -------------------------------------------------------------------------------- /doc/statsderl_transform.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Module statsderl_transform # 4 | * [Data Types](#types) 5 | * [Function Index](#index) 6 | * [Function Details](#functions) 7 | 8 | 9 | 10 | ## Data Types ## 11 | 12 | 13 | 14 | 15 | ### forms() ### 16 | 17 | 18 |

19 | forms() = [erl_parse:abstract_form() | erl_parse:form_info()]
20 | 
21 | 22 | 23 | 24 | ## Function Index ## 25 | 26 | 27 |
parse_transform/2
28 | 29 | 30 | 31 | 32 | ## Function Details ## 33 | 34 | 35 | 36 | ### parse_transform/2 ### 37 | 38 |

39 | parse_transform(Forms::forms(), Options::[compile:option()]) -> forms()
40 | 
41 |
42 | 43 | -------------------------------------------------------------------------------- /doc/statsderl_udp.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Module statsderl_udp # 4 | * [Function Index](#index) 5 | * [Function Details](#functions) 6 | 7 | 8 | 9 | ## Function Index ## 10 | 11 | 12 |
header/2
13 | 14 | 15 | 16 | 17 | ## Function Details ## 18 | 19 | 20 | 21 | ### header/2 ### 22 | 23 |

24 | header(IP::inet:ip_address(), Port::inet:port_number()) -> iodata()
25 | 
26 |
27 | 28 | -------------------------------------------------------------------------------- /doc/statsderl_utils.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Module statsderl_utils # 4 | * [Data Types](#types) 5 | * [Function Index](#index) 6 | * [Function Details](#functions) 7 | 8 | 9 | 10 | ## Data Types ## 11 | 12 | 13 | 14 | 15 | ### base_key() ### 16 | 17 | 18 |

19 | base_key() = base_key_part() | [base_key_part()]
20 | 
21 | 22 | 23 | 24 | 25 | ### base_key_part() ### 26 | 27 | 28 |

29 | base_key_part() = hostname | name | sname | undefined | iodata()
30 | 
31 | 32 | 33 | 34 | ## Function Index ## 35 | 36 | 37 |
base_key/1
error_msg/2
timestamp/0
timing_now/1
timing_now_us/1
38 | 39 | 40 | 41 | 42 | ## Function Details ## 43 | 44 | 45 | 46 | ### base_key/1 ### 47 | 48 |

49 | base_key(Key::base_key()) -> iodata()
50 | 
51 |
52 | 53 | 54 | 55 | ### error_msg/2 ### 56 | 57 |

58 | error_msg(Format::string(), Data::[term()]) -> ok
59 | 
60 |
61 | 62 | 63 | 64 | ### timestamp/0 ### 65 | 66 |

67 | timestamp() -> erlang:timestamp()
68 | 
69 |
70 | 71 | 72 | 73 | ### timing_now/1 ### 74 | 75 |

76 | timing_now(Timestamp::erlang:timestamp()) -> non_neg_integer()
77 | 
78 |
79 | 80 | 81 | 82 | ### timing_now_us/1 ### 83 | 84 |

85 | timing_now_us(Timestamp::erlang:timestamp()) -> non_neg_integer()
86 | 
87 |
88 | 89 | -------------------------------------------------------------------------------- /include/statsderl.hrl: -------------------------------------------------------------------------------- 1 | %% macros 2 | -define(APP, statsderl). 3 | -define(CHILD(Name, Mod), {Name, {Mod, start_link, [Name]}, permanent, 5000, worker, [Mod]}). 4 | -define(CLIENT, statsderl_client). 5 | -define(MAX_UNSIGNED_INT_32, 4294967295). 6 | 7 | %% env vars 8 | -define(ENV(Key, Default), application:get_env(?APP, Key, Default)). 9 | -define(ENV_BASEKEY, base_key). 10 | -define(ENV_HOSTNAME, hostname). 11 | -define(ENV_PORT, port). 12 | -define(ENV_VARS, [?ENV_BASEKEY, ?ENV_HOSTNAME, ?ENV_PORT]). 13 | 14 | %% defaults 15 | -define(DEFAULT_BACKLOG_SIZE, 4096). 16 | -define(DEFAULT_BASEKEY, undefined). 17 | -define(DEFAULT_HOSTNAME, "127.0.0.1"). 18 | -define(DEFAULT_POOL_SIZE, 4). 19 | -define(DEFAULT_PORT, 8125). 20 | 21 | %% types 22 | -type base_key() :: base_key_part() | [base_key_part()]. 23 | -type base_key_part() :: hostname | name | sname | undefined | iodata(). 24 | -type key() :: iodata(). 25 | -type operation() :: {cast, iodata()} | 26 | {counter, key(), value(), sample_rate()} | 27 | {gauge, key(), value()} | 28 | {gauge_decrement, key(), value()} | 29 | {gauge_increment, key(), value()} | 30 | {timing, key(), value()} | 31 | {timing_now, key(), erlang:timestamp()} | 32 | {timing_now_us, key(), erlang:timestamp()}. 33 | -type pool_size() :: pos_integer(). 34 | -type sample_rate() :: number(). 35 | -type value() :: number(). 36 | -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | {cover_export_enabled, true}. 2 | 3 | {deps, [ 4 | {granderl, "0.1.5"}, 5 | {parse_trans, "3.0.0"}, 6 | {shackle, {git, "https://github.com/lpgauth/shackle.git", {tag, "0.6.16"}}} 7 | ]}. 8 | 9 | {edoc_opts, [ 10 | {app_default, "http://www.erlang.org/doc/man"}, 11 | {doclet, edown_doclet}, 12 | {image, ""}, 13 | {includes, ["include"]}, 14 | {preprocess, true}, 15 | {stylesheet, ""}, 16 | {title, "statsderl"} 17 | ]}. 18 | 19 | {erl_opts, [ 20 | debug_info 21 | ]}. 22 | 23 | {plugins, [rebar3_hex]}. 24 | 25 | {profiles, [ 26 | {compile, [ 27 | {erl_opts, [ 28 | warnings_as_errors, 29 | warn_export_all, 30 | warn_export_vars, 31 | warn_missing_spec, 32 | warn_obsolete_guard, 33 | warn_shadow_vars, 34 | warn_untyped_record, 35 | warn_unused_import, 36 | warn_unused_vars 37 | ]} 38 | ]}, 39 | {edoc, [ 40 | {deps, [ 41 | {edown, 42 | {git, "https://github.com/uwiger/edown.git", {tag, "0.7"}}} 43 | ]} 44 | ]}, 45 | {test, [ 46 | {deps, [ 47 | {meck, 48 | {git, "https://github.com/eproxus/meck.git", {tag, "0.8.7"}}}, 49 | {fprofx, 50 | {git, "https://github.com/ransomr/fprofx.git", {branch, "master"}}} 51 | ]} 52 | ]} 53 | ]}. 54 | 55 | {xref_checks, [ 56 | deprecated_functions, 57 | deprecated_function_calls, 58 | locals_not_used, 59 | undefined_function_calls 60 | ]}. 61 | -------------------------------------------------------------------------------- /rebar.config.script: -------------------------------------------------------------------------------- 1 | case erlang:function_exported(rebar3, main, 1) of 2 | true -> 3 | CONFIG; 4 | false -> 5 | [{deps, [ 6 | {granderl, ".*", 7 | {git, "https://github.com/tokenrove/granderl.git", {tag, "v0.1.5"}}}, 8 | {parse_trans, ".*", 9 | {git, "https://github.com/uwiger/parse_trans.git", {tag, "3.0.0"}}}, 10 | {shackle, ".*", 11 | {git, "https://github.com/lpgauth/shackle.git", {tag, "0.6.16"}}} 12 | ]} | lists:keydelete(deps, 1, CONFIG)] 13 | end. 14 | -------------------------------------------------------------------------------- /rebar.lock: -------------------------------------------------------------------------------- 1 | {"1.2.0", 2 | [{<<"foil">>,{pkg,<<"foil">>,<<"0.1.0">>},0}, 3 | {<<"granderl">>,{pkg,<<"granderl">>,<<"0.1.5">>},0}, 4 | {<<"metal">>,{pkg,<<"metal">>,<<"0.1.1">>},0}, 5 | {<<"parse_trans">>,{pkg,<<"parse_trans">>,<<"3.0.0">>},0}, 6 | {<<"shackle">>,{pkg,<<"shackle">>,<<"0.6.11">>},0}]}. 7 | [ 8 | {pkg_hash,[ 9 | {<<"foil">>, <<"16406493144743F633505E51406F7BE6BF17A3772DFD62BCDE3107C654704BEE">>}, 10 | {<<"granderl">>, <<"F20077A68BD80B8D8783BD15A052813C6483771DEC1A5B837D307CBE92F14122">>}, 11 | {<<"metal">>, <<"5D3D1322DA7BCD34B94FED5486F577973685298883954F7A3E517EF5EF6953F5">>}, 12 | {<<"parse_trans">>, <<"9E96B1C9C3A0DF54E7B76F8F685D38BFA1EB21B31E042B1D1A5A70258E4DB1E3">>}, 13 | {<<"shackle">>, <<"949245E1AE690DECAD887B046CCCA595E5F3FC798A317131466985F5BBA64298">>}]}, 14 | {pkg_hash_ext,[ 15 | {<<"foil">>, <<"BA8A1773D1AF12BD24E06657C87547AA81872950F7C503F39E0F31E39BB2BC39">>}, 16 | {<<"granderl">>, <<"0641473F29BC3211C832A6DD3ADAA04544A5DFFC1C62372556946F236DF2DAD6">>}, 17 | {<<"metal">>, <<"88B82B634998A1A768DEDCD372C2F7E657B19445325C0AF5CCBAC62C77210F1D">>}, 18 | {<<"parse_trans">>, <<"5495A3309051DF7F510BC1FDABDA92DFF5417186A8F66D4419AAD6BA0AF5F0CB">>}, 19 | {<<"shackle">>, <<"BAF9443E210309594BC683CDE056FFA66AF85FC1D26779164F855E0818ABEC28">>}]} 20 | ]. 21 | -------------------------------------------------------------------------------- /src/statsderl.app.src: -------------------------------------------------------------------------------- 1 | {application, statsderl, [ 2 | {applications, [kernel, stdlib, granderl, shackle]}, 3 | {description, "High Performance Erlang StatsD Client"}, 4 | {env, []}, 5 | {licenses, ["MIT"]}, 6 | {links, [{"GitHub", "https://github.com/lpgauth/statsderl"}]}, 7 | {mod, {statsderl_app, []}}, 8 | {registered, []}, 9 | {vsn, git} 10 | ]}. 11 | -------------------------------------------------------------------------------- /src/statsderl.erl: -------------------------------------------------------------------------------- 1 | -module(statsderl). 2 | -include("statsderl.hrl"). 3 | 4 | -compile(inline). 5 | -compile({inline_size, 512}). 6 | 7 | %% public 8 | -export([ 9 | counter/3, 10 | decrement/3, 11 | gauge/3, 12 | gauge_decrement/3, 13 | gauge_increment/3, 14 | increment/3, 15 | timing/3, 16 | timing_fun/3, 17 | timing_now/3, 18 | timing_now_us/3 19 | ]). 20 | 21 | %% public 22 | -spec counter(key(), value(), sample_rate()) -> 23 | ok. 24 | 25 | counter(Key, Value, Rate) -> 26 | statsderl_sample:rate(Rate, {counter, Key, Value, Rate}). 27 | 28 | -spec decrement(key(), value(), sample_rate()) -> 29 | ok. 30 | 31 | decrement(Key, Value, Rate) when Value >= 0 -> 32 | statsderl_sample:rate(Rate, {counter, Key, -Value, Rate}). 33 | 34 | -spec gauge(key(), value(), sample_rate()) -> 35 | ok. 36 | 37 | gauge(Key, Value, Rate) when Value >= 0 -> 38 | statsderl_sample:rate(Rate, {gauge, Key, Value}). 39 | 40 | -spec gauge_decrement(key(), value(), sample_rate()) -> 41 | ok. 42 | 43 | gauge_decrement(Key, Value, Rate) when Value >= 0 -> 44 | statsderl_sample:rate(Rate, {gauge_decrement, Key, Value}). 45 | 46 | -spec gauge_increment(key(), value(), sample_rate()) -> 47 | ok. 48 | 49 | gauge_increment(Key, Value, Rate) when Value >= 0 -> 50 | statsderl_sample:rate(Rate, {gauge_increment, Key, Value}). 51 | 52 | -spec increment(key(), value(), sample_rate()) -> 53 | ok. 54 | 55 | increment(Key, Value, Rate) when Value >= 0 -> 56 | statsderl_sample:rate(Rate, {counter, Key, Value, Rate}). 57 | 58 | -spec timing(key(), value(), sample_rate()) -> 59 | ok. 60 | 61 | timing(Key, Value, Rate) -> 62 | statsderl_sample:rate(Rate, {timing, Key, Value}). 63 | 64 | -spec timing_fun(key(), fun(), sample_rate()) -> 65 | any(). 66 | 67 | timing_fun(Key, Fun, Rate) -> 68 | Timestamp = statsderl_utils:timestamp(), 69 | Result = Fun(), 70 | timing_now(Key, Timestamp, Rate), 71 | Result. 72 | 73 | -spec timing_now(key(), erlang:timestamp(), sample_rate()) -> 74 | ok. 75 | 76 | timing_now(Key, Timestamp, Rate) -> 77 | statsderl_sample:rate(Rate, {timing_now, Key, Timestamp}). 78 | 79 | -spec timing_now_us(key(), erlang:timestamp(), sample_rate()) -> 80 | ok. 81 | 82 | timing_now_us(Key, Timestamp, Rate) -> 83 | statsderl_sample:rate(Rate, {timing_now_us, Key, Timestamp}). 84 | -------------------------------------------------------------------------------- /src/statsderl_app.erl: -------------------------------------------------------------------------------- 1 | -module(statsderl_app). 2 | -include("statsderl.hrl"). 3 | 4 | -export([ 5 | start/0, 6 | stop/0 7 | ]). 8 | 9 | -behaviour(application). 10 | -export([ 11 | start/2, 12 | stop/1 13 | ]). 14 | 15 | %% public 16 | -spec start() -> 17 | {ok, [atom()]} | {error, term()}. 18 | 19 | start() -> 20 | application:ensure_all_started(?APP). 21 | 22 | -spec stop() -> 23 | ok | {error, {not_started, ?APP}}. 24 | 25 | stop() -> 26 | application:stop(?APP). 27 | 28 | %% application callbacks 29 | -spec start(application:start_type(), term()) -> 30 | {ok, pid()}. 31 | 32 | start(_StartType, _StartArgs) -> 33 | statsderl_sup:start_link(). 34 | 35 | -spec stop(term()) -> 36 | ok. 37 | 38 | stop(_State) -> 39 | shackle_pool:stop(?APP), 40 | ok. 41 | -------------------------------------------------------------------------------- /src/statsderl_client.erl: -------------------------------------------------------------------------------- 1 | -module(statsderl_client). 2 | -include("statsderl.hrl"). 3 | 4 | -compile(inline). 5 | -compile({inline_size, 512}). 6 | 7 | -behavior(shackle_client). 8 | -export([ 9 | init/1, 10 | setup/2, 11 | handle_request/2, 12 | handle_data/2, 13 | terminate/1 14 | ]). 15 | 16 | -record(state, { 17 | base_key :: iodata() 18 | }). 19 | 20 | -type state() :: #state {}. 21 | 22 | %% shackle_server callbacks 23 | -spec init(undefined) -> 24 | {ok, state()}. 25 | 26 | init(_Opts) -> 27 | BaseKey = ?ENV(?ENV_BASEKEY, ?DEFAULT_BASEKEY), 28 | 29 | {ok, #state { 30 | base_key = statsderl_utils:base_key(BaseKey) 31 | }}. 32 | 33 | -spec setup(inet:socket(), state()) -> 34 | {ok, state()}. 35 | 36 | setup(_Socket, State) -> 37 | {ok, State}. 38 | 39 | -spec handle_request(term(), state()) -> 40 | {ok, undefined, iolist(), state()}. 41 | 42 | handle_request({cast, Data}, #state { 43 | base_key = BaseKey 44 | } = State) -> 45 | 46 | {ok, undefined, [BaseKey, Data], State}; 47 | handle_request(Request, #state { 48 | base_key = BaseKey 49 | } = State) -> 50 | 51 | Data = statsderl_protocol:encode(Request), 52 | 53 | {ok, undefined, [BaseKey, Data], State}. 54 | 55 | -spec handle_data(binary(), state()) -> 56 | {ok, [], state()}. 57 | 58 | handle_data(_Data, State) -> 59 | {ok, [], State}. 60 | 61 | -spec terminate(state()) -> ok. 62 | 63 | terminate(_State) -> 64 | ok. 65 | -------------------------------------------------------------------------------- /src/statsderl_protocol.erl: -------------------------------------------------------------------------------- 1 | -module(statsderl_protocol). 2 | -include("statsderl.hrl"). 3 | 4 | -compile(inline). 5 | -compile({inline_size, 512}). 6 | 7 | -export([ 8 | encode/1 9 | ]). 10 | 11 | %% public 12 | -spec encode(operation()) -> iodata(). 13 | 14 | encode({counter, Key, Value, SampleRate}) -> 15 | [Key, <<":">>, format_value(Value), <<"|c">>, 16 | format_sample_rate(SampleRate)]; 17 | encode({gauge, Key, Value}) -> 18 | [Key, <<":">>, format_value(Value), <<"|g">>]; 19 | encode({gauge_decrement, Key, Value}) -> 20 | [Key, <<":-">>, format_value(Value), <<"|g">>]; 21 | encode({gauge_increment, Key, Value}) -> 22 | [Key, <<":+">>, format_value(Value), <<"|g">>]; 23 | encode({timing, Key, Value}) -> 24 | [Key, <<":">>, format_value(Value), <<"|ms">>]. 25 | 26 | %% private 27 | format_sample_rate(SampleRate) when SampleRate >= 1 -> 28 | <<>>; 29 | format_sample_rate(SampleRate) -> 30 | [<<"|@">>, float_to_list(SampleRate, [compact, {decimals, 6}])]. 31 | 32 | format_value(Value) when is_integer(Value) -> 33 | integer_to_list(Value); 34 | format_value(Value) when is_float(Value) -> 35 | float_to_list(Value, [{decimals, 2}]). 36 | -------------------------------------------------------------------------------- /src/statsderl_sample.erl: -------------------------------------------------------------------------------- 1 | -module(statsderl_sample). 2 | -include("statsderl.hrl"). 3 | 4 | -compile(inline). 5 | -compile({inline_size, 512}). 6 | 7 | -export([ 8 | rate/2, 9 | rate_scaled/2 10 | ]). 11 | 12 | %% public 13 | -spec rate(sample_rate(), operation()) -> 14 | ok. 15 | 16 | rate(1, Operation) -> 17 | operation(Operation); 18 | rate(1.0, Operation) -> 19 | operation(Operation); 20 | rate(Rate, Operation) -> 21 | RateInt = trunc(Rate * ?MAX_UNSIGNED_INT_32), 22 | rate_scaled(RateInt, Operation). 23 | 24 | -spec rate_scaled(non_neg_integer(), operation()) -> 25 | ok. 26 | 27 | rate_scaled(RateInt, Operation) -> 28 | Rand = granderl:uniform(?MAX_UNSIGNED_INT_32), 29 | case Rand =< RateInt of 30 | true -> 31 | operation(Operation); 32 | false -> 33 | ok 34 | end. 35 | 36 | %% private 37 | cast(Request) -> 38 | shackle:cast(?APP, Request, undefined), 39 | ok. 40 | 41 | operation({timing_now, Key, Value}) -> 42 | Value2 = statsderl_utils:timing_now(Value), 43 | cast({timing, Key, Value2}); 44 | operation({timing_now_us, Key, Value}) -> 45 | Value2 = statsderl_utils:timing_now_us(Value), 46 | cast({timing, Key, Value2}); 47 | operation(Operation) -> 48 | cast(Operation). 49 | -------------------------------------------------------------------------------- /src/statsderl_sup.erl: -------------------------------------------------------------------------------- 1 | -module(statsderl_sup). 2 | -include("statsderl.hrl"). 3 | 4 | %% public 5 | -export([ 6 | start_link/0 7 | ]). 8 | 9 | -behaviour(supervisor). 10 | -export([ 11 | init/1 12 | ]). 13 | 14 | 15 | %% public 16 | -spec start_link() -> 17 | {ok, pid()}. 18 | 19 | start_link() -> 20 | supervisor:start_link({local, ?MODULE}, ?MODULE, []). 21 | 22 | %% supervisor callbacks 23 | -spec init([]) -> 24 | {ok, {{one_for_one, 5, 10}, []}}. 25 | 26 | init(_Args) -> 27 | BacklogSize = ?ENV(backlog_size, ?DEFAULT_BACKLOG_SIZE), 28 | Hostname = ?ENV(?ENV_HOSTNAME, ?DEFAULT_HOSTNAME), 29 | PoolSize = ?ENV(pool_size, ?DEFAULT_POOL_SIZE), 30 | Port = ?ENV(?ENV_PORT, ?DEFAULT_PORT), 31 | 32 | ClientOpts = [ 33 | {address, Hostname}, 34 | {port, Port}, 35 | {protocol, shackle_udp} 36 | ], 37 | PoolOtps = [ 38 | {backlog_size, BacklogSize}, 39 | {pool_size, PoolSize} 40 | ], 41 | ok = shackle_pool:start(?APP, ?CLIENT, ClientOpts, PoolOtps), 42 | 43 | {ok, {{one_for_one, 5, 10}, []}}. 44 | -------------------------------------------------------------------------------- /src/statsderl_transform.erl: -------------------------------------------------------------------------------- 1 | -module(statsderl_transform). 2 | -include("statsderl.hrl"). 3 | 4 | -export([ 5 | parse_transform/2 6 | ]). 7 | 8 | -define(ATOM(Atom), {atom, 0, Atom}). 9 | -define(BIN_ELEMENT(String), {bin_element, 0, String, default, default}). 10 | -define(BINARY(String), {bin, 0, [?BIN_ELEMENT(?STRING(String))]}). 11 | -define(INTEGER(Int), {integer, 0, Int}). 12 | -define(OP_NEGATIVE(X), {op, 0, '-', X}). 13 | -define(STRING(String), {string, 0, String}). 14 | -define(TUPLE(List), {tuple, 0, List}). 15 | 16 | -type forms() :: [erl_parse:abstract_form() | erl_parse:form_info()]. 17 | 18 | %% public 19 | -spec parse_transform(forms(), [compile:option()]) -> 20 | forms(). 21 | 22 | parse_transform(Forms, _Options) -> 23 | parse_trans:plain_transform(fun do_transform/1, Forms). 24 | 25 | do_transform({call, _, {_, _, {_, _, ?APP}, Function}, _} = F) -> 26 | replace(safe_normalize(Function), F); 27 | do_transform(_Form) -> 28 | continue. 29 | 30 | %% private 31 | call(Function, Arg1, Arg2) -> 32 | {call, 0, {remote, 0, 33 | {atom, 0, statsderl_sample}, 34 | {atom, 0, Function}}, [Arg1, Arg2] 35 | }. 36 | 37 | encode(Operation) -> 38 | Encoded = statsderl_protocol:encode(Operation), 39 | binary_to_list(iolist_to_binary(Encoded)). 40 | 41 | not_undefined([]) -> 42 | true; 43 | not_undefined([undefined | _]) -> 44 | false; 45 | not_undefined([_ | T]) -> 46 | not_undefined(T). 47 | 48 | op_code(increment) -> 49 | counter; 50 | op_code(decrement) -> 51 | counter; 52 | op_code(Function) -> 53 | Function. 54 | 55 | packet(counter, Key, Value, Rate) -> 56 | Key2 = safe_normalize(Key), 57 | Value2 = safe_normalize(Value), 58 | Rate2 = safe_normalize(Rate), 59 | 60 | case not_undefined([Key2, Value2, Rate2]) of 61 | true -> 62 | encode({counter, Key2, Value2, Rate2}); 63 | false -> 64 | undefined 65 | end; 66 | packet(OpCode, Key, Value, _Rate) -> 67 | Key2 = safe_normalize(Key), 68 | Value2 = safe_normalize(Value), 69 | 70 | case not_undefined([Key2, Value2]) of 71 | true -> 72 | encode({OpCode, Key2, Value2}); 73 | false -> 74 | undefined 75 | end. 76 | 77 | rate_scaled({float, _, RateValue}) -> 78 | trunc(RateValue * ?MAX_UNSIGNED_INT_32); 79 | rate_scaled({integer, _, RateValue}) -> 80 | trunc(RateValue * ?MAX_UNSIGNED_INT_32); 81 | rate_scaled(_) -> 82 | undefined. 83 | 84 | replace(timing_fun, F) -> 85 | F; 86 | replace(Function, {_, _, _, [Key, Value, Rate]} = F) -> 87 | RateScaled = rate_scaled(Rate), 88 | OpCode = op_code(Function), 89 | Value2 = value(Function, Value), 90 | Packet = packet(OpCode, Key, Value2, Rate), 91 | 92 | case {RateScaled, Packet} of 93 | {undefined, undefined} -> 94 | F; 95 | {_, undefined} -> 96 | case OpCode of 97 | counter -> 98 | sample_scaled(RateScaled, 99 | ?TUPLE([?ATOM(OpCode), Key, Value2, Rate])); 100 | _ -> 101 | sample_scaled(RateScaled, 102 | ?TUPLE([?ATOM(OpCode), Key, Value2])) 103 | end; 104 | {undefined, _} -> 105 | sample(Rate, 106 | ?TUPLE([?ATOM(cast), ?BINARY(Packet)])); 107 | _ -> 108 | sample_scaled(RateScaled, 109 | ?TUPLE([?ATOM(cast), ?BINARY(Packet)])) 110 | end. 111 | 112 | sample(Rate, Arguments) -> 113 | call(rate, Rate, Arguments). 114 | 115 | sample_scaled(RateScaled, Arguments) -> 116 | call(rate_scaled, ?INTEGER(RateScaled), Arguments). 117 | 118 | safe_normalize(AbsTerm) -> 119 | try erl_parse:normalise(AbsTerm) 120 | catch 121 | _:_ -> 122 | undefined 123 | end. 124 | 125 | value(decrement, Value) -> 126 | ?OP_NEGATIVE(Value); 127 | value(_, Value) -> 128 | Value. 129 | -------------------------------------------------------------------------------- /src/statsderl_utils.erl: -------------------------------------------------------------------------------- 1 | -module(statsderl_utils). 2 | -include("statsderl.hrl"). 3 | 4 | -compile(inline). 5 | -compile({inline_size, 512}). 6 | 7 | -export([ 8 | base_key/1, 9 | error_msg/2, 10 | timestamp/0, 11 | timing_now/1, 12 | timing_now_us/1 13 | ]). 14 | 15 | %% public 16 | -spec base_key(base_key()) -> 17 | iodata(). 18 | 19 | base_key(hostname) -> 20 | [hostname(), $.]; 21 | base_key(name) -> 22 | [name(), $.]; 23 | base_key(sname) -> 24 | [sname(), $.]; 25 | base_key(undefined) -> 26 | ""; 27 | base_key(Key) when is_binary(Key) -> 28 | [Key, $.]; 29 | base_key([]) -> 30 | []; 31 | base_key([H | T] = Key) -> 32 | case io_lib:printable_unicode_list(Key) of 33 | true -> 34 | [Key, $.]; 35 | false -> 36 | [base_key(H) | base_key(T)] 37 | end. 38 | 39 | -spec error_msg(string(), [term()]) -> 40 | ok. 41 | 42 | error_msg(Format, Data) -> 43 | error_logger:error_msg("[statsderl] " ++ Format, Data). 44 | 45 | -spec timestamp() -> 46 | erlang:timestamp(). 47 | 48 | timestamp() -> 49 | os:timestamp(). 50 | 51 | -spec timing_now(erlang:timestamp()) -> 52 | non_neg_integer(). 53 | 54 | timing_now(Timestamp) -> 55 | timing_now_us(Timestamp) div 1000. 56 | 57 | -spec timing_now_us(erlang:timestamp()) -> 58 | non_neg_integer(). 59 | 60 | timing_now_us(Timestamp) -> 61 | timer:now_diff(statsderl_utils:timestamp(), Timestamp). 62 | 63 | %% private 64 | hostname() -> 65 | {ok, Hostname} = inet:gethostname(), 66 | Hostname. 67 | 68 | name() -> 69 | Name = atom_to_list(node()), 70 | re:replace(Name, "@", ".", [global, {return, list}]). 71 | 72 | sname() -> 73 | string:sub_word(atom_to_list(node()), 1, $@). 74 | -------------------------------------------------------------------------------- /test/statsderl_profile.erl: -------------------------------------------------------------------------------- 1 | -module(statsderl_profile). 2 | 3 | -compile({parse_transform, statsderl_transform}). 4 | 5 | -export([ 6 | fprofx/0 7 | ]). 8 | 9 | -define(N, 10000). 10 | -define(P, 20). 11 | 12 | %% public 13 | -spec fprofx() -> ok. 14 | 15 | fprofx() -> 16 | statsderl_app:start(), 17 | [counter() || _ <- lists:seq(1, 100)], 18 | statsderl_app:stop(), 19 | 20 | fprofx:start(), 21 | {ok, Tracer} = fprofx:profile(start), 22 | fprofx:trace([start, {procs, new}, {tracer, Tracer}]), 23 | 24 | {ok, [statsderl]} = statsderl_app:start(), 25 | timer:sleep(250), 26 | 27 | Self = self(), 28 | [spawn(fun () -> 29 | counter(), 30 | Self ! exit 31 | end) || _ <- lists:seq(1, ?P)], 32 | wait(), 33 | 34 | fprofx:trace(stop), 35 | fprofx:analyse([totals, {dest, ""}]), 36 | fprofx:stop(), 37 | application:stop(statsderl), 38 | 39 | ok. 40 | 41 | %% private 42 | counter() -> 43 | [statsderl:counter(["test", <<".test">>], 5, 0.25) || 44 | _ <- lists:seq(1, ?N)]. 45 | 46 | wait() -> 47 | wait(?P). 48 | 49 | wait(0) -> 50 | ok; 51 | wait(X) -> 52 | receive 53 | exit -> 54 | wait(X - 1) 55 | end. 56 | -------------------------------------------------------------------------------- /test/statsderl_tests.erl: -------------------------------------------------------------------------------- 1 | -module(statsderl_tests). 2 | -include_lib("statsderl/include/statsderl.hrl"). 3 | -include_lib("eunit/include/eunit.hrl"). 4 | 5 | %% tests 6 | statsderl_base_key_test() -> 7 | assert_base_key("base_key", <<"base_key.">>), 8 | assert_base_key(<<"base_key">>, <<"base_key.">>). 9 | 10 | statsderl_test_() -> 11 | {setup, 12 | fun () -> setup() end, 13 | fun (Socket) -> cleanup(Socket) end, 14 | {with, [ 15 | fun counter_subtest/1, 16 | fun decrement_subtest/1, 17 | fun gauge_decrement_subtest/1, 18 | fun gauge_increment_subtest/1, 19 | fun gauge_subtest/1, 20 | fun increment_subtest/1, 21 | fun sampling_rate_subtest/1, 22 | fun timing_fun_subtest/1, 23 | fun timing_subtest/1, 24 | fun timing_now_subtest/1, 25 | fun timing_now_us_subtest/1 26 | ]} 27 | }. 28 | 29 | %% subtests 30 | counter_subtest(Socket) -> 31 | statsderl:counter("test", 1.123, 1), 32 | assert_packet(Socket, <<"test:1.12|c">>). 33 | 34 | decrement_subtest(Socket) -> 35 | statsderl:decrement("test", 1, 1.0), 36 | assert_packet(Socket, <<"test:-1|c">>). 37 | 38 | gauge_subtest(Socket) -> 39 | statsderl:gauge("test", 1, 1), 40 | assert_packet(Socket, <<"test:1|g">>). 41 | 42 | gauge_decrement_subtest(Socket) -> 43 | statsderl:gauge_decrement("test", 1, 1), 44 | assert_packet(Socket, <<"test:-1|g">>). 45 | 46 | gauge_increment_subtest(Socket) -> 47 | statsderl:gauge_increment("test", 1, 1), 48 | assert_packet(Socket, <<"test:+1|g">>). 49 | 50 | increment_subtest(Socket) -> 51 | statsderl:increment("test", 1, 1), 52 | assert_packet(Socket, <<"test:1|c">>). 53 | 54 | sampling_rate_subtest(Socket) -> 55 | meck:new(granderl, [passthrough, no_history]), 56 | meck:expect(granderl, uniform, fun 57 | (4) -> 2; 58 | (?MAX_UNSIGNED_INT_32) -> 1 59 | end), 60 | statsderl:counter("test", 1, 0.1234), 61 | assert_packet(Socket, <<"test:1|c|@0.1234">>), 62 | meck:expect(granderl, uniform, fun 63 | (4) -> 2; 64 | (?MAX_UNSIGNED_INT_32) -> ?MAX_UNSIGNED_INT_32 65 | end), 66 | statsderl:counter("test", 1, 0.1234), 67 | meck:unload(granderl). 68 | 69 | timing_fun_subtest(Socket) -> 70 | meck:new(statsderl_utils, [passthrough, no_history]), 71 | Seq = meck:loop([{1448, 573975, 400000}, {1448, 573975, 500000}]), 72 | meck:expect(statsderl_utils, timestamp, [], Seq), 73 | TestResult = "testresult", 74 | Result = statsderl:timing_fun("test", fun () -> TestResult end, 1), 75 | assert_packet(Socket, <<"test:100|ms">>), 76 | ?assertEqual(Result, TestResult), 77 | meck:unload(statsderl_utils). 78 | 79 | timing_subtest(Socket) -> 80 | statsderl:timing("test", 1, 1), 81 | assert_packet(Socket, <<"test:1|ms">>). 82 | 83 | timing_now_subtest(Socket) -> 84 | meck:new(statsderl_utils, [passthrough, no_history]), 85 | Seq = meck:loop([{1448, 573976, 400000}, {1448, 573976, 500000}]), 86 | meck:expect(statsderl_utils, timestamp, [], Seq), 87 | statsderl:timing_now("test", statsderl_utils:timestamp(), 1), 88 | assert_packet(Socket, <<"test:100|ms">>), 89 | meck:unload(statsderl_utils). 90 | 91 | timing_now_us_subtest(Socket) -> 92 | meck:new(statsderl_utils, [passthrough, no_history]), 93 | Seq = meck:loop([{1448, 573977, 400000}, {1448, 573977, 500000}]), 94 | meck:expect(statsderl_utils, timestamp, [], Seq), 95 | statsderl:timing_now_us("test", statsderl_utils:timestamp(), 1), 96 | assert_packet(Socket, <<"test:100000|ms">>), 97 | meck:unload(statsderl_utils). 98 | 99 | %% helpers 100 | assert_base_key(BaseKey, Expected) -> 101 | Socket = setup([{?ENV_BASEKEY, BaseKey}]), 102 | statsderl:counter("test", 1, 1), 103 | {ok, {_Address, _Port, Packet}} = gen_udp:recv(Socket, 0), 104 | ?assertEqual(<>, Packet), 105 | cleanup(Socket). 106 | 107 | assert_packet(Socket, Expected) -> 108 | {ok, {_Address, _Port, Packet}} = gen_udp:recv(Socket, 0), 109 | ?assertEqual(Expected, Packet). 110 | 111 | cleanup(Socket) -> 112 | ok = gen_udp:close(Socket), 113 | statsderl_app:stop(). 114 | 115 | setup() -> 116 | setup([]). 117 | 118 | setup(EnvVars) -> 119 | error_logger:tty(false), 120 | application:load(?APP), 121 | [application:unset_env(?APP, K) || K <- ?ENV_VARS], 122 | [application:set_env(?APP, K, V) || {K, V} <- EnvVars], 123 | {ok, Socket} = gen_udp:open(?DEFAULT_PORT, [binary, {active, false}]), 124 | timer:sleep(100), 125 | statsderl_app:start(), 126 | timer:sleep(1000), 127 | Socket. 128 | -------------------------------------------------------------------------------- /test/statsderl_transform_tests.erl: -------------------------------------------------------------------------------- 1 | -module(statsderl_transform_tests). 2 | -include_lib("statsderl/include/statsderl.hrl"). 3 | -include_lib("eunit/include/eunit.hrl"). 4 | 5 | -compile({parse_transform, statsderl_transform}). 6 | 7 | %% tests 8 | statsderl_test_() -> 9 | {setup, 10 | fun () -> setup() end, 11 | fun (Socket) -> cleanup(Socket) end, 12 | {with, [ 13 | fun counter_subtest/1, 14 | fun decrement_subtest/1, 15 | fun gauge_subtest/1, 16 | fun increment_subtest/1, 17 | fun timing_fun_subtest/1 18 | ]} 19 | }. 20 | 21 | %% subtests 22 | counter_subtest(Socket) -> 23 | A = "test", 24 | B = 5, 25 | C = 1, 26 | statsderl:counter("test", 1, 1), 27 | assert_packet(Socket, <<"test:1|c">>), 28 | statsderl:counter(["test", A], 1, 1), 29 | assert_packet(Socket, <<"testtest:1|c">>), 30 | statsderl:counter("test", B, 1), 31 | assert_packet(Socket, <<"test:5|c">>), 32 | statsderl:counter("test", 1, C), 33 | assert_packet(Socket, <<"test:1|c">>). 34 | 35 | decrement_subtest(Socket) -> 36 | statsderl:decrement("test", 1, 1.0), 37 | assert_packet(Socket, <<"test:-1|c">>). 38 | 39 | gauge_subtest(Socket) -> 40 | A = "test", 41 | B = 5, 42 | C = 1, 43 | statsderl:gauge("test", 1, 1), 44 | assert_packet(Socket, <<"test:1|g">>), 45 | statsderl:gauge(["test", A], 1, 1), 46 | assert_packet(Socket, <<"testtest:1|g">>), 47 | statsderl:gauge("test", B, 1), 48 | assert_packet(Socket, <<"test:5|g">>), 49 | statsderl:gauge("test", 1, C), 50 | assert_packet(Socket, <<"test:1|g">>). 51 | 52 | increment_subtest(Socket) -> 53 | statsderl:increment("test", 1, 1), 54 | assert_packet(Socket, <<"test:1|c">>). 55 | 56 | timing_fun_subtest(Socket) -> 57 | meck:new(statsderl_utils, [passthrough, no_history]), 58 | Seq = meck:loop([{1448, 573975, 400000}, {1448, 573975, 500000}]), 59 | meck:expect(statsderl_utils, timestamp, [], Seq), 60 | statsderl:timing_fun("test", fun () -> ok end, 1), 61 | assert_packet(Socket, <<"test:100|ms">>), 62 | meck:unload(statsderl_utils). 63 | 64 | %% helpers 65 | assert_packet(Socket, Expected) -> 66 | {ok, {_Address, _Port, Packet}} = gen_udp:recv(Socket, 0), 67 | ?assertEqual(Expected, Packet). 68 | 69 | cleanup(Socket) -> 70 | ok = gen_udp:close(Socket), 71 | statsderl_app:stop(). 72 | 73 | setup() -> 74 | error_logger:tty(false), 75 | application:load(?APP), 76 | {ok, Socket} = gen_udp:open(?DEFAULT_PORT, [binary, {active, false}]), 77 | timer:sleep(100), 78 | statsderl_app:start(), 79 | timer:sleep(1000), 80 | Socket. 81 | -------------------------------------------------------------------------------- /test/statsderl_utils_tests.erl: -------------------------------------------------------------------------------- 1 | -module(statsderl_utils_tests). 2 | -include_lib("statsderl/include/statsderl.hrl"). 3 | -include_lib("eunit/include/eunit.hrl"). 4 | 5 | base_key_test() -> 6 | meck:new(inet, [passthrough, no_history, unstick]), 7 | meck:expect(inet, gethostname, fun () -> 8 | {ok, "myhost"} 9 | end), 10 | 11 | assert_base_key(name, <<"nonode.nohost.">>), 12 | assert_base_key(sname, <<"nonode.">>), 13 | assert_base_key(undefined, <<"">>), 14 | assert_base_key(["hello", <<"world">>], <<"hello.world.">>), 15 | assert_base_key(["rtb", "gateway", hostname], <<"rtb.gateway.myhost.">>), 16 | 17 | meck:unload(inet). 18 | 19 | %% private 20 | assert_base_key(BaseKey, Expected) -> 21 | BaseKey2 = iolist_to_binary(statsderl_utils:base_key(BaseKey)), 22 | ?assertEqual(Expected, BaseKey2). 23 | --------------------------------------------------------------------------------