simple_apm 1.1.0 → 1.1.2

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.
@@ -0,0 +1,203 @@
1
+ require 'uri'
2
+ require 'time'
3
+ require 'active_support/parameter_filter'
4
+
5
+ module SimpleApm
6
+ class DiagnosticReport
7
+ FILTERED = '[FILTERED]'.freeze
8
+ SECRET_KEYS = /password|passwd|secret|token|authorization|cookie|session|api[_-]?key|credential|private[_-]?key/i
9
+
10
+ def self.for_request(request, date:)
11
+ new(date).with_query_date { |report| report.request_report(request) }
12
+ end
13
+
14
+ def self.for_action(action, date:)
15
+ new(date).with_query_date { |report| report.action_report(action) }
16
+ end
17
+
18
+ def initialize(date)
19
+ @date = date.to_s
20
+ @generated_at = Time.now.utc.iso8601
21
+ @sample_reports = {}
22
+ @filter = ActiveSupport::ParameterFilter.new(Rails.application.config.filter_parameters + [SECRET_KEYS])
23
+ end
24
+
25
+ def with_query_date
26
+ previous_date = Thread.current['apm_query_date']
27
+ SimpleApm::RedisKey.query_date = @date
28
+ yield self
29
+ ensure
30
+ Thread.current['apm_query_date'] = previous_date
31
+ end
32
+
33
+ def request_report(request)
34
+ sqls = request.sqls
35
+ http_requests = request.net_http_requests
36
+ envelope('request').merge(
37
+ request: {
38
+ request_id: request.request_id, action_name: request.action_name,
39
+ controller: request.controller, action: request.action, method: request.method,
40
+ format: request.format, url: redact_url(request.url), host: request.host,
41
+ remote_addr: request.remote_addr, started_at: request.started&.to_s,
42
+ status: request.status, exception: redact_exception(request.exception)
43
+ },
44
+ timings: {
45
+ total_seconds: number(request.during), database_seconds: number(request.db_runtime),
46
+ view_seconds: number(request.view_runtime), external_http_seconds: number(request.net_http_during)
47
+ },
48
+ counters: {
49
+ queries: integer(request.queries_count), cached_queries: integer(request.cached_queries_count),
50
+ stored_sql_statements: sqls.length, stored_http_calls: http_requests.length
51
+ },
52
+ memory: { completed_mb: number(request.completed_memory), change_mb: number(request.memory_during) },
53
+ sql: sqls.map { |sql| sql_report(sql) },
54
+ http_calls: http_requests.map { |call| http_report(call) },
55
+ limitations: [
56
+ 'SQL details contain all stored statements; statements at or below the configured SQL threshold are not stored.',
57
+ 'Status and SQL cache flags may be unavailable in older records. Request parameters/bodies and HTTP response status/bodies are not collected.',
58
+ 'Process memory is RSS, not allocations attributable solely to this request.'
59
+ ],
60
+ sql_storage_threshold_seconds: SimpleApm::Setting::SQL_CRITICAL_TIME.to_f
61
+ )
62
+ end
63
+
64
+ def action_report(action)
65
+ count = action.click_count.to_i
66
+ limit = [SimpleApm::Setting::ACTION_SLOW_REQUEST_LIMIT.to_i, 0].max
67
+ slow_samples = limit.zero? ? [] : SimpleApm::SlowRequest.list_by_action(action.name, limit)
68
+ totals = {
69
+ total_seconds: action.time.to_f, database_seconds: action.db_time.to_f,
70
+ external_http_seconds: action.http_time.to_f, queries: action.queries_count.to_i,
71
+ cached_queries: action.cached_queries_count.to_i
72
+ }
73
+ envelope('action').merge(
74
+ action_name: action.name,
75
+ metrics: {
76
+ requests: count, totals: totals,
77
+ averages: totals.transform_values { |value| count.positive? ? value.to_f / count : 0.0 },
78
+ fastest_seconds: number(action.fast_time), slowest_seconds: number(action.slow_time)
79
+ },
80
+ samples: {
81
+ fastest: sample(action.fast_id, action.fast_time),
82
+ slowest: sample(action.slow_id, action.slow_time),
83
+ retained_slow: slow_samples.map { |slow| sample(slow.request_id, slow.during) },
84
+ retained_slow_limit: limit
85
+ },
86
+ limitations: [
87
+ 'Aggregates cover recorded requests on the query date. Details are retained for fastest, slowest and configured slow samples only; these are not representative of all requests.',
88
+ 'Stored request details may be unavailable. A missing sample is explicitly marked available=false.',
89
+ 'Historical request records and SQL/HTTP lists may outlive their current slow-sample ranking.'
90
+ ]
91
+ )
92
+ end
93
+
94
+ private
95
+
96
+ def envelope(type)
97
+ {
98
+ schema_version: '1.0', type: type, application: SimpleApm::Setting::APP_NAME,
99
+ query_date: @date, generated_at: @generated_at,
100
+ units: { duration: 'seconds', memory: 'MB', memory_bytes_per_mb: 1_048_576 },
101
+ redaction: 'Best effort: Rails filter_parameters and common credential keys; SQL string literals are filtered. Review remaining values before sharing.'
102
+ }
103
+ end
104
+
105
+ def sample(id, duration)
106
+ request = SimpleApm::Request.find(id) if id.present?
107
+ {
108
+ request_id: id, duration_seconds: number(duration), available: !request.nil?,
109
+ report: request ? (@sample_reports[id] ||= request_report(request)) : nil
110
+ }
111
+ end
112
+
113
+ def sql_report(sql)
114
+ statement = redact_text(sql.sql).to_s
115
+ statement = statement.gsub(/\$(\w*)\$.*?\$\1\$/m, "'#{FILTERED}'")
116
+ statement = statement.gsub(/'(?:''|\\.|[^'\\])*'/m, "'#{FILTERED}'")
117
+ binds = redact_binds(sql.value)
118
+ {
119
+ name: sql.name, statement: statement,
120
+ full_statement: binds.present? ? "#{statement}\n\nParameters: #{binds.to_json}" : statement,
121
+ binds: binds, cached: sql.respond_to?(:cached) ? sql.cached : nil,
122
+ duration_seconds: number(sql.during), started_at: sql.started&.to_s,
123
+ location: { filename: sql.filename, line: sql.line, method: sql.method }
124
+ }
125
+ end
126
+
127
+ def http_report(call)
128
+ {
129
+ name: call.name, url: redact_url(call.url), host: call.host, path: redact_url(call.path),
130
+ duration_seconds: number(call.during), started_at: call.started&.to_s,
131
+ location: { filename: call.filename, line: call.line }
132
+ }
133
+ end
134
+
135
+ def number(value)
136
+ value.nil? ? nil : value.to_f
137
+ end
138
+
139
+ def integer(value)
140
+ value.nil? ? nil : value.to_i
141
+ end
142
+
143
+ def redact_binds(value)
144
+ if value.is_a?(Array)
145
+ value.map do |bind|
146
+ if bind.is_a?(Array) && bind.length == 2
147
+ [bind[0], redact_text(@filter.filter_param(bind[0].to_s, bind[1]))]
148
+ else
149
+ redact_binds(bind)
150
+ end
151
+ end
152
+ elsif value.is_a?(Hash)
153
+ @filter.filter(value.deep_dup).transform_values { |item| redact_text(item) }
154
+ else
155
+ redact_text(value)
156
+ end
157
+ end
158
+
159
+ def redact_exception(value)
160
+ exception = value.is_a?(String) ? JSON.parse(value) : value
161
+ redact_text(exception)
162
+ rescue JSON::ParserError
163
+ redact_text(value)
164
+ end
165
+
166
+ def redact_url(value)
167
+ return nil if value.nil?
168
+
169
+ url = URI.parse(value.to_s)
170
+ url.userinfo = 'FILTERED' if url.userinfo
171
+ if url.query
172
+ parameters = URI.decode_www_form(url.query).map do |key, item|
173
+ filter_key = key.gsub(/\[([^\]]*)\]/, '.\\1')
174
+ [key, @filter.filter_param(filter_key, item)]
175
+ end
176
+ url.query = URI.encode_www_form(parameters).gsub('%5BFILTERED%5D', FILTERED)
177
+ end
178
+ redact_text(url.to_s)
179
+ rescue URI::InvalidURIError, ArgumentError
180
+ redact_text(value.to_s.gsub(%r{([a-z][a-z0-9+.-]*://)[^/\s?#]+@}i, '\\1FILTERED@'))
181
+ end
182
+
183
+ def redact_text(value)
184
+ case value
185
+ when Hash
186
+ @filter.filter(value.deep_dup).transform_values { |item| redact_text(item) }
187
+ when Array
188
+ value.map { |item| redact_text(item) }
189
+ when String
190
+ text = value.gsub(%r{([a-z][a-z0-9+.-]*://)[^/\s?#]+@}i, '\\1FILTERED@')
191
+ text = text.gsub(/\b(Bearer|Basic)\s+[A-Za-z0-9+\/=._-]+/i, "\\1 #{FILTERED}")
192
+ text.gsub(/([\w.\[\]-]+)(["'`]?\s*[=:]\s*)('(?:''|[^'])*'|"(?:\\.|[^"])*"|[^\s&,;\)]+)/) do |match|
193
+ key, separator, original = Regexp.last_match(1), Regexp.last_match(2), Regexp.last_match(3)
194
+ filtered = @filter.filter_param(key, original)
195
+ mask = original.start_with?("'", '"') ? "#{original[0]}#{FILTERED}#{original[0]}" : FILTERED
196
+ filtered == FILTERED ? "#{key}#{separator}#{mask}" : match
197
+ end
198
+ else
199
+ value
200
+ end
201
+ end
202
+ end
203
+ end
@@ -1,7 +1,7 @@
1
1
  # 请求对应的SQL查询列表
2
2
  module SimpleApm
3
3
  class Sql
4
- attr_accessor :request_id, :sql, :line, :filename, :method, :name, :during, :started, :value
4
+ attr_accessor :request_id, :sql, :line, :filename, :method, :name, :during, :started, :value, :cached
5
5
  def initialize(h, request = nil)
6
6
  h.each do |k, v|
7
7
  send("#{k}=", v) rescue puts "attr #{k} not set!"
@@ -1,7 +1,7 @@
1
1
  <!DOCTYPE html>
2
- <html>
2
+ <html lang="<%= simple_apm_locale %>">
3
3
  <head>
4
- <title>Simple Apm</title>
4
+ <title><%= t("simple_apm.#{action_name}.title", default: 'Simple APM') %> · Simple APM</title>
5
5
  <meta charset="utf-8">
6
6
  <meta content="IE=Edge,chrome=1" http-equiv="X-UA-Compatible">
7
7
  <meta content="width=device-width, initial-scale=1.0" name="viewport">
@@ -12,36 +12,18 @@
12
12
  <script src="//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
13
13
  <script src="//cdnjs.cloudflare.com/ajax/libs/echarts/4.1.0/echarts.js"></script>
14
14
  <script src="//cdn.datatables.net/1.10.15/js/jquery.dataTables.min.js"></script>
15
- <script type="text/javascript">
16
- // JqueryDataTable自定义排序方法
17
- jQuery.extend(jQuery.fn.dataTableExt.oSort, {
18
- "sec-pre": function (a) {
19
- var x = String(a).replace(/<[\s\S]*?>/g, ""); //去除html标记
20
- x = x.replace(/&amp;nbsp;/ig, ""); //去除空格
21
- x = x.replace(/%/, ""); //去除百分号
22
- if (x.indexOf('ms') > 0) {
23
- x = x.replace(/ms/, "");
24
- return parseFloat(x) / 1000;
25
- } else if (x.indexOf('min') > 0) {
26
- x = x.replace(/min/, "");
27
- return parseFloat(x) * 60;
28
- } else {
29
- x = x.replace(/min/, "");
30
- return parseFloat(x)
31
- }
32
- },
33
- "sec-asc": function (a, b) { //正序排序引用方法
34
- return ((a < b) ? -1 : ((a > b) ? 1 : 0));
35
- },
36
- "sec-desc": function (a, b) { //倒序排序引用方法
37
- return ((a < b) ? 1 : ((a > b) ? -1 : 0));
38
- }
39
- });
40
- </script>
15
+ <script type="application/json" id="apm-ui-config"><%= json_escape({
16
+ table: table_language,
17
+ copied: t('simple_apm.diagnostics.copied'),
18
+ copy_failed: t('simple_apm.diagnostics.copy_failed'),
19
+ load_error: t('simple_apm.index.load_error')
20
+ }.to_json).html_safe %></script>
21
+ <%= javascript_include_tag "simple_apm/application" %>
41
22
 
42
23
  <%= csrf_meta_tags %>
43
24
  </head>
44
25
  <body class="simple-apm">
26
+ <a class="apm-skip-link" href="#main-content"><%= t('simple_apm.common.skip_content') %></a>
45
27
  <div id="page-container" class="apm-shell">
46
28
  <nav class="apm-topbar">
47
29
  <div class="apm-topbar-inner">
@@ -52,50 +34,90 @@
52
34
  <div class="apm-nav">
53
35
  <%
54
36
  [
55
- [dashboard_path, 'Dashboard'],
56
- [index_path, '慢事务列表'],
57
- [actions_path, 'ActionList'],
58
- [data_path, '数据管理']
37
+ [dashboard_path(apm_date: apm_date), t("simple_apm.layout.nav_dashboard"), %w[dashboard]],
38
+ [index_path(apm_date: apm_date), t("simple_apm.layout.nav_slow_requests"), %w[index show]],
39
+ [actions_path(apm_date: apm_date), t("simple_apm.layout.nav_actions"), %w[actions action_info]],
40
+ [data_path(apm_date: apm_date), t("simple_apm.layout.nav_data"), %w[data]]
59
41
  ].each do |m|
60
42
  %>
61
- <a class="navbar-brand <%= 'active' if request.url =~ /#{m[0]}/ %>"
62
- href="<%= m[0] %>" >
43
+ <a class="navbar-brand <%= 'active' if m[2].include?(action_name) %>"
44
+ href="<%= m[0] %>" <%= 'aria-current=page' if m[2].include?(action_name) %>>
63
45
  <%= m[1] %>
64
46
  </a>
65
47
  <% end %>
66
48
  </div>
49
+ <div class="apm-language-switch" aria-label="<%= t("simple_apm.layout.language") %>">
50
+ <% SimpleApm::ApplicationController::AVAILABLE_LOCALES.each do |locale| %>
51
+ <%= link_to t("simple_apm.locales.#{locale}"),
52
+ set_locale_path(locale: locale),
53
+ class: ("active" if simple_apm_locale == locale) %>
54
+ <% end %>
55
+ </div>
67
56
  <div class="select-apm-date apm-date-select">
68
- <span>统计日期</span>
69
- <%= select_tag 'apm_date', options_for_select(SimpleApm::Redis.in_apm_days, apm_date), class: 'form-control' %>
57
+ <% date_path = action_name == 'show' ? (@request ? action_info_path : index_path) : request.path %>
58
+ <%= form_tag date_path, method: :get, class: 'apm-date-form' do %>
59
+ <label for="apm_date"><%= t("simple_apm.layout.date") %></label>
60
+ <%= hidden_field_tag :action_name, @request.action_name if action_name == 'show' && @request %>
61
+ <%= hidden_field_tag :action_name, @action.name if action_name == 'action_info' && @action %>
62
+ <%= hidden_field_tag :count, @sample_limit if action_name == 'index' %>
63
+ <%= select_tag 'apm_date', options_for_select((SimpleApm::Redis.in_apm_days + [apm_date]).uniq.sort.reverse, apm_date), class: 'form-control' %>
64
+ <noscript><button type="submit" class="btn btn-primary"><%= t('simple_apm.common.apply') %></button></noscript>
65
+ <% end %>
70
66
  </div>
71
67
  </div>
72
68
  </nav>
73
- <main class="apm-content">
69
+ <main id="main-content" class="apm-content" tabindex="-1">
70
+ <% if flash[:notice].present? %>
71
+ <div class="apm-notice" role="status"><%= flash[:notice] %></div>
72
+ <% end %>
74
73
  <%= yield %>
75
74
  </main>
76
75
  </div>
77
76
  <footer class="apm-footer">
78
77
  <div class="apm-footer-status">
79
78
  <% if SimpleApm::Redis.running? %>
80
- <span class="apm-status apm-status-ok">数据收集中</span>
79
+ <span class="apm-status apm-status-ok"><%= t("simple_apm.layout.collected") %></span>
81
80
  <% else %>
82
- <span class="apm-status apm-status-danger">已暂停</span>
81
+ <span class="apm-status apm-status-danger"><%= t("simple_apm.common.paused") %></span>
83
82
  <% end %>
84
83
  </div>
85
84
  <div class="apm-footer-meta">
86
85
  <% SimpleApm::Redis.simple_info.each do |k, v| %>
87
- <span><label><%= k %>:</label><%= v %></span>
86
+ <%
87
+ redis_label_key =
88
+ case k
89
+ when '系统内存'
90
+ 'system_memory'
91
+ when '当前内存占用'
92
+ 'current_memory'
93
+ when '当前内存占用(rss)'
94
+ 'current_memory_rss'
95
+ when '占用内存峰值'
96
+ 'peak_memory'
97
+ when 'redis版本'
98
+ 'redis_version'
99
+ when /db[0-9]+/
100
+ 'db'
101
+ end
102
+ redis_label = redis_label_key ? t("simple_apm.layout.redis_info.#{redis_label_key}") : k
103
+ %>
104
+ <span><label><%= redis_label %>:</label><%= v %></span>
88
105
  <% end %>
89
106
  </div>
90
107
  </footer>
91
- <div id="sql-modal" class="modal" aria-hidden="true" role="dialog" style="display: none;" tabindex="-1">
108
+ <div id="sql-modal" class="modal" aria-hidden="true" aria-labelledby="sql-modal-title" role="dialog" style="display: none;" tabindex="-1">
92
109
  <div class="modal-dialog modal-lg modal-dialog-popout">
93
110
  <div class="apm-modal-panel">
94
111
  <div class="apm-modal-header">
95
- <div class="apm-modal-title">SQL</div>
112
+ <h2 id="sql-modal-title" class="apm-modal-title">SQL</h2>
113
+ <div class="apm-toolbar-actions">
114
+ <button type="button" class="btn btn-primary" data-copy-target="sql-modal-text"><%= t('simple_apm.common.copy') %></button>
115
+ <button type="button" class="btn apm-button-secondary" data-dismiss="modal"><%= t('simple_apm.common.close') %></button>
116
+ </div>
96
117
  </div>
97
118
  <div class="apm-modal-body">
98
- <pre class="modal-content" data-lang="SQL"></pre>
119
+ <pre id="sql-modal-text" class="modal-content" data-lang="SQL"></pre>
120
+ <p class="apm-copy-status" role="status" aria-live="polite"></p>
99
121
  </div>
100
122
  </div>
101
123
 
@@ -103,22 +125,4 @@
103
125
  </div>
104
126
 
105
127
  </body>
106
- <script>
107
- <% if flash[:notice].present? %>
108
- alert(<%= flash[:notice].to_json.html_safe %>)
109
- <% end %>
110
- var sql_modal_obj = $('#sql-modal')
111
- $('.select-apm-date select').on('change', function () {
112
- window.location.href = '<%= set_apm_date_path %>?apm_date=' + this.value
113
- })
114
- $('.sql').on('click', function () {
115
- sql_modal_obj.find('.modal-content').html($(this).html())
116
- sql_modal_obj.modal('show')
117
- })
118
- // 手写部分ujs
119
- $("a[data-confirm]").on('click', function () {
120
- return confirm($(this).data('confirm'))
121
- })
122
-
123
- </script>
124
128
  </html>
@@ -0,0 +1,17 @@
1
+ <section class="apm-panel apm-export">
2
+ <div class="apm-panel-header">
3
+ <div>
4
+ <h2 class="apm-panel-title"><%= t("simple_apm.diagnostics.title") %></h2>
5
+ <p class="apm-panel-subtitle"><%= t("simple_apm.diagnostics.hint") %></p>
6
+ </div>
7
+ <button type="button" class="btn btn-primary" data-copy-target="diagnostic-report"><%= t("simple_apm.diagnostics.copy") %></button>
8
+ </div>
9
+ <div class="apm-panel-body">
10
+ <details>
11
+ <summary><%= t("simple_apm.diagnostics.preview") %></summary>
12
+ <label class="sr-only" for="diagnostic-report"><%= t("simple_apm.diagnostics.title") %></label>
13
+ <textarea id="diagnostic-report" class="apm-report-text apm-mono" readonly rows="14" spellcheck="false"><%= JSON.pretty_generate(report) %></textarea>
14
+ </details>
15
+ <p class="apm-copy-status" role="status" aria-live="polite"></p>
16
+ </div>
17
+ </section>
@@ -0,0 +1,18 @@
1
+ <section class="apm-panel">
2
+ <div class="apm-panel-header">
3
+ <div>
4
+ <h2 class="apm-panel-title"><%= t('simple_apm.show.timing_title') %></h2>
5
+ <p class="apm-panel-subtitle"><%= t('simple_apm.show.timing_hint') %></p>
6
+ </div>
7
+ </div>
8
+ <div class="apm-panel-body">
9
+ <% parts.each do |label, duration| %>
10
+ <% percentage = duration_percent(duration, total) %>
11
+ <div class="apm-runtime-row">
12
+ <span><%= label %></span>
13
+ <meter min="0" max="100" value="<%= percentage %>" aria-label="<%= label %>"><%= percentage %>%</meter>
14
+ <span><%= sec_str duration %> · <%= percentage %>%</span>
15
+ </div>
16
+ <% end %>
17
+ </div>
18
+ </section>
@@ -1,73 +1,94 @@
1
+ <nav class="apm-breadcrumb" aria-label="<%= t('simple_apm.action_info.title') %>">
2
+ <%= link_to t('simple_apm.common.back_actions'), actions_path(apm_date: apm_date) %>
3
+ </nav>
1
4
  <div class="apm-page-header">
2
5
  <div>
3
- <h1 class="apm-page-title">Action 概况</h1>
4
- <p class="apm-page-subtitle"><%= @action.name %></p>
6
+ <h1 class="apm-page-title"><%= t("simple_apm.action_info.title") %></h1>
7
+ <p class="apm-page-subtitle apm-mono"><%= @action.name %> · <%= apm_date %></p>
5
8
  </div>
6
9
  </div>
7
10
 
8
11
  <div class="apm-metrics">
9
12
  <div class="apm-stat">
10
- <p class="apm-stat-label">当日点击次数</p>
11
- <div class="apm-stat-value"><%= @action.click_count %></div>
13
+ <p class="apm-stat-label"><%= t("simple_apm.action_info.daily_hits") %></p>
14
+ <div class="apm-stat-value"><%= number_with_delimiter @action.click_count.to_i %></div>
12
15
  </div>
13
16
  <div class="apm-stat">
14
- <p class="apm-stat-label">平均响应时间</p>
15
- <div class="apm-stat-value"><%= sec_str @action.time.to_f/@action.click_count.to_i %></div>
17
+ <p class="apm-stat-label"><%= t("simple_apm.action_info.avg_response_time") %></p>
18
+ <div class="apm-stat-value"><%= sec_str @action.avg_time %></div>
16
19
  </div>
17
20
  <div class="apm-stat">
18
- <p class="apm-stat-label">平均DB时间</p>
21
+ <p class="apm-stat-label"><%= t("simple_apm.action_info.avg_db_time") %></p>
19
22
  <div class="apm-stat-value"><%= sec_str @action.avg_db_time %></div>
23
+ <div class="apm-stat-meta"><%= t('simple_apm.action_info.avg_http_time') %>: <%= sec_str @action.avg_http_time %></div>
20
24
  </div>
21
25
  <div class="apm-stat">
22
- <p class="apm-stat-label">平均SQL次数</p>
26
+ <p class="apm-stat-label"><%= t("simple_apm.action_info.avg_sql_count") %></p>
23
27
  <div class="apm-stat-value"><%= @action.avg_queries_count.round(1) %></div>
28
+ <div class="apm-stat-meta"><%= t('simple_apm.action_info.cached_queries') %>: <%= @action.avg_cached_queries_count.round(1) %></div>
24
29
  </div>
25
30
  <div class="apm-stat">
26
- <p class="apm-stat-label">最慢响应时间</p>
27
- <div class="apm-stat-value"><%= link_to sec_str(@action.slow_time), show_path(id: @action.slow_id) %></div>
28
- <div class="apm-stat-meta">最快 <%= link_to sec_str(@action.fast_time), show_path(id: @action.fast_id) %></div>
31
+ <p class="apm-stat-label"><%= t("simple_apm.action_info.slowest_response_time") %></p>
32
+ <div class="apm-stat-value"><%= link_to sec_str(@action.slow_time), show_path(id: @action.slow_id, apm_date: apm_date) %></div>
33
+ <div class="apm-stat-meta"><%= t("simple_apm.action_info.fastest") %> <%= link_to sec_str(@action.fast_time), show_path(id: @action.fast_id, apm_date: apm_date) %></div>
29
34
  </div>
30
35
  </div>
31
36
 
37
+ <%= render 'diagnostic_export', report: SimpleApm::DiagnosticReport.for_action(@action, date: apm_date) %>
38
+ <%= render 'timings', total: @action.avg_time, parts: [[t('simple_apm.common.db'), @action.avg_db_time], [t('simple_apm.common.http'), @action.avg_http_time]] %>
39
+
32
40
  <section class="apm-panel">
33
41
  <div class="apm-panel-header">
34
42
  <div>
35
- <h2 class="apm-panel-title">慢请求样本</h2>
36
- <p class="apm-panel-subtitle">保留该 Action 下响应最慢的请求。</p>
43
+ <h2 class="apm-panel-title"><%= t("simple_apm.action_info.slow_samples_title") %></h2>
44
+ <p class="apm-panel-subtitle"><%= t("simple_apm.action_info.slow_samples_subtitle") %></p>
37
45
  </div>
38
46
  </div>
39
47
  <div class="apm-panel-body">
40
48
  <div class="apm-table-wrap">
41
- <table class="table table-bordered">
49
+ <table id="action-samples" class="table">
50
+ <thead>
42
51
  <tr>
43
- <th>响应时间</th>
44
- <th>访问时间</th>
45
- <th>内存波动</th>
46
- <th>请求id</th>
47
- <th>访问ip</th>
48
- <th>server</th>
52
+ <th><%= t("simple_apm.action_info.response_time") %></th>
53
+ <th><%= t('simple_apm.common.db') %></th>
54
+ <th><%= t('simple_apm.common.sql') %></th>
55
+ <th><%= t('simple_apm.common.view') %></th>
56
+ <th><%= t('simple_apm.common.http') %></th>
57
+ <th><%= t("simple_apm.action_info.access_time") %></th>
58
+ <th><%= t("simple_apm.action_info.memory_delta") %></th>
59
+ <th><%= t("simple_apm.action_info.request_id") %></th>
60
+ <th><%= t('simple_apm.common.status') %></th>
61
+ <th><%= t("simple_apm.action_info.ip") %></th>
62
+ <th><%= t("simple_apm.common.server") %></th>
49
63
  </tr>
64
+ </thead>
65
+ <tbody>
50
66
  <% @action.slow_requests.each do |slow_request| %>
51
67
  <%
52
68
  r = slow_request.request
53
69
  next if r.blank?
54
70
  %>
55
71
  <tr>
56
- <td>
57
- <%= sec_str r.during %>
58
- (DB: <%= sec_str r.db_runtime %> ,
59
- SQL: <%= r.queries_count.to_i %> ,
60
- View: <%= sec_str r.view_runtime %>
61
- <%= "HTTP: #{sec_str r.net_http_during}" if r.net_http_during.to_f > 0 %>)
62
- </td>
72
+ <td data-order="<%= r.during.to_f %>"><%= link_to sec_str(r.during), show_path(id: r.request_id, apm_date: apm_date) %></td>
73
+ <td data-order="<%= r.db_runtime.to_f %>"><%= sec_str r.db_runtime %></td>
74
+ <td><%= r.queries_count.to_i %></td>
75
+ <td data-order="<%= r.view_runtime.to_f %>"><%= sec_str r.view_runtime %></td>
76
+ <td data-order="<%= r.net_http_during.to_f %>"><%= sec_str r.net_http_during %></td>
63
77
  <td><%= time_label r.started %></td>
64
- <td><%= r.memory_during.to_f.round(1) %></td>
65
- <td><%= link_to r.request_id, show_path(id: r.request_id) %></td>
78
+ <td data-order="<%= r.memory_during.to_f %>"><%= r.memory_during.to_f.round(1) %> MB</td>
79
+ <td class="apm-mono"><%= link_to r.request_id, show_path(id: r.request_id, apm_date: apm_date) %></td>
80
+ <td><%= r.status.presence || t('simple_apm.common.unknown') %></td>
66
81
  <td><%= r.remote_addr %></td>
67
82
  <td><%= r.host %></td>
68
83
  </tr>
69
84
  <% end %>
85
+ </tbody>
70
86
  </table>
71
87
  </div>
72
88
  </div>
73
89
  </section>
90
+ <script>
91
+ $(function () {
92
+ $('#action-samples').DataTable(window.SimpleApmUI.tableOptions({order: [[0, 'desc']]}));
93
+ });
94
+ </script>