logjam_agent 0.29.5 → 0.32.1

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.
@@ -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.5"
2
+ VERSION = "0.32.1"
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,18 @@ 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
+ reset_without_locking
70
57
  end
71
58
  end
72
59
 
60
+ def ensure_ping_at_exit
61
+ return if @ping_ensured
62
+ at_exit { ping; reset }
63
+ @ping_ensured = true
64
+ end
65
+
73
66
  def forward(data, options={})
74
67
  app_env = options[:app_env] || @app_env
75
68
  key = options[:routing_key] || "logs.#{app_env.sub('-','.')}"
@@ -77,27 +70,57 @@ module LogjamAgent
77
70
  key += ".#{engine}"
78
71
  end
79
72
  msg = LogjamAgent.encode_payload(data)
80
- if options[:sync]
81
- send_receive(app_env, key, msg)
82
- else
83
- 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
84
79
  end
85
80
  rescue => error
86
81
  reraise_expectation_errors!
87
82
  raise ForwardingError.new(error.message)
88
83
  end
89
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
+
90
114
  def publish(app_env, key, data)
91
115
  info = pack_info(@sequence = next_fixnum(@sequence))
92
116
  parts = [app_env, key, data, info]
93
117
  if socket.send_strings(parts, ZMQ::DONTWAIT) < 0
118
+ error = ZMQ::Util.error_string
94
119
  reset if connection_specs.size > 1
95
- raise "ZMQ error on publishing: #{ZMQ::Util.error_string}"
120
+ raise "ZMQ error on publishing: #{error}"
96
121
  end
97
122
  end
98
123
 
99
- private
100
-
101
124
  def log_warning(message)
102
125
  LogjamAgent.error_handler.call ForwardingWarning.new(message)
103
126
  end
@@ -110,12 +133,12 @@ module LogjamAgent
110
133
  answer_parts = []
111
134
  if socket.send_strings(request_parts) < 0
112
135
  log_warning "ZMQ error on sending: #{ZMQ::Util.error_string}"
113
- reset
136
+ reset_without_locking
114
137
  return nil
115
138
  end
116
139
  if socket.recv_strings(answer_parts) < 0
117
140
  log_warning "ZMQ error on receiving: #{ZMQ::Util.error_string}"
118
- reset
141
+ reset_without_locking
119
142
  return nil
120
143
  end
121
144
  if answer_parts.first != "" || !VALID_RESPONSE_CODES.include?(answer_parts.second.to_s.to_i)
@@ -125,8 +148,10 @@ module LogjamAgent
125
148
  end
126
149
 
127
150
  def ping
128
- if @socket && !send_receive("ping", @app_env, "{}", NO_COMPRESSION)
129
- 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
130
155
  end
131
156
  end
132
157