You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

796 lines
38 KiB

14 years ago
14 years ago
14 years ago
14 years ago
14 years ago
14 years ago
14 years ago
14 years ago
14 years ago
14 years ago
14 years ago
14 years ago
14 years ago
14 years ago
14 years ago
14 years ago
14 years ago
14 years ago
14 years ago
14 years ago
14 years ago
14 years ago
14 years ago
  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.