signalman 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +28 -0
  4. data/Rakefile +8 -0
  5. data/app/assets/config/signalman_manifest.js +0 -0
  6. data/app/controllers/signalman/application_controller.rb +4 -0
  7. data/app/controllers/signalman/generators/models_controller.rb +15 -0
  8. data/app/controllers/signalman/generators/scaffolds_controller.rb +15 -0
  9. data/app/controllers/signalman/jobs_controller.rb +11 -0
  10. data/app/controllers/signalman/mails_controller.rb +11 -0
  11. data/app/controllers/signalman/queries_controller.rb +11 -0
  12. data/app/controllers/signalman/requests_controller.rb +11 -0
  13. data/app/controllers/signalman/views_controller.rb +11 -0
  14. data/app/helpers/signalman/events_helper.rb +18 -0
  15. data/app/models/signalman/event.rb +25 -0
  16. data/app/models/signalman.rb +5 -0
  17. data/app/views/layouts/signalman/application.html.erb +22 -0
  18. data/app/views/signalman/generators/models/create.html.erb +5 -0
  19. data/app/views/signalman/generators/models/show.html.erb +36 -0
  20. data/app/views/signalman/generators/scaffolds/create.html.erb +9 -0
  21. data/app/views/signalman/generators/scaffolds/show.html.erb +36 -0
  22. data/app/views/signalman/jobs/index.html.erb +28 -0
  23. data/app/views/signalman/jobs/show.html.erb +17 -0
  24. data/app/views/signalman/mails/index.html.erb +28 -0
  25. data/app/views/signalman/mails/show.html.erb +17 -0
  26. data/app/views/signalman/queries/index.html.erb +24 -0
  27. data/app/views/signalman/queries/show.html.erb +17 -0
  28. data/app/views/signalman/requests/index.html.erb +31 -0
  29. data/app/views/signalman/requests/show.html.erb +19 -0
  30. data/app/views/signalman/shared/_flash.html.erb +7 -0
  31. data/app/views/signalman/shared/_nav.html.erb +30 -0
  32. data/app/views/signalman/views/index.html.erb +27 -0
  33. data/app/views/signalman/views/show.html.erb +20 -0
  34. data/config/routes.rb +16 -0
  35. data/db/migrate/20230729122215_create_signalman_events.rb +15 -0
  36. data/lib/signalman/engine.rb +16 -0
  37. data/lib/signalman/version.rb +3 -0
  38. data/lib/signalman.rb +183 -0
  39. data/lib/tasks/signalman_tasks.rake +4 -0
  40. metadata +98 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: b339dda80c8dea8c1ce6c651d543c808fdcacc634b013d27e3791fe9be6c1a13
