mission_control-servers 0.3.3 → 0.4.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 (27) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/builds/mission_control_servers_application.css +1969 -1
  3. data/app/controllers/mission_control/servers/dashboards/request_charts_controller.rb +16 -0
  4. data/app/controllers/mission_control/servers/dashboards/request_totals_controller.rb +15 -0
  5. data/app/controllers/mission_control/servers/ingresses_controller.rb +6 -2
  6. data/app/controllers/mission_control/servers/service_settings_controller.rb +2 -1
  7. data/app/javascript/mission_control/servers/controllers/hostname_select_controller.js +19 -0
  8. data/app/javascript/mission_control/servers/controllers/status_chart_controller.js +97 -0
  9. data/app/models/mission_control/servers/project.rb +1 -0
  10. data/app/models/mission_control/servers/request.rb +29 -0
  11. data/app/models/mission_control/servers/service.rb +69 -65
  12. data/app/models/mission_control/servers/service_setting.rb +7 -0
  13. data/app/views/mission_control/servers/dashboards/request_charts/show.html.erb +15 -0
  14. data/app/views/mission_control/servers/dashboards/request_totals/show.html.erb +13 -0
  15. data/app/views/mission_control/servers/projects/_panels.html.erb +15 -3
  16. data/app/views/mission_control/servers/service_settings/edit.html.erb +33 -1
  17. data/config/routes.rb +2 -0
  18. data/db/migrate/20240213011036_create_mission_control_servers_requests.rb +15 -0
  19. data/db/migrate/20240214021836_add_hostname_to_service_settings.rb +5 -0
  20. data/db/migrate/20240214160849_add_missing_mission_control_server_indexes.rb +7 -0
  21. data/lib/mission_control/servers/engine.rb +9 -0
  22. data/lib/mission_control/servers/request_tally_middleware.rb +53 -0
  23. data/lib/mission_control/servers/routing_helpers.rb +2 -0
  24. data/lib/mission_control/servers/version.rb +1 -1
  25. data/lib/mission_control/servers.rb +1 -0
  26. data/lib/tasks/mission_control/servers_tasks.rake +1 -1
  27. metadata +18 -8
@@ -0,0 +1,16 @@
1
+ module MissionControl::Servers
2
+ class Dashboards::RequestChartsController < ApplicationController
3
+ def show
4
+ @project = MissionControl::Servers::Project.find_by(token: params[:project_id])
5
+ @hostname = params[:hostname]
6
+ recent_requests = @project.requests.recent_totals(@project, @hostname)
7
+ @summary_hash = {
8
+ sum_2xx: recent_requests.sum(:sum_2xx).to_i,
9
+ sum_3xx: recent_requests.sum(:sum_3xx).to_i,
10
+ sum_4xx: recent_requests.sum(:sum_4xx).to_i,
11
+ sum_5xx: recent_requests.sum(:sum_5xx).to_i,
12
+ unknown: recent_requests.sum(:unknown).to_i
13
+ }
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,15 @@
1
+ module MissionControl::Servers
2
+ class Dashboards::RequestTotalsController < ApplicationController
3
+ def show
4
+ @project = Project.find_by(token: params[:project_id])
5
+ @hostname = params[:hostname]
6
+ recent_requests = @project.requests.recent_totals(@project, @hostname)
7
+ @total_sum = recent_requests.sum(:sum_2xx).to_i +
8
+ recent_requests.sum(:sum_3xx).to_i +
9
+ recent_requests.sum(:sum_4xx).to_i +
10
+ recent_requests.sum(:sum_5xx).to_i +
11
+ recent_requests.sum(:unknown).to_i
12
+
13
+ end
14
+ end
15
+ end
@@ -5,8 +5,8 @@ module MissionControl::Servers
5
5
 
6
6
  def create
7
7
  ingress = @project.services.new(ingress_params)
8
-
9
- if ingress.save
8
+ request = @project.requests.new(tally_params)
9
+ if ingress.save && request.save
10
10
  head :ok
