├── .credo.exs ├── .formatter.exs ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── config └── config.exs ├── lib └── plug │ └── logger_json.ex ├── mix.exs ├── mix.lock └── test ├── plug └── logger_json_test.exs └── test_helper.exs /.credo.exs: -------------------------------------------------------------------------------- 1 | # This file contains the configuration for Credo. 2 | # 3 | # If you find anything wrong or unclear in this file, please report an 4 | # issue on GitHub: https://github.com/rrrene/credo/issues 5 | %{ 6 | # 7 | # You can have as many configs as you like in the `configs:` field. 8 | configs: [ 9 | %{ 10 | # 11 | # Run any config using `mix credo -C `. If no config name is given 12 | # "default" is used. 13 | name: "default", 14 | # 15 | # these are the files included in the analysis 16 | files: %{ 17 | # 18 | # you can give explicit globs or simply directories 19 | # in the latter case `**/*.{ex,exs}` will be used 20 | included: ["lib/", "src/", "web/", "apps/"], 21 | excluded: [] 22 | }, 23 | # 24 | # The `checks:` field contains all the checks that are run. You can 25 | # customize the parameters of any given check by adding a second element 26 | # to the tuple. 27 | # 28 | # There are two ways of deactivating a check: 29 | # 1. deleting the check from this list 30 | # 2. putting `false` as second element (to quickly "comment it out"): 31 | # 32 | # {Credo.Check.Consistency.ExceptionNames, false} 33 | # 34 | checks: [ 35 | {Credo.Check.Consistency.ExceptionNames}, 36 | {Credo.Check.Consistency.LineEndings}, 37 | {Credo.Check.Consistency.SpaceAroundOperators}, 38 | {Credo.Check.Consistency.SpaceInParentheses}, 39 | {Credo.Check.Consistency.TabsOrSpaces}, 40 | 41 | # For some checks, like AliasUsage, you can only customize the priority 42 | # Priority values are: `low, normal, high, higher` 43 | # {Credo.Check.Design.AliasUsage, priority: :low}, 44 | # For others you can set parameters 45 | {Credo.Check.Design.AliasUsage, false}, 46 | {Credo.Check.Design.DuplicatedCode, mass_threshold: 16, nodes_threshold: 2}, 47 | 48 | # You can also customize the exit_status of each check. 49 | # If you don't want TODO comments to cause `mix credo` to fail, just 50 | # set this value to 0 (zero). 51 | {Credo.Check.Design.TagTODO, exit_status: 2}, 52 | {Credo.Check.Design.TagFIXME}, 53 | 54 | {Credo.Check.Readability.FunctionNames}, 55 | {Credo.Check.Readability.MaxLineLength, priority: :low, max_length: 100}, 56 | {Credo.Check.Readability.ModuleAttributeNames}, 57 | {Credo.Check.Readability.ModuleDoc}, 58 | {Credo.Check.Readability.ModuleNames}, 59 | {Credo.Check.Readability.PredicateFunctionNames}, 60 | {Credo.Check.Readability.TrailingBlankLine}, 61 | {Credo.Check.Readability.TrailingWhiteSpace}, 62 | {Credo.Check.Readability.VariableNames}, 63 | 64 | {Credo.Check.Refactor.ABCSize}, 65 | {Credo.Check.Refactor.CaseTrivialMatches}, 66 | {Credo.Check.Refactor.CondStatements}, 67 | {Credo.Check.Refactor.FunctionArity}, 68 | {Credo.Check.Refactor.MatchInCondition}, 69 | {Credo.Check.Refactor.PipeChainStart}, 70 | {Credo.Check.Refactor.CyclomaticComplexity}, 71 | {Credo.Check.Refactor.NegatedConditionsInUnless}, 72 | {Credo.Check.Refactor.NegatedConditionsWithElse}, 73 | {Credo.Check.Refactor.Nesting}, 74 | {Credo.Check.Refactor.UnlessWithElse}, 75 | 76 | {Credo.Check.Warning.IExPry}, 77 | {Credo.Check.Warning.IoInspect}, 78 | {Credo.Check.Warning.NameRedeclarationByAssignment}, 79 | {Credo.Check.Warning.NameRedeclarationByCase}, 80 | {Credo.Check.Warning.NameRedeclarationByDef}, 81 | {Credo.Check.Warning.NameRedeclarationByFn}, 82 | {Credo.Check.Warning.OperationOnSameValues}, 83 | {Credo.Check.Warning.UnusedEnumOperation}, 84 | {Credo.Check.Warning.UnusedKeywordOperation}, 85 | {Credo.Check.Warning.UnusedListOperation}, 86 | {Credo.Check.Warning.UnusedStringOperation}, 87 | {Credo.Check.Warning.UnusedTupleOperation}, 88 | {Credo.Check.Warning.OperationWithConstantResult}, 89 | ] 90 | } 91 | ] 92 | } 93 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"], 3 | line_length: 120 4 | ] 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /cover 3 | /deps 4 | erl_crash.dump 5 | *.ez 6 | .local.plt 7 | doc/ 8 | .elixir_ls -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: elixir 3 | elixir: 4 | - 1.3.4 5 | - 1.4.5 6 | - 1.5.3 7 | - 1.6.5 8 | - 1.7.0 9 | - 1.7.4 10 | - 1.8.1 11 | otp_release: 12 | - 19.0 13 | - 19.1 14 | - 19.2 15 | - 19.3 16 | - 20.0 17 | - 20.1 18 | matrix: 19 | exclude: 20 | - elixir: 1.3.4 21 | otp_release: 20.0 22 | - elixir: 1.3.4 23 | otp_release: 20.1 24 | - elixir: 1.5.3 25 | otp_release: 19.2 26 | - elixir: 1.5.3 27 | otp_release: 20.1 28 | - elixir: 1.7.0 29 | otp_release: 19.2 30 | - elixir: 1.7.4 31 | otp_release: 19.2 32 | - elixir: 1.7.4 33 | otp_release: 20.1 34 | - elixir: 1.8.1 35 | otp_release: 19.0 36 | - elixir: 1.8.1 37 | otp_release: 19.1 38 | - elixir: 1.8.1 39 | otp_release: 19.2 40 | - elixir: 1.8.1 41 | otp_release: 19.3 -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Master 4 | 5 | ## 0.7.0 6 | * Updated Readme 7 | * Updated Dependencies 8 | * Ran Formatter 9 | * Fixed warnings up to 1.8 10 | * Support nested param filtering 11 | 12 | ## 0.6.0 13 | * Extra configration that allows control of logging debug fields. 14 | * Allow built-in logging keys to be suppressed 15 | * Add Elixir 1.5.3 official support 16 | * Add Erlang/OTP 20.1 official support 17 | * Update dev & test dependencies 18 | 19 | ## 0.5.0 20 | * Extra attributes configration that allows for logging custom metrics 21 | * Add Elixir 1.5.1 22 | * Add Erlang/OTP 20 23 | * Add Erlang/OTP 19.3 to test suite 24 | 25 | ## v0.4.0 26 | * Breaking Changes! 27 | * Log errors with logger level `error` instead of `info` 28 | * Change `status` field to an integer type instead of string 29 | * Drop official support of Elixir 1.2 and OTP 18 30 | * Add support for Elixir 1.4 and OTP 19.1 & 19.2 31 | * Update depedencies 32 | * Fix dialyzer warnings 33 | 34 | ## v0.3.1 35 | * Update depedencies 36 | * Fix Elixir 1.4 warnings 37 | 38 | ## v0.3.0 39 | * Breaking Changes! 40 | * Drop the `fastly_duration` log value 41 | * Add the ability to change verbosity. Log levels warn/debug will return everything from 0.2 minus fastly_duration. Log levels info/error will return a subset of warn/debug that is missing params, client_ip, & client_version. 42 | 43 | ## v0.2 44 | * Add support for error logging tagged with the request id. Errors now are bunched up as a single JSON message in addition to the standard output for easier parsing of errors and matching requests to the resulting error. 45 | -------------------------------------------------------------------------------- /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 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2016 Bleacher Report 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PlugLoggerJson 2 | [![Hex pm](http://img.shields.io/hexpm/v/plug_logger_json.svg?style=flat)](https://hex.pm/packages/plug_logger_json) 3 | [![Build Status](https://travis-ci.org/bleacherreport/plug_logger_json.svg?branch=master)](https://travis-ci.org/bleacherreport/plug_logger_json) 4 | [![License](https://img.shields.io/badge/license-Apache%202-blue.svg)](https://github.com/bleacherreport/plug_logger_json/blob/master/LICENSE) 5 | 6 | A comprehensive JSON logger Plug. 7 | 8 | ## Dependencies 9 | 10 | * Plug 11 | * Poison 12 | 13 | ## Elixir & Erlang Support 14 | 15 | The support policy is to support the last 2 major versions of Erlang and the three last minor versions of Elixir. 16 | 17 | ## Installation 18 | 19 | 1. add plug_logger_json to your list of dependencies in `mix.exs`: 20 | 21 | ```elixir 22 | def deps do 23 | [{:plug_logger_json, "~> 0.7.0"}] 24 | end 25 | ``` 26 | 27 | 2. ensure plug_logger_json is started before your application (Skip if using Elixir 1.4 or greater): 28 | 29 | ```elixir 30 | def application do 31 | [applications: [:plug_logger_json]] 32 | end 33 | ``` 34 | 35 | 3. Replace `Plug.Logger` with either: 36 | 37 | * `Plug.LoggerJSON, log: Logger.level`, 38 | * `Plug.LoggerJSON, log: Logger.level, extra_attributes_fn: &MyPlug.extra_attributes/1` in your plug pipeline (in `endpoint.ex` for Phoenix apps), 39 | 40 | ## Recommended Setup 41 | 42 | ### Configure `plug_logger_json` 43 | 44 | Add to your `config/config.exs` or `config/env_name.exs` if you want to filter params or headers or suppress any logged keys: 45 | 46 | ```elixir 47 | config :plug_logger_json, 48 | filtered_keys: ["password", "authorization"], 49 | suppressed_keys: ["api_version", "log_type"] 50 | ``` 51 | 52 | ### Configure the logger (console) 53 | 54 | In your `config/config.exs` or `config/env_name.exs`: 55 | 56 | ```elixir 57 | config :logger, :console, 58 | format: "$message\n", 59 | level: :info, # You may want to make this an env variable to change verbosity of the logs 60 | metadata: [:request_id] 61 | ``` 62 | 63 | ### Configure the logger (file) 64 | 65 | Do the following: 66 | 67 | * update deps in `mix.exs` with the following: 68 | 69 | ```elixir 70 | def deps do 71 | [{:logger_file_backend, "~> 0.0.10"}] 72 | end 73 | ``` 74 | 75 | * add to your `config/config.exs` or `config/env_name.exs`: 76 | 77 | ```elixir 78 | config :logger, 79 | format: "$message\n", 80 | backends: [{LoggerFileBackend, :log_file}, :console] 81 | 82 | config :logger, :log_file, 83 | format: "$message\n", 84 | level: :info, 85 | metadata: [:request_id], 86 | path: "log/my_pipeline.log" 87 | ``` 88 | 89 | * ensure you are using `Plug.Parsers` (Phoenix adds this to `endpoint.ex` by default) to parse params as well as request body: 90 | 91 | ```elixir 92 | plug Plug.Parsers, 93 | parsers: [:urlencoded, :multipart, :json], 94 | pass: ["*/*"], 95 | json_decoder: Poison 96 | ``` 97 | 98 | ## Error Logging 99 | 100 | In `router.ex` of your Phoenix project or in your plug pipeline: 101 | 102 | * add `require Logger`, 103 | * add `use Plug.ErrorHandler`, 104 | * add the following two private functions: 105 | 106 | ```elixir 107 | defp handle_errors(%Plug.Conn{status: 500} = conn, %{kind: kind, reason: reason, stack: stacktrace}) do 108 | Plug.LoggerJSON.log_error(kind, reason, stacktrace) 109 | send_resp(conn, 500, Poison.encode!(%{errors: %{detail: "Internal server error"}})) 110 | end 111 | 112 | defp handle_errors(_, _), do: nil 113 | ``` 114 | 115 | ## Extra Attributes 116 | 117 | Additional data can be logged alongside the request by specifying a function to call which returns a map: 118 | 119 | ```elixir 120 | def extra_attributes(conn) do 121 | map = %{ 122 | "user_id" => get_in(conn.assigns, [:user, :user_id]), 123 | "other_id" => get_in(conn.private, [:private_resource, :id]), 124 | "should_not_appear" => conn.private[:does_not_exist] 125 | } 126 | 127 | map 128 | |> Enum.filter(&(&1 !== nil)) 129 | |> Enum.into(%{}) 130 | end 131 | 132 | plug Plug.LoggerJSON, 133 | log: Logger.level(), 134 | extra_attributes_fn: &MyPlug.extra_attributes/1 135 | ``` 136 | 137 | In this example, the `:user_id` is retrieved from `conn.assigns.user.user_id` and added to the log if it exists. In the example, any values that are `nil` are filtered from the map. It is a requirement that the value is serialiazable as JSON by the Poison library, otherwise an error will be raised when attempting to encode the value. 138 | 139 | ## Log Verbosity 140 | 141 | `LoggerJSON` plug supports two levels of logging: 142 | 143 | * `info` / `error` will log: 144 | 145 | * api_version, 146 | * date_time, 147 | * duration, 148 | * log_type, 149 | * method, 150 | * path, 151 | * request_id, 152 | * status 153 | 154 | * `warn` / `debug` will log everything from info and: 155 | 156 | * client_ip, 157 | * client_version, 158 | * params / request_body. 159 | 160 | The above are default. It is possible to override them by setting a `include_debug_logging` option to: 161 | 162 | * `false` – means the extra debug fields (client_ip, client_version, and params) WILL NOT get logged. 163 | * `true` – means the extra fields WILL get logged. 164 | * Not setting this option will keep the defaults above. 165 | 166 | Example: 167 | 168 | ```elixir 169 | plug Plug.LoggerJSON, 170 | log: Logger.level, 171 | include_debug_logging: true 172 | ``` 173 | 174 | ## Contributing 175 | 176 | Before submitting your pull request, please run: 177 | 178 | * `mix credo --strict`, 179 | * `mix coveralls`, 180 | * `mix dialyzer`, 181 | * update changelog. 182 | 183 | Please squash your pull request's commits into a single commit with a message and detailed description explaining the commit. 184 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for 9 | # 3rd-party users, it should be done in your "mix.exs" file. 10 | 11 | # You can configure for your application as: 12 | # 13 | # config :plug_logger_json, key: :value 14 | # 15 | # And access this configuration in your application as: 16 | # 17 | # Application.get_env(:plug_logger_json, :key) 18 | # 19 | # Or configure a 3rd-party app: 20 | # 21 | # config :logger, level: :info 22 | # 23 | 24 | # It is also possible to import configuration files, relative to this 25 | # directory. For example, you can emulate configuration per environment 26 | # by uncommenting the line below and defining dev.exs, test.exs and such. 27 | # Configuration from the imported file will override the ones defined 28 | # here (which is why it is important to import them last). 29 | # 30 | # import_config "#{Mix.env}.exs" 31 | # 32 | config :logger, :console, 33 | format: "$message\n", 34 | level: :debug, 35 | metadata: [:request_id] 36 | 37 | config :plug_logger_json, 38 | filtered_keys: ["password", "authorization"] 39 | -------------------------------------------------------------------------------- /lib/plug/logger_json.ex: -------------------------------------------------------------------------------- 1 | defmodule Plug.LoggerJSON do 2 | @moduledoc """ 3 | A plug for logging basic request information in the format: 4 | ```json 5 | { 6 | "api_version": "N/A" 7 | "client_ip": "23.235.46.37" 8 | "client_version": "ios/1.6.7", 9 | "date_time": "2016-05-31T18:00:13Z", 10 | "duration": 4.670, 11 | "handler": "fronts#index" 12 | "log_type": "http", 13 | "method": "POST", 14 | "params": { 15 | "user":"jkelly", 16 | "password":"[FILTERED]" 17 | }, 18 | "path": "/", 19 | "request_id": "d90jcl66vp09r8tke3utjsd1pjrg4ln8", 20 | "status": "200" 21 | } 22 | ``` 23 | 24 | To use it, just plug it into the desired module. 25 | plug Plug.LoggerJSON, log: :debug 26 | ## Options 27 | * `:log` - The log level at which this plug should log its request info. 28 | Default is `:info`. 29 | * `:extra_attributes_fn` - Function to call with `conn` to add additional 30 | fields to the requests. Default is `nil`. Please see "Extra Fields" section 31 | for more information. 32 | 33 | ## Extra Fields 34 | 35 | Additional data can be logged alongside the request by specifying a function 36 | to call which returns a map: 37 | 38 | def extra_attributes(conn) do 39 | map = %{ 40 | "user_id" => get_in(conn.assigns, [:user, :user_id]), 41 | "other_id" => get_in(conn.private, [:private_resource, :id]), 42 | "should_not_appear" => conn.private[:does_not_exist] 43 | } 44 | 45 | map 46 | |> Enum.filter(&(&1 !== nil)) 47 | |> Enum.into(%{}) 48 | end 49 | 50 | plug Plug.LoggerJSON, log: Logger.level, 51 | extra_attributes_fn: &MyPlug.extra_attributes/1 52 | 53 | In this example, the `:user_id` is retrieved from `conn.assigns.user.user_id` 54 | and added to the log if it exists. In the example, any values that are `nil` 55 | are filtered from the map. It is a requirement that the value is 56 | serialiazable as JSON by the Poison library, otherwise an error will be raised 57 | when attempting to encode the value. 58 | """ 59 | 60 | alias Plug.Conn 61 | 62 | @behaviour Plug 63 | 64 | require Logger 65 | 66 | @typedoc """ 67 | Type for a plug option 68 | """ 69 | @type opts :: binary | tuple | atom | integer | float | [opts] | %{opts => opts} 70 | 71 | @typedoc """ 72 | Type for time 73 | """ 74 | @type time :: {non_neg_integer(), non_neg_integer(), non_neg_integer()} 75 | 76 | @spec init(opts) :: opts 77 | def init(opts), do: opts 78 | 79 | @spec call(Plug.Conn.t(), opts) :: Plug.Conn.t() 80 | def call(conn, level_or_opts) when is_atom(level_or_opts) do 81 | call(conn, level: level_or_opts) 82 | end 83 | 84 | def call(conn, opts) do 85 | level = Keyword.get(opts, :log, :info) 86 | start = :os.timestamp() 87 | 88 | Conn.register_before_send(conn, fn conn -> 89 | :ok = log(conn, level, start, opts) 90 | conn 91 | end) 92 | end 93 | 94 | @spec log(Plug.Conn.t(), atom(), time(), opts) :: atom() | no_return() 95 | def log(conn, level, start, opts \\ []) 96 | def log(conn, :error, start, opts), do: log(conn, :info, start, opts) 97 | def log(conn, :info, start, opts), do: log_message(conn, :info, start, opts) 98 | def log(conn, :warn, start, opts), do: log(conn, :debug, start, opts) 99 | 100 | def log(conn, :debug, start, opts) do 101 | log_message(conn, :info, start, Keyword.put_new(opts, :include_debug_logging, true)) 102 | end 103 | 104 | @spec log_error(atom(), map(), list()) :: atom() 105 | def log_error(kind, reason, stacktrace) do 106 | _ = 107 | Logger.log(:error, fn -> 108 | %{ 109 | "log_type" => "error", 110 | "message" => Exception.format(kind, reason, stacktrace), 111 | "request_id" => Logger.metadata()[:request_id] 112 | } 113 | |> Poison.encode!() 114 | end) 115 | end 116 | 117 | @spec log_message(Plug.Conn.t(), atom(), time(), opts) :: atom() 118 | defp log_message(conn, level, start, opts) do 119 | Logger.log(level, fn -> 120 | conn 121 | |> basic_logging(start) 122 | |> Map.merge(debug_logging(conn, opts)) 123 | |> Map.merge(phoenix_attributes(conn)) 124 | |> Map.merge(extra_attributes(conn, opts)) 125 | |> Poison.encode!() 126 | end) 127 | end 128 | 129 | defp basic_logging(conn, start) do 130 | stop = :os.timestamp() 131 | duration = :timer.now_diff(stop, start) 132 | req_id = Logger.metadata()[:request_id] 133 | req_headers = format_map_list(conn.req_headers) 134 | 135 | log_json = %{ 136 | "api_version" => Map.get(req_headers, "accept", "N/A"), 137 | "date_time" => iso8601(:calendar.now_to_datetime(:os.timestamp())), 138 | "duration" => Float.round(duration / 1000, 3), 139 | "log_type" => "http", 140 | "method" => conn.method, 141 | "path" => conn.request_path, 142 | "request_id" => req_id, 143 | "status" => conn.status 144 | } 145 | 146 | Map.drop(log_json, Application.get_env(:plug_logger_json, :suppressed_keys, [])) 147 | end 148 | 149 | defp extra_attributes(conn, opts) do 150 | case Keyword.get(opts, :extra_attributes_fn) do 151 | fun when is_function(fun) -> fun.(conn) 152 | _ -> %{} 153 | end 154 | end 155 | 156 | @spec client_version(%{String.t() => String.t()}) :: String.t() 157 | defp client_version(headers) do 158 | headers 159 | |> Map.get("x-client-version", "N/A") 160 | |> case do 161 | "N/A" -> 162 | Map.get(headers, "user-agent", "N/A") 163 | 164 | accept_value -> 165 | accept_value 166 | end 167 | end 168 | 169 | @spec debug_logging(Plug.Conn.t(), opts) :: map() 170 | defp debug_logging(conn, opts) do 171 | case Keyword.get(opts, :include_debug_logging) do 172 | true -> 173 | req_headers = format_map_list(conn.req_headers) 174 | 175 | %{ 176 | "client_ip" => format_ip(Map.get(req_headers, "x-forwarded-for", "N/A")), 177 | "client_version" => client_version(req_headers), 178 | "params" => format_map_list(conn.params) 179 | } 180 | 181 | _ -> 182 | %{} 183 | end 184 | end 185 | 186 | @spec filter_values(struct(), [binary()]) :: binary() 187 | defp filter_values(%{__struct__: mod} = struct, filters) when is_atom(mod) do 188 | struct 189 | |> Map.from_struct() 190 | |> filter_values(filters) 191 | end 192 | 193 | @spec filter_values(map(), [binary()]) :: [{binary(), any()}] 194 | defp filter_values(%{} = map, filters) do 195 | Enum.into(map, %{}, fn {k, v} -> 196 | if is_binary(k) and k in filters do 197 | {k, "[FILTERED]"} 198 | else 199 | {k, filter_values(v, filters)} 200 | end 201 | end) 202 | end 203 | 204 | @spec filter_values([{binary(), any()}], [binary()]) :: [{binary(), any()}] 205 | defp filter_values(list, filters) when is_list(list) do 206 | Enum.map(list, &filter_values(&1, filters)) 207 | end 208 | 209 | defp filter_values(other, _filters), do: format_value(other) 210 | 211 | @spec format_ip(String.t()) :: String.t() 212 | defp format_ip("N/A") do 213 | "N/A" 214 | end 215 | 216 | defp format_ip(x_forwarded_for) do 217 | hd(String.split(x_forwarded_for, ", ")) 218 | end 219 | 220 | @spec format_map_list(Enumerable.t()) :: map() 221 | defp format_map_list(enumerable) do 222 | enumerable 223 | |> filter_values(Application.get_env(:plug_logger_json, :filtered_keys, [])) 224 | |> Enum.into(%{}) 225 | end 226 | 227 | defp format_value(value) when is_binary(value) do 228 | String.slice(value, 0..500) 229 | end 230 | 231 | defp format_value(value) do 232 | value 233 | end 234 | 235 | defp iso8601({{year, month, day}, {hour, minute, second}}) do 236 | zero_pad(year, 4) <> 237 | "-" <> 238 | zero_pad(month, 2) <> 239 | "-" <> 240 | zero_pad(day, 2) <> "T" <> zero_pad(hour, 2) <> ":" <> zero_pad(minute, 2) <> ":" <> zero_pad(second, 2) <> "Z" 241 | end 242 | 243 | @spec phoenix_attributes(map()) :: map() 244 | defp phoenix_attributes(%{private: %{phoenix_controller: controller, phoenix_action: action}}) do 245 | %{"handler" => "#{controller}##{action}"} 246 | end 247 | 248 | defp phoenix_attributes(_) do 249 | %{"handler" => "N/A"} 250 | end 251 | 252 | @spec zero_pad(1..3_000, non_neg_integer()) :: String.t() 253 | defp zero_pad(val, count) do 254 | num = Integer.to_string(val) 255 | :binary.copy("0", count - byte_size(num)) <> num 256 | end 257 | end 258 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule PlugLoggerJson.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :plug_logger_json, 7 | build_embedded: Mix.env() == :prod, 8 | deps: deps(), 9 | dialyzer: [ 10 | plt_add_deps: true 11 | ], 12 | description: "Elixir Plug that formats http request logs as json", 13 | docs: [extras: ["README.md"]], 14 | elixir: "~> 1.3", 15 | homepage_url: "https://github.com/bleacherreport/plug_logger_json", 16 | name: "Plug Logger JSON", 17 | package: package(), 18 | preferred_cli_env: [coveralls: :test, "coveralls.detail": :test, "coveralls.post": :test, "coveralls.html": :test], 19 | source_url: "https://github.com/bleacherreport/plug_logger_json", 20 | start_permanent: Mix.env() == :prod, 21 | test_coverage: [tool: ExCoveralls], 22 | version: "0.7.0" 23 | ] 24 | end 25 | 26 | def application do 27 | [applications: [:logger, :plug]] 28 | end 29 | 30 | defp deps do 31 | [ 32 | {:credo, "~> 1.0.2", only: [:dev]}, 33 | {:dialyxir, "~> 0.5.1", only: [:dev]}, 34 | {:earmark, "~> 1.3.1", only: [:dev]}, 35 | {:ex_doc, "~> 0.19.3", only: [:dev]}, 36 | {:excoveralls, "~> 0.10.5", only: [:test]}, 37 | {:plug, "~> 1.0"}, 38 | {:poison, "~> 1.5 or ~> 2.0 or ~> 3.0 or ~> 4.0"} 39 | ] 40 | end 41 | 42 | defp package do 43 | [ 44 | files: ["lib", "mix.exs", "README*", "LICENSE*"], 45 | licenses: ["Apache 2.0"], 46 | links: %{"GitHub" => "https://github.com/bleacherreport/plug_logger_json"}, 47 | maintainers: ["John Kelly, Ben Marx"] 48 | ] 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm"}, 3 | "certifi": {:hex, :certifi, "2.4.2", "75424ff0f3baaccfd34b1214184b6ef616d89e420b258bb0a5ea7d7bc628f7f0", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"}, 4 | "credo": {:hex, :credo, "1.0.2", "88bc918f215168bf6ce7070610a6173c45c82f32baa08bdfc80bf58df2d103b6", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"}, 5 | "dialyxir": {:hex, :dialyxir, "0.5.1", "b331b091720fd93e878137add264bac4f644e1ddae07a70bf7062c7862c4b952", [:mix], [], "hexpm"}, 6 | "earmark": {:hex, :earmark, "1.3.1", "73812f447f7a42358d3ba79283cfa3075a7580a3a2ed457616d6517ac3738cb9", [:mix], [], "hexpm"}, 7 | "ex_doc": {:hex, :ex_doc, "0.19.3", "3c7b0f02851f5fc13b040e8e925051452e41248f685e40250d7e40b07b9f8c10", [:mix], [{:earmark, "~> 1.2", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.10", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"}, 8 | "excoveralls": {:hex, :excoveralls, "0.10.5", "7c912c4ec0715a6013647d835c87cde8154855b9b84e256bc7a63858d5f284e3", [:mix], [{:hackney, "~> 1.13", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"}, 9 | "exjsx": {:hex, :exjsx, "4.0.0", "60548841e0212df401e38e63c0078ec57b33e7ea49b032c796ccad8cde794b5c", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, repo: "hexpm", optional: false]}], "hexpm"}, 10 | "hackney": {:hex, :hackney, "1.15.0", "287a5d2304d516f63e56c469511c42b016423bcb167e61b611f6bad47e3ca60e", [:rebar3], [{:certifi, "2.4.2", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.4", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"}, 11 | "idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"}, 12 | "jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"}, 13 | "jsx": {:hex, :jsx, "2.8.3", "a05252d381885240744d955fbe3cf810504eb2567164824e19303ea59eef62cf", [:mix, :rebar3], [], "hexpm"}, 14 | "makeup": {:hex, :makeup, "0.8.0", "9cf32aea71c7fe0a4b2e9246c2c4978f9070257e5c9ce6d4a28ec450a839b55f", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"}, 15 | "makeup_elixir": {:hex, :makeup_elixir, "0.13.0", "be7a477997dcac2e48a9d695ec730b2d22418292675c75aa2d34ba0909dcdeda", [:mix], [{:makeup, "~> 0.8", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm"}, 16 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"}, 17 | "mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm"}, 18 | "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], [], "hexpm"}, 19 | "nimble_parsec": {:hex, :nimble_parsec, "0.5.0", "90e2eca3d0266e5c53f8fbe0079694740b9c91b6747f2b7e3c5d21966bba8300", [:mix], [], "hexpm"}, 20 | "parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm"}, 21 | "plug": {:hex, :plug, "1.7.2", "d7b7db7fbd755e8283b6c0a50be71ec0a3d67d9213d74422d9372effc8e87fd1", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}], "hexpm"}, 22 | "plug_crypto": {:hex, :plug_crypto, "1.0.0", "18e49317d3fa343f24620ed22795ec29d4a5e602d52d1513ccea0b07d8ea7d4d", [:mix], [], "hexpm"}, 23 | "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"}, 24 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.4", "f0eafff810d2041e93f915ef59899c923f4568f4585904d010387ed74988e77b", [:make, :mix, :rebar3], [], "hexpm"}, 25 | "unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [:rebar3], [], "hexpm"}, 26 | } 27 | -------------------------------------------------------------------------------- /test/plug/logger_json_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Plug.LoggerJSONTest do 2 | use ExUnit.Case 3 | use Plug.Test 4 | 5 | import ExUnit.CaptureIO 6 | require Logger 7 | 8 | defmodule MyDebugPlug do 9 | use Plug.Builder 10 | 11 | plug(Plug.LoggerJSON, log: :debug, extra_attributes_fn: &__MODULE__.extra_attributes/1) 12 | 13 | plug(Plug.Parsers, 14 | parsers: [:urlencoded, :multipart, :json], 15 | pass: ["*/*"], 16 | json_decoder: Poison 17 | ) 18 | 19 | plug(:passthrough) 20 | 21 | defp passthrough(conn, _) do 22 | Plug.Conn.send_resp(conn, 200, "Passthrough") 23 | end 24 | 25 | def extra_attributes(conn) do 26 | map = %{ 27 | "user_id" => get_in(conn.assigns, [:user, :user_id]), 28 | "other_id" => get_in(conn.private, [:private_resource, :id]), 29 | "should_not_appear" => conn.private[:does_not_exist] 30 | } 31 | 32 | map 33 | |> Enum.filter(&(&1 !== nil)) 34 | |> Enum.into(%{}) 35 | end 36 | end 37 | 38 | defmodule MyInfoPlug do 39 | use Plug.Builder 40 | 41 | plug(Plug.LoggerJSON, log: :info) 42 | 43 | plug(Plug.Parsers, 44 | parsers: [:urlencoded, :multipart, :json], 45 | pass: ["*/*"], 46 | json_decoder: Poison 47 | ) 48 | 49 | plug(:passthrough) 50 | 51 | defp passthrough(conn, _) do 52 | Plug.Conn.send_resp(conn, 200, "Passthrough") 53 | end 54 | end 55 | 56 | defmodule MyInfoPlugWithIncludeDebugLogging do 57 | use Plug.Builder 58 | 59 | plug(Plug.LoggerJSON, log: :info, include_debug_logging: true) 60 | 61 | plug(Plug.Parsers, 62 | parsers: [:urlencoded, :multipart, :json], 63 | pass: ["*/*"], 64 | json_decoder: Poison 65 | ) 66 | 67 | plug(:passthrough) 68 | 69 | defp passthrough(conn, _) do 70 | Plug.Conn.send_resp(conn, 200, "Passthrough") 71 | end 72 | end 73 | 74 | defp remove_colors(message) do 75 | message 76 | |> String.replace("\e[36m", "") 77 | |> String.replace("\e[31m", "") 78 | |> String.replace("\e[22m", "") 79 | |> String.replace("\n\e[0m", "") 80 | |> String.replace("{\"requ", "{\"requ") 81 | end 82 | 83 | defp call(conn, plug \\ MyDebugPlug) do 84 | get_log(fn -> plug.call(conn, []) end) 85 | end 86 | 87 | defp get_log(func) do 88 | data = 89 | capture_io(:user, fn -> 90 | Process.put(:get_log, func.()) 91 | Logger.flush() 92 | end) 93 | 94 | {Process.get(:get_log), data} 95 | end 96 | 97 | test "correct output - no params or headers" do 98 | {_conn, message} = 99 | conn(:get, "/") 100 | |> call 101 | 102 | map = 103 | message 104 | |> remove_colors 105 | |> Poison.decode!() 106 | 107 | assert map["api_version"] == "N/A" 108 | assert map["client_ip"] == "N/A" 109 | assert map["client_version"] == "N/A" 110 | assert map["date_time"] 111 | assert map["duration"] 112 | assert map["handler"] == "N/A" 113 | assert map["log_type"] == "http" 114 | assert map["method"] == "GET" 115 | assert map["params"] == %{} 116 | assert map["path"] == "/" 117 | assert map["request_id"] == nil 118 | assert map["status"] == 200 119 | end 120 | 121 | test "correct output - params and headers" do 122 | {_conn, message} = 123 | conn(:get, "/", fake_param: "1") 124 | |> put_req_header("authorization", "f3443890-6683-4a25-8094-f23cf10b72d0") 125 | |> put_req_header("content-type", "application/json") 126 | |> call 127 | 128 | map = 129 | message 130 | |> remove_colors 131 | |> Poison.decode!() 132 | 133 | assert map["api_version"] == "N/A" 134 | assert map["client_ip"] == "N/A" 135 | assert map["client_version"] == "N/A" 136 | assert map["date_time"] 137 | assert map["duration"] 138 | assert map["handler"] == "N/A" 139 | assert map["log_type"] == "http" 140 | assert map["method"] == "GET" 141 | assert map["params"] == %{"fake_param" => "1"} 142 | assert map["path"] == "/" 143 | assert map["request_id"] == nil 144 | assert map["status"] == 200 145 | end 146 | 147 | test "doesn't include debug log lines for MyInfoPlug" do 148 | {_conn, message} = 149 | conn(:get, "/", fake_param: "1") 150 | |> put_req_header("x-forwarded-for", "209.49.75.165") 151 | |> put_req_header("x-client-version", "ios/1.5.4") 152 | |> call(MyInfoPlug) 153 | 154 | map = 155 | message 156 | |> remove_colors 157 | |> Poison.decode!() 158 | 159 | assert map["client_ip"] == nil 160 | assert map["client_version"] == nil 161 | assert map["params"] == nil 162 | end 163 | 164 | test "include debug log lines for MyInfoPlugWithIncludeDebugLogging" do 165 | {_conn, message} = 166 | conn(:get, "/", fake_param: "1") 167 | |> put_req_header("x-forwarded-for", "209.49.75.165") 168 | |> put_req_header("x-client-version", "ios/1.5.4") 169 | |> call(MyInfoPlugWithIncludeDebugLogging) 170 | 171 | map = 172 | message 173 | |> remove_colors 174 | |> Poison.decode!() 175 | 176 | assert map["client_ip"] == "209.49.75.165" 177 | assert map["client_version"] == "ios/1.5.4" 178 | assert map["params"] == %{"fake_param" => "1"} 179 | end 180 | 181 | test "correct output - Phoenix" do 182 | {_conn, message} = 183 | conn(:get, "/") 184 | |> put_private(:phoenix_controller, Plug.LoggerJSONTest) 185 | |> put_private(:phoenix_action, :show) 186 | |> put_private(:phoenix_format, "json") 187 | |> call 188 | 189 | map = 190 | message 191 | |> remove_colors 192 | |> Poison.decode!() 193 | 194 | assert map["api_version"] == "N/A" 195 | assert map["client_ip"] == "N/A" 196 | assert map["client_version"] == "N/A" 197 | assert map["date_time"] 198 | assert map["duration"] 199 | assert map["handler"] == "Elixir.Plug.LoggerJSONTest#show" 200 | assert map["log_type"] == "http" 201 | assert map["method"] == "GET" 202 | assert map["params"] == %{} 203 | assert map["path"] == "/" 204 | assert map["request_id"] == nil 205 | assert map["status"] == 200 206 | end 207 | 208 | test "correct output - Post request JSON" do 209 | json = 210 | %{ 211 | "reaction" => %{ 212 | "reaction" => "other", 213 | "track_id" => "7550", 214 | "type" => "emoji", 215 | "user_id" => "a2e684ee-2e5f-4e4d-879a-bb253908eef3" 216 | } 217 | } 218 | |> Poison.encode!() 219 | 220 | {_conn, message} = 221 | conn(:post, "/", json) 222 | |> put_req_header("content-type", "application/json") 223 | |> call 224 | 225 | map = 226 | message 227 | |> remove_colors 228 | |> Poison.decode!() 229 | 230 | assert map["api_version"] == "N/A" 231 | assert map["client_ip"] == "N/A" 232 | assert map["client_version"] == "N/A" 233 | assert map["date_time"] 234 | assert map["duration"] 235 | assert map["handler"] == "N/A" 236 | assert map["log_type"] == "http" 237 | assert map["method"] == "POST" 238 | 239 | assert map["params"] == %{ 240 | "reaction" => %{ 241 | "reaction" => "other", 242 | "track_id" => "7550", 243 | "type" => "emoji", 244 | "user_id" => "a2e684ee-2e5f-4e4d-879a-bb253908eef3" 245 | } 246 | } 247 | 248 | assert map["path"] == "/" 249 | assert map["request_id"] == nil 250 | assert map["status"] == 200 251 | end 252 | 253 | test "correct output - X-forwarded-for header" do 254 | {_conn, message} = 255 | conn(:get, "/") 256 | |> put_req_header("x-forwarded-for", "209.49.75.165") 257 | |> put_private(:phoenix_controller, Plug.LoggerJSONTest) 258 | |> put_private(:phoenix_action, :show) 259 | |> put_private(:phoenix_format, "json") 260 | |> call 261 | 262 | map = 263 | message 264 | |> remove_colors 265 | |> Poison.decode!() 266 | 267 | assert map["api_version"] == "N/A" 268 | assert map["client_ip"] == "209.49.75.165" 269 | assert map["client_version"] == "N/A" 270 | assert map["date_time"] 271 | assert map["duration"] 272 | assert map["handler"] == "Elixir.Plug.LoggerJSONTest#show" 273 | assert map["log_type"] == "http" 274 | assert map["method"] == "GET" 275 | assert map["params"] == %{} 276 | assert map["path"] == "/" 277 | assert map["request_id"] == nil 278 | assert map["status"] == 200 279 | end 280 | 281 | test "correct output - client version header" do 282 | {_conn, message} = 283 | conn(:get, "/") 284 | |> put_req_header("x-client-version", "ios/1.5.4") 285 | |> call 286 | 287 | map = 288 | message 289 | |> remove_colors 290 | |> Poison.decode!() 291 | 292 | assert map["client_version"] == "ios/1.5.4" 293 | end 294 | 295 | test "correct output - custom paths" do 296 | {_conn, message} = 297 | conn(:get, "/") 298 | |> put_req_header("x-client-version", "ios/1.5.4") 299 | |> assign(:user, %{user_id: "123"}) 300 | |> put_private(:private_resource, %{id: 456}) 301 | |> call() 302 | 303 | map = 304 | message 305 | |> remove_colors 306 | |> Poison.decode!() 307 | 308 | assert map["user_id"] == "123" 309 | assert map["other_id"] == 456 310 | refute map["should_not_appear"] 311 | end 312 | 313 | test "correct output - nested filtered keys" do 314 | Application.put_env(:plug_logger_json, :filtered_keys, ["password"]) 315 | 316 | user = 317 | conn(:post, "/", %{user: %{password: "secret", username: "me"}}) 318 | |> call() 319 | |> elem(1) 320 | |> remove_colors() 321 | |> Poison.decode!() 322 | |> get_in(["params", "user"]) 323 | 324 | assert user["password"] == "[FILTERED]" 325 | assert user["username"] == "me" 326 | end 327 | 328 | test "correct output - structs in params" do 329 | params = 330 | conn(:post, "/", %{photo: %Plug.Upload{}}) 331 | |> call() 332 | |> elem(1) 333 | |> remove_colors() 334 | |> Poison.decode!() 335 | |> get_in(["params"]) 336 | 337 | assert params["photo"] == %{"content_type" => nil, "filename" => nil, "path" => nil} 338 | end 339 | 340 | describe "500 error" do 341 | test "logs the error" do 342 | stacktrace = [ 343 | {MyDebugPlug, :index, 2, [file: 'web/controllers/reaction_controller.ex', line: 53]}, 344 | {MyDebugPlug, :action, 2, [file: 'web/controllers/reaction_controller.ex', line: 1]}, 345 | {MyDebugPlug, :phoenix_controller_pipeline, 2, [file: 'web/controllers/reaction_controller.ex', line: 1]}, 346 | {MyDebugPlug, :instrument, 4, [file: 'lib/reactions/endpoint.ex', line: 1]}, 347 | {MyDebugPlug, :dispatch, 2, [file: 'lib/phoenix/router.ex', line: 261]}, 348 | {MyDebugPlug, :do_call, 2, [file: 'web/router.ex', line: 1]}, 349 | {MyDebugPlug, :call, 2, [file: 'lib/plug/error_handler.ex', line: 64]}, 350 | {MyDebugPlug, :phoenix_pipeline, 1, [file: 'lib/reactions/endpoint.ex', line: 1]}, 351 | {MyDebugPlug, :call, 2, [file: 'lib/reactions/endpoint.ex', line: 1]}, 352 | {Plug.Adapters.Cowboy.Handler, :upgrade, 4, [file: 'lib/plug/adapters/cowboy/handler.ex', line: 15]}, 353 | {:cowboy_protocol, :execute, 4, [file: 'src/cowboy_protocol.erl', line: 442]} 354 | ] 355 | 356 | {_conn, _} = 357 | conn(:get, "/") 358 | |> call 359 | 360 | {_, message} = get_log(fn -> Plug.LoggerJSON.log_error(:error, %RuntimeError{message: "ERROR"}, stacktrace) end) 361 | 362 | error_log = 363 | message 364 | |> remove_colors 365 | |> Poison.decode!() 366 | 367 | assert error_log["log_type"] == "error" 368 | assert error_log["message"] 369 | assert error_log["request_id"] == nil 370 | end 371 | end 372 | end 373 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------