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

553 line
19 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 年之前
  1. -module(rumFormatter).
  2. -include("eRum.hrl").
  3. -ifdef(TEST).
  4. -include_lib("eunit/include/eunit.hrl").
  5. -endif.
  6. %%
  7. %% Exported Functions
  8. %%
  9. -export([format/2, format/3]).
  10. %%
  11. %% API Functions
  12. %%
  13. %% @doc Provides a generic, default formatting for log messages using a semi-iolist as configuration. Any iolist allowed
  14. %% elements in the configuration are printed verbatim. Atoms in the configuration are treated as metadata properties
  15. %% and extracted from the log message. Optionally, a tuple of {atom(),semi-iolist()} can be used. The atom will look
  16. %% up the property, but if not found it will use the semi-iolist() instead. These fallbacks can be similarly nested
  17. %% or refer to other properties, if desired. You can also use a {atom, semi-iolist(), semi-iolist()} formatter, which
  18. %% acts like a ternary operator's true/false branches.
  19. %%
  20. %% The metadata properties date,time, message, severity, and sev will always exist.
  21. %% The properties pid, file, line, module, and function will always exist if the parser transform is used.
  22. %%
  23. %% Example:
  24. %%
  25. %% `["Foo"]' -> "Foo", regardless of message content.
  26. %%
  27. %% `[message]' -> The content of the logged message, alone.
  28. %%
  29. %% `[{pid,"Unknown Pid"}]' -> "?.?.?" if pid is in the metadata, "Unknown Pid" if not.
  30. %%
  31. %% `[{pid, ["My pid is ", pid], ["Unknown Pid"]}]' -> if pid is in the metada print "My pid is ?.?.?", otherwise print "Unknown Pid"
  32. %% @end
  33. -spec format(rumMsg:rumMsg(), list(), list()) -> any().
  34. format(Msg, [], Colors) ->
  35. format(Msg, [{eol, "\n"}], Colors);
  36. format(Msg, [{eol, EOL}], Colors) ->
  37. Config = case application:get_env(lager, metadata_whitelist) of
  38. undefined -> config(EOL, []);
  39. {ok, Whitelist} -> config(EOL, Whitelist)
  40. end,
  41. format(Msg, Config, Colors);
  42. format(Message, Config, Colors) ->
  43. [case V of
  44. color -> output_color(Message, Colors);
  45. _ -> output(V, Message)
  46. end || V <- Config].
  47. -spec format(rumMsg:rumMsg(), list()) -> any().
  48. format(Msg, Config) ->
  49. format(Msg, Config, []).
  50. -spec output(term(), rumMsg:rumMsg()) -> iolist().
  51. output(message, Msg) -> rumMsg:message(Msg);
  52. output(date, Msg) ->
  53. {D, _T} = rumMsg:datetime(Msg),
  54. D;
  55. output(time, Msg) ->
  56. {_D, T} = rumMsg:datetime(Msg),
  57. T;
  58. output(severity, Msg) ->
  59. atom_to_list(rumMsg:severity(Msg));
  60. output(severity_upper, Msg) ->
  61. uppercase_severity(rumMsg:severity(Msg));
  62. output(blank, _Msg) ->
  63. output({blank, " "}, _Msg);
  64. output(node, _Msg) ->
  65. output({node, atom_to_list(node())}, _Msg);
  66. output({blank, Fill}, _Msg) ->
  67. Fill;
  68. output(sev, Msg) ->
  69. %% Write brief acronym for the severity level (e.g. debug -> $D)
  70. [rumUtil:levelToChr(rumMsg:severity(Msg))];
  71. output(metadata, Msg) ->
  72. output({metadata, "=", " "}, Msg);
  73. output({metadata, IntSep, FieldSep}, Msg) ->
  74. MD = lists:keysort(1, rumMsg:metadata(Msg)),
  75. string:join([io_lib:format("~s~s~p", [K, IntSep, V]) || {K, V} <- MD], FieldSep);
  76. output({pterm, Key}, Msg) ->
  77. output({pterm, Key, ""}, Msg);
  78. output({pterm, Key, Default}, _Msg) ->
  79. make_printable(maybe_get_persistent_term(Key, Default));
  80. output(Prop, Msg) when is_atom(Prop) ->
  81. Metadata = rumMsg:metadata(Msg),
  82. make_printable(get_metadata(Prop, Metadata, <<"Undefined">>));
  83. output({Prop, Default}, Msg) when is_atom(Prop) ->
  84. Metadata = rumMsg:metadata(Msg),
  85. make_printable(get_metadata(Prop, Metadata, output(Default, Msg)));
  86. output({Prop, Present, Absent}, Msg) when is_atom(Prop) ->
  87. %% sort of like a poor man's ternary operator
  88. Metadata = rumMsg:metadata(Msg),
  89. case get_metadata(Prop, Metadata) of
  90. undefined ->
  91. [output(V, Msg) || V <- Absent];
  92. _ ->
  93. [output(V, Msg) || V <- Present]
  94. end;
  95. output({Prop, Present, Absent, Width}, Msg) when is_atom(Prop) ->
  96. %% sort of like a poor man's ternary operator
  97. Metadata = rumMsg:metadata(Msg),
  98. case get_metadata(Prop, Metadata) of
  99. undefined ->
  100. [output(V, Msg, Width) || V <- Absent];
  101. _ ->
  102. [output(V, Msg, Width) || V <- Present]
  103. end;
  104. output(Other, _) -> make_printable(Other).
  105. output(message, Msg, _Width) -> rumMsg:message(Msg);
  106. output(date, Msg, _Width) ->
  107. {D, _T} = rumMsg:datetime(Msg),
  108. D;
  109. output(time, Msg, _Width) ->
  110. {_D, T} = rumMsg:datetime(Msg),
  111. T;
  112. output(severity, Msg, Width) ->
  113. make_printable(atom_to_list(rumMsg:severity(Msg)), Width);
  114. output(sev, Msg, _Width) ->
  115. %% Write brief acronym for the severity level (e.g. debug -> $D)
  116. [rumUtil:levelToChr(rumMsg:severity(Msg))];
  117. output(node, Msg, _Width) ->
  118. output({node, atom_to_list(node())}, Msg, _Width);
  119. output(blank, _Msg, _Width) ->
  120. output({blank, " "}, _Msg, _Width);
  121. output({blank, Fill}, _Msg, _Width) ->
  122. Fill;
  123. output(metadata, Msg, _Width) ->
  124. output({metadata, "=", " "}, Msg, _Width);
  125. output({metadata, IntSep, FieldSep}, Msg, _Width) ->
  126. MD = lists:keysort(1, rumMsg:metadata(Msg)),
  127. [string:join([io_lib:format("~s~s~p", [K, IntSep, V]) || {K, V} <- MD], FieldSep)];
  128. output({pterm, Key}, Msg, Width) ->
  129. output({pterm, Key, ""}, Msg, Width);
  130. output({pterm, Key, Default}, _Msg, _Width) ->
  131. make_printable(maybe_get_persistent_term(Key, Default));
  132. output(Prop, Msg, Width) when is_atom(Prop) ->
  133. Metadata = rumMsg:metadata(Msg),
  134. make_printable(get_metadata(Prop, Metadata, <<"Undefined">>), Width);
  135. output({Prop, Default}, Msg, Width) when is_atom(Prop) ->
  136. Metadata = rumMsg:metadata(Msg),
  137. make_printable(get_metadata(Prop, Metadata, output(Default, Msg)), Width);
  138. output(Other, _, Width) -> make_printable(Other, Width).
  139. output_color(_Msg, []) -> [];
  140. output_color(Msg, Colors) ->
  141. Level = rumMsg:severity(Msg),
  142. case lists:keyfind(Level, 1, Colors) of
  143. {_, Color} -> Color;
  144. _ -> []
  145. end.
  146. -spec make_printable(any()) -> iolist().
  147. make_printable(A) when is_atom(A) -> atom_to_list(A);
  148. make_printable(P) when is_pid(P) -> pid_to_list(P);
  149. make_printable(L) when is_list(L) orelse is_binary(L) -> L;
  150. make_printable(Other) -> io_lib:format("~p", [Other]).
  151. make_printable(A, W) when is_integer(W) -> string:left(make_printable(A), W);
  152. make_printable(A, {Align, W}) when is_integer(W) ->
  153. case Align of
  154. left ->
  155. string:left(make_printable(A), W);
  156. centre ->
  157. string:centre(make_printable(A), W);
  158. right ->
  159. string:right(make_printable(A), W);
  160. _ ->
  161. string:left(make_printable(A), W)
  162. end;
  163. make_printable(A, _W) -> make_printable(A).
  164. %% persistent term was introduced in OTP 21.2, so
  165. %% if we're running on an older OTP, just return the
  166. %% default value.
  167. -ifdef(OTP_RELEASE).
  168. maybe_get_persistent_term(Key, Default) ->
  169. try
  170. persistent_term:get(Key, Default)
  171. catch
  172. _:undef -> Default
  173. end.
  174. -else.
  175. maybe_get_persistent_term(_Key, Default) -> Default.
  176. -endif.
  177. run_function(Function, Default) ->
  178. try Function() of
  179. Result ->
  180. Result
  181. catch
  182. _:_ ->
  183. Default
  184. end.
  185. get_metadata(Key, Metadata) ->
  186. get_metadata(Key, Metadata, undefined).
  187. get_metadata(Key, Metadata, Default) ->
  188. case lists:keyfind(Key, 1, Metadata) of
  189. false ->
  190. Default;
  191. {Key, Value} when is_function(Value) ->
  192. run_function(Value, Default);
  193. {Key, Value} ->
  194. Value
  195. end.
  196. config(EOL, []) ->
  197. [
  198. date, " ", time, " ", color, "[", severity, "] ",
  199. {pid, ""},
  200. {module, [
  201. {pid, ["@"], ""},
  202. module,
  203. {function, [":", function], ""},
  204. {line, [":", line], ""}], ""},
  205. " ", message, EOL
  206. ];
  207. config(EOL, MetaWhitelist) ->
  208. [
  209. date, " ", time, " ", color, "[", severity, "] ",
  210. {pid, ""},
  211. {module, [
  212. {pid, ["@"], ""},
  213. module,
  214. {function, [":", function], ""},
  215. {line, [":", line], ""}], ""},
  216. " "
  217. ] ++
  218. [{M, [atom_to_list(M), "=", M, " "], ""} || M <- MetaWhitelist] ++
  219. [message, EOL].
  220. uppercase_severity(debug) -> "DEBUG";
  221. uppercase_severity(info) -> "INFO";
  222. uppercase_severity(notice) -> "NOTICE";
  223. uppercase_severity(warning) -> "WARNING";
  224. uppercase_severity(error) -> "ERROR";
  225. uppercase_severity(critical) -> "CRITICAL";
  226. uppercase_severity(alert) -> "ALERT";
  227. uppercase_severity(emergency) -> "EMERGENCY".
  228. -ifdef(TEST).
  229. date_time_now() ->
  230. Now = os:timestamp(),
  231. {Date, Time} = rumUtil:format_time(rumUtil:maybe_utc(rumUtil:localtime_ms(Now))),
  232. {Date, Time, Now}.
  233. basic_test_() ->
  234. {Date, Time, Now} = date_time_now(),
  235. [{"Default formatting test",
  236. ?_assertEqual(iolist_to_binary([Date, " ", Time, " [error] ", pid_to_list(self()), " Message\n"]),
  237. iolist_to_binary(format(rumMsg:new("Message",
  238. Now,
  239. error,
  240. [{pid, self()}],
  241. []),
  242. [])))
  243. },
  244. {"Basic Formatting",
  245. ?_assertEqual(<<"Simplist Format">>,
  246. iolist_to_binary(format(rumMsg:new("Message",
  247. Now,
  248. error,
  249. [{pid, self()}],
  250. []),
  251. ["Simplist Format"])))
  252. },
  253. {"Default equivalent formatting test",
  254. ?_assertEqual(iolist_to_binary([Date, " ", Time, " [error] ", pid_to_list(self()), " Message\n"]),
  255. iolist_to_binary(format(rumMsg:new("Message",
  256. Now,
  257. error,
  258. [{pid, self()}],
  259. []),
  260. [date, " ", time, " [", severity, "] ", pid, " ", message, "\n"]
  261. )))
  262. },
  263. {"Non existent metadata can default to string",
  264. ?_assertEqual(iolist_to_binary([Date, " ", Time, " [error] Fallback Message\n"]),
  265. iolist_to_binary(format(rumMsg:new("Message",
  266. Now,
  267. error,
  268. [{pid, self()}],
  269. []),
  270. [date, " ", time, " [", severity, "] ", {does_not_exist, "Fallback"}, " ", message, "\n"]
  271. )))
  272. },
  273. {"Non existent metadata can default to other metadata",
  274. ?_assertEqual(iolist_to_binary([Date, " ", Time, " [error] Fallback Message\n"]),
  275. iolist_to_binary(format(rumMsg:new("Message",
  276. Now,
  277. error,
  278. [{pid, "Fallback"}],
  279. []),
  280. [date, " ", time, " [", severity, "] ", {does_not_exist, pid}, " ", message, "\n"]
  281. )))
  282. },
  283. {"Non existent metadata can default to a string2",
  284. ?_assertEqual(iolist_to_binary(["Unknown Pid"]),
  285. iolist_to_binary(format(rumMsg:new("Message",
  286. Now,
  287. error,
  288. [],
  289. []),
  290. [{pid, ["My pid is ", pid], ["Unknown Pid"]}]
  291. )))
  292. },
  293. {"Metadata can have extra formatting",
  294. ?_assertEqual(iolist_to_binary(["My pid is hello"]),
  295. iolist_to_binary(format(rumMsg:new("Message",
  296. Now,
  297. error,
  298. [{pid, hello}],
  299. []),
  300. [{pid, ["My pid is ", pid], ["Unknown Pid"]}]
  301. )))
  302. },
  303. {"Metadata can have extra formatting1",
  304. ?_assertEqual(iolist_to_binary(["servername"]),
  305. iolist_to_binary(format(rumMsg:new("Message",
  306. Now,
  307. error,
  308. [{pid, hello}, {server, servername}],
  309. []),
  310. [{server, {pid, ["(", pid, ")"], ["(Unknown Server)"]}}]
  311. )))
  312. },
  313. {"Metadata can have extra formatting2",
  314. ?_assertEqual(iolist_to_binary(["(hello)"]),
  315. iolist_to_binary(format(rumMsg:new("Message",
  316. Now,
  317. error,
  318. [{pid, hello}],
  319. []),
  320. [{server, {pid, ["(", pid, ")"], ["(Unknown Server)"]}}]
  321. )))
  322. },
  323. {"Metadata can have extra formatting3",
  324. ?_assertEqual(iolist_to_binary(["(Unknown Server)"]),
  325. iolist_to_binary(format(rumMsg:new("Message",
  326. Now,
  327. error,
  328. [],
  329. []),
  330. [{server, {pid, ["(", pid, ")"], ["(Unknown Server)"]}}]
  331. )))
  332. },
  333. {"Metadata can be printed in its enterity",
  334. ?_assertEqual(iolist_to_binary(["bar=2 baz=3 foo=1"]),
  335. iolist_to_binary(format(rumMsg:new("Message",
  336. Now,
  337. error,
  338. [{foo, 1}, {bar, 2}, {baz, 3}],
  339. []),
  340. [metadata]
  341. )))
  342. },
  343. {"Metadata can be printed in its enterity with custom seperators",
  344. ?_assertEqual(iolist_to_binary(["bar->2, baz->3, foo->1"]),
  345. iolist_to_binary(format(rumMsg:new("Message",
  346. Now,
  347. error,
  348. [{foo, 1}, {bar, 2}, {baz, 3}],
  349. []),
  350. [{metadata, "->", ", "}]
  351. )))
  352. },
  353. {"Metadata can have extra formatting with width 1",
  354. ?_assertEqual(iolist_to_binary(["(hello )(hello )(hello)(hello)(hello)"]),
  355. iolist_to_binary(format(rumMsg:new("Message",
  356. Now,
  357. error,
  358. [{pid, hello}],
  359. []),
  360. ["(", {pid, [pid], "", 10}, ")",
  361. "(", {pid, [pid], "", {bad_align, 10}}, ")",
  362. "(", {pid, [pid], "", bad10}, ")",
  363. "(", {pid, [pid], "", {right, bad20}}, ")",
  364. "(", {pid, [pid], "", {bad_align, bad20}}, ")"]
  365. )))
  366. },
  367. {"Metadata can have extra formatting with width 2",
  368. ?_assertEqual(iolist_to_binary(["(hello )"]),
  369. iolist_to_binary(format(rumMsg:new("Message",
  370. Now,
  371. error,
  372. [{pid, hello}],
  373. []),
  374. ["(", {pid, [pid], "", {left, 10}}, ")"]
  375. )))
  376. },
  377. {"Metadata can have extra formatting with width 3",
  378. ?_assertEqual(iolist_to_binary(["( hello)"]),
  379. iolist_to_binary(format(rumMsg:new("Message",
  380. Now,
  381. error,
  382. [{pid, hello}],
  383. []),
  384. ["(", {pid, [pid], "", {right, 10}}, ")"]
  385. )))
  386. },
  387. {"Metadata can have extra formatting with width 4",
  388. ?_assertEqual(iolist_to_binary(["( hello )"]),
  389. iolist_to_binary(format(rumMsg:new("Message",
  390. Now,
  391. error,
  392. [{pid, hello}],
  393. []),
  394. ["(", {pid, [pid], "", {centre, 10}}, ")"]
  395. )))
  396. },
  397. {"Metadata can have extra formatting with width 5",
  398. ?_assertEqual(iolist_to_binary(["error |hello ! ( hello )"]),
  399. iolist_to_binary(format(rumMsg:new("Message",
  400. Now,
  401. error,
  402. [{pid, hello}],
  403. []),
  404. [{x, "", [severity, {blank, "|"}, pid], 10}, "!", blank, "(", {pid, [pid], "", {centre, 10}}, ")"]
  405. )))
  406. },
  407. {"Metadata can have extra formatting with width 6",
  408. ?_assertEqual(iolist_to_binary([Time, Date, " bar=2 baz=3 foo=1 pid=hello EMessage"]),
  409. iolist_to_binary(format(rumMsg:new("Message",
  410. Now,
  411. error,
  412. [{pid, hello}, {foo, 1}, {bar, 2}, {baz, 3}],
  413. []),
  414. [{x, "", [time]}, {x, "", [date], 20}, blank, {x, "", [metadata], 30}, blank, {x, "", [sev], 10}, message, {message, message, "", {right, 20}}]
  415. )))
  416. },
  417. {"Uppercase Severity Formatting - DEBUG",
  418. ?_assertEqual(<<"DEBUG Simplist Format">>,
  419. iolist_to_binary(format(rumMsg:new("Message",
  420. Now,
  421. debug,
  422. [{pid, self()}],
  423. []),
  424. [severity_upper, " Simplist Format"])))
  425. },
  426. {"Uppercase Severity Formatting - INFO",
  427. ?_assertEqual(<<"INFO Simplist Format">>,
  428. iolist_to_binary(format(rumMsg:new("Message",
  429. Now,
  430. info,
  431. [{pid, self()}],
  432. []),
  433. [severity_upper, " Simplist Format"])))
  434. },
  435. {"Uppercase Severity Formatting - NOTICE",
  436. ?_assertEqual(<<"NOTICE Simplist Format">>,
  437. iolist_to_binary(format(rumMsg:new("Message",
  438. Now,
  439. notice,
  440. [{pid, self()}],
  441. []),
  442. [severity_upper, " Simplist Format"])))
  443. },
  444. {"Uppercase Severity Formatting - WARNING",
  445. ?_assertEqual(<<"WARNING Simplist Format">>,
  446. iolist_to_binary(format(rumMsg:new("Message",
  447. Now,
  448. warning,
  449. [{pid, self()}],
  450. []),
  451. [severity_upper, " Simplist Format"])))
  452. },
  453. {"Uppercase Severity Formatting - ERROR",
  454. ?_assertEqual(<<"ERROR Simplist Format">>,
  455. iolist_to_binary(format(rumMsg:new("Message",
  456. Now,
  457. error,
  458. [{pid, self()}],
  459. []),
  460. [severity_upper, " Simplist Format"])))
  461. },
  462. {"Uppercase Severity Formatting - CRITICAL",
  463. ?_assertEqual(<<"CRITICAL Simplist Format">>,
  464. iolist_to_binary(format(rumMsg:new("Message",
  465. Now,
  466. critical,
  467. [{pid, self()}],
  468. []),
  469. [severity_upper, " Simplist Format"])))
  470. },
  471. {"Uppercase Severity Formatting - ALERT",
  472. ?_assertEqual(<<"ALERT Simplist Format">>,
  473. iolist_to_binary(format(rumMsg:new("Message",
  474. Now,
  475. alert,
  476. [{pid, self()}],
  477. []),
  478. [severity_upper, " Simplist Format"])))
  479. },
  480. {"Uppercase Severity Formatting - EMERGENCY",
  481. ?_assertEqual(<<"EMERGENCY Simplist Format">>,
  482. iolist_to_binary(format(rumMsg:new("Message",
  483. Now,
  484. emergency,
  485. [{pid, self()}],
  486. []),
  487. [severity_upper, " Simplist Format"])))
  488. },
  489. {"pterm presence test",
  490. %% skip test on OTP < 21
  491. case list_to_integer(erlang:system_info(otp_release)) >= 21 of
  492. true ->
  493. ?_assertEqual(<<"Pterm is: something">>,
  494. begin
  495. persistent_term:put(thing, something),
  496. Ret = iolist_to_binary(format(rumMsg:new("Message",
  497. Now,
  498. emergency,
  499. [{pid, self()}],
  500. []),
  501. ["Pterm is: ", {pterm, thing}])),
  502. persistent_term:erase(thing),
  503. Ret
  504. end);
  505. false -> ?_assert(true)
  506. end
  507. },
  508. {"pterm absence test",
  509. ?_assertEqual(<<"Pterm is: nothing">>,
  510. iolist_to_binary(format(rumMsg:new("Message",
  511. Now,
  512. emergency,
  513. [{pid, self()}],
  514. []),
  515. ["Pterm is: ", {pterm, thing, "nothing"}])))
  516. },
  517. {"node formatting basic",
  518. begin
  519. [N, "foo"] = format(rumMsg:new("Message",
  520. Now,
  521. info,
  522. [{pid, self()}],
  523. []),
  524. [node, "foo"]),
  525. ?_assertNotMatch(nomatch, re:run(N, <<"@">>))
  526. end
  527. }
  528. ].
  529. -endif.