11
11
  else
12
12
  head :unprocessable_entity
@@ -22,5 +22,9 @@ module MissionControl::Servers
22
22
  def ingress_params
23
23
  params.require(:service).permit(:hostname, :cpu, :mem_used, :mem_free, :disk_free)
24
24
  end
25
+
26
+ def tally_params
27
+ params.require(:tally).permit(:hostname, :'sum_2xx', :'sum_3xx', :'sum_4xx', :'sum_5xx', :unknown)
28
+ end
25
29
  end
26
30
  end
@@ -5,6 +5,7 @@ module MissionControl::Servers
5
5
  @service_setting = @project.service_settings.find_or_initialize_by(hostname: params[:id])
6
6
  @service_setting.label = params[:id] if @service_setting.new_record?
7
7
  @service_setting.save # To limit having to have a create action
8
+ @requests = @project.requests.select(:hostname).distinct.pluck(:hostname)
8
9
  end
9
10
 
10
11
  def update
@@ -21,7 +22,7 @@ module MissionControl::Servers
21
22
  end
22
23
 
23
24
  def label
24
- params.require(:service_setting).permit(:label)
25
+ params.require(:service_setting).permit(:label, :hostname, :request_hostname)
25
26
  end
26
27
  end
27
28
  end
@@ -0,0 +1,19 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ static targets = ["input"]
5
+
6
+ connect() {
7
+ console.log("Hostname select controller connected");
8
+ }
9
+
10
+ copy(event) {
11
+ event.preventDefault();
12
+
13
+ const hostnameElement = event.target.closest('li').querySelector('[data-target="hostname"]');
14
+
15
+ if (hostnameElement) {
16
+ this.inputTarget.value = hostnameElement.textContent.trim();
17
+ }
18
+ }
19
+ }
@@ -0,0 +1,97 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ static values = {
5
+ data: String,
6
+ darkBorderColor: "rgb(255, 255, 255, 0.1)",
7
+ lightBorderColor: "rgb(0, 0, 0, 0.1)"
8
+ }
9
+
10
+ connect() {
11
+ this.observeHtmlClassChanges()
12
+ this.values = JSON.parse(this.dataValue)
13
+ this.displayChart()
14
+ }
15
+
16
+ observeHtmlClassChanges() {
17
+ const observer = new MutationObserver(mutations => {
18
+ mutations.forEach(mutation => {
19
+ if (mutation.type === "attributes" && mutation.attributeName === "class") {
20
+ this.updateChartColors()
21
+ }
22
+ })
23
+ })
24
+ observer.observe(document.documentElement, { attributes: true })
25
+ }
26
+
27
+ updateChartColors() {
28
+ const isDarkMode = document.documentElement.classList.contains('dark')
29
+ const borderColor = isDarkMode ? this.darkBorderColorValue : this.lightBorderColorValue
30
+ const backgroundColors = isDarkMode ? this.darkModeBackgroundColors() : this.lightModeBackgroundColors()
31
+
32
+ this.chart.data.datasets.forEach(dataset => {
33
+ dataset.borderColor = borderColor
34
+ dataset.backgroundColor = backgroundColors
35
+ })
36
+ this.chart.update()
37
+ }
38
+
39
+ darkModeBackgroundColors() {
40
+ return [
41
+ "rgb(45, 167, 167)",
42
+ "rgb(204, 175, 55)",
43
+ "rgb(204, 85, 0)",
44
+ "rgb(150, 75, 75)",
45
+ "rgb(85, 85, 255)"
46
+ ]
47
+ }
48
+
49
+ lightModeBackgroundColors() {
50
+ return [
51
+ "rgb(75, 192, 192)",
52
+ "rgb(255, 159, 64)",
53
+ "rgb(255, 99, 132)",
54
+ "rgb(153, 102, 255)",
55
+ "rgb(54, 162, 235)"
56
+ ]
57
+ }
58
+
59
+
60
+ displayChart() {
61
+ const isDarkMode = document.documentElement.classList.contains('dark')
62
+ const backgroundColors = isDarkMode ? this.darkModeBackgroundColors() : this.lightModeBackgroundColors()
63
+ const borderColor = isDarkMode ? this.darkBorderColorValue : this.lightBorderColorValue
64
+
65
+ this.chart = new Chart(this.element, {
66
+ type: 'doughnut',
67
+ data: {
68
+ labels: ['2xx', '3xx', '4xx', '5xx', 'Unknown'],
69
+ datasets: [{
70
+ data: [
71
+ this.values.sum_2xx,
72
+ this.values.sum_3xx,
73
+ this.values.sum_4xx,
74
+ this.values.sum_5xx,
75
+ this.values.unknown
76
+ ],
77
+ backgroundColor: backgroundColors,
78
+ borderColor: borderColor,
79
+ borderWidth: 1,
80
+ hoverOffset: 4
81
+ }]
82
+ },
83
+ options: {
84
+ responsive: true,
85
+ plugins: {
86
+ legend: {
87
+ position: 'top',
88
+ },
89
+ title: {
90
+ display: true,
91
+ text: 'HTTP Status Codes Distribution'
92
+ }
93
+ }
94
+ }
95
+ })
96
+ }
97
+ }
@@ -1,6 +1,7 @@
1
1
  module MissionControl::Servers
