skylight 0.0.2
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/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
|