mission_control-servers 0.3.2 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
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 +14 -12
  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