loga 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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