inst_statsd 2.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml 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