logtail-ruby 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.
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