influxdb-client 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. data/.circleci/config.yml +157 -0
  3. data/.circleci/setup-rubygems.sh +3 -0
  4. data/.codecov.yml +3 -0
  5. data/.github/PULL_REQUEST_TEMPLATE +8 -0
  6. data/.gitignore +16 -0
  7. data/.rubocop.yml +38 -0
  8. data/CHANGELOG.md +37 -0
  9. data/Gemfile +24 -0
  10. data/LICENSE +21 -0
  11. data/README.md +268 -0
  12. data/Rakefile +37 -0
  13. data/bin/generate-sources.sh +23 -0
  14. data/bin/influxdb-onboarding.sh +39 -0
  15. data/bin/influxdb-restart.sh +60 -0
  16. data/bin/pom.xml +34 -0
  17. data/bin/swagger.yml +9867 -0
  18. data/examples/influxdb_18_example.rb +35 -0
  19. data/influxdb-client.gemspec +53 -0
  20. data/lib/influxdb2/client.rb +30 -0
  21. data/lib/influxdb2/client/client.rb +96 -0
  22. data/lib/influxdb2/client/default_api.rb +95 -0
  23. data/lib/influxdb2/client/delete_api.rb +80 -0
  24. data/lib/influxdb2/client/flux_csv_parser.rb +251 -0
  25. data/lib/influxdb2/client/flux_table.rb +99 -0
  26. data/lib/influxdb2/client/health_api.rb +49 -0
  27. data/lib/influxdb2/client/influx_error.rb +31 -0
  28. data/lib/influxdb2/client/models/delete_predicate_request.rb +215 -0
  29. data/lib/influxdb2/client/models/dialect.rb +317 -0
  30. data/lib/influxdb2/client/models/health_check.rb +256 -0
  31. data/lib/influxdb2/client/models/query.rb +284 -0
  32. data/lib/influxdb2/client/point.rb +215 -0
  33. data/lib/influxdb2/client/query_api.rb +93 -0
  34. data/lib/influxdb2/client/version.rb +23 -0
  35. data/lib/influxdb2/client/worker.rb +115 -0
  36. data/lib/influxdb2/client/write_api.rb +243 -0
  37. data/test/influxdb/client_test.rb +92 -0
  38. data/test/influxdb/delete_api_integration_test.rb +100 -0
  39. data/test/influxdb/delete_api_test.rb +121 -0
  40. data/test/influxdb/flux_csv_parser_test.rb +401 -0
  41. data/test/influxdb/point_test.rb +221 -0
  42. data/test/influxdb/query_api_integration_test.rb +58 -0
  43. data/test/influxdb/query_api_stream_test.rb +98 -0
  44. data/test/influxdb/query_api_test.rb +96 -0
  45. data/test/influxdb/write_api_batching_test.rb +292 -0
  46. data/test/influxdb/write_api_integration_test.rb +76 -0
  47. data/test/influxdb/write_api_test.rb +275 -0
  48. data/test/test_helper.rb +39 -0
  49. metadata +217 -0
