|
|
@@ -146,29 +146,22 @@ on_stop(InstId, _State = #{pool_name := PoolName}) ->
|
|
|
on_get_status(_InstId, State = #{client_config := Config}) ->
|
|
|
case emqx_s3_client:aws_config(Config) of
|
|
|
{error, Reason} ->
|
|
|
- {?status_disconnected, State, Reason};
|
|
|
+ {?status_disconnected, State, map_error_details(Reason)};
|
|
|
AWSConfig ->
|
|
|
try erlcloud_s3:list_buckets(AWSConfig) of
|
|
|
Props when is_list(Props) ->
|
|
|
?status_connected
|
|
|
catch
|
|
|
- error:{aws_error, {http_error, _Code, _, Reason}} ->
|
|
|
- {?status_disconnected, State, Reason};
|
|
|
- error:{aws_error, {socket_error, Reason}} ->
|
|
|
- {?status_disconnected, State, Reason}
|
|
|
+ error:Error ->
|
|
|
+ {?status_disconnected, State, map_error_details(Error)}
|
|
|
end
|
|
|
end.
|
|
|
|
|
|
-spec on_add_channel(_InstanceId :: resource_id(), state(), channel_id(), channel_config()) ->
|
|
|
{ok, state()} | {error, _Reason}.
|
|
|
on_add_channel(_InstId, State = #{channels := Channels}, ChannelId, Config) ->
|
|
|
- try
|
|
|
- ChannelState = start_channel(State, Config),
|
|
|
- {ok, State#{channels => Channels#{ChannelId => ChannelState}}}
|
|
|
- catch
|
|
|
- throw:Reason ->
|
|
|
- {error, Reason}
|
|
|
- end.
|
|
|
+ ChannelState = start_channel(State, Config),
|
|
|
+ {ok, State#{channels => Channels#{ChannelId => ChannelState}}}.
|
|
|
|
|
|
-spec on_remove_channel(_InstanceId :: resource_id(), state(), channel_id()) ->
|
|
|
{ok, state()}.
|
|
|
@@ -217,7 +210,8 @@ start_channel(State, #{
|
|
|
max_records := MaxRecords
|
|
|
},
|
|
|
container := Container,
|
|
|
- bucket := Bucket
|
|
|
+ bucket := Bucket,
|
|
|
+ key := Key
|
|
|
}
|
|
|
}) ->
|
|
|
AggregId = {Type, Name},
|
|
|
@@ -226,7 +220,7 @@ start_channel(State, #{
|
|
|
max_records => MaxRecords,
|
|
|
work_dir => work_dir(Type, Name)
|
|
|
},
|
|
|
- Template = ensure_ok(emqx_bridge_s3_upload:mk_key_template(Parameters)),
|
|
|
+ Template = emqx_bridge_s3_upload:mk_key_template(Key),
|
|
|
DeliveryOpts = #{
|
|
|
bucket => Bucket,
|
|
|
key => Template,
|
|
|
@@ -253,11 +247,6 @@ start_channel(State, #{
|
|
|
on_stop => fun() -> ?AGGREG_SUP:delete_child(AggregId) end
|
|
|
}.
|
|
|
|
|
|
-ensure_ok({ok, V}) ->
|
|
|
- V;
|
|
|
-ensure_ok({error, Reason}) ->
|
|
|
- throw(Reason).
|
|
|
-
|
|
|
upload_options(Parameters) ->
|
|
|
#{acl => maps:get(acl, Parameters, undefined)}.
|
|
|
|
|
|
@@ -285,7 +274,7 @@ channel_status(#{mode := aggregated, aggreg_id := AggregId, bucket := Bucket}, S
|
|
|
check_bucket_accessible(Bucket, #{client_config := Config}) ->
|
|
|
case emqx_s3_client:aws_config(Config) of
|
|
|
{error, Reason} ->
|
|
|
- throw({unhealthy_target, Reason});
|
|
|
+ throw({unhealthy_target, map_error_details(Reason)});
|
|
|
AWSConfig ->
|
|
|
try erlcloud_s3:list_objects(Bucket, [{max_keys, 1}], AWSConfig) of
|
|
|
Props when is_list(Props) ->
|
|
|
@@ -293,8 +282,8 @@ check_bucket_accessible(Bucket, #{client_config := Config}) ->
|
|
|
catch
|
|
|
error:{aws_error, {http_error, 404, _, _Reason}} ->
|
|
|
throw({unhealthy_target, "Bucket does not exist"});
|
|
|
- error:{aws_error, {socket_error, Reason}} ->
|
|
|
- throw({unhealthy_target, emqx_utils:format(Reason)})
|
|
|
+ error:Error ->
|
|
|
+ throw({unhealthy_target, map_error_details(Error)})
|
|
|
end
|
|
|
end.
|
|
|
|
|
|
@@ -304,8 +293,7 @@ check_aggreg_upload_errors(AggregId) ->
|
|
|
%% TODO
|
|
|
%% This approach means that, for example, 3 upload failures will cause
|
|
|
%% the channel to be marked as unhealthy for 3 consecutive health checks.
|
|
|
- ErrorMessage = emqx_utils:format(Error),
|
|
|
- throw({unhealthy_target, ErrorMessage});
|
|
|
+ throw({unhealthy_target, map_error_details(Error)});
|
|
|
[] ->
|
|
|
ok
|
|
|
end.
|
|
|
@@ -384,16 +372,38 @@ run_aggregated_upload(InstId, ChannelID, Records, #{aggreg_id := AggregId}) ->
|
|
|
?tp(s3_bridge_aggreg_push_ok, #{instance_id => InstId, name => AggregId}),
|
|
|
ok;
|
|
|
{error, Reason} ->
|
|
|
- {error, {unrecoverable_error, Reason}}
|
|
|
+ {error, {unrecoverable_error, emqx_utils:explain_posix(Reason)}}
|
|
|
end.
|
|
|
|
|
|
-map_error({socket_error, _} = Reason) ->
|
|
|
- {recoverable_error, Reason};
|
|
|
-map_error(Reason = {aws_error, Status, _, _Body}) when Status >= 500 ->
|
|
|
+map_error(Error) ->
|
|
|
+ {map_error_class(Error), map_error_details(Error)}.
|
|
|
+
|
|
|
+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
|
|
|
- {recoverable_error, Reason};
|
|
|
-map_error(Reason) ->
|
|
|
- {unrecoverable_error, Reason}.
|
|
|
+ recoverable_error;
|
|
|
+map_error_class(_Error) ->
|
|
|
+ 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({failed_to_obtain_credentials, Error}) ->
|
|
|
+ emqx_utils:format("Unable to obtain AWS credentials: ~s", [map_error_details(Error)]);
|
|
|
+map_error_details({upload_failed, Error}) ->
|
|
|
+ map_error_details(Error);
|
|
|
+map_error_details(Error) ->
|
|
|
+ Error.
|
|
|
|
|
|
render_bucket(Template, Data) ->
|
|
|
case emqx_template:render(Template, {emqx_jsonish, Data}) of
|
|
|
@@ -416,6 +426,32 @@ render_content(Template, Data) ->
|
|
|
iolist_to_string(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
|
|
|
|
|
|
-spec init_transfer_state(buffer_map(), map()) -> emqx_s3_upload:t().
|