loga 1.4.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/circle.yml CHANGED
@@ -1,16 +1,16 @@
1
1
  dependencies:
2
2
  override:
3
- - rvm-exec 2.2.2 bundle install -j 4
4
- - rvm-exec 2.2.2 bundle exec appraisal install -j 4
3
+ - rvm-exec 2.2.5 bundle install -j 4
4
+ - rvm-exec 2.2.5 bundle exec appraisal install -j 4
5
5
  - rvm-exec 2.3.1 bundle install -j 4
6
6
  - rvm-exec 2.3.1 bundle exec appraisal install -j 4
7
7
  test:
8
8
  override:
9
- - RACK_ENV=development rvm-exec 2.2.2 bundle exec appraisal rspec
10
- - RACK_ENV=production rvm-exec 2.2.2 bundle exec appraisal rspec
9
+ - RACK_ENV=development rvm-exec 2.2.5 bundle exec appraisal rspec
10
+ - RACK_ENV=production rvm-exec 2.2.5 bundle exec appraisal rspec
11
11
  - RACK_ENV=development rvm-exec 2.3.1 bundle exec appraisal rspec
12
12
  - RACK_ENV=production rvm-exec 2.3.1 bundle exec appraisal rspec
13
- - rvm-exec 2.2.2 bundle exec rubocop
13
+ - rvm-exec 2.2.5 bundle exec rubocop
14
14
  deployment:
15
15
  gemfury:
16
16
  tag: /.*/
@@ -5,23 +5,27 @@ require 'loga/utilities'
5
5
  require 'loga/event'
6
6
  require 'loga/formatter'
7
7
  require 'loga/parameter_filter'
8
- require 'loga/revision_strategy'
9
8
  require 'loga/rack/logger'
10
9
  require 'loga/rack/request'
11
10
  require 'loga/rack/request_id'
12
11
  require 'loga/railtie' if defined?(Rails)
13
12
 
14
13
  module Loga
15
- def self.configuration
16
- @configuration ||= Configuration.new
17
- end
14
+ ConfigurationError = Class.new(StandardError)
18
15
 
19
- def self.configure
20
- yield configuration
16
+ def self.configuration
17
+ if @configuration.nil?
18
+ raise ConfigurationError,
19
+ 'Loga has not been configured. Configure with Loga.configure(options)'
20
+ end
21
+ @configuration
21
22
  end
22
23
 
23
- def self.initialize!
24
- configuration.initialize!
24
+ def self.configure(options, framework_options = {})
25
+ unless @configuration.nil?
26
+ raise ConfigurationError, 'Loga has already been configured'
27
+ end
28
+ @configuration ||= Configuration.new(options, framework_options)
25
29
  end
26
30
 
27
31
  def self.logger
@@ -1,76 +1,127 @@
1
+ require 'active_support/core_ext/object/blank'
2
+ require 'active_support/version'
3
+ require 'loga/service_version_strategies'
1
4
  require 'logger'
2
5
  require 'socket'
3
6
 
4
7
  module Loga
5
8
  class Configuration
6
- attr_accessor :service_name,
7
- :service_version,
8
- :device,
9
- :sync,
10
- :filter_parameters,
11
- :level,
12
- :host,
13
- :enabled,
14
- :silence_rails_rack_logger
15
-
16
- attr_reader :logger
17
-
18
- def initialize
19
- @host = gethostname
20
- @device = nil
21
- @sync = true
22
- @level = :info
23
- @filter_parameters = []
24
- @service_version = :git
25
-
26
- # Rails specific configuration
27
- @enabled = true
28
- @silence_rails_rack_logger = true
9
+ DEFAULT_KEYS = %i(
10
+ device
11
+ filter_exceptions
12
+ filter_parameters
13
+ format
14
+ host
15
+ level
16
+ service_name
17
+ service_version
18
+ sync
19
+ tags
20
+ ).freeze
21
+
22
+ FRAMEWORK_EXCEPTIONS = %w(
23
+ ActionController::RoutingError
24
+ ActiveRecord::RecordNotFound
25
+ Sinatra::NotFound
26
+ ).freeze
27
+
28
+ attr_accessor(*DEFAULT_KEYS)
29
+ attr_reader :logger, :service_version
30
+ private_constant :DEFAULT_KEYS
31
+
32
+ def initialize(user_options = {}, framework_options = {})
33
+ options = default_options.merge(framework_options)
34
+ .merge(environment_options)
35
+ .merge(user_options)
36
+
37
+ DEFAULT_KEYS.each do |attribute|
38
+ public_send("#{attribute}=", options[attribute])
39
+ end
40
+
41
+ raise ConfigurationError, 'Service name cannot be blank' if service_name.blank?
42
+ raise ConfigurationError, 'Device cannot be blank' if device.blank?
43
+
44
+ @service_version = initialize_service_version
45
+ @logger = initialize_logger
29
46
  end
