logtail-ruby 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/main.yml +33 -0
  3. data/.gitignore +24 -0
  4. data/.rspec +2 -0
  5. data/CHANGELOG.md +12 -0
  6. data/Gemfile +10 -0
  7. data/LICENSE.md +15 -0
  8. data/README.md +4 -0
  9. data/Rakefile +72 -0
  10. data/lib/logtail.rb +36 -0
  11. data/lib/logtail/config.rb +154 -0
  12. data/lib/logtail/config/integrations.rb +17 -0
  13. data/lib/logtail/context.rb +9 -0
  14. data/lib/logtail/contexts.rb +12 -0
  15. data/lib/logtail/contexts/http.rb +31 -0
  16. data/lib/logtail/contexts/release.rb +52 -0
  17. data/lib/logtail/contexts/runtime.rb +23 -0
  18. data/lib/logtail/contexts/session.rb +24 -0
  19. data/lib/logtail/contexts/system.rb +29 -0
  20. data/lib/logtail/contexts/user.rb +28 -0
  21. data/lib/logtail/current_context.rb +168 -0
  22. data/lib/logtail/event.rb +36 -0
  23. data/lib/logtail/events.rb +10 -0
  24. data/lib/logtail/events/controller_call.rb +44 -0
  25. data/lib/logtail/events/error.rb +40 -0
  26. data/lib/logtail/events/exception.rb +10 -0
  27. data/lib/logtail/events/sql_query.rb +26 -0
  28. data/lib/logtail/events/template_render.rb +25 -0
  29. data/lib/logtail/integration.rb +40 -0
  30. data/lib/logtail/integrator.rb +50 -0
  31. data/lib/logtail/log_devices.rb +8 -0
  32. data/lib/logtail/log_devices/http.rb +368 -0
  33. data/lib/logtail/log_devices/http/flushable_dropping_sized_queue.rb +52 -0
  34. data/lib/logtail/log_devices/http/request_attempt.rb +20 -0
  35. data/lib/logtail/log_entry.rb +110 -0
  36. data/lib/logtail/logger.rb +270 -0
  37. data/lib/logtail/logtail.rb +36 -0
  38. data/lib/logtail/timer.rb +21 -0
  39. data/lib/logtail/util.rb +7 -0
  40. data/lib/logtail/util/non_nil_hash_builder.rb +40 -0
  41. data/lib/logtail/version.rb +3 -0
  42. data/logtail-ruby.gemspec +43 -0
  43. data/spec/README.md +13 -0
  44. data/spec/logtail/current_context_spec.rb +113 -0
  45. data/spec/logtail/events/controller_call_spec.rb +12 -0
  46. data/spec/logtail/events/error_spec.rb +15 -0
  47. data/spec/logtail/log_devices/http_spec.rb +185 -0
  48. data/spec/logtail/log_entry_spec.rb +22 -0
  49. data/spec/logtail/logger_spec.rb +227 -0
  50. data/spec/spec_helper.rb +22 -0
  51. data/spec/support/logtail.rb +5 -0
  52. data/spec/support/socket_hostname.rb +12 -0
  53. data/spec/support/timecop.rb +3 -0
  54. data/spec/support/webmock.rb +3 -0
  55. metadata +238 -0
@@ -0,0 +1,36 @@
1
+ # Base (must come first, order matters)
2
+ require "logtail/version"
3
+ require "logtail/config"
4
+ require "logtail/util"
5
+
6
+ # Load frameworks
7
+
8
+ # Other (sorted alphabetically)
9
+ require "logtail/contexts"
10
+ require "logtail/current_context"
11
+ require "logtail/events"
12
+ require "logtail/integration"
13
+ require "logtail/log_devices"
14
+ require "logtail/log_entry"
15
+ require "logtail/logger"
16
+ require "logtail/timer"
17
+ require "logtail/integrator"
18
+ require "logtail/integration"
19
+
20
+ module Logtail
21
+ # Access the main configuration object. Please see {{Logtail::Config}} for more details.
22
+ def self.config
23
+ Config.instance
24
+ end
25
+
26
+ # Starts a timer for timing events. Please see {{Logtail::Logtail.start}} for more details.
27
+ def self.start_timer
28
+ Timer.start
29
+ end
30
+
31
+ # Adds context to all logs written within the passed block. Please see
32
+ # {{Logtail::CurrentContext.with}} for a more detailed description with examples.
33
+ def self.with_context(context, &block)
34
+ CurrentContext.with(context, &block)
35
+ end
36
+ end
@@ -0,0 +1,21 @@
1
+ module Logtail
2
+ # This is an ultra-simple abstraction for timing code. This provides a little
3
+ # more control around how Logtail automatically processes "timers".
4
+ #
5
+ # @example
6
+ # timer = Logtail::Timer.start
7
+ # # ... code to time
8
+ # logger.info("My log message", my_event: {time_ms: timer})
9
+ module Timer
10
+ # Abstract for starting a logtail. Currently this is simply calling `Time.now`.
11
+ def self.start
12
+ Time.now
13
+ end
14
+
15
+ # Get the duration in milliseconds from the object returned in {#start}
16
+ def self.duration_ms(timer)
17
+ now = Time.now
18
+ (now - timer) * 1000.0
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,7 @@
1
+ require "logtail/util/non_nil_hash_builder"
2
+
3
+ module Logtail
4
+ # @private
5
+ module Util
6
+ end
7
+ end
@@ -0,0 +1,40 @@
1
+ require 'json'
2
+
3
+ module Logtail
4
+ module Util
5
+ # @private
6
+ #
7
+ # The purpose of this class is to efficiently build a hash that does not
8
+ # include nil values. It's proactive instead of reactive, avoiding the
9
+ # need to traverse and reduce a new hash dropping blanks.
10
+ class NonNilHashBuilder
11
+ class << self
12
+ def build(&block)
13
+ builder = new
14
+ yield builder
15
+ builder.target
16
+ end
17
+ end
18
+
19
+ attr_reader :target
20
+
21
+ def initialize
22
+ @target = {}
23
+ end
24
+
25
+ def add(k, v, options = {})
26
+ if !v.nil?
27
+ if options[:json_encode]
28
+ v = v.to_json
29
+ end
30
+
31
+ if options[:limit]
32
+ v = v.byteslice(0, options[:limit])
33
+ end
34
+
35
+ @target[k] = v
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,3 @@
1
+ module Logtail
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,43 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $LOAD_PATH.push File.expand_path("../lib", __FILE__)
3
+ require "logtail/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "logtail-ruby"
7
+ spec.version = Logtail::VERSION
8
+ spec.platform = Gem::Platform::RUBY
9
+ spec.authors = ["Logtail"]
10
+ spec.email = ["hi@logtail.com"]
11
+ spec.homepage = "https://github.com/logtail/logtail-ruby"
12
+ spec.license = "ISC"
13
+
14
+ spec.summary = "Query logs like you query your database. https://logtail.com"
15
+
16
+ spec.metadata["bug_tracker_uri"] = "#{spec.homepage}/issues"
17
+ spec.metadata["changelog_uri"] = "#{spec.homepage}/tree/master/CHANGELOG.md"
18
+ spec.metadata["homepage_uri"] = spec.homepage
19
+ spec.metadata["source_code_uri"] = spec.homepage
20
+
21
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.2.0")
22
+
23
+ spec.files = `git ls-files`.split("\n")
24
+ spec.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
25
+ spec.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
26
+ spec.require_paths = ["lib"]
27
+
28
+ spec.add_dependency('msgpack', '~> 1.0')
29
+
30
+ spec.add_development_dependency('bundler-audit', '>= 0')
31
+ spec.add_development_dependency('rails_stdout_logging', '>= 0')
32
+ spec.add_development_dependency('rake', '>= 0')
33
+ spec.add_development_dependency('rspec', '~> 3.4')
34
+ spec.add_development_dependency('rspec-its', '>= 0')
35
+ spec.add_development_dependency('timecop', '>= 0')
36
+ spec.add_development_dependency('webmock', '~> 2.3')
37
+
38
+ if RUBY_PLATFORM == "java"
39
+ spec.add_development_dependency('activerecord-jdbcsqlite3-adapter', '>= 0')
40
+ else
41
+ spec.add_development_dependency('sqlite3', '>= 0')
42
+ end
43
+ end
data/spec/README.md ADDED
@@ -0,0 +1,13 @@
1
+ # Testing
2
+
3
+ To get started:
4
+
5
+ ```shell
6
+ bundle install
7
+ ```
8
+
9
+ You can run tests like this:
10
+
11
+ ```shell
12
+ bundle exec rspec
13
+ ```
@@ -0,0 +1,113 @@
1
+ require "spec_helper"
2
+
3
+ describe Logtail::CurrentContext do
4
+ describe ".initialize" do
5
+ it "should not set the release context" do
6
+ context = described_class.send(:new)
7
+ expect(context.send(:hash)[:release]).to be_nil
8
+ end
9
+
10
+ it "should set the system context" do
11
+ context = described_class.send(:new)
12
+ system_content = context.fetch(:system)
13
+ expect(system_content[:hostname]).to_not be_nil
14
+ end
15
+
16
+ it "should set the runtime context" do
17
+ context = described_class.send(:new)
18
+ runtime_context = context.fetch(:runtime)
19
+ expect(runtime_context[:thread_id]).to_not be_nil
20
+ end
21
+
22
+ context "with Heroku dyno metadata" do
23
+ around(:each) do |example|
24
+ ENV['HEROKU_SLUG_COMMIT'] = "2c3a0b24069af49b3de35b8e8c26765c1dba9ff0"
25
+ ENV['HEROKU_RELEASE_CREATED_AT'] = "2015-04-02T18:00:42Z"
26
+ ENV['HEROKU_RELEASE_VERSION'] = "v2.3.1"
27
+
28
+ example.run
29
+
30
+ ENV.delete('HEROKU_SLUG_COMMIT')
31
+ ENV.delete('HEROKU_RELEASE_CREATED_AT')
32
+ ENV.delete('HEROKU_RELEASE_VERSION')
33
+
34
+ described_class.reset
35
+ end
36
+
37
+ it "should automatically set the release context" do
38
+ context = described_class.send(:new)
39
+ expect(context.send(:hash)[:release]).to eq({:commit_hash=>"2c3a0b24069af49b3de35b8e8c26765c1dba9ff0", :created_at=>"2015-04-02T18:00:42Z", :version=>"v2.3.1"})
40
+ end
41
+ end
42
+
43
+ context "with generic env vars" do
44
+ around(:each) do |example|
45
+ ENV['RELEASE_COMMIT'] = "2c3a0b24069af49b3de35b8e8c26765c1dba9ff0"
46
+ ENV['RELEASE_CREATED_AT'] = "2015-04-02T18:00:42Z"
47
+ ENV['RELEASE_VERSION'] = "v2.3.1"
48
+
49
+ example.run
50
+
51
+ ENV.delete('RELEASE_COMMIT')
52
+ ENV.delete('RELEASE_CREATED_AT')
53
+ ENV.delete('RELEASE_VERSION')
54
+
55
+ described_class.reset
56
+ end
57
+
58
+ it "should automatically set the release context" do
59
+ context = described_class.send(:new)
60
+ expect(context.send(:hash)[:release]).to eq({:commit_hash=>"2c3a0b24069af49b3de35b8e8c26765c1dba9ff0", :created_at=>"2015-04-02T18:00:42Z", :version=>"v2.3.1"})
61
+ end
62
+ end
63
+ end
64
+
65
+ describe ".add" do
66
+ after(:each) do
67
+ described_class.reset
68
+ end
69
+
70
+ it "should add the context" do
71
+ expect(described_class.instance.send(:hash)[:build]).to be_nil
72
+
73
+ described_class.add({build: {version: "1.0.0"}})
74
+ expect(described_class.instance.send(:hash)[:build]).to eq({:version=>"1.0.0"})
75
+
76
+ described_class.add({testing: {key: "value"}})
77
+ expect(described_class.instance.send(:hash)[:build]).to eq({:version=>"1.0.0"})
78
+ expect(described_class.instance.send(:hash)[:testing]).to eq({:key=>"value"})
79
+ end
80
+ end
81
+
82
+ describe ".remove" do
83
+ it "should remove context by key" do
84
+ context = {:build=>{:version=>"1.0.0"}}
85
+ described_class.add(context)
86
+ expect(described_class.instance.send(:hash)[:build]).to eq({:version=>"1.0.0"})
87
+
88
+ described_class.remove(:build)
89
+ expect(described_class.instance.send(:hash)[:build]).to be_nil
90
+ end
91
+ end
92
+
93
+ describe ".with" do
94
+ it "should merge the context and cleanup on block exit" do
95
+ expect(described_class.instance.send(:hash)[:build]).to be_nil
96
+
97
+ described_class.with({build: {version: "1.0.0"}}) do
98
+ expect(described_class.instance.send(:hash)[:build]).to eq({:version=>"1.0.0"})
99
+
100
+ described_class.with({testing: {key: "value"}}) do
101
+ expect(described_class.instance.send(:hash)[:build]).to eq({:version=>"1.0.0"})
102
+ expect(described_class.instance.send(:hash)[:testing]).to eq({:key=>"value"})
103
+ end
104
+
105
+ expect(described_class.instance.send(:hash)[:build]).to eq({:version=>"1.0.0"})
106
+ expect(described_class.instance.send(:hash)[:testing]).to be_nil
107
+ end
108
+
109
+ expect(described_class.instance.send(:hash)[:build]).to be_nil
110
+ expect(described_class.instance.send(:hash)[:testing]).to be_nil
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,12 @@
1
+ # encoding: UTF-8
2
+
3
+ require "spec_helper"
4
+
5
+ describe Logtail::Events::ControllerCall do
6
+ describe ".initialize" do
7
+ it "sanitizes the password param" do
8
+ # event = described_class.new(controller: 'controller', action: 'action', params: {password: 'password'})
9
+ # expect(event.params).to eq({'password' => '[sanitized]'})
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,15 @@
1
+ require "spec_helper"
2
+
3
+ describe Logtail::Events::Error do
4
+ describe "#to_hash" do
5
+ it "should jsonify the stacktrace" do
6
+ backtrace = [
7
+ "/path/to/file1.rb:26:in `function1'",
8
+ "path/to/file2.rb:86:in `function2'"
9
+ ]
10
+
11
+ exception_event = described_class.new(name: "RuntimeError", error_message: "Boom", backtrace: backtrace)
12
+ expect(exception_event.backtrace_json).to eq("[\"/path/to/file1.rb:26:in `function1'\",\"path/to/file2.rb:86:in `function2'\"]")
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,185 @@
1
+ require "spec_helper"
2
+
3
+ # Note: these tests access instance variables and private methods as a means of
4
+ # not muddying the public API. This object should expose a simple buffer like
5
+ # API, tests should not alter that.
6
+ describe Logtail::LogDevices::HTTP do
7
+ describe "#initialize" do
8
+ it "should initialize properly" do
9
+ http = described_class.new("MYKEY", flush_interval: 0.1)
10
+
11
+ # Ensure that threads have not started
12
+ thread = http.instance_variable_get(:@flush_thread)
13
+ expect(thread).to be_nil
14
+ thread = http.instance_variable_get(:@request_outlet_thread)
15
+ expect(thread).to be_nil
16
+ end
17
+ end
18
+
19
+ describe "#write" do
20
+ let(:http) { described_class.new("MYKEY") }
21
+ let(:msg_queue) { http.instance_variable_get(:@msg_queue) }
22
+
23
+ it "should buffer the messages" do
24
+ http.write("test log message")
25
+ expect(msg_queue.flush).to eq(["test log message"])
26
+ http.close
27
+ end
28
+
29
+ it "should start the flush threads" do
30
+ http.write("test log message")
31
+
32
+ thread = http.instance_variable_get(:@flush_thread)
33
+ expect(thread).to be_alive
34
+ thread = http.instance_variable_get(:@request_outlet_thread)
35
+ expect(thread).to be_alive
36
+ expect(http).to receive(:flush).exactly(1).times
37
+ http.close
38
+ end
39
+
40
+ context "with a low batch size" do
41
+ let(:http) { described_class.new("MYKEY", :batch_size => 2) }
42
+
43
+ it "should attempt a delivery when the limit is exceeded" do
44
+ http.write("test")
45
+ expect(http).to receive(:flush_async).exactly(1).times
46
+ http.write("my log message")
47
+ expect(http).to receive(:flush).exactly(1).times
48
+ http.close
49
+ end
50
+ end
51
+ end
52
+
53
+ describe "#close" do
54
+ let(:http) { described_class.new("MYKEY") }
55
+
56
+ it "should kill the threads" do
57
+ http.send(:ensure_flush_threads_are_started)
58
+ http.close
59
+ thread = http.instance_variable_get(:@flush_thread)
60
+ sleep 0.1 # too fast!
61
+ expect(thread).to_not be_alive
62
+ thread = http.instance_variable_get(:@request_outlet_thread)
63
+ sleep 0.1 # too fast!
64
+ expect(thread).to_not be_alive
65
+ end
66
+
67
+ it "should attempt a delivery" do
68
+ message = "a" * 19
69
+ http.write(message)
70
+ expect(http).to receive(:flush).exactly(1).times
71
+ http.close
72
+ end
73
+ end
74
+
75
+ # Testing a private method because it helps break down our tests
76
+ describe "#flush" do
77
+ let(:time) { Time.utc(2016, 9, 1, 12, 0, 0) }
78
+
79
+ it "should deliver the request" do
80
+ http = described_class.new("MYKEY", flush_continuously: false)
81
+ log_entry = Logtail::LogEntry.new("INFO", time, nil, "test log message 1", nil, nil)
82
+ http.write(log_entry)
83
+ log_entry = Logtail::LogEntry.new("INFO", time, nil, "test log message 2", nil, nil)
84
+ http.write(log_entry)
85
+ expect(http).to receive(:flush_async).exactly(2).times
86
+ http.send(:flush)
87
+ http.close
88
+ end
89
+ end
90
+
91
+ # Testing a private method because it helps break down our tests
92
+ describe "#flush_async" do
93
+ let(:time) { Time.utc(2016, 9, 1, 12, 0, 0) }
94
+
95
+ it "should add a request to the queue" do
96
+ http = described_class.new("MYKEY", flush_continuously: false)
97
+ log_entry = Logtail::LogEntry.new("INFO", time, nil, "test log message 1", nil, nil)
98
+ http.write(log_entry)
99
+ log_entry = Logtail::LogEntry.new("INFO", time, nil, "test log message 2", nil, nil)
100
+ http.write(log_entry)
101
+ http.send(:flush_async)
102
+ request_queue = http.instance_variable_get(:@request_queue)
103
+ request_attempt = request_queue.deq
104
+ expect(request_attempt.request).to be_kind_of(Net::HTTP::Post)
105
+ expect(request_attempt.request.body).to start_with("\x92\x83\xA5level\xA4INFO\xA2dt\xBB2016-09-01T12:00:00.000000Z\xA7message\xB2test log message 1".force_encoding("ASCII-8BIT"))
106
+
107
+ message_queue = http.instance_variable_get(:@msg_queue)
108
+ expect(message_queue.size).to eq(0)
109
+ end
110
+ end
111
+
112
+ # Testing a private method because it helps break down our tests
113
+ describe "#intervaled_flush" do
114
+ it "should start a intervaled flush thread and flush on an interval" do
115
+ http = described_class.new("MYKEY", flush_interval: 0.1)
116
+ http.send(:ensure_flush_threads_are_started)
117
+ expect(http).to receive(:flush_async).at_least(3).times
118
+ sleep 1.1 # iterations check every 0.5 seconds
119
+ http.close
120
+ end
121
+ end
122
+
123
+ # Outlet
124
+ describe "#request_outlet" do
125
+ let(:time) { Time.utc(2016, 9, 1, 12, 0, 0) }
126
+
127
+ it "should deliver requests on an interval" do
128
+ stub = stub_request(:post, "https://logs.logtail.com/").
129
+ with(
130
+ :body => start_with("\x92\x83\xA5level\xA4INFO\xA2dt\xBB2016-09-01T12:00:00.000000Z\xA7message\xB2test log message 1".force_encoding("ASCII-8BIT")),
131
+ :headers => {
132
+ 'Authorization' => 'Bearer MYKEY',
133
+ 'Content-Type' => 'application/msgpack',
134
+ 'User-Agent' => "Logtail Ruby/#{Logtail::VERSION} (HTTP)"
135
+ }
136
+ ).
137
+ to_return(:status => 200, :body => "", :headers => {})
138
+
139
+ http = described_class.new("MYKEY", flush_interval: 0.1)
140
+ log_entry1 = Logtail::LogEntry.new("INFO", time, nil, "test log message 1", nil, nil)
141
+ http.write(log_entry1)
142
+ log_entry2 = Logtail::LogEntry.new("INFO", time, nil, "test log message 2", nil, nil)
143
+ http.write(log_entry2)
144
+ sleep 2
145
+
146
+ expect(stub).to have_been_requested.times(1)
147
+
148
+ http.close
149
+ end
150
+ end
151
+
152
+ describe "#deliver_requests" do
153
+ it "should handle exceptions properly and return" do
154
+ allow_any_instance_of(Net::HTTP).to receive(:request).and_raise("boom")
155
+
156
+ http_device = described_class.new("MYKEY", flush_continuously: false)
157
+ req_queue = http_device.instance_variable_get(:@request_queue)
158
+
159
+ # Place a request on the queue
160
+ request = Net::HTTP::Post.new("/")
161
+ request_attempt = Logtail::LogDevices::HTTP::RequestAttempt.new(request)
162
+ request_attempt.attempted!
163
+ req_queue.enq(request_attempt)
164
+
165
+ # Start a HTTP connection to test the method directly
166
+ http = http_device.send(:build_http)
167
+ http.start do |conn|
168
+ result = http_device.send(:deliver_requests, conn)
169
+ expect(result).to eq(false)
170
+ end
171
+
172
+ expect(req_queue.size).to eq(1)
173
+
174
+ # Start a HTTP connection to test the method directly
175
+ http = http_device.send(:build_http)
176
+ http.start do |conn|
177
+ result = http_device.send(:deliver_requests, conn)
178
+ expect(result).to eq(false)
179
+ end
180
+
181
+ # Ensure the request gets discards after 3 attempts
182
+ expect(req_queue.size).to eq(0)
183
+ end
184
+ end
185
+ end