Current section
Files
Jump to
Current section
Files
src/lager_transform.erl
%% @copyright 2019-2024 Guilherme Andrade
%%
%% Permission is hereby granted, free of charge, to any person obtaining a
%% copy of this software and associated documentation files (the "Software"),
%% to deal in the Software without restriction, including without limitation
%% the rights to use, copy, modify, merge, publish, distribute, sublicense,
%% and/or sell copies of the Software, and to permit persons to whom the
%% Software is furnished to do so, subject to the following conditions:
%%
%% The above copyright notice and this permission notice shall be included in
%% all copies or substantial portions of the Software.
%%
%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
%% DEALINGS IN THE SOFTWARE.
-module(lager_transform).
-ifdef(E48).
-moduledoc "Parse transform".
-endif.
-include_lib("stdlib/include/assert.hrl").
-include("lager.hrl").
%%-------------------------------------------------------------------
%% API Function Exports
%%-------------------------------------------------------------------
-export([parse_transform/2]).
-ignore_xref([parse_transform/2]).
%%-------------------------------------------------------------------
%% Internal API Function Exports
%%-------------------------------------------------------------------
-export([get_pr_context/1]).
%% ------------------------------------------------------------------
%% Macro Definitions
%% ------------------------------------------------------------------
-define(pr_context_attribute_name, '___$fake_lager.pr_context').
%%-------------------------------------------------------------------
%% Record and Type Definitions
%%-------------------------------------------------------------------
-record(module_context, {
name :: module(),
file :: string() | undefined,
sinks :: [atom(), ...],
pr_context :: fake_lager_pr:context()
}).
-record(function_context, {
module :: module(),
file :: string() | undefined,
name :: atom(),
arity :: arity(),
sinks :: [atom(), ...]
}).
%% ------------------------------------------------------------------
%% Static Check Tweaks
%% ------------------------------------------------------------------
-elvis([
{elvis_style, macro_naming_convention, disable},
{elvis_style, no_throw, disable},
{elvis_text_style, max_line_length, disable}
]).
%%-------------------------------------------------------------------
%% API Function Definitions
%%-------------------------------------------------------------------
-spec parse_transform(term(), [tuple()]) -> term().
parse_transform(Ast, Options) ->
_ = check_for_unsupported_options(Options),
ExtraSinks = proplists:get_value(lager_extra_sinks, Options, []),
Sinks = [lager | ExtraSinks],
%write_terms("ast_before.txt", Ast),
Module = get_module(Ast),
File = get_file(Ast),
InitialContext = #module_context{
name = Module,
file = File,
sinks = Sinks,
pr_context = fake_lager_pr:new_context()
},
{MappedAst, FinalContext} =
lists:mapfoldl(
fun mapfold_ast_statement/2,
InitialContext,
Ast
),
AstWithRecordDefs =
insert_pr_context_attribute(
MappedAst,
FinalContext#module_context.pr_context
),
%write_terms("ast_after.txt", AstWithRecordDefs),
AstWithRecordDefs.
%%-------------------------------------------------------------------
%% Internal API Function Definitions
%%-------------------------------------------------------------------
-spec get_pr_context(Module) -> Context when
Module :: module(),
Context :: fake_lager_pr:context().
%% @private
get_pr_context(Module) ->
Attributes = apply(Module, module_info, [attributes]),
case lists:keyfind(?pr_context_attribute_name, 1, Attributes) of
{?pr_context_attribute_name, [PrContext]} ->
PrContext;
false ->
throw(no_pr_context)
end.
%%-------------------------------------------------------------------
%% Internal Function Definitions - Metadata
%%-------------------------------------------------------------------
get_module(Ast) ->
ModuleAttribute = lists:keyfind(module, 3, Ast),
?assertNotEqual(false, ModuleAttribute),
case erl_syntax_lib:analyze_module_attribute(ModuleAttribute) of
Module when is_atom(Module) ->
Module;
{Module, _Params} when is_atom(Module) ->
Module
end.
get_file(Ast) ->
case lists:keyfind(file, 3, Ast) of
false ->
undefined;
FileAttribute ->
{[_ | _] = File, _Line} = erl_syntax_lib:analyze_file_attribute(FileAttribute),
File
end.
%%-------------------------------------------------------------------
%% Internal Function Definitions - Tree Walking
%%-------------------------------------------------------------------
mapfold_ast_statement({function, Anno, Name, Arity, Clauses}, Context) ->
#module_context{name = Module, file = File, sinks = Sinks} = Context,
FunctionContext = #function_context{
module = Module,
file = File,
name = Name,
arity = Arity,
sinks = Sinks
},
MappedClauses = [walk_function_statements(Clause, FunctionContext) || Clause <- Clauses],
MappedStatement = {function, Anno, Name, Arity, MappedClauses},
{MappedStatement, Context};
mapfold_ast_statement({attribute, _, record, {Name, Fields}} = Statement, Context) ->
#module_context{pr_context = PrContext} = Context,
FieldNames = record_field_names(Fields),
UpdatedPrContext = fake_lager_pr:save_record_def(Name, FieldNames, PrContext),
UpdatedContext = Context#module_context{pr_context = UpdatedPrContext},
{Statement, UpdatedContext};
mapfold_ast_statement(Statement, Context) ->
{Statement, Context}.
walk_function_statements(
{call, Anno,
{remote, RemoteAnno, {atom, _ModuleAnno, Module}, {atom, _FunctionAnno, Function}} =
InvocationClause,
Args},
Context
) ->
Arity = length(Args),
MappedArgs = [walk_function_statements(Arg, Context) || Arg <- Args],
SinkModules = Context#function_context.sinks,
case
lists:member(Module, SinkModules) andalso
lists:member(Arity, [1, 2, 3]) andalso
(lists:member(Function, ?LEVELS) orelse
{true_but_unsafe, lists:keyfind(Function, 1, ?LEVELS_UNSAFE)})
of
true when
% replace with `ok'
Function =:= none
->
{atom, RemoteAnno, ok};
true ->
transform_call(Anno, Function, MappedArgs, Module, Context);
{true_but_unsafe, {_, Level}} ->
transform_call(Anno, Level, MappedArgs, Module, Context);
_ ->
{call, Anno, InvocationClause, MappedArgs}
end;
walk_function_statements(Node, Context) when is_tuple(Node) ->
erl_syntax:revert(
erl_syntax_lib:map_subtrees(
fun(Child) -> walk_function_statements(Child, Context) end,
Node
)
);
walk_function_statements(Node, _Context) ->
Node.
%%-------------------------------------------------------------------
%% Internal Function Definitions - Transformation of Logging Calls
%%-------------------------------------------------------------------
transform_call(Anno, Level, Args, Module, Context) ->
{Format, FormatArgs} = format_and_args(Anno, Args),
Location = location_metadata(Anno, Context),
CallMetadata = call_metadata(Anno, Args, Module),
% Gate the whole call on a compile-time-inlined `logger:allow/2' check, exactly as
% logger's own `?LOG_*' macros do (and as lager's original inlined level check did).
% The format string, arguments and metadata textually live only in the `true' branch,
% so they are evaluated solely when the level is enabled - no fun-wrapping or cost
% heuristic needed, and side effects in the arguments fire exactly when the message is
% emitted.
%
% The enabled branch dispatches through `logger:macro_log/5' rather than
% `logger:Level/3': like the macros, this goes straight to `logger:log_allowed/4' and
% skips the redundant `logger_config:allow/2' check that `logger:Level/3' (via
% `do_log/3') would otherwise repeat. `macro_log/5' has been exported for exactly this
% purpose since logger landed in OTP 21.
%
% The first argument is a populated `logger:location()' map carrying `mfa'/`file'/`line',
% built exactly as logger's own `?LOCATION' macro does. Passing a populated location
% (rather than `#{}') keeps the call within `macro_log/5's published contract -
% `location()' mandates those three keys - so dialyzer/eqwalizer runs over downstream
% transformed modules stay clean. Per logger's documented merge order the location is
% also the lowest-priority metadata source, overridden by process and call-site metadata,
% matching standard logger semantics.
AllowCall =
{call, Anno, {remote, Anno, {atom, Anno, logger}, {atom, Anno, allow}}, [
{atom, Anno, Level},
{atom, Anno, Context#function_context.module}
]},
LogCall =
{call, Anno, {remote, Anno, {atom, Anno, logger}, {atom, Anno, macro_log}}, [
Location,
{atom, Anno, Level},
Format,
FormatArgs,
CallMetadata
]},
{'case', Anno, AllowCall, [
{clause, Anno, [{atom, Anno, true}], [], [LogCall]},
{clause, Anno, [{atom, Anno, false}], [], [{atom, Anno, ok}]}
]}.
format_and_args(Anno, Args) ->
case Args of
[Format] ->
% empty argument list
{Format, {nil, Anno}};
[Format, FormatArgs] ->
{Format, FormatArgs};
[_Metadata, Format, FormatArgs] ->
{Format, FormatArgs}
end.
%%-------------------------------------------------------------------
%% Internal Function Definitions - Logging Call Metadata
%%-------------------------------------------------------------------
% Builds the `logger:location()' map (`macro_log/5's first argument): the
% `mfa'/`file'/`line' triple, exactly as logger's `?LOCATION' macro. All three
% keys are mandatory in `location()', so `file' is always emitted - falling back
% to the empty string in the (practically impossible) absence of a `-file'
% attribute - to keep the literal a valid `location()' for downstream dialyzer.
location_metadata(Anno, Context) ->
Line = erl_anno:line(Anno),
File =
case Context#function_context.file of
undefined -> "";
DefinedFile -> DefinedFile
end,
{map, Anno, [
{map_field_assoc, Anno, {atom, Anno, mfa},
{tuple, Anno, [
{atom, Anno, Context#function_context.module},
{atom, Anno, Context#function_context.name},
{integer, Anno, Context#function_context.arity}
]}},
{map_field_assoc, Anno, {atom, Anno, file}, {string, Anno, File}},
{map_field_assoc, Anno, {atom, Anno, line}, {integer, Anno, Line}}
]}.
% Builds the call metadata (`macro_log/5's last argument): `lager_sink' for extra
% sinks plus any user-supplied metadata. The `mfa'/`file'/`line' triple lives in
% the location map instead (see location_metadata/2).
call_metadata(Anno, Args, Module) ->
SinkMetadata = sink_metadata(Anno, Module),
case Args of
[_Fmt] ->
SinkMetadata;
[_Fmt, _FmtArgs] ->
SinkMetadata;
[ExtraMetadataList, _Fmt, _FmtArgs] ->
extended_call_metadata(Anno, SinkMetadata, ExtraMetadataList)
end.
sink_metadata(Anno, Module) ->
Associations =
% lager_sink => atom()
case Module of
lager ->
[];
CustomSink ->
[{map_field_assoc, Anno, {atom, Anno, lager_sink}, {atom, Anno, CustomSink}}]
end,
{map, Anno, Associations}.
extended_call_metadata(Anno, BaseMetadata, ExtraMetadataList) ->
case transform_metadata_list_into_map_field_associations(ExtraMetadataList) of
{true, ExtraMetadataAssociations} ->
{map, _, BaseMetadataAssociations} = BaseMetadata,
{map, Anno, BaseMetadataAssociations ++ ExtraMetadataAssociations};
false ->
runtime_merged_extended_call_metadata(Anno, BaseMetadata, ExtraMetadataList)
end.
transform_metadata_list_into_map_field_associations(ExtraMetadataList) ->
transform_metadata_list_into_map_field_associations_recur(ExtraMetadataList, []).
transform_metadata_list_into_map_field_associations_recur(Clause, Acc) ->
case Clause of
{cons, ConsAnno, {tuple, _, [{atom, _, _} = KeyTerm, ValueTerm]}, NextClause} ->
Assoc = {map_field_assoc, ConsAnno, KeyTerm, ValueTerm},
UpdatedAcc = [Assoc | Acc],
transform_metadata_list_into_map_field_associations_recur(NextClause, UpdatedAcc);
{nil, _} ->
Associations = lists:reverse(Acc),
{true, Associations};
_ ->
false
end.
runtime_merged_extended_call_metadata(Anno, BaseMetadata, ExtraMetadataList) ->
% maps:merge(ExtraMetadata, BaseMetadata)
{call, Anno, {remote, Anno, {atom, Anno, maps}, {atom, Anno, merge}},
% Map1 - maps:from_list(ExtraMetadataList)
[
{call, Anno, {remote, Anno, {atom, Anno, maps}, {atom, Anno, from_list}}, [
ExtraMetadataList
]},
% Map2 - BaseMetadata
BaseMetadata
]}.
%%-------------------------------------------------------------------
%% Internal Function Definitions - Pretty Printing of Records
%%-------------------------------------------------------------------
record_field_names(FieldsFromAst) ->
lists:map(fun record_field_name/1, FieldsFromAst).
%% `record_field_name/1' copied from original `lager_transform',
%% licensed under Apache 2.0
record_field_name({record_field, _, {atom, _, FieldName}}) ->
FieldName;
record_field_name({record_field, _, {atom, _, FieldName}, _Default}) ->
FieldName;
record_field_name({typed_record_field, Field, _Type}) ->
record_field_name(Field).
insert_pr_context_attribute([Attribute | Next], RecordDefs) ->
case Attribute of
{attribute, Line, module, _} = ModuleAttribute ->
[
ModuleAttribute,
{attribute, Line, ?pr_context_attribute_name, [RecordDefs]}
| Next
];
OtherAttribute ->
[
OtherAttribute
| insert_pr_context_attribute(Next, RecordDefs)
]
end;
insert_pr_context_attribute([], _RecordDefs) ->
% There's no module attribute - let the compiler handle it rather than have us crash
[].
%%-------------------------------------------------------------------
%% Internal Function Definitions - Utiliities
%%-------------------------------------------------------------------
check_for_unsupported_options(Options) ->
lists:foreach(
fun
({lager_print_records_flag, true}) ->
ok;
({Key, Value}) when
Key =:= lager_truncation_size;
Key =:= lager_print_records_flag;
Key =:= lager_function_transforms
->
error_logger:error_msg("[error] Unsupported option: '~s~n'", [{Key, Value}]),
exit(normal);
(_) ->
ok
end,
Options
).
%write_terms(FilenameSuffix, List) ->
% {attribute, _Anno, module, Module} = lists:keyfind(module, 3, List),
% Filename = atom_to_list(Module) ++ "." ++ FilenameSuffix,
% Format = fun(Term) -> io_lib:format("~tp.~n", [Term]) end,
% Text = lists:map(Format, List),
% file:write_file(Filename, Text).