30
47
 
31
- def initialize!
32
- @service_name = service_name.to_s.strip
33
- @service_version = compute_service_version
48
+ def format=(name)
49
+ @format = name.to_s.to_sym
50
+ end
34
51
 
35
- initialize_logger
52
+ def service_name=(name)
53
+ @service_name = name.to_s.strip
36
54
  end
37
55
 
38
- def configure
39
- yield self
56
+ def structured?
57
+ format == :gelf
40
58
  end
41
59
 
42
60
  private
43
61
 
44
- def compute_service_version
45
- RevisionStrategy.call(service_version)
62
+ def default_options
63
+ {
64
+ device: STDOUT,
65
+ filter_exceptions: FRAMEWORK_EXCEPTIONS,
66
+ filter_parameters: [],
67
+ format: :simple,
68
+ host: hostname,
69
+ level: :info,
70
+ sync: true,
71
+ tags: [],
72
+ }
46
73
  end
47
74
 
48
- def initialize_logger
49
- device.sync = sync
75
+ def environment_options
76
+ { format: ENV['LOGA_FORMAT'].presence }.reject { |_, v| v.nil? }
77
+ end
78
+
79
+ def initialize_service_version
80
+ service_version || ServiceVersionStrategies.call
81
+ end
50
82
 
83
+ def initialize_logger
84
+ device.sync = sync
51
85
  logger = Logger.new(device)
52
- logger.formatter = Formatter.new(
53
- service_name: service_name,
54
- service_version: service_version,
55
- host: host,
56
- )
86
+ logger.formatter = assign_formatter
57
87
  logger.level = constantized_log_level
58
- rescue
59
- logger = Logger.new(STDERR)
60
- logger.level = Logger::ERROR
61
- logger.error 'Loga could not be initialized'
62
- ensure
63
- @logger = TaggedLogging.new(logger)
88
+ TaggedLogging.new(logger)
64
89
  end
65
90
 
66
91
  def constantized_log_level
67
92
  Logger.const_get(level.to_s.upcase)
68
93
  end
69
94
 
70
- def gethostname
95
+ def hostname
71
96
  Socket.gethostname
72
- rescue Exception
97
+ rescue SystemCallError
73
98
  'unknown.host'
74
99
  end
100
+
101
+ def assign_formatter
102
+ if format == :gelf
103
+ Formatter.new(
104
+ service_name: service_name,
105
+ service_version: service_version,
106
+ host: host,
107
+ )
108
+ else
109
+ active_support_simple_formatter
110
+ end
111
+ end
112
+
113
+ def active_support_simple_formatter
114
+ case ActiveSupport::VERSION::MAJOR
115
+ when 3
116
+ require 'active_support/core_ext/logger'
117
+ Logger::SimpleFormatter.new
118
+ when 4..5
119
+ require 'active_support/logger'
120
+ ActiveSupport::Logger::SimpleFormatter.new
121
+ else
122
+ raise Loga::ConfigurationError,
123
+ "ActiveSupport #{ActiveSupport::VERSION::MAJOR} is unsupported"
124
+ end
125
+ end
75
126
  end
76
127
  end
@@ -10,6 +10,17 @@ module Loga
10
10
  @type = opts[:type]
11
11
  end
12
12
 
13
+ def to_s
14
+ output = ["#{timestamp.iso8601(3)} #{message}"]
15
+ if exception
16
+ output.push exception.to_s
17
+ output.push exception.backtrace.join("\n")
18
+ end
19
+ output.join("\n")
20
+ end
21
+
22
+ alias inspect to_s
23
+
13
24
  private
