logstash-filter-opensearch-manticore 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,52 @@
1
+ # Copyright OpenSearch Contributors
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ require 'opensearch'
5
+ require 'opensearch/transport/transport/connections/selector'
6
+
7
+ # elasticsearch-transport versions prior to 7.2.0 suffered of a race condition on accessing
8
+ # the connection pool. This issue was fixed (in 7.2.0) with
9
+ # https://github.com/elastic/elasticsearch-ruby/commit/15f9d78591a6e8823948494d94b15b0ca38819d1
10
+ #
11
+ # This plugin, at the moment, is using elasticsearch >= 5.0.5
12
+ # When this requirement ceases, this patch could be removed.
13
+ module OpenSearch
14
+ module Transport
15
+ module Transport
16
+ module Connections
17
+ module Selector
18
+
19
+ # "Round-robin" selector strategy (default).
20
+ #
21
+ class RoundRobin
22
+ include Base
23
+
24
+ # @option arguments [Connections::Collection] :connections Collection with connections.
25
+ #
26
+ def initialize(arguments = {})
27
+ super
28
+ @mutex = Mutex.new
29
+ @current = nil
30
+ end
31
+
32
+ # Returns the next connection from the collection, rotating them in round-robin fashion.
33
+ #
34
+ # @return [Connections::Connection]
35
+ #
36
+ def select(options={})
37
+ @mutex.synchronize do
38
+ conns = connections
39
+ if @current && (@current < conns.size-1)
40
+ @current += 1
41
+ else
42
+ @current = 0
43
+ end
44
+ conns[@current]
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,44 @@
1
+ # Copyright OpenSearch Contributors
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ # encoding: utf-8
5
+ require "opensearch"
6
+ require "opensearch/transport/transport/http/manticore"
7
+
8
+
9
+ # elasticsearch-transport 7.2.0 - 7.14.0 had a bug where setting http headers
10
+ # ES::Client.new ..., transport_options: { headers: { 'Authorization' => ... } }
11
+ # would be lost https://github.com/elastic/elasticsearch-ruby/issues/1428
12
+ #
13
+ # NOTE: needs to be idempotent as filter OpenSearch plugin might apply the same patch!
14
+ #
15
+ # @private
16
+ module OpenSearch
17
+ module Transport
18
+ module Transport
19
+ module HTTP
20
+ class Manticore
21
+
22
+ def apply_headers(request_options, options)
23
+ headers = (options && options[:headers]) || {}
24
+ headers[CONTENT_TYPE_STR] = find_value(headers, CONTENT_TYPE_REGEX) || DEFAULT_CONTENT_TYPE
25
+
26
+ # this code is necessary to grab the correct user-agent header
27
+ # when this method is invoked with apply_headers(@request_options, options)
28
+ # from https://github.com/opensearch-project/opensearch-ruby/blob/main/opensearch-transport/lib/opensearch/transport/transport/http/manticore.rb#L122-L123
29
+ transport_user_agent = nil
30
+ if (options && options[:transport_options] && options[:transport_options][:headers])
31
+ transport_headers = options[:transport_options][:headers]
32
+ transport_user_agent = find_value(transport_headers, USER_AGENT_REGEX)
33
+ end
34
+
35
+ headers[USER_AGENT_STR] = transport_user_agent || find_value(headers, USER_AGENT_REGEX) || user_agent_header
36
+ headers[ACCEPT_ENCODING] = GZIP if use_compression?
37
+ (request_options[:headers] ||= {}).merge!(headers) # this line was changed
38
+ end
39
+
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,281 @@
1
+ # encoding: utf-8
2
+ require "logstash/filters/base"
3
+ require "logstash/namespace"
4
+ require "logstash/json"
5
+ require "logstash/util/safe_uri"
6
+ java_import "java.util.concurrent.ConcurrentHashMap"
7
+ require "opensearch"
8
+ require "opensearch/transport/transport/http/manticore"
9
+ require_relative "opensearch/patches/_opensearch_transport_http_manticore"
10
+ require_relative "opensearch/patches/_opensearch_transport_connections_selector"
11
+
12
+
13
+ class LogStash::Filters::OpenSearch < LogStash::Filters::Base
14
+ config_name "opensearch"
15
+
16
+ DEFAULT_HOST = ::LogStash::Util::SafeURI.new("//localhost:9200")
17
+
18
+ # List of opensearch hosts to use for querying.
19
+ config :hosts, :validate => :array, :default => [ DEFAULT_HOST ]
20
+
21
+ # Comma-delimited list of index names to search; use `_all` or empty string to perform the operation on all indices.
22
+ # Field substitution (e.g. `index-name-%{date_field}`) is available
23
+ config :index, :validate => :string, :default => ""
24
+
25
+ # OpenSearch query string. Read the OpenSearch query string documentation.
26
+ # for more info at: https://www.elastic.co/guide/en/opensearch/reference/master/query-dsl-query-string-query.html#query-string-syntax
27
+ config :query, :validate => :string
28
+
29
+ # File path to opensearch query in DSL format. Read the OpenSearch query documentation
30
+ # for more info at: https://www.elastic.co/guide/en/opensearch/reference/current/query-dsl.html
31
+ config :query_template, :validate => :string
32
+
33
+ # Comma-delimited list of `<field>:<direction>` pairs that define the sort order
34
+ config :sort, :validate => :string, :default => "@timestamp:desc"
35
+
36
+ # Array of fields to copy from old event (found via opensearch) into new event
37
+ config :fields, :validate => :array, :default => {}
38
+
39
+ # Hash of docinfo fields to copy from old event (found via opensearch) into new event
40
+ config :docinfo_fields, :validate => :hash, :default => {}
41
+
42
+ # Hash of aggregation names to copy from opensearch response into Logstash event fields
43
+ config :aggregation_fields, :validate => :hash, :default => {}
44
+
45
+ # Basic Auth - username
46
+ config :user, :validate => :string
47
+
48
+ # Basic Auth - password
49
+ config :password, :validate => :password
50
+
51
+ # Cloud ID, from the Elastic Cloud web console. If set `hosts` should not be used.
52
+ #
53
+ # For more info, check out the https://www.elastic.co/guide/en/logstash/current/connecting-to-cloud.html#_cloud_id[Logstash-to-Cloud documentation]
54
+ config :cloud_id, :validate => :string
55
+
56
+ # Cloud authentication string ("<username>:<password>" format) is an alternative for the `user`/`password` configuration.
57
+ #
58
+ # For more info, check out the https://www.elastic.co/guide/en/logstash/current/connecting-to-cloud.html#_cloud_auth[Logstash-to-Cloud documentation]
59
+ config :cloud_auth, :validate => :password
60
+
61
+ # Authenticate using OpenSearch API key.
62
+ # format is id:api_key (as returned by https://www.elastic.co/guide/en/opensearch/reference/current/security-api-create-api-key.html[Create API key])
63
+ config :api_key, :validate => :password
64
+
65
+ # SSL
66
+ config :ssl, :validate => :boolean, :default => false
67
+
68
+ # SSL Certificate Authority file
69
+ config :ca_file, :validate => :path
70
+
71
+ # Whether results should be sorted or not
72
+ config :enable_sort, :validate => :boolean, :default => true
73
+
74
+ # How many results to return
75
+ config :result_size, :validate => :number, :default => 1
76
+
77
+ # Tags the event on failure to look up geo information. This can be used in later analysis.
78
+ config :tag_on_failure, :validate => :array, :default => ["_opensearch_lookup_failure"]
79
+
80
+ attr_reader :clients_pool
81
+
82
+ def register
83
+ @clients_pool = java.util.concurrent.ConcurrentHashMap.new
84
+
85
+ #Load query if it exists
86
+ if @query_template
87
+ if File.zero?(@query_template)
88
+ raise "template is empty"
89
+ end
90
+ file = File.open(@query_template, 'r')
91
+ @query_dsl = file.read
92
+ end
93
+
94
+ validate_authentication
95
+
96
+ @transport_options = {:headers => {}}
97
+ @transport_options[:headers].merge!(setup_basic_auth(user, password))
98
+ @transport_options[:headers].merge!({'user-agent' => prepare_user_agent })
99
+ @transport_options[:request_timeout] = @request_timeout_seconds unless @request_timeout_seconds.nil?
100
+ @transport_options[:connect_timeout] = @connect_timeout_seconds unless @connect_timeout_seconds.nil?
101
+ @transport_options[:socket_timeout] = @socket_timeout_seconds unless @socket_timeout_seconds.nil?
102
+
103
+ @hosts = setup_hosts
104
+ @ssl_options = setup_ssl
105
+
106
+ test_connection!
107
+ end # def register
108
+
109
+ def filter(event)
110
+ matched = false
111
+ begin
112
+ params = {:index => event.sprintf(@index) }
113
+
114
+ if @query_dsl
115
+ query = LogStash::Json.load(event.sprintf(@query_dsl))
116
+ params[:body] = query
117
+ else
118
+ query = event.sprintf(@query)
119
+ params[:q] = query
120
+ params[:size] = result_size
121
+ params[:sort] = @sort if @enable_sort
122
+ end
123
+
124
+ @logger.debug("Querying opensearch for lookup", :params => params)
125
+
126
+ results = get_client.search(params)
127
+ raise "OpenSearch query error: #{results["_shards"]["failures"]}" if results["_shards"].include? "failures"
128
+
129
+ event.set("[@metadata][total_hits]", extract_total_from_hits(results['hits']))
130
+
131
+ resultsHits = results["hits"]["hits"]
132
+ if !resultsHits.nil? && !resultsHits.empty?
133
+ matched = true
134
+ @fields.each do |old_key, new_key|
135
+ old_key_path = extract_path(old_key)
136
+ set = resultsHits.map do |doc|
137
+ extract_value(doc["_source"], old_key_path)
138
+ end
139
+ event.set(new_key, set.count > 1 ? set : set.first)
140
+ end
141
+ @docinfo_fields.each do |old_key, new_key|
142
+ old_key_path = extract_path(old_key)
143
+ set = resultsHits.map do |doc|
144
+ extract_value(doc, old_key_path)
145
+ end
146
+ event.set(new_key, set.count > 1 ? set : set.first)
147
+ end
148
+ end
149
+
150
+ resultsAggs = results["aggregations"]
151
+ if !resultsAggs.nil? && !resultsAggs.empty?
152
+ matched = true
153
+ @aggregation_fields.each do |agg_name, ls_field|
154
+ event.set(ls_field, resultsAggs[agg_name])
155
+ end
156
+ end
157
+
158
+ rescue => e
159
+ if @logger.trace?
160
+ @logger.warn("Failed to query opensearch for previous event", :index => @index, :query => query, :event => event.to_hash, :error => e.message, :backtrace => e.backtrace)
161
+ elsif @logger.debug?
162
+ @logger.warn("Failed to query opensearch for previous event", :index => @index, :error => e.message, :backtrace => e.backtrace)
163
+ else
164
+ @logger.warn("Failed to query opensearch for previous event", :index => @index, :error => e.message)
165
+ end
166
+ @tag_on_failure.each{|tag| event.tag(tag)}
167
+ else
168
+ filter_matched(event) if matched
169
+ end
170
+ end # def filter
171
+
172
+ private
173
+
174
+ def new_client
175
+ # NOTE: could pass cloud-id/cloud-auth to client but than we would need to be stricter on ES version requirement
176
+ # and also LS parsing might differ from ES client's parsing so for consistency we do not pass cloud options ...
177
+ OpenSearch::Client.new(
178
+ :hosts => @hosts,
179
+ :transport_options => @transport_options,
180
+ :transport_class => ::OpenSearch::Transport::Transport::HTTP::Manticore,
181
+ :ssl => @ssl_options
182
+ )
183
+ end
184
+
185
+ def get_client
186
+ @clients_pool.computeIfAbsent(Thread.current, lambda { |x| new_client })
187
+ end
188
+
189
+ # get an array of path elements from a path reference
190
+ def extract_path(path_reference)
191
+ return [path_reference] unless path_reference.start_with?('[') && path_reference.end_with?(']')
192
+
193
+ path_reference[1...-1].split('][')
194
+ end
195
+
196
+ # given a Hash and an array of path fragments, returns the value at the path
197
+ # @param source [Hash{String=>Object}]
198
+ # @param path [Array{String}]
199
+ # @return [Object]
200
+ def extract_value(source, path)
201
+ path.reduce(source) do |memo, old_key_fragment|
202
+ break unless memo.include?(old_key_fragment)
203
+ memo[old_key_fragment]
204
+ end
205
+ end
206
+
207
+ # Given a "hits" object from an OpenSearch response, return the total number of hits in
208
+ # the result set.
209
+ # @param hits [Hash{String=>Object}]
210
+ # @return [Integer]
211
+ def extract_total_from_hits(hits)
212
+ total = hits['total']
213
+
214
+ # OpenSearch 7.x produces an object containing `value` and `relation` in order
215
+ # to enable unambiguous reporting when the total is only a lower bound; if we get
216
+ # an object back, return its `value`.
217
+ return total['value'] if total.kind_of?(Hash)
218
+
219
+ total
220
+ end
221
+
222
+ def hosts_default?(hosts)
223
+ # NOTE: would be nice if pipeline allowed us a clean way to detect a config default :
224
+ hosts.is_a?(Array) && hosts.size == 1 && hosts.first.equal?(DEFAULT_HOST)
225
+ end
226
+
227
+ def validate_authentication
228
+ authn_options = 0
229
+ authn_options += 1 if @cloud_auth
230
+ authn_options += 1 if (@api_key && @api_key.value)
231
+ authn_options += 1 if (@user || (@password && @password.value))
232
+
233
+ if authn_options > 1
234
+ raise LogStash::ConfigurationError, 'Multiple authentication options are specified, please only use one of user/password, cloud_auth or api_key'
235
+ end
236
+
237
+ if @api_key && @api_key.value && @ssl != true
238
+ raise(LogStash::ConfigurationError, "Using api_key authentication requires SSL/TLS secured communication using the `ssl => true` option")
239
+ end
240
+ end
241
+
242
+ def setup_hosts
243
+ @hosts = Array(@hosts).map { |host| host.to_s } # potential SafeURI#to_s
244
+ if @ssl
245
+ @hosts.map do |h|
246
+ host, port = h.split(":")
247
+ { :host => host, :scheme => 'https', :port => port }
248
+ end
249
+ else
250
+ @hosts
251
+ end
252
+ end
253
+
254
+ def setup_ssl
255
+ return { :ssl => true, :ca_file => @ca_file } if @ssl && @ca_file
256
+ return { :ssl => true, :verify => false } if @ssl # Setting verify as false if ca_file is not provided
257
+ end
258
+
259
+ def setup_basic_auth(user, password)
260
+ return {} unless user && password && password.value
261
+
262
+ token = ::Base64.strict_encode64("#{user}:#{password.value}")
263
+ { 'Authorization' => "Basic #{token}" }
264
+ end
265
+
266
+ def prepare_user_agent
267
+ os_name = java.lang.System.getProperty('os.name')
268
+ os_version = java.lang.System.getProperty('os.version')
269
+ os_arch = java.lang.System.getProperty('os.arch')
270
+ jvm_vendor = java.lang.System.getProperty('java.vendor')
271
+ jvm_version = java.lang.System.getProperty('java.version')
272
+
273
+ plugin_version = Gem.loaded_specs["logstash-filter-opensearch-manticore"].version
274
+ # example: logstash/7.14.1 (OS=Linux-5.4.0-84-generic-amd64; JVM=AdoptOpenJDK-11.0.11) logstash-input-opensearch/4.10.0
275
+ "logstash/#{LOGSTASH_VERSION} (OS=#{os_name}-#{os_version}-#{os_arch}; JVM=#{jvm_vendor}-#{jvm_version}) logstash-#{@plugin_type}-#{config_name}/#{plugin_version}"
276
+ end
277
+
278
+ def test_connection!
279
+ get_client.client.ping
280
+ end
281
+ end #class LogStash::Filters::OpenSearch
@@ -0,0 +1,29 @@
1
+ Gem::Specification.new do |s|
2
+
3
+ s.name = 'logstash-filter-opensearch-manticore'
4
+ s.version = '0.1.0'
5
+ s.licenses = ['Apache License (2.0)']
6
+ s.summary = "Copies fields from previous log events in OpenSearch to current events "
7
+ s.description = "This gem is a Logstash plugin required to be installed on top of the Logstash core pipeline using $LS_HOME/bin/logstash-plugin install gemname. This gem is not a stand-alone program"
8
+ s.authors = ["Anton Klyba"]
9
+ s.email = 'anarhyst266@gmail.com'
10
+ s.homepage = "https://github.com/Anarhyst266/logstash-filter-opensearch-manticore"
11
+ s.require_paths = ["lib"]
12
+
13
+ # Files
14
+ s.files = Dir["lib/**/*","spec/**/*","*.gemspec","*.md","CONTRIBUTORS","Gemfile","LICENSE","NOTICE.TXT", "vendor/jar-dependencies/**/*.jar", "vendor/jar-dependencies/**/*.rb", "VERSION", "docs/**/*"]
15
+
16
+ # Tests
17
+ s.test_files = s.files.grep(%r{^(test|spec|features)/})
18
+
19
+ # Special flag to let us know this is actually a logstash plugin
20
+ s.metadata = { "logstash_plugin" => "true", "logstash_group" => "filter" }
21
+
22
+ # Gem dependencies
23
+ s.add_runtime_dependency "logstash-core-plugin-api", ">= 1.60", "<= 2.99"
24
+ s.add_runtime_dependency 'opensearch-ruby'
25
+ s.add_runtime_dependency 'manticore', "~> 0.6"
26
+
27
+ s.add_development_dependency 'logstash-devutils'
28
+ end
29
+
@@ -0,0 +1,70 @@
1
+ {
2
+ "took": 49,
3
+ "timed_out": false,
4
+ "_shards": {
5
+ "total": 155,
6
+ "successful": 155,
7
+ "failed": 0
8
+ },
9
+ "hits": {
10
+ "total": {
11
+ "value": 13476,
12
+ "relation": "eq"
13
+ },
14
+ "max_score": 1,
15
+ "hits": [{
16
+ "_index": "logstash-2014.08.26",
17
+ "_type": "logs",
18
+ "_id": "AVVY76L_AW7v0kX8KXo4",
19
+ "_score": 1,
20
+ "_source": {
21
+ "request": "/doc/index.html?org/opensearch/action/search/SearchResponse.html",
22
+ "agent": "\"Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)\"",
23
+ "geoip": {
24
+ "timezone": "America/Los_Angeles",
25
+ "ip": "66.249.73.185",
26
+ "latitude": 37.386,
27
+ "continent_code": "NA",
28
+ "city_name": "Mountain View",
29
+ "country_code2": "US",
30
+ "country_name": "United States",
31
+ "dma_code": 807,
32
+ "country_code3": "US",
33
+ "region_name": "California",
34
+ "location": [-122.0838,
35
+ 37.386
36
+ ],
37
+ "postal_code": "94035",
38
+ "longitude": -122.0838,
39
+ "region_code": "CA"
40
+ },
41
+ "auth": "-",
42
+ "ident": "-",
43
+ "verb": "GET",
44
+ "useragent": {
45
+ "os": "Other",
46
+ "major": "2",
47
+ "minor": "1",
48
+ "name": "Googlebot",
49
+ "os_name": "Other",
50
+ "device": "Spider"
51
+ },
52
+ "message": "66.249.73.185 - - [26/Aug/2014:21:22:13 +0000] \"GET /doc/index.html?org/opensearch/action/search/SearchResponse.html HTTP/1.1\" 404 294 \"-\" \"Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)\"",
53
+ "referrer": "\"-\"",
54
+ "@timestamp": "2014-08-26T21:22:13.000Z",
55
+ "response": 404,
56
+ "bytes": 294,
57
+ "clientip": "66.249.73.185",
58
+ "@version": "1",
59
+ "host": "skywalker",
60
+ "httpversion": "1.1",
61
+ "timestamp": "26/Aug/2014:21:22:13 +0000"
62
+ }
63
+ }]
64
+ },
65
+ "aggregations": {
66
+ "bytes_avg": {
67
+ "value": 294
68
+ }
69
+ }
70
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "query": {
3
+ "query_string": {
4
+ "query": "response: 404"
5
+ }
6
+ }
7
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "query": {
3
+ "terms": {
4
+ "lock": [ "잠금", "uzávěr" ]
5
+ }
6
+ }
7
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "took": 16,
3
+ "timed_out": false,
4
+ "_shards": {
5
+ "total": 155,
6
+ "successful": 100,
7
+ "failed": 55,
8
+ "failures": [
9
+ {
10
+ "shard": 0,
11
+ "index": "logstash-2014.08.26",
12
+ "node": "YI1MT0H-Q469pFgAVTXI2g",
13
+ "reason": {
14
+ "type": "search_parse_exception",
15
+ "reason": "No mapping found for [@timestamp] in order to sort on"
16
+ }
17
+ }
18
+ ]
19
+ },
20
+ "hits": {
21
+ "total": 0,
22
+ "max_score": null,
23
+ "hits": []
24
+ }
25
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "took": 49,
3
+ "timed_out": false,
4
+ "_shards": {
5
+ "total": 155,
6
+ "successful": 155,
7
+ "failed": 0
8
+ },
9
+ "hits": {
10
+ "total": 13476,
11
+ "max_score": 1,
12
+ "hits": []
13
+ },
14
+ "aggregations": {
15
+ "bytes_avg": {
16
+ "value": 294
17
+ }
18
+ }
19
+ }
@@ -0,0 +1,67 @@
1
+ {
2
+ "took": 49,
3
+ "timed_out": false,
4
+ "_shards": {
5
+ "total": 155,
6
+ "successful": 155,
7
+ "failed": 0
8
+ },
9
+ "hits": {
10
+ "total": 13476,
11
+ "max_score": 1,
12
+ "hits": [{
13
+ "_index": "logstash-2014.08.26",
14
+ "_type": "logs",
15
+ "_id": "AVVY76L_AW7v0kX8KXo4",
16
+ "_score": 1,
17
+ "_source": {
18
+ "request": "/doc/index.html?org/opensearch/action/search/SearchResponse.html",
19
+ "agent": "\"Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)\"",
20
+ "geoip": {
21
+ "timezone": "America/Los_Angeles",
22
+ "ip": "66.249.73.185",
23
+ "latitude": 37.386,
24
+ "continent_code": "NA",
25
+ "city_name": "Mountain View",
26
+ "country_code2": "US",
27
+ "country_name": "United States",
28
+ "dma_code": 807,
29
+ "country_code3": "US",
30
+ "region_name": "California",
31
+ "location": [-122.0838,
32
+ 37.386
33
+ ],
34
+ "postal_code": "94035",
35
+ "longitude": -122.0838,
36
+ "region_code": "CA"
37
+ },
38
+ "auth": "-",
39
+ "ident": "-",
40
+ "verb": "GET",
41
+ "useragent": {
42
+ "os": "Other",
43
+ "major": "2",
44
+ "minor": "1",
45
+ "name": "Googlebot",
46
+ "os_name": "Other",
47
+ "device": "Spider"
48
+ },
49
+ "message": "66.249.73.185 - - [26/Aug/2014:21:22:13 +0000] \"GET /doc/index.html?org/opensearch/action/search/SearchResponse.html HTTP/1.1\" 404 294 \"-\" \"Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)\"",
50
+ "referrer": "\"-\"",
51
+ "@timestamp": "2014-08-26T21:22:13.000Z",
52
+ "response": 404,
53
+ "bytes": 294,
54
+ "clientip": "66.249.73.185",
55
+ "@version": "1",
56
+ "host": "skywalker",
57
+ "httpversion": "1.1",
58
+ "timestamp": "26/Aug/2014:21:22:13 +0000"
59
+ }
60
+ }]
61
+ },
62
+ "aggregations": {
63
+ "bytes_avg": {
64
+ "value": 294
65
+ }
66
+ }
67
+ }