inst_statsd 2.0.4

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: f5fcae338209105c5074036e543c03f719f4b3ae
4
+ data.tar.gz: dbb3c4e3de0a2138737637b86b27ae9764fa7c27
5
+ SHA512:
6
+ metadata.gz: 00c31868c01435faa215bccbfaca3675fa86c0f7b7e4a356b57c7c04cdfcf83b20f4a33d31c58c0a46d94731f9c3bf01d1ea0d05f0c6204bfe15cd7daf26bdc6
7
+ data.tar.gz: 91bec914d1e70b5b497b35e675c9d947f0d8924fce7a20d4b8e9c7d4366becd43fa138f61df7ddc7820ef16e77afdb4764b6d9adaa5f9191cb0f4409d2b982f2
@@ -0,0 +1,65 @@
1
+ require 'statsd'
2
+
3
+ module InstStatsd
4
+ VALID_SETTINGS = %i[host port namespace append_hostname mask negative_mask batch_size batch_byte_size].freeze
5
+
6
+ class ConfigurationError < StandardError; end
7
+
8
+ require 'inst_statsd/statsd'
9
+ require 'inst_statsd/block_stat'
10
+ require 'inst_statsd/block_tracking'
11
+ require 'inst_statsd/request_stat'
12
+ require 'inst_statsd/counter'
13
+ require 'inst_statsd/sql_tracker'
14
+ require 'inst_statsd/default_tracking'
15
+ require 'inst_statsd/request_logger'
16
+ require 'inst_statsd/request_tracking'
17
+ require 'inst_statsd/null_logger'
18
+
19
+ class << self
20
+ def settings
21
+ @settings ||= env_settings
22
+ end
23
+
24
+ def settings=(value)
25
+ @settings = validate_settings(value)
26
+ end
27
+
28
+ def validate_settings(value)
29
+ return nil if value.nil?
30
+
31
+ validated = {}
32
+ value.each do |k, v|
33
+ unless VALID_SETTINGS.include?(k.to_sym)
34
+ raise InstStatsd::ConfigurationError, "Invalid key: #{k}"
35
+ end
36
+ v = Regexp.new(v) if %i[mask negative_mask].include?(k.to_sym) && v.is_a?(String)
37
+ validated[k.to_sym] = v
38
+ end
39
+
40
+ env_settings.merge(validated)
41
+ end
42
+
43
+ def env_settings(env = ENV)
44
+ config = {
45
+ host: env.fetch('INST_STATSD_HOST', nil),
46
+ port: env.fetch('INST_STATSD_PORT', nil),
47
+ namespace: env.fetch('INST_STATSD_NAMESPACE', nil),
48
+ append_hostname: env.fetch('INST_STATSD_APPEND_HOSTNAME', nil)
49
+ }
50
+ config.delete_if { |_k, v| v.nil? }
51
+ convert_bool(config, :append_hostname)
52
+ config[:host] ? config : {}
53
+ end
54
+
55
+ def convert_bool(hash, key)
56
+ value = hash[key]
57
+ return if value.nil?
58
+ unless ['true', 'True', 'false', 'False', true, false].include?(value)
59
+ message = "#{key} must be a boolean, or the string representation of a boolean, got: #{value}"
60
+ raise InstStatsd::ConfigurationError, message
61
+ end
62
+ hash[key] = ['true', 'True', true].include?(value)
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,37 @@
1
+ module InstStatsd
2
+ class BlockStat
3
+
4
+ attr_accessor :stats
5
+ attr_accessor :common_key
6
+
7
+ def initialize(common_key, statsd=InstStatsd::Statsd)
8
+ self.common_key = common_key
9
+ @statsd = statsd
10
+ @stats = {}
11
+ end
12
+
13
+ def subtract_exclusives(stats)
14
+ @exclusives ||= {}
15
+ stats.each do |(key, value)|
16
+ @exclusives[key] ||= 0.0
17
+ @exclusives[key] += value
18
+ end
19
+ end
20
+
21
+ def exclusive_stats
22
+ return nil unless @exclusives
23
+ stats.map { |key, value| [key, value - (@exclusives[key] || 0.0)] }.to_h
24
+ end
25
+
26
+ def report
27
+ if common_key
28
+ stats.each do |(key, value)|
29
+ @statsd.timing("#{common_key}.#{key}", value)
30
+ end
31
+ exclusive_stats&.each do |(key, value)|
32
+ @statsd.timing("#{common_key}.exclusive.#{key}", value)
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,62 @@
1
+ require 'benchmark'
2
+
3
+ module InstStatsd
4
+ class BlockTracking
5
+ class << self
6
+ attr_accessor :logger
7
+
8
+ [:mask, :negative_mask].each do |method|
9
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
10
+ def #{method}
11
+ InstStatsd.settings[:#{method}]
12
+ end
13
+
14
+ def #{method}=(value)
15
+ InstStatsd.settings[:#{method}] = value
16
+ end
17
+ RUBY
18
+ end
19
+
20
+ def track(key, category: nil, statsd: InstStatsd::Statsd, only: nil)
21
+ return yield if mask && mask !~ key
22
+ return yield if negative_mask && negative_mask =~ key
23
+
24
+ cookies = if only
25
+ Array(only).map { |name| [name, Counter.counters[name].start] }
26
+ else
27
+ Counter.counters.map { |(name, counter)| [name, counter.start] }
28
+ end
29
+ block_stat = InstStatsd::BlockStat.new(key, statsd)
30
+ stack(category).push(block_stat) if category
31
+
32
+ result = nil
33
+ elapsed = Benchmark.realtime do
34
+ result = yield
35
+ end
36
+ # to be consistent with ActionPack, measure in milliseconds
37
+ elapsed *= 1000
38
+
39
+ block_stat.stats = cookies.map { |(name, cookie)| [name, Counter.counters[name].finalize_count(cookie)] }.to_h
40
+ block_stat.stats['total'] = elapsed
41
+ # we need to make sure to report exclusive timings, even if nobody called us re-entrantly
42
+ block_stat.subtract_exclusives({}) if category
43
+ block_stat.report
44
+ logger.log(block_stat, "STATSD #{key}") if logger
45
+ # -1 is ourselves; we want to subtract from the block above us
46
+ stack(category)[-2].subtract_exclusives(block_stat.stats) if category && stack(category)[-2]
47
+
48
+ result
49
+ ensure
50
+ stack(category).pop if category && stack(category).last == block_stat
51
+ end
52
+
53
+ private
54
+
55
+ def stack(category)
56
+ Thread.current[:stats_block_stack] ||= {}
57
+ Thread.current[:stats_block_stack][category] ||= []
58
+ end
59
+ end
60
+
61
+ end
62
+ end
@@ -0,0 +1,44 @@
1
+ module InstStatsd
2
+ class Counter
3
+ class << self
4
+ def counters
5
+ @counters ||= {}
6
+ end
7
+
8
+ def register(counter)
9
+ counters[counter.key] = counter
10
+ end
11
+ end
12
+
13
+ attr_reader :key
14
+ attr_reader :blocked_names
15
+
16
+ def initialize(key, blocked_names=[])
17
+ @blocked_names = blocked_names
18
+ @key = key
19
+ @tls_key = "statsd.#{key}"
20
+ self.class.register(self)
21
+ end
22
+
23
+ def start
24
+ Thread.current[@tls_key] ||= 0
25
+ end
26
+
27
+ def track(name)
28
+ Thread.current[@tls_key] += 1 if Thread.current[@tls_key] && accepted_name?(name)
29
+ end
30
+
31
+ def finalize_count(cookie)
32
+ Thread.current[@tls_key] - cookie
33
+ end
34
+
35
+ def count
36
+ Thread.current[@tls_key]
37
+ end
38
+
39
+ def accepted_name?(name)
40
+ !blocked_names.include?(name)
41
+ end
42
+
43
+ end
44
+ end
@@ -0,0 +1,42 @@
1
+ require "active_support"
2
+
3
+ module InstStatsd
4
+ class DefaultTracking
5
+ def self.track_sql
6
+ return if @sql_tracker
7
+ @sql_tracker = InstStatsd::SqlTracker.new(blocked_names: ['SCHEMA'])
8
+ ActiveSupport::Notifications.subscribe(/sql\.active_record/) {|*args| update_sql_count(*args)}
9
+ end
10
+
11
+ def self.track_active_record
12
+ return if @ar_counter
13
+ require 'aroi'
14
+
15
+ ::Aroi::Instrumentation.instrument_creation!
16
+ @ar_counter = InstStatsd::Counter.new('active_record')
17
+ ActiveSupport::Notifications.subscribe(/instance\.active_record/) {|*args| update_active_record_count(*args)}
18
+ end
19
+
20
+ def self.track_cache
21
+ return if @cache_read_counter
22
+
23
+ @cache_read_counter = InstStatsd::Counter.new('cache.read')
24
+ ActiveSupport::Notifications.subscribe(/cache_read\.active_support/) {|*args| update_cache_read_count(*args)}
25
+ end
26
+
27
+ private
28
+
29
+ def self.update_sql_count(_name, _start, _finish, _id, payload)
30
+ @sql_tracker.track payload.fetch(:name), payload.fetch(:sql)
31
+ end
32
+
33
+ def self.update_active_record_count(_name, _start, _finish, _id, payload)
34
+ @ar_counter.track payload.fetch(:name, '')
35
+ end
36
+
37
+ def self.update_cache_read_count(_name, _start, _finish, _id, _payload)
38
+ @cache_read_counter.track "read"
39
+ end
40
+
41
+ end
42
+ end
@@ -0,0 +1,11 @@
1
+ require 'logger'
2
+
3
+ module InstStatsd
4
+ class NullLogger < Logger
5
+ def initialize(*args)
6
+ end
7
+
8
+ def add(*args, &block)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,25 @@
1
+ module InstStatsd
2
+ class RequestLogger
3
+
4
+ def initialize(logger)
5
+ @logger = logger || InstStatsd::NullLogger.new
6
+ end
7
+
8
+ def log(request_stat, header=nil)
9
+ @logger.info(build_log_message(request_stat, header))
10
+ end
11
+
12
+ def build_log_message(request_stat, header=nil)
13
+ header ||= "STATSD"
14
+ message = "[#{header}]"
15
+ request_stat.stats.each do |(name, value)|
16
+ message += " (#{name.to_s.gsub('.', '_')}: #{"%.2f" % value})"
17
+ end
18
+ request_stat.exclusive_stats&.each do |(name, value)|
19
+ message += " (exclusive_#{name.to_s.gsub('.', '_')}: #{"%.2f" % value})"
20
+ end
21
+ message
22
+ end
23
+
24
+ end
25
+ end
@@ -0,0 +1,49 @@
1
+ module InstStatsd
2
+ class RequestStat < BlockStat
3
+ def initialize(name, start, finish, id, payload, statsd=InstStatsd::Statsd)
4
+ super(nil, statsd)
5
+ @name = name
6
+ @start = start
7
+ @finish = finish
8
+ @id = id
9
+ @payload = payload
10
+ end
11
+
12
+ def common_key
13
+ common_key = super
14
+ return common_key if common_key
15
+ self.common_key = "request.#{controller}.#{action}" if controller && action
16
+ end
17
+
18
+ def report
19
+ stats['total'] = total
20
+ stats['view'] = view_runtime if view_runtime
21
+ stats['db'] = db_runtime if db_runtime
22
+ super
23
+ end
24
+
25
+ def db_runtime
26
+ @payload.fetch(:db_runtime, nil)
27
+ end
28
+
29
+ def view_runtime
30
+ @payload.fetch(:view_runtime, nil)
31
+ end
32
+
33
+ def controller
34
+ @payload.fetch(:params, {})['controller']
35
+ end
36
+
37
+ def action
38
+ @payload.fetch(:params, {})['action']
39
+ end
40
+
41
+ def total
42
+ if (!@finish || !@start)
43
+ return 0
44
+ end
45
+ (@finish - @start) * 1000
46
+ end
47
+
48
+ end
49
+ end
@@ -0,0 +1,27 @@
1
+ module InstStatsd
2
+ class RequestTracking
3
+
4
+ def self.enable(logger: nil)
5
+ @logger = RequestLogger.new(logger)
6
+ track_timing
7
+ end
8
+
9
+ private
10
+
11
+ def self.track_timing
12
+ ActiveSupport::Notifications.subscribe(/start_processing\.action_controller/, &method(:start_processing))
13
+ ActiveSupport::Notifications.subscribe(/process_action\.action_controller/, &method(:finalize_processing))
14
+ end
15
+
16
+ def self.start_processing(*_args)
17
+ @cookies = Counter.counters.map { |(name, counter)| [name, counter.start] }
18
+ end
19
+
20
+ def self.finalize_processing *args
21
+ request_stat = InstStatsd::RequestStat.new(*args)
22
+ request_stat.stats = @cookies.map { |(name, cookie)| [name, Counter.counters[name].finalize_count(cookie)] }.to_h
23
+ request_stat.report
24
+ @logger.log(request_stat)
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,50 @@
1
+ module InstStatsd
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, InstStatsd::Counter.new('sql.read'))
10
+ @write_counts = opts.fetch(:write_counter, InstStatsd::Counter.new('sql.write'))
11
+ @cache_counts = opts.fetch(:cache_counter, InstStatsd::Counter.new('sql.cache'))
12
+ end
13
+
14
+ def start
15
+ [read_counts, write_counts, cache_counts].map(&: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 finalize_counts(cookies)
31
+ [
32
+ read_counts.finalize_count(cookies[0]),
33
+ write_counts.finalize_count(cookies[1]),
34
+ cache_counts.finalize_count(cookies[2])
35
+ ]
36
+ end
37
+
38
+ private
39
+
40
+ def accepted_name?(name)
41
+ !!(name && !blocked_names.include?(name))
42
+ end
43
+
44
+ def truncate(sql, length=15)
45
+ sql ||= ''
46
+ sql.strip[0..length]
47
+ end
48
+
49
+ end
50
+ end
@@ -0,0 +1,102 @@
1
+ # Proxy class to communicate messages to statsd
2
+ # Available statsd messages are described in:
3
+ # https://github.com/etsy/statsd/blob/master/README.md
4
+ # https://github.com/reinh/statsd/blob/master/lib/statsd.rb
5
+ #
6
+ # So for instance:
7
+ # ms = Benchmark.ms { ..code.. }
8
+ # InstStatsd::Statsd.timing("my_stat", ms)
9
+ #
10
+ # Configured in config/statsd.yml, see config/statsd.yml.example
11
+ # At least a host needs to be defined for the environment, all other config is optional
12
+ #
13
+ # If a namespace is defined in statsd.yml, it'll be prepended to the stat name.
14
+ # The hostname of the server will be appended to the stat name, unless `append_hostname: false` is specified in the config.
15
+ # 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"
16
+ # (assuming the default statsd/graphite configuration)
17
+ #
18
+ # If statsd isn't configured and enabled, then calls to InstStatsd::Statsd.* will do nothing and return nil
19
+
20
+ module InstStatsd
21
+ module Statsd
22
+ # replace "." in key names with another character to avoid creating spurious sub-folders in graphite
23
+ def self.escape(str, replacement = '_')
24
+ str.respond_to?(:gsub) ? str.gsub('.', replacement) : str
25
+ end
26
+
27
+ def self.hostname
28
+ @hostname ||= Socket.gethostname.split('.').first
29
+ end
30
+
31
+ %w[increment decrement count gauge timing].each do |method|
32
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
33
+ def self.#{method}(stat, *args)
34
+ if self.instance
35
+ if Array === stat
36
+ stat.each do |st|
37
+ self.#{method}(st, *args)
38
+ end
39
+ return
40
+ end
41
+
42
+ if self.append_hostname?
43
+ stat_name = "\#{stat}.\#{hostname}"
44
+ else
45
+ stat_name = stat.to_s
46
+ end
47
+ self.instance.#{method}(stat_name, *args)
48
+ else
49
+ nil
50
+ end
51
+ end
52
+ RUBY
53
+ end
54
+
55
+ def self.time(stat, sample_rate = 1)
56
+ start = Time.now
57
+ result = yield
58
+ timing(stat, ((Time.now - start) * 1000).round, sample_rate)
59
+ result
60
+ end
61
+
62
+ def self.batch
63
+ return yield unless (old_instance = instance)
64
+ old_instance.batch do |batch|
65
+ Thread.current[:inst_statsd] = batch
66
+ yield
67
+ end
68
+ ensure
69
+ Thread.current[:inst_statsd] = old_instance
70
+ end
71
+
72
+ def self.instance
73
+ thread_statsd = Thread.current[:inst_statsd]
74
+ return thread_statsd if thread_statsd
75
+
76
+ unless defined?(@statsd)
77
+ statsd_settings = InstStatsd.settings
78
+
79
+ if statsd_settings && statsd_settings[:host]
80
+ @statsd = ::Statsd.new(statsd_settings[:host])
81
+ @statsd.port = statsd_settings[:port] if statsd_settings[:port]
82
+ @statsd.namespace = statsd_settings[:namespace] if statsd_settings[:namespace]
83
+ @statsd.batch_size = statsd_settings[:batch_size] if statsd_settings.key?(:batch_size)
84
+ @statsd.batch_byte_size = statsd_settings[:batch_byte_size] if statsd_settings.key?(:batch_byte_size)
85
+ @append_hostname = !statsd_settings.key?(:append_hostname) || !!statsd_settings[:append_hostname]
86
+ else
87
+ @statsd = nil
88
+ end
89
+ end
90
+ @statsd
91
+ end
92
+
93
+ def self.append_hostname?
94
+ @append_hostname
95
+ end
96
+
97
+ def self.reset_instance
98
+ remove_instance_variable(:@statsd) if defined?(@statsd)
99
+ Thread.current[:inst_statsd] = nil
100
+ end
101
+ end
102
+ end