statsd-instrument 3.8.0 → 3.9.1
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 +4 -4
- data/.github/pull_request_template.md +14 -0
- data/.github/workflows/benchmark.yml +2 -2
- data/.github/workflows/lint.yml +1 -1
- data/.github/workflows/tests.yml +2 -2
- data/.ruby-version +1 -1
- data/CHANGELOG.md +18 -0
- data/Gemfile +7 -0
- data/README.md +46 -0
- data/Rakefile +11 -0
- data/benchmark/local-udp-throughput +178 -13
- data/benchmark/send-metrics-to-local-udp-receiver +6 -4
- data/lib/statsd/instrument/aggregator.rb +269 -0
- data/lib/statsd/instrument/{batched_udp_sink.rb → batched_sink.rb} +40 -24
- data/lib/statsd/instrument/client.rb +101 -15
- data/lib/statsd/instrument/datagram.rb +6 -2
- data/lib/statsd/instrument/datagram_builder.rb +21 -0
- data/lib/statsd/instrument/environment.rb +42 -7
- data/lib/statsd/instrument/{udp_sink.rb → sink.rb} +34 -25
- data/lib/statsd/instrument/udp_connection.rb +39 -0
- data/lib/statsd/instrument/uds_connection.rb +52 -0
- data/lib/statsd/instrument/version.rb +1 -1
- data/lib/statsd/instrument.rb +9 -3
- data/test/aggregator_test.rb +142 -0
- data/test/client_test.rb +48 -1
- data/test/datagram_builder_test.rb +5 -0
- data/test/dispatcher_stats_test.rb +3 -3
- data/test/environment_test.rb +4 -4
- data/test/integration_test.rb +51 -0
- data/test/test_helper.rb +6 -1
- data/test/udp_sink_test.rb +7 -6
- data/test/uds_sink_test.rb +187 -0
- metadata +16 -8
| @@ -0,0 +1,269 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module StatsD
         | 
| 4 | 
            +
              module Instrument
         | 
| 5 | 
            +
                class AggregationKey
         | 
| 6 | 
            +
                  attr_reader :name, :tags, :no_prefix, :type, :hash
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                  def initialize(name, tags, no_prefix, type)
         | 
| 9 | 
            +
                    @name = name
         | 
| 10 | 
            +
                    @tags = tags
         | 
| 11 | 
            +
                    @no_prefix = no_prefix
         | 
| 12 | 
            +
                    @type = type
         | 
| 13 | 
            +
                    @hash = [@name, @tags, @no_prefix, @type].hash
         | 
| 14 | 
            +
                  end
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                  def ==(other)
         | 
| 17 | 
            +
                    other.is_a?(self.class) &&
         | 
| 18 | 
            +
                      @name == other.name &&
         | 
| 19 | 
            +
                      @tags == other.tags &&
         | 
| 20 | 
            +
                      @no_prefix == other.no_prefix &&
         | 
| 21 | 
            +
                      @type == other.type
         | 
| 22 | 
            +
                  end
         | 
| 23 | 
            +
                  alias_method :eql?, :==
         | 
| 24 | 
            +
                end
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                class Aggregator
         | 
| 27 | 
            +
                  DEFAULT_MAX_CONTEXT_SIZE = 250
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                  CONST_SAMPLE_RATE = 1.0
         | 
| 30 | 
            +
                  COUNT = :c
         | 
| 31 | 
            +
                  DISTRIBUTION = :d
         | 
| 32 | 
            +
                  MEASURE = :ms
         | 
| 33 | 
            +
                  HISTOGRAM = :h
         | 
| 34 | 
            +
                  GAUGE = :g
         | 
| 35 | 
            +
                  private_constant :COUNT, :DISTRIBUTION, :MEASURE, :HISTOGRAM, :GAUGE, :CONST_SAMPLE_RATE
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                  class << self
         | 
| 38 | 
            +
                    def finalize(aggregation_state, sink, datagram_builders, datagram_builder_class, default_tags)
         | 
| 39 | 
            +
                      proc do
         | 
| 40 | 
            +
                        aggregation_state.each do |key, agg_value|
         | 
| 41 | 
            +
                          no_prefix = key.no_prefix
         | 
| 42 | 
            +
                          datagram_builders[no_prefix] ||= datagram_builder_class.new(
         | 
| 43 | 
            +
                            prefix: no_prefix ? nil : @metric_prefix,
         | 
| 44 | 
            +
                            default_tags: default_tags,
         | 
| 45 | 
            +
                          )
         | 
| 46 | 
            +
                          case key.type
         | 
| 47 | 
            +
                          when COUNT
         | 
| 48 | 
            +
                            sink << datagram_builders[no_prefix].c(
         | 
| 49 | 
            +
                              key.name,
         | 
| 50 | 
            +
                              agg_value,
         | 
| 51 | 
            +
                              CONST_SAMPLE_RATE,
         | 
| 52 | 
            +
                              key.tags,
         | 
| 53 | 
            +
                            )
         | 
| 54 | 
            +
                          when DISTRIBUTION, MEASURE, HISTOGRAM
         | 
| 55 | 
            +
                            sink << datagram_builders[no_prefix].timing_value_packed(
         | 
| 56 | 
            +
                              key.name,
         | 
| 57 | 
            +
                              key.type.to_s,
         | 
| 58 | 
            +
                              agg_value,
         | 
| 59 | 
            +
                              CONST_SAMPLE_RATE,
         | 
| 60 | 
            +
                              key.tags,
         | 
| 61 | 
            +
                            )
         | 
| 62 | 
            +
                          when GAUGE
         | 
| 63 | 
            +
                            sink << datagram_builders[no_prefix].g(
         | 
| 64 | 
            +
                              key.name,
         | 
| 65 | 
            +
                              agg_value,
         | 
| 66 | 
            +
                              CONST_SAMPLE_RATE,
         | 
| 67 | 
            +
                              key.tags,
         | 
| 68 | 
            +
                            )
         | 
| 69 | 
            +
                          else
         | 
| 70 | 
            +
                            StatsD.logger.error { "[#{self.class.name}] Unknown aggregation type: #{key.type}" }
         | 
| 71 | 
            +
                          end
         | 
| 72 | 
            +
                        end
         | 
