canvas_statsd 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 13ba02ddc541f1ef3eebb7aee35f60022eea1e24
4
+ data.tar.gz: 55456a369e6654ca2320832c9478971644107774
5
+ SHA512:
6
+ metadata.gz: 6b5af0a665e04b9b1979c65fbbb65be875e5e5fa12aec4fbf7dfa0f4f4cc655ea8514f20d88369078204450efc5cf247a6025cf7275be63f05709d5839c13113
7
+ data.tar.gz: dd77f76a3c1be35b3c0e9af4cee153e399c70d98331328c31d4a0557191578dd3d1d35d698e3adbaabd95b9418f0cb619baf1677b28affb81e0fb1582443affb
@@ -0,0 +1,37 @@
1
+ require 'statsd'
2
+ require "aroi"
3
+
4
+ module CanvasStatsd
5
+
6
+ require "canvas_statsd/statsd"
7
+ require "canvas_statsd/request_stat"
8
+ require "canvas_statsd/counter"
9
+ require "canvas_statsd/sql_tracker"
10
+ require "canvas_statsd/default_tracking"
11
+
12
+ def self.settings
13
+ @settings || env_settings
14
+ end
15
+
16
+ def self.settings=(value)
17
+ @settings = value
18
+ end
19
+
20
+ def self.env_settings(env=ENV)
21
+ config = {
22
+ host: env.fetch('CANVAS_STATSD_HOST', nil),
23
+ port: env.fetch('CANVAS_STATSD_PORT', nil),
24
+ namespace: env.fetch('CANVAS_STATSD_NAMESPACE', nil),
25
+ append_hostname: env.fetch('CANVAS_STATSD_APPEND_HOSTNAME', nil),
26
+ }
27
+ config.delete_if {|k,v| v.nil?}
28
+ config[:append_hostname] = false if config[:append_hostname] == 'false';
29
+ config[:append_hostname] = true if config[:append_hostname] == 'true';
30
+ config[:host] ? config : {}
31
+ end
32
+
33
+ def self.track_default_metrics options={}
34
+ CanvasStatsd::DefaultTracking.track_default_metrics options
35
+ end
36
+
37
+ end
@@ -0,0 +1,35 @@
1
+ module CanvasStatsd
2
+ class Counter
3
+
4
+ attr_reader :key
5
+ attr_reader :blocked_names
6
+
7
+ def initialize(key, blocked_names=[])
8
+ @blocked_names = blocked_names
9
+ @key = key
10
+ end
11
+
12
+ def start
13
+ Thread.current[key] = 0
14
+ end
15
+
16
+ def track(name)
17
+ Thread.current[key] += 1 if Thread.current[key] && accepted_name?(name)
18
+ end
19
+
20
+ def finalize_count
21
+ final_count = count
22
+ Thread.current[key] = 0
23
+ final_count
24
+ end
25
+
26
+ def count
27
+ Thread.current[key]
28
+ end
29
+
30
+ def accepted_name?(name)
31
+ !blocked_names.include?(name)
32
+ end
33
+
34
+ end
35
+ end
@@ -0,0 +1,63 @@
1
+ module CanvasStatsd
2
+ class DefaultTracking
3
+
4
+ @ar_counter = CanvasStatsd::Counter.new('ar_counter')
5
+ @sql_tracker = CanvasStatsd::SqlTracker.new(blocked_names: ['SCHEMA'])
6
+
7
+ def self.track_default_metrics(options={})
8
+ options = {sql: true, active_record: true}.merge(options)
9
+ track_timing
10
+ track_sql if !!options[:sql]
11
+ track_active_record if !!options[:active_record]
12
+ end
13
+
14
+ def self.subscribe type, &block
15
+ ActiveSupport::Notifications.subscribe type, &block
16
+ end
17
+
18
+ private
19
+
20
+ def self.instrument_active_record_creation
21
+ ::Aroi::Instrumentation.instrument_creation!
22
+ end
23
+
24
+ def self.track_timing
25
+ subscribe(/start_processing.action_controller/) {|*args| start_processing(*args)}
26
+ subscribe(/process_action.action_controller/) {|*args| finalize_processing(*args)}
27
+ end
28
+
29
+ def self.track_sql
30
+ @tracking_sql = true
31
+ subscribe(/sql.active_record/) {|*args| update_sql_count(*args)}
32
+ end
33
+
34
+ def self.track_active_record
35
+ instrument_active_record_creation
36
+ @tracking_active_record = true
37
+ subscribe(/instance.active_record/) {|*args| update_active_record_count(*args)}
38
+ end
39
+
40
+ def self.start_processing *args
41
+ @sql_tracker.start
42
+ @ar_counter.start
43
+ end
44
+
45
+ def self.update_sql_count name, start, finish, id, payload
46
+ @sql_tracker.track payload.fetch(:name), payload.fetch(:sql)
47
+ end
48
+
49
+ def self.update_active_record_count name, start, finish, id, payload
50
+ @ar_counter.track payload.fetch(:name, '')
51
+ end
52
+
53
+ def self.finalize_processing *args
54
+ request_stat = CanvasStatsd::RequestStat.new(*args)
55
+ request_stat.ar_count = @ar_counter.finalize_count if @tracking_active_record
56
+ request_stat.sql_read_count = @sql_tracker.num_reads if @tracking_sql
57
+ request_stat.sql_write_count = @sql_tracker.num_writes if @tracking_sql
58
+ request_stat.sql_cache_count = @sql_tracker.num_caches if @tracking_sql
59
+ request_stat.report
60
+ end
61
+
62
+ end
63
+ end
@@ -0,0 +1,55 @@
1
+ module CanvasStatsd
2
+ class RequestStat
3
+
4
+ attr_accessor :sql_read_count
5
+ attr_accessor :sql_write_count
6
+ attr_accessor :sql_cache_count
7
+ attr_accessor :ar_count
8
+
9
+ def initialize(name, start, finish, id, payload, statsd=CanvasStatsd::Statsd)
10
+ @name = name
11
+ @start = start
12
+ @finish = finish
13
+ @id = id
14
+ @payload = payload
15
+ @statsd = statsd
16
+ end
17
+
18
+ def report
19
+ if controller && action
20
+ common_key = "request.#{controller}.#{action}"
21
+ @statsd.timing("#{common_key}.total", ms)
22
+ @statsd.timing("#{common_key}.view", view_runtime) if view_runtime
23
+ @statsd.timing("#{common_key}.db", db_runtime) if db_runtime
24
+ @statsd.timing("#{common_key}.sql.read", sql_read_count) if sql_read_count
25
+ @statsd.timing("#{common_key}.sql.write", sql_write_count) if sql_write_count
26
+ @statsd.timing("#{common_key}.sql.cache", sql_cache_count) if sql_cache_count
27
+ @statsd.timing("#{common_key}.active_record", ar_count) if ar_count
28
+ end
29
+ end
30
+
31
+ def db_runtime
32
+ @payload.fetch(:db_runtime, nil)
33
+ end
34
+
35
+ def view_runtime
36
+ @payload.fetch(:view_runtime, nil)
37
+ end
38
+
39
+ def controller
40
+ @payload.fetch(:params, {})['controller']
41
+ end
42
+
43
+ def action
44
+ @payload.fetch(:params, {})['action']
45
+ end
46
+
47
+ def ms
48
+ if (!@finish || !@start)
49
+ return 0
50
+ end
51
+ (@finish - @start) * 1000
52
+ end
53
+
54
+ end
55
+ end
@@ -0,0 +1,54 @@
1
+ module CanvasStatsd
2
+ class SqlTracker
3
+
4
+ attr_reader :blocked_names, :read_counts, :write_counts, :cache_counts
5
+
6
+ def initialize(opts=nil)
7
+ opts ||= {}
8
+ @blocked_names = opts.fetch(:blocked_names, [])
9
+ @read_counts = opts.fetch(:read_counter, CanvasStatsd::Counter.new('sql_read_counter'))
10
+ @write_counts = opts.fetch(:write_counter, CanvasStatsd::Counter.new('sql_write_counter'))
11
+ @cache_counts = opts.fetch(:cache_counter, CanvasStatsd::Counter.new('sql_cache_counter'))
12
+ end
13
+
14
+ def start
15
+ [read_counts, write_counts, cache_counts].each(&:start)
16
+ end
17
+
18
+ def track name, sql
19
+ return unless sql && accepted_name?(name)
20
+
21
+ if name.match(/CACHE/)
22
+ cache_counts.track name
23
+ elsif truncate(sql).match(/SELECT/) || name.match(/LOAD/)
24
+ read_counts.track(sql)
25
+ else
26
+ write_counts.track(sql)
27
+ end
28
+ end
29
+
30
+ def num_reads
31
+ read_counts.finalize_count
32
+ end
33
+
34
+ def num_writes
35
+ write_counts.finalize_count
36
+ end
37
+
38
+ def num_caches
39
+ cache_counts.finalize_count
40
+ end
41
+
42
+ private
43
+
44
+ def accepted_name?(name)
45
+ !!(name && !blocked_names.include?(name))
46
+ end
47
+
48
+ def truncate(sql, length=15)
49
+ sql ||= ''
50
+ sql.strip[0..length]
51
+ end
52
+
53
+ end
54
+ end
@@ -0,0 +1,104 @@
1
+ #
2
+ # Copyright (C) 2012 Instructure, Inc.
3
+ #
4
+ # This file is part of Canvas.
5
+ #
6
+ # Canvas is free software: you can redistribute it and/or modify it under
7
+ # the terms of the GNU Affero General Public License as published by the Free
8
+ # Software Foundation, version 3 of the License.
9
+ #
10
+ # Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
11
+ # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
12
+ # A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
13
+ # details.
14
+ #
15
+ # You should have received a copy of the GNU Affero General Public License along
16
+ # with this program. If not, see <http://www.gnu.org/licenses/>.
17
+ #
18
+
19
+ # Proxy class to communicate messages to statsd
20
+ # Available statsd messages are described in:
21
+ # https://github.com/etsy/statsd/blob/master/README.md
22
+ # https://github.com/reinh/statsd/blob/master/lib/statsd.rb
23
+ #
24
+ # So for instance:
25
+ # ms = Benchmark.ms { ..code.. }
26
+ # CanvasStatsd::Statsd.timing("my_stat", ms)
27
+ #
28
+ # Configured in config/statsd.yml, see config/statsd.yml.example
29
+ # At least a host needs to be defined for the environment, all other config is optional
30
+ #
31
+ # If a namespace is defined in statsd.yml, it'll be prepended to the stat name.
32
+ # The hostname of the server will be appended to the stat name, unless `append_hostname: false` is specified in the config.
33
+ # So if the namespace is "canvas" and the hostname is "app01", the final stat name of "my_stat" would be "stats.canvas.my_stat.app01"
34
+ # (assuming the default statsd/graphite configuration)
35
+ #
36
+ # If statsd isn't configured and enabled, then calls to CanvasStatsd::Statsd.* will do nothing and return nil
37
+
38
+ module CanvasStatsd
39
+ module Statsd
40
+ # replace "." in key names with another character to avoid creating spurious sub-folders in graphite
41
+ def self.escape(str, replacement = '_')
42
+ str.gsub('.', replacement)
43
+ end
44
+
45
+ def self.hostname
46
+ @hostname ||= Socket.gethostname.split(".").first
47
+ end
48
+
49
+ %w(increment decrement count gauge timing).each do |method|
50
+ class_eval <<-RUBY, __FILE__, __LINE__+1
51
+ def self.#{method}(stat, *args)
52
+ if self.instance
53
+ if Array === stat
54
+ stat.each do |st|
55
+ self.#{method}(st, *args)
56
+ end
57
+ return
58
+ end
59
+
60
+ if self.append_hostname?
61
+ stat_name = "\#{stat}.\#{hostname}"
62
+ else
63
+ stat_name = stat.to_s
64
+ end
65
+ self.instance.#{method}(stat_name, *args)
66
+ else
67
+ nil
68
+ end
69
+ end
70
+ RUBY
71
+ end
72
+
73
+ def self.time(stat, sample_rate=1)
74
+ start = Time.now
75
+ result = yield
76
+ self.timing(stat, ((Time.now - start) * 1000).round, sample_rate)
77
+ result
78
+ end
79
+
80
+ def self.instance
81
+ if !defined?(@statsd)
82
+ statsd_settings = CanvasStatsd.settings
83
+
84
+ if statsd_settings && statsd_settings[:host]
85
+ @statsd = ::Statsd.new(statsd_settings[:host])
86
+ @statsd.port = statsd_settings[:port] if statsd_settings[:port]
87
+ @statsd.namespace = statsd_settings[:namespace] if statsd_settings[:namespace]
88
+ @append_hostname = !statsd_settings.key?(:append_hostname) || !!statsd_settings[:append_hostname]
89
+ else
90
+ @statsd = nil
91
+ end
92
+ end
93
+ @statsd
94
+ end
95
+
96
+ def self.append_hostname?
97
+ @append_hostname
98
+ end
99
+
100
+ def self.reset_instance
101
+ remove_instance_variable(:@statsd) if defined?(@statsd)
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,125 @@
1
+ #
2
+ # Copyright (C) 2014 Instructure, Inc.
3
+ #
4
+ # This file is part of Canvas.
5
+ #
6
+ # Canvas is free software: you can redistribute it and/or modify it under
7
+ # the terms of the GNU Affero General Public License as published by the Free
8
+ # Software Foundation, version 3 of the License.
9
+ #
10
+ # Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
11
+ # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
12
+ # A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
13
+ # details.
14
+ #
15
+ # You should have received a copy of the GNU Affero General Public License along
16
+ # with this program. If not, see <http://www.gnu.org/licenses/>.
17
+ #
18
+
19
+ require 'spec_helper'
20
+
21
+ describe CanvasStatsd do
22
+ before(:each) do
23
+ CanvasStatsd.settings = nil
24
+ end
25
+
26
+ after(:each) do
27
+ [
28
+ 'CANVAS_STATSD_HOST',
29
+ 'CANVAS_STATSD_NAMESPACE',
30
+ 'CANVAS_STATSD_PORT',
31
+ 'CANVAS_STATSD_APPEND_HOST_NAME',
32
+ ].each {|k| ENV.delete k}
33
+ end
34
+
35
+ describe ".settings" do
36
+ it "have default settings" do
37
+ expect(CanvasStatsd.settings).to eq({})
38
+ end
39
+
40
+ it "can be assigned a new value" do
41
+ settings = {foo: 'bar', baz: 'apple'}
42
+ CanvasStatsd.settings = settings
43
+
44
+ expect(CanvasStatsd.settings).to eq settings
45
+ end
46
+
47
+ it 'pulls from ENV if not already set' do
48
+ ENV['CANVAS_STATSD_HOST'] = 'statsd.example.org'
49
+ ENV['CANVAS_STATSD_NAMESPACE'] = 'canvas'
50
+
51
+ expected = {
52
+ host: 'statsd.example.org',
53
+ namespace: 'canvas',
54
+ }
55
+ expect(CanvasStatsd.settings).to eq(expected)
56
+
57
+ end
58
+
59
+ it 'configured settings take precedence over ENV settings' do
60
+ ENV['CANVAS_STATSD_HOST'] = 'statsd.example.org'
61
+ ENV['CANVAS_STATSD_NAMESPACE'] = 'canvas'
62
+
63
+ settings = {foo: 'bar', baz: 'apple'}
64
+ CanvasStatsd.settings = settings
65
+ expect(CanvasStatsd.settings).to eq settings
66
+ end
67
+ end
68
+
69
+ describe ".env_settings" do
70
+ it 'returns empty hash when no CANVAS_STATSD_HOST found' do
71
+ env = {
72
+ 'CANVAS_STATSD_NAMESPACE' => 'canvas'
73
+ }
74
+ expect(CanvasStatsd.env_settings(env)).to eq({})
75
+ end
76
+
77
+ it 'builds settings hash from environment vars' do
78
+ env = {
79
+ 'CANVAS_STATSD_HOST' => 'statsd.example.org',
80
+ 'CANVAS_STATSD_NAMESPACE' => 'canvas',
81
+ }
82
+ expected = {
83
+ host: 'statsd.example.org',
84
+ namespace: 'canvas',
85
+ }
86
+ expect(CanvasStatsd.env_settings(env)).to eq(expected)
87
+ end
88
+
89
+ it 'uses ENV if env argument hash not passed' do
90
+ ENV['CANVAS_STATSD_HOST'] = 'statsd.example.org'
91
+ ENV['CANVAS_STATSD_NAMESPACE'] = 'canvas'
92
+
93
+ expected = {
94
+ host: 'statsd.example.org',
95
+ namespace: 'canvas',
96
+ }
97
+ expect(CanvasStatsd.env_settings).to eq(expected)
98
+ end
99
+
100
+ it 'converts env append_hostname "false" to boolean' do
101
+ env = {
102
+ 'CANVAS_STATSD_HOST' => 'statsd.example.org',
103
+ 'CANVAS_STATSD_APPEND_HOSTNAME' => 'false',
104
+ }
105
+ expected = {
106
+ host: 'statsd.example.org',
107
+ append_hostname: false,
108
+ }
109
+ expect(CanvasStatsd.env_settings(env)).to eq(expected)
110
+ end
111
+
112
+ it 'converts env append_hostname "true" to boolean' do
113
+ env = {
114
+ 'CANVAS_STATSD_HOST' => 'statsd.example.org',
115
+ 'CANVAS_STATSD_APPEND_HOSTNAME' => 'true',
116
+ }
117
+ expected = {
118
+ host: 'statsd.example.org',
119
+ append_hostname: true,
120
+ }
121
+ expect(CanvasStatsd.env_settings(env)).to eq(expected)
122
+ end
123
+ end
124
+
125
+ end
@@ -0,0 +1,63 @@
1
+ require 'spec_helper'
2
+
3
+ describe CanvasStatsd::Counter do
4
+
5
+ let(:subject) { CanvasStatsd::Counter.new('test', ['foo']) }
6
+
7
+ describe "#accepted_name?" do
8
+ it 'should return true for names not in blocked_names' do
9
+ expect(subject.accepted_name?('bar')).to eq true
10
+ end
11
+
12
+ it 'should return false for names in blocked_names' do
13
+ expect(subject.accepted_name?('foo')).to eq false
14
+ end
15
+
16
+ it 'should return true for empty string names' do
17
+ expect(subject.accepted_name?('')).to eq true
18
+ end
19
+
20
+ it 'should return true for empty nil names' do
21
+ expect(subject.accepted_name?(nil)).to eq true
22
+ end
23
+ end
24
+
25
+ describe "#start" do
26
+ it 'should reset count to zero' do
27
+ subject.start
28
+ expect(subject.count).to eq 0
29
+ end
30
+ end
31
+
32
+ describe "#track" do
33
+ it 'should increment when given allowed names' do
34
+ subject.start
35
+ subject.track('bar')
36
+ subject.track('baz')
37
+ expect(subject.count).to eq 2
38
+ end
39
+
40
+ it 'should not increment when given a blocked name' do
41
+ subject.start
42
+ subject.track('foo') #shouldn't count as foo is a blocked name
43
+ subject.track('name')
44
+ expect(subject.count).to eq 1
45
+ end
46
+ end
47
+
48
+ describe "#finalize_count" do
49
+ it 'should return the current count' do
50
+ subject.start
51
+ subject.track('bar')
52
+ expect(subject.finalize_count).to eq 1
53
+ end
54
+
55
+ it 'should reset the current count to 0' do
56
+ subject.start
57
+ subject.track('bar')
58
+ subject.finalize_count
59
+ expect(subject.count).to eq 0
60
+ end
61
+ end
62
+
63
+ end
@@ -0,0 +1,41 @@
1
+ require 'spec_helper'
2
+
3
+ describe CanvasStatsd::DefaultTracking do
4
+
5
+ describe '#track_default_metrics' do
6
+ it 'should track timing, sql, and active_record by default' do
7
+ expect(CanvasStatsd::DefaultTracking).to receive(:track_timing)
8
+ expect(CanvasStatsd::DefaultTracking).to receive(:track_sql)
9
+ expect(CanvasStatsd::DefaultTracking).to receive(:track_active_record)
10
+ CanvasStatsd::DefaultTracking.track_default_metrics
11
+ end
12
+
13
+ it 'should not track sql when sql: false option' do
14
+ expect(CanvasStatsd::DefaultTracking).not_to receive(:track_sql)
15
+ CanvasStatsd::DefaultTracking.track_default_metrics sql: false
16
+ end
17
+
18
+ it 'should not track active_record when active_record: false option' do
19
+ expect(CanvasStatsd::DefaultTracking).not_to receive(:track_active_record)
20
+ CanvasStatsd::DefaultTracking.track_default_metrics active_record: false
21
+ end
22
+ end
23
+
24
+ describe '#track_active_record' do
25
+ it 'should turn on active record instrumentation' do
26
+ expect(CanvasStatsd::DefaultTracking).to receive(:instrument_active_record_creation)
27
+ CanvasStatsd::DefaultTracking.send(:track_active_record)
28
+ end
29
+ end
30
+
31
+ describe '#subscribe' do
32
+ it 'should subscribe via ActiveSupport::Notifications' do
33
+ target = double()
34
+ CanvasStatsd::DefaultTracking.subscribe(/test.notification/) {|*args| target.callback(*args)}
35
+ expect(target).to receive(:callback)
36
+ ActiveSupport::Notifications.instrument('test.notification') {}
37
+ end
38
+ end
39
+
40
+ end
41
+
@@ -0,0 +1,170 @@
1
+ require 'spec_helper'
2
+
3
+
4
+ def create_subject(payload={}, statsd=nil)
5
+ args = ['name', 1000, 1001, 1234, payload]
6
+ args << statsd if statsd
7
+ CanvasStatsd::RequestStat.new(*args)
8
+ end
9
+
10
+
11
+ describe CanvasStatsd::RequestStat do
12
+
13
+ describe '#db_runtime' do
14
+ it 'should return the payload db_runtime' do
15
+ rs = create_subject({db_runtime: 11.11})
16
+ expect(rs.db_runtime).to eq 11.11
17
+ end
18
+
19
+ it 'should return nil when payload db_runtime key doesnt exists' do
20
+ rs = create_subject
21
+ expect(rs.db_runtime).to eq nil
22
+ end
23
+ end
24
+
25
+ describe '#view_runtime' do
26
+ it 'should return the payload view_runtime' do
27
+ rs = create_subject(view_runtime: 11.11)
28
+ expect(rs.view_runtime).to eq 11.11
29
+ end
30
+
31
+ it 'should return nil when payload view_runtime key doesnt exists' do
32
+ rs = create_subject
33
+ expect(rs.view_runtime).to eq nil
34
+ end
35
+ end
36
+
37
+ describe '#controller' do
38
+ it "should return params['controller']" do
39
+ rs = create_subject({params: {'controller' => 'foo'}})
40
+ expect(rs.controller).to eq 'foo'
41
+ end
42
+
43
+ it 'should return nil if no params are available' do
44
+ rs = create_subject
45
+ expect(rs.controller).to eq nil
46
+ end
47
+
48
+ it 'should return nil if no controller is available on params' do
49
+ rs = create_subject({params: {}})
50
+ expect(rs.controller).to eq nil
51
+ end
52
+ end
53
+
54
+ describe '#action' do
55
+ it "should return params['action']" do
56
+ rs = create_subject({params: {'action' => 'index'}})
57
+ expect(rs.action).to eq 'index'
58
+ end
59
+
60
+ it 'should return nil if no params are available' do
61
+ rs = create_subject
62
+ expect(rs.action).to eq nil
63
+ end
64
+
65
+ it 'should return nil if no action is available on params' do
66
+ rs = create_subject({params: {}})
67
+ expect(rs.action).to eq nil
68
+ end
69
+ end
70
+
71
+ describe '#ms' do
72
+ it 'correctly calcuates milliseconds from start, finish' do
73
+ rs = create_subject({params: {}})
74
+ # start and finish are in seconds
75
+ expect(rs.ms).to eq 1000
76
+ end
77
+
78
+ it 'defaults to zero if either start or finish are nil' do
79
+ rs = CanvasStatsd::RequestStat.new('name', nil, 1001, 1111, {params: {}})
80
+ expect(rs.ms).to eq 0
81
+ rs = CanvasStatsd::RequestStat.new('name', 1, nil, 1111, {params: {}})
82
+ expect(rs.ms).to eq 0
83
+ end
84
+ end
85
+
86
+ describe '#report' do
87
+ it 'doesnt send stats when no controller or action' do
88
+ statsd = double
89
+ rs = create_subject({params: {}}, statsd)
90
+ expect(statsd).to_not receive(:timing).with('request.foo.index', 1000)
91
+ rs.report
92
+ end
93
+
94
+ it 'sends total timing when controller && action are present, doesnt send db, or view if they are not' do
95
+ statsd = double
96
+ payload = {
97
+ params: {
98
+ 'controller' => 'foo',
99
+ 'action' => 'index'
100
+ }
101
+ }
102
+ rs = create_subject(payload, statsd)
103
+ expect(statsd).to receive(:timing).with('request.foo.index.total', 1000)
104
+ rs.report
105
+ end
106
+
107
+ it 'sends view_runtime and db_runtime when present' do
108
+ statsd = double
109
+ payload = {
110
+ view_runtime: 70.1,
111
+ db_runtime: 100.2,
112
+ params: {
113
+ 'controller' => 'foo',
114
+ 'action' => 'index'
115
+ }
116
+ }
117
+ rs = create_subject(payload, statsd)
118
+ allow(statsd).to receive(:timing).with('request.foo.index.total', 1000)
119
+ expect(statsd).to receive(:timing).with('request.foo.index.view', 70.1)
120
+ expect(statsd).to receive(:timing).with('request.foo.index.db', 100.2)
121
+ rs.report
122
+ end
123
+
124
+ describe 'sql stats' do
125
+
126
+ before :each do
127
+ @statsd = double
128
+ payload = {
129
+ params: {
130
+ 'controller' => 'foo',
131
+ 'action' => 'index'
132
+ }
133
+ }
134
+ @rs = create_subject(payload, @statsd)
135
+ end
136
+
137
+ it 'doesnt send sql stats when they dont exist' do
138
+ allow(@statsd).to receive(:timing).with('request.foo.index.total', 1000)
139
+ expect(@statsd).to_not receive(:timing).with('request.foo.index.sql.read', kind_of(Numeric))
140
+ expect(@statsd).to_not receive(:timing).with('request.foo.index.sql.write', kind_of(Numeric))
141
+ expect(@statsd).to_not receive(:timing).with('request.foo.index.sql.cache', kind_of(Numeric))
142
+ @rs.report
143
+ end
144
+
145
+ it 'sends sql_read_count when present' do
146
+ @rs.sql_read_count = 10
147
+ allow(@statsd).to receive(:timing).with('request.foo.index.total', 1000)
148
+ expect(@statsd).to receive(:timing).with('request.foo.index.sql.read', 10)
149
+ @rs.report
150
+ end
151
+
152
+ it 'sends sql_read_count when present' do
153
+ @rs.sql_write_count = 3
154
+ allow(@statsd).to receive(:timing).with('request.foo.index.total', 1000)
155
+ expect(@statsd).to receive(:timing).with('request.foo.index.sql.write', 3)
156
+ @rs.report
157
+ end
158
+
159
+ it 'sends sql_cache_count when present' do
160
+ @rs.sql_cache_count = 1
161
+ allow(@statsd).to receive(:timing).with('request.foo.index.total', 1000)
162
+ expect(@statsd).to receive(:timing).with('request.foo.index.sql.cache', 1)
163
+ @rs.report
164
+ end
165
+
166
+ end
167
+
168
+ end
169
+
170
+ end
@@ -0,0 +1,77 @@
1
+ require 'spec_helper'
2
+
3
+ module CanvasStatsd
4
+ describe SqlTracker do
5
+
6
+ describe '#start' do
7
+ it 'resets values to zero' do
8
+ subject = SqlTracker.new
9
+ subject.start
10
+ subject.track 'CACHE', 'SELECT * FROM some_table'
11
+ subject.start
12
+ expect(subject.num_caches).to eq(0)
13
+ expect(subject.num_reads).to eq(0)
14
+ expect(subject.num_writes).to eq(0)
15
+ end
16
+ end
17
+
18
+ describe '#track' do
19
+ before :each do
20
+ @subject = SqlTracker.new
21
+ @subject.start
22
+ end
23
+
24
+ it 'considers CACHE above all' do
25
+ @subject.track 'CACHE', 'SELECT * FROM some_table'
26
+ expect(@subject.num_caches).to eq(1)
27
+ expect(@subject.num_reads).to eq(0)
28
+ end
29
+
30
+ it 'marks as read when select is in the first 15 chars of the sql' do
31
+ @subject.track 'LOAD', ' SELECT "context_external_tools".* FROM'
32
+ expect(@subject.num_reads).to eq(1)
33
+ expect(@subject.num_writes).to eq(0)
34
+ end
35
+
36
+ it 'marks as read with no select, but a LOAD name' do
37
+ @subject.track 'LOAD', 'WITH RECURSIVE t AS'
38
+ expect(@subject.num_reads).to eq(1)
39
+ expect(@subject.num_writes).to eq(0)
40
+ end
41
+
42
+ it 'doesnt track names set as blocked' do
43
+ tracker = SqlTracker.new(blocked_names: ['SCHEMA'])
44
+ tracker.start
45
+ tracker.track 'SCHEMA', 'SELECT * FROM some_table'
46
+ expect(tracker.num_reads).to eq(0)
47
+ end
48
+
49
+ it 'doesnt track nil names or sql values' do
50
+ @subject.track nil, 'SELECT *'
51
+ @subject.track 'CACHE', nil
52
+ expect(@subject.num_reads).to eq(0)
53
+ end
54
+
55
+ it 'passes full sql to counter.track calls for reads' do
56
+ sql = ' SELECT \'context_external_tools\'.* FROM'
57
+ read_counter = double()
58
+ allow(read_counter).to receive(:start)
59
+ expect(read_counter).to receive(:track).with sql
60
+ tracker = SqlTracker.new(read_counter: read_counter)
61
+ tracker.start
62
+ tracker.track 'LOAD', sql
63
+ end
64
+
65
+ it 'passes full sql to counter.track calls for writes' do
66
+ sql = ' UPDATE \'context_external_tools\'.* FROM'
67
+ write_counter = double()
68
+ allow(write_counter).to receive(:start)
69
+ expect(write_counter).to receive(:track).with sql
70
+ tracker = SqlTracker.new(write_counter: write_counter)
71
+ tracker.start
72
+ tracker.track 'UPDATE', sql
73
+ end
74
+ end
75
+
76
+ end
77
+ end
@@ -0,0 +1,74 @@
1
+ #
2
+ # Copyright (C) 2012 Instructure, Inc.
3
+ #
4
+ # This file is part of Canvas.
5
+ #
6
+ # Canvas is free software: you can redistribute it and/or modify it under
7
+ # the terms of the GNU Affero General Public License as published by the Free
8
+ # Software Foundation, version 3 of the License.
9
+ #
10
+ # Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
11
+ # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
12
+ # A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
13
+ # details.
14
+ #
15
+ # You should have received a copy of the GNU Affero General Public License along
16
+ # with this program. If not, see <http://www.gnu.org/licenses/>.
17
+ #
18
+
19
+ require 'spec_helper'
20
+
21
+ describe CanvasStatsd::Statsd do
22
+ METHODS = %w(increment decrement count gauge timing)
23
+
24
+ it "appends the hostname to stat names by default" do
25
+ CanvasStatsd::Statsd.stub(:hostname).and_return("testhost")
26
+ statsd = double
27
+ CanvasStatsd::Statsd.stub(:instance).and_return(statsd)
28
+ CanvasStatsd::Statsd.stub(:append_hostname?).and_return(true)
29
+ METHODS.each do |method|
30
+ expect(statsd).to receive(method).with("test.name.testhost", "test")
31
+ CanvasStatsd::Statsd.send(method, "test.name", "test")
32
+ end
33
+ expect(statsd).to receive("timing").with("test.name.testhost", anything, anything)
34
+ expect(CanvasStatsd::Statsd.time("test.name") { "test" }).to eq "test"
35
+ end
36
+
37
+ it "omits hostname if specified in config" do
38
+ expect(CanvasStatsd::Statsd).to receive(:hostname).never
39
+ statsd = double
40
+ CanvasStatsd::Statsd.stub(:instance).and_return(statsd)
41
+ CanvasStatsd::Statsd.stub(:append_hostname?).and_return(false)
42
+ METHODS.each do |method|
43
+ expect(statsd).to receive(method).with("test.name", "test")
44
+ CanvasStatsd::Statsd.send(method, "test.name", "test")
45
+ end
46
+ expect(statsd).to receive("timing").with("test.name", anything, anything)
47
+ expect(CanvasStatsd::Statsd.time("test.name") { "test" }).to eq "test"
48
+ end
49
+
50
+ it "ignores all calls if statsd isn't enabled" do
51
+ CanvasStatsd::Statsd.stub(:instance).and_return(nil)
52
+ METHODS.each do |method|
53
+ expect(CanvasStatsd::Statsd.send(method, "test.name")).to be_nil
54
+ end
55
+ expect(CanvasStatsd::Statsd.time("test.name") { "test" }).to eq "test"
56
+ end
57
+
58
+ it "configures a statsd instance" do
59
+ expect(CanvasStatsd::Statsd.instance).to be_nil
60
+
61
+ CanvasStatsd.settings = { :host => "testhost", :namespace => "test", :port => 1234 }
62
+ CanvasStatsd::Statsd.reset_instance
63
+
64
+ instance = CanvasStatsd::Statsd.instance
65
+ expect(instance).to be_a ::Statsd
66
+ expect(instance.host).to eq "testhost"
67
+ expect(instance.port).to eq 1234
68
+ expect(instance.namespace).to eq "test"
69
+ end
70
+
71
+ after do
72
+ CanvasStatsd::Statsd.reset_instance
73
+ end
74
+ end
@@ -0,0 +1,9 @@
1
+ require 'canvas_statsd'
2
+
3
+ RSpec.configure do |config|
4
+ config.treat_symbols_as_metadata_keys_with_true_values = true
5
+ config.run_all_when_everything_filtered = true
6
+ config.filter_run :focus
7
+
8
+ config.order = 'random'
9
+ end
data/test.sh ADDED
@@ -0,0 +1,16 @@
1
+ #!/bin/bash
2
+ result=0
3
+
4
+ echo "################ canvas_statsd ################"
5
+ echo "################ Running tests against Rails 3 ################"
6
+ bundle check || bundle install
7
+ bundle exec rspec spec
8
+ let result=$result+$?
9
+
10
+ if [ $result -eq 0 ]; then
11
+ echo "SUCCESS"
12
+ else
13
+ echo "FAILURE"
14
+ fi
15
+
16
+ exit $result
metadata ADDED
@@ -0,0 +1,136 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: canvas_statsd
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Nick Cloward
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-05-13 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: statsd-ruby
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '='
18
+ - !ruby/object:Gem::Version
19
+ version: 1.0.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '='
25
+ - !ruby/object:Gem::Version
26
+ version: 1.0.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: aroi
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '='
32
+ - !ruby/object:Gem::Version
33
+ version: 0.0.3
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '='
39
+ - !ruby/object:Gem::Version
40
+ version: 0.0.3
41
+ - !ruby/object:Gem::Dependency
42
+ name: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.5'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.5'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ description:
84
+ email:
85
+ - ncloward@instructure.com
86
+ executables: []
87
+ extensions: []
88
+ extra_rdoc_files: []
89
+ files:
90
+ - lib/canvas_statsd.rb
91
+ - lib/canvas_statsd/counter.rb
92
+ - lib/canvas_statsd/default_tracking.rb
93
+ - lib/canvas_statsd/request_stat.rb
94
+ - lib/canvas_statsd/sql_tracker.rb
95
+ - lib/canvas_statsd/statsd.rb
96
+ - spec/canvas_statsd/canvas_statsd_spec.rb
97
+ - spec/canvas_statsd/counter_spec.rb
98
+ - spec/canvas_statsd/default_tracking_spec.rb
99
+ - spec/canvas_statsd/request_stat_spec.rb
100
+ - spec/canvas_statsd/sql_tracker_spec.rb
101
+ - spec/canvas_statsd/statsd_spec.rb
102
+ - spec/spec_helper.rb
103
+ - test.sh
104
+ homepage: https://github.com/instructure/canvas_statsd
105
+ licenses:
106
+ - MIT
107
+ metadata: {}
108
+ post_install_message:
109
+ rdoc_options: []
110
+ require_paths:
111
+ - lib
112
+ required_ruby_version: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: '0'
117
+ required_rubygems_version: !ruby/object:Gem::Requirement
118
+ requirements:
119
+ - - ">="
120
+ - !ruby/object:Gem::Version
121
+ version: '0'
122
+ requirements: []
123
+ rubyforge_project:
124
+ rubygems_version: 2.4.6
125
+ signing_key:
126
+ specification_version: 4
127
+ summary: Statsd for Canvas
128
+ test_files:
129
+ - spec/canvas_statsd/canvas_statsd_spec.rb
130
+ - spec/canvas_statsd/counter_spec.rb
131
+ - spec/canvas_statsd/default_tracking_spec.rb
132
+ - spec/canvas_statsd/request_stat_spec.rb
133
+ - spec/canvas_statsd/sql_tracker_spec.rb
134
+ - spec/canvas_statsd/statsd_spec.rb
135
+ - spec/spec_helper.rb
136
+ has_rdoc: