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.
- checksums.yaml +4 -4
- data/README.md +43 -31
- data/app/assets/javascripts/simple_apm/application.js +94 -0
- data/app/assets/stylesheets/simple_apm/application.css +130 -6
- data/app/controllers/simple_apm/apm_controller.rb +33 -17
- data/app/controllers/simple_apm/application_controller.rb +15 -1
- data/app/helpers/simple_apm/application_helper.rb +24 -0
- data/app/models/simple_apm/action.rb +5 -5
- data/app/models/simple_apm/diagnostic_report.rb +203 -0
- data/app/models/simple_apm/sql.rb +1 -1
- data/app/views/layouts/simple_apm/application.html.erb +65 -61
- data/app/views/simple_apm/apm/_diagnostic_export.html.erb +17 -0
- data/app/views/simple_apm/apm/_timings.html.erb +18 -0
- data/app/views/simple_apm/apm/action_info.html.erb +50 -29
- data/app/views/simple_apm/apm/actions.html.erb +44 -25
- data/app/views/simple_apm/apm/dashboard.html.erb +54 -35
- data/app/views/simple_apm/apm/data.html.erb +69 -25
- data/app/views/simple_apm/apm/index.html.erb +49 -21
- data/app/views/simple_apm/apm/missing.html.erb +8 -0
- data/app/views/simple_apm/apm/show.html.erb +69 -44
- data/config/locales/simple_apm.en.yml +200 -0
- data/config/locales/simple_apm.zh-CN.yml +200 -0
- data/config/routes.rb +2 -1
- data/lib/simple_apm/version.rb +1 -1
- data/lib/simple_apm/worker.rb +4 -1
- metadata +7 -1
|
@@ -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
|
|
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="
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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,
|
|
56
|
-
[index_path,
|
|
57
|
-
[actions_path,
|
|
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
|
|
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
|
-
|
|
69
|
-
<%=
|
|
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"
|
|
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"
|
|
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
|
-
|
|
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
|
-
<
|
|
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"
|
|
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"
|
|
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"
|
|
15
|
-
<div class="apm-stat-value"><%= sec_str @action.
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
36
|
-
<p class="apm-panel-subtitle"
|
|
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
|
|
49
|
+
<table id="action-samples" class="table">
|
|
50
|
+
<thead>
|
|
42
51
|
<tr>
|
|
43
|
-
<th
|
|
44
|
-
<th
|
|
45
|
-
<th
|
|
46
|
-
<th
|
|
47
|
-
<th
|
|
48
|
-
<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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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)
|
|
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>
|