2
2
  class Project < ApplicationRecord
3
3
  has_many :services, dependent: :destroy
4
+ has_many :requests, dependent: :destroy
4
5
  has_many :service_settings, dependent: :destroy
5
6
  has_many :public_projects, dependent: :destroy
6
7
  has_secure_token :token
@@ -0,0 +1,29 @@
1
+ module MissionControl::Servers
2
+ class Request < ApplicationRecord
3
+ belongs_to :project
4
+ after_create :trim_old_requests
5
+
6
+ class << self
7
+ def recent_totals(project, hostname)
8
+ recent_requests(project, hostname)
9
+ end
10
+
11
+ def recent_requests(project, hostname)
12
+ where(
13
+ hostname: [service_setting(project, hostname), hostname].compact.uniq,
14
+ created_at: 1.minute.ago..
15
+ )
16
+ end
17
+
18
+ def service_setting(project, hostname)
19
+ project.service_settings.find_by(hostname: hostname)&.request_hostname
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def trim_old_requests
26
+ project.requests.where(hostname: hostname).where(created_at: ..1.hour.ago).delete_all
27
+ end
28
+ end
29
+ end
@@ -6,83 +6,87 @@ module MissionControl::Servers
6
6
 
7
7
  scope :ordered, -> { order(created_at: :desc) }
8
8
 
9
- def self.combo_history(services, start_time: 1.hour.ago, end_time: Time.now.utc)
10
- timestamps = (start_time.to_i..end_time.to_i).step(60).map { |t| Time.at(t).utc.change(sec: 0).to_i }
11
- grouped_services = services.group_by { |service| service.created_at.utc.change(sec: 0).to_i }
12
-
13
- cpu_usages = []
14
- memory_usages = []
15
- created_at_times = []
16
-
17
- timestamps.each do |timestamp|
18
- relevant_services = grouped_services[timestamp] || []
19
- max_cpu_service = relevant_services.max_by(&:cpu)
20
- max_memory_service = relevant_services.max_by do |service|
21
- return 0 if service.mem_used.to_f + service.mem_free.to_f == 0.0
22
- service.mem_used.to_f / (service.mem_used.to_f + service.mem_free.to_f)
9
+ class << self
10
+ def combo_history(services, start_time: 1.hour.ago, end_time: Time.now.utc)
11
+ timestamps = (start_time.to_i..end_time.to_i).step(60).map { |t| Time.at(t).utc.change(sec: 0).to_i }
12
+ grouped_services = services.group_by { |service| service.created_at.utc.change(sec: 0).to_i }
13
+
14
+ cpu_usages = []
15
+ memory_usages = []
16
+ created_at_times = []
17
+
18
+ timestamps.each do |timestamp|
19
+ relevant_services = grouped_services[timestamp] || []
20
+ max_cpu_service = relevant_services.max_by(&:cpu)
21
+ max_memory_service = relevant_services.max_by do |service|
22
+ return 0 if service.mem_used.to_f + service.mem_free.to_f == 0.0
23
+ service.mem_used.to_f / (service.mem_used.to_f + service.mem_free.to_f)
24
+ end
25
+ cpu_usages << (max_cpu_service&.cpu || 0).to_f
26
+ if max_memory_service
27
+ total_memory = max_memory_service.mem_used.to_f + max_memory_service.mem_free.to_f
28
+ percentage_memory_used = (max_memory_service.mem_used.to_f / total_memory) * 100
29
+ memory_usages << percentage_memory_used.to_i
30
+ else
31
+ memory_usages << 0.0
32
+ end
33
+ created_at_times << Time.at(timestamp).utc.strftime('%H:%M%p')
23
34
  end
