leafy-rack 0.4.0-java

Sign up to get free protection for your applications and to get access to all the features.
data/lib/leafy/rack.rb ADDED
@@ -0,0 +1 @@
1
+ require 'leafy-rack_jars'
@@ -0,0 +1,73 @@
1
+ require 'leafy/rack/metrics'
2
+ require 'leafy/rack/health'
3
+ require 'leafy/rack/ping'
4
+ require 'leafy/rack/thread_dump'
5
+
6
+ module Leafy
7
+ module Rack
8
+ class Admin
9
+
10
+ def self.response( path )
11
+ path = path.sub( /^\//, '' )
12
+ [
13
+ 200,
14
+ { 'Content-Type' => 'text/html' },
15
+ [ <<EOF
16
+ <DOCTYPE html>
17
+ <html>
18
+ <head>
19
+ <title>metrics</title>
20
+ </head>
21
+ <body>
22
+ <h1>menu</h1>
23
+ <ul>
24
+ <li><a href='#{path}/metrics'>metrics</a> (<a href='#{path}/metrics?pretty'>pretty</a>)</li>
25
+ <li><a href='#{path}/health'>health</a> (<a href='#{path}/health?pretty'>pretty</a>)</li>
26
+ <li><a href='#{path}/ping'>ping</a></li>
27
+ <li><a href='#{path}/threads'>thread-dump</a></li>
28
+ </ul>
29
+ </body>
30
+ </html>
31
+ EOF
32
+ ]
33
+ ]
34
+ end
35
+
36
+ def initialize(app, metrics_registry, health_registry, path = '/admin')
37
+ @app = app
38
+ @path = path
39
+ @metrics = metrics_registry
40
+ @health = health_registry
41
+ end
42
+
43
+ def call(env)
44
+ if ( path = env['PATH_INFO'] ).start_with? @path
45
+ dispatch( path.sub( /#{@path}/, ''), env )
46
+ else
47
+ @app.call( env )
48
+ end
49
+ end
50
+
51
+ private
52
+
53
+ def dispatch( path, env )
54
+ case path
55
+ when '/metrics'
56
+ Metrics.response( @metrics.metrics, env )
57
+ when '/health'
58
+ Health.response( @health.health, env )
59
+ when '/ping'
60
+ Ping.response
61
+ when '/threads'
62
+ ThreadDump.response
63
+ when '/'
64
+ Admin.response( @path )
65
+ when ''
66
+ Admin.response( @path )
67
+ else # let the app deal with "not found"
68
+ @app.call( env )
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,43 @@
1
+ require 'leafy/rack'
2
+ require 'leafy/json/health_writer'
3
+ require 'json' unless defined? :JSON
4
+
5
+ module Leafy
6
+ module Rack
7
+ class Health
8
+
9
+ def self.response( health, env )
10
+ data = health.run_health_checks
11
+ is_healthy = data.values.all? { |r| r.healthy? }
12
+ json = if env[ 'QUERY_STRING' ] == 'pretty'
13
+ JSON.pretty_generate( data.to_hash )
14
+ else
15
+ # to use data.to_hash.to_json did produce
16
+ # a different json structure on some
17
+ # rack setup
18
+ JSON.generate( data.to_hash )
19
+ end
20
+ [
21
+ is_healthy ? 200 : 503,
22
+ { 'Content-Type' => 'application/json',
23
+ 'Cache-Control' => 'must-revalidate,no-cache,no-store' },
24
+ [ json ]
25
+ ]
26
+ end
27
+
28
+ def initialize(app, registry, path = '/health')
29
+ @app = app
30
+ @path = path
31
+ @registry = registry
32
+ end
33
+
34
+ def call(env)
35
+ if env['PATH_INFO'] == @path
36
+ Health.response( @registry.health, env )
37
+ else
38
+ @app.call( env )
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,20 @@
1
+ require 'leafy/rack'
2
+ require 'leafy/instrumented/instrumented'
3
+
4
+ module Leafy
5
+ module Rack
6
+ class Instrumented
7
+
8
+ def initialize( app, instrumented )
9
+ @app = app
10
+ @instrumented = instrumented
11
+ end
12
+
13
+ def call( env )
14
+ @instrumented.call do
15
+ @app.call env
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,35 @@
1
+ require 'leafy/rack'
2
+ require 'leafy/metrics'
3
+ require 'leafy/json/metrics_writer'
4
+ module Leafy
5
+ module Rack
6
+
7
+ class Metrics
8
+
9
+ WRITER = ::Leafy::Json::MetricsWriter.new
10
+
11
+ def self.response( metrics, env )
12
+ [
13
+ 200,
14
+ { 'Content-Type' => 'application/json',
15
+ 'Cache-Control' => 'must-revalidate,no-cache,no-store' },
16
+ [ WRITER.to_json( metrics, env[ 'QUERY_STRING' ] == 'pretty' ) ]
17
+ ]
18
+ end
19
+
20
+ def initialize(app, registry, path = '/metrics')
21
+ @app = app
22
+ @path = path
23
+ @registry = registry
24
+ end
25
+
26
+ def call(env)
27
+ if env['PATH_INFO'] == @path
28
+ Metrics.response( @registry.metrics, env )
29
+ else
30
+ @app.call( env )
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,29 @@
1
+ require 'leafy/rack'
2
+ module Leafy
3
+ module Rack
4
+ class Ping
5
+
6
+ def self.response
7
+ [
8
+ 200,
9
+ { 'Content-Type' => 'text/plain',
10
+ 'Cache-Control' => 'must-revalidate,no-cache,no-store' },
11
+ [ 'pong' ]
12
+ ]
13
+ end
14
+
15
+ def initialize(app, path = '/ping')
16
+ @app = app
17
+ @path = path
18
+ end
19
+
20
+ def call(env)
21
+ if env['PATH_INFO'] == @path
22
+ Ping.response
23
+ else
24
+ @app.call( env )
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,56 @@
1
+ require 'leafy/rack'
2
+
3
+ java_import java.lang.management.ManagementFactory
4
+
5
+ module Leafy
6
+ module Rack
7
+ class ThreadDumpWriter
8
+
9
+ def initialize
10
+ begin
11
+ # Some PaaS like Google App Engine blacklist java.lang.managament
12
+ @threads = com.codahale.metrics.jvm.ThreadDump.new(ManagementFactory.getThreadMXBean());
13
+ rescue LoadError
14
+ # we won't be able to provide thread dump
15
+ end
16
+ end
17
+
18
+ def to_text
19
+ if @threads
20
+ # TODO make this stream
21
+ output = java.io.ByteArrayOutputStream.new
22
+ @threads.dump(output)
23
+ output.to_s
24
+ end
25
+ end
26
+ end
27
+
28
+ class ThreadDump
29
+
30
+ WRITER = ThreadDumpWriter.new
31
+
32
+ def self.response
33
+ dump = WRITER.to_text
34
+ [
35
+ 200,
36
+ { 'Content-Type' => 'text/plain',
37
+ 'Cache-Control' => 'must-revalidate,no-cache,no-store' },
38
+ [ dump ? dump : 'Sorry your runtime environment does not allow to dump threads.' ]
39
+ ]
40
+ end
41
+
42
+ def initialize(app, path = '/threads')
43
+ @app = app
44
+ @path = path
45
+ end
46
+
47
+ def call(env)
48
+ if env['PATH_INFO'] == @path
49
+ ThreadDump.response
50
+ else
51
+ @app.call( env )
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,6 @@
1
+ module Leafy
2
+ module Rack
3
+ VERSION = '0.4.0'.freeze
4
+ end
5
+ end
6
+
@@ -0,0 +1,141 @@
1
+ require_relative 'setup'
2
+
3
+ require 'leafy/rack/admin'
4
+ require 'yaml'
5
+ require 'json'
6
+
7
+ describe Leafy::Rack::Admin do
8
+
9
+ subject { Leafy::Rack::Admin }
10
+
11
+ let( :expected_headers ) do
12
+ { 'Content-Type' => 'text/html' }
13
+ end
14
+
15
+ let( :metrics ) { Leafy::Metrics::Registry.new }
16
+
17
+ let( :health ) { Leafy::Health::Registry.new }
18
+
19
+ let( :health_report ) do
20
+ { 'version' => '3.0.0',
21
+ 'gauges' => {},
22
+ 'counters' => {},
23
+ 'histograms' => {},
24
+ 'meters' => {},
25
+ 'timers' => {}
26
+ }
27
+ end
28
+
29
+ let( :result ){ [ 200, nil, [] ] }
30
+ let( :app ) do
31
+ Proc.new() { result }
32
+ end
33
+
34
+ it 'has response' do
35
+ status, headers, body = subject.response( "/path" )
36
+ expect( status ).to eq 200
37
+ expect( headers.to_yaml).to eq expected_headers.to_yaml
38
+ expect( body.join.count("\n" ) ).to eq 15
39
+ expect( body.join.gsub( 'path/' ).collect { |f| f }.size ).to eq 6
40
+ end
41
+
42
+ describe 'default path' do
43
+ subject { Leafy::Rack::Admin.new( app, metrics, health ) }
44
+
45
+ it 'passes request if not matches the given path' do
46
+ env = { 'PATH_INFO'=> '/something' }
47
+ expect( subject.call( env ) ).to eq result
48
+ end
49
+
50
+ it 'shows menu page on admin path' do
51
+ env = { 'PATH_INFO'=> '/admin' }
52
+ status, headers, body = subject.call( env )
53
+ expect( status ).to eq 200
54
+ expect( headers.to_yaml).to eq expected_headers.to_yaml
55
+ expect( body.join.count("\n" ) ).to eq 15
56
+ end
57
+
58
+ it 'pongs on ping path' do
59
+ env = { 'PATH_INFO'=> '/admin/ping' }
60
+ status, _, body = subject.call( env )
61
+ expect( status ).to eq 200
62
+ expect( body.join ).to eq 'pong'
63
+ end
64
+
65
+ it 'thread dump on threads path' do
66
+ env = { 'PATH_INFO'=> '/admin/threads' }
67
+ status, _, body = subject.call( env )
68
+ expect( status ).to eq 200
69
+ expect( body.join.count( "\n" ) ).to be > 100
70
+ end
71
+
72
+ it 'reports metrics on metrics path' do
73
+ env = { 'PATH_INFO'=> '/admin/metrics' }
74
+ status, _, body = subject.call( env )
75
+ expect( status ).to eq 200
76
+ expect( body.join.count( "\n" ) ).to eq 0
77
+ body = JSON.parse(body.join)
78
+ expect( body.to_yaml ).to eq health_report.to_yaml
79
+ end
80
+
81
+ it 'reports health-checks on health path' do
82
+ env = { 'PATH_INFO'=> '/admin/health' }
83
+ status, _, body = subject.call( env )
84
+ expect( status ).to eq 200
85
+ expect( body.join.count( "\n" ) ).to eq 0
86
+ body = JSON.parse(body.join)
87
+ expect( body.to_yaml ).to eq "--- {}\n"
88
+ end
89
+ end
90
+
91
+ describe 'custom path' do
92
+ subject { Leafy::Rack::Admin.new( app, metrics, health, '/custom' ) }
93
+
94
+ it 'passes request if not matches the given path' do
95
+ env = { 'PATH_INFO'=> '/something' }
96
+ expect( subject.call( env ) ).to eq result
97
+ env = { 'PATH_INFO'=> '/ping' }
98
+ expect( subject.call( env ) ).to eq result
99
+ end
100
+
101
+ it 'shows menu page on admin path' do
102
+ env = { 'PATH_INFO'=> '/custom' }
103
+ status, headers, body = subject.call( env )
104
+ expect( status ).to eq 200
105
+ expect( headers.to_yaml).to eq expected_headers.to_yaml
106
+ expect( body.join.count("\n" ) ).to eq 15
107
+ end
108
+
109
+ it 'pongs on ping path' do
110
+ env = { 'PATH_INFO'=> '/custom/ping' }
111
+ status, _, body = subject.call( env )
112
+ expect( status ).to eq 200
113
+ expect( body.join ).to eq 'pong'
114
+ end
115
+
116
+ it 'thread dump on threads path' do
117
+ env = { 'PATH_INFO'=> '/custom/threads' }
118
+ status, _, body = subject.call( env )
119
+ expect( status ).to eq 200
120
+ expect( body.join.count( "\n" ) ).to be > 100
121
+ end
122
+
123
+ it 'reports metrics on metrics path' do
124
+ env = { 'PATH_INFO'=> '/custom/metrics' }
125
+ status, _, body = subject.call( env )
126
+ expect( status ).to eq 200
127
+ expect( body.join.count( "\n" ) ).to eq 0
128
+ body = JSON.parse(body.join)
129
+ expect( body.to_yaml ).to eq health_report.to_yaml
130
+ end
131
+
132
+ it 'reports health-checks on health path' do
133
+ env = { 'PATH_INFO'=> '/custom/health' }
134
+ status, _, body = subject.call( env )
135
+ expect( status ).to eq 200
136
+ expect( body.join.count( "\n" ) ).to eq 0
137
+ body = JSON.parse(body.join)
138
+ expect( body.to_yaml ).to eq "--- {}\n"
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,26 @@
1
+ require_relative 'setup'
2
+
3
+ require 'leafy/instrumented/basic_instrumented'
4
+
5
+ describe Leafy::Instrumented::BasicInstrumented do
6
+
7
+ subject { Leafy::Instrumented::BasicInstrumented.new( registry, "name" ) }
8
+
9
+ let( :registry ) { Leafy::Metrics::Registry.new }
10
+
11
+ let( :app ) do
12
+ Proc.new() do
13
+ sleep 0.1
14
+ [ 200, nil, "" ]
15
+ end
16
+ end
17
+
18
+ it 'collects metrics for a call' do
19
+ _, _, _ = subject.call do
20
+ app.call
21
+ end
22
+ expect( registry.metrics.meters.keys ).to eq [ 'name.responseCodes.other' ]
23
+ expect( registry.metrics.meters.values.collect { |a| a.count } ).to eq [ 1 ]
24
+ expect( registry.metrics.meters.values.first.mean_rate ).to be > 5.0
25
+ end
26
+ end