loga 1.4.0 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +21 -9
- data/CHANGELOG.md +50 -0
- data/Guardfile +45 -0
- data/LICENSE.txt +29 -0
- data/README.md +149 -81
- data/circle.yml +5 -5
- data/lib/loga.rb +12 -8
- data/lib/loga/configuration.rb +97 -46
- data/lib/loga/event.rb +11 -0
- data/lib/loga/rack/logger.rb +21 -16
- data/lib/loga/rack/request.rb +18 -6
- data/lib/loga/railtie.rb +74 -33
- data/lib/loga/service_version_strategies.rb +19 -0
- data/lib/loga/version.rb +1 -1
- data/loga.gemspec +5 -1
- data/spec/fixtures/rails32/config/application.rb +6 -6
- data/spec/fixtures/rails40/config/application.rb +6 -6
- data/spec/fixtures/rails50/config/application.rb +6 -6
- data/spec/integration/rails/railtie_spec.rb +69 -61
- data/spec/integration/rails/request_spec.rb +17 -18
- data/spec/integration/sinatra_spec.rb +53 -14
- data/spec/support/request_spec.rb +11 -10
- data/spec/unit/loga/configuration_spec.rb +163 -57
- data/spec/unit/loga/event_spec.rb +31 -1
- data/spec/unit/loga/rack/logger_spec.rb +22 -10
- data/spec/unit/loga/rack/request_spec.rb +19 -26
- data/spec/unit/loga/service_version_strategies_spec.rb +37 -0
- data/spec/unit/loga_spec.rb +52 -15
- metadata +53 -8
- data/.rubocop_todo.yml +0 -33
- data/lib/loga/revision_strategy.rb +0 -32
- data/spec/unit/loga/revision_strategy_spec.rb +0 -41
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'active_support/core_ext/object/blank'
|
2
|
+
|
3
|
+
module Loga
|
4
|
+
class ServiceVersionStrategies
|
5
|
+
# Redirect stderror to /dev/null when git binary or git directory not available
|
6
|
+
SCM_GIT = -> { `git rev-parse --short HEAD 2>/dev/null` }
|
7
|
+
REVISION_FILE = -> { begin; File.read('REVISION'); rescue Errno::ENOENT; nil; end }
|
8
|
+
DEFAULT = -> { 'unknown.sha' }
|
9
|
+
STRATEGIES = [SCM_GIT, REVISION_FILE, DEFAULT].freeze
|
10
|
+
|
11
|
+
def self.call
|
12
|
+
new.call
|
13
|
+
end
|
14
|
+
|
15
|
+
def call
|
16
|
+
STRATEGIES.map(&:call).find(&:presence).strip
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
data/lib/loga/version.rb
CHANGED
data/loga.gemspec
CHANGED
@@ -10,6 +10,7 @@ Gem::Specification.new do |spec|
|
|
10
10
|
spec.email = ['engineering@fundingcircle.com']
|
11
11
|
spec.summary = 'Facilitate log aggregation via unified logging'
|
12
12
|
spec.description = 'Log aggregation through unified logging middleware, while respecting the original log format.'
|
13
|
+
spec.license = 'BSD-3-Clause'
|
13
14
|
spec.homepage = 'https://github.com/FundingCircle/loga'
|
14
15
|
|
15
16
|
spec.files = `git ls-files -z`.split("\x0")
|
@@ -25,7 +26,10 @@ Gem::Specification.new do |spec|
|
|
25
26
|
spec.add_development_dependency 'pry'
|
26
27
|
spec.add_development_dependency 'rack-test'
|
27
28
|
spec.add_development_dependency 'rake'
|
28
|
-
spec.add_development_dependency 'rspec', '~> 3.
|
29
|
+
spec.add_development_dependency 'rspec', '~> 3.5.0'
|
29
30
|
spec.add_development_dependency 'rubocop', '~> 0.40.0'
|
30
31
|
spec.add_development_dependency 'timecop'
|
32
|
+
spec.add_development_dependency 'guard', '~> 2.13'
|
33
|
+
spec.add_development_dependency 'guard-rubocop', '~> 1.2'
|
34
|
+
spec.add_development_dependency 'guard-rspec', '~> 4.7.3'
|
31
35
|
end
|
@@ -61,11 +61,11 @@ module Rails32
|
|
61
61
|
# parameters by using an attr_accessible or attr_protected declaration.
|
62
62
|
# config.active_record.whitelist_attributes = true
|
63
63
|
config.log_tags = [ :uuid, 'TEST_TAG' ]
|
64
|
-
config.loga
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
64
|
+
config.loga = {
|
65
|
+
device: STREAM,
|
66
|
+
host: 'bird.example.com',
|
67
|
+
service_name: 'hello_world_app',
|
68
|
+
service_version: '1.0',
|
69
|
+
}
|
70
70
|
end
|
71
71
|
end
|
@@ -27,11 +27,11 @@ module Rails40
|
|
27
27
|
# config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
|
28
28
|
# config.i18n.default_locale = :de
|
29
29
|
config.log_tags = [ :uuid, 'TEST_TAG' ]
|
30
|
-
config.loga
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
30
|
+
config.loga = {
|
31
|
+
device: STREAM,
|
32
|
+
host: 'bird.example.com',
|
33
|
+
service_name: 'hello_world_app',
|
34
|
+
service_version: '1.0',
|
35
|
+
}
|
36
36
|
end
|
37
37
|
end
|
@@ -24,11 +24,11 @@ module Rails50
|
|
24
24
|
# Application configuration should go into files in config/initializers
|
25
25
|
# -- all .rb files in that directory are automatically loaded.
|
26
26
|
config.log_tags = [ :request_id, 'TEST_TAG' ]
|
27
|
-
config.loga
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
27
|
+
config.loga = {
|
28
|
+
device: STREAM,
|
29
|
+
host: 'bird.example.com',
|
30
|
+
service_name: 'hello_world_app',
|
31
|
+
service_version: '1.0',
|
32
|
+
}
|
33
33
|
end
|
34
34
|
end
|
@@ -3,85 +3,93 @@ require 'ostruct'
|
|
3
3
|
RSpec.describe Loga::Railtie do
|
4
4
|
let(:app) { Rails.application }
|
5
5
|
let(:middlewares) { app.middleware.middlewares }
|
6
|
-
let(:initializers) { described_class.initializers }
|
7
6
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
context 'when loga is disabled' do
|
17
|
-
let(:loga) { Loga::Configuration.new.tap { |c| c.enabled = false } }
|
18
|
-
|
19
|
-
it 'is not initialized' do
|
20
|
-
expect(config.logger).to be_nil
|
7
|
+
context 'development', if: Rails.env.development? do
|
8
|
+
describe 'loga_initialize_logger' do
|
9
|
+
let(:formatter) do
|
10
|
+
if ActiveSupport::VERSION::MAJOR == 3
|
11
|
+
Logger::SimpleFormatter
|
12
|
+
else
|
13
|
+
ActiveSupport::Logger::SimpleFormatter
|
14
|
+
end
|
21
15
|
end
|
22
|
-
end
|
23
16
|
|
24
|
-
|
25
|
-
|
17
|
+
it 'assign Loga logger to Rails logger' do
|
18
|
+
expect(Loga.logger).to equal(Rails.logger)
|
19
|
+
end
|
26
20
|
|
27
|
-
it '
|
28
|
-
expect(
|
21
|
+
it 'configures Loga with a simple formatter' do
|
22
|
+
expect(Loga.configuration.logger.formatter).to be_a(formatter)
|
29
23
|
end
|
30
24
|
end
|
31
25
|
end
|
32
26
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
27
|
+
context 'production', if: Rails.env.production? do
|
28
|
+
describe 'loga_initialize_logger' do
|
29
|
+
it 'assign Loga logger to Rails logger' do
|
30
|
+
expect(Loga.logger).to equal(Rails.logger)
|
31
|
+
end
|
37
32
|
|
38
|
-
|
39
|
-
|
40
|
-
|
33
|
+
it 'configures Loga with a structured formatter' do
|
34
|
+
expect(Loga.configuration.logger.formatter).to be_a(Loga::Formatter)
|
35
|
+
end
|
41
36
|
|
42
|
-
|
43
|
-
|
44
|
-
|
37
|
+
it 'disables colorized logging' do
|
38
|
+
expect(app.config.colorize_logging).to eq(false)
|
39
|
+
end
|
45
40
|
end
|
46
|
-
|
47
|
-
|
41
|
+
|
42
|
+
describe 'loga_initialize_middleware' do
|
43
|
+
it 'inserts Loga::Rack::Logger middleware after Rails::Rack::Logger' do
|
44
|
+
expect(middlewares.index(Loga::Rack::Logger))
|
45
|
+
.to eq(middlewares.index(Rails::Rack::Logger) + 1)
|
46
|
+
end
|
48
47
|
end
|
49
48
|
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
49
|
+
describe 'instrumentation' do
|
50
|
+
let(:listeners) do
|
51
|
+
ActiveSupport::Notifications.notifier.listeners_for(notification)
|
52
|
+
end
|
53
|
+
let(:subscribers) do
|
54
|
+
listeners.map { |l| l.instance_variable_get(:@delegate).class }
|
55
|
+
end
|
57
56
|
|
58
|
-
|
59
|
-
|
57
|
+
context 'ActionView' do
|
58
|
+
[
|
59
|
+
'render_collection.action_view',
|
60
|
+
'render_partial.action_view',
|
61
|
+
'render_template.action_view',
|
62
|
+
].each do |notification|
|
63
|
+
let(:notification) { notification }
|
64
|
+
|
65
|
+
it 'removes ActionView::LogSubscriber' do
|
66
|
+
expect(subscribers).to_not include(ActionView::LogSubscriber)
|
67
|
+
end
|
60
68
|
end
|
61
69
|
end
|
62
|
-
end
|
63
70
|
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
71
|
+
context 'ActionController' do
|
72
|
+
[
|
73
|
+
'exist_fragment?.action_controller',
|
74
|
+
'expire_fragment.action_controller',
|
75
|
+
'expire_page.action_controller',
|
76
|
+
'halted_callback.action_controller',
|
77
|
+
'logger.action_controller',
|
78
|
+
'process_action.action_controller',
|
79
|
+
'read_fragment.action_controller',
|
80
|
+
'redirect_to.action_controller',
|
81
|
+
'send_data.action_controller',
|
82
|
+
'send_file.action_controller',
|
83
|
+
'start_processing.action_controller',
|
84
|
+
'unpermitted_parameters.action_controller',
|
85
|
+
'write_fragment.action_controller',
|
86
|
+
'write_page.action_controller',
|
87
|
+
].each do |notification|
|
88
|
+
let(:notification) { notification }
|
82
89
|
|
83
|
-
|
84
|
-
|
90
|
+
it 'removes ActionController::LogSubscriber' do
|
91
|
+
expect(subscribers).to_not include(ActionController::LogSubscriber)
|
92
|
+
end
|
85
93
|
end
|
86
94
|
end
|
87
95
|
end
|
@@ -1,23 +1,22 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
|
-
describe '
|
3
|
+
RSpec.describe 'Structured logging with Rails', timecop: true,
|
4
|
+
if: Rails.env.production? do
|
4
5
|
let(:app) { Rails.application }
|
5
6
|
|
6
|
-
let(:
|
7
|
-
[]
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
s.close
|
14
|
-
s.reopen
|
15
|
-
end
|
7
|
+
let(:log_entries) do
|
8
|
+
entries = []
|
9
|
+
STREAM.tap do |s|
|
10
|
+
s.rewind
|
11
|
+
entries = s.read.split("\n").map { |line| JSON.parse(line) }
|
12
|
+
s.close
|
13
|
+
s.reopen
|
16
14
|
end
|
15
|
+
entries
|
17
16
|
end
|
18
17
|
|
19
|
-
let(:
|
20
|
-
let(:json_response)
|
18
|
+
let(:last_log_entry) { log_entries.last }
|
19
|
+
let(:json_response) { JSON.parse(last_response.body) }
|
21
20
|
|
22
21
|
include_examples 'request logger'
|
23
22
|
|
@@ -28,13 +27,13 @@ describe 'Integration with Rails', timecop: true do
|
|
28
27
|
|
29
28
|
it 'includes the controller name and action' do
|
30
29
|
get '/ok'
|
31
|
-
expect(
|
30
|
+
expect(last_log_entry).to include('_request.controller' => 'ApplicationController#ok')
|
32
31
|
end
|
33
32
|
|
34
33
|
describe 'LogSubscriber' do
|
35
34
|
context 'ActionController' do
|
36
35
|
let(:action_controller_notifications) do
|
37
|
-
|
36
|
+
log_entries.select { |e| e.to_json =~ /Processing by|Completed/ }
|
38
37
|
end
|
39
38
|
|
40
39
|
it 'silences ActionController::LogSubscriber' do
|
@@ -45,7 +44,7 @@ describe 'Integration with Rails', timecop: true do
|
|
45
44
|
|
46
45
|
context 'ActionView' do
|
47
46
|
let(:action_view_notifications) do
|
48
|
-
|
47
|
+
log_entries.select { |e| e.to_json =~ /Rendered/ }
|
49
48
|
end
|
50
49
|
|
51
50
|
it 'silences ActionView::LogSubscriber' do
|
@@ -58,8 +57,8 @@ describe 'Integration with Rails', timecop: true do
|
|
58
57
|
describe 'when request causes ActionDispatch 404' do
|
59
58
|
it 'does not log ActionDispatch::DebugExceptions' do
|
60
59
|
get '/not_found', {}, 'HTTP_X_REQUEST_ID' => '471a34dc'
|
61
|
-
expect(
|
62
|
-
expect(
|
60
|
+
expect(log_entries.count).to eq(1)
|
61
|
+
expect(last_log_entry['short_message']).to eq('GET /not_found 404 in 0ms')
|
63
62
|
end
|
64
63
|
end
|
65
64
|
end
|
@@ -1,18 +1,20 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
|
-
describe '
|
3
|
+
RSpec.describe 'Structured logging with Sinatra', timecop: true do
|
4
4
|
let(:io) { StringIO.new }
|
5
|
+
let(:format) {}
|
5
6
|
before do
|
6
7
|
Loga.reset
|
7
|
-
Loga.configure
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
8
|
+
Loga.configure(
|
9
|
+
device: io,
|
10
|
+
filter_parameters: [:password],
|
11
|
+
format: format,
|
12
|
+
service_name: 'hello_world_app',
|
13
|
+
service_version: '1.0',
|
14
|
+
tags: [:uuid, 'TEST_TAG'],
|
15
|
+
)
|
14
16
|
end
|
15
|
-
let(:
|
17
|
+
let(:last_log_entry) do
|
16
18
|
io.rewind
|
17
19
|
JSON.parse(io.read)
|
18
20
|
end
|
@@ -24,7 +26,7 @@ describe 'Rack request logger with Sinatra', timecop: true do
|
|
24
26
|
set :show_exceptions, false
|
25
27
|
|
26
28
|
use Loga::Rack::RequestId
|
27
|
-
use Loga::Rack::Logger
|
29
|
+
use Loga::Rack::Logger
|
28
30
|
|
29
31
|
error do
|
30
32
|
status 500
|
@@ -50,10 +52,47 @@ describe 'Rack request logger with Sinatra', timecop: true do
|
|
50
52
|
end
|
51
53
|
end
|
52
54
|
|
53
|
-
|
55
|
+
context 'when RACK_ENV is production', if: ENV['RACK_ENV'].eql?('production') do
|
56
|
+
let(:format) { :gelf }
|
57
|
+
include_examples 'request logger'
|
58
|
+
|
59
|
+
it 'does not include the controller name and action' do
|
60
|
+
get '/ok'
|
61
|
+
expect(last_log_entry).to_not include('_request.controller')
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
context 'when RACK_ENV is production', if: ENV['RACK_ENV'].eql?('development') do
|
66
|
+
let(:format) { :simple }
|
67
|
+
let(:last_log_entry) do
|
68
|
+
io.rewind
|
69
|
+
io.read
|
70
|
+
end
|
71
|
+
|
72
|
+
context 'get request' do
|
73
|
+
it 'logs the request' do
|
74
|
+
get '/ok', username: 'yoshi'
|
75
|
+
expect(last_log_entry)
|
76
|
+
.to eq("#{time_anchor.iso8601(3)} GET /ok?username=yoshi 200 in 0ms\n")
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
context 'request with redirect' do
|
81
|
+
it 'specifies the original path' do
|
82
|
+
get '/new'
|
83
|
+
expect(last_log_entry).to eql("#{time_anchor.iso8601(3)} GET /new 302 in 0ms\n")
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
context 'when the request raises an exception' do
|
88
|
+
let(:log_entry_match) do
|
89
|
+
%r{GET /error 500 in 0ms.undefined method `name' for nil:NilClass..+sinatra_spec}m
|
90
|
+
end
|
54
91
|
|
55
|
-
|
56
|
-
|
57
|
-
|
92
|
+
it 'logs the request with the exception' do
|
93
|
+
get '/error'
|
94
|
+
expect(last_log_entry).to match(log_entry_match)
|
95
|
+
end
|
96
|
+
end
|
58
97
|
end
|
59
98
|
end
|
@@ -5,7 +5,7 @@ RSpec.shared_examples 'request logger' do
|
|
5
5
|
{ username: 'yoshi' },
|
6
6
|
'HTTP_USER_AGENT' => 'Chrome', 'HTTP_X_REQUEST_ID' => '471a34dc'
|
7
7
|
|
8
|
-
expect(
|
8
|
+
expect(last_log_entry).to include(
|
9
9
|
'version' => '1.1',
|
10
10
|
'host' => 'bird.example.com',
|
11
11
|
'short_message' => 'GET /ok?username=yoshi 200 in 0ms',
|
@@ -35,7 +35,7 @@ RSpec.shared_examples 'request logger' do
|
|
35
35
|
{ email: 'hello@world.com' },
|
36
36
|
'HTTP_USER_AGENT' => 'Chrome', 'HTTP_X_REQUEST_ID' => '471a34dc'
|
37
37
|
|
38
|
-
expect(
|
38
|
+
expect(last_log_entry).to include(
|
39
39
|
'version' => '1.1',
|
40
40
|
'host' => 'bird.example.com',
|
41
41
|
'short_message' => 'POST /users?username=yoshi 200 in 0ms',
|
@@ -58,7 +58,8 @@ RSpec.shared_examples 'request logger' do
|
|
58
58
|
|
59
59
|
it 'preseves request parameters' do
|
60
60
|
post '/users?username=yoshi', email: 'hello@world.com'
|
61
|
-
expect(json_response)
|
61
|
+
expect(json_response)
|
62
|
+
.to include('email' => 'hello@world.com', 'username' => 'yoshi')
|
62
63
|
end
|
63
64
|
end
|
64
65
|
|
@@ -66,7 +67,7 @@ RSpec.shared_examples 'request logger' do
|
|
66
67
|
it 'specifies the original path' do
|
67
68
|
get '/new', {}, 'HTTP_USER_AGENT' => 'Chrome', 'HTTP_X_REQUEST_ID' => '471a34dc'
|
68
69
|
|
69
|
-
expect(
|
70
|
+
expect(last_log_entry).to include(
|
70
71
|
'version' => '1.1',
|
71
72
|
'host' => 'bird.example.com',
|
72
73
|
'short_message' => 'GET /new 302 in 0ms',
|
@@ -94,7 +95,7 @@ RSpec.shared_examples 'request logger' do
|
|
94
95
|
{ username: 'yoshi' },
|
95
96
|
'HTTP_USER_AGENT' => 'Chrome', 'HTTP_X_REQUEST_ID' => '471a34dc'
|
96
97
|
|
97
|
-
expect(
|
98
|
+
expect(last_log_entry).to include(
|
98
99
|
'version' => '1.1',
|
99
100
|
'host' => 'bird.example.com',
|
100
101
|
'short_message' => 'GET /error?username=yoshi 500 in 0ms',
|
@@ -123,7 +124,7 @@ RSpec.shared_examples 'request logger' do
|
|
123
124
|
it 'does not log the framework exception' do
|
124
125
|
get '/not_found', {}, 'HTTP_X_REQUEST_ID' => '471a34dc'
|
125
126
|
|
126
|
-
expect(
|
127
|
+
expect(last_log_entry).to include(
|
127
128
|
'version' => '1.1',
|
128
129
|
'host' => 'bird.example.com',
|
129
130
|
'short_message' => 'GET /not_found 404 in 0ms',
|
@@ -152,13 +153,13 @@ RSpec.shared_examples 'request logger' do
|
|
152
153
|
let(:params) { { password: 'password123' } }
|
153
154
|
|
154
155
|
it 'filters the parameter from the params hash' do
|
155
|
-
expect(
|
156
|
+
expect(last_log_entry).to include(
|
156
157
|
'_request.params' => { 'password' => '[FILTERED]' },
|
157
158
|
)
|
158
159
|
end
|
159
160
|
|
160
161
|
it 'filters the parameter from the message' do
|
161
|
-
expect(
|
162
|
+
expect(last_log_entry).to include(
|
162
163
|
'short_message' => 'GET /ok?password=[FILTERED] 200 in 0ms',
|
163
164
|
)
|
164
165
|
end
|
@@ -168,13 +169,13 @@ RSpec.shared_examples 'request logger' do
|
|
168
169
|
let(:params) { { users: [password: 'password123'] } }
|
169
170
|
|
170
171
|
it 'filters the parameter from the params hash' do
|
171
|
-
expect(
|
172
|
+
expect(last_log_entry).to include(
|
172
173
|
'_request.params' => { 'users' => ['password' => '[FILTERED]'] },
|
173
174
|
)
|
174
175
|
end
|
175
176
|
|
176
177
|
it 'filters the parameter from the message' do
|
177
|
-
expect(
|
178
|
+
expect(last_log_entry).to include(
|
178
179
|
'short_message' => 'GET /ok?users[][password]=[FILTERED] 200 in 0ms',
|
179
180
|
)
|
180
181
|
end
|