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.
@@ -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