timberio 1.0.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (133) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +34 -0
  3. data/.gitignore +14 -0
  4. data/Appraisals +37 -0
  5. data/Gemfile +22 -0
  6. data/LICENSE +38 -0
  7. data/README.md +22 -0
  8. data/Rakefile +4 -0
  9. data/TODO +4 -0
  10. data/benchmark/README.md +26 -0
  11. data/benchmark/rails_request.rb +68 -0
  12. data/benchmark/support/rails.rb +69 -0
  13. data/circle.yml +27 -0
  14. data/docs/installation/rails_on_heroku.md +31 -0
  15. data/docs/installation/rails_over_http.md +22 -0
  16. data/gemfiles/rails_3.0.X.gemfile +25 -0
  17. data/gemfiles/rails_3.1.X.gemfile +25 -0
  18. data/gemfiles/rails_3.2.X.gemfile +25 -0
  19. data/gemfiles/rails_4.0.X.gemfile +26 -0
  20. data/gemfiles/rails_4.1.X.gemfile +26 -0
  21. data/gemfiles/rails_4.2.X.gemfile +26 -0
  22. data/gemfiles/rails_5.0.X.gemfile +26 -0
  23. data/gemfiles/rails_edge.gemfile +27 -0
  24. data/lib/timber/api_settings.rb +17 -0
  25. data/lib/timber/bootstrap.rb +45 -0
  26. data/lib/timber/config.rb +25 -0
  27. data/lib/timber/context.rb +76 -0
  28. data/lib/timber/context_snapshot.rb +64 -0
  29. data/lib/timber/contexts/dynamic_values.rb +59 -0
  30. data/lib/timber/contexts/exception.rb +40 -0
  31. data/lib/timber/contexts/http_request.rb +22 -0
  32. data/lib/timber/contexts/http_requests/action_controller_specific.rb +48 -0
  33. data/lib/timber/contexts/http_requests/rack/params.rb +26 -0
  34. data/lib/timber/contexts/http_requests/rack.rb +105 -0
  35. data/lib/timber/contexts/http_response.rb +19 -0
  36. data/lib/timber/contexts/http_responses/action_controller.rb +76 -0
  37. data/lib/timber/contexts/logger.rb +33 -0
  38. data/lib/timber/contexts/organization.rb +33 -0
  39. data/lib/timber/contexts/organizations/action_controller.rb +34 -0
  40. data/lib/timber/contexts/server.rb +21 -0
  41. data/lib/timber/contexts/servers/heroku_specific.rb +48 -0
  42. data/lib/timber/contexts/sql_queries/active_record.rb +30 -0
  43. data/lib/timber/contexts/sql_queries/active_record_specific/binds.rb +37 -0
  44. data/lib/timber/contexts/sql_queries/active_record_specific.rb +59 -0
  45. data/lib/timber/contexts/sql_query.rb +18 -0
  46. data/lib/timber/contexts/template_render.rb +17 -0
  47. data/lib/timber/contexts/template_renders/action_view.rb +29 -0
  48. data/lib/timber/contexts/template_renders/action_view_specific.rb +51 -0
  49. data/lib/timber/contexts/user.rb +39 -0
  50. data/lib/timber/contexts/users/action_controller.rb +34 -0
  51. data/lib/timber/contexts.rb +23 -0
  52. data/lib/timber/current_context.rb +58 -0
  53. data/lib/timber/current_line_indexes.rb +35 -0
  54. data/lib/timber/frameworks/rails.rb +24 -0
  55. data/lib/timber/frameworks.rb +21 -0
  56. data/lib/timber/internal_logger.rb +35 -0
  57. data/lib/timber/log_device.rb +40 -0
  58. data/lib/timber/log_devices/heroku_logplex/hybrid_formatter.rb +14 -0
  59. data/lib/timber/log_devices/heroku_logplex.rb +14 -0
  60. data/lib/timber/log_devices/http/log_pile.rb +86 -0
  61. data/lib/timber/log_devices/http/log_truck/delivery.rb +116 -0
  62. data/lib/timber/log_devices/http/log_truck.rb +87 -0
  63. data/lib/timber/log_devices/http.rb +28 -0
  64. data/lib/timber/log_devices/io/formatter.rb +46 -0
  65. data/lib/timber/log_devices/io/hybrid_formatter.rb +41 -0
  66. data/lib/timber/log_devices/io/hybrid_hidden_formatter.rb +36 -0
  67. data/lib/timber/log_devices/io/json_formatter.rb +11 -0
  68. data/lib/timber/log_devices/io/logfmt_formatter.rb +11 -0
  69. data/lib/timber/log_devices/io.rb +41 -0
  70. data/lib/timber/log_devices.rb +4 -0
  71. data/lib/timber/log_line.rb +33 -0
  72. data/lib/timber/logger.rb +20 -0
  73. data/lib/timber/macros/compactor.rb +16 -0
  74. data/lib/timber/macros/date_formatter.rb +9 -0
  75. data/lib/timber/macros/deep_merger.rb +11 -0
  76. data/lib/timber/macros/logfmt_encoder.rb +77 -0
  77. data/lib/timber/macros.rb +4 -0
  78. data/lib/timber/patterns/delegated_singleton.rb +21 -0
  79. data/lib/timber/patterns/to_json.rb +22 -0
  80. data/lib/timber/patterns/to_logfmt.rb +9 -0
  81. data/lib/timber/patterns.rb +3 -0
  82. data/lib/timber/probe.rb +21 -0
  83. data/lib/timber/probes/action_controller_base.rb +31 -0
  84. data/lib/timber/probes/action_dispatch_debug_exceptions.rb +57 -0
  85. data/lib/timber/probes/active_support_log_subscriber/action_controller.rb +15 -0
  86. data/lib/timber/probes/active_support_log_subscriber/action_view.rb +26 -0
  87. data/lib/timber/probes/active_support_log_subscriber/active_record.rb +13 -0
  88. data/lib/timber/probes/active_support_log_subscriber.rb +62 -0
  89. data/lib/timber/probes/heroku.rb +30 -0
  90. data/lib/timber/probes/logger.rb +31 -0
  91. data/lib/timber/probes/rack.rb +36 -0
  92. data/lib/timber/probes/server.rb +18 -0
  93. data/lib/timber/probes.rb +24 -0
  94. data/lib/timber/version.rb +3 -0
  95. data/lib/timber.rb +27 -0
  96. data/spec/spec_helper.rb +27 -0
  97. data/spec/support/action_controller.rb +4 -0
  98. data/spec/support/action_view.rb +4 -0
  99. data/spec/support/active_record.rb +28 -0
  100. data/spec/support/coveralls.rb +2 -0
  101. data/spec/support/rails/templates/_partial.html +1 -0
  102. data/spec/support/rails/templates/template.html +1 -0
  103. data/spec/support/rails.rb +33 -0
  104. data/spec/support/simplecov.rb +9 -0
  105. data/spec/support/socket_hostname.rb +12 -0
  106. data/spec/support/timber.rb +23 -0
  107. data/spec/support/timecop.rb +3 -0
  108. data/spec/support/webmock.rb +2 -0
  109. data/spec/timber/bootstrap_spec.rb +31 -0
  110. data/spec/timber/context_snapshot_spec.rb +10 -0
  111. data/spec/timber/context_spec.rb +4 -0
  112. data/spec/timber/contexts/exception_spec.rb +34 -0
  113. data/spec/timber/contexts/organizations/action_controller_spec.rb +49 -0
  114. data/spec/timber/contexts/users/action_controller_spec.rb +65 -0
  115. data/spec/timber/current_line_indexes_spec.rb +40 -0
  116. data/spec/timber/frameworks/rails_spec.rb +9 -0
  117. data/spec/timber/log_devices/heroku_logplex_spec.rb +45 -0
  118. data/spec/timber/log_devices/http/log_truck/delivery_spec.rb +66 -0
  119. data/spec/timber/log_devices/http/log_truck_spec.rb +65 -0
  120. data/spec/timber/log_devices/io/hybrid_hidden_formatter_spec.rb +28 -0
  121. data/spec/timber/log_line_spec.rb +49 -0
  122. data/spec/timber/macros/compactor_spec.rb +19 -0
  123. data/spec/timber/macros/logfmt_encoder_spec.rb +89 -0
  124. data/spec/timber/patterns/to_json_spec.rb +40 -0
  125. data/spec/timber/probes/action_controller_base_spec.rb +43 -0
  126. data/spec/timber/probes/action_controller_log_subscriber/action_controller_spec.rb +35 -0
  127. data/spec/timber/probes/action_controller_log_subscriber/action_view_spec.rb +44 -0
  128. data/spec/timber/probes/action_controller_log_subscriber/active_record_spec.rb +26 -0
  129. data/spec/timber/probes/action_dispatch_debug_exceptions_spec.rb +45 -0
  130. data/spec/timber/probes/logger_spec.rb +20 -0
  131. data/spec/timber/probes/rack_spec.rb +26 -0
  132. data/timberio.gemspec +20 -0
  133. metadata +210 -0
