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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 20c4854a122796cdb1c34546e8808f04ad6137c8d3d5e1936b547d329822fa9f
4
- data.tar.gz: 74a75a66e084d2d5464a42a1e594fe74575c9d5bba915570c1196b66e99eb5c7
3
+ metadata.gz: d98ff928f6478abea71e3375957adc7de6d5a3865019be549026b4d615b58fa0
4
+ data.tar.gz: ee6fbdf30feb8499da5ca323931272b6d270d41ccdf9cadc86213d7da5fd50ac
5
5
  SHA512:
6
- metadata.gz: 8253d3b452494e21d4fed612fd6f1076435296ba689750ac6ea72055931bd56909968e99707fb3d448aacd9f1c6829cf28ff78c9e2d56a7b90f9125e1a296aa7
7
- data.tar.gz: 2617e187209c01c3d3eb7413aef561dd80c295b95dd8ca10a7b1282cc918847afef4bb3b1decd5dbc85ebf0e6beddbce769b8ba6a3aecfb92260173e6d0a3a40
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
- 围绕Rack记录请求级别的相关信息,使用redis作为数据存储/计算工具来记录慢事务
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
- 数据传递核心为:[Active Support Instrumentation](https://guides.rubyonrails.org/active_support_instrumentation.html)
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
- 处理Instrument方式为开启一个不影响主线程的常驻线程,循环计算处理数据
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
- 获取内存信息用到了gem: [get_process_mem](https://github.com/schneems/get_process_mem),经测试在linux系统耗时在1ms以下
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
- ![Dashboard](public/dashboard.jpg)
33
+ ![Dashboard](public/dashbord.png)
27
34
 
28
- - 慢请求列表
35
+ - Slow Requests
29
36
  ![Slow Requests](public/slow_requests.png)
30
37
 
31
- - Action列表
38
+ - Action List
32
39
  ![Action List](public/action_list.png)
33
40
 
34
- - 请求详情
41
+ - Request Info
35
42
  ![Request Info](public/request_info.png)
36
43
 
37
- - Action概况
38
- ![Action Info](public/action_info.png)
39
-
40
- - 数据管理
41
- ![Data](public/data-manage.png)
44
+ - Action Info
45
+ ![Action Info](public/action_detail.png)
42
46
 
47
+ - Data Management
48
+ ![Data Management](public/data-manage.png)
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 'simple_apm'
70
+ gem "simple_apm"
61
71
  ```
62
72
 
63
- And then execute:
73
+ Then run:
74
+
64
75
  ```bash
65
- $ bundle
76
+ bundle
66
77
  ```
67
78
 
68
-
69
79
  ## Contributing
70
- Contribution directions go here.
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: #d97706;
29
+ --apm-warning: #a35b05;
30
30
  --apm-success: #15803d;
31
- --apm-shadow: 0 16px 40px rgba(15, 23, 42, 0.08);
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: 9px 12px;
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(params[:count] || 200).map do |r|
20
- request = r.request rescue next
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
- sec_str(request.during),
25
- sec_str(request.db_runtime),
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
- sec_str(request.view_runtime),
28
- sec_str(request.net_http_during),
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] ? '删除成功!' : r[:msg]
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] = "成功删除#{del_count}条数据"
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] = "成功删除#{del_count}条数据"
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
- "<a href=#{url.to_json}>#{name}</a>"
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
- helper_method :apm_date
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