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,7 @@
1
+ require 'timecop'
2
+
3
+ shared_context 'timecop', timecop: true do
4
+ # Allows fixed timestamps
5
+ before(:all) { Timecop.freeze(time_anchor) }
6
+ after(:all) { Timecop.return }
7
+ end
@@ -0,0 +1,123 @@
1
+ require 'spec_helper'
2
+
3
+ describe Loga::Configuration do
4
+ subject do
5
+ described_class.new.tap { |config| config.device = STDOUT }
6
+ end
7
+
8
+ describe 'initialize' do
9
+ subject { described_class.new }
10
+ context 'defaults' do
11
+ specify { expect(subject.host).to eq(hostname_anchor) }
12
+ specify { expect(subject.level).to eq(:info) }
13
+ specify { expect(subject.device).to eq(nil) }
14
+ specify { expect(subject.sync).to eq(true) }
15
+ specify { expect(subject.filter_parameters).to eq([]) }
16
+ specify { expect(subject.service_name).to eq(nil) }
17
+ specify { expect(subject.service_version).to eq(:git) }
18
+ end
19
+
20
+ context 'when hostname cannot be resolved' do
21
+ before do
22
+ allow(Socket).to receive(:gethostname).and_raise(Exception)
23
+ end
24
+
25
+ it 'uses a default hostname' do
26
+ expect(subject.host).to eq('unknown.host')
27
+ end
28
+ end
29
+ end
30
+
31
+ describe '#initialize!' do
32
+ before do
33
+ subject.tap do |config|
34
+ config.service_name = ' hello_world_app '
35
+ config.service_version = " 1.0\n"
36
+ end
37
+ end
38
+
39
+ it 'initializes the formatter with stiped service name and version' do
40
+ expect(Loga::Formatter).to receive(:new)
41
+ .with(service_name: 'hello_world_app',
42
+ service_version: '1.0',
43
+ host: hostname_anchor)
44
+ subject.initialize!
45
+ end
46
+
47
+ context 'when service_version is :git' do
48
+ before { subject.service_version = :git }
49
+ it 'computes the service_version with git' do
50
+ subject.initialize!
51
+ expect(subject.service_version).to match(/\h+/)
52
+ end
53
+ end
54
+
55
+ describe 'logger' do
56
+ let(:logdev) { subject.logger.instance_variable_get(:@logdev) }
57
+
58
+ context 'when device is nil' do
59
+ before do
60
+ subject.device = nil
61
+ allow(STDERR).to receive(:write)
62
+ end
63
+ let(:error_message) { /Loga could not be initialized/ }
64
+ it 'uses STDERR' do
65
+ subject.initialize!
66
+ expect(logdev.dev).to eq(STDERR)
67
+ end
68
+ it 'logs an error to STDERR' do
69
+ expect(STDERR).to receive(:write).with(error_message)
70
+ subject.initialize!
71
+ end
72
+ end
73
+
74
+ {
75
+ debug: 0,
76
+ info: 1,
77
+ warn: 2,
78
+ error: 3,
79
+ fatal: 4,
80
+ unknown: 5,
81
+ }.each do |sym, level|
82
+ context "when log level is #{sym}" do
83
+ before { subject.level = sym }
84
+ it "uses log level #{sym}" do
85
+ subject.initialize!
86
+ expect(subject.logger.level).to eq(level)
87
+ end
88
+ end
89
+ end
90
+
91
+ context 'when sync is false' do
92
+ before { subject.sync = false }
93
+ it 'uses warn log level' do
94
+ subject.initialize!
95
+ expect(logdev.dev.sync).to eq(false)
96
+ end
97
+ end
98
+ end
99
+ end
100
+
101
+ describe '#logger' do
102
+ context 'when initialized' do
103
+ before { subject.initialize! }
104
+ it 'returns a logger' do
105
+ expect(subject.logger).to be_a(Logger)
106
+ end
107
+
108
+ it 'returns a tagged logger' do
109
+ expect(subject.logger).to respond_to(:tagged)
110
+ end
111
+ end
112
+
113
+ context 'when not initialized' do
114
+ specify { expect(subject.logger).to be_nil }
115
+ end
116
+ end
117
+
118
+ describe '#configure' do
119
+ it 'yields self' do
120
+ expect { |b| subject.configure(&b) }.to yield_with_args(subject)
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,20 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe Loga::Event do
4
+ describe 'initialize' do
5
+ context 'no message is passed' do
6
+ it 'sets message to an empty string' do
7
+ expect(subject.message).to eq ''
8
+ end
9
+ end
10
+
11
+ context 'message is passed' do
12
+ let(:message) { "stuff \xC2".force_encoding 'ASCII-8BIT' }
13
+ let(:subject) { described_class.new message: message }
14
+
15
+ it 'sanitizes the input to be UTF-8 convertable' do
16
+ expect(subject.message.to_json).to eq '"stuff ?"'
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,186 @@
1
+ require 'spec_helper'
2
+
3
+ describe Loga::Formatter do
4
+ let(:service_name) { 'loga' }
5
+ let(:service_version) { '725e032a' }
6
+ let(:host) { 'www.example.com' }
7
+ let(:params) do
8
+ {
9
+ service_name: service_name,
10
+ service_version: service_version,
11
+ host: host,
12
+ }
13
+ end
14
+
15
+ subject { described_class.new(params) }
16
+
17
+ shared_examples 'valid GELF message' do
18
+ it 'includes the required fields' do
19
+ expect(json).to include('version' => '1.1',
20
+ 'host' => host,
21
+ 'short_message' => be_a(String),
22
+ 'timestamp' => be_a(Float),
23
+ 'level' => 6,
24
+ )
25
+ end
26
+
27
+ it 'includes Loga additional fields' do
28
+ expect(json).to include('_service.name' => service_name,
29
+ '_service.version' => service_version,
30
+ '_tags' => [],
31
+ )
32
+ end
33
+
34
+ it 'outputs the timestamp in seconds since UNIX epoch' do
35
+ expect(json).to include('timestamp' => time_anchor_unix)
36
+ end
37
+ end
38
+
39
+ describe '#call(severity, time, _progname, message)' do
40
+ subject { super().call(severity, time_anchor, nil, message) }
41
+
42
+ let(:severity) { 'INFO' }
43
+ let(:message) { 'Tree house magic' }
44
+ let(:json) { JSON.parse(subject) }
45
+
46
+ context 'when the message parameter is a String' do
47
+ it 'the short_message is that String' do
48
+ expect(json['short_message']).to eq(message)
49
+ end
50
+
51
+ include_examples 'valid GELF message'
52
+ end
53
+
54
+ context 'when the message parameter is a nil' do
55
+ let(:message) { nil }
56
+ it 'the short_message is empty' do
57
+ expect(json['short_message']).to eq('')
58
+ end
59
+
60
+ include_examples 'valid GELF message'
61
+ end
62
+
63
+ context 'when message parameter is a Hash' do
64
+ let(:message) { { message: 'Wooden house' } }
65
+
66
+ it 'the short_message is a String reprentation of that Hash' do
67
+ expect(json['short_message']).to eq('{:message=>"Wooden house"}')
68
+ end
69
+
70
+ include_examples 'valid GELF message'
71
+ end
72
+
73
+ context 'when message parameter is an Object' do
74
+ let(:message) { Object.new }
75
+
76
+ it 'the short_message is a String reprentation of that Object' do
77
+ expect(json['short_message']).to match(/#<Object:\dx\h+>/)
78
+ end
79
+
80
+ include_examples 'valid GELF message'
81
+ end
82
+
83
+ context 'when the message parameter is a Loga::Event' do
84
+ let(:options) { { message: 'Wooden house' } }
85
+ let(:message) { Loga::Event.new(options) }
86
+
87
+ include_examples 'valid GELF message'
88
+
89
+ it 'the short_message is the Event message' do
90
+ expect(json['short_message']).to eq(message.message)
91
+ end
92
+
93
+ context 'when the Event has a timestamp' do
94
+ let(:time) { Time.new(2010, 12, 15, 9, 30, 5.323, '+02:00') }
95
+ let(:time_in_unix) { BigDecimal.new('1292398205.323') }
96
+ let(:options) { { timestamp: time } }
97
+
98
+ it 'uses the Event timestamp' do
99
+ expect(json['timestamp']).to eq(time_in_unix)
100
+ end
101
+ end
102
+
103
+ context 'when the Event has a type' do
104
+ let(:options) { { type: 'request' } }
105
+
106
+ specify { expect(json['_type']).to eq(message.type) }
107
+ end
108
+
109
+ context 'when the Event no type' do
110
+ specify { expect(json).to_not include('_type') }
111
+ end
112
+
113
+ context 'when the Event has an exception' do
114
+ let(:backtrace) { %w(a b) }
115
+ let(:exception) do
116
+ StandardError.new('Foo Error').tap { |e| e.set_backtrace backtrace }
117
+ end
118
+ let(:options) { { exception: exception } }
119
+
120
+ specify { expect(json['_exception.klass']).to eq('StandardError') }
121
+ specify { expect(json['_exception.message']).to eq('Foo Error') }
122
+ specify { expect(json['_exception.backtrace']).to eq("a\nb") }
123
+
124
+ context 'when the backtrace is larger than 10 lines' do
125
+ let(:backtrace) { ('a'..'z').to_a }
126
+ it 'truncates the backtrace' do
127
+ expect(json['_exception.backtrace']).to eq("a\nb\nc\nd\ne\nf\ng\nh\ni\nj")
128
+ end
129
+ end
130
+ end
131
+
132
+ context 'when the Event has no exception' do
133
+ specify { expect(json).to_not include(/_exception.+/) }
134
+ end
135
+
136
+ context 'when the Event has data' do
137
+ let(:options) do
138
+ {
139
+ data: {
140
+ user_id: 1,
141
+ user: {
142
+ email: 'hello@world.com',
143
+ address: {
144
+ postcode: 'ABCD',
145
+ },
146
+ },
147
+ },
148
+ }
149
+ end
150
+
151
+ specify { expect(json['_user_id']).to eq(1) }
152
+ specify { expect(json['_user.email']).to eq('hello@world.com') }
153
+ specify { expect(json['_user.address']).to eq('postcode' => 'ABCD') }
154
+ end
155
+
156
+ context 'when the Event data contains fiels identical to the formatter fields' do
157
+ let(:options) do
158
+ {
159
+ data: {
160
+ service: { name: 'Malicious Tags' },
161
+ },
162
+ }
163
+ end
164
+
165
+ include_examples 'valid GELF message'
166
+ end
167
+ end
168
+
169
+ {
170
+ 'DEBUG' => 7,
171
+ 'INFO' => 6,
172
+ 'WARN' => 4,
173
+ 'ERROR' => 3,
174
+ 'FATAL' => 2,
175
+ 'UNKNOWN' => 1,
176
+ }.each do |ruby_severity, syslog_level|
177
+ context "with severity #{ruby_severity}" do
178
+ let(:severity) { ruby_severity }
179
+
180
+ it "maps to level #{syslog_level}" do
181
+ expect(json['level']).to eq(syslog_level)
182
+ end
183
+ end
184
+ end
185
+ end
186
+ end
@@ -0,0 +1,76 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe Loga::ParameterFilter do
4
+ let(:filters) { [:password, /token/] }
5
+
6
+ shared_examples 'compiled filter' do
7
+ let(:compiled_filters) { described_class::CompiledFilter }
8
+
9
+ before do
10
+ allow(compiled_filters).to receive(:compile).and_call_original
11
+ end
12
+
13
+ it 'compiles filters only once' do
14
+ expect(compiled_filters).to receive(:compile).once
15
+ 2.times { subject.filter(params) }
16
+ end
17
+ end
18
+
19
+ describe '#filter(params)' do
20
+ subject { described_class.new(filters) }
21
+
22
+ let(:params) do
23
+ {
24
+ password: 'password123',
25
+ email: 'hello@world.com',
26
+ token: 'ABC',
27
+ }
28
+ end
29
+
30
+ let(:result) do
31
+ {
32
+ password: '[FILTERED]',
33
+ email: 'hello@world.com',
34
+ token: '[FILTERED]',
35
+ }
36
+ end
37
+
38
+ context 'when no filters are applied' do
39
+ let(:filters) { [] }
40
+
41
+ it 'returns params' do
42
+ expect(subject.filter(params)).to match(params)
43
+ end
44
+
45
+ include_examples 'compiled filter'
46
+ end
47
+
48
+ context 'when params is shallow' do
49
+ it 'returns filtered params' do
50
+ expect(subject.filter(params)).to match(result)
51
+ end
52
+
53
+ include_examples 'compiled filter'
54
+ end
55
+
56
+ context 'when params has a nested Hash' do
57
+ let(:params) { { user: super() } }
58
+
59
+ it 'returns filtered params' do
60
+ expect(subject.filter(params)).to match(user: result)
61
+ end
62
+
63
+ include_examples 'compiled filter'
64
+ end
65
+
66
+ context 'when params has a nested Array' do
67
+ let(:params) { { users: [super(), super()] } }
68
+
69
+ it 'returns filtered params' do
70
+ expect(subject.filter(params)).to match(users: [result, result])
71
+ end
72
+
73
+ include_examples 'compiled filter'
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,114 @@
1
+ require 'spec_helper'
2
+ require 'rack/test'
3
+
4
+ describe Loga::Rack::Logger do
5
+ let(:env) { Rack::MockRequest.env_for('/about_us?limit=1', options) }
6
+ let(:options) { {} }
7
+ let(:app) { ->(_env) { [response_status, {}, ''] } }
8
+ let(:logger) { double(:logger) }
9
+
10
+ subject { described_class.new(app, logger) }
11
+
12
+ before do
13
+ allow(logger).to receive(:info)
14
+ allow(logger).to receive(:error)
15
+ end
16
+
17
+ shared_examples 'logs the event' do |details|
18
+ let(:level) { details[:level] }
19
+
20
+ before do
21
+ allow(subject).to receive(:started_at).and_return(:timestamp)
22
+ allow(subject).to receive(:duration_in_ms).with(any_args).and_return(5)
23
+ end
24
+
25
+ it 'instantiates a Loga::Event' do
26
+ expect(Loga::Event).to receive(:new).with(
27
+ data: {
28
+ request: {
29
+ 'status' => response_status,
30
+ 'method' => 'GET',
31
+ 'path' => '/about_us',
32
+ 'params' => { 'limit' => '1' },
33
+ 'request_id' => nil,
34
+ 'request_ip' => nil,
35
+ 'user_agent' => nil,
36
+ 'duration' => 5,
37
+ },
38
+ },
39
+ exception: logged_exception,
40
+ message: %r{^GET \/about_us\?limit=1 #{response_status} in \d+ms$},
41
+ timestamp: :timestamp,
42
+ type: 'request',
43
+ )
44
+
45
+ subject.call(env)
46
+ end
47
+
48
+ it "logs the Loga::Event with severity #{details[:level]}" do
49
+ expect(logger).to receive(level).with(an_instance_of(Loga::Event))
50
+ subject.call(env)
51
+ end
52
+ end
53
+
54
+ describe '#call(env)' do
55
+ let(:exception) { StandardError.new }
56
+ let(:logged_exception) { nil }
57
+ let(:response_status) { 200 }
58
+
59
+ context 'when an exception is raised' do
60
+ let(:app) { ->(_env) { fail exception } }
61
+
62
+ it 'does not rescue the exception' do
63
+ expect { subject.call(env) }.to raise_error(StandardError)
64
+ end
65
+ end
66
+
67
+ context 'when an exception wrapped by ActionDispatch' do
68
+ let(:response_status) { 500 }
69
+ let(:logged_exception) { exception }
70
+ let(:options) { { 'action_dispatch.exception' => exception } }
71
+
72
+ include_examples 'logs the event', level: :error
73
+ end
74
+
75
+ context 'when an exception wrapped by Sinatra' do
76
+ let(:response_status) { 500 }
77
+ let(:logged_exception) { exception }
78
+ let(:options) { { 'sinatra.error' => exception } }
79
+
80
+ include_examples 'logs the event', level: :error
81
+ end
82
+
83
+ context 'when the exception is ActionController::RoutingError' do
84
+ let(:response_status) { 404 }
85
+ let(:exception) { double(class: 'ActionController::RoutingError') }
86
+ let(:options) { { 'action_dispatch.exception' => exception } }
87
+
88
+ include_examples 'logs the event', level: :info
89
+ end
90
+
91
+ context 'when no exception is raised' do
92
+ include_examples 'logs the event', level: :info
93
+ end
94
+
95
+ context 'when the logger is tagged' do
96
+ let(:logger) { double(:logger, tagged: true) }
97
+
98
+ before do
99
+ allow(subject).to receive(:call_app).with(any_args).and_return(:response)
100
+ allow(subject).to receive(:compute_tags).with(any_args).and_return(:tag)
101
+ allow(logger).to receive(:tagged).with('hello') do |&block|
102
+ block.call
103
+ end
104
+ end
105
+
106
+ it 'yields the app with tags' do
107
+ expect(logger).to receive(:tagged).with(:tag) do |&block|
108
+ expect(block.call).to eq(:response)
109
+ end
110
+ subject.call(env)
111
+ end
112
+ end
113
+ end
114
+ end