loga 1.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.
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