fluent-plugin-elasticsearch-stats 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'metadata'
4
+ require_relative 'metric'
5
+
6
+ module Fluent
7
+ module Plugin
8
+ module ElasticsearchStats
9
+ class BaseData
10
+ attr_reader :data
11
+
12
+ # FIXME
13
+ # skip_system_indices
14
+ def initialize(data, family: self.class::NAME, metadata: Metadata.new, metric: Metric.new)
15
+ @data = data
16
+ @family = family
17
+ @metadata = metadata
18
+ @metric = metric
19
+
20
+ initialize_metadata
21
+ end
22
+
23
+ def extract_metrics
24
+ []
25
+ end
26
+
27
+ private
28
+
29
+ attr_reader :family, :metadata, :metric
30
+
31
+ def initialize_metadata
32
+ metadata.set(label: 'cluster_name', value: cluster_name) if cluster_name
33
+ end
34
+
35
+ def cluster_name
36
+ data['cluster_name']
37
+ end
38
+
39
+ def extract_indices_metrics
40
+ base_metrics = extract_indices_base_metrics
41
+ aggregated_metrics = extract_indices_aggregated_metrics(base_metrics)
42
+
43
+ base_metrics + aggregated_metrics
44
+ end
45
+
46
+ def extract_indices_base_metrics
47
+ return [] if !indices || indices.empty?
48
+
49
+ metrics = []
50
+ indices.each do |index_name, index_stats|
51
+ local_metadata = metadata.dup.set(label: 'index', value: index_name)
52
+ flattened_stats = Utils.hash_flatten_keys(index_stats, separator: metric.name_separator)
53
+ flattened_stats.each do |k, v|
54
+ metrics << metric.format(name: ['index', k], value: v, family: family, metadata: local_metadata)
55
+ end
56
+ end
57
+ metrics.compact
58
+ end
59
+
60
+ def extract_indices_aggregated_metrics(base_metrics)
61
+ metrics = []
62
+
63
+ grouped_metrics = {}
64
+ base_metrics.each do |base_metric|
65
+ index_base = base_metric[metadata.label_for('index_base')]
66
+ next unless index_base
67
+
68
+ base_metric_name = base_metric[metric.name_label]
69
+ grouped_metrics[index_base] ||= {}
70
+ grouped_metrics[index_base][base_metric_name] ||= []
71
+ grouped_metrics[index_base][base_metric_name] << base_metric
72
+ end
73
+
74
+ grouped_metrics.each do |index_base, index_base_metrics|
75
+ local_metadata = metadata.dup
76
+ .set(label: 'index', value: index_base)
77
+ .set(label: 'aggregated', value: true)
78
+
79
+ index_base_metrics.each do |metric_name, metric_name_metrics|
80
+ metric_values = metric_name_metrics.map { |a_metric| a_metric[metric.value_label] }
81
+ count = metric_values.size
82
+ min = metric_values.min
83
+ max = metric_values.max
84
+ sum = metric_values.sum
85
+ avg = sum / count.to_f
86
+
87
+ metrics << metric.format(name: [metric_name, 'count'],
88
+ value: count,
89
+ family: family,
90
+ metadata: local_metadata)
91
+ metrics << metric.format(name: [metric_name, 'min'],
92
+ value: min,
93
+ family: family,
94
+ metadata: local_metadata)
95
+ metrics << metric.format(name: [metric_name, 'max'],
96
+ value: max,
97
+ family: family,
98
+ metadata: local_metadata)
99
+ metrics << metric.format(name: [metric_name, 'sum'],
100
+ value: sum,
101
+ family: family,
102
+ metadata: local_metadata)
103
+ metrics << metric.format(name: [metric_name, 'avg'],
104
+ value: avg,
105
+ family: family,
106
+ metadata: local_metadata)
107
+ end
108
+ end
109
+
110
+ metrics.compact
111
+ end
112
+
113
+ def status
114
+ data['status']
115
+ end
116
+
117
+ def indices
118
+ data['indices']
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,150 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'faraday'
4
+
5
+ module Fluent
6
+ module Plugin
7
+ module ElasticsearchStats
8
+ class Client
9
+ class Error < StandardError
10
+ end
11
+
12
+ TIMEOUT = 10
13
+ USER_AGENT = 'elasticsearch_stats'
14
+
15
+ LOCAL = false
16
+ CLUSTER_HEALTH_LEVEL = 'cluster'
17
+ NODES_STATS_LEVEL = 'cluster'
18
+ INDICES_STATS_LEVEL = 'indices'
19
+
20
+ ALLOWED_CLUSTER_HEALTH_LEVELS = %i[cluster indices shards].freeze
21
+ ALLOWED_NODES_STATS_LEVELS = %i[nodes indices shards].freeze
22
+ ALLOWED_NODES_STATS_METRICS = %i[
23
+ adaptive_selection
24
+ breaker
25
+ discovery
26
+ fs
27
+ http
28
+ indexing_pressure
29
+ indices
30
+ ingest
31
+ jvm
32
+ os
33
+ process
34
+ repositories
35
+ thread_pool
36
+ transport
37
+ ].freeze
38
+ ALLOWED_INDICES_LEVELS = %i[cluster indices shards].freeze
39
+ ALLOWED_INDICES_METRICS = %i[
40
+ _all
41
+ completion
42
+ docs
43
+ fielddata
44
+ flush
45
+ get
46
+ indexing
47
+ merge
48
+ query_cache
49
+ refresh
50
+ request_cache
51
+ search
52
+ segments
53
+ store
54
+ translog
55
+ ].freeze
56
+
57
+ attr_reader :url, :timeout, :username, :password, :user_agent,
58
+ :ca_file, :verify_ssl, :log, :client
59
+
60
+ def initialize(url:, timeout: TIMEOUT, username: nil, password: nil,
61
+ user_agent: USER_AGENT, ca_file: nil, verify_ssl: true,
62
+ log: nil)
63
+ @url = url
64
+ @timeout = timeout
65
+ @username = username
66
+ @password = password
67
+ @user_agent = user_agent
68
+ @ca_file = ca_file
69
+ @verify_ssl = verify_ssl
70
+ @log = log
71
+ end
72
+
73
+ # https://www.elastic.co/guide/en/elasticsearch/reference/current/cluster-health.html
74
+ def cluster_health(level: CLUSTER_HEALTH_LEVEL, local: LOCAL)
75
+ endpoint = '/_cluster/health'
76
+ params = { level: level, local: local, timeout: "#{timeout}s" }
77
+ get(endpoint, params)
78
+ end
79
+
80
+ # https://www.elastic.co/guide/en/elasticsearch/reference/current/cluster-stats.html
81
+ def cluster_stats
82
+ endpoint = '/_cluster/stats'
83
+ params = { timeout: "#{timeout}s" }
84
+ get(endpoint, params)
85
+ end
86
+
87
+ # https://www.elastic.co/guide/en/elasticsearch/reference/current/cluster-nodes-stats.html
88
+ def nodes_stats(level: NODES_STATS_LEVEL, metrics: nil)
89
+ endpoint = '/_nodes/stats'
90
+ endpoint += "/#{metrics.join(',')}" if metrics&.any?
91
+ params = { level: level, timeout: "#{timeout}s" }
92
+ get(endpoint, params)
93
+ end
94
+
95
+ # https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-stats.html
96
+ def indices_stats(indices: [:_all], level: INDICES_STATS_LEVEL, metrics: nil)
97
+ endpoint = "/_stats"
98
+ endpoint = "/#{indices.join(',')}#{endpoint}" if !indices.nil? && !indices.empty?
99
+ endpoint += "/#{metrics.join(',')}" if metrics&.any?
100
+ params = {
101
+ level: level
102
+ }
103
+ get(endpoint, params)
104
+ end
105
+
106
+ # https://www.elastic.co/guide/en/elasticsearch/reference/current/dangling-indices-list.html
107
+ def dangling
108
+ endpoint = '/_dangling'
109
+ get(endpoint)
110
+ end
111
+
112
+ private
113
+
114
+ def get(endpoint, params = nil, header = nil)
115
+ response = conn.get(endpoint, params, header)
116
+ response.body
117
+ rescue Faraday::Error => e
118
+ log.error("while get #{endpoint}: #{e}")
119
+ raise Error, "error on get #{endpoint}", e
120
+ end
121
+
122
+ def conn
123
+ faraday_options = {
124
+ request: {
125
+ open_timeout: timeout + 1,
126
+ read_timeout: timeout + 1,
127
+ write_timeout: timeout + 1
128
+ },
129
+ ssl: {
130
+ verify: verify_ssl,
131
+ ca_file: ca_file
132
+ },
133
+ headers: {
134
+ 'User-Agent' => user_agent
135
+ }
136
+ }
137
+
138
+ @conn ||= Faraday.new(url: url, **faraday_options) do |config|
139
+ config.request :authorization, :basic, username, password if username && password
140
+ config.request :json
141
+ config.response :json
142
+ config.response :raise_error
143
+ config.response :logger, log, headers: false, bodies: false, log_level: :debug if log
144
+ config.adapter :net_http
145
+ end
146
+ end
147
+ end
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_data'
4
+
5
+ module Fluent
6
+ module Plugin
7
+ module ElasticsearchStats
8
+ class ClusterHealthData < BaseData
9
+ NAME = 'cluster_health'
10
+
11
+ def extract_metrics
12
+ extract_cluster_metrics + extract_indices_metrics
13
+ end
14
+
15
+ private
16
+
17
+ def extract_cluster_metrics
18
+ data.each_with_object([]) do |(k, v), metrics|
19
+ metrics << metric.format(name: ['cluster', k], value: v, family: family, metadata: metadata)
20
+ end.compact
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_data'
4
+
5
+ module Fluent
6
+ module Plugin
7
+ module ElasticsearchStats
8
+ class ClusterStatsData < BaseData
9
+ NAME = 'cluster_stats'
10
+
11
+ def extract_metrics
12
+ extract_base_metrics + extract_indices_metrics + extract_nodes_metrics
13
+ end
14
+
15
+ private
16
+
17
+ def extract_base_metrics
18
+ status_metric = metric.format(
19
+ name: %w[cluster status], value: status, family: family, metadata: metadata
20
+ )
21
+ [status_metric]
22
+ end
23
+
24
+ def extract_indices_metrics
25
+ metrics = []
26
+
27
+ flattened_data = Utils.hash_flatten_keys(indices, separator: metric.name_separator)
28
+ flattened_data.each do |k, v|
29
+ metrics << metric.format(name: ['indices', k], value: v, family: family, metadata: metadata)
30
+ end
31
+ metrics.compact
32
+ end
33
+
34
+ def extract_nodes_metrics
35
+ metrics = []
36
+ flattened_data = Utils.hash_flatten_keys(nodes, separator: metric.name_separator)
37
+ flattened_data.each do |k, v|
38
+ metrics << metric.format(name: ['nodes', k], value: v, family: family, metadata: metadata)
39
+ end
40
+ metrics.compact
41
+ end
42
+
43
+ def nodes
44
+ data['nodes']
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fluent
4
+ module Plugin
5
+ module ElasticsearchStats
6
+ class Collector
7
+ attr_reader :client, :stats_config, :log
8
+
9
+ def initialize(client:,
10
+ stats_config: {},
11
+ log: nil)
12
+ @client = client
13
+ @stats_config = stats_config
14
+ @log = log
15
+ end
16
+
17
+ def collect_stats_metrics
18
+ metrics = []
19
+ metrics += collect_cluster_health_metrics
20
+ metrics += collect_cluster_stats_metrics
21
+ metrics += collect_nodes_stats_metrics
22
+ metrics += collect_indices_stats_metrics
23
+ metrics += collect_dangling_metrics
24
+ metrics.compact
25
+ end
26
+
27
+ def collect_cluster_health_metrics
28
+ return [] unless stats_config.cluster_health
29
+
30
+ without_error(rescue_return: []) do
31
+ data = client.cluster_health(
32
+ level: stats_config.cluster_health_level,
33
+ local: stats_config.cluster_health_local
34
+ )
35
+ ClusterHealthData.new(data).extract_metrics
36
+ end
37
+ end
38
+
39
+ def collect_cluster_stats_metrics
40
+ return [] unless stats_config.cluster_stats
41
+
42
+ without_error(rescue_return: []) do
43
+ data = client.cluster_stats
44
+ ClusterStatsData.new(data).extract_metrics
45
+ end
46
+ end
47
+
48
+ def collect_nodes_stats_metrics
49
+ return [] unless stats_config.nodes_stats
50
+
51
+ without_error(rescue_return: []) do
52
+ data = client.nodes_stats(
53
+ level: stats_config.nodes_stats_level,
54
+ metrics: stats_config.nodes_stats_metrics
55
+ )
56
+ NodesStatsData.new(data).extract_metrics
57
+ end
58
+ end
59
+
60
+ def collect_indices_stats_metrics
61
+ return [] unless stats_config.indices_stats
62
+
63
+ without_error(rescue_return: []) do
64
+ data = client.indices_stats(
65
+ indices: stats_config.indices,
66
+ level: stats_config.indices_stats_level
67
+ )
68
+ IndicesStatsData.new(data).extract_metrics
69
+ end
70
+ end
71
+
72
+ def collect_dangling_metrics
73
+ return [] unless stats_config.dangling
74
+
75
+ without_error(rescue_return: []) do
76
+ data = client.dangling
77
+ DanglingData.new(data).extract_metrics
78
+ end
79
+ end
80
+
81
+ # FIXME: inject metadata !
82
+ # cluster metadata / info ?
83
+ def metadata
84
+ Metadata.new
85
+ .set(label: 'cluster_url', value: client.url)
86
+ end
87
+
88
+ def without_error(error: StandardError, rescue_return: nil)
89
+ yield
90
+ rescue error => e
91
+ log.error("while collecting metrics: #{e}")
92
+ rescue_return
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_data'
4
+
5
+ module Fluent
6
+ module Plugin
7
+ module ElasticsearchStats
8
+ class DanglingData < BaseData
9
+ NAME = 'dangling'
10
+
11
+ def extract_metrics
12
+ generate_dangling_indices_count
13
+ end
14
+
15
+ def generate_dangling_indices_count
16
+ metrics = []
17
+ metrics << metric.format(name: %w[dangling all count],
18
+ value: dangling_indices.size,
19
+ family: family,
20
+ metadata: metadata)
21
+ metrics
22
+ end
23
+
24
+ private
25
+
26
+ def dangling_indices
27
+ data.fetch('dangling_indices', [])
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_data'
4
+
5
+ module Fluent
6
+ module Plugin
7
+ module ElasticsearchStats
8
+ class IndicesStatsData < BaseData
9
+ NAME = 'indices_stats'
10
+
11
+ def extract_metrics
12
+ extract_all_metrics + extract_indices_metrics
13
+ end
14
+
15
+ def extract_all_metrics
16
+ return [] if !_all || _all.empty?
17
+
18
+ metrics = []
19
+ flattened = Utils.hash_flatten_keys(_all, separator: metric.name_separator)
20
+ flattened.each do |k, v|
21
+ metrics << metric.format(name: ['all_indices', k], value: v, family: family, metadata: metadata)
22
+ end
23
+ metrics.compact
24
+ end
25
+
26
+ private
27
+
28
+ def _all
29
+ data['_all']
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fluent
4
+ module Plugin
5
+ module ElasticsearchStats
6
+ class Metadata
7
+ class << self
8
+ attr_accessor :metadata_prefix
9
+ end
10
+
11
+ attr_reader :metadata, :metadata_prefix
12
+
13
+ def initialize(metadata = {}, metadata_prefix: self.class.metadata_prefix)
14
+ @metadata = {}.update(metadata)
15
+ @metadata_prefix = metadata_prefix
16
+ end
17
+
18
+ def set(label:, value:)
19
+ return self if label.nil? || label.to_s.empty?
20
+
21
+ @metadata[label_for(label)] = value
22
+
23
+ self
24
+ end
25
+
26
+ def get(label)
27
+ metadata[label_for(label)]
28
+ end
29
+
30
+ def dup
31
+ self.class.new(
32
+ metadata.clone,
33
+ metadata_prefix: metadata_prefix.clone
34
+ )
35
+ end
36
+
37
+ def label_for(label)
38
+ "#{metadata_prefix}#{label}"
39
+ end
40
+
41
+ def to_h
42
+ metadata.clone
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fluent
4
+ module Plugin
5
+ module ElasticsearchStats
6
+ class Metric
7
+ DEFAULT_NAME_SEPARATOR = '/'
8
+ DEFAULT_TIMESTAMP_FORMAT = :iso
9
+
10
+ class << self
11
+ attr_accessor :metric_prefix, :index_base_pattern, :index_base_replacement
12
+
13
+ def name_separator
14
+ @name_separator ||= DEFAULT_NAME_SEPARATOR
15
+ end
16
+
17
+ attr_writer :name_separator, :timestamp_format
18
+
19
+ def timestamp_format
20
+ @timestamp_format ||= DEFAULT_TIMESTAMP_FORMAT
21
+ end
22
+ end
23
+
24
+ attr_reader :metric_prefix, :timestamp_format, :index_base_pattern, :index_base_replacement, :name_separator
25
+
26
+ def initialize(metric_prefix: nil, timestamp_format: nil, index_base_pattern: nil, index_base_replacement: nil,
27
+ name_separator: nil)
28
+ @metric_prefix = metric_prefix || self.class.metric_prefix
29
+ @timestamp_format = timestamp_format || self.class.timestamp_format
30
+ @index_base_pattern = index_base_pattern || self.class.index_base_pattern
31
+ @index_base_replacement = index_base_replacement || self.class.index_base_replacement
32
+ @name_separator = name_separator || self.class.name_separator
33
+ end
34
+
35
+ def convert_status(status)
36
+ case status.downcase
37
+ when 'green' then 1
38
+ when 'yellow' then 2
39
+ when 'red' then 3
40
+ else 0
41
+ end
42
+ end
43
+
44
+ def timenow_epochmillis
45
+ (Time.now.utc.to_f * 1000).to_i
46
+ end
47
+
48
+ def timenow_iso
49
+ Time.now.utc.iso8601(3)
50
+ end
51
+
52
+ def timenow
53
+ send("timenow_#{timestamp_format}")
54
+ end
55
+
56
+ def format(name:, value:, family: nil, metadata: nil)
57
+ return if name.nil? || name.empty? || value.nil?
58
+
59
+ value = convert_status(value) if name == 'status' || name[-1] == 'status'
60
+ name = name.join(name_separator) if name.respond_to?(:join)
61
+ family = family.join(name_separator) if name.respond_to?(:join)
62
+
63
+ return unless value.is_a?(Numeric)
64
+
65
+ index_base = compute_index_base(metadata.get('index'))
66
+ local_metadata = metadata.dup
67
+ local_metadata.set(label: 'index_base', value: index_base) if index_base
68
+
69
+ metric = {}
70
+ metric.update(local_metadata.to_h)
71
+ metric['timestamp'] = timenow
72
+ metric[name_label] = name.to_s
73
+ metric[value_label] = value
74
+ metric[family_label] = family.to_s if family
75
+ metric
76
+ end
77
+
78
+ def name_label
79
+ "#{metric_prefix}name"
80
+ end
81
+
82
+ def value_label
83
+ "#{metric_prefix}value"
84
+ end
85
+
86
+ def family_label
87
+ "#{metric_prefix}family"
88
+ end
89
+
90
+ def compute_index_base(index_name)
91
+ return nil unless index_name
92
+ return nil if !index_base_pattern || !index_base_replacement
93
+
94
+ index_name.gsub(index_base_pattern, index_base_replacement)
95
+ rescue StandardError
96
+ nil
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_data'
4
+
5
+ module Fluent
6
+ module Plugin
7
+ module ElasticsearchStats
8
+ class NodesStatsData < BaseData
9
+ NAME = 'nodes_stats'
10
+
11
+ def extract_metrics
12
+ metrics = []
13
+ nodes.each_value do |node_stats|
14
+ metadata.dup
15
+ .set(label: 'hostname', value: node_stats['name'])
16
+ .set(label: 'host', value: node_stats['host'])
17
+
18
+ node_stats.each do |stat_family, stats|
19
+ next unless stats.is_a?(Hash)
20
+
21
+ stats.delete('timestamp')
22
+ stats.delete('indices') if stat_family == 'indices'
23
+ # FIXME: indices stats to extract ?
24
+
25
+ flattened_stats = Utils.hash_flatten_keys(stats, separator: metric.name_separator)
26
+ flattened_stats.each do |k, v|
27
+ metrics << metric.format(name: ['node', stat_family, k], value: v, family: family, metadata: metadata)
28
+ end
29
+ end
30
+ end
31
+ metrics.compact
32
+ end
33
+
34
+ private
35
+
36
+ def nodes
37
+ data['nodes']
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end