Skip to content

Commit

Permalink
Initial work
Browse files Browse the repository at this point in the history
  • Loading branch information
thesecretmaster committed Dec 25, 2018
1 parent 6101c3c commit c901eb8
Show file tree
Hide file tree
Showing 24 changed files with 393 additions and 1 deletion.
13 changes: 13 additions & 0 deletions app/assets/javascripts/cable.js
@@ -0,0 +1,13 @@
// Action Cable provides the framework to deal with WebSockets in Rails.
// You can generate new channels where WebSocket features live using the `rails generate channel` command.
//
//= require action_cable
//= require_self
//= require_tree ./channels

(function() {
this.App || (this.App = {});

App.cable = ActionCable.createConsumer();

}).call(this);
12 changes: 12 additions & 0 deletions app/assets/javascripts/channels/redis_log.coffee
@@ -0,0 +1,12 @@
App.redis_log = App.cable.subscriptions.create "RedisLogChannel",
connected: ->
# Called when the subscription is ready for use on the server

disconnected: ->
# Called when the subscription has been terminated by the server

received: (data) ->
# Called when there's incoming data on the websocket for this channel

new: ->
@perform 'new'
3 changes: 3 additions & 0 deletions app/assets/javascripts/redis_log.coffee
@@ -0,0 +1,3 @@
# Place all the behaviors and hooks related to the matching controller here.
# All this logic will automatically be available in application.js.
# You can use CoffeeScript in this file: http://coffeescript.org/
27 changes: 27 additions & 0 deletions app/assets/stylesheets/redis_log.scss
@@ -0,0 +1,27 @@
// Place all the styles related to the RedisLog controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/

.square-borders {
border-radius: 0px !important;
margin-bottom: -1px;
}
.list-item-label {
background-color: #f5f5f5 !important;
display:table !important;
width:100%;
table-layout: fixed;
}
.list-item-label > a {
overflow: hidden;
white-space: nowrap;
display: table-cell;
text-overflow: ellipsis;
}
.list-item-label:hover > a, .list-item-label > a:hover {
text-decoration: none;
}

.list-item-label > div {
display: table-cell;
}
13 changes: 13 additions & 0 deletions app/channels/redis_log_channel.rb
@@ -0,0 +1,13 @@
class RedisLogChannel < ApplicationCable::Channel
def subscribed
# binding.irb
stream_from "redis_log_channel" if !current_user.nil? && current_user.has_role?(:developer)
end

def unsubscribed
# Any cleanup needed when channel is unsubscribed
end

def new
end
end
39 changes: 39 additions & 0 deletions app/controllers/application_controller.rb
Expand Up @@ -7,6 +7,7 @@ class ApplicationController < ActionController::Base
before_action :configure_permitted_parameters, if: :devise_controller?
before_action :check_auth_required
before_action :deduplicate_ajax_requests
before_action :redis_log_request

before_action do
Rack::MiniProfiler.authorize_request if current_user&.has_role?(:developer)
Expand Down Expand Up @@ -64,6 +65,44 @@ def verify_at_least_one_diamond

private

def redis_log_request
Rack::MiniProfiler.step('Logging to redis') do
@request_time ||= Time.now.to_f
request.set_header "redis_logs.log_key", redis_log_key
request.set_header "redis_logs.timestamp", @request_time
request.set_header "redis_logs.request_id", request.uuid
redis.zadd "requests", @request_time, request.uuid
log_redis headers: headers, params: request.filtered_parameters.except(:controller, :action)
if !session[:redis_log_id].present?
session[:redis_log_id] = SecureRandom.base64
session[:redis_log_id] = SecureRandom.base64 while redis.sadd("sessions", session[:redis_log_id]) == 0
end
redis.mapped_hmset redis_log_key, {
path: request.filtered_path,
impersonator_id: session[:impersonator_id],
user_id: user_signed_in? ? current_user.id : nil,
session_id: session[:redis_log_id],
sha: CurrentCommit
}
redis.zadd "session/#{session[:redis_log_id]}/requests", @request_time, request.uuid
redis.hsetnx("session/#{session[:redis_log_id]}", "start", @request_time)
redis.hset("session/#{session[:redis_log_id]}", "end", @request_time)
redis.zadd "user_sessions/#{current_user.id}", @request_time, session[:redis_log_id] if user_signed_in?
RedisLogJob.perform_later(request.uuid, @request_time)
end
end