| 73 | 
            +
                        aggregation_state.clear
         | 
| 74 | 
            +
                      end
         | 
| 75 | 
            +
                    end
         | 
| 76 | 
            +
                  end
         | 
| 77 | 
            +
             | 
| 78 | 
            +
                  # @param sink [#<<] The sink to write the aggregated metrics to.
         | 
| 79 | 
            +
                  # @param datagram_builder_class [Class] The class to use for building datagrams.
         | 
| 80 | 
            +
                  # @param prefix [String] The prefix to add to all metrics.
         | 
| 81 | 
            +
                  # @param default_tags [Array<String>] The tags to add to all metrics.
         | 
| 82 | 
            +
                  # @param flush_interval [Float] The interval at which to flush the aggregated metrics.
         | 
| 83 | 
            +
                  # @param max_values [Integer] The maximum number of values to aggregate before flushing.
         | 
| 84 | 
            +
                  def initialize(
         | 
| 85 | 
            +
                    sink,
         | 
| 86 | 
            +
                    datagram_builder_class,
         | 
| 87 | 
            +
                    prefix,
         | 
| 88 | 
            +
                    default_tags,
         | 
| 89 | 
            +
                    flush_interval: 5.0,
         | 
| 90 | 
            +
                    max_values: DEFAULT_MAX_CONTEXT_SIZE
         | 
| 91 | 
            +
                  )
         | 
| 92 | 
            +
                    @sink = sink
         | 
| 93 | 
            +
                    @datagram_builder_class = datagram_builder_class
         | 
| 94 | 
            +
                    @metric_prefix = prefix
         | 
| 95 | 
            +
                    @default_tags = default_tags
         | 
| 96 | 
            +
                    @datagram_builders = {
         | 
| 97 | 
            +
                      true: nil,
         | 
| 98 | 
            +
                      false: nil,
         | 
| 99 | 
            +
                    }
         | 
| 100 | 
            +
                    @max_values = max_values
         | 
| 101 | 
            +
             | 
| 102 | 
            +
                    # Mutex protects the aggregation_state and flush_thread from concurrent access
         | 
| 103 | 
            +
                    @mutex = Mutex.new
         | 
| 104 | 
            +
                    @aggregation_state = {}
         | 
| 105 | 
            +
             | 
| 106 | 
            +
                    @pid = Process.pid
         | 
| 107 | 
            +
                    @flush_interval = flush_interval
         | 
| 108 | 
            +
                    @flush_thread = Thread.new do
         | 
| 109 | 
            +
                      Thread.current.abort_on_exception = true
         | 
| 110 | 
            +
                      loop do
         | 
| 111 | 
            +
                        sleep(@flush_interval)
         | 
| 112 | 
            +
                        thread_healthcheck
         | 
| 113 | 
            +
                        flush
         | 
| 114 | 
            +
                      rescue => e
         | 
| 115 | 
            +
                        StatsD.logger.error { "[#{self.class.name}] Error in flush thread: #{e}" }
         | 
| 116 | 
            +
                        raise e
         | 
| 117 | 
            +
                      end
         | 
| 118 | 
            +
                    end
         | 
| 119 | 
            +
             | 
| 120 | 
            +
                    ObjectSpace.define_finalizer(
         | 
| 121 | 
            +
                      self,
         | 
| 122 | 
            +
                      self.class.finalize(@aggregation_state, @sink, @datagram_builders, @datagram_builder_class, @default_tags),
         | 
| 123 | 
            +
                    )
         | 
| 124 | 
            +
                  end
         | 
| 125 | 
            +
             | 
| 126 | 
            +
                  # Increment a counter by a given value and save it for later flushing.
         | 
| 127 | 
            +
                  # @param name [String] The name of the counter.
         | 
| 128 | 
            +
                  # @param value [Integer] The value to increment the counter by.
         | 
| 129 | 
            +
                  # @param tags [Hash{String, Symbol => String},Array<String>] The tags to attach to the counter.
         | 
| 130 | 
            +
                  # @param no_prefix [Boolean] If true, the metric will not be prefixed.
         | 
| 131 | 
            +
                  # @return [void]
         | 
| 132 | 
            +
                  def increment(name, value = 1, tags: [], no_prefix: false)
         | 
| 133 | 
            +
                    unless thread_healthcheck
         | 
| 134 | 
            +
                      sink << datagram_builder(no_prefix: no_prefix).c(name, value, CONST_SAMPLE_RATE, tags)
         | 
| 135 | 
            +
                      return
         | 
| 136 | 
            +
                    end
         | 
| 137 | 
            +
             | 
| 138 | 
            +
                    tags = tags_sorted(tags)
         | 
| 139 | 
            +
                    key = packet_key(name, tags, no_prefix, COUNT)
         | 
| 140 | 
            +
             | 
| 141 | 
            +
                    @mutex.synchronize do
         | 
| 142 | 
            +
                      @aggregation_state[key] ||= 0
         | 
| 143 | 
            +
                      @aggregation_state[key] += value
         | 
| 144 | 
            +
                    end
         | 
| 145 | 
            +
                  end
         | 
| 146 | 
            +
             | 
| 147 | 
            +
                  def aggregate_timing(name, value, tags: [], no_prefix: false, type: DISTRIBUTION)
         | 
| 148 | 
            +
                    unless thread_healthcheck
         | 
| 149 | 
            +
                      sink << datagram_builder(no_prefix: no_prefix).timing_value_packed(
         | 
| 150 | 
            +
                        name, type, [value], CONST_SAMPLE_RATE, tags
         | 
| 151 | 
            +
                      )
         | 
| 152 | 
            +
                      return
         | 
| 153 | 
            +
                    end
         | 
| 154 | 
            +
             | 
| 155 | 
            +
                    tags = tags_sorted(tags)
         | 
| 156 | 
            +
                    key = packet_key(name, tags, no_prefix, type)
         | 
| 157 | 
            +
             | 
| 158 | 
            +
                    @mutex.synchronize do
         | 
| 159 | 
            +
                      values = @aggregation_state[key] ||= []
         | 
| 160 | 
            +
                      if values.size + 1 >= @max_values
         | 
| 161 | 
            +
                        do_flush
         | 
