Error Tracking from Scratch

Episode #317 by David Kimura

Summary

In this episode, we look at creating a middleware to track errors to publish to another error "from scratch" error monitoring application.
error middleware rails 15:26

Resources

Download Source Code

Summary

# Terminal
bin/rails middleware
touch tmp/caching-dev.txt

# Web Application
# config/application.rb
require_relative "boot"
require "rails/all"
Bundler.require(*Rails.groups)
require './lib/middlewares/main.rb'

module TrackerExample
  class Application < Rails::Application
    config.load_defaults 7.0
    config.middleware.use CaptureExceptions
  end
end

# Web Application
# lib/middlewares/main.rb
require_relative 'capture_exceptions.rb'

# Web Application
# lib/middlewares/capture_exceptions.rb
require 'net/http'
class CaptureExceptions
  def initialize(app)
    @app = app
  end

  def call(env)
    begin
      @app.call(env)
    rescue StandardError => e
      @exception = e
      capture_exceptions(env)
      raise e
    end
  end

  private

  def capture_exceptions(env)
    uri = URI('http://localhost:3001/record_exceptions')
    Net::HTTP.post_form(uri,
      message: @exception.message,
      backtrace: @exception.backtrace[0..5].join("\n"),
      source_location: source_location,
      method: env['REQUEST_METHOD'],
      uri: env['REQUEST_URL']
    )
  end

  def source_location
    lines[start_line..end_line].join
  end

  def source
    @exception.backtrace.first.split(':')
  end

  def file_location
    source[0]
  end

  def start_line
    [source[1].to_i - 5, 0].max
  end

  def end_line
    source[1].to_i + 5
  end

  def lines
    File.readlines(file_location)
  end

end

# Tracker Application
# config/routes.rb
Rails.application.routes.draw do
  resource :record_exceptions, only: :create
  root to: 'welcome#index'
end

# Tracker Application
# app/controllers/record_exceptions_controller.rb
class RecordExceptionsController < ApplicationController
  skip_before_action :verify_authenticity_token

  def create
    Rails.cache.write((Time.current.to_f * 1000), payload)
    head :ok
  end

  private

  def payload
    {
      message: params[:message],
      backtrace: params[:backtrace],
      method: params[:method],
      uri: params[:uri],
      source_location: params[:source_location]
    }.to_json
  end
end

# Tracker Application
# views/welcome/index.html.erb
<% Rails.cache.instance_variable_get(:@data).keys.map { |key| JSON.parse(Rails.cache.fetch(key)).merge(time: key) }.reverse.each do |error| %>
  <strong>Method:</strong> <%= error["method"] %><br>
  <strong>URI:</strong> <%= error["uri"] %><br>
  <strong>Time:</strong> <%= Time.at(error[:time].to_i / 1000) %>
  <strong>Backtrace:</strong> <%= raw error["backtrace"].split("\n").join("<br>") %>
  <strong>Source Location:</strong> <%= error["source_location"] %>
<% end %>

# Tracker Application
# views/welcome/index.html.erb
<div class="accordion accordion-flush" id="accordionExample">
  <% Rails.cache.instance_variable_get(:@data).keys.map { |key| JSON.parse(Rails.cache.fetch(key)).merge(time: key) }.reverse.each do |error| %>

    <div class="accordion-item">
      <h2 class="accordion-header" id="heading<%= error[:time].to_i %>">
        <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#body<%= error[:time].to_i %>">
          <%= error["message"] %>
        </button>
      </h2>
      <div id="body<%= error[:time].to_i %>" class="accordion-collapse collapse" data-bs-parent="#accordionExample">
        <div class="accordion-body">

          <div class="card border-primary mb-3">
            <div class="card-header">Info</div>
            <div class="card-body text-primary">
              <p class="card-text">
                <strong>Method</strong> <%= error["method"] %><br>
                <strong>URI</strong> <%= error["uri"] %><br>
                <strong>Time</strong> <%= Time.at(error[:time].to_i / 1000) %>
              </p>
            </div>
          </div>

          <div class="card border-primary mb-3">
            <div class="card-header">Backtrace</div>
            <div class="card-body text-primary">
              <p class="card-text">
                <pre><%= raw error["backtrace"].split("\n").join("<br>") %></pre>
              </p>
            </div>
          </div>

          <div class="card border-primary mb-3">
            <div class="card-header">
              Source Location:
              <%= error["backtrace"].split("\n").first %>
            </div>
            <div class="card-body text-primary">
              <p class="card-text">
                <pre><%= error["source_location"] %></pre>
              </p>
            </div>
          </div>

        </div>
      </div>
    </div>

  <% end %>
</div>