atatus 1.0.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 (115) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +16 -0
  3. data/CHANGELOG.md +11 -0
  4. data/Gemfile +57 -0
  5. data/LICENSE +65 -0
  6. data/LICENSE-THIRD-PARTY +205 -0
  7. data/README.md +13 -0
  8. data/Rakefile +19 -0
  9. data/atatus.gemspec +36 -0
  10. data/atatus.yml +2 -0
  11. data/bench/.gitignore +2 -0
  12. data/bench/app.rb +53 -0
  13. data/bench/benchmark.rb +36 -0
  14. data/bench/report.rb +55 -0
  15. data/bench/rubyprof.rb +39 -0
  16. data/bench/stackprof.rb +23 -0
  17. data/bin/build_docs +5 -0
  18. data/bin/console +15 -0
  19. data/bin/setup +8 -0
  20. data/bin/with_framework +7 -0
  21. data/lib/atatus.rb +325 -0
  22. data/lib/atatus/agent.rb +260 -0
  23. data/lib/atatus/central_config.rb +141 -0
  24. data/lib/atatus/central_config/cache_control.rb +34 -0
  25. data/lib/atatus/collector/base.rb +329 -0
  26. data/lib/atatus/collector/builder.rb +317 -0
  27. data/lib/atatus/collector/transport.rb +72 -0
  28. data/lib/atatus/config.rb +248 -0
  29. data/lib/atatus/config/bytes.rb +25 -0
  30. data/lib/atatus/config/duration.rb +23 -0
  31. data/lib/atatus/config/options.rb +134 -0
  32. data/lib/atatus/config/regexp_list.rb +13 -0
  33. data/lib/atatus/context.rb +33 -0
  34. data/lib/atatus/context/request.rb +11 -0
  35. data/lib/atatus/context/request/socket.rb +19 -0
  36. data/lib/atatus/context/request/url.rb +42 -0
  37. data/lib/atatus/context/response.rb +22 -0
  38. data/lib/atatus/context/user.rb +42 -0
  39. data/lib/atatus/context_builder.rb +97 -0
  40. data/lib/atatus/deprecations.rb +22 -0
  41. data/lib/atatus/error.rb +22 -0
  42. data/lib/atatus/error/exception.rb +46 -0
  43. data/lib/atatus/error/log.rb +24 -0
  44. data/lib/atatus/error_builder.rb +76 -0
  45. data/lib/atatus/instrumenter.rb +224 -0
  46. data/lib/atatus/internal_error.rb +6 -0
  47. data/lib/atatus/logging.rb +55 -0
  48. data/lib/atatus/metadata.rb +19 -0
  49. data/lib/atatus/metadata/process_info.rb +18 -0
  50. data/lib/atatus/metadata/service_info.rb +61 -0
  51. data/lib/atatus/metadata/system_info.rb +35 -0
  52. data/lib/atatus/metadata/system_info/container_info.rb +121 -0
  53. data/lib/atatus/metadata/system_info/hw_info.rb +118 -0
  54. data/lib/atatus/metadata/system_info/os_info.rb +31 -0
  55. data/lib/atatus/metrics.rb +98 -0
  56. data/lib/atatus/metrics/cpu_mem.rb +240 -0
  57. data/lib/atatus/metrics/vm.rb +60 -0
  58. data/lib/atatus/metricset.rb +19 -0
  59. data/lib/atatus/middleware.rb +76 -0
  60. data/lib/atatus/naively_hashable.rb +21 -0
  61. data/lib/atatus/normalizers.rb +68 -0
  62. data/lib/atatus/normalizers/action_controller.rb +27 -0
  63. data/lib/atatus/normalizers/action_mailer.rb +26 -0
  64. data/lib/atatus/normalizers/action_view.rb +77 -0
  65. data/lib/atatus/normalizers/active_record.rb +45 -0
  66. data/lib/atatus/opentracing.rb +346 -0
  67. data/lib/atatus/rails.rb +61 -0
  68. data/lib/atatus/railtie.rb +30 -0
  69. data/lib/atatus/span.rb +125 -0
  70. data/lib/atatus/span/context.rb +40 -0
  71. data/lib/atatus/span_helpers.rb +44 -0
  72. data/lib/atatus/spies.rb +86 -0
  73. data/lib/atatus/spies/action_dispatch.rb +28 -0
  74. data/lib/atatus/spies/delayed_job.rb +68 -0
  75. data/lib/atatus/spies/elasticsearch.rb +36 -0
  76. data/lib/atatus/spies/faraday.rb +70 -0
  77. data/lib/atatus/spies/http.rb +44 -0
  78. data/lib/atatus/spies/json.rb +22 -0
  79. data/lib/atatus/spies/mongo.rb +87 -0
  80. data/lib/atatus/spies/net_http.rb +70 -0
  81. data/lib/atatus/spies/rake.rb +45 -0
  82. data/lib/atatus/spies/redis.rb +27 -0
  83. data/lib/atatus/spies/sequel.rb +47 -0
  84. data/lib/atatus/spies/sidekiq.rb +89 -0
  85. data/lib/atatus/spies/sinatra.rb +41 -0
  86. data/lib/atatus/spies/tilt.rb +27 -0
  87. data/lib/atatus/sql_summarizer.rb +35 -0
  88. data/lib/atatus/stacktrace.rb +16 -0
  89. data/lib/atatus/stacktrace/frame.rb +52 -0
  90. data/lib/atatus/stacktrace_builder.rb +104 -0
  91. data/lib/atatus/subscriber.rb +77 -0
  92. data/lib/atatus/trace_context.rb +85 -0
  93. data/lib/atatus/transaction.rb +100 -0
  94. data/lib/atatus/transport/base.rb +174 -0
  95. data/lib/atatus/transport/connection.rb +156 -0
  96. data/lib/atatus/transport/connection/http.rb +116 -0
  97. data/lib/atatus/transport/connection/proxy_pipe.rb +75 -0
  98. data/lib/atatus/transport/filters.rb +43 -0
  99. data/lib/atatus/transport/filters/secrets_filter.rb +74 -0
  100. data/lib/atatus/transport/serializers.rb +93 -0
  101. data/lib/atatus/transport/serializers/context_serializer.rb +85 -0
  102. data/lib/atatus/transport/serializers/error_serializer.rb +77 -0
  103. data/lib/atatus/transport/serializers/metadata_serializer.rb +70 -0
  104. data/lib/atatus/transport/serializers/metricset_serializer.rb +28 -0
  105. data/lib/atatus/transport/serializers/span_serializer.rb +80 -0
  106. data/lib/atatus/transport/serializers/transaction_serializer.rb +37 -0
  107. data/lib/atatus/transport/worker.rb +73 -0
  108. data/lib/atatus/util.rb +42 -0
  109. data/lib/atatus/util/inflector.rb +93 -0
  110. data/lib/atatus/util/lru_cache.rb +48 -0
  111. data/lib/atatus/util/prefixed_logger.rb +18 -0
  112. data/lib/atatus/util/throttle.rb +35 -0
  113. data/lib/atatus/version.rb +5 -0
  114. data/vendor/.gitkeep +0 -0
  115. metadata +190 -0
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ $LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
5
+ require 'active_record'
6
+ require 'action_controller/railtie'
7
+ require 'atatus'
8
+ require 'atatus/railtie'
9
+
10
+ $log = Logger.new('/tmp/bench.log')
11
+
12
+ ActiveRecord::Base.logger = $log
13
+
14
+ ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: '/tmp/bench.sqlite3')
15
+
16
+ ActiveRecord::Schema.define do
17
+ create_table :posts, force: true do |t|
18
+ t.string :title
19
+ t.timestamps
20
+ end
21
+ end
22
+
23
+ class Post < ActiveRecord::Base
24
+ end
25
+
26
+ 10.times { |i| Post.create! title: "Post #{i}" }
27
+
28
+ class ApplicationController < ActionController::Base
29
+ def index
30
+ render inline: '<%= Post.pluck(:title).join(", ") %>'
31
+ end
32
+
33
+ def favicon
34
+ render nothing: true
35
+ end
36
+ end
37
+
38
+ class App < Rails::Application
39
+ config.secret_key_base = '__secret'
40
+ config.logger = $log
41
+ config.eager_load = false
42
+
43
+ config.atatus.disable_send = true
44
+ config.atatus.logger = $log
45
+ config.atatus.log_level = Logger::DEBUG
46
+ end
47
+
48
+ App.initialize!
49
+
50
+ App.routes.draw do
51
+ get '/favicon.ico', to: 'application#favicon'
52
+ root to: 'application#index'
53
+ end
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ ENV['RAILS_ENV'] = 'production'
5
+
6
+ require 'bundler'
7
+ require 'bundler/setup'
8
+
9
+ require 'benchmark'
10
+ include Benchmark
11
+ require 'rack/test'
12
+
13
+ require './bench/app'
14
+
15
+ def app
16
+ App
17
+ end
18
+
19
+ include Rack::Test::Methods
20
+
21
+ def perform
22
+ 10_000.times do
23
+ get '/'
24
+ end
25
+ end
26
+
27
+ bench = Benchmark.benchmark(CAPTION, 15, FORMAT) do |b|
28
+ perform # warm up
29
+
30
+ b.report('with agent:') { perform }
31
+
32
+ Atatus.stop
33
+ perform # warm up
34
+
35
+ b.report('without agent:') { perform }
36
+ end
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'time'
5
+ require 'json'
6
+
7
+ input = STDIN.read.split("\n")
8
+
9
+ git_sha, git_msg = `git log -n 1 --pretty="format:%H|||%s"`.split('|||')
10
+ git_date = `git log -n 1 --pretty="format:%ai"`
11
+ platform = Gem::Platform.local
12
+
13
+ def doc(payload)
14
+ puts({ index: { _index: "benchmark-ruby", _type: "_doc" } }.to_json)
15
+ puts(payload.to_json)
16
+ end
17
+
18
+ meta = {
19
+ executed_at: Time.new.iso8601,
20
+ 'git.commit' => git_sha,
21
+ 'git.date' => String(git_date).strip != '' && Time.parse(git_date).iso8601,
22
+ 'git.subject' => git_msg,
23
+ hostname: `hostname`.chomp,
24
+ engine: RUBY_ENGINE,
25
+ arch: platform.cpu,
26
+ os: platform.os,
27
+ ruby_version: "#{RUBY_ENGINE == 'jruby' ? 'j' : ''}#{RUBY_VERSION}"
28
+ }
29
+
30
+ results =
31
+ input
32
+ .grep(/^with/)
33
+ .map do |line|
34
+ title = line.match(/^(.*):/) { |m| m[1] }
35
+ user, system, total, real = line.scan(/[0-9\.]+/).map(&:to_f)
36
+ meta.merge(
37
+ title: title,
38
+ user: user,
39
+ system: system,
40
+ total: total,
41
+ real: real,
42
+ )
43
+ end
44
+
45
+ results.each { |result| doc result }
46
+
47
+ overhead =
48
+ (results[0][:total] - results[1][:total]) *
49
+ 1000 / # milliseconds
50
+ 10_000 # transactions
51
+
52
+ doc meta.merge(
53
+ title: 'overhead',
54
+ overhead: overhead
55
+ )
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ruby-prof'
4
+ require 'rack/test'
5
+
6
+ $LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
7
+ require 'atatus'
8
+
9
+ require './bench/app'
10
+ include App::Helpers
11
+
12
+ def perform(app, count: 1000)
13
+ app.start
14
+
15
+ transactions = count.times.map do |i|
16
+ Atatus.transaction "Transaction##{i}",
17
+ context: Atatus.build_context(app.mock_env) do
18
+ Atatus.span('Number one') { 'ok 1' }
19
+ Atatus.span('Number two') { 'ok 2' }
20
+ Atatus.span('Number three') { 'ok 3' }
21
+ end
22
+ end
23
+
24
+ app.stop
25
+ end
26
+
27
+ def do_bench(transaction_count: 1000, **config)
28
+ with_app(config) do |app|
29
+ profile = RubyProf::Profile.new
30
+ profile.exclude_common_methods!
31
+ profile.start
32
+ perform(app, count: transaction_count)
33
+ profile.stop
34
+ printer = RubyProf::GraphHtmlPrinter.new(profile)
35
+ printer.print(File.open('bench/tmp/out.html', 'w'))
36
+ end
37
+ end
38
+
39
+ do_bench(transaction_count: 10_000)
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'stackprof'
5
+ require 'rack/test'
6
+
7
+ require './bench/app'
8
+
9
+ def app
10
+ App
11
+ end
12
+
13
+ include Rack::Test::Methods
14
+
15
+ puts 'Running '
16
+ profile = StackProf.run(mode: :cpu) do
17
+ 10_000.times do
18
+ get '/'
19
+ end
20
+ end
21
+ puts ''
22
+
23
+ StackProf::Report.new(profile).print_text
@@ -0,0 +1,5 @@
1
+ #!/bin/bash
2
+ set -ex
3
+
4
+ # Requires atatus/docs to be checked out at ../docs
5
+ ../docs/build_docs.pl --doc docs/index.asciidoc --chunk 1
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'atatus'
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require "pry"
12
+ # Pry.start
13
+
14
+ require 'irb'
15
+ IRB.start(__FILE__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,7 @@
1
+ #!/bin/bash -x
2
+ set -e
3
+
4
+ framework=$1
5
+
6
+ FRAMEWORK=$framework bundle update
7
+ FRAMEWORK=$framework ${@:2}
@@ -0,0 +1,325 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'atatus/version'
4
+ require 'atatus/internal_error'
5
+ require 'atatus/logging'
6
+
7
+ # Core
8
+ require 'atatus/agent'
9
+ require 'atatus/config'
10
+ require 'atatus/context'
11
+ require 'atatus/instrumenter'
12
+ require 'atatus/util'
13
+
14
+ require 'atatus/middleware'
15
+
16
+ require 'atatus/railtie' if defined?(::Rails::Railtie)
17
+
18
+ # Atatus
19
+ module Atatus # rubocop:disable Metrics/ModuleLength
20
+ class << self
21
+ ### Life cycle
22
+
23
+ # Starts the Atatus Agent
24
+ #
25
+ # @param config [Config] An instance of Config
26
+ # @return [Agent] The resulting [Agent]
27
+ def start(config = {})
28
+ Agent.start config
29
+ end
30
+
31
+ # Stops the Atatus Agent
32
+ def stop
33
+ Agent.stop
34
+ end
35
+
36
+ # @return [Boolean] Whether there's an [Agent] running
37
+ def running?
38
+ Agent.running?
39
+ end
40
+
41
+ # @return [Agent] Currently running [Agent] if any
42
+ def agent
43
+ Agent.instance
44
+ end
45
+
46
+ ### Metrics
47
+
48
+ # Returns the currently active transaction (if any)
49
+ #
50
+ # @return [Transaction] or `nil`
51
+ def current_transaction
52
+ agent&.current_transaction
53
+ end
54
+
55
+ # Returns the currently active span (if any)
56
+ #
57
+ # @return [Span] or `nil`
58
+ def current_span
59
+ agent&.current_span
60
+ end
61
+
62
+ # rubocop:disable Metrics/AbcSize
63
+ # Get a formatted string containing transaction, span, and trace ids.
64
+ # If a block is provided, the ids are yielded.
65
+ #
66
+ # @yield [String|nil, String|nil, String|nil] The transaction, span,
67
+ # and trace ids.
68
+ # @return [String] Unless block given
69
+ def log_ids
70
+ trace_id = (current_transaction || current_span)&.trace_id
71
+ if block_given?
72
+ return yield(current_transaction&.id, current_span&.id, trace_id)
73
+ end
74
+
75
+ ids = []
76
+ ids << "transaction.id=#{current_transaction.id}" if current_transaction
77
+ ids << "span.id=#{current_span.id}" if current_span
78
+ ids << "trace.id=#{trace_id}" if trace_id
79
+ ids.join(' ')
80
+ end
81
+ # rubocop:enable Metrics/AbcSize
82
+
83
+ # rubocop:disable Metrics/MethodLength
84
+ # Start a new transaction
85
+ #
86
+ # @param name [String] A description of the transaction, eg
87
+ # `ExamplesController#index`
88
+ # @param type [String] The kind of the transaction, eg `app.request.get` or
89
+ # `db.mysql2.query`
90
+ # @param context [Context] An optional [Context]
91
+ # @return [Transaction]
92
+ def start_transaction(
93
+ name = nil,
94
+ type = nil,
95
+ context: nil,
96
+ trace_context: nil
97
+ )
98
+ agent&.start_transaction(
99
+ name,
100
+ type,
101
+ context: context,
102
+ trace_context: trace_context
103
+ )
104
+ end
105
+ # rubocop:enable Metrics/MethodLength
106
+
107
+ # Ends the current transaction with `result`
108
+ #
109
+ # @param result [String] The result of the transaction
110
+ # @return [Transaction]
111
+ def end_transaction(result = nil)
112
+ agent&.end_transaction(result)
113
+ end
114
+
115
+ # rubocop:disable Metrics/MethodLength
116
+ # Wrap a block in a Transaction, ending it after the block
117
+ #
118
+ # @param name [String] A description of the transaction, eg
119
+ # `ExamplesController#index`
120
+ # @param type [String] The kind of the transaction, eg `app.request.get` or
121
+ # `db.mysql2.query`
122
+ # @param context [Context] An optional [Context]
123
+ # @yield [Transaction]
124
+ # @return result of block
125
+ def with_transaction(
126
+ name = nil,
127
+ type = nil,
128
+ context: nil,
129
+ trace_context: nil
130
+ )
131
+ unless block_given?
132
+ raise ArgumentError,
133
+ 'expected a block. Do you want `start_transaction\' instead?'
134
+ end
135
+
136
+ return yield(nil) unless agent
137
+
138
+ begin
139
+ transaction =
140
+ start_transaction(
141
+ name,
142
+ type,
143
+ context: context,
144
+ trace_context: trace_context
145
+ )
146
+ yield transaction
147
+ ensure
148
+ end_transaction
149
+ end
150
+ end
151
+ # rubocop:enable Metrics/MethodLength
152
+
153
+ # Start a new span
154
+ #
155
+ # @param name [String] A description of the span, eq `SELECT FROM "users"`
156
+ # @param type [String] The span type, eq `db`
157
+ # @param subtype [String] The span subtype, eq `postgresql`
158
+ # @param action [String] The span action type, eq `connect` or `query`
159
+ # @param context [Span::Context] Context information about the span
160
+ # @param include_stacktrace [Boolean] Whether or not to capture a stacktrace
161
+ # @return [Span]
162
+ def start_span(
163
+ name,
164
+ type = nil,
165
+ subtype: nil,
166
+ action: nil,
167
+ context: nil,
168
+ include_stacktrace: true,
169
+ trace_context: nil
170
+ )
171
+ agent&.start_span(
172
+ name,
173
+ type,
174
+ subtype: subtype,
175
+ action: action,
176
+ context: context,
177
+ trace_context: trace_context
178
+ ).tap do |span|
179
+ break unless span && include_stacktrace
180
+ break unless agent.config.span_frames_min_duration?
181
+
182
+ span.original_backtrace ||= caller
183
+ end
184
+ end
185
+ # rubocop:enable Metrics/MethodLength, Metrics/ParameterLists
186
+
187
+ # Ends the current span
188
+ #
189
+ # @return [Span]
190
+ def end_span
191
+ agent&.end_span
192
+ end
193
+
194
+ # rubocop:disable Metrics/MethodLength, Metrics/ParameterLists
195
+ # Wrap a block in a Span, ending it after the block
196
+ #
197
+ # @param name [String] A description of the span, eq `SELECT FROM "users"`
198
+ # @param type [String] The kind of span, eq `db.mysql2.query`
199
+ # @param context [Span::Context] Context information about the span
200
+ # @param include_stacktrace [Boolean] Whether or not to capture a stacktrace
201
+ # @yield [Span]
202
+ # @return Result of block
203
+ def with_span(
204
+ name,
205
+ type = nil,
206
+ subtype: nil,
207
+ action: nil,
208
+ context: nil,
209
+ include_stacktrace: true,
210
+ trace_context: nil
211
+ )
212
+ unless block_given?
213
+ raise ArgumentError,
214
+ 'expected a block. Do you want `start_span\' instead?'
215
+ end
216
+
217
+ return yield nil unless agent
218
+
219
+ begin
220
+ span =
221
+ start_span(
222
+ name,
223
+ type,
224
+ subtype: subtype,
225
+ action: action,
226
+ context: context,
227
+ include_stacktrace: include_stacktrace,
228
+ trace_context: trace_context
229
+ )
230
+ yield span
231
+ ensure
232
+ end_span
233
+ end
234
+ end
235
+ # rubocop:enable Metrics/MethodLength, Metrics/ParameterLists
236
+
237
+ # Build a [Context] from a Rack `env`. The context may include information
238
+ # about the request, response, current user and more
239
+ #
240
+ # @param rack_env [Rack::Env] A Rack env
241
+ # @return [Context] The built context
242
+ def build_context(
243
+ rack_env: nil,
244
+ for_type: :transaction
245
+ )
246
+ agent&.build_context(rack_env: rack_env, for_type: for_type)
247
+ end
248
+
249
+ ### Errors
250
+
251
+ # Report and exception to APM
252
+ #
253
+ # @param exception [Exception] The exception
254
+ # @param context [Context] An optional [Context]
255
+ # @param handled [Boolean] Whether the exception was rescued
256
+ # @return [String] ID of the generated [Error]
257
+ def report(exception, context: nil, handled: true)
258
+ agent&.report(exception, context: context, handled: handled)
259
+ end
260
+
261
+ # Report a custom string error message to APM
262
+ #
263
+ # @param message [String] The message
264
+ # @param context [Context] An optional [Context]
265
+ # @return [String] ID of the generated [Error]
266
+ def report_message(message, context: nil, **attrs)
267
+ agent&.report_message(
268
+ message,
269
+ context: context,
270
+ backtrace: caller,
271
+ **attrs
272
+ )
273
+ end
274
+
275
+ ### Context
276
+
277
+ # Set a _label_ value for the current transaction
278
+ #
279
+ # @param key [String,Symbol] A key
280
+ # @param value [Object] A value
281
+ # @return [Object] The given value
282
+ def set_label(key, value)
283
+ case value
284
+ when TrueClass,
285
+ FalseClass,
286
+ Numeric,
287
+ NilClass,
288
+ String
289
+ agent&.set_label(key, value)
290
+ else
291
+ agent&.set_label(key, value.to_s)
292
+ end
293
+ end
294
+
295
+ # Provide further context for the current transaction
296
+ #
297
+ # @param custom [Hash] A hash with custom information. Can be nested.
298
+ # @return [Hash] The current custom context
299
+ def set_custom_context(custom)
300
+ agent&.set_custom_context(custom)
301
+ end
302
+
303
+ # Provide a user to the current transaction
304
+ #
305
+ # @param user [Object] An object representing a user
306
+ # @return [Object] Given user
307
+ def set_user(user)
308
+ agent&.set_user(user)
309
+ end
310
+
311
+ # Provide a filter to transform payloads before sending them off
312
+ #
313
+ # @param key [Symbol] Unique filter key
314
+ # @param callback [Object, Proc] A filter that responds to #call(payload)
315
+ # @yield [Hash] A filter. Used if provided. Otherwise using `callback`
316
+ # @return [Bool] true
317
+ def add_filter(key, callback = nil, &block)
318
+ if callback.nil? && !block_given?
319
+ raise ArgumentError, '#add_filter needs either `callback\' or a block'
320
+ end
321
+
322
+ agent&.add_filter(key, block || callback)
323
+ end
324
+ end
325
+ end