loga 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 (102) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +25 -0
  3. data/.rspec +2 -0
  4. data/.rubocop.yml +19 -0
  5. data/.rubocop_todo.yml +33 -0
  6. data/Appraisals +14 -0
  7. data/Gemfile +8 -0
  8. data/README.md +147 -0
  9. data/Rakefile +9 -0
  10. data/circle.yml +23 -0
  11. data/gemfiles/rails32.gemfile +11 -0
  12. data/gemfiles/rails40.gemfile +11 -0
  13. data/gemfiles/sinatra14.gemfile +11 -0
  14. data/gemfiles/unit.gemfile +9 -0
  15. data/lib/loga.rb +33 -0
  16. data/lib/loga/configuration.rb +96 -0
  17. data/lib/loga/event.rb +21 -0
  18. data/lib/loga/ext/rails/rack/logger3.rb +21 -0
  19. data/lib/loga/ext/rails/rack/logger4.rb +13 -0
  20. data/lib/loga/formatter.rb +104 -0
  21. data/lib/loga/parameter_filter.rb +65 -0
  22. data/lib/loga/rack/logger.rb +102 -0
  23. data/lib/loga/rack/request.rb +77 -0
  24. data/lib/loga/rack/request_id.rb +44 -0
  25. data/lib/loga/railtie.rb +139 -0
  26. data/lib/loga/tagged_logging.rb +76 -0
  27. data/lib/loga/utilities.rb +7 -0
  28. data/lib/loga/version.rb +3 -0
  29. data/loga.gemspec +31 -0
  30. data/spec/fixtures/README.md +8 -0
  31. data/spec/fixtures/rails32/Rakefile +7 -0
  32. data/spec/fixtures/rails32/app/controllers/application_controller.rb +28 -0
  33. data/spec/fixtures/rails32/app/helpers/application_helper.rb +2 -0
  34. data/spec/fixtures/rails32/app/views/layouts/application.html.erb +14 -0
  35. data/spec/fixtures/rails32/app/views/user.html.erb +1 -0
  36. data/spec/fixtures/rails32/config.ru +4 -0
  37. data/spec/fixtures/rails32/config/application.rb +71 -0
  38. data/spec/fixtures/rails32/config/boot.rb +6 -0
  39. data/spec/fixtures/rails32/config/environment.rb +5 -0
  40. data/spec/fixtures/rails32/config/environments/development.rb +26 -0
  41. data/spec/fixtures/rails32/config/environments/production.rb +50 -0
  42. data/spec/fixtures/rails32/config/environments/test.rb +35 -0
  43. data/spec/fixtures/rails32/config/initializers/backtrace_silencers.rb +7 -0
  44. data/spec/fixtures/rails32/config/initializers/inflections.rb +15 -0
  45. data/spec/fixtures/rails32/config/initializers/mime_types.rb +5 -0
  46. data/spec/fixtures/rails32/config/initializers/secret_token.rb +7 -0
  47. data/spec/fixtures/rails32/config/initializers/session_store.rb +8 -0
  48. data/spec/fixtures/rails32/config/initializers/wrap_parameters.rb +10 -0
  49. data/spec/fixtures/rails32/config/locales/en.yml +5 -0
  50. data/spec/fixtures/rails32/config/routes.rb +64 -0
  51. data/spec/fixtures/rails32/public/404.html +26 -0
  52. data/spec/fixtures/rails32/public/422.html +26 -0
  53. data/spec/fixtures/rails32/public/500.html +25 -0
  54. data/spec/fixtures/rails32/public/favicon.ico +0 -0
  55. data/spec/fixtures/rails32/public/index.html +241 -0
  56. data/spec/fixtures/rails32/public/robots.txt +5 -0
  57. data/spec/fixtures/rails32/script/rails +6 -0
  58. data/spec/fixtures/rails40/Rakefile +6 -0
  59. data/spec/fixtures/rails40/app/controllers/application_controller.rb +30 -0
  60. data/spec/fixtures/rails40/app/helpers/application_helper.rb +2 -0
  61. data/spec/fixtures/rails40/app/views/layouts/application.html.erb +14 -0
  62. data/spec/fixtures/rails40/app/views/user.html.erb +1 -0
  63. data/spec/fixtures/rails40/bin/bundle +3 -0
  64. data/spec/fixtures/rails40/bin/rails +4 -0
  65. data/spec/fixtures/rails40/bin/rake +4 -0
  66. data/spec/fixtures/rails40/config.ru +4 -0
  67. data/spec/fixtures/rails40/config/application.rb +37 -0
  68. data/spec/fixtures/rails40/config/boot.rb +4 -0
  69. data/spec/fixtures/rails40/config/environment.rb +5 -0
  70. data/spec/fixtures/rails40/config/environments/development.rb +24 -0
  71. data/spec/fixtures/rails40/config/environments/production.rb +65 -0
  72. data/spec/fixtures/rails40/config/environments/test.rb +39 -0
  73. data/spec/fixtures/rails40/config/initializers/backtrace_silencers.rb +7 -0
  74. data/spec/fixtures/rails40/config/initializers/filter_parameter_logging.rb +4 -0
  75. data/spec/fixtures/rails40/config/initializers/inflections.rb +16 -0
  76. data/spec/fixtures/rails40/config/initializers/mime_types.rb +5 -0
  77. data/spec/fixtures/rails40/config/initializers/secret_token.rb +12 -0
  78. data/spec/fixtures/rails40/config/initializers/session_store.rb +3 -0
  79. data/spec/fixtures/rails40/config/initializers/wrap_parameters.rb +9 -0
  80. data/spec/fixtures/rails40/config/locales/en.yml +23 -0
  81. data/spec/fixtures/rails40/config/routes.rb +62 -0
  82. data/spec/fixtures/rails40/public/404.html +58 -0
  83. data/spec/fixtures/rails40/public/422.html +58 -0
  84. data/spec/fixtures/rails40/public/500.html +57 -0
  85. data/spec/fixtures/rails40/public/favicon.ico +0 -0
  86. data/spec/fixtures/rails40/public/robots.txt +5 -0
  87. data/spec/integration/rails/railtie_spec.rb +64 -0
  88. data/spec/integration/rails/request_spec.rb +42 -0
  89. data/spec/integration/sinatra_spec.rb +54 -0
  90. data/spec/spec_helper.rb +39 -0
  91. data/spec/support/helpers.rb +16 -0
  92. data/spec/support/request_spec.rb +183 -0
  93. data/spec/support/timecop_shared.rb +7 -0
  94. data/spec/unit/loga/configuration_spec.rb +123 -0
  95. data/spec/unit/loga/event_spec.rb +20 -0
  96. data/spec/unit/loga/formatter_spec.rb +186 -0
  97. data/spec/unit/loga/parameter_filter_spec.rb +76 -0
  98. data/spec/unit/loga/rack/logger_spec.rb +114 -0
  99. data/spec/unit/loga/rack/request_spec.rb +70 -0
  100. data/spec/unit/loga/utilities_spec.rb +16 -0
  101. data/spec/unit/loga_spec.rb +41 -0
  102. metadata +357 -0
