logjam_agent 0.30.0 → 0.32.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,19 @@
1
+ # see https://github.com/chuckremes/ffi-rzmq/pull/133
2
+
3
+ require 'ffi-rzmq'
4
+ require 'ffi-rzmq/message'
5
+
6
+ module ZMQ
7
+ class Message
8
+ def copy_in_bytes bytes, len
9
+ data_buffer = LibC.malloc len
10
+ # writes the exact number of bytes, no null byte to terminate string
11
+ data_buffer.put_bytes 0, bytes, 0, len
12
+
13
+ # use libC to call free on the data buffer; earlier versions used an
14
+ # FFI::Function here that called back into Ruby, but Rubinius won't
15
+ # support that and there are issues with the other runtimes too
16
+ LibZMQ.zmq_msg_init_data @pointer, data_buffer, len, LibC::Free, nil
17
+ end
18
+ end
19
+ end
@@ -7,14 +7,19 @@ module LogjamAgent
7
7
  class Logger < ActiveSupport::LogSubscriber
8
8
  def initialize(app, taggers = nil)
9
9
  @app = app
10
- @taggers = taggers || Rails.application.config.log_tags || []
10
+ @taggers = taggers || (defined?(Rails::Railtie) ? Rails.application.config.log_tags : []) || []
11
11
  @hostname = LogjamAgent.hostname
12
12
  @asset_prefix = Rails.application.config.assets.prefix rescue "---"
13
13
  @ignore_asset_requests = LogjamAgent.ignore_asset_requests
14
14
  end
15
15
 
16
16
  def call(env)
17
- request = ActionDispatch::Request.new(env)
17
+ if env["logjam_agent.framework"] == :sinatra
18
+ request = ::Sinatra::Request.new(env)
19
+ env["rack.logger"] = logger
20
+ else
21
+ request = ActionDispatch::Request.new(env)
22
+ end
18
23
 
19
24
  if logger.respond_to?(:tagged) && !@taggers.empty?
20
25
  logger.tagged(compute_tags(request)) { call_app(request, env) }
@@ -88,7 +93,7 @@ module LogjamAgent
88
93
  spoofed = nil
89
94
  ip = nil
90
95
  begin
91
- ip = LogjamAgent.ip_obfuscator(env["action_dispatch.remote_ip"].to_s)
96
+ ip = LogjamAgent.ip_obfuscator((env["action_dispatch.remote_ip"] || request.ip).to_s)
92
97
  rescue ActionDispatch::RemoteIp::IpSpoofAttackError => spoofed
93
98
  ip = "*** SPOOFED IP ***"
94
99
  end
@@ -107,6 +112,11 @@ module LogjamAgent
107
112
  if completed_info = Thread.current.thread_variable_get(:time_bandits_completed_info)
108
113
  _, additions, view_time, _ = completed_info
109
114
  end
115
+ additions ||= []
116
+ if env["logjam_agent.framework"] == :sinatra
117
+ TimeBandits.consumed
118
+ additions.concat TimeBandits.runtimes
119
+ end
110
120
  logjam_request = LogjamAgent.request
111
121
 
112
122
  if (allowed_time_ms = env['HTTP_X_LOGJAM_CALLER_TIMEOUT'].to_i) > 0 && (run_time_ms > allowed_time_ms)
@@ -123,9 +133,9 @@ module LogjamAgent
123
133
  info message unless logjam_request.ignored?
124
134
 
125
135
  ActiveSupport::LogSubscriber.flush_all!
126
- request_info = {
127
- :total_time => run_time_ms, :code => status, :view_time => view_time || 0.0, :wait_time => wait_time_ms
128
- }
136
+ request_info = { :total_time => run_time_ms, :code => status }
137
+ request_info[:view_time] = view_time if view_time
138
+ request_info[:wait_time] = wait_time_ms if wait_time_ms > 0
129
139
  logjam_request.fields.merge!(request_info)
130
140
 
131
141
  env["time_bandits.metrics"] = TimeBandits.metrics
@@ -153,7 +163,7 @@ module LogjamAgent
153
163
 
154
164
  result
155
165
  rescue Exception => e
156
- Rails.logger.error(e)
166
+ logger.error(e)
157
167
  result
158
168
  end
159
169
 
@@ -197,82 +207,10 @@ module LogjamAgent
197
207
 
198
208
  headers
199
209
  end
200
-
201
210
  end
202
211
  end
203
212
  end
204
213
 