| 162 | 
            +
                      end
         | 
| 163 | 
            +
                      values << value
         | 
| 164 | 
            +
                    end
         | 
| 165 | 
            +
                  end
         | 
| 166 | 
            +
             | 
| 167 | 
            +
                  def gauge(name, value, tags: [], no_prefix: false)
         | 
| 168 | 
            +
                    unless thread_healthcheck
         | 
| 169 | 
            +
                      sink << datagram_builder(no_prefix: no_prefix).g(name, value, CONST_SAMPLE_RATE, tags)
         | 
| 170 | 
            +
                      return
         | 
| 171 | 
            +
                    end
         | 
| 172 | 
            +
             | 
| 173 | 
            +
                    tags = tags_sorted(tags)
         | 
| 174 | 
            +
                    key = packet_key(name, tags, no_prefix, GAUGE)
         | 
| 175 | 
            +
             | 
| 176 | 
            +
                    @mutex.synchronize do
         | 
| 177 | 
            +
                      @aggregation_state[key] = value
         | 
| 178 | 
            +
                    end
         | 
| 179 | 
            +
                  end
         | 
| 180 | 
            +
             | 
| 181 | 
            +
                  def flush
         | 
| 182 | 
            +
                    @mutex.synchronize { do_flush }
         | 
| 183 | 
            +
                  end
         | 
| 184 | 
            +
             | 
| 185 | 
            +
                  private
         | 
| 186 | 
            +
             | 
| 187 | 
            +
                  EMPTY_ARRAY = [].freeze
         | 
| 188 | 
            +
             | 
| 189 | 
            +
                  def do_flush
         | 
| 190 | 
            +
                    @aggregation_state.each do |key, value|
         | 
| 191 | 
            +
                      case key.type
         | 
| 192 | 
            +
                      when COUNT
         | 
| 193 | 
            +
                        @sink << datagram_builder(no_prefix: key.no_prefix).c(
         | 
| 194 | 
            +
                          key.name,
         | 
| 195 | 
            +
                          value,
         | 
| 196 | 
            +
                          CONST_SAMPLE_RATE,
         | 
| 197 | 
            +
                          key.tags,
         | 
| 198 | 
            +
                        )
         | 
| 199 | 
            +
                      when DISTRIBUTION, MEASURE, HISTOGRAM
         | 
| 200 | 
            +
                        @sink << datagram_builder(no_prefix: key.no_prefix).timing_value_packed(
         | 
| 201 | 
            +
                          key.name,
         | 
| 202 | 
            +
                          key.type.to_s,
         | 
| 203 | 
            +
                          value,
         | 
| 204 | 
            +
                          CONST_SAMPLE_RATE,
         | 
| 205 | 
            +
                          key.tags,
         | 
| 206 | 
            +
                        )
         | 
| 207 | 
            +
                      when GAUGE
         | 
| 208 | 
            +
                        @sink << datagram_builder(no_prefix: key.no_prefix).g(
         | 
| 209 | 
            +
                          key.name,
         | 
| 210 | 
            +
                          value,
         | 
| 211 | 
            +
                          CONST_SAMPLE_RATE,
         | 
| 212 | 
            +
                          key.tags,
         | 
| 213 | 
            +
                        )
         | 
| 214 | 
            +
                      else
         | 
| 215 | 
            +
                        StatsD.logger.error { "[#{self.class.name}] Unknown aggregation type: #{key.type}" }
         | 
| 216 | 
            +
                      end
         | 
| 217 | 
            +
                    end
         | 
| 218 | 
            +
                    @aggregation_state.clear
         | 
| 219 | 
            +
                  end
         | 
| 220 | 
            +
             | 
| 221 | 
            +
                  def tags_sorted(tags)
         | 
| 222 | 
            +
                    return "" if tags.nil? || tags.empty?
         | 
| 223 | 
            +
             | 
| 224 | 
            +
                    if tags.is_a?(Hash)
         | 
| 225 | 
            +
                      tags = tags.sort_by { |k, _v| k.to_s }.map! { |k, v| "#{k}:#{v}" }
         | 
| 226 | 
            +
                    else
         | 
| 227 | 
            +
                      tags.sort!
         | 
| 228 | 
            +
                    end
         | 
| 229 | 
            +
                    datagram_builder(no_prefix: false).normalize_tags(tags)
         | 
| 230 | 
            +
                  end
         | 
| 231 | 
            +
             | 
| 232 | 
            +
                  def packet_key(name, tags = "".b, no_prefix = false, type = COUNT)
         | 
| 233 | 
            +
                    AggregationKey.new(DatagramBuilder.normalize_string(name), tags, no_prefix, type).freeze
         | 
| 234 | 
            +
                  end
         | 
| 235 | 
            +
             | 
| 236 | 
            +
                  def datagram_builder(no_prefix:)
         | 
| 237 | 
            +
                    @datagram_builders[no_prefix] ||= @datagram_builder_class.new(
         | 
| 238 | 
            +
                      prefix: no_prefix ? nil : @metric_prefix,
         | 
| 239 | 
            +
                      default_tags: @default_tags,
         | 
| 240 | 
            +
                    )
         | 
| 241 | 
            +
                  end
         | 
| 242 | 
            +
             | 
| 243 | 
            +
                  def thread_healthcheck
         | 
| 244 | 
            +
                    @mutex.synchronize do
         | 
| 245 | 
            +
                      unless @flush_thread&.alive?
         | 
| 246 | 
            +
                        return false unless Thread.main.alive?
         | 
| 247 | 
            +
             | 
| 248 | 
            +
                        if @pid != Process.pid
         | 
| 249 | 
            +
                          StatsD.logger.info { "[#{self.class.name}] Restarting the flush thread after fork" }
         | 
| 250 | 
            +
                          @pid = Process.pid
         | 
| 251 | 
            +
                          @aggregation_state.clear
         | 
| 252 | 
            +
                        else
         | 
| 253 | 
            +
                          StatsD.logger.info { "[#{self.class.name}] Restarting the flush thread" }
         | 
| 254 | 
            +
                        end
         | 
| 255 | 
            +
                        @flush_thread = Thread.new do
         | 
| 256 | 
            +
                          Thread.current.abort_on_exception = true
         | 