24
- cpu_usages << (max_cpu_service&.cpu || 0).to_f
25
- if max_memory_service
26
- total_memory = max_memory_service.mem_used.to_f + max_memory_service.mem_free.to_f
27
- percentage_memory_used = (max_memory_service.mem_used.to_f / total_memory) * 100
28
- memory_usages << percentage_memory_used.to_i
29
- else
30
- memory_usages << 0.0
31
- end
32
- created_at_times << Time.at(timestamp).utc.strftime('%H:%M%p')
35
+
36
+ data = [
37
+ {
38
+ label: 'CPU Usage',
39
+ data: cpu_usages,
40
+ fill: true,
41
+ borderColor: "rgb(54, 162, 235)",
42
+ tension: 0.25
43
+ },
44
+ {
45
+ label: 'Memory Usage',
46
+ data: memory_usages,
47
+ fill: true,
48
+ borderColor: "rgb(235, 127, 54)",
49
+ tension: 0.25
50
+ }
51
+ ]
52
+ { created_at_times: created_at_times, data: data }
33
53
  end
34
54
 
35
- data = [
36
- {
37
- label: 'CPU Usage',
38
- data: cpu_usages,
39
- fill: true,
40
- borderColor: "rgb(54, 162, 235)",
41
- tension: 0.25
42
- },
43
- {
44
- label: 'Memory Usage',
45
- data: memory_usages,
46
- fill: true,
47
- borderColor: "rgb(235, 127, 54)",
48
- tension: 0.25
49
- }
50
- ]
51
- { created_at_times: created_at_times, data: data }
52
- end
53
55
 
54
- def self.cpu_usage_history(services, start_time: 1.hour.ago, end_time: Time.now.utc)
55
- timestamps = (start_time.to_i..end_time.to_i).step(60).map { |t| Time.at(t).utc.change(sec: 0).to_i }
56
- grouped_services = services.group_by { |service| service.created_at.utc.change(sec: 0).to_i }
56
+ def cpu_usage_history(services, start_time: 1.hour.ago, end_time: Time.now.utc)
57
+ timestamps = (start_time.to_i..end_time.to_i).step(60).map { |t| Time.at(t).utc.change(sec: 0).to_i }
58
+ grouped_services = services.group_by { |service| service.created_at.utc.change(sec: 0).to_i }
57
59
 
58
- cpu_usages = []
59
- created_at_times = []
60
+ cpu_usages = []
61
+ created_at_times = []
62
+
63
+ timestamps.each do |timestamp|
64
+ relevant_services = grouped_services[timestamp] || []
65
+ max_cpu_service = relevant_services.max_by(&:cpu)
66
+ cpu_usages << (max_cpu_service&.cpu || 0).to_f
67
+ created_at_times << Time.at(timestamp).utc.strftime('%H:%M%p')
68
+ end
60
69
 
