Current section

Files

Jump to
zotonic_mod_logging src models m_log_ui.erl
Raw

src/models/m_log_ui.erl

%% @author Marc Worrell
%% @copyright 2019-2021 Marc Worrell
%% @doc Model for ui log messages.
%% Copyright 2019-2021 Marc Worrell
%%
%% 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(m_log_ui).
-author("Marc Worrell").
-behaviour(zotonic_model).
%% interface functions
-export([
m_get/3,
insert_event/2,
insert/2,
get/2,
search_query/2,
periodic_cleanup/1,
install/1
]).
-include_lib("zotonic_core/include/zotonic.hrl").
m_get([ Index | Rest ], _Msg, Context) ->
case z_acl:is_admin(Context) of
true -> {ok, {get(z_convert:to_integer(Index), Context), Rest}};
false -> {error, eacces}
end;
m_get(_Vs, _Msg, _Context) ->
{error, unknown_path}.
get(Id, Context) ->
case z_db:select(log_ui, Id, Context) of
{ok, R} -> R;
{error, enoent} -> undefined
end.
insert(Props, Context) ->
z_db:insert(log_ui, Props, Context).
insert_event(Props, Context) when is_map(Props) ->
insert_event(maps:to_list(Props), Context);
insert_event(Props, Context) when is_list(Props) ->
case is_event_loggable(Props) of
true ->
UserId = z_acl:user(Context),
Props1 = lists:filter( fun filter_prop/1, Props ),
Props2 = lists:map( fun map_prop/1, Props1 ),
Type = case proplists:get_value(type, Props2) of
undefined -> <<"error">>;
T -> T
end,
Message = [
{user_id, UserId},
{type, Type},
{remote_ip, m_req:get(peer, Context)}
] ++ proplists:delete(type, Props2),
MsgUserProps = maybe_add_user_props(Message, Context),
z_db:insert(log_ui, MsgUserProps, Context);
false ->
{error, ignore}
end.
is_event_loggable(Props) ->
not is_ignored_bot(proplists:get_value(<<"user_agent">>, Props)).
is_ignored_bot(undefined) ->
false;
is_ignored_bot(UserAgent) ->
binary:match(UserAgent, <<"YandexMobileBot">>) /= nomatch
orelse binary:match(UserAgent, <<"YandexBot">>) /= nomatch
orelse binary:match(UserAgent, <<"pingbot">>) /= nomatch
orelse binary:match(UserAgent, <<"BingPreview">>) /= nomatch.
maybe_add_user_props(Props, Context) ->
case proplists:get_value(user_id, Props) of
undefined ->
Props;
UserId ->
[
{user_name_first, m_rsc:p_no_acl(UserId, name_first, Context)},
{user_name_surname, m_rsc:p_no_acl(UserId, name_surname, Context)},
{user_email_raw, m_rsc:p_no_acl(UserId, email_raw, Context)}
| Props
]
end.
filter_prop({_, V}) when not is_binary(V), not is_number(V) -> false;
filter_prop({<<"type">>, _}) -> true;
filter_prop({<<"message">>, _}) -> true;
filter_prop({<<"file">>, _}) -> true;
filter_prop({<<"line">>, _}) -> true;
filter_prop({<<"col">>, _}) -> true;
filter_prop({<<"stack">>, _}) -> true;
filter_prop({<<"user_agent">>, _}) -> true;
filter_prop({<<"url">>, _}) -> true;
filter_prop(_) -> false.
map_prop({K, null}) -> map_prop({K, undefined});
map_prop({<<"type">>, T}) ->
T1 = case T of
<<"fatal">> -> <<"fatal">>;
<<"error">> -> <<"error">>;
<<"warning">> -> <<"warning">>;
<<"notice">> -> <<"notice">>;
<<"info">> -> <<"info">>;
<<"debug">> -> <<"debug">>;
_ -> <<"error">>
end,
{type, T1};
map_prop({<<"message">>, M}) when is_binary(M) -> {message, z_string:truncatechars(M, 500)};
map_prop({<<"stack">>, M}) when is_binary(M) -> {stack, z_string:truncatechars(M, 1000)};
map_prop({<<"file">>, M}) when is_binary(M) -> {file, z_string:truncatechars(M, 500)};
map_prop({<<"line">>, M}) -> {line, z_convert:to_integer(M)};
map_prop({<<"col">>, M}) -> {col, z_convert:to_integer(M)};
map_prop({<<"user_agent">>, M}) when is_binary(M) -> {user_agent, z_string:truncatechars(M, 500)};
map_prop({<<"url">>, M}) when is_binary(M) -> {url, z_string:truncatechars(M, 500)}.
-spec search_query( map(), z:context() ) -> #search_sql{}.
search_query(Args, Context) ->
% Filter on log type
W1 = case z_convert:to_binary( maps:get(<<"type">>, Args, <<"warning">>) ) of
<<"error">> -> " type = 'error' ";
<<"warning">> -> " type in ('warning', 'error') ";
<<"notice">> -> " type in ('notice', 'warning', 'error') ";
<<"info">> -> " type <> 'debug' ";
<<"debug">> -> "";
_ -> " type in ('warning', 'error') "
end,
As1 = [],
% Filter on user-id
{W2, As2} = case maps:get(<<"user">>, Args, undefined) of
undefined -> {W1, As1};
"" -> {W1, As1};
<<>> -> {W1, As1};
User ->
try
WU = case W1 of
"" -> " user_id = $" ++ integer_to_list(length(As1) + 1);
_ -> W1 ++ " and user_id = $" ++ integer_to_list(length(As1) + 1)
end,
{WU, As1 ++ [ m_rsc:rid(User, Context) ]}
catch
_:_ ->
{W1, As1}
end
end,
% SQL search question
#search_sql{
select = "id",
from = "log_ui",
where = W2,
order = "id DESC",
args = As2,
assoc = false
}.
%% @doc Periodic cleanup of max 10K items older than 1 week
periodic_cleanup(Context) ->
z_db:q("
delete from log_ui
where id in (
select id
from log_ui
where created < now() - interval '1 weeks'
limit 10000
)",
Context,
300000).
install(Context) ->
case z_db:table_exists(log_ui, Context) of
true ->
case z_db:column(log_ui, remote_ip, Context) of
{ok, #column_def{ length = N }} when N < 42 ->
z_db:q("alter table log_ui alter column remote_ip type character varying(42)", Context),
z_db:flush(Context);
_ ->
ok
end,
ok;
false ->
z_db:q("
create table log_ui (
id bigserial not null,
rsc_id int,
user_id int,
type character varying(80) not null default ''::character varying,
remote_ip character varying(42),
props bytea,
created timestamp with time zone not null default now(),
constraint log_ui_pkey primary key (id),
constraint fk_log_ui_rsc_id foreign key (rsc_id)
references rsc(id)
on delete set null on update cascade,
constraint fk_log_ui_user_id foreign key (user_id)
references rsc(id)
on delete set null on update cascade
)
", Context),
Indices = [
{"fki_log_ui_rsc_id", "rsc_id"},
{"fki_log_ui_user_id", "user_id"},
{"log_ui_type_created_key", "type, created"},
{"log_ui_created_key", "created"}
],
[ z_db:q("create index "++Name++" on log ("++Cols++")", Context) || {Name, Cols} <- Indices ]
end.