logjam_agent 0.29.4 → 0.32.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -5,11 +5,19 @@ module LogjamAgent
5
5
  def initialize
6
6
  @hostname = LogjamAgent.hostname
7
7
  @app_name = "rails"
8
- @attributes = []
9
8
  @newline = ActiveSupport::VERSION::STRING < "4.0" ? "" : "\n"
10
9
  end
11
10
 
12
- attr_accessor :attributes
11
+ def attributes=(attributes)
12
+ Thread.current.thread_variable_set(:__logjam_formatter_attributes__, attributes)
13
+ end
14
+
15
+ def attributes
16
+ unless attributes = Thread.current.thread_variable_get(:__logjam_formatter_attributes__)
17
+ attributes = Thread.current.thread_variable_set(:__logjam_formatter_attributes__, [])
18
+ end
19
+ attributes
20
+ end
13
21
 
14
22
  SEV_LABEL = Logger::SEV_LABEL.map{|sev| "%-5s" % sev}
15
23
 
@@ -33,7 +41,7 @@ module LogjamAgent
33
41
  "#{format_severity(severity)} #{format_time(timestamp)}#{render_attributes}#{format_host_info(progname)}: #{format_message(msg)}#{@newline}"
34
42
  end
35
43
 
36
- if !defined?(Rails) || Rails.env.development?
44
+ if !defined?(Rails::Railtie) || Rails.env.development?
37
45
  def format_host_info(progname); ""; end
38
46
  else
39
47
  def format_host_info(progname)
@@ -42,19 +50,19 @@ module LogjamAgent
42
50
  end
43
51
 
44
52
  def render_attributes
45
- @attributes.map{|key, value| " #{key}[#{value}]"}.join
53
+ attributes.map{|key, value| " #{key}[#{value}]"}.join
46
54
  end
47
55
 
48
56
  def set_attribute(name, value)
49
- if attribute = @attributes.detect{|n,v| n == name}
57
+ if attribute = attributes.detect{|n,v| n == name}
50
58
  attribute[1] = value
51
59
  else
52
- @attributes << [name, value]
60
+ attributes << [name, value]
53
61
  end
54
62
  end
55
63
 
56
64
  def reset_attributes
57
- @attributes = []
65
+ self.attributes = []
58
66
  end
59
67
  end
60
68
  end
@@ -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.29.4"
2
+ VERSION = "0.32.0"
3
3
  end
@@ -15,7 +15,8 @@ module LogjamAgent
15
15
  @config[:host] = "localhost" if @config[:host].blank?
16
16
  @sequence = SEQUENCE_START
17
17
  @socket = nil
18
- at_exit { ping; reset }
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,26 +51,21 @@ 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
- @socket.setsockopt(ZMQ::LINGER, @config[:linger])
57
- @socket.setsockopt(ZMQ::SNDHWM, @config[:snd_hwm])
58
- @socket.setsockopt(ZMQ::RCVHWM, @config[:rcv_hwm])
59
- @socket.setsockopt(ZMQ::RCVTIMEO, @config[:rcv_timeo])
60
- @socket.setsockopt(ZMQ::SNDTIMEO, @config[:snd_timeo])
61
- spec = connection_specs.sort_by{rand}.first
62
- @socket.connect(spec)
63
- @socket
64
- end
65
-
66
54
  def reset
67
- if @socket
68
- @socket.close
69
- @socket = nil
55
+ @socket_mutex.synchronize do
56
+ if @socket
57
+ @socket.close
58
+ @socket = nil
59
+ end
70
60
  end
71
61
  end
72
62
 
63
+ def ensure_ping_at_exit
64
+ return if @ping_ensured
65
+ at_exit { ping; reset }
66
+ @ping_ensured = true
67
+ end
68
+
73
69
  def forward(data, options={})
74
70
  app_env = options[:app_env] || @app_env
75
71
  key = options[:routing_key] || "logs.#{app_env.sub('-','.')}"
@@ -77,27 +73,50 @@ module LogjamAgent
77
73
  key += ".#{engine}"
78
74
  end
79
75
  msg = LogjamAgent.encode_payload(data)
80
- if options[:sync]
81
- send_receive(app_env, key, msg)
82
- else
83
- publish(app_env, key, msg)
76
+ @socket_mutex.synchronize do
77
+ if options[:sync]
78
+ send_receive(app_env, key, msg)
79
+ else
80
+ publish(app_env, key, msg)
81
+ end
84
82
  end
85
83
  rescue => error
86
84
  reraise_expectation_errors!
87
85
  raise ForwardingError.new(error.message)
88
86
  end
89
87
 
88
+ private
89
+
90
+ # this method assumes the caller holds the socket mutex
91
+ def socket
92
+ return @socket if @socket
93
+ @socket = self.class.context.socket(ZMQ::DEALER)
94
+ raise "ZMQ error on socket creation: #{ZMQ::Util.error_string}" if @socket.nil?
95
+ if LogjamAgent.ensure_ping_at_exit
96
+ ensure_ping_at_exit
97
+ else
98
+ at_exit { reset }
99
+ end
100
+ @socket.setsockopt(ZMQ::LINGER, @config[:linger])
101
+ @socket.setsockopt(ZMQ::SNDHWM, @config[:snd_hwm])
102
+ @socket.setsockopt(ZMQ::RCVHWM, @config[:rcv_hwm])
103
+ @socket.setsockopt(ZMQ::RCVTIMEO, @config[:rcv_timeo])
104
+ @socket.setsockopt(ZMQ::SNDTIMEO, @config[:snd_timeo])
105
+ spec = connection_specs.sort_by{rand}.first
106
+ @socket.connect(spec)
107
+ @socket
108
+ end
109
+
90
110
  def publish(app_env, key, data)
91
111
  info = pack_info(@sequence = next_fixnum(@sequence))
92
112
  parts = [app_env, key, data, info]
93
113
  if socket.send_strings(parts, ZMQ::DONTWAIT) < 0
114
+ error = ZMQ::Util.error_string
94
115
  reset if connection_specs.size > 1
95
- raise "ZMQ error on publishing: #{ZMQ::Util.error_string}"
116
+ raise "ZMQ error on publishing: #{error}"
96
117
  end
97
118
  end
98
119
 
99
- private
100
-
101
120
  def log_warning(message)
102
121
  LogjamAgent.error_handler.call ForwardingWarning.new(message)
103
122
  end
@@ -125,8 +144,10 @@ module LogjamAgent
125
144
  end
126
145
 
127
146
  def ping
128
- if @socket && !send_receive("ping", "", "{}", NO_COMPRESSION)
129
- log_warning "failed to receive pong"
147
+ @socket_mutex.synchronize do
148
+ if @socket && !send_receive("ping", @app_env, "{}", NO_COMPRESSION)
149
+ log_warning "failed to receive pong"
150
+ end
130
151
  end
131
152
  end
132
153