simple_apm 0.1.0

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,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: ba1cd7b2606ced6aff23e56dae261c81de9ae06fb8dec56d0e91e4e7bb13aacb
4
+ data.tar.gz: 9205720ac911bd244e9ddb94f4c3e5ad1c40c3f6df3de13fb350a9cfcb7d1164
5
+ SHA512:
6
+ metadata.gz: 05340b8118889c1351bbd31aaddb2696fe46d6418dced5c532e453f1dd2a0a1aa93a5b4f5122b75384daa188edebeb3d23510122a91100bb72edc125350c44da
7
+ data.tar.gz: 46a70c6afe5e5464e6610260d7375bb740d66180169f2061eec633b187ba8b9ce3f6dee632303e7261bf1b3acc4f7fafd277975bf1a44426ba78e0a67d6f70c7
@@ -0,0 +1,20 @@
1
+ Copyright 2018 yuanyin.xia
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,42 @@
1
+ # 开发中……
2
+ # SimpleApm
3
+ 基于Redis的简单的Web请求性能监控/慢事务追踪工具
4
+
5
+ 以天为维度记录:
6
+ - 最慢的1000个(默认1000)请求
7
+ - 记录每个action最慢的100次请求
8
+ - 记录每个action的平均访问时间
9
+ - 记录每个小时请求量
10
+ - 记录慢请求的详情和对应SQL详情(多余的会删掉)
11
+ - 以10分钟为刻度记录平均/最慢访问时间、次数等性能指标
12
+
13
+ ## Usage
14
+
15
+ ```ruby
16
+ # routes.rb
17
+ mount SimpleApm::Engine => '/apm'
18
+ ```
19
+
20
+
21
+ ## Installation
22
+ Add this line to your application's Gemfile:
23
+
24
+ ```ruby
25
+ gem 'simple_apm'
26
+ ```
27
+
28
+ And then execute:
29
+ ```bash
30
+ $ bundle
31
+ ```
32
+
33
+ Or install it yourself as:
34
+ ```bash
35
+ $ gem install simple_apm
36
+ ```
37
+
38
+ ## Contributing
39
+ Contribution directions go here.
40
+
41
+ ## License
42
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
@@ -0,0 +1,36 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'rdoc/task'
8
+
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = 'rdoc'
11
+ rdoc.title = 'SimpleApm'
12
+ rdoc.options << '--line-numbers'
13
+ rdoc.rdoc_files.include('README.md')
14
+ rdoc.rdoc_files.include('lib/**/*.rb')
15
+ end
16
+
17
+ APP_RAKEFILE = File.expand_path("../test/dummy/Rakefile", __FILE__)
18
+ load 'rails/tasks/engine.rake'
19
+
20
+
21
+ load 'rails/tasks/statistics.rake'
22
+
23
+
24
+
25
+ require 'bundler/gem_tasks'
26
+
27
+ require 'rake/testtask'
28
+
29
+ Rake::TestTask.new(:test) do |t|
30
+ t.libs << 'test'
31
+ t.pattern = 'test/**/*_test.rb'
32
+ t.verbose = false
33
+ end
34
+
35
+
36
+ task default: :test
@@ -0,0 +1,2 @@
1
+ //= link_directory ../javascripts/simple_apm .js
2
+ //= link_directory ../stylesheets/simple_apm .css
@@ -0,0 +1,13 @@
1
+ // This is a manifest file that'll be compiled into application.js, which will include all the files
2
+ // listed below.
3
+ //
4
+ // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
5
+ // or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path.
6
+ //
7
+ // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
8
+ // compiled file. JavaScript code in this file should be added after the last require_* statement.
9
+ //
10
+ // Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
11
+ // about supported directives.
12
+ //
13
+ //= require_tree .
@@ -0,0 +1,46 @@
1
+ /*
2
+ * This is a manifest file that'll be compiled into application.css, which will include all the files
3
+ * listed below.
4
+ *
5
+ * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6
+ * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
7
+ *
8
+ * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9
+ * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
10
+ * files in this directory. Styles in this file should be added after the last require_* statement.
11
+ * It is generally better to create a new file per style scope.
12
+ *
13
+ *= require_tree .
14
+ *= require_self
15
+ */
16
+ footer{
17
+ text-align: center;
18
+ padding: 10px;
19
+ border-top: 1px solid #999999;
20
+ }
21
+ a.navbar-brand.active{
22
+ color: #00AEEF !important;
23
+ }
24
+ #page-container{
25
+ min-height: 80vh;
26
+ }
27
+ .select-apm-date{
28
+ width: auto;
29
+ float: right;
30
+ margin-top: 8px;
31
+ }
32
+ .select-apm-date>select{
33
+ display: inline-block;
34
+ width: auto;
35
+ }
36
+ .sql{
37
+ max-width: 300px;
38
+ white-space: nowrap;
39
+ text-overflow:ellipsis;
40
+ overflow: hidden;
41
+ color: #00AEEF;
42
+ cursor: pointer;
43
+ }
44
+ pre {
45
+ white-space: pre-wrap;
46
+ }
@@ -0,0 +1,69 @@
1
+ require_dependency "simple_apm/application_controller"
2
+
3
+ module SimpleApm
4
+ class ApmController < ApplicationController
5
+ include SimpleApm::ApplicationHelper
6
+ before_action :set_query_date
7
+
8
+ def dashboard
9
+ d = SimpleApm::RedisKey.query_date == Time.now.strftime('%Y-%m-%d') ? Time.now.strftime('%H:%M') : '23:50'
10
+ data = SimpleApm::Hit.chart_data(0, d)
11
+ @x_names = data.keys.sort
12
+ @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
+ @hits_arr = @x_names.map{|n| data[n][:hits] rescue 0}
14
+ end
15
+
16
+ def index
17
+ respond_to do |format|
18
+ format.json do
19
+ @slow_requests = SimpleApm::SlowRequest.list(params[:count]||200).map do |r|
20
+ request = r.request
21
+ [
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),
26
+ sec_str(request.view_runtime),
27
+ request.host,
28
+ request.remote_addr
29
+ ]
30
+ end
31
+ render json: {data: @slow_requests}
32
+ end
33
+ format.html
34
+ end
35
+ end
36
+
37
+ def show
38
+ @request = SimpleApm::Request.find(params[:id])
39
+ end
40
+
41
+ def actions
42
+ @actions = SimpleApm::Action.all_names.map{|n| SimpleApm::Action.find(n)}
43
+ end
44
+
45
+ def action_info
46
+ @action = SimpleApm::Action.find(params[:action_name])
47
+ end
48
+
49
+ def change_date
50
+ session[:apm_date] = params[:date]
51
+ redirect_to request.referer
52
+ end
53
+
54
+ def set_apm_date
55
+ # set_query_date
56
+ redirect_to action: :dashboard
57
+ end
58
+
59
+ private
60
+ def set_query_date
61
+ session[:apm_date] = params[:apm_date] if params[:apm_date].present?
62
+ SimpleApm::RedisKey.query_date = session[:apm_date]
63
+ end
64
+
65
+ def link_to(name, url)
66
+ "<a href=#{url.to_json}>#{name}</a>"
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,10 @@
1
+ module SimpleApm
2
+ class ApplicationController < ActionController::Base
3
+ protect_from_forgery with: :exception
4
+ helper_method :apm_date
5
+
6
+ def apm_date
7
+ session[:apm_date].presence || Time.now.strftime("%Y-%m-%d")
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,27 @@
1
+ module SimpleApm
2
+ module ApplicationHelper
3
+ def time_label(t, full = false)
4
+ Time.parse(t).strftime("#{'%Y-%m-%d ' if full}%H:%M:%S") rescue ''
5
+ end
6
+
7
+ def sec_str(sec, force = nil)
8
+ _sec = sec.to_f
9
+
10
+ if force == 'min'
11
+ return "#{(_sec / 60).to_f.round(1)}min"
12
+ elsif force == 's'
13
+ return "#{_sec.round(2)}s"
14
+ elsif force == 'ms'
15
+ return "#{(_sec * 1000).round}ms"
16
+ end
17
+
18
+ if (_sec / 60).to_i > 0
19
+ "#{(_sec / 60).to_f.round(1)}min"
20
+ elsif _sec.to_i > 0
21
+ "#{_sec.round(2)}s"
22
+ else
23
+ "#{(_sec * 1000).round}ms"
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,4 @@
1
+ module SimpleApm
2
+ class ApplicationJob < ActiveJob::Base
3
+ end
4
+ end
@@ -0,0 +1,6 @@
1
+ module SimpleApm
2
+ class ApplicationMailer < ActionMailer::Base
3
+ default from: 'from@example.com'
4
+ layout 'mailer'
5
+ end
6
+ end
@@ -0,0 +1,72 @@
1
+ # 请求 Controller#Action
2
+ module SimpleApm
3
+ class Action
4
+ attr_accessor :name, :click_count, :time, :slow_time, :slow_id, :fast_time, :fast_id
5
+
6
+ def initialize(h)
7
+ h.each do |k, v|
8
+ send("#{k}=", v)
9
+ end
10
+ end
11
+
12
+ def fastest_request
13
+ @fastest_request ||= SimpleApm::Request.find(fast_id)
14
+ end
15
+ def slowest_request
16
+ @slowest_request ||= SimpleApm::Request.find(slow_id)
17
+ end
18
+
19
+ # @return [Array<SimpleApm::SlowRequest>]
20
+ def slow_requests(limit = 20, offset = 0)
21
+ @slow_requests ||= SimpleApm::SlowRequest.list_by_action(name, limit, offset)
22
+ end
23
+
24
+ def avg_time
25
+ time.to_f/click_count.to_i
26
+ end
27
+
28
+ class << self
29
+ def find(action_name)
30
+ SimpleApm::Action.new SimpleApm::Redis.hgetall(info_key(action_name)).merge(name: action_name)
31
+ end
32
+
33
+ # @param h [Hash] 一次request请求的信息
34
+ def update_by_request(h)
35
+ SimpleApm::Redis.sadd(action_list_key, h['action_name'])
36
+ _key = info_key h['action_name']
37
+ _request_store = false
38
+ # 点击次数
39
+ SimpleApm::Redis.hincrby _key, 'click_count', 1
40
+ # 总时间
41
+ SimpleApm::Redis.hincrbyfloat _key, 'time', h['during']
42
+ _slow = SimpleApm::Redis.hget _key, 'slow_time'
43
+ if _slow.nil? || h['during'].to_f > _slow.to_f
44
+ # 记录最慢访问
45
+ SimpleApm::Redis.hmset _key, 'slow_time', h['during'], 'slow_id', h['request_id']
46
+ _request_store = true
47
+ end
48
+ _fast = SimpleApm::Redis.hget _key, 'fast_time'
49
+ if _fast.nil? || h['during'].to_f < _fast.to_f
50
+ # 记录最快访问
51
+ SimpleApm::Redis.hmset _key, 'fast_time', h['during'], 'fast_id', h['request_id']
52
+ _request_store = true
53
+ end
54
+ _request_store
55
+ end
56
+
57
+ # @return [Array<String>]
58
+ def all_names
59
+ SimpleApm::Redis.smembers(action_list_key)
60
+ end
61
+
62
+ def action_list_key
63
+ SimpleApm::RedisKey['action-names']
64
+ end
65
+
66
+ def info_key(action_name)
67
+ SimpleApm::RedisKey["action-info:#{action_name}"]
68
+ end
69
+ end
70
+
71
+ end
72
+ end
@@ -0,0 +1,5 @@
1
+ module SimpleApm
2
+ class ApplicationRecord < ActiveRecord::Base
3
+ self.abstract_class = true
4
+ end
5
+ end
@@ -0,0 +1,54 @@
1
+ # action的点击数,按小时来显示
2
+ module SimpleApm
3
+ class Hit
4
+
5
+
6
+ class << self
7
+ def chart_data(start_time = '00:00', end_time = '23:50', per = 'minute')
8
+ start_hour = start_time.to_s.split(':').first.to_i
9
+ end_hour = [end_time.to_s.split(':').first.to_i, 23].min
10
+ end_min = end_time.to_s.split(':')[1]
11
+ minutes = %w[00 10 20 30 40 50]
12
+ redis_result = Hash[SimpleApm::Redis.hgetall(minute_key)]
13
+ result_hash = {}
14
+ start_hour.upto(end_hour).each do |_hour|
15
+ minutes.each do |_min|
16
+ break if end_hour.to_i==_hour && end_min && _min.to_i > end_min.to_i
17
+ k = "#{to_double_str _hour}:#{to_double_str _min}"
18
+ _time = redis_result["#{k}:time"].to_f
19
+ _hits = redis_result["#{k}:hits"].to_i
20
+ if per =='minute'
21
+ result_hash[k] = {time: _time, hits: _hits}
22
+ else
23
+ _key = to_double_str _hour
24
+ result_hash[_key] ||= { time: 0.0, hits: 0 }
25
+ result_hash[_key][:time] += _time
26
+ result_hash[_key][:hits] += _hits
27
+ end
28
+ end
29
+ end
30
+ result_hash
31
+ end
32
+
33
+
34
+ def update_by_request(h)
35
+ # SimpleApm::Redis.hincrby hour_hit_key, Time.now.hour, 1
36
+ minute_base = "#{to_double_str Time.now.hour}:#{to_double_str 10 * (Time.now.min / 10)}"
37
+ SimpleApm::Redis.hincrby minute_key, "#{minute_base}:hits", 1
38
+ SimpleApm::Redis.hincrbyfloat minute_key, "#{minute_base}:time", h['during']
39
+ end
40
+
41
+ def to_double_str(i)
42
+ i.to_s.size==1 ? "0#{i}" : i.to_s
43
+ end
44
+
45
+ def minute_key
46
+ SimpleApm::RedisKey['per-10-minute']
47
+ end
48
+
49
+ # def hour_hit_key
50
+ # SimpleApm::RedisKey['hour-hits']
51
+ # end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,35 @@
1
+ # 单个请求信息
2
+ module SimpleApm
3
+ class Request
4
+ attr_accessor :request_id, :action_name,
5
+ :during, :started, :db_runtime, :view_runtime,
6
+ :controller, :action, :format, :method,
7
+ :host, :remote_addr,
8
+ :exception, :status
9
+ def initialize(h)
10
+ h.each do |k, v|
11
+ send("#{k}=", v) rescue puts "attr #{k} not set!"
12
+ end
13
+ end
14
+
15
+ def sqls
16
+ @sqls ||= SimpleApm::Sql.find_by_request_id(request_id)
17
+ end
18
+
19
+
20
+ class << self
21
+
22
+ def find(id)
23
+ SimpleApm::Request.new JSON.parse(SimpleApm::Redis.hget(key, id))
24
+ end
25
+
26
+ def create(h)
27
+ SimpleApm::Redis.hmset key, h['request_id'], h.to_json
28
+ end
29
+
30
+ def key
31
+ SimpleApm::RedisKey['requests']
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,76 @@
1
+ # 慢请求列表,包括最慢的N个请求,指定Action的最慢的N个请求
2
+ module SimpleApm
3
+ class SlowRequest
4
+ attr_accessor :action_name, :request_id, :during
5
+ def initialize(request_id, during, action_name = nil)
6
+ self.action_name = action_name
7
+ self.request_id = request_id
8
+ self.during = during
9
+ end
10
+
11
+
12
+ def request
13
+ @request_info ||= SimpleApm::Request.find(request_id)
14
+ end
15
+ alias :info :request
16
+
17
+ def sqls
18
+ @request_sqls ||= SimpleApm::Sql.find_by_request_id(request_id)
19
+ end
20
+
21
+ class << self
22
+ # 存储最慢的1000个请求和每个action的最慢100次请求
23
+ def update_by_request(info)
24
+ in_action = update_action(info[:action_name], info[:during], info[:request_id])
25
+ in_slow_request = update_request(info[:during], info[:request_id])
26
+ in_action || in_slow_request
27
+ end
28
+
29
+ # @param action_name [String] 请求名ControllerName#ActionName
30
+ # @param during [Float] 耗时
31
+ # @param request_id [Hash] 请求id
32
+ # @return [Boolean] 是否插入成功
33
+ def update_action(action_name, during, request_id)
34
+ SimpleApm::Redis.zadd(action_key(action_name), during, request_id)
35
+ SimpleApm::Redis.zremrangebyrank(action_key(action_name), 0, -SimpleApm::Setting::ACTION_SLOW_REQUEST_LIMIT - 1)
36
+ SimpleApm::Redis.zrank(action_key(action_name), request_id).present?
37
+ end
38
+
39
+ # @param request_id [Hash] 请求id
40
+ # @param during [Float] 耗时
41
+ # @return [Boolean] 是否插入成功
42
+ def update_request(during, request_id)
43
+ # 记录最慢请求列表1000个
44
+ SimpleApm::Redis.zadd(key, during, request_id)
45
+ SimpleApm::Redis.zremrangebyrank(key, 0, -SimpleApm::Setting::SLOW_ACTIONS_LIMIT - 1)
46
+ SimpleApm::Redis.zrank(key, request_id).present?
47
+ end
48
+
49
+ # 从慢到快的排序
50
+ # @return [Array<SimpleApm::SlowRequest>]
51
+ def list_by_action(action_name, limit = 100, offset = 0)
52
+ SimpleApm::Redis.zrevrange(
53
+ action_key(action_name), offset, limit, with_scores: true
54
+ ).map{ |x| SimpleApm::SlowRequest.new(x[0], x[1], action_name)}
55
+ end
56
+
57
+ # 从慢到快的排序
58
+ # @return [Array<SimpleApm::SlowRequest>]
59
+ def list(limit = 100, offset = 0)
60
+ SimpleApm::Redis.zrevrange(
61
+ key, offset.to_i, limit.to_i - 1, with_scores: true
62
+ ).map{ |x| SimpleApm::SlowRequest.new(x[0], x[1])}
63
+ end
64
+
65
+ def key
66
+ SimpleApm::RedisKey['slow-requests']
67
+ end
68
+
69
+ def action_key(action_name = nil)
70
+ SimpleApm::RedisKey["action-slow:#{action_name}"]
71
+ end
72
+
73
+ end
74
+
75
+ end
76
+ end
@@ -0,0 +1,43 @@
1
+ # 请求对应的SQL查询列表
2
+ module SimpleApm
3
+ class Sql
4
+ attr_accessor :request_id, :sql, :line, :filename, :method, :name, :during, :started, :value
5
+ def initialize(h, request = nil)
6
+ h.each do |k, v|
7
+ send("#{k}=", v) rescue puts "attr #{k} not set!"
8
+ end
9
+ @request = request
10
+ end
11
+
12
+ def full_sql
13
+ _sql = sql.to_s.gsub(/^[\s]*/,'')
14
+ if value.present?
15
+ _sql << "\n\nParameters: #{value}"
16
+ end
17
+ _sql
18
+ end
19
+
20
+ def request
21
+ @request ||= SimpleApm::Request.find(request_id)
22
+ end
23
+
24
+ class << self
25
+ # @return [Array<SimpleApm::Sql>]
26
+ def find_by_request_id(request_id)
27
+ SimpleApm::Redis.lrange(key(request_id), 0, -1).map{|x|SimpleApm::Sql.new JSON.parse(x)}
28
+ end
29
+
30
+ def delete_by_request_id(request_id)
31
+ SimpleApm::Redis.del(key(request_id))
32
+ end
33
+
34
+ def create(request_id, info)
35
+ SimpleApm::Redis.rpush(key(request_id), info.to_json)
36
+ end
37
+
38
+ def key(request_id)
39
+ SimpleApm::RedisKey["sql:#{request_id}"]
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,61 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Simple Apm</title>
5
+ <meta charset="utf-8">
6
+ <meta content="IE=Edge,chrome=1" http-equiv="X-UA-Compatible">
7
+ <meta content="width=device-width, initial-scale=1.0" name="viewport">
8
+ <link rel="stylesheet" href="//cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
9
+ <link rel="stylesheet" href="//cdn.datatables.net/1.10.15/css/jquery.dataTables.min.css">
10
+ <script src="//cdn.bootcss.com/jquery/1.12.4/jquery.min.js"></script>
11
+ <script src="//cdn.bootcss.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
12
+ <script src="//cdn.bootcss.com/echarts/4.1.0/echarts.js"></script>
13
+ <script src="//cdn.datatables.net/1.10.15/js/jquery.dataTables.min.js"></script>
14
+ <%= stylesheet_link_tag "simple_apm/application", media: "all" %>
15
+ <%= javascript_include_tag "simple_apm/application" %>
16
+ <%= csrf_meta_tags %>
17
+ </head>
18
+ <body>
19
+ <div id="page-container">
20
+ <nav class="navbar navbar-default navbar-fixed-top">
21
+ <div class="container">
22
+ <div class="navbar-header">
23
+ <% [[dashboard_path, 'Dashboard'], [index_path, '慢事务列表'], [actions_path, 'ActionList']].each do |m| %>
24
+ <a class="navbar-brand <%= 'active' if request.url=~/#{m[0]}/ %>" href="<%= m[0] %>" ><%= m[1] %></a>
25
+ <% end %>
26
+ </div>
27
+ <div class="select-apm-date">
28
+ 统计日期
29
+ <%= select_tag 'apm_date', options_for_select(SimpleApm::Redis.in_apm_days, apm_date), class: 'form-control'%>
30
+ </div>
31
+
32
+ </div>
33
+ </nav>
34
+ <div class="container" style="margin-top: 55px">
35
+ <%= yield %>
36
+ </div>
37
+ </div>
38
+ <footer style="text-align: center">
39
+ <% SimpleApm::Redis.simple_info.each do |k, v| %>
40
+ <label><%= k %>:</label><span><%= v %></span>
41
+ <% end %>
42
+ </footer>
43
+ <div id="sql-modal" class="modal" aria-hidden="true" role="dialog" style="display: none;" tabindex="-1">
44
+ <div class="modal-dialog modal-lg modal-dialog-popout">
45
+ <pre class="modal-content" data-lang="SQL">
46
+ </pre>
47
+ </div>
48
+ </div>
49
+
50
+ </body>
51
+ <script>
52
+ var sql_modal_obj = $('#sql-modal')
53
+ $('.select-apm-date select').on('change',function(){
54
+ window.location.href = '<%= set_apm_date_path %>?apm_date='+this.value
55
+ })
56
+ $('.sql').on('click', function(){
57
+ sql_modal_obj.find('.modal-content').html($(this).html())
58
+ sql_modal_obj.modal('show')
59
+ })
60
+ </script>
61
+ </html>
@@ -0,0 +1,41 @@
1
+ <h4>请求概况</h4>
2
+ <h5><%= @action.name%></h5>
3
+ <p>
4
+ <label>当日点击次数:</label>
5
+ <span><%= @action.click_count %></span>
6
+ </p>
7
+ <p>
8
+ <label>平均响应时间:</label>
9
+ <span><%= sec_str @action.time.to_f/@action.click_count.to_i %></span>
10
+ </p>
11
+ <p>
12
+ <label>最慢响应时间:</label>
13
+ <span><%= link_to sec_str(@action.slow_time), show_path(id: @action.slow_id) %></span>
14
+ </p>
15
+ <p>
16
+ <label>最快响应时间:</label>
17
+ <span><%= link_to sec_str(@action.fast_time), show_path(id: @action.fast_id) %></span>
18
+ </p>
19
+ <table class="table table-bordered">
20
+ <tr>
21
+ <th>响应时间</th>
22
+ <th>访问时间</th>
23
+ <th>请求id</th>
24
+ <th>访问ip</th>
25
+ <th>server</th>
26
+ </tr>
27
+ <% @action.slow_requests.each do |slow_request| %>
28
+ <% r = slow_request.request %>
29
+ <tr>
30
+ <td>
31
+ <%= sec_str r.during %>
32
+ (DB: <%= sec_str r.db_runtime %> , View: <%= sec_str r.view_runtime %>)
33
+ </td>
34
+ <td><%= time_label r.started %></td>
35
+ <td><%= link_to r.request_id, show_path(id: r.request_id) %></td>
36
+ <td><%= r.remote_addr %></td>
37
+ <td><%= r.host %></td>
38
+ </tr>
39
+ <% end %>
40
+ </table>
41
+
@@ -0,0 +1,30 @@
1
+ <table id="actions-table" class="table">
2
+ <thead>
3
+ <tr>
4
+ <th>Name</th>
5
+ <th>点击次数</th>
6
+ <th>平均响应时间</th>
7
+ <th>最快响应时间</th>
8
+ <th>最慢响应时间</th>
9
+ </tr>
10
+ </thead>
11
+ <tbody>
12
+ <% @actions.each do |action| %>
13
+ <tr>
14
+ <td><%= link_to action.name, action_info_path(action_name: action.name) %></td>
15
+ <td><%= action.click_count %></td>
16
+ <td><%= sec_str action.avg_time, 's' %></td>
17
+ <td><%= link_to sec_str(action.fast_time, 's'), show_path(id: action.fast_id) %></td>
18
+ <td><%= link_to sec_str(action.slow_time, 's'), show_path(id: action.slow_id) %></td>
19
+ </tr>
20
+ <% end %>
21
+ </tbody>
22
+ </table>
23
+ <script type="text/javascript">
24
+ $(document).ready(function(){
25
+ $('#actions-table').DataTable({
26
+ iDisplayLength: 25
27
+ })
28
+
29
+ })
30
+ </script>
@@ -0,0 +1,35 @@
1
+ <div id="time_chart" style="height: 400px;width: auto"></div>
2
+ <div id="hits_chart" style="height: 400px;width: auto"></div>
3
+ <script>
4
+ var time_chart = echarts.init(document.getElementById('time_chart')),
5
+ hits_chart = echarts.init(document.getElementById('hits_chart'));
6
+ time_chart.setOption({
7
+ title: {
8
+ text: '响应时间'
9
+ },
10
+ tooltip: {},
11
+ xAxis: {
12
+ data: <%= @x_names.to_json.html_safe %>
13
+ },
14
+ yAxis: {type: 'value'},
15
+ series: [{
16
+ type: 'line',
17
+ data: <%= @time_arr.to_json.html_safe %>
18
+ }]
19
+ })
20
+
21
+ hits_chart.setOption({
22
+ title: {
23
+ text: '访问次数'
24
+ },
25
+ tooltip: {},
26
+ xAxis: {
27
+ data: <%= @x_names.to_json.html_safe %>
28
+ },
29
+ yAxis: {type: 'value'},
30
+ series: [{
31
+ type: 'line',
32
+ data: <%= @hits_arr.to_json.html_safe %>
33
+ }]
34
+ })
35
+ </script>
@@ -0,0 +1,38 @@
1
+ <table id="slow-requests" class="table">
2
+ <thead>
3
+ <tr>
4
+ <th>请求时间</th>
5
+ <th>action</th>
6
+ <th>总耗时</th>
7
+ <th>db_time</th>
8
+ <th>view time</th>
9
+ <th>host</th>
10
+ <th>remote_addr</th>
11
+ </tr>
12
+ </thead>
13
+ <%# @slow_requests.each do |r| %>
14
+ <!-- <tr>-->
15
+ <%# request = r.request %>
16
+ <!-- <td>-->
17
+ <%#= link_to time_label(request.started), show_path(id: request.request_id)%>
18
+ <!-- </td>-->
19
+ <!-- <td><%#= link_to request.action_name, action_info_path(action_name: request.action_name)%></td>-->
20
+ <!-- <td><%#= sec_str request.during %></td>-->
21
+ <!-- <td><%#= sec_str request.db_runtime %></td>-->
22
+ <!-- <td><%#= sec_str request.view_runtime%></td>-->
23
+ <!-- <td><%#= request.host %></td>-->
24
+ <!-- <td><%#= request.remote_addr %></td>-->
25
+ <!-- </tr>-->
26
+ <%# end %>
27
+ </table>
28
+
29
+ <script type="text/javascript">
30
+ $(document).ready(function () {
31
+ $('#slow-requests').DataTable({
32
+ ajax: '<%= index_path %>.json',
33
+ iDisplayLength: 25,
34
+ ordering: false
35
+ })
36
+
37
+ })
38
+ </script>
@@ -0,0 +1,61 @@
1
+ <h4><%= @request.action_name %>.<%= @request.format %></h4>
2
+ <!-- @host="xyy", @remote_addr="127.0.0.1", @method="GET", @format="html", @exception="null"-->
3
+ <p>
4
+ <label>开始时间:</label>
5
+ <span><%= time_label @request.started, true %></span>
6
+ </p>
7
+ <p>
8
+ <label>响应时间:</label>
9
+ <span>
10
+ <%= sec_str @request.during %>
11
+ (数据库:<%= sec_str @request.db_runtime %>, View: <%= sec_str @request.view_runtime %>)
12
+ </span>
13
+ </p>
14
+ <p>
15
+ <label>server:</label>
16
+ <span><%= @request.host %></span>
17
+ </p>
18
+ <p>
19
+ <label>访问ip:</label>
20
+ <span><%= @request.remote_addr %></span>
21
+ </p>
22
+ <% if @request.exception.present? && @request.exception != 'null' %>
23
+ <p>
24
+ <label>报错信息: </label>
25
+ <pre><%= @request.exception %></pre>
26
+ </p>
27
+ <% end %>
28
+ <table id="sql-table" class="table">
29
+ <thead>
30
+ <tr>
31
+ <td>time</td>
32
+ <td>name</td>
33
+ <td>sql</td>
34
+ <td>时间</td>
35
+ <td>位置</td>
36
+ </tr>
37
+ </thead>
38
+ <tbody>
39
+ <% @request.sqls.each do |sql| %>
40
+ <tr>
41
+ <td><%= time_label sql.started %></td>
42
+ <td>
43
+ <%= sql.name %>
44
+ </td>
45
+ <td>
46
+ <div class="sql"><%= sql.full_sql -%></div>
47
+ </td>
48
+ <td><%= sec_str sql.during %></td>
49
+ <td><%= sql.filename %>:<%= sql.line %></td>
50
+ </tr>
51
+ <% end %>
52
+ </tbody>
53
+ </table>
54
+ <script type="text/javascript">
55
+ $(document).ready(function(){
56
+ $('#sql-table').DataTable({
57
+ bPaginate: false
58
+ })
59
+
60
+ })
61
+ </script>
@@ -0,0 +1,9 @@
1
+ SimpleApm::Engine.routes.draw do
2
+ get 'dashboard', to: 'apm#dashboard'
3
+ get 'index', to: 'apm#index'
4
+ get 'show', to: 'apm#show'
5
+ get 'action_info', to: 'apm#action_info'
6
+ get 'actions', to: 'apm#actions'
7
+ get 'set_apm_date', to: 'apm#set_apm_date'
8
+
9
+ end
@@ -0,0 +1,65 @@
1
+ require "simple_apm/setting"
2
+ require "simple_apm/redis"
3
+ require "simple_apm/engine"
4
+
5
+ module SimpleApm
6
+ ActiveSupport::Notifications.subscribe('process_action.action_controller') do |name, started, finished, unique_id, payload|
7
+ request_id = Thread.current['action_dispatch.request_id']
8
+ if request_id.present?
9
+ action_name = "#{payload[:controller]}##{payload[:action]}" #payload[:format]
10
+ info = {
11
+ request_id: request_id,
12
+ action_name: action_name,
13
+ during: finished - started,
14
+ started: started.to_s,
15
+ db_runtime: payload[:db_runtime].to_f/1000,
16
+ view_runtime: payload[:view_runtime].to_f/1000,
17
+ controller: payload[:controller],
18
+ action: payload[:action],
19
+ host: Socket.gethostname,
20
+ remote_addr: payload[:headers]['REMOTE_ADDR'],
21
+ method: payload[:method],
22
+ format: payload[:format],
23
+ exception: payload[:exception].presence.to_json
24
+ }.with_indifferent_access
25
+ info[:status] = '500' if payload[:exception]
26
+ # 存储
27
+ in_slow = SimpleApm::SlowRequest.update_by_request info
28
+ in_action_info = SimpleApm::Action.update_by_request info
29
+ SimpleApm::Hit.update_by_request info
30
+ if in_action_info || in_slow
31
+ SimpleApm::Request.create info
32
+ else
33
+ SimpleApm::Sql.delete_by_request_id(request_id)
34
+ end
35
+ end
36
+ # rescue => e
37
+ # Logger.new("#{Rails.root}/log/simple_apm.log").info e.inspect
38
+ end
39
+
40
+ ActiveSupport::Notifications.subscribe 'sql.active_record' do |name, started, finished, unique_id, payload|
41
+ request_id = Thread.current['action_dispatch.request_id'].presence||Thread.main['action_dispatch.request_id']
42
+ if request_id.present?
43
+ dev_caller = caller.detect { |c| c.include? Rails.root.to_s }
44
+ if dev_caller
45
+ c = Callsite.parse(dev_caller)
46
+ payload.merge!(:line => c.line, :filename => c.filename.to_s.gsub(Rails.root.to_s,''), :method => c.method)
47
+ end
48
+ # ActiveRecord::Relation::QueryAttribute
49
+ sql_value = payload[:binds].map{|q|[q.name, q.value]}
50
+ info = {
51
+ request_id: request_id,
52
+ name: payload[:name],
53
+ during: finished - started,
54
+ started: started,
55
+ sql: payload[:sql],
56
+ value: sql_value,
57
+ filename: payload[:filename],
58
+ line: payload[:line]
59
+ }.with_indifferent_access
60
+ SimpleApm::Sql.create request_id, info
61
+ end
62
+ # rescue => e
63
+ # Logger.new("#{Rails.root}/log/simple_apm.log").info e.inspect
64
+ end
65
+ end
@@ -0,0 +1,18 @@
1
+ module SimpleApm
2
+ class Rack
3
+ def initialize(app)
4
+ @app = app
5
+ end
6
+
7
+ def call(env)
8
+ Thread.current['action_dispatch.request_id'] = env['action_dispatch.request_id']
9
+ @app.call(env)
10
+ end
11
+ end
12
+
13
+ class Engine < ::Rails::Engine
14
+ isolate_namespace SimpleApm
15
+ config.app_middleware.use SimpleApm::Rack
16
+ end
17
+
18
+ end
@@ -0,0 +1,63 @@
1
+ # 删除 所有
2
+ # redis-cli keys "simple_apm:*" | xargs redis-cli del
3
+ module SimpleApm
4
+ class Redis
5
+ class << self
6
+ def instance
7
+ @current ||= ::Redis::Namespace.new(
8
+ :simple_apm,
9
+ :redis => ::Redis.new(
10
+ url: SimpleApm::Setting::REDIS_URL,
11
+ driver: SimpleApm::Setting::REDIS_DRIVER
12
+ )
13
+ )
14
+ end
15
+
16
+ # http://redisdoc.com/server/info.html
17
+ def simple_info
18
+ h = {}
19
+ redis.info.each do |k, v|
20
+ if k == 'total_system_memory_human'
21
+ h['系统内存'] = v
22
+ elsif k == 'used_memory_rss_human'
23
+ h['当前内存占用(rss)'] = v
24
+ elsif k == 'used_memory_peak_human'
25
+ h['占用内存峰值'] = v
26
+ elsif k == 'redis_version'
27
+ h['redis版本'] = v
28
+ elsif k =~ /db[0-9]+/
29
+ h[k] = v
30
+ end
31
+ end
32
+ h
33
+ end
34
+
35
+ # 所有统计的日期,通过hits来判断
36
+ def in_apm_days
37
+ SimpleApm::Redis.keys('*:action-names').map{|x|x.split(':').first}.sort
38
+ end
39
+
40
+ def method_missing(method, *args)
41
+ instance.send(method, *args)
42
+ rescue NoMethodError
43
+ super(method, *args)
44
+ end
45
+ end
46
+ end
47
+
48
+ class RedisKey
49
+ class << self
50
+ def query_date=(d = nil)
51
+ Thread.current['apm_query_date'] = d
52
+ end
53
+
54
+ def query_date
55
+ Thread.current['apm_query_date'] || Time.now.strftime('%Y-%m-%d')
56
+ end
57
+
58
+ def [](key, _date = nil)
59
+ "#{_date||query_date}:#{key}"
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,12 @@
1
+ module SimpleApm
2
+ class Setting
3
+ ApmSettings = YAML.load(IO.read(Dir.join(Rails.root, 'configs', 'simple_apm.yml'))) rescue {}
4
+ REDIS_URL = ApmSettings['redis_url'].presence || 'redis://localhost:6379/0'
5
+ # nil , hiredis ...
6
+ REDIS_DRIVER = ApmSettings['redis_driver']
7
+ # 最慢的请求数存储量
8
+ SLOW_ACTIONS_LIMIT = ApmSettings['slow_actions_limit'].presence || 1000
9
+ # 每个action存最慢的请求量
10
+ ACTION_SLOW_REQUEST_LIMIT = ApmSettings['action_slow_request_limit'].presence || 100
11
+ end
12
+ end
@@ -0,0 +1,3 @@
1
+ module SimpleApm
2
+ VERSION = '0.1.0'
3
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :simple_apm do
3
+ # # Task goes here
4
+ # end
metadata ADDED
@@ -0,0 +1,116 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: simple_apm
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - yuanyin.xia
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-06-01 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '5.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '5.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: redis
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '4.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '4.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: redis-namespace
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.5'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.5'
55
+ description: 'xyy: Simple Apm View for rails using redis.'
56
+ email:
57
+ - 454536909@qq.com
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - MIT-LICENSE
63
+ - README.md
64
+ - Rakefile
65
+ - app/assets/config/simple_apm_manifest.js
66
+ - app/assets/javascripts/simple_apm/application.js
67
+ - app/assets/stylesheets/simple_apm/application.css
68
+ - app/controllers/simple_apm/apm_controller.rb
69
+ - app/controllers/simple_apm/application_controller.rb
70
+ - app/helpers/simple_apm/application_helper.rb
71
+ - app/jobs/simple_apm/application_job.rb
72
+ - app/mailers/simple_apm/application_mailer.rb
73
+ - app/models/simple_apm/action.rb
74
+ - app/models/simple_apm/application_record.rb
75
+ - app/models/simple_apm/hit.rb
76
+ - app/models/simple_apm/request.rb
77
+ - app/models/simple_apm/slow_request.rb
78
+ - app/models/simple_apm/sql.rb
79
+ - app/views/layouts/simple_apm/application.html.erb
80
+ - app/views/simple_apm/apm/action_info.html.erb
81
+ - app/views/simple_apm/apm/actions.html.erb
82
+ - app/views/simple_apm/apm/dashboard.html.erb
83
+ - app/views/simple_apm/apm/index.html.erb
84
+ - app/views/simple_apm/apm/show.html.erb
85
+ - config/routes.rb
86
+ - lib/simple_apm.rb
87
+ - lib/simple_apm/engine.rb
88
+ - lib/simple_apm/redis.rb
89
+ - lib/simple_apm/setting.rb
90
+ - lib/simple_apm/version.rb
91
+ - lib/tasks/simple_apm_tasks.rake
92
+ homepage: https://github.com/xiayuanyin/simple_apm
93
+ licenses:
94
+ - MIT
95
+ metadata: {}
96
+ post_install_message:
97
+ rdoc_options: []
98
+ require_paths:
99
+ - lib
100
+ required_ruby_version: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - ">="
103
+ - !ruby/object:Gem::Version
104
+ version: '0'
105
+ required_rubygems_version: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ requirements: []
111
+ rubyforge_project:
112
+ rubygems_version: 2.7.3
113
+ signing_key:
114
+ specification_version: 4
115
+ summary: 'xyy: Simple Rails Apm'
116
+ test_files: []