Permalink
Browse files

Add a Statsd::Admin class.

  • Loading branch information...
1 parent 0193c58 commit 1dc205adac701bed04695ed9870bdb78f87f1607 @schuyler schuyler committed Jun 19, 2013
Showing with 224 additions and 1 deletion.
  1. +119 −0 lib/statsd.rb
  2. +8 −1 spec/helper.rb
  3. +97 −0 spec/statsd_admin_spec.rb
View
@@ -1,5 +1,6 @@
require 'socket'
require 'forwardable'
+require 'multi_json'
# = Statsd: A Statsd client (https://github.com/etsy/statsd)
#
@@ -87,6 +88,124 @@ def send_to_socket(message)
end
+ class Admin
+ # StatsD host. Defaults to 127.0.0.1.
+ attr_reader :host
+
+ # StatsD admin port. Defaults to 8126.
+ attr_reader :port
+
+ class << self
+ # Set to a standard logger instance to enable debug logging.
+ attr_accessor :logger
+ end
+
+ # @attribute [w] host
+ # Writes are not thread safe.
+ def host=(host)
+ @host = host || '127.0.0.1'
+ end
+
+ # @attribute [w] port
+ # Writes are not thread safe.
+ def port=(port)
+ @port = port || 8126
+ end
+
+ # @param [String] host your statsd host
+ # @param [Integer] port your statsd port
+ def initialize(host = '127.0.0.1', port = 8126)
+ self.host, self.port = host, port
+ end
+
+ # Reads all gauges from StatsD.
+ def gauges
+ read_metric :gauges
+ end
+
+ # Reads all timers from StatsD.
+ def timers
+ read_metric :timers
+ end
+
+ # Reads all counters from StatsD.
+ def counters
+ read_metric :counters
+ end
+
+ # @param[String] item
+ # Deletes one or more gauges. Wildcards are allowed.
+ def delgauges item
+ delete_metric :gauges, item
+ end
+
+ # @param[String] item
+ # Deletes one or more timers. Wildcards are allowed.
+ def deltimers item
+ delete_metric :timers, item
+ end
+
+ # @param[String] item
+ # Deletes one or more counters. Wildcards are allowed.
+ def delcounters item
+ delete_metric :counters, item
+ end
+
+ def stats
+ # the format of "stats" isn't JSON, who knows why
+ send_to_socket "stats"
+ result = read_from_socket
+ items = {}
+ result.split("\n").each do |line|
+ key, val = line.chomp.split(": ")
+ items[key] = val.to_i
+ end
+ items
+ end
+
+ private
+
+ def read_metric name
+ send_to_socket name
+ result = read_from_socket
+ # for some reason, the reply looks like JSON, but isn't, quite
+ MultiJson.load result.gsub("'", "\"")
+ end
+
+ def delete_metric name, item
+ send_to_socket "del#{name} #{item}"
+ result = read_from_socket
+ deleted = []
+ result.split("\n").each do |line|
+ deleted << line.chomp.split(": ")[-1]
+ end
+ deleted
+ end
+
+ def send_to_socket(message)
+ self.class.logger.debug { "Statsd: #{message}" } if self.class.logger
+ socket.write(message.to_s + "\n")
+ rescue => boom
+ self.class.logger.error { "Statsd: #{boom.class} #{boom}" } if self.class.logger
+ nil
+ end
+
+
+ def read_from_socket
+ buffer = ""
+ loop do
+ line = socket.readline
+ break if line == "END\n"
+ buffer += line
+ end
+ socket.readline # clear the closing newline out of the socket
+ buffer
+ end
+
+ def socket
+ Thread.current[:statsd_admin_socket] ||= TCPSocket.new(host, port)
+ end
+ end
# A namespace to prepend to all statsd calls.
attr_reader :namespace
View
@@ -29,6 +29,13 @@ def to_s
end
def inspect
- "<FakeUDPSocket: #{@buffer.inspect}>"
+ "<#{self.class.name}: #{@buffer.inspect}>"
+ end
+end
+
+class FakeTCPSocket < FakeUDPSocket
+ alias_method :readline, :recv
+ def write(message)
+ @buffer.push message
end
end
View
@@ -0,0 +1,97 @@
+require 'helper'
+
+describe Statsd::Admin do
+ class Statsd::Admin
+ public :socket
+ end
+
+ before do
+ @admin = Statsd::Admin.new('localhost', 1234)
+ @socket = Thread.current[:statsd_admin_socket] = FakeTCPSocket.new
+ end
+
+ after { Thread.current[:statsd_socket] = nil }
+
+ describe "#initialize" do
+ it "should set the host and port" do
+ @admin.host.must_equal 'localhost'
+ @admin.port.must_equal 1234
+ end
+
+ it "should default the host to 127.0.0.1 and port to 8126" do
+ statsd = Statsd::Admin.new
+ statsd.host.must_equal '127.0.0.1'
+ statsd.port.must_equal 8126
+ end
+ end
+
+ describe "#host and #port" do
+ it "should set host and port" do
+ @admin.host = '1.2.3.4'
+ @admin.port = 5678
+ @admin.host.must_equal '1.2.3.4'
+ @admin.port.must_equal 5678
+ end
+
+ it "should not resolve hostnames to IPs" do
+ @admin.host = 'localhost'
+ @admin.host.must_equal 'localhost'
+ end
+
+ it "should set nil host to default" do
+ @admin.host = nil
+ @admin.host.must_equal '127.0.0.1'
+ end
+
+ it "should set nil port to default" do
+ @admin.port = nil
+ @admin.port.must_equal 8126
+ end
+ end
+
+ %w(gauges counters timers).each do |action|
+ describe "##{action}" do
+ it "should send a command and return a Hash" do
+ ["{'foo.bar': 0,\n",
+ "'foo.baz': 1,\n",
+ "'foo.quux': 2 }\n",
+ "END\n","\n"].each do |line|
+ @socket.write line
+ end
+ result = @admin.send action.to_sym
+ result.must_be_kind_of Hash
+ result.size.must_equal 3
+ @socket.readline.must_equal "#{action}\n"
+ end
+ end
+
+ describe "#del#{action}" do
+ it "should send a command and return an Array" do
+ ["deleted: foo.bar\n",
+ "deleted: foo.baz\n",
+ "deleted: foo.quux\n",
+ "END\n", "\n"].each do |line|
+ @socket.write line
+ end
+ result = @admin.send "del#{action}", "foo.*"
+ result.must_be_kind_of Array
+ result.size.must_equal 3
+ @socket.readline.must_equal "del#{action} foo.*\n"
+ end
+ end
+ end
+
+ describe "#stats" do
+ it "should send a command and return a Hash" do
+ ["whatever: 0\n", "END\n", "\n"].each do |line|
+ @socket.write line
+ end
+ result = @admin.stats
+ result.must_be_kind_of Hash
+ result["whatever"].must_equal 0
+ @socket.readline.must_equal "stats\n"
+ end
+ end
+end
+
+

0 comments on commit 1dc205a

Please sign in to comment.