def log_redis(**opts)
opts.each do |key, val|
redis.mapped_hmset "#{redis_log_key}/#{key}", val unless val.empty?
end
end

def redis_log_key
# We include the time just to doubly ensure that the uuid is unique
"request/#{@request_time}/#{request.uuid}"
end

def check_auth_required
return unless redis.get('require_auth_all_pages') == '1' # SiteSetting['require_auth_all_pages']
return if user_signed_in? || devise_controller? || (controller_name == 'users' && action_name == 'missing_privileges')
Expand Down
70 changes: 70 additions & 0 deletions app/controllers/redis_log_controller.rb
@@ -0,0 +1,70 @@
class RedisLogController < ApplicationController
before_action :authenticate_user!
before_action :verify_developer

def by_user
user_id = params[:id]
@sessions = redis.zrevrange("user_sessions/#{user_id}", 0, 20).map do |session_id|
requests = redis.zrange("session/#{session_id}/requests", 0, 20, with_scores: true)
redis.hgetall("session/#{session_id}").merge({
id: session_id,
user_id: user_id,
count: requests.length,
requests: requests.map { |request_id, timestamp|
redis.hgetall("request/#{timestamp}/#{request_id}").merge({
headers: redis.hgetall("request/#{timestamp}/#{request_id}/headers"),
params: redis.hgetall("request/#{timestamp}/#{request_id}/params"),
exception: redis.hgetall("request/#{timestamp}/#{request_id}/exception"),
timestamp: timestamp,
request_id: request_id,
key: "#{timestamp.to_s.gsub('.', '-')}-#{request_id}"
})}
})
end
render :user_sessions
end

def index
@requests = redis.zrevrange("requests", 1, 11, with_scores: true).map do |request_id, timestamp|
redis.hgetall("request/#{timestamp}/#{request_id}").merge({
headers: redis.hgetall("request/#{timestamp}/#{request_id}/headers"),
params: redis.hgetall("request/#{timestamp}/#{request_id}/params"),
exception: redis.hgetall("request/#{timestamp}/#{request_id}/exception"),
timestamp: timestamp,
request_id: request_id,
key: "#{timestamp.to_s.gsub('.', '-')}-#{request_id}"
})
end
end

def by_session
session_id = params[:id]
@requests = redis.zrange("session/#{session_id}/requests", 0, 20, with_scores: true).map do |request_id, timestamp|
redis.hgetall("request/#{timestamp}/#{request_id}").merge({
headers: redis.hgetall("request/#{timestamp}/#{request_id}/headers"),
params: redis.hgetall("request/#{timestamp}/#{request_id}/params"),
exception: redis.hgetall("request/#{timestamp}/#{request_id}/exception"),
timestamp: timestamp,
request_id: request_id,
key: "#{timestamp.to_s.gsub('.', '-')}-#{request_id}"
})
end

render :index
end

def by_status
status = params[:status]
@requests = redis.zrevrange("requests/status/#{status}", 0, 20, with_scores: true).map do |request_id, timestamp|
redis.hgetall("request/#{timestamp}/#{request_id}").merge({
headers: redis.hgetall("request/#{timestamp}/#{request_id}/headers"),
params: redis.hgetall("request/#{timestamp}/#{request_id}/params"),
exception: redis.hgetall("request/#{timestamp}/#{request_id}/exception"),
timestamp: timestamp,
request_id: request_id,
key: "#{timestamp.to_s.gsub('.', '-')}-#{request_id}"
})
end
render :index
end
end
2 changes: 2 additions & 0 deletions app/helpers/redis_log_helper.rb
@@ -0,0 +1,2 @@
module RedisLogHelper
end
1 change: 1 addition & 0 deletions app/javascript/cable/index.js
Expand Up @@ -7,3 +7,4 @@ import './posts';
import './post';
import './topbar';
import './status';
import './redis_logs';
26 changes: 26 additions & 0 deletions app/javascript/cable/redis_logs.js
@@ -0,0 +1,26 @@
import { route } from '../util';
import cable from './cable';

