peephole 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: b84f93566189ed05ae8a5555258543dc9b5eceeb
4
+ data.tar.gz: c49f1a5a8da8b8d3fa9c2dd4182880763641b88f
5
+ SHA512:
6
+ metadata.gz: 59390333d669f65f7ee1c06c34fa2f8c7e85bd3afdb3a92cab16f2041ae264382c43cdb0cbc7c7dc5fc82f2784684b5439a3b4987e2ed6c94ac3ed79d3587a1a
7
+ data.tar.gz: 781fefaaa54e3c64ef6bde2a4f32fc577080548c496b96199e7de8c0431089a0b48fa5528245a62544603d9caa0274f390a2a2eaea19a931180e9c9d77b65efa
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2015 tnantoka
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,58 @@
1
+ # Peephole
2
+
3
+ [![Circle CI](https://circleci.com/gh/tnantoka/peephole.svg?style=svg)](https://circleci.com/gh/tnantoka/peephole)
4
+ [![Code Climate](https://codeclimate.com/github/tnantoka/peephole/badges/gpa.svg)](https://codeclimate.com/github/tnantoka/peephole)
5
+ [![Test Coverage](https://codeclimate.com/github/tnantoka/peephole/badges/coverage.svg)](https://codeclimate.com/github/tnantoka/peephole/coverage)
6
+
7
+ A log viewer engine for Rails.
8
+
9
+ ![](screenshot.png)
10
+
11
+ ## Installation
12
+
13
+ ```
14
+ # Gemfile
15
+ gem 'peephole'
16
+
17
+ $ bundle
18
+
19
+ $ rails g peephole:install
20
+ create config/initializers/log_tags.rb
21
+ create config/initializers/peephole.rb
22
+ ```
23
+
24
+ ## Configuration
25
+
26
+ ```
27
+ # config/initializers/peephole.rb
28
+ Peephole.configure do |config|
29
+ config.paginates_per = 200
30
+ config.peeper? do
31
+ # current_user.role.admin?
32
+
33
+ # admin_user_signed_in?
34
+
35
+ # authenticate_or_request_with_http_basic do |user, pass|
36
+ # user == 'user' && pass == 'pass'
37
+ # end
38
+ end
39
+ end
40
+ ```
41
+
42
+ ## Example
43
+
44
+ ```
45
+ $ git clone git@github.com:tnantoka/peephole.git
46
+ $ cd peephole/spec/dummy
47
+ $ rails s
48
+ $ open http://localhost:3000/peephole
49
+ ```
50
+
51
+ ## License
52
+
53
+ MIT
54
+
55
+ ## Author
56
+
57
+ [@tnantoka][https://twitter.com/tnantoka]
58
+
data/Rakefile ADDED
@@ -0,0 +1,28 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'rdoc/task'
8
+
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = 'rdoc'
11
+ rdoc.title = 'Peephole'
12
+ rdoc.options << '--line-numbers'
13
+ rdoc.rdoc_files.include('README.rdoc')
14
+ rdoc.rdoc_files.include('lib/**/*.rb')
15
+ end
16
+
17
+ APP_RAKEFILE = File.expand_path("../spec/dummy/Rakefile", __FILE__)
18
+ load 'rails/tasks/engine.rake'
19
+
20
+
21
+ load 'rails/tasks/statistics.rake'
22
+
23
+
24
+
25
+ Bundler::GemHelper.install_tasks
26
+
27
+ RSpec::Core::RakeTask.new(:spec)
28
+ task default: :spec
@@ -0,0 +1,13 @@
1
+ // This is a manifest file that'll be compiled into application.js, which will include all the files
2
+ // listed below.
3
+ //
4
+ // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
5
+ // or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path.
6
+ //
7
+ // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
8
+ // compiled file.
9
+ //
10
+ // Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
11
+ // about supported directives.
12
+ //
13
+ //= require_tree .
@@ -0,0 +1,16 @@
1
+ /*
2
+ * This is a manifest file that'll be compiled into application.css, which will include all the files
3
+ * listed below.
4
+ *
5
+ * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6
+ * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
7
+ *
8
+ * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9
+ * compiled file so the styles you add here take precedence over styles defined in any styles
10
+ * defined in the other CSS/SCSS files in this directory. It is generally better to create a new
11
+ * file per style scope.
12
+ *
13
+ *= require peephole/bootstrap/css/bootstrap
14
+ *= require_tree .
15
+ *= require_self
16
+ */
@@ -0,0 +1,29 @@
1
+ #peephole .table pre {
2
+ margin-bottom: 0;
3
+ }
4
+
5
+ #peephole .text-break {
6
+ word-break: break-all;
7
+ }
8
+
9
+ #peephole .navbar {
10
+ border-left: 0;
11
+ border-right: 0;
12
+ }
13
+
14
+ #peephole .navbar.navbar-default {
15
+ background: none;
16
+ }
17
+
18
+ #peephole .well {
19
+ box-shadow: none;
20
+ }
21
+
22
+ #peephole .navbar,
23
+ #peephole .breadcrumb,
24
+ #peephole .pager li a,
25
+ #peephole .well,
26
+ #peephole pre,
27
+ #peephole .label {
28
+ border-radius: 0;
29
+ }
@@ -0,0 +1,10 @@
1
+ module Peephole
2
+ class ApplicationController < ::ApplicationController
3
+ before_action :authenticate_user!
4
+
5
+ private
6
+ def authenticate_user!
7
+ redirect_to main_app.root_url unless instance_eval(&Peephole.config.peeper?)
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,21 @@
1
+ require_dependency "peephole/application_controller"
2
+
3
+ module Peephole
4
+ class LogfilesController < ApplicationController
5
+ before_action :set_logfile
6
+
7
+ def show
8
+ @page = params[:page].to_i.nonzero? || 1
9
+ @loglines, @eof = @logfile.loglines(@page)
10
+ end
11
+
12
+ def download
13
+ send_file @logfile.path
14
+ end
15
+
16
+ private
17
+ def set_logfile
18
+ @logfile = Logfile.find(params[:id])
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,9 @@
1
+ require_dependency "peephole/application_controller"
2
+
3
+ module Peephole
4
+ class WelcomeController < ApplicationController
5
+ def index
6
+ @logfiles = Logfile.all
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,15 @@
1
+ module Peephole
2
+ module ApplicationHelper
3
+ def status_label(status)
4
+ label = case status
5
+ when /\A2/
6
+ 'success'
7
+ when /\A3/
8
+ 'info'
9
+ else
10
+ 'danger'
11
+ end
12
+ content_tag(:span, status, class: "label label-#{label}")
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,39 @@
1
+ module Peephole
2
+ class Logfile
3
+ attr_accessor :filename, :path
4
+
5
+ LOG_PATH = Rails.root.join('log')
6
+
7
+ class << self
8
+ def glob
9
+ Dir.glob(LOG_PATH.join("#{Rails.env}.log*"))
10
+ end
11
+
12
+ def all
13
+ glob.map do |path|
14
+ Logfile.new(path)
15
+ end
16
+ end
17
+
18
+ def find(filename)
19
+ path = glob.find do |path|
20
+ File.basename(path) == filename
21
+ end
22
+ Logfile.new(path)
23
+ end
24
+ end
25
+
26
+ def initialize(path)
27
+ self.path = path
28
+ self.filename = File.basename(path)
29
+ end
30
+
31
+ def to_param
32
+ filename
33
+ end
34
+
35
+ def loglines(page)
36
+ Logline.where(path, page)
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,93 @@
1
+ module Peephole
2
+ class Logline
3
+ attr_accessor :uuid, :type, :num
4
+ attr_accessor :method, :target, :started_at
5
+ attr_accessor :params
6
+ attr_accessor :status
7
+
8
+ module TYPE
9
+ STARTED = 1
10
+ PARAMS = 2
11
+ COMPLETED = 3
12
+ end
13
+
14
+ class << self
15
+ def first(page)
16
+ (page - 1) * Peephole.config.paginates_per
17
+ end
18
+
19
+ def last(page)
20
+ page * Peephole.config.paginates_per
21
+ end
22
+
23
+ def where(path, page)
24
+ loglines = []
25
+ logmap = {}
26
+ eof = true
27
+ each(path, page) do |line, i|
28
+ next if i < first(page)
29
+ if i >= last(page)
30
+ eof = false
31
+ break
32
+ end
33
+ parse(line, i, loglines, logmap)
34
+ end
35
+ [loglines, eof]
36
+ end
37
+
38
+ def parse(line, i, loglines, logmap)
39
+ logline = new(line, i)
40
+ case logline.type
41
+ when TYPE::STARTED
42
+ logmap[logline.uuid] = logline if logline.uuid.present?
43
+ when TYPE::PARAMS
44
+ line = logmap[logline.uuid].presence || logline
45
+ line.params = logline.params
46
+ loglines << line
47
+ when TYPE::COMPLETED
48
+ logmap[logline.uuid].try(:status=, logline.status)
49
+ end
50
+ end
51
+
52
+ def each(path, page, &block)
53
+ iterator = case path.to_s
54
+ when /\.gz\z/
55
+ Zlib::GzipReader.open(path).each
56
+ else
57
+ IO.foreach(path)
58
+ end
59
+ iterator.with_index(&block)
60
+ end
61
+ end
62
+
63
+ def initialize(line, num)
64
+ self.num = num
65
+
66
+ case line
67
+ when / Started (\w+) "(.+)" .+ at (.+)/
68
+ self.method = $1
69
+ self.target = $2
70
+ self.started_at = Time.parse($3)
71
+ self.type = TYPE::STARTED
72
+ when / Parameters: ({.+})/
73
+ params = JSON.parse($1.gsub('=>', ':'))
74
+ params.dup.each do |k, v|
75
+ if v.is_a?(Hash)
76
+ v.each do |k2, v2|
77
+ params["#{k}[#{k2}]"] = v2
78
+ end
79
+ params.delete(k)
80
+ end
81
+ end
82
+ self.params = params
83
+ self.type = TYPE::PARAMS
84
+ when / Completed (\d+)/
85
+ self.status = $1
86
+ self.type = TYPE::COMPLETED
87
+ end
88
+ if line =~ /\[(\w+\-\w+\-\w+\-\w+\-\w+)\] /
89
+ self.uuid = $1
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,33 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Peephole</title>
5
+ <%= stylesheet_link_tag "peephole/application", media: "all" %>
6
+ <%= javascript_include_tag "peephole/application" %>
7
+ <%= csrf_meta_tags %>
8
+ </head>
9
+ <body id="peephole">
10
+
11
+ <nav class="navbar navbar-default">
12
+ <div class="container-fluid">
13
+ <div class="navbar-header">
14
+ <%= link_to 'Peephole', :root, class: 'navbar-brand' %>
15
+ </div>
16
+ </div>
17
+ </nav>
18
+
19
+ <div class="container-fluid">
20
+ <%= yield %>
21
+
22
+ <footer>
23
+ <p>
24
+ <a href="https://github.com/tnantoka/peephole" target="_blank" class="text-muted">
25
+ Peephole
26
+ <%= Peephole::VERSION %>
27
+ </a>
28
+ </p>
29
+ </footer>
30
+ </div>
31
+
32
+ </body>
33
+ </html>
@@ -0,0 +1,24 @@
1
+ <nav>
2
+ <ul class="pager">
3
+ <% if @page == 1 %>
4
+ <li class="previous disabled"><a>&larr; Older</a></li>
5
+ <% else %>
6
+ <li class="previous">
7
+ <%= link_to url_for(params.merge(page: @page - 1)) do %>
8
+ &larr; Older
9
+ <% end %>
10
+ </li>
11
+ <% end %>
12
+ <% if @eof %>
13
+ <li class="next disabled"><a>Newer &rarr;</a></li>
14
+ <% else %>
15
+ <li class="next">
16
+ <%= link_to url_for(params.merge(page: @page + 1)) do %>
17
+ Newer &rarr;
18
+ <% end %>
19
+ </li>
20
+ <% end %>
21
+ </ul>
22
+ </nav>
23
+
24
+
@@ -0,0 +1,55 @@
1
+ <ol class="breadcrumb">
2
+ <li><%= link_to 'Home', :root %></li>
3
+ <li><%= link_to @logfile.filename, logfile_path(@logfile) %></li>
4
+ <li class="active">
5
+ Displaying lines <%= Peephole::Logline.first(@page) %> - <%= Peephole::Logline.last(@page) %>
6
+ </li>
7
+ </ol>
8
+
9
+ <%= render 'pager' %>
10
+
11
+ <table class="table table-bordered">
12
+ <% if @loglines.present? %>
13
+ <tr>
14
+ <td>#num</td>
15
+ <td>
16
+ started_at<br>
17
+ <span class="text-muted">uuid</span>
18
+ </td>
19
+ <td>
20
+ method <code>target</code>
21
+ </td>
22
+ <td>status</td>
23
+ <td colspan="2">params</td>
24
+ </tr>
25
+ <% @loglines.each do |logline| %>
26
+ <% logline.params.each_with_index do |(key, value), i| %>
27
+ <tr>
28
+ <% if i.zero? %>
29
+ <% rowspan = logline.params.size %>
30
+ <td rowspan="<%= rowspan %>">
31
+ #<%= logline.num %>
32
+ </td>
33
+ <td rowspan="<%= rowspan %>" class="text-break">
34
+ <%= logline.started_at %><br>
35
+ <span class="text-muted"><%= logline.uuid %></span>
36
+ </td>
37
+ <td rowspan="<%= rowspan %>" class="text-break">
38
+ <%= logline.method %>
39
+ <code><%= logline.target %></code>
40
+ </td>
41
+ <td rowspan="<%= rowspan %>"><%= status_label(logline.status) %></td>
42
+ <% end %>
43
+ <td><%= key %></td>
44
+ <td>
45
+ <pre><%= value %></pre>
46
+ </td>
47
+ </tr>
48
+ <% end %>
49
+ <% end %>
50
+ <% else %>
51
+ <div class="well">No logline found.</div>
52
+ <% end %>
53
+ </table>
54
+
55
+ <%= render 'pager' %>
@@ -0,0 +1,19 @@
1
+ <ol class="breadcrumb">
2
+ <li class="active">Home</li>
3
+ </ol>
4
+
5
+ <table class="table">
6
+ <tbody>
7
+ <% @logfiles.each do |logfile| %>
8
+ <tr>
9
+ <td><%= link_to logfile.filename, logfile_path(logfile) %></td>
10
+ <td class="text-right">
11
+ <%= link_to download_logfile_path(logfile) do %>
12
+ <span class="glyphicon glyphicon-download-alt"></span>
13
+ <% end %>
14
+ </td>
15
+ </tr>
16
+ <% end %>
17
+ </tbody>
18
+ </table>
19
+
data/config/routes.rb ADDED
@@ -0,0 +1,10 @@
1
+ Peephole::Engine.routes.draw do
2
+ get 'welcome/index'
3
+ root 'welcome#index'
4
+
5
+ resources :logfiles, only: [:show], id: /.+/ do
6
+ member do
7
+ get :download
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ module Peephole
2
+ class InstallGenerator < Rails::Generators::Base
3
+ source_root File.expand_path('../templates', __FILE__)
4
+
5
+ def install
6
+ template 'log_tags.rb', 'config/initializers/log_tags.rb'
7
+ template 'initializer.rb', 'config/initializers/peephole.rb'
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,6 @@
1
+ Peephole.configure do |config|
2
+ config.paginates_per = 200
3
+ config.peeper? do
4
+ !Rails.env.production?
5
+ end
6
+ end
@@ -0,0 +1 @@
1
+ Rails.application.config.log_tags = [:uuid]
@@ -0,0 +1,15 @@
1
+ module Peephole
2
+ class Config
3
+ include ActiveSupport::Configurable
4
+
5
+ config_accessor :paginates_per do
6
+ 200
7
+ end
8
+
9
+ DEFAULT_PEEPER = proc { !Rails.env.production? }
10
+ def peeper?(&block)
11
+ @peeper = block if block_given?
12
+ @peeper.presence || DEFAULT_PEEPER
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,12 @@
1
+ module Peephole
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace Peephole
4
+
5
+ config.generators do |g|
6
+ g.stylesheets false
7
+ g.javascripts false
8
+ g.helper false
9
+ g.test_framework :rspec, view_specs: false, helper_specs: false
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,3 @@
1
+ module Peephole
2
+ VERSION = "0.0.1"
3
+ end
data/lib/peephole.rb ADDED
@@ -0,0 +1,14 @@
1
+ require "peephole/engine"
2
+ require "peephole/config"
3
+
4
+ module Peephole
5
+ class << self
6
+ def configure(&block)
7
+ yield config
8
+ end
9
+
10
+ def config
11
+ @config ||= Config.new
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :peephole do
3
+ # # Task goes here
4
+ # end
metadata ADDED
@@ -0,0 +1,125 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: peephole
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - tnantoka
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-07-16 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 4.2.3
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 4.2.3
27
+ - !ruby/object:Gem::Dependency
28
+ name: sqlite3
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec-rails
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: simplecov
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ description: Peephole is a Rails engine that provides interface to view logs.
70
+ email:
71
+ - tnantoka@bornneet.com
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - MIT-LICENSE
77
+ - README.md
78
+ - Rakefile
79
+ - app/assets/javascripts/peephole/application.js
80
+ - app/assets/stylesheets/peephole/application.css
81
+ - app/assets/stylesheets/peephole/peephole.css
82
+ - app/controllers/peephole/application_controller.rb
83
+ - app/controllers/peephole/logfiles_controller.rb
84
+ - app/controllers/peephole/welcome_controller.rb
85
+ - app/helpers/peephole/application_helper.rb
86
+ - app/models/peephole/logfile.rb
87
+ - app/models/peephole/logline.rb
88
+ - app/views/layouts/peephole/application.html.erb
89
+ - app/views/peephole/logfiles/_pager.html.erb
90
+ - app/views/peephole/logfiles/show.html.erb
91
+ - app/views/peephole/welcome/index.html.erb
92
+ - config/routes.rb
93
+ - lib/generators/peephole/install_generator.rb
94
+ - lib/generators/peephole/templates/initializer.rb
95
+ - lib/generators/peephole/templates/log_tags.rb
96
+ - lib/peephole.rb
97
+ - lib/peephole/config.rb
98
+ - lib/peephole/engine.rb
99
+ - lib/peephole/version.rb
100
+ - lib/tasks/peephole_tasks.rake
101
+ homepage: https://github.com/tnantoka/peephole
102
+ licenses:
103
+ - MIT
104
+ metadata: {}
105
+ post_install_message:
106
+ rdoc_options: []
107
+ require_paths:
108
+ - lib
109
+ required_ruby_version: !ruby/object:Gem::Requirement
110
+ requirements:
111
+ - - ">="
112
+ - !ruby/object:Gem::Version
113
+ version: '0'
114
+ required_rubygems_version: !ruby/object:Gem::Requirement
115
+ requirements:
116
+ - - ">="
117
+ - !ruby/object:Gem::Version
118
+ version: '0'
119
+ requirements: []
120
+ rubyforge_project:
121
+ rubygems_version: 2.4.5
122
+ signing_key:
123
+ specification_version: 4
124
+ summary: Log viewer for Rails
125
+ test_files: []