feat(kafka_consumer): add mqtt topic placeholder support
Fixes https://emqx.atlassian.net/browse/EMQX-10678
This commit is contained in:
parent
a0467fa298
commit
9900a32850
|
@ -1,7 +1,7 @@
|
||||||
%% -*- mode: erlang -*-
|
%% -*- mode: erlang -*-
|
||||||
{application, emqx_bridge_kafka, [
|
{application, emqx_bridge_kafka, [
|
||||||
{description, "EMQX Enterprise Kafka Bridge"},
|
{description, "EMQX Enterprise Kafka Bridge"},
|
||||||
{vsn, "0.1.6"},
|
{vsn, "0.1.7"},
|
||||||
{registered, [emqx_bridge_kafka_consumer_sup]},
|
{registered, [emqx_bridge_kafka_consumer_sup]},
|
||||||
{applications, [
|
{applications, [
|
||||||
kernel,
|
kernel,
|
||||||
|
|
|
@ -125,7 +125,7 @@ values(consumer) ->
|
||||||
topic_mapping => [
|
topic_mapping => [
|
||||||
#{
|
#{
|
||||||
kafka_topic => <<"kafka-topic-1">>,
|
kafka_topic => <<"kafka-topic-1">>,
|
||||||
mqtt_topic => <<"mqtt/topic/1">>,
|
mqtt_topic => <<"mqtt/topic/${.offset}">>,
|
||||||
qos => 1,
|
qos => 1,
|
||||||
payload_template => <<"${.}">>
|
payload_template => <<"${.}">>
|
||||||
},
|
},
|
||||||
|
|
|
@ -69,7 +69,7 @@
|
||||||
topic_mapping := #{
|
topic_mapping := #{
|
||||||
kafka_topic() := #{
|
kafka_topic() := #{
|
||||||
payload_template := emqx_placeholder:tmpl_token(),
|
payload_template := emqx_placeholder:tmpl_token(),
|
||||||
mqtt_topic => emqx_types:topic(),
|
mqtt_topic_template => emqx_placeholder:tmpl_token(),
|
||||||
qos => emqx_types:qos()
|
qos => emqx_types:qos()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -83,7 +83,7 @@
|
||||||
topic_mapping := #{
|
topic_mapping := #{
|
||||||
kafka_topic() := #{
|
kafka_topic() := #{
|
||||||
payload_template := emqx_placeholder:tmpl_token(),
|
payload_template := emqx_placeholder:tmpl_token(),
|
||||||
mqtt_topic => emqx_types:topic(),
|
mqtt_topic_template => emqx_placeholder:tmpl_token(),
|
||||||
qos => emqx_types:qos()
|
qos => emqx_types:qos()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -235,7 +235,7 @@ do_handle_message(Message, State) ->
|
||||||
value_encoding_mode := ValueEncodingMode
|
value_encoding_mode := ValueEncodingMode
|
||||||
} = State,
|
} = State,
|
||||||
#{
|
#{
|
||||||
mqtt_topic := MQTTTopic,
|
mqtt_topic_template := MQTTTopicTemplate,
|
||||||
qos := MQTTQoS,
|
qos := MQTTQoS,
|
||||||
payload_template := PayloadTemplate
|
payload_template := PayloadTemplate
|
||||||
} = maps:get(KafkaTopic, TopicMapping),
|
} = maps:get(KafkaTopic, TopicMapping),
|
||||||
|
@ -249,6 +249,7 @@ do_handle_message(Message, State) ->
|
||||||
value => encode(Message#kafka_message.value, ValueEncodingMode)
|
value => encode(Message#kafka_message.value, ValueEncodingMode)
|
||||||
},
|
},
|
||||||
Payload = render(FullMessage, PayloadTemplate),
|
Payload = render(FullMessage, PayloadTemplate),
|
||||||
|
MQTTTopic = render(FullMessage, MQTTTopicTemplate),
|
||||||
MQTTMessage = emqx_message:make(ResourceId, MQTTQoS, MQTTTopic, Payload),
|
MQTTMessage = emqx_message:make(ResourceId, MQTTQoS, MQTTTopic, Payload),
|
||||||
_ = emqx:publish(MQTTMessage),
|
_ = emqx:publish(MQTTMessage),
|
||||||
emqx:run_hook(Hookpoint, [FullMessage]),
|
emqx:run_hook(Hookpoint, [FullMessage]),
|
||||||
|
@ -533,15 +534,16 @@ convert_topic_mapping(TopicMappingList) ->
|
||||||
fun(Fields, Acc) ->
|
fun(Fields, Acc) ->
|
||||||
#{
|
#{
|
||||||
kafka_topic := KafkaTopic,
|
kafka_topic := KafkaTopic,
|
||||||
mqtt_topic := MQTTTopic,
|
mqtt_topic := MQTTTopicTemplate0,
|
||||||
qos := QoS,
|
qos := QoS,
|
||||||
payload_template := PayloadTemplate0
|
payload_template := PayloadTemplate0
|
||||||
} = Fields,
|
} = Fields,
|
||||||
PayloadTemplate = emqx_placeholder:preproc_tmpl(PayloadTemplate0),
|
PayloadTemplate = emqx_placeholder:preproc_tmpl(PayloadTemplate0),
|
||||||
|
MQTTTopicTemplate = emqx_placeholder:preproc_tmpl(MQTTTopicTemplate0),
|
||||||
Acc#{
|
Acc#{
|
||||||
KafkaTopic => #{
|
KafkaTopic => #{
|
||||||
payload_template => PayloadTemplate,
|
payload_template => PayloadTemplate,
|
||||||
mqtt_topic => MQTTTopic,
|
mqtt_topic_template => MQTTTopicTemplate,
|
||||||
qos => QoS
|
qos => QoS
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,6 +60,7 @@ only_once_tests() ->
|
||||||
t_node_joins_existing_cluster,
|
t_node_joins_existing_cluster,
|
||||||
t_cluster_node_down,
|
t_cluster_node_down,
|
||||||
t_multiple_topic_mappings,
|
t_multiple_topic_mappings,
|
||||||
|
t_dynamic_mqtt_topic,
|
||||||
t_resource_manager_crash_after_subscriber_started,
|
t_resource_manager_crash_after_subscriber_started,
|
||||||
t_resource_manager_crash_before_subscriber_started
|
t_resource_manager_crash_before_subscriber_started
|
||||||
].
|
].
|
||||||
|
@ -329,6 +330,23 @@ init_per_testcase(t_multiple_topic_mappings = TestCase, Config0) ->
|
||||||
],
|
],
|
||||||
Config = [{topic_mapping, TopicMapping} | Config0],
|
Config = [{topic_mapping, TopicMapping} | Config0],
|
||||||
common_init_per_testcase(TestCase, Config);
|
common_init_per_testcase(TestCase, Config);
|
||||||
|
init_per_testcase(t_dynamic_mqtt_topic = TestCase, Config0) ->
|
||||||
|
KafkaTopic =
|
||||||
|
<<
|
||||||
|
(atom_to_binary(TestCase))/binary,
|
||||||
|
(integer_to_binary(erlang:unique_integer()))/binary
|
||||||
|
>>,
|
||||||
|
TopicMapping =
|
||||||
|
[
|
||||||
|
#{
|
||||||
|
kafka_topic => KafkaTopic,
|
||||||
|
mqtt_topic => <<"${.topic}/${.value}/${.headers.hkey}">>,
|
||||||
|
qos => 1,
|
||||||
|
payload_template => <<"${.}">>
|
||||||
|
}
|
||||||
|
],
|
||||||
|
Config = [{kafka_topic, KafkaTopic}, {topic_mapping, TopicMapping} | Config0],
|
||||||
|
common_init_per_testcase(TestCase, Config);
|
||||||
init_per_testcase(TestCase, Config) ->
|
init_per_testcase(TestCase, Config) ->
|
||||||
common_init_per_testcase(TestCase, Config).
|
common_init_per_testcase(TestCase, Config).
|
||||||
|
|
||||||
|
@ -336,11 +354,12 @@ common_init_per_testcase(TestCase, Config0) ->
|
||||||
ct:timetrap(timer:seconds(60)),
|
ct:timetrap(timer:seconds(60)),
|
||||||
delete_all_bridges(),
|
delete_all_bridges(),
|
||||||
emqx_config:delete_override_conf_files(),
|
emqx_config:delete_override_conf_files(),
|
||||||
KafkaTopic =
|
KafkaTopic0 =
|
||||||
<<
|
<<
|
||||||
(atom_to_binary(TestCase))/binary,
|
(atom_to_binary(TestCase))/binary,
|
||||||
(integer_to_binary(erlang:unique_integer()))/binary
|
(integer_to_binary(erlang:unique_integer()))/binary
|
||||||
>>,
|
>>,
|
||||||
|
KafkaTopic = proplists:get_value(kafka_topic, Config0, KafkaTopic0),
|
||||||
KafkaType = ?config(kafka_type, Config0),
|
KafkaType = ?config(kafka_type, Config0),
|
||||||
UniqueNum = integer_to_binary(erlang:unique_integer()),
|
UniqueNum = integer_to_binary(erlang:unique_integer()),
|
||||||
MQTTTopic = proplists:get_value(mqtt_topic, Config0, <<"mqtt/topic/", UniqueNum/binary>>),
|
MQTTTopic = proplists:get_value(mqtt_topic, Config0, <<"mqtt/topic/", UniqueNum/binary>>),
|
||||||
|
@ -1674,6 +1693,78 @@ t_bridge_rule_action_source(Config) ->
|
||||||
),
|
),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
|
t_dynamic_mqtt_topic(Config) ->
|
||||||
|
KafkaTopic = ?config(kafka_topic, Config),
|
||||||
|
NPartitions = ?config(num_partitions, Config),
|
||||||
|
ResourceId = resource_id(Config),
|
||||||
|
Payload = emqx_guid:to_hexstr(emqx_guid:gen()),
|
||||||
|
MQTTTopic = emqx_topic:join([KafkaTopic, '#']),
|
||||||
|
?check_trace(
|
||||||
|
begin
|
||||||
|
?assertMatch(
|
||||||
|
{ok, _},
|
||||||
|
create_bridge(Config)
|
||||||
|
),
|
||||||
|
wait_until_subscribers_are_ready(NPartitions, 40_000),
|
||||||
|
{ok, C} = emqtt:start_link(),
|
||||||
|
on_exit(fun() -> emqtt:stop(C) end),
|
||||||
|
{ok, _} = emqtt:connect(C),
|
||||||
|
{ok, _, [0]} = emqtt:subscribe(C, MQTTTopic),
|
||||||
|
ct:pal("subscribed to ~p", [MQTTTopic]),
|
||||||
|
|
||||||
|
{ok, SRef0} = snabbkaffe:subscribe(
|
||||||
|
?match_event(#{
|
||||||
|
?snk_kind := kafka_consumer_handle_message, ?snk_span := {complete, _}
|
||||||
|
}),
|
||||||
|
_NumMsgs = 3,
|
||||||
|
20_000
|
||||||
|
),
|
||||||
|
{_Partition, _OffsetReply} =
|
||||||
|
publish(Config, [
|
||||||
|
%% this will have the last segment defined
|
||||||
|
#{
|
||||||
|
key => <<"mykey">>,
|
||||||
|
value => Payload,
|
||||||
|
headers => [{<<"hkey">>, <<"hvalue">>}]
|
||||||
|
},
|
||||||
|
%% this will not
|
||||||
|
#{
|
||||||
|
key => <<"mykey">>,
|
||||||
|
value => Payload
|
||||||
|
},
|
||||||
|
%% will inject an invalid topic segment
|
||||||
|
#{
|
||||||
|
key => <<"mykey">>,
|
||||||
|
value => <<"+">>
|
||||||
|
}
|
||||||
|
]),
|
||||||
|
{ok, _} = snabbkaffe:receive_events(SRef0),
|
||||||
|
ok
|
||||||
|
end,
|
||||||
|
fun(Trace) ->
|
||||||
|
?assertMatch([_Enter, _Complete | _], ?of_kind(kafka_consumer_handle_message, Trace)),
|
||||||
|
%% the message with invalid topic will fail to be published
|
||||||
|
Published = receive_published(#{n => 2}),
|
||||||
|
ExpectedMQTTTopic0 = emqx_topic:join([KafkaTopic, Payload, <<"hvalue">>]),
|
||||||
|
ExpectedMQTTTopic1 = emqx_topic:join([KafkaTopic, Payload, <<>>]),
|
||||||
|
?assertMatch(
|
||||||
|
[
|
||||||
|
#{
|
||||||
|
topic := ExpectedMQTTTopic0
|
||||||
|
},
|
||||||
|
#{
|
||||||
|
topic := ExpectedMQTTTopic1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
Published
|
||||||
|
),
|
||||||
|
?assertEqual(3, emqx_resource_metrics:received_get(ResourceId)),
|
||||||
|
?assertError({timeout, _}, receive_published(#{timeout => 500})),
|
||||||
|
ok
|
||||||
|
end
|
||||||
|
),
|
||||||
|
ok.
|
||||||
|
|
||||||
%% checks that an existing cluster can be configured with a kafka
|
%% checks that an existing cluster can be configured with a kafka
|
||||||
%% consumer bridge and that the consumers will distribute over the two
|
%% consumer bridge and that the consumers will distribute over the two
|
||||||
%% nodes.
|
%% nodes.
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
Added support for using placeholders to define MQTT Topic in Kafka Consumer bridge topic mappings.
|
Loading…
Reference in New Issue