61
- timestamps.each do |timestamp|
62
- relevant_services = grouped_services[timestamp] || []
63
- max_cpu_service = relevant_services.max_by(&:cpu)
64
- cpu_usages << (max_cpu_service&.cpu || 0).to_f
65
- created_at_times << Time.at(timestamp).utc.strftime('%H:%M%p')
70
+ { cpu_usages: cpu_usages, created_at_times: created_at_times }
66
71
  end
67
72
 
68
- { cpu_usages: cpu_usages, created_at_times: created_at_times }
69
- end
70
73
 
71
- def self.memory_usage_history(services, start_time: 1.hour.ago, end_time: Time.now.utc)
72
- timestamps = (start_time.to_i..end_time.to_i).step(60).map { |t| Time.at(t).utc.change(sec: 0).to_i }
73
- grouped_services = services.group_by { |service| service.created_at.utc.change(sec: 0).to_i }
74
+ def memory_usage_history(services, start_time: 1.hour.ago, end_time: Time.now.utc)
75
+ timestamps = (start_time.to_i..end_time.to_i).step(60).map { |t| Time.at(t).utc.change(sec: 0).to_i }
76
+ grouped_services = services.group_by { |service| service.created_at.utc.change(sec: 0).to_i }
74
77
 
75
- memory_usages = []
76
- created_at_times = []
78
+ memory_usages = []
79
+ created_at_times = []
77
80
 
78
- timestamps.each do |timestamp|
79
- relevant_services = grouped_services[timestamp] || []
80
- max_memory_service = relevant_services.max_by(&:mem_used)
81
- memory_usages << (max_memory_service&.mem_used || 0).to_f
82
- created_at_times << Time.at(timestamp).utc.strftime('%H:%M%p')
83
- end
81
+ timestamps.each do |timestamp|
82
+ relevant_services = grouped_services[timestamp] || []
83
+ max_memory_service = relevant_services.max_by(&:mem_used)
84
+ memory_usages << (max_memory_service&.mem_used || 0).to_f
85
+ created_at_times << Time.at(timestamp).utc.strftime('%H:%M%p')
86
+ end
84
87
 
85
- { memory_usages: memory_usages, created_at_times: created_at_times }
88
+ { memory_usages: memory_usages, created_at_times: created_at_times }
89
+ end
86
90
  end
87
91
 
88
92
  def mem_percent
@@ -1,5 +1,12 @@
1
1
  module MissionControl::Servers
2
2
  class ServiceSetting < ApplicationRecord
3
3
  belongs_to :project
4
+ before_create :set_request_hostname
5
+
6
+ private
7
+
8
+ def set_request_hostname
9
+ self.request_hostname ||= hostname
10
+ end
4
11
  end
5
12
  end
@@ -0,0 +1,15 @@
1
+ <%= turbo_frame_tag dom_id(@project, [@hostname, :request_chart].join('-')) do %>
2
+ <div class="progress-bar" style="height: 5px; width: 100%;">
3
+ <div class="progress" data-refresh-target="progressBar" style="height: 100%; width: 100%;"></div>
4
+ </div>
5
+ <div class="flex flex-col items-center justify-center bg-gray-400/5 p-8">
6
+ <dt class="pie-chart">
7
+ <canvas class="chart"
8
+ data-controller="status-chart"
9
+ data-status-chart-data-value="<%= @summary_hash.to_json %>"></canvas>
10
+ </dt>
11
+ <dd class="card-heading">
12
+ Response Codes
13
+ </dd>
14
+ </div>
15
+ <% end %>
@@ -0,0 +1,13 @@
1
+ <%= turbo_frame_tag dom_id(@project, [@hostname, :request_total].join('-')) do %>
2
+ <div class="progress-bar bg-gray-300" style="height: 5px; width: 100%;">
3
+ <div class="progress bg-blue-500" data-refresh-target="progressBar" style="height: 100%; width: 100%;"></div>
4
+ </div>
5
+ <div class="flex flex-col bg-gray-400/5 p-8">
6
+ <dd class="card-heading">
7
+ Total Requests
8
+ </dd>
9
+ <dt class="stat-chart">
10
+ <%= number_with_delimiter @total_sum %>
11
+ </dt>
12
+ </div>
13
+ <% end %>
@@ -1,7 +1,7 @@
1
1
  <%= tag.div id: [hostname, "tab-content"].join("-"), class: "py-4 sm:py-8 tab-content", "data-target": hostname, "data-tabs-target": "content" do %>