@@ -0,0 +1,9 @@
1
+ require 'simplecov'
2
+
3
+ # save to CircleCI's artifacts directory if we're on CircleCI
4
+ if ENV['CIRCLE_ARTIFACTS']
5
+ dir = File.join(ENV['CIRCLE_ARTIFACTS'], "coverage")
6
+ SimpleCov.coverage_dir(dir)
7
+ end
8
+
9
+ SimpleCov.start
@@ -0,0 +1,12 @@
1
+ require "socket"
2
+
3
+ # Stub out the hostname for tests only. This can't use a normal stub in the
4
+ # test life cycle since our test rails app is loaded once upon initialization.
5
+ # In other words, the rails app gets loaded with the server context inserted
6
+ # before any tests are run.
7
+
8
+ class ::Socket
9
+ def self.gethostname
10
+ "computer-name.domain.com"
11
+ end
12
+ end
@@ -0,0 +1,23 @@
1
+ # Must require last in order to be mocked via webmock
2
+ require 'timber'
3
+
4
+ # Config
5
+ Timber::Config.tap do |config|
6
+ config.application_key = "my_key"
7
+ config.logger.level = ::Logger::FATAL
8
+ end
9
+
10
+ RSpec.configure do |config|
11
+ config.after(:each) do
12
+ Timber::CurrentLineIndexes.reset!
13
+ Timber::LogDevices::HTTP::LogPile.each { |log_pile| log_pile.empty }
14
+
15
+ # Reset permanent context caches since we mock, etc.
16
+ Timber::CurrentContext.send(:stack).each do |context|
17
+ context.instance_variable_set(:"@as_json", nil)
18
+ context.instance_variable_set(:"@json_payload", nil)
19
+ context.instance_variable_set(:"@to_json", nil)
20
+ context.instance_variable_set(:"@to_logfmt", nil)
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,3 @@
1
+ require 'timecop'
2
+
3
+ Timecop.safe_mode = true
@@ -0,0 +1,2 @@
1
+ require 'webmock/rspec'
2
+ WebMock.disable_net_connect!
@@ -0,0 +1,31 @@
1
+ require "spec_helper"
2
+
3
+ describe Timber::Bootstrap do
4
+ describe ".bootstrap!" do
5
+ let(:middleware) { Rack::Builder.new }
6
+ let(:insert_before) { ::Rails::Rack::Logger }
7
+
8
+ def self.it_should_not_bootstrap
9
+ it "should not bootstrap" do
10
+ expect(Timber::Probes).to_not receive(:insert!)
11
+ expect(described_class.bootstrap!(middleware, insert_before)).to be false
12
+ end
13
+ end
14
+
15
+ def self.it_should_bootstrap
16
+ it "should bootstrap" do
17
+ expect(Timber::Probes).to receive(:insert!).once
18
+ expect(described_class.bootstrap!(middleware, insert_before)).to be true
19
+ end
20
+ end
21
+
22
+ it_should_bootstrap
23
+
24
+ context "disabled" do
25
+ before(:each) { Timber::Config.enabled = false }
26
+ after(:each) { Timber::Config.enabled = true }
27
+
28
+ it_should_not_bootstrap
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,10 @@
1
+ require "spec_helper"
2
+
3
+ describe Timber::ContextSnapshot do
4
+ describe "#initialize" do
5
+ it "only includes the valid stack" do
6
+ expect(Timber::CurrentContext).to receive(:valid_stack).once.and_return([])
7
+ described_class.new
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,4 @@
1
+ require "spec_helper"
2
+
3
+ describe Timber::Context do
4
+ end
@@ -0,0 +1,34 @@
1
+ require "spec_helper"
2
+
3
+ describe Timber::Contexts::Exception do
4
+ let(:exception) do
5
+ begin
6
+ raise StandardError.new("this is a message")
7
+ rescue Exception => e
8
+ e
9
+ end
10
+ end
11
+ let(:context) { described_class.new(exception) }
12
+
13
+ describe ".as_json" do
14
+ subject { context.as_json }
15
+ its([:exception, :backtrace]) { should_not be_nil }
16
+ its([:exception, :name]) { should eq("StandardError") }
17
+ its([:exception, :message]) { should eq("this is a message") }
18
+ end
19
+
20
+ describe ".backtrace" do
21
+ subject { context.backtrace }
22
+ its(:size) { should eq(5) }
23
+ end
24
+
25
+ describe ".name" do
26
+ subject { context.name }
27
+ it { should eq("StandardError") }
28
+ end
29
+
30
+ describe ".message" do
31
+ subject { context.message }
32
+ it { should eq("this is a message") }
33
+ end
34
+ end
@@ -0,0 +1,49 @@
1
+ require "spec_helper"
2
+
3
+ describe Timber::Contexts::Organizations::ActionController do
4
+ around(:each) do |example|
5
+ class PagesController < ActionController::Base
6
+ layout nil
7
+
8
+ def index
9
+ render json: {}
10
+ end
11
+
12
+ def method_for_action(action_name)
13
+ action_name
14
+ end
15
+
16
+ private
17
+ def current_organization
18
+ # I want this to execute a query and test logging that query
19
+ Organization.first
20
+ end
21
+ end
22
+
23
+ example.run
24
+
25
+ Object.send(:remove_const, :PagesController)
26
+ end
27
+
28
+ let(:context) { described_class.new(PagesController.new) }
29
+
30
+ describe "#name" do
31
+ subject { context.name }
32
+ it { should be_nil }
33
+
34
+ context "with an organization" do
35
+ before(:each) { Organization.create!(name: "Timber") }
36
+ it { should eq("Timber") }
37
+
38
+ context "with an organization context" do
39
+ around(:each) do |example|
40
+ Timber::CurrentContext.add(context) do
41
+ example.run
42
+ end
43
+ end
44
+
45
+ it { should eq("Timber") }
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,65 @@
1
+ require "spec_helper"
2
+
3
+ describe Timber::Contexts::Users::ActionController do
4
+ around(:each) do |example|
5
+ class PagesController < ActionController::Base
6
+ layout nil
7
+
8
+ def index
9
+ render json: {}
10
+ end
11
+
12
+ def method_for_action(action_name)
13
+ action_name
14
+ end
15
+
16
+ private
17
+ def current_user
18
+ # I want this to execute a query and test logging that query
19
+ @user ||= User.first
20
+ end
21
+ end
22
+
23
+ example.run
24
+
25
+ Object.send(:remove_const, :PagesController)
26
+
27
+ Timber::Probes::ActiveSupportLogSubscriber.insert!
28
+ end
29
+
30
+ let(:context) { described_class.new(PagesController.new) }
31
+
32
+ describe "#email" do
33
+ subject { context.email }
34
+ it { should be_nil }
35
+
36
+ context "with a user" do
37
+ before(:each) { User.create!(email: "a@a.com") }
38
+ it { should eq("a@a.com") }
39
+
40
+ context "with a user context" do
41
+ around(:each) do |example|
42
+ Timber::CurrentContext.add(context) do
43
+ example.run
44
+ end
45
+ end
46
+
47
+ it { should eq("a@a.com") }
48
+
49
+ context "with a debug log level" do
50
+ around(:each) do |example|
51
+ old_level = ::Rails.logger.level
52
+ ::Rails.logger.level = ::Logger::DEBUG
53
+ example.run
54
+ ::Rails.logger.level = old_level
55
+ end
56
+
57
+ # If the user object is not cached, it will create an infinite loop.
58
+ # This is because getting the user executes a query, which in turn creates
59
+ # logs, with tries to grab the user again, etc.
60
+ it { should eq("a@a.com") }
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,40 @@
1
+ require "spec_helper"
2
+
3
+ describe Timber::CurrentLineIndexes do
4
+ def add_log_line
5
+ Timber::LogLine.new("test")
6
+ end
7
+
8
+ describe "#log_line_added" do
9
+ it "only includes the valid stack" do
10
+ expect(Timber::CurrentContext).to receive(:valid_stack).twice.and_return([])
11
+ add_log_line
12
+ end
13
+
14
+ context "with a context" do
15
+ let(:heroku_context) { Timber::Contexts::Servers::HerokuSpecific.new("web.1")}
16
+
17
+ around(:each) do |example|
18
+ Timber::CurrentContext.add(heroku_context) do
19
+ example.run
20
+ end
21
+ end
22
+
23
+ context "with a log line" do
24
+ before(:each) { add_log_line }
25
+
26
+ it "sets the context to 0" do
27
+ expect(described_class.indexes[heroku_context]).to eq(0)
28
+ end
29
+
30
+ context "with an additional log line" do
31
+ before(:each) { add_log_line }
32
+
33
+ it "increments properly" do
34
+ expect(described_class.indexes[heroku_context]).to eq(1)
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,9 @@
1
+ require 'spec_helper'
2
+
3
+ describe Timber::Frameworks::Rails do
4
+ # describe described_class::Railtie do
5
+ # it "includes the initializer" do
6
+ # expect(RailsApp.instance.initializers.collect { |i| i.name }).to include("timber.bootstrap")
7
+ # end
8
+ # end
9
+ end
@@ -0,0 +1,45 @@
1
+ require "spec_helper"
2
+
3
+ describe Timber::LogDevices::HerokuLogplex do
4
+ let(:io) { STDOUT }
5
+ let(:log_device) { described_class.new }
6
+
7
+ describe ".write" do
8
+ let(:time) { Time.utc(2016, 9, 1, 12, 0, 0) }
9
+
10
+ before(:each) do
11
+ server_context = Timber::CurrentContext.get(Timber::Contexts::Server)
12
+ allow(server_context).to receive(:_dt).and_return(time)
13
+ end
14
+
15
+ it "writes a proper logfmt line" do
16
+ expect(io).to receive(:write).with("\e7\e[1;30m@timber.io \e8\e[Kserver.hostname=comp\e8\e[Kuter-name.domain.com\e8\e[K server._dt=2016-09-\e8\e[K01T12:00:00.000000Z \e8\e[Kserver._version=1 se\e8\e[Krver._index=0 _versi\e8\e[Kon=1 _hierarchy=[ser\e8\e[Kver] @original \e8\e[K\e[0mthis is a message\n")
17
+ # Notice we do not have dt for the log line since Heroku provides this
18
+ log_device.write("this is a message\n")
19
+ end
20
+
21
+ context "with a heroku context" do
22
+ around(:each) do |example|
23
+ heroku = Timber::Contexts::Servers::HerokuSpecific.new("web.1")
24
+ Timber::CurrentContext.add(heroku) { example.run }
25
+ end
26
+
27
+ # No need for the heroku context since logplex includes that data by default
28
+ it "does not include the heroku context" do
29
+ expect(io).to receive(:write).with("\e7\e[1;30m@timber.io \e8\e[Kserver.hostname=comp\e8\e[Kuter-name.domain.com\e8\e[K server._dt=2016-09-\e8\e[K01T12:00:00.000000Z \e8\e[Kserver._version=1 se\e8\e[Krver._index=0 _versi\e8\e[Kon=1 _hierarchy=[ser\e8\e[Kver,server.heroku] @original \e8\e[K\e[0mthis is a message\n")
30
+ # Notice we do not have dt for the log line since Heroku provides this
31
+ log_device.write("this is a message\n")
32
+ end
33
+ end
34
+
35
+ context "with multiple lines" do |variable|
36
+ it "does not include the heroku context" do
37
+ expect(io).to receive(:write).with("\e7\e[1;30m@timber.io \e8\e[Kserver.hostname=comp\e8\e[Kuter-name.domain.com\e8\e[K server._dt=2016-09-\e8\e[K01T12:00:00.000000Z \e8\e[Kserver._version=1 se\e8\e[Krver._index=0 _versi\e8\e[Kon=1 _hierarchy=[ser\e8\e[Kver] @original \e8\e[K\e[0mline 1\n")
38
+ expect(io).to receive(:write).with("\e7\e[1;30m@timber.io \e8\e[Kserver.hostname=comp\e8\e[Kuter-name.domain.com\e8\e[K server._dt=2016-09-\e8\e[K01T12:00:00.000000Z \e8\e[Kserver._version=1 se\e8\e[Krver._index=1 _versi\e8\e[Kon=1 _hierarchy=[ser\e8\e[Kver] @original \e8\e[K\e[0mline 2\n")
39
+ expect(io).to receive(:write).with("\e7\e[1;30m@timber.io \e8\e[Kserver.hostname=comp\e8\e[Kuter-name.domain.com\e8\e[K server._dt=2016-09-\e8\e[K01T12:00:00.000000Z \e8\e[Kserver._version=1 se\e8\e[Krver._index=2 _versi\e8\e[Kon=1 _hierarchy=[ser\e8\e[Kver] @original \e8\e[K\e[0mline 3\n")
40
+ # Notice we do not have dt for the log line since Heroku provides this
41
+ log_device.write("line 1\nline 2\nline 3\n")
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,66 @@
1
+ require "spec_helper"
2
+
3
+ describe Timber::LogDevices::HTTP::LogTruck::Delivery do
4
+ describe "#deliver!" do
5
+ let(:time) { Time.utc(2016, 9, 1, 12, 0, 0) }
6
+
7
+ before(:each) { ActiveSupport.send(:silence_warnings) { described_class::RETRY_COUNT = 0 } }
8
+ after(:each) { ActiveSupport.send(:silence_warnings) { described_class::RETRY_COUNT = 3 } }
9
+
10
+ def new_stub
11
+ stub_request(:post, "https://timber-odin.herokuapp.com/agent_log_frames").
12
+ with(:body => "{\"agent_log_frame\": {\"log_lines\": [{\"dt\":\"2016-09-01T12:00:00.000000Z\",\"message\":\"hello\",\"context\":{\"server\":{\"hostname\":\"computer-name.domain.com\",\"_dt\":\"2016-09-01T12:00:00.000000Z\",\"_version\":1,\"_index\":0},\"_version\":1,\"_hierarchy\":[\"server\"]}}]}}",
13
+ :headers => {'Content-Type'=>'application/json'})
14
+ end
15
+
16
+ around(:each) do |example|
17
+ Timecop.freeze(time) { example.run }
18
+ end
19
+
20
+ before(:each) do
21
+ server_context = Timber::CurrentContext.get(Timber::Contexts::Server)
22
+ allow(server_context).to receive(:_dt).and_return(time)
23
+ end
24
+
25
+ let(:log_lines) { [Timber::LogLine.new("hello")] }
26
+ let(:delivery) { described_class.new(Timber::Config.application_key, log_lines) }
27
+ let(:stub) { new_stub }
28
+
29
+ before(:each) { stub }
30
+
31
+ it "should delivery successfully" do
32
+ delivery.deliver!
33
+ expect(stub).to have_been_requested
34
+ end
35
+
36
+ context "timeout error" do
37
+ let(:stub) {
38
+ new_stub.to_timeout
39
+ }
40
+
41
+ it "should raise an error" do
42
+ expect { delivery.deliver! }.to raise_error(Timber::LogDevices::HTTP::LogTruck::Delivery::DeliveryError)
43
+ end
44
+ end
45
+
46
+ context "random error" do
47
+ let(:stub) {
48
+ new_stub.to_raise(StandardError.new("some error"))
49
+ }
50
+
51
+ it "should raise an error" do
52
+ expect { delivery.deliver! }.to raise_error(Timber::LogDevices::HTTP::LogTruck::Delivery::DeliveryError)
53
+ end
54
+ end
55
+
56
+ context "internal server error" do
57
+ let(:stub) {
58
+ new_stub.to_return(status: [500, "Internal Server Error"])
59
+ }
60
+
61
+ it "should raise an error" do
62
+ expect { delivery.deliver! }.to raise_error(Timber::LogDevices::HTTP::LogTruck::Delivery::DeliveryError)
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,65 @@
1
+ require "spec_helper"
2
+
3
+ describe Timber::LogDevices::HTTP::LogTruck do
4
+ describe ".start!" do
5
+ it "spawns a new thread" do
6
+ expect(Thread).to receive(:new).once
7
+ described_class.start!
8
+ end
9
+
10
+ it "delivers" do
11
+ expect(described_class).to receive(:deliver).once
12
+ described_class.start! do |thread|
13
+ thread.kill
14
+ end.join
15
+ end
16
+ end
17
+
18
+ describe ".deliver" do
19
+ let(:log_pile) { Timber::LogDevices::HTTP::LogPile.get(Timber::Config.application_key) }
20
+
21
+ it "doesn't deliver because there is nothing to deliver" do
22
+ expect(log_pile).to_not receive(:deliver)
23
+ described_class.deliver
24
+ end
25
+
26
+ context "with a log pile" do
27
+ before(:each) do
28
+ log_line = Timber::LogLine.new("this is a log line")
29
+ log_pile.drop(log_line)
30
+ end
31
+
32
+ it "delivers once and empties the log pile" do
33
+ expect(log_pile.size).to eq(1)
34
+ expect_any_instance_of(described_class).to receive(:deliver!).once
35
+ described_class.deliver
36
+ expect(log_pile.size).to eq(0)
37
+ end
38
+ end
39
+ end
40
+
41
+ describe "#initialize" do
42
+ let(:log_lines) { [] }
43
+ let(:log_truck) { described_class.new(Timber::Config.application_key, log_lines) }
44
+ subject { log_truck }
45
+
46
+ it "should raise an exception" do
47
+ expect { subject }.to raise_exception(Timber::LogDevices::HTTP::LogTruck::NoPayloadError)
48
+ end
49
+
50
+ context "with log lines" do
51
+ let(:log_lines) { [Timber::LogLine.new("hello")] }
52
+ its(:log_lines) { should eq(log_lines) }
53
+ end
54
+ end
55
+
56
+ describe "#deliver!" do
57
+ let(:log_lines) { [Timber::LogLine.new("hello")] }
58
+ let(:log_truck) { described_class.new(Timber::Config.application_key, log_lines) }
59
+
60
+ it "should delivery successfully" do
61
+ expect_any_instance_of(Timber::LogDevices::HTTP::LogTruck::Delivery).to receive(:deliver!)
62
+ log_truck.deliver!
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,28 @@
1
+ require "spec_helper"
2
+
3
+ describe Timber::LogDevices::IO::HybridHiddenFormatter do
4
+ describe ".format" do
5
+ let(:time) { Time.utc(2016, 9, 1, 12, 0, 0) }
6
+ let(:formatter) { described_class.new }
7
+ let(:message) { "a message" }
8
+ let(:log_line) { Timber::LogLine.new(message) }
9
+ subject { formatter.format(log_line) }
10
+
11
+ before(:each) do
12
+ server_context = Timber::CurrentContext.get(Timber::Contexts::Server)
13
+ allow(server_context).to receive(:_dt).and_return(time)
14
+ end
15
+
16
+ it { should eq("\e7\e[1;30m@timber.io \e8\e[Kserver.hostname=comp\e8\e[Kuter-name.domain.com\e8\e[K server._dt=2016-09-\e8\e[K01T12:00:00.000000Z \e8\e[Kserver._version=1 se\e8\e[Krver._index=0 _versi\e8\e[Kon=1 _hierarchy=[ser\e8\e[Kver] @original \e8\e[K\e[0ma message") }
17
+
18
+ context "with a slash" do
19
+ let(:message) { "this is a long message that exceeds the step size".insert(described_class::CLEAR_STEP_SIZE - 1, "\\") }
20
+
21
+ before(:each) do
22
+ allow(formatter).to receive(:encoded_context).and_return(message)
23
+ end
24
+
25
+ it { should eq("\e7\e[1;30m@timber.io \e8\e[Kthis is a long mess\\a\e8\e[Kge that exceeds the \e8\e[Kstep size @original \e8\e[K\e[0mthis is a long mess\\a\e8\e[Kge that exceeds the \e8\e[Kstep size") }
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,49 @@
1
+ require "spec_helper"
2
+
3
+ describe Timber::LogLine do
4
+ let(:message) { "this is a message" }
5
+ let(:log_line) { described_class.new(message) }
6
+
7
+ around(:each) do |example|
8
+ heroku = Timber::Contexts::Servers::HerokuSpecific.new("web.1")
9
+ Timber::CurrentContext.add(heroku) { example.run }
10
+ end
11
+
12
+ describe "#initialize" do
13
+ subject { log_line }
14
+
15
+ its(:dt) { should_not be_nil }
16
+ its(:message) { should equal(message) }
17
+
18
+ context "non string" do
19
+ let(:message) { :"this is a message" }
20
+ its(:message) { should eq(message.to_s) }
21
+ end
22
+
23
+ context "exceeds bytesize limit" do
24
+ let(:limit) { Timber::APISettings::MESSAGE_BYTE_SIZE_MAX }
25
+ let(:message) { (1..(limit + 1)).collect { "A" }.join }
26
+ its(:message) { should_not eq(message) }
27
+ its(:message) { should eq(message.byteslice(0, Timber::APISettings::MESSAGE_BYTE_SIZE_MAX)) }
28
+ end
29
+
30
+ it "notifies CurrentLineIndexes" do
31
+ expect(Timber::CurrentLineIndexes).to receive(:log_line_added).once
32
+ subject
33
+ end
34
+ end
35
+
36
+ describe "#to_json" do
37
+ # Note: very important that we keep the iso8601 format. Otherwise the Timber API
38
+ # will recognized the date as invalid.
39
+ let(:as_json) do
40
+ {
41
+ dt: log_line.dt.iso8601(6),
42
+ message: log_line.message,
43
+ }.merge(log_line.context_snapshot.as_json)
44
+ end
45
+ let(:json) { as_json.to_json }
46
+ subject { log_line.to_json }
47
+ it { should eq(json) }
48
+ end
49
+ end
@@ -0,0 +1,19 @@
1
+ require "spec_helper"
2
+
3
+ describe Timber::Macros::Compactor do
4
+ describe ".compact" do
5
+ let(:hash) { {} }
6
+ subject { described_class.compact(hash) }
7
+ it { should eq({}) }
8
+
9
+ context "nested" do
10
+ let(:hash) { {:whatever => {:nested => nil}} }
11
+ it { should eq({}) }
12
+ end
13
+
14
+ context "nested with other values" do
15
+ let(:hash) { {:whatever => {:nested => nil, :with_val => 1}, :another => 1} }
16
+ it { should eq({:whatever => {:with_val => 1}, :another => 1}) }
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,89 @@
1
+ require "spec_helper"
2
+
3
+ describe Timber::Macros::LogfmtEncoder do
4
+ describe ".encode" do
5
+ subject { described_class.encode(target) }
6
+
7
+ context "nil" do
8
+ let(:target) { nil }
9
+ it "should raise" do
10
+ expect { subject }.to raise_error(ArgumentError)
11
+ end
12
+ end
13
+
14
+ context "blank hash" do
15
+ let(:target) { {} }
16
+ it { should eq("") }
17
+ end
18
+
19
+ context "simple hash" do
20
+ let(:target) { {"key" => "value"} }
21
+ it { should eq("key=value") }
22
+ end
23
+
24
+ context "with space in value" do
25
+ let(:target) { {"key" => "this is a value"} }
26
+ it { should eq("key=\"this is a value\"") }
27
+ end
28
+
29
+ context "with quote in value" do
30
+ let(:target) { {"key" => "value\"another"} }
31
+ it { should eq("key=\"value\\\"another\"") }
32
+ end
33
+
34
+ context "with a space in the key" do
35
+ let(:target) { {"this is a key" => "value"} }
36
+ it { should eq("\"this is a key\"=value") }
37
+ end
38
+
39
+ context "with a . in the key" do
40
+ let(:target) { {"this.is.a.key" => "value"} }
41
+ it { should eq("\"this.is.a.key\"=value") }
42
+ end
43
+
44
+ context "with a nested hash" do
45
+ let(:target) { {"key" => {"sub_key" => "value"}} }
46
+ it { should eq("key.sub_key=value") }
47
+ end
48
+
49
+ context "with a nested hash and space in the key" do
50
+ let(:target) { {"key" => {"sub key" => "value"}} }
51
+ it { should eq("key.\"sub key\"=value") }
52
+ end
53
+
54
+ context "with a nested hash and . in the key" do
55
+ let(:target) { {"key" => {"sub.key" => "value"}} }
56
+ it { should eq("key.\"sub.key\"=value") }
57
+ end
58
+
59
+ context "with an integer value" do
60
+ let(:target) { {"key" => 1} }
61
+ it { should eq("key=1") }
62
+ end
63
+
64
+ context "with a float value" do
65
+ let(:target) { {"key" => 1.23} }
66
+ it { should eq("key=1.23") }
67
+ end
68
+
69
+ context "with a true value" do
70
+ let(:target) { {"key" => true} }
71
+ it { should eq("key=true") }
72
+ end
73
+
74
+ context "with a false value" do
75
+ let(:target) { {"key" => false} }
76
+ it { should eq("key=false") }
77
+ end
78
+
79
+ context "with an array value" do
80
+ let(:target) { {"key" => ["this is a value",2,3]} }
81
+ it { should eq("key=[\"this is a value\",2,3]") }
82
+ end
83
+
84
+ context "with an array value that has a ," do
85
+ let(:target) { {"key" => ["this,is a value",2,3]} }
86
+ it { should eq("key=[\"this,is a value\",2,3]") }
87
+ end
88
+ end
89
+ end