fix: batch write
This commit is contained in:
parent
6d9944a8e8
commit
91ebd90442
|
@ -3,11 +3,10 @@
|
||||||
]}.
|
]}.
|
||||||
|
|
||||||
{deps, [
|
{deps, [
|
||||||
{emqx, {path, "../../apps/emqx"}},
|
{emqx_connector, {path, "../../apps/emqx_connector"}},
|
||||||
{emqx_connector, {path, "../../apps/emqx_connector"}},
|
{emqx_resource, {path, "../../apps/emqx_resource"}},
|
||||||
{emqx_resource, {path, "../../apps/emqx_resource"}},
|
{emqx_bridge, {path, "../../apps/emqx_bridge"}},
|
||||||
{emqx_bridge, {path, "../../apps/emqx_bridge"}},
|
{greptimedb_client_erl, {git, "https://github.com/GreptimeTeam/greptimedb-client-erl", {tag, "v0.1.1"}}}
|
||||||
{greptimedb_client_erl, {git, "https://github.com/GreptimeTeam/greptimedb-client-erl", {tag, "v0.1.1"}}}
|
|
||||||
]}.
|
]}.
|
||||||
{plugins, [rebar3_path_deps]}.
|
{plugins, [rebar3_path_deps]}.
|
||||||
{project_plugins, [erlfmt]}.
|
{project_plugins, [erlfmt]}.
|
||||||
|
|
|
@ -1,3 +1,298 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2022-2023 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
-module(emqx_bridge_greptimedb).
|
-module(emqx_bridge_greptimedb).
|
||||||
|
|
||||||
-export([]).
|
-include_lib("emqx/include/logger.hrl").
|
||||||
|
-include_lib("emqx_connector/include/emqx_connector.hrl").
|
||||||
|
-include_lib("typerefl/include/types.hrl").
|
||||||
|
-include_lib("hocon/include/hoconsc.hrl").
|
||||||
|
|
||||||
|
-import(hoconsc, [mk/2, enum/1, ref/2]).
|
||||||
|
|
||||||
|
-export([
|
||||||
|
conn_bridge_examples/1
|
||||||
|
]).
|
||||||
|
|
||||||
|
-export([
|
||||||
|
namespace/0,
|
||||||
|
roots/0,
|
||||||
|
fields/1,
|
||||||
|
desc/1
|
||||||
|
]).
|
||||||
|
|
||||||
|
-type write_syntax() :: list().
|
||||||
|
-reflect_type([write_syntax/0]).
|
||||||
|
-typerefl_from_string({write_syntax/0, ?MODULE, to_influx_lines}).
|
||||||
|
-export([to_influx_lines/1]).
|
||||||
|
|
||||||
|
%% -------------------------------------------------------------------------------------------------
|
||||||
|
%% api
|
||||||
|
|
||||||
|
conn_bridge_examples(Method) ->
|
||||||
|
[
|
||||||
|
#{
|
||||||
|
<<"greptimedb_grpc_v1">> => #{
|
||||||
|
summary => <<"Greptimedb HTTP API V2 Bridge">>,
|
||||||
|
value => values("greptimedb_grpc_v1", Method)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
].
|
||||||
|
|
||||||
|
values(Protocol, get) ->
|
||||||
|
values(Protocol, post);
|
||||||
|
values("greptimedb_grpc_v1", post) ->
|
||||||
|
SupportUint = <<"uint_value=${payload.uint_key}u,">>,
|
||||||
|
TypeOpts = #{
|
||||||
|
bucket => <<"example_bucket">>,
|
||||||
|
org => <<"examlpe_org">>,
|
||||||
|
token => <<"example_token">>,
|
||||||
|
server => <<"127.0.0.1:4000">>
|
||||||
|
},
|
||||||
|
values(common, "greptimedb_grpc_v1", SupportUint, TypeOpts);
|
||||||
|
values(Protocol, put) ->
|
||||||
|
values(Protocol, post).
|
||||||
|
|
||||||
|
values(common, Protocol, SupportUint, TypeOpts) ->
|
||||||
|
CommonConfigs = #{
|
||||||
|
type => list_to_atom(Protocol),
|
||||||
|
name => <<"demo">>,
|
||||||
|
enable => true,
|
||||||
|
local_topic => <<"local/topic/#">>,
|
||||||
|
write_syntax =>
|
||||||
|
<<"${topic},clientid=${clientid}", " ", "payload=${payload},",
|
||||||
|
"${clientid}_int_value=${payload.int_key}i,", SupportUint/binary,
|
||||||
|
"bool=${payload.bool}">>,
|
||||||
|
precision => ms,
|
||||||
|
resource_opts => #{
|
||||||
|
batch_size => 100,
|
||||||
|
batch_time => <<"20ms">>
|
||||||
|
},
|
||||||
|
server => <<"127.0.0.1:4000">>,
|
||||||
|
ssl => #{enable => false}
|
||||||
|
},
|
||||||
|
maps:merge(TypeOpts, CommonConfigs).
|
||||||
|
|
||||||
|
%% -------------------------------------------------------------------------------------------------
|
||||||
|
%% Hocon Schema Definitions
|
||||||
|
namespace() -> "bridge_greptimedb".
|
||||||
|
|
||||||
|
roots() -> [].
|
||||||
|
|
||||||
|
fields("post_grpc_v1") ->
|
||||||
|
method_fields(post, greptimedb_grpc_v1);
|
||||||
|
fields("put_grpc_v1") ->
|
||||||
|
method_fields(put, greptimedb_grpc_v1);
|
||||||
|
fields("get_grpc_v1") ->
|
||||||
|
method_fields(get, greptimedb_grpc_v1);
|
||||||
|
fields(Type) when
|
||||||
|
Type == greptimedb_grpc_v1
|
||||||
|
->
|
||||||
|
greptimedb_bridge_common_fields() ++
|
||||||
|
connector_fields(Type).
|
||||||
|
|
||||||
|
method_fields(post, ConnectorType) ->
|
||||||
|
greptimedb_bridge_common_fields() ++
|
||||||
|
connector_fields(ConnectorType) ++
|
||||||
|
type_name_fields(ConnectorType);
|
||||||
|
method_fields(get, ConnectorType) ->
|
||||||
|
greptimedb_bridge_common_fields() ++
|
||||||
|
connector_fields(ConnectorType) ++
|
||||||
|
type_name_fields(ConnectorType) ++
|
||||||
|
emqx_bridge_schema:status_fields();
|
||||||
|
method_fields(put, ConnectorType) ->
|
||||||
|
greptimedb_bridge_common_fields() ++
|
||||||
|
connector_fields(ConnectorType).
|
||||||
|
|
||||||
|
greptimedb_bridge_common_fields() ->
|
||||||
|
emqx_bridge_schema:common_bridge_fields() ++
|
||||||
|
[
|
||||||
|
{write_syntax, fun write_syntax/1}
|
||||||
|
] ++
|
||||||
|
emqx_resource_schema:fields("resource_opts").
|
||||||
|
|
||||||
|
connector_fields(Type) ->
|
||||||
|
emqx_bridge_greptimedb_connector:fields(Type).
|
||||||
|
|
||||||
|
type_name_fields(Type) ->
|
||||||
|
[
|
||||||
|
{type, mk(Type, #{required => true, desc => ?DESC("desc_type")})},
|
||||||
|
{name, mk(binary(), #{required => true, desc => ?DESC("desc_name")})}
|
||||||
|
].
|
||||||
|
|
||||||
|
desc("config") ->
|
||||||
|
?DESC("desc_config");
|
||||||
|
desc(Method) when Method =:= "get"; Method =:= "put"; Method =:= "post" ->
|
||||||
|
["Configuration for Greptimedb using `", string:to_upper(Method), "` method."];
|
||||||
|
desc(greptimedb_grpc_v1) ->
|
||||||
|
?DESC(emqx_bridge_greptimedb_connector, "greptimedb_grpc_v1");
|
||||||
|
desc(_) ->
|
||||||
|
undefined.
|
||||||
|
|
||||||
|
write_syntax(type) ->
|
||||||
|
?MODULE:write_syntax();
|
||||||
|
write_syntax(required) ->
|
||||||
|
true;
|
||||||
|
write_syntax(validator) ->
|
||||||
|
[?NOT_EMPTY("the value of the field 'write_syntax' cannot be empty")];
|
||||||
|
write_syntax(converter) ->
|
||||||
|
fun to_influx_lines/1;
|
||||||
|
write_syntax(desc) ->
|
||||||
|
?DESC("write_syntax");
|
||||||
|
write_syntax(format) ->
|
||||||
|
<<"sql">>;
|
||||||
|
write_syntax(_) ->
|
||||||
|
undefined.
|
||||||
|
|
||||||
|
to_influx_lines(RawLines) ->
|
||||||
|
try
|
||||||
|
influx_lines(str(RawLines), [])
|
||||||
|
catch
|
||||||
|
_:Reason:Stacktrace ->
|
||||||
|
Msg = lists:flatten(
|
||||||
|
io_lib:format("Unable to parse Greptimedb line protocol: ~p", [RawLines])
|
||||||
|
),
|
||||||
|
?SLOG(error, #{msg => Msg, error_reason => Reason, stacktrace => Stacktrace}),
|
||||||
|
throw(Msg)
|
||||||
|
end.
|
||||||
|
|
||||||
|
-define(MEASUREMENT_ESC_CHARS, [$,, $\s]).
|
||||||
|
-define(TAG_FIELD_KEY_ESC_CHARS, [$,, $=, $\s]).
|
||||||
|
-define(FIELD_VAL_ESC_CHARS, [$", $\\]).
|
||||||
|
% Common separator for both tags and fields
|
||||||
|
-define(SEP, $\s).
|
||||||
|
-define(MEASUREMENT_TAG_SEP, $,).
|
||||||
|
-define(KEY_SEP, $=).
|
||||||
|
-define(VAL_SEP, $,).
|
||||||
|
-define(NON_EMPTY, [_ | _]).
|
||||||
|
|
||||||
|
influx_lines([] = _RawLines, Acc) ->
|
||||||
|
?NON_EMPTY = lists:reverse(Acc);
|
||||||
|
influx_lines(RawLines, Acc) ->
|
||||||
|
{Acc1, RawLines1} = influx_line(string:trim(RawLines, leading, "\s\n"), Acc),
|
||||||
|
influx_lines(RawLines1, Acc1).
|
||||||
|
|
||||||
|
influx_line([], Acc) ->
|
||||||
|
{Acc, []};
|
||||||
|
influx_line(Line, Acc) ->
|
||||||
|
{?NON_EMPTY = Measurement, Line1} = measurement(Line),
|
||||||
|
{Tags, Line2} = tags(Line1),
|
||||||
|
{?NON_EMPTY = Fields, Line3} = influx_fields(Line2),
|
||||||
|
{Timestamp, Line4} = timestamp(Line3),
|
||||||
|
{
|
||||||
|
[
|
||||||
|
#{
|
||||||
|
measurement => Measurement,
|
||||||
|
tags => Tags,
|
||||||
|
fields => Fields,
|
||||||
|
timestamp => Timestamp
|
||||||
|
}
|
||||||
|
| Acc
|
||||||
|
],
|
||||||
|
Line4
|
||||||
|
}.
|
||||||
|
|
||||||
|
measurement(Line) ->
|
||||||
|
unescape(?MEASUREMENT_ESC_CHARS, [?MEASUREMENT_TAG_SEP, ?SEP], Line, []).
|
||||||
|
|
||||||
|
tags([?MEASUREMENT_TAG_SEP | Line]) ->
|
||||||
|
tags1(Line, []);
|
||||||
|
tags(Line) ->
|
||||||
|
{[], Line}.
|
||||||
|
|
||||||
|
%% Empty line is invalid as fields are required after tags,
|
||||||
|
%% need to break recursion here and fail later on parsing fields
|
||||||
|
tags1([] = Line, Acc) ->
|
||||||
|
{lists:reverse(Acc), Line};
|
||||||
|
%% Matching non empty Acc treats lines like "m, field=field_val" invalid
|
||||||
|
tags1([?SEP | _] = Line, ?NON_EMPTY = Acc) ->
|
||||||
|
{lists:reverse(Acc), Line};
|
||||||
|
tags1(Line, Acc) ->
|
||||||
|
{Tag, Line1} = tag(Line),
|
||||||
|
tags1(Line1, [Tag | Acc]).
|
||||||
|
|
||||||
|
tag(Line) ->
|
||||||
|
{?NON_EMPTY = Key, Line1} = key(Line),
|
||||||
|
{?NON_EMPTY = Val, Line2} = tag_val(Line1),
|
||||||
|
{{Key, Val}, Line2}.
|
||||||
|
|
||||||
|
tag_val(Line) ->
|
||||||
|
{Val, Line1} = unescape(?TAG_FIELD_KEY_ESC_CHARS, [?VAL_SEP, ?SEP], Line, []),
|
||||||
|
{Val, strip_l(Line1, ?VAL_SEP)}.
|
||||||
|
|
||||||
|
influx_fields([?SEP | Line]) ->
|
||||||
|
fields1(string:trim(Line, leading, "\s"), []).
|
||||||
|
|
||||||
|
%% Timestamp is optional, so fields may be at the very end of the line
|
||||||
|
fields1([Ch | _] = Line, Acc) when Ch =:= ?SEP; Ch =:= $\n ->
|
||||||
|
{lists:reverse(Acc), Line};
|
||||||
|
fields1([] = Line, Acc) ->
|
||||||
|
{lists:reverse(Acc), Line};
|
||||||
|
fields1(Line, Acc) ->
|
||||||
|
{Field, Line1} = field(Line),
|
||||||
|
fields1(Line1, [Field | Acc]).
|
||||||
|
|
||||||
|
field(Line) ->
|
||||||
|
{?NON_EMPTY = Key, Line1} = key(Line),
|
||||||
|
{Val, Line2} = field_val(Line1),
|
||||||
|
{{Key, Val}, Line2}.
|
||||||
|
|
||||||
|
field_val([$" | Line]) ->
|
||||||
|
{Val, [$" | Line1]} = unescape(?FIELD_VAL_ESC_CHARS, [$"], Line, []),
|
||||||
|
%% Quoted val can be empty
|
||||||
|
{Val, strip_l(Line1, ?VAL_SEP)};
|
||||||
|
field_val(Line) ->
|
||||||
|
%% Unquoted value should not be un-escaped according to Greptimedb protocol,
|
||||||
|
%% as it can only hold float, integer, uinteger or boolean value.
|
||||||
|
%% However, as templates are possible, un-escaping is applied here,
|
||||||
|
%% which also helps to detect some invalid lines, e.g.: "m,tag=1 field= ${timestamp}"
|
||||||
|
{Val, Line1} = unescape(?TAG_FIELD_KEY_ESC_CHARS, [?VAL_SEP, ?SEP, $\n], Line, []),
|
||||||
|
{?NON_EMPTY = Val, strip_l(Line1, ?VAL_SEP)}.
|
||||||
|
|
||||||
|
timestamp([?SEP | Line]) ->
|
||||||
|
Line1 = string:trim(Line, leading, "\s"),
|
||||||
|
%% Similarly to unquoted field value, un-escape a timestamp to validate and handle
|
||||||
|
%% potentially escaped characters in a template
|
||||||
|
{T, Line2} = unescape(?TAG_FIELD_KEY_ESC_CHARS, [?SEP, $\n], Line1, []),
|
||||||
|
{timestamp1(T), Line2};
|
||||||
|
timestamp(Line) ->
|
||||||
|
{undefined, Line}.
|
||||||
|
|
||||||
|
timestamp1(?NON_EMPTY = Ts) -> Ts;
|
||||||
|
timestamp1(_Ts) -> undefined.
|
||||||
|
|
||||||
|
%% Common for both tag and field keys
|
||||||
|
key(Line) ->
|
||||||
|
{Key, Line1} = unescape(?TAG_FIELD_KEY_ESC_CHARS, [?KEY_SEP], Line, []),
|
||||||
|
{Key, strip_l(Line1, ?KEY_SEP)}.
|
||||||
|
|
||||||
|
%% Only strip a character between pairs, don't strip it(and let it fail)
|
||||||
|
%% if the char to be stripped is at the end, e.g.: m,tag=val, field=val
|
||||||
|
strip_l([Ch, Ch1 | Str], Ch) when Ch1 =/= ?SEP ->
|
||||||
|
[Ch1 | Str];
|
||||||
|
strip_l(Str, _Ch) ->
|
||||||
|
Str.
|
||||||
|
|
||||||
|
unescape(EscapeChars, SepChars, [$\\, Char | T], Acc) ->
|
||||||
|
ShouldEscapeBackslash = lists:member($\\, EscapeChars),
|
||||||
|
Acc1 =
|
||||||
|
case lists:member(Char, EscapeChars) of
|
||||||
|
true -> [Char | Acc];
|
||||||
|
false when not ShouldEscapeBackslash -> [Char, $\\ | Acc]
|
||||||
|
end,
|
||||||
|
unescape(EscapeChars, SepChars, T, Acc1);
|
||||||
|
unescape(EscapeChars, SepChars, [Char | T] = L, Acc) ->
|
||||||
|
IsEscapeChar = lists:member(Char, EscapeChars),
|
||||||
|
case lists:member(Char, SepChars) of
|
||||||
|
true -> {lists:reverse(Acc), L};
|
||||||
|
false when not IsEscapeChar -> unescape(EscapeChars, SepChars, T, [Char | Acc])
|
||||||
|
end;
|
||||||
|
unescape(_EscapeChars, _SepChars, [] = L, Acc) ->
|
||||||
|
{lists:reverse(Acc), L}.
|
||||||
|
|
||||||
|
str(A) when is_atom(A) ->
|
||||||
|
atom_to_list(A);
|
||||||
|
str(B) when is_binary(B) ->
|
||||||
|
binary_to_list(B);
|
||||||
|
str(S) when is_list(S) ->
|
||||||
|
S.
|
||||||
|
|
|
@ -1,84 +1,179 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2022-2023 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
-module(emqx_bridge_greptimedb_connector).
|
-module(emqx_bridge_greptimedb_connector).
|
||||||
|
|
||||||
|
-include_lib("emqx_connector/include/emqx_connector.hrl").
|
||||||
|
|
||||||
|
-include_lib("hocon/include/hoconsc.hrl").
|
||||||
|
-include_lib("typerefl/include/types.hrl").
|
||||||
-include_lib("emqx/include/logger.hrl").
|
-include_lib("emqx/include/logger.hrl").
|
||||||
-include_lib("snabbkaffe/include/snabbkaffe.hrl").
|
-include_lib("snabbkaffe/include/snabbkaffe.hrl").
|
||||||
|
|
||||||
%% `emqx_resource' API
|
-import(hoconsc, [mk/2, enum/1, ref/2]).
|
||||||
|
|
||||||
|
-behaviour(emqx_resource).
|
||||||
|
|
||||||
|
%% callbacks of behaviour emqx_resource
|
||||||
-export([
|
-export([
|
||||||
callback_mode/0,
|
callback_mode/0,
|
||||||
on_start/2,
|
on_start/2,
|
||||||
on_stop/2,
|
on_stop/2,
|
||||||
on_get_status/2,
|
|
||||||
on_query/3,
|
on_query/3,
|
||||||
on_query_async/4,
|
|
||||||
on_batch_query/3,
|
on_batch_query/3,
|
||||||
on_batch_query_async/4
|
on_get_status/2
|
||||||
]).
|
]).
|
||||||
|
|
||||||
-define(GREPTIMEDB_DEFAULT_PORT, 4001).
|
-export([
|
||||||
|
roots/0,
|
||||||
|
namespace/0,
|
||||||
|
fields/1,
|
||||||
|
desc/1
|
||||||
|
]).
|
||||||
|
|
||||||
|
%% only for test
|
||||||
|
-export([is_unrecoverable_error/1]).
|
||||||
|
|
||||||
|
-type ts_precision() :: ns | us | ms | s.
|
||||||
|
|
||||||
|
%% Allocatable resources
|
||||||
|
-define(greptime_client, greptime_client).
|
||||||
|
|
||||||
|
-define(GREPTIMEDB_DEFAULT_PORT, 4000).
|
||||||
|
|
||||||
|
-define(DEFAULT_DB, <<"public">>).
|
||||||
|
|
||||||
-define(GREPTIMEDB_HOST_OPTIONS, #{
|
-define(GREPTIMEDB_HOST_OPTIONS, #{
|
||||||
default_port => ?GREPTIMEDB_DEFAULT_PORT
|
default_port => ?GREPTIMEDB_DEFAULT_PORT
|
||||||
}).
|
}).
|
||||||
|
|
||||||
%%-------------------------------------------------------------------------------------
|
-define(DEFAULT_TIMESTAMP_TMPL, "${timestamp}").
|
||||||
%% `emqx_resource' API
|
|
||||||
%%-------------------------------------------------------------------------------------
|
%% -------------------------------------------------------------------------------------------------
|
||||||
callback_mode() -> async_if_possible.
|
%% resource callback
|
||||||
|
callback_mode() -> always_sync.
|
||||||
|
|
||||||
on_start(InstId, Config) ->
|
on_start(InstId, Config) ->
|
||||||
|
%% InstID as pool would be handled by greptimedb client
|
||||||
|
%% so there is no need to allocate pool_name here
|
||||||
|
%% See: greptimedb:start_client/1
|
||||||
start_client(InstId, Config).
|
start_client(InstId, Config).
|
||||||
|
|
||||||
on_stop(_InstId, #{client := Client}) ->
|
on_stop(InstId, _State) ->
|
||||||
greptimedb:stop_client(Client).
|
case emqx_resource:get_allocated_resources(InstId) of
|
||||||
|
#{?greptime_client := Client} ->
|
||||||
|
greptimedb:stop_client(Client);
|
||||||
|
_ ->
|
||||||
|
ok
|
||||||
|
end.
|
||||||
|
|
||||||
on_get_status(_InstId, _State) ->
|
on_query(InstId, {send_message, Data}, _State = #{write_syntax := SyntaxLines, client := Client}) ->
|
||||||
%% FIXME
|
case data_to_points(Data, SyntaxLines) of
|
||||||
connected.
|
{ok, Points} ->
|
||||||
|
?tp(
|
||||||
|
greptimedb_connector_send_query,
|
||||||
|
#{points => Points, batch => false, mode => sync}
|
||||||
|
),
|
||||||
|
do_query(InstId, Client, Points);
|
||||||
|
{error, ErrorPoints} ->
|
||||||
|
?tp(
|
||||||
|
greptimedb_connector_send_query_error,
|
||||||
|
#{batch => false, mode => sync, error => ErrorPoints}
|
||||||
|
),
|
||||||
|
log_error_points(InstId, ErrorPoints),
|
||||||
|
{error, {unrecoverable_error, ErrorPoints}}
|
||||||
|
end.
|
||||||
|
|
||||||
on_query(_InstanceId, {send_message, _Message}, _State) ->
|
%% Once a Batched Data trans to points failed.
|
||||||
todo.
|
%% This batch query failed
|
||||||
|
on_batch_query(InstId, BatchData, _State = #{write_syntax := SyntaxLines, client := Client}) ->
|
||||||
|
case parse_batch_data(InstId, BatchData, SyntaxLines) of
|
||||||
|
{ok, Points} ->
|
||||||
|
?tp(
|
||||||
|
greptimedb_connector_send_query,
|
||||||
|
#{points => Points, batch => true, mode => sync}
|
||||||
|
),
|
||||||
|
do_query(InstId, Client, Points);
|
||||||
|
{error, Reason} ->
|
||||||
|
?tp(
|
||||||
|
greptimedb_connector_send_query_error,
|
||||||
|
#{batch => true, mode => sync, error => Reason}
|
||||||
|
),
|
||||||
|
{error, {unrecoverable_error, Reason}}
|
||||||
|
end.
|
||||||
|
|
||||||
on_query_async(_InstanceId, {send_message, _Message}, _ReplyFunAndArgs0, _State) ->
|
on_get_status(_InstId, #{client := Client}) ->
|
||||||
todo.
|
case greptimedb:is_alive(Client) of
|
||||||
|
true ->
|
||||||
|
connected;
|
||||||
|
false ->
|
||||||
|
disconnected
|
||||||
|
end.
|
||||||
|
|
||||||
on_batch_query(
|
%% -------------------------------------------------------------------------------------------------
|
||||||
_ResourceID,
|
%% schema
|
||||||
_BatchReq,
|
namespace() -> connector_greptimedb.
|
||||||
_State
|
|
||||||
) ->
|
|
||||||
todo.
|
|
||||||
|
|
||||||
on_batch_query_async(
|
roots() ->
|
||||||
_InstId,
|
[
|
||||||
_BatchData,
|
{config, #{
|
||||||
{_ReplyFun, _Args},
|
type => hoconsc:union(
|
||||||
_State
|
[
|
||||||
) ->
|
hoconsc:ref(?MODULE, greptimedb_grpc_v1)
|
||||||
todo.
|
]
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
].
|
||||||
|
|
||||||
|
fields(common) ->
|
||||||
|
[
|
||||||
|
{server, server()},
|
||||||
|
{precision,
|
||||||
|
%% The greptimedb only supports these 4 precision:
|
||||||
|
%% See "https://github.com/influxdata/greptimedb/blob/
|
||||||
|
%% 6b607288439a991261307518913eb6d4e280e0a7/models/points.go#L487" for
|
||||||
|
%% more information.
|
||||||
|
mk(enum([ns, us, ms, s]), #{
|
||||||
|
required => false, default => ms, desc => ?DESC("precision")
|
||||||
|
})}
|
||||||
|
];
|
||||||
|
fields(greptimedb_grpc_v1) ->
|
||||||
|
fields(common) ++
|
||||||
|
[
|
||||||
|
{dbname, mk(binary(), #{required => true, desc => ?DESC("dbname")})}
|
||||||
|
] ++ emqx_connector_schema_lib:ssl_fields().
|
||||||
|
|
||||||
|
server() ->
|
||||||
|
Meta = #{
|
||||||
|
required => false,
|
||||||
|
default => <<"127.0.0.1:4000">>,
|
||||||
|
desc => ?DESC("server"),
|
||||||
|
converter => fun convert_server/2
|
||||||
|
},
|
||||||
|
emqx_schema:servers_sc(Meta, ?GREPTIMEDB_HOST_OPTIONS).
|
||||||
|
|
||||||
|
desc(common) ->
|
||||||
|
?DESC("common");
|
||||||
|
desc(greptimedb_grpc_v1) ->
|
||||||
|
?DESC("greptimedb_grpc_v1").
|
||||||
|
|
||||||
|
%% -------------------------------------------------------------------------------------------------
|
||||||
%% internal functions
|
%% internal functions
|
||||||
|
|
||||||
start_client(InstId, Config) ->
|
start_client(InstId, Config) ->
|
||||||
ClientConfig = client_config(InstId, Config),
|
ClientConfig = client_config(InstId, Config),
|
||||||
?SLOG(info, #{
|
?SLOG(info, #{
|
||||||
msg => "starting GreptimeDB connector",
|
msg => "starting greptimedb connector",
|
||||||
connector => InstId,
|
connector => InstId,
|
||||||
config => emqx_utils:redact(Config),
|
config => emqx_utils:redact(Config),
|
||||||
client_config => emqx_utils:redact(ClientConfig)
|
client_config => emqx_utils:redact(ClientConfig)
|
||||||
}),
|
}),
|
||||||
try
|
try do_start_client(InstId, ClientConfig, Config) of
|
||||||
case greptimedb:start_client(ClientConfig) of
|
Res = {ok, #{client := Client}} ->
|
||||||
{ok, Client} ->
|
ok = emqx_resource:allocate_resource(InstId, ?greptime_client, Client),
|
||||||
{ok, #{client => Client}};
|
Res;
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
?tp(greptimedb_connector_start_failed, #{error => Reason}),
|
{error, Reason}
|
||||||
?SLOG(warning, #{
|
|
||||||
msg => "failed_to_start_greptimedb_connector",
|
|
||||||
connector => InstId,
|
|
||||||
reason => Reason
|
|
||||||
}),
|
|
||||||
{error, Reason}
|
|
||||||
end
|
|
||||||
catch
|
catch
|
||||||
E:R:S ->
|
E:R:S ->
|
||||||
?tp(greptimedb_connector_start_exception, #{error => {E, R}}),
|
?tp(greptimedb_connector_start_exception, #{error => {E, R}}),
|
||||||
|
@ -92,9 +187,64 @@ start_client(InstId, Config) ->
|
||||||
{error, R}
|
{error, R}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
do_start_client(
|
||||||
|
InstId,
|
||||||
|
ClientConfig,
|
||||||
|
Config = #{write_syntax := Lines}
|
||||||
|
) ->
|
||||||
|
Precision = maps:get(precision, Config, ms),
|
||||||
|
case greptimedb:start_client(ClientConfig) of
|
||||||
|
{ok, Client} ->
|
||||||
|
case greptimedb:is_alive(Client, true) of
|
||||||
|
true ->
|
||||||
|
State = #{
|
||||||
|
client => Client,
|
||||||
|
dbname => proplists:get_value(dbname, ClientConfig, ?DEFAULT_DB),
|
||||||
|
write_syntax => to_config(Lines, Precision)
|
||||||
|
},
|
||||||
|
?SLOG(info, #{
|
||||||
|
msg => "starting greptimedb connector success",
|
||||||
|
connector => InstId,
|
||||||
|
client => redact_auth(Client),
|
||||||
|
state => redact_auth(State)
|
||||||
|
}),
|
||||||
|
{ok, State};
|
||||||
|
{false, Reason} ->
|
||||||
|
?tp(greptimedb_connector_start_failed, #{
|
||||||
|
error => greptimedb_client_not_alive, reason => Reason
|
||||||
|
}),
|
||||||
|
?SLOG(warning, #{
|
||||||
|
msg => "failed_to_start_greptimedb_connector",
|
||||||
|
connector => InstId,
|
||||||
|
client => redact_auth(Client),
|
||||||
|
reason => Reason
|
||||||
|
}),
|
||||||
|
%% no leak
|
||||||
|
_ = greptimedb:stop_client(Client),
|
||||||
|
{error, greptimedb_client_not_alive}
|
||||||
|
end;
|
||||||
|
{error, {already_started, Client0}} ->
|
||||||
|
?tp(greptimedb_connector_start_already_started, #{}),
|
||||||
|
?SLOG(info, #{
|
||||||
|
msg => "restarting greptimedb connector, found already started client",
|
||||||
|
connector => InstId,
|
||||||
|
old_client => redact_auth(Client0)
|
||||||
|
}),
|
||||||
|
_ = greptimedb:stop_client(Client0),
|
||||||
|
do_start_client(InstId, ClientConfig, Config);
|
||||||
|
{error, Reason} ->
|
||||||
|
?tp(greptimedb_connector_start_failed, #{error => Reason}),
|
||||||
|
?SLOG(warning, #{
|
||||||
|
msg => "failed_to_start_greptimedb_connector",
|
||||||
|
connector => InstId,
|
||||||
|
reason => Reason
|
||||||
|
}),
|
||||||
|
{error, Reason}
|
||||||
|
end.
|
||||||
|
|
||||||
client_config(
|
client_config(
|
||||||
InstId,
|
InstId,
|
||||||
_Config = #{
|
Config = #{
|
||||||
server := Server
|
server := Server
|
||||||
}
|
}
|
||||||
) ->
|
) ->
|
||||||
|
@ -103,12 +253,369 @@ client_config(
|
||||||
{endpoints, [{http, str(Host), Port}]},
|
{endpoints, [{http, str(Host), Port}]},
|
||||||
{pool_size, erlang:system_info(schedulers)},
|
{pool_size, erlang:system_info(schedulers)},
|
||||||
{pool, InstId},
|
{pool, InstId},
|
||||||
{pool_type, random}
|
{pool_type, random},
|
||||||
|
{timeunit, maps:get(precision, Config, ms)}
|
||||||
|
] ++ protocol_config(Config).
|
||||||
|
|
||||||
|
protocol_config(
|
||||||
|
#{
|
||||||
|
dbname := DbName,
|
||||||
|
ssl := SSL
|
||||||
|
} = Config
|
||||||
|
) ->
|
||||||
|
[
|
||||||
|
{dbname, DbName}
|
||||||
|
] ++ auth(Config) ++
|
||||||
|
ssl_config(SSL).
|
||||||
|
|
||||||
|
ssl_config(#{enable := false}) ->
|
||||||
|
[
|
||||||
|
{https_enabled, false}
|
||||||
|
];
|
||||||
|
ssl_config(SSL = #{enable := true}) ->
|
||||||
|
[
|
||||||
|
{https_enabled, true},
|
||||||
|
{transport, ssl},
|
||||||
|
{transport_opts, emqx_tls_lib:to_client_opts(SSL)}
|
||||||
].
|
].
|
||||||
|
|
||||||
|
auth(#{username := Username, password := Password}) ->
|
||||||
|
[
|
||||||
|
{auth, {basic, #{username => Username, password => Password}}}
|
||||||
|
];
|
||||||
|
auth(_) ->
|
||||||
|
[].
|
||||||
|
|
||||||
|
redact_auth(Term) ->
|
||||||
|
emqx_utils:redact(Term, fun is_auth_key/1).
|
||||||
|
|
||||||
|
is_auth_key(Key) when is_binary(Key) ->
|
||||||
|
string:equal("authorization", Key, true);
|
||||||
|
is_auth_key(_) ->
|
||||||
|
false.
|
||||||
|
|
||||||
|
%% -------------------------------------------------------------------------------------------------
|
||||||
|
%% Query
|
||||||
|
do_query(InstId, Client, Points) ->
|
||||||
|
case greptimedb:write_batch(Client, Points) of
|
||||||
|
{ok, _} ->
|
||||||
|
?SLOG(debug, #{
|
||||||
|
msg => "greptimedb write point success",
|
||||||
|
connector => InstId,
|
||||||
|
points => Points
|
||||||
|
});
|
||||||
|
{error, {401, _, _}} ->
|
||||||
|
?tp(greptimedb_connector_do_query_failure, #{error => <<"authorization failure">>}),
|
||||||
|
?SLOG(error, #{
|
||||||
|
msg => "greptimedb_authorization_failed",
|
||||||
|
client => redact_auth(Client),
|
||||||
|
connector => InstId
|
||||||
|
}),
|
||||||
|
{error, {unrecoverable_error, <<"authorization failure">>}};
|
||||||
|
{error, Reason} = Err ->
|
||||||
|
?tp(greptimedb_connector_do_query_failure, #{error => Reason}),
|
||||||
|
?SLOG(error, #{
|
||||||
|
msg => "greptimedb write point failed",
|
||||||
|
connector => InstId,
|
||||||
|
reason => Reason
|
||||||
|
}),
|
||||||
|
case is_unrecoverable_error(Err) of
|
||||||
|
true ->
|
||||||
|
{error, {unrecoverable_error, Reason}};
|
||||||
|
false ->
|
||||||
|
{error, {recoverable_error, Reason}}
|
||||||
|
end
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% -------------------------------------------------------------------------------------------------
|
||||||
|
%% Tags & Fields Config Trans
|
||||||
|
|
||||||
|
to_config(Lines, Precision) ->
|
||||||
|
to_config(Lines, [], Precision).
|
||||||
|
|
||||||
|
to_config([], Acc, _Precision) ->
|
||||||
|
lists:reverse(Acc);
|
||||||
|
to_config([Item0 | Rest], Acc, Precision) ->
|
||||||
|
Ts0 = maps:get(timestamp, Item0, undefined),
|
||||||
|
{Ts, FromPrecision, ToPrecision} = preproc_tmpl_timestamp(Ts0, Precision),
|
||||||
|
Item = #{
|
||||||
|
measurement => emqx_placeholder:preproc_tmpl(maps:get(measurement, Item0)),
|
||||||
|
timestamp => Ts,
|
||||||
|
precision => {FromPrecision, ToPrecision},
|
||||||
|
tags => to_kv_config(maps:get(tags, Item0)),
|
||||||
|
fields => to_kv_config(maps:get(fields, Item0))
|
||||||
|
},
|
||||||
|
to_config(Rest, [Item | Acc], Precision).
|
||||||
|
|
||||||
|
%% pre-process the timestamp template
|
||||||
|
%% returns a tuple of three elements:
|
||||||
|
%% 1. The timestamp template itself.
|
||||||
|
%% 2. The source timestamp precision (ms if the template ${timestamp} is used).
|
||||||
|
%% 3. The target timestamp precision (configured for the client).
|
||||||
|
preproc_tmpl_timestamp(undefined, Precision) ->
|
||||||
|
%% not configured, we default it to the message timestamp
|
||||||
|
preproc_tmpl_timestamp(?DEFAULT_TIMESTAMP_TMPL, Precision);
|
||||||
|
preproc_tmpl_timestamp(Ts, Precision) when is_integer(Ts) ->
|
||||||
|
%% a const value is used which is very much unusual, but we have to add a special handling
|
||||||
|
{Ts, Precision, Precision};
|
||||||
|
preproc_tmpl_timestamp(Ts, Precision) when is_list(Ts) ->
|
||||||
|
preproc_tmpl_timestamp(iolist_to_binary(Ts), Precision);
|
||||||
|
preproc_tmpl_timestamp(<<?DEFAULT_TIMESTAMP_TMPL>> = Ts, Precision) ->
|
||||||
|
{emqx_placeholder:preproc_tmpl(Ts), ms, Precision};
|
||||||
|
preproc_tmpl_timestamp(Ts, Precision) when is_binary(Ts) ->
|
||||||
|
%% a placehold is in use. e.g. ${payload.my_timestamp}
|
||||||
|
%% we can only hope it the value will be of the same precision in the configs
|
||||||
|
{emqx_placeholder:preproc_tmpl(Ts), Precision, Precision}.
|
||||||
|
|
||||||
|
to_kv_config(KVfields) ->
|
||||||
|
maps:fold(fun to_maps_config/3, #{}, proplists:to_map(KVfields)).
|
||||||
|
|
||||||
|
to_maps_config(K, V, Res) ->
|
||||||
|
NK = emqx_placeholder:preproc_tmpl(bin(K)),
|
||||||
|
NV = emqx_placeholder:preproc_tmpl(bin(V)),
|
||||||
|
Res#{NK => NV}.
|
||||||
|
|
||||||
|
%% -------------------------------------------------------------------------------------------------
|
||||||
|
%% Tags & Fields Data Trans
|
||||||
|
parse_batch_data(InstId, BatchData, SyntaxLines) ->
|
||||||
|
{Points, Errors} = lists:foldl(
|
||||||
|
fun({send_message, Data}, {ListOfPoints, ErrAccIn}) ->
|
||||||
|
case data_to_points(Data, SyntaxLines) of
|
||||||
|
{ok, Points} ->
|
||||||
|
{[Points | ListOfPoints], ErrAccIn};
|
||||||
|
{error, ErrorPoints} ->
|
||||||
|
log_error_points(InstId, ErrorPoints),
|
||||||
|
{ListOfPoints, ErrAccIn + 1}
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
{[], 0},
|
||||||
|
BatchData
|
||||||
|
),
|
||||||
|
case Errors of
|
||||||
|
0 ->
|
||||||
|
{ok, lists:flatten(Points)};
|
||||||
|
_ ->
|
||||||
|
?SLOG(error, #{
|
||||||
|
msg => io_lib:format("Greptimedb trans point failed, count: ~p", [Errors]),
|
||||||
|
connector => InstId,
|
||||||
|
reason => points_trans_failed
|
||||||
|
}),
|
||||||
|
{error, points_trans_failed}
|
||||||
|
end.
|
||||||
|
|
||||||
|
-spec data_to_points(map(), [
|
||||||
|
#{
|
||||||
|
fields := [{binary(), binary()}],
|
||||||
|
measurement := binary(),
|
||||||
|
tags := [{binary(), binary()}],
|
||||||
|
timestamp := emqx_placeholder:tmpl_token() | integer(),
|
||||||
|
precision := {From :: ts_precision(), To :: ts_precision()}
|
||||||
|
}
|
||||||
|
]) -> {ok, [map()]} | {error, term()}.
|
||||||
|
data_to_points(Data, SyntaxLines) ->
|
||||||
|
lines_to_points(Data, SyntaxLines, [], []).
|
||||||
|
|
||||||
|
%% When converting multiple rows data into Greptimedb Line Protocol, they are considered to be strongly correlated.
|
||||||
|
%% And once a row fails to convert, all of them are considered to have failed.
|
||||||
|
lines_to_points(_, [], Points, ErrorPoints) ->
|
||||||
|
case ErrorPoints of
|
||||||
|
[] ->
|
||||||
|
{ok, Points};
|
||||||
|
_ ->
|
||||||
|
%% ignore trans succeeded points
|
||||||
|
{error, ErrorPoints}
|
||||||
|
end;
|
||||||
|
lines_to_points(Data, [#{timestamp := Ts} = Item | Rest], ResultPointsAcc, ErrorPointsAcc) when
|
||||||
|
is_list(Ts)
|
||||||
|
->
|
||||||
|
TransOptions = #{return => rawlist, var_trans => fun data_filter/1},
|
||||||
|
case parse_timestamp(emqx_placeholder:proc_tmpl(Ts, Data, TransOptions)) of
|
||||||
|
{ok, TsInt} ->
|
||||||
|
Item1 = Item#{timestamp => TsInt},
|
||||||
|
continue_lines_to_points(Data, Item1, Rest, ResultPointsAcc, ErrorPointsAcc);
|
||||||
|
{error, BadTs} ->
|
||||||
|
lines_to_points(Data, Rest, ResultPointsAcc, [
|
||||||
|
{error, {bad_timestamp, BadTs}} | ErrorPointsAcc
|
||||||
|
])
|
||||||
|
end;
|
||||||
|
lines_to_points(Data, [#{timestamp := Ts} = Item | Rest], ResultPointsAcc, ErrorPointsAcc) when
|
||||||
|
is_integer(Ts)
|
||||||
|
->
|
||||||
|
continue_lines_to_points(Data, Item, Rest, ResultPointsAcc, ErrorPointsAcc).
|
||||||
|
|
||||||
|
parse_timestamp([TsInt]) when is_integer(TsInt) ->
|
||||||
|
{ok, TsInt};
|
||||||
|
parse_timestamp([TsBin]) ->
|
||||||
|
try
|
||||||
|
{ok, binary_to_integer(TsBin)}
|
||||||
|
catch
|
||||||
|
_:_ ->
|
||||||
|
{error, TsBin}
|
||||||
|
end.
|
||||||
|
|
||||||
|
continue_lines_to_points(Data, Item, Rest, ResultPointsAcc, ErrorPointsAcc) ->
|
||||||
|
case line_to_point(Data, Item) of
|
||||||
|
#{fields := Fields} when map_size(Fields) =:= 0 ->
|
||||||
|
%% greptimedb client doesn't like empty field maps...
|
||||||
|
ErrorPointsAcc1 = [{error, no_fields} | ErrorPointsAcc],
|
||||||
|
lines_to_points(Data, Rest, ResultPointsAcc, ErrorPointsAcc1);
|
||||||
|
Point ->
|
||||||
|
lines_to_points(Data, Rest, [Point | ResultPointsAcc], ErrorPointsAcc)
|
||||||
|
end.
|
||||||
|
|
||||||
|
line_to_point(
|
||||||
|
Data,
|
||||||
|
#{
|
||||||
|
measurement := Measurement,
|
||||||
|
tags := Tags,
|
||||||
|
fields := Fields,
|
||||||
|
timestamp := Ts,
|
||||||
|
precision := Precision
|
||||||
|
} = Item
|
||||||
|
) ->
|
||||||
|
{_, EncodedTags} = maps:fold(fun maps_config_to_data/3, {Data, #{}}, Tags),
|
||||||
|
{_, EncodedFields} = maps:fold(fun maps_config_to_data/3, {Data, #{}}, Fields),
|
||||||
|
TableName = emqx_placeholder:proc_tmpl(Measurement, Data),
|
||||||
|
{TableName, [
|
||||||
|
maps:without([precision], Item#{
|
||||||
|
tags => EncodedTags,
|
||||||
|
fields => EncodedFields,
|
||||||
|
timestamp => maybe_convert_time_unit(Ts, Precision)
|
||||||
|
})
|
||||||
|
]}.
|
||||||
|
|
||||||
|
maybe_convert_time_unit(Ts, {FromPrecision, ToPrecision}) ->
|
||||||
|
erlang:convert_time_unit(Ts, time_unit(FromPrecision), time_unit(ToPrecision)).
|
||||||
|
|
||||||
|
time_unit(s) -> second;
|
||||||
|
time_unit(ms) -> millisecond;
|
||||||
|
time_unit(us) -> microsecond;
|
||||||
|
time_unit(ns) -> nanosecond.
|
||||||
|
|
||||||
|
maps_config_to_data(K, V, {Data, Res}) ->
|
||||||
|
KTransOptions = #{return => rawlist, var_trans => fun key_filter/1},
|
||||||
|
VTransOptions = #{return => rawlist, var_trans => fun data_filter/1},
|
||||||
|
NK0 = emqx_placeholder:proc_tmpl(K, Data, KTransOptions),
|
||||||
|
NV = emqx_placeholder:proc_tmpl(V, Data, VTransOptions),
|
||||||
|
case {NK0, NV} of
|
||||||
|
{[undefined], _} ->
|
||||||
|
{Data, Res};
|
||||||
|
%% undefined value in normal format [undefined] or int/uint format [undefined, <<"i">>]
|
||||||
|
{_, [undefined | _]} ->
|
||||||
|
{Data, Res};
|
||||||
|
_ ->
|
||||||
|
NK = list_to_binary(NK0),
|
||||||
|
{Data, Res#{NK => value_type(NV)}}
|
||||||
|
end.
|
||||||
|
|
||||||
|
value_type([Int, <<"i">>]) when
|
||||||
|
is_integer(Int)
|
||||||
|
->
|
||||||
|
greptimedb_values:int64_value(Int);
|
||||||
|
value_type([UInt, <<"u">>]) when
|
||||||
|
is_integer(UInt)
|
||||||
|
->
|
||||||
|
greptimedb_values:uint64_value(UInt);
|
||||||
|
value_type([Float]) when is_float(Float) ->
|
||||||
|
Float;
|
||||||
|
value_type([<<"t">>]) ->
|
||||||
|
greptimedb_values:boolean_value(true);
|
||||||
|
value_type([<<"T">>]) ->
|
||||||
|
greptimedb_values:boolean_value(true);
|
||||||
|
value_type([true]) ->
|
||||||
|
greptimedb_values:boolean_value(true);
|
||||||
|
value_type([<<"TRUE">>]) ->
|
||||||
|
greptimedb_values:boolean_value(true);
|
||||||
|
value_type([<<"True">>]) ->
|
||||||
|
greptimedb_values:boolean_value(true);
|
||||||
|
value_type([<<"f">>]) ->
|
||||||
|
greptimedb_values:boolean_value(false);
|
||||||
|
value_type([<<"F">>]) ->
|
||||||
|
greptimedb_values:boolean_value(false);
|
||||||
|
value_type([false]) ->
|
||||||
|
greptimedb_values:boolean_value(false);
|
||||||
|
value_type([<<"FALSE">>]) ->
|
||||||
|
greptimedb_values:boolean_value(false);
|
||||||
|
value_type([<<"False">>]) ->
|
||||||
|
greptimedb_values:boolean_value(false);
|
||||||
|
value_type(Val) ->
|
||||||
|
#{values => #{string_values => Val, datatype => 'STRING'}}.
|
||||||
|
|
||||||
|
key_filter(undefined) -> undefined;
|
||||||
|
key_filter(Value) -> emqx_utils_conv:bin(Value).
|
||||||
|
|
||||||
|
data_filter(undefined) -> undefined;
|
||||||
|
data_filter(Int) when is_integer(Int) -> Int;
|
||||||
|
data_filter(Number) when is_number(Number) -> Number;
|
||||||
|
data_filter(Bool) when is_boolean(Bool) -> Bool;
|
||||||
|
data_filter(Data) -> bin(Data).
|
||||||
|
|
||||||
|
bin(Data) -> emqx_utils_conv:bin(Data).
|
||||||
|
|
||||||
|
%% helper funcs
|
||||||
|
log_error_points(InstId, Errs) ->
|
||||||
|
lists:foreach(
|
||||||
|
fun({error, Reason}) ->
|
||||||
|
?SLOG(error, #{
|
||||||
|
msg => "greptimedb trans point failed",
|
||||||
|
connector => InstId,
|
||||||
|
reason => Reason
|
||||||
|
})
|
||||||
|
end,
|
||||||
|
Errs
|
||||||
|
).
|
||||||
|
|
||||||
|
convert_server(<<"http://", Server/binary>>, HoconOpts) ->
|
||||||
|
convert_server(Server, HoconOpts);
|
||||||
|
convert_server(<<"https://", Server/binary>>, HoconOpts) ->
|
||||||
|
convert_server(Server, HoconOpts);
|
||||||
|
convert_server(Server, HoconOpts) ->
|
||||||
|
emqx_schema:convert_servers(Server, HoconOpts).
|
||||||
|
|
||||||
str(A) when is_atom(A) ->
|
str(A) when is_atom(A) ->
|
||||||
atom_to_list(A);
|
atom_to_list(A);
|
||||||
str(B) when is_binary(B) ->
|
str(B) when is_binary(B) ->
|
||||||
binary_to_list(B);
|
binary_to_list(B);
|
||||||
str(S) when is_list(S) ->
|
str(S) when is_list(S) ->
|
||||||
S.
|
S.
|
||||||
|
|
||||||
|
is_unrecoverable_error({error, {unrecoverable_error, _}}) ->
|
||||||
|
true;
|
||||||
|
is_unrecoverable_error(_) ->
|
||||||
|
false.
|
||||||
|
|
||||||
|
%%===================================================================
|
||||||
|
%% eunit tests
|
||||||
|
%%===================================================================
|
||||||
|
|
||||||
|
-ifdef(TEST).
|
||||||
|
-include_lib("eunit/include/eunit.hrl").
|
||||||
|
|
||||||
|
is_auth_key_test_() ->
|
||||||
|
[
|
||||||
|
?_assert(is_auth_key(<<"Authorization">>)),
|
||||||
|
?_assertNot(is_auth_key(<<"Something">>)),
|
||||||
|
?_assertNot(is_auth_key(89))
|
||||||
|
].
|
||||||
|
|
||||||
|
%% for coverage
|
||||||
|
desc_test_() ->
|
||||||
|
[
|
||||||
|
?_assertMatch(
|
||||||
|
{desc, _, _},
|
||||||
|
desc(common)
|
||||||
|
),
|
||||||
|
?_assertMatch(
|
||||||
|
{desc, _, _},
|
||||||
|
desc(greptimedb_grpc_v1)
|
||||||
|
),
|
||||||
|
?_assertMatch(
|
||||||
|
{desc, _, _},
|
||||||
|
hocon_schema:field_schema(server(), desc)
|
||||||
|
),
|
||||||
|
?_assertMatch(
|
||||||
|
connector_greptimedb,
|
||||||
|
namespace()
|
||||||
|
)
|
||||||
|
].
|
||||||
|
-endif.
|
||||||
|
|
|
@ -0,0 +1,348 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
-module(emqx_bridge_greptimedb_tests).
|
||||||
|
|
||||||
|
-include_lib("eunit/include/eunit.hrl").
|
||||||
|
|
||||||
|
-define(INVALID_LINES, [
|
||||||
|
" ",
|
||||||
|
" \n",
|
||||||
|
" \n\n\n ",
|
||||||
|
"\n",
|
||||||
|
" \n\n \n \n",
|
||||||
|
"measurement",
|
||||||
|
"measurement ",
|
||||||
|
"measurement,tag",
|
||||||
|
"measurement field",
|
||||||
|
"measurement,tag field",
|
||||||
|
"measurement,tag field ${timestamp}",
|
||||||
|
"measurement,tag=",
|
||||||
|
"measurement,tag=tag1",
|
||||||
|
"measurement,tag =",
|
||||||
|
"measurement field=",
|
||||||
|
"measurement field= ",
|
||||||
|
"measurement field = ",
|
||||||
|
"measurement, tag = field = ",
|
||||||
|
"measurement, tag = field = ",
|
||||||
|
"measurement, tag = tag_val field = field_val",
|
||||||
|
"measurement, tag = tag_val field = field_val ${timestamp}",
|
||||||
|
"measurement,= = ${timestamp}",
|
||||||
|
"measurement,t=a, f=a, ${timestamp}",
|
||||||
|
"measurement,t=a,t1=b, f=a,f1=b, ${timestamp}",
|
||||||
|
"measurement,t=a,t1=b, f=a,f1=b,",
|
||||||
|
"measurement,t=a, t1=b, f=a,f1=b,",
|
||||||
|
"measurement,t=a,,t1=b, f=a,f1=b,",
|
||||||
|
"measurement,t=a,,t1=b f=a,,f1=b",
|
||||||
|
"measurement,t=a,,t1=b f=a,f1=b ${timestamp}",
|
||||||
|
"measurement, f=a,f1=b",
|
||||||
|
"measurement, f=a,f1=b ${timestamp}",
|
||||||
|
"measurement,, f=a,f1=b ${timestamp}",
|
||||||
|
"measurement,, f=a,f1=b",
|
||||||
|
"measurement,, f=a,f1=b,, ${timestamp}",
|
||||||
|
"measurement f=a,f1=b,, ${timestamp}",
|
||||||
|
"measurement,t=a f=a,f1=b,, ${timestamp}",
|
||||||
|
"measurement,t=a f=a,f1=b,, ",
|
||||||
|
"measurement,t=a f=a,f1=b,,",
|
||||||
|
"measurement, t=a f=a,f1=b",
|
||||||
|
"measurement,t=a f=a, f1=b",
|
||||||
|
"measurement,t=a f=a, f1=b ${timestamp}",
|
||||||
|
"measurement, t=a f=a, f1=b ${timestamp}",
|
||||||
|
"measurement,t= a f=a,f1=b ${timestamp}",
|
||||||
|
"measurement,t= a f=a,f1 =b ${timestamp}",
|
||||||
|
"measurement, t = a f = a,f1 = b ${timestamp}",
|
||||||
|
"measurement,t=a f=a,f1=b \n ${timestamp}",
|
||||||
|
"measurement,t=a \n f=a,f1=b \n ${timestamp}",
|
||||||
|
"measurement,t=a \n f=a,f1=b \n ",
|
||||||
|
"\n measurement,t=a \n f=a,f1=b \n ${timestamp}",
|
||||||
|
"\n measurement,t=a \n f=a,f1=b \n",
|
||||||
|
%% not escaped backslash in a quoted field value is invalid
|
||||||
|
"measurement,tag=1 field=\"val\\1\""
|
||||||
|
]).
|
||||||
|
|
||||||
|
-define(VALID_LINE_PARSED_PAIRS, [
|
||||||
|
{"m1,tag=tag1 field=field1 ${timestamp1}", #{
|
||||||
|
measurement => "m1",
|
||||||
|
tags => [{"tag", "tag1"}],
|
||||||
|
fields => [{"field", "field1"}],
|
||||||
|
timestamp => "${timestamp1}"
|
||||||
|
}},
|
||||||
|
{"m2,tag=tag2 field=field2", #{
|
||||||
|
measurement => "m2",
|
||||||
|
tags => [{"tag", "tag2"}],
|
||||||
|
fields => [{"field", "field2"}],
|
||||||
|
timestamp => undefined
|
||||||
|
}},
|
||||||
|
{"m3 field=field3 ${timestamp3}", #{
|
||||||
|
measurement => "m3",
|
||||||
|
tags => [],
|
||||||
|
fields => [{"field", "field3"}],
|
||||||
|
timestamp => "${timestamp3}"
|
||||||
|
}},
|
||||||
|
{"m4 field=field4", #{
|
||||||
|
measurement => "m4",
|
||||||
|
tags => [],
|
||||||
|
fields => [{"field", "field4"}],
|
||||||
|
timestamp => undefined
|
||||||
|
}},
|
||||||
|
{"m5,tag=tag5,tag_a=tag5a,tag_b=tag5b field=field5,field_a=field5a,field_b=field5b ${timestamp5}",
|
||||||
|
#{
|
||||||
|
measurement => "m5",
|
||||||
|
tags => [{"tag", "tag5"}, {"tag_a", "tag5a"}, {"tag_b", "tag5b"}],
|
||||||
|
fields => [{"field", "field5"}, {"field_a", "field5a"}, {"field_b", "field5b"}],
|
||||||
|
timestamp => "${timestamp5}"
|
||||||
|
}},
|
||||||
|
{"m6,tag=tag6,tag_a=tag6a,tag_b=tag6b field=field6,field_a=field6a,field_b=field6b", #{
|
||||||
|
measurement => "m6",
|
||||||
|
tags => [{"tag", "tag6"}, {"tag_a", "tag6a"}, {"tag_b", "tag6b"}],
|
||||||
|
fields => [{"field", "field6"}, {"field_a", "field6a"}, {"field_b", "field6b"}],
|
||||||
|
timestamp => undefined
|
||||||
|
}},
|
||||||
|
{"m7,tag=tag7,tag_a=\"tag7a\",tag_b=tag7b field=\"field7\",field_a=field7a,field_b=\"field7b\"",
|
||||||
|
#{
|
||||||
|
measurement => "m7",
|
||||||
|
tags => [{"tag", "tag7"}, {"tag_a", "\"tag7a\""}, {"tag_b", "tag7b"}],
|
||||||
|
fields => [{"field", "field7"}, {"field_a", "field7a"}, {"field_b", "field7b"}],
|
||||||
|
timestamp => undefined
|
||||||
|
}},
|
||||||
|
{"m8,tag=tag8,tag_a=\"tag8a\",tag_b=tag8b field=\"field8\",field_a=field8a,field_b=\"field8b\" ${timestamp8}",
|
||||||
|
#{
|
||||||
|
measurement => "m8",
|
||||||
|
tags => [{"tag", "tag8"}, {"tag_a", "\"tag8a\""}, {"tag_b", "tag8b"}],
|
||||||
|
fields => [{"field", "field8"}, {"field_a", "field8a"}, {"field_b", "field8b"}],
|
||||||
|
timestamp => "${timestamp8}"
|
||||||
|
}},
|
||||||
|
{"m9,tag=tag9,tag_a=\"tag9a\",tag_b=tag9b field=\"field9\",field_a=field9a,field_b=\"\" ${timestamp9}",
|
||||||
|
#{
|
||||||
|
measurement => "m9",
|
||||||
|
tags => [{"tag", "tag9"}, {"tag_a", "\"tag9a\""}, {"tag_b", "tag9b"}],
|
||||||
|
fields => [{"field", "field9"}, {"field_a", "field9a"}, {"field_b", ""}],
|
||||||
|
timestamp => "${timestamp9}"
|
||||||
|
}},
|
||||||
|
{"m10 field=\"\" ${timestamp10}", #{
|
||||||
|
measurement => "m10",
|
||||||
|
tags => [],
|
||||||
|
fields => [{"field", ""}],
|
||||||
|
timestamp => "${timestamp10}"
|
||||||
|
}}
|
||||||
|
]).
|
||||||
|
|
||||||
|
-define(VALID_LINE_EXTRA_SPACES_PARSED_PAIRS, [
|
||||||
|
{"\n m1,tag=tag1 field=field1 ${timestamp1} \n", #{
|
||||||
|
measurement => "m1",
|
||||||
|
tags => [{"tag", "tag1"}],
|
||||||
|
fields => [{"field", "field1"}],
|
||||||
|
timestamp => "${timestamp1}"
|
||||||
|
}},
|
||||||
|
{" m2,tag=tag2 field=field2 ", #{
|
||||||
|
measurement => "m2",
|
||||||
|
tags => [{"tag", "tag2"}],
|
||||||
|
fields => [{"field", "field2"}],
|
||||||
|
timestamp => undefined
|
||||||
|
}},
|
||||||
|
{" m3 field=field3 ${timestamp3} ", #{
|
||||||
|
measurement => "m3",
|
||||||
|
tags => [],
|
||||||
|
fields => [{"field", "field3"}],
|
||||||
|
timestamp => "${timestamp3}"
|
||||||
|
}},
|
||||||
|
{" \n m4 field=field4\n ", #{
|
||||||
|
measurement => "m4",
|
||||||
|
tags => [],
|
||||||
|
fields => [{"field", "field4"}],
|
||||||
|
timestamp => undefined
|
||||||
|
}},
|
||||||
|
{" \n m5,tag=tag5,tag_a=tag5a,tag_b=tag5b field=field5,field_a=field5a,field_b=field5b ${timestamp5} \n",
|
||||||
|
#{
|
||||||
|
measurement => "m5",
|
||||||
|
tags => [{"tag", "tag5"}, {"tag_a", "tag5a"}, {"tag_b", "tag5b"}],
|
||||||
|
fields => [{"field", "field5"}, {"field_a", "field5a"}, {"field_b", "field5b"}],
|
||||||
|
timestamp => "${timestamp5}"
|
||||||
|
}},
|
||||||
|
{" m6,tag=tag6,tag_a=tag6a,tag_b=tag6b field=field6,field_a=field6a,field_b=field6b\n ", #{
|
||||||
|
measurement => "m6",
|
||||||
|
tags => [{"tag", "tag6"}, {"tag_a", "tag6a"}, {"tag_b", "tag6b"}],
|
||||||
|
fields => [{"field", "field6"}, {"field_a", "field6a"}, {"field_b", "field6b"}],
|
||||||
|
timestamp => undefined
|
||||||
|
}}
|
||||||
|
]).
|
||||||
|
|
||||||
|
-define(VALID_LINE_PARSED_ESCAPED_CHARS_PAIRS, [
|
||||||
|
{"m\\ =1\\,,\\,tag\\ \\==\\=tag\\ 1\\, \\,fie\\ ld\\ =\\ field\\,1 ${timestamp1}", #{
|
||||||
|
measurement => "m =1,",
|
||||||
|
tags => [{",tag =", "=tag 1,"}],
|
||||||
|
fields => [{",fie ld ", " field,1"}],
|
||||||
|
timestamp => "${timestamp1}"
|
||||||
|
}},
|
||||||
|
{"m2,tag=tag2 field=\"field \\\"2\\\",\n\"", #{
|
||||||
|
measurement => "m2",
|
||||||
|
tags => [{"tag", "tag2"}],
|
||||||
|
fields => [{"field", "field \"2\",\n"}],
|
||||||
|
timestamp => undefined
|
||||||
|
}},
|
||||||
|
{"m\\ 3 field=\"field3\" ${payload.timestamp\\ 3}", #{
|
||||||
|
measurement => "m 3",
|
||||||
|
tags => [],
|
||||||
|
fields => [{"field", "field3"}],
|
||||||
|
timestamp => "${payload.timestamp 3}"
|
||||||
|
}},
|
||||||
|
{"m4 field=\"\\\"field\\\\4\\\"\"", #{
|
||||||
|
measurement => "m4",
|
||||||
|
tags => [],
|
||||||
|
fields => [{"field", "\"field\\4\""}],
|
||||||
|
timestamp => undefined
|
||||||
|
}},
|
||||||
|
{
|
||||||
|
"m5\\,mA,tag=\\=tag5\\=,\\,tag_a\\,=tag\\ 5a,tag_b=tag5b \\ field\\ =field5,"
|
||||||
|
"field\\ _\\ a=field5a,\\,field_b\\ =\\=\\,\\ field5b ${timestamp5}",
|
||||||
|
#{
|
||||||
|
measurement => "m5,mA",
|
||||||
|
tags => [{"tag", "=tag5="}, {",tag_a,", "tag 5a"}, {"tag_b", "tag5b"}],
|
||||||
|
fields => [
|
||||||
|
{" field ", "field5"}, {"field _ a", "field5a"}, {",field_b ", "=, field5b"}
|
||||||
|
],
|
||||||
|
timestamp => "${timestamp5}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{"m6,tag=tag6,tag_a=tag6a,tag_b=tag6b field=\"field6\",field_a=\"field6a\",field_b=\"field6b\"",
|
||||||
|
#{
|
||||||
|
measurement => "m6",
|
||||||
|
tags => [{"tag", "tag6"}, {"tag_a", "tag6a"}, {"tag_b", "tag6b"}],
|
||||||
|
fields => [{"field", "field6"}, {"field_a", "field6a"}, {"field_b", "field6b"}],
|
||||||
|
timestamp => undefined
|
||||||
|
}},
|
||||||
|
{
|
||||||
|
"\\ \\ m7\\ \\ ,tag=\\ tag\\,7\\ ,tag_a=\"tag7a\",tag_b\\,tag1=tag7b field=\"field7\","
|
||||||
|
"field_a=field7a,field_b=\"field7b\\\\\n\"",
|
||||||
|
#{
|
||||||
|
measurement => " m7 ",
|
||||||
|
tags => [{"tag", " tag,7 "}, {"tag_a", "\"tag7a\""}, {"tag_b,tag1", "tag7b"}],
|
||||||
|
fields => [{"field", "field7"}, {"field_a", "field7a"}, {"field_b", "field7b\\\n"}],
|
||||||
|
timestamp => undefined
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"m8,tag=tag8,tag_a=\"tag8a\",tag_b=tag8b field=\"field8\",field_a=field8a,"
|
||||||
|
"field_b=\"\\\"field\\\" = 8b\" ${timestamp8}",
|
||||||
|
#{
|
||||||
|
measurement => "m8",
|
||||||
|
tags => [{"tag", "tag8"}, {"tag_a", "\"tag8a\""}, {"tag_b", "tag8b"}],
|
||||||
|
fields => [{"field", "field8"}, {"field_a", "field8a"}, {"field_b", "\"field\" = 8b"}],
|
||||||
|
timestamp => "${timestamp8}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{"m\\9,tag=tag9,tag_a=\"tag9a\",tag_b=tag9b field\\=field=\"field9\",field_a=field9a,field_b=\"\" ${timestamp9}",
|
||||||
|
#{
|
||||||
|
measurement => "m\\9",
|
||||||
|
tags => [{"tag", "tag9"}, {"tag_a", "\"tag9a\""}, {"tag_b", "tag9b"}],
|
||||||
|
fields => [{"field=field", "field9"}, {"field_a", "field9a"}, {"field_b", ""}],
|
||||||
|
timestamp => "${timestamp9}"
|
||||||
|
}},
|
||||||
|
{"m\\,10 \"field\\\\\"=\"\" ${timestamp10}", #{
|
||||||
|
measurement => "m,10",
|
||||||
|
tags => [],
|
||||||
|
%% backslash should not be un-escaped in tag key
|
||||||
|
fields => [{"\"field\\\\\"", ""}],
|
||||||
|
timestamp => "${timestamp10}"
|
||||||
|
}}
|
||||||
|
]).
|
||||||
|
|
||||||
|
-define(VALID_LINE_PARSED_ESCAPED_CHARS_EXTRA_SPACES_PAIRS, [
|
||||||
|
{" \n m\\ =1\\,,\\,tag\\ \\==\\=tag\\ 1\\, \\,fie\\ ld\\ =\\ field\\,1 ${timestamp1} ", #{
|
||||||
|
measurement => "m =1,",
|
||||||
|
tags => [{",tag =", "=tag 1,"}],
|
||||||
|
fields => [{",fie ld ", " field,1"}],
|
||||||
|
timestamp => "${timestamp1}"
|
||||||
|
}},
|
||||||
|
{" m2,tag=tag2 field=\"field \\\"2\\\",\n\" ", #{
|
||||||
|
measurement => "m2",
|
||||||
|
tags => [{"tag", "tag2"}],
|
||||||
|
fields => [{"field", "field \"2\",\n"}],
|
||||||
|
timestamp => undefined
|
||||||
|
}},
|
||||||
|
{" m\\ 3 field=\"field3\" ${payload.timestamp\\ 3} ", #{
|
||||||
|
measurement => "m 3",
|
||||||
|
tags => [],
|
||||||
|
fields => [{"field", "field3"}],
|
||||||
|
timestamp => "${payload.timestamp 3}"
|
||||||
|
}},
|
||||||
|
{" m4 field=\"\\\"field\\\\4\\\"\" ", #{
|
||||||
|
measurement => "m4",
|
||||||
|
tags => [],
|
||||||
|
fields => [{"field", "\"field\\4\""}],
|
||||||
|
timestamp => undefined
|
||||||
|
}},
|
||||||
|
{
|
||||||
|
" m5\\,mA,tag=\\=tag5\\=,\\,tag_a\\,=tag\\ 5a,tag_b=tag5b \\ field\\ =field5,"
|
||||||
|
"field\\ _\\ a=field5a,\\,field_b\\ =\\=\\,\\ field5b ${timestamp5} ",
|
||||||
|
#{
|
||||||
|
measurement => "m5,mA",
|
||||||
|
tags => [{"tag", "=tag5="}, {",tag_a,", "tag 5a"}, {"tag_b", "tag5b"}],
|
||||||
|
fields => [
|
||||||
|
{" field ", "field5"}, {"field _ a", "field5a"}, {",field_b ", "=, field5b"}
|
||||||
|
],
|
||||||
|
timestamp => "${timestamp5}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{" m6,tag=tag6,tag_a=tag6a,tag_b=tag6b field=\"field6\",field_a=\"field6a\",field_b=\"field6b\" ",
|
||||||
|
#{
|
||||||
|
measurement => "m6",
|
||||||
|
tags => [{"tag", "tag6"}, {"tag_a", "tag6a"}, {"tag_b", "tag6b"}],
|
||||||
|
fields => [{"field", "field6"}, {"field_a", "field6a"}, {"field_b", "field6b"}],
|
||||||
|
timestamp => undefined
|
||||||
|
}}
|
||||||
|
]).
|
||||||
|
|
||||||
|
invalid_write_syntax_line_test_() ->
|
||||||
|
[?_assertThrow(_, to_influx_lines(L)) || L <- ?INVALID_LINES].
|
||||||
|
|
||||||
|
invalid_write_syntax_multiline_test_() ->
|
||||||
|
LinesList = [
|
||||||
|
join("\n", ?INVALID_LINES),
|
||||||
|
join("\n\n\n", ?INVALID_LINES),
|
||||||
|
join("\n\n", lists:reverse(?INVALID_LINES))
|
||||||
|
],
|
||||||
|
[?_assertThrow(_, to_influx_lines(Lines)) || Lines <- LinesList].
|
||||||
|
|
||||||
|
valid_write_syntax_test_() ->
|
||||||
|
test_pairs(?VALID_LINE_PARSED_PAIRS).
|
||||||
|
|
||||||
|
valid_write_syntax_with_extra_spaces_test_() ->
|
||||||
|
test_pairs(?VALID_LINE_EXTRA_SPACES_PARSED_PAIRS).
|
||||||
|
|
||||||
|
valid_write_syntax_escaped_chars_test_() ->
|
||||||
|
test_pairs(?VALID_LINE_PARSED_ESCAPED_CHARS_PAIRS).
|
||||||
|
|
||||||
|
valid_write_syntax_escaped_chars_with_extra_spaces_test_() ->
|
||||||
|
test_pairs(?VALID_LINE_PARSED_ESCAPED_CHARS_EXTRA_SPACES_PAIRS).
|
||||||
|
|
||||||
|
test_pairs(PairsList) ->
|
||||||
|
{Lines, AllExpected} = lists:unzip(PairsList),
|
||||||
|
JoinedLines = join("\n", Lines),
|
||||||
|
JoinedLines1 = join("\n\n\n", Lines),
|
||||||
|
JoinedLines2 = join("\n\n", lists:reverse(Lines)),
|
||||||
|
SingleLineTests =
|
||||||
|
[
|
||||||
|
?_assertEqual([Expected], to_influx_lines(Line))
|
||||||
|
|| {Line, Expected} <- PairsList
|
||||||
|
],
|
||||||
|
JoinedLinesTests =
|
||||||
|
[
|
||||||
|
?_assertEqual(AllExpected, to_influx_lines(JoinedLines)),
|
||||||
|
?_assertEqual(AllExpected, to_influx_lines(JoinedLines1)),
|
||||||
|
?_assertEqual(lists:reverse(AllExpected), to_influx_lines(JoinedLines2))
|
||||||
|
],
|
||||||
|
SingleLineTests ++ JoinedLinesTests.
|
||||||
|
|
||||||
|
join(Sep, LinesList) ->
|
||||||
|
lists:flatten(lists:join(Sep, LinesList)).
|
||||||
|
|
||||||
|
to_influx_lines(RawLines) ->
|
||||||
|
OldLevel = emqx_logger:get_primary_log_level(),
|
||||||
|
try
|
||||||
|
%% mute error logs from this call
|
||||||
|
emqx_logger:set_primary_log_level(none),
|
||||||
|
emqx_bridge_greptimedb:to_influx_lines(RawLines)
|
||||||
|
after
|
||||||
|
emqx_logger:set_primary_log_level(OldLevel)
|
||||||
|
end.
|
Loading…
Reference in New Issue