diff --git a/include/ec_cmd_log.hrl b/include/ec_cmd_log.hrl new file mode 100644 index 0000000..170d399 --- /dev/null +++ b/include/ec_cmd_log.hrl @@ -0,0 +1,24 @@ +%% -*- erlang-indent-level: 4; indent-tabs-mode: nil; fill-column: 80 -*- +%%% Copyright 2012 Erlware, LLC. 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. +%%%--------------------------------------------------------------------------- +%%% @author Eric Merritt +%%% @copyright (C) 2012 Erlware, LLC. + +-define(EC_ERROR, 0). +-define(EC_WARN, 1). +-define(EC_INFO, 2). +-define(EC_DEBUG, 3). diff --git a/src/ec_cmd_log.erl b/src/ec_cmd_log.erl new file mode 100644 index 0000000..0be4143 --- /dev/null +++ b/src/ec_cmd_log.erl @@ -0,0 +1,255 @@ +%% -*- erlang-indent-level: 4; indent-tabs-mode: nil; fill-column: 80 -*- +%%% Copyright 2012 Erlware, LLC. 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. +%%%--------------------------------------------------------------------------- +%%% @author Eric Merritt +%%% @copyright (C) 2012 Erlware, LLC. +%%% +%%% @doc This provides simple output functions for command line apps. You should +%%% use this to talk to the users if you are wrting code for the system +-module(ec_cmd_log). + +-export([new/1, + new/2, + log/4, + should/2, + debug/2, + debug/3, + info/2, + info/3, + error/2, + error/3, + warn/2, + warn/3, + log_level/1, + atom_log_level/1, + format/1]). + +-include_lib("erlware_commons/include/ec_cmd_log.hrl"). + +-define(RED, 31). +-define(GREEN, 32). +-define(YELLOW, 33). +-define(BLUE, 34). +-define(MAGENTA, 35). +-define(CYAN, 36). + +-define(PREFIX, "===> "). + +-record(state_t, {mod=?MODULE :: ec_log, + log_level=0 :: int_log_level(), + caller=api :: api | command_line}). + +%%============================================================================ +%% types +%%============================================================================ +-export_type([t/0, + int_log_level/0, + atom_log_level/0, + log_level/0, + log_fun/0]). + +-type log_level() :: int_log_level() | atom_log_level(). + +-type int_log_level() :: 0..3. + +-type atom_log_level() :: error | warn | info | debug. + +-type log_fun() :: fun(() -> iolist()). + +-type color() :: 31..36. + +-opaque t() :: record(state_t). + +%%============================================================================ +%% API +%%============================================================================ +%% @doc Create a new 'log level' for the system +-spec new(log_level()) -> t(). +new(LogLevel) -> + new(LogLevel, api). + +new(LogLevel, Caller) when LogLevel >= 0, LogLevel =< 3 -> + #state_t{mod=?MODULE, log_level=LogLevel, caller=Caller}; +new(AtomLogLevel, Caller) + when AtomLogLevel =:= error; + AtomLogLevel =:= warn; + AtomLogLevel =:= info; + AtomLogLevel =:= debug -> + LogLevel = case AtomLogLevel of + error -> 0; + warn -> 1; + info -> 2; + debug -> 3 + end, + new(LogLevel, Caller). + +%% @doc log at the debug level given the current log state with a string or +%% function that returns a string +-spec debug(t(), string() | log_fun()) -> ok. +debug(LogState, Fun) + when erlang:is_function(Fun) -> + log(LogState, ?EC_DEBUG, fun() -> + colorize(LogState, ?CYAN, false, Fun()) + end); +debug(LogState, String) -> + debug(LogState, "~s~n", [String]). + +%% @doc log at the debug level given the current log state with a format string +%% and argements @see io:format/2 +-spec debug(t(), string(), [any()]) -> ok. +debug(LogState, FormatString, Args) -> + log(LogState, ?EC_DEBUG, colorize(LogState, ?CYAN, false, FormatString), Args). + +%% @doc log at the info level given the current log state with a string or +%% function that returns a string +-spec info(t(), string() | log_fun()) -> ok. +info(LogState, Fun) + when erlang:is_function(Fun) -> + log(LogState, ?EC_INFO, fun() -> + colorize(LogState, ?GREEN, false, Fun()) + end); +info(LogState, String) -> + info(LogState, "~s~n", [String]). + +%% @doc log at the info level given the current log state with a format string +%% and argements @see io:format/2 +-spec info(t(), string(), [any()]) -> ok. +info(LogState, FormatString, Args) -> + log(LogState, ?EC_INFO, colorize(LogState, ?GREEN, false, FormatString), Args). + +%% @doc log at the error level given the current log state with a string or +%% format string that returns a function +-spec error(t(), string() | log_fun()) -> ok. +error(LogState, Fun) + when erlang:is_function(Fun) -> + log(LogState, ?EC_ERROR, fun() -> + colorize(LogState, ?RED, false, Fun()) + end); +error(LogState, String) -> + error(LogState, "~s~n", [String]). + +%% @doc log at the error level given the current log state with a format string +%% and argements @see io:format/2 +-spec error(t(), string(), [any()]) -> ok. +error(LogState, FormatString, Args) -> + log(LogState, ?EC_ERROR, colorize(LogState, ?GREEN, false, FormatString), Args). + +%% @doc log at the warn level given the current log state with a string or +%% format string that returns a function +-spec warn(t(), string() | log_fun()) -> ok. +warn(LogState, Fun) + when erlang:is_function(Fun) -> + log(LogState, ?EC_WARN, fun() -> colorize(LogState, ?MAGENTA, false, Fun()) end); +warn(LogState, String) -> + warn(LogState, "~s~n", [String]). + +%% @doc log at the warn level given the current log state with a format string +%% and argements @see io:format/2 +-spec warn(t(), string(), [any()]) -> ok. +warn(LogState, FormatString, Args) -> + log(LogState, ?EC_WARN, colorize(LogState, ?MAGENTA, false, FormatString), Args). + +%% @doc Execute the fun passed in if log level is as expected. +-spec log(t(), int_log_level(), log_fun()) -> ok. +log(#state_t{mod=?MODULE, log_level=DetailLogLevel}, LogLevel, Fun) + when DetailLogLevel >= LogLevel -> + io:format("~s~n", [Fun()]); +log(_, _, _) -> + ok. + +%% @doc when the module log level is less then or equal to the log level for the +%% call then write the log info out. When its not then ignore the call. +-spec log(t(), int_log_level(), string(), [any()]) -> ok. +log(#state_t{mod=?MODULE, log_level=DetailLogLevel}, LogLevel, FormatString, Args) + when DetailLogLevel >= LogLevel, + erlang:is_list(Args) -> + io:format(FormatString, Args); +log(_, _, _, _) -> + ok. + +%% @doc return a boolean indicating if the system should log for the specified +%% levelg +-spec should(t(), int_log_level() | any()) -> boolean(). +should(#state_t{mod=?MODULE, log_level=DetailLogLevel}, LogLevel) + when DetailLogLevel >= LogLevel -> + true; +should(_, _) -> + false. + +%% @doc get the current log level as an integer +-spec log_level(t()) -> int_log_level(). +log_level(#state_t{mod=?MODULE, log_level=DetailLogLevel}) -> + DetailLogLevel. + +%% @doc get the current log level as an atom +-spec atom_log_level(t()) -> atom_log_level(). +atom_log_level(#state_t{mod=?MODULE, log_level=?EC_ERROR}) -> + error; +atom_log_level(#state_t{mod=?MODULE, log_level=?EC_WARN}) -> + warn; +atom_log_level(#state_t{mod=?MODULE, log_level=?EC_INFO}) -> + info; +atom_log_level(#state_t{mod=?MODULE, log_level=?EC_DEBUG}) -> + debug. + +-spec format(t()) -> iolist(). +format(Log) -> + [<<"(">>, + ec_cnv:to_binary(log_level(Log)), <<":">>, + ec_cnv:to_binary(atom_log_level(Log)), + <<")">>]. + +-spec colorize(t(), color(), boolean(), string()) -> string(). +colorize(#state_t{caller=command_line}, Color, false, Msg) when is_integer(Color) -> + colorize_(Color, 0, Msg); +colorize(_LogState, _Color, _Bold, Msg) -> + Msg. + +-spec colorize_(color(), integer(), string()) -> string(). +colorize_(Color, Bold, Msg) when is_integer(Color), is_integer(Bold)-> + lists:flatten(io_lib:format("\033[~B;~Bm~s~s\033[0m", [Bold, Color, ?PREFIX, Msg])). + +%%%=================================================================== +%%% Test Functions +%%%=================================================================== + +-ifndef(NOTEST). +-include_lib("eunit/include/eunit.hrl"). + +should_test() -> + ErrorLogState = new(error), + ?assertMatch(true, should(ErrorLogState, ?EC_ERROR)), + ?assertMatch(true, not should(ErrorLogState, ?EC_INFO)), + ?assertMatch(true, not should(ErrorLogState, ?EC_DEBUG)), + ?assertEqual(?EC_ERROR, log_level(ErrorLogState)), + ?assertEqual(error, atom_log_level(ErrorLogState)), + + InfoLogState = new(info), + ?assertMatch(true, should(InfoLogState, ?EC_ERROR)), + ?assertMatch(true, should(InfoLogState, ?EC_INFO)), + ?assertMatch(true, not should(InfoLogState, ?EC_DEBUG)), + ?assertEqual(?EC_INFO, log_level(InfoLogState)), + ?assertEqual(info, atom_log_level(InfoLogState)), + + DebugLogState = new(debug), + ?assertMatch(true, should(DebugLogState, ?EC_ERROR)), + ?assertMatch(true, should(DebugLogState, ?EC_INFO)), + ?assertMatch(true, should(DebugLogState, ?EC_DEBUG)), + ?assertEqual(?EC_DEBUG, log_level(DebugLogState)), + ?assertEqual(debug, atom_log_level(DebugLogState)). + +-endif.