rails_observatory 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (97) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +42 -0
  3. data/Rakefile +8 -0
  4. data/app/assets/config/rails_observatory_manifest.js +2 -0
  5. data/app/assets/images/rails_observatory/logo.svg +8 -0
  6. data/app/assets/js/application.js +88 -0
  7. data/app/assets/js/controllers/chart_controller.js +176 -0
  8. data/app/assets/js/controllers/event_details_controller.js +15 -0
  9. data/app/assets/js/controllers/index.js +9 -0
  10. data/app/assets/js/controllers/sparkline_controller.js +72 -0
  11. data/app/assets/stylesheets/application/card.css +51 -0
  12. data/app/assets/stylesheets/application/chart.css +34 -0
  13. data/app/assets/stylesheets/application/dropdown.css +62 -0
  14. data/app/assets/stylesheets/application/global_modifiers.css +10 -0
  15. data/app/assets/stylesheets/application/query_table.css +68 -0
  16. data/app/assets/stylesheets/application/side_nav.css +62 -0
  17. data/app/assets/stylesheets/application/side_panel.css +35 -0
  18. data/app/assets/stylesheets/application/tab_nav.css +64 -0
  19. data/app/assets/stylesheets/application/table_chart.css +66 -0
  20. data/app/assets/stylesheets/application/tbd.css +70 -0
  21. data/app/assets/stylesheets/application/top_nav.css +33 -0
  22. data/app/assets/stylesheets/application.css +42 -0
  23. data/app/assets/stylesheets/elements/a.css +8 -0
  24. data/app/assets/stylesheets/elements/button.css +21 -0
  25. data/app/assets/stylesheets/elements/details.css +12 -0
  26. data/app/assets/stylesheets/elements/root.css +26 -0
  27. data/app/assets/stylesheets/elements/section.css +9 -0
  28. data/app/assets/stylesheets/errors/show/details.css +13 -0
  29. data/app/assets/stylesheets/layout/app.css +23 -0
  30. data/app/assets/stylesheets/layout/details-side-panel.css +15 -0
  31. data/app/assets/stylesheets/layout/requests.css +45 -0
  32. data/app/assets/stylesheets/layout/two-column.css +17 -0
  33. data/app/assets/stylesheets/mixins/nav_button.css +19 -0
  34. data/app/assets/stylesheets/requests/stats.css +35 -0
  35. data/app/controllers/rails_observatory/application_controller.rb +24 -0
  36. data/app/controllers/rails_observatory/errors_controller.rb +27 -0
  37. data/app/controllers/rails_observatory/jobs_controller.rb +25 -0
  38. data/app/controllers/rails_observatory/mailers_controller.rb +11 -0
  39. data/app/controllers/rails_observatory/requests_controller.rb +33 -0
  40. data/app/helpers/rails_observatory/application_helper.rb +110 -0
  41. data/app/jobs/rails_observatory/application_job.rb +4 -0
  42. data/app/mailers/rails_observatory/application_mailer.rb +6 -0
  43. data/app/views/layouts/rails_observatory/application.html.erb +93 -0
  44. data/app/views/new_user_mailer/greeting.html.erb +1 -0
  45. data/app/views/posts/index.html.erb +1 -0
  46. data/app/views/rails_observatory/application/_chart.html.erb +23 -0
  47. data/app/views/rails_observatory/application/_events_table.html.erb +24 -0
  48. data/app/views/rails_observatory/application/_sparkline.html.erb +17 -0
  49. data/app/views/rails_observatory/application/_trace.html.erb +122 -0
  50. data/app/views/rails_observatory/errors/index.html.erb +87 -0
  51. data/app/views/rails_observatory/errors/show.html.erb +193 -0
  52. data/app/views/rails_observatory/jobs/_table_chart.html.erb +29 -0
  53. data/app/views/rails_observatory/jobs/index.html.erb +20 -0
  54. data/app/views/rails_observatory/jobs/show.html.erb +8 -0
  55. data/app/views/rails_observatory/logs/index.html.erb +18 -0
  56. data/app/views/rails_observatory/mailers/index.html.erb +11 -0
  57. data/app/views/rails_observatory/mailers/show.html.erb +10 -0
  58. data/app/views/rails_observatory/requests/_text_gauge.html.erb +4 -0
  59. data/app/views/rails_observatory/requests/index.html.erb +56 -0
  60. data/app/views/rails_observatory/requests/show.html.erb +16 -0
  61. data/config/routes.rb +7 -0
  62. data/lib/rails_observatory/action_mailer_subscriber.rb +14 -0
  63. data/lib/rails_observatory/engine.rb +49 -0
  64. data/lib/rails_observatory/event_collector.rb +43 -0
  65. data/lib/rails_observatory/log_collector.rb +46 -0
  66. data/lib/rails_observatory/mailer_previews/delivered_mail_preview.rb +9 -0
  67. data/lib/rails_observatory/middleware.rb +77 -0
  68. data/lib/rails_observatory/models/error.rb +67 -0
  69. data/lib/rails_observatory/models/event_collection.rb +137 -0
  70. data/lib/rails_observatory/models/events.rb +22 -0
  71. data/lib/rails_observatory/models/job_trace.rb +28 -0
  72. data/lib/rails_observatory/models/logs.rb +9 -0
  73. data/lib/rails_observatory/models/mail_delivery.rb +33 -0
  74. data/lib/rails_observatory/models/redis_model.rb +112 -0
  75. data/lib/rails_observatory/models/request_trace.rb +29 -0
  76. data/lib/rails_observatory/railties/active_job_instrumentation.rb +48 -0
  77. data/lib/rails_observatory/railties/redis_runtime.rb +11 -0
  78. data/lib/rails_observatory/redis/logging_middleware.rb +22 -0
  79. data/lib/rails_observatory/redis/redis_client_instrumentation.rb +18 -0
  80. data/lib/rails_observatory/redis/time_series/increment_script.lua +67 -0
  81. data/lib/rails_observatory/redis/time_series/insertion.rb +73 -0
  82. data/lib/rails_observatory/redis/time_series/query_builder.rb +149 -0
  83. data/lib/rails_observatory/redis/time_series/timing_script.lua +89 -0
  84. data/lib/rails_observatory/redis/time_series.rb +91 -0
  85. data/lib/rails_observatory/serializers/event_serializer.rb +19 -0
  86. data/lib/rails_observatory/serializers/headers_serializer.rb +12 -0
  87. data/lib/rails_observatory/serializers/job_serializer.rb +11 -0
  88. data/lib/rails_observatory/serializers/mail_delivery_job_serializer.rb +14 -0
  89. data/lib/rails_observatory/serializers/request_serializer.rb +17 -0
  90. data/lib/rails_observatory/serializers/response_serializer.rb +14 -0
  91. data/lib/rails_observatory/serializers/serializer.rb +51 -0
  92. data/lib/rails_observatory/version.rb +3 -0
  93. data/lib/rails_observatory.rb +3 -0
  94. data/public/assets/js/application.js +11186 -0
  95. data/public/assets/logo_with_text.svg +21 -0
  96. data/public/assets/stylesheets/application.css +757 -0
  97. metadata +197 -0
