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 年前
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:expand_path(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:createLogfile(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:rotateLogfile(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:ensureLogfile(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:rotateLogfile(Name, Count) of
  217. ok ->
  218. %% go around the loop again, we'll do another rotation check and hit the next clause of ensureLogfile
  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:isFileChanged(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:openLogfile(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:openLogfile(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, log_root, 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, log_root),
  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.