skylight 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +20 -0
- data/lib/skylight.rb +33 -0
- data/lib/skylight/compat.rb +9 -0
- data/lib/skylight/compat/notifications.rb +175 -0
- data/lib/skylight/compat/notifications/fanout.rb +166 -0
- data/lib/skylight/compat/notifications/instrumenter.rb +65 -0
- data/lib/skylight/config.rb +82 -0
- data/lib/skylight/connection.rb +25 -0
- data/lib/skylight/instrumenter.rb +106 -0
- data/lib/skylight/json_proto.rb +82 -0
- data/lib/skylight/middleware.rb +23 -0
- data/lib/skylight/railtie.rb +67 -0
- data/lib/skylight/subscriber.rb +37 -0
- data/lib/skylight/trace.rb +109 -0
- data/lib/skylight/util/atomic.rb +73 -0
- data/lib/skylight/util/bytes.rb +40 -0
- data/lib/skylight/util/clock.rb +37 -0
- data/lib/skylight/util/ewma.rb +32 -0
- data/lib/skylight/util/gzip.rb +15 -0
- data/lib/skylight/util/queue.rb +93 -0
- data/lib/skylight/util/uniform_sample.rb +63 -0
- data/lib/skylight/util/uuid.rb +33 -0
- data/lib/skylight/version.rb +3 -0
- data/lib/skylight/worker.rb +232 -0
- metadata +85 -0
@@ -0,0 +1,82 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
module Skylight
|
4
|
+
class Config
|
5
|
+
|
6
|
+
def self.load_from_yaml(path)
|
7
|
+
new do |config|
|
8
|
+
data = YAML.load_file(path)
|
9
|
+
data.each do |key, value|
|
10
|
+
if config.respond_to?("#{key}=")
|
11
|
+
config.send("#{key}=", value)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def initialize(attrs = {})
|
18
|
+
@ssl = true
|
19
|
+
@deflate = true
|
20
|
+
@host = "agent.skylight.io"
|
21
|
+
@port = 443
|
22
|
+
@interval = 5
|
23
|
+
@protocol = JsonProto.new(self)
|
24
|
+
@max_pending_traces = 500
|
25
|
+
@samples_per_interval = 100
|
26
|
+
|
27
|
+
@logger = Logger.new(STDOUT)
|
28
|
+
@logger.level = Logger::INFO
|
29
|
+
|
30
|
+
attrs.each do |k, v|
|
31
|
+
if respond_to?("#{k}=")
|
32
|
+
send("#{k}=", v)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
yield self if block_given?
|
37
|
+
end
|
38
|
+
|
39
|
+
attr_accessor :authentication_token
|
40
|
+
|
41
|
+
attr_accessor :ssl
|
42
|
+
alias_method :ssl?, :ssl
|
43
|
+
|
44
|
+
attr_accessor :deflate
|
45
|
+
alias_method :deflate?, :deflate
|
46
|
+
|
47
|
+
attr_accessor :host
|
48
|
+
|
49
|
+
attr_accessor :port
|
50
|
+
|
51
|
+
attr_accessor :samples_per_interval
|
52
|
+
|
53
|
+
attr_accessor :interval
|
54
|
+
|
55
|
+
attr_accessor :max_pending_traces
|
56
|
+
|
57
|
+
attr_reader :protocol
|
58
|
+
def protocol=(val)
|
59
|
+
if val.is_a?(String) || val.is_a?(Symbol)
|
60
|
+
class_name = val.to_s.capitalize+"Proto"
|
61
|
+
val = Skylight.const_get(class_name).new(self)
|
62
|
+
end
|
63
|
+
@protocol = val
|
64
|
+
end
|
65
|
+
|
66
|
+
attr_accessor :logger
|
67
|
+
|
68
|
+
def log_level
|
69
|
+
logger && logger.level
|
70
|
+
end
|
71
|
+
|
72
|
+
def log_level=(level)
|
73
|
+
if logger
|
74
|
+
if level.is_a?(String) || level.is_a?(Symbol)
|
75
|
+
level = Logger.const_get(level.to_s.upcase)
|
76
|
+
end
|
77
|
+
logger.level = level
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Skylight
|
2
|
+
class Connection
|
3
|
+
|
4
|
+
def self.open(host, port, ssl)
|
5
|
+
conn = new(host, port, ssl)
|
6
|
+
conn.open
|
7
|
+
conn
|
8
|
+
end
|
9
|
+
|
10
|
+
def initialize(host, port, ssl)
|
11
|
+
@host = host
|
12
|
+
@port = port
|
13
|
+
@ssl = ssl
|
14
|
+
end
|
15
|
+
|
16
|
+
def open
|
17
|
+
# stuff
|
18
|
+
end
|
19
|
+
|
20
|
+
def close
|
21
|
+
# stuff
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
module Skylight
|
2
|
+
class Instrumenter
|
3
|
+
|
4
|
+
# Maximum number of traces to sample for each interval
|
5
|
+
SAMPLE_SIZE = 100
|
6
|
+
|
7
|
+
# Time interval for each sample in seconds
|
8
|
+
INTERVAL = 5
|
9
|
+
|
10
|
+
def self.start!(config = Config.new)
|
11
|
+
# Convert a hash to a config object
|
12
|
+
if Hash === config
|
13
|
+
config = Config.new config
|
14
|
+
end
|
15
|
+
|
16
|
+
new(config).start!
|
17
|
+
end
|
18
|
+
|
19
|
+
attr_reader :config, :worker, :samples
|
20
|
+
|
21
|
+
def initialize(config)
|
22
|
+
@config = config
|
23
|
+
@worker = Worker.new(self)
|
24
|
+
end
|
25
|
+
|
26
|
+
def start!
|
27
|
+
@worker.start!
|
28
|
+
Subscriber.register!
|
29
|
+
|
30
|
+
# Ensure properly configured
|
31
|
+
return unless config
|
32
|
+
|
33
|
+
# Ensure that there is an API token
|
34
|
+
unless config.authentication_token
|
35
|
+
if logger = config.logger
|
36
|
+
logger.warn "[SKYLIGHT] No authentication token provided; cannot start agent."
|
37
|
+
end
|
38
|
+
|
39
|
+
return
|
40
|
+
end
|
41
|
+
|
42
|
+
self
|
43
|
+
end
|
44
|
+
|
45
|
+
def trace(endpoint = nil)
|
46
|
+
# If there already is a trace going on, then just continue
|
47
|
+
if Thread.current[Trace::KEY]
|
48
|
+
return yield
|
49
|
+
end
|
50
|
+
|
51
|
+
# If the request should not be sampled, yield
|
52
|
+
unless trace = create_trace(endpoint)
|
53
|
+
return yield
|
54
|
+
end
|
55
|
+
|
56
|
+
# Otherwise, setup the new trace and continue
|
57
|
+
begin
|
58
|
+
Thread.current[Trace::KEY] = trace
|
59
|
+
yield(trace)
|
60
|
+
ensure
|
61
|
+
Thread.current[Trace::KEY] = nil
|
62
|
+
|
63
|
+
begin
|
64
|
+
trace.commit
|
65
|
+
process(trace)
|
66
|
+
rescue Exception => e
|
67
|
+
error(e)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
def create_trace(endpoint)
|
75
|
+
Trace.new(endpoint)
|
76
|
+
# Paranoia
|
77
|
+
rescue => e
|
78
|
+
error e
|
79
|
+
nil
|
80
|
+
end
|
81
|
+
|
82
|
+
def process(trace)
|
83
|
+
debug "Submitting trace to worker"
|
84
|
+
unless @worker.submit(trace)
|
85
|
+
config.logger.warn("[SKYLIGHT] Could not submit trace to worker")
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def error(msg)
|
90
|
+
return unless l = config.logger
|
91
|
+
|
92
|
+
if Error == msg
|
93
|
+
msg = "#{e.message} (#{e.class}) - #{e.backtrace && e.backtrace.first}"
|
94
|
+
end
|
95
|
+
|
96
|
+
l.error "[SKYLIGHT] #{msg}"
|
97
|
+
rescue
|
98
|
+
end
|
99
|
+
|
100
|
+
def debug(msg)
|
101
|
+
return unless l = config.logger
|
102
|
+
l.debug "[SKYLIGHT] #{msg}"
|
103
|
+
end
|
104
|
+
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module Skylight
|
4
|
+
class JsonProto
|
5
|
+
def initialize(config)
|
6
|
+
@config = config
|
7
|
+
end
|
8
|
+
|
9
|
+
def write(out, from, counts, sample)
|
10
|
+
|
11
|
+
# {
|
12
|
+
# batch: {
|
13
|
+
# timestamp: 123456, // Second granularity
|
14
|
+
# endpoints: [
|
15
|
+
# {
|
16
|
+
# name: "WidgetsController#index",
|
17
|
+
# count: 100, // There can be a higher count # than there are traces
|
18
|
+
# traces: [
|
19
|
+
# {
|
20
|
+
# // Trace UUID
|
21
|
+
# uuid: "d11d7190-40cc-11e2-a25f-0800200c9a66",
|
22
|
+
# spans: [
|
23
|
+
# [
|
24
|
+
# null, // parent-id -- index of the parent span or null if root node
|
25
|
+
# 0292352, // Span start timestamp in 0.1ms granularity
|
26
|
+
# 20, // Duration of the span in 0.1ms granularity
|
27
|
+
# "action_controller.process", // Span category
|
28
|
+
# "Processing WidgetsController#index" // Span description
|
29
|
+
# ],
|
30
|
+
# [
|
31
|
+
# 0, // The previous span is this span's parent
|
32
|
+
# 1340923,
|
33
|
+
# 0, // No duration
|
34
|
+
# "log.info", // category
|
35
|
+
# "Doing some stuff..." // Span description
|
36
|
+
# ]
|
37
|
+
# ]
|
38
|
+
# }
|
39
|
+
# ]
|
40
|
+
# },
|
41
|
+
# // etc...
|
42
|
+
# ]
|
43
|
+
# }
|
44
|
+
# }
|
45
|
+
|
46
|
+
hash = {
|
47
|
+
:batch => {
|
48
|
+
:timestamp => Util.clock.to_seconds(from),
|
49
|
+
}
|
50
|
+
}
|
51
|
+
|
52
|
+
hash[:batch][:endpoints] = counts.map do |endpoint, count|
|
53
|
+
ehash = {
|
54
|
+
:name => endpoint,
|
55
|
+
:count => count
|
56
|
+
}
|
57
|
+
|
58
|
+
traces = sample.select{|t| t.endpoint == endpoint }
|
59
|
+
|
60
|
+
ehash[:traces] = traces.map do |t|
|
61
|
+
# thash = { :uuid => t.ident }
|
62
|
+
thash = { :uuid => "TODO" }
|
63
|
+
|
64
|
+
thash[:spans] = t.spans.map do |s|
|
65
|
+
[s.parent,
|
66
|
+
s.started_at,
|
67
|
+
s.ended_at - s.started_at,
|
68
|
+
s.category,
|
69
|
+
s.description
|
70
|
+
]
|
71
|
+
end
|
72
|
+
|
73
|
+
thash
|
74
|
+
end
|
75
|
+
|
76
|
+
ehash
|
77
|
+
end
|
78
|
+
|
79
|
+
out << hash.to_json
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Skylight
|
2
|
+
class Middleware
|
3
|
+
attr_reader :instrumenter
|
4
|
+
|
5
|
+
def self.new(app, instrumenter)
|
6
|
+
return app unless instrumenter
|
7
|
+
super
|
8
|
+
end
|
9
|
+
|
10
|
+
def initialize(app, instrumenter)
|
11
|
+
@instrumenter = instrumenter
|
12
|
+
@app = app
|
13
|
+
end
|
14
|
+
|
15
|
+
def call(env)
|
16
|
+
instrumenter.trace("Rack") do
|
17
|
+
ActiveSupport::Notifications.instrument("rack.request") do
|
18
|
+
@app.call(env)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'skylight'
|
2
|
+
require 'rails'
|
3
|
+
|
4
|
+
module Skylight
|
5
|
+
class Railtie < Rails::Railtie
|
6
|
+
# The environments in which skylight should be inabled
|
7
|
+
config.environments = ['production']
|
8
|
+
|
9
|
+
# The path to the configuration file
|
10
|
+
config.skylight_config_path = "config/skylight.yml"
|
11
|
+
|
12
|
+
attr_accessor :instrumenter
|
13
|
+
|
14
|
+
initializer "skylight.configure" do |app|
|
15
|
+
if self.instrumenter = load_instrumenter
|
16
|
+
Rails.logger.debug "[SKYLIGHT] Installing middleware"
|
17
|
+
app.middleware.insert 0, Middleware, instrumenter
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def environments
|
24
|
+
Array(config.environments).map { |e| e && e.to_s }.compact
|
25
|
+
end
|
26
|
+
|
27
|
+
def load_instrumenter
|
28
|
+
if environments.include?(Rails.env.to_s)
|
29
|
+
if c = load_config
|
30
|
+
Rails.logger.debug "[SKYLIGHT] Starting instrumenter"
|
31
|
+
Instrumenter.start!(c)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
# Paranoia
|
35
|
+
rescue
|
36
|
+
nil
|
37
|
+
end
|
38
|
+
|
39
|
+
def load_config
|
40
|
+
unless path = config.skylight_config_path
|
41
|
+
Rails.logger.warn "[SKYLIGHT] Path to config YAML file unset"
|
42
|
+
return
|
43
|
+
end
|
44
|
+
|
45
|
+
path = File.expand_path(Rails.root.join(path))
|
46
|
+
|
47
|
+
unless File.exist?(path)
|
48
|
+
Rails.logger.warn "[SKYLIGHT] Config does not exist at `#{path}`"
|
49
|
+
return
|
50
|
+
end
|
51
|
+
|
52
|
+
ret = Config.load_from_yaml(path)
|
53
|
+
|
54
|
+
unless ret.authentication_token
|
55
|
+
Rails.logger.warn "[SKYLIGHT] Config does not include an authentication token"
|
56
|
+
return
|
57
|
+
end
|
58
|
+
|
59
|
+
ret.logger = Rails.logger
|
60
|
+
|
61
|
+
ret
|
62
|
+
rescue => e
|
63
|
+
Rails.logger.error "[SKYLIGHT] #{e.message} (#{e.class}) - #{e.backtrace.first}"
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Skylight
|
2
|
+
# TODO: Handle filtering out notifications that we don't care about
|
3
|
+
class Subscriber
|
4
|
+
PROCESS_ACTION = "process_action.action_controller"
|
5
|
+
|
6
|
+
def self.register!
|
7
|
+
ActiveSupport::Notifications.subscribe nil, new
|
8
|
+
end
|
9
|
+
|
10
|
+
def start(name, id, payload)
|
11
|
+
return unless trace = Trace.current
|
12
|
+
|
13
|
+
if name == PROCESS_ACTION
|
14
|
+
trace.endpoint = controller_action(payload)
|
15
|
+
end
|
16
|
+
|
17
|
+
trace.start(name, nil, payload)
|
18
|
+
end
|
19
|
+
|
20
|
+
def finish(name, id, payload)
|
21
|
+
return unless trace = Trace.current
|
22
|
+
trace.stop
|
23
|
+
end
|
24
|
+
|
25
|
+
def measure(name, id, payload)
|
26
|
+
return unless trace = Trace.current
|
27
|
+
trace.record(name, nil, payload)
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def controller_action(payload)
|
33
|
+
"#{payload[:controller]}##{payload[:action]}"
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
module Skylight
|
2
|
+
class Trace
|
3
|
+
KEY = :__skylight_current_trace
|
4
|
+
|
5
|
+
def self.current
|
6
|
+
Thread.current[KEY]
|
7
|
+
end
|
8
|
+
|
9
|
+
# Struct to track each span
|
10
|
+
class Span < Struct.new(
|
11
|
+
:parent,
|
12
|
+
:started_at,
|
13
|
+
:ended_at,
|
14
|
+
:category,
|
15
|
+
:description,
|
16
|
+
:annotations)
|
17
|
+
|
18
|
+
def key
|
19
|
+
@key ||= [category, description]
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
attr_reader :endpoint, :ident, :spans
|
24
|
+
attr_writer :endpoint
|
25
|
+
|
26
|
+
def initialize(endpoint = "Unknown", ident = nil)
|
27
|
+
@ident = ident
|
28
|
+
@endpoint = endpoint
|
29
|
+
@spans = []
|
30
|
+
|
31
|
+
# Tracks the ID of the current parent
|
32
|
+
@parent = nil
|
33
|
+
end
|
34
|
+
|
35
|
+
def from
|
36
|
+
return unless span = @spans.first
|
37
|
+
span.started_at
|
38
|
+
end
|
39
|
+
|
40
|
+
def to
|
41
|
+
return unless span = @spans.last
|
42
|
+
span.ended_at
|
43
|
+
end
|
44
|
+
|
45
|
+
def record(cat, desc = nil, annot = nil)
|
46
|
+
span = build_span(cat, desc, annot)
|
47
|
+
span.ended_at = span.started_at
|
48
|
+
|
49
|
+
@spans << span
|
50
|
+
|
51
|
+
self
|
52
|
+
end
|
53
|
+
|
54
|
+
def start(cat, desc = nil, annot = nil)
|
55
|
+
span = build_span(cat, desc, annot)
|
56
|
+
|
57
|
+
@parent = @spans.length
|
58
|
+
|
59
|
+
@spans << span
|
60
|
+
|
61
|
+
self
|
62
|
+
end
|
63
|
+
|
64
|
+
def stop
|
65
|
+
# Find last unclosed span
|
66
|
+
span = @spans.last
|
67
|
+
while span && span.ended_at
|
68
|
+
span = span.parent ? @spans[span.parent] : nil
|
69
|
+
end
|
70
|
+
|
71
|
+
raise "trace unbalanced" unless span
|
72
|
+
|
73
|
+
# Set ended_at
|
74
|
+
span.ended_at = now
|
75
|
+
|
76
|
+
# Update the parent
|
77
|
+
@parent = @spans[@parent].parent
|
78
|
+
|
79
|
+
self
|
80
|
+
end
|
81
|
+
|
82
|
+
# Requires global synchronization
|
83
|
+
def commit
|
84
|
+
raise "trace unbalanced" if @parent
|
85
|
+
|
86
|
+
@ident ||= gen_ident
|
87
|
+
|
88
|
+
# No more changes should be made
|
89
|
+
freeze
|
90
|
+
|
91
|
+
self
|
92
|
+
end
|
93
|
+
|
94
|
+
private
|
95
|
+
|
96
|
+
def now
|
97
|
+
Util.clock.now
|
98
|
+
end
|
99
|
+
|
100
|
+
def gen_ident
|
101
|
+
Util::UUID.gen Digest::MD5.digest(@endpoint)[0, 2]
|
102
|
+
end
|
103
|
+
|
104
|
+
def build_span(cat, desc, annot)
|
105
|
+
Span.new(@parent, now, nil, cat, desc || "", annot)
|
106
|
+
end
|
107
|
+
|
108
|
+
end
|
109
|
+
end
|