feat(iotdb): improve the IoTDB bridge to v2 style

This commit is contained in:
firest 2024-01-02 20:02:28 +08:00
parent a87df28dfc
commit d16458ccd0
13 changed files with 666 additions and 170 deletions

View File

@ -84,7 +84,8 @@ hard_coded_action_info_modules_ee() ->
emqx_bridge_pgsql_action_info, emqx_bridge_pgsql_action_info,
emqx_bridge_syskeeper_action_info, emqx_bridge_syskeeper_action_info,
emqx_bridge_timescale_action_info, emqx_bridge_timescale_action_info,
emqx_bridge_redis_action_info emqx_bridge_redis_action_info,
emqx_bridge_iotdb_action_info
]. ].
-else. -else.
hard_coded_action_info_modules_ee() -> hard_coded_action_info_modules_ee() ->

View File

@ -25,7 +25,7 @@
]). ]).
%% emqx_connector_resource behaviour callbacks %% emqx_connector_resource behaviour callbacks
-export([connector_config/1]). -export([connector_config/2]).
-export([producer_converter/2, host_opts/0]). -export([producer_converter/2, host_opts/0]).
@ -326,7 +326,7 @@ values(producer) ->
%% `emqx_connector_resource' API %% `emqx_connector_resource' API
%%------------------------------------------------------------------------------------------------- %%-------------------------------------------------------------------------------------------------
connector_config(Config) -> connector_config(Config, _) ->
%% Default port for AEH is 9093 %% Default port for AEH is 9093
BootstrapHosts0 = maps:get(bootstrap_hosts, Config), BootstrapHosts0 = maps:get(bootstrap_hosts, Config),
BootstrapHosts = emqx_schema:parse_servers( BootstrapHosts = emqx_schema:parse_servers(

View File

@ -24,7 +24,7 @@
]). ]).
%% emqx_connector_resource behaviour callbacks %% emqx_connector_resource behaviour callbacks
-export([connector_config/1]). -export([connector_config/2]).
-export([host_opts/0]). -export([host_opts/0]).
@ -288,7 +288,7 @@ values(action) ->
%% `emqx_connector_resource' API %% `emqx_connector_resource' API
%%------------------------------------------------------------------------------------------------- %%-------------------------------------------------------------------------------------------------
connector_config(Config) -> connector_config(Config, _) ->
%% Default port for Confluent is 9092 %% Default port for Confluent is 9092
BootstrapHosts0 = maps:get(bootstrap_hosts, Config), BootstrapHosts0 = maps:get(bootstrap_hosts, Config),
BootstrapHosts = emqx_schema:parse_servers( BootstrapHosts = emqx_schema:parse_servers(

View File

@ -133,17 +133,8 @@ fields("get_" ++ Type) ->
fields("config_bridge_v2") -> fields("config_bridge_v2") ->
fields("http_action"); fields("http_action");
fields("config_connector") -> fields("config_connector") ->
[ emqx_connector_schema:common_fields() ++
{enable, connector_url_headers() ++
mk(
boolean(),
#{
desc => <<"Enable or disable this connector">>,
default => true
}
)},
{description, emqx_schema:description_schema()}
] ++ connector_url_headers() ++
connector_opts() ++ connector_opts() ++
emqx_connector_schema:resource_opts_ref(?MODULE, connector_resource_opts); emqx_connector_schema:resource_opts_ref(?MODULE, connector_resource_opts);
fields(connector_resource_opts) -> fields(connector_resource_opts) ->

View File

@ -1,5 +1,5 @@
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved. %% Copyright (c) 2023-2024 EMQ Technologies Co., Ltd. All Rights Reserved.
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
-module(emqx_bridge_iotdb). -module(emqx_bridge_iotdb).
@ -8,7 +8,12 @@
-include_lib("hocon/include/hoconsc.hrl"). -include_lib("hocon/include/hoconsc.hrl").
-include_lib("emqx_resource/include/emqx_resource.hrl"). -include_lib("emqx_resource/include/emqx_resource.hrl").
-import(hoconsc, [mk/2, enum/1, ref/2]). -import(hoconsc, [mk/2, enum/1, ref/2, array/1]).
-export([
bridge_v2_examples/1,
conn_bridge_examples/1
]).
%% hocon_schema API %% hocon_schema API
-export([ -export([
@ -18,8 +23,8 @@
desc/1 desc/1
]). ]).
%% emqx_bridge_enterprise "unofficial" API -define(CONNECTOR_TYPE, iotdb).
-export([conn_bridge_examples/1]). -define(ACTION_TYPE, ?CONNECTOR_TYPE).
%%------------------------------------------------------------------------------------------------- %%-------------------------------------------------------------------------------------------------
%% `hocon_schema' API %% `hocon_schema' API
@ -29,24 +34,140 @@ namespace() -> "bridge_iotdb".
roots() -> []. roots() -> [].
fields("config") -> %%-------------------------------------------------------------------------------------------------
basic_config() ++ request_config(); %% v2 schema
fields("post") -> %%-------------------------------------------------------------------------------------------------
fields(action) ->
{iotdb,
mk(
hoconsc:map(name, ref(?MODULE, action_config)),
#{
desc => <<"IoTDB Action Config">>,
required => false
}
)};
fields(action_config) ->
emqx_resource_schema:override(
emqx_bridge_v2_schema:make_producer_action_schema(
mk(
ref(?MODULE, action_parameters),
#{
required => true, desc => ?DESC("action_parameters")
}
)
),
[ [
type_field(), {resource_opts,
name_field() mk(ref(?MODULE, action_resource_opts), #{
] ++ fields("config"); default => #{},
fields("put") -> desc => ?DESC(emqx_resource_schema, "resource_opts")
fields("config"); })}
fields("get") -> ]
emqx_bridge_schema:status_fields() ++ fields("post"); );
fields("creation_opts") -> fields(action_resource_opts) ->
lists:filter( lists:filter(
fun({K, _V}) -> fun({K, _V}) ->
not lists:member(K, unsupported_opts()) not lists:member(K, unsupported_opts())
end, end,
emqx_resource_schema:fields("creation_opts") emqx_bridge_v2_schema:resource_opts_fields()
); );
fields(action_parameters) ->
[
{is_aligned,
mk(
boolean(),
#{
desc => ?DESC("config_is_aligned"),
default => false
}
)},
{device_id,
mk(
binary(),
#{
desc => ?DESC("config_device_id")
}
)},
{iotdb_version,
mk(
hoconsc:enum([?VSN_1_1_X, ?VSN_1_0_X, ?VSN_0_13_X]),
#{
desc => ?DESC("config_iotdb_version"),
default => ?VSN_1_1_X
}
)},
{data,
mk(
array(ref(?MODULE, action_parameters_data)),
#{
desc => ?DESC("action_parameters_data")
}
)}
] ++
proplists_without(
[path, method, body, headers, request_timeout],
emqx_bridge_http_schema:fields("parameters_opts")
);
fields(action_parameters_data) ->
[
{timestamp,
mk(
binary(),
#{
desc => ?DESC("config_parameters_timestamp"),
default => <<"now">>
}
)},
{measurement,
mk(
binary(),
#{
required => true,
desc => ?DESC("config_parameters_measurement")
}
)},
{data_type,
mk(
binary(),
#{
required => true,
desc => ?DESC("config_parameters_data_type"),
validator => fun(Type) ->
lists:member(Type, [
<<"TEXT">>,
<<"BOOLEAN">>,
<<"INT32">>,
<<"INT64">>,
<<"FLOAT">>,
<<"DOUBLE">>
])
end
}
)},
{value,
mk(
binary(),
#{
required => true,
desc => ?DESC("config_parameters_value")
}
)}
];
fields("post_bridge_v2") ->
emqx_bridge_schema:type_and_name_fields(enum([iotdb])) ++ fields(action_config);
fields("put_bridge_v2") ->
fields(action_config);
fields("get_bridge_v2") ->
emqx_bridge_schema:status_fields() ++ fields("post_bridge_v2");
%%-------------------------------------------------------------------------------------------------
%% v1 schema
%%-------------------------------------------------------------------------------------------------
fields("config") ->
basic_config() ++ request_config();
fields("creation_opts") ->
proplists_without(unsupported_opts(), emqx_resource_schema:fields("creation_opts"));
fields(auth_basic) -> fields(auth_basic) ->
[ [
{username, mk(binary(), #{required => true, desc => ?DESC("config_auth_basic_username")})}, {username, mk(binary(), #{required => true, desc => ?DESC("config_auth_basic_username")})},
@ -55,22 +176,28 @@ fields(auth_basic) ->
required => true, required => true,
desc => ?DESC("config_auth_basic_password") desc => ?DESC("config_auth_basic_password")
})} })}
]. ];
fields("post") ->
emqx_bridge_schema:type_and_name_fields(enum([iotdb])) ++ fields("config");
fields("put") ->
fields("config");
fields("get") ->
emqx_bridge_schema:status_fields() ++ fields("post").
desc("config") -> desc("config") ->
?DESC("desc_config"); ?DESC("desc_config");
desc("creation_opts") -> desc(action_config) ->
?DESC(emqx_resource_schema, "creation_opts"); ?DESC("desc_config");
desc("post") -> desc(action_parameters) ->
["Configuration for IoTDB using `POST` method."]; ?DESC("action_parameters");
desc(Name) -> desc(action_parameters_data) ->
lists:member(Name, struct_names()) orelse throw({missing_desc, Name}), ?DESC("action_parameters_data");
?DESC(Name). desc(auth_basic) ->
"Basic Authentication";
struct_names() -> desc(Method) when Method =:= "get"; Method =:= "put"; Method =:= "post" ->
[ ["Configuration for IoTDB using `", string:to_upper(Method), "` method."];
auth_basic desc(_) ->
]. undefined.
basic_config() -> basic_config() ->
[ [
@ -160,30 +287,43 @@ unsupported_opts() ->
batch_time batch_time
]. ].
%%====================================================================================== %%-------------------------------------------------------------------------------------------------
%% v2 examples
%%-------------------------------------------------------------------------------------------------
type_field() -> bridge_v2_examples(Method) ->
{type, [
mk(
hoconsc:enum([iotdb]),
#{ #{
required => true, <<"iotdb">> =>
desc => ?DESC("desc_type")
}
)}.
name_field() ->
{name,
mk(
binary(),
#{ #{
required => true, summary => <<"Apache IoTDB Bridge">>,
desc => ?DESC("desc_name") value => emqx_bridge_v2_schema:action_values(
Method, ?ACTION_TYPE, ?CONNECTOR_TYPE, action_values()
)
} }
)}. }
].
%%====================================================================================== action_values() ->
#{
parameters => #{
data => [
#{
timestamp => now,
measurement => <<"status">>,
data_type => <<"BOOLEAN">>,
value => <<"${st}">>
}
],
is_aligned => false,
device_id => <<"${clientid}">>,
iotdb_version => ?VSN_1_1_X
}
}.
%%-------------------------------------------------------------------------------------------------
%% v1 examples
%%-------------------------------------------------------------------------------------------------
conn_bridge_examples(Method) -> conn_bridge_examples(Method) ->
[ [
#{ #{

View File

@ -0,0 +1,71 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2023-2024 EMQ Technologies Co., Ltd. All Rights Reserved.
%%--------------------------------------------------------------------
-module(emqx_bridge_iotdb_action_info).
-behaviour(emqx_action_info).
-elvis([{elvis_style, invalid_dynamic_call, disable}]).
%% behaviour callbacks
-export([
action_type_name/0,
bridge_v1_config_to_action_config/2,
bridge_v1_config_to_connector_config/1,
bridge_v1_type_name/0,
connector_action_config_to_bridge_v1_config/2,
connector_type_name/0,
schema_module/0
]).
-import(emqx_utils_conv, [bin/1]).
-define(ACTION_TYPE, iotdb).
-define(SCHEMA_MODULE, emqx_bridge_iotdb).
action_type_name() -> ?ACTION_TYPE.
bridge_v1_type_name() -> ?ACTION_TYPE.
connector_type_name() -> ?ACTION_TYPE.
schema_module() -> ?SCHEMA_MODULE.
connector_action_config_to_bridge_v1_config(ConnectorConfig, ActionConfig) ->
MergedConfig =
emqx_utils_maps:deep_merge(
maps:without(
[<<"description">>, <<"local_topic">>, <<"connector">>, <<"data">>],
emqx_utils_maps:unindent(<<"parameters">>, ActionConfig)
),
ConnectorConfig
),
BridgeV1Keys = schema_keys("config"),
maps:with(BridgeV1Keys, MergedConfig).
bridge_v1_config_to_action_config(BridgeV1Config, ConnectorName) ->
ActionTopLevelKeys = schema_keys(action_config),
ActionParametersKeys = schema_keys(action_parameters),
ActionKeys = ActionTopLevelKeys ++ ActionParametersKeys,
ActionConfig = make_config_map(ActionKeys, ActionParametersKeys, BridgeV1Config),
emqx_utils_maps:update_if_present(
<<"resource_opts">>,
fun emqx_bridge_v2_schema:project_to_actions_resource_opts/1,
ActionConfig#{<<"connector">> => ConnectorName}
).
bridge_v1_config_to_connector_config(BridgeV1Config) ->
ConnectorKeys = schema_keys(emqx_bridge_iotdb_connector, config),
emqx_utils_maps:update_if_present(
<<"resource_opts">>,
fun emqx_connector_schema:project_to_connector_resource_opts/1,
maps:with(ConnectorKeys, BridgeV1Config)
).
make_config_map(PickKeys, IndentKeys, Config) ->
Conf0 = maps:with(PickKeys, Config#{<<"data">> => []}),
emqx_utils_maps:indent(<<"parameters">>, IndentKeys, Conf0).
schema_keys(Name) ->
schema_keys(?SCHEMA_MODULE, Name).
schema_keys(Mod, Name) ->
[bin(Key) || Key <- proplists:get_keys(Mod:fields(Name))].

View File

@ -1,10 +1,13 @@
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved. %% Copyright (c) 2023-2024 EMQ Technologies Co., Ltd. All Rights Reserved.
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
-module(emqx_bridge_iotdb_impl). -module(emqx_bridge_iotdb_connector).
-behaviour(emqx_resource).
-include("emqx_bridge_iotdb.hrl"). -include("emqx_bridge_iotdb.hrl").
-include_lib("emqx/include/logger.hrl"). -include_lib("emqx/include/logger.hrl").
-include_lib("hocon/include/hoconsc.hrl").
-include_lib("snabbkaffe/include/snabbkaffe.hrl"). -include_lib("snabbkaffe/include/snabbkaffe.hrl").
%% `emqx_resource' API %% `emqx_resource' API
@ -14,9 +17,25 @@
on_stop/2, on_stop/2,
on_get_status/2, on_get_status/2,
on_query/3, on_query/3,
on_query_async/4 on_query_async/4,
on_add_channel/4,
on_remove_channel/3,
on_get_channels/1,
on_get_channel_status/3
]). ]).
-export([
namespace/0,
roots/0,
fields/1,
desc/1,
connector_examples/1,
connector_example_values/0
]).
%% emqx_connector_resource behaviour callbacks
-export([connector_config/2]).
-type config() :: -type config() ::
#{ #{
base_url := #{ base_url := #{
@ -29,33 +48,140 @@
pool_type := random | hash, pool_type := random | hash,
pool_size := pos_integer(), pool_size := pos_integer(),
request => undefined | map(), request => undefined | map(),
is_aligned => boolean(),
iotdb_version => binary(),
device_id => binary() | undefined,
atom() => _ atom() => _
}. }.
-type state() :: -type state() ::
#{ #{
base_path := _, base_path := _,
base_url := #{
scheme := http | https,
host := iolist(),
port := inet:port_number(),
path := _
},
connect_timeout := pos_integer(), connect_timeout := pos_integer(),
pool_type := random | hash, pool_type := random | hash,
pool_size := pos_integer(), channels := map(),
request => undefined | map(), request => undefined | map(),
is_aligned => boolean(),
iotdb_version => binary(),
device_id => binary() | undefined,
atom() => _ atom() => _
}. }.
-type manager_id() :: binary(). -type manager_id() :: binary().
-define(CONNECTOR_TYPE, iotdb).
-import(hoconsc, [mk/2, enum/1, ref/2]).
%%-------------------------------------------------------------------------------------
%% connector examples
%%-------------------------------------------------------------------------------------
connector_examples(Method) ->
[
#{
<<"iotdb">> =>
#{
summary => <<"Apache IoTDB Connector">>,
value => emqx_connector_schema:connector_values(
Method, ?CONNECTOR_TYPE, connector_example_values()
)
}
}
].
connector_example_values() ->
#{
name => <<"iotdb_connector">>,
type => iotdb,
enable => true,
authentication => #{
<<"username">> => <<"root">>,
<<"password">> => <<"*****">>
},
base_url => <<"http://iotdb.local:18080/">>,
connect_timeout => <<"15s">>,
pool_type => <<"random">>,
pool_size => 8,
enable_pipelining => 100,
ssl => #{enable => false}
}.
%%-------------------------------------------------------------------------------------
%% schema
%%-------------------------------------------------------------------------------------
namespace() -> "iotdb".
roots() ->
[{config, #{type => hoconsc:ref(?MODULE, config)}}].
fields(config) ->
proplists_without([url, headers], emqx_bridge_http_schema:fields("config_connector")) ++
fields("connection_fields");
fields("connection_fields") ->
[
{base_url,
mk(
emqx_schema:url(),
#{
required => true,
desc => ?DESC(emqx_bridge_iotdb, "config_base_url")
}
)},
{authentication,
mk(
hoconsc:union([ref(?MODULE, auth_basic)]),
#{
default => auth_basic, desc => ?DESC("config_authentication")
}
)}
];
fields(auth_basic) ->
[
{username, mk(binary(), #{required => true, desc => ?DESC("config_auth_basic_username")})},
{password,
emqx_schema_secret:mk(#{
required => true,
desc => ?DESC("config_auth_basic_password")
})}
];
fields("post") ->
emqx_connector_schema:type_and_name_fields(enum([iotdb])) ++ fields(config);
fields("put") ->
fields(config);
fields("get") ->
emqx_bridge_schema:status_fields() ++ fields("post").
desc(config) ->
?DESC("desc_config");
desc(Method) when Method =:= "get"; Method =:= "put"; Method =:= "post" ->
["Configuration for IoTDB using `", string:to_upper(Method), "` method."];
desc(_) ->
undefined.
connector_config(Conf, #{name := Name, parse_confs := ParseConfs}) ->
#{
base_url := BaseUrl,
authentication :=
#{
username := Username,
password := Password0
}
} = Conf,
Password = emqx_secret:unwrap(Password0),
BasicToken = base64:encode(<<Username/binary, ":", Password/binary>>),
WebhookConfig =
Conf#{
url => BaseUrl,
headers => [
{<<"Content-type">>, <<"application/json">>},
{<<"Authorization">>, BasicToken}
]
},
ParseConfs(
<<"http">>,
Name,
WebhookConfig
).
proplists_without(Keys, List) ->
[El || El = {K, _} <- List, not lists:member(K, Keys)].
%%------------------------------------------------------------------------------------- %%-------------------------------------------------------------------------------------
%% `emqx_resource' API %% `emqx_resource' API
%%------------------------------------------------------------------------------------- %%-------------------------------------------------------------------------------------
@ -73,7 +199,7 @@ on_start(InstanceId, Config) ->
request => maps:get(request, State, <<>>) request => maps:get(request, State, <<>>)
}), }),
?tp(iotdb_bridge_started, #{instance_id => InstanceId}), ?tp(iotdb_bridge_started, #{instance_id => InstanceId}),
{ok, maps:merge(Config, State)}; {ok, State#{channels => #{}}};
{error, Reason} -> {error, Reason} ->
?SLOG(error, #{ ?SLOG(error, #{
msg => "failed_to_start_iotdb_bridge", msg => "failed_to_start_iotdb_bridge",
@ -103,19 +229,20 @@ on_get_status(InstanceId, State) ->
{ok, pos_integer(), [term()], term()} {ok, pos_integer(), [term()], term()}
| {ok, pos_integer(), [term()]} | {ok, pos_integer(), [term()]}
| {error, term()}. | {error, term()}.
on_query(InstanceId, {send_message, Message}, State) -> on_query(InstanceId, {ChannelId, _Message} = Req, #{channels := Channels} = State) ->
?tp(iotdb_bridge_on_query, #{instance_id => InstanceId}), ?tp(iotdb_bridge_on_query, #{instance_id => InstanceId}),
?SLOG(debug, #{ ?SLOG(debug, #{
msg => "iotdb_bridge_on_query_called", msg => "iotdb_bridge_on_query_called",
instance_id => InstanceId, instance_id => InstanceId,
send_message => Message, send_message => Req,
state => emqx_utils:redact(State) state => emqx_utils:redact(State)
}), }),
case make_iotdb_insert_request(Message, State) of
case try_render_message(Req, Channels) of
{ok, IoTDBPayload} -> {ok, IoTDBPayload} ->
handle_response( handle_response(
emqx_bridge_http_connector:on_query( emqx_bridge_http_connector:on_query(
InstanceId, {send_message, IoTDBPayload}, State InstanceId, {ChannelId, IoTDBPayload}, State
) )
); );
Error -> Error ->
@ -124,15 +251,17 @@ on_query(InstanceId, {send_message, Message}, State) ->
-spec on_query_async(manager_id(), {send_message, map()}, {function(), [term()]}, state()) -> -spec on_query_async(manager_id(), {send_message, map()}, {function(), [term()]}, state()) ->
{ok, pid()} | {error, empty_request}. {ok, pid()} | {error, empty_request}.
on_query_async(InstanceId, {send_message, Message}, ReplyFunAndArgs0, State) -> on_query_async(
InstanceId, {ChannelId, _Message} = Req, ReplyFunAndArgs0, #{channels := Channels} = State
) ->
?tp(iotdb_bridge_on_query_async, #{instance_id => InstanceId}), ?tp(iotdb_bridge_on_query_async, #{instance_id => InstanceId}),
?SLOG(debug, #{ ?SLOG(debug, #{
msg => "iotdb_bridge_on_query_async_called", msg => "iotdb_bridge_on_query_async_called",
instance_id => InstanceId, instance_id => InstanceId,
send_message => Message, send_message => Req,
state => emqx_utils:redact(State) state => emqx_utils:redact(State)
}), }),
case make_iotdb_insert_request(Message, State) of case try_render_message(Req, Channels) of
{ok, IoTDBPayload} -> {ok, IoTDBPayload} ->
ReplyFunAndArgs = ReplyFunAndArgs =
{ {
@ -143,12 +272,71 @@ on_query_async(InstanceId, {send_message, Message}, ReplyFunAndArgs0, State) ->
[] []
}, },
emqx_bridge_http_connector:on_query_async( emqx_bridge_http_connector:on_query_async(
InstanceId, {send_message, IoTDBPayload}, ReplyFunAndArgs, State InstanceId, {ChannelId, IoTDBPayload}, ReplyFunAndArgs, State
); );
Error -> Error ->
Error Error
end. end.
on_add_channel(
InstanceId,
#{channels := Channels} = OldState0,
ChannelId,
#{
parameters := #{iotdb_version := Version, data := Data} = Parameter
}
) ->
case maps:is_key(ChannelId, Channels) of
true ->
{error, already_exists};
_ ->
%% update HTTP channel
InsertTabletPathV1 = <<"rest/v1/insertTablet">>,
InsertTabletPathV2 = <<"rest/v2/insertTablet">>,
Path =
case Version of
?VSN_1_1_X -> InsertTabletPathV2;
_ -> InsertTabletPathV1
end,
HTTPReq = #{
parameters => Parameter#{
path => Path,
method => <<"post">>
}
},
{ok, OldState} = emqx_bridge_http_connector:on_add_channel(
InstanceId, OldState0, ChannelId, HTTPReq
),
%% update IoTDB channel
DeviceId = maps:get(device_id, Parameter, <<>>),
Channel = Parameter#{
device_id => emqx_placeholder:preproc_tmpl(DeviceId),
data := preproc_data_template(Data)
},
Channels2 = Channels#{ChannelId => Channel},
{ok, OldState#{channels := Channels2}}
end.
on_remove_channel(InstanceId, #{channels := Channels} = OldState0, ChannelId) ->
{ok, OldState} = emqx_bridge_http_connector:on_remove_channel(InstanceId, OldState0, ChannelId),
Channels2 = maps:remove(ChannelId, Channels),
{ok, OldState#{channels => Channels2}}.
on_get_channels(InstanceId) ->
emqx_bridge_v2:get_channels_for_connector(InstanceId).
on_get_channel_status(_InstanceId, ChannelId, #{channels := Channels}) ->
case maps:is_key(ChannelId, Channels) of
true ->
connected;
_ ->
{error, not_exists}
end.
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Internal Functions %% Internal Functions
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
@ -238,14 +426,14 @@ iot_timestamp(Timestamp, _, _) when is_integer(Timestamp) ->
iot_timestamp(TimestampTkn, Msg, Nows) -> iot_timestamp(TimestampTkn, Msg, Nows) ->
iot_timestamp(emqx_placeholder:proc_tmpl(TimestampTkn, Msg), Nows). iot_timestamp(emqx_placeholder:proc_tmpl(TimestampTkn, Msg), Nows).
iot_timestamp(<<"now_us">>, #{now_us := NowUs}) ->
NowUs;
iot_timestamp(<<"now_ns">>, #{now_ns := NowNs}) ->
NowNs;
iot_timestamp(Timestamp, #{now_ms := NowMs}) when iot_timestamp(Timestamp, #{now_ms := NowMs}) when
Timestamp =:= <<"now">>; Timestamp =:= <<"now_ms">>; Timestamp =:= <<>> Timestamp =:= <<"now">>; Timestamp =:= <<"now_ms">>; Timestamp =:= <<>>
-> ->
NowMs; NowMs;
iot_timestamp(Timestamp, #{now_us := NowUs}) when Timestamp =:= <<"now_us">> ->
NowUs;
iot_timestamp(Timestamp, #{now_ns := NowNs}) when Timestamp =:= <<"now_ns">> ->
NowNs;
iot_timestamp(Timestamp, _) when is_binary(Timestamp) -> iot_timestamp(Timestamp, _) when is_binary(Timestamp) ->
binary_to_integer(Timestamp). binary_to_integer(Timestamp).
@ -304,25 +492,13 @@ convert_float(Str) when is_binary(Str) ->
convert_float(undefined) -> convert_float(undefined) ->
null. null.
make_iotdb_insert_request(Message, State) -> make_iotdb_insert_request(DataList, IsAligned, DeviceId, IotDBVsn) ->
Payloads = to_list(parse_payload(get_payload(Message))),
IsAligned = maps:get(is_aligned, State, false),
IotDBVsn = maps:get(iotdb_version, State, ?VSN_1_1_X),
case {device_id(Message, Payloads, State), preproc_data_list(Payloads)} of
{undefined, _} ->
{error, device_id_missing};
{_, []} ->
{error, invalid_data};
{DeviceId, PreProcessedData} ->
DataList = proc_data(PreProcessedData, Message),
InitAcc = #{timestamps => [], measurements => [], dtypes => [], values => []}, InitAcc = #{timestamps => [], measurements => [], dtypes => [], values => []},
Rows = replace_dtypes(aggregate_rows(DataList, InitAcc), IotDBVsn), Rows = replace_dtypes(aggregate_rows(DataList, InitAcc), IotDBVsn),
{ok,
maps:merge(Rows, #{ maps:merge(Rows, #{
iotdb_field_key(is_aligned, IotDBVsn) => IsAligned, iotdb_field_key(is_aligned, IotDBVsn) => IsAligned,
iotdb_field_key(device_id, IotDBVsn) => DeviceId iotdb_field_key(device_id, IotDBVsn) => DeviceId
})} }).
end.
replace_dtypes(Rows0, IotDBVsn) -> replace_dtypes(Rows0, IotDBVsn) ->
{Types, Rows} = maps:take(dtypes, Rows0), {Types, Rows} = maps:take(dtypes, Rows0),
@ -404,13 +580,12 @@ iotdb_field_key(data_types, ?VSN_0_13_X) ->
to_list(List) when is_list(List) -> List; to_list(List) when is_list(List) -> List;
to_list(Data) -> [Data]. to_list(Data) -> [Data].
device_id(Message, Payloads, State) -> %% If device_id is missing from the channel data, try to find it from the payload
case maps:get(device_id, State, undefined) of device_id(Message, Payloads, Channel) ->
undefined -> case maps:get(device_id, Channel, <<>>) of
%% [FIXME] there could be conflicting device-ids in the Payloads <<>> ->
maps:get(<<"device_id">>, hd(Payloads), undefined); maps:get(<<"device_id">>, hd(Payloads), undefined);
DeviceId -> DeviceIdTkn ->
DeviceIdTkn = emqx_placeholder:preproc_tmpl(DeviceId),
emqx_placeholder:proc_tmpl(DeviceIdTkn, Message) emqx_placeholder:proc_tmpl(DeviceIdTkn, Message)
end. end.
@ -430,3 +605,47 @@ eval_response_body(Body, Resp) ->
#{<<"code">> := 200} -> Resp; #{<<"code">> := 200} -> Resp;
Reason -> {error, Reason} Reason -> {error, Reason}
end. end.
preproc_data_template(DataList) ->
lists:map(
fun(
#{
timestamp := Timestamp,
measurement := Measurement,
data_type := DataType,
value := Value
}
) ->
#{
timestamp => emqx_placeholder:preproc_tmpl(Timestamp),
measurement => emqx_placeholder:preproc_tmpl(Measurement),
data_type => DataType,
value => emqx_placeholder:preproc_tmpl(Value)
}
end,
DataList
).
try_render_message({ChannelId, Msg}, Channels) ->
case maps:find(ChannelId, Channels) of
{ok, Channel} ->
{ok, render_channel_message(Channel, Msg)};
_ ->
{error, {unrecoverable_error, {invalid_channel_id, ChannelId}}}
end.
render_channel_message(#{is_aligned := IsAligned, iotdb_version := IoTDBVsn} = Channel, Message) ->
Payloads = to_list(parse_payload(get_payload(Message))),
DataTemplate = get_data_template(Channel, Payloads),
DataList = proc_data(DataTemplate, Message),
DeviceId = device_id(Message, Payloads, Channel),
make_iotdb_insert_request(DataList, IsAligned, DeviceId, IoTDBVsn).
%% Get the message template.
%% In order to be compatible with 4.4, the template version has higher priority
%% This is a template, using it
get_data_template(#{data := Data}, _Payloads) when Data =/= [] ->
Data;
%% This is a self-describing message
get_data_template(#{data := []}, Payloads) ->
preproc_data_list(Payloads).

View File

@ -302,54 +302,18 @@ parse_confs(
method => undefined method => undefined
} }
}; };
parse_confs(<<"iotdb">>, Name, Conf) -> parse_confs(ConnectorType, Name, Config) ->
%% [FIXME] this has no place here, it's used in parse_confs/3, which should connector_config(ConnectorType, Name, Config).
%% rather delegate to a behavior callback than implementing domain knowledge
%% here (reversed dependency)
InsertTabletPathV1 = <<"rest/v1/insertTablet">>,
InsertTabletPathV2 = <<"rest/v2/insertTablet">>,
#{
base_url := BaseURL,
authentication :=
#{
username := Username,
password := Password
}
} = Conf,
BasicToken = base64:encode(<<Username/binary, ":", Password/binary>>),
%% This version atom correspond to the macro ?VSN_1_1_X in
%% emqx_connector_iotdb.hrl. It would be better to use the macro directly, but
%% this cannot be done without introducing a dependency on the
%% emqx_iotdb_connector app (which is an EE app).
DefaultIOTDBConnector = 'v1.1.x',
Version = maps:get(iotdb_version, Conf, DefaultIOTDBConnector),
InsertTabletPath =
case Version of
DefaultIOTDBConnector -> InsertTabletPathV2;
_ -> InsertTabletPathV1
end,
WebhookConfig =
Conf#{
method => <<"post">>,
url => <<BaseURL/binary, InsertTabletPath/binary>>,
headers => [
{<<"Content-type">>, <<"application/json">>},
{<<"Authorization">>, BasicToken}
]
},
parse_confs(
<<"webhook">>,
Name,
WebhookConfig
);
parse_confs(ConnectorType, _Name, Config) ->
connector_config(ConnectorType, Config).
connector_config(ConnectorType, Config) -> connector_config(ConnectorType, Name, Config) ->
Mod = connector_impl_module(ConnectorType), Mod = connector_impl_module(ConnectorType),
case erlang:function_exported(Mod, connector_config, 1) of case erlang:function_exported(Mod, connector_config, 2) of
true -> true ->
Mod:connector_config(Config); Mod:connector_config(Config, #{
type => ConnectorType,
name => Name,
parse_confs => fun parse_confs/3
});
false -> false ->
Config Config
end. end.