| 257 | 
            +
                          loop do
         | 
| 258 | 
            +
                            sleep(@flush_interval)
         | 
| 259 | 
            +
                            thread_healthcheck
         | 
| 260 | 
            +
                            flush
         | 
| 261 | 
            +
                          end
         | 
| 262 | 
            +
                        end
         | 
| 263 | 
            +
                      end
         | 
| 264 | 
            +
                      true
         | 
| 265 | 
            +
                    end
         | 
| 266 | 
            +
                  end
         | 
| 267 | 
            +
                end
         | 
| 268 | 
            +
              end
         | 
| 269 | 
            +
            end
         | 
| @@ -1,22 +1,30 @@ | |
| 1 1 | 
             
            # frozen_string_literal: true
         | 
| 2 2 |  | 
| 3 | 
            +
            require "forwardable"
         | 
| 4 | 
            +
             | 
| 3 5 | 
             
            module StatsD
         | 
| 4 6 | 
             
              module Instrument
         | 
| 5 | 
            -
                 | 
| 6 | 
            -
             | 
| 7 | 
            -
             | 
| 7 | 
            +
                class BatchedSink
         | 
| 8 | 
            +
                  extend Forwardable
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                  def_delegator :@sink, :host
         | 
| 11 | 
            +
                  def_delegator :@sink, :port
         | 
| 12 | 
            +
             | 
| 8 13 | 
             
                  DEFAULT_THREAD_PRIORITY = 100
         | 
| 9 14 | 
             
                  DEFAULT_BUFFER_CAPACITY = 5_000
         | 
| 10 15 | 
             
                  # https://docs.datadoghq.com/developers/dogstatsd/high_throughput/?code-lang=ruby#ensure-proper-packet-sizes
         | 
| 11 16 | 
             
                  DEFAULT_MAX_PACKET_SIZE = 1472
         | 
| 12 17 | 
             
                  DEFAULT_STATISTICS_INTERVAL = 0 # in seconds, and 0 implies disabled-by-default.
         | 
| 13 18 |  | 
| 14 | 
            -
                  attr_reader :host, :port
         | 
| 15 | 
            -
             | 
| 16 19 | 
             
                  class << self
         | 
| 17 20 | 
             
                    def for_addr(addr, **kwargs)
         | 
| 18 | 
            -
                       | 
| 19 | 
            -
             | 
| 21 | 
            +
                      if addr.include?(":")
         | 
| 22 | 
            +
                        sink = StatsD::Instrument::Sink.for_addr(addr)
         | 
| 23 | 
            +
                        new(sink, **kwargs)
         | 
| 24 | 
            +
                      else
         | 
| 25 | 
            +
                        connection = UdsConnection.new(addr)
         | 
| 26 | 
            +
                        new(connection, **kwargs)
         | 
| 27 | 
            +
                      end
         | 
| 20 28 | 
             
                    end
         | 
| 21 29 |  | 
| 22 30 | 
             
                    def finalize(dispatcher)
         | 
| @@ -25,18 +33,15 @@ module StatsD | |
| 25 33 | 
             
                  end
         | 
| 26 34 |  | 
| 27 35 | 
             
                  def initialize(
         | 
| 28 | 
            -
                     | 
| 29 | 
            -
                    port,
         | 
| 36 | 
            +
                    sink,
         | 
| 30 37 | 
             
                    thread_priority: DEFAULT_THREAD_PRIORITY,
         | 
| 31 38 | 
             
                    buffer_capacity: DEFAULT_BUFFER_CAPACITY,
         | 
| 32 39 | 
             
                    max_packet_size: DEFAULT_MAX_PACKET_SIZE,
         | 
| 33 40 | 
             
                    statistics_interval: DEFAULT_STATISTICS_INTERVAL
         | 
| 34 41 | 
             
                  )
         | 
| 35 | 
            -
                    @ | 
| 36 | 
            -
                    @port = port
         | 
| 42 | 
            +
                    @sink = sink
         | 