2
- <div class="mx-auto max-w-7xl px-6 lg:px-8">
3
- <div class="mx-auto max-w-2xl lg:max-w-none">
4
- <dl class="grid grid-cols-1 gap-0.5 overflow-hidden rounded-2xl text-center sm:grid-cols-2 lg:grid-cols-4">
2
+ <div class="mx-auto px-6 lg:px-8">
3
+ <div class="mx-auto lg:max-w-none">
4
+ <dl class="grid grid-cols-1 gap-0.5 overflow-hidden rounded-2xl text-center sm:grid-cols-2 lg:grid-cols-6">
5
5
  <%= turbo_frame_tag dom_id(project, [hostname, :cpu_usage].join('-')),
6
6
  src: project_dashboards_cpu_usage_path(project_id: project.token, hostname: hostname),
7
7
  "data-controller": "refresh",
@@ -25,6 +25,18 @@
25
25
  "data-controller": "refresh",
26
26
  "data-refresh-src-value": project_dashboards_last_seen_path(project_id: project.token, hostname: hostname),
27
27
  "data-refresh-interval-value": 60000 %>
28
+
29
+ <%= turbo_frame_tag dom_id(project, [hostname, :request_total].join('-')),
30
+ src: project_dashboards_request_total_path(project_id: project.token, hostname: hostname),
31
+ "data-controller": "refresh",
32
+ "data-refresh-src-value": project_dashboards_request_total_path(project_id: project.token, hostname: hostname),
33
+ "data-refresh-interval-value": 60000 %>
34
+
35
+ <%= turbo_frame_tag dom_id(project, [hostname, :request_chart].join('-')),
36
+ src: project_dashboards_request_chart_path(project_id: project.token, hostname: hostname),
37
+ "data-controller": "refresh",
38
+ "data-refresh-src-value": project_dashboards_request_chart_path(project_id: project.token, hostname: hostname),
39
+ "data-refresh-interval-value": 60000 %>
28
40
  </dl>
29
41
  <% if params[:combo] == "false" %>
30
42
  <dl class="mt-8 grid grid-cols-1 gap-0.5 overflow-hidden rounded-2xl text-center sm:grid-cols-1 lg:grid-cols-2">
@@ -7,13 +7,45 @@
7
7
  </div>
8
8
  </div>
9
9
 
10
- <div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 my-10">
10
+ <div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 my-10" data-controller="hostname-select">
11
11
  <h1 class="text-2xl text-center font-semibold text-gray-normal mb-8">Editing <%= @service_setting.hostname %></h1>
12
12
  <%= form_with(model: [@project, @service_setting], class: "max-w-lg mx-auto my-10") do |form| %>
13
13
  <%= form.hidden_field :hostname %>
14
14
  <div class="mb-4">
15
15
  <%= form.label :label, class: "block text-gray-700 text-sm font-bold mb-2" %>
16
16
  <%= form.text_field :label, class: "shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" %>
