leafy-rack 0.4.0-java

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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