25개 이상의 토픽을 선택하실 수 없습니다. Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

796 lines
38 KiB

14 년 전
14 년 전
14 년 전
14 년 전
14 년 전
14 년 전
14 년 전
14 년 전
14 년 전
14 년 전
14 년 전
14 년 전
14 년 전
14 년 전
14 년 전
14 년 전
14 년 전
14 년 전
14 년 전
14 년 전
14 년 전
14 년 전
14 년 전
  1. %% -*- coding: latin-1 -*-
  2. %% Copyright (c) 2011-2012 Basho Technologies, Inc. All Rights Reserved.
  3. %%
  4. %% This file is provided to you under the Apache License,
  5. %% Version 2.0 (the "License"); you may not use this file
  6. %% except in compliance with the License. You may obtain
  7. %% a copy of the License at
  8. %%
  9. %% http://www.apache.org/licenses/LICENSE-2.0
  10. %%
  11. %% Unless required by applicable law or agreed to in writing,
  12. %% software distributed under the License is distributed on an
  13. %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  14. %% KIND, either express or implied. See the License for the
  15. %% specific language governing permissions and limitations
  16. %% under the License.
  17. %% @doc File backend for lager, with multiple file support.
  18. %% Multiple files are supported, each with the path and the loglevel being
  19. %% configurable. The configuration paramter for this backend is a list of
  20. %% key-value 2-tuples. See the init() function for the available options.
  21. %% This backend supports external and internal log
  22. %% rotation and will re-open handles to files if the inode changes. It will
  23. %% also rotate the files itself if the size of the file exceeds the
  24. %% `size' and keep `count' rotated files. `date' is
  25. %% an alternate rotation trigger, based on time. See the README for
  26. %% documentation.
  27. %% For performance, the file backend does delayed writes, although it will
  28. %% sync at specific log levels, configured via the `sync_on' option. By default
  29. %% the error level or above will trigger a sync.
  30. -module(lager_file_backend).
  31. -include("lager.hrl").
  32. -behaviour(gen_event).
  33. -ifdef(TEST).
  34. -include_lib("eunit/include/eunit.hrl").
  35. -include_lib("kernel/include/file.hrl").
  36. -compile([{parse_transform, lager_transform}]).
  37. -endif.
  38. -export([init/1, handle_call/2, handle_event/2, handle_info/2, terminate/2,
  39. code_change/3]).
  40. -export([config_to_id/1]).
  41. -define(DEFAULT_LOG_LEVEL, info).
  42. -define(DEFAULT_ROTATION_SIZE, 10485760). %% 10mb
  43. -define(DEFAULT_ROTATION_DATE, "$D0"). %% midnight
  44. -define(DEFAULT_ROTATION_COUNT, 5).
  45. -define(DEFAULT_SYNC_LEVEL, error).
  46. -define(DEFAULT_SYNC_INTERVAL, 1000).
  47. -define(DEFAULT_SYNC_SIZE, 1024*64). %% 64kb
  48. -define(DEFAULT_CHECK_INTERVAL, 1000).
  49. -record(state, {
  50. name :: string(),
  51. level :: {'mask', integer()},
  52. fd :: file:io_device(),
  53. inode :: integer(),
  54. flap=false :: boolean(),
  55. size = 0 :: integer(),
  56. date,
  57. count = 10,
  58. formatter,
  59. formatter_config,
  60. sync_on,
  61. check_interval = ?DEFAULT_CHECK_INTERVAL,
  62. sync_interval = ?DEFAULT_SYNC_INTERVAL,
  63. sync_size = ?DEFAULT_SYNC_SIZE,
  64. last_check = os:timestamp()
  65. }).
  66. -type option() :: {file, string()} | {level, lager:log_level()} |
  67. {size, non_neg_integer()} | {date, string()} |
  68. {count, non_neg_integer()} | {sync_interval, non_neg_integer()} |
  69. {sync_size, non_neg_integer()} | {sync_on, lager:log_level()} |
  70. {check_interval, non_neg_integer()} | {formatter, atom()} |
  71. {formatter_config, term()}.
  72. -spec init([option(),...]) -> {ok, #state{}} | {error, bad_config}.
  73. init({FileName, LogLevel}) when is_list(FileName), is_atom(LogLevel) ->
  74. %% backwards compatability hack
  75. init([{file, FileName}, {level, LogLevel}]);
  76. init({FileName, LogLevel, Size, Date, Count}) when is_list(FileName), is_atom(LogLevel) ->
  77. %% backwards compatability hack
  78. init([{file, FileName}, {level, LogLevel}, {size, Size}, {date, Date}, {count, Count}]);
  79. init([{FileName, LogLevel, Size, Date, Count}, {Formatter,FormatterConfig}]) when is_list(FileName), is_atom(LogLevel), is_atom(Formatter) ->
  80. %% backwards compatability hack
  81. init([{file, FileName}, {level, LogLevel}, {size, Size}, {date, Date}, {count, Count}, {formatter, Formatter}, {formatter_config, FormatterConfig}]);
  82. init([LogFile,{Formatter}]) ->
  83. %% backwards compatability hack
  84. init([LogFile,{Formatter,[]}]);
  85. init([{FileName, LogLevel}, {Formatter,FormatterConfig}]) when is_list(FileName), is_atom(LogLevel), is_atom(Formatter) ->
  86. %% backwards compatability hack
  87. init([{file, FileName}, {level, LogLevel}, {formatter, Formatter}, {formatter_config, FormatterConfig}]);
  88. init(LogFileConfig) when is_list(LogFileConfig) ->
  89. case validate_logfile_proplist(LogFileConfig) of
  90. false ->
  91. %% falied to validate config
  92. {error, {fatal, bad_config}};
  93. Config ->
  94. %% probabably a better way to do this, but whatever
  95. [Name, Level, Date, Size, Count, SyncInterval, SyncSize, SyncOn, CheckInterval, Formatter, FormatterConfig] =
  96. [proplists:get_value(Key, Config) || Key <- [file, level, date, size, count, sync_interval, sync_size, sync_on, check_interval, formatter, formatter_config]],
  97. schedule_rotation(Name, Date),
  98. State0 = #state{name=Name, level=Level, size=Size, date=Date, count=Count, formatter=Formatter,
  99. formatter_config=FormatterConfig, sync_on=SyncOn, sync_interval=SyncInterval, sync_size=SyncSize,
  100. check_interval=CheckInterval},
  101. State = case lager_util:open_logfile(Name, {SyncSize, SyncInterval}) of
  102. {ok, {FD, Inode, _}} ->
  103. State0#state{fd=FD, inode=Inode};
  104. {error, Reason} ->
  105. ?INT_LOG(error, "Failed to open log file ~s with error ~s", [Name, file:format_error(Reason)]),
  106. State0#state{flap=true}
  107. end,
  108. {ok, State}
  109. end.
  110. %% @private
  111. handle_call({set_loglevel, Level}, #state{name=Ident} = State) ->
  112. case validate_loglevel(Level) of
  113. false ->
  114. {ok, {error, bad_loglevel}, State};
  115. Levels ->
  116. ?INT_LOG(notice, "Changed loglevel of ~s to ~p", [Ident, Level]),
  117. {ok, ok, State#state{level=Levels}}
  118. end;
  119. handle_call(get_loglevel, #state{level=Level} = State) ->
  120. {ok, Level, State};
  121. handle_call(_Request, State) ->
  122. {ok, ok, State}.
  123. %% @private
  124. handle_event({log, Message},
  125. #state{name=Name, level=L,formatter=Formatter,formatter_config=FormatConfig} = State) ->
  126. case lager_util:is_loggable(Message,L,{lager_file_backend, Name}) of
  127. true ->
  128. {ok,write(State, lager_msg:timestamp(Message), lager_msg:severity_as_int(Message), Formatter:format(Message,FormatConfig)) };
  129. false ->
  130. {ok, State}
  131. end;
  132. handle_event(_Event, State) ->
  133. {ok, State}.
  134. %% @private
  135. handle_info({rotate, File}, #state{name=File,count=Count,date=Date} = State) ->
  136. lager_util:rotate_logfile(File, Count),
  137. schedule_rotation(File, Date),
  138. {ok, State};
  139. handle_info(_Info, State) ->
  140. {ok, State}.
  141. %% @private
  142. terminate(_Reason, #state{fd=FD}) ->
  143. %% flush and close any file handles
  144. _ = file:datasync(FD),
  145. _ = file:close(FD),
  146. ok.
  147. %% @private
  148. code_change(_OldVsn, State, _Extra) ->
  149. {ok, State}.
  150. %% @private convert the config into a gen_event handler ID
  151. config_to_id({Name,_Severity}) when is_list(Name) ->
  152. {?MODULE, Name};
  153. config_to_id({Name,_Severity,_Size,_Rotation,_Count}) ->
  154. {?MODULE, Name};
  155. config_to_id([{Name,_Severity,_Size,_Rotation,_Count}, _Format]) ->
  156. {?MODULE, Name};
  157. config_to_id([{Name,_Severity}, _Format]) when is_list(Name) ->
  158. {?MODULE, Name};
  159. config_to_id(Config) ->
  160. case proplists:get_value(file, Config) of
  161. undefined ->
  162. erlang:error(no_file);
  163. File ->
  164. {?MODULE, File}
  165. end.
  166. write(#state{name=Name, fd=FD, inode=Inode, flap=Flap, size=RotSize,
  167. count=Count} = State, Timestamp, Level, Msg) ->
  168. LastCheck = timer:now_diff(Timestamp, State#state.last_check) div 1000,
  169. case LastCheck >= State#state.check_interval orelse FD == undefined of
  170. true ->
  171. %% need to check for rotation
  172. case lager_util:ensure_logfile(Name, FD, Inode, {State#state.sync_size, State#state.sync_interval}) of
  173. {ok, {_, _, Size}} when RotSize /= 0, Size > RotSize ->
  174. case lager_util:rotate_logfile(Name, Count) of
  175. ok ->
  176. %% go around the loop again, we'll do another rotation check and hit the next clause of ensure_logfile
  177. write(State, Timestamp, Level, Msg);
  178. {error, Reason} ->
  179. case Flap of
  180. true ->
  181. State;
  182. _ ->
  183. ?INT_LOG(error, "Failed to rotate log file ~s with error ~s", [Name, file:format_error(Reason)]),
  184. State#state{flap=true}
  185. end
  186. end;
  187. {ok, {NewFD, NewInode, _}} ->
  188. %% update our last check and try again
  189. do_write(State#state{last_check=Timestamp, fd=NewFD, inode=NewInode}, Level, Msg);
  190. {error, Reason} ->
  191. case Flap of
  192. true ->
  193. State;
  194. _ ->
  195. ?INT_LOG(error, "Failed to reopen log file ~s with error ~s", [Name, file:format_error(Reason)]),
  196. State#state{flap=true}
  197. end
  198. end;
  199. false ->
  200. do_write(State, Level, Msg)
  201. end.
  202. do_write(#state{fd=FD, name=Name, flap=Flap} = State, Level, Msg) ->
  203. %% delayed_write doesn't report errors
  204. _ = file:write(FD, unicode:characters_to_binary(Msg)),
  205. {mask, SyncLevel} = State#state.sync_on,
  206. case (Level band SyncLevel) /= 0 of
  207. true ->
  208. %% force a sync on any message that matches the 'sync_on' bitmask
  209. Flap2 = case file:datasync(FD) of
  210. {error, Reason2} when Flap == false ->
  211. ?INT_LOG(error, "Failed to write log message to file ~s: ~s",
  212. [Name, file:format_error(Reason2)]),
  213. true;
  214. ok ->
  215. false;
  216. _ ->
  217. Flap
  218. end,
  219. State#state{flap=Flap2};
  220. _ ->
  221. State
  222. end.
  223. validate_loglevel(Level) ->
  224. try lager_util:config_to_mask(Level) of
  225. Levels ->
  226. Levels
  227. catch
  228. _:_ ->
  229. false
  230. end.
  231. validate_logfile_proplist(List) ->
  232. try validate_logfile_proplist(List, []) of
  233. Res ->
  234. case proplists:get_value(file, Res) of
  235. undefined ->
  236. ?INT_LOG(error, "Missing required file option", []),
  237. false;
  238. _File ->
  239. %% merge with the default options
  240. {ok, DefaultRotationDate} = lager_util:parse_rotation_date_spec(?DEFAULT_ROTATION_DATE),
  241. lists:keymerge(1, lists:sort(Res), lists:sort([
  242. {level, validate_loglevel(?DEFAULT_LOG_LEVEL)}, {date, DefaultRotationDate},
  243. {size, ?DEFAULT_ROTATION_SIZE}, {count, ?DEFAULT_ROTATION_COUNT},
  244. {sync_on, validate_loglevel(?DEFAULT_SYNC_LEVEL)}, {sync_interval, ?DEFAULT_SYNC_INTERVAL},
  245. {sync_size, ?DEFAULT_SYNC_SIZE}, {check_interval, ?DEFAULT_CHECK_INTERVAL},
  246. {formatter, lager_default_formatter}, {formatter_config, []}
  247. ]))
  248. end
  249. catch
  250. {bad_config, Msg, Value} ->
  251. ?INT_LOG(error, "~s ~p for file ~p",
  252. [Msg, Value, proplists:get_value(file, List)]),
  253. false
  254. end.
  255. validate_logfile_proplist([], Acc) ->
  256. Acc;
  257. validate_logfile_proplist([{file, File}|Tail], Acc) ->
  258. %% is there any reasonable validation we can do here?
  259. validate_logfile_proplist(Tail, [{file, File}|Acc]);
  260. validate_logfile_proplist([{level, Level}|Tail], Acc) ->
  261. case validate_loglevel(Level) of
  262. false ->
  263. throw({bad_config, "Invalid loglevel", Level});
  264. Res ->
  265. validate_logfile_proplist(Tail, [{level, Res}|Acc])
  266. end;
  267. validate_logfile_proplist([{size, Size}|Tail], Acc) ->
  268. case Size of
  269. S when is_integer(S), S >= 0 ->
  270. validate_logfile_proplist(Tail, [{size, Size}|Acc]);
  271. _ ->
  272. throw({bad_config, "Invalid rotation size", Size})
  273. end;
  274. validate_logfile_proplist([{count, Count}|Tail], Acc) ->
  275. case Count of
  276. C when is_integer(C), C >= 0 ->
  277. validate_logfile_proplist(Tail, [{count, Count}|Acc]);
  278. _ ->
  279. throw({bad_config, "Invalid rotation count", Count})
  280. end;
  281. validate_logfile_proplist([{date, Date}|Tail], Acc) ->
  282. case lager_util:parse_rotation_date_spec(Date) of
  283. {ok, Spec} ->
  284. validate_logfile_proplist(Tail, [{date, Spec}|Acc]);
  285. {error, _} when Date == "" ->
  286. %% legacy config allowed blanks
  287. validate_logfile_proplist(Tail, [{date, undefined}|Acc]);
  288. {error, _} ->
  289. throw({bad_config, "Invalid rotation date", Date})
  290. end;
  291. validate_logfile_proplist([{sync_interval, SyncInt}|Tail], Acc) ->
  292. case SyncInt of
  293. Val when is_integer(Val), Val >= 0 ->
  294. validate_logfile_proplist(Tail, [{sync_interval, Val}|Acc]);
  295. _ ->
  296. throw({bad_config, "Invalid sync interval", SyncInt})
  297. end;
  298. validate_logfile_proplist([{sync_size, SyncSize}|Tail], Acc) ->
  299. case SyncSize of
  300. Val when is_integer(Val), Val >= 0 ->
  301. validate_logfile_proplist(Tail, [{sync_size, Val}|Acc]);
  302. _ ->
  303. throw({bad_config, "Invalid sync size", SyncSize})
  304. end;
  305. validate_logfile_proplist([{check_interval, CheckInt}|Tail], Acc) ->
  306. case CheckInt of
  307. Val when is_integer(Val), Val >= 0 ->
  308. validate_logfile_proplist(Tail, [{check_interval, Val}|Acc]);
  309. always ->
  310. validate_logfile_proplist(Tail, [{check_interval, 0}|Acc]);
  311. _ ->
  312. throw({bad_config, "Invalid check interval", CheckInt})
  313. end;
  314. validate_logfile_proplist([{sync_on, Level}|Tail], Acc) ->
  315. case validate_loglevel(Level) of
  316. false ->
  317. throw({bad_config, "Invalid sync on level", Level});
  318. Res ->
  319. validate_logfile_proplist(Tail, [{sync_on, Res}|Acc])
  320. end;
  321. validate_logfile_proplist([{formatter, Fmt}|Tail], Acc) ->
  322. case is_atom(Fmt) of
  323. true ->
  324. validate_logfile_proplist(Tail, [{formatter, Fmt}|Acc]);
  325. false ->
  326. throw({bad_config, "Invalid formatter module", Fmt})
  327. end;
  328. validate_logfile_proplist([{formatter_config, FmtCfg}|Tail], Acc) ->
  329. case is_list(FmtCfg) of
  330. true ->
  331. validate_logfile_proplist(Tail, [{formatter_config, FmtCfg}|Acc]);
  332. false ->
  333. throw({bad_config, "Invalid formatter config", FmtCfg})
  334. end;
  335. validate_logfile_proplist([Other|_Tail], _Acc) ->
  336. throw({bad_config, "Invalid option", Other}).
  337. schedule_rotation(_, undefined) ->
  338. ok;
  339. schedule_rotation(Name, Date) ->
  340. erlang:send_after(lager_util:calculate_next_rotation(Date) * 1000, self(), {rotate, Name}),
  341. ok.
  342. -ifdef(TEST).
  343. get_loglevel_test() ->
  344. {ok, Level, _} = handle_call(get_loglevel,
  345. #state{name="bar", level=lager_util:config_to_mask(info), fd=0, inode=0}),
  346. ?assertEqual(Level, lager_util:config_to_mask(info)),
  347. {ok, Level2, _} = handle_call(get_loglevel,
  348. #state{name="foo", level=lager_util:config_to_mask(warning), fd=0, inode=0}),
  349. ?assertEqual(Level2, lager_util:config_to_mask(warning)).
  350. rotation_test_() ->
  351. {foreach,
  352. fun() ->
  353. [file:delete(F)||F <- filelib:wildcard("test.log*")],
  354. SyncLevel = validate_loglevel(?DEFAULT_SYNC_LEVEL),
  355. SyncSize = ?DEFAULT_SYNC_SIZE,
  356. SyncInterval = ?DEFAULT_SYNC_INTERVAL,
  357. CheckInterval = 0, %% hard to test delayed mode
  358. #state{name="test.log", level=?DEBUG, sync_on=SyncLevel,
  359. sync_size=SyncSize, sync_interval=SyncInterval, check_interval=CheckInterval}
  360. end,
  361. fun(_) ->
  362. [file:delete(F)||F <- filelib:wildcard("test.log*")]
  363. end,
  364. [fun(DefaultState = #state{sync_size=SyncSize, sync_interval = SyncInterval}) ->
  365. {"External rotation should work",
  366. fun() ->
  367. {ok, {FD, Inode, _}} = lager_util:open_logfile("test.log", {SyncSize, SyncInterval}),
  368. State0 = DefaultState#state{fd=FD, inode=Inode},
  369. ?assertMatch(#state{name="test.log", level=?DEBUG, fd=FD, inode=Inode},
  370. write(State0, os:timestamp(), ?DEBUG, "hello world")),
  371. file:delete("test.log"),
  372. Result = write(State0, os:timestamp(), ?DEBUG, "hello world"),
  373. %% assert file has changed
  374. ?assert(#state{name="test.log", level=?DEBUG, fd=FD, inode=Inode} =/= Result),
  375. ?assertMatch(#state{name="test.log", level=?DEBUG}, Result),
  376. file:rename("test.log", "test.log.1"),
  377. Result2 = write(Result, os:timestamp(), ?DEBUG, "hello world"),
  378. %% assert file has changed
  379. ?assert(Result =/= Result2),
  380. ?assertMatch(#state{name="test.log", level=?DEBUG}, Result2),
  381. ok
  382. end}
  383. end,
  384. fun(DefaultState = #state{sync_size=SyncSize, sync_interval = SyncInterval}) ->
  385. {"Internal rotation and delayed write",
  386. fun() ->
  387. CheckInterval = 3000, % 3 sec
  388. RotationSize = 15,
  389. PreviousCheck = os:timestamp(),
  390. {ok, {FD, Inode, _}} = lager_util:open_logfile("test.log", {SyncSize, SyncInterval}),
  391. State0 = DefaultState#state{
  392. fd=FD, inode=Inode, size=RotationSize,
  393. check_interval=CheckInterval, last_check=PreviousCheck},
  394. %% new message within check interval with sync_on level
  395. Msg1Timestamp = add_secs(PreviousCheck, 1),
  396. State0 = State1 = write(State0, Msg1Timestamp, ?ERROR, "big big message 1"),
  397. %% new message within check interval under sync_on level
  398. %% not written to disk yet
  399. Msg2Timestamp = add_secs(PreviousCheck, 2),
  400. State0 = State2 = write(State1, Msg2Timestamp, ?DEBUG, "buffered message 2"),
  401. %% although file size is big enough...
  402. {ok, FInfo} = file:read_file_info("test.log"),
  403. ?assert(RotationSize < FInfo#file_info.size),
  404. %% ...no rotation yet
  405. ?assertEqual(PreviousCheck, State2#state.last_check),
  406. ?assertNot(filelib:is_regular("test.log.0")),
  407. %% new message after check interval
  408. Msg3Timestamp = add_secs(PreviousCheck, 4),
  409. _State3 = write(State2, Msg3Timestamp, ?DEBUG, "message 3"),
  410. %% rotation happened
  411. ?assert(filelib:is_regular("test.log.0")),
  412. {ok, Bin1} = file:read_file("test.log.0"),
  413. {ok, Bin2} = file:read_file("test.log"),
  414. %% message 1-3 written to file
  415. ?assertEqual(<<"big big message 1buffered message 2">>, Bin1),
  416. %% message 4 buffered, not yet written to file
  417. ?assertEqual(<<"">>, Bin2),
  418. ok
  419. end}
  420. end
  421. ]}.
  422. add_secs({Mega, Secs, Micro}, Add) ->
  423. NewSecs = Secs + Add,
  424. {Mega + NewSecs div 10000000, NewSecs rem 10000000, Micro}.
  425. filesystem_test_() ->
  426. {foreach,
  427. fun() ->
  428. file:write_file("test.log", ""),
  429. error_logger:tty(false),
  430. application:load(lager),
  431. application:set_env(lager, handlers, [{lager_test_backend, info}]),
  432. application:set_env(lager, error_logger_redirect, false),
  433. application:set_env(lager, async_threshold, undefined),
  434. application:start(lager)
  435. end,
  436. fun(_) ->
  437. file:delete("test.log"),
  438. file:delete("test.log.0"),
  439. file:delete("test.log.1"),
  440. application:stop(lager),
  441. error_logger:tty(true)
  442. end,
  443. [
  444. {"under normal circumstances, file should be opened",
  445. fun() ->
  446. gen_event:add_handler(lager_event, lager_file_backend, [{"test.log", info}, {lager_default_formatter}]),
  447. lager:log(error, self(), "Test message"),
  448. {ok, Bin} = file:read_file("test.log"),
  449. Pid = pid_to_list(self()),
  450. ?assertMatch([_, _, "[error]", Pid, "Test message\n"], re:split(Bin, " ", [{return, list}, {parts, 5}]))
  451. end
  452. },
  453. {"don't choke on unicode",
  454. fun() ->
  455. gen_event:add_handler(lager_event, lager_file_backend, [{"test.log", info}, {lager_default_formatter}]),
  456. lager:log(error, self(),"~ts", [[20013,25991,27979,35797]]),
  457. {ok, Bin} = file:read_file("test.log"),
  458. Pid = pid_to_list(self()),
  459. ?assertMatch([_, _, "[error]", Pid, [228,184,173,230,150,135,230,181,139,232,175,149, $\n]], re:split(Bin, " ", [{return, list}, {parts, 5}]))
  460. end
  461. },
  462. {"don't choke on latin-1",
  463. fun() ->
  464. %% XXX if this test fails, check that this file is encoded latin-1, not utf-8!
  465. gen_event:add_handler(lager_event, lager_file_backend, [{"test.log", info}, {lager_default_formatter}]),
  466. lager:log(error, self(),"~ts", ["L���N-�"]),
  467. {ok, Bin} = file:read_file("test.log"),
  468. Pid = pid_to_list(self()),
  469. Res = re:split(Bin, " ", [{return, list}, {parts, 5}]),
  470. ?assertMatch([_, _, "[error]", Pid, [76,195,134,195,157,195,142,78,45,195,175,$\n]], Res)
  471. end
  472. },
  473. {"file can't be opened on startup triggers an error message",
  474. fun() ->
  475. {ok, FInfo} = file:read_file_info("test.log"),
  476. file:write_file_info("test.log", FInfo#file_info{mode = 0}),
  477. gen_event:add_handler(lager_event, lager_file_backend, {"test.log", info, 10*1024*1024, "$D0", 5}),
  478. ?assertEqual(1, lager_test_backend:count()),
  479. {_Level, _Time,Message,_Metadata} = lager_test_backend:pop(),
  480. ?assertEqual("Failed to open log file test.log with error permission denied", lists:flatten(Message))
  481. end
  482. },
  483. {"file that becomes unavailable at runtime should trigger an error message",
  484. fun() ->
  485. gen_event:add_handler(lager_event, lager_file_backend, [{file, "test.log"}, {level, info}, {check_interval, 0}]),
  486. ?assertEqual(0, lager_test_backend:count()),
  487. lager:log(error, self(), "Test message"),
  488. ?assertEqual(1, lager_test_backend:count()),
  489. file:delete("test.log"),
  490. file:write_file("test.log", ""),
  491. {ok, FInfo} = file:read_file_info("test.log"),
  492. file:write_file_info("test.log", FInfo#file_info{mode = 0}),
  493. lager:log(error, self(), "Test message"),
  494. ?assertEqual(3, lager_test_backend:count()),
  495. lager_test_backend:pop(),
  496. lager_test_backend:pop(),
  497. {_Level, _Time, Message,_Metadata} = lager_test_backend:pop(),
  498. ?assertEqual("Failed to reopen log file test.log with error permission denied", lists:flatten(Message))
  499. end
  500. },
  501. {"unavailable files that are fixed at runtime should start having log messages written",
  502. fun() ->
  503. {ok, FInfo} = file:read_file_info("test.log"),
  504. OldPerms = FInfo#file_info.mode,
  505. file:write_file_info("test.log", FInfo#file_info{mode = 0}),
  506. gen_event:add_handler(lager_event, lager_file_backend, [{file, "test.log"},{check_interval, 0}]),
  507. ?assertEqual(1, lager_test_backend:count()),
  508. {_Level, _Time, Message,_Metadata} = lager_test_backend:pop(),
  509. ?assertEqual("Failed to open log file test.log with error permission denied", lists:flatten(Message)),
  510. file:write_file_info("test.log", FInfo#file_info{mode = OldPerms}),
  511. lager:log(error, self(), "Test message"),
  512. {ok, Bin} = file:read_file("test.log"),
  513. Pid = pid_to_list(self()),
  514. ?assertMatch([_, _, "[error]", Pid, "Test message\n"], re:split(Bin, " ", [{return, list}, {parts, 5}]))
  515. end
  516. },
  517. {"external logfile rotation/deletion should be handled",
  518. fun() ->
  519. gen_event:add_handler(lager_event, lager_file_backend, [{file, "test.log"}, {level, info}, {check_interval, 0}]),
  520. ?assertEqual(0, lager_test_backend:count()),
  521. lager:log(error, self(), "Test message1"),
  522. ?assertEqual(1, lager_test_backend:count()),
  523. file:delete("test.log"),
  524. file:write_file("test.log", ""),
  525. lager:log(error, self(), "Test message2"),
  526. {ok, Bin} = file:read_file("test.log"),
  527. Pid = pid_to_list(self()),
  528. ?assertMatch([_, _, "[error]", Pid, "Test message2\n"], re:split(Bin, " ", [{return, list}, {parts, 5}])),
  529. file:rename("test.log", "test.log.0"),
  530. lager:log(error, self(), "Test message3"),
  531. {ok, Bin2} = file:read_file("test.log"),
  532. ?assertMatch([_, _, "[error]", Pid, "Test message3\n"], re:split(Bin2, " ", [{return, list}, {parts, 5}]))
  533. end
  534. },
  535. {"internal size rotation should work",
  536. fun() ->
  537. gen_event:add_handler(lager_event, lager_file_backend, [{file, "test.log"}, {level, info}, {check_interval, 0}, {size, 10}]),
  538. lager:log(error, self(), "Test message1"),
  539. lager:log(error, self(), "Test message1"),
  540. ?assert(filelib:is_regular("test.log.0"))
  541. end
  542. },
  543. {"internal time rotation should work",
  544. fun() ->
  545. gen_event:add_handler(lager_event, lager_file_backend, [{file, "test.log"}, {level, info}, {check_interval, 1000}]),
  546. lager:log(error, self(), "Test message1"),
  547. lager:log(error, self(), "Test message1"),
  548. whereis(lager_event) ! {rotate, "test.log"},
  549. lager:log(error, self(), "Test message1"),
  550. ?assert(filelib:is_regular("test.log.0"))
  551. end
  552. },
  553. {"sync_on option should work",
  554. fun() ->
  555. gen_event:add_handler(lager_event, lager_file_backend, [{file, "test.log"}, {level, info}, {sync_on, "=info"}, {check_interval, 5000}, {sync_interval, 5000}]),
  556. lager:log(error, self(), "Test message1"),
  557. lager:log(error, self(), "Test message1"),
  558. ?assertEqual({ok, <<>>}, file:read_file("test.log")),
  559. lager:log(info, self(), "Test message1"),
  560. {ok, Bin} = file:read_file("test.log"),
  561. ?assert(<<>> /= Bin)
  562. end
  563. },
  564. {"sync_on none option should work (also tests sync_interval)",
  565. fun() ->
  566. gen_event:add_handler(lager_event, lager_file_backend, [{file, "test.log"}, {level, info}, {sync_on, "none"}, {check_interval, 5000}, {sync_interval, 1000}]),
  567. lager:log(error, self(), "Test message1"),
  568. lager:log(error, self(), "Test message1"),
  569. ?assertEqual({ok, <<>>}, file:read_file("test.log")),
  570. lager:log(info, self(), "Test message1"),
  571. ?assertEqual({ok, <<>>}, file:read_file("test.log")),
  572. timer:sleep(1000),
  573. {ok, Bin} = file:read_file("test.log"),
  574. ?assert(<<>> /= Bin)
  575. end
  576. },
  577. {"sync_size option should work",
  578. fun() ->
  579. gen_event:add_handler(lager_event, lager_file_backend, [{file, "test.log"}, {level, info}, {sync_on, "none"}, {check_interval, 5001}, {sync_size, 640}, {sync_interval, 5001}]),
  580. lager:log(error, self(), "Test messageis64bytes"),
  581. lager:log(error, self(), "Test messageis64bytes"),
  582. lager:log(error, self(), "Test messageis64bytes"),
  583. lager:log(error, self(), "Test messageis64bytes"),
  584. lager:log(error, self(), "Test messageis64bytes"),
  585. ?assertEqual({ok, <<>>}, file:read_file("test.log")),
  586. lager:log(error, self(), "Test messageis64bytes"),
  587. lager:log(error, self(), "Test messageis64bytes"),
  588. lager:log(error, self(), "Test messageis64bytes"),
  589. lager:log(error, self(), "Test messageis64bytes"),
  590. ?assertEqual({ok, <<>>}, file:read_file("test.log")),
  591. %% now we've written enough bytes
  592. lager:log(error, self(), "Test messageis64bytes"),
  593. {ok, Bin} = file:read_file("test.log"),
  594. ?assert(<<>> /= Bin)
  595. end
  596. },
  597. {"runtime level changes",
  598. fun() ->
  599. gen_event:add_handler(lager_event, {lager_file_backend, "test.log"}, {"test.log", info}),
  600. ?assertEqual(0, lager_test_backend:count()),
  601. lager:log(info, self(), "Test message1"),
  602. lager:log(error, self(), "Test message2"),
  603. {ok, Bin} = file:read_file("test.log"),
  604. Lines = length(re:split(Bin, "\n", [{return, list}, trim])),
  605. ?assertEqual(Lines, 2),
  606. ?assertEqual(ok, lager:set_loglevel(lager_file_backend, "test.log", warning)),
  607. lager:log(info, self(), "Test message3"), %% this won't get logged
  608. lager:log(error, self(), "Test message4"),
  609. {ok, Bin2} = file:read_file("test.log"),
  610. Lines2 = length(re:split(Bin2, "\n", [{return, list}, trim])),
  611. ?assertEqual(Lines2, 3)
  612. end
  613. },
  614. {"invalid runtime level changes",
  615. fun() ->
  616. gen_event:add_handler(lager_event, lager_file_backend, [{"test.log", info, 10*1024*1024, "$D0", 5}, {lager_default_formatter}]),
  617. gen_event:add_handler(lager_event, lager_file_backend, {"test3.log", info}),
  618. ?assertEqual({error, bad_module}, lager:set_loglevel(lager_file_backend, "test.log", warning))
  619. end
  620. },
  621. {"tracing should work",
  622. fun() ->
  623. gen_event:add_handler(lager_event, lager_file_backend,
  624. {"test.log", critical}),
  625. lager:error("Test message"),
  626. ?assertEqual({ok, <<>>}, file:read_file("test.log")),
  627. {Level, _} = lager_config:get(loglevel),
  628. lager_config:set(loglevel, {Level, [{[{module,
  629. ?MODULE}], ?DEBUG,
  630. {lager_file_backend, "test.log"}}]}),
  631. lager:error("Test message"),
  632. timer:sleep(1000),
  633. {ok, Bin} = file:read_file("test.log"),
  634. ?assertMatch([_, _, "[error]", _, "Test message\n"], re:split(Bin, " ", [{return, list}, {parts, 5}]))
  635. end
  636. },
  637. {"tracing should not duplicate messages",
  638. fun() ->
  639. gen_event:add_handler(lager_event, lager_file_backend,
  640. [{file, "test.log"}, {level, critical}, {check_interval, always}]),
  641. lager:critical("Test message"),
  642. {ok, Bin1} = file:read_file("test.log"),
  643. ?assertMatch([_, _, "[critical]", _, "Test message\n"], re:split(Bin1, " ", [{return, list}, {parts, 5}])),
  644. ok = file:delete("test.log"),
  645. {Level, _} = lager_config:get(loglevel),
  646. lager_config:set(loglevel, {Level, [{[{module,
  647. ?MODULE}], ?DEBUG,
  648. {lager_file_backend, "test.log"}}]}),
  649. lager:critical("Test message"),
  650. {ok, Bin2} = file:read_file("test.log"),
  651. ?assertMatch([_, _, "[critical]", _, "Test message\n"], re:split(Bin2, " ", [{return, list}, {parts, 5}])),
  652. ok = file:delete("test.log"),
  653. lager:error("Test message"),
  654. {ok, Bin3} = file:read_file("test.log"),
  655. ?assertMatch([_, _, "[error]", _, "Test message\n"], re:split(Bin3, " ", [{return, list}, {parts, 5}]))
  656. end
  657. },
  658. {"tracing to a dedicated file should work",
  659. fun() ->
  660. file:delete("foo.log"),
  661. {ok, _} = lager:trace_file("foo.log", [{module, ?MODULE}]),
  662. lager:error("Test message"),
  663. %% not elegible for trace
  664. lager:log(error, self(), "Test message"),
  665. {ok, Bin3} = file:read_file("foo.log"),
  666. ?assertMatch([_, _, "[error]", _, "Test message\n"], re:split(Bin3, " ", [{return, list}, {parts, 5}]))
  667. end
  668. }
  669. ]
  670. }.
  671. formatting_test_() ->
  672. {foreach,
  673. fun() ->
  674. file:write_file("test.log", ""),
  675. file:write_file("test2.log", ""),
  676. error_logger:tty(false),
  677. application:load(lager),
  678. application:set_env(lager, handlers, [{lager_test_backend, info}]),
  679. application:set_env(lager, error_logger_redirect, false),
  680. application:start(lager)
  681. end,
  682. fun(_) ->
  683. file:delete("test.log"),
  684. file:delete("test2.log"),
  685. application:stop(lager),
  686. error_logger:tty(true)
  687. end,
  688. [{"Should have two log files, the second prefixed with 2>",
  689. fun() ->
  690. gen_event:add_handler(lager_event, lager_file_backend,[{"test.log", debug},{lager_default_formatter,["[",severity,"] ", message, "\n"]}]),
  691. gen_event:add_handler(lager_event, lager_file_backend,[{"test2.log", debug},{lager_default_formatter,["2> [",severity,"] ", message, "\n"]}]),
  692. lager:log(error, self(), "Test message"),
  693. ?assertMatch({ok, <<"[error] Test message\n">>},file:read_file("test.log")),
  694. ?assertMatch({ok, <<"2> [error] Test message\n">>},file:read_file("test2.log"))
  695. end
  696. }
  697. ]}.
  698. config_validation_test_() ->
  699. [
  700. {"missing file",
  701. ?_assertEqual(false,
  702. validate_logfile_proplist([{level, info},{size, 10}]))
  703. },
  704. {"bad level",
  705. ?_assertEqual(false,
  706. validate_logfile_proplist([{file, "test.log"}, {level, blah},{size, 10}]))
  707. },
  708. {"bad size",
  709. ?_assertEqual(false,
  710. validate_logfile_proplist([{file, "test.log"}, {size, infinity}]))
  711. },
  712. {"bad count",
  713. ?_assertEqual(false,
  714. validate_logfile_proplist([{file, "test.log"}, {count, infinity}]))
  715. },
  716. {"bad date",
  717. ?_assertEqual(false,
  718. validate_logfile_proplist([{file, "test.log"}, {date, "midnight"}]))
  719. },
  720. {"blank date is ok",
  721. ?_assertMatch([_|_],
  722. validate_logfile_proplist([{file, "test.log"}, {date, ""}]))
  723. },
  724. {"bad sync_interval",
  725. ?_assertEqual(false,
  726. validate_logfile_proplist([{file, "test.log"}, {sync_interval, infinity}]))
  727. },
  728. {"bad sync_size",
  729. ?_assertEqual(false,
  730. validate_logfile_proplist([{file, "test.log"}, {sync_size, infinity}]))
  731. },
  732. {"bad check_interval",
  733. ?_assertEqual(false,
  734. validate_logfile_proplist([{file, "test.log"}, {check_interval, infinity}]))
  735. },
  736. {"bad sync_on level",
  737. ?_assertEqual(false,
  738. validate_logfile_proplist([{file, "test.log"}, {sync_on, infinity}]))
  739. },
  740. {"bad formatter module",
  741. ?_assertEqual(false,
  742. validate_logfile_proplist([{file, "test.log"}, {formatter, "io:format"}]))
  743. },
  744. {"bad formatter config",
  745. ?_assertEqual(false,
  746. validate_logfile_proplist([{file, "test.log"}, {formatter_config, blah}]))
  747. },
  748. {"unknown option",
  749. ?_assertEqual(false,
  750. validate_logfile_proplist([{file, "test.log"}, {rhubarb, spicy}]))
  751. }
  752. ].
  753. -endif.