influxdb 0.1.9 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.rubocop.yml +41 -0
- data/.travis.yml +3 -2
- data/Gemfile +7 -1
- data/README.md +218 -102
- data/Rakefile +2 -6
- data/lib/influxdb.rb +15 -5
- data/lib/influxdb/client.rb +38 -433
- data/lib/influxdb/client/http.rb +123 -0
- data/lib/influxdb/config.rb +66 -0
- data/lib/influxdb/errors.rb +8 -2
- data/lib/influxdb/{logger.rb → logging.rb} +6 -5
- data/lib/influxdb/max_queue.rb +2 -1
- data/lib/influxdb/point_value.rb +27 -25
- data/lib/influxdb/query/cluster.rb +17 -0
- data/lib/influxdb/query/continuous_query.rb +22 -0
- data/lib/influxdb/query/core.rb +110 -0
- data/lib/influxdb/query/database.rb +21 -0
- data/lib/influxdb/query/retention_policy.rb +26 -0
- data/lib/influxdb/query/user.rb +41 -0
- data/lib/influxdb/version.rb +2 -2
- data/lib/influxdb/writer/async.rb +115 -0
- data/lib/influxdb/writer/udp.rb +21 -0
- data/spec/influxdb/cases/async_client_spec.rb +33 -0
- data/spec/influxdb/cases/query_cluster_spec.rb +65 -0
- data/spec/influxdb/cases/query_continuous_query_spec.rb +82 -0
- data/spec/influxdb/cases/query_core.rb +34 -0
- data/spec/influxdb/cases/query_database_spec.rb +58 -0
- data/spec/influxdb/cases/query_retention_policy_spec.rb +84 -0
- data/spec/influxdb/cases/query_series_spec.rb +50 -0
- data/spec/influxdb/cases/query_shard_space_spec.rb +105 -0
- data/spec/influxdb/cases/query_shard_spec.rb +43 -0
- data/spec/influxdb/cases/query_user_spec.rb +127 -0
- data/spec/influxdb/cases/querying_spec.rb +149 -0
- data/spec/influxdb/cases/retry_requests_spec.rb +102 -0
- data/spec/influxdb/cases/udp_client_spec.rb +21 -0
- data/spec/influxdb/cases/write_points_spec.rb +140 -0
- data/spec/influxdb/client_spec.rb +37 -810
- data/spec/influxdb/config_spec.rb +118 -0
- data/spec/influxdb/{logger_spec.rb → logging_spec.rb} +4 -8
- data/spec/influxdb/max_queue_spec.rb +29 -0
- data/spec/influxdb/point_value_spec.rb +81 -14
- data/spec/influxdb/worker_spec.rb +8 -11
- data/spec/spec_helper.rb +7 -10
- metadata +65 -30
- data/lib/influxdb/udp_client.rb +0 -16
- data/lib/influxdb/worker.rb +0 -80
- data/spec/influxdb/udp_client_spec.rb +0 -33
- data/spec/influxdb_spec.rb +0 -4
- data/spec/max_queue_spec.rb +0 -32
@@ -0,0 +1,123 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'cgi'
|
3
|
+
require 'net/http'
|
4
|
+
require 'net/https'
|
5
|
+
|
6
|
+
module InfluxDB
|
7
|
+
# rubocop:disable Metrics/MethodLength
|
8
|
+
# rubocop:disable Metrics/AbcSize
|
9
|
+
module HTTP # :nodoc:
|
10
|
+
def get(url, options = {})
|
11
|
+
connect_with_retry do |http|
|
12
|
+
response = do_request http, Net::HTTP::Get.new(url)
|
13
|
+
if response.is_a? Net::HTTPSuccess
|
14
|
+
handle_successful_response(response, options)
|
15
|
+
elsif response.is_a? Net::HTTPUnauthorized
|
16
|
+
fail InfluxDB::AuthenticationError, response.body
|
17
|
+
else
|
18
|
+
resolve_error(response.body)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def post(url, data)
|
24
|
+
headers = { "Content-Type" => "application/octet-stream" }
|
25
|
+
connect_with_retry do |http|
|
26
|
+
response = do_request http, Net::HTTP::Post.new(url, headers), data
|
27
|
+
if response.is_a? Net::HTTPSuccess
|
28
|
+
return response
|
29
|
+
elsif response.is_a? Net::HTTPUnauthorized
|
30
|
+
fail InfluxDB::AuthenticationError, response.body
|
31
|
+
else
|
32
|
+
resolve_error(response.body)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def connect_with_retry(&block)
|
40
|
+
hosts = config.hosts.dup
|
41
|
+
delay = config.initial_delay
|
42
|
+
retry_count = 0
|
43
|
+
|
44
|
+
begin
|
45
|
+
hosts.push(host = hosts.shift)
|
46
|
+
http = Net::HTTP.new(host, config.port)
|
47
|
+
http.open_timeout = config.open_timeout
|
48
|
+
http.read_timeout = config.read_timeout
|
49
|
+
|
50
|
+
http = setup_ssl(http)
|
51
|
+
|
52
|
+
block.call(http)
|
53
|
+
|
54
|
+
rescue Timeout::Error, *InfluxDB::NET_HTTP_EXCEPTIONS => e
|
55
|
+
retry_count += 1
|
56
|
+
if (config.retry == -1 || retry_count <= config.retry) && !stopped?
|
57
|
+
log :error, "Failed to contact host #{host}: #{e.inspect} - retrying in #{delay}s."
|
58
|
+
sleep delay
|
59
|
+
delay = [config.max_delay, delay * 2].min
|
60
|
+
retry
|
61
|
+
else
|
62
|
+
raise InfluxDB::ConnectionError, "Tried #{retry_count - 1} times to reconnect but failed."
|
63
|
+
end
|
64
|
+
ensure
|
65
|
+
http.finish if http.started?
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def do_request(http, req, data = nil)
|
70
|
+
req.basic_auth config.username, config.password if basic_auth?
|
71
|
+
req.body = data if data
|
72
|
+
http.request(req)
|
73
|
+
end
|
74
|
+
|
75
|
+
def basic_auth?
|
76
|
+
config.auth_method == 'basic_auth'
|
77
|
+
end
|
78
|
+
|
79
|
+
def resolve_error(response)
|
80
|
+
if response =~ /Couldn\'t find series/
|
81
|
+
fail InfluxDB::SeriesNotFound, response
|
82
|
+
else
|
83
|
+
fail InfluxDB::Error, response
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def handle_successful_response(response, options)
|
88
|
+
parsed_response = JSON.parse(response.body) if response.body
|
89
|
+
errors = errors_from_response(parsed_response) if parsed_response
|
90
|
+
fail InfluxDB::QueryError, errors if errors
|
91
|
+
options.fetch(:parse, false) ? parsed_response : response
|
92
|
+
end
|
93
|
+
|
94
|
+
def errors_from_response(parsed_resp)
|
95
|
+
parsed_resp.is_a?(Hash) && parsed_resp.fetch('results', [])
|
96
|
+
.fetch(0, {})
|
97
|
+
.fetch('error', nil)
|
98
|
+
end
|
99
|
+
|
100
|
+
def setup_ssl(http)
|
101
|
+
http.use_ssl = config.use_ssl
|
102
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE unless config.verify_ssl
|
103
|
+
|
104
|
+
return http unless config.use_ssl
|
105
|
+
|
106
|
+
http.cert_store = generate_cert_store
|
107
|
+
http
|
108
|
+
end
|
109
|
+
|
110
|
+
def generate_cert_store
|
111
|
+
store = OpenSSL::X509::Store.new
|
112
|
+
store.set_default_paths
|
113
|
+
if config.ssl_ca_cert
|
114
|
+
if File.directory?(config.ssl_ca_cert)
|
115
|
+
store.add_path(config.ssl_ca_cert)
|
116
|
+
else
|
117
|
+
store.add_file(config.ssl_ca_cert)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
store
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module InfluxDB
|
2
|
+
# InfluxDB client configuration
|
3
|
+
class Config
|
4
|
+
AUTH_METHODS = %w(params basic_auth)
|
5
|
+
|
6
|
+
attr_accessor :hosts,
|
7
|
+
:port,
|
8
|
+
:username,
|
9
|
+
:password,
|
10
|
+
:database,
|
11
|
+
:time_precision,
|
12
|
+
:use_ssl,
|
13
|
+
:verify_ssl,
|
14
|
+
:ssl_ca_cert,
|
15
|
+
:auth_method,
|
16
|
+
:initial_delay,
|
17
|
+
:max_delay,
|
18
|
+
:open_timeout,
|
19
|
+
:read_timeout,
|
20
|
+
:retry,
|
21
|
+
:prefix,
|
22
|
+
:denormalize
|
23
|
+
|
24
|
+
attr_reader :async, :udp
|
25
|
+
|
26
|
+
# rubocop:disable all
|
27
|
+
def initialize(opts = {})
|
28
|
+
@database = opts[:database]
|
29
|
+
@hosts = Array(opts[:hosts] || opts[:host] || ["localhost"])
|
30
|
+
@port = opts.fetch(:port, 8086)
|
31
|
+
@prefix = opts.fetch(:prefix, '')
|
32
|
+
@username = opts.fetch(:username, "root")
|
33
|
+
@password = opts.fetch(:password, "root")
|
34
|
+
@auth_method = AUTH_METHODS.include?(opts[:auth_method]) ? opts[:auth_method] : "params"
|
35
|
+
@use_ssl = opts.fetch(:use_ssl, false)
|
36
|
+
@verify_ssl = opts.fetch(:verify_ssl, true)
|
37
|
+
@ssl_ca_cert = opts.fetch(:ssl_ca_cert, false)
|
38
|
+
@time_precision = opts.fetch(:time_precision, "s")
|
39
|
+
@initial_delay = opts.fetch(:initial_delay, 0.01)
|
40
|
+
@max_delay = opts.fetch(:max_delay, 30)
|
41
|
+
@open_timeout = opts.fetch(:write_timeout, 5)
|
42
|
+
@read_timeout = opts.fetch(:read_timeout, 300)
|
43
|
+
@async = opts.fetch(:async, false)
|
44
|
+
@udp = opts.fetch(:udp, false)
|
45
|
+
@retry = opts.fetch(:retry, nil)
|
46
|
+
@denormalize = opts.fetch(:denormalize, true)
|
47
|
+
@retry =
|
48
|
+
case @retry
|
49
|
+
when Integer
|
50
|
+
@retry
|
51
|
+
when true, nil
|
52
|
+
-1
|
53
|
+
when false
|
54
|
+
0
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def udp?
|
59
|
+
!!udp
|
60
|
+
end
|
61
|
+
|
62
|
+
def async?
|
63
|
+
!!async
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
data/lib/influxdb/errors.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
require "net/http"
|
2
2
|
require "zlib"
|
3
3
|
|
4
|
-
module InfluxDB
|
4
|
+
module InfluxDB # :nodoc:
|
5
5
|
class Error < StandardError
|
6
6
|
end
|
7
7
|
|
@@ -11,9 +11,15 @@ module InfluxDB
|
|
11
11
|
class ConnectionError < Error
|
12
12
|
end
|
13
13
|
|
14
|
+
class SeriesNotFound < Error
|
15
|
+
end
|
16
|
+
|
14
17
|
class JSONParserError < Error
|
15
18
|
end
|
16
19
|
|
20
|
+
class QueryError < Error
|
21
|
+
end
|
22
|
+
|
17
23
|
# Taken from: https://github.com/lostisland/faraday/blob/master/lib/faraday/adapter/net_http.rb
|
18
24
|
NET_HTTP_EXCEPTIONS = [
|
19
25
|
EOFError,
|
@@ -27,7 +33,7 @@ module InfluxDB
|
|
27
33
|
Net::HTTPHeaderSyntaxError,
|
28
34
|
Net::ProtocolError,
|
29
35
|
SocketError,
|
30
|
-
Zlib::GzipFile::Error
|
36
|
+
Zlib::GzipFile::Error
|
31
37
|
]
|
32
38
|
|
33
39
|
NET_HTTP_EXCEPTIONS << OpenSSL::SSL::SSLError if defined?(OpenSSL)
|
@@ -1,19 +1,20 @@
|
|
1
1
|
require 'logger'
|
2
2
|
|
3
3
|
module InfluxDB
|
4
|
-
module Logging
|
4
|
+
module Logging # :nodoc:
|
5
5
|
PREFIX = "[InfluxDB] "
|
6
6
|
|
7
|
-
|
8
|
-
|
7
|
+
class << self
|
8
|
+
attr_writer :logger
|
9
9
|
end
|
10
10
|
|
11
11
|
def self.logger
|
12
|
-
return @logger
|
13
|
-
@logger
|
12
|
+
return false if @logger == false
|
13
|
+
@logger ||= ::Logger.new(STDERR).tap { |logger| logger.level = Logger::INFO }
|
14
14
|
end
|
15
15
|
|
16
16
|
private
|
17
|
+
|
17
18
|
def log(level, message)
|
18
19
|
InfluxDB::Logging.logger.send(level.to_sym, PREFIX + message) if InfluxDB::Logging.logger
|
19
20
|
end
|
data/lib/influxdb/max_queue.rb
CHANGED
@@ -1,11 +1,12 @@
|
|
1
1
|
require "thread"
|
2
2
|
|
3
3
|
module InfluxDB
|
4
|
+
# Queue with max length limit
|
4
5
|
class MaxQueue < Queue
|
5
6
|
attr_reader :max
|
6
7
|
|
7
8
|
def initialize(max = 10_000)
|
8
|
-
|
9
|
+
fail ArgumentError, "queue size must be positive" unless max > 0
|
9
10
|
@max = max
|
10
11
|
super()
|
11
12
|
end
|
data/lib/influxdb/point_value.rb
CHANGED
@@ -1,36 +1,38 @@
|
|
1
|
-
require 'json'
|
2
|
-
|
3
1
|
module InfluxDB
|
4
|
-
|
2
|
+
# Convert data point to string using Line protocol
|
5
3
|
class PointValue
|
6
|
-
|
4
|
+
attr_reader :series, :values, :tags, :timestamp
|
7
5
|
|
8
|
-
def initialize(
|
9
|
-
@
|
6
|
+
def initialize(data)
|
7
|
+
@series = data[:series].gsub(/\s/, '\ ').gsub(',','\,')
|
8
|
+
@values = stringify(data[:values])
|
9
|
+
@tags = stringify(data[:tags])
|
10
|
+
@timestamp = data[:timestamp]
|
10
11
|
end
|
11
12
|
|
12
13
|
def dump
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
14
|
+
dump = "#{@series}"
|
15
|
+
dump << ",#{@tags}" if @tags
|
16
|
+
dump << " #{@values}"
|
17
|
+
dump << " #{@timestamp}" if @timestamp
|
18
|
+
dump
|
18
19
|
end
|
19
20
|
|
20
|
-
|
21
|
-
if maybe_json?
|
22
|
-
begin
|
23
|
-
JSON.parse(value)
|
24
|
-
rescue JSON::ParserError => e
|
25
|
-
value
|
26
|
-
end
|
27
|
-
else
|
28
|
-
value
|
29
|
-
end
|
30
|
-
end
|
21
|
+
private
|
31
22
|
|
32
|
-
def
|
33
|
-
|
23
|
+
def stringify(hash)
|
24
|
+
return nil unless hash && !hash.empty?
|
25
|
+
hash.map do |k, v|
|
26
|
+
key = k.to_s.gsub(/\s/, '\ ').gsub(',','\,')
|
27
|
+
val = v
|
28
|
+
if val.is_a?(String)
|
29
|
+
val.gsub!(/\s/, '\ ')
|
30
|
+
val.gsub!(',', '\,')
|
31
|
+
val.gsub!('"', '\"')
|
32
|
+
val = '"' + val + '"'
|
33
|
+
end
|
34
|
+
"#{key}=#{val}"
|
35
|
+
end.join(',')
|
34
36
|
end
|
35
37
|
end
|
36
|
-
end
|
38
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module InfluxDB
|
2
|
+
module Query
|
3
|
+
module Cluster # :nodoc:
|
4
|
+
def create_cluster_admin(username, password)
|
5
|
+
execute("CREATE USER #{username} WITH PASSWORD '#{password}' WITH ALL PRIVILEGES")
|
6
|
+
end
|
7
|
+
|
8
|
+
def list_cluster_admins
|
9
|
+
list_users.select { |u| u['admin'] }.map { |u| u['username'] }
|
10
|
+
end
|
11
|
+
|
12
|
+
def revoke_cluster_admin_privileges(username)
|
13
|
+
execute("REVOKE ALL PRIVILEGES FROM #{username}")
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module InfluxDB
|
2
|
+
module Query
|
3
|
+
module ContinuousQuery # :nodoc:
|
4
|
+
def list_continuous_queries(database)
|
5
|
+
resp = execute("SHOW CONTINUOUS QUERIES", parse: true)
|
6
|
+
fetch_series(resp).select { |v| v['name'] == database }
|
7
|
+
.fetch(0, {})
|
8
|
+
.fetch('values', [])
|
9
|
+
.map { |v| { 'name' => v.first, 'query' => v.last } }
|
10
|
+
end
|
11
|
+
|
12
|
+
def create_continuous_query(name, database, query)
|
13
|
+
clause = ["CREATE CONTINUOUS QUERY #{name} ON #{database} BEGIN", query, "END"].join("\n")
|
14
|
+
execute(clause)
|
15
|
+
end
|
16
|
+
|
17
|
+
def delete_continuous_query(name, database)
|
18
|
+
execute("DROP CONTINUOUS QUERY #{name} ON #{database}")
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
module InfluxDB
|
2
|
+
module Query # :nodoc: all
|
3
|
+
# rubocop:disable Metrics/AbcSize
|
4
|
+
module Core
|
5
|
+
def ping
|
6
|
+
get "/ping"
|
7
|
+
end
|
8
|
+
|
9
|
+
# rubocop:disable Metrics/MethodLength
|
10
|
+
def query(query, opts = {})
|
11
|
+
precision = opts.fetch(:precision, config.time_precision)
|
12
|
+
denormalize = opts.fetch(:denormalize, config.denormalize)
|
13
|
+
|
14
|
+
url = full_url("/query", q: query, db: config.database, precision: precision)
|
15
|
+
series = fetch_series(get(url, parse: true))
|
16
|
+
|
17
|
+
if block_given?
|
18
|
+
series.each do |s|
|
19
|
+
yield s['name'], s['tags'], denormalize ? denormalize_series(s) : raw_values(s)
|
20
|
+
end
|
21
|
+
else
|
22
|
+
denormalize ? denormalized_series_list(series) : series
|
23
|
+
end
|
24
|
+
end
|
25
|
+
# rubocop:enable Metrics/MethodLength
|
26
|
+
|
27
|
+
# Example:
|
28
|
+
# write_points([
|
29
|
+
# {
|
30
|
+
# series: 'cpu',
|
31
|
+
# tags: { host: 'server_nl', regios: 'us' },
|
32
|
+
# values: {internal: 5, external: 6},
|
33
|
+
# timestamp: 1422568543702900257
|
34
|
+
# },
|
35
|
+
# {
|
36
|
+
# series: 'gpu',
|
37
|
+
# values: {value: 0.9999},
|
38
|
+
# }
|
39
|
+
# ])
|
40
|
+
def write_points(data, precision = nil)
|
41
|
+
data = data.is_a?(Array) ? data : [data]
|
42
|
+
payload = generate_payload(data)
|
43
|
+
writer.write(payload, precision)
|
44
|
+
end
|
45
|
+
|
46
|
+
# Example:
|
47
|
+
# write_point('cpu', tags: {region: 'us'}, values: {internal: 60})
|
48
|
+
def write_point(series, data, precision = nil)
|
49
|
+
data.merge!(series: series)
|
50
|
+
write_points(data, precision)
|
51
|
+
end
|
52
|
+
|
53
|
+
def write(data, precision)
|
54
|
+
precision ||= config.time_precision
|
55
|
+
url = full_url("/write", db: config.database, precision: precision)
|
56
|
+
post(url, data)
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def denormalized_series_list(series)
|
62
|
+
series.map do |s|
|
63
|
+
{
|
64
|
+
'name' => s['name'],
|
65
|
+
'tags' => s['tags'],
|
66
|
+
'values' => denormalize_series(s)
|
67
|
+
}
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def fetch_series(response)
|
72
|
+
response.fetch('results', [])
|
73
|
+
.fetch(0, {})
|
74
|
+
.fetch('series', [])
|
75
|
+
end
|
76
|
+
|
77
|
+
def generate_payload(data)
|
78
|
+
data.map do |point|
|
79
|
+
InfluxDB::PointValue.new(point).dump
|
80
|
+
end.join("\n")
|
81
|
+
end
|
82
|
+
|
83
|
+
def execute(query, options = {})
|
84
|
+
url = full_url("/query", q: query)
|
85
|
+
get(url, options)
|
86
|
+
end
|
87
|
+
|
88
|
+
def denormalize_series(series)
|
89
|
+
Array(series["values"]).map do |values|
|
90
|
+
Hash[series["columns"].zip(values)]
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def raw_values(series)
|
95
|
+
series.select { |k, _| %w(columns values).include?(k) }
|
96
|
+
end
|
97
|
+
|
98
|
+
def full_url(path, params = {})
|
99
|
+
unless basic_auth?
|
100
|
+
params[:u] = config.username
|
101
|
+
params[:p] = config.password
|
102
|
+
end
|
103
|
+
|
104
|
+
query = params.map { |k, v| [CGI.escape(k.to_s), "=", CGI.escape(v.to_s)].join }.join("&")
|
105
|
+
|
106
|
+
URI::Generic.build(path: File.join(config.prefix, path), query: query).to_s
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|