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.
- checksums.yaml +7 -0
- data/.gitignore +4 -0
- data/Gemfile +10 -0
- data/LICENSE +22 -0
- data/README.md +155 -0
- data/Rakefile +14 -0
- data/leafy-rack.gemspec +30 -0
- data/lib/leafy-rack.rb +1 -0
- data/lib/leafy-rack_jars.rb +10 -0
- data/lib/leafy/instrumented/basic_instrumented.rb +35 -0
- data/lib/leafy/instrumented/collected_instrumented.rb +30 -0
- data/lib/leafy/instrumented/instrumented.rb +53 -0
- data/lib/leafy/json/health_writer.rb +15 -0
- data/lib/leafy/json/json_writer.rb +33 -0
- data/lib/leafy/json/metrics_writer.rb +22 -0
- data/lib/leafy/rack.rb +1 -0
- data/lib/leafy/rack/admin.rb +73 -0
- data/lib/leafy/rack/health.rb +43 -0
- data/lib/leafy/rack/instrumented.rb +20 -0
- data/lib/leafy/rack/metrics.rb +35 -0
- data/lib/leafy/rack/ping.rb +29 -0
- data/lib/leafy/rack/thread_dump.rb +56 -0
- data/lib/leafy/rack/version.rb +6 -0
- data/spec/admin_rack_spec.rb +141 -0
- data/spec/basic_instrumented_spec.rb +26 -0
- data/spec/collected_instrumented_spec.rb +32 -0
- data/spec/health_rack_spec.rb +136 -0
- data/spec/health_writer_spec.rb +47 -0
- data/spec/instrumented_rack_spec.rb +37 -0
- data/spec/instrumented_spec.rb +56 -0
- data/spec/metrics_rack_spec.rb +110 -0
- data/spec/metrics_writer_spec.rb +55 -0
- data/spec/ping_rack_spec.rb +63 -0
- data/spec/setup.rb +3 -0
- data/spec/thread_dump_spec.rb +59 -0
- metadata +165 -0
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,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
|