td-logger 0.3.24 → 0.3.25

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,37 @@
1
+ require 'spec_helper'
2
+ require "rack/mock"
3
+ require 'td/logger/agent/rack'
4
+
5
+ describe TreasureData::Logger::Agent::Rack::Hook do
6
+ def app
7
+ @app ||= Rack::Builder.new {
8
+ use TreasureData::Logger::Agent::Rack::Hook
9
+ run lambda {|env| [200, {}, ["body"]]}
10
+ }.to_app
11
+ end
12
+
13
+ let(:dummy_object) { double.as_null_object }
14
+
15
+ subject { Rack::MockRequest.new(app).get("/") }
16
+
17
+ describe ".before" do
18
+ it do
19
+ TreasureData::Logger::Agent::Rack::Hook.before do |env|
20
+ dummy_object.called_at_before!(env)
21
+ end
22
+ expect(subject.status).to eq 200
23
+ expect(dummy_object).to have_received(:called_at_before!).once
24
+ end
25
+ end
26
+
27
+ describe ".after" do
28
+ it do
29
+ TreasureData::Logger::Agent::Rack::Hook.after do |env, result|
30
+ dummy_object.called_at_after!
31
+ result[0] = 201
32
+ end
33
+ expect(subject.status).to eq 201
34
+ expect(dummy_object).to have_received(:called_at_after!).once
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,155 @@
1
+ require 'spec_helper'
2
+ require 'fileutils'
3
+ require 'logger'
4
+
5
+ TMP_DIR = REPO_ROOT.join("tmp")
6
+ FileUtils.rm_rf(TMP_DIR)
7
+
8
+ class Rails
9
+ def self.configuration
10
+ self.new
11
+ end
12
+
13
+ def self.logger
14
+ Logger.new(STDOUT)
15
+ end
16
+
17
+ def self.root
18
+ TMP_DIR
19
+ end
20
+
21
+ def self.env
22
+ "test"
23
+ end
24
+
25
+ def middleware
26
+ self
27
+ end
28
+
29
+ def use(mod)
30
+ end
31
+ end
32
+
33
+ class ActionController
34
+ class Base
35
+ end
36
+ end
37
+
38
+ class ActiveSupport
39
+ class TimeWithZone
40
+ end
41
+ end
42
+
43
+ require 'td/logger/agent/rails' # NOTE: should be require after Rails goodies mocked
44
+
45
+ describe TreasureData::Logger::Agent::Rails do
46
+ describe "#init" do
47
+ shared_examples_for "setup Rails hooks" do
48
+ it "returns true" do
49
+ expect(subject).to eq true
50
+ end
51
+
52
+ it "middleware setup" do
53
+ expect(rails).to receive(:middleware).and_return(rails);
54
+ subject
55
+ end
56
+
57
+ it "Rack hooks register" do
58
+ expect(TreasureData::Logger::Agent::Rack::Hook).to receive(:before)
59
+ subject
60
+ end
61
+
62
+ it "TreasureData::Logger.event.attribute.clear registered on before hook" do
63
+ expect { subject }.to change{
64
+ # TODO: should be refactor later @@before
65
+ TreasureData::Logger::Agent::Rack::Hook.class_variable_get(:@@before).length
66
+ }.by(1)
67
+ end
68
+
69
+ it "registerd before hook called TreasureData::Logger.event.attribtue.clear" do
70
+ subject # for register hooks
71
+ null = double.as_null_object
72
+ allow(TreasureData::Logger).to receive(:event).and_return(null)
73
+ allow(null).to receive(:attribtue).and_return(null)
74
+
75
+ # TODO: should be refactor later @@before
76
+ TreasureData::Logger::Agent::Rack::Hook.class_variable_get(:@@before).last.call
77
+ expect(null).to have_received(:clear)
78
+ end
79
+
80
+ it "mixin ControllerExtension" do
81
+ expect(TreasureData::Logger::Agent::Rails::ControllerExtension).to receive(:init)
82
+ subject
83
+ end
84
+ end
85
+
86
+ let(:config) { TreasureData::Logger::Agent::Rails::Config.new }
87
+ let(:rails) { Rails.new }
88
+
89
+ before { allow(TreasureData::Logger::Agent::Rails::Config).to receive(:init).and_return(config) }
90
+ before do
91
+ fixture_of_methods.each do |method, value|
92
+ allow(config).to receive(method).and_return(value)
93
+ end
94
+ end
95
+
96
+ subject { TreasureData::Logger::Agent::Rails.init(rails) }
97
+
98
+ context "config.disable = true" do
99
+ let(:fixture_of_methods) { {disabled: true} }
100
+
101
+ it { expect(subject).to eq false }
102
+ it { expect(::TreasureData::Logger).to receive(:open_null).with(no_args); subject }
103
+ end
104
+
105
+ context "config.test_mode? == true" do
106
+ let(:fixture_of_methods) { {disabled: false, "test_mode?" => true} }
107
+
108
+ it_behaves_like "setup Rails hooks"
109
+ it { expect(::TreasureData::Logger).to receive(:open_test).with(no_args); subject }
110
+ end
111
+
112
+ context "config.agent_mode? == true" do
113
+ let(:fixture_of_methods) { {
114
+ disabled: false,
115
+ "test_mode?" => false,
116
+ "agent_mode?" => true,
117
+ tag: "tag",
118
+ agent_host: "agent-host",
119
+ agent_port: "9999",
120
+ debug_mode: "debug"
121
+ } }
122
+
123
+ it_behaves_like "setup Rails hooks"
124
+ it {
125
+ expect(::TreasureData::Logger).to receive(:open_agent).with(config.tag, {host: config.agent_host, port: config.agent_port, debug: config.debug_mode})
126
+ subject
127
+ }
128
+ end
129
+
130
+ context "config.agent_mode? and test_mode? both false" do
131
+ let(:fixture_of_methods) { {
132
+ disabled: false,
133
+ "test_mode?" => false,
134
+ "agent_mode?" => false,
135
+ apikey: "APIKEY",
136
+ database: "DB",
137
+ auto_create_table: true,
138
+ debug_mode: "debug"
139
+ } }
140
+
141
+ it_behaves_like "setup Rails hooks"
142
+ it {
143
+ expect(::TreasureData::Logger).to receive(:open).with(config.database, apikey: config.apikey, auto_create_table: config.auto_create_table, debug: config.debug_mode)
144
+ subject
145
+ }
146
+ end
147
+ end
148
+ end
149
+
150
+
151
+ describe ActiveSupport::TimeWithZone do
152
+ it 'has to_msgpack' do
153
+ expect(ActiveSupport::TimeWithZone.method_defined?(:to_msgpack)).to eq(true)
154
+ end
155
+ end
@@ -0,0 +1,136 @@
1
+
2
+ require 'spec_helper'
3
+
4
+ describe TreasureData::Logger::Event do
5
+ describe 'EventPreset' do
6
+ let(:test_logger) do
7
+ Fluent::Logger::TestLogger.new
8
+ end
9
+ before(:each) do
10
+ t = test_logger
11
+ TreasureData::Logger.module_eval do
12
+ class_variable_set(:@@logger, t)
13
+ end
14
+ TD.event.attribute.clear
15
+ end
16
+
17
+ describe 'action' do
18
+ context "`uid` filled" do
19
+ it do
20
+ expect(test_logger).to receive(:post).with(:doit, {:action=>"doit", :foo=>:bar, :uid=>"uid1"}).twice
21
+ TD.event.action(:doit, {:foo=>:bar}, "uid1")
22
+ TD.event.attribute[:uid] = "uid1"
23
+ TD.event.action(:doit, {:foo=>:bar})
24
+ end
25
+ end
26
+
27
+ context "`uid` unfilled" do
28
+ it do
29
+ expect { TD.event.action(:doit, {:foo=>:bar}, nil) }.to raise_error(ArgumentError)
30
+ end
31
+ end
32
+ end
33
+
34
+ describe 'register' do
35
+ context "`uid` filled" do
36
+ it do
37
+ expect(test_logger).to receive(:post).with(:register, {:action=>"register", :uid=>"uid1"}).twice
38
+ TD.event.register("uid1")
39
+ TD.event.attribute[:uid] = "uid1"
40
+ TD.event.register
41
+ end
42
+ end
43
+
44
+ context "`uid` unfilled" do
45
+ it do
46
+ expect { TD.event.register(nil) }.to raise_error(ArgumentError)
47
+ end
48
+ end
49
+ end
50
+
51
+ describe 'login' do
52
+ context "`uid` filled" do
53
+ it do
54
+ expect(test_logger).to receive(:post).with(:login, {:action=>"login", :uid=>"uid1"}).twice
55
+ TD.event.login("uid1")
56
+ TD.event.attribute[:uid] = "uid1"
57
+ TD.event.login
58
+ end
59
+ end
60
+
61
+ context "`uid` unfilled" do
62
+ it do
63
+ expect { TD.event.login(nil) }.to raise_error(ArgumentError)
64
+ end
65
+ end
66
+ end
67
+
68
+ describe 'pay' do
69
+ context "`uid` filled" do
70
+ it do
71
+ expect(test_logger).to receive(:post).with(:pay, {:action=>"pay", :category=>"cat", :sub_category=>"subcat", :name=>"name", :price=>1980, :count=>1, :uid=>"uid1"}).twice
72
+ TD.event.pay("cat", "subcat", "name", 1980, 1, "uid1")
73
+ TD.event.attribute[:uid] = "uid1"
74
+ TD.event.pay("cat", "subcat", "name", 1980, 1)
75
+ end
76
+ end
77
+
78
+ context "`uid` unfilled" do
79
+ it do
80
+ expect { TD.event.pay("cat", "subcat", "name", 1980, 1, nil) }.to raise_error(ArgumentError)
81
+ end
82
+ end
83
+ end
84
+ end
85
+
86
+ describe "Event" do
87
+ let(:attrs) { {default: "attr"} }
88
+ let(:event) { TreasureData::Logger::Event.new }
89
+
90
+ describe "#post" do
91
+ subject { event.post(action, record) }
92
+ let(:action) { "act" }
93
+ let(:record) { {hello: "hello"} }
94
+
95
+ context "with default attributes" do
96
+ before { event.attribute = attrs }
97
+
98
+ it "invoke TreasureData::Logger.post" do
99
+ expect(TreasureData::Logger).to receive(:post).with(action, attrs.merge(record))
100
+ subject
101
+ end
102
+ end
103
+
104
+ context "without default attributes" do
105
+ it "invoke TreasureData::Logger.post" do
106
+ expect(TreasureData::Logger).to receive(:post).with(action, record)
107
+ subject
108
+ end
109
+ end
110
+ end
111
+
112
+ describe "#post_with_time" do
113
+ subject { event.post_with_time(action, record, time) }
114
+ let(:action) { "act" }
115
+ let(:record) { {hello: "hello"} }
116
+ let(:time) { Time.now }
117
+
118
+ context "with default attributes" do
119
+ before { event.attribute = attrs }
120
+
121
+ it "invoke TreasureData::Logger.post_with_time" do
122
+ expect(TreasureData::Logger).to receive(:post_with_time).with(action, attrs.merge(record), time)
123
+ subject
124
+ end
125
+ end
126
+
127
+ context "without default attributes" do
128
+ it "invoke TreasureData::Logger.post_with_time" do
129
+ expect(TreasureData::Logger).to receive(:post_with_time).with(action, record, time)
130
+ subject
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
136
+
@@ -0,0 +1,669 @@
1
+
2
+ require 'spec_helper'
3
+
4
+ describe TreasureData::Logger::TreasureDataLogger do
5
+ context 'init' do
6
+ it 'with apikey' do
7
+ td = TreasureData::Logger::TreasureDataLogger.new('db1', :apikey => 'test_1')
8
+ expect(td.instance_variable_get(:@client).api.apikey).to eq('test_1')
9
+ expect(td.instance_variable_get(:@client).api.instance_variable_get(:@ssl)).to eq(false)
10
+ end
11
+
12
+ it 'with apikey and use_ssl' do
13
+ td = TreasureData::Logger::TreasureDataLogger.new('db1', :apikey => 'test_1', :use_ssl => true)
14
+ expect(td.instance_variable_get(:@client).api.apikey).to eq('test_1')
15
+ expect(td.instance_variable_get(:@client).api.instance_variable_get(:@ssl)).to eq(true)
16
+ end
17
+
18
+ it 'with apikey and ssl' do
19
+ td = TreasureData::Logger::TreasureDataLogger.new('db1', :apikey => 'test_1', :ssl => true)
20
+ expect(td.instance_variable_get(:@client).api.apikey).to eq('test_1')
21
+ expect(td.instance_variable_get(:@client).api.instance_variable_get(:@ssl)).to eq(true)
22
+ end
23
+
24
+ it 'with apikey and HTTP endpoint' do
25
+ td = TreasureData::Logger::TreasureDataLogger.new('db1', :apikey => 'test_1', :endpoint => "http://idontexi.st")
26
+ expect(td.instance_variable_get(:@client).api.apikey).to eq('test_1')
27
+ expect(td.instance_variable_get(:@client).api.instance_variable_get(:@host)).to eq("idontexi.st")
28
+ expect(td.instance_variable_get(:@client).api.instance_variable_get(:@port)).to eq(80)
29
+ expect(td.instance_variable_get(:@client).api.instance_variable_get(:@ssl)).to eq(false)
30
+ end
31
+
32
+ it 'with apikey and HTTPS endpoint' do
33
+ td = TreasureData::Logger::TreasureDataLogger.new('db1', :apikey => 'test_1', :endpoint => "https://idontexi.st")
34
+ expect(td.instance_variable_get(:@client).api.apikey).to eq('test_1')
35
+ expect(td.instance_variable_get(:@client).api.instance_variable_get(:@host)).to eq("idontexi.st")
36
+ expect(td.instance_variable_get(:@client).api.instance_variable_get(:@port)).to eq(443)
37
+ expect(td.instance_variable_get(:@client).api.instance_variable_get(:@ssl)).to eq(true)
38
+ end
39
+
40
+ it 'db config' do
41
+ td = TreasureData::Logger::TreasureDataLogger.new('db1', :apikey => 'test_1')
42
+ time = Time.now
43
+ expect(td).to receive(:add).with('db1', 'table1', {:foo => :bar, :time => time.to_i})
44
+ td.post_with_time('table1', {:foo => :bar}, time)
45
+ end
46
+
47
+ it 'fluent-logger-td compat' do
48
+ td = TreasureData::Logger::TreasureDataLogger.new('db1', :apikey => 'test_2')
49
+ time = Time.now
50
+ expect(td).to receive(:add).with('overwrite', 'table1', {:foo => :bar, :time => time.to_i})
51
+ td.post_with_time('overwrite.table1', {:foo => :bar}, time)
52
+ end
53
+
54
+ ## TODO this causes real upload
55
+ #it 'success' do
56
+ # td = TreasureData::Logger::TreasureDataLogger.new('db1', :apikey => 'test_3')
57
+ # td.post('valid', {}).should == true
58
+ #end
59
+ end
60
+
61
+ context 'validate' do
62
+ it 'validate table name' do
63
+ td = TreasureData::Logger::TreasureDataLogger.new('db1', :apikey => 'test_4')
64
+ expect {
65
+ td.post('invalid-name', {})
66
+ }.to raise_error(RuntimeError)
67
+ expect {
68
+ td.post('', {})
69
+ }.to raise_error(RuntimeError)
70
+ expect {
71
+ td.post('9', {})
72
+ }.to raise_error(RuntimeError)
73
+ end
74
+
75
+ it 'validate database name' do
76
+ td = TreasureData::Logger::TreasureDataLogger.new('invalid-db-name', :apikey => 'test_5')
77
+ expect {
78
+ td.post('table', {})
79
+ }.to raise_error(RuntimeError)
80
+ end
81
+ end
82
+
83
+ it "raise error if `apikey` option is missing" do
84
+ expect{ described_class.new("dummy-tag", {}) }.to raise_error(ArgumentError)
85
+ end
86
+
87
+ describe "#logger" do
88
+ let(:tag_prefix) { "dummy" }
89
+
90
+ subject { described_class.new(tag_prefix, {apikey: "dummy"}.merge(options)).logger }
91
+
92
+ context "`debug` option given" do
93
+ let(:options) { {debug: true} }
94
+
95
+ it { expect(subject.level).to eq ::Logger::DEBUG }
96
+ end
97
+
98
+ context "not `debug` option given" do
99
+ let(:options) { {} }
100
+
101
+ it { expect(subject.level).to eq ::Logger::INFO }
102
+ end
103
+ end
104
+
105
+ describe "#post_with_time" do
106
+ let(:td) { described_class.new(prefix, {apikey: 'test_1'}.merge(options)) }
107
+ let(:tag) { "foo.bar" }
108
+ let(:time) { Time.now }
109
+ let(:prefix) { "dummy" }
110
+ let(:record) { {greeting: "hello", time: 1234567890} }
111
+ let(:options) { {} }
112
+
113
+ subject { td.post_with_time(tag, record, time) }
114
+
115
+ describe "db and table" do
116
+ context "no `tag_prefix` option given" do
117
+ let(:prefix) { "" }
118
+
119
+ it "`db` and `table` determine with tag" do
120
+ db, table = tag.split(".")[-2, 2]
121
+ allow(td).to receive(:add).with(db, table, record)
122
+ subject
123
+ end
124
+ end
125
+
126
+ context "`tag_prefix` option given" do
127
+ let(:prefix) { "prefix" }
128
+
129
+ it "`db` and `table` determine with tag and tag_prefix" do
130
+ db, table = "#{prefix}.#{tag}".split(".")[-2, 2]
131
+ allow(td).to receive(:add).with(db, table, record)
132
+ subject
133
+ end
134
+ end
135
+ end
136
+
137
+ describe "inject time to record" do
138
+ context "record has no `:time` key" do
139
+ let(:record) { {greeting: "hello"} }
140
+
141
+ it "fill-in :time key with `time` argument" do
142
+ allow(td).to receive(:add).with(anything, anything, record.merge(time: time.to_i))
143
+ subject
144
+ end
145
+ end
146
+
147
+ context "record has `:time` key" do
148
+ let(:record) { {greeting: "hello", time: 1234567890 } }
149
+
150
+ it do
151
+ allow(td).to receive(:add).with(anything, anything, record)
152
+ subject
153
+ end
154
+ end
155
+ end
156
+ end
157
+
158
+ describe "#add" do
159
+ let(:td) { described_class.new(prefix, {apikey: 'test_1'}) }
160
+ let(:prefix) { "prefix" }
161
+ let(:db) { "database" }
162
+ let(:table) { "table" }
163
+ let(:valid_message) { {"foo" => "FOO", "bar" => "BAR"} }
164
+
165
+ before do
166
+ # don't try to upload data causing by `at_exit { close }` that registered at TreasureDataLogger#initialize
167
+ # this hack causes "zlib(finalizer): Zlib::GzipWriter object must be closed explicitly." warning, but ignoreable for our tests..
168
+ allow_any_instance_of(TreasureData::Logger::TreasureDataLogger).to receive(:at_exit).and_return(true)
169
+ end
170
+
171
+ subject { td.send(:add, db, table, message) } # NOTE: `add` is private method
172
+
173
+ describe "message type" do
174
+ shared_examples_for "should fail `add` because not a hash" do
175
+ it { is_expected.to eq false }
176
+ it "logging error" do
177
+ expect(td.logger).to receive(:error).with(/TreasureDataLogger: record must be a Hash:/)
178
+ subject
179
+ end
180
+ end
181
+
182
+ context "string" do
183
+ let(:message) { "string" }
184
+ it_behaves_like "should fail `add` because not a hash"
185
+ end
186
+
187
+ context "array" do
188
+ let(:message) { ["hello"] }
189
+ it_behaves_like "should fail `add` because not a hash"
190
+ end
191
+
192
+ context "fixnum" do
193
+ let(:message) { 42 }
194
+ it_behaves_like "should fail `add` because not a hash"
195
+ end
196
+
197
+ context "hash" do
198
+ let(:message) { {foo: 42} }
199
+
200
+ it { is_expected.to eq true }
201
+ end
202
+ end
203
+
204
+ describe "queue is full" do
205
+ let(:queue) { td.instance_variable_get(:@queue) }
206
+ let(:max) { td.instance_variable_get(:@queue_limit) }
207
+ let(:message) { valid_message }
208
+
209
+ before do
210
+ (max + 1).times { queue << [db, table, {"foo" => "bar"}.to_msgpack] }
211
+ end
212
+
213
+ it { is_expected.to eq false }
214
+ it do
215
+ expect(td.logger).to receive(:error).with(/TreasureDataLogger: queue length exceeds limit. can't add new event log:/)
216
+ subject
217
+ end
218
+ end
219
+
220
+ describe "if `to_msgpack` failed" do
221
+ let(:message) { valid_message }
222
+
223
+ before { allow(td).to receive(:to_msgpack) { raise "something error" } }
224
+
225
+ it { is_expected.to eq false }
226
+ it do
227
+ expect(td.logger).to receive(:error).with(/TreasureDataLogger: Can't convert to msgpack:/)
228
+ subject
229
+ end
230
+ end
231
+
232
+ describe "buffer size/cardinality check" do
233
+ let(:max) { TreasureData::Logger::TreasureDataLogger::MAX_KEY_CARDINALITY }
234
+ let(:warn) { TreasureData::Logger::TreasureDataLogger::WARN_KEY_CARDINALITY }
235
+ let(:map_key) { [db, table] }
236
+ let(:record_keys) { max.times }
237
+ let(:message) { record_keys.inject({}){|r, n| r[n] = n; r } }
238
+
239
+ context "buffer size == MAX_KEY_CARDINALITY" do
240
+ it { is_expected.to eq true }
241
+ end
242
+
243
+ context "buffer size > MAX_KEY_CARDINALITY" do
244
+ let(:record_keys) { (max + 1).times }
245
+
246
+ it { is_expected.to eq false }
247
+ it do
248
+ expect(td.logger).to receive(:error).with(/TreasureDataLogger: kind of keys in a buffer exceeds/)
249
+ expect(td.instance_variable_get(:@map)).to receive(:delete).with(map_key)
250
+ subject
251
+ end
252
+ end
253
+
254
+ context "before <= WARN && WARN < after" do
255
+ let(:record_keys) { (warn + 1).times } # just one key
256
+
257
+ it do
258
+ expect(td.logger).to receive(:warn).with("TreasureDataLogger: kind of keys in a buffer exceeds #{warn} which is too large. please check the schema design.")
259
+ subject
260
+ end
261
+ end
262
+
263
+ context "buffer.size > @chunk_limit" do
264
+ let(:chunk_limit) { 0 }
265
+
266
+ before { td.instance_variable_set(:@chunk_limit, chunk_limit) }
267
+
268
+ it do
269
+ expect(td.instance_variable_get(:@queue)).to receive(:<<).with([db, table, anything])
270
+ expect(td.instance_variable_get(:@map)).to receive(:delete).with(map_key)
271
+ expect(td.instance_variable_get(:@cond)).to receive(:signal)
272
+ subject
273
+ end
274
+ end
275
+ end
276
+ end
277
+
278
+ describe "#to_msgpack" do
279
+ let(:td) { described_class.new("prefix", {apikey: 'test_1'}) }
280
+
281
+ subject { td.send(:to_msgpack, target) }
282
+
283
+ shared_examples_for "original to_msgpack only invoked" do
284
+ after { subject }
285
+
286
+ it { expect(target).to receive(:to_msgpack) }
287
+ it { expect(JSON).to_not receive(:dump) }
288
+ it { expect(JSON).to_not receive(:load) }
289
+ end
290
+
291
+ shared_examples_for "JSON.load/dump then to_msgpack invoke" do
292
+ after { subject }
293
+
294
+ it { expect(JSON).to receive(:dump).with(target) }
295
+ it { expect(JSON).to receive(:load) }
296
+ end
297
+
298
+ context "have to_msgpack objects" do
299
+ # see also for official supported objects: https://github.com/msgpack/msgpack-ruby/blob/master/ext/msgpack/core_ext.c
300
+ context "string" do
301
+ let(:target) { "foobar" }
302
+ it_behaves_like "original to_msgpack only invoked"
303
+ end
304
+
305
+ context "array" do
306
+ let(:target) { [1] }
307
+ it_behaves_like "original to_msgpack only invoked"
308
+ end
309
+
310
+ context "hash" do
311
+ let(:target) { {foo: "foo"} }
312
+ it_behaves_like "original to_msgpack only invoked"
313
+ end
314
+
315
+ context "time" do
316
+ let(:target) { Time.now }
317
+ it_behaves_like "original to_msgpack only invoked"
318
+ end
319
+ end
320
+
321
+ context "have not to_msgpack objects" do
322
+ context "date" do
323
+ let(:target) { Date.today }
324
+ it_behaves_like "JSON.load/dump then to_msgpack invoke"
325
+ end
326
+
327
+ context "class" do
328
+ let(:target) { Class.new }
329
+ it_behaves_like "JSON.load/dump then to_msgpack invoke"
330
+ end
331
+ end
332
+ end
333
+
334
+ describe "#upload" do
335
+ let(:td) { described_class.new(prefix, options) }
336
+ let(:prefix) { "prefix" }
337
+ let(:options) { {apikey: "apikey"} }
338
+ let(:db) { "database" }
339
+ let(:table) { "table" }
340
+ let(:message) { {foo: "FOO"} }
341
+
342
+ subject { td.send(:upload, db, table, message.to_msgpack) } # NOTE: `upload` is private method
343
+
344
+ describe "TreasureDataLogger::Client#import success" do
345
+ context 'unuse unique_key' do
346
+ it do
347
+ expect(td.instance_variable_get(:@client)).to receive(:import).with(db, table, "msgpack.gz", anything, anything, nil)
348
+ subject
349
+ end
350
+ end
351
+
352
+ context 'use unique_key' do
353
+ let(:options) { {apikey: "apike", use_unique_key: true} }
354
+
355
+ it do
356
+ expect(td.instance_variable_get(:@client)).to receive(:import).with(db, table, "msgpack.gz", anything, anything, kind_of(String))
357
+ subject
358
+ end
359
+ end
360
+ end
361
+
362
+ describe "TreasureDataLogger::NotFoundError" do
363
+ let(:client) { td.instance_variable_get(:@client) }
364
+
365
+ before do
366
+ # NOTE: raise "not found" client.import at first called, but second and later call not raise
367
+ queue = [true]
368
+ allow(client).to receive(:import){
369
+ raise TreasureData::NotFoundError, "not found" if queue.shift
370
+ true
371
+ }
372
+ end
373
+
374
+ context "auto_create_table options disabled" do
375
+ let(:options) { {apikey: "apikey", auto_create_table: false} }
376
+
377
+ it do
378
+ expect{ subject }.to raise_error(TreasureData::NotFoundError)
379
+ end
380
+ end
381
+
382
+ context "auto_create_table options enabled" do
383
+ let(:options) { {apikey: "apikey", auto_create_table: true} }
384
+
385
+ it "try to create table with exists database" do
386
+ expect(client).to receive(:create_log_table)
387
+ subject
388
+ end
389
+
390
+ it "try to create table with no exists databae" do
391
+ queue = [true]
392
+ allow(client).to receive(:create_log_table){ raise TreasureData::NotFoundError, "not found again" if queue.shift}
393
+ expect(client).to receive(:create_database)
394
+ subject
395
+ end
396
+
397
+ it "retry @client.import after create table" do
398
+ expect(client).to receive(:create_log_table)
399
+ expect(client).to receive(:import).twice # second call is retry
400
+ subject
401
+ end
402
+ end
403
+ end
404
+ end
405
+
406
+ describe "#flush" do
407
+ let(:td) { described_class.new("prefix", {apikey: 'test_1'}) }
408
+ let(:db) { "database" }
409
+ let(:table) { "table" }
410
+
411
+ subject { td.flush }
412
+
413
+ before { allow(td).to receive(:try_flush) } # do not call try_flush actually, try_flush test is in other place
414
+
415
+ it "lock mutex and release" do
416
+ expect(td.instance_variable_get(:@mutex)).to receive(:lock)
417
+ expect(td.instance_variable_get(:@mutex)).to receive(:unlock)
418
+ subject
419
+ end
420
+
421
+ it "call #try_flush" do
422
+ expect(td).to receive(:try_flush).with(no_args)
423
+ subject
424
+ end
425
+
426
+ it "@map to be flushed (enqueue)" do
427
+ buffer = TreasureData::Logger::TreasureDataLogger::Buffer.new
428
+ td.instance_variable_set(:@map, {[db, table] => buffer})
429
+ expect(td.instance_variable_get(:@queue)).to receive(:<<).with([db, table, buffer.flush!])
430
+ subject
431
+ end
432
+
433
+ it "rescue anything error" do
434
+ allow(td).to receive(:try_flush){ raise "something error" }
435
+ expect(td.instance_variable_get(:@logger)).to receive(:error).with(/Unexpected error at flush:/)
436
+ expect(td.instance_variable_get(:@logger)).to receive(:info).at_least(1) # backtrace
437
+ expect(td.instance_variable_get(:@mutex)).to_not be_locked
438
+ subject
439
+ end
440
+ end
441
+
442
+ describe "#try_flush" do
443
+ let(:td) { described_class.new("prefix", {apikey: 'test_1'}) }
444
+ let(:db) { "database" }
445
+ let(:table) { "table" }
446
+
447
+ let(:mutex) { td.instance_variable_get(:@mutex) }
448
+ let(:queue) { td.instance_variable_get(:@queue) }
449
+ let(:logger) { td.instance_variable_get(:@logger) }
450
+
451
+ let(:buffer) { TreasureData::Logger::TreasureDataLogger::Buffer.new }
452
+
453
+ before { allow_any_instance_of(TreasureData::Logger::TreasureDataLogger).to receive(:at_exit).and_return(true) }
454
+ before { mutex.lock } # try_flush is expected mutex locked that called
455
+ after { mutex.unlock }
456
+
457
+ subject { td.send(:try_flush) }
458
+
459
+ describe "force flush small buffers if queue is empty" do
460
+
461
+ before { allow(td).to receive(:upload) } # do not call `upload` actually, that method test is in other place
462
+ before { td.instance_variable_set(:@map, {[db, table] => buffer}) } # dummy data append
463
+
464
+ context "queue is empty" do
465
+ it do
466
+ expect(queue).to receive(:<<).with([db, table, anything])
467
+ subject
468
+ end
469
+ end
470
+
471
+ context "queue is not empty" do
472
+ before { queue << [db, table, buffer.flush!] }
473
+
474
+ it do
475
+ expect(queue).to_not receive(:<<).with([db, table, anything])
476
+ subject
477
+ end
478
+ end
479
+ end
480
+
481
+ context "queue and on-memory buffer are empty" do
482
+ before { allow(td).to receive(:upload) } # do not call `upload` actually, that method test is in other place
483
+
484
+ it "return false" do
485
+ is_expected.to eq false
486
+ end
487
+
488
+ it "don't call #upload" do
489
+ expect(td).to_not receive(:upload)
490
+ subject
491
+ end
492
+ end
493
+
494
+ context "some data having" do
495
+ describe "queue to upload" do
496
+ before do
497
+ times.times do |n|
498
+ buf = TreasureData::Logger::TreasureDataLogger::Buffer.new
499
+ buf.append(n)
500
+ queue << [db, table, buf.flush!]
501
+ end
502
+ end
503
+
504
+ context "100 queue" do
505
+ let(:times) { 100 }
506
+
507
+ it "call upload 100 times" do
508
+ expect(td).to receive(:upload).exactly(times)
509
+ subject
510
+ end
511
+ end
512
+ end
513
+
514
+ describe "upload fail" do
515
+ before do
516
+ 100.times do |n|
517
+ buf = TreasureData::Logger::TreasureDataLogger::Buffer.new
518
+ buf.append(n)
519
+ queue << [db, table, buf.flush!]
520
+ end
521
+ end
522
+
523
+ # NOTE: @retry_limit is hard coded as 12
524
+ before do
525
+ times = error_times.times.to_a
526
+ allow(td).to receive(:upload){
527
+ if n = times.shift
528
+ raise "something error (#{n})"
529
+ else
530
+ true
531
+ end
532
+ }
533
+ end
534
+
535
+ context "errors 0 times" do
536
+ let(:error_times) { 0 }
537
+
538
+ it { expect(logger).to_not receive(:info); subject }
539
+ it { expect(logger).to_not receive(:error); subject }
540
+ end
541
+
542
+ context "errors 1 times" do
543
+ let(:error_times) { 1 }
544
+
545
+ it do
546
+ expect(queue).to_not receive(:clear)
547
+ subject
548
+ end
549
+
550
+ it do
551
+ expect(logger).to receive(:error).with(/Failed to upload event logs to Treasure Data, retrying:/)
552
+ subject
553
+ end
554
+
555
+ it "not trash" do
556
+ expect(logger).to_not receive(:error).with(/Failed to upload event logs to Treasure Data, trashed:/)
557
+ subject
558
+ end
559
+
560
+ it "error_count should reset" do
561
+ expect(td.instance_variable_get(:@error_count)).to eq 0
562
+ subject
563
+ end
564
+ end
565
+
566
+ context "errors 20 times" do
567
+ let(:error_times) { 20 }
568
+
569
+ it do
570
+ skip "try_flush has bug?"
571
+ expect(queue).to receive(:clear)
572
+ subject
573
+ end
574
+
575
+ it do
576
+ skip "try_flush has bug?"
577
+ expect(logger).to receive(:error).with(/Failed to upload event logs to Treasure Data, retrying:/)
578
+ expect(logger).to receive(:error).with(/Failed to upload event logs to Treasure Data, trashed:/)
579
+ subject
580
+ end
581
+
582
+ it do
583
+ skip "try_flush has bug?"
584
+ expect(queue).to_not receive(:clear)
585
+ subject
586
+ end
587
+
588
+ it "output backtrace as INFO level" do
589
+ skip "try_flush has bug?"
590
+ expect(logger).to receive(:info).at_least(1) # backtrace
591
+ subject
592
+ end
593
+
594
+ it "error_count should reset" do
595
+ skip "try_flush has bug?"
596
+ expect(td.instance_variable_get(:@error_count)).to eq 0
597
+ subject
598
+ end
599
+ end
600
+ end
601
+ end
602
+ end
603
+
604
+ describe "#close" do
605
+ let(:td) { described_class.new("prefix", {apikey: 'test_1'}) }
606
+ let(:db) { "database" }
607
+ let(:table) { "table" }
608
+ let(:queue) { td.instance_variable_get(:@queue) }
609
+ let(:logger) { td.instance_variable_get(:@logger) }
610
+
611
+ subject { td.close }
612
+
613
+ context "queue is empty" do
614
+ it do
615
+ expect(td).to_not receive(:upload)
616
+ subject
617
+ end
618
+ end
619
+
620
+ context "queue is not empty" do
621
+ before { queue << [db, table, TreasureData::Logger::TreasureDataLogger::Buffer.new.flush!] }
622
+
623
+ context "upload success" do
624
+ it do
625
+ expect(td).to receive(:upload)
626
+ subject
627
+ end
628
+ end
629
+
630
+ context "upload fail" do
631
+ it do
632
+ allow(td).to receive(:upload){ raise "something error"}
633
+ expect(logger).to receive(:error).with(/Failed to upload event logs to Treasure Data, trashed:/)
634
+ subject
635
+ end
636
+ end
637
+ end
638
+
639
+ context "@map is empty" do
640
+ it do
641
+ expect(td).to_not receive(:upload)
642
+ subject
643
+ end
644
+ end
645
+
646
+ context "@map is not empty" do
647
+ before do
648
+ buffer = TreasureData::Logger::TreasureDataLogger::Buffer.new
649
+ td.instance_variable_set(:@map, {[db, table] => buffer})
650
+ end
651
+
652
+ context "upload success" do
653
+ it do
654
+ expect(td).to receive(:upload)
655
+ subject
656
+ end
657
+ end
658
+
659
+ context "upload fail" do
660
+ it do
661
+ allow(td).to receive(:upload){ raise "something error" }
662
+ expect(logger).to receive(:error).with(/Failed to upload event logs to Treasure Data, trashed:/)
663
+ subject
664
+ end
665
+ end
666
+ end
667
+ end
668
+ end
669
+