fluent-plugin-elasticsearch-stats 0.1.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.
@@ -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