@@ -0,0 +1,25 @@
1
+ module RailsObservatory
2
+ class JobsController < ApplicationController
3
+
4
+ before_action :set_duration
5
+
6
+ around_action :set_time_range
7
+
8
+ def index
9
+ JobTrace.ensure_index
10
+ @recent_jobs = JobTrace.all.take(10)
11
+ end
12
+
13
+ def show
14
+ @job = JobTrace.find(params[:id])
15
+ @events = @job.events
16
+
17
+ end
18
+
19
+ def set_time_range
20
+ TimeSeries.with_slice(duration.seconds.ago..) do
21
+ yield
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,11 @@
1
+ module RailsObservatory
2
+ class MailersController < ApplicationController
3
+
4
+ def index
5
+
6
+ MailDelivery.ensure_index
7
+ @deliveries = MailDelivery.all
8
+
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,33 @@
1
+ module RailsObservatory
2
+ class RequestsController < ApplicationController
3
+
4
+ def index
5
+ @time_range = (duration.seconds.ago..)
6
+
7
+ if params[:controller_action].blank?
8
+ @count_by_controller = TimeSeries.where(name: 'request.count', action: '*')
9
+ .slice(@time_range)
10
+ .downsample(1, using: :sum)
11
+ .select { _1.value > 0 }
12
+ .sort_by(&:value)
13
+ .reverse
14
+
15
+ @latency_by_controller = TimeSeries.where(name: 'request.latency', action: '*')
16
+ .slice(@time_range)
17
+ .downsample(1, using: :avg)
18
+ .index_by { _1.labels[:action] }
19
+ end
20
+
21
+ RequestTrace.ensure_index
22
+ @events = RequestTrace.all
23
+ end
24
+
25
+ def show
26
+ @request = RequestTrace.find(params[:id])
27
+ @middleware_events = @request.events.only('process_middleware.action_dispatch')
28
+ @events = @request.events
29
+ @icicle_chart_series = @events.to_series
30
+ end
31
+
32
+ end
33
+ end
@@ -0,0 +1,110 @@
1
+ require 'rouge'
2
+ module RailsObservatory
3
+ module ApplicationHelper
4
+
5
+ def highlight_source_extract(source_extract)
6
+ source_extract.symbolize_keys => {code:, line_number:}
7
+ fmt = Rouge::Formatters::HTMLLineTable.new(Rouge::Formatters::HTML.new, start_line: code.keys.first.to_s.to_i)
8
+ html = Nokogiri::HTML4(Rouge.highlight(code.values.flatten.join(""), 'ruby', fmt))
9
+ html.css("#line-#{line_number}").add_class('hll')
10
+ line = code[line_number.to_s]
11
+ if line.length > 1
12
+ html.css("#line-#{line_number}").attr('data-highlight-start', line[0].length).attr('data-highlight-length', line[1].length)
13
+ end
14
+ html.to_html
15
+ end
16
+
17
+ def series_for(name:, aggregate_using:, time_range: nil, downsample: 120, **opts)
18
+ series = RailsObservatory::TimeSeries.where(name:, **opts).downsample(downsample, using: aggregate_using)
19
+ series = series.slice(time_range) if time_range
20
+ series
21
+ end
22
+
23
+ def series_value(name:, aggregate_using:)
24
+ series = series_for(name:, aggregate_using: aggregate_using, downsample: 1)
25
+ series.first&.value
26
+ end
27
+
28
+ def time_slice_start
29
+ @time_range.begin.to_i * 1000
30
+ end
31
+
32
+ def time_slice_end
33
+ time = @time_range.end.nil? ? Time.now.to_i : @time_range.end.to_i
34
+ time * 1000
35
+ end
36
+
37
+ def format_event_value(value)
38
+ if value.is_a?(Numeric)
39
+ value.round(2)
40
+ else
41
+ value
42
+ end
43
+ end
44
+
45
+ def pretty_backtrace_location(backtrace_line)
46
+ path, lineno, method = backtrace_line.split(':')
47
+ path = path.sub(Gem.paths.home, '')
48
+ # remove 'in ' from method name
49
+ method = method.match(/in `([^']+) (:?in .*)'/)
50
+ method = method[1] if method
51
+ tag.div(class: 'backtrace-line') do
52
+ tag.span(path, class: '_path') +
53
+ ' in ' +
54
+ tag.span(method, class: '_method') +
55
+ ' at line ' +
56
+ tag.span(lineno, class: '_lineno')
57
+
58
+ end
59
+ end
60
+
61
+ def preview_mail_path(message_id)
62
+ "/rails/mailers/delivered_mail/preview?message_id=#{message_id}"
63
+ end
64
+
65
+ # {"used_memory"=>"46573752",
66
+ # "used_memory_human"=>"44.42M",
67
+ # "used_memory_rss"=>"70778880",
68
+ # "used_memory_rss_human"=>"67.50M",
69
+ # "used_memory_peak"=>"49028032",
70
+ # "used_memory_peak_human"=>"46.76M",
71
+ # "used_memory_peak_perc"=>"94.99%",
72
+ # "used_memory_overhead"=>"2195208",
73
+ # "used_memory_startup"=>"1118008",
74
+ # "used_memory_dataset"=>"44378544",
75
+ # "used_memory_dataset_perc"=>"97.63%",
76
+ # "allocator_allocated"=>"46540912",
77
+ # "allocator_active"=>"70693888",
78
+ # "allocator_resident"=>"70693888",
79
+ # "total_system_memory"=>"8225423360",
80
+ # "total_system_memory_human"=>"7.66G",
81
+ # "used_memory_lua"=>"84992",
82
+ # "used_memory_lua_human"=>"83.00K",
83
+ # "used_memory_scripts"=>"4768",
84
+ # "used_memory_scripts_human"=>"4.66K",
85
+ # "number_of_cached_scripts"=>"2",
86
+ # "maxmemory"=>"0",
87
+ # "maxmemory_human"=>"0B",
88
+ # "maxmemory_policy"=>"noeviction",
89
+ # "allocator_frag_ratio"=>"1.52",
90
+ # "allocator_frag_bytes"=>"24152976",
91
+ # "allocator_rss_ratio"=>"1.00",
92
+ # "allocator_rss_bytes"=>"0",
93
+ # "rss_overhead_ratio"=>"1.00",
94
+ # "rss_overhead_bytes"=>"84992",
95
+ # "mem_fragmentation_ratio"=>"1.52",
96
+ # "mem_fragmentation_bytes"=>"24237968",
97
+ # "mem_not_counted_for_evict"=>"0",
98
+ # "mem_replication_backlog"=>"0",
99
+ # "mem_clients_slaves"=>"0",
100
+ # "mem_clients_normal"=>"51176",
101
+ # "mem_aof_buffer"=>"0",
102
+ # "mem_allocator"=>"libc",
103
+ # "active_defrag_running"=>"0",
104
+ # "lazyfree_pending_objects"=>"0",
105
+ # "lazyfreed_objects"=>"0"}
106
+ def redis_mem_info
107
+ @info ||= Rails.configuration.rails_observatory.redis.call('info', 'memory').split("\r\n").slice(1..).map { _1.split(":") }.to_h
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,4 @@
1
+ module RailsObservatory
2
+ class ApplicationJob < ActiveJob::Base
3
+ end
4
+ end
@@ -0,0 +1,6 @@
1
+ module RailsObservatory
2
+ class ApplicationMailer < ActionMailer::Base
3
+ default from: "from@example.com"
4
+ layout "mailer"
5
+ end
6
+ end
@@ -0,0 +1,93 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <title>Rails Observatory</title>
5
+ <%= csrf_meta_tags %>
6
+ <%= csp_meta_tag %>
7
+ <link rel="preload" href="<%= "#{root_path}assets/stylesheets/application.css" %>" as="style">
8
+ <link rel="stylesheet" href="<%= "#{root_path}assets/stylesheets/application.css" %>">
9
+ <script type="text/javascript" src="<%= "#{root_path}assets/js/application.js" %>"></script>
10
+
11
+ <style>
12
+ <%= Rouge::Theme.find('github.dark').render %>
13
+ </style>
14
+ </head>
15
+ <body class="layout-app">
16
+ <nav class="side-nav layout-app-side-nav">
17
+ <img height="36" src="<%= "#{root_path}assets/logo_with_text.svg" %>" alt="RailsObservatory">
18
+ <ul>
19
+ <li><%= link_to root_path, class: "#{'_active' if request.path == root_path || request.path =~ /requests/}" do %>
20
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" width="24">
21
+ <path stroke-linecap="round" stroke-linejoin="round" d="M3 7.5L7.5 3m0 0L12 7.5M7.5 3v13.5m13.5 0L16.5 21m0 0L12 16.5m4.5 4.5V7.5" />
22
+ </svg>
23
+ Requests
24
+ <% end %></li>
25
+ <li><%= link_to jobs_path, class: "#{'_active' if request.path =~ /jobs/}" do %>
26
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" width="24">
27
+ <path stroke-linecap="round" stroke-linejoin="round" d="M20.25 14.15v4.25c0 1.094-.787 2.036-1.872 2.18-2.087.277-4.216.42-6.378.42s-4.291-.143-6.378-.42c-1.085-.144-1.872-1.086-1.872-2.18v-4.25m16.5 0a2.18 2.18 0 00.75-1.661V8.706c0-1.081-.768-2.015-1.837-2.175a48.114 48.114 0 00-3.413-.387m4.5 8.006c-.194.165-.42.295-.673.38A23.978 23.978 0 0112 15.75c-2.648 0-5.195-.429-7.577-1.22a2.016 2.016 0 01-.673-.38m0 0A2.18 2.18 0 013 12.489V8.706c0-1.081.768-2.015 1.837-2.175a48.111 48.111 0 013.413-.387m7.5 0V5.25A2.25 2.25 0 0013.5 3h-3a2.25 2.25 0 00-2.25 2.25v.894m7.5 0a48.667 48.667 0 00-7.5 0M12 12.75h.008v.008H12v-.008z" />
28
+ </svg>
29
+ Jobs
30
+ <% end %></li>
31
+ <li><%= link_to mailers_path, class: "#{'_active' if request.path =~ /mailers/}" do %>
32
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" width="24">
33
+ <path stroke-linecap="round" stroke-linejoin="round" d="M21.75 6.75v10.5a2.25 2.25 0 01-2.25 2.25h-15a2.25 2.25 0 01-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25m19.5 0v.243a2.25 2.25 0 01-1.07 1.916l-7.5 4.615a2.25 2.25 0 01-2.36 0L3.32 8.91a2.25 2.25 0 01-1.07-1.916V6.75" />
34
+ </svg>
35
+
36
+ Mailers
37
+ <% end %></li>
38
+ <li><%= link_to errors_path, class: "#{'_active' if request.path =~ /errors/}" do %>
39
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" width="24">
40
+ <path stroke-linecap="round" stroke-linejoin="round" d="M12 12.75c1.148 0 2.278.08 3.383.237 1.037.146 1.866.966 1.866 2.013 0 3.728-2.35 6.75-5.25 6.75S6.75 18.728 6.75 15c0-1.046.83-1.867 1.866-2.013A24.204 24.204 0 0112 12.75zm0 0c2.883 0 5.647.508 8.207 1.44a23.91 23.91 0 01-1.152 6.06M12 12.75c-2.883 0-5.647.508-8.208 1.44.125 2.104.52 4.136 1.153 6.06M12 12.75a2.25 2.25 0 002.248-2.354M12 12.75a2.25 2.25 0 01-2.248-2.354M12 8.25c.995 0 1.971-.08 2.922-.236.403-.066.74-.358.795-.762a3.778 3.778 0 00-.399-2.25M12 8.25c-.995 0-1.97-.08-2.922-.236-.402-.066-.74-.358-.795-.762a3.734 3.734 0 01.4-2.253M12 8.25a2.25 2.25 0 00-2.248 2.146M12 8.25a2.25 2.25 0 012.248 2.146M8.683 5a6.032 6.032 0 01-1.155-1.002c.07-.63.27-1.222.574-1.747m.581 2.749A3.75 3.75 0 0115.318 5m0 0c.427-.283.815-.62 1.155-.999a4.471 4.471 0 00-.575-1.752M4.921 6a24.048 24.048 0 00-.392 3.314c1.668.546 3.416.914 5.223 1.082M19.08 6c.205 1.08.337 2.187.392 3.314a23.882 23.882 0 01-5.223 1.082" />
41
+ </svg>
42
+ Errors
43
+ <% end %></li>
44
+ </ul>
45
+ <div class="_redis-stats">
46
+ <strong>Redis</strong> <%= redis_mem_info['used_memory_human'] %> /
47
+ <%= redis_mem_info['used_memory_rss_human'] %>
48
+ </div>
49
+ </nav>
50
+ <nav class="top-nav">
51
+ <span>
52
+ <h1>
53
+ <span class="_subtitle">
54
+ <% if content_for? :subtitle %><%= yield :subtitle %>
55
+ <% end %>
56
+ </span>
57
+ <%= yield :title %>
58
+ </h1>
59
+ </span>
60
+ <%= yield :top_nav %>
61
+ <% unless content_for?(:hide_duration) %>
62
+ <div class="dropdown">
63
+ <button class="secondary">
64
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" height="20">
65
+ <path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z" />
66
+ </svg>
67
+ <span>Past <%= duration.inspect %></span>
68
+ <svg class="h-5 w-5 text-gray-400" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" height="20">
69
+ <path fill-rule="evenodd" d="M10 3a.75.75 0 01.55.24l3.25 3.5a.75.75 0 11-1.1 1.02L10 4.852 7.3 7.76a.75.75 0 01-1.1-1.02l3.25-3.5A.75.75 0 0110 3zm-3.76 9.2a.75.75 0 011.06.04l2.7 2.908 2.7-2.908a.75.75 0 111.1 1.02l-3.25 3.5a.75.75 0 01-1.1 0l-3.25-3.5a.75.75 0 01.04-1.06z" clip-rule="evenodd" />
70
+ </svg>
71
+ </button>
72
+
73
+ <ul>
74
+ <% [5.minutes, 10.minutes, 15.minutes, 30.minutes, 1.hour, 4.hours, 1.day, 2.days, 7.days, 1.month].each do |time_frame| %>
75
+ <li>
76
+ <a href="?duration=<%= time_frame %>" class="<%= 'active' if time_frame == duration %>">
77
+ <span>Past <%= time_frame.inspect %></span>
78
+ <svg height="18" viewBox="0 0 20 20" fill="currentColor">
79
+ <path fill-rule="evenodd" d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z" clip-rule="evenodd" />
80
+ </svg>
81
+ </a>
82
+ </li>
83
+ <% end %>
84
+
85
+ </ul>
86
+ </div>
87
+ <% end %>
88
+ </nav>
89
+ <main class="<%= yield :main_css_class %>">
90
+ <%= yield %>
91
+ </main>
92
+ </body>
93
+ </html>
@@ -0,0 +1 @@
1
+ Greetings!
@@ -0,0 +1 @@
1
+ Posts
@@ -0,0 +1,23 @@
1
+ <% series = local_assigns[:series] %>
2
+
3
+ <div id="<%= rand(100) %>"
4
+ class="chart"
5
+ data-controller="chart"
6
+ data-chart-name="<%= local_assigns[:name] %>"
7
+ data-chart-type-value="<%= local_assigns[:type] %>"
8
+ <% unless local_assigns[:autobound] %>
9
+ data-chart-start-x-value="<%= series.first&.start_time_ms %>"
10
+ data-chart-end-x-value="<%= series.first&.end_time_ms %>"
11
+ <% end %>
12
+ >
13
+ <% if local_assigns[:series] %>
14
+ <div data-chart-target="chart"></div>
15
+ <% local_assigns[:series].each do |series| %>
16
+ <% name = series.respond_to?(:name) ? series.name : series[:name] %>
17
+ <% data = series.respond_to?(:filled_data) ? series.filled_data : series[:data] %>
18
+ <script type="application/json" data-chart-target="data" data-series-name="<%= name.split('/').last %>">
19
+ <%== data.to_json %>
20
+ </script>
21
+ <% end %>
22
+ <% end %>
23
+ </div>
@@ -0,0 +1,24 @@
1
+ <div class="query-table --scrollable" style="--column-count: <%= fields.size %>">
2
+ <header>
3
+ <% fields.each do |f| %>
4
+ <div class="query-table-column"><%= f.to_s.humanize(capitalize: true, keep_id_suffix: true) %></div>
5
+ <% end %>
6
+ </header>
7
+ <div class="query-table-body --scrollable">
8
+ <% events.each do |e| %>
9
+ <div class="query-table-row">
10
+ <% fields.each do |f| %>
11
+ <% if (formatter = local_assigns[:formatters]&.fetch(f, nil)) %>
12
+ <% if formatter.arity == 2 %>
13
+ <div class="query-table-column"><%= formatter.call(e.respond_to?(f) ? e.send(f) : 'unknownattribute', e) %></div>
14
+ <% else %>
15
+ <div class="query-table-column"><%= formatter.call(e.respond_to?(f) ? e.send(f) : 'unknownattribute') %></div>
16
+ <% end %>
17
+ <% else %>
18
+ <div class="query-table-column"><%= format_event_value(e.respond_to?(f) ? e.send(f) : 'unknownattribute') %></div>
19
+ <% end %>
20
+ <% end %>
21
+ </div>
22
+ <% end %>
23
+ </div>
24
+ </div>
@@ -0,0 +1,17 @@
1
+ <% series = Array.wrap(local_assigns[:series]) %>
2
+ <div id="<%= rand(100) %>"
3
+ class="_chart"
4
+ data-controller="sparkline"
5
+ data-sparkline-name="<%= local_assigns[:name] %>"
6
+ data-sparkline-type-value="<%= local_assigns[:type] %>"
7
+ data-sparkline-start-x-value="<%= series.first.start_time.to_i.in_milliseconds %>"
8
+ data-sparkline-end-x-value="<%= series.first.end_time.to_i.in_milliseconds %>">
9
+ <% if series.any? %>
10
+ <div data-sparkline-target="chart"></div>
11
+ <% series.each do |series| %>
12
+ <script type="application/json" data-sparkline-target="data" data-series-name="<%= series.name.split('/').last %>">
13
+ <%== series.filled_data.map { |(ts, v)| v.to_i == 0 ? [ts, 0] : [ts,v.to_i]}.to_json %>
14
+ </script>
15
+ <% end %>
16
+ <% end %>
17
+ </div>
@@ -0,0 +1,122 @@
1
+ <%= render 'chart', type: 'icicle', series: @events.to_series, autobound: true %>
2
+
3
+ <div class="tabs layout-events-breakdown-details">
4
+ <nav class="tabs-nav --sticky">
5
+ <label class="tabs-button">
6
+ Event Details
7
+ <input type="radio" name="tabs" checked>
8
+ </label>
9
+ <label class="tabs-button">
10
+ Logs
11
+ <input type="radio" name="tabs">
12
+ </label>
13
+ <label class="tabs-button">
14
+ Mail
15
+ <% if model.mail_events.size > 0 %>
16
+ <span style="color: var(--black-secondary)"><%= model.mail_events.size %></span>
17
+ <% end %>
18
+ <input type="radio" name="tabs">
19
+ </label>
20
+ <label class="tabs-button">
21
+ Jobs
22
+ <input type="radio" name="tabs">
23
+ </label>
24
+ <label class="tabs-button">
25
+ Errors
26
+ <input type="radio" name="tabs">
27
+ </label>
28
+ </nav>
29
+ <div class="tab-content event-details" data-controller="event-details" data-action="chart:selected@window->event-details#show">
30
+ <% @events.each_with_index do |e, index| %>
31
+ <div class="event-detail" id="<%= e['start_at'] %>" <%= 'hidden' unless index.zero? %> style="padding-inline: 2rem;">
32
+ <div>
33
+ <h3><%= e['name'] %> (<%= e['self_time'].to_f.round(2) %>ms)</h3>
34
+ <span style="color: var(--black-secondary)">Event spanned from <%= e['relative_start_at'].round(2) %>ms
35
+ to <%= e['relative_end_at'].round(2) %>ms
36
+ with a total duration of <%= e['duration'].round(2) %>ms
37
+ </span>
38
+ </div>
39
+ <% if e['middleware_stack'].present? %>
40
+ <div class="table-bar-chart">
41
+ <div class="table-bar-header">Middleware Stack</div>
42
+ <% total_time = e['self_time'] %>
43
+
44
+ <% e['middleware_stack'].each do |middleware_event| %>
45
+ <div class="table-bar-row">
46
+ <% percent = middleware_event['self_time'] / total_time * 100 %>
47
+ <div class="bar" style="width: <%= percent.round(1) %>%"></div>
48
+ <%= middleware_event['payload']['middleware'] %>
49
+ <div> <%= middleware_event['self_time'].to_f.round(2) %>ms</div>
50
+ <div style="color: var(--black-secondary);white-space: nowrap">(<%= percent.round(1) %>% of total)</div>
51
+ </div>
52
+ <% end %>
53
+ </div>
54
+ <% else %>
55
+ <dl style="">
56
+ <dt>Payload</dt>
57
+ <% if e['payload'].empty? %>
58
+ <dd>Nothing in payload</dd>
59
+ <% else %>
60
+ <dd class="highlight">
61
+ <pre class="--scrollable"><%== Rouge.highlight(e['payload'].to_yaml, 'yaml', Rouge::Formatters::HTMLLinewise.new(Rouge::Formatters::HTML.new, class: 'line')) %></pre>
62
+ <!-- <pre style="width:100%;overflow: auto;"><%#== Rouge.highlight(JSON.pretty_generate(e['payload']) , 'json', 'html') %></pre>-->
63
+ </dd>
64
+ <% end %>
65
+
66
+ </dl>
67
+ <% end %>
68
+
69
+ </div>
70
+ <% end %>
71
+ </div>
72
+ <div class="tab-content logs">
73
+ <log-lines style="display:grid;grid-template-columns: min-content min-content 1fr; gap:1rem;padding-block: 1rem;">
74
+ <% model.logs.each do |log| %>
75
+ <log-line style="display: grid; padding-inline: 2rem; grid-template-columns: subgrid;grid-column: 1/-1;">
76
+ <time style="white-space: nowrap"><%= Time.at(log['time']) %></time>
77
+ <log-level><%= log['severity'] %></log-level>
78
+ <log-message><%= log['message'] %></log-message>
79
+ </log-line>
80
+ <% end %>
81
+ </log-lines>
82
+ </div>
83
+ <div class="tab-content mail">
84
+ <simple-list class="simple-list">
85
+ <% if model.mail_events.empty? %>
86
+ <li>No mail enqueued or delivered</li>
87
+ <% end %>
88
+ <% model.mail_events.each do |e| %>
89
+ <li>
90
+ Mail Delivery <%= e.dig('payload', 'mailer') %>
91
+ <div>To: <%= e.dig('payload', 'to') %></div>
92
+ <div>From: <%= e.dig('payload', 'from') %></div>
93
+ <div>Subject: <%= e.dig('payload', 'subject') %></div>
94
+ <a href="<%= preview_mail_path(e.dig('payload', 'message_id')) %>" target="_blank">View Email</a>
95
+ </li>
96
+ <% end %>
97
+ </simple-list>
98
+ </div>
99
+ <div class="tab-content jobs">
100
+ <ul class="simple-list">
101
+ <% if model.events.only('enqueue.active_job').to_a.empty? %>
102
+ <li>No jobs were enqueued</li>
103
+ <% end %>
104
+ <% model.events.only('enqueue.active_job').each do |event| %>
105
+ <li>
106
+ <div class="simple-list-title"><%= event.dig('payload', 'job', 'class') %></div>
107
+ <div>
108
+ Enqueued job (
109
+ <span><%= link_to event.dig('payload', 'job', 'job_id').slice(0...8), job_path(event.dig('payload', 'job', 'job_id')) %></span>
110
+ ) to
111
+ <span><%= event.dig('payload', 'job', 'queue_name') %></span>
112
+ queue at
113
+ <span><%= event['start_at'] %></span>
114
+ </div>
115
+ </li>
116
+ <% end %>
117
+ </ul>
118
+ </div>
119
+ <div class="tab-content errors">
120
+ errors
121
+ </div>
122
+ </div>
@@ -0,0 +1,87 @@
1
+ <% content_for(:title, 'Errors') %>
2
+ <% content_for(:hide_duration, 'true') %>
3
+
4
+
5
+ <style>
6
+ @scope {
7
+ :scope {
8
+ display: flex;
9
+ flex-direction: column;
10
+ gap: 0rem;
11
+ padding: 0;
12
+ }
13
+ }
14
+
15
+ .error {
16
+ padding: 1.5rem 2rem;
17
+ border-bottom: 1px solid var(--divider);
18
+ display: grid;
19
+ grid-template-areas:
20
+ 'name-and-location graph count'
21
+ 'message graph count'
22
+ 'time graph count';
23
+ grid-template-columns: minmax(14rem, 1fr) 200px minmax(min-content, 4rem);
24
+ column-gap: 1rem;
25
+ row-gap: .3rem;
26
+
27
+ ._name-and-location {
28
+ display: flex;
29
+ gap: 1rem;
30
+ align-items: baseline;
31
+ overflow: hidden;
32
+
33
+
34
+ & > :first-child {
35
+ font-size: 1.25rem;
36
+ font-weight: 400;
37
+ }
38
+
39
+ & > :last-child {
40
+ color: var(--black-secondary);
41
+ text-overflow: ellipsis;
42
+ white-space: nowrap;
43
+ overflow: hidden;
44
+ }
45
+ }
46
+
47
+ ._message {
48
+ grid-area: message;
49
+ text-overflow: ellipsis;
50
+ white-space: nowrap;
51
+ overflow: hidden;
52
+ }
53
+
54
+ ._count {
55
+ grid-area: count;
56
+ text-align: center;
57
+ font-size: 1.5rem;
58
+ font-weight: 400;
59
+ align-self: center;
60
+ }
61
+
62
+ ._graph {
63
+ grid-area: graph;
64
+ text-align: right;
65
+ align-items: center;
66
+ }
67
+
68
+ }
69
+ </style>
70
+
71
+ <% @errors.each do |e| %>
72
+ <div class="error">
73
+ <div class="_name-and-location">
74
+ <a href="<%= error_path(e.fingerprint) %>"><%= e.class_name %></a>
75
+ <span><%= e.location.classify %></span>
76
+ </div>
77
+ <div class="_message"><%= e.message %></div>
78
+ <div class="_time"><%= time_ago_in_words(Time.at(e.time)) %> ago</div>
79
+
80
+ <div class="_graph">
81
+ <% if @series_by_fingerprint[e.fingerprint] %>
82
+ <%= render "sparkline", type: "bar", series: @series_by_fingerprint[e.fingerprint] %>
83
+ <% end %>
84
+ </div>
85
+ <div class="_count"><%= @count_by_fingerprint[e.fingerprint] %><%# e.count %></div>
86
+ </div>
87
+ <% end %>