From b273fce3096666d844aed5efdfebbffe8e13c7c9 Mon Sep 17 00:00:00 2001 From: Joel Segerlind Date: Fri, 10 Jul 2015 09:05:47 +0200 Subject: [PATCH 01/11] Use Jetty for HTTP and expose separate Rack app This replaces the old Rack based HTTP server with a Jetty-based one. A Rack app is still exposed, but the handling of it is left to the user. This way, the Rack app can be mounted anywhere in an existing application. Moreover, and more importantly, is that both of these now use the Metrics JSON serializations library, which means that it is possible to get very consistent reporting even with applications using vanilla Metrics. --- lib/multimeter.rb | 48 +------ lib/multimeter/http.rb | 53 ++++++++ lib/multimeter/rack.rb | 70 ++++++++++ multimeter.gemspec | 2 + spec/integration/http_spec.rb | 45 +++++++ .../multimeter/{http_spec.rb => rack_spec.rb} | 76 +++++------ spec/support/json_metrics.rb | 120 ++++++++++++++++++ 7 files changed, 329 insertions(+), 85 deletions(-) create mode 100644 lib/multimeter/http.rb create mode 100644 lib/multimeter/rack.rb create mode 100644 spec/integration/http_spec.rb rename spec/multimeter/{http_spec.rb => rack_spec.rb} (53%) create mode 100644 spec/support/json_metrics.rb diff --git a/lib/multimeter.rb b/lib/multimeter.rb index a40aeb3..322db77 100644 --- a/lib/multimeter.rb +++ b/lib/multimeter.rb @@ -3,12 +3,17 @@ require 'metrics-core-jars' require 'multimeter_metrics' require 'json' +require 'multimeter/http' +require 'multimeter/rack' module Metrics include_package 'com.codahale.metrics' end module Multimeter + extend Http + extend Rack + class MetricRegistry def to_h h = {} @@ -99,50 +104,7 @@ def self.jmx(registry, options = {}) Metrics::JmxReporter.forRegistry(registry.to_java).inDomain(options[:domain] || 'multimeter').build.tap(&:start) end - def self.http(registry, rack_handler, options={}) - server_thread = Java::JavaLang::Thread.new do - rack_handler.run(Http.create_app(registry), options) - end - server_thread.daemon = true - server_thread.name = 'multimeter-http-server' - server_thread.start - server_thread - end - private NANO_TO_MILLI_SCALE = 1.0/1_000_000 - - module Http - class BadRequest < StandardError; end - - COMMON_HEADERS = {'Connection' => 'close'}.freeze - JSON_HEADERS = COMMON_HEADERS.merge('Content-Type' => 'application/json').freeze - JSONP_HEADERS = COMMON_HEADERS.merge('Content-Type' => 'application/javascript').freeze - ERROR_HEADERS = COMMON_HEADERS.merge('Content-Type' => 'text/plain').freeze - - def self.create_app(registry) - proc do |env| - begin - body = registry.to_h.to_json - headers = JSON_HEADERS - if (callback_name = env['QUERY_STRING'][/callback=([^$&]+)/, 1]) - if callback_name =~ /^[\w\d.]+$/ - body = "#{callback_name}(#{body});" - headers = JSONP_HEADERS - else - raise BadRequest - end - else - headers = headers.merge('Access-Control-Allow-Origin' => '*') - end - [200, headers, [body]] - rescue BadRequest => e - [400, ERROR_HEADERS, ['Bad Request']] - rescue => e - [500, ERROR_HEADERS, ["Internal Server Error\n\n", e.message, "\n\t", *e.backtrace.join("\n\t")]] - end - end - end - end end diff --git a/lib/multimeter/http.rb b/lib/multimeter/http.rb new file mode 100644 index 0000000..cee8ecf --- /dev/null +++ b/lib/multimeter/http.rb @@ -0,0 +1,53 @@ +# encoding: utf-8 + +require 'metrics-servlets-jars' +require 'rjack-jetty' + +module Metrics + module Servlets + include_package 'com.codahale.metrics.servlets' + end +end + +module Jetty + include_package 'org.eclipse.jetty.server' + include_package 'org.eclipse.jetty.servlet' +end + +module Multimeter + module Http + def http(registry, options={}) + server = Jetty::Server.new(options[:port] || 5747) + server.handler = create_servlet_context(registry) + Server.new(server) + end + + def create_servlet_context(registry) + context = Jetty::ServletContextHandler.new(Jetty::ServletContextHandler::SESSIONS) + context.context_path = '/' + context.set_attribute(Metrics::Servlets::MetricsServlet::METRICS_REGISTRY, registry.to_java); + context.add_servlet(Metrics::Servlets::MetricsServlet.java_class, '/*') + context + end + + class Server + def initialize(server) + @server = server + end + + def start + @server.start + self + end + + def stop + @server.stop + self + end + + def join + @server.join + end + end + end +end \ No newline at end of file diff --git a/lib/multimeter/rack.rb b/lib/multimeter/rack.rb new file mode 100644 index 0000000..a7e4239 --- /dev/null +++ b/lib/multimeter/rack.rb @@ -0,0 +1,70 @@ +# encoding: utf-8 + +require 'metrics-json-jars' + +module Metrics + module Json + include_package 'com.codahale.metrics.json' + end +end + +module Jackson + module Databind + include_package 'com.fasterxml.jackson.databind' + end +end + +module Multimeter + module Rack + class BadRequest < StandardError; end + + COMMON_HEADERS = {'Connection' => 'close'}.freeze + JSON_HEADERS = COMMON_HEADERS.merge('Content-Type' => 'application/json', 'Access-Control-Allow-Origin' => '*').freeze + JSONP_HEADERS = COMMON_HEADERS.merge('Content-Type' => 'application/javascript').freeze + ERROR_HEADERS = COMMON_HEADERS.merge('Content-Type' => 'text/plain').freeze + + class App + def initialize(registry) + @registry = registry + @object_mapper = Jackson::Databind::ObjectMapper.new + end + + def setup + metrics_module = Metrics::Json::MetricsModule.new(rate_unit = Java::JavaUtilConcurrent::TimeUnit::SECONDS, duration_unit = Java::JavaUtilConcurrent::TimeUnit::MILLISECONDS, show_samples = false) + @object_mapper.register_module(metrics_module) + self + end + + def call(env) + if (callback_name = env['QUERY_STRING'][/callback=([^$&]+)/, 1]) + if callback_name =~ /^[\w\d.]+$/ + body = "#{callback_name}(#{generate_json});" + headers = JSONP_HEADERS + else + raise BadRequest + end + else + headers = JSON_HEADERS + body = generate_json + end + [200, headers, [body]] + rescue BadRequest => e + [400, ERROR_HEADERS, ['Bad Request']] + rescue => e + [500, ERROR_HEADERS, ["Internal Server Error\n\n", e.message, "\n\t", *e.backtrace.join("\n\t")]] + end + + private + + def generate_json + stream = Java::JavaIo::ByteArrayOutputStream.new + @object_mapper.write_value(stream, @registry.to_java) + String.from_java_bytes(stream.to_byte_array) + end + end + + def create_app(registry) + App.new(registry).setup + end + end +end \ No newline at end of file diff --git a/multimeter.gemspec b/multimeter.gemspec index c2e2a73..025c1cb 100644 --- a/multimeter.gemspec +++ b/multimeter.gemspec @@ -18,6 +18,8 @@ Gem::Specification.new do |s| s.rubyforge_project = 'multimeter' s.add_dependency 'metrics-core-jars', '~> 3.1', '< 4.0.0' + s.add_dependency 'metrics-servlets-jars', '~> 3.1' + s.add_dependency 'rjack-jetty', '~> 9.3' s.files = Dir['lib/**/*.rb', 'lib/**/*.jar'] s.require_paths = %w[lib] diff --git a/spec/integration/http_spec.rb b/spec/integration/http_spec.rb new file mode 100644 index 0000000..f2c566b --- /dev/null +++ b/spec/integration/http_spec.rb @@ -0,0 +1,45 @@ +# encoding: utf-8 + +require 'spec_helper' +require 'open-uri' +require 'support/json_metrics' + +module Multimeter + describe Http do + let :registry do + MetricRegistry.new + end + + let :port do + loop do + port = rand(1024...2**15) + begin + TCPSocket.new('localhost', port).close + rescue Errno::ECONNREFUSED + return port + end + end + end + + let :server do + Multimeter.http(registry, port: port) + end + + before do + server.start + end + + after do + server.stop + server.join + end + + describe 'GET /' do + let :json do + open("http://localhost:#{port}").read + end + + include_examples 'json-metrics' + end + end +end \ No newline at end of file diff --git a/spec/multimeter/http_spec.rb b/spec/multimeter/rack_spec.rb similarity index 53% rename from spec/multimeter/http_spec.rb rename to spec/multimeter/rack_spec.rb index dcf5b41..de6d2ea 100644 --- a/spec/multimeter/http_spec.rb +++ b/spec/multimeter/rack_spec.rb @@ -1,72 +1,67 @@ # encoding: utf-8 require 'spec_helper' +require 'support/json_metrics' module Multimeter - describe 'Multimeter.http' do + describe 'Multimeter.create_app' do let :registry do Multimeter.create_registry end - context 'when handling requests' do - def extract_app - barrier = java.util.concurrent.Semaphore.new(0) - app, options = nil, nil - rack_handler = double(:rack_handler) - allow(rack_handler).to receive(:run) do |a, o| - app, options = a, o - barrier.release - end - Multimeter.http(registry, rack_handler) - expect(barrier.try_acquire(5, java.util.concurrent.TimeUnit::SECONDS)).not_to be_falsy - [app, options] - end - - before do - registry.counter('test').inc - end + let :app do + Multimeter.create_app(registry) + end + context 'when handling' do context 'valid requests' do - it 'responds with a JSON document created from calling #to_h on the registry' do - app, options = extract_app - status, headers, body = app.call({'QUERY_STRING' => ''}) - expect(body.join("\n")).to eq('{"test":{"type":"counter","count":1}}') + context 'responds with a JSON document that' do + let :json do + status, headers, body = app.call({'QUERY_STRING' => ''}) + body.join + end + + include_examples 'json-metrics' end it 'responds with application/json' do - app, options = extract_app status, headers, body = app.call({'QUERY_STRING' => ''}) expect(headers).to include('Content-Type' => 'application/json') end it 'responds with Connection: close' do - app, options = extract_app status, headers, body = app.call({'QUERY_STRING' => ''}) expect(headers).to include('Connection' => 'close') end it 'responds with CORS headers' do - app, _ = extract_app _, headers, _ = app.call({'QUERY_STRING' => ''}) expect(headers).to include('Access-Control-Allow-Origin' => '*') end - context 'JSONP' do - it 'responds with a JSON document wrapped in a function call when the callback parameter is given' do - app, options = extract_app - status, headers, body = app.call({'QUERY_STRING' => 'callback=the_cbk'}) - expect(body.join("\n")).to eq('the_cbk({"test":{"type":"counter","count":1}});') + context 'with a JSONP parameter' do + let :env do + { 'QUERY_STRING' => 'callback=the_cbk' } + end + + it 'wraps the body in a function call' do + status, headers, body = app.call(env) + expect(body.join).to match(/\Athe_cbk\(.*\);\Z/) + end + + it 'responds with a valid JSON document' do + status, headers, body = app.call(env) + json = body.join[/(?<=\Athe_cbk\().*(?=\);\Z)/] + expect { JSON.parse(json) }.not_to raise_error end it 'responds with application/javascript for JSONP request' do - app, options = extract_app - status, headers, body = app.call({'QUERY_STRING' => 'callback=the_cbk'}) + status, headers, body = app.call(env) expect(headers).to include('Content-Type' => 'application/javascript') end it 'responds with Connection: close' do - app, options = extract_app - status, headers, body = app.call({'QUERY_STRING' => 'callback=the_cbk'}) + status, headers, body = app.call(env) expect(headers).to include('Connection' => 'close') end end @@ -75,29 +70,26 @@ def extract_app context 'invalid requests' do context '400' do it 'responds with error 400 if the callback contains invalid chars' do - app, options = extract_app status, headers, body = app.call({'QUERY_STRING' => 'callback=apa*&^%$'}) expect(status).to eq(400) end it 'responds with Connection: close' do - app, options = extract_app status, headers, body = app.call({'QUERY_STRING' => 'callback=apa*&^%$'}) expect(headers).to include('Connection' => 'close') end end + context '500' do it 'responds with error 500 if an exception is thrown in the request handling' do - allow(registry).to receive(:to_h).and_raise('blurgh') - app, options = extract_app - status, headers, body = app.call({'QUERY_STRING' => 'callback=apa*&^%$'}) + allow(registry).to receive(:to_java).and_raise('blurgh') + status, headers, body = app.call({'QUERY_STRING' => ''}) expect(status).to eq(500) end it 'responds with Connection: close' do - allow(registry).to receive(:to_h).and_raise('blurgh') - app, options = extract_app - status, headers, body = app.call({'QUERY_STRING' => 'callback=apa*&^%$'}) + allow(registry).to receive(:to_java).and_raise('blurgh') + status, headers, body = app.call({'QUERY_STRING' => ''}) expect(headers).to include('Connection' => 'close') end end diff --git a/spec/support/json_metrics.rb b/spec/support/json_metrics.rb new file mode 100644 index 0000000..9afbd33 --- /dev/null +++ b/spec/support/json_metrics.rb @@ -0,0 +1,120 @@ +require 'json' + +shared_examples 'json-metrics' do + let :metrics do + JSON.parse(json) + end + + it 'returns JSON' do + expect { metrics }.not_to raise_error + end + + shared_examples 'count' do + it 'includes count' do + expect(metric).to have_key('count') + end + end + + shared_examples 'rates' do + { + 'm1_rate' => 'one minute rate', + 'm5_rate' => 'five minute rate', + 'm15_rate' => 'fifteen minute rate', + 'mean_rate' => 'mean rate', + }.each do |key, name| + it "includes the #{name}" do + expect(metric).to have_key(key) + end + end + end + + shared_examples 'histogram' do + { + 'p50' => '50th percentile', + 'p75' => '75th percentile', + 'p95' => '95th percentile', + 'p98' => '98th percentile', + 'p99' => '99th percentile', + 'p999' => '99.9th percentile', + 'min' => 'minimum value', + 'max' => 'maximum value', + 'mean' => 'mean value', + 'stddev' => 'standard deviation', + }.each do |key, name| + it "includes the #{name}" do + expect(metric).to have_key(key) + end + end + end + + context 'with counters' do + before do + registry.counter('spec-counter').inc + end + + let :metric do + metrics['counters']['spec-counter'] + end + + include_examples 'count' + end + + context 'with meters' do + before do + registry.meter('spec-meter').mark + end + + let :metric do + metrics['meters']['spec-meter'] + end + + include_examples 'count' + include_examples 'rates' + end + + context 'with histograms' do + before do + registry.histogram('spec-histogram').update(1) + end + + let :metric do + metrics['histograms']['spec-histogram'] + end + + include_examples 'histogram' + end + + context 'with timers' do + before do + registry.timer('spec-timer').time {} + end + + let :metric do + metrics['timers']['spec-timer'] + end + + include_examples 'count' + include_examples 'rates' + include_examples 'histogram' + end + + context 'with gauges' do + before do + registry.gauge('spec-gauge', :string) { 'a string' } + end + + it 'inclues the value' do + expect(metrics['gauges']['spec-gauge']).to include('value' => 'a string') + end + + context "when type isn't specified" do + before do + registry.gauge('untyped-gauge') { -1 } + end + + it 'failes to serialize' do + expect { metrics }.to raise_error(JSON::ParserError) + end + end + end +end \ No newline at end of file From 7b273c67aa9ba7e91f83a9d349fe79bd76489830 Mon Sep 17 00:00:00 2001 From: Joel Segerlind Date: Tue, 14 Jul 2015 00:58:33 +0200 Subject: [PATCH 02/11] Replace #to_h with #to_json I assume that #to_h is only used as an intermediate step in the serialization for most users, why I think it may be ok to skip it. By using Metrics JSON library, it's easier to maintain a consistent format across applications. Or maybe rather; it is difficult not to. --- ext/java/multimeter/Counter.java | 6 +++ ext/java/multimeter/Gauge.java | 5 ++ ext/java/multimeter/Histogram.java | 6 +++ ext/java/multimeter/Meter.java | 6 +++ ext/java/multimeter/Timer.java | 6 +++ lib/multimeter.rb | 69 ++++++------------------- lib/multimeter/json.rb | 39 ++++++++++++++ spec/multimeter/counter_spec.rb | 9 ++-- spec/multimeter/gauge_spec.rb | 21 +++++--- spec/multimeter/histogram_spec.rb | 31 +++++------ spec/multimeter/meter_spec.rb | 18 +++---- spec/multimeter/metric_registry_spec.rb | 18 ++++--- spec/multimeter/timer_spec.rb | 44 ++++++---------- spec/spec_helper.rb | 1 + 14 files changed, 154 insertions(+), 125 deletions(-) create mode 100644 lib/multimeter/json.rb diff --git a/ext/java/multimeter/Counter.java b/ext/java/multimeter/Counter.java index 1339b7d..adf022d 100644 --- a/ext/java/multimeter/Counter.java +++ b/ext/java/multimeter/Counter.java @@ -12,6 +12,7 @@ import org.jruby.runtime.ObjectAllocator; import org.jruby.runtime.ThreadContext; import org.jruby.runtime.builtin.IRubyObject; +import org.jruby.javasupport.JavaUtil; import static org.jruby.runtime.Visibility.PRIVATE; @@ -30,6 +31,11 @@ public Counter(Ruby runtime, com.codahale.metrics.Counter counter) { this.counter = counter; } + @JRubyMethod(name="to_java") + public IRubyObject toJava(ThreadContext ctx) { + return JavaUtil.convertJavaToUsableRubyObject(ctx.runtime, counter); + } + @JRubyMethod public IRubyObject count(ThreadContext ctx) { return ctx.runtime.newFixnum(counter.getCount()); diff --git a/ext/java/multimeter/Gauge.java b/ext/java/multimeter/Gauge.java index 85167e9..2c026f2 100644 --- a/ext/java/multimeter/Gauge.java +++ b/ext/java/multimeter/Gauge.java @@ -31,6 +31,11 @@ public Gauge(Ruby runtime, com.codahale.metrics.Gauge gauge) { this.gauge = gauge; } + @JRubyMethod(name="to_java") + public IRubyObject toJava(ThreadContext ctx) { + return JavaUtil.convertJavaToUsableRubyObject(ctx.runtime, gauge); + } + @JRubyMethod public IRubyObject value(ThreadContext ctx) { Object value = gauge.getValue(); diff --git a/ext/java/multimeter/Histogram.java b/ext/java/multimeter/Histogram.java index fcc913d..eb6d278 100644 --- a/ext/java/multimeter/Histogram.java +++ b/ext/java/multimeter/Histogram.java @@ -12,6 +12,7 @@ import org.jruby.runtime.ObjectAllocator; import org.jruby.runtime.ThreadContext; import org.jruby.runtime.builtin.IRubyObject; +import org.jruby.javasupport.JavaUtil; import static org.jruby.runtime.Visibility.PRIVATE; @@ -30,6 +31,11 @@ public Histogram(Ruby runtime, com.codahale.metrics.Histogram histogram) { this.histogram = histogram; } + @JRubyMethod(name="to_java") + public IRubyObject toJava(ThreadContext ctx) { + return JavaUtil.convertJavaToUsableRubyObject(ctx.runtime, histogram); + } + @JRubyMethod public IRubyObject count(ThreadContext ctx) { return ctx.runtime.newFixnum(histogram.getCount()); diff --git a/ext/java/multimeter/Meter.java b/ext/java/multimeter/Meter.java index 1fff410..db79050 100644 --- a/ext/java/multimeter/Meter.java +++ b/ext/java/multimeter/Meter.java @@ -12,6 +12,7 @@ import org.jruby.runtime.ObjectAllocator; import org.jruby.runtime.ThreadContext; import org.jruby.runtime.builtin.IRubyObject; +import org.jruby.javasupport.JavaUtil; import static org.jruby.runtime.Visibility.PRIVATE; @@ -30,6 +31,11 @@ public Meter(Ruby runtime, com.codahale.metrics.Meter meter) { this.meter = meter; } + @JRubyMethod(name="to_java") + public IRubyObject toJava(ThreadContext ctx) { + return JavaUtil.convertJavaToUsableRubyObject(ctx.runtime, meter); + } + @JRubyMethod public IRubyObject count(ThreadContext ctx) { return ctx.runtime.newFixnum(meter.getCount()); diff --git a/ext/java/multimeter/Timer.java b/ext/java/multimeter/Timer.java index 7b0e177..e58d651 100644 --- a/ext/java/multimeter/Timer.java +++ b/ext/java/multimeter/Timer.java @@ -12,6 +12,7 @@ import org.jruby.runtime.ObjectAllocator; import org.jruby.runtime.ThreadContext; import org.jruby.runtime.builtin.IRubyObject; +import org.jruby.javasupport.JavaUtil; import static org.jruby.runtime.Visibility.PRIVATE; @@ -35,6 +36,11 @@ public Timer(Ruby runtime, com.codahale.metrics.Timer timer) { this.timer = timer; } + @JRubyMethod(name="to_java") + public IRubyObject toJava(ThreadContext ctx) { + return JavaUtil.convertJavaToUsableRubyObject(ctx.runtime, timer); + } + @JRubyMethod public IRubyObject count(ThreadContext ctx) { return ctx.runtime.newFixnum(timer.getCount()); diff --git a/lib/multimeter.rb b/lib/multimeter.rb index 322db77..9a7f054 100644 --- a/lib/multimeter.rb +++ b/lib/multimeter.rb @@ -5,6 +5,7 @@ require 'json' require 'multimeter/http' require 'multimeter/rack' +require 'multimeter/json' module Metrics include_package 'com.codahale.metrics' @@ -15,84 +16,44 @@ module Multimeter extend Rack class MetricRegistry - def to_h - h = {} - metrics.each do |metric_name, metric| - h[metric_name] = metric.to_h - end - h + def to_json + Json.dump(self) end end class Meter - def to_h - { - :type => :meter, - :count => count, - :mean_rate => mean_rate, - :one_minute_rate => one_minute_rate, - :five_minute_rate => five_minute_rate, - :fifteen_minute_rate => fifteen_minute_rate - } + def to_json + Json.dump(self) end end class Counter - def to_h - { - :type => :counter, - :count => count - } + def to_json + Json.dump(self) end end class Histogram - def to_h - { - :type => :histogram, - :count => count, - }.merge(snapshot.to_h(NANO_TO_MILLI_SCALE)) + def to_json + Json.dump(self) end end class Timer - def to_h - { - :type => :timer, - :count => count, - :mean_rate => mean_rate, - :one_minute_rate => one_minute_rate, - :five_minute_rate => five_minute_rate, - :fifteen_minute_rate => fifteen_minute_rate, - }.merge(snapshot.to_h(NANO_TO_MILLI_SCALE)) + def to_json + Json.dump(self) end end class Snapshot - def to_h(scale=1) - { - :max => max * scale, - :min => min * scale, - :mean => mean * scale, - :std_dev => std_dev * scale, - :median => median * scale, - :percentiles => { - '75' => p75 * scale, - '95' => p95 * scale, - '98' => p98 * scale, - '99' => p99 * scale, - '99.9' => p999 * scale, - } - } + def to_json + Json.dump(self) end end class Gauge - def to_h - { - :type => :gauge, - :value => value, - } + def to_json + Json.dump(self) end end diff --git a/lib/multimeter/json.rb b/lib/multimeter/json.rb new file mode 100644 index 0000000..148127b --- /dev/null +++ b/lib/multimeter/json.rb @@ -0,0 +1,39 @@ +# encoding: utf-8 + +require 'metrics-json-jars' + +module Metrics + module Json + include_package 'com.codahale.metrics.json' + end +end + +module Jackson + module Databind + include_package 'com.fasterxml.jackson.databind' + end +end + +module Multimeter + class Json + def initialize + @object_mapper = Jackson::Databind::ObjectMapper.new + end + + def setup + metrics_module = Metrics::Json::MetricsModule.new(rate_unit = Java::JavaUtilConcurrent::TimeUnit::SECONDS, duration_unit = Java::JavaUtilConcurrent::TimeUnit::MILLISECONDS, show_samples = false) + @object_mapper.register_module(metrics_module) + self + end + + def dump(metric) + stream = Java::JavaIo::ByteArrayOutputStream.new + @object_mapper.write_value(stream, metric.to_java) + String.from_java_bytes(stream.to_byte_array) + end + + def self.dump(metric) + new.setup.dump(metric) + end + end +end \ No newline at end of file diff --git a/spec/multimeter/counter_spec.rb b/spec/multimeter/counter_spec.rb index ba3ed2e..26e6d3c 100644 --- a/spec/multimeter/counter_spec.rb +++ b/spec/multimeter/counter_spec.rb @@ -34,13 +34,10 @@ module Multimeter end end - describe '#to_h' do - it 'returns a hash representation of the counter' do + describe '#to_json' do + it 'returns a json representation of the counter' do counter.inc - expect(counter.to_h).to eq( - :type => :counter, - :count => 1 - ) + expect(JSON.parse(counter.to_json)).to include('count' => 1) end end end diff --git a/spec/multimeter/gauge_spec.rb b/spec/multimeter/gauge_spec.rb index 3be0e13..a485e8d 100644 --- a/spec/multimeter/gauge_spec.rb +++ b/spec/multimeter/gauge_spec.rb @@ -5,7 +5,7 @@ module Multimeter describe Gauge do let :gauge do - MetricRegistry.new.gauge('a_gauge') { @value } + MetricRegistry.new.gauge('a_gauge', :int) { @value } end before do @@ -20,12 +20,19 @@ module Multimeter end end - describe '#to_h' do - it 'returns a hash representation of the gauge' do - expect(gauge.to_h).to eq( - :type => :gauge, - :value => 3 - ) + describe '#to_json' do + it 'returns a json representation of the gauge' do + expect(JSON.parse(gauge.to_json)).to include('value' => 3) + end + + context 'when the gauge is untyped' do + let :gauge do + MetricRegistry.new.gauge('b_gauge') { 1 } + end + + it 'raises JsonMappingException' do + expect { gauge.to_json }.to raise_error(Jackson::Databind::JsonMappingException) + end end end end diff --git a/spec/multimeter/histogram_spec.rb b/spec/multimeter/histogram_spec.rb index 51189ce..7712fc3 100644 --- a/spec/multimeter/histogram_spec.rb +++ b/spec/multimeter/histogram_spec.rb @@ -69,21 +69,22 @@ module Multimeter end end - it 'returns a hash representation of the histogram' do - histogram.update(4) - h = histogram.to_h - expect(h[:type]).to eq(:histogram) - expect(h[:count]).to eq(1) - expect(h[:max]).to be_a(Numeric) - expect(h[:min]).to be_a(Numeric) - expect(h[:mean]).to be_a(Numeric) - expect(h[:std_dev]).to be_a(Numeric) - expect(h[:median]).to be_a(Numeric) - expect(h[:percentiles]['75']).to be_a(Numeric) - expect(h[:percentiles]['95']).to be_a(Numeric) - expect(h[:percentiles]['98']).to be_a(Numeric) - expect(h[:percentiles]['99']).to be_a(Numeric) - expect(h[:percentiles]['99.9']).to be_a(Numeric) + describe '#to_json' do + it 'returns a json representation of the histogram' do + histogram.update(4) + expect(JSON.parse(histogram.to_json)).to include( + 'count' => 1, + 'max' => 4.0, + 'min' => 4.0, + 'mean' => 4.0, + 'stddev' => 0.0, + 'p50' => 4.0, + 'p75' => 4.0, + 'p95' => 4.0, + 'p99' => 4.0, + 'p999' => 4.0, + ) + end end end end diff --git a/spec/multimeter/meter_spec.rb b/spec/multimeter/meter_spec.rb index ec031be..dd98524 100644 --- a/spec/multimeter/meter_spec.rb +++ b/spec/multimeter/meter_spec.rb @@ -42,16 +42,16 @@ module Multimeter end end - describe '#to_h' do - it 'returns a hash representation of the meter' do + describe '#to_json' do + it 'returns a json representation of the meter' do meter.mark - h = meter.to_h - expect(h[:type]).to eq(:meter) - expect(h[:count]).to eq(1) - expect(h[:mean_rate]).to be_a(Numeric) - expect(h[:one_minute_rate]).to be_a(Numeric) - expect(h[:five_minute_rate]).to be_a(Numeric) - expect(h[:fifteen_minute_rate]).to be_a(Numeric) + expect(JSON.parse(meter.to_json)).to include( + 'count' => 1, + 'mean_rate' => be_a(Numeric), + 'm1_rate' => be_a(Numeric), + 'm5_rate' => be_a(Numeric), + 'm15_rate' => be_a(Numeric), + ) end end end diff --git a/spec/multimeter/metric_registry_spec.rb b/spec/multimeter/metric_registry_spec.rb index cf711c9..d17dbb5 100644 --- a/spec/multimeter/metric_registry_spec.rb +++ b/spec/multimeter/metric_registry_spec.rb @@ -159,17 +159,21 @@ module Multimeter end end - describe '#to_h' do - it 'returns a hash representing all the metrics' do + describe '#to_json' do + it 'returns a json representing all the metrics' do metric_registry.counter('a_counter') metric_registry.meter('a_meter') metric_registry.timer('a_timer') metric_registry.histogram('an_histogram') - metric_registry.gauge('a_gauge') { 3 } - h = metric_registry.to_h - expect(h.keys).to contain_exactly(*%w[a_counter a_meter a_timer an_histogram a_gauge]) - h.each do |key, value| - expect(value).to eq(metric_registry.metrics[key].to_h) + metric_registry.gauge('a_gauge', :int) { 3 } + h = JSON.parse(metric_registry.to_json) + metric_types = %w[counters meters timers histograms gauges] + expect(h.keys).to match_array(metric_types + ['version']) + metric_types.each do |type| + metrics = h[type] + metrics.each do |name, value| + expect(value).to eq(JSON.parse(metric_registry.metrics[name].to_json)) + end end end end diff --git a/spec/multimeter/timer_spec.rb b/spec/multimeter/timer_spec.rb index 39ba2a7..b7c8d14 100644 --- a/spec/multimeter/timer_spec.rb +++ b/spec/multimeter/timer_spec.rb @@ -140,35 +140,25 @@ module Multimeter end end - describe '#to_h' do + describe '#to_json' do it 'returns a hash representation of the timer' do timer.update(100, :milliseconds) - h = timer.to_h - expect(h[:type]).to eq(:timer) - expect(h[:count]).to eq(1) - expect(h[:mean_rate]).to be_a(Numeric) - expect(h[:one_minute_rate]).to be_a(Numeric) - expect(h[:five_minute_rate]).to be_a(Numeric) - expect(h[:fifteen_minute_rate]).to be_a(Numeric) - expect(h[:max]).to be_a(Numeric) - expect(h[:min]).to be_a(Numeric) - expect(h[:mean]).to be_a(Numeric) - expect(h[:std_dev]).to be_a(Numeric) - expect(h[:median]).to be_a(Numeric) - expect(h[:percentiles]['75']).to be_a(Numeric) - expect(h[:percentiles]['95']).to be_a(Numeric) - expect(h[:percentiles]['98']).to be_a(Numeric) - expect(h[:percentiles]['99']).to be_a(Numeric) - expect(h[:percentiles]['99.9']).to be_a(Numeric) - end - - it 'scales the durations to milliseconds' do - timer.update(1, :seconds) - h = timer.to_h - expect(h[:max]).to eq(1000.0) - expect(h[:min]).to eq(1000.0) - expect(h[:mean]).to eq(1000.0) - expect(h[:median]).to eq(1000.0) + expect(JSON.parse(timer.to_json)).to include( + 'count' => 1, + 'mean_rate' => be_a(Numeric), + 'm1_rate' => be_a(Numeric), + 'm5_rate' => be_a(Numeric), + 'm15_rate' => be_a(Numeric), + 'max' => 100.0, + 'min' => 100.0, + 'mean' => 100.0, + 'stddev' => 0.0, + 'p50' => 100.0, + 'p75' => 100.0, + 'p95' => 100.0, + 'p99' => 100.0, + 'p999' => 100.0, + ) end end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 94de6a6..c29826e 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,3 +1,4 @@ require 'bundler/setup' require 'slf4j-jars' +require 'json' require 'multimeter' From 5a921cef9db25dbcebee9ab26652630988918d50 Mon Sep 17 00:00:00 2001 From: Joel Segerlind Date: Tue, 14 Jul 2015 01:18:03 +0200 Subject: [PATCH 03/11] Trailing newlines --- lib/multimeter/http.rb | 2 +- lib/multimeter/json.rb | 2 +- lib/multimeter/rack.rb | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/multimeter/http.rb b/lib/multimeter/http.rb index cee8ecf..7044fec 100644 --- a/lib/multimeter/http.rb +++ b/lib/multimeter/http.rb @@ -50,4 +50,4 @@ def join end end end -end \ No newline at end of file +end diff --git a/lib/multimeter/json.rb b/lib/multimeter/json.rb index 148127b..63ed347 100644 --- a/lib/multimeter/json.rb +++ b/lib/multimeter/json.rb @@ -36,4 +36,4 @@ def self.dump(metric) new.setup.dump(metric) end end -end \ No newline at end of file +end diff --git a/lib/multimeter/rack.rb b/lib/multimeter/rack.rb index a7e4239..04a03a6 100644 --- a/lib/multimeter/rack.rb +++ b/lib/multimeter/rack.rb @@ -67,4 +67,4 @@ def create_app(registry) App.new(registry).setup end end -end \ No newline at end of file +end From 5cbc809348ecd7a9e4eda4fb8a9127b1be47fe9d Mon Sep 17 00:00:00 2001 From: Joel Segerlind Date: Tue, 14 Jul 2015 08:53:51 +0200 Subject: [PATCH 04/11] Actually use #to_json now that it exists --- lib/multimeter/rack.rb | 35 +++-------------------------------- 1 file changed, 3 insertions(+), 32 deletions(-) diff --git a/lib/multimeter/rack.rb b/lib/multimeter/rack.rb index 04a03a6..746311b 100644 --- a/lib/multimeter/rack.rb +++ b/lib/multimeter/rack.rb @@ -1,19 +1,5 @@ # encoding: utf-8 -require 'metrics-json-jars' - -module Metrics - module Json - include_package 'com.codahale.metrics.json' - end -end - -module Jackson - module Databind - include_package 'com.fasterxml.jackson.databind' - end -end - module Multimeter module Rack class BadRequest < StandardError; end @@ -26,26 +12,19 @@ class BadRequest < StandardError; end class App def initialize(registry) @registry = registry - @object_mapper = Jackson::Databind::ObjectMapper.new - end - - def setup - metrics_module = Metrics::Json::MetricsModule.new(rate_unit = Java::JavaUtilConcurrent::TimeUnit::SECONDS, duration_unit = Java::JavaUtilConcurrent::TimeUnit::MILLISECONDS, show_samples = false) - @object_mapper.register_module(metrics_module) - self end def call(env) if (callback_name = env['QUERY_STRING'][/callback=([^$&]+)/, 1]) if callback_name =~ /^[\w\d.]+$/ - body = "#{callback_name}(#{generate_json});" + body = "#{callback_name}(#{@registry.to_json});" headers = JSONP_HEADERS else raise BadRequest end else headers = JSON_HEADERS - body = generate_json + body = @registry.to_json end [200, headers, [body]] rescue BadRequest => e @@ -53,18 +32,10 @@ def call(env) rescue => e [500, ERROR_HEADERS, ["Internal Server Error\n\n", e.message, "\n\t", *e.backtrace.join("\n\t")]] end - - private - - def generate_json - stream = Java::JavaIo::ByteArrayOutputStream.new - @object_mapper.write_value(stream, @registry.to_java) - String.from_java_bytes(stream.to_byte_array) - end end def create_app(registry) - App.new(registry).setup + App.new(registry) end end end From 17e1ac4cc71d3244fac00d80feeb5101631ade69 Mon Sep 17 00:00:00 2001 From: Joel Segerlind Date: Tue, 14 Jul 2015 09:43:04 +0200 Subject: [PATCH 05/11] Extract Http into separate gem --- Gemfile | 3 ++- lib/multimeter.rb | 2 -- lib/multimeter/http.rb | 5 +++++ multimeter-http.gemspec | 23 +++++++++++++++++++++++ multimeter.gemspec | 5 ++--- spec/spec_helper.rb | 1 + 6 files changed, 33 insertions(+), 6 deletions(-) create mode 100644 multimeter-http.gemspec diff --git a/Gemfile b/Gemfile index 0b53433..50e1b75 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,7 @@ source 'https://rubygems.org/' -gemspec +gemspec name: 'multimeter' +gemspec name: 'multimeter-http' group :examples do gem 'rack' diff --git a/lib/multimeter.rb b/lib/multimeter.rb index 9a7f054..3423ed8 100644 --- a/lib/multimeter.rb +++ b/lib/multimeter.rb @@ -3,7 +3,6 @@ require 'metrics-core-jars' require 'multimeter_metrics' require 'json' -require 'multimeter/http' require 'multimeter/rack' require 'multimeter/json' @@ -12,7 +11,6 @@ module Metrics end module Multimeter - extend Http extend Rack class MetricRegistry diff --git a/lib/multimeter/http.rb b/lib/multimeter/http.rb index 7044fec..52f897e 100644 --- a/lib/multimeter/http.rb +++ b/lib/multimeter/http.rb @@ -1,5 +1,6 @@ # encoding: utf-8 +require 'multimeter' require 'metrics-servlets-jars' require 'rjack-jetty' @@ -22,6 +23,8 @@ def http(registry, options={}) Server.new(server) end + private + def create_servlet_context(registry) context = Jetty::ServletContextHandler.new(Jetty::ServletContextHandler::SESSIONS) context.context_path = '/' @@ -50,4 +53,6 @@ def join end end end + + extend Http end diff --git a/multimeter-http.gemspec b/multimeter-http.gemspec new file mode 100644 index 0000000..316a8cb --- /dev/null +++ b/multimeter-http.gemspec @@ -0,0 +1,23 @@ +$: << File.expand_path('../lib', __FILE__) + +require 'rake' +require 'multimeter/version' + +Gem::Specification.new do |s| + s.name = 'multimeter-http' + s.version = Multimeter::VERSION + s.platform = 'java' + s.authors = ['Theo Hultberg', 'Joel Segerlind'] + s.email = ['theo@iconara.net', 'joel.segerlind@gmail.com'] + s.homepage = 'http://github.com/iconara/multimeter' + s.summary = 'Multimeter addition for serving metrics over HTTP' + s.description = 'Multimeter addition for serving metrics over HTTP, using Dropwizard\'s (a.k.a. Coda Hale\'s) Metrics libraries under the hood.' + s.license = 'Apache-2.0' + + s.add_dependency 'multimeter', "= #{Multimeter::VERSION}" + s.add_dependency 'metrics-servlets-jars', '~> 3.1' + s.add_dependency 'rjack-jetty', '~> 9.3' + + s.files = Dir['lib/multimeter/http.rb'] + s.require_paths = %w[lib] +end diff --git a/multimeter.gemspec b/multimeter.gemspec index 025c1cb..6d5cf1a 100644 --- a/multimeter.gemspec +++ b/multimeter.gemspec @@ -18,9 +18,8 @@ Gem::Specification.new do |s| s.rubyforge_project = 'multimeter' s.add_dependency 'metrics-core-jars', '~> 3.1', '< 4.0.0' - s.add_dependency 'metrics-servlets-jars', '~> 3.1' - s.add_dependency 'rjack-jetty', '~> 9.3' + s.add_dependency 'metrics-json-jars', '~> 3.1' - s.files = Dir['lib/**/*.rb', 'lib/**/*.jar'] + s.files = %w[lib/multimeter.rb lib/multimeter_metrics.jar lib/multimeter/json.rb lib/multimeter/rack.rb] s.require_paths = %w[lib] end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index c29826e..f68008e 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -2,3 +2,4 @@ require 'slf4j-jars' require 'json' require 'multimeter' +require 'multimeter/http' From cdc253bf031f3e73b4864f807cc3167316f0205f Mon Sep 17 00:00:00 2001 From: Joel Segerlind Date: Tue, 14 Jul 2015 09:43:18 +0200 Subject: [PATCH 06/11] Remove unused constant --- lib/multimeter.rb | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/multimeter.rb b/lib/multimeter.rb index 3423ed8..b1ed170 100644 --- a/lib/multimeter.rb +++ b/lib/multimeter.rb @@ -62,8 +62,4 @@ def self.create_registry def self.jmx(registry, options = {}) Metrics::JmxReporter.forRegistry(registry.to_java).inDomain(options[:domain] || 'multimeter').build.tap(&:start) end - - private - - NANO_TO_MILLI_SCALE = 1.0/1_000_000 end From 6cdd00da6e60e1f91ebb8cc770f16fb9148b5328 Mon Sep 17 00:00:00 2001 From: Joel Segerlind Date: Tue, 21 Jul 2015 00:40:05 +0200 Subject: [PATCH 07/11] Explicitly specify gem name in Rakefile --- Rakefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Rakefile b/Rakefile index 4a76554..bf79ba7 100644 --- a/Rakefile +++ b/Rakefile @@ -14,7 +14,7 @@ Rake::JavaExtensionTask.new('multimeter_metrics') do |ext| end namespace :bundler do - Bundler::GemHelper.install_tasks + Bundler::GemHelper.install_tasks(name: 'multimeter') end task :release => [:spec, :compile, 'bundler:release'] From 00c5dd55169bf9db17c0a20313041f86a01960cc Mon Sep 17 00:00:00 2001 From: Joel Segerlind Date: Tue, 21 Jul 2015 00:40:50 +0200 Subject: [PATCH 08/11] \#to_java is defined for every other class --- ext/java/multimeter/Snapshot.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ext/java/multimeter/Snapshot.java b/ext/java/multimeter/Snapshot.java index f3ba09c..14da82f 100644 --- a/ext/java/multimeter/Snapshot.java +++ b/ext/java/multimeter/Snapshot.java @@ -12,6 +12,7 @@ import org.jruby.runtime.ObjectAllocator; import org.jruby.runtime.ThreadContext; import org.jruby.runtime.builtin.IRubyObject; +import org.jruby.javasupport.JavaUtil; import static org.jruby.runtime.Visibility.PRIVATE; @@ -30,6 +31,11 @@ public Snapshot(Ruby runtime, com.codahale.metrics.Snapshot snapshot) { this.snapshot = snapshot; } + @JRubyMethod(name="to_java") + public IRubyObject toJava(ThreadContext ctx) { + return JavaUtil.convertJavaToUsableRubyObject(ctx.runtime, snapshot); + } + @JRubyMethod public IRubyObject size(ThreadContext ctx) { return ctx.runtime.newFixnum(snapshot.size()); From d185cd5176d009f5eed4916ddb2a2e5eeeb1d834 Mon Sep 17 00:00:00 2001 From: Joel Segerlind Date: Tue, 21 Jul 2015 00:43:42 +0200 Subject: [PATCH 09/11] Do JSON serialization in Java Rather than converting data back and forth --- ext/java/MultimeterMetricsService.java | 2 + ext/java/multimeter/Counter.java | 6 +++ ext/java/multimeter/Gauge.java | 6 +++ ext/java/multimeter/Histogram.java | 6 +++ ext/java/multimeter/JSONSerializer.java | 55 +++++++++++++++++++++++++ ext/java/multimeter/Meter.java | 6 +++ ext/java/multimeter/MetricRegistry.java | 5 +++ ext/java/multimeter/Snapshot.java | 6 +++ ext/java/multimeter/Timer.java | 6 +++ lib/multimeter.rb | 45 +------------------- lib/multimeter/json.rb | 39 ------------------ spec/multimeter/gauge_spec.rb | 4 +- spec/multimeter/rack_spec.rb | 4 +- 13 files changed, 103 insertions(+), 87 deletions(-) create mode 100644 ext/java/multimeter/JSONSerializer.java delete mode 100644 lib/multimeter/json.rb diff --git a/ext/java/MultimeterMetricsService.java b/ext/java/MultimeterMetricsService.java index 7c53adf..808dc72 100644 --- a/ext/java/MultimeterMetricsService.java +++ b/ext/java/MultimeterMetricsService.java @@ -10,6 +10,7 @@ import multimeter.Histogram; import multimeter.Snapshot; import multimeter.Gauge; +import multimeter.JSONSerializer; public class MultimeterMetricsService implements BasicLibraryService { public boolean basicLoad(final Ruby runtime) throws IOException { @@ -20,6 +21,7 @@ public boolean basicLoad(final Ruby runtime) throws IOException { Histogram.setup(runtime); Snapshot.setup(runtime); Gauge.setup(runtime); + JSONSerializer.setup(runtime); return true; } } diff --git a/ext/java/multimeter/Counter.java b/ext/java/multimeter/Counter.java index adf022d..d819ab4 100644 --- a/ext/java/multimeter/Counter.java +++ b/ext/java/multimeter/Counter.java @@ -6,6 +6,7 @@ import org.jruby.RubyObject; import org.jruby.RubyHash; import org.jruby.RubyArray; +import org.jruby.RubyString; import org.jruby.anno.JRubyClass; import org.jruby.anno.JRubyMethod; import org.jruby.runtime.Block; @@ -36,6 +37,11 @@ public IRubyObject toJava(ThreadContext ctx) { return JavaUtil.convertJavaToUsableRubyObject(ctx.runtime, counter); } + @JRubyMethod(name="to_json") + public RubyString toJson(ThreadContext ctx) { + return JSONSerializer.getInstance().serialize(ctx, counter); + } + @JRubyMethod public IRubyObject count(ThreadContext ctx) { return ctx.runtime.newFixnum(counter.getCount()); diff --git a/ext/java/multimeter/Gauge.java b/ext/java/multimeter/Gauge.java index 2c026f2..f2402e4 100644 --- a/ext/java/multimeter/Gauge.java +++ b/ext/java/multimeter/Gauge.java @@ -6,6 +6,7 @@ import org.jruby.RubyObject; import org.jruby.RubyHash; import org.jruby.RubyArray; +import org.jruby.RubyString; import org.jruby.anno.JRubyClass; import org.jruby.anno.JRubyMethod; import org.jruby.runtime.Block; @@ -36,6 +37,11 @@ public IRubyObject toJava(ThreadContext ctx) { return JavaUtil.convertJavaToUsableRubyObject(ctx.runtime, gauge); } + @JRubyMethod(name="to_json") + public RubyString toJson(ThreadContext ctx) { + return JSONSerializer.getInstance().serialize(ctx, gauge); + } + @JRubyMethod public IRubyObject value(ThreadContext ctx) { Object value = gauge.getValue(); diff --git a/ext/java/multimeter/Histogram.java b/ext/java/multimeter/Histogram.java index eb6d278..0d554b3 100644 --- a/ext/java/multimeter/Histogram.java +++ b/ext/java/multimeter/Histogram.java @@ -6,6 +6,7 @@ import org.jruby.RubyObject; import org.jruby.RubyHash; import org.jruby.RubyArray; +import org.jruby.RubyString; import org.jruby.anno.JRubyClass; import org.jruby.anno.JRubyMethod; import org.jruby.runtime.Block; @@ -36,6 +37,11 @@ public IRubyObject toJava(ThreadContext ctx) { return JavaUtil.convertJavaToUsableRubyObject(ctx.runtime, histogram); } + @JRubyMethod(name="to_json") + public RubyString toJson(ThreadContext ctx) { + return JSONSerializer.getInstance().serialize(ctx, histogram); + } + @JRubyMethod public IRubyObject count(ThreadContext ctx) { return ctx.runtime.newFixnum(histogram.getCount()); diff --git a/ext/java/multimeter/JSONSerializer.java b/ext/java/multimeter/JSONSerializer.java new file mode 100644 index 0000000..3e53849 --- /dev/null +++ b/ext/java/multimeter/JSONSerializer.java @@ -0,0 +1,55 @@ +package multimeter; + +import com.codahale.metrics.json.MetricsModule; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.concurrent.TimeUnit; +import org.jruby.Ruby; +import org.jruby.RubyClass; +import org.jruby.RubyException; +import org.jruby.exceptions.RaiseException; +import org.jruby.RubyModule; +import org.jruby.RubyString; +import org.jruby.anno.JRubyClass; +import org.jruby.runtime.ThreadContext; + +public class JSONSerializer { + private ObjectMapper mapper; + private MetricsModule module; + + public static void setup(Ruby runtime) { + RubyModule multimeterModule = runtime.defineModule("Multimeter"); + RubyClass standardError = runtime.getStandardError(); + multimeterModule.defineClassUnder("JSONError", standardError, standardError.getAllocator()); + } + + @JRubyClass(name="Multimeter::JSONError", parent="StandardError") + public static class JSONError {} + + private static JSONSerializer instance; + + public static JSONSerializer getInstance() { + if ( instance == null ) { + instance = new JSONSerializer(); + } + return instance; + } + + private JSONSerializer() { + this.mapper = new ObjectMapper(); + this.module = new MetricsModule(TimeUnit.SECONDS, TimeUnit.MILLISECONDS, false); + mapper.registerModule(module); + } + + public RubyString serialize(ThreadContext ctx, Object object) { + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + try { + mapper.writeValue(stream, object); + return RubyString.newString(ctx.runtime, stream.toByteArray()); + } catch (IOException e) { + RubyClass jsonError = ctx.runtime.getModule("Multimeter").getClass("JSONError"); + throw new RaiseException(RubyException.newException(ctx.runtime, jsonError, e.toString()), false); + } + } +} \ No newline at end of file diff --git a/ext/java/multimeter/Meter.java b/ext/java/multimeter/Meter.java index db79050..2290527 100644 --- a/ext/java/multimeter/Meter.java +++ b/ext/java/multimeter/Meter.java @@ -6,6 +6,7 @@ import org.jruby.RubyObject; import org.jruby.RubyHash; import org.jruby.RubyArray; +import org.jruby.RubyString; import org.jruby.anno.JRubyClass; import org.jruby.anno.JRubyMethod; import org.jruby.runtime.Block; @@ -36,6 +37,11 @@ public IRubyObject toJava(ThreadContext ctx) { return JavaUtil.convertJavaToUsableRubyObject(ctx.runtime, meter); } + @JRubyMethod(name="to_json") + public RubyString toJson(ThreadContext ctx) { + return JSONSerializer.getInstance().serialize(ctx, meter); + } + @JRubyMethod public IRubyObject count(ThreadContext ctx) { return ctx.runtime.newFixnum(meter.getCount()); diff --git a/ext/java/multimeter/MetricRegistry.java b/ext/java/multimeter/MetricRegistry.java index bd8fbc8..b042f72 100644 --- a/ext/java/multimeter/MetricRegistry.java +++ b/ext/java/multimeter/MetricRegistry.java @@ -49,6 +49,11 @@ public IRubyObject toJava(ThreadContext ctx) { return JavaUtil.convertJavaToUsableRubyObject(ctx.runtime, registry); } + @JRubyMethod(name="to_json") + public RubyString toJson(ThreadContext ctx) { + return JSONSerializer.getInstance().serialize(ctx, registry); + } + @JRubyMethod public IRubyObject metrics(ThreadContext ctx) { return metrics.dup(ctx); diff --git a/ext/java/multimeter/Snapshot.java b/ext/java/multimeter/Snapshot.java index 14da82f..f68c30e 100644 --- a/ext/java/multimeter/Snapshot.java +++ b/ext/java/multimeter/Snapshot.java @@ -6,6 +6,7 @@ import org.jruby.RubyObject; import org.jruby.RubyHash; import org.jruby.RubyArray; +import org.jruby.RubyString; import org.jruby.anno.JRubyClass; import org.jruby.anno.JRubyMethod; import org.jruby.runtime.Block; @@ -36,6 +37,11 @@ public IRubyObject toJava(ThreadContext ctx) { return JavaUtil.convertJavaToUsableRubyObject(ctx.runtime, snapshot); } + @JRubyMethod(name="to_json") + public RubyString toJson(ThreadContext ctx) { + return JSONSerializer.getInstance().serialize(ctx, snapshot); + } + @JRubyMethod public IRubyObject size(ThreadContext ctx) { return ctx.runtime.newFixnum(snapshot.size()); diff --git a/ext/java/multimeter/Timer.java b/ext/java/multimeter/Timer.java index e58d651..5b06758 100644 --- a/ext/java/multimeter/Timer.java +++ b/ext/java/multimeter/Timer.java @@ -6,6 +6,7 @@ import org.jruby.RubyObject; import org.jruby.RubyHash; import org.jruby.RubyArray; +import org.jruby.RubyString; import org.jruby.anno.JRubyClass; import org.jruby.anno.JRubyMethod; import org.jruby.runtime.Block; @@ -41,6 +42,11 @@ public IRubyObject toJava(ThreadContext ctx) { return JavaUtil.convertJavaToUsableRubyObject(ctx.runtime, timer); } + @JRubyMethod(name="to_json") + public RubyString toJson(ThreadContext ctx) { + return JSONSerializer.getInstance().serialize(ctx, timer); + } + @JRubyMethod public IRubyObject count(ThreadContext ctx) { return ctx.runtime.newFixnum(timer.getCount()); diff --git a/lib/multimeter.rb b/lib/multimeter.rb index b1ed170..28c2a18 100644 --- a/lib/multimeter.rb +++ b/lib/multimeter.rb @@ -1,10 +1,9 @@ # encoding: utf-8 require 'metrics-core-jars' +require 'metrics-json-jars' require 'multimeter_metrics' -require 'json' require 'multimeter/rack' -require 'multimeter/json' module Metrics include_package 'com.codahale.metrics' @@ -13,48 +12,6 @@ module Metrics module Multimeter extend Rack - class MetricRegistry - def to_json - Json.dump(self) - end - end - - class Meter - def to_json - Json.dump(self) - end - end - - class Counter - def to_json - Json.dump(self) - end - end - - class Histogram - def to_json - Json.dump(self) - end - end - - class Timer - def to_json - Json.dump(self) - end - end - - class Snapshot - def to_json - Json.dump(self) - end - end - - class Gauge - def to_json - Json.dump(self) - end - end - def self.create_registry MetricRegistry.new end diff --git a/lib/multimeter/json.rb b/lib/multimeter/json.rb deleted file mode 100644 index 63ed347..0000000 --- a/lib/multimeter/json.rb +++ /dev/null @@ -1,39 +0,0 @@ -# encoding: utf-8 - -require 'metrics-json-jars' - -module Metrics - module Json - include_package 'com.codahale.metrics.json' - end -end - -module Jackson - module Databind - include_package 'com.fasterxml.jackson.databind' - end -end - -module Multimeter - class Json - def initialize - @object_mapper = Jackson::Databind::ObjectMapper.new - end - - def setup - metrics_module = Metrics::Json::MetricsModule.new(rate_unit = Java::JavaUtilConcurrent::TimeUnit::SECONDS, duration_unit = Java::JavaUtilConcurrent::TimeUnit::MILLISECONDS, show_samples = false) - @object_mapper.register_module(metrics_module) - self - end - - def dump(metric) - stream = Java::JavaIo::ByteArrayOutputStream.new - @object_mapper.write_value(stream, metric.to_java) - String.from_java_bytes(stream.to_byte_array) - end - - def self.dump(metric) - new.setup.dump(metric) - end - end -end diff --git a/spec/multimeter/gauge_spec.rb b/spec/multimeter/gauge_spec.rb index a485e8d..b9424da 100644 --- a/spec/multimeter/gauge_spec.rb +++ b/spec/multimeter/gauge_spec.rb @@ -30,8 +30,8 @@ module Multimeter MetricRegistry.new.gauge('b_gauge') { 1 } end - it 'raises JsonMappingException' do - expect { gauge.to_json }.to raise_error(Jackson::Databind::JsonMappingException) + it 'raises JsonError' do + expect { gauge.to_json }.to raise_error(JSONError) end end end diff --git a/spec/multimeter/rack_spec.rb b/spec/multimeter/rack_spec.rb index de6d2ea..8e08fa0 100644 --- a/spec/multimeter/rack_spec.rb +++ b/spec/multimeter/rack_spec.rb @@ -82,13 +82,13 @@ module Multimeter context '500' do it 'responds with error 500 if an exception is thrown in the request handling' do - allow(registry).to receive(:to_java).and_raise('blurgh') + allow(registry).to receive(:to_json).and_raise('blurgh') status, headers, body = app.call({'QUERY_STRING' => ''}) expect(status).to eq(500) end it 'responds with Connection: close' do - allow(registry).to receive(:to_java).and_raise('blurgh') + allow(registry).to receive(:to_json).and_raise('blurgh') status, headers, body = app.call({'QUERY_STRING' => ''}) expect(headers).to include('Connection' => 'close') end From 124461aa64a334369b052b8b210a097ba69597f9 Mon Sep 17 00:00:00 2001 From: Joel Segerlind Date: Tue, 21 Jul 2015 01:10:31 +0200 Subject: [PATCH 10/11] Remove stray ; --- lib/multimeter/http.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/multimeter/http.rb b/lib/multimeter/http.rb index 52f897e..601355c 100644 --- a/lib/multimeter/http.rb +++ b/lib/multimeter/http.rb @@ -28,7 +28,7 @@ def http(registry, options={}) def create_servlet_context(registry) context = Jetty::ServletContextHandler.new(Jetty::ServletContextHandler::SESSIONS) context.context_path = '/' - context.set_attribute(Metrics::Servlets::MetricsServlet::METRICS_REGISTRY, registry.to_java); + context.set_attribute(Metrics::Servlets::MetricsServlet::METRICS_REGISTRY, registry.to_java) context.add_servlet(Metrics::Servlets::MetricsServlet.java_class, '/*') context end From 57e876b621c7e574866c6b17219e63cfa2a2442d Mon Sep 17 00:00:00 2001 From: Joel Segerlind Date: Tue, 21 Jul 2015 08:49:08 +0200 Subject: [PATCH 11/11] Downgrade Jetty to v9.2.12.0 To support Java 7 --- multimeter-http.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/multimeter-http.gemspec b/multimeter-http.gemspec index 316a8cb..811c102 100644 --- a/multimeter-http.gemspec +++ b/multimeter-http.gemspec @@ -16,7 +16,7 @@ Gem::Specification.new do |s| s.add_dependency 'multimeter', "= #{Multimeter::VERSION}" s.add_dependency 'metrics-servlets-jars', '~> 3.1' - s.add_dependency 'rjack-jetty', '~> 9.3' + s.add_dependency 'rjack-jetty', '~> 9.2.12' s.files = Dir['lib/multimeter/http.rb'] s.require_paths = %w[lib]