├── .gitignore ├── LICENSE ├── README.md ├── config └── sys.config ├── rebar.config ├── rebar.lock ├── src ├── flatlog.app.src └── flatlog.erl └── test ├── flatlog_SUITE.erl ├── prop_flatlog.erl ├── proper-regressions.consult └── silent_logger.erl /.gitignore: -------------------------------------------------------------------------------- 1 | .rebar3 2 | _* 3 | .eunit 4 | *.o 5 | *.beam 6 | *.plt 7 | *.swp 8 | *.swo 9 | .erlang.cookie 10 | ebin 11 | log 12 | erl_crash.dump 13 | .rebar 14 | logs 15 | _build 16 | .idea 17 | *.iml 18 | rebar3.crashdump 19 | *~ 20 | doc/ 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | Copyright 2018, Fred Hebert . 179 | 180 | Licensed under the Apache License, Version 2.0 (the "License"); 181 | you may not use this file except in compliance with the License. 182 | You may obtain a copy of the License at 183 | 184 | http://www.apache.org/licenses/LICENSE-2.0 185 | 186 | Unless required by applicable law or agreed to in writing, software 187 | distributed under the License is distributed on an "AS IS" BASIS, 188 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 189 | See the License for the specific language governing permissions and 190 | limitations under the License. 191 | 192 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | flatlog 2 | ===== 3 | 4 | A custom formatter for the logger application that turns maps into single line 5 | text logs. 6 | 7 | Why? 8 | ---- 9 | 10 | Structured logging is a better approach to logging in general. Fields are more 11 | clearly defined, tool-assisted parsing and consuming of logs is simpler, and 12 | the structure is amenable to good filtering in global or handler filters for 13 | the Erlang `logger` library. 14 | 15 | You could, for example, emit all your logs as structured logs (just maps), and 16 | set up multiple handlers for them: 17 | 18 | - an audit handler, for all critical issues and configuration changes, which 19 | get stored on disk and remotely for long periods of time 20 | - an info log for users, which gets a shorter term durability 21 | - an error log for support tickets, which may instead have a targeted retention 22 | for a few weeks 23 | - a special handler that parses the structured logs and forwards them to a 24 | distributed tracing framework such as opencensus 25 | - extract or hide metrics from logs if you integrate with such a system, and do 26 | it cheaply by just nesting (or removing) a metrics map in the overall report. 27 | 28 | This can be done transparently and after the fact, without major structural 29 | impact to the call site. It lets you far more easily decouple log generation 30 | from its consumption at no heavy cost. 31 | 32 | This formatter focuses on providing a text-based single-line format for 33 | structured logs, which can be human-readable, while being useful to people who 34 | use grep or awk to process logs, or want to forward them to a consumer like 35 | syslogd. 36 | 37 | Usage 38 | ----- 39 | 40 | It is recommended that if you are providing a library, you do not add this 41 | project as a dependency. A code formatter of this kind should be added to a 42 | project in its release repository as a top-level final presentational concern. 43 | 44 | Once the project is added, replace the formatter of the default handler (or add 45 | a custom handler) for structured logging to your `sys.config` file: 46 | 47 | ```erlang 48 | [ 49 | {kernel, [ 50 | {logger, [ 51 | {handler, default, logger_std_h, 52 | #{formatter => {flatlog, #{ 53 | map_depth => 3, 54 | term_depth => 50 55 | }}} 56 | } 57 | ]}, 58 | {logger_level, info} 59 | ]} 60 | ]. 61 | ``` 62 | 63 | The logging output will then be supported. Calling the logger like: 64 | 65 | ```erlang 66 | ?LOG_ERROR( 67 | #{type => event, in => config, user => #{name => <<"bobby">>, id => 12345}, 68 | action => change_password, result => error, details => {entropy, too_low}, 69 | txt => <<"user password not strong enough">>} 70 | ) 71 | ``` 72 | 73 | Will produce a single log line like: 74 | 75 | ```text 76 | when=2018-11-15T18:16:03.411822+00:00 level=error pid=<0.134.0> 77 | at=config:update/3:450 user_name=bobby user_id=12345 type=event 78 | txt="user password not strong enough" result=error in=config 79 | details={entropy,too_low} action=change_password 80 | ``` 81 | 82 | Do note that the `user` map gets flattened such that `#{user => #{name => 83 | bobby}}` gets turned into `user_name=bobby`, ensuring that various subfields in 84 | distinct maps will not clash. 85 | 86 | The default template supplied with the library also includes optional fields for 87 | identifiers as used in distributed tracing framework which can be set in the metadata 88 | for the logger framework, either explicitly or as a process state. The fields are: 89 | 90 | - `id` for individual request identifiers 91 | - `parent_id` for the event or command that initially caused the current 92 | logging event to happen 93 | - `correlation_id` for groupings of related events 94 | 95 | Logs that are not reports (maps) are going to be formatted and handled such 96 | that they can be put inside a structured log. For example: 97 | 98 | ```erlang 99 | ?LOG_INFO("hello ~s", ["world"]) 100 | ``` 101 | 102 | Will result in: 103 | 104 | ```text 105 | when=2018-11-15T18:16:03.411822+00:00 level=info pid=<0.134.0> 106 | at=some:code/0:15 unstructured_log="hello world" 107 | ``` 108 | 109 | Do note that if you are building a release, you will need to manually add 110 | the `flatlog` dependency to your `relx` configuration, since it is 111 | technically not a direct dependency of any application in your system. 112 | 113 | Test 114 | ---- 115 | 116 | ```sh 117 | rebar3 check 118 | ``` 119 | 120 | Features 121 | -------- 122 | 123 | - Printing rules similar to the default Erlang logger formatter, but extended 124 | for binary values that can be represented as text. I.e. rather than 125 | `<<"hello">>`, the value `hello` will be output. A non-representable value 126 | will revert to `<<...>>` 127 | - Linebreaks are escaped to ensure all logs are always on one line, and strings 128 | that contain spaces or equal signs (` ` and `=`) are quoted such that 129 | `"key=name"="hello world"` to be clear. 130 | - Term depth applies on a per-term basis before a data structure is elided with `...` 131 | - Map depth is controllable independently to deal with recursion vs. complexity 132 | of terms 133 | - Colored output can be enabled with `colored => true`. One can color certain 134 | parts of the output using `colored_start` and `colored_end` in `template`. 135 | Per-level colors can be configured with `colored_{log level}`. 136 | 137 | Caveats 138 | ------- 139 | 140 | - No max line length is enforced at the formatter level, since the ordering of 141 | terms in maps is not defined and it could be risky to cut logs early. If a max 142 | line length is to be enforced, you should wrap this formatter into your own. 143 | - Escaping of keys does not carry well to nested maps. I.e. the map 144 | `#{a_b => #{"c d" => x}}` is not well supported: `a_b_"c d"=x` will be 145 | returned, which is nonsensical. For nested maps, you have the 146 | responsibility of ensuring composability. 147 | - The transformations to the log line format is not lossless; it is not 148 | serialization. 149 | 150 | Information is lost regarding whether the initial term was a binary, a 151 | string, or an atom. Similarly, naming a key `user_password` may make it seem 152 | like the `user` map leaks a `password` field, but it is an unrelated field 153 | that looks similar due to flattening. 154 | 155 | If this is unacceptable, you might want to choose another structured log 156 | format such as JSON. 157 | 158 | Roadmap 159 | ------- 160 | 161 | - integration tests 162 | - add example basic usage 163 | - add example usage with optional tracing for IDs 164 | - clean up test suites 165 | - incorporating lager's safer truncating logic (might be a breaking change 166 | prior to 1.0.0) 167 | 168 | Changelog 169 | --------- 170 | 171 | - 0.1.2: added a check for old `error_logger` calls (thanks @hommeabeil) 172 | - 0.1.1: added optionally colored logs (thanks @pfenoll) 173 | -------------------------------------------------------------------------------- /config/sys.config: -------------------------------------------------------------------------------- 1 | [ 2 | {kernel, [ 3 | {logger, [ 4 | {handler, default, logger_std_h, 5 | #{formatter => {flatlog, #{ 6 | map_depth => 3, 7 | term_depth => 50 8 | }}} 9 | } 10 | ]}, 11 | {logger_level, info} 12 | ]} 13 | ]. 14 | 15 | -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | {alias, [ 2 | {check, [xref, dialyzer, edoc, 3 | {proper, "--regressions"}, 4 | {proper, "-c"}, {ct, "-c"}, {cover, "-v --min_coverage=80"}]} 5 | ]}. 6 | 7 | {project_plugins, [rebar3_proper]}. 8 | 9 | {profiles, [ 10 | {test, [ 11 | {erl_opts, [nowarn_export_all]}, 12 | {deps, [proper, recon]} 13 | ]} 14 | ]}. 15 | 16 | {dialyzer, [ 17 | {warnings, [unknown]} 18 | ]}. 19 | 20 | {xref_checks,[ 21 | undefined_function_calls, undefined_functions, locals_not_used, 22 | deprecated_function_calls, deprecated_functions 23 | ]}. 24 | 25 | {proper_opts, [{constraint_tries, 150}]}. 26 | 27 | {shell, [{config, "config/sys.config"}, 28 | {apps, [sasl]}]}. 29 | -------------------------------------------------------------------------------- /rebar.lock: -------------------------------------------------------------------------------- 1 | []. 2 | -------------------------------------------------------------------------------- /src/flatlog.app.src: -------------------------------------------------------------------------------- 1 | {application, flatlog, 2 | [{description, "A custom formatter for the logger application that turns " 3 | "maps into single line text logs"}, 4 | {vsn, "0.1.2"}, 5 | {registered, []}, 6 | {applications, 7 | [kernel, 8 | stdlib 9 | ]}, 10 | {env,[]}, 11 | {modules, []}, 12 | 13 | {licenses, ["Apache 2.0"]}, 14 | {links, [{"Github", "https://github.com/ferd/flatlog/"}]} 15 | ]}. 16 | -------------------------------------------------------------------------------- /src/flatlog.erl: -------------------------------------------------------------------------------- 1 | %%% @doc 2 | %%% This is the main module that exposes custom formatting to the OTP 3 | %%% logger library (part of the `kernel' application since OTP-21). 4 | %%% 5 | %%% The module honors the standard configuration of the kernel's default 6 | %%% logger formatter regarding: max depth, templates. 7 | %%% @end 8 | -module(flatlog). 9 | 10 | %% API exports 11 | -export([format/2]). 12 | 13 | -ifdef(TEST). 14 | -export([format_msg/2, to_string/2]). 15 | -endif. 16 | 17 | -type template() :: [metakey() | {metakey(), template(), template()} | string()]. 18 | -type metakey() :: atom() | [atom()]. 19 | 20 | %%==================================================================== 21 | %% API functions 22 | %%==================================================================== 23 | -spec format(LogEvent, Config) -> unicode:chardata() when 24 | LogEvent :: logger:log_event(), 25 | Config :: logger:formatter_config(). 26 | format(Map = #{msg := {report, #{label := {error_logger, _}, format := Format, args := Terms}}}, UsrConfig) -> 27 | format(Map#{msg := {report, 28 | #{unstructured_log => 29 | unicode:characters_to_binary(io_lib:format(Format, Terms))}}}, 30 | UsrConfig); 31 | format(#{level:=Level, msg:={report, Msg}, meta:=Meta}, UsrConfig) when is_map(Msg) -> 32 | Config = apply_defaults(UsrConfig), 33 | NewMeta = maps:merge(Meta, #{level => Level 34 | ,colored_start => Level 35 | ,colored_end => "\e[0m" 36 | }), 37 | format_log(maps:get(template, Config), Config, Msg, NewMeta); 38 | format(Map = #{msg := {report, KeyVal}}, UsrConfig) when is_list(KeyVal) -> 39 | format(Map#{msg := {report, maps:from_list(KeyVal)}}, UsrConfig); 40 | format(Map = #{msg := {string, String}}, UsrConfig) -> 41 | format(Map#{msg := {report, 42 | #{unstructured_log => 43 | unicode:characters_to_binary(String)}}}, UsrConfig); 44 | format(Map = #{msg := {Format, Terms}}, UsrConfig) -> 45 | format(Map#{msg := {report, 46 | #{unstructured_log => 47 | unicode:characters_to_binary(io_lib:format(Format, Terms))}}}, 48 | UsrConfig). 49 | 50 | %%==================================================================== 51 | %% Internal functions 52 | %%==================================================================== 53 | apply_defaults(Map) -> 54 | maps:merge( 55 | #{term_depth => undefined, 56 | map_depth => -1, 57 | time_offset => 0, 58 | time_designator => $T, 59 | colored => false, 60 | colored_debug => "\e[0;38m", 61 | colored_info => "\e[1;37m", 62 | colored_notice => "\e[1;36m", 63 | colored_warning => "\e[1;33m", 64 | colored_error => "\e[1;31m", 65 | colored_critical => "\e[1;35m", 66 | colored_alert => "\e[1;44m", 67 | colored_emergency => "\e[1;41m", 68 | template => [colored_start, "when=", time, " level=", level, 69 | {id, [" id=", id], ""}, {parent_id, [" parent_id=", parent_id], ""}, 70 | {correlation_id, [" correlation_id=", correlation_id], ""}, 71 | {pid, [" pid=", pid], ""}, " at=", mfa, ":", line, colored_end, " ", msg, "\n"] 72 | }, 73 | Map 74 | ). 75 | 76 | 77 | -spec format_log(template(), Config, Msg, Meta) -> unicode:chardata() when 78 | Config :: logger:formatter_config(), 79 | Msg :: Data, 80 | Meta :: Data, 81 | Data :: #{string() | binary() | atom() => term()}. 82 | format_log(Tpl, Config, Msg, Meta) -> format_log(Tpl, Config, Msg, Meta, []). 83 | 84 | format_log([], _Config, _Msg, _Meta, Acc) -> 85 | lists:reverse(Acc); 86 | format_log([msg | Rest], Config, Msg, Meta, Acc) -> 87 | format_log(Rest, Config, Msg, Meta, [format_msg(Msg, Config) | Acc]); 88 | format_log([Key | Rest], Config, Msg, Meta, Acc) when is_atom(Key) 89 | orelse is_atom(hd(Key)) -> % from OTP 90 | case maps:find(Key, Meta) of 91 | error -> 92 | format_log(Rest, Config, Msg, Meta, Acc); 93 | {ok, Val} -> 94 | format_log(Rest, Config, Msg, Meta, [format_val(Key, Val, Config) | Acc]) 95 | end; 96 | format_log([{Key, IfExists, Else} | Rest], Config, Msg, Meta, Acc) -> 97 | case maps:find(Key, Meta) of 98 | error -> 99 | format_log(Rest, Config, Msg, Meta, [Else | Acc]); 100 | {ok, Val} -> 101 | format_log(Rest, Config, Msg, Meta, 102 | [format_log(IfExists, Config, Msg, #{Key => Val}, []) | Acc]) 103 | end; 104 | format_log([Term | Rest], Config, Msg, Meta, Acc) when is_list(Term) -> 105 | format_log(Rest, Config, Msg, Meta, [Term | Acc]). 106 | 107 | format_msg(Data, Config) -> format_msg("", Data, Config). 108 | 109 | format_msg(Parents, Data, Config=#{map_depth := 0}) when is_map(Data) -> 110 | to_string(truncate_key(Parents), Config)++"=... "; 111 | format_msg(Parents, Data, Config = #{map_depth := Depth}) when is_map(Data) -> 112 | maps:fold( 113 | fun(K, V, Acc) when is_map(V) -> 114 | [format_msg(Parents ++ to_string(K, Config) ++ "_", 115 | V, 116 | Config#{map_depth := Depth-1}) | Acc] 117 | ; (K, V, Acc) -> 118 | [Parents ++ to_string(K, Config), $=, 119 | to_string(V, Config), $\s | Acc] 120 | end, 121 | [], 122 | Data 123 | ). 124 | 125 | 126 | format_val(time, Time, Config) -> 127 | format_time(Time, Config); 128 | format_val(mfa, MFA, Config) -> 129 | escape(format_mfa(MFA, Config)); 130 | format_val(colored_end, _EOC, #{colored := false}) -> ""; 131 | format_val(colored_end, EOC, #{colored := true}) -> EOC; 132 | format_val(colored_start, _Level, #{colored := false}) -> ""; 133 | format_val(colored_start, debug, #{colored := true, colored_debug := BOC}) -> BOC; 134 | format_val(colored_start, info, #{colored := true, colored_info := BOC}) -> BOC; 135 | format_val(colored_start, notice, #{colored := true, colored_notice := BOC}) -> BOC; 136 | format_val(colored_start, warning, #{colored := true, colored_warning := BOC}) -> BOC; 137 | format_val(colored_start, error, #{colored := true, colored_error := BOC}) -> BOC; 138 | format_val(colored_start, critical, #{colored := true, colored_critical := BOC}) -> BOC; 139 | format_val(colored_start, alert, #{colored := true, colored_alert := BOC}) -> BOC; 140 | format_val(colored_start, emergency, #{colored := true, colored_emergency := BOC}) -> BOC; 141 | format_val(_Key, Val, Config) -> 142 | to_string(Val, Config). 143 | 144 | 145 | format_time(N, #{time_offset := O, time_designator := D}) when is_integer(N) -> 146 | calendar:system_time_to_rfc3339(N, [{unit, microsecond}, 147 | {offset, O}, 148 | {time_designator, D}]). 149 | 150 | format_mfa({M, F, A}, _) when is_atom(M), is_atom(F), is_integer(A) -> 151 | [atom_to_list(M), $:, atom_to_list(F), $/, integer_to_list(A)]; 152 | format_mfa({M, F, A}, Config) when is_atom(M), is_atom(F), is_list(A) -> 153 | %% arguments are passed as a literal list ({mod, fun, [a, b, c]}) 154 | format_mfa({M, F, length(A)}, Config); 155 | format_mfa(MFAStr, Config) -> % passing in a pre-formatted string value 156 | to_string(MFAStr,Config). 157 | 158 | to_string(X, _) when is_atom(X) -> 159 | escape(atom_to_list(X)); 160 | to_string(X, _) when is_integer(X) -> 161 | integer_to_list(X); 162 | to_string(X, _) when is_pid(X) -> 163 | pid_to_list(X); 164 | to_string(X, _) when is_reference(X) -> 165 | ref_to_list(X); 166 | to_string(X, C) when is_binary(X) -> 167 | case unicode:characters_to_list(X) of 168 | {_, _, _} -> % error or incomplete 169 | escape(format_str(C, X)); 170 | List -> 171 | case io_lib:printable_list(List) of 172 | true -> escape(List); 173 | _ -> escape(format_str(C, X)) 174 | end 175 | end; 176 | to_string(X, C) when is_list(X) -> 177 | case io_lib:printable_list(X) of 178 | true -> escape(X); 179 | _ -> escape(format_str(C, X)) 180 | end; 181 | to_string(X, C) -> 182 | escape(format_str(C, X)). 183 | 184 | format_str(#{term_depth := undefined}, T) -> 185 | io_lib:format("~0tp", [T]); 186 | format_str(#{term_depth := D}, T) -> 187 | io_lib:format("~0tP", [T, D]). 188 | 189 | escape(Str) -> 190 | case needs_escape(Str) of 191 | false -> 192 | case needs_quoting(Str) of 193 | true -> [$", Str, $"]; 194 | false -> Str 195 | end; 196 | true -> 197 | [$", do_escape(Str), $"] 198 | end. 199 | 200 | needs_quoting(Str) -> 201 | string:find(Str, " ") =/= nomatch orelse 202 | string:find(Str, "=") =/= nomatch. 203 | 204 | needs_escape(Str) -> 205 | string:find(Str, "\"") =/= nomatch orelse 206 | string:find(Str, "\\") =/= nomatch orelse 207 | string:find(Str, "\n") =/= nomatch. 208 | 209 | do_escape([]) -> 210 | []; 211 | do_escape(Str) -> 212 | case string:next_grapheme(Str) of 213 | [$\n | Rest] -> [$\\, $\n | do_escape(Rest)]; 214 | ["\r\n" | Rest] -> [$\\, $\r, $\\, $\n | do_escape(Rest)]; 215 | [$" | Rest] -> [$\\, $" | do_escape(Rest)]; 216 | [$\\ | Rest] -> [$\\, $\\ | do_escape(Rest)]; 217 | [Grapheme | Rest] -> [Grapheme | do_escape(Rest)] 218 | end. 219 | 220 | truncate_key([]) -> []; 221 | truncate_key("_") -> ""; 222 | truncate_key([H|T]) -> [H | truncate_key(T)]. 223 | -------------------------------------------------------------------------------- /test/flatlog_SUITE.erl: -------------------------------------------------------------------------------- 1 | -module(flatlog_SUITE). 2 | -compile(export_all). 3 | -include_lib("common_test/include/ct.hrl"). 4 | -include_lib("stdlib/include/assert.hrl"). 5 | 6 | all() -> 7 | [term_depth, map_depth, unstructured, colored]. 8 | 9 | term_depth() -> 10 | [{docs, "Once a term is too deep, it gets continued with `...'"}]. 11 | term_depth(_) -> 12 | ?assertEqual( 13 | "\"[\\\"01234567890123456789\\\",abc,[d,e|...]]\"", 14 | lists:flatten(flatlog:to_string( 15 | ["01234567890123456789",abc,[d,e,[f,g,h]]] 16 | , #{term_depth => 6} 17 | )) 18 | ), 19 | ok. 20 | 21 | map_depth() -> 22 | [{docs, "A max number of nesting in maps can be provided"}]. 23 | map_depth(_) -> 24 | %% Use custom templates to drop metadata/templates 25 | Template = [msg], 26 | Map = #{a => #{b => #{c => #{d => x}}, 27 | f => g}, 28 | 1 => #{2 => #{3 => x}}}, 29 | ?assertEqual( 30 | "a_f=g a_b_c=... 1_2_3=x ", 31 | lists:flatten( 32 | flatlog:format(#{level => info, msg => {report, Map}, meta => #{}}, 33 | #{template => Template, 34 | map_depth => 3}) 35 | ) 36 | ), 37 | ?assertEqual( 38 | "a=... 1=... ", 39 | lists:flatten( 40 | flatlog:format(#{level => info, msg => {report, Map}, meta => #{}}, 41 | #{template => Template, 42 | map_depth => 1}) 43 | ) 44 | ), 45 | 46 | ok. 47 | 48 | unstructured() -> 49 | [{docs, "logs that aren't structured get passed through with a re-frame"}]. 50 | unstructured(_) -> 51 | ?assertEqual( 52 | "unstructured_log=abc ", 53 | lists:flatten( 54 | flatlog:format(#{level => info, msg => {string, "abc"}, meta => #{}}, 55 | #{template => [msg]}) 56 | ) 57 | ), 58 | ?assertEqual( 59 | "unstructured_log=abc ", 60 | lists:flatten( 61 | flatlog:format(#{level => info, msg => {string, [<<"abc">>]}, meta => #{}}, 62 | #{template => [msg]}) 63 | ) 64 | ), 65 | ?assertEqual( 66 | "unstructured_log=\"hello world\" ", 67 | lists:flatten( 68 | flatlog:format(#{level => info, msg => {"hello ~s", ["world"]}, meta => #{}}, 69 | #{template => [msg]}) 70 | ) 71 | ), 72 | ok. 73 | 74 | colored() -> 75 | [{docs, "colored output logs"}]. 76 | colored(_) -> 77 | ?assertEqual( 78 | "\e[1;37mwhen= level=info at=:\e[0m hi=there \n", 79 | lists:flatten( 80 | flatlog:format(#{level => info, msg => {report, #{hi => there}}, meta => #{}}, 81 | #{colored => true}) 82 | ) 83 | ), 84 | ?assertEqual( 85 | "\e[1;44mwhen= level=alert at=:\e[0m unstructured_log=abc \n", 86 | lists:flatten( 87 | flatlog:format(#{level => alert, msg => {string, "abc"}, meta => #{}}, 88 | #{colored => true}) 89 | ) 90 | ), 91 | ok. 92 | -------------------------------------------------------------------------------- /test/prop_flatlog.erl: -------------------------------------------------------------------------------- 1 | -module(prop_flatlog). 2 | -compile(export_all). 3 | -include_lib("proper/include/proper.hrl"). 4 | -include_lib("stdlib/include/assert.hrl"). 5 | 6 | prop_end_to_end(doc) -> 7 | "log message and formatting can take place over the logger API". 8 | prop_end_to_end() -> 9 | ?SETUP(fun setup/0, 10 | ?FORALL({Lvl, Msg, Meta}, {level(), log_map(), meta()}, 11 | begin 12 | logger:Lvl(Msg, Meta), 13 | true % this property fails on teardown when failing 14 | end)). 15 | 16 | prop_map_keys(doc) -> 17 | "all keys of a map can be found in the log line". 18 | prop_map_keys() -> 19 | ?FORALL({Event, Cfg}, {inner_call(), config()}, 20 | begin 21 | Line = flatlog:format(Event, Cfg), 22 | #{msg := {report, Msg}} = Event, 23 | {Map, Rest} = parse_kv(Line), 24 | Res = maps:filter( 25 | fun(K, _) -> 26 | SK = format(K), 27 | maps:find(SK, Map) =:= error 28 | end, 29 | Msg 30 | ), 31 | case map_size(Res) == 0 of 32 | true -> Rest == []; 33 | false -> 34 | io:format("Generated log: ~ts => ~p~n", [Line, Res]), 35 | false 36 | end 37 | end). 38 | 39 | prop_nested_map_keys(doc) -> 40 | "all keys of a nested map can be found in the log line with the proper " 41 | "key prefixes". 42 | prop_nested_map_keys() -> 43 | ?FORALL({Event, Cfg}, {inner_call_nested(), config()}, 44 | begin 45 | Line = flatlog:format(Event, Cfg), 46 | #{msg := {report, Msg}} = Event, 47 | Map = parse_nested_map(Line), 48 | Keys = flattened_keys(Msg), 49 | Res = lists:filter(fun(K) -> 50 | SK = [format(KPart) || KPart <- K], 51 | recursive_lookup(SK, Map) =:= error 52 | end, 53 | Keys 54 | ), 55 | case Res of 56 | [] -> 57 | true; 58 | _ -> 59 | io:format("Msg ~p~n Generated log: ~ts => ~p~n", 60 | [Msg, Line, Res]), 61 | false 62 | end 63 | end). 64 | 65 | prop_meta(doc) -> 66 | "The metadata fields are rendered fine without interference from the data". 67 | 68 | prop_meta() -> 69 | ?FORALL({Lvl, Msg, Meta, Cfg}, {level(), meta(), meta(), config()}, 70 | begin 71 | Event = #{level => Lvl, msg => {report, Msg}, meta => Meta}, 72 | Line = flatlog:format(Event, Cfg), 73 | {Map, _} = parse_kv(Line), 74 | ExpectedLevel = format(Lvl), 75 | ExpectedTime = calendar:system_time_to_rfc3339( 76 | maps:get(time, Meta), 77 | [{unit, microsecond}, {offset, 0}, 78 | {time_designator, $T}] 79 | ), 80 | %% pid is a dupe value between the default template and the 81 | %% submitted message; by parsing order, the Msg pid is last 82 | %% and should be expected in the parsed result. 83 | ExpectedPid = format(maps:get(pid, Msg)), 84 | case Map of 85 | #{"pid" := ExpectedPid, 86 | "level" := ExpectedLevel, 87 | "when" := ExpectedTime} -> 88 | true; 89 | _ -> 90 | io:format("non-matching line~n~s parsed as ~p~n", [Line, Map]), 91 | io:format("expected ~p~n", 92 | [#{"pid" => ExpectedPid, 93 | "level" => ExpectedLevel, 94 | "when" => ExpectedTime}]), 95 | false 96 | end 97 | end). 98 | 99 | prop_string_printable(doc) -> 100 | "unescaped strings do not require quotation marks". 101 | prop_string_printable() -> 102 | ?FORALL(S, printable([{escape, false}, {quote, false}]), 103 | begin 104 | Formatted = flatlog:to_string(S, #{term_depth => undefined}), 105 | re:run(Formatted, "^<<.*>>$", [unicode, dotall]) == nomatch andalso 106 | re:run(Formatted, "^\".*\"$", [unicode, dotall]) == nomatch andalso 107 | re:run(Formatted, "[\n\r\n\\\\]", [unicode, dotall]) == nomatch 108 | end). 109 | 110 | prop_string_quotable(doc) -> 111 | "strings containing = or spaces are quotable and surrounded by quotes". 112 | prop_string_quotable() -> 113 | ?FORALL(S, printable([{escape, false}, {quote, true}]), 114 | begin 115 | Formatted = flatlog:to_string(S, #{term_depth => undefined}), 116 | re:run(Formatted, "^<<.*>>$", [unicode, dotall]) == nomatch andalso 117 | re:run(Formatted, "^\".*\"$", [unicode, dotall]) =/= nomatch andalso 118 | re:run(Formatted, "[\n\r\n\\\\]", [unicode, dotall]) == nomatch 119 | end). 120 | 121 | prop_string_escapable(doc) -> 122 | "strings that contain escapable characters are quoted; also ensure that " 123 | "binaries never contain <<>> as long as they're printable". 124 | prop_string_escapable() -> 125 | ?FORALL(S, printable([{escape, true}]), 126 | begin 127 | Formatted = flatlog:to_string(S, #{term_depth => undefined}), 128 | (re:run(Formatted, "^<<.*>>$", [unicode, dotall]) =/= nomatch orelse 129 | re:run(Formatted, "^\".*\"$", [unicode, dotall]) =/= nomatch) andalso 130 | re:run(Formatted, "[\n\r\n\\\\]", [unicode, dotall]) =/= nomatch andalso 131 | re:run(Formatted, "^<<\".*\">>$", [unicode, dotall]) == nomatch 132 | end). 133 | 134 | prop_string_unescapable(doc) -> 135 | "unescapable strings or binaries revert to their byte-level output (as in " 136 | "printing with ~w)". 137 | prop_string_unescapable() -> 138 | ?FORALL(S, unprintable(), 139 | begin 140 | Formatted = flatlog:to_string(S, #{term_depth => undefined}), 141 | re:run(Formatted, "^\"?\\[(.+,?)*\\]\"?$") =/= nomatch orelse 142 | re:run(Formatted, "^<<([0-9:]+,?)+>>$") =/= nomatch 143 | end). 144 | 145 | prop_empty_keys(doc) -> 146 | "empty keys are supported and show up as literally nothing". 147 | prop_empty_keys() -> 148 | ?FORALL({Lvl, Msg, Meta, Cfg, K, V}, 149 | {level(), meta(), meta(), config(), 150 | oneof([<<>>, "", '']), printable([])}, 151 | begin 152 | Event = #{level => Lvl, 153 | msg => {report, Msg#{K => V}}, 154 | meta => Meta}, 155 | Line = flatlog:format(Event, Cfg), 156 | string:find(Line, [" =",format(V)]) =/= nomatch 157 | end). 158 | 159 | prop_empty_vals(doc) -> 160 | "empty values are supported and show up as literally nothing". 161 | prop_empty_vals() -> 162 | ?FORALL({Lvl, Msg, Meta, Cfg, K, V}, 163 | {level(), meta(), meta(), config(), 164 | printable([]), oneof([<<>>, "", ''])}, 165 | begin 166 | Event = #{level => Lvl, 167 | msg => {report, Msg#{K => V}}, 168 | meta => Meta}, 169 | Line = flatlog:format(Event, Cfg), 170 | string:find(Line, [format(K),"= "]) =/= nomatch 171 | end). 172 | 173 | %%%%%%%%%%%%%%% 174 | %%% HELPERS %%% 175 | %%%%%%%%%%%%%%% 176 | setup() -> 177 | Handler = #{ 178 | config => #{}, 179 | level => all, 180 | filter_default => log, 181 | formatter => {flatlog, #{}} 182 | }, 183 | logger:add_handler_filter( 184 | default, 185 | test_run, 186 | {fun(Event = #{meta := M}, _) -> 187 | case maps:get(test_meta, M, false) of 188 | true -> stop; 189 | false -> Event 190 | end 191 | end, ok} 192 | ), 193 | logger:add_handler(structured, silent_logger, Handler), 194 | fun() -> 195 | logger:remove_handler(structured), 196 | logger:remove_handler_filter(default, test_run) 197 | end. 198 | 199 | parse_nested_map(Str) -> 200 | {Map, _} = parse_kv(Str), 201 | %% Break up the map into sets of nested key vals... 202 | %% #{a_b_c => x, a_b_d => x, a_c => y} 203 | %% yields 204 | %% #{[a,b,c] => x, [a,b,d] => x, [a,c] => y} 205 | Split = [{string:split(K, "_", all), V} 206 | || {K, V} <- maps:to_list(Map)], 207 | %% Merge maps according to prefixes: 208 | %% [{a => #{b => #{c => x, d => x}, c => y}}] 209 | merge_down(lists:sort(Split)). 210 | 211 | merge_down(List) -> merge_down(List, #{}). 212 | 213 | merge_down([], Map) -> Map; 214 | merge_down([{K, V} | Rest], Map) -> 215 | merge_down(Rest, insert_into(K, V, Map)). 216 | 217 | insert_into([K], V, Map) -> 218 | Map#{K => V}; 219 | insert_into([H|T], V, Map) -> 220 | SubMap = maps:get(H, Map, #{}), 221 | Map#{H => insert_into(T, V, SubMap)}. 222 | 223 | parse_kv(Str) -> parse_k(Str, #{}). 224 | 225 | parse_k(Str0, Map) -> 226 | Str = string:trim(Str0, leading, " "), 227 | case string:next_grapheme(Str) of 228 | [$\n | Rest] -> {Map, Rest}; 229 | [$" | Rest] -> parse_quoted_k(Rest, "", Map); 230 | _ -> parse_k(Str, "", Map) 231 | end. 232 | 233 | parse_k(Str, Acc, Map) -> 234 | case string:next_grapheme(Str) of 235 | [$= | Rest] -> parse_v(Rest, lists:reverse(Acc), Map); 236 | [G | Rest] -> parse_k(Rest, [G|Acc], Map) 237 | end. 238 | 239 | parse_quoted_k(Str, Acc, Map) -> 240 | case string:next_grapheme(Str) of 241 | [$" | Rest] -> 242 | parse_k(Rest, Acc, Map); 243 | [$\\ | Next] -> 244 | [G | Rest] = string:next_grapheme(Next), 245 | parse_quoted_k(Rest, [G|Acc], Map); 246 | [G | Rest] -> 247 | parse_quoted_k(Rest, [G|Acc], Map) 248 | end. 249 | 250 | parse_v(Str, Key, Map) -> 251 | case string:next_grapheme(Str) of 252 | [$" | Rest] -> parse_quoted_v(Rest, Key, "", Map); 253 | _ -> parse_v(Str, Key, "", Map) 254 | end. 255 | 256 | parse_v(Str, Key, Acc, Map) -> 257 | case string:next_grapheme(Str) of 258 | [$\n | Rest] -> {Map#{Key => lists:reverse(Acc)}, Rest}; 259 | [$\s | _] -> parse_k(Str, Map#{Key => lists:reverse(Acc)}); 260 | [G | Rest] -> parse_v(Rest, Key, [G|Acc], Map) 261 | end. 262 | 263 | parse_quoted_v(Str, Key, Acc, Map) -> 264 | case string:next_grapheme(Str) of 265 | [$" | Rest] -> 266 | parse_v(Rest, Key, Acc, Map); 267 | [$\\ | Next] -> 268 | [G | Rest] = string:next_grapheme(Next), 269 | parse_quoted_v(Rest, Key, [G|Acc], Map); 270 | [G | Rest] -> 271 | parse_quoted_v(Rest, Key, [G|Acc], Map) 272 | end. 273 | 274 | flattened_keys(Map) -> flattened_keys(Map, []). 275 | 276 | flattened_keys(Map, Parents) -> 277 | maps:fold(fun(K, V, Acc) when is_map(V) -> 278 | flattened_keys(V, [K|Parents]) ++ Acc 279 | ; (K, _, Acc) -> 280 | [lists:reverse(Parents, [K]) | Acc] 281 | end, [], Map). 282 | 283 | recursive_lookup([K], Map) -> maps:find(K, Map); 284 | recursive_lookup([H|T], Map) -> 285 | case maps:find(H, Map) of 286 | {ok, NewMap} -> recursive_lookup(T, NewMap); 287 | error -> error 288 | end. 289 | 290 | format(K) when is_tuple(K) -> 291 | lists:flatten(io_lib:format("~0tp", [K])); 292 | format(K) when is_integer(K) -> 293 | integer_to_list(K); 294 | format(K) when is_float(K) -> 295 | lists:flatten(io_lib:format("~0tp", [K])); 296 | format(K) -> 297 | lists:flatten(io_lib:format("~ts", [K])). 298 | 299 | 300 | %%%%%%%%%%%%%%%%%% 301 | %%% GENERATORS %%% 302 | %%%%%%%%%%%%%%%%%% 303 | inner_call() -> 304 | ?LET({Lvl, Msg, Meta}, {level(), printable_log_map(), meta()}, 305 | #{level => Lvl, msg => {report, Msg}, meta => Meta}). 306 | 307 | inner_call_nested() -> 308 | ?LET({Lvl, Msg, Meta}, {level(), printable_nested_log_map(), meta()}, 309 | #{level => Lvl, msg => {report, Msg}, meta => Meta}). 310 | 311 | level() -> 312 | oneof([emergency, alert, critical, error, warning, notice, info, debug]). 313 | 314 | meta() -> 315 | ?LET( 316 | {Mandatory, Optional}, 317 | {[{time, integer()}, 318 | {test_meta, true}, % used to filter output of test runs from default 319 | {mfa, oneof([{atom(), atom(), non_neg_integer()}, 320 | {atom(), atom(), list(term())}, 321 | string()])}, 322 | {pid, pid()}, 323 | {line, pos_integer()}], 324 | list(oneof([ 325 | {id, oneof([string(), binary()])}, 326 | {parent_id, oneof([string(), binary()])}, 327 | {correlation_id, oneof([string(), binary()])}, 328 | {"extra", term()} 329 | ]))}, 330 | maps:merge(maps:from_list(Optional), maps:from_list(Mandatory)) 331 | ). 332 | 333 | log_map() -> 334 | map(oneof([string(), atom(), binary()]), term()). 335 | 336 | printable_log_map() -> 337 | ?LET(S, 338 | non_empty(list( 339 | oneof([choose($0,$9), choose($a, $z), choose($A, $Z), $_, $-, $\s]) 340 | )), 341 | map(oneof([S, list_to_atom(S), iolist_to_binary(S)]), 342 | term()) 343 | ). 344 | 345 | printable_nested_log_map() -> printable_nested_log_map(3). 346 | 347 | printable_nested_log_map(0) -> "cut"; 348 | printable_nested_log_map(N) -> 349 | frequency([ 350 | {5, ?LAZY( 351 | ?LET(L, non_empty(list({nesting_key(), 352 | ?SUCHTHAT(T, term(), not is_map(T))})), 353 | maps:from_list(L)) 354 | )}, 355 | {1, ?LAZY( 356 | ?LET(L, list({nesting_key(), printable_nested_log_map(N-1)}), 357 | maps:from_list(L)) 358 | )} 359 | ]). 360 | 361 | nesting_key() -> 362 | ?LET(S, 363 | non_empty(list( 364 | oneof([choose($0,$9), choose($a, $z), choose($A, $Z), $-]) 365 | )), 366 | oneof([S, list_to_atom(S), unicode:characters_to_binary(S)])). 367 | 368 | printable(Props) -> 369 | Escape = proplists:get_value(escape, Props, false), 370 | Quote = proplists:get_value(quote, Props, false), 371 | Printable = case io:printable_range() of 372 | latin1 -> 373 | ?SUCHTHAT(S, list(range(0,255)), io_lib:printable_list(S)); 374 | unicode -> 375 | ?SUCHTHAT(US, ?LET(S, utf8(), unicode:characters_to_list(S)), 376 | io_lib:printable_unicode_list(US)) 377 | end, 378 | String = case {Escape, Quote} of 379 | {true, _} -> 380 | ?SUCHTHAT(S, 381 | ?LET(S, non_empty(list(oneof([Printable, "\"", "\n", "\r\n", "\\"]))), 382 | lists:flatten(S)), 383 | %% Fun cases like "̀ are technically considered escapable 384 | %% by Erlang's own output system but not a proper unicode 385 | %% handling, and regex checks don't cope with that well. 386 | %re:run(S, "[\"\n\r\n\\\\]", [unicode]) =/= nomatch); 387 | length(string:split(S, "\"", all)) > 1 orelse 388 | length(string:split(S, "\n", all)) > 1 orelse 389 | length(string:split(S, "\r\n", all)) > 1 orelse 390 | length(string:split(S, "\\", all)) > 1); 391 | {false, true} -> 392 | ?SUCHTHAT( 393 | Str, 394 | ?LET(S, non_empty(list(oneof([" ", "=", Printable]))), 395 | [Char || Char <- lists:flatten(S), 396 | not lists:member(Char, [$", $\n, $\r, $\\])]), 397 | %% combining marks ([32,768]) contain a space that requires no escaping. 398 | %% the re functions, even with unicode awareness, do not 399 | %% understand that pattern. 400 | %re:run(Str, "[ =]", [unicode]) =/= nomatch 401 | length(string:split(Str, " ", all)) > 1 orelse 402 | length(string:split(Str, "=", all)) > 1 403 | ); 404 | {false, false} -> 405 | ?LET(S, Printable, 406 | [Char || Char <- S, 407 | not lists:member(Char, [$", $\s, $=, $\n, $\r, $\\])]) 408 | end, 409 | ?LET(S, String, 410 | oneof([S, unicode:characters_to_binary(S), list_to_atom(S)] 411 | ++ case Escape orelse Quote of 412 | false -> [number()]; 413 | true -> [] 414 | end)). 415 | 416 | unprintable() -> 417 | oneof([non_empty(list([atom(), float()])), 418 | ?SUCHTHAT(B, non_empty(bitstring()), 419 | bit_size(B) rem 8 =/= 0), 420 | ?SUCHTHAT(B, non_empty(binary()), 421 | not (io_lib:printable_list(binary_to_list(B)) 422 | orelse io_lib:printable_list(unicode:characters_to_list(B)))) 423 | ]). 424 | 425 | 426 | %% fake pid, otherwise we can't store counterexamples 427 | pid() -> 428 | ?LET(N, pos_integer(), "<0." ++ integer_to_list(N) ++ ".0>"). 429 | 430 | config() -> 431 | ?LET(List, 432 | [{unicode, boolean()}], 433 | maps:from_list(List)). 434 | -------------------------------------------------------------------------------- /test/proper-regressions.consult: -------------------------------------------------------------------------------- 1 | %% This one failed due to bad line-breaking escaping 2 | {prop_flatlog,prop_map_keys, 3 | [{#{level => emergency, 4 | meta => 5 | #{line => 1, 6 | mfa => {'','\n',0}, 7 | pid => "<0.7.0>",test_meta => true,time => 0}, 8 | msg => {report,#{}}}, 9 | #{}}]}. 10 | 11 | %% This one failed due to improper escaping detection of backslashes 12 | {prop_flatlog,prop_nested_map_keys, 13 | [{#{level => emergency, 14 | meta => 15 | #{line => 1, 16 | mfa => {'','',0}, 17 | pid => "<0.32.0>",test_meta => true,time => 0}, 18 | msg => {report,#{"0" => ' \\'}}}, 19 | #{}}]}. 20 | -------------------------------------------------------------------------------- /test/silent_logger.erl: -------------------------------------------------------------------------------- 1 | -module(silent_logger). 2 | -compile(export_all). 3 | 4 | adding_handler(Config = #{}) -> 5 | {ok, Config#{}}. 6 | 7 | removing_handler(_Config) -> 8 | ok. 9 | 10 | log(LogEvent, Config) -> 11 | _Bin = logger_h_common:log_to_binary(LogEvent, Config), 12 | ok. 13 | --------------------------------------------------------------------------------