skylight 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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