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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d98ff928f6478abea71e3375957adc7de6d5a3865019be549026b4d615b58fa0
|
|
4
|
+
data.tar.gz: ee6fbdf30feb8499da5ca323931272b6d270d41ccdf9cadc86213d7da5fd50ac
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7b51b09db54530aac2a6e75058c815b6f7f1f3e7abd09c28b7791723722b699ab5e2a378170b074dc06f25a7baba0ea01c61ca5149e6778343d55a49495be81b
|
|
7
|
+
data.tar.gz: 81fec7c8dbb538bf21928426aa21bfc110300f28e19fd18de1b6cc5784fa0d4237765d50015c02f6985cd7516af1cc71f993fc3433f52eb33ca133c3f83b4fe5
|
data/README.md
CHANGED
|
@@ -1,73 +1,85 @@
|
|
|
1
1
|
# SimpleApm (Rails Engine)
|
|
2
|
-
基于Redis的简单的Web请求性能监控/慢事务追踪工具
|
|
3
2
|
|
|
4
|
-
|
|
5
|
-
- 最慢的500个(默认500)请求
|
|
6
|
-
- 记录每个action最慢的20次请求
|
|
7
|
-
- 记录每个action的平均访问时间
|
|
8
|
-
- 记录慢请求的详情和对应SQL详情(多余的会删掉)
|
|
9
|
-
- 以10分钟为刻度记录平均/最慢访问时间、次数等性能指标,并生成图表
|
|
10
|
-
- 记录请求中外部http访问时间
|
|
3
|
+
A lightweight Redis-backed Rails engine for web request performance monitoring and slow request tracing.
|
|
11
4
|
|
|
5
|
+
SimpleApm records request performance by day:
|
|
12
6
|
|
|
13
|
-
|
|
7
|
+
- The slowest requests, 500 by default.
|
|
8
|
+
- The slowest 20 requests for each action.
|
|
9
|
+
- Average response time for each action.
|
|
10
|
+
- Slow request details and related SQL details, trimming excess records.
|
|
11
|
+
- Average and slowest response time, request count, and other metrics in 10-minute intervals, with charts.
|
|
12
|
+
- External HTTP request duration during each request.
|
|
14
13
|
|
|
15
|
-
|
|
14
|
+
The web UI supports English and Chinese. English is used by default, and users can switch languages from the top-right corner.
|
|
16
15
|
|
|
17
|
-
|
|
16
|
+
Action and request detail pages include **Copy for AI** with a JSON preview. Request reports include metadata, timings in seconds, memory in MB, retained SQL and external HTTP calls with code locations. Action reports add daily aggregates and the retained fastest, slowest and slow request samples, marking unavailable details explicitly. Common credential fields and SQL string literals are filtered; reports describe collection thresholds and sampling limits. Clipboard copying includes a fallback for HTTP deployments and manual selection when browser copying is unavailable.
|
|
18
17
|
|
|
19
|
-
|
|
18
|
+
Lists support search and precise duration sorting. Slow requests offer a sample limit and refresh controls, while date-aware links keep the selected day's context across pages. Data cleanup and collection controls use POST forms with confirmation for destructive operations.
|
|
20
19
|
|
|
21
|
-
|
|
20
|
+
## How It Works
|
|
22
21
|
|
|
22
|
+
SimpleApm records request-level data around Rack and uses Redis for storage and aggregation.
|
|
23
|
+
|
|
24
|
+
The core data flow is based on [Active Support Instrumentation](https://guides.rubyonrails.org/active_support_instrumentation.html).
|
|
25
|
+
|
|
26
|
+
Instrumentation events are processed by a long-running worker thread so request handling does not block on metric aggregation.
|
|
27
|
+
|
|
28
|
+
Memory usage is collected with [get_process_mem](https://github.com/schneems/get_process_mem). In Linux testing, collection takes less than 1 ms.
|
|
29
|
+
|
|
30
|
+
## Screenshots
|
|
23
31
|
|
|
24
|
-
## 功能截图
|
|
25
32
|
- Dashboard
|
|
26
|
-

|
|
27
34
|
|
|
28
|
-
-
|
|
35
|
+
- Slow Requests
|
|
29
36
|

|
|
30
37
|
|
|
31
|
-
- Action
|
|
38
|
+
- Action List
|
|
32
39
|

|
|
33
40
|
|
|
34
|
-
-
|
|
41
|
+
- Request Info
|
|
35
42
|

|
|
36
43
|
|
|
37
|
-
- Action
|
|
38
|
-

|
|
44
|
+
- Action Info
|
|
45
|
+

|
|
42
46
|
|
|
47
|
+
- Data Management
|
|
48
|
+

|
|
43
49
|
|
|
44
50
|
## Usage
|
|
45
51
|
|
|
52
|
+
Mount the engine in your Rails routes:
|
|
53
|
+
|
|
46
54
|
```ruby
|
|
47
55
|
# routes.rb
|
|
48
56
|
mount SimpleApm::Engine => "/apm"
|
|
57
|
+
```
|
|
49
58
|
|
|
50
|
-
|
|
51
|
-
rails generate simple_apm:install
|
|
59
|
+
Or run the installer:
|
|
52
60
|
|
|
61
|
+
```bash
|
|
62
|
+
rails generate simple_apm:install
|
|
53
63
|
```
|
|
54
64
|
|
|
55
|
-
|
|
56
65
|
## Installation
|
|
66
|
+
|
|
57
67
|
Add this line to your application's Gemfile:
|
|
58
68
|
|
|
59
69
|
```ruby
|
|
60
|
-
gem
|
|
70
|
+
gem "simple_apm"
|
|
61
71
|
```
|
|
62
72
|
|
|
63
|
-
|
|
73
|
+
Then run:
|
|
74
|
+
|
|
64
75
|
```bash
|
|
65
|
-
|
|
76
|
+
bundle
|
|
66
77
|
```
|
|
67
78
|
|
|
68
|
-
|
|
69
79
|
## Contributing
|
|
70
|
-
|
|
80
|
+
|
|
81
|
+
Issues and pull requests are welcome.
|
|
71
82
|
|
|
72
83
|
## License
|
|
84
|
+
|
|
73
85
|
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
|
@@ -12,4 +12,98 @@
|
|
|
12
12
|
//
|
|
13
13
|
//= require_tree .
|
|
14
14
|
|
|
15
|
+
(function ($) {
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
var config = JSON.parse(document.getElementById('apm-ui-config').textContent);
|
|
19
|
+
window.SimpleApmUI = {
|
|
20
|
+
tableOptions: function (overrides) {
|
|
21
|
+
return $.extend(true, {language: config.table, pageLength: 25, processing: true}, overrides);
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
$.extend($.fn.dataTableExt.oSort, {
|
|
26
|
+
'sec-pre': function (value) {
|
|
27
|
+
var exactSeconds = String(value).match(/data-seconds="([^"]+)"/);
|
|
28
|
+
if (exactSeconds) return parseFloat(exactSeconds[1]) || 0;
|
|
29
|
+
var text = String(value).replace(/<[^>]*>/g, '').replace(/&(?:amp;)?nbsp;/g, '').trim();
|
|
30
|
+
var number = parseFloat(text) || 0;
|
|
31
|
+
if (/ms\b/.test(text)) return number / 1000;
|
|
32
|
+
if (/min\b/.test(text)) return number * 60;
|
|
33
|
+
return number;
|
|
34
|
+
},
|
|
35
|
+
'sec-asc': function (a, b) { return a - b; },
|
|
36
|
+
'sec-desc': function (a, b) { return b - a; }
|
|
37
|
+
});
|
|
38
|
+
$.fn.dataTable.ext.errMode = 'none';
|
|
39
|
+
|
|
40
|
+
function fallbackCopy(text) {
|
|
41
|
+
var input = document.createElement('textarea');
|
|
42
|
+
input.value = text;
|
|
43
|
+
input.setAttribute('readonly', '');
|
|
44
|
+
input.style.position = 'fixed';
|
|
45
|
+
input.style.top = '0';
|
|
46
|
+
input.style.opacity = '0';
|
|
47
|
+
// Keep the temporary input inside a modal so Bootstrap's focus trap allows selection.
|
|
48
|
+
(document.querySelector('.modal.in .apm-modal-body') || document.body).appendChild(input);
|
|
49
|
+
input.select();
|
|
50
|
+
var copied = false;
|
|
51
|
+
try { copied = document.execCommand('copy'); } catch (error) { copied = false; }
|
|
52
|
+
input.remove();
|
|
53
|
+
return copied;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
$(function () {
|
|
57
|
+
$('.select-apm-date select').on('change', function () { this.form.submit(); });
|
|
58
|
+
$(document).on('submit', 'form[data-confirm]', function () {
|
|
59
|
+
return window.confirm(this.getAttribute('data-confirm'));
|
|
60
|
+
});
|
|
61
|
+
$(document).on('click', 'a[data-confirm]', function () {
|
|
62
|
+
return window.confirm(this.getAttribute('data-confirm'));
|
|
63
|
+
});
|
|
64
|
+
$(document).on('error.dt', 'table', function () {
|
|
65
|
+
$(this).closest('.apm-panel').find('.apm-table-feedback').text(config.load_error);
|
|
66
|
+
});
|
|
67
|
+
$(document).on('xhr.dt', 'table', function (event, settings, data) {
|
|
68
|
+
if (data) $(this).closest('.apm-panel').find('.apm-table-feedback').text('');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
$(document).on('click', '.sql', function () {
|
|
72
|
+
$('#sql-modal-text').text(this.textContent);
|
|
73
|
+
$('#sql-modal .apm-copy-status').text('');
|
|
74
|
+
$('#sql-modal').modal('show');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
$(document).on('click', '[data-copy-target]', function () {
|
|
78
|
+
var button = this;
|
|
79
|
+
var target = document.getElementById(button.getAttribute('data-copy-target'));
|
|
80
|
+
var text = target.value === undefined ? target.textContent : target.value;
|
|
81
|
+
var status = $(button).closest('.apm-panel, .apm-modal-panel').find('.apm-copy-status');
|
|
82
|
+
button.disabled = true;
|
|
83
|
+
|
|
84
|
+
function finish(copied) {
|
|
85
|
+
button.disabled = false;
|
|
86
|
+
status.text(copied ? config.copied : config.copy_failed);
|
|
87
|
+
if (copied) {
|
|
88
|
+
button.focus();
|
|
89
|
+
} else if (target.tagName === 'TEXTAREA') {
|
|
90
|
+
$(target).closest('details').prop('open', true);
|
|
91
|
+
target.focus();
|
|
92
|
+
target.select();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (navigator.clipboard && window.isSecureContext) {
|
|
97
|
+
navigator.clipboard.writeText(text).then(function () {
|
|
98
|
+
finish(true);
|
|
99
|
+
}, function () {
|
|
100
|
+
finish(fallbackCopy(text));
|
|
101
|
+
});
|
|
102
|
+
} else {
|
|
103
|
+
finish(fallbackCopy(text));
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
})(jQuery);
|
|
108
|
+
|
|
15
109
|
|
|
@@ -26,9 +26,9 @@
|
|
|
26
26
|
--apm-brand-dark: #0b4f4a;
|
|
27
27
|
--apm-accent: #2563eb;
|
|
28
28
|
--apm-danger: #dc2626;
|
|
29
|
-
--apm-warning: #
|
|
29
|
+
--apm-warning: #a35b05;
|
|
30
30
|
--apm-success: #15803d;
|
|
31
|
-
--apm-shadow: 0
|
|
31
|
+
--apm-shadow: 0 2px 8px rgba(15, 23, 42, 0.04);
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
body.simple-apm {
|
|
@@ -37,6 +37,7 @@ body.simple-apm {
|
|
|
37
37
|
color: var(--apm-ink);
|
|
38
38
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", Arial, sans-serif;
|
|
39
39
|
font-size: 14px;
|
|
40
|
+
line-height: 1.6;
|
|
40
41
|
letter-spacing: 0;
|
|
41
42
|
}
|
|
42
43
|
|
|
@@ -106,7 +107,7 @@ body.simple-apm {
|
|
|
106
107
|
.apm-nav .navbar-brand {
|
|
107
108
|
float: none;
|
|
108
109
|
height: auto;
|
|
109
|
-
padding:
|
|
110
|
+
padding: 13px 12px;
|
|
110
111
|
color: #cbd5e1;
|
|
111
112
|
font-size: 13px;
|
|
112
113
|
line-height: 18px;
|
|
@@ -144,6 +145,36 @@ body.simple-apm {
|
|
|
144
145
|
box-shadow: none;
|
|
145
146
|
}
|
|
146
147
|
|
|
148
|
+
.apm-language-switch {
|
|
149
|
+
display: inline-flex;
|
|
150
|
+
align-items: center;
|
|
151
|
+
gap: 4px;
|
|
152
|
+
padding: 3px;
|
|
153
|
+
background: rgba(255, 255, 255, 0.08);
|
|
154
|
+
border-radius: 6px;
|
|
155
|
+
flex: 0 0 auto;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.apm-language-switch a {
|
|
159
|
+
padding: 5px 8px;
|
|
160
|
+
color: #cbd5e1;
|
|
161
|
+
border-radius: 4px;
|
|
162
|
+
font-size: 12px;
|
|
163
|
+
line-height: 16px;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.apm-language-switch a:hover,
|
|
167
|
+
.apm-language-switch a:focus {
|
|
168
|
+
color: #ffffff;
|
|
169
|
+
text-decoration: none;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.apm-language-switch a.active {
|
|
173
|
+
color: #052e2b;
|
|
174
|
+
background: #5eead4;
|
|
175
|
+
font-weight: 700;
|
|
176
|
+
}
|
|
177
|
+
|
|
147
178
|
.apm-content {
|
|
148
179
|
width: min(1440px, calc(100% - 48px));
|
|
149
180
|
margin: 0 auto;
|
|
@@ -170,6 +201,7 @@ body.simple-apm {
|
|
|
170
201
|
margin: 7px 0 0;
|
|
171
202
|
color: var(--apm-muted);
|
|
172
203
|
font-size: 13px;
|
|
204
|
+
overflow-wrap: anywhere;
|
|
173
205
|
}
|
|
174
206
|
|
|
175
207
|
.apm-toolbar {
|
|
@@ -212,6 +244,8 @@ body.simple-apm {
|
|
|
212
244
|
margin-top: 18px;
|
|
213
245
|
}
|
|
214
246
|
|
|
247
|
+
.apm-grid > .apm-panel { margin-top: 0; }
|
|
248
|
+
|
|
215
249
|
.apm-panel-header {
|
|
216
250
|
display: flex;
|
|
217
251
|
align-items: center;
|
|
@@ -272,6 +306,7 @@ body.simple-apm {
|
|
|
272
306
|
line-height: 1.2;
|
|
273
307
|
font-weight: 700;
|
|
274
308
|
word-break: break-word;
|
|
309
|
+
font-variant-numeric: tabular-nums;
|
|
275
310
|
}
|
|
276
311
|
|
|
277
312
|
.apm-stat-meta {
|
|
@@ -329,6 +364,11 @@ body.simple-apm {
|
|
|
329
364
|
border-top: 1px solid #eef2f7;
|
|
330
365
|
color: #243044;
|
|
331
366
|
vertical-align: middle;
|
|
367
|
+
font-variant-numeric: tabular-nums;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
.simple-apm table.table tbody tr:hover td {
|
|
371
|
+
background: var(--apm-surface-soft);
|
|
332
372
|
}
|
|
333
373
|
|
|
334
374
|
.simple-apm table.table-bordered {
|
|
@@ -385,6 +425,11 @@ body.simple-apm {
|
|
|
385
425
|
.simple-apm .btn {
|
|
386
426
|
border-radius: 6px;
|
|
387
427
|
font-weight: 600;
|
|
428
|
+
min-height: 40px;
|
|
429
|
+
display: inline-flex;
|
|
430
|
+
align-items: center;
|
|
431
|
+
justify-content: center;
|
|
432
|
+
padding: 8px 14px;
|
|
388
433
|
}
|
|
389
434
|
|
|
390
435
|
.simple-apm a.btn,
|
|
@@ -471,6 +516,10 @@ pre {
|
|
|
471
516
|
padding: 14px 18px;
|
|
472
517
|
border-bottom: 1px solid var(--apm-border);
|
|
473
518
|
background: var(--apm-surface-soft);
|
|
519
|
+
display: flex;
|
|
520
|
+
align-items: center;
|
|
521
|
+
justify-content: space-between;
|
|
522
|
+
gap: 12px;
|
|
474
523
|
}
|
|
475
524
|
|
|
476
525
|
.apm-modal-title {
|
|
@@ -517,6 +566,62 @@ pre {
|
|
|
517
566
|
color: #475467;
|
|
518
567
|
}
|
|
519
568
|
|
|
569
|
+
.simple-apm :focus-visible {
|
|
570
|
+
outline: 3px solid var(--apm-accent);
|
|
571
|
+
outline-offset: 3px;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
.apm-skip-link {
|
|
575
|
+
position: fixed;
|
|
576
|
+
top: -100px;
|
|
577
|
+
left: 16px;
|
|
578
|
+
z-index: 1100;
|
|
579
|
+
padding: 12px;
|
|
580
|
+
background: var(--apm-surface);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
.apm-skip-link:focus { top: 8px; }
|
|
584
|
+
|
|
585
|
+
.apm-date-form {
|
|
586
|
+
display: flex;
|
|
587
|
+
align-items: center;
|
|
588
|
+
gap: 8px;
|
|
589
|
+
width: 100%;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
.apm-date-form label { margin: 0; white-space: nowrap; }
|
|
593
|
+
|
|
594
|
+
.simple-apm a.apm-button-secondary,
|
|
595
|
+
.simple-apm .apm-button-secondary {
|
|
596
|
+
background: var(--apm-surface);
|
|
597
|
+
border: 1px solid var(--apm-border-strong);
|
|
598
|
+
color: var(--apm-ink);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
.simple-apm .apm-button-secondary:hover { background: var(--apm-surface-soft); color: var(--apm-accent); }
|
|
602
|
+
.apm-toolbar-actions form { margin: 0; }
|
|
603
|
+
#sample-limit { width: auto; min-width: 80px; }
|
|
604
|
+
.apm-metrics-compact { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
|
605
|
+
.apm-mono { font-family: Menlo, Monaco, Consolas, monospace; font-size: 12px; overflow-wrap: anywhere; }
|
|
606
|
+
.apm-stat-hint { color: var(--apm-muted); font-size: 12px; margin: 8px 0 0; }
|
|
607
|
+
.apm-breadcrumb { margin: 0 0 16px; color: var(--apm-muted); display: flex; gap: 8px; flex-wrap: wrap; }
|
|
608
|
+
.apm-empty-state { padding: 40px 24px; text-align: center; color: var(--apm-muted); }
|
|
609
|
+
.apm-empty-state .apm-toolbar-actions { justify-content: center; margin-top: 20px; }
|
|
610
|
+
.apm-notice { padding: 12px 16px; margin-bottom: 18px; background: #d1fae5; border: 1px solid #a7f3d0; color: #065f46; border-radius: 8px; }
|
|
611
|
+
.apm-copy-status, .apm-table-feedback { color: var(--apm-brand-dark); margin: 8px 0 0; }
|
|
612
|
+
.apm-copy-status:empty, .apm-table-feedback:empty { display: none; }
|
|
613
|
+
.apm-report-text { margin-top: 12px; width: 100%; resize: vertical; border: 1px solid var(--apm-border); border-radius: 6px; background: var(--apm-surface-soft); padding: 14px; line-height: 1.6; }
|
|
614
|
+
.simple-apm summary { cursor: pointer; color: var(--apm-accent); padding: 8px 0; }
|
|
615
|
+
.simple-apm button.sql { border: 0; background: transparent; padding: 4px 0; text-align: left; }
|
|
616
|
+
.apm-runtime-row { display: grid; grid-template-columns: 90px minmax(0, 1fr) 110px; align-items: center; gap: 16px; margin: 10px 0; }
|
|
617
|
+
.apm-runtime-row meter { width: 100%; height: 14px; accent-color: var(--apm-brand); }
|
|
618
|
+
.apm-runtime-row span:last-child { text-align: right; font-variant-numeric: tabular-nums; }
|
|
619
|
+
.apm-status-neutral { background: #e2e8f0; color: #334155; }
|
|
620
|
+
|
|
621
|
+
@media (prefers-reduced-motion: reduce) {
|
|
622
|
+
.simple-apm *, .simple-apm *::before, .simple-apm *::after { animation: none !important; transition: none !important; }
|
|
623
|
+
}
|
|
624
|
+
|
|
520
625
|
@media (max-width: 980px) {
|
|
521
626
|
.apm-topbar-inner,
|
|
522
627
|
.apm-content,
|
|
@@ -539,6 +644,10 @@ pre {
|
|
|
539
644
|
margin-left: auto;
|
|
540
645
|
}
|
|
541
646
|
|
|
647
|
+
.apm-language-switch {
|
|
648
|
+
margin-left: auto;
|
|
649
|
+
}
|
|
650
|
+
|
|
542
651
|
.apm-page-header,
|
|
543
652
|
.apm-toolbar {
|
|
544
653
|
align-items: flex-start;
|
|
@@ -549,7 +658,7 @@ pre {
|
|
|
549
658
|
grid-column: span 12;
|
|
550
659
|
}
|
|
551
660
|
|
|
552
|
-
.apm-metrics {
|
|
661
|
+
.apm-metrics, .apm-metrics-compact {
|
|
553
662
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
554
663
|
}
|
|
555
664
|
}
|
|
@@ -564,6 +673,11 @@ pre {
|
|
|
564
673
|
justify-content: space-between;
|
|
565
674
|
}
|
|
566
675
|
|
|
676
|
+
.apm-language-switch {
|
|
677
|
+
width: auto;
|
|
678
|
+
justify-content: center;
|
|
679
|
+
}
|
|
680
|
+
|
|
567
681
|
.apm-date-select select.form-control {
|
|
568
682
|
flex: 1 1 auto;
|
|
569
683
|
}
|
|
@@ -576,11 +690,21 @@ pre {
|
|
|
576
690
|
font-size: 22px;
|
|
577
691
|
}
|
|
578
692
|
|
|
579
|
-
.apm-metrics {
|
|
580
|
-
grid-template-columns: 1fr;
|
|
693
|
+
.apm-metrics, .apm-metrics-compact {
|
|
694
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
581
695
|
}
|
|
582
696
|
|
|
697
|
+
.apm-metrics .apm-stat:last-child:nth-child(odd) { grid-column: 1 / -1; }
|
|
698
|
+
.apm-stat-value { font-size: 18px; }
|
|
699
|
+
|
|
583
700
|
.apm-detail-list {
|
|
584
701
|
grid-template-columns: 1fr;
|
|
585
702
|
}
|
|
703
|
+
|
|
704
|
+
.apm-panel-header { flex-wrap: wrap; }
|
|
705
|
+
.apm-runtime-row { grid-template-columns: 60px minmax(0, 1fr) 90px; gap: 8px; }
|
|
706
|
+
.apm-nav { gap: 2px; }
|
|
707
|
+
.apm-nav .navbar-brand { white-space: nowrap; padding: 12px 8px; font-size: 12px; }
|
|
708
|
+
.simple-apm .btn { min-height: 44px; }
|
|
709
|
+
.simple-apm .dataTables_wrapper .dataTables_filter input { max-width: 160px; }
|
|
586
710
|
}
|
|
@@ -8,27 +8,32 @@ module SimpleApm
|
|
|
8
8
|
def dashboard
|
|
9
9
|
d = SimpleApm::RedisKey.query_date == Time.now.strftime('%Y-%m-%d') ? Time.now.strftime('%H:%M') : '23:50'
|
|
10
10
|
data = SimpleApm::Hit.chart_data(0, d)
|
|
11
|
+
hits = data.values.sum { |point| point[:hits].to_i }
|
|
12
|
+
time = data.values.sum { |point| point[:time].to_f }
|
|
13
|
+
@daily_stats = {hits: hits, time: time, avg_time: hits.zero? ? 0 : time / hits}
|
|
11
14
|
@x_names = data.keys.sort
|
|
12
15
|
@time_arr = @x_names.map {|n| data[n][:hits].to_i.zero? ? 0 : (data[n][:time].to_f / data[n][:hits].to_i).round(3)}
|
|
13
16
|
@hits_arr = @x_names.map {|n| data[n][:hits] rescue 0}
|
|
14
17
|
end
|
|
15
18
|
|
|
16
19
|
def index
|
|
20
|
+
@sample_limit = [[(params[:count].presence || 200).to_i, 1].max, SimpleApm::Setting::SLOW_ACTIONS_LIMIT.to_i].min
|
|
17
21
|
respond_to do |format|
|
|
18
22
|
format.json do
|
|
19
|
-
@slow_requests = SimpleApm::SlowRequest.list(
|
|
20
|
-
request = r.request
|
|
23
|
+
@slow_requests = SimpleApm::SlowRequest.list(@sample_limit).map do |r|
|
|
24
|
+
request = r.request
|
|
25
|
+
next unless request
|
|
21
26
|
[
|
|
22
|
-
link_to(time_label(request.started), show_path(id: request.request_id)),
|
|
23
|
-
link_to(request.action_name, action_info_path(action_name: request.action_name)),
|
|
24
|
-
|
|
25
|
-
|
|
27
|
+
link_to(time_label(request.started), show_path(id: request.request_id, apm_date: apm_date)),
|
|
28
|
+
link_to(request.action_name, action_info_path(action_name: request.action_name, apm_date: apm_date)),
|
|
29
|
+
duration_cell(request.during),
|
|
30
|
+
duration_cell(request.db_runtime),
|
|
26
31
|
request.queries_count,
|
|
27
|
-
|
|
28
|
-
|
|
32
|
+
duration_cell(request.view_runtime),
|
|
33
|
+
duration_cell(request.net_http_during),
|
|
29
34
|
request.memory_during.to_f.round(1),
|
|
30
|
-
request.host,
|
|
31
|
-
request.remote_addr
|
|
35
|
+
ERB::Util.html_escape(request.host),
|
|
36
|
+
ERB::Util.html_escape(request.remote_addr)
|
|
32
37
|
]
|
|
33
38
|
end.compact
|
|
34
39
|
render json: {data: @slow_requests}
|
|
@@ -39,6 +44,7 @@ module SimpleApm
|
|
|
39
44
|
|
|
40
45
|
def show
|
|
41
46
|
@request = SimpleApm::Request.find(params[:id])
|
|
47
|
+
render :missing, status: :not_found unless @request
|
|
42
48
|
end
|
|
43
49
|
|
|
44
50
|
def actions
|
|
@@ -47,6 +53,7 @@ module SimpleApm
|
|
|
47
53
|
|
|
48
54
|
def action_info
|
|
49
55
|
@action = SimpleApm::Action.find(params[:action_name])
|
|
56
|
+
render :missing, status: :not_found if @action.click_count.to_i.zero?
|
|
50
57
|
end
|
|
51
58
|
|
|
52
59
|
def change_date
|
|
@@ -54,6 +61,11 @@ module SimpleApm
|
|
|
54
61
|
redirect_to request.referer
|
|
55
62
|
end
|
|
56
63
|
|
|
64
|
+
def set_locale
|
|
65
|
+
session[:simple_apm_locale] = params[:locale] if AVAILABLE_LOCALES.include?(params[:locale])
|
|
66
|
+
redirect_back fallback_location: dashboard_path
|
|
67
|
+
end
|
|
68
|
+
|
|
57
69
|
def data
|
|
58
70
|
@data = SimpleApm::Redis.in_apm_days.map {|x| SimpleApm::Hit.day_info(x)}
|
|
59
71
|
end
|
|
@@ -61,21 +73,21 @@ module SimpleApm
|
|
|
61
73
|
def data_delete
|
|
62
74
|
if params[:date].is_a?(String)
|
|
63
75
|
r = SimpleApm::Redis.clear_data(params[:date])
|
|
64
|
-
flash[:notice] = r[:success] ?
|
|
76
|
+
flash[:notice] = r[:success] ? t("simple_apm.flash.deleted") : t("simple_apm.flash.no_data_for_day")
|
|
65
77
|
elsif params[:type]=='month'
|
|
66
78
|
del_count = SimpleApm::Redis.clear_data_before_time(Time.now.at_beginning_of_day - 1.month)
|
|
67
|
-
flash[:notice] = "
|
|
79
|
+
flash[:notice] = t("simple_apm.flash.deleted_count", count: del_count)
|
|
68
80
|
elsif params[:type]=='week'
|
|
69
81
|
del_count = SimpleApm::Redis.clear_data_before_time(Time.now.at_beginning_of_day - 1.week)
|
|
70
|
-
flash[:notice] = "
|
|
82
|
+
flash[:notice] = t("simple_apm.flash.deleted_count", count: del_count)
|
|
71
83
|
elsif params[:type]=='stop_data'
|
|
72
84
|
SimpleApm::Redis.stop!
|
|
73
|
-
flash[:notice] =
|
|
85
|
+
flash[:notice] = t("simple_apm.flash.updated")
|
|
74
86
|
elsif params[:type]=='rerun_data'
|
|
75
87
|
SimpleApm::Redis.rerun!
|
|
76
|
-
flash[:notice] =
|
|
88
|
+
flash[:notice] = t("simple_apm.flash.updated")
|
|
77
89
|
else
|
|
78
|
-
flash[:notice] =
|
|
90
|
+
flash[:notice] = t("simple_apm.flash.unknown_action")
|
|
79
91
|
# r = params[:date].map{|d|SimpleApm::Redis.clear_data(d)}
|
|
80
92
|
# suc, fail = r.partition{|x|x[:success]}
|
|
81
93
|
# flash[:notice] = "成功删除#{suc.length}"
|
|
@@ -97,7 +109,11 @@ module SimpleApm
|
|
|
97
109
|
end
|
|
98
110
|
|
|
99
111
|
def link_to(name, url)
|
|
100
|
-
|
|
112
|
+
helpers.link_to(name, url)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def duration_cell(seconds)
|
|
116
|
+
helpers.content_tag(:span, sec_str(seconds), data: {seconds: seconds.to_f})
|
|
101
117
|
end
|
|
102
118
|
end
|
|
103
119
|
end
|
|
@@ -1,10 +1,24 @@
|
|
|
1
1
|
module SimpleApm
|
|
2
2
|
class ApplicationController < ActionController::Base
|
|
3
|
+
AVAILABLE_LOCALES = %w[en zh-CN].freeze
|
|
4
|
+
|
|
3
5
|
protect_from_forgery with: :exception
|
|
4
|
-
|
|
6
|
+
around_action :switch_simple_apm_locale
|
|
7
|
+
helper_method :apm_date, :simple_apm_locale
|
|
5
8
|
|
|
6
9
|
def apm_date
|
|
7
10
|
session[:apm_date].presence || Time.now.strftime("%Y-%m-%d")
|
|
8
11
|
end
|
|
12
|
+
|
|
13
|
+
def simple_apm_locale
|
|
14
|
+
locale = session[:simple_apm_locale].presence
|
|
15
|
+
AVAILABLE_LOCALES.include?(locale) ? locale : "en"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def switch_simple_apm_locale(&action)
|
|
21
|
+
I18n.with_locale(simple_apm_locale, &action)
|
|
22
|
+
end
|
|
9
23
|
end
|
|
10
24
|
end
|
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
module SimpleApm
|
|
2
2
|
module ApplicationHelper
|
|
3
|
+
def duration_percent(part, total)
|
|
4
|
+
return 0 if total.to_f <= 0
|
|
5
|
+
[[(part.to_f / total.to_f * 100).round(1), 0].max, 100].min
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def table_language
|
|
9
|
+
{
|
|
10
|
+
search: t('simple_apm.table.search'),
|
|
11
|
+
lengthMenu: t('simple_apm.table.length_menu'),
|
|
12
|
+
info: t('simple_apm.table.info'),
|
|
13
|
+
infoEmpty: t('simple_apm.table.info_empty'),
|
|
14
|
+
infoFiltered: t('simple_apm.table.info_filtered'),
|
|
15
|
+
zeroRecords: t('simple_apm.table.zero_records'),
|
|
16
|
+
emptyTable: t('simple_apm.table.empty'),
|
|
17
|
+
loadingRecords: t('simple_apm.table.loading'),
|
|
18
|
+
processing: t('simple_apm.table.processing'),
|
|
19
|
+
paginate: {
|
|
20
|
+
first: t('simple_apm.table.first'), last: t('simple_apm.table.last'),
|
|
21
|
+
next: t('simple_apm.table.next'), previous: t('simple_apm.table.previous')
|
|
22
|
+
},
|
|
23
|
+
aria: {sortAscending: t('simple_apm.table.sort_ascending'), sortDescending: t('simple_apm.table.sort_descending')}
|
|
24
|
+
}
|
|
25
|
+
end
|
|
26
|
+
|
|
3
27
|
def time_label(t, full = false)
|
|
4
28
|
Time.parse(t).strftime("#{'%Y-%m-%d ' if full}%H:%M:%S") rescue ''
|
|
5
29
|
end
|
|
@@ -23,23 +23,23 @@ module SimpleApm
|
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
def avg_http_time
|
|
26
|
-
http_time.to_f/click_count.to_i
|
|
26
|
+
click_count.to_i.zero? ? 0.0 : http_time.to_f/click_count.to_i
|
|
27
27
|
end
|
|
28
28
|
|
|
29
29
|
def avg_db_time
|
|
30
|
-
db_time.to_f/click_count.to_i
|
|
30
|
+
click_count.to_i.zero? ? 0.0 : db_time.to_f/click_count.to_i
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
def avg_queries_count
|
|
34
|
-
queries_count.to_f/click_count.to_i
|
|
34
|
+
click_count.to_i.zero? ? 0.0 : queries_count.to_f/click_count.to_i
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
def avg_cached_queries_count
|
|
38
|
-
cached_queries_count.to_f/click_count.to_i
|
|
38
|
+
click_count.to_i.zero? ? 0.0 : cached_queries_count.to_f/click_count.to_i
|
|
39
39
|
end
|
|
40
40
|
|
|
41
41
|
def avg_time
|
|
42
|
-
time.to_f/click_count.to_i
|
|
42
|
+
click_count.to_i.zero? ? 0.0 : time.to_f/click_count.to_i
|
|
43
43
|
end
|
|
44
44
|
|
|
45
45
|
class << self
|