4
+ data.tar.gz: 482987c4c114ee1e1f3c630c45e37879dc058b70747712e82b5f818da28fe129
5
+ SHA512:
6
+ metadata.gz: 9c998b4967fd840aa5c503fc4aa9242ea6e479300b96bab94d6720ab8fadb2c7630d967deed07f6a6360a3237371a6d58df52ab612366b3ac32c90bd57a1ae4d
7
+ data.tar.gz: 8c17c1381bbf37112493f7cf9afe13322951c2089f9d71b9e5bdd000eab4c843cc3dc19a085d6ce8aac103ef93127e8357c2834dbb5a8932065c612d01fcf29c
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2023 Chris Oliver
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,28 @@
1
+ # Signalman
2
+ Short description and motivation.
3
+
4
+ ## Usage
5
+ How to use my plugin.
6
+
7
+ ## Installation
8
+ Add this line to your application's Gemfile:
9
+
10
+ ```ruby
11
+ gem "signalman"
12
+ ```
13
+
14
+ And then execute:
15
+ ```bash
16
+ $ bundle
17
+ ```
18
+
19
+ Or install it yourself as:
20
+ ```bash
21
+ $ gem install signalman
22
+ ```
23
+
24
+ ## Contributing
25
+ Contribution directions go here.
26
+
27
+ ## License
28
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require "bundler/setup"
2
+
3
+ APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
4
+ load "rails/tasks/engine.rake"
5
+
6
+ load "rails/tasks/statistics.rake"
7
+
8
+ require "bundler/gem_tasks"
File without changes
@@ -0,0 +1,4 @@
1
+ module Signalman
2
+ class ApplicationController < ActionController::Base
3
+ end
4
+ end
@@ -0,0 +1,15 @@
1
+ require "open3"
2
+
3
+ module Signalman
4
+ class Generators::ModelsController < ApplicationController
5
+ def show
6
+ end
7
+
8
+ def create
9
+ @fields = params[:fields].map{ |field| [field[:name], field[:type]].join(":") }
10
+ Bundler.with_original_env do
11
+ @stdout, @stderr, @status = Open3.capture3("rails", "generate", "model", params[:model_name], *@fields)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ require "open3"
2
+
3
+ module Signalman
4
+ class Generators::ScaffoldsController < ApplicationController
5
+ def show
6
+ end
7
+
8
+ def create
9
+ @fields = params[:fields].map{ |field| [field[:name], field[:type]].join(":") }
10
+ Bundler.with_original_env do
11
+ @stdout, @stderr, @status = Open3.capture3("rails", "generate", "scaffold", params[:model_name], *@fields)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,11 @@
1
+ module Signalman
2
+ class JobsController < ApplicationController
3
+ def index
4
+ @events = Event.jobs.recent_first
5
+ end
6
+
7
+ def show
8
+ @event = Event.jobs.find(params[:id])
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ module Signalman
2
+ class MailsController < ApplicationController
3
+ def index
4
+ @events = Event.mails.recent_first
5
+ end
6
+
7
+ def show
8
+ @event = Event.mails.find(params[:id])
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ module Signalman
2
+ class QueriesController < ApplicationController
3
+ def index
4
+ @events = Event.queries.recent_first
5
+ end
6
+
7
+ def show
8
+ @event = Event.queries.find(params[:id])
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ module Signalman
2
+ class RequestsController < ApplicationController
3
+ def index
4
+ @events = Event.requests.recent_first
5
+ end
6
+
7
+ def show
8
+ @event = Event.requests.find(params[:id])
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ module Signalman
2
+ class ViewsController < ApplicationController
3
+ def index
4
+ @events = Event.views.recent_first
5
+ end
6
+
7
+ def show
8
+ @event = Event.views.find(params[:id])
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,18 @@
1
+ module Signalman
2
+ module EventsHelper
3
+ def signalman_path_for(event)
4
+ block = Signalman.events.find{ |key, _| key.match? event.name }.last[:path]
5
+ instance_exec event, &block
6
+ end
7
+
8
+ def badge_for_request_duration(duration, &block)
9
+ if duration <= 200
10
+ tag.span "#{duration.round}ms", class: "bg-gray-100 text-gray-800 px-2 py-1 rounded text-xs"
11
+ elsif duration <= 500
12
+ tag.span "#{duration.round}ms", class: "bg-yellow-100 text-yellow-800 px-2 py-1 rounded text-xs"
13
+ else
14
+ tag.span "#{duration.round}ms", class: "bg-red-100 text-red-800 rounded px-2 py-1 text-xs"
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,25 @@
1
+ class Signalman::Event < ApplicationRecord
2
+ scope :requests, ->{ where(name: "process_action.action_controller") }
3
+ scope :mails, ->{ where(name: "deliver.action_mailer") }
4
+ scope :queries, ->{ where(name: "sql.active_record") }
5
+ scope :views, ->{
6
+ where(name: [
7
+ "render_template.action_view",
8
+ "render_partial.action_view",
9
+ "render_collection.action_view",
10
+ "render_layout.action_view",
11
+ ])
12
+ }
13
+ scope :jobs, -> {
14
+ where(name: [
15
+ "enqueue_at.active_job",
16
+ "enqueue.active_job",
17
+ "perform.active_job",
18
+ "perform_start.active_job",
19
+ "discard.active_job",
20
+ ])
21
+ }
22
+
23
+
24
+ scope :recent_first, ->{ order(created_at: :desc) }
25
+ end
@@ -0,0 +1,5 @@
1
+ module Signalman
2
+ def self.table_name_prefix
3
+ "signalman_"
4
+ end
5
+ end
@@ -0,0 +1,22 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Signalman</title>
5
+ <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <%= csrf_meta_tags %>
7
+ <%= csp_meta_tag %>
8
+
9
+ <script src="https://cdn.tailwindcss.com?plugins=forms,typography,aspect-ratio"></script>
10
+ </head>
11
+
12
+ <body>
13
+ <div class="flex gap-4">
14
+ <%= render "signalman/shared/nav" %>
15
+ <div class="ml-72 w-full p-4 sm:p-8 prose max-w-none prose-a:no-underline">
16
+ <%= render "signalman/shared/flash" %>
17
+ <%= yield %>
18
+ </div>
19
+ </div>
20
+ </body>
21
+ </html>
22
+
@@ -0,0 +1,5 @@
1
+ <pre>
2
+ $ rails generate model <%= params[:model_name] %> <%= @fields.join(" ") %>
3
+ <%= simple_format @stdout %>
4
+ <%= simple_format @stderr %>
5
+ </pre>
@@ -0,0 +1,36 @@
1
+ <h1>Generate Model </h1>
2
+
3
+ <%= form_with url: generators_model_path do |form| %>
4
+ <div>
5
+ <%= form.label :model_name, class: "block" %>
6
+ <%= form.text_field :model_name, placeholder: "User", required: true %>
7
+ </div>
8
+
9
+ <template id="field">
10
+ <div class="mt-4">
11
+ <%= form.text_field "fields[][name]", placeholder: "email", required: true %>
12
+ <%= form.select "fields[][type]", options_for_select(ActiveRecord::Base.connection.class::NATIVE_DATABASE_TYPES.keys.excluding(:primary_key)) %>
13
+ <%= button_tag "Remove", onclick: "event.preventDefault(); this.parentElement.remove()" %>
14
+ </div>
15
+ </template>
16
+
17
+ <label>Fields</label>
18
+ <div id="fields">
19
+ </div>
20
+ <%= button_tag "Add field", onclick: "addField(event)" %>
21
+
22
+ <div class="mt-4">
23
+ <%= form.button "Generate" %>
24
+ </div>
25
+ <% end %>
26
+
27
+ <script>
28
+ const template = document.querySelector("#field")
29
+ const fields = document.querySelector("#fields")
30
+
31
+ function addField(event) {
32
+ event.preventDefault()
33
+ const clone = template.content.cloneNode(true)
34
+ fields.appendChild(clone)
35
+ }
36
+ </script>
@@ -0,0 +1,9 @@
1
+ <% if @status.success? %>
2
+ <p>Visit <%= link_to "/#{params[:model_name].underscore.pluralize}", "/#{params[:model_name].underscore.pluralize}", target: :_blank %></p>
3
+ <% end %>
4
+
5
+ <pre>
6
+ $ rails generate scaffold <%= params[:model_name] %> <%= @fields.join(" ") %>
7
+ <%= simple_format @stdout %>
8
+ <%= simple_format @stderr %>
9
+ </pre>
@@ -0,0 +1,36 @@
1
+ <h1>Generate Scaffold</h1>
2
+
3
+ <%= form_with url: generators_scaffold_path do |form| %>
4
+ <div>
5
+ <%= form.label :model_name, class: "block" %>
6
+ <%= form.text_field :model_name, placeholder: "User", required: true %>
7
+ </div>
8
+
9
+ <template id="field">
10
+ <div class="mt-4">
11
+ <%= form.text_field "fields[][name]", placeholder: "email", required: true %>
12
+ <%= form.select "fields[][type]", options_for_select(ActiveRecord::Base.connection.class::NATIVE_DATABASE_TYPES.keys.excluding(:primary_key)) %>
13
+ <%= button_tag "Remove", onclick: "event.preventDefault(); this.parentElement.remove()" %>
14
+ </div>
15
+ </template>
16
+
17
+ <label>Fields</label>
18
+ <div id="fields">
19
+ </div>
20
+ <%= button_tag "Add field", onclick: "addField(event)" %>
21
+
22
+ <div class="mt-4">
23
+ <%= form.button "Generate" %>
24
+ </div>
25
+ <% end %>
26
+
27
+ <script>
28
+ const template = document.querySelector("#field")
29
+ const fields = document.querySelector("#fields")
30
+
31
+ function addField(event) {
32
+ event.preventDefault()
33
+ const clone = template.content.cloneNode(true)
34
+ fields.appendChild(clone)
35
+ }
36
+ </script>
@@ -0,0 +1,28 @@
1
+ <h1>Jobs</h1>
2
+
3
+ <div class="table w-full">
4
+ <div class="table-header-group">
5
+ <div class="table-row font-bold">
6
+ <div class="table-cell border-b border-neutral-100 p-2">Job</div>
7
+ <div class="table-cell border-b border-neutral-100 p-2">Id</div>
8
+ <div class="table-cell border-b border-neutral-100 p-2">Arguments</div>
9
+ <div class="table-cell border-b border-neutral-100 p-2">Duration</div>
10
+ <div class="table-cell border-b border-neutral-100 p-2">Happened</div>
11
+ </div>
12
+ </div>
13
+ <div class="table-row-group">
14
+ <% @events.each do |event| %>
15
+ <%= link_to signalman_path_for(event), class: "table-row", id: dom_id(event) do %>
16
+ <%= tag.div event.payload["class"], class: "table-cell border-b border-neutral-100 p-2" %>
17
+ <%= tag.div event.payload["id"], class: "table-cell border-b border-neutral-100 p-2" %>
18
+ <%= tag.div event.payload["args"], class: "table-cell border-b border-neutral-100 p-2" %>
19
+ <%= tag.div class: "table-cell border-b border-neutral-100 p-2" do %>
20
+ <%= event.duration.round %>ms
21
+ <% end %>
22
+ <%= tag.div class: "table-cell border-b border-neutral-100 p-2" do %>
23
+ <%= time_ago_in_words event.started_at %>
24
+ <% end %>
25
+ <% end %>
26
+ <% end %>
27
+ </div>
28
+ </div>
@@ -0,0 +1,17 @@
1
+ <h1 class="mb-2"><%= @event.payload["class"] %></h1>
2
+ <div class="text-xs">Started at <%= @event.started_at %></div>
3
+
4
+ <h4>ID</h4>
5
+ <pre><%= @event.payload["id"] %></pre>
6
+
7
+ <h4>Queue Name</h4>
8
+ <pre><%= @event.payload["queue_name"] %></pre>
9
+
10
+ <h4>Enqueued At</h4>
11
+ <pre><%= @event.payload["enqueued_at"] %></pre>
12
+
13
+ <h4>Scheduled At</h4>
14
+ <pre><%= @event.payload["scheduled_at"] || "nil" %></pre>
15
+
16
+ <h4>Arguments</h4>
17
+ <pre><%= @event.payload["args"] %></pre>
@@ -0,0 +1,28 @@
1
+ <h1>Mail</h1>
2
+
3
+ <div class="table w-full">
4
+ <div class="table-header-group">
5
+ <div class="table-row font-bold">
6
+ <div class="table-cell border-b border-neutral-100 p-2">Mailer</div>
7
+ <div class="table-cell border-b border-neutral-100 p-2">Subject</div>
8
+ <div class="table-cell border-b border-neutral-100 p-2">To</div>
9
+ <div class="table-cell border-b border-neutral-100 p-2">Duration</div>
10
+ <div class="table-cell border-b border-neutral-100 p-2">Happened</div>
11
+ </div>
12
+ </div>
13
+ <div class="table-row-group">
14
+ <% @events.each do |event| %>
15
+ <%= link_to signalman_path_for(event), class: "table-row", id: dom_id(event) do %>
16
+ <%= tag.div event.payload["mailer"], class: "table-cell border-b border-neutral-100 p-2" %>
17
+ <%= tag.div event.payload["subject"], class: "table-cell border-b border-neutral-100 p-2" %>
18
+ <%= tag.div event.payload["to"], class: "table-cell border-b border-neutral-100 p-2" %>
19
+ <%= tag.div class: "table-cell border-b border-neutral-100 p-2" do %>
20
+ <%= event.duration.round %>ms
21
+ <% end %>
22
+ <%= tag.div class: "table-cell border-b border-neutral-100 p-2" do %>
23
+ <%= time_ago_in_words event.started_at %>
24
+ <% end %>
25
+ <% end %>
26
+ <% end %>
27
+ </div>
28
+ </div>
@@ -0,0 +1,17 @@
1
+ <h1 class="mb-2"><%= @event.payload["mailer"] %></h1>
2
+ <div class="text-xs">Started at <%= @event.started_at %></div>
3
+
4
+ <h4>Message ID</h4>
5
+ <pre><%= @event.payload["message_id"] %></pre>
6
+
7
+ <h4>Subject</h4>
8
+ <pre><%= @event.payload["subject"] %></pre>
9
+
10
+ <h4>To</h4>
11
+ <pre><%= @event.payload["to"] %></pre>
12
+
13
+ <h4>From </h4>
14
+ <pre><%= @event.payload["from"] %></pre>
15
+
16
+ <h4>Mail</h4>
17
+ <pre><%= @event.payload["mail"] %></pre>
@@ -0,0 +1,24 @@
1
+ <h1>Queries</h1>
2
+
3
+ <div class="table w-full">
4
+ <div class="table-header-group">
5
+ <div class="table-row font-bold">
6
+ <div class="table-cell border-b border-neutral-100 p-2">SQL</div>
7
+ <div class="table-cell border-b border-neutral-100 p-2">Duration</div>
8
+ <div class="table-cell border-b border-neutral-100 p-2">Happened</div>
9
+ </div>
10
+ </div>
11
+ <div class="table-row-group">
12
+ <% @events.each do |event| %>
13
+ <%= link_to signalman_path_for(event), class: "table-row", id: dom_id(event) do %>
14
+ <%= tag.div event.payload["sql"], class: "table-cell border-b border-neutral-100 p-2" %>
15
+ <%= tag.div class: "table-cell border-b border-neutral-100 p-2" do %>
16
+ <%= event.duration.round %>ms
17
+ <% end %>
18
+ <%= tag.div class: "table-cell border-b border-neutral-100 p-2" do %>
19
+ <%= time_ago_in_words event.started_at %>
20
+ <% end %>
21
+ <% end %>
22
+ <% end %>
23
+ </div>
24
+ </div>
@@ -0,0 +1,17 @@
1
+ <h1 class="mb-2"><%= @event.payload["name"] %></h1>
2
+ <div class="text-xs">Started at <%= @event.started_at %></div>
3
+
4
+ <h4>SQL</h4>
5
+ <pre><%= @event.payload["sql"] %></pre>
6
+
7
+ <h4>Binds</h4>
8
+ <pre><%= @event.payload["binds"] %></pre>
9
+
10
+ <h4>Type Casted Binds</h4>
11
+ <pre><%= @event.payload["type_casted_binds"] %></pre>
12
+
13
+ <h4>Statement Name</h4>
14
+ <pre><%= @event.payload["statement_name"] %></pre>
15
+
16
+ <h4>Async</h4>
17
+ <pre><%= @event.payload["async"] %></pre>
@@ -0,0 +1,31 @@
1
+ <h1>Requests</h1>
2
+
3
+ <div class="table w-full">
4
+ <div class="table-header-group">
5
+ <div class="table-row font-bold">
6
+ <div class="table-cell border-b border-neutral-100 p-2">Method</div>
7
+ <div class="table-cell border-b border-neutral-100 p-2">Path</div>
8
+ <div class="table-cell border-b border-neutral-100 p-2">Status</div>
9
+ <div class="table-cell border-b border-neutral-100 p-2">Duration</div>
10
+ <div class="table-cell border-b border-neutral-100 p-2">Happened</div>
11
+ </div>
12
+ </div>
13
+ <div class="table-row-group">
14
+ <% @events.each do |event| %>
15
+ <%= link_to signalman_path_for(event), class: "table-row", id: dom_id(event) do %>
16
+ <%= tag.div event.payload["method"], class: "table-cell border-b border-neutral-100 p-2" %>
17
+ <%= tag.div event.payload["path"], class: "table-cell border-b border-neutral-100 p-2" %>
18
+ <%= tag.div class: "table-cell border-b border-neutral-100 p-2" do %>
19
+ <%= event.payload["status"] %>
20
+ <%= Rack::Utils::HTTP_STATUS_CODES[event.payload["status"]] %>
21
+ <% end %>
22
+ <%= tag.div class: "table-cell border-b border-neutral-100 p-2" do %>
23
+ <%= badge_for_request_duration(event.duration) %>
24
+ <% end %>
25
+ <%= tag.div class: "table-cell border-b border-neutral-100 p-2" do %>
26
+ <%= time_ago_in_words event.started_at %>
27
+ <% end %>
28
+ <% end %>
29
+ <% end %>
30
+ </div>
31
+ </div>
@@ -0,0 +1,19 @@
1
+ <h1 class="mb-2"><%= @event.payload["method"] %> <%= @event.payload["path"] %> as <%= @event.payload["format"].upcase %></h1>
2
+ <div class="text-xs">Started at <%= @event.started_at %></div>
3
+
4
+ <h4>Processed by</h4>
5
+ <pre><%= @event.payload["controller"] %>#<%= @event.payload["action"] %></pre>
6
+ <p class="text-sm">Completed in <%= @event.duration.round %>ms (Views: <%= @event.payload["view_runtime"].round %>ms | ActiveRecord: <%= @event.payload["db_runtime"].round %>ms)</p>
7
+
8
+ <h4>Response</h4>
9
+ <pre><%= @event.payload["status"] %> <%= Rack::Utils::HTTP_STATUS_CODES[@event.payload["status"]] %></pre>
10
+
11
+ <h4>Params</h4>
12
+ <pre><%= @event.payload["params"] %></pre>
13
+
14
+ <h4>Headers</h4>
15
+ <pre>
16
+ <% @event.payload["headers"].sort.each do |key, value| %>
17
+ <%= key %>: <%= value %>
18
+ <% end %>
19
+ </pre>
@@ -0,0 +1,7 @@
1
+ <% if notice %>
2
+ <p><%= notice %></p>
3
+ <% end %>
4
+
5
+ <% if alert %>
6
+ <p><%= alert %></p>
7
+ <% end %>
@@ -0,0 +1,30 @@
1
+ <nav class="p-4 sm:p-8 fixed inset-y-0 w-72 bg-neutral-900 text-white">
2
+ <h1 class="flex items-center gap-1 font-bold">
3
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-6 h-6">
4
+ <path fill-rule="evenodd" d="M10.5 3.798v5.02a3 3 0 01-.879 2.121l-2.377 2.377a9.845 9.845 0 015.091 1.013 8.315 8.315 0 005.713.636l.285-.071-3.954-3.955a3 3 0 01-.879-2.121v-5.02a23.614 23.614 0 00-3 0zm4.5.138a.75.75 0 00.093-1.495A24.837 24.837 0 0012 2.25a25.048 25.048 0 00-3.093.191A.75.75 0 009 3.936v4.882a1.5 1.5 0 01-.44 1.06l-6.293 6.294c-1.62 1.621-.903 4.475 1.471 4.88 2.686.46 5.447.698 8.262.698 2.816 0 5.576-.239 8.262-.697 2.373-.406 3.092-3.26 1.47-4.881L15.44 9.879A1.5 1.5 0 0115 8.818V3.936z" clip-rule="evenodd" />
5
+ </svg>
6
+ <%= link_to "Signalman", root_path %>
7
+ </h1>
8
+
9
+ <br />
10
+ <%= link_to "Back to app", main_app.root_path, class: "block text-sm" %>
11
+
12
+ <br />
13
+ <%= link_to "Requests", requests_path, class: "block" %>
14
+ <%= link_to "Jobs", jobs_path, class: "block" %>
15
+ <%= link_to "Mail", mails_path, class: "block" %>
16
+ <%= link_to "Queries", queries_path, class: "block" %>
17
+ <%= link_to "Views", views_path, class: "block" %>
18
+ <%#= link_to "Cache", requests_path, class: "block" %>
19
+ <%#= link_to "Exceptions", requests_path, class: "block" %>
20
+
21
+ <br/>
22
+ <h2>Generators</h2>
23
+ <%= link_to "Model", generators_model_path, class: "block" %>
24
+ <%= link_to "Scaffold", generators_scaffold_path, class: "block" %>
25
+
26
+ <br/>
27
+ <%= link_to "Routes", "/rails/info/routes", class: "block" %>
28
+ <%= link_to "Mailer Previews", "/rails/mailers", class: "block" %>
29
+ <%= link_to "Inbound Emails", "/rails/conductor/action_mailbox/inbound_emails", class: "block" %>
30
+ </nav>
@@ -0,0 +1,27 @@
1
+ <h1>Views</h1>
2
+
3
+ <div class="table w-full">
4
+ <div class="table-header-group">
5
+ <div class="table-row font-bold">
6
+ <div class="table-cell border-b border-neutral-100 p-2">Type</div>
7
+ <div class="table-cell border-b border-neutral-100 p-2">Identifier</div>
8
+ <div class="table-cell border-b border-neutral-100 p-2">Duration</div>
9
+ <div class="table-cell border-b border-neutral-100 p-2">Happened</div>
10
+ </div>
11
+ </div>
12
+ <div class="table-row-group">
13
+ <% @events.each do |event| %>
14
+ <%= link_to signalman_path_for(event), class: "table-row", id: dom_id(event) do %>
15
+ <%= tag.div event.name.split(".").first.humanize, class: "table-cell border-b border-neutral-100 p-2" %>
16
+ <%= tag.div event.payload["identifier"], class: "table-cell border-b border-neutral-100 p-2" %>
17
+ <%= tag.div class: "table-cell border-b border-neutral-100 p-2" do %>
18
+ <%= badge_for_request_duration(event.duration) %>
19
+ <% end %>
20
+ <%= tag.div class: "table-cell border-b border-neutral-100 p-2" do %>
21
+ <%= time_ago_in_words event.started_at %>
22
+ <% end %>
23
+ <% end %>
24
+ <% end %>
25
+ </div>
26
+ </div>
27
+
@@ -0,0 +1,20 @@
1
+ <h1 class="mb-2"><%= @event.name.split(".").first.humanize %></h1>
2
+ <div class="text-xs">Started at <%= @event.started_at %></div>
3
+
4
+ <h4>Identifier</h4>
5
+ <pre><%= @event.payload["identifier"] %></pre>
6
+
7
+ <% if @event.payload.has_key?("layout") %>
8
+ <h4>Layout</h4>
9
+ <pre><%= @event.payload["layout"] %></pre>
10
+ <% end %>
11
+
12
+ <% if @event.payload.has_key?("count") %>
13
+ <h4>Count</h4>
14
+ <pre><%= @event.payload["count"] %></pre>
15
+ <% end %>
16
+
17
+ <% if @event.payload.has_key?("cache_hits") %>
18
+ <h4>Cache Hits</h4>
19
+ <pre><%= @event.payload["cache_hits"] %></pre>
20
+ <% end %>
data/config/routes.rb ADDED
@@ -0,0 +1,16 @@
1
+ Signalman::Engine.routes.draw do
2
+ resources :requests
3
+ resources :jobs
4
+ resources :cache
5
+ resources :exceptions
6
+ resources :mails
7
+ resources :queries
8
+ resources :views
9
+
10
+ namespace :generators do
11
+ resource :scaffold
12
+ resource :model
13
+ end
14
+
15
+ root "requests#index"
16
+ end
@@ -0,0 +1,15 @@
1
+ class CreateSignalmanEvents < ActiveRecord::Migration[7.0]
2
+ def change
3
+ create_table :signalman_events do |t|
4
+ t.string :name
5
+ t.datetime :started_at
6
+ t.datetime :finished_at
7
+ t.float :duration
8
+ t.json :payload
9
+
10
+ t.timestamps
11
+ end
12
+
13
+ add_index :signalman_events, [:name, :duration]
14
+ end
15
+ end
@@ -0,0 +1,16 @@
1
+ module Signalman
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace Signalman
4
+
5
+ initializer "signalman.register_watchers" do
6
+ Signalman.register_watchers
7
+ end
8
+
9
+ # helpers must be accessible anywhere for Turbo broadcasts
10
+ initializer 'signalman.helpers' do
11
+ ActiveSupport.on_load :action_controller do
12
+ helper Signalman::EventsHelper
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,3 @@
1
+ module Signalman
2
+ VERSION = "0.1.0"
3
+ end
data/lib/signalman.rb ADDED
@@ -0,0 +1,183 @@
1
+ require "signalman/version"
2
+ require "signalman/engine"
3
+
4
+ module Signalman
5
+ class BaseHandler
6
+ attr_reader :current_time, :event
7
+
8
+ def self.call(event)
9
+ current_time = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond)
10
+ new(event, current_time).start
11
+ end
12
+
13
+ def initialize(event, current_time)
14
+ @event, @current_time = event, current_time
15
+ end
16
+
17
+ def start
18
+ process unless skip?
19
+ end
20
+
21
+ def process
22
+ create_event
23
+ end
24
+
25
+ def skip?
26
+ false
27
+ end
28
+
29
+ def create_event(payload = nil)
30
+ payload ||= event.payload
31
+
32
+ Event.create(
33
+ name: event.name,
34
+ started_at: started_at,
35
+ finished_at: finished_at,
36
+ duration: event.duration,
37
+ payload: payload
38
+ )
39
+ end
40
+
41
+ # Time measure since system boot with
42
+ # Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond)
43
+ def started_at
44
+ Time.current - ((current_time - event.time) / 1_000)
45
+ end
46
+
47
+ def finished_at
48
+ Time.current - ((current_time - event.end) / 1_000)
49
+ end
50
+ end
51
+
52
+ class ActionHandler < BaseHandler
53
+ def process
54
+ headers = {}
55
+ event.payload.fetch(:headers, {}).each do |name, value|
56
+ headers[name] = value if name.start_with?("HTTP")
57
+ headers[name] = value if ActionDispatch::Http::Headers::CGI_VARIABLES.include?(name)
58
+
59
+ [
60
+ "action_dispatch.request_id"
61
+ ].each do |header_name|
62
+ headers[name] = value if name == header_name
63
+ end
64
+ end
65
+
66
+ create_event event.payload.slice(
67
+ :method,
68
+ :path,
69
+ :controller,
70
+ :action,
71
+ :params,
72
+ :format,
73
+ :status,
74
+ :db_runtime,
75
+ :view_runtime
76
+ ).merge(headers: headers)
77
+ end
78
+
79
+ def skip?
80
+ event.payload[:controller].start_with?("Signalman::")
81
+ end
82
+ end
83
+
84
+ class ViewHandler < BaseHandler
85
+ def skip?
86
+ event.payload[:identifier].include?("app/views/signalman/")
87
+ end
88
+ end
89
+
90
+ class QueryHandler < BaseHandler
91
+ def skip?
92
+ ["SCHEMA", "TRANSACTION"].include?(event.payload[:name]) ||
93
+ event.payload[:name]&.include?("Signalman::")
94
+ end
95
+
96
+ def process
97
+ create_event event.payload.except(:connection)
98
+ end
99
+ end
100
+
101
+ class MailHandler < BaseHandler
102
+ end
103
+
104
+ class JobHandler < BaseHandler
105
+ def process
106
+ job = event.payload[:job]
107
+ create_event(
108
+ class: job.class.name,
109
+ id: job.job_id,
110
+ enqueued_at: job.enqueued_at,
111
+ scheduled_at: scheduled_at(event),
112
+ queue_name: queue_name(event),
113
+ args: args_info(job)
114
+ )
115
+ end
116
+
117
+ # From ActiveJob::LogSubscriber
118
+ def queue_name(event)
119
+ # Rails 7.1 -> ActiveJob.adapter_name(event.payload[:adapter]) + "(#{event.payload[:job].queue_name})"
120
+ event.payload[:adapter].class.name.demodulize.remove("Adapter") + "(#{event.payload[:job].queue_name})"
121
+ end
122
+
123
+ def args_info(job)
124
+ if job.class.log_arguments? && job.arguments.any?
125
+ " with arguments: " +
126
+ job.arguments.map { |arg| format(arg).inspect }.join(", ")
127
+ else
128
+ ""
129
+ end
130
+ end
131
+
132
+ def format(arg)
133
+ case arg
134
+ when Hash
135
+ arg.transform_values { |value| format(value) }
136
+ when Array
137
+ arg.map { |value| format(value) }
138
+ when GlobalID::Identification
139
+ arg.to_global_id rescue arg
140
+ else
141
+ arg
142
+ end
143
+ end
144
+
145
+ def scheduled_at(event)
146
+ return unless event.payload[:job].scheduled_at
147
+ Time.at(event.payload[:job].scheduled_at).utc
148
+ end
149
+ end
150
+
151
+ cattr_accessor :events, default: {
152
+ "process_action.action_controller" => {
153
+ handler: ActionHandler,
154
+ path: ->(event) { request_path(event) }
155
+ },
156
+ /^\w+\.action_view/ => {
157
+ handler: ViewHandler,
158
+ path: ->(event) { view_path(event) }
159
+ },
160
+ "sql.active_record" => {
161
+ handler: QueryHandler,
162
+ path: ->(event) { query_path(event) }
163
+ },
164
+ "deliver.action_mailer" => {
165
+ handler: MailHandler,
166
+ path: ->(event) { mail_path(event) }
167
+ },
168
+ /^\w+\.active_job/ => {
169
+ handler: JobHandler,
170
+ path: ->(event) { job_path(event) }
171
+ }
172
+ }
173
+
174
+ def self.register_watchers
175
+ events.each do |event_name, options|
176
+ options[:subscriber] = add_watcher event_name, options[:handler]
177
+ end
178
+ end
179
+
180
+ def self.add_watcher(event_name, handler)
181
+ ActiveSupport::Notifications.subscribe event_name, handler
182
+ end
183
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :signalman do
3
+ # # Task goes here
4
+ # end
metadata ADDED
@@ -0,0 +1,98 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: signalman
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Chris Oliver
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2023-08-01 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: 7.0.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 7.0.0
27
+ description: Development tools for Ruby on Rails
28
+ email:
29
+ - excid3@gmail.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - MIT-LICENSE
35
+ - README.md
36
+ - Rakefile
37
+ - app/assets/config/signalman_manifest.js
38
+ - app/controllers/signalman/application_controller.rb
39
+ - app/controllers/signalman/generators/models_controller.rb
40
+ - app/controllers/signalman/generators/scaffolds_controller.rb
41
+ - app/controllers/signalman/jobs_controller.rb
42
+ - app/controllers/signalman/mails_controller.rb
43
+ - app/controllers/signalman/queries_controller.rb
44
+ - app/controllers/signalman/requests_controller.rb
45
+ - app/controllers/signalman/views_controller.rb
46
+ - app/helpers/signalman/events_helper.rb
47
+ - app/models/signalman.rb
48
+ - app/models/signalman/event.rb
49
+ - app/views/layouts/signalman/application.html.erb
50
+ - app/views/signalman/generators/models/create.html.erb
51
+ - app/views/signalman/generators/models/show.html.erb
52
+ - app/views/signalman/generators/scaffolds/create.html.erb
53
+ - app/views/signalman/generators/scaffolds/show.html.erb
54
+ - app/views/signalman/jobs/index.html.erb
55
+ - app/views/signalman/jobs/show.html.erb
56
+ - app/views/signalman/mails/index.html.erb
57
+ - app/views/signalman/mails/show.html.erb
58
+ - app/views/signalman/queries/index.html.erb
59
+ - app/views/signalman/queries/show.html.erb
60
+ - app/views/signalman/requests/index.html.erb
61
+ - app/views/signalman/requests/show.html.erb
62
+ - app/views/signalman/shared/_flash.html.erb
63
+ - app/views/signalman/shared/_nav.html.erb
64
+ - app/views/signalman/views/index.html.erb
65
+ - app/views/signalman/views/show.html.erb
66
+ - config/routes.rb
67
+ - db/migrate/20230729122215_create_signalman_events.rb
68
+ - lib/signalman.rb
69
+ - lib/signalman/engine.rb
70
+ - lib/signalman/version.rb
71
+ - lib/tasks/signalman_tasks.rake
72
+ homepage: https://github.com/excid3/signalman
73
+ licenses:
74
+ - MIT
75
+ metadata:
76
+ homepage_uri: https://github.com/excid3/signalman
77
+ source_code_uri: https://github.com/excid3/signalman
78
+ changelog_uri: https://github.com/excid3/signalman/blob/main/CHANGELOG.md
79
+ post_install_message:
80
+ rdoc_options: []
81
+ require_paths:
82
+ - lib
83
+ required_ruby_version: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - ">="
86
+ - !ruby/object:Gem::Version
87
+ version: '0'
88
+ required_rubygems_version: !ruby/object:Gem::Requirement
89
+ requirements:
90
+ - - ">="
91
+ - !ruby/object:Gem::Version
92
+ version: '0'
93
+ requirements: []
94
+ rubygems_version: 3.4.17
95
+ signing_key:
96
+ specification_version: 4
97
+ summary: Development tools for Ruby on Rails
98
+ test_files: []