17
+ <p class="block text-gray-400 text-sm mt-1">
18
+ This is for display purposes on the dashboard. It's a good idea to name it something that makes sense to you.
19
+ </p>
20
+ </div>
21
+ <div class="mb-4">
22
+ <%= form.label :request_hostname, class: "block text-gray-700 text-sm font-bold mb-2" %>
23
+ <%= form.text_field :request_hostname, "data-hostname-select-target": "input",
24
+ class: "shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" %>
25
+ <p class="block text-gray-400 text-sm mt-1">
26
+ With the server requets and status codes coming in, they may report a different hostname if you're running
27
+ them in docker or a different environment. This is the hostname that will be used to match the server.
28
+ </p>
29
+ </div>
30
+ <div class="border-t border-gray-100 px-4 py-6 sm:col-span-2 sm:px-0">
31
+ <dt class="text-sm font-medium leading-6 text-gray-900">Recent Hosts</dt>
32
+ <p class="block text-gray-400 text-sm mt-1">
33
+ Here are some of the recent hostnames that have been requested. You can select one of these if you'd like.
34
+ </p>
35
+ <dd class="mt-2 text-sm text-gray-900">
36
+ <ul role="list" class="divide-y divide-gray-100 rounded-md border border-gray-200">
37
+ <% @requests.each do |hostname| %>
38
+ <li class="flex items-center justify-between py-4 pl-4 pr-5 text-sm leading-6">
39
+ <div class="flex w-0 flex-1 items-center">
40
+ <span class="truncate font-medium dark:text-indigo-200" data-target="hostname"><%= hostname %></span>
41
+ </div>
42
+ <div class="ml-4 flex-shrink-0">
43
+ <a href="#" data-action="hostname-select#copy" class="font-medium text-indigo-600 dark:text-indigo-200">Select</a>
44
+ </div>
45
+ </li>
46
+ <% end %>
47
+ </ul>
48
+ </dd>
17
49
  </div>
18
50
  <div class="flex items-center justify-between">
19
51
  <%= form.submit class: "btn btn-primary btn-block" %>
data/config/routes.rb CHANGED
@@ -13,6 +13,8 @@ MissionControl::Servers::Engine.routes.draw do
13
13
  resource :cpu_history, only: :show
14
14
  resource :memory_history, only: :show
15
15
  resource :combo_history, only: :show
16
+ resource :request_total, only: :show
17
+ resource :request_chart, only: :show
16
18
  end
17
19
  end
18
20
  root to: "projects#index"
@@ -0,0 +1,15 @@
1
+ class CreateMissionControlServersRequests < ActiveRecord::Migration[7.1]
2
+ def change
3
+ create_table :mission_control_servers_requests do |t|
4
+ t.belongs_to :project, null: false, foreign_key: { to_table: :mission_control_servers_projects }
5
+ t.string :hostname
6
+ t.integer :sum_2xx, default: 0
7
+ t.integer :sum_3xx, default: 0
8
+ t.integer :sum_4xx, default: 0
9
+ t.integer :sum_5xx, default: 0
10
+ t.integer :unknown, default: 0
11
+
12
+ t.timestamps
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,5 @@
1
+ class AddHostnameToServiceSettings < ActiveRecord::Migration[7.1]
2
+ def change
3
+ add_column :mission_control_servers_service_settings, :request_hostname, :string
4
+ end
5
+ end
@@ -0,0 +1,7 @@
1
+ class AddMissingMissionControlServerIndexes < ActiveRecord::Migration[7.1]
2
+ def change
3
+ remove_index :mission_control_servers_services, :hostname
4
+ add_index :mission_control_servers_services, [:hostname, :created_at]
5
+ add_index :mission_control_servers_requests, [:hostname, :created_at]
6
+ end
7
+ end
@@ -5,8 +5,17 @@ require "stimulus-rails"
5
5
  module MissionControl
6
6
  module Servers
7
7
  class Engine < ::Rails::Engine
8
+ if ENV['RAILS_ENV'] == 'test'
9
+ require 'simplecov'
10
+ SimpleCov.start 'rails'
11
+ end
12
+
8
13
  isolate_namespace MissionControl::Servers
9
14
 
15
+ initializer 'mission_control-servers.middleware.request_tally' do |app|
16
+ app.middleware.use RequestTallyMiddleware
17
+ end
18
+
10
19
  initializer "mission_control-servers.assets" do |app|
11
20
  app.config.assets.paths << root.join("app/javascript")
