├── .github └── workflows │ └── build.yml ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── SECURITY.md ├── config └── sys.config ├── rebar.config ├── rebar.lock └── src ├── jsonformat.app.src └── jsonformat.erl /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Erlang CI 2 | 3 | on: [push] 4 | 5 | concurrency: 6 | group: ${{ github.ref }} 7 | cancel-in-progress: ${{ github.ref != format('refs/heads/{0}', github.event.repository.default_branch || 'master') }} 8 | 9 | jobs: 10 | build_and_test: 11 | runs-on: ubuntu-latest 12 | name: OTP ${{matrix.otp}} 13 | strategy: 14 | matrix: 15 | otp: ["24", "25", "26"] 16 | fail-fast: false 17 | steps: 18 | - uses: actions/checkout@v4 19 | - uses: erlef/setup-beam@v1 20 | id: setup-beam 21 | with: 22 | otp-version: ${{matrix.otp}} 23 | rebar3-version: "3" 24 | - name: Compile 25 | run: make compile 26 | - name: Check format 27 | run: make check-format 28 | - name: Run xref 29 | run: make xref 30 | - name: Fetch PLT 31 | uses: actions/cache@v3 32 | id: cache-plt 33 | with: 34 | path: | 35 | _build/default/*_plt 36 | key: dialyzer-${{ github.ref_name }}-${{ matrix.otp }} 37 | restore-keys: | 38 | dialyzer-${{ github.event.repository.default_branch || 'master' }}-${{ matrix.otp }} 39 | dialyzer-${{ github.event.repository.default_branch || 'master' }}- 40 | - name: Run dialyzer 41 | run: make dialyze 42 | - name: Run eunit tests 43 | run: make eunit 44 | - name: Run ct tests 45 | run: make ct 46 | 47 | release: 48 | if: github.ref == 'refs/heads/master' && startsWith(github.event.head_commit.message, 'no-release:') == false 49 | needs: build_and_test 50 | runs-on: ubuntu-latest 51 | steps: 52 | - name: Bump version and push tag 53 | id: tag_version 54 | uses: mathieudutour/github-tag-action@v5.3 55 | with: 56 | github_token: ${{ secrets.GITHUB_TOKEN }} 57 | - name: Create a GitHub release 58 | uses: actions/create-release@v1 59 | env: 60 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 61 | with: 62 | tag_name: ${{ steps.tag_version.outputs.new_tag }} 63 | release_name: Release ${{ steps.tag_version.outputs.new_tag }} 64 | body: ${{ steps.tag_version.outputs.changelog }} 65 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # rebar 3 2 | _build/ 3 | _checkouts/ 4 | 5 | erl_crash.dump 6 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | When contributing to this repository, please first discuss the change you wish to make via issue, 4 | email, or any other method with the owners of this repository before making a change. 5 | 6 | Please note we have a code of conduct, please follow it in all your interactions with the project. 7 | 8 | ## Pull Request Process 9 | 10 | 1. Ensure any install or build dependencies are removed before the end of the layer when doing a 11 | build. 12 | 2. Update the README.md with details of changes to the interface, this includes new environment 13 | variables, exposed ports, useful file locations and container parameters. 14 | 3. Increase the version numbers in any examples files and the README.md to the new version that this 15 | Pull Request would represent. The versioning scheme we use is [SemVer](http://semver.org/). 16 | 4. You may merge the Pull Request in once you have the sign-off of one other developers, or if you 17 | do not have permission to do that, you may request the second reviewer to merge it for you. 18 | 19 | ## Code of Conduct 20 | 21 | ### Our Pledge 22 | 23 | In the interest of fostering an open and welcoming environment, we as 24 | contributors and maintainers pledge to making participation in our project and 25 | our community a harassment-free experience for everyone, regardless of age, body 26 | size, disability, ethnicity, gender identity and expression, level of experience, 27 | nationality, personal appearance, race, religion, or sexual identity and 28 | orientation. 29 | 30 | ### Our Standards 31 | 32 | Examples of behavior that contributes to creating a positive environment 33 | include: 34 | 35 | * Using welcoming and inclusive language 36 | * Being respectful of differing viewpoints and experiences 37 | * Gracefully accepting constructive criticism 38 | * Focusing on what is best for the community 39 | * Showing empathy towards other community members 40 | 41 | Examples of unacceptable behavior by participants include: 42 | 43 | * The use of sexualized language or imagery and unwelcome sexual attention or 44 | advances 45 | * Trolling, insulting/derogatory comments, and personal or political attacks 46 | * Public or private harassment 47 | * Publishing others' private information, such as a physical or electronic 48 | address, without explicit permission 49 | * Other conduct which could reasonably be considered inappropriate in a 50 | professional setting 51 | 52 | ### Our Responsibilities 53 | 54 | Project maintainers are responsible for clarifying the standards of acceptable 55 | behavior and are expected to take appropriate and fair corrective action in 56 | response to any instances of unacceptable behavior. 57 | 58 | Project maintainers have the right and responsibility to remove, edit, or 59 | reject comments, commits, code, wiki edits, issues, and other contributions 60 | that are not aligned to this Code of Conduct, or to ban temporarily or 61 | permanently any contributor for other behaviors that they deem inappropriate, 62 | threatening, offensive, or harmful. 63 | 64 | ### Scope 65 | 66 | This Code of Conduct applies both within project spaces and in public spaces 67 | when an individual is representing the project or its community. Examples of 68 | representing a project or community include using an official project e-mail 69 | address, posting via an official social media account, or acting as an appointed 70 | representative at an online or offline event. Representation of a project may be 71 | further defined and clarified by project maintainers. 72 | 73 | ### Enforcement 74 | 75 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 76 | reported by contacting the project owners. All 77 | complaints will be reviewed and investigated and will result in a response that 78 | is deemed necessary and appropriate to the circumstances. The project team is 79 | obligated to maintain confidentiality with regard to the reporter of an incident. 80 | Further details of specific enforcement policies may be posted separately. 81 | 82 | Project maintainers who do not follow or enforce the Code of Conduct in good 83 | faith may face temporary or permanent repercussions as determined by other 84 | members of the project's leadership. 85 | 86 | ### Attribution 87 | 88 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 89 | available at [http://contributor-covenant.org/version/1/4][version] 90 | 91 | [homepage]: http://contributor-covenant.org 92 | [version]: http://contributor-covenant.org/version/1/4/ 93 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Kivra 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: compile 2 | 3 | compile: 4 | rebar3 compile 5 | 6 | clean: 7 | rebar3 clean 8 | 9 | check-format: 10 | rebar3 fmt --check 11 | 12 | format: 13 | rebar3 fmt 14 | 15 | eunit: 16 | rebar3 eunit 17 | 18 | ct: 19 | rebar3 ct 20 | 21 | dialyze: 22 | rebar3 dialyzer 23 | 24 | xref: 25 | rebar3 xref 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jsonformat 2 | 3 | A custom formatter for Erlang OTP logger that outputs JSON using `jsx` 4 | 5 | ## Getting Started 6 | 7 | This project should be added as a dependency to your project 8 | 9 | ### Prerequisites 10 | 11 | `sys.config`: 12 | 13 | ```erlang 14 | [ { kernel 15 | , [ { logger 16 | ,[ { handler 17 | , default 18 | , logger_std_h 19 | , #{formatter => {jsonformat, #{new_line => true}}} 20 | } 21 | ] } 22 | , {logger_level, info} 23 | ] } 24 | ]. 25 | 26 | ``` 27 | 28 | `shell`: 29 | 30 | ``` 31 | > logger:info(#{a => b}). 32 | {"a":"b","gl":"<0.178.0>","level":"info","pid":"<0.182.0>","report_cb":"fun logger:format_otp_report/1","time":1585902924341139}ok 33 | ``` 34 | 35 | ### Configuration Options 36 | To print each json object to a new line, set `new_line` to `true`: 37 | 38 | ```erlang 39 | #{formatter => {jsonformat, #{ new_line => true }}} 40 | ``` 41 | 42 | To specify which kind of line ending to use, set `new_line_type` to 43 | one of `nl`, `crlf`, `cr`, `unix`, `windows` or `macos9`. These 44 | correspond to: 45 | 46 | * `nl` or `unix` means `\n`, ASCII character 0x0A 47 | * `crlf` or `windows` means `\r\n`, ASCII characters 0x0D 0x0A 48 | * `cr` or `macos9` means `\r`, ASCII character 0x0D 49 | 50 | The default line ending if none is specified is `nl`. 51 | 52 | ```erlang 53 | #{formatter => {jsonformat, #{ new_line => true, new_line_type => crlf }}} 54 | ``` 55 | 56 | To control what is being included in the log object from the metadata, there 57 | are two ways. One can opt-out from fields. Default opts out is `[report_cb]`. 58 | 59 | #{ meta_without => [report_cb, gl, file, domain] } 60 | 61 | Or for very detailed control there is instead opt-in. 62 | 63 | #{ meta_with => [time, mfa, line, user_key, client_key] } 64 | 65 | To rename keys in the resulting json object, provide a `key_mapping`. For 66 | example, to rename the `time` and `level` keys to `timestamp` and `lvl` 67 | respectively, use: 68 | 69 | ```erlang 70 | #{formatter => {jsonformat, #{ key_mapping => #{ time => timestamp 71 | , level => lvl }}}} 72 | ``` 73 | 74 | To format values in the resulting json object, provide a map of `format_funs`. 75 | For example, to format the value associated with the `time` key, use: 76 | 77 | ```erlang 78 | #{formatter => {jsonformat, #{ format_funs => #{ time => fun(T) -> ... end }}} 79 | ``` 80 | 81 | Note that `key_mapping`s are applied before `format_funs`. 82 | 83 | ### Built in formatters 84 | 85 | fun jsonformat:system_time_to_iso8601/1 86 | 87 | Will take a logger:timestamp() and print it in the format of yyyy-mm-ddTHH:MM:SSZ 88 | 89 | ## Running the tests 90 | 91 | ``` 92 | $ rebar3 eunit 93 | ``` 94 | 95 | ### Versioning 96 | 97 | We use [SemVer](http://semver.org/) for versioning. For the versions available, see the [tags on this repository](https://github.com/kivra/jsonformat/tags). 98 | 99 | ### Commit message convention 100 | We use [Conventional Commits](https://www.conventionalcommits.org). 101 | 102 | ## Contributing 103 | 104 | Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on our code of conduct, and the process for submitting pull requests to us. 105 | 106 | ## License 107 | 108 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details 109 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Reporting Security Issues 2 | 3 | The Kivra team and community take security bugs in Kivra seriously. We appreciate your efforts to responsibly disclose your findings, and will make every effort to acknowledge your contributions. 4 | To report a security issue, email [security@kivra.com](mailto:security@kivra.com) and include the word "SECURITY" in the subject line. 5 | The Kivra team will send a response indicating the next steps in handling your report. After the initial reply to your report, the security team will keep you informed of the progress towards a fix and full announcement, and may ask for additional information or guidance. 6 | 7 | ## Disclosure Policy 8 | 9 | When the security team receives a security bug report, they will assign it to a 10 | primary handler. This person will coordinate the fix and release process, 11 | involving the following steps: 12 | 13 | * Confirm the problem and determine the affected versions. 14 | * Audit code to find any potential similar problems. 15 | * Prepare fixes for all releases still under maintenance. These fixes will be 16 | released as fast as possible. 17 | 18 | ## Comments on this Policy 19 | 20 | If you have suggestions on how this process could be improved please submit a 21 | pull request. 22 | -------------------------------------------------------------------------------- /config/sys.config: -------------------------------------------------------------------------------- 1 | %% Copyright (c) 2020, Kivra 2 | %% 3 | %% Permission to use, copy, modify, and/or distribute this software for any 4 | %% purpose with or without fee is hereby granted, provided that the above 5 | %% copyright notice and this permission notice appear in all copies. 6 | %% 7 | %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | 15 | [ { kernel 16 | , [ { logger 17 | , [ { handler 18 | , default 19 | , logger_std_h 20 | , #{formatter => {jsonformat, #{new_line => true}}} 21 | } 22 | ] } 23 | , {logger_level, info} 24 | ] } 25 | ]. 26 | -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | %% Copyright (c) 2020, Kivra 2 | %% 3 | %% Permission to use, copy, modify, and/or distribute this software for any 4 | %% purpose with or without fee is hereby granted, provided that the above 5 | %% copyright notice and this permission notice appear in all copies. 6 | %% 7 | %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | 15 | {deps, [jsx]}. 16 | 17 | {project_plugins, [ 18 | erlfmt 19 | ]}. 20 | 21 | {erlfmt, [ 22 | write 23 | ]}. 24 | 25 | {dialyzer, [{warnings, [unknown]}]}. 26 | 27 | {xref_checks, [ 28 | undefined_function_calls, 29 | undefined_functions, 30 | locals_not_used, 31 | deprecated_function_calls, 32 | deprecated_functions 33 | ]}. 34 | 35 | {shell, [ 36 | {config, "config/sys.config"}, 37 | {apps, [sasl]} 38 | ]}. 39 | -------------------------------------------------------------------------------- /rebar.lock: -------------------------------------------------------------------------------- 1 | {"1.2.0", 2 | [{<<"jsx">>,{pkg,<<"jsx">>,<<"3.1.0">>},0}]}. 3 | [ 4 | {pkg_hash,[ 5 | {<<"jsx">>, <<"D12516BAA0BB23A59BB35DCCAF02A1BD08243FCBB9EFE24F2D9D056CCFF71268">>}]}, 6 | {pkg_hash_ext,[ 7 | {<<"jsx">>, <<"0C5CC8FDC11B53CC25CF65AC6705AD39E54ECC56D1C22E4ADB8F5A53FB9427F3">>}]} 8 | ]. 9 | -------------------------------------------------------------------------------- /src/jsonformat.app.src: -------------------------------------------------------------------------------- 1 | %% Copyright (c) 2020, Kivra 2 | %% 3 | %% Permission to use, copy, modify, and/or distribute this software for any 4 | %% purpose with or without fee is hereby granted, provided that the above 5 | %% copyright notice and this permission notice appear in all copies. 6 | %% 7 | %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | 15 | {application, jsonformat, [ 16 | {description, 17 | "A custom formatter for the erlang logger applications that " 18 | "outputs JSON formatted single line logs"}, 19 | {vsn, git}, 20 | {modules, []}, 21 | {registered, []}, 22 | {applications, [ 23 | kernel, 24 | stdlib, 25 | jsx 26 | ]}, 27 | {licenses, ["MIT"]}, 28 | {links, [{"Github", "https://github.com/kivra/jsonformat/"}]} 29 | ]}. 30 | -------------------------------------------------------------------------------- /src/jsonformat.erl: -------------------------------------------------------------------------------- 1 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 2 | %%% Copyright (c) 2020, Kivra 3 | %%% 4 | %%% Permission to use, copy, modify, and/or distribute this software for any 5 | %%% purpose with or without fee is hereby granted, provided that the above 6 | %%% copyright notice and this permission notice appear in all copies. 7 | %%% 8 | %%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | %%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | %%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | %%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | %%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | %%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | %%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | %%% 16 | %%% @doc Custom formatter for the Erlang OTP logger application which 17 | %%% outputs single-line JSON formatted data 18 | %%% @end 19 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 20 | 21 | %%%_* Module declaration =============================================== 22 | -module(jsonformat). 23 | 24 | %%%_* Exports ========================================================== 25 | -export([format/2]). 26 | -export([system_time_to_iso8601/1]). 27 | -export([system_time_to_iso8601_nano/1]). 28 | 29 | %%%_* Types ============================================================ 30 | -type config() :: #{ 31 | new_line => boolean(), 32 | new_line_type => nl | crlf | cr | unix | windows | macos9, 33 | key_mapping => #{atom() => atom()}, 34 | format_funs => #{atom() => fun((_) -> _)} 35 | }. 36 | 37 | -export_type([config/0]). 38 | 39 | %%%_* Macros =========================================================== 40 | %%%_* Options ---------------------------------------------------------- 41 | -define(NEW_LINE, false). 42 | 43 | %%%_* Code ============================================================= 44 | %%%_ * API ------------------------------------------------------------- 45 | -spec format(logger:log_event(), config()) -> unicode:chardata(). 46 | format( 47 | #{msg := {report, #{format := Format, args := Args, label := {error_logger, _}}}} = Map, Config 48 | ) -> 49 | Report = #{text => io_lib:format(Format, Args)}, 50 | format(Map#{msg := {report, Report}}, Config); 51 | format(#{level := Level, msg := {report, Msg}, meta := Meta}, Config) when is_map(Msg) -> 52 | Data0 = merge_meta(Msg, Meta#{level => Level}, Config), 53 | Data1 = apply_key_mapping(Data0, Config), 54 | Data2 = apply_format_funs(Data1, Config), 55 | encode(pre_encode(Data2, Config), Config); 56 | format(Map = #{msg := {report, KeyVal}}, Config) when is_list(KeyVal) -> 57 | format(Map#{msg := {report, maps:from_list(KeyVal)}}, Config); 58 | format(Map = #{msg := {string, String}}, Config) -> 59 | Report = #{text => unicode:characters_to_binary(String)}, 60 | format(Map#{msg := {report, Report}}, Config); 61 | format(Map = #{msg := {Format, Terms}}, Config) -> 62 | format(Map#{msg := {string, io_lib:format(Format, Terms)}}, Config). 63 | 64 | %%% Useful for converting logger:timestamp() to a readable timestamp. 65 | -spec system_time_to_iso8601(integer()) -> binary(). 66 | system_time_to_iso8601(Epoch) -> 67 | system_time_to_iso8601(Epoch, microsecond). 68 | 69 | -spec system_time_to_iso8601_nano(integer()) -> binary(). 70 | system_time_to_iso8601_nano(Epoch) -> 71 | system_time_to_iso8601(1000 * Epoch, nanosecond). 72 | 73 | -spec system_time_to_iso8601(integer(), erlang:time_unit()) -> binary(). 74 | system_time_to_iso8601(Epoch, Unit) -> 75 | binary:list_to_bin(calendar:system_time_to_rfc3339(Epoch, [{unit, Unit}, {offset, "Z"}])). 76 | 77 | %%%_* Private functions ================================================ 78 | pre_encode(Data, Config) -> 79 | maps:fold( 80 | fun 81 | (K, V, Acc) when is_map(V) -> 82 | maps:put(jsonify(K), pre_encode(V, Config), Acc); 83 | % assume list of maps 84 | (K, Vs, Acc) when is_list(Vs), is_map(hd(Vs)) -> 85 | maps:put(jsonify(K), [pre_encode(V, Config) || V <- Vs, is_map(V)], Acc); 86 | (K, V, Acc) -> 87 | maps:put(jsonify(K), jsonify(V), Acc) 88 | end, 89 | maps:new(), 90 | Data 91 | ). 92 | 93 | merge_meta(Msg, Meta0, Config) -> 94 | Meta1 = meta_without(Meta0, Config), 95 | Meta2 = meta_with(Meta1, Config), 96 | maps:merge(Msg, Meta2). 97 | 98 | encode(Data, Config) -> 99 | Json = jsx:encode(Data), 100 | case new_line(Config) of 101 | true -> [Json, new_line_type(Config)]; 102 | false -> Json 103 | end. 104 | 105 | jsonify(A) when is_atom(A) -> A; 106 | jsonify(B) when is_binary(B) -> B; 107 | jsonify(I) when is_integer(I) -> I; 108 | jsonify(F) when is_float(F) -> F; 109 | jsonify(B) when is_boolean(B) -> B; 110 | jsonify(P) when is_pid(P) -> jsonify(pid_to_list(P)); 111 | jsonify(P) when is_port(P) -> jsonify(port_to_list(P)); 112 | jsonify(F) when is_function(F) -> jsonify(erlang:fun_to_list(F)); 113 | jsonify(L) when is_list(L) -> 114 | try list_to_binary(L) of 115 | S -> S 116 | catch 117 | error:badarg -> 118 | unicode:characters_to_binary(io_lib:format("~0p", [L])) 119 | end; 120 | jsonify({M, F, A}) when is_atom(M), is_atom(F), is_integer(A) -> 121 | <<(a2b(M))/binary, $:, (a2b(F))/binary, $/, (integer_to_binary(A))/binary>>; 122 | jsonify(Any) -> 123 | unicode:characters_to_binary(io_lib:format("~0p", [Any])). 124 | 125 | a2b(A) -> atom_to_binary(A, utf8). 126 | 127 | apply_format_funs(Data, #{format_funs := Callbacks}) -> 128 | maps:fold( 129 | fun 130 | (K, Fun, Acc) when is_map_key(K, Data) -> maps:update_with(K, Fun, Acc); 131 | (_, _, Acc) -> Acc 132 | end, 133 | Data, 134 | Callbacks 135 | ); 136 | apply_format_funs(Data, _) -> 137 | Data. 138 | 139 | apply_key_mapping(Data, #{key_mapping := Mapping}) -> 140 | DataOnlyMapped = 141 | maps:fold( 142 | fun 143 | (K, V, Acc) when is_map_key(K, Data) -> Acc#{V => maps:get(K, Data)}; 144 | (_, _, Acc) -> Acc 145 | end, 146 | #{}, 147 | Mapping 148 | ), 149 | DataNoMapped = maps:without(maps:keys(Mapping), Data), 150 | maps:merge(DataNoMapped, DataOnlyMapped); 151 | apply_key_mapping(Data, _) -> 152 | Data. 153 | 154 | new_line(Config) -> maps:get(new_line, Config, ?NEW_LINE). 155 | 156 | new_line_type(#{new_line_type := nl}) -> <<"\n">>; 157 | new_line_type(#{new_line_type := unix}) -> <<"\n">>; 158 | new_line_type(#{new_line_type := crlf}) -> <<"\r\n">>; 159 | new_line_type(#{new_line_type := windows}) -> <<"\r\n">>; 160 | new_line_type(#{new_line_type := cr}) -> <<"\r">>; 161 | new_line_type(#{new_line_type := macos9}) -> <<"\r">>; 162 | new_line_type(_Default) -> <<"\n">>. 163 | 164 | meta_without(Meta, Config) -> 165 | maps:without(maps:get(meta_without, Config, [report_cb]), Meta). 166 | 167 | meta_with(Meta, #{meta_with := Ks}) -> 168 | maps:with(Ks, Meta); 169 | meta_with(Meta, _ConfigNotPresent) -> 170 | Meta. 171 | 172 | %%%_* Tests ============================================================ 173 | -ifdef(TEST). 174 | -include_lib("eunit/include/eunit.hrl"). 175 | 176 | -define(assertJSONEqual(Expected, Actual), 177 | ?assertEqual(jsx:decode(Expected, [return_maps]), jsx:decode(Actual, [return_maps])) 178 | ). 179 | 180 | format_test() -> 181 | ?assertJSONEqual( 182 | <<"{\"level\":\"alert\",\"text\":\"derp\"}">>, 183 | format(#{level => alert, msg => {string, "derp"}, meta => #{}}, #{}) 184 | ), 185 | ?assertJSONEqual( 186 | <<"{\"herp\":\"derp\",\"level\":\"alert\"}">>, 187 | format(#{level => alert, msg => {report, #{herp => derp}}, meta => #{}}, #{}) 188 | ). 189 | 190 | format_funs_test() -> 191 | Config1 = #{ 192 | format_funs => #{ 193 | time => fun(Epoch) -> Epoch + 1 end, 194 | level => fun(alert) -> info end 195 | } 196 | }, 197 | ?assertJSONEqual( 198 | <<"{\"level\":\"info\",\"text\":\"derp\",\"time\":2}">>, 199 | format(#{level => alert, msg => {string, "derp"}, meta => #{time => 1}}, Config1) 200 | ), 201 | 202 | Config2 = #{ 203 | format_funs => #{ 204 | time => fun(Epoch) -> Epoch + 1 end, 205 | foobar => fun(alert) -> info end 206 | } 207 | }, 208 | ?assertJSONEqual( 209 | <<"{\"level\":\"alert\",\"text\":\"derp\",\"time\":2}">>, 210 | format(#{level => alert, msg => {string, "derp"}, meta => #{time => 1}}, Config2) 211 | ). 212 | 213 | key_mapping_test() -> 214 | Config1 = #{ 215 | key_mapping => #{ 216 | level => lvl, 217 | text => message 218 | } 219 | }, 220 | ?assertJSONEqual( 221 | <<"{\"lvl\":\"alert\",\"message\":\"derp\"}">>, 222 | format(#{level => alert, msg => {string, "derp"}, meta => #{}}, Config1) 223 | ), 224 | 225 | Config2 = #{ 226 | key_mapping => #{ 227 | level => lvl, 228 | text => level 229 | } 230 | }, 231 | ?assertJSONEqual( 232 | <<"{\"level\":\"derp\",\"lvl\":\"alert\"}">>, 233 | format(#{level => alert, msg => {string, "derp"}, meta => #{}}, Config2) 234 | ), 235 | 236 | Config3 = #{ 237 | key_mapping => #{ 238 | level => lvl, 239 | foobar => level 240 | } 241 | }, 242 | ?assertJSONEqual( 243 | <<"{\"lvl\":\"alert\",\"text\":\"derp\"}">>, 244 | format(#{level => alert, msg => {string, "derp"}, meta => #{}}, Config3) 245 | ), 246 | 247 | Config4 = #{ 248 | key_mapping => #{time => timestamp}, 249 | format_funs => #{timestamp => fun(T) -> T + 1 end} 250 | }, 251 | ?assertJSONEqual( 252 | <<"{\"level\":\"alert\",\"text\":\"derp\",\"timestamp\":2}">>, 253 | format(#{level => alert, msg => {string, "derp"}, meta => #{time => 1}}, Config4) 254 | ). 255 | 256 | list_format_test() -> 257 | ErrorReport = 258 | #{ 259 | level => error, 260 | meta => #{time => 1}, 261 | msg => {report, #{report => [{hej, "hopp"}]}} 262 | }, 263 | ?assertJSONEqual( 264 | <<"{\"level\":\"error\",\"report\":\"[{hej,\\\"hopp\\\"}]\",\"time\":1}">>, 265 | format(ErrorReport, #{}) 266 | ). 267 | 268 | meta_without_test() -> 269 | Error = #{ 270 | level => info, 271 | msg => {report, #{answer => 42}}, 272 | meta => #{secret => xyz} 273 | }, 274 | ?assertEqual( 275 | #{ 276 | <<"answer">> => 42, 277 | <<"level">> => <<"info">>, 278 | <<"secret">> => <<"xyz">> 279 | }, 280 | jsx:decode(format(Error, #{}), [return_maps]) 281 | ), 282 | Config2 = #{meta_without => [secret]}, 283 | ?assertEqual( 284 | #{ 285 | <<"answer">> => 42, 286 | <<"level">> => <<"info">> 287 | }, 288 | jsx:decode(format(Error, Config2), [return_maps]) 289 | ), 290 | ok. 291 | 292 | meta_with_test() -> 293 | Error = #{ 294 | level => info, 295 | msg => {report, #{answer => 42}}, 296 | meta => #{secret => xyz} 297 | }, 298 | ?assertEqual( 299 | #{ 300 | <<"answer">> => 42, 301 | <<"level">> => <<"info">>, 302 | <<"secret">> => <<"xyz">> 303 | }, 304 | jsx:decode(format(Error, #{}), [return_maps]) 305 | ), 306 | Config2 = #{meta_with => [level]}, 307 | ?assertEqual( 308 | #{ 309 | <<"answer">> => 42, 310 | <<"level">> => <<"info">> 311 | }, 312 | jsx:decode(format(Error, Config2), [return_maps]) 313 | ), 314 | ok. 315 | 316 | newline_test() -> 317 | ConfigDefault = #{new_line => true}, 318 | ?assertEqual( 319 | [<<"{\"level\":\"alert\",\"text\":\"derp\"}">>, <<"\n">>], 320 | format(#{level => alert, msg => {string, "derp"}, meta => #{}}, ConfigDefault) 321 | ), 322 | ConfigCRLF = #{ 323 | new_line_type => crlf, 324 | new_line => true 325 | }, 326 | ?assertEqual( 327 | [<<"{\"level\":\"alert\",\"text\":\"derp\"}">>, <<"\r\n">>], 328 | format(#{level => alert, msg => {string, "derp"}, meta => #{}}, ConfigCRLF) 329 | ). 330 | 331 | -endif. 332 | --------------------------------------------------------------------------------