feat(message transformation): implement dryrun endpoint
Follow up to https://github.com/emqx/emqx/pull/13199
This commit is contained in:
parent
c70e8252fe
commit
9b3c806ba7
|
@ -26,20 +26,28 @@
|
||||||
on_message_publish/1
|
on_message_publish/1
|
||||||
]).
|
]).
|
||||||
|
|
||||||
|
%% Internal exports
|
||||||
|
-export([run_transformation/2, trace_failure_context_to_map/1]).
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
%% Type declarations
|
%% Type declarations
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
-define(TRACE_TAG, "MESSAGE_TRANSFORMATION").
|
-define(TRACE_TAG, "MESSAGE_TRANSFORMATION").
|
||||||
-define(CONF_ROOT, message_transformation).
|
|
||||||
-define(CONF_ROOT_BIN, <<"message_transformation">>).
|
-record(trace_failure_context, {
|
||||||
-define(TRANSFORMATIONS_CONF_PATH, [?CONF_ROOT, transformations]).
|
transformation :: transformation(),
|
||||||
|
tag :: string(),
|
||||||
|
context :: map()
|
||||||
|
}).
|
||||||
|
-type trace_failure_context() :: #trace_failure_context{}.
|
||||||
|
|
||||||
-type transformation_name() :: binary().
|
-type transformation_name() :: binary().
|
||||||
%% TODO: make more specific typespec
|
%% TODO: make more specific typespec
|
||||||
-type transformation() :: #{atom() => term()}.
|
-type transformation() :: #{atom() => term()}.
|
||||||
%% TODO: make more specific typespec
|
%% TODO: make more specific typespec
|
||||||
-type variform() :: any().
|
-type variform() :: any().
|
||||||
|
-type failure_action() :: ignore | drop | disconnect.
|
||||||
-type operation() :: #{key := [binary(), ...], value := variform()}.
|
-type operation() :: #{key := [binary(), ...], value := variform()}.
|
||||||
-type qos() :: 0..2.
|
-type qos() :: 0..2.
|
||||||
-type rendered_value() :: qos() | boolean() | binary().
|
-type rendered_value() :: qos() | boolean() | binary().
|
||||||
|
@ -62,7 +70,8 @@
|
||||||
|
|
||||||
-export_type([
|
-export_type([
|
||||||
transformation/0,
|
transformation/0,
|
||||||
transformation_name/0
|
transformation_name/0,
|
||||||
|
failure_action/0
|
||||||
]).
|
]).
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
|
@ -125,19 +134,50 @@ on_message_publish(Message = #message{topic = Topic}) ->
|
||||||
%% Internal exports
|
%% Internal exports
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
-spec run_transformation(transformation(), emqx_types:message()) ->
|
||||||
|
{ok, emqx_types:message()} | {failure_action(), trace_failure_context()}.
|
||||||
|
run_transformation(Transformation, MessageIn) ->
|
||||||
|
#{
|
||||||
|
operations := Operations,
|
||||||
|
failure_action := FailureAction,
|
||||||
|
payload_decoder := PayloadDecoder
|
||||||
|
} = Transformation,
|
||||||
|
Fun = fun(Operation, Acc) ->
|
||||||
|
case eval_operation(Operation, Transformation, Acc) of
|
||||||
|
{ok, NewAcc} -> {cont, NewAcc};
|
||||||
|
{error, TraceFailureContext} -> {halt, {error, TraceFailureContext}}
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
PayloadIn = MessageIn#message.payload,
|
||||||
|
case decode(PayloadIn, PayloadDecoder, Transformation) of
|
||||||
|
{ok, InitPayload} ->
|
||||||
|
InitAcc = message_to_context(MessageIn, InitPayload, Transformation),
|
||||||
|
case emqx_utils:foldl_while(Fun, InitAcc, Operations) of
|
||||||
|
#{} = ContextOut ->
|
||||||
|
context_to_message(MessageIn, ContextOut, Transformation);
|
||||||
|
{error, TraceFailureContext} ->
|
||||||
|
{FailureAction, TraceFailureContext}
|
||||||
|
end;
|
||||||
|
{error, TraceFailureContext} ->
|
||||||
|
{FailureAction, TraceFailureContext}
|
||||||
|
end.
|
||||||
|
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
%% Internal functions
|
%% Internal functions
|
||||||
%%------------------------------------------------------------------------------
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
-spec eval_operation(operation(), transformation(), eval_context()) -> {ok, eval_context()} | error.
|
-spec eval_operation(operation(), transformation(), eval_context()) ->
|
||||||
|
{ok, eval_context()} | {error, trace_failure_context()}.
|
||||||
eval_operation(Operation, Transformation, Context) ->
|
eval_operation(Operation, Transformation, Context) ->
|
||||||
#{key := K, value := V} = Operation,
|
#{key := K, value := V} = Operation,
|
||||||
case eval_variform(K, V, Context) of
|
case eval_variform(K, V, Context) of
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
trace_failure(Transformation, "transformation_eval_operation_failure", #{
|
FailureContext = #trace_failure_context{
|
||||||
reason => Reason
|
transformation = Transformation,
|
||||||
}),
|
tag = "transformation_eval_operation_failure",
|
||||||
error;
|
context = #{reason => Reason}
|
||||||
|
},
|
||||||
|
{error, FailureContext};
|
||||||
{ok, Rendered} ->
|
{ok, Rendered} ->
|
||||||
NewContext = put_value(K, Rendered, Context),
|
NewContext = put_value(K, Rendered, Context),
|
||||||
{ok, NewContext}
|
{ok, NewContext}
|
||||||
|
@ -233,14 +273,16 @@ do_run_transformations(Transformations, Message) ->
|
||||||
#{name := Name} = Transformation,
|
#{name := Name} = Transformation,
|
||||||
emqx_message_transformation_registry:inc_matched(Name),
|
emqx_message_transformation_registry:inc_matched(Name),
|
||||||
case run_transformation(Transformation, MessageAcc) of
|
case run_transformation(Transformation, MessageAcc) of
|
||||||
#message{} = NewAcc ->
|
{ok, #message{} = NewAcc} ->
|
||||||
emqx_message_transformation_registry:inc_succeeded(Name),
|
emqx_message_transformation_registry:inc_succeeded(Name),
|
||||||
{cont, NewAcc};
|
{cont, NewAcc};
|
||||||
ignore ->
|
{ignore, TraceFailureContext} ->
|
||||||
|
trace_failure_from_context(TraceFailureContext),
|
||||||
emqx_message_transformation_registry:inc_failed(Name),
|
emqx_message_transformation_registry:inc_failed(Name),
|
||||||
run_message_transformation_failed_hook(Message, Transformation),
|
run_message_transformation_failed_hook(Message, Transformation),
|
||||||
{cont, MessageAcc};
|
{cont, MessageAcc};
|
||||||
FailureAction ->
|
{FailureAction, TraceFailureContext} ->
|
||||||
|
trace_failure_from_context(TraceFailureContext),
|
||||||
trace_failure(Transformation, "transformation_failed", #{
|
trace_failure(Transformation, "transformation_failed", #{
|
||||||
transformation => Name,
|
transformation => Name,
|
||||||
action => FailureAction
|
action => FailureAction
|
||||||
|
@ -270,33 +312,6 @@ do_run_transformations(Transformations, Message) ->
|
||||||
FailureAction
|
FailureAction
|
||||||
end.
|
end.
|
||||||
|
|
||||||
run_transformation(Transformation, MessageIn) ->
|
|
||||||
#{
|
|
||||||
operations := Operations,
|
|
||||||
failure_action := FailureAction,
|
|
||||||
payload_decoder := PayloadDecoder
|
|
||||||
} = Transformation,
|
|
||||||
Fun = fun(Operation, Acc) ->
|
|
||||||
case eval_operation(Operation, Transformation, Acc) of
|
|
||||||
{ok, NewAcc} -> {cont, NewAcc};
|
|
||||||
error -> {halt, FailureAction}
|
|
||||||
end
|
|
||||||
end,
|
|
||||||
PayloadIn = MessageIn#message.payload,
|
|
||||||
case decode(PayloadIn, PayloadDecoder, Transformation) of
|
|
||||||
{ok, InitPayload} ->
|
|
||||||
InitAcc = message_to_context(MessageIn, InitPayload, Transformation),
|
|
||||||
case emqx_utils:foldl_while(Fun, InitAcc, Operations) of
|
|
||||||
#{} = ContextOut ->
|
|
||||||
context_to_message(MessageIn, ContextOut, Transformation);
|
|
||||||
_ ->
|
|
||||||
FailureAction
|
|
||||||
end;
|
|
||||||
error ->
|
|
||||||
%% Error already logged
|
|
||||||
FailureAction
|
|
||||||
end.
|
|
||||||
|
|
||||||
-spec message_to_context(emqx_types:message(), _Payload, transformation()) -> eval_context().
|
-spec message_to_context(emqx_types:message(), _Payload, transformation()) -> eval_context().
|
||||||
message_to_context(#message{} = Message, Payload, Transformation) ->
|
message_to_context(#message{} = Message, Payload, Transformation) ->
|
||||||
#{
|
#{
|
||||||
|
@ -321,7 +336,7 @@ message_to_context(#message{} = Message, Payload, Transformation) ->
|
||||||
}.
|
}.
|
||||||
|
|
||||||
-spec context_to_message(emqx_types:message(), eval_context(), transformation()) ->
|
-spec context_to_message(emqx_types:message(), eval_context(), transformation()) ->
|
||||||
{ok, emqx_types:message()} | _TODO.
|
{ok, emqx_types:message()} | {failure_action(), trace_failure_context()}.
|
||||||
context_to_message(Message, Context, Transformation) ->
|
context_to_message(Message, Context, Transformation) ->
|
||||||
#{
|
#{
|
||||||
failure_action := FailureAction,
|
failure_action := FailureAction,
|
||||||
|
@ -330,9 +345,9 @@ context_to_message(Message, Context, Transformation) ->
|
||||||
#{payload := PayloadOut} = Context,
|
#{payload := PayloadOut} = Context,
|
||||||
case encode(PayloadOut, PayloadEncoder, Transformation) of
|
case encode(PayloadOut, PayloadEncoder, Transformation) of
|
||||||
{ok, Payload} ->
|
{ok, Payload} ->
|
||||||
take_from_context(Context#{payload := Payload}, Message);
|
{ok, take_from_context(Context#{payload := Payload}, Message)};
|
||||||
error ->
|
{error, TraceFailureContext} ->
|
||||||
FailureAction
|
{FailureAction, TraceFailureContext}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
take_from_context(Context, Message) ->
|
take_from_context(Context, Message) ->
|
||||||
|
@ -362,31 +377,43 @@ decode(Payload, #{type := json}, Transformation) ->
|
||||||
{ok, JSON} ->
|
{ok, JSON} ->
|
||||||
{ok, JSON};
|
{ok, JSON};
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
trace_failure(Transformation, "payload_decode_failed", #{
|
TraceFailureContext = #trace_failure_context{
|
||||||
|
transformation = Transformation,
|
||||||
|
tag = "payload_decode_failed",
|
||||||
|
context = #{
|
||||||
decoder => json,
|
decoder => json,
|
||||||
reason => Reason
|
reason => Reason
|
||||||
}),
|
}
|
||||||
error
|
},
|
||||||
|
{error, TraceFailureContext}
|
||||||
end;
|
end;
|
||||||
decode(Payload, #{type := avro, schema := SerdeName}, Transformation) ->
|
decode(Payload, #{type := avro, schema := SerdeName}, Transformation) ->
|
||||||
try
|
try
|
||||||
{ok, emqx_schema_registry_serde:decode(SerdeName, Payload)}
|
{ok, emqx_schema_registry_serde:decode(SerdeName, Payload)}
|
||||||
catch
|
catch
|
||||||
error:{serde_not_found, _} ->
|
error:{serde_not_found, _} ->
|
||||||
trace_failure(Transformation, "payload_decode_schema_not_found", #{
|
TraceFailureContext = #trace_failure_context{
|
||||||
|
transformation = Transformation,
|
||||||
|
tag = "payload_decode_schema_not_found",
|
||||||
|
context = #{
|
||||||
decoder => avro,
|
decoder => avro,
|
||||||
schema_name => SerdeName
|
schema_name => SerdeName
|
||||||
}),
|
}
|
||||||
error;
|
},
|
||||||
|
{error, TraceFailureContext};
|
||||||
Class:Error:Stacktrace ->
|
Class:Error:Stacktrace ->
|
||||||
trace_failure(Transformation, "payload_decode_schema_failure", #{
|
TraceFailureContext = #trace_failure_context{
|
||||||
|
transformation = Transformation,
|
||||||
|
tag = "payload_decode_schema_failure",
|
||||||
|
context = #{
|
||||||
decoder => avro,
|
decoder => avro,
|
||||||
schema_name => SerdeName,
|
schema_name => SerdeName,
|
||||||
kind => Class,
|
kind => Class,
|
||||||
reason => Error,
|
reason => Error,
|
||||||
stacktrace => Stacktrace
|
stacktrace => Stacktrace
|
||||||
}),
|
}
|
||||||
error
|
},
|
||||||
|
{error, TraceFailureContext}
|
||||||
end;
|
end;
|
||||||
decode(
|
decode(
|
||||||
Payload, #{type := protobuf, schema := SerdeName, message_type := MessageType}, Transformation
|
Payload, #{type := protobuf, schema := SerdeName, message_type := MessageType}, Transformation
|
||||||
|
@ -395,22 +422,30 @@ decode(
|
||||||
{ok, emqx_schema_registry_serde:decode(SerdeName, Payload, [MessageType])}
|
{ok, emqx_schema_registry_serde:decode(SerdeName, Payload, [MessageType])}
|
||||||
catch
|
catch
|
||||||
error:{serde_not_found, _} ->
|
error:{serde_not_found, _} ->
|
||||||
trace_failure(Transformation, "payload_decode_schema_not_found", #{
|
TraceFailureContext = #trace_failure_context{
|
||||||
|
transformation = Transformation,
|
||||||
|
tag = "payload_decode_schema_not_found",
|
||||||
|
context = #{
|
||||||
decoder => protobuf,
|
decoder => protobuf,
|
||||||
schema_name => SerdeName,
|
schema_name => SerdeName,
|
||||||
message_type => MessageType
|
message_type => MessageType
|
||||||
}),
|
}
|
||||||
error;
|
},
|
||||||
|
{error, TraceFailureContext};
|
||||||
Class:Error:Stacktrace ->
|
Class:Error:Stacktrace ->
|
||||||
trace_failure(Transformation, "payload_decode_schema_failure", #{
|
TraceFailureContext = #trace_failure_context{
|
||||||
|
transformation = Transformation,
|
||||||
|
tag = "payload_decode_schema_failure",
|
||||||
|
context = #{
|
||||||
decoder => protobuf,
|
decoder => protobuf,
|
||||||
schema_name => SerdeName,
|
schema_name => SerdeName,
|
||||||
message_type => MessageType,
|
message_type => MessageType,
|
||||||
kind => Class,
|
kind => Class,
|
||||||
reason => Error,
|
reason => Error,
|
||||||
stacktrace => Stacktrace
|
stacktrace => Stacktrace
|
||||||
}),
|
}
|
||||||
error
|
},
|
||||||
|
{error, TraceFailureContext}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
encode(Payload, #{type := none}, _Transformation) ->
|
encode(Payload, #{type := none}, _Transformation) ->
|
||||||
|
@ -420,31 +455,43 @@ encode(Payload, #{type := json}, Transformation) ->
|
||||||
{ok, Bin} ->
|
{ok, Bin} ->
|
||||||
{ok, Bin};
|
{ok, Bin};
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
trace_failure(Transformation, "payload_encode_failed", #{
|
TraceFailureContext = #trace_failure_context{
|
||||||
|
transformation = Transformation,
|
||||||
|
tag = "payload_encode_failed",
|
||||||
|
context = #{
|
||||||
encoder => json,
|
encoder => json,
|
||||||
reason => Reason
|
reason => Reason
|
||||||
}),
|
}
|
||||||
error
|
},
|
||||||
|
{error, TraceFailureContext}
|
||||||
end;
|
end;
|
||||||
encode(Payload, #{type := avro, schema := SerdeName}, Transformation) ->
|
encode(Payload, #{type := avro, schema := SerdeName}, Transformation) ->
|
||||||
try
|
try
|
||||||
{ok, emqx_schema_registry_serde:encode(SerdeName, Payload)}
|
{ok, emqx_schema_registry_serde:encode(SerdeName, Payload)}
|
||||||
catch
|
catch
|
||||||
error:{serde_not_found, _} ->
|
error:{serde_not_found, _} ->
|
||||||
trace_failure(Transformation, "payload_encode_schema_not_found", #{
|
TraceFailureContext = #trace_failure_context{
|
||||||
|
transformation = Transformation,
|
||||||
|
tag = "payload_encode_schema_not_found",
|
||||||
|
context = #{
|
||||||
encoder => avro,
|
encoder => avro,
|
||||||
schema_name => SerdeName
|
schema_name => SerdeName
|
||||||
}),
|
}
|
||||||
error;
|
},
|
||||||
|
{error, TraceFailureContext};
|
||||||
Class:Error:Stacktrace ->
|
Class:Error:Stacktrace ->
|
||||||
trace_failure(Transformation, "payload_encode_schema_failure", #{
|
TraceFailureContext = #trace_failure_context{
|
||||||
|
transformation = Transformation,
|
||||||
|
tag = "payload_encode_schema_failure",
|
||||||
|
context = #{
|
||||||
encoder => avro,
|
encoder => avro,
|
||||||
schema_name => SerdeName,
|
schema_name => SerdeName,
|
||||||
kind => Class,
|
kind => Class,
|
||||||
reason => Error,
|
reason => Error,
|
||||||
stacktrace => Stacktrace
|
stacktrace => Stacktrace
|
||||||
}),
|
}
|
||||||
error
|
},
|
||||||
|
{error, TraceFailureContext}
|
||||||
end;
|
end;
|
||||||
encode(
|
encode(
|
||||||
Payload, #{type := protobuf, schema := SerdeName, message_type := MessageType}, Transformation
|
Payload, #{type := protobuf, schema := SerdeName, message_type := MessageType}, Transformation
|
||||||
|
@ -453,24 +500,50 @@ encode(
|
||||||
{ok, emqx_schema_registry_serde:encode(SerdeName, Payload, [MessageType])}
|
{ok, emqx_schema_registry_serde:encode(SerdeName, Payload, [MessageType])}
|
||||||
catch
|
catch
|
||||||
error:{serde_not_found, _} ->
|
error:{serde_not_found, _} ->
|
||||||
trace_failure(Transformation, "payload_encode_schema_not_found", #{
|
TraceFailureContext = #trace_failure_context{
|
||||||
|
transformation = Transformation,
|
||||||
|
tag = "payload_encode_schema_failure",
|
||||||
|
context = #{
|
||||||
encoder => protobuf,
|
encoder => protobuf,
|
||||||
schema_name => SerdeName,
|
schema_name => SerdeName,
|
||||||
message_type => MessageType
|
message_type => MessageType
|
||||||
}),
|
}
|
||||||
error;
|
},
|
||||||
|
{error, TraceFailureContext};
|
||||||
Class:Error:Stacktrace ->
|
Class:Error:Stacktrace ->
|
||||||
trace_failure(Transformation, "payload_encode_schema_failure", #{
|
TraceFailureContext = #trace_failure_context{
|
||||||
|
transformation = Transformation,
|
||||||
|
tag = "payload_encode_schema_failure",
|
||||||
|
context = #{
|
||||||
encoder => protobuf,
|
encoder => protobuf,
|
||||||
schema_name => SerdeName,
|
schema_name => SerdeName,
|
||||||
message_type => MessageType,
|
message_type => MessageType,
|
||||||
kind => Class,
|
kind => Class,
|
||||||
reason => Error,
|
reason => Error,
|
||||||
stacktrace => Stacktrace
|
stacktrace => Stacktrace
|
||||||
}),
|
}
|
||||||
error
|
},
|
||||||
|
{error, TraceFailureContext}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
trace_failure_from_context(
|
||||||
|
#trace_failure_context{
|
||||||
|
transformation = Transformation,
|
||||||
|
tag = Tag,
|
||||||
|
context = Context
|
||||||
|
}
|
||||||
|
) ->
|
||||||
|
trace_failure(Transformation, Tag, Context).
|
||||||
|
|
||||||
|
%% Internal export for HTTP API.
|
||||||
|
trace_failure_context_to_map(
|
||||||
|
#trace_failure_context{
|
||||||
|
tag = Tag,
|
||||||
|
context = Context
|
||||||
|
}
|
||||||
|
) ->
|
||||||
|
Context#{msg => list_to_binary(Tag)}.
|
||||||
|
|
||||||
trace_failure(#{log_failure := #{level := none}} = Transformation, _Msg, _Meta) ->
|
trace_failure(#{log_failure := #{level := none}} = Transformation, _Msg, _Meta) ->
|
||||||
#{
|
#{
|
||||||
name := _Name,
|
name := _Name,
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
-include_lib("typerefl/include/types.hrl").
|
-include_lib("typerefl/include/types.hrl").
|
||||||
-include_lib("hocon/include/hoconsc.hrl").
|
-include_lib("hocon/include/hoconsc.hrl").
|
||||||
-include_lib("emqx/include/logger.hrl").
|
-include_lib("emqx/include/logger.hrl").
|
||||||
|
-include_lib("emqx_utils/include/emqx_message.hrl").
|
||||||
-include_lib("emqx_utils/include/emqx_utils_api.hrl").
|
-include_lib("emqx_utils/include/emqx_utils_api.hrl").
|
||||||
|
|
||||||
%% `minirest' and `minirest_trails' API
|
%% `minirest' and `minirest_trails' API
|
||||||
|
@ -23,6 +24,7 @@
|
||||||
-export([
|
-export([
|
||||||
'/message_transformations'/2,
|
'/message_transformations'/2,
|
||||||
'/message_transformations/reorder'/2,
|
'/message_transformations/reorder'/2,
|
||||||
|
'/message_transformations/dryrun'/2,
|
||||||
'/message_transformations/transformation/:name'/2,
|
'/message_transformations/transformation/:name'/2,
|
||||||
'/message_transformations/transformation/:name/metrics'/2,
|
'/message_transformations/transformation/:name/metrics'/2,
|
||||||
'/message_transformations/transformation/:name/metrics/reset'/2,
|
'/message_transformations/transformation/:name/metrics/reset'/2,
|
||||||
|
@ -36,6 +38,9 @@
|
||||||
-define(TAGS, [<<"Message Transformation">>]).
|
-define(TAGS, [<<"Message Transformation">>]).
|
||||||
-define(METRIC_NAME, message_transformation).
|
-define(METRIC_NAME, message_transformation).
|
||||||
|
|
||||||
|
-type user_property() :: #{binary() => binary()}.
|
||||||
|
-reflect_type([user_property/0]).
|
||||||
|
|
||||||
%%-------------------------------------------------------------------------------------------------
|
%%-------------------------------------------------------------------------------------------------
|
||||||
%% `minirest' and `minirest_trails' API
|
%% `minirest' and `minirest_trails' API
|
||||||
%%-------------------------------------------------------------------------------------------------
|
%%-------------------------------------------------------------------------------------------------
|
||||||
|
@ -49,6 +54,7 @@ paths() ->
|
||||||
[
|
[
|
||||||
"/message_transformations",
|
"/message_transformations",
|
||||||
"/message_transformations/reorder",
|
"/message_transformations/reorder",
|
||||||
|
"/message_transformations/dryrun",
|
||||||
"/message_transformations/transformation/:name",
|
"/message_transformations/transformation/:name",
|
||||||
"/message_transformations/transformation/:name/metrics",
|
"/message_transformations/transformation/:name/metrics",
|
||||||
"/message_transformations/transformation/:name/metrics/reset",
|
"/message_transformations/transformation/:name/metrics/reset",
|
||||||
|
@ -143,6 +149,25 @@ schema("/message_transformations/reorder") ->
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
schema("/message_transformations/dryrun") ->
|
||||||
|
#{
|
||||||
|
'operationId' => '/message_transformations/dryrun',
|
||||||
|
post => #{
|
||||||
|
tags => ?TAGS,
|
||||||
|
summary => <<"Test an input against a configuration">>,
|
||||||
|
description => ?DESC("test_transformation"),
|
||||||
|
'requestBody' =>
|
||||||
|
emqx_dashboard_swagger:schema_with_examples(
|
||||||
|
ref(test_transformation),
|
||||||
|
example_input_test_transformation()
|
||||||
|
),
|
||||||
|
responses =>
|
||||||
|
#{
|
||||||
|
200 => <<"TODO">>,
|
||||||
|
400 => error_schema('BAD_REQUEST', <<"Bad request">>)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
schema("/message_transformations/transformation/:name") ->
|
schema("/message_transformations/transformation/:name") ->
|
||||||
#{
|
#{
|
||||||
'operationId' => '/message_transformations/transformation/:name',
|
'operationId' => '/message_transformations/transformation/:name',
|
||||||
|
@ -267,6 +292,29 @@ fields(reorder) ->
|
||||||
[
|
[
|
||||||
{order, mk(array(binary()), #{required => true, in => body})}
|
{order, mk(array(binary()), #{required => true, in => body})}
|
||||||
];
|
];
|
||||||
|
fields(test_transformation) ->
|
||||||
|
[
|
||||||
|
{transformation,
|
||||||
|
mk(
|
||||||
|
hoconsc:ref(emqx_message_transformation_schema, transformation),
|
||||||
|
#{required => true, in => body}
|
||||||
|
)},
|
||||||
|
{message, mk(ref(test_input_message), #{required => true, in => body})}
|
||||||
|
];
|
||||||
|
fields(test_input_message) ->
|
||||||
|
%% See `emqx_message_transformation:eval_context()'.
|
||||||
|
[
|
||||||
|
{client_attrs, mk(map(), #{required => true})},
|
||||||
|
{payload, mk(binary(), #{required => true})},
|
||||||
|
{qos, mk(range(0, 2), #{required => true})},
|
||||||
|
{retain, mk(boolean(), #{required => true})},
|
||||||
|
{topic, mk(binary(), #{required => true})},
|
||||||
|
{user_property,
|
||||||
|
mk(
|
||||||
|
typerefl:alias("map(binary(), binary())", user_property()),
|
||||||
|
#{required => true}
|
||||||
|
)}
|
||||||
|
];
|
||||||
fields(get_metrics) ->
|
fields(get_metrics) ->
|
||||||
[
|
[
|
||||||
{metrics, mk(ref(metrics), #{})},
|
{metrics, mk(ref(metrics), #{})},
|
||||||
|
@ -343,6 +391,9 @@ fields(node_metrics) ->
|
||||||
'/message_transformations/reorder'(post, #{body := #{<<"order">> := Order}}) ->
|
'/message_transformations/reorder'(post, #{body := #{<<"order">> := Order}}) ->
|
||||||
do_reorder(Order).
|
do_reorder(Order).
|
||||||
|
|
||||||
|
'/message_transformations/dryrun'(post, #{body := Params}) ->
|
||||||
|
do_transformation_dryrun(Params).
|
||||||
|
|
||||||
'/message_transformations/transformation/:name/enable/:enable'(post, #{
|
'/message_transformations/transformation/:name/enable/:enable'(post, #{
|
||||||
bindings := #{name := Name, enable := Enable}
|
bindings := #{name := Name, enable := Enable}
|
||||||
}) ->
|
}) ->
|
||||||
|
@ -436,6 +487,17 @@ example_input_reorder() ->
|
||||||
}
|
}
|
||||||
}.
|
}.
|
||||||
|
|
||||||
|
example_input_test_transformation() ->
|
||||||
|
#{
|
||||||
|
<<"test">> =>
|
||||||
|
#{
|
||||||
|
summary => <<"Test an input against a configuration">>,
|
||||||
|
value => #{
|
||||||
|
todo => true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.
|
||||||
|
|
||||||
example_return_list() ->
|
example_return_list() ->
|
||||||
OtherVal0 = example_transformation([example_avro_check()]),
|
OtherVal0 = example_transformation([example_avro_check()]),
|
||||||
OtherVal = OtherVal0#{name => <<"other_transformation">>},
|
OtherVal = OtherVal0#{name => <<"other_transformation">>},
|
||||||
|
@ -541,6 +603,20 @@ do_reorder(Order) ->
|
||||||
?BAD_REQUEST(Error)
|
?BAD_REQUEST(Error)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
do_transformation_dryrun(Params) ->
|
||||||
|
#{
|
||||||
|
transformation := Transformation,
|
||||||
|
message := Message
|
||||||
|
} = dryrun_input_message_in(Params),
|
||||||
|
case emqx_message_transformation:run_transformation(Transformation, Message) of
|
||||||
|
{ok, #message{} = FinalMessage} ->
|
||||||
|
MessageOut = dryrun_input_message_out(FinalMessage),
|
||||||
|
?OK(MessageOut);
|
||||||
|
{_FailureAction, TraceFailureContext} ->
|
||||||
|
Result = trace_failure_context_out(TraceFailureContext),
|
||||||
|
{400, Result}
|
||||||
|
end.
|
||||||
|
|
||||||
do_enable_disable(Transformation, Enable) ->
|
do_enable_disable(Transformation, Enable) ->
|
||||||
RawTransformation = make_serializable(Transformation),
|
RawTransformation = make_serializable(Transformation),
|
||||||
case emqx_message_transformation:update(RawTransformation#{<<"enable">> => Enable}) of
|
case emqx_message_transformation:update(RawTransformation#{<<"enable">> => Enable}) of
|
||||||
|
@ -654,3 +730,74 @@ operation_out(Operation0) ->
|
||||||
fun(Path) -> iolist_to_binary(lists:join(".", Path)) end,
|
fun(Path) -> iolist_to_binary(lists:join(".", Path)) end,
|
||||||
Operation
|
Operation
|
||||||
).
|
).
|
||||||
|
|
||||||
|
dryrun_input_message_in(Params) ->
|
||||||
|
%% We already check the params against the schema at the API boundary, so we can
|
||||||
|
%% expect it to succeed here.
|
||||||
|
#{root := Result = #{message := Message0}} =
|
||||||
|
hocon_tconf:check_plain(
|
||||||
|
#{roots => [{root, ref(test_transformation)}]},
|
||||||
|
#{<<"root">> => Params},
|
||||||
|
#{atom_key => true}
|
||||||
|
),
|
||||||
|
#{
|
||||||
|
client_attrs := ClientAttrs,
|
||||||
|
payload := Payload,
|
||||||
|
qos := QoS,
|
||||||
|
retain := Retain,
|
||||||
|
topic := Topic,
|
||||||
|
user_property := UserProperty0
|
||||||
|
} = Message0,
|
||||||
|
UserProperty = maps:to_list(UserProperty0),
|
||||||
|
Message1 = #{
|
||||||
|
id => emqx_guid:gen(),
|
||||||
|
timestamp => emqx_message:timestamp_now(),
|
||||||
|
extra => #{},
|
||||||
|
from => <<"test-clientid">>,
|
||||||
|
|
||||||
|
flags => #{retain => Retain},
|
||||||
|
qos => QoS,
|
||||||
|
topic => Topic,
|
||||||
|
payload => Payload,
|
||||||
|
headers => #{
|
||||||
|
client_attrs => ClientAttrs,
|
||||||
|
properties => #{'User-Property' => UserProperty}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Message = emqx_message:from_map(Message1),
|
||||||
|
Result#{message := Message}.
|
||||||
|
|
||||||
|
dryrun_input_message_out(#message{} = Message) ->
|
||||||
|
Retain = emqx_message:get_flag(retain, Message, false),
|
||||||
|
Props = emqx_message:get_header(properties, Message, #{}),
|
||||||
|
UserProperty0 = maps:get('User-Property', Props, []),
|
||||||
|
UserProperty = maps:from_list(UserProperty0),
|
||||||
|
MessageOut0 = emqx_message:to_map(Message),
|
||||||
|
MessageOut = maps:with([payload, qos, topic], MessageOut0),
|
||||||
|
MessageOut#{
|
||||||
|
retain => Retain,
|
||||||
|
user_property => UserProperty
|
||||||
|
}.
|
||||||
|
|
||||||
|
trace_failure_context_out(TraceFailureContext) ->
|
||||||
|
Context0 = emqx_message_transformation:trace_failure_context_to_map(TraceFailureContext),
|
||||||
|
%% Some context keys may not be JSON-encodable.
|
||||||
|
maps:filtermap(
|
||||||
|
fun
|
||||||
|
(reason, Reason) ->
|
||||||
|
case emqx_utils_json:safe_encode(Reason) of
|
||||||
|
{ok, _} ->
|
||||||
|
%% Let minirest encode it if it's structured.
|
||||||
|
true;
|
||||||
|
{error, _} ->
|
||||||
|
%% "Best effort"
|
||||||
|
{true, iolist_to_binary(io_lib:format("~p", [Reason]))}
|
||||||
|
end;
|
||||||
|
(stacktrace, _Stacktrace) ->
|
||||||
|
%% Log?
|
||||||
|
false;
|
||||||
|
(_Key, _Value) ->
|
||||||
|
true
|
||||||
|
end,
|
||||||
|
Context0
|
||||||
|
).
|
||||||
|
|
|
@ -140,6 +140,31 @@ topic_operation(VariformExpr) ->
|
||||||
operation(Key, VariformExpr) ->
|
operation(Key, VariformExpr) ->
|
||||||
{Key, VariformExpr}.
|
{Key, VariformExpr}.
|
||||||
|
|
||||||
|
json_serde() ->
|
||||||
|
#{<<"type">> => <<"json">>}.
|
||||||
|
|
||||||
|
avro_serde(SerdeName) ->
|
||||||
|
#{<<"type">> => <<"avro">>, <<"schema">> => SerdeName}.
|
||||||
|
|
||||||
|
dryrun_input_message() ->
|
||||||
|
dryrun_input_message(_Overrides = #{}).
|
||||||
|
|
||||||
|
dryrun_input_message(Overrides) ->
|
||||||
|
dryrun_input_message(Overrides, _Opts = #{}).
|
||||||
|
|
||||||
|
dryrun_input_message(Overrides, Opts) ->
|
||||||
|
Encoder = maps:get(encoder, Opts, fun emqx_utils_json:encode/1),
|
||||||
|
Defaults = #{
|
||||||
|
client_attrs => #{},
|
||||||
|
payload => #{},
|
||||||
|
qos => 2,
|
||||||
|
retain => true,
|
||||||
|
topic => <<"t/u/v">>,
|
||||||
|
user_property => #{}
|
||||||
|
},
|
||||||
|
InputMessage0 = emqx_utils_maps:deep_merge(Defaults, Overrides),
|
||||||
|
maps:update_with(payload, Encoder, InputMessage0).
|
||||||
|
|
||||||
api_root() -> "message_transformations".
|
api_root() -> "message_transformations".
|
||||||
|
|
||||||
simplify_result(Res) ->
|
simplify_result(Res) ->
|
||||||
|
@ -246,6 +271,13 @@ import_backup(BackupName) ->
|
||||||
Res = request(post, Path, Body),
|
Res = request(post, Path, Body),
|
||||||
simplify_result(Res).
|
simplify_result(Res).
|
||||||
|
|
||||||
|
dryrun_transformation(Transformation, Message) ->
|
||||||
|
Path = emqx_mgmt_api_test_util:api_path([api_root(), "dryrun"]),
|
||||||
|
Params = #{transformation => Transformation, message => Message},
|
||||||
|
Res = request(post, Path, Params),
|
||||||
|
ct:pal("dryrun transformation result:\n ~p", [Res]),
|
||||||
|
simplify_result(Res).
|
||||||
|
|
||||||
connect(ClientId) ->
|
connect(ClientId) ->
|
||||||
connect(ClientId, _IsPersistent = false).
|
connect(ClientId, _IsPersistent = false).
|
||||||
|
|
||||||
|
@ -1491,3 +1523,93 @@ t_client_attrs(_Config) ->
|
||||||
[]
|
[]
|
||||||
),
|
),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
|
%% Smoke tests for the dryrun endpoint.
|
||||||
|
t_dryrun_transformation(_Config) ->
|
||||||
|
?check_trace(
|
||||||
|
begin
|
||||||
|
Name1 = <<"foo">>,
|
||||||
|
Operations = [
|
||||||
|
operation(qos, <<"payload.q">>),
|
||||||
|
operation(topic, <<"concat([topic, '/', payload.t])">>),
|
||||||
|
operation(retain, <<"payload.r">>),
|
||||||
|
operation(<<"user_property.a">>, <<"payload.u.a">>),
|
||||||
|
operation(<<"payload">>, <<"payload.p.hello">>)
|
||||||
|
],
|
||||||
|
Transformation1 = transformation(Name1, Operations),
|
||||||
|
|
||||||
|
%% Good input
|
||||||
|
Message1 = dryrun_input_message(#{
|
||||||
|
payload => #{
|
||||||
|
p => #{<<"hello">> => <<"world">>},
|
||||||
|
q => 1,
|
||||||
|
r => true,
|
||||||
|
t => <<"t">>,
|
||||||
|
u => #{a => <<"b">>}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
?assertMatch(
|
||||||
|
{200, #{
|
||||||
|
<<"payload">> := <<"\"world\"">>,
|
||||||
|
<<"qos">> := 1,
|
||||||
|
<<"retain">> := true,
|
||||||
|
<<"topic">> := <<"t/u/v/t">>,
|
||||||
|
<<"user_property">> := #{<<"a">> := <<"b">>}
|
||||||
|
}},
|
||||||
|
dryrun_transformation(Transformation1, Message1)
|
||||||
|
),
|
||||||
|
|
||||||
|
%% Bad input: fails to decode
|
||||||
|
Message2 = dryrun_input_message(#{payload => "{"}, #{encoder => fun(X) -> X end}),
|
||||||
|
?assertMatch(
|
||||||
|
{400, #{
|
||||||
|
<<"decoder">> := <<"json">>,
|
||||||
|
<<"reason">> := <<_/binary>>
|
||||||
|
}},
|
||||||
|
dryrun_transformation(Transformation1, Message2)
|
||||||
|
),
|
||||||
|
|
||||||
|
%% Bad output: fails to encode
|
||||||
|
MissingSerde = <<"missing_serde">>,
|
||||||
|
Transformation2 = transformation(Name1, [dummy_operation()], #{
|
||||||
|
<<"payload_decoder">> => json_serde(),
|
||||||
|
<<"payload_encoder">> => avro_serde(MissingSerde)
|
||||||
|
}),
|
||||||
|
?assertMatch(
|
||||||
|
{400, #{
|
||||||
|
<<"msg">> := <<"payload_encode_schema_not_found">>,
|
||||||
|
<<"encoder">> := <<"avro">>,
|
||||||
|
<<"schema_name">> := MissingSerde
|
||||||
|
}},
|
||||||
|
dryrun_transformation(Transformation2, Message1)
|
||||||
|
),
|
||||||
|
|
||||||
|
%% Bad input: unbound var during one of the operations
|
||||||
|
Message3 = dryrun_input_message(#{
|
||||||
|
payload => #{
|
||||||
|
p => #{<<"hello">> => <<"world">>},
|
||||||
|
q => 1,
|
||||||
|
%% Missing:
|
||||||
|
%% r => true,
|
||||||
|
t => <<"t">>,
|
||||||
|
u => #{a => <<"b">>}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
?assertMatch(
|
||||||
|
{400, #{
|
||||||
|
<<"msg">> :=
|
||||||
|
<<"transformation_eval_operation_failure">>,
|
||||||
|
<<"reason">> :=
|
||||||
|
#{
|
||||||
|
<<"reason">> := <<"var_unbound">>,
|
||||||
|
<<"var_name">> := <<"payload.r">>
|
||||||
|
}
|
||||||
|
}},
|
||||||
|
dryrun_transformation(Transformation1, Message3)
|
||||||
|
),
|
||||||
|
|
||||||
|
ok
|
||||||
|
end,
|
||||||
|
[]
|
||||||
|
),
|
||||||
|
ok.
|
||||||
|
|
|
@ -276,7 +276,7 @@ resolve_var_value(VarName, Bindings, _Opts) ->
|
||||||
Value;
|
Value;
|
||||||
{error, _Reason} ->
|
{error, _Reason} ->
|
||||||
throw(#{
|
throw(#{
|
||||||
var_name => VarName,
|
var_name => iolist_to_binary(VarName),
|
||||||
reason => var_unbound
|
reason => var_unbound
|
||||||
})
|
})
|
||||||
end.
|
end.
|
||||||
|
|
|
@ -18,6 +18,9 @@ emqx_message_transformation_http_api {
|
||||||
reorder_transformations.desc:
|
reorder_transformations.desc:
|
||||||
"""Reorder of all transformations"""
|
"""Reorder of all transformations"""
|
||||||
|
|
||||||
|
test_transformation.desc:
|
||||||
|
"""Test an input against a transformation"""
|
||||||
|
|
||||||
enable_disable_transformation.desc:
|
enable_disable_transformation.desc:
|
||||||
"""Enable or disable a particular transformation"""
|
"""Enable or disable a particular transformation"""
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue