ffwd 0.1.0

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.
Files changed (61) hide show
  1. checksums.yaml +7 -0
  2. data/bin/ffwd +9 -0
  3. data/bin/fwc +15 -0
  4. data/lib/em/all.rb +68 -0
  5. data/lib/ffwd.rb +250 -0
  6. data/lib/ffwd/channel.rb +62 -0
  7. data/lib/ffwd/circular_buffer.rb +78 -0
  8. data/lib/ffwd/connection.rb +40 -0
  9. data/lib/ffwd/core.rb +173 -0
  10. data/lib/ffwd/core/emitter.rb +38 -0
  11. data/lib/ffwd/core/interface.rb +47 -0
  12. data/lib/ffwd/core/processor.rb +92 -0
  13. data/lib/ffwd/core/reporter.rb +32 -0
  14. data/lib/ffwd/debug.rb +76 -0
  15. data/lib/ffwd/debug/connection.rb +48 -0
  16. data/lib/ffwd/debug/monitor_session.rb +71 -0
  17. data/lib/ffwd/debug/tcp.rb +82 -0
  18. data/lib/ffwd/event.rb +65 -0
  19. data/lib/ffwd/event_emitter.rb +57 -0
  20. data/lib/ffwd/handler.rb +43 -0
  21. data/lib/ffwd/lifecycle.rb +92 -0
  22. data/lib/ffwd/logging.rb +139 -0
  23. data/lib/ffwd/metric.rb +55 -0
  24. data/lib/ffwd/metric_emitter.rb +50 -0
  25. data/lib/ffwd/plugin.rb +149 -0
  26. data/lib/ffwd/plugin/json_line.rb +47 -0
  27. data/lib/ffwd/plugin/json_line/connection.rb +118 -0
  28. data/lib/ffwd/plugin/log.rb +35 -0
  29. data/lib/ffwd/plugin/log/writer.rb +42 -0
  30. data/lib/ffwd/plugin_channel.rb +64 -0
  31. data/lib/ffwd/plugin_loader.rb +121 -0
  32. data/lib/ffwd/processor.rb +96 -0
  33. data/lib/ffwd/processor/count.rb +109 -0
  34. data/lib/ffwd/processor/histogram.rb +200 -0
  35. data/lib/ffwd/processor/rate.rb +116 -0
  36. data/lib/ffwd/producing_client.rb +181 -0
  37. data/lib/ffwd/protocol.rb +28 -0
  38. data/lib/ffwd/protocol/tcp.rb +126 -0
  39. data/lib/ffwd/protocol/tcp/bind.rb +64 -0
  40. data/lib/ffwd/protocol/tcp/connection.rb +107 -0
  41. data/lib/ffwd/protocol/tcp/flushing_connect.rb +135 -0
  42. data/lib/ffwd/protocol/tcp/plain_connect.rb +74 -0
  43. data/lib/ffwd/protocol/udp.rb +48 -0
  44. data/lib/ffwd/protocol/udp/bind.rb +64 -0
  45. data/lib/ffwd/protocol/udp/connect.rb +110 -0
  46. data/lib/ffwd/reporter.rb +65 -0
  47. data/lib/ffwd/retrier.rb +72 -0
  48. data/lib/ffwd/schema.rb +92 -0
  49. data/lib/ffwd/schema/default.rb +36 -0
  50. data/lib/ffwd/schema/spotify100.rb +58 -0
  51. data/lib/ffwd/statistics.rb +29 -0
  52. data/lib/ffwd/statistics/collector.rb +99 -0
  53. data/lib/ffwd/statistics/system_statistics.rb +255 -0
  54. data/lib/ffwd/tunnel.rb +27 -0
  55. data/lib/ffwd/tunnel/plugin.rb +47 -0
  56. data/lib/ffwd/tunnel/tcp.rb +60 -0
  57. data/lib/ffwd/tunnel/udp.rb +61 -0
  58. data/lib/ffwd/utils.rb +46 -0
  59. data/lib/ffwd/version.rb +18 -0
  60. data/lib/fwc.rb +206 -0
  61. metadata +163 -0
@@ -0,0 +1,139 @@
1
+ # $LICENSE
2
+ # Copyright 2013-2014 Spotify AB. All rights reserved.
3
+ #
4
+ # The contents of this file are licensed under the Apache License, Version 2.0
5
+ # (the "License"); you may not use this file except in compliance with the
6
+ # License. You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12
+ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13
+ # License for the specific language governing permissions and limitations under
14
+ # the License.
15
+
16
+ # Logging functionality.
17
+ #
18
+ # Defines FFWD::Logging which when included in either a class or module makes
19
+ # the 'log' field available both as a class and instance field.
20
+ #
21
+ # 'log' in turn is an object with the following fields available.
22
+ #
23
+ # 'debug' - Log a message of level DEBUG..
24
+ # 'info' - Log a message of level INFO.
25
+ # 'warning' - Log a message of level WARNING.
26
+ # 'error' - Log a message of level ERROR.
27
+ #
28
+ # Every function takes the message to log as the only parameter except 'error'
29
+ # which can take an exception as a secondary argument.
30
+ #
31
+ # If an exception is provided, a stacktrace will be printed to log.
32
+ require 'logger'
33
+
34
+ module FFWD
35
+ def self.log
36
+ @log ||= setup_log
37
+ end
38
+
39
+ def self.log_reload
40
+ @log = setup_log
41
+ end
42
+
43
+ def self.setup_log
44
+ if log_config[:file]
45
+ file = log_config[:file]
46
+ shift_age = log_config[:shift_age]
47
+
48
+ return ::Logger.new(file, shift_age=shift_age).tap do |l|
49
+ l.level = log_config[:level]
50
+ l.progname = log_config[:progname]
51
+ end
52
+ end
53
+
54
+ if log_config[:stream]
55
+ return ::Logger.new(log_config[:stream]).tap do |l|
56
+ l.level = log_config[:level]
57
+ l.progname = log_config[:progname]
58
+ end
59
+ end
60
+
61
+ raise "cannot setup loggin with options: #{log_config}"
62
+ end
63
+
64
+ def self.log_config
65
+ return @log_config unless @log_config.nil?
66
+
67
+ @log_config = {
68
+ :file => nil,
69
+ :shift_age => 1,
70
+ :level => Logger::INFO,
71
+ :stream => STDOUT,
72
+ :progname => 'FFWD',
73
+ }
74
+ end
75
+
76
+ def self.log_disable
77
+ @log_disable = true
78
+ end
79
+
80
+ def self.log_disabled?
81
+ @log_disable || false
82
+ end
83
+
84
+ class ClassLogger
85
+ def initialize klass
86
+ @progname = klass.name
87
+ end
88
+
89
+ def debug message
90
+ FFWD.log.debug(@progname){message}
91
+ end
92
+
93
+ def info message
94
+ FFWD.log.info(@progname){message}
95
+ end
96
+
97
+ def warning message
98
+ FFWD.log.warn(@progname){message}
99
+ end
100
+
101
+ def error message, e=nil
102
+ FFWD.log.error(@progname){message}
103
+
104
+ return unless e
105
+
106
+ FFWD.log.error(@progname){"Caused by #{e.class}: #{e}"}
107
+ e.backtrace.each do |b|
108
+ FFWD.log.error(@progname){" #{b}"}
109
+ end
110
+ end
111
+ end
112
+
113
+ class FakeLogger
114
+ def debug message; end
115
+ def info message; end
116
+ def warning message; end
117
+ def error message, e=nil; end
118
+ end
119
+
120
+ module Logging
121
+ module ClassMethods
122
+ attr_accessor :log
123
+ end
124
+
125
+ def log
126
+ self.class.log
127
+ end
128
+
129
+ def self.included klass
130
+ klass.extend ClassMethods
131
+
132
+ if FFWD.log_disabled?
133
+ klass.log = FakeLogger.new
134
+ else
135
+ klass.log = ClassLogger.new klass
136
+ end
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,55 @@
1
+ # $LICENSE
2
+ # Copyright 2013-2014 Spotify AB. All rights reserved.
3
+ #
4
+ # The contents of this file are licensed under the Apache License, Version 2.0
5
+ # (the "License"); you may not use this file except in compliance with the
6
+ # License. You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12
+ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13
+ # License for the specific language governing permissions and limitations under
14
+ # the License.
15
+
16
+ module FFWD
17
+ # Struct used to define all fields related to a metric.
18
+ MetricStruct = Struct.new(
19
+ # The time at which the metric was collected.
20
+ :time,
21
+ # The unique key of the metric.
22
+ :key,
23
+ # A numeric value associated with the metric.
24
+ :value,
25
+ # The host from which the metric originated.
26
+ :host,
27
+ # The source metric this metric was derived from (if any).
28
+ :source,
29
+ # Tags associated to the metric.
30
+ :tags,
31
+ # Attributes (extra fields) associated to the metric.
32
+ :attributes
33
+ )
34
+
35
+ # A convenience class for each individual metric.
36
+ class Metric < MetricStruct
37
+ def self.make opts={}
38
+ new(opts[:time], opts[:key], opts[:value], opts[:host], opts[:source],
39
+ opts[:tags], opts[:attributes])
40
+ end
41
+
42
+ # Convert metric to a sparse hash.
43
+ def to_h
44
+ d = {}
45
+ d[:time] = time.to_i if time
46
+ d[:key] = key if key
47
+ d[:value] = value if value
48
+ d[:host] = host if host
49
+ d[:source] = source if source
50
+ d[:tags] = tags.to_a if tags
51
+ d[:attributes] = attributes if attributes
52
+ d
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,50 @@
1
+ # $LICENSE
2
+ # Copyright 2013-2014 Spotify AB. All rights reserved.
3
+ #
4
+ # The contents of this file are licensed under the Apache License, Version 2.0
5
+ # (the "License"); you may not use this file except in compliance with the
6
+ # License. You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12
+ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13
+ # License for the specific language governing permissions and limitations under
14
+ # the License.
15
+
16
+ require_relative 'utils'
17
+ require_relative 'metric'
18
+
19
+ module FFWD
20
+ # Used to emit metrics to an 'output' channel
21
+ #
22
+ # Can take two parts of a configuration 'base' and 'opts' to decide which
23
+ # metadata emitted metrics should be decorated with.
24
+ class MetricEmitter
25
+ def self.build output, base, opts
26
+ host = opts[:host] || base[:host] || FFWD.current_host
27
+ tags = FFWD.merge_sets base[:tags], opts[:tags]
28
+ attributes = FFWD.merge_hashes base[:attributes], opts[:attributes]
29
+ new output, host, tags, attributes
30
+ end
31
+
32
+ def initialize output, host, tags, attributes
33
+ @output = output
34
+ @host = host
35
+ @tags = tags
36
+ @attributes = attributes
37
+ end
38
+
39
+ def emit m
40
+ m[:time] ||= Time.now
41
+ m[:host] ||= @host if @host
42
+ m[:tags] = FFWD.merge_sets @tags, m[:tags]
43
+ m[:attributes] = FFWD.merge_hashes @attributes, m[:attributes]
44
+
45
+ @output.metric Metric.make(m)
46
+ rescue => e
47
+ log.error "Failed to emit metric", e
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,149 @@
1
+ # $LICENSE
2
+ # Copyright 2013-2014 Spotify AB. All rights reserved.
3
+ #
4
+ # The contents of this file are licensed under the Apache License, Version 2.0
5
+ # (the "License"); you may not use this file except in compliance with the
6
+ # License. You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12
+ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13
+ # License for the specific language governing permissions and limitations under
14
+ # the License.
15
+
16
+ require_relative 'logging'
17
+
18
+ module FFWD
19
+ module Plugin
20
+ class Loaded
21
+ attr_reader :source, :name
22
+
23
+ def initialize source, name, options
24
+ @source = source
25
+ @name = name
26
+ @mod = options[:mod]
27
+ @setup_input_method = load_method @mod, options[:setup_input_method_name]
28
+ @setup_output_method = load_method @mod, options[:setup_output_method_name]
29
+ @setup_tunnel_method = load_method @mod, options[:setup_tunnel_method_name]
30
+ end
31
+
32
+ def capabilities
33
+ capabilities = []
34
+
35
+ if not @setup_input_method.nil?
36
+ capabilities << "input"
37
+ end
38
+
39
+ if not @setup_output_method.nil?
40
+ capabilities << "output"
41
+ end
42
+
43
+ if not @setup_tunnel_method.nil?
44
+ capabilities << "tunnel"
45
+ end
46
+
47
+ return capabilities
48
+ end
49
+
50
+ def can?(kind)
51
+ not get(kind).nil?
52
+ end
53
+
54
+ def get(kind)
55
+ return @setup_input_method if kind == :input
56
+ return @setup_output_method if kind == :output
57
+ return @setup_tunnel_method if kind == :tunnel
58
+ return nil
59
+ end
60
+
61
+ private
62
+
63
+ def load_method mod, method_name
64
+ return nil unless mod.respond_to? method_name
65
+ return mod.method method_name
66
+ end
67
+ end
68
+
69
+ class Setup
70
+ attr_reader :name, :config
71
+
72
+ def initialize name, setup, config
73
+ @name = name
74
+ @setup = setup
75
+ @config = config
76
+ end
77
+
78
+ def setup *args
79
+ @setup.call @config, *args
80
+ end
81
+ end
82
+
83
+ def self.discovered
84
+ @@discovered ||= {}
85
+ end
86
+
87
+ def self.loaded
88
+ @@loaded ||= {}
89
+ end
90
+
91
+ module ClassMethods
92
+ def register_plugin(name, opts={})
93
+ options = {:mod => self}
94
+
95
+ options[:setup_input_method_name] = (opts[:setup_input_method] || :setup_input)
96
+ options[:setup_output_method_name] = (opts[:setup_output_method] || :setup_output)
97
+ options[:setup_tunnel_method_name] = (opts[:setup_tunnel_method] || :setup_tunnel)
98
+
99
+ FFWD::Plugin.discovered[name] = options
100
+ end
101
+ end
102
+
103
+ def self.included mod
104
+ mod.extend ClassMethods
105
+ end
106
+
107
+ def self.category
108
+ 'plugin'
109
+ end
110
+
111
+ def self.load_discovered source
112
+ FFWD::Plugin.discovered.each do |name, options|
113
+ FFWD::Plugin.loaded[name] = Loaded.new source, name, options
114
+ end
115
+
116
+ FFWD::Plugin.discovered.clear
117
+ end
118
+
119
+ def self.load_plugins log, kind_name, config, kind
120
+ result = []
121
+
122
+ if config.nil?
123
+ return result
124
+ end
125
+
126
+ config.each_with_index do |plugin_config, index|
127
+ d = "#{kind_name} plugin ##{index}"
128
+
129
+ if (name = plugin_config[:type]).nil?
130
+ log.error "#{d}: Missing :type attribute for '#{kind_name}'"
131
+ end
132
+
133
+ if (plugin = FFWD::Plugin.loaded[name]).nil?
134
+ log.error "#{d}: Not an available plugin '#{name}'"
135
+ next
136
+ end
137
+
138
+ unless plugin.can?(kind)
139
+ log.error "#{d}: Not an #{kind_name} plugin '#{name}'"
140
+ next
141
+ end
142
+
143
+ result << Setup.new(name, plugin.get(kind), plugin_config)
144
+ end
145
+
146
+ return result
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,47 @@
1
+ # $LICENSE
2
+ # Copyright 2013-2014 Spotify AB. All rights reserved.
3
+ #
4
+ # The contents of this file are licensed under the Apache License, Version 2.0
5
+ # (the "License"); you may not use this file except in compliance with the
6
+ # License. You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12
+ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13
+ # License for the specific language governing permissions and limitations under
14
+ # the License.
15
+
16
+ require 'eventmachine'
17
+
18
+ require 'ffwd/protocol'
19
+ require 'ffwd/plugin'
20
+ require 'ffwd/logging'
21
+
22
+ require_relative 'json_line/connection'
23
+
24
+ module FFWD::Plugin::JsonLine
25
+ include FFWD::Plugin
26
+ include FFWD::Logging
27
+
28
+ register_plugin "json_line"
29
+
30
+ DEFAULT_HOST = "localhost"
31
+ DEFAULT_PORT = 19000
32
+
33
+ def self.setup_input opts, core
34
+ opts[:host] ||= DEFAULT_HOST
35
+ opts[:port] ||= DEFAULT_PORT
36
+ buffer_limit = opts["buffer_limit"] || 1000
37
+ protocol = FFWD.parse_protocol(opts[:protocol] || "tcp")
38
+ protocol.bind opts, core, log, Connection, buffer_limit
39
+ end
40
+
41
+ def self.setup_tunnel opts, core, tunnel
42
+ opts[:port] ||= DEFAULT_PORT
43
+ buffer_limit = opts["buffer_limit"] || 1000
44
+ protocol = FFWD.parse_protocol(opts[:protocol] || "tcp")
45
+ protocol.tunnel opts, core, tunnel, log, Connection, buffer_limit
46
+ end
47
+ end
@@ -0,0 +1,118 @@
1
+ # $LICENSE
2
+ # Copyright 2013-2014 Spotify AB. All rights reserved.
3
+ #
4
+ # The contents of this file are licensed under the Apache License, Version 2.0
5
+ # (the "License"); you may not use this file except in compliance with the
6
+ # License. You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12
+ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13
+ # License for the specific language governing permissions and limitations under
14
+ # the License.
15
+
16
+ require 'eventmachine'
17
+
18
+ require 'ffwd/logging'
19
+ require 'ffwd/connection'
20
+
21
+ module FFWD::Plugin::JsonLine
22
+ class Connection < FFWD::Connection
23
+ include FFWD::Logging
24
+ include EM::Protocols::LineText2
25
+
26
+ EVENT_FIELDS = [
27
+ ["key", :key],
28
+ ["value", :value],
29
+ ["host", :host],
30
+ ["state", :state],
31
+ ["description", :description],
32
+ ["ttl", :ttl],
33
+ ["tags", :tags],
34
+ ["attributes", :attributes],
35
+ ]
36
+
37
+ METRIC_FIELDS = [
38
+ ["proc", :proc],
39
+ ["key", :key],
40
+ ["value", :value],
41
+ ["tags", :tags],
42
+ ["attributes", :attributes]
43
+ ]
44
+
45
+ def self.plugin_type
46
+ "json_line_in"
47
+ end
48
+
49
+ def initialize bind, core, buffer_limit
50
+ @bind = bind
51
+ @core = core
52
+ @buffer_limit = buffer_limit
53
+ end
54
+
55
+ def receive_line data
56
+ data = JSON.load(data)
57
+
58
+ unless type = data["type"]
59
+ log.error "Field 'type' missing from received line"
60
+ return
61
+ end
62
+
63
+ if type == "metric"
64
+ @core.input.metric read_metric(data)
65
+ @bind.increment :received_metric
66
+ return
67
+ end
68
+
69
+ if type == "event"
70
+ @core.input.event read_event(data)
71
+ @bind.increment :received_event
72
+ return
73
+ end
74
+
75
+ log.error "No such type: #{type}"
76
+ rescue => e
77
+ log.error "Failed to receive line", e
78
+ end
79
+
80
+ def read_tags d, source
81
+ return if (tags = d["tags"]).nil?
82
+ d[:tags] = tags.to_set
83
+ end
84
+
85
+ def read_time d, source
86
+ return if (time = d["time"]).nil?
87
+ d[:time] = Time.at time
88
+ end
89
+
90
+ def read_metric data
91
+ d = {}
92
+
93
+ read_tags d, data["tags"]
94
+ read_time d, data["time"]
95
+
96
+ METRIC_FIELDS.each do |from, to|
97
+ next if (v = data[from]).nil?
98
+ d[to] = v
99
+ end
100
+
101
+ d
102
+ end
103
+
104
+ def read_event data
105
+ d = {}
106
+
107
+ read_tags d, data["tags"]
108
+ read_time d, data["time"]
109
+
110
+ EVENT_FIELDS.each do |from, to|
111
+ next if (v = data[from]).nil?
112
+ d[to] = v
113
+ end
114
+
115
+ d
116
+ end
117
+ end
118
+ end