14
25
 
15
26
  # Guard against Encoding::UndefinedConversionError
@@ -3,11 +3,8 @@ module Loga
3
3
  class Logger
4
4
  include Utilities
5
5
 
6
- attr_reader :logger, :taggers
7
- def initialize(app, logger = nil, taggers = nil)
8
- @app = app
9
- @logger = logger
10
- @taggers = taggers || []
6
+ def initialize(app)
7
+ @app = app
11
8
  end
12
9
 
13
10
  def call(env)
@@ -37,6 +34,7 @@ module Loga
37
34
  send_message
38
35
  end
39
36
 
37
+ # rubocop:disable Metrics/LineLength
40
38
  def set_data
41
39
  data['method'] = request.request_method
42
40
  data['path'] = request.original_path
@@ -44,14 +42,15 @@ module Loga
44
42
  data['request_id'] = request.uuid
45
43
  data['request_ip'] = request.ip
46
44
  data['user_agent'] = request.user_agent
47
- data['controller'] = request.action_controller if request.action_controller_instance
45
+ data['controller'] = request.controller_action_name if request.controller_action_name
48
46
  data['duration'] = duration_in_ms(started_at, Time.now)
49
47
  end
48
+ # rubocop:enable Metrics/LineLength
50
49
 
51
50
  def send_message
