PageRenderTime 48ms CodeModel.GetById 17ms RepoModel.GetById 1ms app.codeStats 0ms

/spec/statsd_spec.rb

http://github.com/reinh/statsd
Ruby | 601 lines | 486 code | 108 blank | 7 comment | 11 complexity | f4c50a53b69fc5e9d1926ea889a1eaa1 MD5 | raw file
  1. require 'helper'
  2. describe Statsd do
  3. before do
  4. class Statsd
  5. o, $VERBOSE = $VERBOSE, nil
  6. alias connect_old connect
  7. def connect
  8. $connect_count ||= 1
  9. $connect_count += 1
  10. end
  11. $VERBOSE = o
  12. end
  13. @statsd = Statsd.new('localhost', 1234)
  14. @socket = @statsd.instance_variable_set(:@socket, FakeUDPSocket.new)
  15. end
  16. after do
  17. class Statsd
  18. o, $VERBOSE = $VERBOSE, nil
  19. alias connect connect_old
  20. $VERBOSE = o
  21. end
  22. end
  23. describe "#initialize" do
  24. it "should set the host and port" do
  25. _(@statsd.host).must_equal 'localhost'
  26. _(@statsd.port).must_equal 1234
  27. end
  28. it "should default the host to 127.0.0.1 and port to 8125" do
  29. statsd = Statsd.new
  30. _(statsd.host).must_equal '127.0.0.1'
  31. _(statsd.port).must_equal 8125
  32. end
  33. it "should set delimiter to period by default" do
  34. _(@statsd.delimiter).must_equal "."
  35. end
  36. end
  37. describe "#host and #port" do
  38. it "should set host and port" do
  39. @statsd.host = '1.2.3.4'
  40. @statsd.port = 5678
  41. _(@statsd.host).must_equal '1.2.3.4'
  42. _(@statsd.port).must_equal 5678
  43. end
  44. it "should not resolve hostnames to IPs" do
  45. @statsd.host = 'localhost'
  46. _(@statsd.host).must_equal 'localhost'
  47. end
  48. it "should set nil host to default" do
  49. @statsd.host = nil
  50. _(@statsd.host).must_equal '127.0.0.1'
  51. end
  52. it "should set nil port to default" do
  53. @statsd.port = nil
  54. _(@statsd.port).must_equal 8125
  55. end
  56. it "should allow an IPv6 address" do
  57. @statsd.host = '::1'
  58. _(@statsd.host).must_equal '::1'
  59. end
  60. end
  61. describe "#delimiter" do
  62. it "should set delimiter" do
  63. @statsd.delimiter = "-"
  64. _(@statsd.delimiter).must_equal "-"
  65. end
  66. it "should set default to period if not given a value" do
  67. @statsd.delimiter = nil
  68. _(@statsd.delimiter).must_equal "."
  69. end
  70. end
  71. describe "#increment" do
  72. it "should format the message according to the statsd spec" do
  73. @statsd.increment('foobar')
  74. _(@socket.recv).must_equal ['foobar:1|c']
  75. end
  76. describe "with a sample rate" do
  77. before { class << @statsd; def rand; 0; end; end } # ensure delivery
  78. it "should format the message according to the statsd spec" do
  79. @statsd.increment('foobar', 0.5)
  80. _(@socket.recv).must_equal ['foobar:1|c|@0.5']
  81. end
  82. end
  83. end
  84. describe "#decrement" do
  85. it "should format the message according to the statsd spec" do
  86. @statsd.decrement('foobar')
  87. _(@socket.recv).must_equal ['foobar:-1|c']
  88. end
  89. describe "with a sample rate" do
  90. before { class << @statsd; def rand; 0; end; end } # ensure delivery
  91. it "should format the message according to the statsd spec" do
  92. @statsd.decrement('foobar', 0.5)
  93. _(@socket.recv).must_equal ['foobar:-1|c|@0.5']
  94. end
  95. end
  96. end
  97. describe "#gauge" do
  98. it "should send a message with a 'g' type, per the nearbuy fork" do
  99. @statsd.gauge('begrutten-suffusion', 536)
  100. _(@socket.recv).must_equal ['begrutten-suffusion:536|g']
  101. @statsd.gauge('begrutten-suffusion', -107.3)
  102. _(@socket.recv).must_equal ['begrutten-suffusion:-107.3|g']
  103. end
  104. describe "with a sample rate" do
  105. before { class << @statsd; def rand; 0; end; end } # ensure delivery
  106. it "should format the message according to the statsd spec" do
  107. @statsd.gauge('begrutten-suffusion', 536, 0.1)
  108. _(@socket.recv).must_equal ['begrutten-suffusion:536|g|@0.1']
  109. end
  110. end
  111. end
  112. describe "#timing" do
  113. it "should format the message according to the statsd spec" do
  114. @statsd.timing('foobar', 500)
  115. _(@socket.recv).must_equal ['foobar:500|ms']
  116. end
  117. describe "with a sample rate" do
  118. before { class << @statsd; def rand; 0; end; end } # ensure delivery
  119. it "should format the message according to the statsd spec" do
  120. @statsd.timing('foobar', 500, 0.5)
  121. _(@socket.recv).must_equal ['foobar:500|ms|@0.5']
  122. end
  123. end
  124. end
  125. describe "#set" do
  126. it "should format the message according to the statsd spec" do
  127. @statsd.set('foobar', 765)
  128. _(@socket.recv).must_equal ['foobar:765|s']
  129. end
  130. describe "with a sample rate" do
  131. before { class << @statsd; def rand; 0; end; end } # ensure delivery
  132. it "should format the message according to the statsd spec" do
  133. @statsd.set('foobar', 500, 0.5)
  134. _(@socket.recv).must_equal ['foobar:500|s|@0.5']
  135. end
  136. end
  137. end
  138. describe "#time" do
  139. it "should format the message according to the statsd spec" do
  140. @statsd.time('foobar') { 'test' }
  141. _(@socket.recv).must_equal ['foobar:0|ms']
  142. end
  143. it "should return the result of the block" do
  144. result = @statsd.time('foobar') { 'test' }
  145. _(result).must_equal 'test'
  146. end
  147. describe "when given a block with an explicit return" do
  148. it "should format the message according to the statsd spec" do
  149. lambda { @statsd.time('foobar') { return 'test' } }.call
  150. _(@socket.recv).must_equal ['foobar:0|ms']
  151. end
  152. it "should return the result of the block" do
  153. result = lambda { @statsd.time('foobar') { return 'test' } }.call
  154. _(result).must_equal 'test'
  155. end
  156. end
  157. describe "with a sample rate" do
  158. before { class << @statsd; def rand; 0; end; end } # ensure delivery
  159. it "should format the message according to the statsd spec" do
  160. @statsd.time('foobar', 0.5) { 'test' }
  161. _(@socket.recv).must_equal ['foobar:0|ms|@0.5']
  162. end
  163. end
  164. end
  165. describe "#sampled" do
  166. describe "when the sample rate is 1" do
  167. before { class << @statsd; def rand; raise end; end }
  168. it "should send" do
  169. @statsd.timing('foobar', 500, 1)
  170. _(@socket.recv).must_equal ['foobar:500|ms']
  171. end
  172. end
  173. describe "when the sample rate is greater than a random value [0,1]" do
  174. before { class << @statsd; def rand; 0; end; end } # ensure delivery
  175. it "should send" do
  176. @statsd.timing('foobar', 500, 0.5)
  177. _(@socket.recv).must_equal ['foobar:500|ms|@0.5']
  178. end
  179. end
  180. describe "when the sample rate is less than a random value [0,1]" do
  181. before { class << @statsd; def rand; 1; end; end } # ensure no delivery
  182. it "should not send" do
  183. assert_nil @statsd.timing('foobar', 500, 0.5)
  184. end
  185. end
  186. describe "when the sample rate is equal to a random value [0,1]" do
  187. before { class << @statsd; def rand; 0; end; end } # ensure delivery
  188. it "should send" do
  189. @statsd.timing('foobar', 500, 0.5)
  190. _(@socket.recv).must_equal ['foobar:500|ms|@0.5']
  191. end
  192. end
  193. end
  194. describe "with namespace" do
  195. before { @statsd.namespace = 'service' }
  196. it "should add namespace to increment" do
  197. @statsd.increment('foobar')
  198. _(@socket.recv).must_equal ['service.foobar:1|c']
  199. end
  200. it "should add namespace to decrement" do
  201. @statsd.decrement('foobar')
  202. _(@socket.recv).must_equal ['service.foobar:-1|c']
  203. end
  204. it "should add namespace to timing" do
  205. @statsd.timing('foobar', 500)
  206. _(@socket.recv).must_equal ['service.foobar:500|ms']
  207. end
  208. it "should add namespace to gauge" do
  209. @statsd.gauge('foobar', 500)
  210. _(@socket.recv).must_equal ['service.foobar:500|g']
  211. end
  212. end
  213. describe "with postfix" do
  214. before { @statsd.postfix = 'ip-23-45-56-78' }
  215. it "should add postfix to increment" do
  216. @statsd.increment('foobar')
  217. _(@socket.recv).must_equal ['foobar.ip-23-45-56-78:1|c']
  218. end
  219. it "should add postfix to decrement" do
  220. @statsd.decrement('foobar')
  221. _(@socket.recv).must_equal ['foobar.ip-23-45-56-78:-1|c']
  222. end
  223. it "should add namespace to timing" do
  224. @statsd.timing('foobar', 500)
  225. _(@socket.recv).must_equal ['foobar.ip-23-45-56-78:500|ms']
  226. end
  227. it "should add namespace to gauge" do
  228. @statsd.gauge('foobar', 500)
  229. _(@socket.recv).must_equal ['foobar.ip-23-45-56-78:500|g']
  230. end
  231. end
  232. describe '#postfix=' do
  233. describe "when nil, false, or empty" do
  234. it "should set postfix to nil" do
  235. [nil, false, ''].each do |value|
  236. @statsd.postfix = 'a postfix'
  237. @statsd.postfix = value
  238. assert_nil @statsd.postfix
  239. end
  240. end
  241. end
  242. end
  243. describe "with logging" do
  244. require 'stringio'
  245. before { Statsd.logger = Logger.new(@log = StringIO.new)}
  246. it "should write to the log in debug" do
  247. Statsd.logger.level = Logger::DEBUG
  248. @statsd.increment('foobar')
  249. _(@log.string).must_match "Statsd: foobar:1|c"
  250. end
  251. it "should not write to the log unless debug" do
  252. Statsd.logger.level = Logger::INFO
  253. @statsd.increment('foobar')
  254. _(@log.string).must_be_empty
  255. end
  256. end
  257. describe "stat names" do
  258. it "should accept anything as stat" do
  259. @statsd.increment(Object, 1)
  260. end
  261. it "should replace ruby constant delimeter with graphite package name" do
  262. class Statsd::SomeClass; end
  263. @statsd.increment(Statsd::SomeClass, 1)
  264. _(@socket.recv).must_equal ['Statsd.SomeClass:1|c']
  265. end
  266. describe "custom delimiter" do
  267. before do
  268. @statsd.delimiter = "-"
  269. end
  270. it "should replace ruby constant delimiter with custom delimiter" do
  271. class Statsd::SomeOtherClass; end
  272. @statsd.increment(Statsd::SomeOtherClass, 1)
  273. _(@socket.recv).must_equal ['Statsd-SomeOtherClass:1|c']
  274. end
  275. end
  276. it "should replace statsd reserved chars in the stat name" do
  277. @statsd.increment('ray@hostname.blah|blah.blah:blah', 1)
  278. _(@socket.recv).must_equal ['ray_hostname.blah_blah.blah_blah:1|c']
  279. end
  280. end
  281. describe "handling socket errors" do
  282. before do
  283. require 'stringio'
  284. Statsd.logger = Logger.new(@log = StringIO.new)
  285. @socket.instance_variable_set(:@err_count, 0)
  286. @socket.instance_eval { def write(*) @err_count+=1; raise SocketError end }
  287. end
  288. it "should ignore socket errors" do
  289. assert_nil @statsd.increment('foobar')
  290. end
  291. it "should log socket errors" do
  292. @statsd.increment('foobar')
  293. _(@log.string).must_match 'Statsd: SocketError'
  294. end
  295. it "should retry and reconnect on socket errors" do
  296. $connect_count = 0
  297. @statsd.increment('foobar')
  298. _(@socket.instance_variable_get(:@err_count)).must_equal 5
  299. _($connect_count).must_equal 5
  300. end
  301. end
  302. describe "batching" do
  303. it "should have a default batch size of 10" do
  304. _(@statsd.batch_size).must_equal 10
  305. end
  306. it "should have a default batch byte size of nil" do
  307. assert_nil @statsd.batch_byte_size
  308. end
  309. it "should have a default flush interval of nil" do
  310. assert_nil @statsd.flush_interval
  311. end
  312. it "should have a modifiable batch size" do
  313. @statsd.batch_size = 7
  314. _(@statsd.batch_size).must_equal 7
  315. @statsd.batch do |b|
  316. _(b.batch_size).must_equal 7
  317. end
  318. @statsd.batch_size = nil
  319. @statsd.batch_byte_size = 1472
  320. @statsd.batch do |b|
  321. assert_nil b.batch_size
  322. _(b.batch_byte_size).must_equal 1472
  323. end
  324. end
  325. it 'should have a modifiable flush interval' do
  326. @statsd.flush_interval = 1
  327. _(@statsd.flush_interval).must_equal 1
  328. @statsd.batch do |b|
  329. _(b.flush_interval).must_equal 1
  330. end
  331. end
  332. it "should flush the batch at the batch size or at the end of the block" do
  333. @statsd.batch do |b|
  334. b.batch_size = 3
  335. # The first three should flush, the next two will be flushed when the
  336. # block is done.
  337. 5.times { b.increment('foobar') }
  338. _(@socket.recv).must_equal [(["foobar:1|c"] * 3).join("\n")]
  339. end
  340. _(@socket.recv).must_equal [(["foobar:1|c"] * 2).join("\n")]
  341. end
  342. it "should flush based on batch byte size" do
  343. @statsd.batch do |b|
  344. b.batch_size = nil
  345. b.batch_byte_size = 22
  346. # The first two should flush, the last will be flushed when the
  347. # block is done.
  348. 3.times { b.increment('foobar') }
  349. _(@socket.recv).must_equal [(["foobar:1|c"] * 2).join("\n")]
  350. end
  351. _(@socket.recv).must_equal ["foobar:1|c"]
  352. end
  353. it "should flush immediately when the queue is exactly a batch size" do
  354. @statsd.batch do |b|
  355. b.batch_size = nil
  356. b.batch_byte_size = 21
  357. # The first two should flush together
  358. 2.times { b.increment('foobar') }
  359. _(@socket.recv).must_equal [(["foobar:1|c"] * 2).join("\n")]
  360. end
  361. end
  362. it "should flush when the interval has passed" do
  363. @statsd.batch do |b|
  364. b.batch_size = nil
  365. b.flush_interval = 0.01
  366. # The first two should flush, the last will be flushed when the
  367. # block is done.
  368. 2.times { b.increment('foobar') }
  369. sleep(0.03)
  370. b.increment('foobar')
  371. _(@socket.recv).must_equal [(["foobar:1|c"] * 2).join("\n")]
  372. end
  373. _(@socket.recv).must_equal ["foobar:1|c"]
  374. end
  375. it "should not flush to the socket if the backlog is empty" do
  376. batch = Statsd::Batch.new(@statsd)
  377. batch.flush
  378. _(@socket.recv).must_be :nil?
  379. batch.increment 'foobar'
  380. batch.flush
  381. _(@socket.recv).must_equal %w[foobar:1|c]
  382. end
  383. it "should support setting namespace for the underlying instance" do
  384. batch = Statsd::Batch.new(@statsd)
  385. batch.namespace = 'ns'
  386. _(@statsd.namespace).must_equal 'ns'
  387. end
  388. it "should support setting host for the underlying instance" do
  389. batch = Statsd::Batch.new(@statsd)
  390. batch.host = '1.2.3.4'
  391. _(@statsd.host).must_equal '1.2.3.4'
  392. end
  393. it "should support setting port for the underlying instance" do
  394. batch = Statsd::Batch.new(@statsd)
  395. batch.port = 42
  396. _(@statsd.port).must_equal 42
  397. end
  398. end
  399. describe "#connect" do
  400. it "should reconnect" do
  401. c = $connect_count
  402. @statsd.connect
  403. _(($connect_count - c)).must_equal 1
  404. end
  405. end
  406. end
  407. describe Statsd do
  408. describe "with a real UDP socket" do
  409. it "should actually send stuff over the socket" do
  410. family = Addrinfo.udp(UDPSocket.getaddress('localhost'), 0).afamily
  411. begin
  412. socket = UDPSocket.new family
  413. host, port = 'localhost', 0
  414. socket.bind(host, port)
  415. port = socket.addr[1]
  416. statsd = Statsd.new(host, port)
  417. statsd.increment('foobar')
  418. message = socket.recvfrom(16).first
  419. _(message).must_equal 'foobar:1|c'
  420. ensure
  421. socket.close
  422. end
  423. end
  424. it "should send stuff over an IPv4 socket" do
  425. begin
  426. socket = UDPSocket.new Socket::AF_INET
  427. host, port = '127.0.0.1', 0
  428. socket.bind(host, port)
  429. port = socket.addr[1]
  430. statsd = Statsd.new(host, port)
  431. statsd.increment('foobar')
  432. message = socket.recvfrom(16).first
  433. _(message).must_equal 'foobar:1|c'
  434. ensure
  435. socket.close
  436. end
  437. end
  438. it "should send stuff over an IPv6 socket" do
  439. begin
  440. socket = UDPSocket.new Socket::AF_INET6
  441. host, port = '::1', 0
  442. socket.bind(host, port)
  443. port = socket.addr[1]
  444. statsd = Statsd.new(host, port)
  445. statsd.increment('foobar')
  446. message = socket.recvfrom(16).first
  447. _(message).must_equal 'foobar:1|c'
  448. ensure
  449. socket.close
  450. end
  451. end
  452. end
  453. describe "supports TCP sockets" do
  454. it "should connect to and send stats over TCPv4" do
  455. begin
  456. host, port = '127.0.0.1', 0
  457. server = TCPServer.new host, port
  458. port = server.addr[1]
  459. socket = nil
  460. Thread.new { socket = server.accept }
  461. statsd = Statsd.new(host, port, :tcp)
  462. statsd.increment('foobar')
  463. Timeout.timeout(5) do
  464. Thread.pass while socket == nil
  465. end
  466. message = socket.recvfrom(16).first
  467. _(message).must_equal "foobar:1|c\n"
  468. ensure
  469. socket.close if socket
  470. server.close
  471. end
  472. end
  473. it "should connect to and send stats over TCPv6" do
  474. begin
  475. host, port = '::1', 0
  476. server = TCPServer.new host, port
  477. port = server.addr[1]
  478. socket = nil
  479. Thread.new { socket = server.accept }
  480. statsd = Statsd.new(host, port, :tcp)
  481. statsd.increment('foobar')
  482. Timeout.timeout(5) do
  483. Thread.pass while socket == nil
  484. end
  485. message = socket.recvfrom(16).first
  486. _(message).must_equal "foobar:1|c\n"
  487. ensure
  488. socket.close if socket
  489. server.close
  490. end
  491. end
  492. end
  493. end