| 37 43 | 
             
                    @dispatcher = Dispatcher.new(
         | 
| 38 | 
            -
                       | 
| 39 | 
            -
                      port,
         | 
| 44 | 
            +
                      @sink,
         | 
| 40 45 | 
             
                      buffer_capacity,
         | 
| 41 46 | 
             
                      thread_priority,
         | 
| 42 47 | 
             
                      max_packet_size,
         | 
| @@ -62,6 +67,10 @@ module StatsD | |
| 62 67 | 
             
                    @dispatcher.flush(blocking: blocking)
         | 
| 63 68 | 
             
                  end
         | 
| 64 69 |  | 
| 70 | 
            +
                  def connection
         | 
| 71 | 
            +
                    @sink.connection
         | 
| 72 | 
            +
                  end
         | 
| 73 | 
            +
             | 
| 65 74 | 
             
                  class Buffer < SizedQueue
         | 
| 66 75 | 
             
                    def push_nonblock(item)
         | 
| 67 76 | 
             
                      push(item, true)
         | 
| @@ -81,7 +90,7 @@ module StatsD | |
| 81 90 | 
             
                  end
         | 
| 82 91 |  | 
| 83 92 | 
             
                  class DispatcherStats
         | 
| 84 | 
            -
                    def initialize(interval)
         | 
| 93 | 
            +
                    def initialize(interval, type)
         | 
| 85 94 | 
             
                      # The number of times the batched udp sender needed to
         | 
| 86 95 | 
             
                      # send a statsd line synchronously, due to the buffer
         | 
| 87 96 | 
             
                      # being full.
         | 
| @@ -98,6 +107,12 @@ module StatsD | |
| 98 107 | 
             
                      # The average number of statsd lines per batch.
         | 
| 99 108 | 
             
                      @avg_batch_length = 0
         | 
| 100 109 |  | 
| 110 | 
            +
                      @sync_sends_metric = "statsd_instrument.batched_#{type}_sink.synchronous_sends"
         | 
| 111 | 
            +
                      @batched_sends_metric = "statsd_instrument.batched_#{type}_sink.batched_sends"
         | 
| 112 | 
            +
                      @avg_buffer_length_metric = "statsd_instrument.batched_#{type}_sink.avg_buffer_length"
         | 
| 113 | 
            +
                      @avg_batched_packet_size_metric = "statsd_instrument.batched_#{type}_sink.avg_batched_packet_size"
         | 
| 114 | 
            +
                      @avg_batch_length_metric = "statsd_instrument.batched_#{type}_sink.avg_batch_length"
         | 
| 115 | 
            +
             | 
| 101 116 | 
             
                      @mutex = Mutex.new
         | 
| 102 117 |  | 
| 103 118 | 
             
                      @interval = interval
         | 
| @@ -121,11 +136,11 @@ module StatsD | |
| 121 136 | 
             
                        @since = Process.clock_gettime(Process::CLOCK_MONOTONIC)
         | 
| 122 137 | 
             
                      end
         | 
| 123 138 |  | 
| 124 | 
            -
                      StatsD.increment( | 
| 125 | 
            -
                      StatsD.increment( | 
| 126 | 
            -
                      StatsD.gauge( | 
| 127 | 
            -
                      StatsD.gauge( | 
| 128 | 
            -
                      StatsD.gauge( | 
| 139 | 
            +
                      StatsD.increment(@sync_sends_metric, synchronous_sends)
         | 
| 140 | 
            +
                      StatsD.increment(@batched_sends_metric, batched_sends)
         | 
| 141 | 
            +
                      StatsD.gauge(@avg_buffer_length_metric, avg_buffer_length)
         | 
| 142 | 
            +
                      StatsD.gauge(@avg_batched_packet_size_metric, avg_batched_packet_size)
         | 
| 143 | 
            +
                      StatsD.gauge(@avg_batch_length_metric, avg_batch_length)
         | 
| 129 144 | 
             
                    end
         | 
| 130 145 |  | 
| 131 146 | 
             
                    def increment_synchronous_sends
         | 
| @@ -143,8 +158,8 @@ module StatsD | |
| 143 158 | 
             
                  end
         | 
| 144 159 |  | 
| 145 160 | 
             
                  class Dispatcher
         | 
| 146 | 
            -
                    def initialize( | 
| 147 | 
            -
                      @ | 
| 161 | 
            +
                    def initialize(sink, buffer_capacity, thread_priority, max_packet_size, statistics_interval)
         | 
| 162 | 
            +
                      @sink = sink
         | 
| 148 163 | 
             
                      @interrupted = false
         | 
| 149 164 | 
             
                      @thread_priority = thread_priority
         | 
| 150 165 | 
             
                      @max_packet_size = max_packet_size
         | 
| @@ -153,7 +168,8 @@ module StatsD | |
| 153 168 | 
             
                      @dispatcher_thread = Thread.new { dispatch }
         | 
| 154 169 | 
             
                      @pid = Process.pid
         | 
| 155 170 | 
             
                      if statistics_interval > 0
         | 
| 156 | 
            -
                         | 
| 171 | 
            +
                        type = @sink.connection.type
         | 
| 172 | 
            +
                        @statistics = DispatcherStats.new(statistics_interval, type)
         | 
| 157 173 | 
             
                      end
         | 
| 158 174 | 
             
                    end
         | 
| 159 175 |  | 
| @@ -161,7 +177,7 @@ module StatsD | |
| 161 177 | 
             
                      if !thread_healthcheck || !@buffer.push_nonblock(datagram)
         | 
| 162 178 | 
             
                        # The buffer is full or the thread can't be respawned,
         | 
| 163 179 | 
             
                        # we'll send the datagram synchronously
         | 
| 164 | 
            -
                        @ | 
| 180 | 
            +
                        @sink << datagram
         | 
| 165 181 |  | 
| 166 182 | 
             
                        @statistics&.increment_synchronous_sends
         | 
| 167 183 | 
             
                      end
         | 
| @@ -206,7 +222,7 @@ module StatsD | |
| 206 222 | 
             
                        end
         | 
| 207 223 |  | 
| 208 224 | 
             
                        packet_size = packet.bytesize
         | 
| 209 | 
            -
                        @ | 
| 225 | 
            +
                        @sink << packet
         | 
| 210 226 | 
             
                        packet.clear
         | 
| 211 227 |  | 
| 212 228 | 
             
                        @statistics&.increment_batched_sends(buffer_len, packet_size, batch_len)
         | 
| @@ -40,6 +40,8 @@ module StatsD | |
| 40 40 | 
             
                        implementation: implementation,
         | 
| 41 41 | 
             
                        sink: sink,
         | 
| 42 42 | 
             
                        datagram_builder_class: datagram_builder_class,
         | 
| 43 | 
            +
                        enable_aggregation: env.experimental_aggregation_enabled?,
         | 
| 44 | 
            +
                        aggregation_flush_interval: env.aggregation_interval,
         | 
| 43 45 | 
             
                      )
         | 
| 44 46 | 
             
                    end
         | 
| 45 47 |  | 
| @@ -82,7 +84,7 @@ module StatsD | |
| 82 84 | 
             
                  # Generally, you should use an instance of one of the following classes that
         | 
| 83 85 | 
             
                  # ship with this library:
         | 
| 84 86 | 
             
                  #
         | 
| 85 | 
            -
                  # - {StatsD::Instrument:: | 
| 87 | 
            +
                  # - {StatsD::Instrument::Sink} A sink that will actually emit the provided
         | 
| 86 88 | 
             
                  #   datagrams over UDP.
         | 
| 87 89 | 
             
                  # - {StatsD::Instrument::NullSink} A sink that will simply swallow every
         | 
| 88 90 | 
             
                  #   datagram. This sink is for use when testing your application.
         | 
| @@ -152,7 +154,10 @@ module StatsD | |
| 152 154 | 
             
                    default_tags: nil,
         | 
| 153 155 | 
             
                    implementation: "datadog",
         | 
| 154 156 | 
             
                    sink: StatsD::Instrument::NullSink.new,
         | 
| 155 | 
            -
                    datagram_builder_class: self.class.datagram_builder_class_for_implementation(implementation)
         | 
| 157 | 
            +
                    datagram_builder_class: self.class.datagram_builder_class_for_implementation(implementation),
         | 
| 158 | 
            +
                    enable_aggregation: false,
         | 
| 159 | 
            +
                    aggregation_flush_interval: 2.0,
         | 
| 160 | 
            +
                    aggregation_max_context_size: StatsD::Instrument::Aggregator::DEFAULT_MAX_CONTEXT_SIZE
         | 
| 156 161 | 
             
                  )
         | 
| 157 162 | 
             
                    @sink = sink
         | 
| 158 163 | 
             
                    @datagram_builder_class = datagram_builder_class
         | 
| @@ -162,6 +167,19 @@ module StatsD | |
| 162 167 | 
             
                    @default_sample_rate = default_sample_rate
         | 
| 163 168 |  | 
| 164 169 | 
             
                    @datagram_builder = { false => nil, true => nil }
         | 
| 170 | 
            +
                    @enable_aggregation = enable_aggregation
         | 
| 171 | 
            +
                    @aggregation_flush_interval = aggregation_flush_interval
         | 
| 172 | 
            +
                    if @enable_aggregation
         | 
| 173 | 
            +
                      @aggregator =
         | 
| 174 | 
            +
                        Aggregator.new(
         | 
| 175 | 
            +
                          @sink,
         | 
| 176 | 
            +
                          datagram_builder_class,
         | 
| 177 | 
            +
                          prefix,
         | 
| 178 | 
            +
                          default_tags,
         | 
| 179 | 
            +
                          flush_interval: @aggregation_flush_interval,
         | 
| 180 | 
            +
                          max_values: aggregation_max_context_size,
         | 
| 181 | 
            +
                        )
         | 
| 182 | 
            +
                    end
         | 
| 165 183 | 
             
                  end
         | 
| 166 184 |  | 
| 167 185 | 
             
                  # @!group Metric Methods
         | 
| @@ -201,6 +219,12 @@ module StatsD | |
| 201 219 | 
             
                  # @return [void]
         | 
| 202 220 | 
             
                  def increment(name, value = 1, sample_rate: nil, tags: nil, no_prefix: false)
         | 
| 203 221 | 
             
                    sample_rate ||= @default_sample_rate
         | 
| 222 | 
            +
             | 
| 223 | 
            +
                    if @enable_aggregation
         | 
| 224 | 
            +
                      @aggregator.increment(name, value, tags: tags, no_prefix: no_prefix)
         | 
| 225 | 
            +
                      return StatsD::Instrument::VOID
         | 
| 226 | 
            +
                    end
         | 
| 227 | 
            +
             | 
| 204 228 | 
             
                    if sample_rate.nil? || sample?(sample_rate)
         | 
| 205 229 | 
             
                      emit(datagram_builder(no_prefix: no_prefix).c(name, value, sample_rate, tags))
         | 
| 206 230 | 
             
                    end
         | 
| @@ -215,14 +239,28 @@ module StatsD | |
| 215 239 | 
             
                  # @param tags (see #increment)
         | 
| 216 240 | 
             
                  # @return [void]
         | 
| 217 241 | 
             
                  def measure(name, value = nil, sample_rate: nil, tags: nil, no_prefix: false, &block)
         | 
| 242 | 
            +
                    sample_rate ||= @default_sample_rate
         | 
| 243 | 
            +
                    if sample_rate && !sample?(sample_rate)
         | 
| 244 | 
            +
                      # For all timing metrics, we have to use the sampling logic.
         | 
| 245 | 
            +
                      # Not doing so would impact performance and CPU usage.
         | 
| 246 | 
            +
                      # See Datadog's documentation for more details: https://github.com/DataDog/datadog-go/blob/20af2dbfabbbe6bd0347780cd57ed931f903f223/statsd/aggregator.go#L281-L283
         | 
| 247 | 
            +
             | 
| 248 | 
            +
                      if block_given?
         | 
| 249 | 
            +
                        return yield
         | 
| 250 | 
            +
                      end
         | 
| 251 | 
            +
             | 
| 252 | 
            +
                      return StatsD::Instrument::VOID
         | 
| 253 | 
            +
                    end
         | 
| 254 | 
            +
             | 
| 218 255 | 
             
                    if block_given?
         | 
| 219 256 | 
             
                      return latency(name, sample_rate: sample_rate, tags: tags, metric_type: :ms, no_prefix: no_prefix, &block)
         | 
| 220 257 | 
             
                    end
         | 
| 221 258 |  | 
| 222 | 
            -
                     | 
| 223 | 
            -
             | 
| 224 | 
            -
                       | 
| 259 | 
            +
                    if @enable_aggregation
         | 
| 260 | 
            +
                      @aggregator.aggregate_timing(name, value, tags: tags, no_prefix: no_prefix, type: :ms)
         | 
| 261 | 
            +
                      return StatsD::Instrument::VOID
         | 
| 225 262 | 
             
                    end
         | 
| 263 | 
            +
                    emit(datagram_builder(no_prefix: no_prefix).ms(name, value, sample_rate, tags))
         | 
| 226 264 | 
             
                    StatsD::Instrument::VOID
         | 
| 227 265 | 
             
                  end
         | 
| 228 266 |  | 
| @@ -240,6 +278,11 @@ module StatsD | |
| 240 278 | 
             
                  # @param tags (see #increment)
         | 
| 241 279 | 
             
                  # @return [void]
         | 
| 242 280 | 
             
                  def gauge(name, value, sample_rate: nil, tags: nil, no_prefix: false)
         | 
| 281 | 
            +
                    if @enable_aggregation
         | 
| 282 | 
            +
                      @aggregator.gauge(name, value, tags: tags, no_prefix: no_prefix)
         | 
| 283 | 
            +
                      return StatsD::Instrument::VOID
         | 
| 284 | 
            +
                    end
         | 
| 285 | 
            +
             | 
| 243 286 | 
             
                    sample_rate ||= @default_sample_rate
         | 
| 244 287 | 
             
                    if sample_rate.nil? || sample?(sample_rate)
         | 
| 245 288 | 
             
                      emit(datagram_builder(no_prefix: no_prefix).g(name, value, sample_rate, tags))
         | 
| @@ -275,14 +318,29 @@ module StatsD | |
| 275 318 | 
             
                  # @param tags (see #increment)
         | 
| 276 319 | 
             
                  # @return [void]
         | 
| 277 320 | 
             
                  def distribution(name, value = nil, sample_rate: nil, tags: nil, no_prefix: false, &block)
         | 
| 321 | 
            +
                    sample_rate ||= @default_sample_rate
         | 
| 322 | 
            +
                    if sample_rate && !sample?(sample_rate)
         | 
| 323 | 
            +
                      # For all timing metrics, we have to use the sampling logic.
         | 
| 324 | 
            +
                      # Not doing so would impact performance and CPU usage.
         | 
| 325 | 
            +
                      # See Datadog's documentation for more details: https://github.com/DataDog/datadog-go/blob/20af2dbfabbbe6bd0347780cd57ed931f903f223/statsd/aggregator.go#L281-L283
         | 
| 326 | 
            +
             | 
| 327 | 
            +
                      if block_given?
         | 
| 328 | 
            +
                        return yield
         | 
| 329 | 
            +
                      end
         | 
| 330 | 
            +
             | 
| 331 | 
            +
                      return StatsD::Instrument::VOID
         | 
| 332 | 
            +
                    end
         | 
| 333 | 
            +
             | 
| 278 334 | 
             
                    if block_given?
         | 
| 279 335 | 
             
                      return latency(name, sample_rate: sample_rate, tags: tags, metric_type: :d, no_prefix: no_prefix, &block)
         | 
| 280 336 | 
             
                    end
         | 
| 281 337 |  | 
| 282 | 
            -
                     | 
| 283 | 
            -
             | 
| 284 | 
            -
                       | 
| 338 | 
            +
                    if @enable_aggregation
         | 
| 339 | 
            +
                      @aggregator.aggregate_timing(name, value, tags: tags, no_prefix: no_prefix, type: :d)
         | 
| 340 | 
            +
                      return StatsD::Instrument::VOID
         | 
| 285 341 | 
             
                    end
         | 
| 342 | 
            +
             | 
| 343 | 
            +
                    emit(datagram_builder(no_prefix: no_prefix).d(name, value, sample_rate, tags))
         | 
| 286 344 | 
             
                    StatsD::Instrument::VOID
         | 
| 287 345 | 
             
                  end
         | 
| 288 346 |  | 
| @@ -299,9 +357,19 @@ module StatsD | |
| 299 357 | 
             
                  # @return [void]
         | 
| 300 358 | 
             
                  def histogram(name, value, sample_rate: nil, tags: nil, no_prefix: false)
         | 
| 301 359 | 
             
                    sample_rate ||= @default_sample_rate
         | 
| 302 | 
            -
                    if sample_rate | 
| 303 | 
            -
                       | 
| 360 | 
            +
                    if sample_rate && !sample?(sample_rate)
         | 
| 361 | 
            +
                      # For all timing metrics, we have to use the sampling logic.
         | 
| 362 | 
            +
                      # Not doing so would impact performance and CPU usage.
         | 
| 363 | 
            +
                      # See Datadog's documentation for more details: https://github.com/DataDog/datadog-go/blob/20af2dbfabbbe6bd0347780cd57ed931f903f223/statsd/aggregator.go#L281-L283
         | 
| 364 | 
            +
                      return StatsD::Instrument::VOID
         | 
| 365 | 
            +
                    end
         | 
| 366 | 
            +
             | 
| 367 | 
            +
                    if @enable_aggregation
         | 
| 368 | 
            +
                      @aggregator.aggregate_timing(name, value, tags: tags, no_prefix: no_prefix, type: :h)
         | 
| 369 | 
            +
                      return StatsD::Instrument::VOID
         | 
| 304 370 | 
             
                    end
         | 
| 371 | 
            +
             | 
| 372 | 
            +
                    emit(datagram_builder(no_prefix: no_prefix).h(name, value, sample_rate, tags))
         | 
| 305 373 | 
             
                    StatsD::Instrument::VOID
         | 
| 306 374 | 
             
                  end
         | 
| 307 375 |  | 
| @@ -324,11 +392,15 @@ module StatsD | |
| 324 392 | 
             
                    ensure
         | 
| 325 393 | 
             
                      stop = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond)
         | 
| 326 394 |  | 
| 327 | 
            -
                       | 
| 328 | 
            -
                       | 
| 329 | 
            -
             | 
| 330 | 
            -
                        latency_in_ms  | 
| 331 | 
            -
             | 
| 395 | 
            +
                      metric_type ||= datagram_builder(no_prefix: no_prefix).latency_metric_type
         | 
| 396 | 
            +
                      latency_in_ms = stop - start
         | 
| 397 | 
            +
                      if @enable_aggregation
         | 
| 398 | 
            +
                        @aggregator.aggregate_timing(name, latency_in_ms, tags: tags, no_prefix: no_prefix, type: metric_type)
         | 
| 399 | 
            +
                      else
         | 
| 400 | 
            +
                        sample_rate ||= @default_sample_rate
         | 
| 401 | 
            +
                        if sample_rate.nil? || sample?(sample_rate)
         | 
| 402 | 
            +
                          emit(datagram_builder(no_prefix: no_prefix).send(metric_type, name, latency_in_ms, sample_rate, tags))
         | 
| 403 | 
            +
                        end
         | 
| 332 404 | 
             
                      end
         | 
| 333 405 | 
             
                    end
         | 
| 334 406 | 
             
                  end
         | 
| @@ -386,6 +458,18 @@ module StatsD | |
| 386 458 | 
             
                    ))
         | 
| 387 459 | 
             
                  end
         | 
| 388 460 |  | 
| 461 | 
            +
                  # Forces the client to flush all metrics that are currently buffered, first flushes the aggregation
         | 
| 462 | 
            +
                  # if enabled.
         | 
| 463 | 
            +
                  #
         | 
| 464 | 
            +
                  # @return [void]
         | 
| 465 | 
            +
                  def force_flush
         | 
| 466 | 
            +
                    if @enable_aggregation
         | 
| 467 | 
            +
                      @aggregator.flush
         | 
| 468 | 
            +
                    end
         | 
| 469 | 
            +
                    @sink.flush(blocking: false)
         | 
| 470 | 
            +
                    StatsD::Instrument::VOID
         | 
| 471 | 
            +
                  end
         | 
| 472 | 
            +
             | 
| 389 473 | 
             
                  NO_CHANGE = Object.new
         | 
| 390 474 |  | 
| 391 475 | 
             
                  # Instantiates a new StatsD client that uses the settings of the current client,
         | 
| @@ -427,6 +511,8 @@ module StatsD | |
| 427 511 | 
             
                      default_tags: default_tags == NO_CHANGE ? @default_tags : default_tags,
         | 
| 428 512 | 
             
                      datagram_builder_class:
         | 
| 429 513 | 
             
                        datagram_builder_class == NO_CHANGE ? @datagram_builder_class : datagram_builder_class,
         | 
| 514 | 
            +
                      enable_aggregation: @enable_aggregation,
         | 
| 515 | 
            +
                      aggregation_flush_interval: @aggregation_flush_interval,
         | 
| 430 516 | 
             
                    )
         | 
| 431 517 | 
             
                  end
         | 
| 432 518 |  | 
| @@ -31,7 +31,11 @@ module StatsD | |
| 31 31 | 
             
                    when :c
         | 
| 32 32 | 
             
                      Integer(parsed_datagram[:value])
         | 
| 33 33 | 
             
                    when :g, :h, :d, :kv, :ms
         | 
| 34 | 
            -
                       | 
| 34 | 
            +
                      if parsed_datagram[:value].include?(":")
         | 
| 35 | 
            +
                        parsed_datagram[:value].split(":").map { |v| Float(v) }
         | 
| 36 | 
            +
                      else
         | 
| 37 | 
            +
                        Float(parsed_datagram[:value])
         | 
| 38 | 
            +
                      end
         | 
| 35 39 | 
             
                    when :s
         | 
| 36 40 | 
             
                      String(parsed_datagram[:value])
         | 
| 37 41 | 
             
                    else
         | 
| @@ -68,7 +72,7 @@ module StatsD | |
| 68 72 |  | 
| 69 73 | 
             
                  PARSER = %r{
         | 
| 70 74 | 
             
                    \A
         | 
| 71 | 
            -
                    (?<name>[^\:\|\@]+)\:(?<value>[^\:\|\@]+)\|(?<type>c|ms|g|s|h|d)
         | 
| 75 | 
            +
                    (?<name>[^\:\|\@]+)\:(?<value>(?:[^\:\|\@]+:)*[^\:\|\@]+)\|(?<type>c|ms|g|s|h|d)
         | 
| 72 76 | 
             
                    (?:\|\@(?<sample_rate>\d*(?:\.\d*)?))?
         | 
| 73 77 | 
             
                    (?:\|\#(?<tags>(?:[^\|,]+(?:,[^\|,]+)*)))?
         | 
| 74 78 | 
             
                    \n? # In some implementations, the datagram may include a trailing newline.
         | 
| @@ -5,6 +5,7 @@ module StatsD | |
| 5 5 | 
             
                # @note This class is part of the new Client implementation that is intended
         | 
| 6 6 | 
             
                #   to become the new default in the next major release of this library.
         | 
| 7 7 | 
             
                class DatagramBuilder
         | 
| 8 | 
            +
                  extend Forwardable
         | 
| 8 9 | 
             
                  class << self
         | 
| 9 10 | 
             
                    def unsupported_datagram_types(*types)
         | 
| 10 11 | 
             
                      types.each do |type|
         | 
| @@ -17,6 +18,11 @@ module StatsD | |
| 17 18 | 
             
                    def datagram_class
         | 
| 18 19 | 
             
                      StatsD::Instrument::Datagram
         | 
| 19 20 | 
             
                    end
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                    def normalize_string(string)
         | 
| 23 | 
            +
                      string = string.tr("|#", "_") if /[|#]/.match?(string)
         | 
| 24 | 
            +
                      string
         | 
| 25 | 
            +
                    end
         | 
| 20 26 | 
             
                  end
         | 
| 21 27 |  | 
| 22 28 | 
             
                  def initialize(prefix: nil, default_tags: nil)
         | 
| @@ -48,6 +54,12 @@ module StatsD | |
| 48 54 | 
             
                    generate_generic_datagram(name, value, "d", sample_rate, tags)
         | 
| 49 55 | 
             
                  end
         | 
| 50 56 |  | 
| 57 | 
            +
                  def timing_value_packed(name, type, values, sample_rate, tags)
         | 
| 58 | 
            +
                    # here values is an array
         | 
| 59 | 
            +
                    values = values.join(":")
         | 
| 60 | 
            +
                    generate_generic_datagram(name, values, type, sample_rate, tags)
         | 
| 61 | 
            +
                  end
         | 
| 62 | 
            +
             | 
| 51 63 | 
             
                  def kv(name, value, sample_rate, tags)
         | 
| 52 64 | 
             
                    generate_generic_datagram(name, value, "kv", sample_rate, tags)
         | 
| 53 65 | 
             
                  end
         | 
| @@ -56,6 +68,10 @@ module StatsD | |
| 56 68 | 
             
                    :ms
         | 
| 57 69 | 
             
                  end
         | 
| 58 70 |  | 
| 71 | 
            +
                  def normalize_tags(tags, buffer = "".b)
         | 
| 72 | 
            +
                    compile_tags(tags, buffer)
         | 
| 73 | 
            +
                  end
         | 
| 74 | 
            +
             | 
| 59 75 | 
             
                  protected
         | 
| 60 76 |  | 
| 61 77 | 
             
                  # Utility function to remove invalid characters from a StatsD metric name
         | 
| @@ -88,6 +104,11 @@ module StatsD | |
| 88 104 | 
             
                  end
         | 
| 89 105 |  | 
| 90 106 | 
             
                  def compile_tags(tags, buffer = "".b)
         | 
| 107 | 
            +
                    if tags.is_a?(String)
         | 
| 108 | 
            +
                      tags = self.class.normalize_string(tags) if /[|,]/.match?(tags)
         | 
| 109 | 
            +
                      buffer << tags
         | 
| 110 | 
            +
                      return buffer
         | 
| 111 | 
            +
                    end
         | 
| 91 112 | 
             
                    if tags.is_a?(Hash)
         | 
| 92 113 | 
             
                      first = true
         | 
| 93 114 | 
             
                      tags.each do |key, value|
         |