loga 1.4.0 → 2.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.
- 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
|