52
51
  event = Loga::Event.new(
53
52
  data: { request: data },
54
- exception: fetch_exception,
53
+ exception: compute_exception,
55
54
  message: compute_message,
56
55
  timestamp: started_at,
57
56
  type: 'request',
@@ -59,6 +58,10 @@ module Loga
59
58
  logger.public_send(compute_level, event)
60
59
  end
61
60
 
61
+ def logger
62
+ Loga.logger
63
+ end
64
+
62
65
  def compute_message
63
66
  '%{method} %{filtered_full_path} %{status} in %{duration}ms' % {
64
67
  method: request.request_method,
@@ -69,25 +72,23 @@ module Loga
69
72
  end
70
73
 
71
74
  def compute_level
72
- fetch_exception ? :error : :info
75
+ compute_exception ? :error : :info
73
76
  end
74
77
 
75
- def fetch_exception
76
- framework_exception.tap do |e|
77
- return filtered_exceptions.include?(e.class.to_s) ? nil : e
78
- end
78
+ def compute_exception
79
+ filter_exceptions.include?(exception.class.to_s) ? nil : exception
79
80
  end
80
81
 
81
- def framework_exception
82
+ def exception
82
83
  env['loga.exception'] || env['action_dispatch.exception'] || env['sinatra.error']
83
84
  end
84
85
 
85
- def filtered_exceptions
86
- %w(ActionController::RoutingError Sinatra::NotFound)
86
+ def filter_exceptions
87
+ Loga.configuration.filter_exceptions
87
88
  end
88
89
 
89
90
  def compute_tags(request)
90
- taggers.collect do |tag|
91
+ tags.map do |tag|
91
92
  case tag
92
93
  when Proc
93
94
  tag.call(request)
@@ -98,6 +99,10 @@ module Loga
98
99
  end
99
100
  end
100
101
  end
102
+
103
+ def tags
104
+ Loga.configuration.tags
105
+ end
101
106
  end
102
107
  end
103
108
  end
@@ -18,22 +18,28 @@ module Loga
18
18
 
19
19
  alias request_id uuid
20
20
 
21
- def action_controller
22
- "#{action_controller_instance.class}##{action_controller_instance.action_name}"
23
- end
24
-
25
- def action_controller_instance
26
- @action_controller_instance ||= env[ACTION_CONTROLLER_INSTANCE]
21
+ # Builds a namespaced controller name and action name string.
22
+ #
23
+ # class Admin::UsersController
24
+ # def show
25
+ # end
26
+ # end
27
+ #
28
+ # => "Admin::UsersController#show"
29
+ def controller_action_name
30
+ aci && "#{aci.class.name}##{aci.action_name}"
27
31
  end
28
32
 
29
33
  def original_path
30
34
  env['loga.request.original_path']
31
35
  end
32
36
 
37
+ # rubocop:disable Metrics/LineLength
33
38
  def filtered_full_path
34
39
  @filtered_full_path ||=
35
40
  query_string.empty? ? original_path : "#{original_path}?#{filtered_query_string}"
36
41
  end
42
+ # rubocop:enable Metrics/LineLength
37
43
 
38
44
  def filtered_parameters
39
45
  @filtered_parameters ||= filtered_query_hash.merge(filtered_form_hash)
@@ -83,6 +89,12 @@ module Loga
83
89
  def action_dispatch_filter_params
84
90
  env['action_dispatch.parameter_filter'] || []
85
91
  end
92
+
93
+ def action_controller_instance
94
+ @action_controller_instance ||= env[ACTION_CONTROLLER_INSTANCE]
95
+ end
96
+
97
+ alias aci action_controller_instance
86
98
  end
87
99
  end
88
100
  end
@@ -2,41 +2,70 @@ require 'loga'
2
2
 
3
3
  module Loga
4
4
  class Railtie < Rails::Railtie
5
+ config.loga = {}
6
+
7
+ # This service class initializes Loga with user options, framework smart
8
+ # options and update Rails logger with Loga
5
9
  class InitializeLogger
10
+ def self.call(app)
11
+ new(app).call
12
+ end
13
+
6
14
  def initialize(app)
7
15
  @app = app
8
16
  end
9
17
 
10
18
  def call
11
- loga.tap do |config|
12
- config.sync = sync
13
- config.level = app.config.log_level
14
- end
15
- loga.initialize!
16
- app.config.logger = loga.logger
19
+ validate_user_options
20
+ Loga.configure(user_options, rails_options)
21
+ app.config.colorize_logging = false if Loga.configuration.structured?
22
+ app.config.logger = Loga.logger
17
23
  end
18
24
 
19
25
  private
20
26
 
21
27
  attr_reader :app
22
28
 
23
- def loga
29
+ def rails_options
30
+ {
31
+ format: format,
32
+ level: app.config.log_level,
33
+ sync: sync,
34
+ tags: app.config.log_tags || [],
35
+ }.merge(device_options)
36
+ end
37
+
38
+ def device_options
39
+ Rails.env.test? ? { device: File.open('log/test.log', 'a') } : {}
40
+ end
41
+
42
+ def user_options
24
43
  app.config.loga
25
44
  end
26
45
 
46
+ def format
47
+ Rails.env.production? ? :gelf : :simple
48
+ end
49
+
27
50
  def sync
28
51
  Rails::VERSION::MAJOR > 3 ? app.config.autoflush_log : true
29
52
  end
30
53
 
31
- def constantized_log_level
32
- Logger.const_get(app.config.log_level.to_s.upcase)
54
+ # rubocop:disable Metrics/LineLength
55
+ def validate_user_options
56
+ if user_options[:tags].present?
57
+ raise Loga::ConfigurationError, 'Configure tags with Rails config.log_tags'
58
+ elsif user_options[:level].present?
59
+ raise Loga::ConfigurationError, 'Configure level with Rails config.log_level'
60
+ elsif user_options[:filter_parameters].present?
61
+ raise Loga::ConfigurationError, 'Configure filter_parameters with Rails config.filter_parameters'
62
+ end
33
63
  end
64
+ # rubocop:enable Metrics/LineLength
34
65
  end
35
66
 
36
- config.loga = Loga::Configuration.new
37
-
38
67
  initializer :loga_initialize_logger, before: :initialize_logger do |app|
39
- InitializeLogger.new(app).call if app.config.loga.enabled
68
+ InitializeLogger.call(app)
40
69
  end
41
70
 
42
71
  class InitializeMiddleware
@@ -56,15 +85,19 @@ module Loga
56
85
  end
57
86
  end
58
87
 
88
+ def self.call(app)
89
+ new(app).call
90
+ end
91
+
59
92
  def initialize(app)
60
93
  @app = app
61
94
  end
62
95
 
63
96
  def call
64
97
  insert_loga_rack_logger
65
- disable_rails_rack_logger
98
+ silence_rails_rack_logger
66
99
  insert_exceptions_catcher
67
- disable_action_dispatch_debug_exceptions
100
+ silence_action_dispatch_debug_exceptions_logger
68
101
  end
69
102
 
70
103
  private
@@ -81,45 +114,51 @@ module Loga
81
114
 
82
115
  # Removes start of request log
83
116
  # (e.g. Started GET "/users" for 127.0.0.1 at 2015-12-24 23:59:00 +0000)
84
- def disable_rails_rack_logger
85
- return unless app.config.loga.silence_rails_rack_logger
86
-
117
+ def silence_rails_rack_logger
87
118
  case Rails::VERSION::MAJOR
88
- when 3 then require 'loga/ext/rails/rack/logger3.rb'
119
+ when 3 then require 'loga/ext/rails/rack/logger3.rb'
120
+ when 4..5 then require 'loga/ext/rails/rack/logger.rb'
89
121
  else
90
- require 'loga/ext/rails/rack/logger.rb'
122
+ raise Loga::ConfigurationError,
123
+ "Rails #{Rails::VERSION::MAJOR} is unsupported"
91
124
  end
92
125
  end
93
126
 
94
- def disable_action_dispatch_debug_exceptions
127
+ # Removes unstructured exception output. Exceptions are logged with
128
+ # Loga::Rack::Logger instead
129
+ def silence_action_dispatch_debug_exceptions_logger
95
130
  require 'loga/ext/rails/rack/debug_exceptions.rb'
96
131
  end
97
132
 
98
133
  def insert_loga_rack_logger
99
- app.middleware.insert_after Rails::Rack::Logger,
100
- Loga::Rack::Logger,
101
- app.config.logger,
102
- app.config.log_tags
134
+ app.middleware.insert_after Rails::Rack::Logger, Loga::Rack::Logger
103
135
  end
104
136
  end
105
137
 
106
138
  initializer :loga_initialize_middleware do |app|
107
- InitializeMiddleware.new(app).call if app.config.loga.enabled
108
- app.config.colorize_logging = false
139
+ InitializeMiddleware.call(app) if Loga.configuration.structured?
109
140
  end
110
141
 
111
142
  class InitializeInstrumentation
143
+ def self.call
144
+ new.call
145
+ end
146
+
112
147
  def call
113
- remove_existing_log_subscriptions
148
+ ensure_subscriptions_attached
149
+ remove_log_subscriptions
114
150
  end
115
151
 
116
152
  private
117
153
 
118
- # rubocop:disable Metrics/CyclomaticComplexity
119
- def remove_existing_log_subscriptions
154
+ # Ensure LogSubscribers are attached when available
155
+ def ensure_subscriptions_attached
120
156
  ActionView::Base if defined?(ActionView::Base)
121
157
  ActionController::Base if defined?(ActionController::Base)
158
+ end
122
159
 
160
+ # rubocop:disable Metrics/LineLength
161
+ def remove_log_subscriptions
123
162
  ActiveSupport::LogSubscriber.log_subscribers.each do |subscriber|
124
163
  case subscriber
125
164
  when defined?(ActionView::LogSubscriber) && ActionView::LogSubscriber
@@ -129,10 +168,12 @@ module Loga
129
168
  end
130
169
  end
131
170
  end
132
- # rubocop:enable Metrics/CyclomaticComplexity
171
+ # rubocop:enable Metrics/LineLength
133
172
 
134
173
  def unsubscribe(component, subscriber)
135
- events = subscriber.public_methods(false).reject { |method| method.to_s == 'call' }
174
+ events = subscriber
175
+ .public_methods(false)
176
+ .reject { |method| method.to_s == 'call' }
136
177
  events.each do |event|
137
178
  ActiveSupport::Notifications
138
179
  .notifier
@@ -146,8 +187,8 @@ module Loga
146
187
  end
147
188
  end
148
189
 
149
- config.after_initialize do |app|
150
- InitializeInstrumentation.new.call if app.config.loga.enabled
190
+ config.after_initialize do |_|
191
+ InitializeInstrumentation.call if Loga.configuration.structured?
151
192
  end
152
193
  end
153
194
  end