Merge pull request #13052 from kjellwinblad/kjell/evaluate_lazy_trace_values_in_normal_log_formatters/EMQX-12380

fix: evaluate lazy values in normal log formatters when level is debug
This commit is contained in:
Kjell Winblad 2024-05-16 15:02:53 +02:00 committed by GitHub
commit ba74135079
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 137 additions and 30 deletions

View File

@ -76,7 +76,9 @@ best_effort_json_obj(Input) ->
best_effort_json_obj(Input, Config). best_effort_json_obj(Input, Config).
-spec format(logger:log_event(), config()) -> iodata(). -spec format(logger:log_event(), config()) -> iodata().
format(#{level := Level, msg := Msg, meta := Meta}, Config0) when is_map(Config0) -> format(#{level := _Level, msg := _Msg, meta := _Meta} = Entry, Config0) when is_map(Config0) ->
#{level := Level, msg := Msg, meta := Meta} =
emqx_logger_textfmt:evaluate_lazy_values_if_dbg_level(Entry),
Config = add_default_config(Config0), Config = add_default_config(Config0),
[format(Msg, Meta#{level => Level}, Config), "\n"]. [format(Msg, Meta#{level => Level}, Config), "\n"].

View File

@ -16,15 +16,20 @@
-module(emqx_logger_textfmt). -module(emqx_logger_textfmt).
-include("emqx_trace.hrl").
-export([format/2]). -export([format/2]).
-export([check_config/1]). -export([check_config/1]).
-export([try_format_unicode/1]). -export([try_format_unicode/1]).
%% Used in the other log formatters
-export([evaluate_lazy_values_if_dbg_level/1, evaluate_lazy_values/1]).
check_config(X) -> logger_formatter:check_config(maps:without([timestamp_format], X)). check_config(X) -> logger_formatter:check_config(maps:without([timestamp_format], X)).
%% Principle here is to delegate the formatting to logger_formatter:format/2 %% Principle here is to delegate the formatting to logger_formatter:format/2
%% as much as possible, and only enrich the report with clientid, peername, topic, username %% as much as possible, and only enrich the report with clientid, peername, topic, username
format(#{msg := {report, ReportMap}, meta := Meta} = Event, Config) when is_map(ReportMap) -> format(#{msg := {report, ReportMap0}, meta := _Meta} = Event0, Config) when is_map(ReportMap0) ->
#{msg := {report, ReportMap}, meta := Meta} = Event = evaluate_lazy_values_if_dbg_level(Event0),
%% The most common case, when entering from SLOG macro %% The most common case, when entering from SLOG macro
%% i.e. logger:log(Level, #{msg => "my_msg", foo => bar}) %% i.e. logger:log(Level, #{msg => "my_msg", foo => bar})
ReportList = enrich_report(ReportMap, Meta), ReportList = enrich_report(ReportMap, Meta),
@ -40,12 +45,39 @@ format(#{msg := {string, String}} = Event, Config) ->
%% copied from logger_formatter:format/2 %% copied from logger_formatter:format/2
%% unsure how this case is triggered %% unsure how this case is triggered
format(Event#{msg => {"~ts ", [String]}}, Config); format(Event#{msg => {"~ts ", [String]}}, Config);
format(#{msg := Msg0, meta := Meta} = Event, Config) -> format(#{msg := _Msg, meta := _Meta} = Event0, Config) ->
#{msg := Msg0, meta := Meta} = Event1 = evaluate_lazy_values_if_dbg_level(Event0),
%% For format strings like logger:log(Level, "~p", [Var]) %% For format strings like logger:log(Level, "~p", [Var])
%% and logger:log(Level, "message", #{key => value}) %% and logger:log(Level, "message", #{key => value})
Msg1 = enrich_client_info(Msg0, Meta), Msg1 = enrich_client_info(Msg0, Meta),
Msg2 = enrich_topic(Msg1, Meta), Msg2 = enrich_topic(Msg1, Meta),
fmt(Event#{msg := Msg2}, Config). fmt(Event1#{msg := Msg2}, Config).
%% Most log entries with lazy values are trace events with level debug. So to
%% be more efficient we only search for lazy values to evaluate in the entries
%% with level debug in the main log formatters.
evaluate_lazy_values_if_dbg_level(#{level := debug} = Map) ->
evaluate_lazy_values(Map);
evaluate_lazy_values_if_dbg_level(Map) ->
Map.
evaluate_lazy_values(Map) when is_map(Map) ->
maps:map(fun evaluate_lazy_values_kv/2, Map);
evaluate_lazy_values({report, Report}) ->
{report, evaluate_lazy_values(Report)};
evaluate_lazy_values(V) ->
V.
evaluate_lazy_values_kv(_K, #emqx_trace_format_func_data{function = Formatter, data = V}) ->
try
NewV = Formatter(V),
evaluate_lazy_values(NewV)
catch
_:_ ->
V
end;
evaluate_lazy_values_kv(_K, V) ->
evaluate_lazy_values(V).
fmt(#{meta := #{time := Ts}} = Data, Config) -> fmt(#{meta := #{time := Ts}} = Data, Config) ->
Timestamp = Timestamp =

View File

@ -15,11 +15,9 @@
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
-module(emqx_trace_formatter). -module(emqx_trace_formatter).
-include("emqx_mqtt.hrl"). -include("emqx_mqtt.hrl").
-include("emqx_trace.hrl").
-export([format/2]). -export([format/2]).
-export([format_meta_map/1]). -export([format_meta_map/1]).
-export([evaluate_lazy_values/1]).
%% logger_formatter:config/0 is not exported. %% logger_formatter:config/0 is not exported.
-type config() :: map(). -type config() :: map().
@ -30,35 +28,20 @@
LogEvent :: logger:log_event(), LogEvent :: logger:log_event(),
Config :: config(). Config :: config().
format( format(
#{level := debug, meta := Meta0 = #{trace_tag := Tag}, msg := Msg}, #{level := debug, meta := _Meta = #{trace_tag := _Tag}, msg := _Msg} = Entry,
#{payload_encode := PEncode} #{payload_encode := PEncode}
) -> ) ->
Meta1 = evaluate_lazy_values(Meta0), #{level := debug, meta := Meta = #{trace_tag := Tag}, msg := Msg} =
emqx_logger_textfmt:evaluate_lazy_values(Entry),
Time = emqx_utils_calendar:now_to_rfc3339(microsecond), Time = emqx_utils_calendar:now_to_rfc3339(microsecond),
ClientId = to_iolist(maps:get(clientid, Meta1, "")), ClientId = to_iolist(maps:get(clientid, Meta, "")),
Peername = maps:get(peername, Meta1, ""), Peername = maps:get(peername, Meta, ""),
MetaBin = format_meta(Meta1, PEncode), MetaBin = format_meta(Meta, PEncode),
Msg1 = to_iolist(Msg), Msg1 = to_iolist(Msg),
Tag1 = to_iolist(Tag), Tag1 = to_iolist(Tag),
[Time, " [", Tag1, "] ", ClientId, "@", Peername, " msg: ", Msg1, ", ", MetaBin, "\n"]; [Time, " [", Tag1, "] ", ClientId, "@", Peername, " msg: ", Msg1, ", ", MetaBin, "\n"];
format(Event, Config) -> format(Event, Config) ->
emqx_logger_textfmt:format(evaluate_lazy_values(Event), Config). emqx_logger_textfmt:format(Event, Config).
evaluate_lazy_values(Map) when is_map(Map) ->
maps:map(fun evaluate_lazy_values_kv/2, Map);
evaluate_lazy_values(V) ->
V.
evaluate_lazy_values_kv(_K, #emqx_trace_format_func_data{function = Formatter, data = V}) ->
try
NewV = Formatter(V),
evaluate_lazy_values(NewV)
catch
_:_ ->
V
end;
evaluate_lazy_values_kv(_K, V) ->
evaluate_lazy_values(V).
format_meta_map(Meta) -> format_meta_map(Meta) ->
Encode = emqx_trace_handler:payload_encode(), Encode = emqx_trace_handler:payload_encode(),

View File

@ -36,8 +36,8 @@ format(
LogMap, LogMap,
#{payload_encode := PEncode} = Config #{payload_encode := PEncode} = Config
) -> ) ->
LogMap0 = maybe_format_msg(LogMap, Config), LogMap0 = emqx_logger_textfmt:evaluate_lazy_values(LogMap),
LogMap1 = emqx_trace_formatter:evaluate_lazy_values(LogMap0), LogMap1 = maybe_format_msg(LogMap0, Config),
%% We just make some basic transformations on the input LogMap and then do %% We just make some basic transformations on the input LogMap and then do
%% an external call to create the JSON text %% an external call to create the JSON text
Time = emqx_utils_calendar:now_to_rfc3339(microsecond), Time = emqx_utils_calendar:now_to_rfc3339(microsecond),

View File

@ -0,0 +1,90 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2019-2024 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_logger_fmt_SUITE).
-compile(export_all).
-compile(nowarn_export_all).
-include_lib("eunit/include/eunit.hrl").
-include("emqx_trace.hrl").
all() -> emqx_common_test_helpers:all(?MODULE).
init_per_testcase(_TestCase, Config) ->
Config.
end_per_testcase(_TestCase, Config) ->
Config.
t_text_fmt_lazy_values(_) ->
check_fmt_lazy_values(emqx_logger_textfmt).
t_text_fmt_lazy_values_only_in_debug_level_events(_) ->
check_fmt_lazy_values_only_in_debug_level_events(emqx_logger_textfmt).
t_json_fmt_lazy_values(_) ->
check_fmt_lazy_values(emqx_logger_jsonfmt).
t_json_fmt_lazy_values_only_in_debug_level_events(_) ->
check_fmt_lazy_values_only_in_debug_level_events(emqx_logger_jsonfmt).
check_fmt_lazy_values(FormatModule) ->
LogEntryIOData = FormatModule:format(event_with_lazy_value(), conf()),
LogEntryBin = unicode:characters_to_binary(LogEntryIOData),
%% Result of lazy evealuation should exist
?assertNotEqual(nomatch, binary:match(LogEntryBin, [<<"hej">>])),
%% The lazy value should have been evaluated
?assertEqual(nomatch, binary:match(LogEntryBin, [<<"emqx_trace_format_func_data">>])),
ok.
check_fmt_lazy_values_only_in_debug_level_events(FormatModule) ->
%% For performace reason we only search for lazy values to evaluate if log level is debug
WarningEvent = (event_with_lazy_value())#{level => info},
LogEntryIOData = FormatModule:format(WarningEvent, conf()),
LogEntryBin = unicode:characters_to_binary(LogEntryIOData),
%% The input data for the formatting should exist
?assertNotEqual(nomatch, binary:match(LogEntryBin, [<<"hej">>])),
%% The lazy value should not have been evaluated
?assertNotEqual(nomatch, binary:match(LogEntryBin, [<<"emqx_trace_format_func_data">>])),
ok.
conf() ->
#{
time_offset => [],
chars_limit => unlimited,
depth => 100,
single_line => true,
template => ["[", level, "] ", msg, "\n"],
timestamp_format => auto
}.
event_with_lazy_value() ->
#{
meta => #{
pid => what,
time => 1715763862274127,
gl => what,
report_cb => fun logger:format_otp_report/1
},
msg =>
{report, #{
reason =>
#emqx_trace_format_func_data{function = fun(Data) -> Data end, data = hej},
msg => hej
}},
level => debug
}.