12
21
  app.config.assets.precompile += %w[ mission_control_servers_manifest ]
@@ -0,0 +1,53 @@
1
+ module MissionControl
2
+ module Servers
3
+ class RequestTallyMiddleware
4
+ @@tally = Hash.new(0)
5
+
6
+ def initialize(app)
7
+ @app = app
8
+ @hostname = if ENV["KAMAL_VERSION"].present?
9
+ Socket.gethostname.to_s.split("-").first
10
+ else
11
+ ENV.fetch('HOSTNAME', Socket.gethostname)
12
+ end
13
+ end
14
+
15
+ def call(env)
16
+ req = Rack::Request.new(env)
17
+ if ingress_request?(req)
18
+ req.update_param('tally', @@tally.clone.merge('hostname' => @hostname))
19
+ @@tally.clear
20
+ end
21
+
22
+ status, headers, response = @app.call(env)
23
+
24
+ if !ingress_request?(req)
25
+ category = categorize_status(status)
26
+ update_tally(category)
27
+ end
28
+
29
+ [status, headers, response]
30
+ end
31
+
32
+ private
33
+
34
+ def ingress_request?(req)
35
+ req.post? && req.path.match?(/^\/[^\/]+\/projects\/[^\/]+\/ingress$/)
36
+ end
37
+
38
+ def update_tally(category)
39
+ @@tally[category] += 1
40
+ end
41
+
42
+ def categorize_status(status)
43
+ case status
44
+ when 200..299 then 'sum_2xx'
45
+ when 300..399 then 'sum_3xx'
46
+ when 400..499 then 'sum_4xx'
47
+ when 500..599 then 'sum_5xx'
48
+ else 'unknown'
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -11,6 +11,8 @@ module MissionControl
11
11
  get "#{engine_mount_path}/projects/:project_id/dashboards/cpu_history", to: "mission_control/servers/dashboards/cpu_histories#show"
12
12
  get "#{engine_mount_path}/projects/:project_id/dashboards/memory_history", to: "mission_control/servers/dashboards/memory_histories#show"
13
13
  get "#{engine_mount_path}/projects/:project_id/dashboards/combo_history", to: "mission_control/servers/dashboards/combo_histories#show"
14
+ get "#{engine_mount_path}/projects/:project_id/dashboards/request_total", to: "mission_control/servers/dashboards/request_totals#show"
15
+ get "#{engine_mount_path}/projects/:project_id/dashboards/request_chart", to: "mission_control/servers/dashboards/request_charts#show"
14
16
  get "#{engine_mount_path}/projects/:project_id/public_projects/new", to: "mission_control/servers/public_projects#new", as: :new_project_public_project
15
17
  get "#{engine_mount_path}/projects/:project_id/public_projects/:id", to: "mission_control/servers/public_projects#show"
16
18
  get "#{engine_mount_path}/projects/:project_id/script", to: "mission_control/servers/scripts#show"
@@ -1,5 +1,5 @@
1
1
  module MissionControl
2
2
  module Servers
3
- VERSION = "0.3.3"
3
+ VERSION = "0.4.0"
4
4
  end
5
5
  end
@@ -2,6 +2,7 @@ require "mission_control/servers/version"
2
2
  require "mission_control/servers/engine"
3
3
  require "mission_control/servers/configuration"
4
4
  require "mission_control/servers/routing_helpers"
5
+ require "mission_control/servers/request_tally_middleware"
5
6
 
6
7
  require "zeitwerk"
7
8
 
@@ -5,5 +5,5 @@ task :tailwind_engine_watch do
5
5
  -i #{MissionControl::Servers::Engine.root.join("app/assets/stylesheets/mission_control/servers/application.tailwind.css")} \
6
6
  -o #{MissionControl::Servers::Engine.root.join("app/assets/builds/mission_control_servers_application.css")} \
7
7
  -c #{MissionControl::Servers::Engine.root.join("config/tailwind.config.js")} \
8
- --minify -w"
8
+ -w"
9
9
  end