/* TODO: Add websockets to the other pages */
let redis;
route('/redis_log/index', () => {
redis = cable.subscriptions.create('RedisLogChannel', {
received(data) {
var matched_log = $('#logs div[data-log-id="'+ data.key +'"]');
console.log($.parseHTML(data.html)[1]);
if (matched_log.length > 0) {
console.log(matched_log);
matched_log[0].replaceWith($.parseHTML(data.html)[1]);
} else {
$("#logs").prepend($.parseHTML(data.html)[1]);
}
}
});
}, () => {
if (!redis) {
return;
}
redis.unsubscribe();
redis = null;
});
});
20 changes: 20 additions & 0 deletions app/jobs/redis_log_job.rb
@@ -0,0 +1,20 @@
class RedisLogJob < ApplicationJob
queue_as :default

def perform(request_id, timestamp)
info = redis.hgetall("request/#{timestamp}/#{request_id}").merge({
headers: redis.hgetall("request/#{timestamp}/#{request_id}/headers"),
params: redis.hgetall("request/#{timestamp}/#{request_id}/params"),
exception: redis.hgetall("request/#{timestamp}/#{request_id}/exception"),
timestamp: timestamp,
request_id: request_id,
key: "#{timestamp.to_s.gsub('.', '-')}-#{request_id}"
})
ActionCable.server.broadcast "redis_log_channel", key: "#{timestamp.to_s.gsub('.', '-')}-#{request_id}",
html: ApplicationController.render(
template: 'redis_log/_row',
locals: { req: info, wrapped: true },
layout: nil
)
end
end
16 changes: 16 additions & 0 deletions app/views/redis_log/_header.html.erb
@@ -0,0 +1,16 @@
<span class="text-<%= req['status'].to_i == 200 ? 'success' : 'danger' %>" href="#<%= req[:key] %>">
<%= render 'redis_log/user', impersonator_id: req['impersonator_id'], user_id: req['user_id'], session_id: req['session_id'] %>
<code><%= link_to req['status'], redis_log_by_status_path(status: req['status']), style: 'display: inline;' %></code> <%= req['method'] %> <%= req['path'] %>
</span>
<div class="pull-right">
<code>
<span style="display:inline-block;">
<%= Time.at(req[:timestamp]).strftime("%-y/%-m/%-d %-H:%M:%S.%-3N") %>
</span>&nbsp;·&nbsp;<span style="display:inline-block;">
<i class="fas fa-database" title="DB time"></i>
<%= number_with_precision(req['db_runtime'].to_f, precision: 3).rjust(7, "\u00A0") %>s
· <i class="fas fa-desktop" title="View time"></i>
<%= number_with_precision(req['view_runtime'].to_f, precision: 3).rjust(7, "\u00A0") %>s
</span>
</code>
</div>
16 changes: 16 additions & 0 deletions app/views/redis_log/_param_table.html.erb
@@ -0,0 +1,16 @@
<table class="table">
<% params.each do |k, v| %>
<tr>
<td><%= k %></td>
<td>
<% if v.include?("\n") %>
<pre style="overflow:scroll;max-height:20em;"><%= raw(sanitize(v).gsub("\n", "<br>")) %></pre>
<% elsif v.nil? %>
<code>nil</code>
<% else %>
<code><%= v %></code>
<% end %>
</td>
</tr>
<% end %>
</table>
39 changes: 39 additions & 0 deletions app/views/redis_log/_row.html.erb
@@ -0,0 +1,39 @@
<% wrapped ||= false %>
<% if wrapped %><div data-log-id="<%= req[:key] %>"><% end %>
<li class="list-group-item list-item-label" data-toggle="collapse" role="button" aria-expanded="false" aria-controls="<%= req[:key] %>" href="#<%= req[:key] %>">
<%= render 'redis_log/header', req: req %>
</li>
<div class="collapse" style="margin-bottom:-1px" id="<%= req[:key] %>">
<li class="list-group-item square-borders">
<p>
<%= link_to "session", redis_log_by_session_path(id: req['session_id']) %>&nbsp;|
<%= link_to "user", redis_log_by_user_path(id: req['user_id']) %>
</p>
<h4><code><%= req['controller'] %>#<%= req['action'] %> (<%= req['path'] %>.<%= req['format'] %>)</code></h4>
<% if !req[:exception].empty? %>
<h3><%= req['status'] %> Exception:</h3>
<pre><%= req['exception'] %></pre>
<%= render 'redis_log/param_table', params: req[:exception] %>
<% end %>
<% %w[Params Headers].each do |key| %>
<h4><%= key %></h4>
<% if req[key.downcase.to_sym].empty? %>
(empty)
<% else %>
<%= render 'redis_log/param_table', heading: key, params: req[key.downcase.to_sym] %>
<% end %>
<% end %>
<table>
<tr>
<td>Request ID</td>
<td><code><%= req[:request_id] %></code></td>
</tr>
<tr>
<td>Commit SHA</td>
<td><code><%= req['sha'] %></code></td>
</tr>
</table>
</li>
</div>
<% if wrapped %></div><% end %>
11 changes: 11 additions & 0 deletions app/views/redis_log/_user.html.erb
@@ -0,0 +1,11 @@
<% impersonator_id ||= nil %>
<% if impersonator_id.present? %>
[<%= User.find(impersonator_id).username %> (impersonating <%= User.find(user_id).username %>)]
<% elsif user_id.present? %>
[<%= User.find(user_id).username %>]
<% elsif session_id.nil? %>
[NOSESS]
<% else %>
<img src="https://robohash.org/<%= session_id %>?size=20x20">
<% end %>
5 changes: 5 additions & 0 deletions app/views/redis_log/index.html.erb
@@ -0,0 +1,5 @@
<ul class="list-group" id='logs'>
<% @requests.each do |req| %>
<%= render 'row', req: req, wrapped: true %>
<% end %>
</ul>
2 changes: 2 additions & 0 deletions app/views/redis_log/status.html.erb
@@ -0,0 +1,2 @@
<h1>RedisLog#status</h1>
<p>Find me in app/views/redis_log/status.html.erb</p>
2 changes: 2 additions & 0 deletions app/views/redis_log/user.html.erb
@@ -0,0 +1,2 @@
<h1>RedisLog#user</h1>
<p>Find me in app/views/redis_log/user.html.erb</p>
17 changes: 17 additions & 0 deletions app/views/redis_log/user_sessions.html.erb
@@ -0,0 +1,17 @@
<ul class="list-group">
<% @sessions.each do |session| %>
<% css_id = session[:id].gsub(/[^a-zA-Z0-9\-\_]/, '') %>
<li class="list-group-item list-item-label" data-toggle="collapse" role="button" aria-expanded="false" aria-controls="<%= css_id %>" href="#<%= css_id %>">
<span class="badge"><%= session[:count] %></span>
<%= render 'redis_log/user', session_id: session[:id], user_id: session[:user_id] %>
<%= Time.at(session['start'].to_i).strftime("%-y/%-m/%-d %-H:%M") %> - <%= Time.at(session['end'].to_i).strftime("%-y/%-m/%-d %-H:%M") %>
</li>
<div class="collapse" style="margin-bottom:-1px" id="<%= css_id %>">
<ul class="list-group">
<% session[:requests].each do |req| %>
<%= render 'redis_log/row', req: req %>
<% end %>
</ul>
</div>
<% end %>
</ul>
20 changes: 20 additions & 0 deletions config/initializers/redis-logging.rb
@@ -0,0 +1,20 @@
ActiveSupport::Notifications.subscribe "process_action.action_controller" do |name, started, finished, unique_id, data|
request_id = data[:headers]["action_dispatch.request_id"]
redis_log_id = data[:headers]["rack.session"]["redis_log_id"]
redis_log_key = data[:headers]["redis_logs.log_key"]
request_timestamp = data[:headers]["redis_logs.timestamp"]
redis.zadd "requests/status/#{data[:status]}", request_timestamp, request_id
redis.mapped_hmset redis_log_key, data.slice(:controller, :action, :format, :method, :status, :view_runtime, :db_runtime)
if data[:exception].present?
redis.hset redis_log_key, "exception", data[:exception].join("\n")
ex = data[:exception_object]
redis.mapped_hmset "#{redis_log_key}/exception", {
file_name: ex.try(:file_name),
annotated_source_code: ex.try(:annoted_source_code)&.join("\n"),
line_number: ex.try(:line_number),
backtrace: ex.try(:backtrace)&.join("\n"),
message: ex.try(:message)
}
end
RedisLogJob.perform_later(request_id, request_timestamp)
end
2 changes: 1 addition & 1 deletion config/initializers/revision.rb
@@ -1,3 +1,3 @@
# frozen_string_literal: true

CurrentCommit = (File.read('REVISION').strip if File.readable?('REVISION')) # rubocop:disable Style/ConstantName
CurrentCommit = File.readable?('REVISION') ? File.read('REVISION').strip : `git rev-parse --short HEAD`.chomp # rubocop:disable Style/ConstantName

0 comments on commit c901eb8

Please sign in to comment.