diff --git a/src/lager_crash_log.erl b/src/lager_crash_log.erl index 9e9b84e..68e0678 100644 --- a/src/lager_crash_log.erl +++ b/src/lager_crash_log.erl @@ -63,11 +63,10 @@ handle_call(_Call, _From, State) -> %% @private handle_cast({log, Event}, #state{name=Name, fd=FD, inode=Inode, flap=Flap, fmtmaxbytes=FmtMaxBytes} = State) -> - TermMaxSize = 500, %% borrowed from riak_err {ReportStr, Pid, MsgStr, _ErrorP} = case Event of {error, _GL, {Pid1, Fmt, Args}} -> - {"ERROR REPORT", Pid1, limited_fmt(Fmt, Args, TermMaxSize, FmtMaxBytes), true}; + {"ERROR REPORT", Pid1, limited_fmt(Fmt, Args, FmtMaxBytes), true}; {error_report, _GL, {Pid1, std_error, Rep}} -> {"ERROR REPORT", Pid1, limited_str(Rep, FmtMaxBytes), true}; {error_report, _GL, Other} -> @@ -120,7 +119,7 @@ code_change(_OldVsn, State, _Extra) -> %% ===== Begin code lifted from riak_err ===== --spec limited_fmt(string(), list(), integer(), integer()) -> iolist(). +-spec limited_fmt(string(), list(), integer()) -> iolist(). %% @doc Format Fmt and Args similar to what io_lib:format/2 does but with %% limits on how large the formatted string may be. %% @@ -128,19 +127,8 @@ code_change(_OldVsn, State, _Extra) -> %% formatting is done by trunc_io:print/2, where FmtMaxBytes is used %% to limit the formatted string's size. -limited_fmt(Fmt, Args, TermMaxSize, FmtMaxBytes) -> - TermSize = erts_debug:flat_size(Args), - if TermSize > TermMaxSize -> - ["Oversize args for format \"", Fmt, "\": \n", - [ - begin - {Str, _} = trunc_io:print(lists:nth(N, Args), FmtMaxBytes), - [" arg", integer_to_list(N), ": ", Str, "\n"] - end || N <- lists:seq(1, length(Args)) - ]]; - true -> - io_lib:format(Fmt, Args) - end. +limited_fmt(Fmt, Args, FmtMaxBytes) -> + trunc_io:format(Fmt, Args, FmtMaxBytes). limited_str(Term, FmtMaxBytes) -> {Str, _} = trunc_io:print(Term, FmtMaxBytes), diff --git a/src/trunc_io.erl b/src/trunc_io.erl index a354385..288c384 100644 --- a/src/trunc_io.erl +++ b/src/trunc_io.erl @@ -29,10 +29,133 @@ -module(trunc_io). -author('matthias@corelatus.se'). %% And thanks to Chris Newcombe for a bug fix --export([print/2, fprint/2, safe/2]). % interface functions +-export([format/3, print/2, fprint/2, safe/2]). % interface functions -export([perf/0, perf/3, perf1/0, test/0, test/2]). % testing functions -version("$Id: trunc_io.erl,v 1.11 2009-02-23 12:01:06 matthias Exp $"). +format(String, Args, Max) -> + Parts = re:split(String, + "(~(?:-??\\d+\\.|\\*\\.|\\.|)(?:-??\\d+\\.|\\*\\.|\\.|)(?:-??\\d+|\\*|)(?:t|)(?:[cfegswpWPBX#bx+ni~]))", + [{return, list}, trim]), + Maxlen = Max - length(String), + format(Parts, Args, Maxlen, [], []). + +format([], _Args, Max, Acc, ArgAcc) -> + FmtArgs = resolve_futures(Max, ArgAcc), + io_lib:format(lists:flatten(lists:reverse(Acc)), lists:reverse(FmtArgs)); +format([[] | T], Args, Max, Acc, ArgAcc) -> + % discard the null list generated by split + format(T, Args, Max, Acc, ArgAcc); +format(["~~" | T], Args, Max, Acc, ArgAcc) -> + format(T, Args, Max+1, ["~~" | Acc], ArgAcc); +format(["~n" | T], Args, Max, Acc, ArgAcc) -> + % ignore newlines for the purposes of argument indexing + format(T, Args, Max+1, ["~n" | Acc], ArgAcc); +format(["~i" | T], [AH | AT], Max, Acc, ArgAcc) -> + % ~i means ignore this argument, but we'll just pass it through + format(T, AT, Max+2, ["~i" | Acc], [AH | ArgAcc]); +format([[$~|H]| T], [AH1, AH2 | AT], Max, Acc, ArgAcc) when H == "X"; H == "x" -> + %% ~X consumes 2 arguments. It only prints integers so we can leave it alone + format(T, AT, Max, ["~X" | Acc], [AH2, AH1 | ArgAcc]); +format([[$~|H]| T], [AH1, _AH2 | AT], Max, Acc, ArgAcc) when H == "W"; H == "P" -> + %% ~P and ~W consume 2 arguments, the second one being a depth limiter. + %% trunc_io isn't (yet) depth aware, so we can't honor this format string + %% safely at the moment, so just treat it like a regular ~p + %% TODO support for depth limiting + case print(AH1, Max + 2) of + {_Res, Max} -> + % this isn't the last argument, but it consumed all available space + % delay calculating the print size until the end + format(T, AT, Max + 2, ["~s" | Acc], [{future, AH1} | ArgAcc]); + {String, Length} -> + format(T, AT, Max + 2 - Length, ["~s" | Acc], [String | ArgAcc]) + end; +format([[$~|H]| T], [AH | AT], Max, Acc, ArgAcc) when length(H) == 1 -> + % single character format specifier, relatively simple + case H of + _ when H == "p"; H == "w"; H == "s" -> + %okay, these are prime candidates for rewriting + case print(AH, Max + 2) of + {_Res, Max} -> + % this isn't the last argument, but it consumed all available space + % delay calculating the print size until the end + format(T, AT, Max + 2, ["~s" | Acc], [{future, AH} | ArgAcc]); + {String, Length} -> + {Value, RealLen} = case H of + "s" -> + % strip off the doublequotes + {string:substr(String, 2, length(String) -2), Length -2}; + _ -> + {String, Length} + end, + format(T, AT, Max + 2 - RealLen, ["~s" | Acc], [Value | ArgAcc]) + end; + _ -> + % whatever, just pass them on through + format(T, AT, Max, [[$~ | H] | Acc], [AH | ArgAcc]) + end; +format([[$~|H]| T], [AH | AT], Max, Acc, ArgAcc) -> + % complicated format specifier, gonna have to parse it.. + %case re:run(H, "^(?:-??(\\d+|\\*)\\.|\\.|)(?:-??(\\d+|\\*)\\.|\\.|)(-??.|)(t|)([cfegswpWPBX#bx+ni])$", [{capture, all_but_first, list}]) of + %% its actually simpler to just look at the last character in the string + case lists:nth(length(H), H) of + C when C == $p; C == $w; C == $s -> + %{match, [_F, _P, _Pad, _Mod, C]} when C == "p"; C=="w"; C=="s" -> + %okay, these are prime candidates for rewriting + case print(AH, Max + length(H) + 1) of + {_Res, Max} -> + % this isn't the last argument, but it consumed all available space + % delay calculating the print size until the end + format(T, AT, Max + length(H) + 1, ["~s" | Acc], [{future, AH} | ArgAcc]); + {String, Length} -> + {Value, RealLen} = case H of + "s" -> + % strip off the doublequotes + {string:substr(String, 2, length(String) -2), Length -2}; + _ -> + {String, Length} + end, + format(T, AT, Max + length(H) + 1 - RealLen, ["~s" | Acc], [Value | ArgAcc]) + end; + C when C == $P; C == $W -> + %{match, [_F, _P, _Pad, _Mod, C]} when C == "P"; C == "W" -> + %% ~P and ~W consume 2 arguments, the second one being a depth limiter. + %% trunc_io isn't (yet) depth aware, so we can't honor this format string + %% safely at the moment, so just treat it like a regular ~p + %% TODO support for depth limiting + [_ | AT2] = AT, + case print(AH, Max + 2) of + {_Res, Max} -> + % this isn't the last argument, but it consumed all available space + % delay calculating the print size until the end + format(T, AT2, Max + 2, ["~s" | Acc], [{future, AH} | ArgAcc]); + {String, Length} -> + format(T, AT2, Max + 2 - Length, ["~s" | Acc], [String | ArgAcc]) + end; + C when C == $X; C == $x -> + %{match, [_F, _P, _Pad, _Mod, C]} when C == "X"; C == "x" -> + %% ~X consumes 2 arguments. It only prints integers so we can leave it alone + [AH2 | AT2] = AT, + format(T, AT2, Max, [[$~|H]|Acc], [AH2, AH |ArgAcc]); + _ -> + format(T, AT, Max, [[$~|H] | Acc], [AH|ArgAcc]) + %nomatch -> + %io:format("unable to match format pattern ~~~p~n", [H]), + %format(T, AT, Max, [[$~|H] | Acc], [AH|ArgAcc]) + end; +format([H | T], Args, Max, Acc, ArgAcc) -> + format(T, Args, Max, [H | Acc], ArgAcc). + +%% for all the really big terms encountered in a format/3 call, try to give each of them an equal share +resolve_futures(Max, Args) -> + Count = length(lists:filter(fun({future, _}) -> true; (_) -> false end, Args)), + case Count of + 0 -> + Args; + _ -> + SingleFmt = Max div Count, + lists:map(fun({future, Value}) -> element(1, print(Value, SingleFmt)); (X) -> X end, Args) + end. %% @doc Returns an flattened list containing the ASCII representation of the given %% term. @@ -76,18 +199,28 @@ print(Atom, _Max) when is_atom(Atom) -> print(<<>>, _Max) -> {"<<>>", 4}; +print(Binary, 0) when is_binary(Binary) -> + {"<<..>>", 6}; + print(Binary, Max) when is_binary(Binary) -> B = binary_to_list(Binary, 1, lists:min([Max, size(Binary)])), {L, Len} = alist_start(B, Max-4), - {["<<", L, ">>"], Len}; + {["<<", L, ">>"], Len+4}; print(Float, _Max) when is_float(Float) -> L = float_to_list(Float), {L, length(L)}; -print(Fun, _Max) when is_function(Fun) -> +print(Fun, Max) when is_function(Fun) -> L = erlang:fun_to_list(Fun), - {L, length(L)}; + case length(L) > Max of + true -> + S = erlang:max(5, Max), + Res = string:substr(L, 1, S) ++ "..>", + {Res, length(Res)}; + _ -> + {L, length(L)} + end; print(Integer, _Max) when is_integer(Integer) -> L = integer_to_list(Integer), @@ -146,7 +279,7 @@ alist_start([], _) -> {"[]", 2}; alist_start(_, Max) when Max < 4 -> {"...", 3}; alist_start([H|T], Max) when H >= 16#20, H =< 16#7e -> % definitely printable {L, Len} = alist([H|T], Max-1), - {[$\"|L], Len + 1}; + {[$"|L], Len + 1}; alist_start([H|T], Max) when H == 9; H == 10; H == 13 -> % show as space {L, Len} = alist(T, Max-1), {[$ |L], Len + 1}; @@ -164,7 +297,7 @@ alist([H|T], Max) when H == 9; H == 10; H == 13 -> % show as space {[$ |L], Len + 1}; alist(L, Max) -> {R, Len} = list_body(L, Max-3), - {[$\", $[, R, $]], Len + 3}. + {[$", $[, R, $]], Len + 3}. %%-------------------- diff --git a/test/trunc_io_eqc.erl b/test/trunc_io_eqc.erl new file mode 100644 index 0000000..f97830a --- /dev/null +++ b/test/trunc_io_eqc.erl @@ -0,0 +1,186 @@ +%% ------------------------------------------------------------------- +%% +%% trunc_io_eqc: QuickCheck test for trunc_io:format with maxlen +%% +%% Copyright (c) 2007-2011 Basho Technologies, Inc. All Rights Reserved. +%% +%% This file is provided to you 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(trunc_io_eqc). + +-ifdef(TEST). +-ifdef(EQC). +-export([test/0, test/1, check/0]). + +-include_lib("eqc/include/eqc.hrl"). +-include_lib("eunit/include/eunit.hrl"). + +-define(QC_OUT(P), + eqc:on_output(fun(Str, Args) -> io:format(user, Str, Args) end, P)). + +%%==================================================================== +%% eunit test +%%==================================================================== + +eqc_test_() -> + {timeout, 300, + {spawn, + [?_assertEqual(true, quickcheck(numtests(500, ?QC_OUT(prop_format()))))] + }}. + +%%==================================================================== +%% Shell helpers +%%==================================================================== + +test() -> + test(100). + +test(N) -> + quickcheck(numtests(N, prop_format())). + +check() -> + check(prop_format(), current_counterexample()). + +%%==================================================================== +%% Generators +%%==================================================================== + +gen_fmt_args() -> + list(oneof([gen_print_str(), + "~~", + {"~p", gen_any(5)}, + {"~w", gen_any(5)}, + {"~s", gen_print_str()}, + {"~P", gen_any(5), 4}, + {"~W", gen_any(5), 4}, + {"~i", gen_any(5)}, + {"~B", nat()}, + {"~b", nat()}, + {"~X", nat(), "0x"}, + {"~x", nat(), "0x"}, + {"~.10#", nat()}, + {"~.10+", nat()}, + {"~.36B", nat()}, + {"~62P", gen_any(5), 4}, + {"~c", gen_char()}, + {"~tc", gen_char()} + %{"~f", real()}, %% floats like to make the fudge limit fail, so don't enable them + %{"~10.f", real()}, + %{"~g", real()}, + %{"~10.g", real()}, + %{"~e", real()}, + %{"~10.e", real()} + ])). + + +%% Generates a printable string +gen_print_str() -> + ?LET(Xs, list(char()), [X || X <- Xs, io_lib:printable_list([X]), X /= $~]). + +gen_any(MaxDepth) -> + oneof([largeint(), + gen_atom(), + nat(), + %real(), + binary(), + gen_pid(), + gen_port(), + gen_ref(), + gen_fun()] ++ + [?LAZY(list(gen_any(MaxDepth - 1))) || MaxDepth /= 0] ++ + [?LAZY(gen_tuple(gen_any(MaxDepth - 1))) || MaxDepth /= 0]). + +gen_atom() -> + elements([abc, def, ghi]). + +gen_tuple(Gen) -> + ?LET(Xs, list(Gen), list_to_tuple(Xs)). + +gen_max_len() -> %% Generate length from 3 to whatever. Needs space for ... in output + ?LET(Xs, int(), 3 + abs(Xs)). + +gen_pid() -> + ?LAZY(spawn(fun() -> ok end)). + +gen_port() -> + ?LAZY(begin + Port = erlang:open_port({spawn, "true"}, []), + erlang:port_close(Port), + Port + end). + +gen_ref() -> + ?LAZY(make_ref()). + +gen_fun() -> + ?LAZY(fun() -> ok end). + +gen_char() -> + oneof(lists:seq($A, $z)). + +%%==================================================================== +%% Property +%%==================================================================== + +%% Checks that trunc_io:format produces output less than or equal to MaxLen +prop_format() -> + ?FORALL({FmtArgs, MaxLen}, {gen_fmt_args(), gen_max_len()}, + begin + FudgeLen = 50, %% trunc_io does not correctly calc safe size of pid/port/numbers/funs + {FmtStr, Args} = build_fmt_args(FmtArgs), + try + Str = lists:flatten(trunc_io:format(FmtStr, Args, MaxLen)), + ?WHENFAIL(begin + io:format(user, "FmtStr: ~p\n", [FmtStr]), + io:format(user, "Args: ~p\n", [Args]), + io:format(user, "FudgeLen: ~p\n", [FudgeLen]), + io:format(user, "MaxLen: ~p\n", [MaxLen]), + io:format(user, "ActLen: ~p\n", [length(Str)]), + io:format(user, "Str: ~p\n", [Str]) + end, + %% Make sure the result is a printable list + %% and if the format string is less than the length, + %% the result string is less than the length. + conjunction([{printable, Str == "" orelse + io_lib:printable_list(Str)}, + {length, length(FmtStr) > MaxLen orelse + length(Str) =< MaxLen + FudgeLen}])) + catch + _:Err -> + io:format(user, "\nException: ~p\n", [Err]), + io:format(user, "FmtStr: ~p\n", [FmtStr]), + io:format(user, "Args: ~p\n", [Args]), + false + end + end). + +%%==================================================================== +%% Internal helpers +%%==================================================================== + +%% Build a tuple of {Fmt, Args} from a gen_fmt_args() return +build_fmt_args(FmtArgs) -> + F = fun({Fmt, Arg}, {FmtStr0, Args0}) -> + {FmtStr0 ++ Fmt, Args0 ++ [Arg]}; + ({Fmt, Arg1, Arg2}, {FmtStr0, Args0}) -> + {FmtStr0 ++ Fmt, Args0 ++ [Arg1, Arg2]}; + (Str, {FmtStr0, Args0}) -> + {FmtStr0 ++ Str, Args0} + end, + lists:foldl(F, {"", []}, FmtArgs). + +-endif. % (TEST). +-endif. % (EQC).