rackstash 0.0.1 → 0.1.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.
@@ -1,3 +1,3 @@
1
1
  module Rackstash
2
- VERSION = "0.0.1"
2
+ VERSION = "0.1.0"
3
3
  end
@@ -0,0 +1,9 @@
1
+ desc "Execute a sub-task in a rackstash log scope"
2
+ task :with_rackstash, [:task] do |t, args|
3
+ Rake::Task[:environment].invoke if Rake::Task[:environment]
4
+
5
+ Rackstash.tags |= ["rake", "rake::#{args[:task]}"]
6
+ Rackstash.with_log_buffer do
7
+ Rake::Task[args[:task]].invoke
8
+ end
9
+ end
@@ -1,35 +1,35 @@
1
- # coding: utf-8
2
- # Copyright 2016 Holger Just
3
- #
4
- # This software may be modified and distributed under the terms
5
- # of the MIT license. See the LICENSE.txt file for details.
6
-
1
+ # -*- encoding: utf-8 -*-
7
2
  lib = File.expand_path('../lib', __FILE__)
8
3
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
9
4
  require 'rackstash/version'
10
5
 
11
- Gem::Specification.new do |spec|
12
- spec.name = 'rackstash'
13
- spec.version = Rackstash::VERSION
14
- spec.authors = ['Holger Just']
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "rackstash"
8
+ gem.version = Rackstash::VERSION
9
+ gem.authors = ["Holger Just, Planio GmbH"]
10
+ gem.email = ["holger@plan.io"]
11
+ gem.description = %q{Making Rack and Rails logs useful}
12
+ gem.summary = %q{Making Rack and Rails logs useful}
13
+ gem.homepage = "https://github.com/planio-gmbh/rackstash"
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ["lib"]
15
19
 
16
- spec.summary = 'This is a placeholder for now'
17
- spec.description = <<-TXT.tap { |s| s.gsub!(/\s+|\n/, ' ').strip! }
18
- This is just a placeholder for now. Please have a look at
19
- https://github.com/meineerde/rackstash for any progress on releasing this
20
- gem.
21
- TXT
22
- spec.homepage = 'https://github.com/meineerde/rackstash'
23
- spec.license = 'MIT'
20
+ gem.add_runtime_dependency "logstash-event", "< 1.2.0"
21
+ gem.add_runtime_dependency "activesupport"
22
+ gem.add_runtime_dependency "thor"
24
23
 
25
- spec.required_ruby_version = '>= 2.1.0'
24
+ gem.add_development_dependency "minitest"
25
+ gem.add_development_dependency "rake"
26
+ gem.add_development_dependency "json"
26
27
 
27
- files = `git ls-files -z`.split("\x0")
28
- spec.files = files.reject { |f| f.match(%r{^(test|spec|feature)/}) }
29
- spec.bindir = 'exe'
30
- spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
31
- spec.require_paths = ['lib']
28
+ if RbConfig::CONFIG["RUBY_INSTALL_NAME"] == "rbx"
29
+ gem.add_runtime_dependency "rubysl", "~> 2.0"
32
30
 
33
- spec.add_development_dependency 'bundler', '~> 1.11'
34
- spec.add_development_dependency 'rake', '~> 10.0'
31
+ gem.add_development_dependency 'rubinius-coverage', '~> 2.0'
32
+ gem.add_development_dependency 'parser', '~> 2.1.0.pre1'
33
+ gem.add_development_dependency 'racc', '~> 1.4'
34
+ end
35
35
  end