@@ -0,0 +1,215 @@
1
+ # The MIT License
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ module InfluxDB2
22
+ DEFAULT_WRITE_PRECISION = WritePrecision::NANOSECOND
23
+ ESCAPE_KEY_LIST = ['\\'.freeze, ','.freeze, ' '.freeze, '='.freeze].freeze
24
+ ESCAPE_VALUE_LIST = ['\\'.freeze, '"'.freeze].freeze
25
+
26
+ # Point defines the values that will be written to the database.
27
+ # Ref: http://bit.ly/influxdata-point
28
+ class Point
29
+ # Create DataPoint instance for specified measurement name.
30
+ #
31
+ # @example InfluxDB::Point.new(name: "h2o",
32
+ # tags: {host: 'aws', region: 'us'},
33
+ # fields: {level: 5, saturation: "99%"},
34
+ # time: 123)
35
+ #
36
+ # @param [String] name the measurement name for the point.
37
+ # @param [Hash] tags the tag set for the point
38
+ # @param [Hash] fields the fields for the point
39
+ # @param [Integer] time the timestamp for the point
40
+ # @param [WritePrecision] precision the precision for the unix timestamps within the body line-protocol
41
+ def initialize(name:, tags: nil, fields: nil, time: nil, precision: DEFAULT_WRITE_PRECISION)
42
+ @name = name
43
+ @tags = tags || {}
44
+ @fields = fields || {}
45
+ @time = time
46
+ @precision = precision
47
+ end
48
+ attr_reader :precision
49
+
50
+ # Create DataPoint instance from specified data.
51
+ #
52
+ # @example Point.fromHash({
53
+ # name: 'cpu',
54
+ # tags: { host: 'server_nl', regios: 'us' },
55
+ # fields: {internal: 5, external: 6},
56
+ # time: 1422568543702900257
57
+ # })
58
+ #
59
+ # @param [Hash] data
60
+ def self.from_hash(data)
61
+ obj = new(name: data[:name], tags: data[:tags], fields: data[:fields], time: data[:time])
62
+ obj
63
+ end
64
+
65
+ # Adds or replaces a tag value for a point.
66
+ #
67
+ # @example InfluxDB::Point.new(name: "h2o")
68
+ # .add_tag("location", "europe")
69
+ # .add_field("level", 2)
70
+ #
71
+ # @param [Object] key the tag name
72
+ # @param [Object] value the tag value
73
+ def add_tag(key, value)
74
+ @tags[key] = value
75
+ self
76
+ end
77
+
78
+ # Adds or replaces a field value for a point.
79
+ #
80
+ # @example InfluxDB::Point.new(name: "h2o")
81
+ # .add_tag("location", "europe")
82
+ # .add_field("level", 2)
83
+ #
84
+ # @param [Object] key the field name
85
+ # @param [Object] value the field value
86
+ def add_field(key, value)
87
+ @fields[key] = value
88
+ self
89
+ end
90
+
91
+ # Updates the timestamp for the point.
92
+ #
93
+ # @example InfluxDB::Point.new(name: "h2o")
94
+ # .add_tag("location", "europe")
95
+ # .add_field("level", 2)
96
+ # .time(Time.new(2015, 10, 15, 8, 20, 15), InfluxDB::WritePrecision::MILLISECOND)
97
+ #
98
+ # @example InfluxDB::Point.new(name: "h2o")
99
+ # .add_tag("location", "europe")
100
+ # .add_field("level", 2)
101
+ # .time(123, InfluxDB::WritePrecision::NANOSECOND)
102
+ #
103
+ # @param [Object] time the timestamp
104
+ # @param [WritePrecision] precision the timestamp precision
105
+ def time(time, precision)
106
+ @time = time
107
+ @precision = precision
108
+ self
109
+ end
110
+
111
+ # If there is no field then return nil.
112
+ #
113
+ # @return a string representation of the point
114
+ def to_line_protocol
115
+ line_protocol = ''
116
+ measurement = _escape_key(@name || '')
117
+
118
+ line_protocol << measurement
119
+
120
+ tags = _escape_tags
121
+ line_protocol << ",#{tags}" unless tags.empty?
122
+ line_protocol << ' '.freeze if line_protocol[-1] == '\\'
123
+
124
+ fields = _escape_fields
125
+ return nil if fields.empty?
126
+
127
+ line_protocol << " #{fields}" if fields
128
+ timestamp = _escape_time
129
+ line_protocol << " #{timestamp}" if timestamp
130
+
131
+ line_protocol
132
+ end
133
+
134
+ private
135
+
136
+ def _escape_tags
137
+ return if @tags.nil?
138
+
139
+ @tags.sort.to_h.map do |k, v|
140
+ key = _escape_key(k.to_s)
141
+ value = _escape_key(v.to_s)
142
+ if key.empty? || value.empty?
143
+ nil
144
+ else
145
+ "#{key}=#{value}"
146
+ end
147
+ end.reject(&:nil?).join(','.freeze)
148
+ end
149
+
150
+ def _escape_fields
151
+ return if @fields.nil?
152
+
153
+ @fields.sort.to_h.map do |k, v|
154
+ key = _escape_key(k.to_s)
155
+ value = _escape_value(v)
156
+ if key.empty? || value.empty?
157
+ nil
158
+ else
159
+ "#{key}=#{value}"
160
+ end
161
+ end.reject(&:nil?).join(','.freeze)
162
+ end
163
+
164
+ def _escape_key(value)
165
+ result = value.dup
166
+ ESCAPE_KEY_LIST.each do |ch|
167
+ result = result.gsub(ch) { "\\#{ch}" }
168
+ end
169
+ result
170
+ end
171
+
172
+ def _escape_value(value)
173
+ if value.nil?
174
+ ''
175
+ elsif value.is_a?(String)
176
+ result = value.dup
177
+ ESCAPE_VALUE_LIST.each do |ch|
178
+ result = result.gsub(ch) { "\\#{ch}" }
179
+ end
180
+ '"'.freeze + result + '"'.freeze
181
+ elsif value.is_a?(Integer)
182
+ "#{value}i"
183
+ elsif [Float::INFINITY, -Float::INFINITY].include?(value)
184
+ ''
185
+ else
186
+ value.to_s
187
+ end
188
+ end
189
+
190
+ def _escape_time
191
+ if @time.nil?
192
+ nil
193
+ elsif @time.is_a?(Integer)
194
+ @time.to_s
195
+ elsif @time.is_a?(Float)
196
+ @time.round.to_s
197
+ elsif @time.is_a?(Time)
198
+ nano_seconds = @time.to_i * 1e9
199
+ nano_seconds += @time.tv_nsec
200
+ case @precision || DEFAULT_WRITE_PRECISION
201
+ when InfluxDB2::WritePrecision::MILLISECOND then
202
+ (nano_seconds / 1e6).round
203
+ when InfluxDB2::WritePrecision::SECOND then
204
+ (nano_seconds / 1e9).round
205
+ when InfluxDB2::WritePrecision::MICROSECOND then
206
+ (nano_seconds / 1e3).round
207
+ when InfluxDB2::WritePrecision::NANOSECOND then
208
+ nano_seconds.round
209
+ end
210
+ else
211
+ @time.to_s
212
+ end
213
+ end
214
+ end
215
+ end
@@ -0,0 +1,93 @@
1
+ # The MIT License
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+ require_relative 'models/dialect'
21
+ require_relative 'models/query'
22
+ require_relative 'flux_csv_parser'
23
+ require 'json'
24
+
25
+ module InfluxDB2
26
+ # The client of the InfluxDB 2.0 that implement Query HTTP API endpoint.
27
+ #
28
+ class QueryApi < DefaultApi
29
+ DEFAULT_DIALECT = InfluxDB2::Dialect.new(header: true, delimiter: ',', comment_prefix: '#',
30
+ annotations: %w[datatype group default])
31
+
32
+ # @param [Hash] options The options to be used by the client.
33
+ def initialize(options:)
34
+ super(options: options)
35
+ end
36
+
37
+ # @param [Object] query the flux query to execute. The data could be represent by [String], [Query]
38
+ # @param [String] org specifies the source organization
39
+ # @return [String] result of query
40
+ def query_raw(query: nil, org: nil, dialect: DEFAULT_DIALECT)
41
+ _post_query(query: query, org: org, dialect: dialect).read_body
42
+ end
43
+
44
+ # @param [Object] query the flux query to execute. The data could be represent by [String], [Query]
45
+ # @param [String] org specifies the source organization
46
+ # @return [Array] list of FluxTables which are matched the query
47
+ def query(query: nil, org: nil, dialect: DEFAULT_DIALECT)
48
+ response = query_raw(query: query, org: org, dialect: dialect)
49
+ parser = InfluxDB2::FluxCsvParser.new(response)
50
+
51
+ parser.parse
52
+ parser.tables
53
+ end
54
+
55
+ # @param [Object] query the flux query to execute. The data could be represent by [String], [Query]
56
+ # @param [String] org specifies the source organization
57
+ # @return stream of Flux Records
58
+ def query_stream(query: nil, org: nil, dialect: DEFAULT_DIALECT)
59
+ response = _post_query(query: query, org: org, dialect: dialect)
60
+
61
+ InfluxDB2::FluxCsvParser.new(response, stream: true)
62
+ end
63
+
64
+ private
65
+
66
+ def _post_query(query: nil, org: nil, dialect: DEFAULT_DIALECT)
67
+ org_param = org || @options[:org]
68
+ _check('org', org_param)
69
+
70
+ payload = _generate_payload(query, dialect)
71
+ return nil if payload.nil?
72
+
73
+ uri = URI.parse(File.join(@options[:url], '/api/v2/query'))
74
+ uri.query = URI.encode_www_form(org: org_param)
75
+
76
+ _post_json(payload.to_body.to_json, uri)
77
+ end
78
+
79
+ def _generate_payload(query, dialect)
80
+ if query.nil?
81
+ nil
82
+ elsif query.is_a?(Query)
83
+ query
84
+ elsif query.is_a?(String)
85
+ if query.empty?
86
+ nil
87
+ else
88
+ Query.new(query: query, dialect: dialect, type: nil)
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,23 @@
1
+ # The MIT License
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ module InfluxDB2
22
+ VERSION = '1.3.0'.freeze
23
+ end
@@ -0,0 +1,115 @@
1
+ # The MIT License
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ module InfluxDB2
22
+ # Worker for handling write batching queue
23
+ #
24
+ class Worker
25
+ def initialize(api_client, write_options)
26
+ @api_client = api_client
27
+ @write_options = write_options
28
+
29
+ @queue = Queue.new
30
+ @queue_event = Queue.new
31
+
32
+ @queue_event.push(true)
33
+
34
+ @thread_flush = Thread.new do
35
+ until api_client.closed
36
+ sleep @write_options.flush_interval.to_f / 1_000
37
+ _check_background_queue
38
+ end
39
+ end
40
+
41
+ @thread_size = Thread.new do
42
+ until api_client.closed
43
+ _check_background_queue(size: true) if @queue.length >= @write_options.batch_size
44
+ sleep 0.01
45
+ end
46
+ end
47
+ end
48
+
49
+ def push(payload)
50
+ if payload.respond_to? :each
51
+ payload.each do |item|
52
+ push(item)
53
+ end
54
+ else
55
+ @queue.push(payload)
56
+ end
57
+ end
58
+
59
+ def flush_all
60
+ _check_background_queue until @queue.empty?
61
+ end
62
+
63
+ private
64
+
65
+ def _check_background_queue(size: false)
66
+ @queue_event.pop
67
+ data = {}
68
+ points = 0
69
+
70
+ if size && @queue.length < @write_options.batch_size
71
+ @queue_event.push(true)
72
+ return
73
+ end
74
+
75
+ while (points < @write_options.batch_size) && !@queue.empty?
76
+ begin
77
+ item = @queue.pop(true)
78
+ key = item.key
79
+ data[key] = [] unless data.key?(key)
80
+ data[key] << item.data
81
+ points += 1
82
+ rescue ThreadError
83
+ @queue_event.push(true)
84
+ return
85
+ end
86
+ end
87
+
88
+ begin
89
+ _write(data) unless data.values.flatten.empty?
90
+ ensure
91
+ @queue_event.push(true)
92
+ end
93
+ end
94
+
95
+ def _write(data)
96
+ data.each do |key, points|
97
+ _write_raw(key, points)
98
+ end
99
+ end
100
+
101
+ def _write_raw(key, points)
102
+ if @write_options.jitter_interval.positive?
103
+ jitter_delay = (@write_options.jitter_interval.to_f / 1_000) * rand
104
+ sleep jitter_delay
105
+ end
106
+ @api_client.write_raw(points.join("\n"), precision: key.precision, bucket: key.bucket, org: key.org)
107
+ rescue InfluxError => e
108
+ raise e if e.code.nil? || !(%w[429 503].include? e.code)
109
+
110
+ timeout = e.retry_after.empty? ? @write_options.retry_interval.to_f / 1_000 : e.retry_after.to_f
111
+ sleep timeout
112
+ _write_raw(key, points)
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,243 @@
1
+ # The MIT License
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+ require_relative 'worker'
21
+
22
+ module InfluxDB2
23
+ module WriteType
24
+ SYNCHRONOUS = 1
25
+ BATCHING = 2
26
+ end
27
+
28
+ # Creates write api configuration.
29
+ #
30
+ class WriteOptions
31
+ # @param [WriteType] write_type: methods of write (batching, synchronous)
32
+ # @param [Integer] batch_size: the number of data point to collect in batch
33
+ # @param [Integer] flush_interval: flush data at least in this interval
34
+ # @param [Integer] retry_interval: number of milliseconds to retry unsuccessful write.
35
+ # The retry interval is used when the InfluxDB server does not specify "Retry-After" header.
36
+ # @param [Integer] jitter_interval: the number of milliseconds to increase the batch flush interval
37
+ # by a random amount
38
+ def initialize(write_type: WriteType::SYNCHRONOUS, batch_size: 1_000, flush_interval: 1_000, retry_interval: 1_000,
39
+ jitter_interval: 0)
40
+ _check_not_negative('batch_size', batch_size)
41
+ _check_not_negative('flush_interval', flush_interval)
42
+ _check_not_negative('retry_interval', retry_interval)
43
+ _check_positive('jitter_interval', jitter_interval)
44
+ @write_type = write_type
45
+ @batch_size = batch_size
46
+ @flush_interval = flush_interval
47
+ @retry_interval = retry_interval
48
+ @jitter_interval = jitter_interval
49
+ end
50
+
51
+ attr_reader :write_type, :batch_size, :flush_interval, :retry_interval, :jitter_interval
52
+
53
+ def _check_not_negative(key, value)
54
+ raise ArgumentError, "The '#{key}' should be positive or zero, but is: #{value}" if value <= 0
55
+ end
56
+
57
+ def _check_positive(key, value)
58
+ raise ArgumentError, "The '#{key}' should be positive number, but is: #{value}" if value < 0
59
+ end
60
+ end
61
+
62
+ SYNCHRONOUS = InfluxDB2::WriteOptions.new(write_type: WriteType::SYNCHRONOUS)
63
+
64
+ # Precision constants.
65
+ #
66
+ class WritePrecision
67
+ SECOND = 's'.freeze
68
+ MILLISECOND = 'ms'.freeze
69
+ MICROSECOND = 'us'.freeze
70
+ NANOSECOND = 'ns'.freeze
71
+
72
+ def get_from_value(value)
73
+ constants = WritePrecision.constants.select { |c| WritePrecision.const_get(c) == value }
74
+ raise "The time precision #{value} is not supported." if constants.empty?
75
+
76
+ value
77
+ end
78
+ end
79
+
80
+ # Write time series data into InfluxDB.
81
+ #
82
+ class WriteApi < DefaultApi
83
+ # @param [Hash] options The options to be used by the client.
84
+ # @param [WriteOptions] write_options Write api configuration.
85
+ def initialize(options:, write_options: SYNCHRONOUS)
86
+ super(options: options)
87
+ @write_options = write_options
88
+ @closed = false
89
+ end
90
+ attr_reader :closed
91
+
92
+ # Write data into specified Bucket.
93
+ #
94
+ # @example write(data:
95
+ # [
96
+ # {
97
+ # name: 'cpu',
98
+ # tags: { host: 'server_nl', region: 'us' },
99
+ # fields: {internal: 5, external: 6},
100
+ # time: 1422568543702900257
101
+ # },
102
+ # {name: 'gpu', fields: {value: 0.9999}}
103
+ # ],
104
+ # precision: InfluxDB::WritePrecision::NANOSECOND,
105
+ # bucket: 'my-bucket',
106
+ # org: 'my-org'
107
+ # )
108
+ #
109
+ # @example write(data: 'h2o,location=west value=33i 15')
110
+ #
111
+ # @example point = InfluxDB::Point.new(name: 'h2o')
112
+ # .add_tag('location', 'europe')
113
+ # .add_field('level', 2)
114
+ #
115
+ # hash = { name: 'h2o', tags: { host: 'aws', region: 'us' }, fields: { level: 5, saturation: '99%' }, time: 123 }
116
+ #
117
+ # write(data: ['h2o,location=west value=33i 15', point, hash])
118
+ #
119
+ # @param [Object] data DataPoints to write into InfluxDB. The data could be represent by [Hash], [Point], [String]
120
+ # or by collection of these types
121
+ # @param [WritePrecision] precision The precision for the unix timestamps within the body line-protocol
122
+ # @param [String] bucket specifies the destination bucket for writes
123
+ # @param [String] org specifies the destination organization for writes
124
+ def write(data:, precision: nil, bucket: nil, org: nil)
125
+ precision_param = precision || @options[:precision]
126
+ bucket_param = bucket || @options[:bucket]
127
+ org_param = org || @options[:org]
128
+ _check('precision', precision_param)
129
+ _check('bucket', bucket_param)
130
+ _check('org', org_param)
131
+
132
+ payload = _generate_payload(data, bucket: bucket_param, org: org_param, precision: precision_param)
133
+ return nil if payload.nil?
134
+
135
+ if WriteType::BATCHING == @write_options.write_type
136
+ _worker.push(payload)
137
+ else
138
+ write_raw(payload, precision: precision_param, bucket: bucket_param, org: org_param)
139
+ end
140
+ end
141
+
142
+ # @return [ true ] Always true.
143
+ def close!
144
+ _worker.flush_all unless _worker.nil?
145
+ @closed = true
146
+ true
147
+ end
148
+
149
+ # @param [String] payload data as String
150
+ # @param [WritePrecision] precision The precision for the unix timestamps within the body line-protocol
151
+ # @param [String] bucket specifies the destination bucket for writes
152
+ # @param [String] org specifies the destination organization for writes
153
+ def write_raw(payload, precision: nil, bucket: nil, org: nil)
154
+ precision_param = precision || @options[:precision]
155
+ bucket_param = bucket || @options[:bucket]
156
+ org_param = org || @options[:org]
157
+ _check('precision', precision_param)
158
+ _check('bucket', bucket_param)
159
+ _check('org', org_param)
160
+
161
+ return nil unless payload.instance_of?(String) || payload.empty?
162
+
163
+ uri = URI.parse(File.join(@options[:url], '/api/v2/write'))
164
+ uri.query = URI.encode_www_form(bucket: bucket_param, org: org_param, precision: precision_param.to_s)
165
+
166
+ _post_text(payload, uri)
167
+ end
168
+
169
+ # Item for batching queue
170
+ class BatchItem
171
+ def initialize(key, data)
172
+ @key = key
173
+ @data = data
174
+ end
175
+ attr_reader :key, :data
176
+ end
177
+
178
+ # Key for batch item
179
+ class BatchItemKey
180
+ def initialize(bucket, org, precision = DEFAULT_WRITE_PRECISION)
181
+ @bucket = bucket
182
+ @org = org
183
+ @precision = precision
184
+ end
185
+ attr_reader :bucket, :org, :precision
186
+
187
+ def ==(other)
188
+ @bucket == other.bucket && @org == other.org && @precision == other.precision
189
+ end
190
+
191
+ alias eql? ==
192
+
193
+ def hash
194
+ @bucket.hash ^ @org.hash ^ @precision.hash # XOR
195
+ end
196
+ end
197
+
198
+ private
199
+
200
+ WORKER_MUTEX = Mutex.new
201
+ def _worker
202
+ return nil unless @write_options.write_type == WriteType::BATCHING
203
+
204
+ return @worker if @worker
205
+
206
+ WORKER_MUTEX.synchronize do
207
+ # this return is necessary because the previous mutex holder
208
+ # might have already assigned the @worker
209
+ return @worker if @worker
210
+
211
+ @worker = Worker.new(self, @write_options)
212
+ end
213
+ end
214
+
215
+ def _generate_payload(data, precision: nil, bucket: nil, org: nil)
216
+ if data.nil?
217
+ nil
218
+ elsif data.is_a?(Point)
219
+ _generate_payload(data.to_line_protocol, bucket: bucket, org: org, precision: data.precision ||
220
+ DEFAULT_WRITE_PRECISION)
221
+ elsif data.is_a?(String)
222
+ if data.empty?
223
+ nil
224
+ elsif @write_options.write_type == WriteType::BATCHING
225
+ BatchItem.new(BatchItemKey.new(bucket, org, precision), data)
226
+ else
227
+ data
228
+ end
229
+ elsif data.is_a?(Hash)
230
+ _generate_payload(Point.from_hash(data), bucket: bucket, org: org, precision: precision)
231
+ elsif data.respond_to? :map
232
+ payloads = data.map do |item|
233
+ _generate_payload(item, bucket: bucket, org: org, precision: precision)
234
+ end.reject(&:nil?)
235
+ if @write_options.write_type == WriteType::BATCHING
236
+ payloads
237
+ else
238
+ payloads.join("\n".freeze)
239
+ end
240
+ end
241
+ end
242
+ end
243
+ end