rewrite from lager
選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

1174 行
51 KiB

4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
4年前
  1. %% @doc File backend for lager, with multiple file support.
  2. %% Multiple files are supported, each with the path and the loglevel being
  3. %% configurable. The configuration paramter for this backend is a list of
  4. %% key-value 2-tuples. See the init() function for the available options.
  5. %% This backend supports external and internal log
  6. %% rotation and will re-open handles to files if the inode changes. It will
  7. %% also rotate the files itself if the size of the file exceeds the
  8. %% `size' and keep `count' rotated files. `date' is
  9. %% an alternate rotation trigger, based on time. See the README for
  10. %% documentation.
  11. %% For performance, the file backend does delayed writes, although it will
  12. %% sync at specific log levels, configured via the `sync_on' option. By default
  13. %% the error level or above will trigger a sync.
  14. -module(rumBackendFile).
  15. -include("eRum.hrl").
  16. -include_lib("kernel/include/file.hrl").
  17. -behaviour(gen_event).
  18. -ifdef(TEST).
  19. -include_lib("eunit/include/eunit.hrl").
  20. -compile([{parse_transform, lager_transform}]).
  21. -endif.
  22. -export([init/1, handle_call/2, handle_event/2, handle_info/2, terminate/2,
  23. code_change/3]).
  24. -export([config_to_id/1]).
  25. -define(DEFAULT_LOG_LEVEL, info).
  26. -define(DEFAULT_ROTATION_SIZE, 10485760). %% 10mb
  27. -define(DEFAULT_ROTATION_DATE, "$D0"). %% midnight
  28. -define(DEFAULT_ROTATION_COUNT, 5).
  29. -define(DEFAULT_ROTATION_MOD, lager_rotator_default).
  30. -define(DEFAULT_SYNC_LEVEL, error).
  31. -define(DEFAULT_SYNC_INTERVAL, 1000).
  32. -define(DEFAULT_SYNC_SIZE, 1024 * 64). %% 64kb
  33. -define(DEFAULT_CHECK_INTERVAL, 1000).
  34. -record(state, {
  35. name :: string(),
  36. level :: {'mask', integer()},
  37. fd :: file:io_device() | undefined,
  38. inode :: integer() | undefined,
  39. ctime :: file:date_time() | undefined,
  40. flap = false :: boolean(),
  41. size = 0 :: integer(),
  42. date :: undefined | string(),
  43. count = 10 :: integer(),
  44. rotator = lager_util :: atom(),
  45. shaper :: rumShaper(),
  46. formatter :: atom(),
  47. formatter_config :: any(),
  48. sync_on :: {'mask', integer()},
  49. check_interval = ?DEFAULT_CHECK_INTERVAL :: non_neg_integer(),
  50. sync_interval = ?DEFAULT_SYNC_INTERVAL :: non_neg_integer(),
  51. sync_size = ?DEFAULT_SYNC_SIZE :: non_neg_integer(),
  52. last_check = os:timestamp() :: erlang:timestamp(),
  53. os_type :: atom()
  54. }).
  55. -type option() :: {file, string()} | {level, rumLevel()} |
  56. {size, non_neg_integer()} | {date, string()} |
  57. {count, non_neg_integer()} | {rotator, atom()} |
  58. {high_water_mark, non_neg_integer()} |
  59. {flush_queue, boolean()} |
  60. {flush_threshold, non_neg_integer()} |
  61. {sync_interval, non_neg_integer()} |
  62. {sync_size, non_neg_integer()} | {sync_on, rumLevel()} |
  63. {check_interval, non_neg_integer()} | {formatter, atom()} |
  64. {formatter_config, term()}.
  65. -spec init([option(), ...]) -> {ok, #state{}} | {error, {fatal, bad_config}}.
  66. init({FileName, LogLevel}) when is_list(FileName), is_atom(LogLevel) ->
  67. %% backwards compatibility hack
  68. init([{file, FileName}, {level, LogLevel}]);
  69. init({FileName, LogLevel, Size, Date, Count}) when is_list(FileName), is_atom(LogLevel) ->
  70. %% backwards compatibility hack
  71. init([{file, FileName}, {level, LogLevel}, {size, Size}, {date, Date}, {count, Count}]);
  72. init([{FileName, LogLevel, Size, Date, Count}, {Formatter, FormatterConfig}]) when is_list(FileName), is_atom(LogLevel), is_atom(Formatter) ->
  73. %% backwards compatibility hack
  74. init([{file, FileName}, {level, LogLevel}, {size, Size}, {date, Date}, {count, Count}, {formatter, Formatter}, {formatter_config, FormatterConfig}]);
  75. init([LogFile, {Formatter}]) ->
  76. %% backwards compatibility hack
  77. init([LogFile, {Formatter, []}]);
  78. init([{FileName, LogLevel}, {Formatter, FormatterConfig}]) when is_list(FileName), is_atom(LogLevel), is_atom(Formatter) ->
  79. %% backwards compatibility hack
  80. init([{file, FileName}, {level, LogLevel}, {formatter, Formatter}, {formatter_config, FormatterConfig}]);
  81. init(LogFileConfig) when is_list(LogFileConfig) ->
  82. case validate_logfile_proplist(LogFileConfig) of
  83. false ->
  84. %% falied to validate config
  85. {error, {fatal, bad_config}};
  86. Config ->
  87. %% probabably a better way to do this, but whatever
  88. [RelName, Level, Date, Size, Count, Rotator, HighWaterMark, Flush, SyncInterval, SyncSize, SyncOn, CheckInterval, Formatter, FormatterConfig] =
  89. [proplists:get_value(Key, Config) || Key <- [file, level, date, size, count, rotator, high_water_mark, flush_queue, sync_interval, sync_size, sync_on, check_interval, formatter, formatter_config]],
  90. FlushThr = proplists:get_value(flush_threshold, Config, 0),
  91. Name = rumUtil:parsePath(RelName),
  92. schedule_rotation(Name, Date),
  93. Shaper = rumUtil:maybe_flush(Flush, #rumShaper{hwm = HighWaterMark, flushThreshold = FlushThr, id = Name}),
  94. State0 = #state{name = Name, level = Level, size = Size, date = Date, count = Count, rotator = Rotator,
  95. shaper = Shaper, formatter = Formatter, formatter_config = FormatterConfig,
  96. sync_on = SyncOn, sync_interval = SyncInterval, sync_size = SyncSize, check_interval = CheckInterval},
  97. State = case Rotator:create_logfile(Name, {SyncSize, SyncInterval}) of
  98. {ok, {FD, Inode, Ctime, _Size}} ->
  99. State0#state{fd = FD, inode = Inode, ctime = Ctime};
  100. {error, Reason} ->
  101. ?INT_LOG(error, "Failed to open log file ~ts with error ~s", [Name, file:format_error(Reason)]),
  102. State0#state{flap = true}
  103. end,
  104. {ok, State}
  105. end.
  106. %% @private
  107. handle_call({set_loglevel, Level}, #state{name = Ident} = State) ->
  108. case validate_loglevel(Level) of
  109. false ->
  110. {ok, {error, bad_loglevel}, State};
  111. Levels ->
  112. ?INT_LOG(notice, "Changed loglevel of ~s to ~p", [Ident, Level]),
  113. {ok, ok, State#state{level = Levels}}
  114. end;
  115. handle_call(get_loglevel, #state{level = Level} = State) ->
  116. {ok, Level, State};
  117. handle_call({set_loghwm, Hwm}, #state{shaper = Shaper, name = Name} = State) ->
  118. case validate_logfile_proplist([{file, Name}, {high_water_mark, Hwm}]) of
  119. false ->
  120. {ok, {error, bad_log_hwm}, State};
  121. _ ->
  122. NewShaper = Shaper#rumShaper{hwm = Hwm},
  123. ?INT_LOG(notice, "Changed loghwm of ~ts to ~p", [Name, Hwm]),
  124. {ok, {last_loghwm, Shaper#rumShaper.hwm}, State#state{shaper = NewShaper}}
  125. end;
  126. handle_call(rotate, State = #state{name = File}) ->
  127. {ok, NewState} = handle_info({rotate, File}, State),
  128. {ok, ok, NewState};
  129. handle_call(_Request, State) ->
  130. {ok, ok, State}.
  131. %% @private
  132. handle_event({log, Message},
  133. #state{name = Name, level = L, shaper = Shaper, formatter = Formatter, formatter_config = FormatConfig} = State) ->
  134. case rumUtil:is_loggable(Message, L, {lager_file_backend, Name}) of
  135. true ->
  136. case rumUtil:check_hwm(Shaper) of
  137. {true, Drop, #rumShaper{hwm = Hwm} = NewShaper} ->
  138. NewState = case Drop > 0 of
  139. true ->
  140. Report = io_lib:format(
  141. "lager_file_backend dropped ~p messages in the last second that exceeded the limit of ~p messages/sec",
  142. [Drop, Hwm]),
  143. ReportMsg = rumMsg:new(Report, warning, [], []),
  144. write(State, rumMsg:timestamp(ReportMsg),
  145. rumMsg:severity_as_int(ReportMsg), Formatter:format(ReportMsg, FormatConfig));
  146. false ->
  147. State
  148. end,
  149. {ok, write(NewState#state{shaper = NewShaper},
  150. rumMsg:timestamp(Message), rumMsg:severity_as_int(Message),
  151. Formatter:format(Message, FormatConfig))};
  152. {false, _, #rumShaper{dropped = D} = NewShaper} ->
  153. {ok, State#state{shaper = NewShaper#rumShaper{dropped = D + 1}}}
  154. end;
  155. false ->
  156. {ok, State}
  157. end;
  158. handle_event(_Event, State) ->
  159. {ok, State}.
  160. %% @private
  161. handle_info({rotate, File}, #state{name = File, count = Count, date = Date, rotator = Rotator} = State0) ->
  162. State1 = close_file(State0),
  163. _ = Rotator:rotate_logfile(File, Count),
  164. schedule_rotation(File, Date),
  165. {ok, State1};
  166. handle_info({shaper_expired, Name}, #state{shaper = Shaper, name = Name, formatter = Formatter, formatter_config = FormatConfig} = State) ->
  167. _ = case Shaper#rumShaper.dropped of
  168. 0 ->
  169. ok;
  170. Dropped ->
  171. Report = io_lib:format(
  172. "lager_file_backend dropped ~p messages in the last second that exceeded the limit of ~p messages/sec",
  173. [Dropped, Shaper#rumShaper.hwm]),
  174. ReportMsg = rumMsg:new(Report, warning, [], []),
  175. write(State, rumMsg:timestamp(ReportMsg),
  176. rumMsg:severity_as_int(ReportMsg), Formatter:format(ReportMsg, FormatConfig))
  177. end,
  178. {ok, State#state{shaper = Shaper#rumShaper{dropped = 0, mps = 0, lastTime = os:timestamp()}}};
  179. handle_info(_Info, State) ->
  180. {ok, State}.
  181. %% @private
  182. terminate(_Reason, State) ->
  183. %% leaving this function call unmatched makes dialyzer cranky
  184. _ = close_file(State),
  185. ok.
  186. %% @private
  187. code_change(_OldVsn, State, _Extra) ->
  188. {ok, State}.
  189. %% Convert the config into a gen_event handler ID
  190. config_to_id({Name, _Severity}) when is_list(Name) ->
  191. {?MODULE, Name};
  192. config_to_id({Name, _Severity, _Size, _Rotation, _Count}) ->
  193. {?MODULE, Name};
  194. config_to_id([{Name, _Severity, _Size, _Rotation, _Count}, _Format]) ->
  195. {?MODULE, Name};
  196. config_to_id([{Name, _Severity}, _Format]) when is_list(Name) ->
  197. {?MODULE, Name};
  198. config_to_id(Config) ->
  199. case proplists:get_value(file, Config) of
  200. undefined ->
  201. erlang:error(no_file);
  202. File ->
  203. {?MODULE, File}
  204. end.
  205. write(#state{name = Name, fd = FD,
  206. inode = Inode, ctime = Ctime,
  207. flap = Flap, size = RotSize,
  208. count = Count, rotator = Rotator} = State0, Timestamp, Level, Msg) ->
  209. case write_should_check(State0, Timestamp) of
  210. true ->
  211. %% need to check for rotation
  212. Buffer = {State0#state.sync_size, State0#state.sync_interval},
  213. case Rotator:ensure_logfile(Name, FD, Inode, Ctime, Buffer) of
  214. {ok, {_FD, _Inode, _Ctime, Size}} when RotSize > 0, Size > RotSize ->
  215. State1 = close_file(State0),
  216. case Rotator:rotate_logfile(Name, Count) of
  217. ok ->
  218. %% go around the loop again, we'll do another rotation check and hit the next clause of ensure_logfile
  219. write(State1, Timestamp, Level, Msg);
  220. {error, Reason} ->
  221. case Flap of
  222. true ->
  223. State1;
  224. _ ->
  225. ?INT_LOG(error, "Failed to rotate log file ~ts with error ~s", [Name, file:format_error(Reason)]),
  226. State1#state{flap = true}
  227. end
  228. end;
  229. {ok, {NewFD, NewInode, NewCtime, _Size}} ->
  230. %% update our last check and try again
  231. State1 = State0#state{last_check = Timestamp, fd = NewFD, inode = NewInode, ctime = NewCtime},
  232. do_write(State1, Level, Msg);
  233. {error, Reason} ->
  234. case Flap of
  235. true ->
  236. State0;
  237. _ ->
  238. ?INT_LOG(error, "Failed to reopen log file ~ts with error ~s", [Name, file:format_error(Reason)]),
  239. State0#state{flap = true}
  240. end
  241. end;
  242. false ->
  243. do_write(State0, Level, Msg)
  244. end.
  245. write_should_check(#state{fd = undefined}, _Timestamp) ->
  246. true;
  247. write_should_check(#state{last_check = LastCheck0, check_interval = CheckInterval,
  248. name = Name, inode = Inode0, ctime = Ctime0}, Timestamp) ->
  249. LastCheck1 = timer:now_diff(Timestamp, LastCheck0) div 1000,
  250. case LastCheck1 >= CheckInterval of
  251. true ->
  252. true;
  253. _ ->
  254. % We need to know if the file has changed "out from under lager" so we don't
  255. % write to an invalid FD
  256. {Result, _FInfo} = rumUtil:has_file_changed(Name, Inode0, Ctime0),
  257. Result
  258. end.
  259. do_write(#state{fd = FD, name = Name, flap = Flap} = State, Level, Msg) ->
  260. %% delayed_write doesn't report errors
  261. _ = file:write(FD, unicode:characters_to_binary(Msg)),
  262. {mask, SyncLevel} = State#state.sync_on,
  263. case (Level band SyncLevel) =/= 0 of
  264. true ->
  265. %% force a sync on any message that matches the 'sync_on' bitmask
  266. Flap2 = case file:datasync(FD) of
  267. {error, Reason2} when Flap == false ->
  268. ?INT_LOG(error, "Failed to write log message to file ~ts: ~s",
  269. [Name, file:format_error(Reason2)]),
  270. true;
  271. ok ->
  272. false;
  273. _ ->
  274. Flap
  275. end,
  276. State#state{flap = Flap2};
  277. _ ->
  278. State
  279. end.
  280. validate_loglevel(Level) ->
  281. try rumUtil:config_to_mask(Level) of
  282. Levels ->
  283. Levels
  284. catch
  285. _:_ ->
  286. false
  287. end.
  288. validate_logfile_proplist(List) ->
  289. try validate_logfile_proplist(List, []) of
  290. Res ->
  291. case proplists:get_value(file, Res) of
  292. undefined ->
  293. ?INT_LOG(error, "Missing required file option", []),
  294. false;
  295. _File ->
  296. %% merge with the default options
  297. {ok, DefaultRotationDate} = rumUtil:parse_rotation_date_spec(?DEFAULT_ROTATION_DATE),
  298. lists:keymerge(1, lists:sort(Res), lists:sort([
  299. {level, validate_loglevel(?DEFAULT_LOG_LEVEL)}, {date, DefaultRotationDate},
  300. {size, ?DEFAULT_ROTATION_SIZE}, {count, ?DEFAULT_ROTATION_COUNT},
  301. {rotator, ?DEFAULT_ROTATION_MOD},
  302. {sync_on, validate_loglevel(?DEFAULT_SYNC_LEVEL)}, {sync_interval, ?DEFAULT_SYNC_INTERVAL},
  303. {sync_size, ?DEFAULT_SYNC_SIZE}, {check_interval, ?DEFAULT_CHECK_INTERVAL},
  304. {formatter, lager_default_formatter}, {formatter_config, []}
  305. ]))
  306. end
  307. catch
  308. {bad_config, Msg, Value} ->
  309. ?INT_LOG(error, "~s ~p for file ~tp",
  310. [Msg, Value, proplists:get_value(file, List)]),
  311. false
  312. end.
  313. validate_logfile_proplist([], Acc) ->
  314. Acc;
  315. validate_logfile_proplist([{file, File} | Tail], Acc) ->
  316. %% is there any reasonable validation we can do here?
  317. validate_logfile_proplist(Tail, [{file, File} | Acc]);
  318. validate_logfile_proplist([{level, Level} | Tail], Acc) ->
  319. case validate_loglevel(Level) of
  320. false ->
  321. throw({bad_config, "Invalid loglevel", Level});
  322. Res ->
  323. validate_logfile_proplist(Tail, [{level, Res} | Acc])
  324. end;
  325. validate_logfile_proplist([{size, Size} | Tail], Acc) ->
  326. case Size of
  327. S when is_integer(S), S >= 0 ->
  328. validate_logfile_proplist(Tail, [{size, Size} | Acc]);
  329. _ ->
  330. throw({bad_config, "Invalid rotation size", Size})
  331. end;
  332. validate_logfile_proplist([{count, Count} | Tail], Acc) ->
  333. case Count of
  334. C when is_integer(C), C >= 0 ->
  335. validate_logfile_proplist(Tail, [{count, Count} | Acc]);
  336. _ ->
  337. throw({bad_config, "Invalid rotation count", Count})
  338. end;
  339. validate_logfile_proplist([{rotator, Rotator} | Tail], Acc) ->
  340. case is_atom(Rotator) of
  341. true ->
  342. validate_logfile_proplist(Tail, [{rotator, Rotator} | Acc]);
  343. false ->
  344. throw({bad_config, "Invalid rotation module", Rotator})
  345. end;
  346. validate_logfile_proplist([{high_water_mark, HighWaterMark} | Tail], Acc) ->
  347. case HighWaterMark of
  348. Hwm when is_integer(Hwm), Hwm >= 0 ->
  349. validate_logfile_proplist(Tail, [{high_water_mark, Hwm} | Acc]);
  350. _ ->
  351. throw({bad_config, "Invalid high water mark", HighWaterMark})
  352. end;
  353. validate_logfile_proplist([{date, Date} | Tail], Acc) ->
  354. case rumUtil:parse_rotation_date_spec(Date) of
  355. {ok, Spec} ->
  356. validate_logfile_proplist(Tail, [{date, Spec} | Acc]);
  357. {error, _} when Date == "" ->
  358. %% legacy config allowed blanks
  359. validate_logfile_proplist(Tail, [{date, undefined} | Acc]);
  360. {error, _} ->
  361. throw({bad_config, "Invalid rotation date", Date})
  362. end;
  363. validate_logfile_proplist([{sync_interval, SyncInt} | Tail], Acc) ->
  364. case SyncInt of
  365. Val when is_integer(Val), Val >= 0 ->
  366. validate_logfile_proplist(Tail, [{sync_interval, Val} | Acc]);
  367. _ ->
  368. throw({bad_config, "Invalid sync interval", SyncInt})
  369. end;
  370. validate_logfile_proplist([{sync_size, SyncSize} | Tail], Acc) ->
  371. case SyncSize of
  372. Val when is_integer(Val), Val >= 0 ->
  373. validate_logfile_proplist(Tail, [{sync_size, Val} | Acc]);
  374. _ ->
  375. throw({bad_config, "Invalid sync size", SyncSize})
  376. end;
  377. validate_logfile_proplist([{check_interval, CheckInt} | Tail], Acc) ->
  378. case CheckInt of
  379. Val when is_integer(Val), Val >= 0 ->
  380. validate_logfile_proplist(Tail, [{check_interval, Val} | Acc]);
  381. always ->
  382. validate_logfile_proplist(Tail, [{check_interval, 0} | Acc]);
  383. _ ->
  384. throw({bad_config, "Invalid check interval", CheckInt})
  385. end;
  386. validate_logfile_proplist([{sync_on, Level} | Tail], Acc) ->
  387. case validate_loglevel(Level) of
  388. false ->
  389. throw({bad_config, "Invalid sync on level", Level});
  390. Res ->
  391. validate_logfile_proplist(Tail, [{sync_on, Res} | Acc])
  392. end;
  393. validate_logfile_proplist([{formatter, Fmt} | Tail], Acc) ->
  394. case is_atom(Fmt) of
  395. true ->
  396. validate_logfile_proplist(Tail, [{formatter, Fmt} | Acc]);
  397. false ->
  398. throw({bad_config, "Invalid formatter module", Fmt})
  399. end;
  400. validate_logfile_proplist([{formatter_config, FmtCfg} | Tail], Acc) ->
  401. case is_list(FmtCfg) of
  402. true ->
  403. validate_logfile_proplist(Tail, [{formatter_config, FmtCfg} | Acc]);
  404. false ->
  405. throw({bad_config, "Invalid formatter config", FmtCfg})
  406. end;
  407. validate_logfile_proplist([{flush_queue, FlushCfg} | Tail], Acc) ->
  408. case is_boolean(FlushCfg) of
  409. true ->
  410. validate_logfile_proplist(Tail, [{flush_queue, FlushCfg} | Acc]);
  411. false ->
  412. throw({bad_config, "Invalid queue flush flag", FlushCfg})
  413. end;
  414. validate_logfile_proplist([{flush_threshold, Thr} | Tail], Acc) ->
  415. case Thr of
  416. _ when is_integer(Thr), Thr >= 0 ->
  417. validate_logfile_proplist(Tail, [{flush_threshold, Thr} | Acc]);
  418. _ ->
  419. throw({bad_config, "Invalid queue flush threshold", Thr})
  420. end;
  421. validate_logfile_proplist([Other | _Tail], _Acc) ->
  422. throw({bad_config, "Invalid option", Other}).
  423. schedule_rotation(_, undefined) ->
  424. ok;
  425. schedule_rotation(Name, Date) ->
  426. erlang:send_after(rumUtil:calculate_next_rotation(Date) * 1000, self(), {rotate, Name}),
  427. ok.
  428. close_file(#state{fd = undefined} = State) ->
  429. State;
  430. close_file(#state{fd = FD} = State) ->
  431. %% Flush and close any file handles.
  432. %% delayed write can cause file:close not to do a close
  433. _ = file:datasync(FD),
  434. _ = file:close(FD),
  435. _ = file:close(FD),
  436. State#state{fd = undefined}.
  437. -ifdef(TEST).
  438. get_loglevel_test() ->
  439. {ok, Level, _} = handle_call(get_loglevel,
  440. #state{name = "bar", level = rumUtil:config_to_mask(info), fd = 0, inode = 0, ctime = undefined}),
  441. ?assertEqual(Level, rumUtil:config_to_mask(info)),
  442. {ok, Level2, _} = handle_call(get_loglevel,
  443. #state{name = "foo", level = rumUtil:config_to_mask(warning), fd = 0, inode = 0, ctime = undefined}),
  444. ?assertEqual(Level2, rumUtil:config_to_mask(warning)).
  445. rotation_test_() ->
  446. {foreach,
  447. fun() ->
  448. SyncLevel = validate_loglevel(?DEFAULT_SYNC_LEVEL),
  449. SyncSize = ?DEFAULT_SYNC_SIZE,
  450. SyncInterval = ?DEFAULT_SYNC_INTERVAL,
  451. Rotator = ?DEFAULT_ROTATION_MOD,
  452. CheckInterval = 0, %% hard to test delayed mode
  453. {ok, TestDir} = rumUtil:create_test_dir(),
  454. TestLog = filename:join(TestDir, "test.log"),
  455. {OsType, _} = os:type(),
  456. #state{name = TestLog,
  457. level = ?DEBUG,
  458. sync_on = SyncLevel,
  459. sync_size = SyncSize,
  460. sync_interval = SyncInterval,
  461. check_interval = CheckInterval,
  462. rotator = Rotator,
  463. os_type = OsType}
  464. end,
  465. fun(#state{}) ->
  466. ok = rumUtil:delete_test_dir()
  467. end, [
  468. fun(DefaultState = #state{name = TestLog, os_type = OsType, sync_size = SyncSize, sync_interval = SyncInterval, rotator = Rotator}) ->
  469. {"External rotation should work",
  470. fun() ->
  471. case OsType of
  472. win32 ->
  473. % Note: test is skipped on win32 due to the fact that a file can't be deleted or renamed
  474. % while a process has an open file handle referencing it
  475. ok;
  476. _ ->
  477. {ok, {FD, Inode, Ctime, _Size}} = Rotator:open_logfile(TestLog, {SyncSize, SyncInterval}),
  478. State0 = DefaultState#state{fd = FD, inode = Inode, ctime = Ctime},
  479. State1 = write(State0, os:timestamp(), ?DEBUG, "hello world"),
  480. ?assertMatch(#state{name = TestLog, level = ?DEBUG, fd = FD, inode = Inode, ctime = Ctime}, State1),
  481. ?assertEqual(ok, file:delete(TestLog)),
  482. State2 = write(State0, os:timestamp(), ?DEBUG, "hello world"),
  483. %% assert file has changed
  484. ExpState1 = #state{name = TestLog, level = ?DEBUG, fd = FD, inode = Inode, ctime = Ctime},
  485. ?assertNotEqual(ExpState1, State2),
  486. ?assertMatch(#state{name = TestLog, level = ?DEBUG}, State2),
  487. ?assertEqual(ok, file:rename(TestLog, TestLog ++ ".1")),
  488. State3 = write(State2, os:timestamp(), ?DEBUG, "hello world"),
  489. %% assert file has changed
  490. ?assertNotEqual(State3, State2),
  491. ?assertMatch(#state{name = TestLog, level = ?DEBUG}, State3),
  492. ok
  493. end
  494. end}
  495. end,
  496. fun(DefaultState = #state{name = TestLog, sync_size = SyncSize, sync_interval = SyncInterval, rotator = Rotator}) ->
  497. {"Internal rotation and delayed write",
  498. fun() ->
  499. TestLog0 = TestLog ++ ".0",
  500. CheckInterval = 3000, % 3 sec
  501. RotationSize = 15,
  502. PreviousCheck = os:timestamp(),
  503. {ok, {FD, Inode, Ctime, _Size}} = Rotator:open_logfile(TestLog, {SyncSize, SyncInterval}),
  504. State0 = DefaultState#state{
  505. fd = FD, inode = Inode, ctime = Ctime, size = RotationSize,
  506. check_interval = CheckInterval, last_check = PreviousCheck},
  507. %% new message within check interval with sync_on level
  508. Msg1Timestamp = add_secs(PreviousCheck, 1),
  509. State1 = write(State0, Msg1Timestamp, ?ERROR, "big big message 1"),
  510. ?assertEqual(State0, State1),
  511. %% new message within check interval under sync_on level
  512. %% not written to disk yet
  513. Msg2Timestamp = add_secs(PreviousCheck, 2),
  514. State2 = write(State1, Msg2Timestamp, ?DEBUG, "buffered message 2"),
  515. ?assertEqual(State0, State2),
  516. % Note: we must ensure at least one second (DEFAULT_SYNC_INTERVAL) has passed
  517. % for message 1 and 2 to be written to disk
  518. ElapsedMs = timer:now_diff(os:timestamp(), PreviousCheck) div 1000,
  519. case ElapsedMs > SyncInterval of
  520. true ->
  521. ok;
  522. _ ->
  523. S = SyncInterval - ElapsedMs,
  524. timer:sleep(S)
  525. end,
  526. %% although file size is big enough...
  527. {ok, FInfo} = file:read_file_info(TestLog, [raw]),
  528. ?assert(RotationSize < FInfo#file_info.size),
  529. %% ...no rotation yet
  530. ?assertEqual(PreviousCheck, State2#state.last_check),
  531. ?assertNot(filelib:is_regular(TestLog0)),
  532. %% new message after check interval
  533. Msg3Timestamp = add_secs(PreviousCheck, 4),
  534. _State3 = write(State2, Msg3Timestamp, ?DEBUG, "message 3"),
  535. %% rotation happened
  536. ?assert(filelib:is_regular(TestLog0)),
  537. {ok, Bin1} = file:read_file(TestLog0),
  538. {ok, Bin2} = file:read_file(TestLog),
  539. %% message 1-2 written to file
  540. ?assertEqual(<<"big big message 1buffered message 2">>, Bin1),
  541. %% message 3 buffered, not yet written to file
  542. ?assertEqual(<<"">>, Bin2),
  543. ok
  544. end}
  545. end
  546. ]}.
  547. add_secs({Mega, Secs, Micro}, Add) ->
  548. NewSecs = Secs + Add,
  549. {Mega + NewSecs div 10000000, NewSecs rem 10000000, Micro}.
  550. filesystem_test_() ->
  551. {foreach,
  552. fun() ->
  553. ok = error_logger:tty(false),
  554. ok = rumUtil:safe_application_load(lager),
  555. ok = application:set_env(lager, handlers, [{lager_test_backend, info}]),
  556. ok = application:set_env(lager, errLoggerRedirect, false),
  557. ok = application:set_env(lager, asyncThreshold, undefined),
  558. {ok, _TestDir} = rumUtil:create_test_dir(),
  559. ok = eRum:start(),
  560. %% race condition where lager logs its own start up
  561. %% makes several tests fail. See test/lager_test_backend
  562. %% around line 800 for more information.
  563. ok = timer:sleep(5),
  564. ok = lager_test_backend:flush()
  565. end,
  566. fun(_) ->
  567. ok = application:stop(lager),
  568. ok = application:stop(goldrush),
  569. ok = error_logger:tty(true),
  570. ok = rumUtil:delete_test_dir()
  571. end, [
  572. {"under normal circumstances, file should be opened",
  573. fun() ->
  574. {ok, TestDir} = rumUtil:get_test_dir(),
  575. TestLog = filename:join(TestDir, "test.log"),
  576. gen_event:add_handler(rumEvent, lager_file_backend,
  577. [{TestLog, info}, {lager_default_formatter}]),
  578. eRum:log(error, self(), "Test message"),
  579. {ok, Bin} = file:read_file(TestLog),
  580. Pid = pid_to_list(self()),
  581. ?assertMatch([_, _, "[error]", Pid, "Test message\n"],
  582. re:split(Bin, " ", [{return, list}, {parts, 5}]))
  583. end},
  584. {"don't choke on unicode",
  585. fun() ->
  586. {ok, TestDir} = rumUtil:get_test_dir(),
  587. TestLog = filename:join(TestDir, "test.log"),
  588. gen_event:add_handler(rumEvent, lager_file_backend,
  589. [{TestLog, info}, {lager_default_formatter}]),
  590. eRum:log(error, self(), "~ts", [[20013, 25991, 27979, 35797]]),
  591. {ok, Bin} = file:read_file(TestLog),
  592. Pid = pid_to_list(self()),
  593. ?assertMatch([_, _, "[error]", Pid,
  594. [228, 184, 173, 230, 150, 135, 230, 181, 139, 232, 175, 149, $\n]],
  595. re:split(Bin, " ", [{return, list}, {parts, 5}]))
  596. end},
  597. {"don't choke on latin-1",
  598. fun() ->
  599. {ok, TestDir} = rumUtil:get_test_dir(),
  600. TestLog = filename:join(TestDir, "test.log"),
  601. %% XXX if this test fails, check that this file is encoded latin-1, not utf-8!
  602. gen_event:add_handler(rumEvent, lager_file_backend,
  603. [{TestLog, info}, {lager_default_formatter}]),
  604. eRum:log(error, self(), "~ts", [[76, 198, 221, 206, 78, $-, 239]]),
  605. {ok, Bin} = file:read_file(TestLog),
  606. Pid = pid_to_list(self()),
  607. Res = re:split(Bin, " ", [{return, list}, {parts, 5}]),
  608. ?assertMatch([_, _, "[error]", Pid,
  609. [76, 195, 134, 195, 157, 195, 142, 78, 45, 195, 175, $\n]], Res)
  610. end},
  611. {"file can't be opened on startup triggers an error message",
  612. fun() ->
  613. {ok, TestDir} = rumUtil:get_test_dir(),
  614. TestLog = filename:join(TestDir, "test.log"),
  615. ?assertEqual(ok, rumUtil:safe_write_file(TestLog, [])),
  616. {ok, FInfo0} = file:read_file_info(TestLog, [raw]),
  617. FInfo1 = FInfo0#file_info{mode = 0},
  618. ?assertEqual(ok, file:write_file_info(TestLog, FInfo1)),
  619. gen_event:add_handler(rumEvent, lager_file_backend,
  620. {TestLog, info, 10 * 1024 * 1024, "$D0", 5}),
  621. % Note: required on win32, do this early to prevent subsequent failures
  622. % from preventing cleanup
  623. ?assertEqual(ok, file:write_file_info(TestLog, FInfo0)),
  624. ?assertEqual(1, lager_test_backend:count()),
  625. {_Level, _Time, Message, _Metadata} = lager_test_backend:pop(),
  626. MessageFlat = lists:flatten(Message),
  627. ?assertEqual(
  628. "Failed to open log file " ++ TestLog ++ " with error permission denied",
  629. MessageFlat)
  630. end},
  631. {"file that becomes unavailable at runtime should trigger an error message",
  632. fun() ->
  633. case os:type() of
  634. {win32, _} ->
  635. % Note: test is skipped on win32 due to the fact that a file can't be
  636. % deleted or renamed while a process has an open file handle referencing it
  637. ok;
  638. _ ->
  639. {ok, TestDir} = rumUtil:get_test_dir(),
  640. TestLog = filename:join(TestDir, "test.log"),
  641. gen_event:add_handler(rumEvent, lager_file_backend,
  642. [{file, TestLog}, {level, info}, {check_interval, 0}]),
  643. ?assertEqual(0, lager_test_backend:count()),
  644. eRum:log(error, self(), "Test message"),
  645. ?assertEqual(1, lager_test_backend:count()),
  646. ?assertEqual(ok, file:delete(TestLog)),
  647. ?assertEqual(ok, rumUtil:safe_write_file(TestLog, "")),
  648. {ok, FInfo0} = file:read_file_info(TestLog, [raw]),
  649. FInfo1 = FInfo0#file_info{mode = 0},
  650. ?assertEqual(ok, file:write_file_info(TestLog, FInfo1)),
  651. eRum:log(error, self(), "Test message"),
  652. lager_test_backend:pop(),
  653. lager_test_backend:pop(),
  654. {_Level, _Time, Message, _Metadata} = lager_test_backend:pop(),
  655. ?assertEqual(
  656. "Failed to reopen log file " ++ TestLog ++ " with error permission denied",
  657. lists:flatten(Message))
  658. end
  659. end},
  660. {"unavailable files that are fixed at runtime should start having log messages written",
  661. fun() ->
  662. {ok, TestDir} = rumUtil:get_test_dir(),
  663. TestLog = filename:join(TestDir, "test.log"),
  664. ?assertEqual(ok, rumUtil:safe_write_file(TestLog, [])),
  665. {ok, FInfo} = file:read_file_info(TestLog, [raw]),
  666. OldPerms = FInfo#file_info.mode,
  667. ?assertEqual(ok, file:write_file_info(TestLog, FInfo#file_info{mode = 0})),
  668. gen_event:add_handler(rumEvent, lager_file_backend,
  669. [{file, TestLog}, {check_interval, 0}]),
  670. ?assertEqual(1, lager_test_backend:count()),
  671. {_Level, _Time, Message, _Metadata} = lager_test_backend:pop(),
  672. ?assertEqual(
  673. "Failed to open log file " ++ TestLog ++ " with error permission denied",
  674. lists:flatten(Message)),
  675. ?assertEqual(ok, file:write_file_info(TestLog, FInfo#file_info{mode = OldPerms})),
  676. eRum:log(error, self(), "Test message"),
  677. {ok, Bin} = file:read_file(TestLog),
  678. Pid = pid_to_list(self()),
  679. ?assertMatch([_, _, "[error]", Pid, "Test message\n"],
  680. re:split(Bin, " ", [{return, list}, {parts, 5}]))
  681. end},
  682. {"external logfile rotation/deletion should be handled",
  683. fun() ->
  684. case os:type() of
  685. {win32, _} ->
  686. % Note: test is skipped on win32 due to the fact that a file can't be deleted or renamed
  687. % while a process has an open file handle referencing it
  688. ok;
  689. _ ->
  690. {ok, TestDir} = rumUtil:get_test_dir(),
  691. TestLog = filename:join(TestDir, "test.log"),
  692. TestLog0 = TestLog ++ ".0",
  693. gen_event:add_handler(rumEvent, lager_file_backend,
  694. [{file, TestLog}, {level, info}, {check_interval, 0}]),
  695. ?assertEqual(0, lager_test_backend:count()),
  696. eRum:log(error, self(), "Test message1"),
  697. ?assertEqual(1, lager_test_backend:count()),
  698. ?assertEqual(ok, file:delete(TestLog)),
  699. ?assertEqual(ok, rumUtil:safe_write_file(TestLog, "")),
  700. eRum:log(error, self(), "Test message2"),
  701. ?assertEqual(2, lager_test_backend:count()),
  702. {ok, Bin} = file:read_file(TestLog),
  703. Pid = pid_to_list(self()),
  704. ?assertMatch([_, _, "[error]", Pid, "Test message2\n"],
  705. re:split(Bin, " ", [{return, list}, {parts, 5}])),
  706. ?assertEqual(ok, file:rename(TestLog, TestLog0)),
  707. eRum:log(error, self(), "Test message3"),
  708. ?assertEqual(3, lager_test_backend:count()),
  709. {ok, Bin2} = file:read_file(TestLog),
  710. ?assertMatch([_, _, "[error]", Pid, "Test message3\n"],
  711. re:split(Bin2, " ", [{return, list}, {parts, 5}]))
  712. end
  713. end},
  714. {"internal size rotation should work",
  715. fun() ->
  716. {ok, TestDir} = rumUtil:get_test_dir(),
  717. TestLog = filename:join(TestDir, "test.log"),
  718. TestLog0 = TestLog ++ ".0",
  719. gen_event:add_handler(rumEvent, lager_file_backend,
  720. [{file, TestLog}, {level, info}, {check_interval, 0}, {size, 10}]),
  721. eRum:log(error, self(), "Test message1"),
  722. eRum:log(error, self(), "Test message1"),
  723. ?assert(filelib:is_regular(TestLog0))
  724. end},
  725. {"internal time rotation should work",
  726. fun() ->
  727. {ok, TestDir} = rumUtil:get_test_dir(),
  728. TestLog = filename:join(TestDir, "test.log"),
  729. TestLog0 = TestLog ++ ".0",
  730. gen_event:add_handler(rumEvent, lager_file_backend,
  731. [{file, TestLog}, {level, info}, {check_interval, 1000}]),
  732. eRum:log(error, self(), "Test message1"),
  733. eRum:log(error, self(), "Test message1"),
  734. whereis(rumEvent) ! {rotate, TestLog},
  735. eRum:log(error, self(), "Test message1"),
  736. ?assert(filelib:is_regular(TestLog0))
  737. end},
  738. {"rotation call should work",
  739. fun() ->
  740. {ok, TestDir} = rumUtil:get_test_dir(),
  741. TestLog = filename:join(TestDir, "test.log"),
  742. TestLog0 = TestLog ++ ".0",
  743. gen_event:add_handler(rumEvent, {lager_file_backend, TestLog},
  744. [{file, TestLog}, {level, info}, {check_interval, 1000}]),
  745. eRum:log(error, self(), "Test message1"),
  746. eRum:log(error, self(), "Test message1"),
  747. gen_event:call(rumEvent, {lager_file_backend, TestLog}, rotate, infinity),
  748. eRum:log(error, self(), "Test message1"),
  749. ?assert(filelib:is_regular(TestLog0))
  750. end},
  751. {"sync_on option should work",
  752. fun() ->
  753. {ok, TestDir} = rumUtil:get_test_dir(),
  754. TestLog = filename:join(TestDir, "test.log"),
  755. gen_event:add_handler(rumEvent, lager_file_backend, [{file, TestLog},
  756. {level, info}, {sync_on, "=info"}, {check_interval, 5000}, {sync_interval, 5000}]),
  757. eRum:log(error, self(), "Test message1"),
  758. eRum:log(error, self(), "Test message1"),
  759. ?assertEqual({ok, <<>>}, file:read_file(TestLog)),
  760. eRum:log(info, self(), "Test message1"),
  761. {ok, Bin} = file:read_file(TestLog),
  762. ?assert(<<>> /= Bin)
  763. end},
  764. {"sync_on none option should work (also tests sync_interval)",
  765. fun() ->
  766. {ok, TestDir} = rumUtil:get_test_dir(),
  767. TestLog = filename:join(TestDir, "test.log"),
  768. gen_event:add_handler(rumEvent, lager_file_backend, [{file, TestLog},
  769. {level, info}, {sync_on, "none"}, {check_interval, 5000}, {sync_interval, 1000}]),
  770. eRum:log(error, self(), "Test message1"),
  771. eRum:log(error, self(), "Test message1"),
  772. ?assertEqual({ok, <<>>}, file:read_file(TestLog)),
  773. eRum:log(info, self(), "Test message1"),
  774. ?assertEqual({ok, <<>>}, file:read_file(TestLog)),
  775. timer:sleep(2000),
  776. {ok, Bin} = file:read_file(TestLog),
  777. ?assert(<<>> /= Bin)
  778. end},
  779. {"sync_size option should work",
  780. fun() ->
  781. {ok, TestDir} = rumUtil:get_test_dir(),
  782. TestLog = filename:join(TestDir, "test.log"),
  783. gen_event:add_handler(rumEvent, lager_file_backend, [{file, TestLog}, {level, info},
  784. {sync_on, "none"}, {check_interval, 5001}, {sync_size, 640}, {sync_interval, 5001}]),
  785. eRum:log(error, self(), "Test messageis64bytes"),
  786. eRum:log(error, self(), "Test messageis64bytes"),
  787. eRum:log(error, self(), "Test messageis64bytes"),
  788. eRum:log(error, self(), "Test messageis64bytes"),
  789. eRum:log(error, self(), "Test messageis64bytes"),
  790. ?assertEqual({ok, <<>>}, file:read_file(TestLog)),
  791. eRum:log(error, self(), "Test messageis64bytes"),
  792. eRum:log(error, self(), "Test messageis64bytes"),
  793. eRum:log(error, self(), "Test messageis64bytes"),
  794. eRum:log(error, self(), "Test messageis64bytes"),
  795. ?assertEqual({ok, <<>>}, file:read_file(TestLog)),
  796. %% now we've written enough bytes
  797. eRum:log(error, self(), "Test messageis64bytes"),
  798. {ok, Bin} = file:read_file(TestLog),
  799. ?assert(<<>> /= Bin)
  800. end},
  801. {"runtime level changes",
  802. fun() ->
  803. {ok, TestDir} = rumUtil:get_test_dir(),
  804. TestLog = filename:join(TestDir, "test.log"),
  805. gen_event:add_handler(rumEvent, {lager_file_backend, TestLog}, {TestLog, info}),
  806. ?assertEqual(0, lager_test_backend:count()),
  807. eRum:log(info, self(), "Test message1"),
  808. eRum:log(error, self(), "Test message2"),
  809. {ok, Bin} = file:read_file(TestLog),
  810. Lines = length(re:split(Bin, "\n", [{return, list}, trim])),
  811. ?assertEqual(Lines, 2),
  812. ?assertEqual(ok, eRum:set_loglevel(lager_file_backend, TestLog, warning)),
  813. eRum:log(info, self(), "Test message3"), %% this won't get logged
  814. eRum:log(error, self(), "Test message4"),
  815. {ok, Bin2} = file:read_file(TestLog),
  816. Lines2 = length(re:split(Bin2, "\n", [{return, list}, trim])),
  817. ?assertEqual(Lines2, 3)
  818. end},
  819. {"invalid runtime level changes",
  820. fun() ->
  821. {ok, TestDir} = rumUtil:get_test_dir(),
  822. TestLog = filename:join(TestDir, "test.log"),
  823. TestLog3 = filename:join(TestDir, "test3.log"),
  824. gen_event:add_handler(rumEvent, lager_file_backend,
  825. [{TestLog, info, 10 * 1024 * 1024, "$D0", 5}, {lager_default_formatter}]),
  826. gen_event:add_handler(rumEvent, lager_file_backend, {TestLog3, info}),
  827. ?assertEqual({error, bad_module}, eRum:set_loglevel(lager_file_backend, TestLog, warning))
  828. end},
  829. {"tracing should work",
  830. fun() ->
  831. {ok, TestDir} = rumUtil:get_test_dir(),
  832. TestLog = filename:join(TestDir, "test.log"),
  833. gen_event:add_handler(rumEvent, lager_file_backend, {TestLog, critical}),
  834. eRum:error("Test message"),
  835. ?assertEqual({ok, <<>>}, file:read_file(TestLog)),
  836. {Level, _} = rumConfig:get({rumEvent, loglevel}),
  837. rumConfig:set({rumEvent, loglevel}, {Level,
  838. [{[{module, ?MODULE}], ?DEBUG, {lager_file_backend, TestLog}}]}),
  839. eRum:error("Test message"),
  840. timer:sleep(1000),
  841. {ok, Bin} = file:read_file(TestLog),
  842. ?assertMatch([_, _, "[error]", _, "Test message\n"],
  843. re:split(Bin, " ", [{return, list}, {parts, 5}]))
  844. end},
  845. {"tracing should not duplicate messages",
  846. fun() ->
  847. case os:type() of
  848. {win32, _} ->
  849. % Note: test is skipped on win32 due to the fact that a file can't be
  850. % deleted or renamed while a process has an open file handle referencing it
  851. ok;
  852. _ ->
  853. {ok, TestDir} = rumUtil:get_test_dir(),
  854. TestLog = filename:join(TestDir, "test.log"),
  855. gen_event:add_handler(rumEvent, lager_file_backend,
  856. [{file, TestLog}, {level, critical}, {check_interval, always}]),
  857. timer:sleep(500),
  858. eRum:critical("Test message"),
  859. {ok, Bin1} = file:read_file(TestLog),
  860. ?assertMatch([_, _, "[critical]", _, "Test message\n"],
  861. re:split(Bin1, " ", [{return, list}, {parts, 5}])),
  862. ?assertEqual(ok, file:delete(TestLog)),
  863. {Level, _} = rumConfig:get({rumEvent, loglevel}),
  864. rumConfig:set({rumEvent, loglevel},
  865. {Level, [{[{module, ?MODULE}], ?DEBUG, {lager_file_backend, TestLog}}]}),
  866. eRum:critical("Test message"),
  867. {ok, Bin2} = file:read_file(TestLog),
  868. ?assertMatch([_, _, "[critical]", _, "Test message\n"],
  869. re:split(Bin2, " ", [{return, list}, {parts, 5}])),
  870. ?assertEqual(ok, file:delete(TestLog)),
  871. eRum:error("Test message"),
  872. {ok, Bin3} = file:read_file(TestLog),
  873. ?assertMatch([_, _, "[error]", _, "Test message\n"],
  874. re:split(Bin3, " ", [{return, list}, {parts, 5}]))
  875. end
  876. end},
  877. {"tracing to a dedicated file should work",
  878. fun() ->
  879. {ok, TestDir} = rumUtil:get_test_dir(),
  880. TestLog = filename:join(TestDir, "foo.log"),
  881. {ok, _} = eRum:trace_file(TestLog, [{module, ?MODULE}]),
  882. eRum:error("Test message"),
  883. %% not eligible for trace
  884. eRum:log(error, self(), "Test message"),
  885. {ok, Bin3} = file:read_file(TestLog),
  886. ?assertMatch([_, _, "[error]", _, "Test message\n"],
  887. re:split(Bin3, " ", [{return, list}, {parts, 5}]))
  888. end},
  889. {"tracing to a dedicated file should work even if root_log is set",
  890. fun() ->
  891. {ok, TestDir} = rumUtil:get_test_dir(),
  892. LogName = "foo.log",
  893. LogPath = filename:join(TestDir, LogName),
  894. application:set_env(lager, logRoot, TestDir),
  895. {ok, _} = eRum:trace_file(LogName, [{module, ?MODULE}]),
  896. eRum:error("Test message"),
  897. %% not eligible for trace
  898. eRum:log(error, self(), "Test message"),
  899. {ok, Bin3} = file:read_file(LogPath),
  900. application:unset_env(lager, logRoot),
  901. ?assertMatch([_, _, "[error]", _, "Test message\n"],
  902. re:split(Bin3, " ", [{return, list}, {parts, 5}]))
  903. end},
  904. {"tracing with options should work",
  905. fun() ->
  906. {ok, TestDir} = rumUtil:get_test_dir(),
  907. TestLog = filename:join(TestDir, "foo.log"),
  908. TestLog0 = TestLog ++ ".0",
  909. {ok, _} = eRum:trace_file(TestLog, [{module, ?MODULE}],
  910. [{size, 20}, {check_interval, 1}]),
  911. eRum:error("Test message"),
  912. ?assertNot(filelib:is_regular(TestLog0)),
  913. %% rotation is sensitive to intervals between
  914. %% writes so we sleep to exceed the 1
  915. %% millisecond interval specified by
  916. %% check_interval above
  917. timer:sleep(2),
  918. eRum:error("Test message"),
  919. timer:sleep(10),
  920. ?assert(filelib:is_regular(TestLog0))
  921. end},
  922. {"no silent hwm drops",
  923. fun() ->
  924. MsgCount = 15,
  925. {ok, TestDir} = rumUtil:get_test_dir(),
  926. TestLog = filename:join(TestDir, "test.log"),
  927. gen_event:add_handler(rumEvent, lager_file_backend, [{file, TestLog}, {level, info},
  928. {high_water_mark, 5}, {flush_queue, false}, {sync_on, "=warning"}]),
  929. {_, _, MS} = os:timestamp(),
  930. % start close to the beginning of a new second
  931. ?assertEqual(ok, timer:sleep((1000000 - MS) div 1000 + 1)),
  932. [eRum:log(info, self(), "Foo ~p", [K]) || K <- lists:seq(1, MsgCount)],
  933. ?assertEqual(MsgCount, lager_test_backend:count()),
  934. % Note: bumped from 1000 to 1250 to ensure delayed write flushes to disk
  935. ?assertEqual(ok, timer:sleep(1250)),
  936. {ok, Bin} = file:read_file(TestLog),
  937. Last = lists:last(re:split(Bin, "\n", [{return, list}, trim])),
  938. ?assertMatch([_, _, _, _, "lager_file_backend dropped 10 messages in the last second that exceeded the limit of 5 messages/sec"],
  939. re:split(Last, " ", [{return, list}, {parts, 5}]))
  940. end}
  941. ]}.
  942. trace_files_test_() ->
  943. {foreach,
  944. fun() ->
  945. {ok, TestDir} = rumUtil:get_test_dir(),
  946. Log = filename:join(TestDir, "test.log"),
  947. Debug = filename:join(TestDir, "debug.log"),
  948. Events = filename:join(TestDir, "events.log"),
  949. ok = error_logger:tty(false),
  950. ok = rumUtil:safe_application_load(lager),
  951. ok = application:set_env(lager, handlers, [
  952. {lager_file_backend, [
  953. {file, Log},
  954. {level, error},
  955. {formatter, lager_default_formatter},
  956. {formatter_config, [message, "\n"]}
  957. ]}
  958. ]),
  959. ok = application:set_env(lager, traces, [
  960. { % get default level of debug
  961. {lager_file_backend, Debug}, [{module, ?MODULE}]
  962. },
  963. { % Handler Filters Level
  964. {lager_file_backend, Events}, [{module, ?MODULE}], notice
  965. }
  966. ]),
  967. ok = application:set_env(lager, asyncThreshold, undefined),
  968. ok = eRum:start(),
  969. {Log, Debug, Events}
  970. end,
  971. fun({_, _, _}) ->
  972. catch ets:delete(lager_config),
  973. ok = application:unset_env(lager, traces),
  974. ok = application:stop(lager),
  975. ok = rumUtil:delete_test_dir(),
  976. ok = error_logger:tty(true)
  977. end, [
  978. fun({Log, Debug, Events}) ->
  979. {"a trace using file backend set up in configuration should work",
  980. fun() ->
  981. eRum:error("trace test error message"),
  982. eRum:info("info message"),
  983. %% not eligible for trace
  984. eRum:log(error, self(), "Not trace test message"),
  985. {ok, BinInfo} = file:read_file(Events),
  986. ?assertMatch([_, _, "[error]", _, "trace test error message\n"],
  987. re:split(BinInfo, " ", [{return, list}, {parts, 5}])),
  988. ?assert(filelib:is_regular(Log)),
  989. {ok, BinInfo2} = file:read_file(Log),
  990. ?assertMatch(["trace test error message", "Not trace test message\n"],
  991. re:split(BinInfo2, "\n", [{return, list}, {parts, 2}])),
  992. ?assert(filelib:is_regular(Debug)),
  993. %% XXX Aughhhh, wish I could force this to flush somehow...
  994. % should take about 1 second, try for 3 ...
  995. ?assertEqual(2, count_lines_until(2, add_secs(os:timestamp(), 3), Debug, 0))
  996. end}
  997. end
  998. ]}.
  999. count_lines_until(Lines, Timeout, File, Last) ->
  1000. case timer:now_diff(Timeout, os:timestamp()) > 0 of
  1001. true ->
  1002. timer:sleep(333),
  1003. {ok, Bin} = file:read_file(File),
  1004. case erlang:length(re:split(Bin, "\n", [{return, list}, trim])) of
  1005. Count when Count < Lines ->
  1006. count_lines_until(Lines, Timeout, File, Count);
  1007. Count ->
  1008. Count
  1009. end;
  1010. _ ->
  1011. Last
  1012. end.
  1013. formatting_test_() ->
  1014. {foreach,
  1015. fun() ->
  1016. {ok, TestDir} = rumUtil:get_test_dir(),
  1017. Log1 = filename:join(TestDir, "test.log"),
  1018. Log2 = filename:join(TestDir, "test2.log"),
  1019. ?assertEqual(ok, rumUtil:safe_write_file(Log1, [])),
  1020. ?assertEqual(ok, rumUtil:safe_write_file(Log2, [])),
  1021. ok = error_logger:tty(false),
  1022. ok = rumUtil:safe_application_load(lager),
  1023. ok = application:set_env(lager, handlers, [{lager_test_backend, info}]),
  1024. ok = application:set_env(lager, errLoggerRedirect, false),
  1025. ok = eRum:start(),
  1026. %% same race condition issue
  1027. ok = timer:sleep(5),
  1028. {ok, Log1, Log2}
  1029. end,
  1030. fun({ok, _, _}) ->
  1031. ok = application:stop(lager),
  1032. ok = application:stop(goldrush),
  1033. ok = rumUtil:delete_test_dir(),
  1034. ok = error_logger:tty(true)
  1035. end, [
  1036. fun({ok, Log1, Log2}) ->
  1037. {"Should have two log files, the second prefixed with 2>",
  1038. fun() ->
  1039. gen_event:add_handler(rumEvent, lager_file_backend,
  1040. [{Log1, debug}, {lager_default_formatter, ["[", severity, "] ", message, "\n"]}]),
  1041. gen_event:add_handler(rumEvent, lager_file_backend,
  1042. [{Log2, debug}, {lager_default_formatter, ["2> [", severity, "] ", message, "\n"]}]),
  1043. eRum:log(error, self(), "Test message"),
  1044. ?assertMatch({ok, <<"[error] Test message\n">>}, file:read_file(Log1)),
  1045. ?assertMatch({ok, <<"2> [error] Test message\n">>}, file:read_file(Log2))
  1046. end}
  1047. end
  1048. ]}.
  1049. config_validation_test_() ->
  1050. [
  1051. {"missing file",
  1052. ?_assertEqual(false,
  1053. validate_logfile_proplist([{level, info}, {size, 10}]))
  1054. },
  1055. {"bad level",
  1056. ?_assertEqual(false,
  1057. validate_logfile_proplist([{file, "test.log"}, {level, blah}, {size, 10}]))
  1058. },
  1059. {"bad size",
  1060. ?_assertEqual(false,
  1061. validate_logfile_proplist([{file, "test.log"}, {size, infinity}]))
  1062. },
  1063. {"bad count",
  1064. ?_assertEqual(false,
  1065. validate_logfile_proplist([{file, "test.log"}, {count, infinity}]))
  1066. },
  1067. {"bad high water mark",
  1068. ?_assertEqual(false,
  1069. validate_logfile_proplist([{file, "test.log"}, {high_water_mark, infinity}]))
  1070. },
  1071. {"bad date",
  1072. ?_assertEqual(false,
  1073. validate_logfile_proplist([{file, "test.log"}, {date, "midnight"}]))
  1074. },
  1075. {"blank date is ok",
  1076. ?_assertMatch([_ | _],
  1077. validate_logfile_proplist([{file, "test.log"}, {date, ""}]))
  1078. },
  1079. {"bad sync_interval",
  1080. ?_assertEqual(false,
  1081. validate_logfile_proplist([{file, "test.log"}, {sync_interval, infinity}]))
  1082. },
  1083. {"bad sync_size",
  1084. ?_assertEqual(false,
  1085. validate_logfile_proplist([{file, "test.log"}, {sync_size, infinity}]))
  1086. },
  1087. {"bad check_interval",
  1088. ?_assertEqual(false,
  1089. validate_logfile_proplist([{file, "test.log"}, {check_interval, infinity}]))
  1090. },
  1091. {"bad sync_on level",
  1092. ?_assertEqual(false,
  1093. validate_logfile_proplist([{file, "test.log"}, {sync_on, infinity}]))
  1094. },
  1095. {"bad formatter module",
  1096. ?_assertEqual(false,
  1097. validate_logfile_proplist([{file, "test.log"}, {formatter, "io:format"}]))
  1098. },
  1099. {"bad formatter config",
  1100. ?_assertEqual(false,
  1101. validate_logfile_proplist([{file, "test.log"}, {formatter_config, blah}]))
  1102. },
  1103. {"unknown option",
  1104. ?_assertEqual(false,
  1105. validate_logfile_proplist([{file, "test.log"}, {rhubarb, spicy}]))
  1106. }
  1107. ].
  1108. -endif.