205
- # patch the actioncontroller logsubscriber to set the action on the logjam logger as soon as it starts processing the request
206
- require 'action_controller/metal/instrumentation'
207
- require 'action_controller/log_subscriber'
208
-
209
- module ActionController #:nodoc:
210
-
211
- class LogSubscriber
212
- if Rails::VERSION::STRING =~ /\A3\.0/
213
- def start_processing(event)
214
- payload = event.payload
215
- params = payload[:params].except(*INTERNAL_PARAMS)
216
-
217
- controller = payload[:controller]
218
- action = payload[:action]
219
- full_name = "#{controller}##{action}"
220
- action_name = LogjamAgent.action_name_proc.call(full_name)
221
-
222
- LogjamAgent.request.fields[:action] = action_name
223
-
224
- info " Processing by #{full_name} as #{payload[:formats].first.to_s.upcase}"
225
- info " Parameters: #{params.inspect}" unless params.empty?
226
- end
227
-
228
- elsif Rails::VERSION::STRING =~ /\A3\.1/
229
-
230
- def start_processing(event)
231
- payload = event.payload
232
- params = payload[:params].except(*INTERNAL_PARAMS)
233
- format = payload[:format]
234
- format = format.to_s.upcase if format.is_a?(Symbol)
235
-
236
- controller = payload[:controller]
237
- action = payload[:action]
238
- full_name = "#{controller}##{action}"
239
- action_name = LogjamAgent.action_name_proc.call(full_name)
240
-
241
- LogjamAgent.request.fields[:action] = action_name
242
-
243
- info " Processing by #{full_name} as #{format}"
244
- info " Parameters: #{params.inspect}" unless params.empty?
245
- end
246
-
247
- elsif Rails::VERSION::STRING =~ /\A(3\.2|4|5|6)/
248
-
249
- # Rails 4.1 uses method_added to automatically subscribe newly
250
- # added methods. Since start_processing is already defined, the
251
- # net effect is that start_processing gets called
252
- # twice. Therefore, we temporarily switch to protected mode and
253
- # change it back later to public.
254
- protected
255
- def start_processing(event)
256
- payload = event.payload
257
- params = payload[:params].except(*INTERNAL_PARAMS)
258
- format = payload[:format]
259
- format = format.to_s.upcase if format.is_a?(Symbol)
260
-
261
- controller = payload[:controller]
262
- action = payload[:action]
263
- full_name = "#{controller}##{action}"
264
- action_name = LogjamAgent.action_name_proc.call(full_name)
265
-
266
- LogjamAgent.request.fields[:action] = action_name
267
-
268
- info "Processing by #{full_name} as #{format}"
269
- info " Parameters: #{params.inspect}" unless params.empty?
270
- end
271
- public :start_processing
272
-
273
- else
274
- raise "logjam_agent ActionController monkey patch is not compatible with your Rails version"
275
- end
276
- end
277
-
214
+ if defined?(Rails::Railtie)
215
+ require_relative "rails_support"
278
216
  end
