loga 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +25 -0
- data/.rspec +2 -0
- data/.rubocop.yml +19 -0
- data/.rubocop_todo.yml +33 -0
- data/Appraisals +14 -0
- data/Gemfile +8 -0
- data/README.md +147 -0
- data/Rakefile +9 -0
- data/circle.yml +23 -0
- data/gemfiles/rails32.gemfile +11 -0
- data/gemfiles/rails40.gemfile +11 -0
- data/gemfiles/sinatra14.gemfile +11 -0
- data/gemfiles/unit.gemfile +9 -0
- data/lib/loga.rb +33 -0
- data/lib/loga/configuration.rb +96 -0
- data/lib/loga/event.rb +21 -0
- data/lib/loga/ext/rails/rack/logger3.rb +21 -0
- data/lib/loga/ext/rails/rack/logger4.rb +13 -0
- data/lib/loga/formatter.rb +104 -0
- data/lib/loga/parameter_filter.rb +65 -0
- data/lib/loga/rack/logger.rb +102 -0
- data/lib/loga/rack/request.rb +77 -0
- data/lib/loga/rack/request_id.rb +44 -0
- data/lib/loga/railtie.rb +139 -0
- data/lib/loga/tagged_logging.rb +76 -0
- data/lib/loga/utilities.rb +7 -0
- data/lib/loga/version.rb +3 -0
- data/loga.gemspec +31 -0
- data/spec/fixtures/README.md +8 -0
- data/spec/fixtures/rails32/Rakefile +7 -0
- data/spec/fixtures/rails32/app/controllers/application_controller.rb +28 -0
- data/spec/fixtures/rails32/app/helpers/application_helper.rb +2 -0
- data/spec/fixtures/rails32/app/views/layouts/application.html.erb +14 -0
- data/spec/fixtures/rails32/app/views/user.html.erb +1 -0
- data/spec/fixtures/rails32/config.ru +4 -0
- data/spec/fixtures/rails32/config/application.rb +71 -0
- data/spec/fixtures/rails32/config/boot.rb +6 -0
- data/spec/fixtures/rails32/config/environment.rb +5 -0
- data/spec/fixtures/rails32/config/environments/development.rb +26 -0
- data/spec/fixtures/rails32/config/environments/production.rb +50 -0
- data/spec/fixtures/rails32/config/environments/test.rb +35 -0
- data/spec/fixtures/rails32/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/fixtures/rails32/config/initializers/inflections.rb +15 -0
- data/spec/fixtures/rails32/config/initializers/mime_types.rb +5 -0
- data/spec/fixtures/rails32/config/initializers/secret_token.rb +7 -0
- data/spec/fixtures/rails32/config/initializers/session_store.rb +8 -0
- data/spec/fixtures/rails32/config/initializers/wrap_parameters.rb +10 -0
- data/spec/fixtures/rails32/config/locales/en.yml +5 -0
- data/spec/fixtures/rails32/config/routes.rb +64 -0
- data/spec/fixtures/rails32/public/404.html +26 -0
- data/spec/fixtures/rails32/public/422.html +26 -0
- data/spec/fixtures/rails32/public/500.html +25 -0
- data/spec/fixtures/rails32/public/favicon.ico +0 -0
- data/spec/fixtures/rails32/public/index.html +241 -0
- data/spec/fixtures/rails32/public/robots.txt +5 -0
- data/spec/fixtures/rails32/script/rails +6 -0
- data/spec/fixtures/rails40/Rakefile +6 -0
- data/spec/fixtures/rails40/app/controllers/application_controller.rb +30 -0
- data/spec/fixtures/rails40/app/helpers/application_helper.rb +2 -0
- data/spec/fixtures/rails40/app/views/layouts/application.html.erb +14 -0
- data/spec/fixtures/rails40/app/views/user.html.erb +1 -0
- data/spec/fixtures/rails40/bin/bundle +3 -0
- data/spec/fixtures/rails40/bin/rails +4 -0
- data/spec/fixtures/rails40/bin/rake +4 -0
- data/spec/fixtures/rails40/config.ru +4 -0
- data/spec/fixtures/rails40/config/application.rb +37 -0
- data/spec/fixtures/rails40/config/boot.rb +4 -0
- data/spec/fixtures/rails40/config/environment.rb +5 -0
- data/spec/fixtures/rails40/config/environments/development.rb +24 -0
- data/spec/fixtures/rails40/config/environments/production.rb +65 -0
- data/spec/fixtures/rails40/config/environments/test.rb +39 -0
- data/spec/fixtures/rails40/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/fixtures/rails40/config/initializers/filter_parameter_logging.rb +4 -0
- data/spec/fixtures/rails40/config/initializers/inflections.rb +16 -0
- data/spec/fixtures/rails40/config/initializers/mime_types.rb +5 -0
- data/spec/fixtures/rails40/config/initializers/secret_token.rb +12 -0
- data/spec/fixtures/rails40/config/initializers/session_store.rb +3 -0
- data/spec/fixtures/rails40/config/initializers/wrap_parameters.rb +9 -0
- data/spec/fixtures/rails40/config/locales/en.yml +23 -0
- data/spec/fixtures/rails40/config/routes.rb +62 -0
- data/spec/fixtures/rails40/public/404.html +58 -0
- data/spec/fixtures/rails40/public/422.html +58 -0
- data/spec/fixtures/rails40/public/500.html +57 -0
- data/spec/fixtures/rails40/public/favicon.ico +0 -0
- data/spec/fixtures/rails40/public/robots.txt +5 -0
- data/spec/integration/rails/railtie_spec.rb +64 -0
- data/spec/integration/rails/request_spec.rb +42 -0
- data/spec/integration/sinatra_spec.rb +54 -0
- data/spec/spec_helper.rb +39 -0
- data/spec/support/helpers.rb +16 -0
- data/spec/support/request_spec.rb +183 -0
- data/spec/support/timecop_shared.rb +7 -0
- data/spec/unit/loga/configuration_spec.rb +123 -0
- data/spec/unit/loga/event_spec.rb +20 -0
- data/spec/unit/loga/formatter_spec.rb +186 -0
- data/spec/unit/loga/parameter_filter_spec.rb +76 -0
- data/spec/unit/loga/rack/logger_spec.rb +114 -0
- data/spec/unit/loga/rack/request_spec.rb +70 -0
- data/spec/unit/loga/utilities_spec.rb +16 -0
- data/spec/unit/loga_spec.rb +41 -0
- metadata +357 -0
data/lib/loga/event.rb
ADDED
@@ -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,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
|