Code Monkey home page Code Monkey logo

statsd-instrument's Introduction

StatsD client for Ruby apps

This is a ruby client for statsd (https://github.com/statsd/statsd). It provides a lightweight way to track and measure metrics in your application.

We call out to statsd by sending data over a UDP socket. UDP sockets are fast, but unreliable, there is no guarantee that your data will ever arrive at its location. In other words, fire and forget. This is perfect for this use case because it means your code doesn't get bogged down trying to log statistics. We send data to statsd several times per request and haven't noticed a performance hit.

For more information about StatsD, see the README of the StatsD project.

Configuration

It's recommended to configure this library by setting environment variables. The following environment variables are supported:

  • STATSD_ADDR: (default localhost:8125) The address to send the StatsD UDP datagrams to.

  • STATSD_IMPLEMENTATION: (default: datadog). The StatsD implementation you are using. statsd and datadog are supported. Some features are only available on certain implementations,

  • STATSD_ENV: The environment StatsD will run in. If this is not set explicitly, this will be determined based on other environment variables, like RAILS_ENV or ENV. The library will behave differently:

    • In the production and staging environment, the library will actually send UDP packets.
    • In the test environment, it will swallow all calls, but allows you to capture them for testing purposes. See below for notes on writing tests.
    • In development and all other environments, it will write all calls to the log (StatsD.logger, which by default writes to STDOUT).
  • STATSD_SAMPLE_RATE: (default: 1.0) The default sample rate to use for all metrics. This can be used to reduce the amount of network traffic and CPU overhead the usage of this library generates. This can be overridden in a metric method call.

  • STATSD_PREFIX: The prefix to apply to all metric names. This can be overridden in a metric method call.

  • STATSD_DEFAULT_TAGS: A comma-separated list of tags to apply to all metrics. (Note: tags are not supported by all implementations.)

  • STATSD_BUFFER_CAPACITY: (default: 5000) The maximum amount of events that may be buffered before emitting threads will start to block. Increasing this value may help for application generating spikes of events. However if the application emit events faster than they can be sent, increasing it won't help. If set to 0, batching will be disabled, and events will be sent in individual UDP packets, which is much slower.

  • STATSD_FLUSH_INTERVAL: (default: 1) Deprecated. Setting this to 0 is equivalent to setting STATSD_BUFFER_CAPACITY to 0.

  • STATSD_MAX_PACKET_SIZE: (default: 1472) The maximum size of UDP packets. If your network is properly configured to handle larger packets you may try to increase this value for better performance, but most network can't handle larger packets.

  • STATSD_BATCH_STATISTICS_INTERVAL: (default: "0") If non-zero, the BatchedUDPSink will track and emit statistics on this interval to the default sink for your environment. The current tracked statistics are:

    • statsd_instrument.batched_udp_sink.batched_sends: The number of batches sent, of any size.
    • statsd_instrument.batched_udp_sink.synchronous_sends: The number of times the batched udp sender needed to send a statsd line synchronously, due to the buffer being full.
    • statsd_instrument.batched_udp_sink.avg_buffer_length: The average buffer length, measured at the beginning of each batch.
    • statsd_instrument.batched_udp_sink.avg_batched_packet_size: The average per-batch byte size of the packet sent to the underlying UDPSink.
    • statsd_instrument.batched_udp_sink.avg_batch_length: The average number of statsd lines per batch.

StatsD keys

StatsD keys look like 'admin.logins.api.success'. Dots are used as namespace separators.

Usage

You can either use the basic methods to submit stats over StatsD, or you can use the metaprogramming methods to instrument your methods with some basic stats (call counts, successes & failures, and timings).

StatsD.measure

Lets you benchmark how long the execution of a specific method takes.

# You can pass a key and a ms value
StatsD.measure('GoogleBase.insert', 2.55)

# or more commonly pass a block that calls your code
StatsD.measure('GoogleBase.insert') do
  GoogleBase.insert(product)
end

StatsD.increment

Lets you increment a key in statsd to keep a count of something. If the specified key doesn't exist it will create it for you.

# increments default to +1
StatsD.increment('GoogleBase.insert')
# you can also specify how much to increment the key by
StatsD.increment('GoogleBase.insert', 10)
# you can also specify a sample rate, so only 1/10 of events
# actually get to statsd. Useful for very high volume data
StatsD.increment('GoogleBase.insert', sample_rate: 0.1)

StatsD.gauge

A gauge is a single numerical value that tells you the state of the system at a point in time. A good example would be the number of messages in a queue.

StatsD.gauge('GoogleBase.queued', 12, sample_rate: 1.0)

Normally, you shouldn't update this value too often, and therefore there is no need to sample this kind metric.

StatsD.set

A set keeps track of the number of unique values that have been seen. This is a good fit for keeping track of the number of unique visitors. The value can be a string.

# Submit the customer ID to the set. It will only be counted if it hasn't been seen before.
StatsD.set('GoogleBase.customers', "12345", sample_rate: 1.0)

Because you are counting unique values, the results of using a sampling value less than 1.0 can lead to unexpected, hard to interpret results.

StatsD.histogram

Builds a histogram of numeric values.

StatsD.histogram('Order.value', order.value_in_usd.to_f, tags: { source: 'POS' })

Because you are counting unique values, the results of using a sampling value less than 1.0 can lead to unexpected, hard to interpret results.

Note: This is only supported by the beta datadog implementation.

StatsD.distribution

A modified gauge that submits a distribution of values over a sample period. Arithmetic and statistical calculations (percentiles, average, etc.) on the data set are performed server side rather than client side like a histogram.

StatsD.distribution('shipit.redis_connection', 3)

Note: This is only supported by the beta datadog implementation.

StatsD.event

An event is a (title, text) tuple that can be used to correlate metrics with something that occurred within the system. This is a good fit for instance to correlate response time variation with a deploy of the new code.

StatsD.event('shipit.deploy', 'started')

Note: This is only supported by the datadog implementation.

Events support additional metadata such as date_happened, hostname, aggregation_key, priority, source_type_name, alert_type.

StatsD.service_check

An event is a (check_name, status) tuple that can be used to monitor the status of services your application depends on.

StatsD.service_check('shipit.redis_connection', 'ok')

Note: This is only supported by the datadog implementation.

Service checks support additional metadata such as timestamp, hostname, message.

Metaprogramming Methods

As mentioned, it's most common to use the provided metaprogramming methods. This lets you define all of your instrumentation in one file and not litter your code with instrumentation details. You should enable a class for instrumentation by extending it with the StatsD::Instrument class.

GoogleBase.extend StatsD::Instrument

Then use the methods provided below to instrument methods in your class.

statsd_measure

This will measure how long a method takes to run, and submits the result to the given key.

GoogleBase.statsd_measure :insert, 'GoogleBase.insert'

statsd_count

This will increment the given key even if the method doesn't finish (ie. raises).

GoogleBase.statsd_count :insert, 'GoogleBase.insert'

Note how I used the 'GoogleBase.insert' key above when measuring this method, and I reused here when counting the method calls. StatsD automatically separates these two kinds of stats into namespaces so there won't be a key collision here.

statsd_count_if

This will only increment the given key if the method executes successfully.

GoogleBase.statsd_count_if :insert, 'GoogleBase.insert'

So now, if GoogleBase#insert raises an exception or returns false (ie. result == false), we won't increment the key. If you want to define what success means for a given method you can pass a block that takes the result of the method.

GoogleBase.statsd_count_if :insert, 'GoogleBase.insert' do |response|
  response.code == 200
end

In the above example we will only increment the key in statsd if the result of the block returns true. So the method is returning a Net::HTTP response and we're checking the status code.

statsd_count_success

Similar to statsd_count_if, except this will increment one key in the case of success and another key in the case of failure.

GoogleBase.statsd_count_success :insert, 'GoogleBase.insert'

So if this method fails execution (raises or returns false) we'll increment the failure key ('GoogleBase.insert.failure'), otherwise we'll increment the success key ('GoogleBase.insert.success'). Notice that we're modifying the given key before sending it to statsd.

Again you can pass a block to define what success means.

GoogleBase.statsd_count_success :insert, 'GoogleBase.insert' do |response|
  response.code == 200
end

Instrumenting Class Methods

You can instrument class methods, just like instance methods, using the metaprogramming methods. You simply have to configure the instrumentation on the singleton class of the Class you want to instrument.

AWS::S3::Base.singleton_class.statsd_measure :request, 'S3.request'

Dynamic Metric Names

You can use a lambda function instead of a string dynamically set the name of the metric. The lambda function must accept two arguments: the object the function is being called on and the array of arguments passed.

GoogleBase.statsd_count :insert, lambda{|object, args| object.class.to_s.downcase + "." + args.first.to_s + ".insert" }

Tags

The Datadog implementation supports tags, which you can use to slice and dice metrics in their UI. You can specify a list of tags as an option, either standalone tag (e.g. "mytag"), or key value based, separated by a colon: "env:production".

StatsD.increment('my.counter', tags: ['env:production', 'unicorn'])
GoogleBase.statsd_count :insert, 'GoogleBase.insert', tags: ['env:production']

If implementation is not set to :datadog, tags will not be included in the UDP packets, and a warning is logged to StatsD.logger.

You can use lambda function that instead of a list of tags to set the metric tags. Like the dynamic metric name, the lambda function must accept two arguments: the object the function is being called on and the array of arguments passed.

metric_tagger = lambda { |object, args| { "key": args.first } }
GoogleBase.statsd_count(:insert, 'GoogleBase.insert', tags: metric_tagger)

You can only use the dynamic tag while using the instrumentation through metaprogramming methods

Testing

This library comes with a module called StatsD::Instrument::Assertions and StatsD::Instrument::Matchers to help you write tests to verify StatsD is called properly.

minitest

class MyTestcase < Minitest::Test
  include StatsD::Instrument::Assertions

  def test_some_metrics
    # This will pass if there is exactly one matching StatsD call
    # it will ignore any other, non matching calls.
    assert_statsd_increment('counter.name', sample_rate: 1.0) do
      StatsD.increment('unrelated') # doesn't match
      StatsD.increment('counter.name', sample_rate: 1.0) # matches
      StatsD.increment('counter.name', sample_rate: 0.1) # doesn't match
    end

    # Set `times` if there will be multiple matches:
    assert_statsd_increment('counter.name', times: 2) do
      StatsD.increment('unrelated') # doesn't match
      StatsD.increment('counter.name', sample_rate: 1.0) # matches
      StatsD.increment('counter.name', sample_rate: 0.1) # matches too
    end
  end

  def test_no_udp_traffic
    # Verifies no StatsD calls occurred at all.
    assert_no_statsd_calls do
      do_some_work
    end

    # Verifies no StatsD calls occurred for the given metric.
    assert_no_statsd_calls('metric_name') do
      do_some_work
    end
  end

  def test_more_complicated_stuff
    # capture_statsd_calls will capture all the StatsD calls in the
    # given block, and returns them as an array. You can then run your
    # own assertions on it.
    metrics = capture_statsd_calls do
      StatsD.increment('mycounter', sample_rate: 0.01)
    end

    assert_equal 1, metrics.length
    assert_equal 'mycounter', metrics[0].name
    assert_equal :c, metrics[0].type
    assert_equal 1, metrics[0].value
    assert_equal 0.01, metrics[0].sample_rate
  end
end

RSpec

RSpec.configure do |config|
  config.include StatsD::Instrument::Matchers
end

RSpec.describe 'Matchers' do
  context 'trigger_statsd_increment' do
    it 'will pass if there is exactly one matching StatsD call' do
      expect { StatsD.increment('counter') }.to trigger_statsd_increment('counter')
    end

    it 'will pass if it matches the correct number of times' do
      expect {
        2.times do
          StatsD.increment('counter')
        end
      }.to trigger_statsd_increment('counter', times: 2)
    end

    it 'will pass if it matches argument' do
      expect {
        StatsD.measure('counter', 0.3001)
      }.to trigger_statsd_measure('counter', value: be_between(0.29, 0.31))
    end

    it 'will pass if there is no matching StatsD call on negative expectation' do
      expect { StatsD.increment('other_counter') }.not_to trigger_statsd_increment('counter')
    end

    it 'will pass if every statsD call matches its call tag variations' do
      expect do
        StatsD.increment('counter', tags: ['variation:a'])
        StatsD.increment('counter', tags: ['variation:b'])
      end.to trigger_statsd_increment('counter', times: 1, tags: ["variation:a"]).and trigger_statsd_increment('counter', times: 1, tags: ["variation:b"])
    end
  end
end

Notes

Compatibility

The library is tested against Ruby 2.3 and higher. We are not testing on different Ruby implementations besides MRI, but we expect it to work on other implementations as well.

Reliance on DNS

Out of the box StatsD is set up to be unidirectional fire-and-forget over UDP. Configuring the StatsD host to be a non-ip will trigger a DNS lookup (i.e. a synchronous TCP round trip). This can be particularly problematic in clouds that have a shared DNS infrastructure such as AWS.

  1. Using a hardcoded IP avoids the DNS lookup but generally requires an application deploy to change.
  2. Hardcoding the DNS/IP pair in /etc/hosts allows the IP to change without redeploying your application but fails to scale as the number of servers increases.
  3. Installing caching software such as nscd that uses the DNS TTL avoids most DNS lookups but makes the exact moment of change indeterminate.

Links

This library was developed for shopify.com and is MIT licensed.

statsd-instrument's People

Contributors

aslam avatar betamatt avatar byroot avatar casperisfine avatar catlee avatar codegourmet avatar csfrancis avatar davidcornu avatar dylanahsmith avatar fabiormoura avatar fmejia97 avatar freeatnet avatar fw42 avatar haeky avatar jstorimer avatar kirs avatar leandromoreira avatar mangara avatar marijacvetkovik avatar miniluke avatar pedro-stanaka avatar rafaelzimmermann avatar raginpirate avatar sambostock avatar sgrif avatar sirupsen avatar smith avatar titanous avatar wvanbergen avatar wyattwalter avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

statsd-instrument's Issues

BatchedUDPSink: flush on exit should be improve

@Mangara just noticed that a particular metric we send not long before the process exit is way down. This is without a doubt due to the newly introduced batching.

I'll look at improving the reliability of sending events soon before the process exit. This issue is mostly a remainder.

cc @wvanbergen

Some metrics seems to be skipped on exceptions

Based of some dashboard data it lead me to believe we do not properly collect the data correctly on exceptions (at least measure). I didn't explore it with more depth but looking at the code that seems to be the case (should at least add a test to cover that).

Eg.:

StatsD.measure(StatsD::Instrument.generate_metric_name(name, self, *args), nil, *metric_options) { super(*args, &block) }

A good scenario where this would occur often by design is when measuring an http request that have an open or read timeout. In these cases, we wouldn't not record failures and it would lead the event consumers to believe "it never happen" while in reality we are simply not issuing event for them.

cc @maximebedard @surrahman

NoMethodError: undefined method shopifycloud.assets.upload_threads:1|c' for nil:NilClass

This showed up when a cloud app didn't set the statsd env vars properly and ended up with invalid StatsD configuration. The stack trace was

[FATAL][2018-11-07 13:58:11 +0000]	      Starting asset upload to cdn.shopifycloud.com
[FATAL][2018-11-07 13:58:11 +0000]	      [StatsD] Errno::ECONNREFUSED: Connection refused - send(2)
[FATAL][2018-11-07 13:58:11 +0000]	      [StatsD] Errno::ECONNREFUSED: Connection refused - send(2)
[FATAL][2018-11-07 13:58:11 +0000]	      [StatsD] Errno::ECONNREFUSED: Connection refused - send(2)
[FATAL][2018-11-07 13:58:11 +0000]	      [StatsD] Errno::ECONNREFUSED: Connection refused - send(2)
[FATAL][2018-11-07 13:58:11 +0000]	      [StatsD] Errno::ECONNREFUSED: Connection refused - send(2)
[FATAL][2018-11-07 13:58:11 +0000]	      #<Thread:0x0000000004aa37d8@/app/vendor/bundle/ruby/2.5.0/gems/shopify-cloud-1.4.2/lib/shopify-cloud/asset_uploading/asset_uploader.rb:124 run> terminated with exception (report_on_exception is true):
[FATAL][2018-11-07 13:58:11 +0000]	      /app/vendor/bundle/ruby/2.5.0/gems/statsd-instrument-2.3.0/lib/statsd/instrument/backends/udp_backend.rb:140:in `block in write_packet': undefined method `shopifycloud.assets.upload_threads:1|c' for nil:NilClass (NoMethodError)
[FATAL][2018-11-07 13:58:11 +0000]	      from /app/vendor/ruby-2.5.3/lib/ruby/2.5.0/monitor.rb:226:in `mon_synchronize'
[FATAL][2018-11-07 13:58:11 +0000]	      from /app/vendor/bundle/ruby/2.5.0/gems/statsd-instrument-2.3.0/lib/statsd/instrument/backends/udp_backend.rb:139:in `write_packet'
[FATAL][2018-11-07 13:58:11 +0000]	      from /app/vendor/bundle/ruby/2.5.0/gems/statsd-instrument-2.3.0/lib/statsd/instrument/backends/udp_backend.rb:111:in `collect_metric'
[FATAL][2018-11-07 13:58:11 +0000]	      from /app/vendor/bundle/ruby/2.5.0/gems/statsd-instrument-2.3.0/lib/statsd/instrument.rb:456:in `collect_metric'
[FATAL][2018-11-07 13:58:11 +0000]	      from /app/vendor/bundle/ruby/2.5.0/gems/statsd-instrument-2.3.0/lib/statsd/instrument.rb:332:in `increment'
[FATAL][2018-11-07 13:58:11 +0000]	      from /app/vendor/bundle/ruby/2.5.0/gems/shopify-cloud-1.4.2/lib/shopify-cloud/asset_uploading/asset_uploader.rb:125:in `block (2 levels) in consume_in_parallel'
[FATAL][2018-11-07 13:58:11 +0000]	      [StatsD] Errno::ECONNREFUSED: Connection refused - send(2)
[FATAL][2018-11-07 13:58:11 +0000]	      [StatsD] Errno::ECONNREFUSED: Connection refused - send(2)
[FATAL][2018-11-07 13:58:11 +0000]	      [StatsD] Errno::ECONNREFUSED: Connection refused - send(2)
[FATAL][2018-11-07 13:58:11 +0000]	      [StatsD] Errno::ECONNREFUSED: Connection refused - send(2)
[FATAL][2018-11-07 13:58:11 +0000]	      [StatsD] Errno::ECONNREFUSED: Connection refused - send(2)
[FATAL][2018-11-07 13:58:11 +0000]	      [StatsD] Errno::ECONNREFUSED: Connection refused - send(2)
[FATAL][2018-11-07 13:58:11 +0000]	      [StatsD] Errno::ECONNREFUSED: Connection refused - send(2)
[FATAL][2018-11-07 13:58:11 +0000]	      [StatsD] Errno::ECONNREFUSED: Connection refused - send(2)
[FATAL][2018-11-07 13:58:11 +0000]	      [StatsD] Errno::ECONNREFUSED: Connection refused - send(2)
[FATAL][2018-11-07 13:58:11 +0000]	      [StatsD] Errno::ECONNREFUSED: Connection refused - send(2)
[FATAL][2018-11-07 13:58:11 +0000]	      rake aborted!
[FATAL][2018-11-07 13:58:11 +0000]	      NoMethodError: undefined method `shopifycloud.assets.upload_threads:1|c' for nil:NilClass
[FATAL][2018-11-07 13:58:11 +0000]	      /app/vendor/bundle/ruby/2.5.0/gems/statsd-instrument-2.3.0/lib/statsd/instrument/backends/udp_backend.rb:140:in `block in write_packet'
[FATAL][2018-11-07 13:58:11 +0000]	      /app/vendor/bundle/ruby/2.5.0/gems/statsd-instrument-2.3.0/lib/statsd/instrument/backends/udp_backend.rb:139:in `write_packet'
[FATAL][2018-11-07 13:58:11 +0000]	      /app/vendor/bundle/ruby/2.5.0/gems/statsd-instrument-2.3.0/lib/statsd/instrument/backends/udp_backend.rb:111:in `collect_metric'
[FATAL][2018-11-07 13:58:11 +0000]	      /app/vendor/bundle/ruby/2.5.0/gems/statsd-instrument-2.3.0/lib/statsd/instrument.rb:456:in `collect_metric'
[FATAL][2018-11-07 13:58:11 +0000]	      /app/vendor/bundle/ruby/2.5.0/gems/statsd-instrument-2.3.0/lib/statsd/instrument.rb:332:in `increment'
[FATAL][2018-11-07 13:58:11 +0000]	      /app/vendor/bundle/ruby/2.5.0/gems/shopify-cloud-1.4.2/lib/shopify-cloud/asset_uploading/asset_uploader.rb:125:in `block (2 levels) in consume_in_parallel'
[FATAL][2018-11-07 13:58:11 +0000]	      Tasks: TOP => cloudplatform:upload_assets
[FATAL][2018-11-07 13:58:11 +0000]	      (See full trace by running task with --trace)

It seems that somehow the socket on line https://github.com/Shopify/statsd-instrument/blob/master/lib/statsd/instrument/backends/udp_backend.rb#L140 can end up being nil (despite the existing nil guards) and ends up turning the packet write into a message send, with the message being the statsd datagram payload.

/cc @clayton-shopify

Dynamic tags

Is it possible to generate dynamic tags in a similar same way that you can generate dynamic measurement names?

spring rspec "uninitialized constant StatsD::Instrument::Matchers"

$ bundle exec rspec # works fine
$ spring rspec
/Users/rsilva/work/project/spec/spec_helper.rb:149:in `block in <top (required)>': uninitialized constant StatsD::Instrument::Matchers (NameError)
        from /Users/rsilva/.rbenv/versions/2.1.6/lib/ruby/gems/2.1.0/gems/rspec-core-3.3.2/lib/rspec/core.rb:97:in `configure'
        from /Users/rsilva/work/project/spec/spec_helper.rb:98:in `<top (required)>'
        from /Users/rsilva/work/project/spec/controllers/example_spec.rb:1:in `<top (required)>'
        from /Users/rsilva/.rbenv/versions/2.1.6/lib/ruby/gems/2.1.0/gems/rspec-core-3.3.2/lib/rspec/core/configuration.rb:1327:in `block in load_spec_files'
        from /Users/rsilva/.rbenv/versions/2.1.6/lib/ruby/gems/2.1.0/gems/rspec-core-3.3.2/lib/rspec/core/configuration.rb:1325:in `each'
        from /Users/rsilva/.rbenv/versions/2.1.6/lib/ruby/gems/2.1.0/gems/rspec-core-3.3.2/lib/rspec/core/configuration.rb:1325:in `load_spec_files'
        from /Users/rsilva/.rbenv/versions/2.1.6/lib/ruby/gems/2.1.0/gems/rspec-core-3.3.2/lib/rspec/core/runner.rb:102:in `setup'
        from /Users/rsilva/.rbenv/versions/2.1.6/lib/ruby/gems/2.1.0/gems/rspec-core-3.3.2/lib/rspec/core/runner.rb:88:in `run'
        from /Users/rsilva/.rbenv/versions/2.1.6/lib/ruby/gems/2.1.0/gems/rspec-core-3.3.2/lib/rspec/core/runner.rb:73:in `run'
        from /Users/rsilva/.rbenv/versions/2.1.6/lib/ruby/gems/2.1.0/gems/rspec-core-3.3.2/lib/rspec/core/runner.rb:41:in `invoke'
        from /Users/rsilva/.rbenv/versions/2.1.6/lib/ruby/gems/2.1.0/gems/rspec-core-3.3.2/exe/rspec:4:in `<top (required)>'
        from /Users/rsilva/.rbenv/versions/2.1.6/lib/ruby/gems/2.1.0/gems/spring-commands-rspec-1.0.4/lib/spring/commands/rspec.rb:18:in `call'
        from /Users/rsilva/.rbenv/versions/2.1.6/lib/ruby/2.1.0/rubygems/core_ext/kernel_require.rb:55:in `require'
        from /Users/rsilva/.rbenv/versions/2.1.6/lib/ruby/2.1.0/rubygems/core_ext/kernel_require.rb:55:in `require'
        from -e:1:in `<main>'

In spec_helper.rb:

RSpec.configure do |config|
  ...
  config.include StatsD::Instrument::Matchers
end
$ ruby -v
ruby 2.1.6p336 (2015-04-13 revision 50298) [x86_64-darwin14.0]
$ bundle show statsd-instrument
/Users/rsilva/.rbenv/versions/2.1.6/lib/ruby/gems/2.1.0/gems/statsd-instrument-2.0.10
$ bundle show rails
/Users/rsilva/.rbenv/versions/2.1.6/lib/ruby/gems/2.1.0/gems/rails-4.2.4

Incidentally, the Matchers module can't be found from rails console, even without using spring:

$ spring rails console
[1] pry(main)> require 'statsd-instrument'
=> false
[2] pry(main)> StatsD::Instrument::Matchers
NameError: uninitialized constant StatsD::Instrument::Matchers
from (pry):2:in `<main>'

$ bundle exec rails console
[1] pry(main)> require 'statsd-instrument'
=> false
[2] pry(main)> StatsD::Instrument::Matchers
NameError: uninitialized constant StatsD::Instrument::Matchers
from (pry):2:in `<main>'
[3] pry(main)> StatsD::Instrument
=> StatsD::Instrument

I'm really confused as to why it can't see StatsD::Instrument::Matchers. Any ideas?

Memory leak when using BatchedUDPSink

We noticed a memory leak on one of our Sidekiq deployments. With rbtrace and diffing heap dumps the indication was it was from statsd-instrument gem.

Retained STRING 755984 objects of size 221275072/228127604 (in bytes) at: /artifacts/bundle/ruby/2.7.0/gems/statsd-instrument-3.1.2/lib/statsd/instrument/datagram_builder.rb:86

After setting STATSD_FLUSH_INTERVAL to 0, the memory leak disappeared (to use the UDPSink instead of BatchedUDPSink)

Memory graphs before:

After:

Unable to require statsd/instrument if rspec already required

If a project is using rspec 2, you cannot require statsd/instrument if you've already required rspec:

$ bundle exec irb
irb(main):001:0> require "rspec"
=> true
irb(main):002:0> require "statsd/instrument"
NameError: uninitialized constant RSpec::Matchers::Composable
    from /Users/peteringlesby/.rbenv/versions/2.0.0-p353/lib/ruby/gems/2.0.0/gems/statsd-instrument-2.0.11/lib/statsd/instrument/matchers.rb:14:in `<class:Matcher>'
    from /Users/peteringlesby/.rbenv/versions/2.0.0-p353/lib/ruby/gems/2.0.0/gems/statsd-instrument-2.0.11/lib/statsd/instrument/matchers.rb:13:in `<module:Matchers>'
    from /Users/peteringlesby/.rbenv/versions/2.0.0-p353/lib/ruby/gems/2.0.0/gems/statsd-instrument-2.0.11/lib/statsd/instrument/matchers.rb:3:in `<top (required)>'
    from /Users/peteringlesby/.rbenv/versions/2.0.0-p353/lib/ruby/gems/2.0.0/gems/statsd-instrument-2.0.11/lib/statsd/instrument.rb:382:in `require'
    from /Users/peteringlesby/.rbenv/versions/2.0.0-p353/lib/ruby/gems/2.0.0/gems/statsd-instrument-2.0.11/lib/statsd/instrument.rb:382:in `<top (required)>'
    from (irb):2:in `require'
    from (irb):2
    from /Users/peteringlesby/.rbenv/versions/2.0.0-p353/bin/irb:12:in `<main>'

Requiring statsd/instrument by itself works fine:

$ bundle exec irb
irb(main):001:0> require "statsd/instrument"
=> true

There are no problems with rspec 3.

I've tested with rspec 2.11.1, and rspec 3.4.4, with the most recent version of stats-instrument.

Metaprogramming methods + Rails controllers

Hey there,

Let me start by saying how easy it is to use this library. Really a joy. Thanks!

I have a question after integrating the metaprogramming methods in a Rails 4 app โ€” particularly controllers. When I use statsd_count_success or statsd_count_if with a block to define what success is, I'm finding that I have to do string searching because the controller action returns the response as a string (or type of string). For example:

MyController.statsd_count_success :create, 'create' do |response|
  !response.to_s.include?('error_explanation')
end

I call to_s because sometimes it's an array of strings/buffers. What I'm not able to do โ€” as far as I can tell โ€” is check the response code or anything like that.

Am I doing something wrong? It would be great to be able to access the (controller) object itself.

Thanks for your help!

trigger_statsd_increment does not support multiple calls with different tags

๐Ÿ‘‹ , thansk for the great library

Issue

My use-case is that I have a method which issues a bunch of statsD calls to the same metric with different tags. I then want to verify that this metric has correctly received the expected amount of calls, each with their respective tags. Here is a test I would expect to pass:

require 'statsd-instrument'

RSpec.configure do |config|
  config.include StatsD::Instrument::Matchers
end

RSpec.describe 'Matchers' do
  context 'trigger_statsd_increment' do
    it 'will pass if every statsD call matches its calls' do
      expect do
        StatsD.increment('counter', tags: ['variation:a'])
        StatsD.increment('counter', tags: ['variation:b'])
      end.to trigger_statsd_increment('counter', times: 2, tags: [['variation:a'], ['variation:b']])
    end
  end
end

However, today this test fails.

matcher = RSpec::Matchers::BuiltIn::Match.new(options[expectation])

will test that for each call to increment, all the tags expected are present. We are not allowed to do different tags per calls.

Thus, likewise

      end.to trigger_statsd_increment('counter', times: 2, tags: ['variation:a', 'variation:b'])

would not work either

Rewriting the test as

RSpec.describe 'Matchers' do
  context 'trigger_statsd_increment' do
    it 'will pass if every statsD call matches its calls' do
      expect do
        StatsD.increment('counter', tags: ['variation:a'])
        StatsD.increment('counter', tags: ['variation:b'])
      end.to trigger_statsd_increment('counter', times: 1, tags: ["variation:a"]).and trigger_statsd_increment('counter', times: 1, tags: ["variation:b"])
    end
  end
end

Does not pass either and fails with

The numbers of StatsD calls for metric counter was unexpected. Expected 1, got 2

Indeed

metrics = capture_statsd_calls(&block)
metrics = metrics.select { |m| m.type == metric_type && m.name == metric_name }
if metrics.empty?
raise RSpec::Expectations::ExpectationNotMetError, "No StatsD calls for metric #{metric_name} were made."
elsif options[:times] && options[:times] != metrics.length
raise RSpec::Expectations::ExpectationNotMetError, "The numbers of StatsD calls for metric " \
"#{metric_name} was unexpected. Expected #{options[:times].inspect}, got #{metrics.length}"
end

will first test for the metric name and return the error above as the error has not been matched enough times. The tags are not looked at for this to fail.

Propositions

A: support trigger_statsd_increment('counter', times: 2, tags: [['variation:a'], ['variation:b']])

Here, each trigger_statsd_increment is linked by the metric name.

The tags would have 2 behaviors when present:

  • it is a list of lists, and then each element is supposed to match the call to metric_name in the order those calls happened
  • it is a list, and then we keep the behavior of today: all the calls for metric_name need to have tags match the unique list of expected tags. This would make the change retrocompatible (I don't expect anyone to use lists of list of tags)

B: support trigger_statsd_increment('counter', times: 1, tags: ["variation:a"]).and trigger_statsd_increment('counter', times: 1, tags: ["variation:b"])

When matching the times, we make sure that both the metric_name and the tags match.

The advantage I find to this is that it matches the API for metric_name matching where each metric_name is supposed to be assessed in different calls :

    it 'will pass if every statsD call matches its calls' do
        expect do
          StatsD.increment('counter')
          StatsD.increment('counter2')
        end.to trigger_statsd_increment('counter', times: 1).and trigger_statsd_increment('counter2', times: 1)
      end  
    end

That change would make it that different ((metric_name,tags) should be assessed in different calls).

Implementation

I'm willing to implement those changes, and favor proposition A. WDYT ?

Counting an object's property on success (metaprogramming)

Is there a possibility to count an object's property on success?

Let's say i have a class like this

class EasterBunny
  def eggs
    5
  end

  def hide
    rand > 0.5 # 50/50 chance of returning 'true'
  end
end

Now i would like to be able to instrument that class in a metaprogramming style

EasterBunny.extend StatsD::Instrument
EasterBunny.statsd_count_if :hide, 'hidden'

This would increase the counter by 1 for every successful run.

But what i'd actually want to know would be "how many eggs were hidden", so in the example i'd need a way to increase the counter by bunny.eggs amount for every successful run.

Is there a metaprogramming way to do this? I saw the block-form of ..count_if, but that gets the return value of a function, not the object itself.

[RFC] Exception handling and api normalization

Fixes: #110

Follow-up on #111, #112

To improve consistency of this library and try to minimize unexpected side-effects from developers, I have a few proposal I'd like to make.

changes to .statsd_count_if

The README currently states the following:

This will only increment the given key if the method executes successfully. So now, if GoogleBase#insert raises an exception or returns false (ie. result == false), we won't increment the key. If you want to define what success means for a given method you can pass a block that takes the result of the method.

I propose that instead of comparing against a false value that we use the ruby if directly to evaluate the truthiness/falsyness of the value. If the method raises, no metric will be pushed.

changes to .statsd_count_success

I propose that we evaluate the condition to deliver the metric exactly the same way defined in .statsd_count_if. However, instead of creating 2 buckets for success and failure, I propose we add a 3rd bucket error for situations where the method raises.

changes to measure .statsd_measure

I propose that this method would push metrics for all situations (normal execution, raise). In other words, it would behave exactly the same way as .statsd_count.

add .statsd_measure_success, .statsd_measure_if

I propose we match the same API we do for .statsd_count_*. This would allow us to give more flexibility as to which metrics we want to observe in the end.

changes to #measure

I suggest we create 2 distinct methods:

  • measure (push a single metrics in all cases -- even when the method raises)
  • measure_success (push a metric in 3 distinct buckets success, failure, error)

Note: measure_success would have to accept a proc in order to allow the consumer to define what success means. By default it will simply check truthy/falsy values.

changes to tags/automatic suffixes

For StatsD implementations that supports tags, instead of pushing a metric with a suffix MetricName.success|failure|error, we could push them as a tag with the key status:#{status}.

Any thoughts/concerns? โค๏ธ

@tjoyal @simon-shopify @wvanbergen @alexsnaps @dnlserrano

Add option to intercept tags

It would be nice to be able to attach or modify tags via an interceptor. The most common use case this is useful is when common tags(s) are used everywhere in code.

For instance, recently our platform team introduced the concept of runtime and they provided an interface to identify which runtime our service is currently running onto as in Runtime.current.

We decided to add the runtime tag to all our metrics but we did that by monkey-patching StatsD::Metric#initialize to insert the runtime tag into @tags as the snippet below shows:

    RUNTIME_ID_PREFIX = "runtime_id:"

    def initialize(*)
      super
            @tags ||= []

      unless @tags.any? { |tag| tag.start_with?(RUNTIME_ID_PREFIX) }
        @tags << "#{RUNTIME_ID_PREFIX}#{Runtime.current}"
      end
    end

What are your thoughts to allow passing a new proc to StatsD and then use it for intercepting tags before metric is instantiated at

backend.collect_metric(metric = StatsD::Instrument::Metric.new(options))

StatsD.tags_appender = proc do |tags|
  tags[:runtime_id] = Runtime.current
end

assert_statsd_increment errors from 2.1.1 and above

We are using 2.0.12 version right once we moved to 2.1.1 version we started seeing errors as below

No StatsD calls for metric app.portfolio.app.payment.rejected of type c were made.

I could see there was change made on #86 which is breaking Minitest:Assertions

Sample block of code we used is

it 'increments the rejected statsd counter' do
          assert_statsd_increment('app.portfolio.app.payment.rejected ', times: 1) do
            processor.call
      end
  end

The above assertion works fine on 2.0.12 and 2.1.0

Prefix cannot be removed once set

The documentation for the prefix option states

The prefix can be overridden by any metric call by setting the
no_prefix keyword argument to true. We recommend against doing this,
but this behavior is retained for backwards compatibility.
Rather, when you feel the need to do this, we recommend instantiating
a new client without prefix (using clone_with_options), and using it
to emit the metric.

However, this does not work due to the way clone_with_options works:

def clone_with_options(prefix: nil, ...)
  self.class.new(prefix: prefix || @prefix, ...)
end

So calling clone_with_options(prefix: nil) will maintain the same prefix as before.

Use CaptureBackend in Test environment

Hi statsd-instrument team,

I want to provide convenient RSpec matchers for this gem (similar to your minitest assertions) and was wondering why the CaptureBackend is not used by default in test environments (but the NullBackend instead).

In my opinion this would simplify the implementation a lot, instead of mocking & replacing the NullBackend with a CaptureBackend in a block (as done currently), you'd just need to configure the Backend once (either in a spec_helper explicitly or via ENVs implicitly).

What do you think?

Improve Error Messages

Here's a sample error I received while working with this gem.

ArgumentError: A value is required for metric type :h.

I think we could improve the message here to something like

ArgumentError:  histogram was called with the value parameter set to nil

The error wasn't hard to debug but it could have been easier, I don't mind taking ownership of this one

@wvanbergen

assertion calls with no_prefix: true will set the expectation name to nil instead of just keeping the name

assertion calls with no_prefix: true will set the expectation name to nil instead of just keeping the name

assert_statsd_increment('PlusMetrics.storefront.requests', no_prefix: true) do

end

will expect an increment with no name because of this

@name = client.prefix ? "#{client.prefix}.#{name}" : name unless no_prefix

Some metrics are being skipped on rescuing Timeout Error

I have an application which supports background jobs (report generations). I have a use-case in which if a report is taking too long i timeout the report. Below is the pseudo code:

def generate_report(user_id)
  timeout_after = 10800 # In seconds # 3hours
  StatsD.increment('report_generation', 1, tags:["name:report_generation_start", "user_id:#{user_id}"])
  formats = [:csv, :xlsx]
  formats.each do |r_format|
    begin
      Timeout.timeout(timeout_after) do
        report = send("generate_#{r_format}")
      end
    rescue Timeout::Error => e
      StatsD.increment('report_generation', 1, tags:["name:report_generation_timeout_error", "user_id:#{user_id}"])
      break
    end
  end
  StatsD.increment('report_generation', 1, tags:["name:report_generation_completed", "user_id:#{user_id}"])
end

I ran 3 background jobs for different users (let's say id 1, 2, 3).

On successful generation i see all 3 metrics being pushed correctly. However, on timeout error i see name:report_generation_start being pushed 3 times but name:report_generation_timeout_error, name:report_generation_completed being pushed only once or twice (there's always a missing entry atleast)

Using return or raising exception causes metric collection to be skipped in distribution/measure

Ruby interpreter version: 2.4.4p296
The statsd-instrument version. v2.3.0.beta5
The StatsD backend you are using. UDPBackend

Using StatsD.distribution or StatsD.measure it is possible to skip the metric collection. This can happen if an exception is raised or if you return from the block.

An example of it working successfully:

def expensive_method
  StatsD.measure('my_metric') do
    result = expensive_operation
    result.success
  end
end

An example of it breaking:

def expensive_method_broken_metrics
  StatsD.measure('my_metric') do
    result = expensive_operation
    return true if result.success # Skips metric collection
    false
  end
end

The fix would be something like:

def measure(key, value = nil, *metric_options, &block)
  value, metric_options = parse_options(value, metric_options)
  type = (!metric_options.empty? && metric_options.first[:as_dist] ? :d : :ms)

  result = nil
  value  = 1000 * StatsD::Instrument.duration { result = block.call } if block_given?
ensure # Add this
  metric = collect_metric(type, key, value, metric_options)
  result = metric unless block_given?
  return result # Make sure this value is returned from measure
end

Test false-positive when tags are updated after metric is aleady emitted.

We have a BaseAction (which models a business action) which automates latency metrics (via StatsD.distribution). It has a method that subclasses can override to define additional tags they want stapled onto this auto-generated metric.

We found out that modifying the tags asynchronously (e.g. in a promise returned from a batch loader) makes the tags not show up in prod (obviously, they've already been ommitted), but they still pass the tests.

Here's a minimal example to illustrate the issue

#!/usr/bin/ruby

require 'bundler/inline'

gemfile(true) do 
  source 'https://rubygems.org'
  gem 'statsd-instrument'
  gem 'minitest'
end

require "minitest/autorun"

class DemoTest < MiniTest::Unit::TestCase
  include StatsD::Instrument::Assertions
  
  def test_happy_case_has_tags_set
    assert_statsd_distribution("happy_case", tags: { a: 1, b: 2 }) do
      happy_case
    end
  end
  
  def test_broken_case_still_has_tags_set
    assert_statsd_distribution("broken_case", tags: { a: 1, b: 2 }) do
      broken_case
    end
  end
  
  private
  
  def happy_case
    StatsD.distribution("happy_case", tags: { a: 1, b: 2 }) do
      sleep(0.1) # Simulate some work
    end
  end
  
  def broken_case
    tags = { a: 1 }
    StatsD.distribution("broken_case", tags: tags) do
      sleep(0.1) # Simulate some work
    end
    
    # At some later point, tags are merged in asynchronously, after the distribution is already done
    tags.merge!(b: 2)
  end
end

Get rid of the global object.

This is not a feature request, just a question if such feature would be useful. If it is, I also would like to implement it myself.
The point:
The gem is great, but it is implemented as global object(a module, to be precise), what leads to impossibility to use several different instances of it in one application, which can be very useful. I personally twice run in such circumstances where it was necessary and where our team had to switch to another competing gem. Once was necessary to have different prefixes and another time to have different servers where metrics are collected.
If the gem maintainer agrees that such functionality is welcome, I would implement it. Also, I guess, I can preserve the current interface doing it.

`ArgumentError` in Ruby 3 when instrumenting method with positional and keyword arguments

  • Your Ruby interpreter version: 3.0.6
  • The statsd-instrument version. 3.5.10 (first noticed on a fork of 2.9.2)
  • The StatsD backend you are using: Datadog

When trying to instrument a method in the form def my_method(arg1, kwarg1:, kwarg2:), we're receiving an ArgumentError since upgrading to Ruby 3.0.

ArgumentError: wrong number of arguments (given 2, expected 1; required keywords: kwarg1, kwarg2)

Looking at the measure metaprogramming method, this seems to be caused by capturing all arguments in *args and not also anticipating keywords with a **kwargs. (This issue exists for other metaprogramming methods as well.

Forking and updating these methods to look like this fixes the issue for us (we haven't considered how it affects generate_metric_name and generate_tags:

define_method(method) do |*args, **kwargs, &block|
  client ||= StatsD.singleton_client
  key = StatsD::Instrument.generate_metric_name(name, self, *args)
  generated_tags = StatsD::Instrument.generate_tags(tags, self, *args)
  client.measure(key, sample_rate: sample_rate, tags: generated_tags, no_prefix: no_prefix) do
    super(*args, **kwargs, &block)
  end
end

I might be missing something here because I'd expect this to be a more widespread issue with Ruby 3.0+ if teams mix positional and keyword arguments. Are any others having this problem? Happy to take a stab at a PR, but I wanted more input first.

Datadog Origin Detection support?

Datadog Origin Detection allows stats emitted by Workloads in Kubernetes clusters to be identified by the Datadog Agent running in the cluster. That way, the tags from that Workload specified by Datadog Autodiscovery can be added before shipping them to Datadog.

This is usually accomplished by adding a tag (dd.internal.entity_id) to all datagrams emitted, and the value of that tag is the Pod UID (usually provided via DD_ENTITY_ID ENV var).

The other option is support for Unix Domain Sockets, where Origin Detection happens automatically.

Any chance this library supports that in the future?

Calling `statsd_measure` after prepending a module causes infinite loop

To reproduce:

require "bundler/inline"

gemfile(true) do
  gem "statsd-instrument", github: "shopify/statsd-instrument"
end

class Foo
  def foo
    puts "foo"
  end
end

module MyPatch
  def foo
    puts "before foo"
    super
  end
end

Foo.prepend(MyPatch)

Foo.extend(StatsD::Instrument)
Foo.statsd_measure :foo, "Foo!"

Foo.new.foo

Output:

before foo
before foo
before foo
before foo
before foo
before foo
before foo
before foo
before foo
/Users/sean/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/bundler/gems/statsd-instrument-154487eb6a1d/lib/statsd/instrument.rb:75:in `block (3 levels) in statsd_measure': stack level too deep (SystemStackError)
    from /Users/sean/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/bundler/gems/statsd-instrument-154487eb6a1d/lib/statsd/instrument.rb:284:in `block in measure'
    from /Users/sean/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/bundler/gems/statsd-instrument-154487eb6a1d/lib/statsd/instrument.rb:53:in `duration'
    from /Users/sean/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/bundler/gems/statsd-instrument-154487eb6a1d/lib/statsd/instrument.rb:284:in `measure'
    from /Users/sean/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/bundler/gems/statsd-instrument-154487eb6a1d/lib/statsd/instrument.rb:75:in `block (2 levels) in statsd_measure'
    from foo.rb:16:in `foo'
    from /Users/sean/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/bundler/gems/statsd-instrument-154487eb6a1d/lib/statsd/instrument.rb:75:in `block (3 levels) in statsd_measure'
    from /Users/sean/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/bundler/gems/statsd-instrument-154487eb6a1d/lib/statsd/instrument.rb:284:in `block in measure'
    from /Users/sean/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/bundler/gems/statsd-instrument-154487eb6a1d/lib/statsd/instrument.rb:53:in `duration'
     ... 10758 levels...
    from /Users/sean/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/bundler/gems/statsd-instrument-154487eb6a1d/lib/statsd/instrument.rb:284:in `measure'
    from /Users/sean/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/bundler/gems/statsd-instrument-154487eb6a1d/lib/statsd/instrument.rb:75:in `block (2 levels) in statsd_measure'
    from foo.rb:16:in `foo'
    from foo.rb:25:in `<main>'

This would likely be solved by changing statsd_measure to use Module.prepend, which this gem can now do as it doesn't support 1.9.3

Rubocop has dropped support for Ruby 2.3

Rubocop has dropped support for Ruby 2.3:

rubocop/rubocop@5873894

Which we use in tests:

investigate(RuboCop::ProcessedSource.new(source, 2.3, nil))

I don't think this gem pins the version of Rubocop in any way - unless I'm missing something? So these tests now fail.

  1) Error:
Rubocop::SplatArgumentsTest#test_no_offenses:
ArgumentError: RuboCop found unknown Ruby version: 2.3
    /Users/chrisseaton/.gem/ruby/2.6.5/gems/rubocop-0.82.0/lib/rubocop/processed_source.rb:191:in `parser_class'
    /Users/chrisseaton/.gem/ruby/2.6.5/gems/rubocop-0.82.0/lib/rubocop/processed_source.rb:200:in `create_parser'
    /Users/chrisseaton/.gem/ruby/2.6.5/gems/rubocop-0.82.0/lib/rubocop/processed_source.rb:158:in `parse'
    /Users/chrisseaton/.gem/ruby/2.6.5/gems/rubocop-0.82.0/lib/rubocop/processed_source.rb:36:in `initialize'
    /Users/chrisseaton/src/github.com/Shopify/statsd-instrument/test/helpers/rubocop_helper.rb:11:in `new'
    /Users/chrisseaton/src/github.com/Shopify/statsd-instrument/test/helpers/rubocop_helper.rb:11:in `assert_no_offenses'
    /Users/chrisseaton/src/github.com/Shopify/statsd-instrument/test/rubocop/splat_arguments_test.rb:15:in `test_no_offenses'

I found this problem in TruffleRuby, but seems to apply to MRI as well.

Rubocop Cop for nested assertions

It should be possible to write a Rubocop Cop for the following

Bad

test "something" do
  assert_statsd_increment("metric.1") do
    assert_statsd_increment("metric.2") do
  # ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Capture datagrams instead of nesting assertion blocks
      # ...
        assert_statsd_increment("metric.n") do
      # ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Capture datagrams instead of nesting assertion blocks
          # actual work
        end
      # ...
    end
  end
end

Good

test "something" do
  capture_statsd_datagrams do
    # actual work
  end

  assert_statsd_increment("metric.1", datagrams: datagrams)
  assert_statsd_increment("metric.2", datagrams: datagrams)
  # ...
  assert_statsd_increment("metric.n", datagrams: datagrams)
end

Should we create such a Cop?

`normalize_tags` is quite expensive in aggregate.

I've been profiling application code and I've noticed that normalize_tags is quite expensive when statsd is used at scale. The profile below represents a significant overhead for the profile in question.

statsd-instrument 3.5.12

image
   def normalize_tags(tags)
        return [] unless tags

        tags = tags.map { |k, v| "#{k}:#{v}" } if tags.is_a?(Hash)

        # Fast path when no string replacement is needed
        return tags unless tags.any? { |tag| /[|,]/.match?(tag) }

        tags.map { |tag| tag.tr("|,", "") }
      end

def normalize_tags(tags)
return [] unless tags
tags = tags.map { |k, v| "#{k}:#{v}" } if tags.is_a?(Hash)
# Fast path when no string replacement is needed
return tags unless tags.any? { |tag| /[|,]/.match?(tag) }
tags.map { |tag| tag.tr("|,", "") }
end

Instrument Class Methods

An example of how to instrument a class method with the metaprogramming options would be excellent. An example would be AWS::S3::Base.request.

Setting non-default backend for environment

Hello,

I'm trying to use non-default backends from the default settings for statsd-instrument, and as best I can tell they're being overwritten. It would appear that they are being overwritten here: https://github.com/Shopify/statsd-instrument/blob/master/lib/statsd/instrument/railtie.rb#L11-L13

as that is not checking for whether or not the backend is already set, it's simply setting it to be the defaults. I've tried making an initializer that sets StatsD.backend = StatsD::Instrument::Backends::NullBackend.new. After the application loads, it goes back to being set as StatsD::Instrument::Backends::LoggerBackend. I've also tried setting this in config/environments/development.rb, but upon inspection in a console after the application loads it's still LoggerBackend.

I did come across issue #73 , so I tried interacting (by simply calling StatsD.backend) with the backend prior to changing it, which did not yield the desired results either.

What's the correct procedure for setting a new default backend for an environment?

Apologies if this is in the documentation somewhere, but I searched and could not find any references outside of simply setting StatsD.backend, which is not working properly for me.

ThreadError: can't alloc thread

We're investigating a weird issue that sometime happens in job workers.

ThreadError: can't alloc thread
~/gems/statsd-instrument-3.1.1/lib/statsd/instrument/batched_udp_sink.rb:68:in `new'
~/gems/statsd-instrument-3.1.1/lib/statsd/instrument/batched_udp_sink.rb:68:in `<<'
~/gems/statsd-instrument-3.1.1/lib/statsd/instrument/batched_udp_sink.rb:36:in `<<'
~/gems/statsd-instrument-3.1.1/lib/statsd/instrument/client.rb:437:in `emit'
~/gems/statsd-instrument-3.1.1/lib/statsd/instrument/client.rb:317:in `ensure in latency'

Looking at MRI this error seem to happen when the main thread is dead: https://github.com/ruby/ruby/blob/0fb782ee38ea37fd5fe8b1f775f8ad866a82a3f0/thread.c#L1027-L1029

Which initially sounded like nonsense, but after digging more, it turns out that this error is raised from another background thread, e.g:

Thread.new do
  loop do
    StatsD.measure ...
  end
end

So the MRI error makes somewhat some sense.

Theory

I assume that somehow the main thread is indeed dead, but that a background thread is still alive and working, causing this issue.

I have to dig more to replicate this kind of scenario, until now I thought the Ruby process would abort once the main thread is dead.

Not being able to control backends if initialized outside application

Hi.
I am trying to initialiaze a backend from another gem which does almost every app_config for my apps. Here I want to set correct prefixes and backend.

The problem is that StatsD is overwriting my backend with the default logger-backend.
Is there any way to turn of the fallback backends?
I would like to be in control of what StatsD backend is set in the different environments myself.

assert_statsd_call improvements

  • Error message: The amount of StatsD calls for metric Storefront.Cart.Activity was unexpected should have expected/actual.
  • You should be able to submit tags in any form (set, array or hash).

Rails instrumentation with meta programming is not happening

When I try instrumenting a class in rails app with meta programming.
I have tried testing the instrumentation in 2 ways.

  1. Calling directly Object method from rails console
  2. Calling via Rails request

The first one have passed with successful result where as the 2nd one did not pass at all.
I am posting this after research.

Can anyone tell me why the 2nd way is failing... ??

Rails reload fails when instrumenting class methods in config/initializers

Environment

Ruby interpreter version: ruby 2.7.5
statsd-instrument version: 3.1.2
StatsD backend: Datadog
Rails: 7.0.2.3
Machine: Mac M1
OS: Mac OS Monterey 12.3.1

Description

Anything that triggers reloading will throw error from statsd-instrument when instrumenting class methods from config/initializer.

How to reproduce

Given this code in Rails:

# app/services/pin/manager.rb

module Pin
  module Manager
        class << self
           def consume_and_grant
           end
        end
  end
end
# config/initializers/statsd.rb

Rails.application.reloader.to_prepare do
  Pin::Manager.singleton_class.extend(StatsD::Instrument)
  Pin::Manager.singleton_class.statsd_count_success(:consume_and_grant, "Pin.Manager.consume_and_grant")
end

Reload fails in console:

  1. Run in terminal: bin/rails console
  2. Reload in rails console: reload!

Observe this error:

Reloading...
ArgumentError: Already instrumented consume_and_grant for
from /Users/<user>/.gem/ruby/2.7.5/gems/statsd-instrument-3.1.2/lib/statsd/instrument.rb:263:in `add_to_method'

Possible cause

StatsD::Instrument having a cache that is likely not being reset on reload

Workaround

You can instrument inside the file that has the module being instrumented:

# app/services/pin/manager.rb

module Pin
  module Manager
        class << self
            def consume_and_grant
            end
        end
  end
end

Pin::Manager.singleton_class.extend(StatsD::Instrument)
Pin::Manager.singleton_class.statsd_count_success(:consume_and_grant, "Pin.Manager.consume_and_grant")

Instrumenting a Rails model fails

It seems that extending a rails model "destroys" it completely.

Suppose I have a rails model

class User < ActiveRecord::Base
end

# See how many users I have
::User.all.count # => 20

# And I instrument it
::User.extend StatsD::Instrument

# If I try to run any operation on the model it is like it would lost the connection with the db or something really strange is happening 

::User.all.count  # => 0

And any other operation just fails, like creating new users, or anything related to the class.

Does it make sense?

Histograms not working

I have StatsD configured like so:

StatsD.backend = StatsD::Instrument::Backends::UDPBackend.new(
  "localhost:8125",
  :datadog,
)

But I'm seeing "Metric type :h not supported on statsd implementation" in the log.

I'm running on Heroku using this buildpack to install statsd itself.

Any ideas what I'm doing wrong?

New version breaks with RSpec version ~ 3.4

O hi!

I get an error when I run my tests with version 2.0.12 with rspec-core version 3.4.1:

lib/ruby/gems/2.3.0/gems/statsd-instrument-2.0.12/lib/statsd/instrument/matchers.rb:2:in `require': cannot load such file -- rspec/version (LoadError)

I looks this was introduced by commit 3dd6033. Version 2.0.11 seems to work fine.

DNS Lookups

While most people are probably aware of the effect of dns lookups, it doesn't hurt to have a paragraph in the README.md. For example something like the following.

DNS Lookup
If you use a non-ip as the statsd host, be aware that each UDP packet sent will trigger a DNS lookup. This breaks the fire-and-forget functionality since the udp packet cannot be sent until the DNS network round trip has completed. Popular methods around this issue are hardcoded /etc/hosts files and the use of software such as nscd.

Support for block execution elapsed time measurement for the distribution method.

StatsD.measure supports passing a block to measure elapsed time or of a value. This is really useful to measure the amount of time a method has spent executing.

The new distribution method is great but doesn't support passing a block like measure does.

There's a few options:

  • Support an option on measure to use distribution
  • Support passing a block to distribution and measure it (seems a bit specific :/)
  • Adding a new method that does measurement, using the distribution metric type.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.