simple_apm 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []