fluent-plugin-bigquery 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +18 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +13 -0
- data/README.md +140 -0
- data/Rakefile +11 -0
- data/fluent-plugin-bigquery.gemspec +29 -0
- data/lib/fluent/plugin/bigquery/load_request_body_wrapper.rb +173 -0
- data/lib/fluent/plugin/bigquery/version.rb +6 -0
- data/lib/fluent/plugin/out_bigquery.rb +296 -0
- data/test/helper.rb +33 -0
- data/test/test_load_request_body_wrapper.rb +190 -0
- metadata +157 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 793b3fbd0189044538497bcef9dc244adf987d86
|
4
|
+
data.tar.gz: 115ff35e20bf3e3fe58e54978f7e659678e14a17
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: d230732372df108fbcdf59aec5485f2837028531fb1cb9edf0582dfc31654a747bb35494e9de646067e295960709e778d9e69f41f80f1a8810e122b952c971e4
|
7
|
+
data.tar.gz: fc7dbd59a34a44f9f8f2aef3829fc352636c58a5fad3d8ddbf661368c912d8a2ba5d6f2080c66fc85c32f615916c3ad980f3e4e3a9b6e967bc8bfc1fc5712e2c
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
Copyright (c) 2012- TAGOMORI Satoshi
|
2
|
+
|
3
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
you may not use this file except in compliance with the License.
|
5
|
+
You may obtain a copy of the License at
|
6
|
+
|
7
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
|
9
|
+
Unless required by applicable law or agreed to in writing, software
|
10
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
See the License for the specific language governing permissions and
|
13
|
+
limitations under the License.
|
data/README.md
ADDED
@@ -0,0 +1,140 @@
|
|
1
|
+
# fluent-plugin-webhdfs
|
2
|
+
|
3
|
+
Fluentd output plugin to load/insert data into Google BigQuery.
|
4
|
+
|
5
|
+
* insert data over streaming inserts
|
6
|
+
* for continuous real-time insertions, under many limitations
|
7
|
+
* https://developers.google.com/bigquery/streaming-data-into-bigquery#usecases
|
8
|
+
* (NOT IMPLEMENTED) load data
|
9
|
+
* for data loading as batch jobs, for big amount of data
|
10
|
+
* https://developers.google.com/bigquery/loading-data-into-bigquery
|
11
|
+
|
12
|
+
Current version of this plugin supports Google API with Service Account Authentication, and does not support OAuth.
|
13
|
+
|
14
|
+
## Configuration
|
15
|
+
|
16
|
+
### Streming inserts
|
17
|
+
|
18
|
+
For service account authentication, generate service account private key file and email key, then upload private key file onto your server.
|
19
|
+
|
20
|
+
Configure insert specifications with target table schema, with your credentials. This is minimum configurations:
|
21
|
+
|
22
|
+
```apache
|
23
|
+
<match dummy>
|
24
|
+
type bigquery
|
25
|
+
|
26
|
+
method insert # default
|
27
|
+
|
28
|
+
email xxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxx@developer.gserviceaccount.com
|
29
|
+
private_key_path /home/username/.keys/00000000000000000000000000000000-privatekey.p12
|
30
|
+
# private_key_passphrase notasecret # default
|
31
|
+
|
32
|
+
project yourproject_id
|
33
|
+
dataset yourdataset_id
|
34
|
+
table tablename
|
35
|
+
|
36
|
+
time_format %s
|
37
|
+
time_field time
|
38
|
+
|
39
|
+
field_integer time,status,bytes
|
40
|
+
field_string rhost,vhost,path,method,protocol,agent,referer
|
41
|
+
field_float requestime
|
42
|
+
field_boolean bot_access,loginsession
|
43
|
+
</match>
|
44
|
+
```
|
45
|
+
|
46
|
+
For high rate inserts over streaming inserts, you should specify flush intervals and buffer chunk options:
|
47
|
+
|
48
|
+
```apache
|
49
|
+
<match dummy>
|
50
|
+
type bigquery
|
51
|
+
|
52
|
+
method insert # default
|
53
|
+
|
54
|
+
flush_interval 1 # flush as frequent as possible
|
55
|
+
|
56
|
+
buffer_chunk_records_limit 300 # default rate limit for users is 100
|
57
|
+
buffer_queue_limit 10240 # 1MB * 10240 -> 10GB!
|
58
|
+
|
59
|
+
num_threads 16
|
60
|
+
|
61
|
+
email xxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxx@developer.gserviceaccount.com
|
62
|
+
private_key_path /home/username/.keys/00000000000000000000000000000000-privatekey.p12
|
63
|
+
# private_key_passphrase notasecret # default
|
64
|
+
|
65
|
+
project yourproject_id
|
66
|
+
dataset yourdataset_id
|
67
|
+
tables accesslog1,accesslog2,accesslog3
|
68
|
+
|
69
|
+
time_format %s
|
70
|
+
time_field time
|
71
|
+
|
72
|
+
field_integer time,status,bytes
|
73
|
+
field_string rhost,vhost,path,method,protocol,agent,referer
|
74
|
+
field_float requestime
|
75
|
+
field_boolean bot_access,loginsession
|
76
|
+
</match>
|
77
|
+
```
|
78
|
+
|
79
|
+
Important options for high rate events are:
|
80
|
+
|
81
|
+
* `tables`
|
82
|
+
* 2 or more tables are available with ',' separator
|
83
|
+
* `out_bigquery` uses these tables for Table Sharding inserts
|
84
|
+
* these must have same schema
|
85
|
+
* `buffer_chunk_records_limit`
|
86
|
+
* number of records over streaming inserts API call is limited as 100, per second, per table
|
87
|
+
* default average rate limit is 100, and spike rate limit is 1000
|
88
|
+
* `out_bigquery` flushes buffer with 100 records for 1 inserts API call
|
89
|
+
* `buffer_queue_limit`
|
90
|
+
* BigQuery streaming inserts needs very small buffer chunks
|
91
|
+
* for high-rate events, `buffer_queue_limit` should be configured with big number
|
92
|
+
* Max 1GB memory may be used under network problem in default configuration
|
93
|
+
* `buffer_chunk_limit (default 1MB)` x `buffer_queue_limit (default 1024)`
|
94
|
+
* `num_threads`
|
95
|
+
* threads for insert api calls in parallel
|
96
|
+
* specify this option for 100 or more records per seconds
|
97
|
+
* 10 or more threads seems good for inserts over internet
|
98
|
+
* less threads may be good for Google Compute Engine instances (with low latency for BigQuery)
|
99
|
+
* `flush_interval`
|
100
|
+
* `1` is lowest value, without patches on Fluentd v0.10.41 or earlier
|
101
|
+
* see `patches` below
|
102
|
+
|
103
|
+
### patches
|
104
|
+
|
105
|
+
This plugin depends on `fluent-plugin-buffer-lightening`, and it includes monkey patch module for BufferedOutput plugin, to realize high rate and low latency flushing. With this patch, sub 1 second flushing available.
|
106
|
+
|
107
|
+
To use this feature, execute fluentd with `-r fluent/plugin/output_try_flush_interval_patch` option.
|
108
|
+
And configure `flush_interval` and `try_flush_interval` with floating point value.
|
109
|
+
|
110
|
+
```apache
|
111
|
+
<match dummy>
|
112
|
+
type bigquery
|
113
|
+
|
114
|
+
method insert # default
|
115
|
+
|
116
|
+
flush_interval 0.2
|
117
|
+
try_flush_interval 0.05
|
118
|
+
|
119
|
+
buffer_chunk_records_limit 300 # default rate limit for users is 100
|
120
|
+
buffer_queue_limit 10240 # 1MB * 10240 -> 10GB!
|
121
|
+
|
122
|
+
num_threads 16
|
123
|
+
|
124
|
+
# credentials, project/dataset/table and schema specs.
|
125
|
+
</match>
|
126
|
+
```
|
127
|
+
|
128
|
+
With this configuration, flushing will be done in 0.25 seconds after record inputs in the worst case.
|
129
|
+
|
130
|
+
## TODO
|
131
|
+
|
132
|
+
* support Load API
|
133
|
+
* with automatically configured flush/buffer options
|
134
|
+
* support RECORD field
|
135
|
+
* and support optional data fields
|
136
|
+
* support NULLABLE/REQUIRED/REPEATED field options
|
137
|
+
* OAuth installed application credentials support
|
138
|
+
* Google API discovery expiration
|
139
|
+
* Error classes
|
140
|
+
* check row size limits
|
data/Rakefile
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'fluent/plugin/bigquery/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "fluent-plugin-bigquery"
|
8
|
+
spec.version = Fluent::BigQueryPlugin::VERSION
|
9
|
+
spec.authors = ["TAGOMORI Satoshi"]
|
10
|
+
spec.email = ["tagomoris@gmail.com"]
|
11
|
+
spec.description = %q{Fluentd plugin to store data on Google BigQuery, by load, or by stream inserts}
|
12
|
+
spec.summary = %q{Fluentd plugin to store data on Google BigQuery}
|
13
|
+
spec.homepage = "https://github.com/tagomoris/fluent-plugin-bigquery"
|
14
|
+
spec.license = "APLv2"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "rake"
|
22
|
+
spec.add_runtime_dependency "google-api-client", "~> 0.6.4"
|
23
|
+
spec.add_runtime_dependency "fluentd"
|
24
|
+
spec.add_runtime_dependency "fluent-mixin-plaintextformatter", '>= 0.2.1'
|
25
|
+
spec.add_runtime_dependency "fluent-mixin-config-placeholders", ">= 0.2.0"
|
26
|
+
spec.add_runtime_dependency "fluent-plugin-buffer-lightening"
|
27
|
+
|
28
|
+
spec.add_development_dependency "fluent-plugin-dummydata-producer"
|
29
|
+
end
|
@@ -0,0 +1,173 @@
|
|
1
|
+
module Fluent
|
2
|
+
module BigQueryPlugin
|
3
|
+
class LoadRequestBodyWrapper
|
4
|
+
# body can be a instance of IO (#rewind, #read, #to_str)
|
5
|
+
# http://rubydoc.info/github/google/google-api-ruby-client/Google/APIClient/Request#body-instance_method
|
6
|
+
|
7
|
+
# http://rubydoc.info/github/google/google-api-ruby-client/Google/APIClient#execute-instance_method
|
8
|
+
# (Google::APIClient::Method) api_method: The method object or the RPC name of the method being executed.
|
9
|
+
# (Hash, Array) parameters: The parameters to send to the method.
|
10
|
+
# (String) body: The body of the request.
|
11
|
+
# (Hash, Array) headers: The HTTP headers for the request.
|
12
|
+
# (Hash) options: A set of options for the request, of which:
|
13
|
+
# (#generate_authenticated_request) :authorization (default: true)
|
14
|
+
# - The authorization mechanism for the response. Used only if :authenticated is true.
|
15
|
+
# (TrueClass, FalseClass) :authenticated (default: true)
|
16
|
+
# - true if the request must be signed or somehow authenticated, false otherwise.
|
17
|
+
# (TrueClass, FalseClass) :gzip (default: true) - true if gzip enabled, false otherwise.
|
18
|
+
|
19
|
+
# https://developers.google.com/bigquery/loading-data-into-bigquery#loaddatapostrequest
|
20
|
+
|
21
|
+
JSON_PRETTY_DUMP = JSON::State.new(space: " ", indent:" ", object_nl:"\n", array_nl:"\n")
|
22
|
+
|
23
|
+
CONTENT_TYPE_FIRST = "Content-Type: application/json; charset=UTF-8\n\n"
|
24
|
+
CONTENT_TYPE_SECOND = "Content-Type: application/octet-stream\n\n"
|
25
|
+
|
26
|
+
MULTIPART_BOUNDARY = "--xxx\n"
|
27
|
+
MULTIPART_BOUNDARY_END = "--xxx--\n"
|
28
|
+
|
29
|
+
def initialize(project_id, dataset_id, table_id, field_defs, buffer)
|
30
|
+
@metadata = {
|
31
|
+
configuration: {
|
32
|
+
load: {
|
33
|
+
sourceFormat: "<required for JSON files>",
|
34
|
+
schema: {
|
35
|
+
fields: field_defs
|
36
|
+
},
|
37
|
+
destinationTable: {
|
38
|
+
projectId: project_id,
|
39
|
+
datasetId: dataset_id,
|
40
|
+
tableId: table_id
|
41
|
+
}
|
42
|
+
}
|
43
|
+
}
|
44
|
+
}
|
45
|
+
|
46
|
+
@non_buffer = MULTIPART_BOUNDARY + CONTENT_TYPE_FIRST + @metadata.to_json(JSON_PRETTY_DUMP) + "\n" +
|
47
|
+
MULTIPART_BOUNDARY + CONTENT_TYPE_SECOND
|
48
|
+
@non_buffer.force_encoding("ASCII-8BIT")
|
49
|
+
@non_buffer_bytesize = @non_buffer.bytesize
|
50
|
+
|
51
|
+
@buffer = buffer # read
|
52
|
+
@buffer_bytesize = @buffer.size # Fluentd Buffer Chunk #size -> bytesize
|
53
|
+
|
54
|
+
@footer = MULTIPART_BOUNDARY_END.force_encoding("ASCII-8BIT")
|
55
|
+
|
56
|
+
@contents_bytesize = @non_buffer_bytesize + @buffer_bytesize
|
57
|
+
@total_bytesize = @contents_bytesize + MULTIPART_BOUNDARY_END.bytesize
|
58
|
+
|
59
|
+
@whole_data = nil
|
60
|
+
|
61
|
+
@counter = 0
|
62
|
+
@eof = false
|
63
|
+
end
|
64
|
+
|
65
|
+
# sample_body = <<EOF
|
66
|
+
# --xxx
|
67
|
+
# Content-Type: application/json; charset=UTF-8
|
68
|
+
#
|
69
|
+
# {
|
70
|
+
# "configuration": {
|
71
|
+
# "load": {
|
72
|
+
# "sourceFormat": "<required for JSON files>",
|
73
|
+
# "schema": {
|
74
|
+
# "fields": [
|
75
|
+
# {"name":"f1", "type":"STRING"},
|
76
|
+
# {"name":"f2", "type":"INTEGER"}
|
77
|
+
# ]
|
78
|
+
# },
|
79
|
+
# "destinationTable": {
|
80
|
+
# "projectId": "projectId",
|
81
|
+
# "datasetId": "datasetId",
|
82
|
+
# "tableId": "tableId"
|
83
|
+
# }
|
84
|
+
# }
|
85
|
+
# }
|
86
|
+
# }
|
87
|
+
# --xxx
|
88
|
+
# Content-Type: application/octet-stream
|
89
|
+
#
|
90
|
+
# <your data>
|
91
|
+
# --xxx--
|
92
|
+
# EOF
|
93
|
+
def rewind
|
94
|
+
@counter = 0
|
95
|
+
@eof = false
|
96
|
+
end
|
97
|
+
|
98
|
+
def eof?
|
99
|
+
@eof
|
100
|
+
end
|
101
|
+
|
102
|
+
def to_str
|
103
|
+
rewind
|
104
|
+
self.read # all data
|
105
|
+
end
|
106
|
+
|
107
|
+
def read(length=nil, outbuf="")
|
108
|
+
raise ArgumentError, "negative read length" if length && length < 0
|
109
|
+
return (length.nil? || length == 0) ? "" : nil if @eof
|
110
|
+
return outbuf if length == 0
|
111
|
+
|
112
|
+
# read all data
|
113
|
+
if length.nil? || length >= @total_bytesize
|
114
|
+
@whole_data ||= @buffer.read.force_encoding("ASCII-8BIT")
|
115
|
+
|
116
|
+
if @counter.zero?
|
117
|
+
outbuf.replace(@non_buffer)
|
118
|
+
outbuf << @whole_data
|
119
|
+
outbuf << @footer
|
120
|
+
elsif @counter < @non_buffer_bytesize
|
121
|
+
outbuf.replace(@non_buffer[ @counter .. -1 ])
|
122
|
+
outbuf << @whole_data
|
123
|
+
outbuf << @footer
|
124
|
+
elsif @counter < @contents_bytesize
|
125
|
+
outbuf.replace(@whole_data[ (@counter - @non_buffer_bytesize) .. -1 ])
|
126
|
+
outbuf << @footer
|
127
|
+
else
|
128
|
+
outbuf.replace(@footer[ (@counter - @contents_bytesize) .. -1 ])
|
129
|
+
end
|
130
|
+
@counter = @total_bytesize
|
131
|
+
@eof = true
|
132
|
+
return outbuf
|
133
|
+
end
|
134
|
+
|
135
|
+
# In ruby script level (non-ext module), we cannot prevent to change outbuf length or object re-assignment
|
136
|
+
outbuf.replace("")
|
137
|
+
|
138
|
+
# return first part (metadata)
|
139
|
+
if @counter < @non_buffer_bytesize
|
140
|
+
non_buffer_part = @non_buffer[@counter, length]
|
141
|
+
if non_buffer_part
|
142
|
+
outbuf << non_buffer_part
|
143
|
+
length -= non_buffer_part.bytesize
|
144
|
+
@counter += non_buffer_part.bytesize
|
145
|
+
end
|
146
|
+
end
|
147
|
+
return outbuf if length < 1
|
148
|
+
|
149
|
+
# return second part (buffer content)
|
150
|
+
if @counter < @contents_bytesize
|
151
|
+
@whole_data ||= @buffer.read.force_encoding("ASCII-8BIT")
|
152
|
+
buffer_part = @whole_data[@counter - @non_buffer_bytesize, length]
|
153
|
+
if buffer_part
|
154
|
+
outbuf << buffer_part
|
155
|
+
length -= buffer_part.bytesize
|
156
|
+
@counter += buffer_part.bytesize
|
157
|
+
end
|
158
|
+
end
|
159
|
+
return outbuf if length < 1
|
160
|
+
|
161
|
+
# return footer
|
162
|
+
footer_part = @footer[@counter - @contents_bytesize, length]
|
163
|
+
if footer_part
|
164
|
+
outbuf << footer_part
|
165
|
+
@counter += footer_part.bytesize
|
166
|
+
@eof = true if @counter >= @total_bytesize
|
167
|
+
end
|
168
|
+
|
169
|
+
outbuf
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
@@ -0,0 +1,296 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
|
3
|
+
require 'fluent/plugin/bigquery/version'
|
4
|
+
|
5
|
+
require 'fluent/mixin/config_placeholders'
|
6
|
+
require 'fluent/mixin/plaintextformatter'
|
7
|
+
|
8
|
+
## TODO: load implementation
|
9
|
+
# require 'fluent/plugin/bigquery/load_request_body_wrapper'
|
10
|
+
|
11
|
+
require 'fluent/plugin/output_try_flush_interval_patch'
|
12
|
+
|
13
|
+
module Fluent
|
14
|
+
### TODO: error classes for each api error responses
|
15
|
+
# class BigQueryAPIError < StandardError
|
16
|
+
# end
|
17
|
+
|
18
|
+
class BigQueryOutput < BufferedOutput
|
19
|
+
Fluent::Plugin.register_output('bigquery', self)
|
20
|
+
|
21
|
+
# https://developers.google.com/bigquery/browser-tool-quickstart
|
22
|
+
# https://developers.google.com/bigquery/bigquery-api-quickstart
|
23
|
+
|
24
|
+
config_set_default :buffer_type, 'lightening'
|
25
|
+
|
26
|
+
config_set_default :flush_interval, 0.25
|
27
|
+
config_set_default :try_flush_interval, 0.05
|
28
|
+
|
29
|
+
config_set_default :buffer_chunk_records_limit, 100
|
30
|
+
config_set_default :buffer_chunk_limit, 1000000
|
31
|
+
config_set_default :buffer_queue_limit, 1024
|
32
|
+
|
33
|
+
### for loads
|
34
|
+
### TODO: different default values for buffering between 'load' and insert
|
35
|
+
# config_set_default :flush_interval, 1800 # 30min => 48 imports/day
|
36
|
+
# config_set_default :buffer_chunk_limit, 1000**4 # 1.0*10^12 < 1TB (1024^4)
|
37
|
+
|
38
|
+
### OAuth credential
|
39
|
+
# config_param :client_id, :string
|
40
|
+
# config_param :client_secret, :string
|
41
|
+
|
42
|
+
### Service Account credential
|
43
|
+
config_param :email, :string
|
44
|
+
config_param :private_key_path, :string
|
45
|
+
config_param :private_key_passphrase, :string, :default => 'notasecret'
|
46
|
+
|
47
|
+
# see as simple reference
|
48
|
+
# https://github.com/abronte/BigQuery/blob/master/lib/bigquery.rb
|
49
|
+
config_param :project, :string
|
50
|
+
|
51
|
+
# dataset_name
|
52
|
+
# The name can be up to 1,024 characters long, and consist of A-Z, a-z, 0-9, and the underscore,
|
53
|
+
# but it cannot start with a number or underscore, or have spaces.
|
54
|
+
config_param :dataset, :string
|
55
|
+
|
56
|
+
# table_id
|
57
|
+
# In Table ID, enter a name for your new table. Naming rules are the same as for your dataset.
|
58
|
+
config_param :table, :string, :default => nil
|
59
|
+
config_param :tables, :string, :default => nil
|
60
|
+
|
61
|
+
config_param :field_string, :string, :default => nil
|
62
|
+
config_param :field_integer, :string, :default => nil
|
63
|
+
config_param :field_float, :string, :default => nil
|
64
|
+
config_param :field_boolean, :string, :default => nil
|
65
|
+
### TODO: record field stream inserts doesn't works well?
|
66
|
+
### At table creation, table type json + field type record -> field type validation fails
|
67
|
+
### At streaming inserts, schema cannot be specified
|
68
|
+
# config_param :field_record, :string, :defualt => nil
|
69
|
+
# config_param :optional_data_field, :string, :default => nil
|
70
|
+
|
71
|
+
config_param :time_format, :string, :default => nil
|
72
|
+
config_param :localtime, :bool, :default => nil
|
73
|
+
config_param :utc, :bool, :default => nil
|
74
|
+
config_param :time_field, :string, :default => nil
|
75
|
+
|
76
|
+
config_param :method, :string, :default => 'insert' # or 'load' # TODO: not implemented now
|
77
|
+
|
78
|
+
config_param :load_size_limit, :integer, :default => 1000**4 # < 1TB (1024^4) # TODO: not implemented now
|
79
|
+
### method: 'load'
|
80
|
+
# https://developers.google.com/bigquery/loading-data-into-bigquery
|
81
|
+
# Maximum File Sizes:
|
82
|
+
# File Type Compressed Uncompressed
|
83
|
+
# CSV 1 GB With new-lines in strings: 4 GB
|
84
|
+
# Without new-lines in strings: 1 TB
|
85
|
+
# JSON 1 GB 1 TB
|
86
|
+
|
87
|
+
config_param :row_size_limit, :integer, :default => 100*1000 # < 100KB # configurable in google ?
|
88
|
+
# config_param :insert_size_limit, :integer, :default => 1000**2 # < 1MB
|
89
|
+
# config_param :rows_per_second_limit, :integer, :default => 1000 # spike limit
|
90
|
+
### method: ''Streaming data inserts support
|
91
|
+
# https://developers.google.com/bigquery/streaming-data-into-bigquery#usecases
|
92
|
+
# Maximum row size: 100 KB
|
93
|
+
# Maximum data size of all rows, per insert: 1 MB
|
94
|
+
# Maximum rows per second: 100 rows per second, per table, with allowed and occasional bursts of up to 1,000 rows per second.
|
95
|
+
# If you exceed 100 rows per second for an extended period of time, throttling might occur.
|
96
|
+
### Toooooooooooooo short/small per inserts and row!
|
97
|
+
|
98
|
+
### Table types
|
99
|
+
# https://developers.google.com/bigquery/docs/tables
|
100
|
+
#
|
101
|
+
# type - The following data types are supported; see Data Formats for details on each data type:
|
102
|
+
# STRING
|
103
|
+
# INTEGER
|
104
|
+
# FLOAT
|
105
|
+
# BOOLEAN
|
106
|
+
# RECORD A JSON object, used when importing nested records. This type is only available when using JSON source files.
|
107
|
+
#
|
108
|
+
# mode - Whether a field can be null. The following values are supported:
|
109
|
+
# NULLABLE - The cell can be null.
|
110
|
+
# REQUIRED - The cell cannot be null.
|
111
|
+
# REPEATED - Zero or more repeated simple or nested subfields. This mode is only supported when using JSON source files.
|
112
|
+
|
113
|
+
def initialize
|
114
|
+
super
|
115
|
+
require 'google/api_client'
|
116
|
+
require 'google/api_client/client_secrets'
|
117
|
+
require 'google/api_client/auth/installed_app'
|
118
|
+
end
|
119
|
+
|
120
|
+
def configure(conf)
|
121
|
+
super
|
122
|
+
|
123
|
+
if (!@table && !@tables) || (@table && @table)
|
124
|
+
raise Fluent::ConfigError, "'table' or 'tables' must be specified, and both are invalid"
|
125
|
+
end
|
126
|
+
|
127
|
+
@tablelist = @tables ? @tables.split(',') : [@table]
|
128
|
+
|
129
|
+
@fields = {}
|
130
|
+
if @field_string
|
131
|
+
@field_string.split(',').each do |fieldname|
|
132
|
+
@fields[fieldname] = :string
|
133
|
+
end
|
134
|
+
end
|
135
|
+
if @field_integer
|
136
|
+
@field_integer.split(',').each do |fieldname|
|
137
|
+
@fields[fieldname] = :integer
|
138
|
+
end
|
139
|
+
end
|
140
|
+
if @field_float
|
141
|
+
@field_float.split(',').each do |fieldname|
|
142
|
+
@fields[fieldname] = :float
|
143
|
+
end
|
144
|
+
end
|
145
|
+
if @field_boolean
|
146
|
+
@field_boolean.split(',').each do |fieldname|
|
147
|
+
@fields[fieldname] = :boolean
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
if @localtime.nil?
|
152
|
+
if @utc
|
153
|
+
@localtime = false
|
154
|
+
end
|
155
|
+
end
|
156
|
+
@timef = TimeFormatter.new(@time_format, @localtime)
|
157
|
+
end
|
158
|
+
|
159
|
+
def start
|
160
|
+
super
|
161
|
+
|
162
|
+
@bq = client.discovered_api("bigquery", "v2") # TODO: refresh with specified expiration
|
163
|
+
@cached_client = nil
|
164
|
+
@cached_client_expiration = nil
|
165
|
+
|
166
|
+
@tables_queue = @tablelist.dup.shuffle
|
167
|
+
@tables_mutex = Mutex.new
|
168
|
+
end
|
169
|
+
|
170
|
+
def shutdown
|
171
|
+
super
|
172
|
+
# nothing to do
|
173
|
+
end
|
174
|
+
|
175
|
+
def client
|
176
|
+
return @cached_client if @cached_client && @cached_client_expiration > Time.now
|
177
|
+
|
178
|
+
client = Google::APIClient.new(
|
179
|
+
:application_name => 'Fluentd BigQuery plugin',
|
180
|
+
:application_version => Fluent::BigQueryPlugin::VERSION
|
181
|
+
)
|
182
|
+
|
183
|
+
key = Google::APIClient::PKCS12.load_key( @private_key_path, @private_key_passphrase )
|
184
|
+
asserter = Google::APIClient::JWTAsserter.new(
|
185
|
+
@email,
|
186
|
+
"https://www.googleapis.com/auth/bigquery",
|
187
|
+
key
|
188
|
+
)
|
189
|
+
# refresh_auth
|
190
|
+
client.authorization = asserter.authorize
|
191
|
+
@cached_client_expiration = Time.now + 1800
|
192
|
+
@cached_client = client
|
193
|
+
end
|
194
|
+
|
195
|
+
def insert(table_id, rows)
|
196
|
+
res = client().execute(
|
197
|
+
:api_method => @bq.tabledata.insert_all,
|
198
|
+
:parameters => {
|
199
|
+
'projectId' => @project,
|
200
|
+
'datasetId' => @dataset,
|
201
|
+
'tableId' => table_id,
|
202
|
+
},
|
203
|
+
:body_object => {
|
204
|
+
"rows" => rows
|
205
|
+
}
|
206
|
+
)
|
207
|
+
if res.status != 200
|
208
|
+
# api_error? -> client cache clear
|
209
|
+
@cached_client = nil
|
210
|
+
|
211
|
+
message = res.body
|
212
|
+
if res.body =~ /^\{/
|
213
|
+
begin
|
214
|
+
res_obj = JSON.parse(res.body)
|
215
|
+
message = res_obj['error']['message'] || res.body
|
216
|
+
rescue => e
|
217
|
+
$log.warn "Parse error: google api error response body", :body => res.body
|
218
|
+
end
|
219
|
+
end
|
220
|
+
$log.error "tabledata.insertAll API", :project_id => @project_id, :dataset => @dataset_id, :table => table_id, :code => res.status, :message => message
|
221
|
+
raise "failed to insert into bigquery" # TODO: error class
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
def load
|
226
|
+
# https://developers.google.com/bigquery/loading-data-into-bigquery#loaddatapostrequest
|
227
|
+
raise NotImplementedError # TODO
|
228
|
+
end
|
229
|
+
|
230
|
+
def format_record(record)
|
231
|
+
out = {}
|
232
|
+
@fields.each do |key, type|
|
233
|
+
value = record[key]
|
234
|
+
next if value.nil? # field does not exists, or null value
|
235
|
+
out[key] = case type
|
236
|
+
when :string then record[key].to_s
|
237
|
+
when :integer then record[key].to_i
|
238
|
+
when :float then record[key].to_f
|
239
|
+
when :boolean then !!record[key]
|
240
|
+
# when :record
|
241
|
+
else
|
242
|
+
raise "BUG: unknown field type #{type}"
|
243
|
+
end
|
244
|
+
end
|
245
|
+
out
|
246
|
+
end
|
247
|
+
|
248
|
+
def format_stream(tag, es)
|
249
|
+
super
|
250
|
+
buf = ''
|
251
|
+
es.each do |time, record|
|
252
|
+
row = if @time_field
|
253
|
+
format_record(record.merge({@time_field => @timef.format(time)}))
|
254
|
+
else
|
255
|
+
format_record(record)
|
256
|
+
end
|
257
|
+
buf << {"json" => row}.to_msgpack unless row.empty?
|
258
|
+
end
|
259
|
+
buf
|
260
|
+
end
|
261
|
+
|
262
|
+
def write(chunk)
|
263
|
+
rows = []
|
264
|
+
chunk.msgpack_each do |row_object|
|
265
|
+
# TODO: row size limit
|
266
|
+
rows << row_object
|
267
|
+
end
|
268
|
+
|
269
|
+
# TODO: method
|
270
|
+
|
271
|
+
insert_table = @tables_mutex.synchronize do
|
272
|
+
t = @tables_queue.shift
|
273
|
+
@tables_queue.push t
|
274
|
+
t
|
275
|
+
end
|
276
|
+
insert(insert_table, rows)
|
277
|
+
end
|
278
|
+
|
279
|
+
# def client_oauth # not implemented
|
280
|
+
# raise NotImplementedError, "OAuth needs browser authentication..."
|
281
|
+
#
|
282
|
+
# client = Google::APIClient.new(
|
283
|
+
# :application_name => 'Example Ruby application',
|
284
|
+
# :application_version => '1.0.0'
|
285
|
+
# )
|
286
|
+
# bigquery = client.discovered_api('bigquery', 'v2')
|
287
|
+
# flow = Google::APIClient::InstalledAppFlow.new(
|
288
|
+
# :client_id => @client_id
|
289
|
+
# :client_secret => @client_secret
|
290
|
+
# :scope => ['https://www.googleapis.com/auth/bigquery']
|
291
|
+
# )
|
292
|
+
# client.authorization = flow.authorize # browser authentication !
|
293
|
+
# client
|
294
|
+
# end
|
295
|
+
end
|
296
|
+
end
|
data/test/helper.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler'
|
3
|
+
begin
|
4
|
+
Bundler.setup(:default, :development)
|
5
|
+
rescue Bundler::BundlerError => e
|
6
|
+
$stderr.puts e.message
|
7
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
8
|
+
exit e.status_code
|
9
|
+
end
|
10
|
+
require 'test/unit'
|
11
|
+
|
12
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
13
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
14
|
+
require 'fluent/test'
|
15
|
+
unless ENV.has_key?('VERBOSE')
|
16
|
+
nulllogger = Object.new
|
17
|
+
nulllogger.instance_eval {|obj|
|
18
|
+
def method_missing(method, *args)
|
19
|
+
# pass
|
20
|
+
end
|
21
|
+
}
|
22
|
+
$log = nulllogger
|
23
|
+
end
|
24
|
+
|
25
|
+
require 'fluent/buffer'
|
26
|
+
require 'fluent/plugin/buf_memory'
|
27
|
+
require 'fluent/plugin/buf_file'
|
28
|
+
|
29
|
+
require 'fluent/plugin/out_bigquery'
|
30
|
+
require 'fluent/plugin/bigquery/load_request_body_wrapper'
|
31
|
+
|
32
|
+
class Test::Unit::TestCase
|
33
|
+
end
|
@@ -0,0 +1,190 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
require 'helper'
|
3
|
+
require 'json'
|
4
|
+
require 'tempfile'
|
5
|
+
|
6
|
+
class LoadRequestBodyWrapperTest < Test::Unit::TestCase
|
7
|
+
def content_alphabet(repeat)
|
8
|
+
(0...repeat).map{|i| "#{i}0123456789\n" }.join
|
9
|
+
end
|
10
|
+
|
11
|
+
def content_kana(repeat)
|
12
|
+
(0...repeat).map{|i| "#{i}あいうえおかきくけこ\n" }.join
|
13
|
+
end
|
14
|
+
|
15
|
+
def mem_chunk(repeat=10, kana=false)
|
16
|
+
content = kana ? content_kana(repeat) : content_alphabet(repeat)
|
17
|
+
Fluent::MemoryBufferChunk.new('bc_mem', content)
|
18
|
+
end
|
19
|
+
|
20
|
+
def file_chunk(repeat=10, kana=false)
|
21
|
+
content = kana ? content_kana(repeat) : content_alphabet(repeat)
|
22
|
+
tmpfile = Tempfile.new('fluent_bigquery_plugin_test')
|
23
|
+
buf = Fluent::FileBufferChunk.new('bc_mem', tmpfile.path, tmpfile.object_id)
|
24
|
+
buf << content
|
25
|
+
buf
|
26
|
+
end
|
27
|
+
|
28
|
+
def field_defs
|
29
|
+
[{"name" => "field1", "type" => "STRING"}, {"name" => "field2", "type" => "INTEGER"}]
|
30
|
+
end
|
31
|
+
|
32
|
+
def check_meta(blank, first, last)
|
33
|
+
assert_equal "", blank
|
34
|
+
|
35
|
+
header1, body1 = first.split("\n\n")
|
36
|
+
assert_equal "Content-Type: application/json; charset=UTF-8", header1
|
37
|
+
metadata = JSON.parse(body1)
|
38
|
+
assert_equal "<required for JSON files>", metadata["configuration"]["load"]["sourceFormat"]
|
39
|
+
assert_equal "field1", metadata["configuration"]["load"]["schema"]["fields"][0]["name"]
|
40
|
+
assert_equal "STRING", metadata["configuration"]["load"]["schema"]["fields"][0]["type"]
|
41
|
+
assert_equal "field2", metadata["configuration"]["load"]["schema"]["fields"][1]["name"]
|
42
|
+
assert_equal "INTEGER", metadata["configuration"]["load"]["schema"]["fields"][1]["type"]
|
43
|
+
assert_equal "pname1", metadata["configuration"]["load"]["destinationTable"]["projectId"]
|
44
|
+
assert_equal "dname1", metadata["configuration"]["load"]["destinationTable"]["datasetId"]
|
45
|
+
assert_equal "tname1", metadata["configuration"]["load"]["destinationTable"]["tableId"]
|
46
|
+
|
47
|
+
assert_equal "--\n", last
|
48
|
+
end
|
49
|
+
|
50
|
+
def check_ascii(data)
|
51
|
+
blank, first, second, last = data.split(/--xxx\n?/)
|
52
|
+
|
53
|
+
check_meta(blank, first, last)
|
54
|
+
|
55
|
+
header2, body2 = second.split("\n\n")
|
56
|
+
assert_equal "Content-Type: application/octet-stream", header2
|
57
|
+
i = 0
|
58
|
+
body2.each_line do |line|
|
59
|
+
assert_equal "#{i}0123456789\n", line
|
60
|
+
i += 1
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def check_kana(data)
|
65
|
+
blank, first, second, last = data.split(/--xxx\n?/)
|
66
|
+
|
67
|
+
check_meta(blank, first, last)
|
68
|
+
|
69
|
+
header2, body2 = second.split("\n\n")
|
70
|
+
assert_equal "Content-Type: application/octet-stream", header2
|
71
|
+
i = 0
|
72
|
+
body2.each_line do |line|
|
73
|
+
assert_equal "#{i}あいうえおかきくけこ\n", line
|
74
|
+
i += 1
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def setup
|
79
|
+
@klass = Fluent::BigQueryPlugin::LoadRequestBodyWrapper
|
80
|
+
self
|
81
|
+
end
|
82
|
+
|
83
|
+
def test_memory_buf
|
84
|
+
d1 = @klass.new('pname1', 'dname1', 'tname1', field_defs(), mem_chunk(10))
|
85
|
+
data1 = d1.read.force_encoding("UTF-8")
|
86
|
+
check_ascii(data1)
|
87
|
+
|
88
|
+
d2 = @klass.new('pname1', 'dname1', 'tname1', field_defs(), mem_chunk(10))
|
89
|
+
data2 = ""
|
90
|
+
while !d2.eof? do
|
91
|
+
buf = " "
|
92
|
+
objid = buf.object_id
|
93
|
+
data2 << d2.read(20, buf)
|
94
|
+
assert_equal objid, buf.object_id
|
95
|
+
end
|
96
|
+
data2.force_encoding("UTF-8")
|
97
|
+
|
98
|
+
assert_equal data1.size, data2.size
|
99
|
+
end
|
100
|
+
|
101
|
+
def test_memory_buf2
|
102
|
+
d1 = @klass.new('pname1', 'dname1', 'tname1', field_defs(), mem_chunk(100000))
|
103
|
+
data1 = d1.read.force_encoding("UTF-8")
|
104
|
+
check_ascii(data1)
|
105
|
+
|
106
|
+
d2 = @klass.new('pname1', 'dname1', 'tname1', field_defs(), mem_chunk(100000))
|
107
|
+
data2 = ""
|
108
|
+
while !d2.eof? do
|
109
|
+
buf = " "
|
110
|
+
objid = buf.object_id
|
111
|
+
data2 << d2.read(2048, buf)
|
112
|
+
assert_equal objid, buf.object_id
|
113
|
+
end
|
114
|
+
data2.force_encoding("UTF-8")
|
115
|
+
|
116
|
+
assert_equal data1.size, data2.size
|
117
|
+
end
|
118
|
+
|
119
|
+
def test_memory_buf3 # kana
|
120
|
+
d1 = @klass.new('pname1', 'dname1', 'tname1', field_defs(), mem_chunk(100000, true))
|
121
|
+
data1 = d1.read.force_encoding("UTF-8")
|
122
|
+
check_kana(data1)
|
123
|
+
|
124
|
+
d2 = @klass.new('pname1', 'dname1', 'tname1', field_defs(), mem_chunk(100000, true))
|
125
|
+
data2 = ""
|
126
|
+
while !d2.eof? do
|
127
|
+
buf = " "
|
128
|
+
objid = buf.object_id
|
129
|
+
data2 << d2.read(2048, buf)
|
130
|
+
assert_equal objid, buf.object_id
|
131
|
+
end
|
132
|
+
data2.force_encoding("UTF-8")
|
133
|
+
|
134
|
+
assert_equal data1.size, data2.size
|
135
|
+
end
|
136
|
+
|
137
|
+
def test_file_buf
|
138
|
+
d1 = @klass.new('pname1', 'dname1', 'tname1', field_defs(), file_chunk(10))
|
139
|
+
data1 = d1.read.force_encoding("UTF-8")
|
140
|
+
check_ascii(data1)
|
141
|
+
|
142
|
+
d2 = @klass.new('pname1', 'dname1', 'tname1', field_defs(), file_chunk(10))
|
143
|
+
data2 = ""
|
144
|
+
while !d2.eof? do
|
145
|
+
buf = " "
|
146
|
+
objid = buf.object_id
|
147
|
+
data2 << d2.read(20, buf)
|
148
|
+
assert_equal objid, buf.object_id
|
149
|
+
end
|
150
|
+
data2.force_encoding("UTF-8")
|
151
|
+
|
152
|
+
assert_equal data1.size, data2.size
|
153
|
+
end
|
154
|
+
|
155
|
+
def test_file_buf2
|
156
|
+
d1 = @klass.new('pname1', 'dname1', 'tname1', field_defs(), file_chunk(100000))
|
157
|
+
data1 = d1.read.force_encoding("UTF-8")
|
158
|
+
check_ascii(data1)
|
159
|
+
|
160
|
+
d2 = @klass.new('pname1', 'dname1', 'tname1', field_defs(), file_chunk(100000))
|
161
|
+
data2 = ""
|
162
|
+
while !d2.eof? do
|
163
|
+
buf = " "
|
164
|
+
objid = buf.object_id
|
165
|
+
data2 << d2.read(20480, buf)
|
166
|
+
assert_equal objid, buf.object_id
|
167
|
+
end
|
168
|
+
data2.force_encoding("UTF-8")
|
169
|
+
|
170
|
+
assert_equal data1.size, data2.size
|
171
|
+
end
|
172
|
+
|
173
|
+
def test_file_buf3 # kana
|
174
|
+
d1 = @klass.new('pname1', 'dname1', 'tname1', field_defs(), file_chunk(100000, true))
|
175
|
+
data1 = d1.read.force_encoding("UTF-8")
|
176
|
+
check_kana(data1)
|
177
|
+
|
178
|
+
d2 = @klass.new('pname1', 'dname1', 'tname1', field_defs(), file_chunk(100000, true))
|
179
|
+
data2 = ""
|
180
|
+
while !d2.eof? do
|
181
|
+
buf = " "
|
182
|
+
objid = buf.object_id
|
183
|
+
data2 << d2.read(20480, buf)
|
184
|
+
assert_equal objid, buf.object_id
|
185
|
+
end
|
186
|
+
data2.force_encoding("UTF-8")
|
187
|
+
|
188
|
+
assert_equal data1.size, data2.size
|
189
|
+
end
|
190
|
+
end
|
metadata
ADDED
@@ -0,0 +1,157 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: fluent-plugin-bigquery
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- TAGOMORI Satoshi
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2013-12-23 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rake
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - '>='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - '>='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: google-api-client
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ~>
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 0.6.4
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ~>
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 0.6.4
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: fluentd
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: fluent-mixin-plaintextformatter
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 0.2.1
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - '>='
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 0.2.1
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: fluent-mixin-config-placeholders
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - '>='
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 0.2.0
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - '>='
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 0.2.0
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: fluent-plugin-buffer-lightening
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - '>='
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :runtime
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - '>='
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: fluent-plugin-dummydata-producer
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - '>='
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - '>='
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
description: Fluentd plugin to store data on Google BigQuery, by load, or by stream
|
112
|
+
inserts
|
113
|
+
email:
|
114
|
+
- tagomoris@gmail.com
|
115
|
+
executables: []
|
116
|
+
extensions: []
|
117
|
+
extra_rdoc_files: []
|
118
|
+
files:
|
119
|
+
- .gitignore
|
120
|
+
- Gemfile
|
121
|
+
- LICENSE.txt
|
122
|
+
- README.md
|
123
|
+
- Rakefile
|
124
|
+
- fluent-plugin-bigquery.gemspec
|
125
|
+
- lib/fluent/plugin/bigquery/load_request_body_wrapper.rb
|
126
|
+
- lib/fluent/plugin/bigquery/version.rb
|
127
|
+
- lib/fluent/plugin/out_bigquery.rb
|
128
|
+
- test/helper.rb
|
129
|
+
- test/test_load_request_body_wrapper.rb
|
130
|
+
homepage: https://github.com/tagomoris/fluent-plugin-bigquery
|
131
|
+
licenses:
|
132
|
+
- APLv2
|
133
|
+
metadata: {}
|
134
|
+
post_install_message:
|
135
|
+
rdoc_options: []
|
136
|
+
require_paths:
|
137
|
+
- lib
|
138
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
139
|
+
requirements:
|
140
|
+
- - '>='
|
141
|
+
- !ruby/object:Gem::Version
|
142
|
+
version: '0'
|
143
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
144
|
+
requirements:
|
145
|
+
- - '>='
|
146
|
+
- !ruby/object:Gem::Version
|
147
|
+
version: '0'
|
148
|
+
requirements: []
|
149
|
+
rubyforge_project:
|
150
|
+
rubygems_version: 2.0.3
|
151
|
+
signing_key:
|
152
|
+
specification_version: 4
|
153
|
+
summary: Fluentd plugin to store data on Google BigQuery
|
154
|
+
test_files:
|
155
|
+
- test/helper.rb
|
156
|
+
- test/test_load_request_body_wrapper.rb
|
157
|
+
has_rdoc:
|