├── .formatter.exs ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── config └── config.exs ├── lib ├── rollbax.ex └── rollbax │ ├── client.ex │ ├── exception.ex │ ├── item.ex │ ├── logger.ex │ ├── reporter.ex │ └── reporter │ ├── silencing.ex │ └── standard.ex ├── mix.exs ├── mix.lock ├── pages └── Using Rollbax in Plug-based applications.md └── test ├── rollbax ├── client_test.exs ├── client_via_proxy_test.exs └── logger_test.exs ├── rollbax_test.exs └── test_helper.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 3 | ] 4 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | format: 11 | name: Code linting 12 | runs-on: ubuntu-18.04 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | 17 | - name: Set up Elixir environment 18 | uses: erlef/setup-elixir@v1 19 | with: 20 | otp-version: 21.3 21 | elixir-version: 1.8 22 | 23 | - run: mix deps.get 24 | 25 | - name: Check formatting 26 | run: mix format --check-formatted 27 | 28 | - name: Check compilation warnings 29 | run: mix compile --warnings-as-errors 30 | 31 | test: 32 | name: Test suite 33 | runs-on: ubuntu-16.04 34 | 35 | strategy: 36 | matrix: 37 | versions: 38 | - otp: 18.3 39 | elixir: 1.5 40 | - otp: 21.3 41 | elixir: 1.8 42 | 43 | env: 44 | MIX_ENV: test 45 | 46 | steps: 47 | - uses: actions/checkout@v2 48 | 49 | - name: Set up Elixir environment 50 | uses: erlef/setup-elixir@v1 51 | with: 52 | elixir-version: ${{ matrix.versions.elixir }} 53 | otp-version: ${{ matrix.versions.otp }} 54 | 55 | - name: Install dependencies 56 | run: mix deps.get --only test 57 | 58 | - name: Run tests 59 | run: mix test 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /deps 3 | /doc 4 | erl_crash.dump 5 | *.ez 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v0.11.0 4 | 5 | * Added handling of rate limiting from the Rollbar API. 6 | 7 | ## v0.10.0 8 | 9 | * **BREAKING CHANGE**: Increased Elixir version requirement to 1.4 and higher. 10 | * Made invalid configurations raise. 11 | * Allowed to configure Rollbax to use a proxy (#105). 12 | 13 | ## v0.9.2 14 | 15 | * Fixed some code that wouldn't let Rollbax start if `:enabled` was `false` but the access token or environment were not set. 16 | 17 | ## v0.9.1 18 | 19 | * Fixed a bug where we didn't list Jason as an application in the `:applications` key. 20 | 21 | ## v0.9.0 22 | 23 | * **BREAKING CHANGE**: Increased Elixir version requirement to 1.3 and higher. 24 | * Introduced `Rollbax.report_message/4`. 25 | * Reworked logging support. Now `Rollbax.Logger` is not a `Logger` backend, and you cannot send logs to Rollbar automatically via `Logger.*` macros (Rollbar is not a logging aggregation service after all! :stuck_out_tongue:). Use `Rollbax.report_message/4` instead. Check out the documentation for more information on how to use the new `Rollbax.Logger`. 26 | * Made the `:access_token` configuration parameter be only required if `:enabled` is `true`. 27 | * Added support for customizing the Rollbar API endpoint. 28 | * Stopped overriding occurrence data provided by the user. 29 | * Added support for runtime configuration through a callback that can be set with `:config_callback`. 30 | * Dropped support for configuring some options through `{:system, variable}` "special" values. The new `:config_callback` configuration option allows to fetch variables from the environment at runtime, so that should be used instead. 31 | 32 | ## v0.8.2 33 | 34 | * Made sure that JSON encoding never cause `Rollbax.Client` crashing. 35 | * Improved formatting of stacktraces, and exceptions reported as exits. 36 | * Fixed a possible infinite loop when a report is send while `Rollbax.Client` is not available. 37 | 38 | ## v0.8.1 39 | 40 | * Fixed a bug when reporting a term that is not an exception and using kind `:error` in `Rollbax.report/5`. 41 | 42 | ## v0.8.0 43 | 44 | * Fixed a bug with custom data not being reported correctly. 45 | * Bumped Elixir requirement from ~> 1.0 to ~> 1.1. 46 | 47 | ## v0.7.0 48 | 49 | * Added support for blacklisting logger messages through the `:blacklist` configuration option. This way, it's possible to prevent logged messages that match a given pattern from being reported. 50 | * Started allowing globally-set custom data: the data in the `:custom` configuration option for the `:rollbax` application is now sent alongside everything reported to Rollbax (and merged with report-specific custom data). 51 | 52 | ## v0.6.1 53 | 54 | * Fixed a bug involving invalid unicode codepoints in `Rollbax.Logger`. 55 | 56 | ## v0.6.0 57 | 58 | * Removed `Rollbax.report/2` in favour of `Rollbax.report/3`: this new function takes the "kind" of the exception (`:error`, `:exit`, or `:throw`) so that items on Rollbar are displayed more nicely. 59 | * Renamed `Rollbax.Notifier` to `Rollbax.Logger`. 60 | * Started logging (with level `:error`) when the Rollbar API replies with an error. 61 | * Started putting the metadata associated with `Logger` calls in the `"message"` part of the reported item instead of the `"custom"` data associated with it. 62 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Aleksei Magusev 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rollbax 2 | 3 | [![Build Status](https://travis-ci.org/ForzaElixir/rollbax.svg?branch=master "Build Status")](https://travis-ci.org/ForzaElixir/rollbax) 4 | [![Hex Version](https://img.shields.io/hexpm/v/rollbax.svg "Hex Version")](https://hex.pm/packages/rollbax) 5 | 6 | Elixir client for [Rollbar](https://rollbar.com). 7 | 8 | ## Installation 9 | 10 | Add Rollbax as a dependency to your `mix.exs` file: 11 | 12 | ```elixir 13 | defp deps() do 14 | [{:rollbax, ">= 0.0.0"}] 15 | end 16 | ``` 17 | 18 | Then run `mix deps.get` in your shell to fetch the dependencies. Add `:rollbax` to your list of `:applications` if you're not using `:extra_applications`. 19 | 20 | ## Usage 21 | 22 | Rollbax requires some configuration in order to work. For example, in `config/config.exs`: 23 | 24 | ```elixir 25 | config :rollbax, 26 | access_token: "ffb8056a621f309eeb1ed87fa0c7", 27 | environment: "production" 28 | ``` 29 | 30 | Then, exceptions (errors, exits, and throws) can be reported to Rollbar using `Rollbax.report/3`: 31 | 32 | ```elixir 33 | try do 34 | DoesNotExist.for_sure() 35 | rescue 36 | exception -> 37 | Rollbax.report(:error, exception, System.stacktrace()) 38 | end 39 | ``` 40 | 41 | For detailed information on configuration and usage, take a look at the [online documentation](http://hexdocs.pm/rollbax). 42 | 43 | ### Crash reports 44 | 45 | Rollbax provides a way to automatically report crashes from OTP processes (GenServers, Tasks, and so on). It can be enabled with: 46 | 47 | ```elixir 48 | config :rollbax, enable_crash_reports: true 49 | ``` 50 | 51 | For more information, check out the documentation for [`Rollbax.Logger`](http://hexdocs.pm/rollbax/Rollbax.Logger.html). 52 | If you had previously configured `Rollbax.Logger` to be a Logger backend (for example `config :logger, backends: [Rollbax.Logger]`), you will need to remove since `Rollbax.Logger` is not a Logger backend anymore and you will get crashes if you use it as such. 53 | 54 | ### Plug and Phoenix 55 | 56 | For examples on how to take advantage of Rollbax in Plug-based applications (including Phoenix applications), have a look at the ["Using Rollbax in Plug-based applications" page in the documentation](http://hexdocs.pm/rollbax/using-rollbax-in-plug-based-applications.html). 57 | 58 | ### Non-production reporting 59 | 60 | For non-production environments error reporting can be either disabled completely (by setting `:enabled` to `false`) or replaced with logging of exceptions (by setting `:enabled` to `:log`). 61 | 62 | ```elixir 63 | config :rollbax, enabled: :log 64 | ``` 65 | 66 | ### Using a proxy to reach the Rollbar server 67 | 68 | For environments which require a proxy to connect to hosts outside of the network, the `:proxy` config entry can be added with a proxy URL as it's defined in the [hackney documentation](https://github.com/benoitc/hackney#proxy-a-connection). 69 | 70 | ```elixir 71 | config :rollbax, proxy: "https://my-secure-proxy:5001" 72 | ``` 73 | 74 | ## Contributing 75 | 76 | To run tests, run `$ mix test --no-start`. The `--no-start` bit is important so that tests don't fail (because of the `:rollbax` application being started without an `:access_token` specifically). 77 | 78 | When making changes to the code, adhere to this [Elixir style guide](https://github.com/lexmag/elixir-style-guide). 79 | 80 | Finally, thanks for contributing! :) 81 | 82 | ## License 83 | 84 | This software is licensed under [the ISC license](LICENSE). 85 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | config :ex_unit, 4 | assert_receive_timeout: 800, 5 | refute_receive_timeout: 200 6 | -------------------------------------------------------------------------------- /lib/rollbax.ex: -------------------------------------------------------------------------------- 1 | defmodule Rollbax do 2 | @moduledoc """ 3 | This module provides functions to report any kind of exception or message to 4 | [Rollbar](https://rollbar.com). 5 | 6 | ## Configuration 7 | 8 | The `:rollbax` application needs to be configured properly in order to 9 | work. This configuration can be done, for example, in `config/config.exs`: 10 | 11 | config :rollbax, 12 | access_token: "9309123491", 13 | environment: "production" 14 | 15 | The following is a comprehensive list of configuration options supported by Rollbax: 16 | 17 | * `:access_token` - (binary or `nil`) the token needed to access the [Rollbar 18 | Items API (POST)](https://rollbar.com/docs/api/items_post/). As of now, Rollbar provides 19 | several access tokens for different "parts" of their API: for this configuration option, the 20 | `"post_server_item"` access token is needed. This option is required only when the 21 | `:enabled` option is set to `true`, and can be `nil` otherwise. 22 | 23 | * `:environment` - (binary) the environment that will be attached to each reported exception. 24 | 25 | * `:enabled` - (`true | false | :log`) decides whether things reported with `report/5` or 26 | `report_message/4` are actually reported to Rollbar. If `true`, they are reported; if 27 | `false`, `report/5` and `report_message/4` don't do anything; if `:log`, things reported 28 | with `report/5` and `report_message/4` are instead logged to the shell. 29 | 30 | * `:custom` - (map) a map of any arbitrary metadata you want to attach to everything reported 31 | by Rollbax. If custom data is specified in an individual call to `report/5` or 32 | `report_message/5` it will be merged with the global data, with the individual data taking 33 | precedence in case of conflicts. Defaults to `%{}`. 34 | 35 | * `:api_endpoint` - (binary) the Rollbar endpoint to report exceptions and messages to. 36 | Defaults to `https://api.rollbar.com/api/1/item/`. 37 | 38 | * `:enable_crash_reports` - see `Rollbax.Logger`. 39 | 40 | * `:reporters` - see `Rollbax.Logger`. 41 | 42 | * `:proxy` - (binary) a proxy that can be used to connect to the Rollbar host. For more 43 | information about the format of the proxy, check the proxy URL description in the 44 | [hackney documentation](https://github.com/benoitc/hackney#proxy-a-connection). 45 | 46 | ## Runtime configuration 47 | 48 | Configuration can be modified at runtime by providing a configuration callback, like this: 49 | 50 | config :rollbax, 51 | config_callback: {MyModule, :my_function} 52 | 53 | In the example above, `MyModule.my_function/1` will be called with the existing configuration as 54 | an argument. It's supposed to return a keyword list representing a possibly modified 55 | configuration. This can for example be used to read system environment variables at runtime when 56 | the application starts: 57 | 58 | defmodule MyModule do 59 | def my_function(config) do 60 | Keyword.put(config, :access_token, System.get_env("ROLLBAR_ACCESS_TOKEN")) 61 | end 62 | end 63 | 64 | ## Logger backend 65 | 66 | Rollbax provides a module that reports logged crashes and exits to Rollbar. For more 67 | information, look at the documentation for `Rollbax.Logger`. 68 | """ 69 | 70 | use Application 71 | 72 | @allowed_message_levels [:critical, :error, :warning, :info, :debug] 73 | 74 | @doc false 75 | def start(_type, _args) do 76 | config = init_config() 77 | 78 | unless config[:enabled] in [true, false, :log] do 79 | raise ArgumentError, ":enabled may be only one of: true, false, or :log" 80 | end 81 | 82 | if config[:enabled] == true and is_nil(config[:access_token]) do 83 | raise ArgumentError, ":access_token is required when :enabled is true" 84 | end 85 | 86 | if config[:enable_crash_reports] do 87 | # We do this because the handler will read `:reporters` out of the app's environment. 88 | Application.put_env(:rollbax, :reporters, config[:reporters]) 89 | :error_logger.add_report_handler(Rollbax.Logger) 90 | end 91 | 92 | children = [ 93 | {Rollbax.Client, config} 94 | ] 95 | 96 | Supervisor.start_link(children, strategy: :one_for_one) 97 | end 98 | 99 | defp init_config() do 100 | env = Application.get_all_env(:rollbax) 101 | 102 | config = 103 | env 104 | |> Keyword.take([ 105 | :enabled, 106 | :custom, 107 | :api_endpoint, 108 | :enable_crash_reports, 109 | :reporters, 110 | :proxy 111 | ]) 112 | |> put_if_present(:environment, env[:environment]) 113 | |> put_if_present(:access_token, env[:access_token]) 114 | 115 | case Application.get_env(:rollbax, :config_callback) do 116 | {config_callback_mod, config_callback_fun} -> 117 | apply(config_callback_mod, config_callback_fun, [config]) 118 | 119 | nil -> 120 | config 121 | end 122 | end 123 | 124 | defp put_if_present(keyword, key, value) do 125 | if value, do: Keyword.put(keyword, key, value), else: keyword 126 | end 127 | 128 | @doc """ 129 | Reports the given error/exit/throw. 130 | 131 | `kind` specifies the kind of exception being reported while `value` specifies 132 | the value of that exception. `kind` can be: 133 | 134 | * `:error` - reports an exception defined with `defexception`. `value` must 135 | be an exception, or this function will raise an `ArgumentError` exception. 136 | 137 | * `:exit` - reports an exit. `value` can be any term. 138 | 139 | * `:throw` - reports a thrown term. `value` can be any term. 140 | 141 | The `custom` and `occurrence_data` arguments can be used to customize metadata 142 | sent to Rollbar. `custom` is a map of any arbitrary metadata you want to 143 | attach to the exception being reported. `occurrence_data` is a map of 144 | key-value pairs where keys and values should be understood by the [Rollbar 145 | POST API for items](https://rollbar.com/docs/api/items_post/); for example, as 146 | of now Rollbar understands the `"person"` field and uses it to display users 147 | which an exception affected: `occurrence_data` can be used to attach 148 | `"person"` data to an exception being reported. Refer to the Rollbar API 149 | (linked above) for what keys are supported and what the corresponding values 150 | should be. 151 | 152 | This function is **fire-and-forget**: it will always return `:ok` right away and 153 | perform the reporting of the given exception in the background so as to not block 154 | the caller. 155 | 156 | ## Examples 157 | 158 | Exceptions can be reported directly: 159 | 160 | Rollbax.report(:error, ArgumentError.exception("oops"), System.stacktrace()) 161 | #=> :ok 162 | 163 | Often, you'll want to report something you either rescued or caught. For 164 | rescued exceptions: 165 | 166 | try do 167 | raise ArgumentError, "oops" 168 | rescue 169 | exception -> 170 | Rollbax.report(:error, exception, System.stacktrace()) 171 | # You can also reraise the exception here with reraise/2 172 | end 173 | 174 | For caught exceptions: 175 | 176 | try do 177 | throw(:oops) 178 | # or exit(:oops) 179 | catch 180 | kind, value -> 181 | Rollbax.report(kind, value, System.stacktrace()) 182 | end 183 | 184 | Using custom data: 185 | 186 | Rollbax.report(:exit, :oops, System.stacktrace(), %{"weather" => "rainy"}) 187 | 188 | """ 189 | @spec report(:error | :exit | :throw, any, [any], map, map) :: :ok 190 | def report(kind, value, stacktrace, custom \\ %{}, occurrence_data \\ %{}) 191 | when kind in [:error, :exit, :throw] and is_list(stacktrace) and is_map(custom) and 192 | is_map(occurrence_data) do 193 | {class, message} = Rollbax.Item.exception_class_and_message(kind, value) 194 | 195 | report_exception(%Rollbax.Exception{ 196 | class: class, 197 | message: message, 198 | stacktrace: stacktrace, 199 | custom: custom, 200 | occurrence_data: occurrence_data 201 | }) 202 | end 203 | 204 | @doc """ 205 | Reports the given `message`. 206 | 207 | `message` will be reported as a simple Rollbar message, for example, without a stacktrace. 208 | `level` is the level of the message, which can be one of: 209 | 210 | * `:critical` 211 | * `:error` 212 | * `:warning` 213 | * `:info` 214 | * `:debug` 215 | 216 | `custom` and `occurrence_data` work exactly like they do in `report/5`. 217 | 218 | ## Examples 219 | 220 | Rollbax.report_message(:critical, "Everything is on fire!") 221 | #=> :ok 222 | 223 | """ 224 | @spec report_message(:critical | :error | :warning | :info | :debug, IO.chardata(), map, map) :: 225 | :ok 226 | def report_message(level, message, custom \\ %{}, occurrence_data \\ %{}) 227 | when level in @allowed_message_levels and is_map(custom) and is_map(occurrence_data) do 228 | body = message |> IO.chardata_to_string() |> Rollbax.Item.message_body() 229 | Rollbax.Client.emit(level, System.system_time(:second), body, custom, occurrence_data) 230 | end 231 | 232 | @doc false 233 | @spec report_exception(Rollbax.Exception.t()) :: :ok 234 | def report_exception(%Rollbax.Exception{} = exception) do 235 | %{ 236 | class: class, 237 | message: message, 238 | stacktrace: stacktrace, 239 | custom: custom, 240 | occurrence_data: occurrence_data 241 | } = exception 242 | 243 | body = Rollbax.Item.exception_body(class, message, stacktrace) 244 | Rollbax.Client.emit(:error, System.system_time(:second), body, custom, occurrence_data) 245 | end 246 | end 247 | -------------------------------------------------------------------------------- /lib/rollbax/client.ex: -------------------------------------------------------------------------------- 1 | defmodule Rollbax.Client do 2 | @moduledoc false 3 | 4 | # This GenServer keeps a pre-built bare-bones version of an exception (a 5 | # "draft") to be reported to Rollbar, which is then filled with the data 6 | # related to each specific exception when such exception is being 7 | # reported. This GenServer is also responsible for actually sending data to 8 | # the Rollbar API and receiving responses from said API. 9 | 10 | use GenServer 11 | 12 | require Logger 13 | 14 | alias Rollbax.Item 15 | 16 | @name __MODULE__ 17 | @hackney_pool __MODULE__ 18 | @headers [{"content-type", "application/json"}] 19 | 20 | ## GenServer state 21 | 22 | defstruct [:draft, :url, :enabled, :hackney_opts, hackney_responses: %{}, rate_limited?: false] 23 | 24 | ## Public API 25 | 26 | def start_link(config) do 27 | state = %__MODULE__{ 28 | draft: Item.draft(config[:access_token], config[:environment], config[:custom]), 29 | url: config[:api_endpoint], 30 | enabled: config[:enabled], 31 | hackney_opts: build_hackney_opts(config) 32 | } 33 | 34 | GenServer.start_link(__MODULE__, state, name: @name) 35 | end 36 | 37 | defp build_hackney_opts(config) do 38 | hackney_extra_opts = 39 | if proxy = config[:proxy] do 40 | [proxy: proxy] 41 | else 42 | [] 43 | end 44 | 45 | [:async, pool: @hackney_pool] ++ hackney_extra_opts 46 | end 47 | 48 | def emit(level, timestamp, body, custom, occurrence_data) 49 | when is_atom(level) and is_integer(timestamp) and timestamp > 0 and is_map(body) and 50 | is_map(custom) and is_map(occurrence_data) do 51 | if pid = Process.whereis(@name) do 52 | event = {Atom.to_string(level), timestamp, body, custom, occurrence_data} 53 | GenServer.cast(pid, {:emit, event}) 54 | else 55 | Logger.warn( 56 | "(Rollbax) Trying to report an exception but the :rollbax application has not been started", 57 | rollbax: false 58 | ) 59 | end 60 | end 61 | 62 | ## GenServer callbacks 63 | 64 | def init(state) do 65 | Logger.metadata(rollbax: false) 66 | :ok = :hackney_pool.start_pool(@hackney_pool, max_connections: 20) 67 | {:ok, state} 68 | end 69 | 70 | def terminate(_reason, _state) do 71 | :ok = :hackney_pool.stop_pool(@hackney_pool) 72 | end 73 | 74 | def handle_cast({:emit, _event}, %{rate_limited?: true} = state) do 75 | Logger.info("(Rollbax) ignored report due to rate limiting") 76 | {:noreply, state} 77 | end 78 | 79 | def handle_cast({:emit, _event}, %{enabled: false} = state) do 80 | {:noreply, state} 81 | end 82 | 83 | def handle_cast({:emit, event}, %{enabled: :log} = state) do 84 | Logger.info(["(Rollbax) registered report.\n", event_to_chardata(event)]) 85 | {:noreply, state} 86 | end 87 | 88 | def handle_cast({:emit, event}, %{enabled: true} = state) do 89 | case compose_json(state.draft, event) do 90 | {:ok, payload} -> 91 | case :hackney.post(state.url, @headers, payload, state.hackney_opts) do 92 | {:ok, _ref} -> 93 | :ok 94 | 95 | {:error, reason} -> 96 | Logger.error("(Rollbax) connection error: #{inspect(reason)}") 97 | end 98 | 99 | {:error, exception} -> 100 | Logger.error([ 101 | "(Rollbax) failed to encode report below ", 102 | "for reason: ", 103 | Exception.message(exception), 104 | ?\n, 105 | event_to_chardata(event) 106 | ]) 107 | end 108 | 109 | {:noreply, state} 110 | end 111 | 112 | def handle_info({:hackney_response, ref, response}, state) do 113 | new_state = handle_hackney_response(ref, response, state) 114 | {:noreply, new_state} 115 | end 116 | 117 | def handle_info(:lift_rate_limiting, state) do 118 | {:noreply, %{state | rate_limited?: false}} 119 | end 120 | 121 | def handle_info(message, state) do 122 | Logger.info("(Rollbax) unexpected message: #{inspect(message)}") 123 | {:noreply, state} 124 | end 125 | 126 | ## Helper functions 127 | 128 | defp compose_json(draft, event) do 129 | draft 130 | |> Item.compose(event) 131 | |> Jason.encode_to_iodata() 132 | end 133 | 134 | defp event_to_chardata({level, timestamp, body, custom, occurrence_data}) do 135 | [ 136 | inspect(body), 137 | "\nLevel: ", 138 | level, 139 | "\nTimestamp: ", 140 | Integer.to_string(timestamp), 141 | "\nCustom data: ", 142 | inspect(custom), 143 | "\nOccurrence data: ", 144 | inspect(occurrence_data) 145 | ] 146 | end 147 | 148 | defp handle_hackney_response(ref, :done, %{hackney_responses: responses} = state) do 149 | {_code, body} = Map.fetch!(responses, ref) 150 | 151 | case Jason.decode(body) do 152 | {:ok, %{"err" => 1, "message" => message}} when is_binary(message) -> 153 | Logger.error("(Rollbax) API returned an error: #{inspect(message)}") 154 | 155 | {:ok, response} -> 156 | Logger.debug("(Rollbax) API response: #{inspect(response)}") 157 | 158 | {:error, _} -> 159 | Logger.error("(Rollbax) API returned malformed JSON: #{inspect(body)}") 160 | end 161 | 162 | %{state | hackney_responses: Map.delete(responses, ref)} 163 | end 164 | 165 | defp handle_hackney_response( 166 | ref, 167 | {:status, code, description}, 168 | %{hackney_responses: responses} = state 169 | ) do 170 | if code != 200 do 171 | Logger.error("(Rollbax) unexpected API status: #{code}/#{description}") 172 | end 173 | 174 | %{state | hackney_responses: Map.put(responses, ref, {code, []})} 175 | end 176 | 177 | defp handle_hackney_response(ref, {:headers, headers}, %{hackney_responses: responses} = state) do 178 | Logger.debug("(Rollbax) API headers: #{inspect(headers)}") 179 | 180 | # It is possible that we receive multiple rate limited responses. 181 | # Ideally, we should schedule rate limit lifting only once. 182 | with %{^ref => {429, _body}} <- responses, 183 | :ok <- schedule_rate_limit_lifting(headers) do 184 | %{state | rate_limited?: true} 185 | else 186 | _other -> state 187 | end 188 | end 189 | 190 | defp handle_hackney_response(ref, body_chunk, %{hackney_responses: responses} = state) 191 | when is_binary(body_chunk) do 192 | responses = 193 | Map.update!(responses, ref, fn {code, body} -> 194 | {code, [body | body_chunk]} 195 | end) 196 | 197 | %{state | hackney_responses: responses} 198 | end 199 | 200 | defp handle_hackney_response(ref, {:error, reason}, %{hackney_responses: responses} = state) do 201 | Logger.error("(Rollbax) connection error: #{inspect(reason)}") 202 | %{state | hackney_responses: Map.delete(responses, ref)} 203 | end 204 | 205 | defp schedule_rate_limit_lifting(headers) do 206 | with {_, remaining_seconds} when remaining_seconds != nil <- 207 | List.keyfind(headers, "X-Rate-Limit-Remaining-Seconds", 0), 208 | {remaining_seconds, ""} <- Integer.parse(remaining_seconds) do 209 | Process.send_after(self(), :lift_rate_limiting, remaining_seconds * 1_000) 210 | :ok 211 | else 212 | _other -> :error 213 | end 214 | end 215 | end 216 | -------------------------------------------------------------------------------- /lib/rollbax/exception.ex: -------------------------------------------------------------------------------- 1 | defmodule Rollbax.Exception do 2 | @type t :: %__MODULE__{ 3 | class: String.t(), 4 | message: String.t(), 5 | stacktrace: Exception.stacktrace(), 6 | custom: map, 7 | occurrence_data: map 8 | } 9 | 10 | defstruct [ 11 | :class, 12 | :message, 13 | :stacktrace, 14 | custom: %{}, 15 | occurrence_data: %{} 16 | ] 17 | end 18 | -------------------------------------------------------------------------------- /lib/rollbax/item.ex: -------------------------------------------------------------------------------- 1 | defmodule Rollbax.Item do 2 | @moduledoc false 3 | 4 | # This module is responsible for building the payload for a Rollbar "Item". 5 | # Refer to https://rollbar.com/docs/api/items_post for documentation on such 6 | # payload. 7 | 8 | @spec draft(String.t() | nil, String.t() | nil, map) :: map 9 | def draft(token, environment, custom) 10 | when (is_binary(token) or is_nil(token)) and (is_binary(environment) or is_nil(environment)) and 11 | is_map(custom) do 12 | data = %{ 13 | "server" => %{ 14 | "host" => host() 15 | }, 16 | "environment" => environment, 17 | "language" => "Elixir v" <> System.version(), 18 | "platform" => System.otp_release(), 19 | "notifier" => notifier() 20 | } 21 | 22 | %{ 23 | "access_token" => token, 24 | "data" => put_custom(data, custom) 25 | } 26 | end 27 | 28 | @spec compose(map, {String.t(), pos_integer, map, map, map}) :: map 29 | def compose(draft, {level, timestamp, body, custom, occurrence_data}) 30 | when is_map(draft) and is_binary(level) and is_integer(timestamp) and timestamp > 0 and 31 | is_map(body) and is_map(custom) and is_map(occurrence_data) do 32 | Map.update!(draft, "data", fn data -> 33 | data 34 | |> Map.merge(occurrence_data) 35 | |> put_custom(custom) 36 | |> Map.put("body", body) 37 | |> Map.put("level", level) 38 | |> Map.put("timestamp", timestamp) 39 | end) 40 | end 41 | 42 | @doc """ 43 | Returns a map representing the body to be used for representing an "exception" 44 | on Rollbar. 45 | 46 | `class` and `message` are strings that will be used as the class and message 47 | of the reported exception. `stacktrace` is the stacktrace of the error. 48 | """ 49 | @spec exception_body(String.t(), String.t(), [any]) :: map 50 | def exception_body(class, message, stacktrace) 51 | when is_binary(class) and is_binary(message) and is_list(stacktrace) do 52 | %{ 53 | "trace" => %{ 54 | "frames" => stacktrace_to_frames(stacktrace), 55 | "exception" => %{ 56 | "class" => class, 57 | "message" => message 58 | } 59 | } 60 | } 61 | end 62 | 63 | @doc """ 64 | Returns a map representing the body to be used for representing a "message" on 65 | Rollbar. 66 | """ 67 | @spec message_body(String.t()) :: map 68 | def message_body(message) when is_binary(message) do 69 | %{"message" => %{"body" => message}} 70 | end 71 | 72 | @doc """ 73 | Returns the exception class and message for the given Elixir error. 74 | 75 | `kind` can be one of `:throw`, `:exit`, or `:error`. A `{class, message}` 76 | tuple is returned. 77 | """ 78 | @spec exception_class_and_message(:throw | :exit | :error, any) :: {String.t(), String.t()} 79 | def exception_class_and_message(kind, value) 80 | 81 | def exception_class_and_message(:throw, value) do 82 | {"throw", inspect(value)} 83 | end 84 | 85 | def exception_class_and_message(:exit, value) do 86 | message = 87 | if Exception.exception?(value) do 88 | Exception.format_banner(:error, value) 89 | else 90 | Exception.format_exit(value) 91 | end 92 | 93 | {"exit", message} 94 | end 95 | 96 | def exception_class_and_message(:error, error) do 97 | exception = Exception.normalize(:error, error) 98 | {inspect(exception.__struct__), Exception.message(exception)} 99 | end 100 | 101 | defp stacktrace_to_frames(stacktrace) do 102 | Enum.map(stacktrace, &stacktrace_entry_to_frame/1) 103 | end 104 | 105 | defp stacktrace_entry_to_frame({module, fun, arity, location}) when is_integer(arity) do 106 | method = Exception.format_mfa(module, fun, arity) <> maybe_format_application(module) 107 | put_location(%{"method" => method}, location) 108 | end 109 | 110 | defp stacktrace_entry_to_frame({module, fun, arity, location}) when is_list(arity) do 111 | method = Exception.format_mfa(module, fun, length(arity)) <> maybe_format_application(module) 112 | args = Enum.map(arity, &inspect/1) 113 | put_location(%{"method" => method, "args" => args}, location) 114 | end 115 | 116 | defp stacktrace_entry_to_frame({fun, arity, location}) when is_integer(arity) do 117 | %{"method" => Exception.format_fa(fun, arity)} 118 | |> put_location(location) 119 | end 120 | 121 | defp stacktrace_entry_to_frame({fun, arity, location}) when is_list(arity) do 122 | %{"method" => Exception.format_fa(fun, length(arity)), "args" => Enum.map(arity, &inspect/1)} 123 | |> put_location(location) 124 | end 125 | 126 | defp maybe_format_application(module) do 127 | case :application.get_application(module) do 128 | {:ok, application} -> 129 | " (" <> Atom.to_string(application) <> ")" 130 | 131 | :undefined -> 132 | "" 133 | end 134 | end 135 | 136 | defp put_location(frame, location) do 137 | if file = location[:file] do 138 | frame = Map.put(frame, "filename", List.to_string(file)) 139 | 140 | if line = location[:line] do 141 | Map.put(frame, "lineno", line) 142 | else 143 | frame 144 | end 145 | else 146 | frame 147 | end 148 | end 149 | 150 | defp put_custom(data, custom) do 151 | if map_size(custom) == 0 do 152 | data 153 | else 154 | Map.update(data, "custom", custom, &Map.merge(&1, custom)) 155 | end 156 | end 157 | 158 | defp host() do 159 | {:ok, host} = :inet.gethostname() 160 | List.to_string(host) 161 | end 162 | 163 | defp notifier() do 164 | %{ 165 | "name" => "Rollbax", 166 | "version" => unquote(Mix.Project.config()[:version]) 167 | } 168 | end 169 | end 170 | -------------------------------------------------------------------------------- /lib/rollbax/logger.ex: -------------------------------------------------------------------------------- 1 | defmodule Rollbax.Logger do 2 | @moduledoc """ 3 | A module that can be used to report crashes and exits to Rollbar. 4 | 5 | In Elixir and Erlang, crashes from GenServers and other processes are reported through 6 | `:error_logger`. When installed, this module installs an `:error_logger` handler that can be 7 | used to report such crashes to Rollbar automatically. 8 | 9 | In order to use this functionality, you must configure the `:rollbax` application to report 10 | crashes with: 11 | 12 | config :rollbax, :enable_crash_reports, true 13 | 14 | All the configuration options for reporting crashes are documented in detail below. If you are 15 | upgrading from an older version of Rollbax that used this module as a logger backend via 16 | `config :logger, backends: [:console, Rollbax.Logger]` this config should be removed. 17 | 18 | `Rollbax.Logger` implements a mechanism of reporting based on *reporters*, which are modules 19 | that implement the `Rollbax.Reporter` behaviour. Every message received by `Rollbax.Logger` is 20 | run through a list of reporters and the behaviour is determined by the return value of each 21 | reporter's `c:Rollbax.Reporter.handle_event/2` callback: 22 | 23 | * when the callback returns a `Rollbax.Exception` struct, the exception is reported to Rollbar 24 | and no other reporters are called 25 | 26 | * when the callback returns `:next`, the reporter is skipped and Rollbax moves on to the next 27 | reporter 28 | 29 | * when the callback returns `:ignore`, the reported message is ignored and no more reporters 30 | are tried. 31 | 32 | The list of reporters can be configured in the `:reporters` key in the `:rollbax` application 33 | configuration. By default this list only contains `Rollbax.Reporter.Standard` (see its 34 | documentation for more information). Rollbax also comes equipped with a 35 | `Rollbax.Reporter.Silencing` reporter that doesn't report anything it receives. For examples on 36 | how to provide your own reporters, look at the source for `Rollbax.Reporter.Standard`. 37 | 38 | ## Configuration 39 | 40 | The following reporting-related options can be used to configure the `:rollbax` application: 41 | 42 | * `:enable_crash_reports` (boolean) - when `true`, `Rollbax.Logger` is registered as an 43 | `:error_logger` handler and the whole reporting flow described above is executed. 44 | 45 | * `:reporters` (list) - a list of modules implementing the `Rollbax.Reporter` behaviour. 46 | Defaults to `[Rollbax.Reporter.Standard]`. 47 | 48 | """ 49 | 50 | @behaviour :gen_event 51 | 52 | defstruct [:reporters] 53 | 54 | @doc false 55 | def init(_args) do 56 | reporters = Application.get_env(:rollbax, :reporters, [Rollbax.Reporter.Standard]) 57 | {:ok, %__MODULE__{reporters: reporters}} 58 | end 59 | 60 | @doc false 61 | def handle_event(event, state) 62 | 63 | # If the event is on a different node than the current node, we ignore it. 64 | def handle_event({_level, gl, _event}, state) 65 | when node(gl) != node() do 66 | {:ok, state} 67 | end 68 | 69 | def handle_event({level, _gl, event}, %__MODULE__{reporters: reporters} = state) do 70 | :ok = run_reporters(reporters, level, event) 71 | {:ok, state} 72 | end 73 | 74 | @doc false 75 | def handle_call(request, _state) do 76 | exit({:bad_call, request}) 77 | end 78 | 79 | @doc false 80 | def handle_info(_message, state) do 81 | {:ok, state} 82 | end 83 | 84 | @doc false 85 | def terminate(_reason, _state) do 86 | :ok 87 | end 88 | 89 | @doc false 90 | def code_change(_old_vsn, state, _extra) do 91 | {:ok, state} 92 | end 93 | 94 | defp run_reporters([reporter | rest], level, event) do 95 | case reporter.handle_event(level, event) do 96 | %Rollbax.Exception{} = exception -> 97 | Rollbax.report_exception(exception) 98 | 99 | :next -> 100 | run_reporters(rest, level, event) 101 | 102 | :ignore -> 103 | :ok 104 | end 105 | end 106 | 107 | # If no reporter ignored or reported this event, then we're gonna report this 108 | # as a Rollbar "message" with the same logic that Logger uses to translate 109 | # messages (so that it will have Elixir syntax when reported). 110 | defp run_reporters([], _level, _event) do 111 | :ok 112 | end 113 | end 114 | -------------------------------------------------------------------------------- /lib/rollbax/reporter.ex: -------------------------------------------------------------------------------- 1 | defmodule Rollbax.Reporter do 2 | @moduledoc """ 3 | Behaviour to be implemented by Rollbax reporters that wish to report `:error_logger` messages to 4 | Rollbar. See `Rollbax.Logger` for more information. 5 | """ 6 | 7 | @callback handle_event(type :: term, event :: term) :: Rollbax.Exception.t() | :next | :ignore 8 | end 9 | -------------------------------------------------------------------------------- /lib/rollbax/reporter/silencing.ex: -------------------------------------------------------------------------------- 1 | defmodule Rollbax.Reporter.Silencing do 2 | @moduledoc """ 3 | A `Rollbax.Reporter` that ignores all messages that go through it. 4 | """ 5 | 6 | @behaviour Rollbax.Reporter 7 | 8 | def handle_event(_type, _event) do 9 | :ignore 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/rollbax/reporter/standard.ex: -------------------------------------------------------------------------------- 1 | defmodule Rollbax.Reporter.Standard do 2 | @moduledoc """ 3 | A `Rollbax.Reporter` that translates crashes and exits from processes to nicely-formatted 4 | Rollbar exceptions. 5 | """ 6 | 7 | @behaviour Rollbax.Reporter 8 | 9 | def handle_event(:error, {_pid, format, data}) do 10 | handle_error_format(format, data) 11 | end 12 | 13 | def handle_event(_type, _event) do 14 | :next 15 | end 16 | 17 | # Errors in a GenServer. 18 | defp handle_error_format('** Generic server ' ++ _, [name, last_message, state, reason]) do 19 | {class, message, stacktrace} = format_as_exception(reason, "GenServer terminating") 20 | 21 | %Rollbax.Exception{ 22 | class: class, 23 | message: message, 24 | stacktrace: stacktrace, 25 | custom: %{ 26 | "name" => inspect(name), 27 | "last_message" => inspect(last_message), 28 | "state" => inspect(state) 29 | } 30 | } 31 | end 32 | 33 | # Errors in a GenEvent handler. 34 | defp handle_error_format('** gen_event handler ' ++ _, [ 35 | name, 36 | manager, 37 | last_message, 38 | state, 39 | reason 40 | ]) do 41 | {class, message, stacktrace} = format_as_exception(reason, "gen_event handler terminating") 42 | 43 | %Rollbax.Exception{ 44 | class: class, 45 | message: message, 46 | stacktrace: stacktrace, 47 | custom: %{ 48 | "name" => inspect(name), 49 | "manager" => inspect(manager), 50 | "last_message" => inspect(last_message), 51 | "state" => inspect(state) 52 | } 53 | } 54 | end 55 | 56 | # Errors in a task. 57 | defp handle_error_format('** Task ' ++ _, [name, starter, function, arguments, reason]) do 58 | {class, message, stacktrace} = format_as_exception(reason, "Task terminating") 59 | 60 | %Rollbax.Exception{ 61 | class: class, 62 | message: message, 63 | stacktrace: stacktrace, 64 | custom: %{ 65 | "name" => inspect(name), 66 | "started_from" => inspect(starter), 67 | "function" => inspect(function), 68 | "arguments" => inspect(arguments) 69 | } 70 | } 71 | end 72 | 73 | defp handle_error_format('** State machine ' ++ _ = message, data) do 74 | if charlist_contains?(message, 'Callback mode') do 75 | :next 76 | else 77 | handle_gen_fsm_error(data) 78 | end 79 | end 80 | 81 | # Errors in a regular process. 82 | defp handle_error_format('Error in process ' ++ _, [pid, {reason, stacktrace}]) do 83 | exception = Exception.normalize(:error, reason) 84 | 85 | %Rollbax.Exception{ 86 | class: "error in process (#{inspect(exception.__struct__)})", 87 | message: Exception.message(exception), 88 | stacktrace: stacktrace, 89 | custom: %{ 90 | "pid" => inspect(pid) 91 | } 92 | } 93 | end 94 | 95 | # Any other error (for example, the ones logged through 96 | # :error_logger.error_msg/1). This reporter doesn't report those to Rollbar. 97 | defp handle_error_format(_format, _data) do 98 | :next 99 | end 100 | 101 | defp handle_gen_fsm_error([name, last_event, state, data, reason]) do 102 | {class, message, stacktrace} = format_as_exception(reason, "State machine terminating") 103 | 104 | %Rollbax.Exception{ 105 | class: class, 106 | message: message, 107 | stacktrace: stacktrace, 108 | custom: %{ 109 | "name" => inspect(name), 110 | "last_event" => inspect(last_event), 111 | "state" => inspect(state), 112 | "data" => inspect(data) 113 | } 114 | } 115 | end 116 | 117 | defp handle_gen_fsm_error(_data) do 118 | :next 119 | end 120 | 121 | defp format_as_exception({maybe_exception, [_ | _] = maybe_stacktrace} = reason, class) do 122 | # We do this &Exception.format_stacktrace_entry/1 dance just to ensure that 123 | # "maybe_stacktrace" is a valid stacktrace. If it's not, 124 | # Exception.format_stacktrace_entry/1 will raise an error and we'll treat it 125 | # as not a stacktrace. 126 | try do 127 | Enum.each(maybe_stacktrace, &Exception.format_stacktrace_entry/1) 128 | catch 129 | :error, _ -> 130 | format_stop_as_exception(reason, class) 131 | else 132 | :ok -> 133 | format_error_as_exception(maybe_exception, maybe_stacktrace, class) 134 | end 135 | end 136 | 137 | defp format_as_exception(reason, class) do 138 | format_stop_as_exception(reason, class) 139 | end 140 | 141 | defp format_stop_as_exception(reason, class) do 142 | {class <> " (stop)", Exception.format_exit(reason), _stacktrace = []} 143 | end 144 | 145 | defp format_error_as_exception(reason, stacktrace, class) do 146 | case Exception.normalize(:error, reason, stacktrace) do 147 | %ErlangError{} -> 148 | {class, Exception.format_exit(reason), stacktrace} 149 | 150 | exception -> 151 | class = class <> " (" <> inspect(exception.__struct__) <> ")" 152 | {class, Exception.message(exception), stacktrace} 153 | end 154 | end 155 | 156 | defp charlist_contains?(charlist, part) do 157 | :string.str(charlist, part) != 0 158 | end 159 | end 160 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Rollbax.Mixfile do 2 | use Mix.Project 3 | 4 | @version "0.11.0" 5 | 6 | @default_api_endpoint "https://api.rollbar.com/api/1/item/" 7 | 8 | def project() do 9 | [ 10 | app: :rollbax, 11 | version: @version, 12 | elixir: "~> 1.5", 13 | build_embedded: Mix.env() == :prod, 14 | start_permanent: Mix.env() == :prod, 15 | description: "Exception tracking and logging from Elixir to Rollbar", 16 | package: package(), 17 | deps: deps(), 18 | aliases: [test: "test --no-start"], 19 | name: "Rollbax", 20 | docs: [ 21 | main: "Rollbax", 22 | source_ref: "v#{@version}", 23 | source_url: "https://github.com/elixir-addicts/rollbax", 24 | extras: ["pages/Using Rollbax in Plug-based applications.md"] 25 | ] 26 | ] 27 | end 28 | 29 | def application() do 30 | [ 31 | extra_applications: [:logger], 32 | env: env(), 33 | mod: {Rollbax, []} 34 | ] 35 | end 36 | 37 | defp deps() do 38 | [ 39 | {:hackney, "~> 1.1"}, 40 | {:jason, "~> 1.0"}, 41 | {:ex_doc, "~> 0.18", only: :dev, runtime: false}, 42 | {:plug, "~> 1.4", only: :test}, 43 | {:cowboy, "~> 1.1", only: :test} 44 | ] 45 | end 46 | 47 | defp package() do 48 | [ 49 | maintainers: ["Aleksei Magusev", "Andrea Leopardi", "Eric Meadows-Jönsson"], 50 | licenses: ["ISC"], 51 | links: %{"GitHub" => "https://github.com/elixir-addicts/rollbax"} 52 | ] 53 | end 54 | 55 | defp env() do 56 | [ 57 | enabled: true, 58 | custom: %{}, 59 | api_endpoint: @default_api_endpoint, 60 | enable_crash_reports: false, 61 | reporters: [Rollbax.Reporter.Standard] 62 | ] 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "cowboy": {:hex, :cowboy, "1.1.2", "61ac29ea970389a88eca5a65601460162d370a70018afe6f949a29dca91f3bb0", [:rebar3], [{:cowlib, "~> 1.0.2", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.3.2", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "f4763bbe08233eceed6f24bc4fcc8d71c17cfeafa6439157c57349aa1bb4f17c"}, 3 | "cowlib": {:hex, :cowlib, "1.0.2", "9d769a1d062c9c3ac753096f868ca121e2730b9a377de23dec0f7e08b1df84ee", [:make], [], "hexpm", "db622da03aa039e6366ab953e31186cc8190d32905e33788a1acb22744e6abd2"}, 4 | "earmark": {:hex, :earmark, "1.2.4", "99b637c62a4d65a20a9fb674b8cffb8baa771c04605a80c911c4418c69b75439", [:mix], [], "hexpm", "1b34655872366414f69dd987cb121c049f76984b6ac69f52fff6d8fd64d29cfd"}, 5 | "ex_doc": {:hex, :ex_doc, "0.18.1", "37c69d2ef62f24928c1f4fdc7c724ea04aecfdf500c4329185f8e3649c915baf", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}], "hexpm", "f050061c87ad39478c942995b5a20c40f2c0bc06525404613b8b0474cb8bd796"}, 6 | "hackney": {:hex, :hackney, "1.3.2", "43bd07ab88753f5e136e38fddd2a09124bee25733b03361eeb459d0173fc17ab", [:make, :rebar], [{:idna, "~> 1.0.2", [hex: :idna, repo: "hexpm", optional: false]}, {:ssl_verify_hostname, "~> 1.0.5", [hex: :ssl_verify_hostname, repo: "hexpm", optional: false]}], "hexpm", "9b811cff637b29f9c7e2c61abf01986c85cd4f64a9422315fd803993b4e82615"}, 7 | "idna": {:hex, :idna, "1.0.3", "d456a8761cad91c97e9788c27002eb3b773adaf5c893275fc35ba4e3434bbd9b", [:rebar3], [], "hexpm", "357d489a51112db4f216034406834f9172b3c0ff5a12f83fb28b25ca271541d1"}, 8 | "jason": {:hex, :jason, "1.2.2", "ba43e3f2709fd1aa1dce90aaabfd039d000469c05c56f0b8e31978e03fa39052", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "18a228f5f0058ee183f29f9eae0805c6e59d61c3b006760668d8d18ff0d12179"}, 9 | "mime": {:hex, :mime, "1.0.1", "05c393850524767d13a53627df71beeebb016205eb43bfbd92d14d24ec7a1b51", [:mix], [], "hexpm", "8aad5eef6d9d20899918868b10e79fc2dafe72a79102882c2947999c10b30cd9"}, 10 | "plug": {:hex, :plug, "1.4.3", "236d77ce7bf3e3a2668dc0d32a9b6f1f9b1f05361019946aae49874904be4aed", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1", [hex: :cowboy, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}], "hexpm", "c1c408c57a1e4c88c365b9aff1198c350e22b765dbb97a460e9e6bd9364c6194"}, 11 | "ranch": {:hex, :ranch, "1.3.2", "e4965a144dc9fbe70e5c077c65e73c57165416a901bd02ea899cfd95aa890986", [:rebar3], [], "hexpm", "6e56493a862433fccc3aca3025c946d6720d8eedf6e3e6fb911952a7071c357f"}, 12 | "ssl_verify_hostname": {:hex, :ssl_verify_hostname, "1.0.6", "45866d958d9ae51cfe8fef0050ab8054d25cba23ace43b88046092aa2c714645", [:make], [], "hexpm", "72b2fc8a8e23d77eed4441137fefa491bbf4a6dc52e9c0045f3f8e92e66243b5"}, 13 | } 14 | -------------------------------------------------------------------------------- /pages/Using Rollbax in Plug-based applications.md: -------------------------------------------------------------------------------- 1 | # Using Rollbax in Plug-based applications 2 | 3 | [Plug](https://github.com/elixir-lang/plug) provides the `Plug.ErrorHandler` plug which plays very well with Rollbax. As you can see in [the documentation for `Plug.ErrorHandler`](https://hexdocs.pm/plug/Plug.ErrorHandler.html), this plug can be used to "catch" exceptions that happen inside a given plug and act on them. This can be used to report all exceptions happening in that plug to Rollbar. For example: 4 | 5 | ```elixir 6 | defmodule MyApp.Router do 7 | use Plug.Router # or `use MyApp.Web, :router` for Phoenix apps 8 | use Plug.ErrorHandler 9 | 10 | defp handle_errors(conn, %{kind: kind, reason: reason, stack: stacktrace}) do 11 | Rollbax.report(kind, reason, stacktrace) 12 | end 13 | end 14 | ``` 15 | 16 | Rollbax also supports attaching *metadata* to a reported exception as well as overriding Rollbar data for a reported exception. Both these can be used to have more detailed reports. For example, in the code snippet above, we could report the request parameters as metadata to be attached to the exception: 17 | 18 | ```elixir 19 | defp handle_errors(conn, %{kind: kind, reason: reason, stack: stacktrace}) do 20 | Rollbax.report(kind, reason, stacktrace, %{method: conn.method}) 21 | end 22 | ``` 23 | 24 | Since Rollbar supports the concept of "request" and "server" in the [Item POST API](https://rollbar.com/docs/api/items_post/), a lot of data that Rollbar will be able to understand can be attached to a reported exceptions. These data is not referred as "custom data" but rather as **occurrence data**. To add data about the host, the request, and more that Rollbax understands pass them as the `occurrence_data` argument. For example: 25 | 26 | ```elixir 27 | defp handle_errors(conn, %{kind: kind, reason: reason, stack: stacktrace}) do 28 | conn = 29 | conn 30 | |> Plug.Conn.fetch_cookies() 31 | |> Plug.Conn.fetch_query_params() 32 | 33 | params = 34 | case conn.params do 35 | %Plug.Conn.Unfetched{aspect: :params} -> "unfetched" 36 | other -> other 37 | end 38 | 39 | occurrence_data = %{ 40 | "request" => %{ 41 | "cookies" => conn.req_cookies, 42 | "url" => Plug.Conn.request_url(conn), 43 | "user_ip" => List.to_string(:inet.ntoa(conn.remote_ip)), 44 | "headers" => Enum.into(conn.req_headers, %{}), 45 | "method" => conn.method, 46 | "params" => params, 47 | }, 48 | "server" => %{ 49 | "pid" => System.get_env("MY_SERVER_PID"), 50 | "host" => "#{System.get_env("MY_HOSTNAME")}:#{System.get_env("MY_PORT")}", 51 | "root" => System.get_env("MY_APPLICATION_PATH"), 52 | }, 53 | } 54 | 55 | Rollbax.report(kind, reason, stacktrace, _custom_data = %{}, occurrence_data) 56 | end 57 | ``` 58 | 59 | Check the [documentation for the Rollbar API](https://rollbar.com/docs/api/items_post/) for all the supported values that can form a "request". 60 | 61 | ## Sensitive data 62 | 63 | In the examples above, *all* parameters are fetched from the connection and forwarded to Rollbar (in the `"params"` key); this means that any sensitive data such as passwords or authentication keys will be sent to Rollbar as well. A good idea may be to scrub any sensitive data out of the parameters before reporting errors to Rollbar. For example: 64 | 65 | ```elixir 66 | defp handle_errors(conn, error) do 67 | conn = 68 | conn 69 | |> Plug.Conn.fetch_cookies() 70 | |> Plug.Conn.fetch_query_params() 71 | 72 | params = normalize_params(conn.params) 73 | 74 | # Same as the examples above 75 | end 76 | 77 | defp normalize_params(%Plug.Conn.Unfetched{aspect: :params}) do 78 | "unfetched" 79 | end 80 | 81 | defp normalize_params(%{} = map) do 82 | Enum.into(map, %{}, fn {key, value} -> 83 | if is_binary(key) and String.contains?(key, ["password"]) do 84 | {key, "[FILTERED]"} 85 | else 86 | {key, normalize_params(value)} 87 | end 88 | end) 89 | end 90 | 91 | defp normalize_params([_ | _] = list) do 92 | Enum.map(list, &normalize_params/1) 93 | end 94 | 95 | defp normalize_params(other), do: other 96 | ``` 97 | -------------------------------------------------------------------------------- /test/rollbax/client_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Rollbax.ClientTest do 2 | use ExUnit.RollbaxCase 3 | 4 | alias Rollbax.Client 5 | 6 | setup_all do 7 | {:ok, pid} = start_rollbax_client("token1", "test", %{qux: "custom"}) 8 | 9 | on_exit(fn -> 10 | ensure_rollbax_client_down(pid) 11 | end) 12 | end 13 | 14 | setup do 15 | {:ok, _} = RollbarAPI.start(self()) 16 | on_exit(&RollbarAPI.stop/0) 17 | end 18 | 19 | describe "emit/5" do 20 | test "fills in the right data" do 21 | body = %{"message" => %{"body" => "pass"}} 22 | custom = %{foo: "bar"} 23 | :ok = Client.emit(:warn, System.system_time(:second), body, custom, %{}) 24 | 25 | assert %{ 26 | "access_token" => "token1", 27 | "data" => %{ 28 | "environment" => "test", 29 | "level" => "warn", 30 | "body" => %{"message" => %{"body" => "pass"}}, 31 | "custom" => %{"foo" => "bar", "qux" => "custom"} 32 | } 33 | } = assert_performed_request() 34 | end 35 | 36 | test "gives precedence to custom values over global ones" do 37 | body = %{"message" => %{"body" => "pass"}} 38 | custom = %{qux: "overridden", quux: "another"} 39 | :ok = Client.emit(:warn, System.system_time(:second), body, custom, %{}) 40 | 41 | assert assert_performed_request()["data"]["custom"] == 42 | %{"qux" => "overridden", "quux" => "another"} 43 | end 44 | 45 | test "gives precedence to user occurrence data over data from Rollbax" do 46 | body = %{"message" => %{"body" => "pass"}} 47 | occurrence_data = %{"server" => %{"host" => "example.net"}} 48 | :ok = Client.emit(:warn, System.system_time(:second), body, _custom = %{}, occurrence_data) 49 | 50 | assert assert_performed_request()["data"]["server"] == %{"host" => "example.net"} 51 | end 52 | end 53 | 54 | test "mass sending" do 55 | body = %{"message" => %{"body" => "pass"}} 56 | 57 | Enum.each(1..60, fn _ -> 58 | :ok = Client.emit(:error, System.system_time(:second), body, %{}, %{}) 59 | end) 60 | 61 | Enum.each(1..60, fn _ -> 62 | assert_performed_request() 63 | end) 64 | end 65 | 66 | test "endpoint is down" do 67 | :ok = RollbarAPI.stop() 68 | 69 | log = 70 | capture_log(fn -> 71 | payload = %{"message" => %{"body" => "miss"}} 72 | :ok = Client.emit(:error, System.system_time(:second), payload, %{}, %{}) 73 | end) 74 | 75 | assert log =~ "[error] (Rollbax) connection error: :econnrefused" 76 | refute_receive {:api_request, _body} 77 | end 78 | 79 | test "rate limiting" do 80 | body = %{"message" => %{"body" => "pass"}} 81 | 82 | log = 83 | capture_log(fn -> 84 | :ok = 85 | Client.emit(:error, System.system_time(:second), body, %{rate_limit_seconds: "1"}, %{}) 86 | end) 87 | 88 | assert log =~ "unexpected API status: 429/Too Many Requests" 89 | 90 | assert_performed_request() 91 | 92 | Process.sleep(100) 93 | 94 | log = 95 | capture_log(fn -> 96 | :ok = Client.emit(:error, System.system_time(:second), body, %{}, %{}) 97 | end) 98 | 99 | assert log =~ "(Rollbax) ignored report due to rate limiting" 100 | refute_receive {:api_request, _body} 101 | 102 | Process.sleep(1000) 103 | 104 | :ok = Client.emit(:error, System.system_time(:second), body, %{}, %{}) 105 | 106 | assert_performed_request() 107 | end 108 | 109 | test "errors from the API are logged" do 110 | log = 111 | capture_log(fn -> 112 | :ok = Client.emit(:error, System.system_time(:second), %{}, %{return_error?: true}, %{}) 113 | assert_performed_request() 114 | end) 115 | 116 | assert log =~ ~s{[error] (Rollbax) unexpected API status: 400} 117 | assert log =~ ~s{[error] (Rollbax) API returned an error: "that was a bad request"} 118 | end 119 | 120 | test "invalid item failure" do 121 | log = 122 | capture_log(fn -> 123 | payload = %{"message" => %{"body" => <<208>>}} 124 | :ok = Client.emit(:error, System.system_time(:second), payload, %{}, %{}) 125 | refute_receive {:api_request, _body} 126 | end) 127 | 128 | assert log =~ "[error] (Rollbax) failed to encode report below for reason: invalid byte 0xD0" 129 | 130 | assert log =~ ~r""" 131 | %{"message" => %{"body" => <<208>>}} 132 | Level: error 133 | Timestamp: \d+ 134 | Custom data: %{} 135 | Occurrence data: %{} 136 | """ 137 | end 138 | end 139 | -------------------------------------------------------------------------------- /test/rollbax/client_via_proxy_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Rollbax.ClientViaProxyTest do 2 | use ExUnit.RollbaxCase 3 | 4 | alias Rollbax.Client 5 | 6 | setup_all do 7 | {:ok, pid} = 8 | start_rollbax_client( 9 | "token1", 10 | "test", 11 | %{proxied: "yes"}, 12 | "http://localhost:5005", 13 | "http://localhost:7004" 14 | ) 15 | 16 | on_exit(fn -> 17 | ensure_rollbax_client_down(pid) 18 | end) 19 | end 20 | 21 | setup do 22 | {:ok, _} = RollbarAPI.start(self(), 7004) 23 | on_exit(&RollbarAPI.stop/0) 24 | end 25 | 26 | describe "with :proxy" do 27 | test "client sends message to proxy server" do 28 | body = %{"message" => %{"body" => "pass"}} 29 | occurrence_data = %{"server" => %{"host" => "example.net"}} 30 | :ok = Client.emit(:warn, System.system_time(:second), body, _custom = %{}, occurrence_data) 31 | 32 | assert_receive {:api_request, body} 33 | json_body = Jason.decode!(body) 34 | assert json_body["data"]["server"] == %{"host" => "example.net"} 35 | assert json_body["data"]["custom"] == %{"proxied" => "yes"} 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /test/rollbax/logger_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Rollbax.LoggerTest do 2 | use ExUnit.RollbaxCase 3 | 4 | setup_all do 5 | {:ok, pid} = start_rollbax_client("token1", "test") 6 | 7 | on_exit(fn -> 8 | ensure_rollbax_client_down(pid) 9 | end) 10 | end 11 | 12 | setup context do 13 | {:ok, _pid} = RollbarAPI.start(self()) 14 | 15 | if reporters = context[:reporters] do 16 | Application.put_env(:rollbax, :reporters, reporters) 17 | else 18 | Application.delete_env(:rollbax, :reporters) 19 | end 20 | 21 | :error_logger.add_report_handler(Rollbax.Logger) 22 | 23 | on_exit(fn -> 24 | RollbarAPI.stop() 25 | :error_logger.delete_report_handler(Rollbax.Logger) 26 | end) 27 | end 28 | 29 | test "GenServer terminating with an Elixir error" do 30 | defmodule Elixir.MyGenServer do 31 | use GenServer 32 | 33 | def init(args), do: {:ok, args} 34 | 35 | def handle_cast(:raise_elixir, _state) do 36 | Map.fetch!(Map.new(), :nonexistent_key) 37 | end 38 | end 39 | 40 | {:ok, gen_server} = GenServer.start(MyGenServer, {}) 41 | 42 | capture_log(fn -> 43 | GenServer.cast(gen_server, :raise_elixir) 44 | end) 45 | 46 | data = assert_performed_request()["data"] 47 | 48 | # Check the exception. 49 | assert data["body"]["trace"]["exception"] == %{ 50 | "class" => "GenServer terminating (KeyError)", 51 | "message" => "key :nonexistent_key not found in: %{}" 52 | } 53 | 54 | assert [frame] = find_frames_for_current_file(data["body"]["trace"]["frames"]) 55 | assert frame["method"] == "MyGenServer.handle_cast/2" 56 | 57 | assert data["custom"]["last_message"] =~ "$gen_cast" 58 | assert data["custom"]["name"] == inspect(gen_server) 59 | assert data["custom"]["state"] == "{}" 60 | after 61 | purge_module(MyGenServer) 62 | end 63 | 64 | test "GenServer terminating with an Erlang error" do 65 | defmodule Elixir.MyGenServer do 66 | use GenServer 67 | 68 | def init(args), do: {:ok, args} 69 | 70 | def handle_cast(:raise_erlang, state) do 71 | :maps.find(:a_key, [:not_a, %{}]) 72 | {:noreply, state} 73 | end 74 | end 75 | 76 | {:ok, gen_server} = GenServer.start(MyGenServer, {}) 77 | 78 | capture_log(fn -> 79 | GenServer.cast(gen_server, :raise_erlang) 80 | end) 81 | 82 | data = assert_performed_request()["data"] 83 | 84 | assert data["body"]["trace"]["exception"] == %{ 85 | "class" => "GenServer terminating (BadMapError)", 86 | "message" => "expected a map, got: [:not_a, %{}]" 87 | } 88 | 89 | assert [frame] = find_frames_for_current_file(data["body"]["trace"]["frames"]) 90 | assert frame["method"] == "MyGenServer.handle_cast/2" 91 | 92 | assert data["custom"]["last_message"] =~ "$gen_cast" 93 | assert data["custom"]["name"] == inspect(gen_server) 94 | assert data["custom"]["state"] == "{}" 95 | after 96 | purge_module(MyGenServer) 97 | end 98 | 99 | test "GenServer terminating because of an exit" do 100 | defmodule Elixir.MyGenServer do 101 | use GenServer 102 | 103 | def init(args), do: {:ok, args} 104 | 105 | def handle_cast(:call_self, state) do 106 | GenServer.call(self(), {:call, :self}) 107 | {:noreply, state} 108 | end 109 | end 110 | 111 | {:ok, gen_server} = GenServer.start(MyGenServer, {}) 112 | 113 | capture_log(fn -> 114 | GenServer.cast(gen_server, :call_self) 115 | end) 116 | 117 | data = assert_performed_request()["data"] 118 | 119 | exception = data["body"]["trace"]["exception"] 120 | assert exception["class"] == "GenServer terminating" 121 | assert exception["message"] =~ "exited in: GenServer.call(#{inspect(gen_server)}" 122 | assert exception["message"] =~ "process attempted to call itself" 123 | 124 | assert [frame] = find_frames_for_current_file(data["body"]["trace"]["frames"]) 125 | assert frame["method"] == "MyGenServer.handle_cast/2" 126 | 127 | assert data["custom"]["last_message"] =~ "$gen_cast" 128 | assert data["custom"]["name"] == inspect(gen_server) 129 | assert data["custom"]["state"] == "{}" 130 | after 131 | purge_module(MyGenServer) 132 | end 133 | 134 | test "GenServer stopping" do 135 | defmodule Elixir.MyGenServer do 136 | use GenServer 137 | 138 | def init(args), do: {:ok, args} 139 | 140 | def handle_cast(:stop, state) do 141 | {:stop, :stop_reason, state} 142 | end 143 | end 144 | 145 | {:ok, gen_server} = GenServer.start(MyGenServer, {}) 146 | 147 | capture_log(fn -> 148 | GenServer.cast(gen_server, :stop) 149 | end) 150 | 151 | data = assert_performed_request()["data"] 152 | 153 | # Check the exception. 154 | assert data["body"]["trace"]["exception"] == %{ 155 | "class" => "GenServer terminating (stop)", 156 | "message" => ":stop_reason" 157 | } 158 | 159 | assert data["body"]["trace"]["frames"] == [] 160 | 161 | assert data["custom"]["last_message"] =~ "$gen_cast" 162 | assert data["custom"]["name"] == inspect(gen_server) 163 | assert data["custom"]["state"] == "{}" 164 | after 165 | purge_module(MyGenServer) 166 | end 167 | 168 | test "gen_event terminating" do 169 | defmodule Elixir.MyGenEventHandler do 170 | @behaviour :gen_event 171 | 172 | def init(state), do: {:ok, state} 173 | def terminate(_reason, _state), do: :ok 174 | def code_change(_old_vsn, state, _extra), do: {:ok, state} 175 | def handle_call(_request, state), do: {:ok, :ok, state} 176 | def handle_info(_message, state), do: {:ok, state} 177 | 178 | def handle_event(:raise_error, state) do 179 | raise "oops" 180 | {:ok, state} 181 | end 182 | end 183 | 184 | {:ok, manager} = :gen_event.start() 185 | :ok = :gen_event.add_handler(manager, MyGenEventHandler, {}) 186 | 187 | capture_log(fn -> 188 | :gen_event.notify(manager, :raise_error) 189 | 190 | data = assert_performed_request()["data"] 191 | 192 | # Check the exception. 193 | assert data["body"]["trace"]["exception"] == %{ 194 | "class" => "gen_event handler terminating (RuntimeError)", 195 | "message" => "oops" 196 | } 197 | 198 | assert [frame] = find_frames_for_current_file(data["body"]["trace"]["frames"]) 199 | assert frame["method"] == "MyGenEventHandler.handle_event/2" 200 | 201 | assert data["custom"] == %{ 202 | "name" => "MyGenEventHandler", 203 | "manager" => inspect(manager), 204 | "last_message" => ":raise_error", 205 | "state" => "{}" 206 | } 207 | end) 208 | after 209 | purge_module(MyGenEventHandler) 210 | end 211 | 212 | test "process raising an error" do 213 | capture_log(fn -> 214 | pid = spawn(fn -> raise "oops" end) 215 | 216 | data = assert_performed_request()["data"] 217 | 218 | assert data["body"]["trace"]["exception"] == %{ 219 | "class" => "error in process (RuntimeError)", 220 | "message" => "oops" 221 | } 222 | 223 | assert [frame] = find_frames_for_current_file(data["body"]["trace"]["frames"]) 224 | 225 | assert frame["method"] =~ 226 | ~r[anonymous fn/0 in Rollbax.LoggerTest.(\")?test process raising an error(\")?/1] 227 | 228 | assert data["custom"] == %{"pid" => inspect(pid)} 229 | end) 230 | end 231 | 232 | test "task with anonymous function raising an error" do 233 | capture_log(fn -> 234 | {:ok, task} = Task.start(fn -> raise "oops" end) 235 | 236 | data = assert_performed_request()["data"] 237 | 238 | assert data["body"]["trace"]["exception"] == %{ 239 | "class" => "Task terminating (RuntimeError)", 240 | "message" => "oops" 241 | } 242 | 243 | assert [frame] = find_frames_for_current_file(data["body"]["trace"]["frames"]) 244 | 245 | assert frame["method"] =~ 246 | ~r[anonymous fn/0 in Rollbax.LoggerTest.(\")?test task with anonymous function raising an error(\")?/1] 247 | 248 | assert data["custom"]["name"] == inspect(task) 249 | assert data["custom"]["function"] =~ ~r/\A#Function<.* in Rollbax\.LoggerTest/ 250 | assert data["custom"]["arguments"] == "[]" 251 | end) 252 | end 253 | 254 | test "task with mfa raising an error" do 255 | defmodule Elixir.MyModule do 256 | def raise_error(message), do: raise(message) 257 | end 258 | 259 | capture_log(fn -> 260 | {:ok, task} = Task.start(MyModule, :raise_error, ["my message"]) 261 | 262 | data = assert_performed_request()["data"] 263 | 264 | assert data["body"]["trace"]["exception"] == %{ 265 | "class" => "Task terminating (RuntimeError)", 266 | "message" => "my message" 267 | } 268 | 269 | assert [frame] = find_frames_for_current_file(data["body"]["trace"]["frames"]) 270 | assert frame["method"] == "MyModule.raise_error/1" 271 | 272 | assert data["custom"] == %{ 273 | "name" => inspect(task), 274 | "function" => "&MyModule.raise_error/1", 275 | "arguments" => ~s(["my message"]), 276 | "started_from" => inspect(self()) 277 | } 278 | end) 279 | after 280 | purge_module(MyModule) 281 | end 282 | 283 | if List.to_integer(:erlang.system_info(:otp_release)) < 19 do 284 | test "gen_fsm terminating" do 285 | defmodule Elixir.MyGenFsm do 286 | @behaviour :gen_fsm 287 | def init(data), do: {:ok, :idle, data} 288 | def terminate(_reason, _state, _data), do: :ok 289 | def code_change(_vsn, state, data, _extra), do: {:ok, state, data} 290 | def handle_event(_event, state, data), do: {:next_state, state, data} 291 | def handle_sync_event(_event, _from, state, data), do: {:next_state, state, data} 292 | def handle_info(_message, state, data), do: {:next_state, state, data} 293 | 294 | def idle(:error, state) do 295 | :maps.find(:a_key, _not_a_map = []) 296 | {:next_state, :idle, state} 297 | end 298 | end 299 | 300 | capture_log(fn -> 301 | {:ok, gen_fsm} = :gen_fsm.start(MyGenFsm, {}, _opts = []) 302 | 303 | :gen_fsm.send_event(gen_fsm, :error) 304 | 305 | data = assert_performed_request()["data"] 306 | 307 | # Check the exception. 308 | assert data["body"]["trace"]["exception"] == %{ 309 | "class" => "State machine terminating (BadMapError)", 310 | "message" => "expected a map, got: []" 311 | } 312 | 313 | assert [frame] = find_frames_for_current_file(data["body"]["trace"]["frames"]) 314 | assert frame["method"] == "MyGenFsm.idle/2" 315 | 316 | assert data["custom"] == %{ 317 | "last_event" => ":error", 318 | "name" => inspect(gen_fsm), 319 | "state" => ":idle", 320 | "data" => "{}" 321 | } 322 | end) 323 | after 324 | purge_module(MyGenFsm) 325 | end 326 | end 327 | 328 | test "when the endpoint is down, no logs are reported" do 329 | :ok = RollbarAPI.stop() 330 | 331 | capture_log(fn -> 332 | spawn(fn -> raise "oops" end) 333 | refute_receive {:api_request, _body} 334 | end) 335 | end 336 | 337 | @tag reporters: [Rollbax.Reporter.Silencing] 338 | test "reporters can skip events" do 339 | capture_log(fn -> 340 | spawn(fn -> raise "oops" end) 341 | refute_receive {:api_request, _body} 342 | end) 343 | end 344 | 345 | defp find_frames_for_current_file(frames) do 346 | current_file = Path.relative_to_cwd(__ENV__.file) 347 | Enum.filter(frames, &(&1["filename"] == current_file)) 348 | end 349 | 350 | defp purge_module(module) do 351 | :code.delete(module) 352 | :code.purge(module) 353 | end 354 | end 355 | -------------------------------------------------------------------------------- /test/rollbax_test.exs: -------------------------------------------------------------------------------- 1 | defmodule RollbaxTest do 2 | use ExUnit.RollbaxCase 3 | 4 | setup_all do 5 | {:ok, pid} = start_rollbax_client("token1", "test") 6 | 7 | on_exit(fn -> 8 | ensure_rollbax_client_down(pid) 9 | end) 10 | end 11 | 12 | setup do 13 | {:ok, _} = RollbarAPI.start(self()) 14 | on_exit(&RollbarAPI.stop/0) 15 | end 16 | 17 | describe "report/5" do 18 | test "with an error" do 19 | stacktrace = [{Test, :report, 2, [file: 'file.exs', line: 16]}] 20 | exception = RuntimeError.exception("pass") 21 | :ok = Rollbax.report(:error, exception, stacktrace, %{}, %{uuid: "d4c7"}) 22 | 23 | assert %{ 24 | "data" => %{ 25 | "body" => %{"trace" => trace}, 26 | "environment" => "test", 27 | "level" => "error", 28 | "uuid" => "d4c7" 29 | } 30 | } = assert_performed_request() 31 | 32 | assert trace == %{ 33 | "exception" => %{ 34 | "class" => "RuntimeError", 35 | "message" => "pass" 36 | }, 37 | "frames" => [ 38 | %{"filename" => "file.exs", "lineno" => 16, "method" => "Test.report/2"} 39 | ] 40 | } 41 | end 42 | 43 | test "with an error that is not an exception" do 44 | stacktrace = [{Test, :report, 2, [file: 'file.exs', line: 16]}] 45 | error = {:badmap, nil} 46 | :ok = Rollbax.report(:error, error, stacktrace, %{}, %{}) 47 | 48 | assert %{"class" => "BadMapError", "message" => "expected a map, got: nil"} = 49 | assert_performed_request()["data"]["body"]["trace"]["exception"] 50 | end 51 | 52 | test "with an exit" do 53 | stacktrace = [{Test, :report, 2, [file: 'file.exs', line: 16]}] 54 | :ok = Rollbax.report(:exit, :oops, stacktrace) 55 | 56 | assert %{ 57 | "data" => %{ 58 | "body" => %{"trace" => trace}, 59 | "level" => "error" 60 | } 61 | } = assert_performed_request() 62 | 63 | assert trace == %{ 64 | "exception" => %{ 65 | "class" => "exit", 66 | "message" => ":oops" 67 | }, 68 | "frames" => [ 69 | %{"filename" => "file.exs", "lineno" => 16, "method" => "Test.report/2"} 70 | ] 71 | } 72 | end 73 | 74 | test "with an exit where the term is an exception" do 75 | stacktrace = [{Test, :report, 2, [file: 'file.exs', line: 16]}] 76 | 77 | exception = 78 | try do 79 | raise "oops" 80 | rescue 81 | exception -> exception 82 | end 83 | 84 | :ok = Rollbax.report(:exit, exception, stacktrace, %{}, %{}) 85 | 86 | assert %{"class" => "exit", "message" => "** (RuntimeError) oops"} = 87 | assert_performed_request()["data"]["body"]["trace"]["exception"] 88 | end 89 | 90 | test "with a throw" do 91 | stacktrace = [{Test, :report, 2, [file: 'file.exs', line: 16]}] 92 | :ok = Rollbax.report(:throw, :oops, stacktrace) 93 | 94 | assert %{ 95 | "data" => %{ 96 | "body" => %{"trace" => trace}, 97 | "level" => "error" 98 | } 99 | } = assert_performed_request() 100 | 101 | assert trace == %{ 102 | "exception" => %{ 103 | "class" => "throw", 104 | "message" => ":oops" 105 | }, 106 | "frames" => [ 107 | %{"filename" => "file.exs", "lineno" => 16, "method" => "Test.report/2"} 108 | ] 109 | } 110 | end 111 | 112 | test "includes stacktraces in the function name if there's an application" do 113 | # Let's use some modules that belong to an application and some that don't. 114 | stacktrace = [ 115 | {:crypto, :strong_rand_bytes, 1, [file: 'crypto.erl', line: 1]}, 116 | {List, :to_string, 1, [file: 'list.ex', line: 10]}, 117 | {NoApp, :for_this_module, 3, [file: 'nofile.ex', line: 1]} 118 | ] 119 | 120 | :ok = Rollbax.report(:throw, :oops, stacktrace) 121 | 122 | assert [ 123 | %{"method" => ":crypto.strong_rand_bytes/1 (crypto)"}, 124 | %{"method" => "List.to_string/1 (elixir)"}, 125 | %{"method" => "NoApp.for_this_module/3"} 126 | ] = assert_performed_request()["data"]["body"]["trace"]["frames"] 127 | end 128 | end 129 | 130 | test "report_message/4" do 131 | :ok = Rollbax.report_message(:critical, "Everything is on fire!") 132 | 133 | assert %{ 134 | "data" => %{ 135 | "level" => "critical", 136 | "body" => %{"message" => %{"body" => "Everything is on fire!"}} 137 | } 138 | } = assert_performed_request() 139 | end 140 | 141 | describe "start/2" do 142 | setup do 143 | on_exit(fn -> 144 | Application.delete_env(:rollbax, :enabled) 145 | end) 146 | end 147 | 148 | test "when :enabled config value is invalid, it raises" do 149 | Application.put_env(:rollbax, :enabled, "invalid_enabled_config_value") 150 | 151 | assert_raise ArgumentError, ":enabled may be only one of: true, false, or :log", fn -> 152 | Rollbax.start(:temporary, []) 153 | end 154 | end 155 | 156 | test "when :enabled config value is true but :access_token is nil, it raises" do 157 | Application.put_env(:rollbax, :enabled, true) 158 | Application.delete_env(:rollbax, :access_token) 159 | 160 | assert_raise ArgumentError, ":access_token is required when :enabled is true", fn -> 161 | Rollbax.start(:temporary, []) 162 | end 163 | end 164 | end 165 | end 166 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | Logger.configure(level: :info) 2 | Application.ensure_all_started(:hackney) 3 | ExUnit.start() 4 | 5 | defmodule ExUnit.RollbaxCase do 6 | use ExUnit.CaseTemplate 7 | 8 | using _ do 9 | quote do 10 | import unquote(__MODULE__) 11 | end 12 | end 13 | 14 | def start_rollbax_client( 15 | token, 16 | env, 17 | custom \\ %{}, 18 | api_endpoint \\ "http://localhost:4004", 19 | proxy \\ nil 20 | ) do 21 | Rollbax.Client.start_link( 22 | api_endpoint: api_endpoint, 23 | access_token: token, 24 | environment: env, 25 | enabled: true, 26 | custom: custom, 27 | proxy: proxy 28 | ) 29 | end 30 | 31 | def ensure_rollbax_client_down(pid) do 32 | ref = Process.monitor(pid) 33 | 34 | receive do 35 | {:DOWN, ^ref, _, _, _} -> :ok 36 | end 37 | end 38 | 39 | def capture_log(fun) do 40 | ExUnit.CaptureIO.capture_io(:user, fn -> 41 | fun.() 42 | Process.sleep(200) 43 | Logger.flush() 44 | end) 45 | end 46 | 47 | def assert_performed_request() do 48 | assert_receive {:api_request, body} 49 | Jason.decode!(body) 50 | end 51 | end 52 | 53 | defmodule RollbarAPI do 54 | alias Plug.Conn 55 | alias Plug.Adapters.Cowboy 56 | 57 | import Conn 58 | 59 | def start(pid, port \\ 4004) do 60 | Cowboy.http(__MODULE__, [test: pid], port: port) 61 | end 62 | 63 | def stop() do 64 | Process.sleep(100) 65 | Cowboy.shutdown(__MODULE__.HTTP) 66 | Process.sleep(100) 67 | end 68 | 69 | def init(opts) do 70 | Keyword.fetch!(opts, :test) 71 | end 72 | 73 | def call(%Conn{method: "POST"} = conn, test) do 74 | {:ok, body, conn} = read_body(conn) 75 | Process.sleep(30) 76 | send(test, {:api_request, body}) 77 | 78 | custom = Jason.decode!(body)["data"]["custom"] 79 | 80 | cond do 81 | custom["return_error?"] -> 82 | send_resp(conn, 400, ~s({"err": 1, "message": "that was a bad request"})) 83 | 84 | rate_limit_seconds = custom["rate_limit_seconds"] -> 85 | conn 86 | |> put_resp_header("X-Rate-Limit-Remaining-Seconds", rate_limit_seconds) 87 | |> send_resp(429, "{}") 88 | 89 | true -> 90 | send_resp(conn, 200, "{}") 91 | end 92 | end 93 | 94 | def call(conn, _test) do 95 | send_resp(conn, 404, "Not Found") 96 | end 97 | end 98 | --------------------------------------------------------------------------------