@@ -0,0 +1,21 @@
1
+ module Loga
2
+ class Event
3
+ attr_accessor :data, :exception, :message, :timestamp, :type
4
+
5
+ def initialize(opts = {})
6
+ @data = opts[:data]
7
+ @exception = opts[:exception]
8
+ @message = safe_encode(opts[:message])
9
+ @timestamp = opts[:timestamp]
10
+ @type = opts[:type]
11
+ end
12
+
13
+ private
14
+
15
+ # Guard against Encoding::UndefinedConversionError
16
+ # http://stackoverflow.com/questions/13003287/encodingundefinedconversionerror
17
+ def safe_encode(text)
18
+ text.to_s.encode('UTF-8', invalid: :replace, undef: :replace, replace: '?')
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,21 @@
1
+ require 'rails/rack/logger'
2
+
3
+ module Rails
4
+ module Rack
5
+ class Logger
6
+ protected
7
+
8
+ def call_app(_request, env)
9
+ @app.call(env)
10
+ ensure
11
+ ActiveSupport::LogSubscriber.flush_all!
12
+ end
13
+
14
+ private
15
+
16
+ def compute_tags(_request)
17
+ []
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,13 @@
1
+ require 'rails/rack/logger'
2
+
3
+ module Rails
4
+ module Rack
5
+ class Logger
6
+ private
7
+
8
+ def logger
9
+ ::Logger.new('/dev/null')
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,104 @@
1
+ require 'logger'
2
+ require 'json'
3
+
4
+ module Loga
5
+ class Formatter < Logger::Formatter
6
+ include TaggedLogging::Formatter
7
+
8
+ GELF_VERSION = '1.1'.freeze
9
+ SYSLOG_LEVEL_MAPPING = {
10
+ 'DEBUG' => 7,
11
+ 'INFO' => 6,
12
+ 'WARN' => 4,
13
+ 'ERROR' => 3,
14
+ 'FATAL' => 2,
15
+ 'UNKNOWN' => 1,
16
+ }.freeze
17
+ DEFAULT_TYPE = 'default'.freeze
18
+
19
+ def initialize(opts)
20
+ @service_name = opts.fetch(:service_name)
21
+ @service_version = opts.fetch(:service_version)
22
+ @host = opts.fetch(:host)
23
+ end
24
+
25
+ def call(severity, time, _progname, message)
26
+ event = build_event(time, message)
27
+ payload = format_additional_fields(event.data)
28
+
29
+ payload[:short_message] = event.message
30
+ payload[:timestamp] = compute_timestamp(event.timestamp)
31
+ payload[:host] = @host
32
+ payload[:level] = compute_level(severity)
33
+ payload[:version] = GELF_VERSION
34
+
35
+ "#{payload.to_json}\n"
36
+ end
37
+
38
+ private
39
+
40
+ def build_event(time, message)
41
+ event = case message
42
+ when Loga::Event
43
+ message
44
+ else
45
+ Loga::Event.new(message: message)
46
+ end
47
+
48
+ event.timestamp ||= time
49
+ event.data ||= {}
50
+ event.data.tap do |hash|
51
+ hash.merge! compute_exception(event.exception)
52
+ hash.merge! compute_type(event.type)
53
+ # Overwrite hash with Loga's additional fields
54
+ hash.merge! loga_additional_fields
55
+ end
56
+ event
57
+ end
58
+
59
+ def compute_timestamp(timestamp)
60
+ (timestamp.to_f * 1000).floor / 1000.0
61
+ end
62
+
63
+ def compute_level(severity)
64
+ SYSLOG_LEVEL_MAPPING[severity]
65
+ end
66
+
67
+ def format_additional_fields(fields)
68
+ fields.each_with_object({}) do |(main_key, values), hash|
69
+ if values.is_a?(Hash)
70
+ values.each do |sub_key, sub_values|
71
+ hash["_#{main_key}.#{sub_key}"] = sub_values
72
+ end
73
+ else
74
+ hash["_#{main_key}"] = values
75
+ end
76
+ end
77
+ end
78
+
79
+ def compute_exception(exception)
80
+ return {} unless exception
81
+ {
82
+ exception: {
83
+ klass: exception.class.to_s,
84
+ message: exception.message,
85
+ backtrace: exception.backtrace.first(10).join("\n"),
86
+ },
87
+ }
88
+ end
89
+
90
+ def compute_type(type)
91
+ type ? { type: type } : {}
92
+ end
93
+
94
+ def loga_additional_fields
95
+ {
96
+ service: {
97
+ name: @service_name,
98
+ version: @service_version,
99
+ },
100
+ tags: current_tags,
101
+ }
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,65 @@
1
+ module Loga
2
+ class ParameterFilter
3
+ FILTERED = '[FILTERED]'.freeze
4
+
5
+ attr_accessor :filters
6
+
7
+ def initialize(filters)
8
+ @filters = filters
9
+ end
10
+
11
+ def filter(params)
12
+ compiled_filters.call(params)
13
+ end
14
+
15
+ private
16
+
17
+ def compiled_filters
18
+ @compiled_filters ||= CompiledFilter.compile(filters)
19
+ end
20
+
21
+ class CompiledFilter
22
+ def self.compile(filters)
23
+ ->(params) { params.dup } if filters.empty?
24
+
25
+ regexps = []
26
+ strings = []
27
+
28
+ filters.each do |item|
29
+ if item.is_a?(Regexp)
30
+ regexps << item
31
+ else
32
+ strings << Regexp.escape(item.to_s)
33
+ end
34
+ end
35
+
36
+ regexps << Regexp.new(strings.join('|'), true) unless strings.empty?
37
+ new regexps
38
+ end
39
+
40
+ attr_reader :regexps
41
+
42
+ def initialize(regexps)
43
+ @regexps = regexps
44
+ end
45
+
46
+ def call(original_params)
47
+ filtered_params = {}
48
+
49
+ original_params.each do |key, value|
50
+ if regexps.any? { |r| key =~ r }
51
+ value = FILTERED
52
+ elsif value.is_a?(Hash)
53
+ value = call(value)
54
+ elsif value.is_a?(Array)
55
+ value = value.map { |v| v.is_a?(Hash) ? call(v) : v }
56
+ end
57
+
58
+ filtered_params[key] = value
59
+ end
60
+
61
+ filtered_params
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,102 @@
1
+ module Loga
2
+ module Rack
3
+ class Logger
4
+ include Utilities
5
+
6
+ attr_reader :logger, :taggers
7
+ def initialize(app, logger = nil, taggers = nil)
8
+ @app = app
9
+ @logger = logger
10
+ @taggers = taggers || []
11
+ end
12
+
13
+ def call(env)
14
+ request = Loga::Rack::Request.new(env)
15
+ env['loga.request.original_path'] = request.path
16
+
17
+ if logger.respond_to?(:tagged)
18
+ logger.tagged(compute_tags(request)) { call_app(request, env) }
19
+ else
20
+ call_app(request, env)
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ attr_reader :data, :env, :request, :started_at
27
+
28
+ def call_app(request, env)
29
+ @data = {}
30
+ @env = env
31
+ @request = request
32
+ @started_at = Time.now
33
+
34
+ @app.call(env).tap { |status, _headers, _body| data['status'] = status.to_i }
35
+ ensure
36
+ set_data
37
+ send_message
38
+ end
39
+
40
+ def set_data
41
+ data['method'] = request.request_method
42
+ data['path'] = request.original_path
43
+ data['params'] = request.filtered_parameters
44
+ data['request_id'] = request.uuid
45
+ data['request_ip'] = request.ip
46
+ data['user_agent'] = request.user_agent
47
+ data['duration'] = duration_in_ms(started_at, Time.now)
48
+ end
49
+
50
+ def send_message
51
+ event = Loga::Event.new(
52
+ data: { request: data },
53
+ exception: fetch_exception,
54
+ message: compute_message,
55
+ timestamp: started_at,
56
+ type: 'request',
57
+ )
58
+ logger.public_send(compute_level, event)
59
+ end
60
+
61
+ def compute_message
62
+ '%{method} %{filtered_full_path} %{status} in %{duration}ms' % {
63
+ method: request.request_method,
64
+ filtered_full_path: request.filtered_full_path,
65
+ status: data['status'],
66
+ duration: data['duration'],
67
+ }
68
+ end
69
+
70
+ def compute_level
71
+ fetch_exception ? :error : :info
72
+ end
73
+
74
+ def fetch_exception
75
+ framework_exception.tap do |e|
76
+ return filtered_exceptions.include?(e.class.to_s) ? nil : e
77
+ end
78
+ end
79
+
80
+ def framework_exception
81
+ env['loga.exception'] || env['action_dispatch.exception'] || env['sinatra.error']
82
+ end
83
+
84
+ def filtered_exceptions
85
+ %w(ActionController::RoutingError Sinatra::NotFound)
86
+ end
87
+
88
+ def compute_tags(request)
89
+ taggers.collect do |tag|
90
+ case tag
91
+ when Proc
92
+ tag.call(request)
93
+ when Symbol
94
+ request.send(tag)
95
+ else
96
+ tag
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,77 @@
1
+ require 'rack/request'
2
+ require 'rack/utils'
3
+
4
+ module Loga
5
+ module Rack
6
+ class Request < ::Rack::Request
7
+ ACTION_DISPATCH_REQUEST_ID = 'action_dispatch.request_id'.freeze
8
+
9
+ def initialize(env)
10
+ super
11
+ @uuid = nil
12
+ end
13
+
14
+ def uuid
15
+ @uuid ||= env[ACTION_DISPATCH_REQUEST_ID]
16
+ end
17
+
18
+ def original_path
19
+ env['loga.request.original_path']
20
+ end
21
+
22
+ def filtered_full_path
23
+ @filtered_full_path ||=
24
+ query_string.empty? ? original_path : "#{original_path}?#{filtered_query_string}"
25
+ end
26
+
27
+ def filtered_parameters
28
+ @filtered_parameters ||= filtered_query_hash.merge(filtered_form_hash)
29
+ end
30
+
31
+ def filtered_query_hash
32
+ @filtered_query_hash ||= filter_hash(query_hash)
33
+ end
34
+
35
+ def filtered_form_hash
36
+ @filter_form_hash ||= filter_hash(form_hash)
37
+ end
38
+
39
+ private
40
+
41
+ def query_hash
42
+ params
43
+ env['rack.request.query_hash'] || {}
44
+ end
45
+
46
+ def form_hash
47
+ params
48
+ env['rack.request.form_hash'] || {}
49
+ end
50
+
51
+ def filter_hash(hash)
52
+ parameter_filter.filter(hash)
53
+ end
54
+
55
+ KV_RE = '[^&;=]+'
56
+ PAIR_RE = /(#{KV_RE})=(#{KV_RE})/
57
+ def filtered_query_string
58
+ query_string.gsub(PAIR_RE) do |_|
59
+ parameter_filter.filter([[$1, $2]]).first.join('=')
60
+ end
61
+ end
62
+
63
+ def parameter_filter
64
+ @filter_parameters ||=
65
+ ParameterFilter.new(loga_filter_parameters | action_dispatch_filter_params)
66
+ end
67
+
68
+ def loga_filter_parameters
69
+ Loga.configuration.filter_parameters || []
70
+ end
71
+
72
+ def action_dispatch_filter_params
73
+ env['action_dispatch.parameter_filter'] || []
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,44 @@
1
+ # Shamelessly copied from ActionDispatch::RequestId
2
+ require 'securerandom'
3
+ require 'active_support/core_ext/string/access'
4
+ require 'active_support/core_ext/object/blank'
5
+
6
+ # rubocop:disable Metrics/LineLength, Lint/AssignmentInCondition, Style/GuardClause
7
+ module Loga
8
+ module Rack
9
+ # Makes a unique request id available to the action_dispatch.request_id env variable (which is then accessible through
10
+ # ActionDispatch::Request#uuid) and sends the same id to the client via the X-Request-Id header.
11
+ #
12
+ # The unique request id is either based on the X-Request-Id header in the request, which would typically be generated
13
+ # by a firewall, load balancer, or the web server, or, if this header is not available, a random uuid. If the
14
+ # header is accepted from the outside world, we sanitize it to a max of 255 chars and alphanumeric and dashes only.
15
+ #
16
+ # The unique request id can be used to trace a request end-to-end and would typically end up being part of log files
17
+ # from multiple pieces of the stack.
18
+ class RequestId
19
+ def initialize(app)
20
+ @app = app
21
+ end
22
+
23
+ def call(env)
24
+ env['action_dispatch.request_id'] = external_request_id(env) || internal_request_id
25
+ @app.call(env).tap do |_status, headers, _body|
26
+ headers['X-Request-Id'] = env['action_dispatch.request_id']
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ def external_request_id(env)
33
+ if request_id = env['HTTP_X_REQUEST_ID'].presence
34
+ request_id.gsub(/[^\w\-]/, '').first(255)
35
+ end
36
+ end
37
+
38
+ def internal_request_id
39
+ SecureRandom.uuid
40
+ end
41
+ end
42
+ end
43
+ end
44
+ # rubocop:enable Metrics/LineLength, Lint/AssignmentInCondition, Style/GuardClause