@@ -0,0 +1,191 @@
1
+ require 'test_helper'
2
+ require 'rackstash/buffered_logger'
3
+
4
+ require 'stringio'
5
+ require 'json'
6
+
7
+ describe Rackstash::BufferedLogger do
8
+ let(:log_output){ StringIO.new }
9
+ let(:base_logger){ Logger.new(log_output) }
10
+
11
+ def log_line
12
+ log_output.string.lines.to_a.last
13
+ end
14
+ def json
15
+ JSON.parse(log_line)
16
+ end
17
+
18
+ subject do
19
+ Rackstash::BufferedLogger.new(base_logger)
20
+ end
21
+
22
+ it "must be properly initialized" do
23
+ subject.logger.formatter.must_be_instance_of Rackstash::BufferedLogger::SimpleFormatter
24
+ subject.buffering?.must_equal false
25
+ subject.fields.must_equal nil
26
+ subject.tags.must_equal nil
27
+ subject.source.must_equal nil
28
+ end
29
+
30
+ describe "when passing a logger" do
31
+ it "delegates only defined methods" do
32
+ # sanity
33
+ base_logger.wont_respond_to :flush
34
+ base_logger.wont_respond_to :auto_flushing
35
+
36
+ base_logger.instance_eval{ def flush; end }
37
+
38
+ base_logger.must_respond_to :flush
39
+ base_logger.wont_respond_to :auto_flushing
40
+ subject.must_respond_to :flush
41
+ subject.wont_respond_to :auto_flushing
42
+ end
43
+
44
+ it "delegates later methods too" do
45
+ base_logger.wont_respond_to :auto_flushing # sanity
46
+ base_logger.instance_eval{ def auto_flushing; end }
47
+
48
+ base_logger.must_respond_to :auto_flushing
49
+ subject.must_respond_to :auto_flushing
50
+ end
51
+ end
52
+
53
+ describe "when using the Logger API" do
54
+ it "forwards base methods to the underlying logger" do
55
+ subject.logger.must_be_same_as base_logger
56
+
57
+ subject.level.must_equal base_logger.level
58
+ subject.progname.must_be_same_as base_logger.progname
59
+ end
60
+ end
61
+
62
+ describe "when logging unbuffered" do
63
+ it "supports adding log messages" do
64
+ subject.add nil, "log_empty"
65
+ json["@message"].must_equal "[UNKNOWN] log_empty"
66
+
67
+ %w[debug info warn error fatal unknown].each do |severity|
68
+ subject.send severity, "log_#{severity}"
69
+
70
+ tag = "[#{severity.upcase}] ".rjust(10)
71
+ json["@message"].must_equal "#{tag}log_#{severity}"
72
+ end
73
+ end
74
+
75
+ it "supports the << method" do
76
+ subject << "Hello World"
77
+ log_output.string.must_equal "Hello World"
78
+ end
79
+
80
+ it "ignores the instruction to not log a message" do
81
+ subject.do_not_log!.must_equal false
82
+ subject.info "Hello World"
83
+ json["@message"].must_equal " [INFO] Hello World"
84
+ end
85
+
86
+ it "includes the default fields" do
87
+ subject.info "Foo Bar Baz"
88
+
89
+ json = self.json
90
+ json.keys.sort.must_equal %w[@fields @message @source @tags @timestamp]
91
+ json["@fields"].keys.sort.must_equal %w[log_id pid]
92
+
93
+ json["@fields"]["pid"].must_equal Process.pid
94
+ json["@fields"]["log_id"].must_match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/)
95
+ json["@message"].must_equal " [INFO] Foo Bar Baz"
96
+ json["@source"].must_be_nil
97
+ json["@tags"].must_equal []
98
+ # Timestamp is less than 2 seconds ago
99
+ timestamp_range = ((Time.now-2)..Time.now)
100
+ method = timestamp_range.respond_to?(:cover?) ? :cover? : :===
101
+ timestamp_range.must_be method, Time.parse(json["@timestamp"])
102
+ end
103
+
104
+ it "allows to log nil" do
105
+ subject.info nil
106
+ json["@message"].must_equal " [INFO] "
107
+ end
108
+
109
+ it "allows to log numerics" do
110
+ subject.info 12.123
111
+ json["@message"].must_equal " [INFO] 12.123"
112
+ end
113
+
114
+ it "allows to set a source" do
115
+ subject.source = "BufferedLoggerTest"
116
+ subject.info nil
117
+ json["@source"].must_equal "BufferedLoggerTest"
118
+ end
119
+ end
120
+
121
+ describe "when using a buffer" do
122
+ it "should buffer logs" do
123
+ subject.with_buffer do
124
+ subject.info("Hello")
125
+ log_output.string.must_be_empty
126
+ subject.info("World")
127
+ end
128
+
129
+ json["@message"].must_equal " [INFO] Hello\n [INFO] World"
130
+ end
131
+
132
+ it "can set additional tags" do
133
+ subject.with_buffer do
134
+ subject.tags << :foo
135
+ subject.info("Hello")
136
+ end
137
+
138
+ json["@tags"].must_equal ["foo"]
139
+ json["@message"].must_equal " [INFO] Hello"
140
+ end
141
+
142
+ it "can set additional fields" do
143
+ subject.with_buffer do
144
+ subject.fields[:foo] = :bar
145
+ subject.info("Hello")
146
+ end
147
+
148
+ json["@fields"]["foo"].must_equal "bar"
149
+ json["@message"].must_equal " [INFO] Hello"
150
+ end
151
+
152
+ it "can overwrite automatically filled fields" do
153
+ subject.with_buffer do
154
+ subject.fields[:pid] = "foobarbaz"
155
+ subject.info("Hello")
156
+ end
157
+
158
+ json["@fields"]["pid"].must_equal "foobarbaz"
159
+ json["@message"].must_equal " [INFO] Hello"
160
+ end
161
+
162
+ it "captures exceptions" do
163
+ exception = Class.new(StandardError) do
164
+ def self.name
165
+ "SomethingWrongError"
166
+ end
167
+ end
168
+
169
+ proc do
170
+ subject.with_buffer do
171
+ raise exception, "Something is wrong"
172
+ end
173
+ end.must_raise(exception)
174
+
175
+ json["@message"].must_equal ""
176
+ json["@fields"]["error"].must_equal "SomethingWrongError"
177
+ json["@fields"]["error_message"].must_equal "Something is wrong"
178
+ json["@fields"]["error_backtrace"].must_match(/\A#{__FILE__}:\d+/)
179
+ json["@fields"]["error_backtrace"].must_match(/^#{File.expand_path("../../lib/rackstash/buffered_logger.rb", __FILE__)}:\d+:in `with_buffer'$/)
180
+ end
181
+
182
+ it "doesn't log anything when using do_not_log!" do
183
+ subject.with_buffer do
184
+ subject.do_not_log!.must_equal true
185
+ subject.info "Hello World"
186
+ end
187
+
188
+ log_line.must_be :nil?
189
+ end
190
+ end
191
+ end
@@ -0,0 +1,89 @@
1
+ require 'test_helper'
2
+ require 'rackstash'
3
+
4
+ if Rackstash.framework == "rails3"
5
+ require 'rackstash/log_subscriber'
6
+ require 'active_support/notifications'
7
+
8
+ describe Rackstash::LogSubscriber do
9
+ let(:log_output){ StringIO.new }
10
+ let(:base_logger){ Logger.new(log_output) }
11
+ let(:logger){ Rackstash::BufferedLogger.new(base_logger) }
12
+
13
+ let(:flush_and_pop_buffer){ logger.flush_and_pop_buffer }
14
+ let(:json) do
15
+ flush_and_pop_buffer
16
+
17
+ JSON.parse(log_output.string.lines.to_a.last)
18
+ end
19
+
20
+ let(:subscriber){ Rackstash::LogSubscriber.new }
21
+ let(:event) do
22
+ now = Time.now
23
+ duration = 123 # milliseconds
24
+ ActiveSupport::Notifications::Event.new(
25
+ "process_action.action_controller", now, now + (duration.to_f / 1000), 2, {
26
+ :status => 200, :format => "application/json", :method => "GET", :path => "/home?foo=bar",
27
+ :params => {
28
+ "controller" => "home", "action" => "index", "foo" => "bar"
29
+ }, :db_runtime => 0.02, :view_runtime => 0.01
30
+ }
31
+ )
32
+ end
33
+
34
+ let(:redirect) do
35
+ ActiveSupport::Notifications::Event.new(
36
+ 'redirect_to.action_controller', Time.now, Time.now, 1, :location => 'http://example.com', :status => 302
37
+ )
38
+ end
39
+
40
+ before do
41
+ @original_rails_logger = Rails.logger
42
+ Rails.logger = logger
43
+ logger.push_buffer
44
+ end
45
+ after do
46
+ flush_and_pop_buffer
47
+ Rails.logger = @original_rails_logger
48
+ end
49
+
50
+ describe "#redirect_to" do
51
+ it "should store the location in a thread local variable" do
52
+ subscriber.redirect_to(redirect)
53
+ Thread.current[:rackstash_location].must_equal "http://example.com"
54
+ end
55
+ end
56
+
57
+ describe "#process_action" do
58
+ it "includes the controller and action" do
59
+ subscriber.process_action(event)
60
+ json["@fields"]["controller"].must_equal "home"
61
+ json["@fields"]["action"].must_equal "index"
62
+ end
63
+
64
+ it "includes the duration" do
65
+ subscriber.process_action(event)
66
+ json["@fields"]["duration"].must_be_within_epsilon 123
67
+ end
68
+
69
+ it "should include the view rendering time" do
70
+ subscriber.process_action(event)
71
+ json["@fields"]["view"].must_be_within_epsilon 0.01
72
+ end
73
+
74
+ it "should include the database rendering time" do
75
+ subscriber.process_action(event)
76
+ json["@fields"]["db"].must_be_within_epsilon 0.02
77
+ end
78
+
79
+ it "should add a 500 status when an exception occurred" do
80
+ event.payload[:status] = nil
81
+ event.payload[:exception] = ['AbstractController::ActionNotFound', 'Route not found']
82
+ subscriber.process_action(event)
83
+
84
+ json["@fields"]["error"].must_equal "AbstractController::ActionNotFound"
85
+ json["@fields"]["error_message"].must_equal "Route not found"
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,104 @@
1
+ require 'test_helper'
2
+ require 'rackstash'
3
+
4
+ describe Rackstash do
5
+ let(:log_output){ StringIO.new }
6
+ let(:base_logger){ Logger.new(log_output) }
7
+ def json
8
+ JSON.parse(log_output.string.lines.to_a.last)
9
+ end
10
+
11
+ subject do
12
+ Rackstash::BufferedLogger.new(base_logger)
13
+ end
14
+
15
+ describe "fields" do
16
+ after do
17
+ Rackstash.fields = HashWithIndifferentAccess.new
18
+ end
19
+
20
+ it "can be defined as a Hash" do
21
+ Rackstash.fields = {:foo => :bar}
22
+ Rackstash.fields.must_be_instance_of HashWithIndifferentAccess
23
+
24
+ subject.info("foo")
25
+ json["@fields"]["foo"].must_equal "bar"
26
+ end
27
+
28
+ it "can be defined as a proc" do
29
+ Rackstash.fields = proc do
30
+ {:foo => :baz}
31
+ end
32
+
33
+ subject.info("foo")
34
+ json["@fields"]["foo"].must_equal "baz"
35
+ end
36
+
37
+ it "can be defined as a lambda" do
38
+ Rackstash.fields = lambda do
39
+ {:foo => :baz}
40
+ end
41
+
42
+ subject.info("foo")
43
+ json["@fields"]["foo"].must_equal "baz"
44
+ end
45
+ end
46
+
47
+ describe "request_fields" do
48
+ after do
49
+ Rackstash.request_fields = HashWithIndifferentAccess.new
50
+ end
51
+
52
+ let(:controller) do
53
+ controller = Class.new(Object){attr_accessor :status}.new
54
+ controller.status = "running"
55
+ controller
56
+ end
57
+
58
+ it "won't be included in unbuffered mode" do
59
+ Rackstash.request_fields = {:foo => :bar}
60
+
61
+ subject.info("foo")
62
+ json["@fields"].keys.wont_include "foo"
63
+ end
64
+
65
+ it "can be defined as a hash" do
66
+ Rackstash.request_fields = {:foo => :bar}
67
+
68
+ Rackstash.request_fields(controller).must_be_instance_of HashWithIndifferentAccess
69
+ Rackstash.request_fields(controller).must_equal({"foo" => :bar})
70
+
71
+ # TODO: fake a real request and ensure that the field gets set in the log output
72
+ end
73
+
74
+ it "can be defined as a proc" do
75
+ Rackstash.request_fields = proc do |controller|
76
+ {
77
+ :foo => :bar,
78
+ :status => @status,
79
+ :instance_status => controller.status
80
+ }
81
+ end
82
+
83
+ Rackstash.request_fields(controller).must_be_instance_of HashWithIndifferentAccess
84
+ Rackstash.request_fields(controller).must_equal({"foo" => :bar, "status" => "running", "instance_status" => "running"})
85
+
86
+ # TODO: fake a real request and ensure that the field gets set in the log output
87
+ end
88
+
89
+ it "can be defined as a lambda" do
90
+ Rackstash.request_fields = lambda do |controller|
91
+ {
92
+ :foo => :bar,
93
+ :status => @status,
94
+ :instance_status => controller.status
95
+ }
96
+ end
97
+
98
+ Rackstash.request_fields(controller).must_be_instance_of HashWithIndifferentAccess
99
+ Rackstash.request_fields(controller).must_equal({"foo" => :bar, "status" => "running", "instance_status" => "running"})
100
+
101
+ # TODO: fake a real request and ensure that the field gets set in the log output
102
+ end
103
+ end
104
+ end