feat(bridge-s3): provide more meaningful error details in status

This commit is contained in:
Andrew Mayorov 2024-06-25 14:45:51 +02:00
parent 98e4ea6fde
commit f3ffbd4710
No known key found for this signature in database
GPG Key ID: 2837C62ACFBFED5D
2 changed files with 62 additions and 13 deletions

View File

@ -146,16 +146,14 @@ on_stop(InstId, _State = #{pool_name := PoolName}) ->
on_get_status(_InstId, State = #{client_config := Config}) -> on_get_status(_InstId, State = #{client_config := Config}) ->
case emqx_s3_client:aws_config(Config) of case emqx_s3_client:aws_config(Config) of
{error, Reason} -> {error, Reason} ->
{?status_disconnected, State, Reason}; {?status_disconnected, State, map_error_details(Reason)};
AWSConfig -> AWSConfig ->
try erlcloud_s3:list_buckets(AWSConfig) of try erlcloud_s3:list_buckets(AWSConfig) of
Props when is_list(Props) -> Props when is_list(Props) ->
?status_connected ?status_connected
catch catch
error:{aws_error, {http_error, _Code, _, Reason}} -> error:Error ->
{?status_disconnected, State, Reason}; {?status_disconnected, State, map_error_details(Error)}
error:{aws_error, {socket_error, Reason}} ->
{?status_disconnected, State, Reason}
end end
end. end.
@ -284,8 +282,8 @@ check_bucket_accessible(Bucket, #{client_config := Config}) ->
catch catch
error:{aws_error, {http_error, 404, _, _Reason}} -> error:{aws_error, {http_error, 404, _, _Reason}} ->
throw({unhealthy_target, "Bucket does not exist"}); throw({unhealthy_target, "Bucket does not exist"});
error:{aws_error, {socket_error, Reason}} -> error:Error ->
throw({unhealthy_target, emqx_utils:format(Reason)}) throw({unhealthy_target, map_error_details(Error)})
end end
end. end.
@ -378,13 +376,31 @@ run_aggregated_upload(InstId, ChannelID, Records, #{aggreg_id := AggregId}) ->
{error, {unrecoverable_error, Reason}} {error, {unrecoverable_error, Reason}}
end. end.
map_error({socket_error, _} = Reason) -> map_error(Error) ->
{recoverable_error, Reason}; {map_error_class(Error), map_error_details(Error)}.
map_error(Reason = {aws_error, Status, _, _Body}) when Status >= 500 ->
map_error_class({s3_error, _, _}) ->
unrecoverable_error;
map_error_class({aws_error, Error}) ->
map_error_class(Error);
map_error_class({socket_error, _}) ->
recoverable_error;
map_error_class({http_error, Status, _, _}) when Status >= 500 ->
%% https://docs.aws.amazon.com/AmazonS3/latest/API/ErrorResponses.html#ErrorCodeList %% https://docs.aws.amazon.com/AmazonS3/latest/API/ErrorResponses.html#ErrorCodeList
{recoverable_error, Reason}; recoverable_error;
map_error(Reason) -> map_error_class(_Error) ->
{unrecoverable_error, Reason}. unrecoverable_error.
map_error_details({s3_error, Code, Message}) ->
emqx_utils:format("S3 error: ~s ~s", [Code, Message]);
map_error_details({aws_error, Error}) ->
map_error_details(Error);
map_error_details({socket_error, Reason}) ->
emqx_utils:format("Socket error: ~s", [emqx_utils:readable_error_msg(Reason)]);
map_error_details({http_error, _, _, _} = Error) ->
emqx_utils:format("AWS error: ~s", [map_aws_error_details(Error)]);
map_error_details(Error) ->
Error.
render_bucket(Template, Data) -> render_bucket(Template, Data) ->
case emqx_template:render(Template, {emqx_jsonish, Data}) of case emqx_template:render(Template, {emqx_jsonish, Data}) of
@ -407,6 +423,32 @@ render_content(Template, Data) ->
iolist_to_string(IOList) -> iolist_to_string(IOList) ->
unicode:characters_to_list(IOList). unicode:characters_to_list(IOList).
%%
-include_lib("xmerl/include/xmerl.hrl").
-spec map_aws_error_details(_AWSError) ->
unicode:chardata().
map_aws_error_details({http_error, _Status, _, Body}) ->
try xmerl_scan:string(unicode:characters_to_list(Body), [{quiet, true}]) of
{Error = #xmlElement{name = 'Error'}, _} ->
map_aws_error_details(Error);
_ ->
Body
catch
exit:_ ->
Body
end;
map_aws_error_details(#xmlElement{content = Content}) ->
Code = extract_xml_text(lists:keyfind('Code', #xmlElement.name, Content)),
Message = extract_xml_text(lists:keyfind('Message', #xmlElement.name, Content)),
[Code, $:, $\s | Message].
extract_xml_text(#xmlElement{content = Content}) ->
[Fragment || #xmlText{value = Fragment} <- Content];
extract_xml_text(false) ->
[].
%% `emqx_connector_aggreg_delivery` APIs %% `emqx_connector_aggreg_delivery` APIs
-spec init_transfer_state(buffer_map(), map()) -> emqx_s3_upload:t(). -spec init_transfer_state(buffer_map(), map()) -> emqx_s3_upload:t().

View File

@ -159,6 +159,13 @@ t_start_broken_update_restart(Config) ->
_Attempts = 20, _Attempts = 20,
?assertEqual({ok, disconnected}, emqx_resource_manager:health_check(ConnectorId)) ?assertEqual({ok, disconnected}, emqx_resource_manager:health_check(ConnectorId))
), ),
?assertMatch(
{ok,
{{_HTTP, 200, _}, _, #{
<<"status_reason">> := <<"AWS error: SignatureDoesNotMatch:", _/bytes>>
}}},
emqx_bridge_v2_testlib:get_connector_api(Type, Name)
),
?assertMatch( ?assertMatch(
{ok, {{_HTTP, 200, _}, _, _}}, {ok, {{_HTTP, 200, _}, _, _}},
emqx_bridge_v2_testlib:update_connector_api(Name, Type, ConnectorConf) emqx_bridge_v2_testlib:update_connector_api(Name, Type, ConnectorConf)