├── .github └── workflows │ ├── publish.yml │ └── test.yml ├── .gitignore ├── Changelog.md ├── LICENSE.txt ├── Makefile ├── Readme.md ├── files ├── app.config └── influxdb.conf ├── include ├── influx_udp.hrl └── influx_udp_priv.hrl ├── rebar.config ├── rebar.lock ├── src ├── influx_line.erl ├── influx_udp.app.src ├── influx_udp.erl ├── influx_udp_app.erl ├── influx_udp_sup.erl └── influx_udp_worker.erl └── test ├── influx_udp_test.erl └── test_udp_server.erl /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | publish: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Check out 13 | uses: actions/checkout@v2 14 | 15 | - name: Publish to Hex.pm 16 | uses: erlangpack/github-action@v1 17 | env: 18 | HEX_API_KEY: ${{ secrets.HEX_API_KEY }} 19 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | name: OTP ${{matrix.otp}} 13 | strategy: 14 | matrix: 15 | otp: [20.3, 21.3, 22.1, 23.1] 16 | include: 17 | - otp: '20.3' 18 | rebar: '3.15.2' 19 | - otp: '21.3' 20 | rebar: '3.15.2' 21 | - otp: '22.1' 22 | rebar: '3.17.0' 23 | - otp: '23.1' 24 | rebar: '3.17.0' 25 | fail-fast: false 26 | steps: 27 | - uses: actions/checkout@v2.0.0 28 | - uses: ErlGang/setup-erlang@master 29 | with: 30 | otp-version: ${{matrix.otp}} 31 | - uses: actions/cache@v2 32 | with: 33 | path: ~/.cache/rebar3 34 | key: ${{runner.os}}-${{matrix.otp}}-${{hashFiles('rebar.config')}} 35 | - run: curl -LO https://github.com/erlang/rebar3/releases/download/${{matrix.rebar}}/rebar3 36 | - run: chmod +x rebar3 37 | - run: ./rebar3 do compile, dialyzer, eunit, ct 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.DS_Store 3 | *.log 4 | *.cookie 5 | log 6 | *~ 7 | *.sublime-project 8 | *.sublime-workspace 9 | 10 | # XCode noise 11 | *.swp 12 | *~.nib 13 | build/ 14 | *.pbxuser 15 | *.perspective 16 | *.perspectivev3 17 | *.mode1v3 18 | *.mode2v3 19 | ff/ 20 | 21 | # IDEA files 22 | .idea/* 23 | *.iml 24 | deps 25 | out 26 | # erlang binaries and release 27 | 28 | ebin/ 29 | *.beam 30 | .eunit 31 | *.dump 32 | _build/ 33 | doc/ 34 | -------------------------------------------------------------------------------- /Changelog.md: -------------------------------------------------------------------------------- 1 | # Change log 2 | 3 | ## master 4 | 5 | ## 1.1.2 (2021-10-30) 🎃 6 | 7 | - Fix type specs. 8 | 9 | ## 1.1.1 (2020-12-29) 10 | 11 | - Fix Erlang Logger compatibility. 12 | 13 | ## 1.1.0 (2020-08-17) 14 | 15 | - Drop Lager dependency. 16 | 17 | Now standard Logger or `error_logger` (for OTP <21) is used instead. 18 | 19 | [PR](https://github.com/palkan/influx_udp/pull/16). 20 | 21 | ## 1.0.0 (2019-01-06) 22 | 23 | [Hex package](https://hex.pm/packages/influx_udp) is available 🎉. 24 | 25 | - Default pool behaviour changed. 26 | 27 | Previously, the default configuration had both `influx_host` and `influx_port` defined, which made 28 | the application start the _default_ pool connecting to `127.0.0.1:4444` (which didn't make a lot of sense, did it?). 29 | 30 | We also had a special `{default_pool, false}` configuration variable to disable the default pool. 31 | 32 | We removed the default value for `influx_host` (and changed the default `influx_port` to **8089**). 33 | 34 | Now if `influx_host` is not defined, we do not start the default pool. 35 | 36 | ## 0.9.1 (2015-06-14) 37 | 38 | - Add Line encoder 39 | 40 | ## 0.9.0 (2015-06-14) 41 | 42 | - Add support for InfluxDB 0.9 (tags, line protocol) 43 | 44 | ## 0.8.0 (2015-06-13) 45 | 46 | - Add pools functionality 47 | 48 | Major and minor versions now follows InfluxDB version. 49 | 50 | ## 0.1.0 (2014-12-17) 51 | 52 | - Basic functionality. 53 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014-2019 Vladimir Dementyev 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | REBAR=./rebar 2 | 3 | run-influxdb: 4 | docker run --rm \ 5 | -p 8086:8086 -p 8089:8089/udp \ 6 | -v ${PWD}/files/influxdb.conf:/etc/influxdb/influxdb.conf:ro \ 7 | -e INFLUXDB_DB=db \ 8 | influxdb:latest 9 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | ![Build](https://github.com/palkan/influx_udp/workflows/Test/badge.svg) 2 | [![Hex Version](https://img.shields.io/hexpm/v/influx_udp.svg)](https://hex.pm/packages/influx_udp) 3 | 4 | # Erlang InfluxDB UDP Writer 5 | 6 | Write data to [InfluxDB](http://influxdb.com) (>= 0.9) via UDP (see [InfluxDB docs](https://docs.influxdata.com/influxdb/v1.7/supported_protocols/udp/)). 7 | 8 | **Erlang/OTP version:** >=17.1 9 | 10 | ## Setup 11 | 12 | rebar.config 13 | ```erlang 14 | %% using Hex 15 | {deps, [ 16 | influx_udp 17 | ]}. 18 | 19 | %% from source 20 | {deps, [ 21 | {influx_udp, ".*", {git, "https://github.com/palkan/influx_udp.git", "master"}} 22 | ]}. 23 | ``` 24 | 25 | app.config 26 | ```erlang 27 | [ 28 | {influx_udp, 29 | [ 30 | {influx_host, '127.0.0.1'}, 31 | {influx_port, 8089}, 32 | {pool_size, 5}, %% defaults to 3 33 | {max_overflow, 10} %% defaults to 1 34 | ] 35 | } 36 | ]. 37 | ``` 38 | 39 | ## Usage 40 | 41 | First, you need to start the application: 42 | 43 | ```erlang 44 | influx_udp:start(). 45 | ``` 46 | 47 | Now you can create _pools_ and write data to InfluxDB. 48 | 49 | ## Pools 50 | 51 | The default pool is started on application start if you specified `influx_host` and `influx_port` in the configuration file (see above). 52 | 53 | You can run pools manually: 54 | 55 | ```erlang 56 | influx_udp:start_pool(my_pool, #{ host => 'yet.another.influx.host' }). 57 | ``` 58 | 59 | Options not specified in `influx_udp:start_pool/2` would be taken from the default configuration. 60 | 61 | ## Writing data 62 | 63 | ```erlang 64 | 65 | %% Writing to the named pool with tags 66 | influx_udp:write_to( 67 | my_pool, 68 | Series::string()|atom()|binary(), Points::list(map())|list(proplists:proplist())|map()|proplists:proplist(), 69 | Tags::proplists:proplist()|map()). 70 | 71 | influx_udp:write_to(my_pool, "cpu", [{value, 88}], [{host, 'eu-west'}]). 72 | 73 | %% Writing to default pool 74 | influx_udp:write("cpu", [#{value => 88}, #{value => 22}, #{value => 33}], [{host, 'eu-west'}]). 75 | 76 | %% Writing data with time 77 | influx_udp:write("cpu", #{value => 88}, #{host => 'eu-west'}, 1434055562000000000). 78 | 79 | %% or with current time 80 | influx_udp:write("cpu", #{value => 88}, #{host => 'eu-west'}, true). 81 | 82 | %% Writing to default pool without tags 83 | influx_udp:write(Series, Points). 84 | 85 | %% Writing raw valid InfluxDB input data 86 | influx_udp:write(Data::binary()). 87 | 88 | %% or 89 | influx_udp:write_to(my_pool, Data::binary()). 90 | 91 | %% Write Influx-valid map or proplist 92 | influx_udp:write(#{ measurement => test, fields => #{ val => 1} }). 93 | 94 | %% or many points 95 | influx_udp:write( 96 | #{ measurement => test, fields => #{ val => 1} }, 97 | #{ measurement => test2, fields => #{ val => 2}, tags => { host => test}} 98 | ) 99 | ``` 100 | 101 | ## Encoder 102 | 103 | Module `influx_line` provides methods to encode erlang terms to Line protocol. 104 | Encoder automatically sets timestamps (unique) when encoding list of points (see below). 105 | 106 | ```erlang 107 | 108 | %% convert map or proplist to line 109 | influx_line:encode(#{ measurement => test, fields => #{ val => 1} }). 110 | 111 | #=> <<"test val=1">> 112 | 113 | %% convert list of points to lines 114 | influx_line:encode([ 115 | #{ measurement => test, fields => #{ val => 1} }, 116 | #{ measurement => test2, fields => #{ val => 2}, tags => { host => test}} 117 | ]). 118 | 119 | #=> <<"test val=1 1434305562895000000\ntest2,host=test val=2 1434305562895000001">> 120 | 121 | %% convert any map/proplist to line 122 | influx_line:encode(test, #{ val => 1}). 123 | 124 | #=> <<"test val=1">> 125 | 126 | %% convert many points with the same measurement and tags to line 127 | influx_line:encode(test, [#{ val => 1}, #{ val => 2}], #{ host => test}, 100). 128 | 129 | #=> <<"test,host=test val=1 100\ntest,host=test val=2 101\n">> 130 | ``` 131 | 132 | ## Contributing 133 | 134 | Bug reports and pull requests are welcome on GitHub at https://github.com/palkan/influx_udp. 135 | 136 | ## License 137 | 138 | The library is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). 139 | -------------------------------------------------------------------------------- /files/app.config: -------------------------------------------------------------------------------- 1 | [ 2 | {influx_udp, 3 | [ 4 | {influx_host, 'localhost'}, 5 | {influx_port, 8089}, 6 | {pool_size, 5}, 7 | {debug, true} 8 | ] 9 | }, 10 | {sasl, [{sasl_error_logger, false}]}, 11 | {lager, [ 12 | {handlers, [ 13 | {lager_console_backend, debug} 14 | ]} 15 | ] 16 | }, 17 | {kernel, 18 | [ 19 | {logger_level, debug}, 20 | {logger, 21 | [{handler, default, logger_std_h, 22 | #{formatter => {logger_formatter, #{single_line => true}}}} 23 | ] 24 | } 25 | ] 26 | } 27 | ]. 28 | -------------------------------------------------------------------------------- /files/influxdb.conf: -------------------------------------------------------------------------------- 1 | reporting-disabled = false 2 | bind-address = "127.0.0.1:8088" 3 | 4 | [meta] 5 | dir = "/var/lib/influxdb/meta" 6 | retention-autocreate = true 7 | logging-enabled = true 8 | 9 | [data] 10 | dir = "/var/lib/influxdb/data" 11 | index-version = "inmem" 12 | wal-dir = "/var/lib/influxdb/wal" 13 | wal-fsync-delay = "0s" 14 | validate-keys = false 15 | query-log-enabled = true 16 | cache-max-memory-size = 1073741824 17 | cache-snapshot-memory-size = 26214400 18 | cache-snapshot-write-cold-duration = "10m0s" 19 | compact-full-write-cold-duration = "4h0m0s" 20 | compact-throughput = 50331648 21 | compact-throughput-burst = 50331648 22 | max-series-per-database = 1000000 23 | max-values-per-tag = 100000 24 | max-concurrent-compactions = 0 25 | max-index-log-file-size = 1048576 26 | trace-logging-enabled = false 27 | tsm-use-madv-willneed = false 28 | 29 | [coordinator] 30 | write-timeout = "10s" 31 | max-concurrent-queries = 0 32 | query-timeout = "0s" 33 | log-queries-after = "0s" 34 | max-select-point = 0 35 | max-select-series = 0 36 | max-select-buckets = 0 37 | 38 | [retention] 39 | enabled = true 40 | check-interval = "30m0s" 41 | 42 | [shard-precreation] 43 | enabled = true 44 | check-interval = "10m0s" 45 | advance-period = "30m0s" 46 | 47 | [monitor] 48 | store-enabled = true 49 | store-database = "_internal" 50 | store-interval = "10s" 51 | 52 | [subscriber] 53 | enabled = true 54 | http-timeout = "30s" 55 | insecure-skip-verify = false 56 | ca-certs = "" 57 | write-concurrency = 40 58 | write-buffer-size = 1000 59 | 60 | [http] 61 | enabled = true 62 | bind-address = ":8086" 63 | auth-enabled = false 64 | log-enabled = true 65 | suppress-write-log = false 66 | write-tracing = false 67 | flux-enabled = false 68 | pprof-enabled = true 69 | debug-pprof-enabled = false 70 | https-enabled = false 71 | https-certificate = "/etc/ssl/influxdb.pem" 72 | https-private-key = "" 73 | max-row-limit = 0 74 | max-connection-limit = 0 75 | shared-secret = "" 76 | realm = "InfluxDB" 77 | unix-socket-enabled = false 78 | unix-socket-permissions = "0777" 79 | bind-socket = "/var/run/influxdb.sock" 80 | max-body-size = 25000000 81 | access-log-path = "" 82 | max-concurrent-write-limit = 0 83 | max-enqueued-write-limit = 0 84 | enqueued-write-timeout = 30000000000 85 | 86 | [logging] 87 | format = "auto" 88 | level = "info" 89 | suppress-logo = false 90 | 91 | [[graphite]] 92 | enabled = false 93 | bind-address = ":2003" 94 | database = "graphite" 95 | retention-policy = "" 96 | protocol = "tcp" 97 | batch-size = 5000 98 | batch-pending = 10 99 | batch-timeout = "1s" 100 | consistency-level = "one" 101 | separator = "." 102 | udp-read-buffer = 0 103 | 104 | [[collectd]] 105 | enabled = false 106 | bind-address = ":25826" 107 | database = "collectd" 108 | retention-policy = "" 109 | batch-size = 5000 110 | batch-pending = 10 111 | batch-timeout = "10s" 112 | read-buffer = 0 113 | typesdb = "/usr/share/collectd/types.db" 114 | security-level = "none" 115 | auth-file = "/etc/collectd/auth_file" 116 | parse-multivalue-plugin = "split" 117 | 118 | [[opentsdb]] 119 | enabled = false 120 | bind-address = ":4242" 121 | database = "opentsdb" 122 | retention-policy = "" 123 | consistency-level = "one" 124 | tls-enabled = false 125 | certificate = "/etc/ssl/influxdb.pem" 126 | batch-size = 1000 127 | batch-pending = 5 128 | batch-timeout = "1s" 129 | log-point-errors = true 130 | 131 | [[udp]] 132 | enabled = true 133 | bind-address = ":8089" 134 | database = "db" 135 | retention-policy = "" 136 | batch-size = 1 137 | batch-pending = 1 138 | read-buffer = 0 139 | batch-timeout = "1s" 140 | precision = "" 141 | 142 | [continuous_queries] 143 | log-enabled = true 144 | enabled = true 145 | query-stats-enabled = false 146 | run-interval = "1s" 147 | 148 | [tls] 149 | min-version = "" 150 | max-version = "" 151 | 152 | -------------------------------------------------------------------------------- /include/influx_udp.hrl: -------------------------------------------------------------------------------- 1 | -type(key() ::atom()|binary()|string()). 2 | 3 | -type(influx_data_point() ::map()|list({key(), any()})). 4 | -type(influx_data_points() ::influx_data_point()|list(influx_data_point())). 5 | 6 | -export_type([influx_data_point/0, influx_data_points/0]). -------------------------------------------------------------------------------- /include/influx_udp_priv.hrl: -------------------------------------------------------------------------------- 1 | -define(APP, influx_udp). 2 | -define(Config(X,Y), ulitos_app:get_var(?APP, X, Y)). 3 | 4 | 5 | %% log functions 6 | 7 | -ifdef(OLD_LOGGER). 8 | 9 | -ifdef(TEST). 10 | 11 | -define(D(X), error_logger:info_msg("[DEBUG] ~p:~p ~p~n",[?MODULE, ?LINE, X])). 12 | -define(I(X), error_logger:info_msg("[INFO] ~p:~p ~p~n",[?MODULE, ?LINE, X])). 13 | -define(E(X), error_logger:info_msg("[ERROR] ~p:~p ~p~n",[?MODULE, ?LINE, X])). 14 | 15 | -else. 16 | 17 | -define(D(X), begin _ = X end). 18 | -define(I(X), error_logger:info_msg("~p:~p ~p",[?MODULE, ?LINE, X])). 19 | -define(E(X), error_logger:error_msg("~p:~p ~p",[?MODULE, ?LINE, X])). 20 | 21 | -endif. 22 | 23 | -else. % Use Erlang/OTP 21+ Logger. 24 | 25 | -include_lib("kernel/include/logger.hrl"). 26 | 27 | -define(D(X), ?LOG_DEBUG(X)). 28 | -define(I(X), ?LOG_INFO(X)). 29 | -define(E(X), ?LOG_ERROR(X)). 30 | 31 | -endif. 32 | -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | %% -*- mode: Erlang; -*- 2 | 3 | {require_otp_vsn, "18"}. 4 | {deps, [ 5 | ulitos, 6 | poolboy 7 | ]}. 8 | 9 | {profiles, [ 10 | {prod, [ 11 | {erl_opts, [warn_unused_vars, warnings_as_errors]} 12 | ]}, 13 | {test, [ 14 | {deps, [eunit_formatters]}, 15 | {erl_opts, [debug_info]}, 16 | {eunit_opts, [no_tty, {report, {eunit_progress, [colored, profile]}}]} 17 | ]} 18 | ]}. 19 | 20 | {plugins, [rebar3_hex]}. 21 | 22 | {erl_opts, [debug_info, {platform_define, "^(18|19|20)\.", 'OLD_LOGGER'}]}. 23 | 24 | {cover_enabled, true}. 25 | 26 | {eunit_opts, [ 27 | no_tty, 28 | {report, {eunit_progress, [colored, profile]}} 29 | ]}. 30 | 31 | {shell, [{config, "files/app.config"}, {apps, [ulitos, poolboy, influx_udp]}]}. 32 | 33 | {clean_files, ["*.eunit", "ebin/*.beam"]}. 34 | -------------------------------------------------------------------------------- /rebar.lock: -------------------------------------------------------------------------------- 1 | {"1.1.0", 2 | [{<<"poolboy">>,{pkg,<<"poolboy">>,<<"1.5.2">>},0}, 3 | {<<"ulitos">>,{pkg,<<"ulitos">>,<<"0.4.0">>},0}]}. 4 | [ 5 | {pkg_hash,[ 6 | {<<"poolboy">>, <<"392B007A1693A64540CEAD79830443ABF5762F5D30CF50BC95CB2C1AAAFA006B">>}, 7 | {<<"ulitos">>, <<"BCDF0528AF4B59F1CB7D710E4B056688751C600833F31F504FEC4BAA88F0F42B">>}]} 8 | ]. 9 | -------------------------------------------------------------------------------- /src/influx_line.erl: -------------------------------------------------------------------------------- 1 | %%% @doc 2 | %%% InfluxDB Line protocol encoder. 3 | %%% @end 4 | -module(influx_line). 5 | -include_lib("influx_udp/include/influx_udp_priv.hrl"). 6 | -include_lib("influx_udp/include/influx_udp.hrl"). 7 | 8 | -define(Q, <<"\"">>). 9 | -define(SPACE, <<" ">>). 10 | -define(NEW_LINE, <<"\n">>). 11 | -define(EMPTY, <<"">>). 12 | -define(COMMA, <<",">>). 13 | -define(EQ, <<"=">>). 14 | 15 | -export([ 16 | encode/1, 17 | encode/2, 18 | encode/3, 19 | encode/4, 20 | encode_tags/1, 21 | encode_fields/1, 22 | timestamp/0 23 | ]). 24 | 25 | %% ------------------------------------------------------------------ 26 | %% API Function Definitions 27 | %% ------------------------------------------------------------------ 28 | 29 | %% @doc 30 | %% Encode InfluxDB data point (or list of points) to line. 31 | %% Point should contain 'measurement' and 'fields' keys (as atoms). 32 | %% Other available keys: 'tags', 'time'. 33 | %% Return binary line(-s) or `{error, invalid_data}' if any point contains 34 | %% neither 'measurement' nor 'fields' key. 35 | %% 36 | %% Example: 37 | %% 38 | %% ``` 39 | %% influx_line:encode( 40 | %% #{ 41 | %% measurement => cpu, 42 | %% fields => #{ value => 43}, 43 | %% tags => #{ host => 'eu-west', ip => "127.0.0.1" }, 44 | %% time => 10000000000 45 | %% } 46 | %% ). 47 | %% <<"cpu,host=eu-west,ip="127.0.0.1" value=43 10000000000\n">> 48 | %% ''' 49 | %% @end 50 | -spec encode(Points::influx_data_points()) -> Line::binary()|{error, Reason::atom()}. 51 | encode(#{} = Data) -> 52 | encode_with_time(Data, undefined); 53 | 54 | encode([{_, _}|_] = Data) -> 55 | encode_with_time(Data, undefined); 56 | 57 | encode(List) when is_list(List) -> 58 | ?D(encode_list), 59 | encode_points_list(undefined, List, timestamp(), []); 60 | 61 | encode(_Other) -> 62 | ?D({unknown_data, _Other}), 63 | {error, invalid_data}. 64 | 65 | %% @doc 66 | %% Encode measurement and fields (or list of fields) to line. 67 | %% @end 68 | -spec encode( 69 | Name::string()|atom()|binary(), 70 | Fields::influx_data_points() 71 | ) -> Line::binary() | {error, Reason::atom()}. 72 | encode(Name, Fields) -> 73 | encode(Name, Fields, [], undefined). 74 | 75 | %% @doc 76 | %% Encode measurement, fields (or list of fields) and tags to line. 77 | %% @end 78 | -spec encode( 79 | Name::string()|atom()|binary(), 80 | Fields::influx_data_points(), 81 | Tags::influx_data_point()|binary() 82 | ) -> Line::binary() | {error, Reason::atom()}. 83 | encode(Name, Fields, Tags) -> 84 | encode(Name, Fields, Tags, undefined). 85 | 86 | %% @doc 87 | %% Encode measurement, fields (or list of fields), tags and time to line. 88 | %% If Time is integer then it's used as point time. 89 | %% If Time is `true' then `inlux_line:timestamp()' is used as point time. 90 | %% If there are several points, then every point is set a uniq time 91 | %% (by incrementing provided time). 92 | %% 93 | %% Note: if there are several points and no time specified then current time 94 | %% is used as base time. 95 | %% 96 | %% Example: 97 | %% 98 | %% ``` 99 | %% %% several points (uniq times) 100 | %% influx_line:encode( 101 | %% cpu, 102 | %% [ 103 | %% #{ value => 43 }, 104 | %% #{ value => 12 } 105 | %% ], 106 | %% #{ host => 'eu-west', ip => "127.0.0.1" }, 107 | %% 1000000000 108 | %% ). 109 | %% <<"cpu,host=eu-west,ip="127.0.0.1" value=43 1000000000\n 110 | %% cpu,host=eu-west,ip="127.0.0.1" value=12 1000000001\n">> 111 | %% 112 | %% %% one point without time 113 | %% influx_line:encode( 114 | %% cpu, 115 | %% [ 116 | %% #{ value => 43 }, 117 | %% #{ value => 12 } 118 | %% ], 119 | %% #{ host => 'eu-west', ip => "127.0.0.1" } 120 | %% ). 121 | %% <<"cpu,host=eu-west,ip="127.0.0.1" value=43\n">> 122 | %% ''' 123 | %% @end 124 | -spec encode( 125 | Name::string()|atom()|binary(), 126 | Points::list(influx_data_point())|influx_data_point(), 127 | Tags::influx_data_point()|binary(), 128 | Time::non_neg_integer()|atom() 129 | ) -> Line::binary() | {error, Reason::atom()}. 130 | encode(Name, #{} = Map, Tags, Time) -> 131 | encode_item(Name, Map, Tags, Time); 132 | 133 | encode(Name, [{_, _}|_] = List, Tags, Time) -> 134 | encode_item(Name, List, Tags, Time); 135 | 136 | encode(Name, [[{_, _}|_]|_] = Points, Tags, Time) -> 137 | encode_list(Name, Points, Tags, Time); 138 | 139 | encode(Name, [#{}|_] = Points, Tags, Time) -> 140 | encode_list(Name, Points, Tags, Time); 141 | 142 | encode(_, _, _, _) -> {error, invalid_data}. 143 | 144 | %% @doc 145 | %% Returns current UTC time in nanoseconds 146 | %% @end 147 | -spec timestamp() -> Time::non_neg_integer(). 148 | timestamp() -> 149 | ulitos:timestamp()*1000000. 150 | 151 | %% ------------------------------------------------------------------ 152 | %% Internal Function Definitions 153 | %% ------------------------------------------------------------------ 154 | encode_item(Name, Item, Tags, Time) -> 155 | BTags = encode_tags(Tags), 156 | Measurement = to_key(Name), 157 | Prefix = <>, 158 | encode_item_with_prefix(Prefix, Item, Time). 159 | 160 | encode_item_with_prefix(Prefix, Item, Time) -> 161 | BTime = encode_time(Time), 162 | Fields = encode_fields(Item), 163 | concat_line(Prefix, Fields, BTime). 164 | 165 | encode_list(Name, [Item], Tags, Time) -> 166 | encode_item(Name, Item, Tags, Time); 167 | 168 | encode_list(Name, List, Tags, Time) when is_integer(Time) -> 169 | BTags = encode_tags(Tags), 170 | Measurement = to_key(Name), 171 | Prefix = <>, 172 | encode_list_with_time(undefined, List, Prefix, Time, []); 173 | 174 | encode_list(Name, List, Tags, _Time) -> 175 | encode_list(Name, List, Tags, influx_line:timestamp()). 176 | 177 | encode_list_with_time(Item, [], _, _, Acc) -> 178 | lists:foldl(fun concat_lines/2, ?EMPTY, [Item|Acc]); 179 | 180 | encode_list_with_time(undefined, [H|Rest], Prefix, Time, Acc) -> 181 | encode_list_with_time( 182 | encode_item_with_prefix(Prefix, H, Time), 183 | Rest, 184 | Prefix, 185 | Time + 1, 186 | Acc 187 | ); 188 | 189 | encode_list_with_time(Item, [H|Rest], Prefix, Time, Acc) -> 190 | encode_list_with_time( 191 | encode_item_with_prefix(Prefix, H, Time), 192 | Rest, 193 | Prefix, 194 | Time + 1, 195 | [Item|Acc] 196 | ). 197 | 198 | encode_points_list({error, _} = Error, _, _, _) -> 199 | Error; 200 | 201 | encode_points_list(Item, [], _, Acc) -> 202 | lists:foldl(fun concat_lines/2, ?EMPTY, [Item|Acc]); 203 | 204 | encode_points_list(undefined, [H|Rest], Time, Acc) -> 205 | encode_points_list( 206 | encode_with_time(H, Time), 207 | Rest, 208 | Time + 1, 209 | Acc 210 | ); 211 | 212 | encode_points_list(Item, [H|Rest], Time, Acc) -> 213 | encode_points_list( 214 | encode_with_time(H, Time), 215 | Rest, 216 | Time + 1, 217 | [Item|Acc] 218 | ). 219 | 220 | encode_with_time([{_, _}|_] = List, BaseTime) -> 221 | M = proplists:get_value(measurement, List, undefined), 222 | F = proplists:get_value(fields, List, undefined), 223 | if M =:= undefined orelse F =:= undefined 224 | -> {error, invalid_data}; 225 | true -> 226 | Time = encode_time( 227 | proplists:get_value(time, List, BaseTime) 228 | ), 229 | Tags = encode_tags( 230 | proplists:get_value(tags, List, []) 231 | ), 232 | Measurement = to_key(M), 233 | Fields = encode_fields(F), 234 | concat_line(Measurement, Tags, Fields, Time) 235 | end; 236 | 237 | encode_with_time(#{measurement := M, fields := F} = Map, BaseTime) -> 238 | Time = encode_time( 239 | maps:get(time, Map, BaseTime) 240 | ), 241 | Tags = encode_tags( 242 | maps:get(tags, Map, []) 243 | ), 244 | Measurement = to_key(M), 245 | Fields = encode_fields(F), 246 | concat_line(Measurement, Tags, Fields, Time); 247 | 248 | encode_with_time(_, _) -> {error, invalid_data}. 249 | 250 | encode_time(undefined) -> 251 | ?EMPTY; 252 | 253 | encode_time(true) -> 254 | encode_time(timestamp()); 255 | 256 | encode_time(N) -> 257 | to_time_val(N). 258 | 259 | encode_tags(Tags) when is_binary(Tags) -> 260 | Tags; 261 | 262 | encode_tags(#{} = Map) -> 263 | remove_trailing_comma( 264 | maps:fold(fun encode_map_tags/3, ?COMMA, Map) 265 | ); 266 | 267 | encode_tags(List) when is_list(List) -> 268 | remove_trailing_comma( 269 | lists:foldl(fun encode_list_tags/2, ?COMMA, List) 270 | ); 271 | 272 | encode_tags(_) -> ?SPACE. 273 | 274 | encode_fields(#{} = Map) -> 275 | remove_trailing_comma( 276 | maps:fold(fun encode_map_fields/3, ?SPACE, Map) 277 | ); 278 | 279 | encode_fields(List) when is_list(List) -> 280 | remove_trailing_comma( 281 | lists:foldl(fun encode_list_fields/2, ?SPACE, List) 282 | ); 283 | 284 | encode_fields(_) -> {error, invalid_data}. 285 | 286 | encode_map_fields(K, V, Acc) -> 287 | BK = to_key(K), 288 | BV = to_val(V), 289 | <>. 290 | 291 | encode_list_fields({K, V}, Acc) -> 292 | encode_map_fields(K, V, Acc). 293 | 294 | encode_map_tags(K, V, Acc) -> 295 | BK = to_key(K), 296 | BV = to_tag_val(V), 297 | <>. 298 | 299 | encode_list_tags({K, V}, Acc) -> 300 | encode_map_tags(K, V, Acc). 301 | 302 | to_key(A) when is_atom(A) -> 303 | to_key(atom_to_list(A)); 304 | 305 | to_key(S) when is_list(S) -> 306 | list_to_binary(S); 307 | 308 | to_key(N) when is_integer(N) -> 309 | integer_to_binary(N); 310 | 311 | to_key(S) -> S. 312 | 313 | to_val(true) -> 314 | <<"true">>; 315 | 316 | to_val(false) -> 317 | <<"false">>; 318 | 319 | to_val(N) when is_integer(N) -> 320 | integer_to_binary(N); 321 | 322 | to_val(N) when is_float(N) -> 323 | float_to_binary(N); 324 | 325 | to_val(A) when is_atom(A) -> 326 | to_val(atom_to_list(A)); 327 | 328 | to_val(S) when is_list(S) -> 329 | to_val(list_to_binary(S)); 330 | 331 | to_val(S) when is_binary(S) -> 332 | Escaped = escape_string(S), 333 | <>. 334 | 335 | to_tag_val(V) when is_integer(V) -> 336 | to_val(V); 337 | 338 | to_tag_val(V) when is_float(V) -> 339 | to_val(V); 340 | 341 | to_tag_val(V) when is_atom(V) -> 342 | to_tag_val(atom_to_list(V)); 343 | 344 | to_tag_val(V) when is_list(V) -> 345 | to_tag_val(list_to_binary(V)); 346 | 347 | to_tag_val(V) when is_binary(V) -> 348 | escape_string(V). 349 | 350 | to_time_val(V) -> 351 | B = to_tag_val(V), 352 | <>. 353 | 354 | escape_string(Tag) -> 355 | binary:replace(Tag, 356 | [?COMMA, ?Q, ?SPACE, ?EQ], 357 | <<"\\">>, 358 | [global, {insert_replaced, 1}] 359 | ). 360 | 361 | concat_line(A, B, C) -> 362 | <>. 363 | 364 | concat_line(A, B, C, D) -> 365 | <>. 366 | 367 | concat_lines(Acc, B) -> 368 | concat_line(Acc, ?NEW_LINE, B). 369 | 370 | remove_trailing_comma(?EMPTY) -> ?EMPTY; 371 | 372 | remove_trailing_comma(B) -> binary:part(B, {0, byte_size(B) - 1}). 373 | 374 | -ifdef(TEST). 375 | -include_lib("eunit/include/eunit.hrl"). 376 | 377 | to_key_test() -> 378 | ?assertEqual(<<"test">>, to_key(test)), 379 | ?assertEqual(<<"test">>, to_key("test")), 380 | ?assertEqual(<<"test">>, to_key(<<"test">>)), 381 | ?assertEqual(<<"123">>, to_key(123)). 382 | 383 | to_val_test() -> 384 | ?assertEqual(<<"\"test\"">>, to_val(test)), 385 | ?assertEqual(<<"\"test\"">>, to_val("test")), 386 | ?assertEqual(<<"\"test\"">>, to_val(<<"test">>)), 387 | ?assertEqual(<<"true">>, to_val(true)), 388 | ?assertEqual(<<"false">>, to_val(false)), 389 | ?assertEqual(<<"\"te\\ st\"">>, to_val(<<"te st">>)), 390 | ?assertEqual(<<"\"te\\,st\"">>, to_val(<<"te,st">>)), 391 | ?assertEqual(<<"\"te\\\"st\"">>, to_val(<<"te\"st">>)), 392 | ?assertEqual(<<"\"t\\ e\\\"s\\,t\\\"\"">>, to_val(<<"t e\"s,t\"">>)), 393 | ?assertEqual(<<"123">>, to_val(123)). 394 | 395 | to_tag_val_test() -> 396 | ?assertEqual(<<"test">>, to_tag_val(test)), 397 | ?assertEqual(<<"test">>, to_tag_val("test")), 398 | ?assertEqual(<<"test">>, to_tag_val(<<"test">>)), 399 | ?assertEqual(<<"true">>, to_tag_val(true)), 400 | ?assertEqual(<<"false">>, to_tag_val(false)), 401 | ?assertEqual(<<"te\\ st">>, to_tag_val(<<"te st">>)), 402 | ?assertEqual(<<"te\\,st">>, to_tag_val(<<"te,st">>)), 403 | ?assertEqual(<<"te\\\"st">>, to_tag_val(<<"te\"st">>)), 404 | ?assertEqual(<<"t\\ e\\\"s\\,t\\\"">>, to_tag_val(<<"t e\"s,t\"">>)), 405 | ?assertEqual(<<"123">>, to_tag_val(123)). 406 | 407 | 408 | encode_time_test() -> 409 | ?assertEqual(<<" 123">>, encode_time(123)), 410 | ?assertEqual(<<"">>, encode_time(undefined)). 411 | 412 | encode_2_proplist_point_test() -> 413 | ?assertEqual( 414 | <<"test 1=1">>, 415 | encode(test, [{1, 1}]) 416 | ). 417 | 418 | encode_2_many_proplist_points_test() -> 419 | ?assertEqual( 420 | <<"test 1=1 1\ntest 1=3 2\n">>, 421 | encode("test", [ [{<<"1">>, 1}], [{<<"1">>, 3}] ], [], 1) 422 | ). 423 | 424 | encode_2_map_point_test() -> 425 | ?assertEqual( 426 | <<"test 1=1">>, 427 | encode(<<"test">>, #{1 => 1}) 428 | ). 429 | 430 | encode_2_many_map_points_test() -> 431 | ?assertEqual( 432 | <<"test 1=1 1\ntest 1=3 2\n">>, 433 | encode(<<"test">>, [#{'1' => 1}, #{'1' => 3}], [], 1) 434 | ). 435 | 436 | encode_3_invalid_test() -> 437 | ?assertEqual( 438 | {error, invalid_data}, 439 | encode(<<"test">>, 'bla-bla', []) 440 | ). 441 | 442 | encode_map_point_test() -> 443 | ?assertEqual( 444 | <<"test,host=eu-west,ip=1.1.1.1 val=10,text=\"hello\" 123">>, 445 | encode( 446 | #{ 447 | measurement => <<"test">>, 448 | fields => [{"val", 10}, {text, <<"hello">>}], 449 | tags => [{host, "eu-west"}, {ip, '1.1.1.1'}], 450 | time => 123 451 | } 452 | ) 453 | ). 454 | 455 | encode_proplist_point_test() -> 456 | ?assertEqual( 457 | <<"test,host=eu-west,ip=1.1.1.1 val=10,text=\"hello\" 123">>, 458 | encode( 459 | [ 460 | {measurement, <<"test">>}, 461 | {fields, [{"val", 10}, {text, <<"hello">>}]}, 462 | {tags, #{host => "eu-west", ip => '1.1.1.1'}}, 463 | {time, 123} 464 | ] 465 | ) 466 | ). 467 | 468 | encode_map_points_test() -> 469 | ?assertEqual( 470 | <<"test val=10 1\ntest2 val=20 2\n">>, 471 | encode( 472 | [ 473 | #{ 474 | measurement => <<"test">>, 475 | fields => #{"val" => 10}, 476 | time => "1" 477 | }, 478 | #{ 479 | measurement => <<"test2">>, 480 | fields => #{"val" => 20}, 481 | time => <<"2">> 482 | } 483 | ] 484 | ) 485 | ). 486 | 487 | encode_proplist_points_test() -> 488 | ?assertEqual( 489 | <<"test val=10 1\ntest2 val=20 2\n">>, 490 | encode( 491 | [ 492 | [ 493 | {measurement, <<"test">>}, 494 | {fields, #{"val" => 10}}, 495 | {time, 1} 496 | ], 497 | [ 498 | {measurement, <<"test2">>}, 499 | {fields, #{"val" => 20}}, 500 | {time, 2} 501 | ] 502 | ] 503 | ) 504 | ). 505 | 506 | encode_map_without_measurement_test() -> 507 | ?assertEqual( 508 | {error, invalid_data}, 509 | encode( 510 | #{ 511 | fields => #{ val => 10} 512 | } 513 | ) 514 | ). 515 | 516 | encode_map_without_fields_test() -> 517 | ?assertEqual( 518 | {error, invalid_data}, 519 | encode( 520 | #{ 521 | measurement => test, 522 | tags => #{ val => 10} 523 | } 524 | ) 525 | ). 526 | 527 | encode_proplist_without_measurement_test() -> 528 | ?assertEqual( 529 | {error, invalid_data}, 530 | encode( 531 | [ 532 | {fields, [{val, 10}] } 533 | ] 534 | ) 535 | ). 536 | 537 | encode_proplist_without_fields_test() -> 538 | ?assertEqual( 539 | {error, invalid_data}, 540 | encode( 541 | [ 542 | {measurement, test}, 543 | {tags, #{ val => 10}} 544 | ] 545 | ) 546 | ). 547 | 548 | encode_at_least_one_invalid_test() -> 549 | ?assertEqual( 550 | {error, invalid_data}, 551 | encode( 552 | [ 553 | [ 554 | {measurement, test}, 555 | {fields, #{ val => 10}} 556 | ], 557 | [ 558 | {tags, #{ val => 10}} 559 | ] 560 | ] 561 | ) 562 | ). 563 | 564 | encode_3_with_proplist_tags_test() -> 565 | ?assertEqual( 566 | <<"test,host=eu-west,ip=1.1.1.1 1=1">>, 567 | encode(<<"test">>, #{1 => 1}, [{host, "eu-west"}, {ip, '1.1.1.1'}]) 568 | ). 569 | 570 | encode_3_with_map_tags_test() -> 571 | ?assertEqual( 572 | <<"test,host=eu-west,ip=1.1.1.1 1=1">>, 573 | encode(<<"test">>, #{1 => 1}, #{host => 'eu-west', ip => <<"1.1.1.1">>}) 574 | ). 575 | 576 | encode_3_with_binary_tags_test() -> 577 | ?assertEqual( 578 | <<"test,host=eu-west,ip=1.1.1.1 1=1">>, 579 | encode(<<"test">>, #{1 => 1}, <<",host=eu-west,ip=1.1.1.1">>) 580 | ). 581 | 582 | encode_3_many_points_with_tags_test() -> 583 | ?assertEqual( 584 | <<"test,host=eu-west,num=111 memory=\"high\",cpu=20 100\ntest,host=eu-west,num=111 memory=\"low\",cpu=30 101\n">>, 585 | encode( 586 | <<"test">>, 587 | [ 588 | [{memory, "high"}, {"cpu", 20}], 589 | [{<<"memory">>, "low"}, {cpu, 30}] 590 | ], 591 | #{host => 'eu-west', num => 111}, 592 | 100 593 | ) 594 | ). 595 | 596 | encode_4_test() -> 597 | ?assertEqual( 598 | <<"test,host=eu-west,ip=1.1.1.1 1=1 123123">>, 599 | encode(<<"test">>, #{1 => 1}, [{host, "eu-west"}, {ip, '1.1.1.1'}], 123123) 600 | ). 601 | 602 | encode_4_many_points_test() -> 603 | ?assertEqual( 604 | <<"test,host=eu-west,ip=1.1.1.1 1=1,text=\"hello\" 123123">>, 605 | encode(<<"test">>, 606 | [[{"1", 1}, {text, <<"hello">>}]], 607 | [{host, "eu-west"}, {ip, '1.1.1.1'}], 608 | 123123 609 | ) 610 | ). 611 | 612 | encode_escape_tags_test() -> 613 | ?assertEqual( 614 | <<"test,host=eu\\ we\\=st,ip=1\\,1\\,1\\,1 1=1">>, 615 | encode(<<"test">>, 616 | [[{"1", 1}]], 617 | [{host, "eu we=st"}, {ip, '1,1,1,1'}] 618 | ) 619 | ). 620 | 621 | encode_escape_fields_test() -> 622 | ?assertEqual( 623 | <<"test,host=eu-west 1=1,text=\"hello\\ \\\"man\\=\\=human\\,\\ yo!\\\"\"">>, 624 | encode(<<"test">>, 625 | [[{"1", 1}, {text, <<"hello \"man==human, yo!\"">>}]], 626 | [{host, 'eu-west'}] 627 | ) 628 | ). 629 | 630 | -endif. -------------------------------------------------------------------------------- /src/influx_udp.app.src: -------------------------------------------------------------------------------- 1 | {application, influx_udp, 2 | [ 3 | {description, "InfluxDB UDP writer"}, 4 | {vsn, "1.1.2"}, 5 | {registered, []}, 6 | {applications, [kernel, stdlib, ulitos, poolboy]}, 7 | {mod, { influx_udp_app, []}}, 8 | {env, []}, 9 | {maintainers, ["Vladimir Dementyev"]}, 10 | {licenses, ["MIT"]}, 11 | {links, [{"Github", "https://github.com/palkan/influx_udp"}]}, 12 | {exclude_files, ["files/app.config", "files/influxdb.conf", "Makefile", ".travis.yml"]} 13 | ] 14 | }. 15 | -------------------------------------------------------------------------------- /src/influx_udp.erl: -------------------------------------------------------------------------------- 1 | -module(influx_udp). 2 | -include_lib("influx_udp/include/influx_udp_priv.hrl"). 3 | -include_lib("influx_udp/include/influx_udp.hrl"). 4 | -define(SERVER, ?MODULE). 5 | -define(DEPS, [ulitos, poolboy]). 6 | 7 | %% ------------------------------------------------------------------ 8 | %% API Function Exports 9 | %% ------------------------------------------------------------------ 10 | -export([start/0, stop/0]). 11 | 12 | -export([start_link/0, init_server/0]). 13 | 14 | -export([ 15 | start_pool/2, 16 | write/1, 17 | write/2, 18 | write/3, 19 | write/4, 20 | write_to/2, 21 | write_to/3, 22 | write_to/4, 23 | write_to/5 24 | ]). 25 | 26 | %% ------------------------------------------------------------------ 27 | %% gen_server Function Exports 28 | %% ------------------------------------------------------------------ 29 | 30 | -export([handle_call/3, handle_cast/2, handle_info/2, 31 | terminate/2, code_change/3]). 32 | 33 | -record(state, 34 | { 35 | defaults ::map() %% default pool configuration 36 | }). 37 | 38 | %% ------------------------------------------------------------------ 39 | %% API Function Definitions 40 | %% ------------------------------------------------------------------ 41 | 42 | start_link() -> 43 | proc_lib:start_link(?SERVER, init_server, []). 44 | 45 | %% =================================================================== 46 | %% Application callbacks 47 | %% =================================================================== 48 | 49 | start() -> 50 | ulitos_app:ensure_started(?DEPS), 51 | application:start(influx_udp). 52 | 53 | stop() -> 54 | application:stop(influx_udp). 55 | 56 | 57 | %% @doc 58 | %% Start new pool with Name and Options (as map) 59 | %% Options: 60 | %% 61 | %% - host - InfluxDB hostname 62 | %% 63 | %% - port - InflxuDB UDP port 64 | %% 65 | %% - pool_size - Pool size (numer of workers) 66 | %% 67 | %% - max_overflow - Pool max overflow size 68 | %% @end 69 | -spec start_pool(Name::atom(), Options::map()) -> {ok, pid()} | {error, term()}. 70 | start_pool(Name, Options) -> 71 | gen_server:call(?SERVER, {start_pool, Name, Options}). 72 | 73 | %% @doc 74 | %% Write binary data or map/proplist point(-s) to influx using default pool. 75 | %% Note: Assumed that data represents InfluxDB-valid input. 76 | %% @end 77 | -spec write(Data::binary()|influx_data_points()) -> ok. 78 | write(Data) -> 79 | write_to(default, Data). 80 | 81 | %% @doc 82 | %% Write data points to Series using default pool. 83 | %% @end 84 | - spec write( 85 | Series::atom()|string()|binary(), 86 | Points::list(influx_data_point())|influx_data_point() 87 | ) -> ok. 88 | write(Series, Points) -> 89 | write_to(default, Series, Points). 90 | 91 | %% @doc 92 | %% Write data points with Tags to Series using default pool. 93 | %% @end 94 | -spec write( 95 | Series::atom()|string()|binary(), 96 | Points::list(influx_data_point())|influx_data_point(), 97 | Tags::influx_data_point() 98 | ) -> ok. 99 | write(Series, Points, Tags) -> 100 | write_to(default, Series, Points, Tags). 101 | 102 | %% @doc 103 | %% Write data points with Tags and Time to Series using default pool. 104 | %% @end 105 | -spec write( 106 | Series::atom()|string()|binary(), 107 | Points::list(influx_data_point())|influx_data_point(), 108 | Tags::influx_data_point(), 109 | Time::non_neg_integer()|atom() 110 | ) -> ok. 111 | write(Series, Points, Tags, Time) -> 112 | write_to(default, Series, Points, Tags, Time). 113 | 114 | 115 | %% @doc 116 | %% Write binary data or map/proplist point(-s) to influx using named pool. 117 | %% @end 118 | -spec write_to(Pool::atom(), Data::binary()|influx_data_points()) -> ok. 119 | write_to(Pool, Data) -> 120 | gen_server:call(?SERVER, {send_to_pool, Pool, {write, Data}}). 121 | 122 | %% @doc 123 | %% Write data points to Series using named pool. 124 | %% @end 125 | -spec write_to( 126 | Pool::atom(), 127 | Series::atom()|string()|binary(), 128 | Points::list(influx_data_point())|influx_data_point() 129 | ) -> ok. 130 | write_to(Pool, Series, Points) -> 131 | gen_server:call( 132 | ?SERVER, 133 | {send_to_pool, Pool, {write, Series, Points}} 134 | ). 135 | 136 | 137 | %% @doc 138 | %% Write data points with Tags to Series using named pool. 139 | %% @end 140 | -spec write_to( 141 | Pool::atom(), 142 | Series::atom()|string()|binary(), 143 | Points::list(influx_data_point())|influx_data_point(), 144 | Tags::influx_data_point() 145 | ) -> ok. 146 | write_to(Pool, Series, Points, Tags) -> 147 | gen_server:call( 148 | ?SERVER, 149 | {send_to_pool, Pool, {write, Series, Points, Tags}} 150 | ). 151 | 152 | %% @doc 153 | %% Write data points with Tags and Time to Series using named pool. 154 | %% @end 155 | -spec write_to( 156 | Pool::atom(), 157 | Series::atom()|string()|binary(), 158 | Points::list(influx_data_point())|influx_data_point(), 159 | Tags::influx_data_point(), 160 | Time::non_neg_integer()|atom() 161 | ) -> ok. 162 | write_to(Pool, Series, Points, Tags, Time) -> 163 | gen_server:call( 164 | ?SERVER, 165 | {send_to_pool, Pool, {write, Series, Points, Tags, Time}} 166 | ). 167 | 168 | init_server() -> 169 | erlang:register(?SERVER, self()), 170 | proc_lib:init_ack({ok, self()}), 171 | 172 | Defaults = #{ 173 | port => ?Config(influx_port, 8089), 174 | host => ?Config(influx_host, undefined), 175 | pool_size => ?Config(pool_size, 3), 176 | max_overflow => ?Config(max_overflow, 1) 177 | }, 178 | 179 | start_default_pool(Defaults), 180 | 181 | gen_server:enter_loop(?SERVER, [], #state{defaults = Defaults}). 182 | 183 | %% ------------------------------------------------------------------ 184 | %% gen_server Function Definitions 185 | %% ------------------------------------------------------------------ 186 | handle_call( 187 | {start_pool, Name, Options}, 188 | _, 189 | #state{defaults = Defaults} = State 190 | ) -> 191 | {reply, start_pool_(Name, maps:merge(Defaults, Options)), State}; 192 | 193 | handle_call({send_to_pool, Pool, Msg}, _, State) -> 194 | poolboy:transaction(Pool, 195 | fun(Worker) -> 196 | gen_server:call(Worker, Msg) 197 | end 198 | ), 199 | {reply, ok, State}; 200 | 201 | handle_call(_Request, _From, State) -> 202 | {reply, ok, State}. 203 | 204 | handle_cast(_Msg, State) -> 205 | {noreply, State}. 206 | 207 | handle_info(_Info, State) -> 208 | {noreply, State}. 209 | 210 | terminate(_Reason, _State) -> 211 | ok. 212 | 213 | code_change(_OldVsn, State, _Extra) -> 214 | {ok, State}. 215 | 216 | %% ------------------------------------------------------------------ 217 | %% Internal Function Definitions 218 | %% ------------------------------------------------------------------ 219 | -spec start_default_pool(Defaults::map()) -> ok | false. 220 | start_default_pool(#{ host := undefined }) -> false; 221 | 222 | start_default_pool(Defaults) -> 223 | start_pool_(default, Defaults), 224 | ok. 225 | 226 | -spec start_pool_( 227 | Name::atom(), 228 | Options::map() 229 | ) -> {ok, Pid::pid()} | {error, Reason::term()}. 230 | start_pool_(Name, #{ host := Hostname } = Options) -> 231 | Addr = 232 | case inet:getaddr(Hostname, inet) of 233 | {ok, _Addr} = Res -> Res; 234 | {error, _Reason} -> inet:getaddr(Hostname, inet6) 235 | end, 236 | 237 | case Addr of 238 | {ok, Host} -> 239 | ?I(#{msg => "Start clients pool", host => Host, options => Options}), 240 | influx_udp_sup:start_pool( 241 | Name, 242 | maps:update(host, Host, Options) 243 | ); 244 | {error, Error} -> 245 | ?E(#{msg => "Failed to resolve influxdb host", host => Hostname, error => Error}), 246 | {error, Error} 247 | end. 248 | -------------------------------------------------------------------------------- /src/influx_udp_app.erl: -------------------------------------------------------------------------------- 1 | -module(influx_udp_app). 2 | -behaviour(application). 3 | -include_lib("influx_udp/include/influx_udp_priv.hrl"). 4 | -include_lib("influx_udp/include/influx_udp.hrl"). 5 | -define(SERVER, ?MODULE). 6 | 7 | -export([start/2, stop/1]). 8 | 9 | start(_StartType, _StartArgs) -> 10 | ?I(#{msg => "Starting application: influx_udp"}), 11 | influx_udp_sup:start_link(). 12 | 13 | stop(_State) -> 14 | ok. -------------------------------------------------------------------------------- /src/influx_udp_sup.erl: -------------------------------------------------------------------------------- 1 | -module(influx_udp_sup). 2 | -behaviour(supervisor). 3 | -include_lib("influx_udp/include/influx_udp_priv.hrl"). 4 | 5 | %% API 6 | -export([start_link/0, start_pool/2]). 7 | 8 | %% Supervisor callbacks 9 | -export([init/1]). 10 | 11 | %% Helper macro for declaring children of supervisor 12 | -define( 13 | CHILD(I, Type), {I, {I, start_link, []}, permanent, 5000, Type, [I]} 14 | ). 15 | 16 | %% =================================================================== 17 | %% API functions 18 | %% =================================================================== 19 | 20 | start_link() -> 21 | supervisor:start_link({local, ?MODULE}, ?MODULE, []). 22 | 23 | -spec start_pool( 24 | Name::atom(), 25 | Options::map() 26 | ) -> {ok, undefined | pid()} | 27 | {ok, undefined | pid(), any()} | 28 | {error, any()}. 29 | start_pool(Name, Options) -> 30 | SizeArgs = [ 31 | {size, maps:get(pool_size, Options)}, 32 | {max_overflow, maps:get(max_overflow, Options)} 33 | ], 34 | 35 | PoolArgs = [ 36 | {name, {local, Name}}, 37 | {worker_module, influx_udp_worker} 38 | ] ++ SizeArgs, 39 | 40 | supervisor:start_child( 41 | ?MODULE, 42 | poolboy:child_spec(Name, PoolArgs, [{options, Options}]) 43 | ). 44 | 45 | %% =================================================================== 46 | %% Supervisor callbacks 47 | %% =================================================================== 48 | 49 | init([]) -> 50 | Children = [ 51 | ?CHILD(influx_udp, worker) 52 | ], 53 | {ok, { {one_for_one, 5, 10}, Children} }. 54 | -------------------------------------------------------------------------------- /src/influx_udp_worker.erl: -------------------------------------------------------------------------------- 1 | -module(influx_udp_worker). 2 | -behaviour(gen_server). 3 | -behaviour(poolboy_worker). 4 | -include_lib("influx_udp/include/influx_udp_priv.hrl"). 5 | -include_lib("influx_udp/include/influx_udp.hrl"). 6 | -define(SERVER, ?MODULE). 7 | 8 | %% ------------------------------------------------------------------ 9 | %% API Function Exports 10 | %% ------------------------------------------------------------------ 11 | 12 | -export([start_link/1]). 13 | 14 | %% ------------------------------------------------------------------ 15 | %% gen_server Function Exports 16 | %% ------------------------------------------------------------------ 17 | 18 | -export([init/1, handle_call/3, handle_cast/2, handle_info/2, 19 | terminate/2, code_change/3]). 20 | 21 | -record(state, { 22 | socket ::gen_udp:socket()|undefined, 23 | port ::inet:port_number()|undefined, 24 | host ::inet:ip_address()|inet:ip_hostname()|undefined, 25 | local_port ::inet:port_number()|undefined, 26 | debug = false ::boolean() 27 | }). 28 | 29 | %% ------------------------------------------------------------------ 30 | %% API Function Definitions 31 | %% ------------------------------------------------------------------ 32 | 33 | start_link(Args) -> 34 | gen_server:start_link(?MODULE, Args, []). 35 | 36 | %% ------------------------------------------------------------------ 37 | %% gen_server Function Definitions 38 | %% ------------------------------------------------------------------ 39 | init([{options, #{ host := Host, port := Port} = _Options}]) -> 40 | AddrFamily = addr_family(Host), 41 | {ok, Socket} = gen_udp:open(0, [binary, {active, false}, AddrFamily]), 42 | {ok, LocalPort} = inet:port(Socket), 43 | ?I(#{msg => "Open UDP socket on port", port => LocalPort}), 44 | { 45 | ok, 46 | #state{ 47 | socket = Socket, 48 | host = Host, 49 | port = Port, 50 | local_port = LocalPort, 51 | debug = ?Config(debug, false) 52 | } 53 | }. 54 | 55 | handle_call({write, Bin}, _From, State) when is_binary(Bin) -> 56 | {reply, send_data(State, Bin), State}; 57 | 58 | handle_call({write, Points}, _From, State) -> 59 | {reply, send_data(State, influx_line:encode(Points)), State}; 60 | 61 | handle_call({write, Series, Points}, _From, State) -> 62 | {reply, send_data(State, influx_line:encode(Series, Points)), State}; 63 | 64 | handle_call({write, Series, Points, Tags}, _From, State) -> 65 | {reply, send_data(State, influx_line:encode(Series, Points, Tags)), State}; 66 | 67 | handle_call({write, Series, Points, Tags, Time}, _From, State) -> 68 | {reply, send_data(State, influx_line:encode(Series, Points, Tags, Time)), State}; 69 | 70 | handle_call(_Request, _From, State) -> 71 | {reply, ok, State}. 72 | 73 | handle_cast(_Msg, State) -> 74 | {noreply, State}. 75 | 76 | handle_info(_Info, State) -> 77 | {noreply, State}. 78 | 79 | terminate(_Reason, #state{socket=Socket}) -> 80 | gen_udp:close(Socket), 81 | ok. 82 | 83 | code_change(_OldVsn, State, _Extra) -> 84 | {ok, State}. 85 | 86 | %% ------------------------------------------------------------------ 87 | %% Internal Function Definitions 88 | %% ------------------------------------------------------------------ 89 | 90 | addr_family({_, _, _, _}) -> inet; 91 | addr_family({_, _, _, _, _, _, _, _}) -> inet6. 92 | 93 | send_data(_, {error, Reason} = Error) -> 94 | ?E(#{error => Reason}), 95 | Error; 96 | 97 | send_data(#state{socket=Socket, port=Port, host=Host, debug = Debug}, Data) -> 98 | debug_send(Data, Debug), 99 | gen_udp:send(Socket, Host, Port, Data). 100 | 101 | debug_send(_, false) -> ok; 102 | 103 | debug_send(Bin, _) -> 104 | ?D(#{event => send_binary_data, size => byte_size(Bin), data => Bin}). 105 | -------------------------------------------------------------------------------- /test/influx_udp_test.erl: -------------------------------------------------------------------------------- 1 | -module(influx_udp_test). 2 | -include_lib("influx_udp/include/influx_udp_priv.hrl"). 3 | -include_lib("influx_udp/include/influx_udp.hrl"). 4 | -include_lib("eunit/include/eunit.hrl"). 5 | 6 | -define(setup(F), {setup, fun setup_/0, fun cleanup_/1, F}). 7 | 8 | %% The default influx_udp port 9 | -define(PORT, 8089). 10 | -define(PORT2, 44515). 11 | 12 | setup_() -> 13 | {ok, UDP1} = test_udp_server:start(?PORT), 14 | {ok, UDP2} = test_udp_server:start(?PORT2), 15 | 16 | %% Set default configuration 17 | ulitos_app:set_var(?APP, influx_host, '127.0.0.1'), 18 | 19 | influx_udp:start(), 20 | {ok, _Pid} = influx_udp:start_pool(test, #{ pool_size => 1, port => ?PORT2}), 21 | {UDP1, UDP2}. 22 | 23 | cleanup_({UDP1, UDP2}) -> 24 | influx_udp:stop(), 25 | test_udp_server:stop(UDP1), 26 | test_udp_server:stop(UDP2). 27 | 28 | write_point_test_() -> 29 | [{"Write point", 30 | ?setup( 31 | fun(Config) -> 32 | {inorder, 33 | [ 34 | write_point_t_(Config) 35 | ] 36 | } 37 | end 38 | ) 39 | }]. 40 | 41 | write_point_pool_test_() -> 42 | [{"Write point to named pool", 43 | ?setup( 44 | fun(Config) -> 45 | {inorder, 46 | [ 47 | write_point_to_pool_t_(Config) 48 | ] 49 | } 50 | end 51 | ) 52 | }]. 53 | 54 | write_influx_point_test_() -> 55 | [{"Write point to named pool", 56 | ?setup( 57 | fun(Config) -> 58 | {inorder, 59 | [ 60 | write_influx_point_t_(Config) 61 | ] 62 | } 63 | end 64 | ) 65 | }]. 66 | 67 | 68 | write_point_t_({UDP1, _UDP2}) -> 69 | influx_udp:write(test, [{val, 1}], [{host, test}], 100), 70 | timer:sleep(1), 71 | [ 72 | ?_assertEqual(<<"test,host=test val=1 100">>, gen_server:call(UDP1, msg)) 73 | ]. 74 | 75 | write_point_to_pool_t_({_UDP1, UDP2}) -> 76 | influx_udp:write_to(test, <<"test_pool">>), 77 | timer:sleep(1), 78 | [ 79 | ?_assertEqual(<<"test_pool">>, gen_server:call(UDP2, msg)) 80 | ]. 81 | 82 | write_influx_point_t_({UDP1, _UDP2}) -> 83 | influx_udp:write([ 84 | #{ measurement => <<"test">>, fields => #{ val => 1}, time => 1}, 85 | #{ measurement => <<"test2">>, fields => #{ val => 2}, time => 2} 86 | ]), 87 | timer:sleep(1), 88 | [ 89 | ?_assertEqual(<<"test val=1 1\ntest2 val=2 2\n">>, gen_server:call(UDP1, msg)) 90 | ]. 91 | -------------------------------------------------------------------------------- /test/test_udp_server.erl: -------------------------------------------------------------------------------- 1 | -module(test_udp_server). 2 | -behaviour(gen_server). 3 | -include_lib("influx_udp/include/influx_udp_priv.hrl"). 4 | -include_lib("influx_udp/include/influx_udp.hrl"). 5 | -define(SERVER, ?MODULE). 6 | 7 | %% ------------------------------------------------------------------ 8 | %% API Function Exports 9 | %% ------------------------------------------------------------------ 10 | 11 | -export([start/1, stop/1]). 12 | 13 | %% ------------------------------------------------------------------ 14 | %% gen_server Function Exports 15 | %% ------------------------------------------------------------------ 16 | 17 | -export([init/1, handle_call/3, handle_cast/2, handle_info/2, 18 | terminate/2, code_change/3]). 19 | 20 | -record(state,{ 21 | sock, 22 | msg 23 | }). 24 | 25 | %% ------------------------------------------------------------------ 26 | %% API Function Definitions 27 | %% ------------------------------------------------------------------ 28 | 29 | start(Port) -> 30 | gen_server:start(?MODULE, [Port], []). 31 | 32 | stop(Pid) -> 33 | Pid ! stop. 34 | 35 | %% ------------------------------------------------------------------ 36 | %% gen_server Function Definitions 37 | %% ------------------------------------------------------------------ 38 | 39 | init([Port]) -> 40 | ?D({start_test_server, Port}), 41 | {ok, Socket} = gen_udp:open(Port, [binary, {active, once}]), 42 | ?D({test_server_start_listening}), 43 | {ok, #state{sock=Socket}}. 44 | 45 | handle_call(msg, _, #state{msg=Msg}=State) -> 46 | {reply, Msg, State}; 47 | 48 | handle_call(_Request, _From, State) -> 49 | {reply, ok, State}. 50 | 51 | handle_cast(_Msg, State) -> 52 | {noreply, State}. 53 | 54 | handle_info({udp, Socket, _Host, _Port, Bin}, #state{sock=Socket}=State) -> 55 | ?D({message_received, Bin}), 56 | inet:setopts(Socket, [{active, once}]), 57 | {noreply, State#state{msg=Bin}}; 58 | 59 | handle_info(stop, State) -> 60 | {stop, normal, State}; 61 | 62 | handle_info(_Info, State) -> 63 | {noreply, State}. 64 | 65 | terminate(_Reason, #state{sock=Sock}) -> 66 | gen_udp:close(Sock), 67 | ok. 68 | 69 | code_change(_OldVsn, State, _Extra) -> 70 | {ok, State}. 71 | 72 | %% ------------------------------------------------------------------ 73 | %% Internal Function Definitions 74 | %% ------------------------------------------------------------------ 75 | --------------------------------------------------------------------------------