@@ -0,0 +1,26 @@
1
+ # patch the actioncontroller logsubscriber to set the action on the logjam logger as soon as it starts processing the request
2
+ require 'action_controller/metal/instrumentation'
3
+ require 'action_controller/log_subscriber'
4
+
5
+ module ActionController #:nodoc:
6
+
7
+ class LogSubscriber
8
+ def start_processing(event)
9
+ payload = event.payload
10
+ params = payload[:params].except(*INTERNAL_PARAMS)
11
+ format = payload[:format]
12
+ format = format.to_s.upcase if format.is_a?(Symbol)
13
+
14
+ controller = payload[:controller]
15
+ action = payload[:action]
16
+ full_name = "#{controller}##{action}"
17
+ action_name = LogjamAgent.action_name_proc.call(full_name)
18
+
19
+ LogjamAgent.request.fields[:action] = action_name
20
+
21
+ info "Processing by #{full_name} as #{format}"
22
+ info " Parameters: #{params.inspect}" unless params.empty?
23
+ end
24
+ end
25
+
26
+ end
@@ -0,0 +1,32 @@
1
+ require 'sinatra/base'
2
+ begin
3
+ require 'active_support/parameter_filter'
4
+ rescue LoadError
5
+ require_relative '../active_support/parameter_filter'
6
+ end
7
+
8
+ # Extend the Sinatra Request class with some methods to make it look more like an
9
+ # ActionDispatch request.
10
+
11
+ class Sinatra::Request
12
+ alias_method :method, :request_method
13
+ def query_parameters; self.GET; end
14
+ def request_parameters; self.POST; end
15
+
16
+ def parameter_filter
17
+ ActiveSupport::ParameterFilter.new(LogjamAgent.parameter_filters)
18
+ end
19
+
20
+ KV_RE = '[^&;=]+'
21
+ PAIR_RE = %r{(#{KV_RE})=(#{KV_RE})}
22
+
23
+ def filtered_path
24
+ return path if query_string.empty?
25
+ filter = parameter_filter
26
+ filtered_query_string = query_string.gsub(PAIR_RE) do |_|
27
+ filter.filter($1 => $2).first.join("=")
28
+ end
29
+ "#{path}?#{filtered_query_string}"
30
+ end
31
+
32
+ end
@@ -80,6 +80,9 @@ module LogjamAgent
80
80
  # disable logjam request forwarding by default in test environment
81
81
  LogjamAgent.disable! if Rails.env.test?
82
82
 
83
+ # only sent pings in production like environments
84
+ LogjamAgent.ensure_ping_at_exit = !%w(test development).include?(Rails.env.to_s)
85
+
83
86
  # patch controller testing to create a logjam request, because middlewares aren't executed
84
87
  if Rails.env.test?
85
88
  ActiveSupport.on_load(:action_controller) do
@@ -0,0 +1,22 @@
1
+ module LogjamAgent
2
+ class Receiver
3
+ def initialize
4
+ @socket = ZMQForwarder.context.socket(ZMQ::ROUTER)
5
+ @socket.setsockopt(ZMQ::RCVTIMEO, 100)
6
+ if @socket.bind("inproc://app") < 0
7
+ raise "ZMQ error on binding: #{ZMQ::Util.error_string}"
8
+ end
9
+ at_exit { @socket.close }
10
+ end
11
+
12
+ def receive
13
+ answer_parts = []
14
+ if @socket.recv_strings(answer_parts) < 0
15
+ raise "ZMQ error on receiving: #{ZMQ::Util.error_string}"
16
+ end
17
+ answer_parts.shift
18
+ answer_parts[2] = JSON.parse(answer_parts[2])
19
+ answer_parts
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,116 @@
1
+ require 'sinatra'
2
+ require 'logger'
3
+ require 'logjam_agent'
4
+ require 'logjam_agent/middleware'
5
+ require 'logjam_agent/rack/sinatra_request'
6
+ require 'logjam_agent/rack/logger'
7
+ require 'time_bandits'
8
+
9
+ module LogjamAgent
10
+ module Sinatra
11
+ class Middleware
12
+ def initialize(app)
13
+ app_with_logging = LogjamAgent::Rack::Logger.new(app)
14
+ @app = LogjamAgent::Middleware.new(app_with_logging, :sinatra)
15
+ end
16
+ def call(env)
17
+ @app.call(env)
18
+ end
19
+ end
20
+
21
+ module Helpers
22
+ def action_name(action_name)
23
+ LogjamAgent.request.fields[:action] = action_name
24
+ end
25
+
26
+ def logger
27
+ LogjamAgent.logger
28
+ end
29
+ end
30
+
31
+ def setup_logjam_logger
32
+ log_path = ENV["APP_LOG_TO_STDOUT"].present? ? STDOUT : "#{settings.root}/log/#{LogjamAgent.environment_name}.log"
33
+ logger = LogjamAgent::BufferedLogger.new(log_path) rescue LogjamAgent::BufferedLogger.new(STDERR)
34
+
35
+ loglevel = settings.respond_to?(:loglevel) ? settings.loglevel : :info
36
+ logger.level = ::Logger.const_get(loglevel.to_s.upcase)
37
+
38
+ LogjamAgent.log_device_log_level = logger.level
39
+ LogjamAgent.log_device_log_level = ::Logger::ERROR unless %i[test development].include?(settings.environment.to_sym)
40
+
41
+ logger.formatter = LogjamAgent::SyslogLikeFormatter.new
42
+ logger = ActiveSupport::TaggedLogging.new(logger)
43
+ LogjamAgent.logger = logger
44
+ ActiveSupport::LogSubscriber.logger = logger
45
+
46
+ log_path = ENV["APP_LOG_TO_STDOUT"].present? ? STDOUT : "#{settings.root}/log/logjam_agent_error.log"
47
+ forwarding_error_logger = ::Logger.new(log_path) rescue ::Logger.new(STDERR)
48
+ forwarding_error_logger.level = ::Logger::ERROR
49
+ forwarding_error_logger.formatter = ::Logger::Formatter.new
50
+ LogjamAgent.forwarding_error_logger = forwarding_error_logger
51
+
52
+ truncate_overlong_params = lambda { |key, value|
53
+ max_size = LogjamAgent.max_logged_size_for(key)
54
+ if value.is_a?(String) && value.size > max_size
55
+ value[max_size..-1] = " ... [TRUNCATED]"
56
+ end
57
+ }
58
+ LogjamAgent.parameter_filters << truncate_overlong_params
59
+ end
60
+
61
+ def self.registered(app)
62
+ app.helpers Helpers
63
+ LogjamAgent.environment_name = ENV['LOGJAM_ENV'] || app.settings.environment.to_s
64
+ LogjamAgent.auto_detect_logged_exceptions
65
+ LogjamAgent.disable! if app.settings.environment.to_sym == :test
66
+ end
67
+ end
68
+ end
69
+
70
+ # For classic apps.
71
+ Sinatra.register LogjamAgent::Sinatra
72
+
73
+ # We already supply a logger.
74
+ Sinatra::Base.class_eval do
75
+ class << self
76
+ def setup_logging(builder); end
77
+ end
78
+ end
79
+
80
+ # Patch Sinatra's render logic to compute corrected view times.
81
+ module LogjamAgent
82
+ module ComputeRenderTimes
83
+ def render(engine, data, options = {}, locals = {}, &block)
84
+ consumed_before_rendering = TimeBandits.consumed
85
+ result = exception = nil
86
+ duration = Benchmark.ms do
87
+ begin
88
+ result = super
89
+ rescue => exception
90
+ end
91
+ end
92
+ consumed_during_rendering = TimeBandits.consumed - consumed_before_rendering
93
+ duration -= consumed_during_rendering
94
+ raise exception if exception
95
+ result
96
+ ensure
97
+ Thread.current.thread_variable_set(
98
+ :time_bandits_completed_info,
99
+ [ duration, ["Views: %.3fms" % duration.to_f], duration, "" ]
100
+ )
101
+ end
102
+ end
103
+ end
104
+
105
+ Sinatra::Base.prepend LogjamAgent::ComputeRenderTimes
106
+
107
+ # Define exception, but don't do anything about it. Sneaky!
108
+ module ActionDispatch
109
+ module RemoteIp
110
+ class IpSpoofAttackError < StandardError; end
111
+ end
112
+ end
113
+
114
+ # Add GC time bandit
115
+ TimeBandits.reset
116
+ TimeBandits.add TimeBandits::TimeConsumers::GarbageCollection.instance if GC.respond_to? :enable_stats
@@ -41,7 +41,7 @@ module LogjamAgent
41
41
  "#{format_severity(severity)} #{format_time(timestamp)}#{render_attributes}#{format_host_info(progname)}: #{format_message(msg)}#{@newline}"
42
42
  end
43
43
 
44
- if !defined?(Rails) || Rails.env.development?
44
+ if !defined?(Rails::Railtie) || Rails.env.development?
45
45
  def format_host_info(progname); ""; end
46
46
  else
47
47
  def format_host_info(progname)
@@ -62,7 +62,12 @@ module LogjamAgent
62
62
  protocol, host, port = %r{\A(?:([^:]+)://)?([^:]+)(?::(\d+))?\z}.match(spec).captures
63
63
  protocol ||= "tcp"
64
64
  port ||= default_port
65
- "#{protocol}://#{host}:#{port}"
65
+ if protocol == "inproc"
66
+ # should only be used for integration tests
67
+ "#{protocol}://#{host}"
68
+ else
69
+ "#{protocol}://#{host}:#{port}"
70
+ end
66
71
  end
67
72
  end
68
73
  end
@@ -1,3 +1,3 @@
1
1
  module LogjamAgent
2
- VERSION = "0.30.0"
2
+ VERSION = "0.32.3"
3
3
  end
@@ -16,6 +16,7 @@ module LogjamAgent
16
16
  @sequence = SEQUENCE_START
17
17
  @socket = nil
18
18
  @ping_ensured = false
19
+ @socket_mutex = Mutex.new
19
20
  end
20
21
 
21
22
  def connection_specs
@@ -35,11 +36,11 @@ module LogjamAgent
35
36
  }
36
37
  end
37
38
 
38
- @@mutex = Mutex.new
39
+ @@context_mutex = Mutex.new
39
40
  @@zmq_context = nil
40
41
 
41
42
  def self.context
42
- @@mutex.synchronize do
43
+ @@context_mutex.synchronize do
43
44
  @@zmq_context ||=
44
45
  begin
45
46
  require 'ffi-rzmq'
@@ -50,24 +51,9 @@ module LogjamAgent
50
51
  end
51
52
  end
52
53
 
53
- def socket
54
- return @socket if @socket
55
- @socket = self.class.context.socket(ZMQ::DEALER)
56
- ensure_ping_at_exit
57
- @socket.setsockopt(ZMQ::LINGER, @config[:linger])
58
- @socket.setsockopt(ZMQ::SNDHWM, @config[:snd_hwm])
59
- @socket.setsockopt(ZMQ::RCVHWM, @config[:rcv_hwm])
60
- @socket.setsockopt(ZMQ::RCVTIMEO, @config[:rcv_timeo])
61
- @socket.setsockopt(ZMQ::SNDTIMEO, @config[:snd_timeo])
62
- spec = connection_specs.sort_by{rand}.first
63
- @socket.connect(spec)
64
- @socket
65
- end
66
-
67
54
  def reset
68
- if @socket
69
- @socket.close
70
- @socket = nil
55
+ @socket_mutex.synchronize do
56
+ reset_without_locking
71
57
  end
72
58
  end
73
59
 
@@ -84,27 +70,57 @@ module LogjamAgent
84
70
  key += ".#{engine}"
85
71
  end
86
72
  msg = LogjamAgent.encode_payload(data)
87
- if options[:sync]
88
- send_receive(app_env, key, msg)
89
- else
90
- publish(app_env, key, msg)
73
+ @socket_mutex.synchronize do
74
+ if options[:sync]
75
+ send_receive(app_env, key, msg)
76
+ else
77
+ publish(app_env, key, msg)
78
+ end
91
79
  end
92
80
  rescue => error
93
81
  reraise_expectation_errors!
94
82
  raise ForwardingError.new(error.message)
95
83
  end
96
84
 
85
+ private
86
+
87
+ def reset_without_locking
88
+ if @socket
89
+ @socket.close
90
+ @socket = nil
91
+ end
92
+ end
93
+
94
+ # this method assumes the caller holds the socket mutex
95
+ def socket
96
+ return @socket if @socket
97
+ @socket = self.class.context.socket(ZMQ::DEALER)
98
+ raise "ZMQ error on socket creation: #{ZMQ::Util.error_string}" if @socket.nil?
99
+ if LogjamAgent.ensure_ping_at_exit
100
+ ensure_ping_at_exit
101
+ else
102
+ at_exit { reset }
103
+ end
104
+ @socket.setsockopt(ZMQ::LINGER, @config[:linger])
105
+ @socket.setsockopt(ZMQ::SNDHWM, @config[:snd_hwm])
106
+ @socket.setsockopt(ZMQ::RCVHWM, @config[:rcv_hwm])
107
+ @socket.setsockopt(ZMQ::RCVTIMEO, @config[:rcv_timeo])
108
+ @socket.setsockopt(ZMQ::SNDTIMEO, @config[:snd_timeo])
109
+ spec = connection_specs.sort_by{rand}.first
110
+ @socket.connect(spec)
111
+ @socket
112
+ end
113
+
97
114
  def publish(app_env, key, data)
98
115
  info = pack_info(@sequence = next_fixnum(@sequence))
99
116
  parts = [app_env, key, data, info]
100
117
  if socket.send_strings(parts, ZMQ::DONTWAIT) < 0
118
+ error = ZMQ::Util.error_string
101
119
  reset if connection_specs.size > 1
102
- raise "ZMQ error on publishing: #{ZMQ::Util.error_string}"
120
+ raise "ZMQ error on publishing: #{error}"
103
121
  end
104
122
  end
105
123
 
106
- private
107
-
108
124
  def log_warning(message)
109
125
  LogjamAgent.error_handler.call ForwardingWarning.new(message)
110
126
  end
@@ -117,12 +133,12 @@ module LogjamAgent
117
133
  answer_parts = []
118
134
  if socket.send_strings(request_parts) < 0
119
135
  log_warning "ZMQ error on sending: #{ZMQ::Util.error_string}"
120
- reset
136
+ reset_without_locking
121
137
  return nil
122
138
  end
123
139
  if socket.recv_strings(answer_parts) < 0
124
140
  log_warning "ZMQ error on receiving: #{ZMQ::Util.error_string}"
125
- reset
141
+ reset_without_locking
126
142
  return nil
127
143
  end
128
144
  if answer_parts.first != "" || !VALID_RESPONSE_CODES.include?(answer_parts.second.to_s.to_i)
@@ -132,8 +148,10 @@ module LogjamAgent
132
148
  end
133
149
 
134
150
  def ping
135
- if @socket && !send_receive("ping", @app_env, "{}", NO_COMPRESSION)
136
- log_warning "failed to receive pong"
151
+ @socket_mutex.synchronize do
152
+ if @socket && !send_receive("ping", @app_env, "{}", NO_COMPRESSION)
153
+ log_warning "failed to receive pong"
154
+ end
137
155
  end
138
156
  end
139
157