View File

@ -46,6 +46,8 @@ resource_type(timescale) ->
emqx_postgresql; emqx_postgresql;
resource_type(redis) -> resource_type(redis) ->
emqx_bridge_redis_connector; emqx_bridge_redis_connector;
resource_type(iotdb) ->
emqx_bridge_iotdb_connector;
resource_type(Type) -> resource_type(Type) ->
error({unknown_connector_type, Type}). error({unknown_connector_type, Type}).
@ -56,6 +58,8 @@ connector_impl_module(azure_event_hub_producer) ->
emqx_bridge_azure_event_hub; emqx_bridge_azure_event_hub;
connector_impl_module(confluent_producer) -> connector_impl_module(confluent_producer) ->
emqx_bridge_confluent_producer; emqx_bridge_confluent_producer;
connector_impl_module(iotdb) ->
emqx_bridge_iotdb_connector;
connector_impl_module(_ConnectorType) -> connector_impl_module(_ConnectorType) ->
undefined. undefined.
@ -159,6 +163,14 @@ connector_structs() ->
desc => <<"Timescale Connector Config">>, desc => <<"Timescale Connector Config">>,
required => false required => false
} }
)},
{iotdb,
mk(
hoconsc:map(name, ref(emqx_bridge_iotdb_connector, config)),
#{
desc => <<"IoTDB Connector Config">>,
required => false
}
)} )}
]. ].
@ -175,7 +187,8 @@ schema_modules() ->
emqx_bridge_syskeeper_proxy, emqx_bridge_syskeeper_proxy,
emqx_bridge_timescale, emqx_bridge_timescale,
emqx_postgresql_connector_schema, emqx_postgresql_connector_schema,
emqx_bridge_redis_schema emqx_bridge_redis_schema,
emqx_bridge_iotdb_connector
]. ].
api_schemas(Method) -> api_schemas(Method) ->
@ -201,7 +214,8 @@ api_schemas(Method) ->
api_ref(emqx_bridge_syskeeper_proxy, <<"syskeeper_proxy">>, Method), api_ref(emqx_bridge_syskeeper_proxy, <<"syskeeper_proxy">>, Method),
api_ref(emqx_bridge_timescale, <<"timescale">>, Method ++ "_connector"), api_ref(emqx_bridge_timescale, <<"timescale">>, Method ++ "_connector"),
api_ref(emqx_postgresql_connector_schema, <<"pgsql">>, Method ++ "_connector"), api_ref(emqx_postgresql_connector_schema, <<"pgsql">>, Method ++ "_connector"),
api_ref(emqx_bridge_redis_schema, <<"redis">>, Method ++ "_connector") api_ref(emqx_bridge_redis_schema, <<"redis">>, Method ++ "_connector"),
api_ref(emqx_bridge_iotdb_connector, <<"iotdb">>, Method)
]. ].
api_ref(Module, Type, Method) -> api_ref(Module, Type, Method) ->

View File

@ -142,7 +142,9 @@ connector_type_to_bridge_types(syskeeper_forwarder) ->
connector_type_to_bridge_types(syskeeper_proxy) -> connector_type_to_bridge_types(syskeeper_proxy) ->
[]; [];
connector_type_to_bridge_types(timescale) -> connector_type_to_bridge_types(timescale) ->
[timescale]. [timescale];
connector_type_to_bridge_types(iotdb) ->
[iotdb].
actions_config_name() -> <<"actions">>. actions_config_name() -> <<"actions">>.

View File

@ -23,7 +23,7 @@
-export([namespace/0, roots/0, fields/1, desc/1]). -export([namespace/0, roots/0, fields/1, desc/1]).
-export([create_opts/1, resource_opts_meta/0]). -export([create_opts/1, resource_opts_meta/0, override/2]).
%% range interval in ms %% range interval in ms
-define(HEALTH_CHECK_INTERVAL_RANGE_MIN, 1). -define(HEALTH_CHECK_INTERVAL_RANGE_MIN, 1).

View File

@ -70,4 +70,54 @@ desc_name.desc:
desc_name.label: desc_name.label:
"""Bridge Name""" """Bridge Name"""
config_parameters_timestamp.desc:
"""Timestamp. Placeholders in format of ${var} is supported, the finally value can be:</br>
- now: use the `now_ms` which is contained in the payload as timestamp
- now_ms: same as above
- now_us: use the `now_us` which is contained in the payload as timestamp
- now_ns: use the `now_us` which is contained in the payload as timestamp
- any other: use the value directly as the timestamp
"""
config_parameters_timestamp.label:
"""Timestamp"""
config_parameters_measurement.desc:
"""Measurement. Placeholders in format of ${var} is supported"""
config_parameters_measurement.label:
"""Measurement"""
config_parameters_data_type.desc:
"""Data Type, can be:</br>
- TEXT
- BOOLEAN
- INT32
- INT64
- FLOAT
- DOUBLE
"""
config_parameters_data_type.label:
"""Data type"""
config_parameters_value.desc:
"""Value. Placeholders in format of ${var} is supported"""
config_parameters_value.label:
"""Value"""
action_parameters_data.desc:
"""IoTDB action parameter data"""
action_parameters_data.label:
"""Parameter Data"""
action_parameters.desc:
"""IoTDB action parameters"""
action_parameters.label:
"""Parameters"""
} }

View File

@ -0,0 +1,44 @@
emqx_bridge_iotdb_connector {
config_authentication.desc:
"""Authentication configuration"""
config_authentication.label:
"""Authentication"""
auth_basic.desc:
"""Parameters for basic authentication."""
auth_basic.label:
"""Basic auth params"""
config_auth_basic_username.desc:
"""The username as configured at the IoTDB REST interface"""
config_auth_basic_username.label:
"""HTTP Basic Auth Username"""
config_auth_basic_password.desc:
"""The password as configured at the IoTDB REST interface"""
config_auth_basic_password.label:
"""HTTP Basic Auth Password"""
config_base_url.desc:
"""The base URL of the external IoTDB service's REST interface."""
config_base_url.label:
"""IoTDB REST Service Base URL"""
config_max_retries.desc:
"""HTTP request max retry times if failed."""
config_max_retries.label:
"""HTTP Request Max Retries"""
desc_config.desc:
"""Configuration for Apache IoTDB bridge."""
desc_config.label:
"""IoTDB Bridge Configuration"""
}