Skip to content

Commit cc8dc52

Browse files
authored
Add ECS support (#129)
Restructure metadata and create ECS failure event
1 parent 72a07d6 commit cc8dc52

File tree

5 files changed

+421
-209
lines changed

5 files changed

+421
-209
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
## 5.1.0
2+
- Add ECS support [#129](https://github.com/logstash-plugins/logstash-input-http_poller/pull/129)
3+
14
## 5.0.2
25
- [DOC]Expanded url option to include Manticore keys [#119](https://github.com/logstash-plugins/logstash-input-http_poller/pull/119)
36

docs/index.asciidoc

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,36 @@ The above snippet will create two files `downloaded_cert.pem` and `downloaded_tr
8787
----------------------------------
8888

8989

90+
[id="plugins-{type}s-{plugin}-ecs_metadata"]
91+
==== Event Metadata and the Elastic Common Schema (ECS)
92+
93+
This input will add metadata about the HTTP connection itself to each event.
94+
95+
When ECS compatibility is disabled, metadata was added to a variety of non-standard top-level fields, which has the potential to create confusion and schema conflicts downstream.
96+
97+
With ECS Compatibility Mode, we can ensure a pipeline maintains access to this metadata throughout the event's lifecycle without polluting the top-level namespace.
98+
99+
Here’s how ECS compatibility mode affects output.
100+
[cols="<l,<l,e,<e"]
101+
|=======================================================================
102+
| ECS disabled | ECS v1 | Availability | Description
103+
104+
| [@metadata][host] | [@metadata][input][http_poller][request][host][hostname] | Always | Hostname
105+
| [@metadata][code] | [@metadata][input][http_poller][response][status_code] | When server responds a valid status code | HTTP response code
106+
| [@metadata][response_headers] | [@metadata][input][http_poller][response][headers] | When server responds with headers | HTTP headers of the response
107+
| [@metadata][response_message] | [@metadata][input][http_poller][response][status_message] | When server responds with status line | message of status line of HTTP headers
108+
| [@metadata][runtime_seconds] | [@metadata][input][http_poller][response][elapsed_time_ns] | When server responds a valid status code | elapsed time of calling endpoint. ECS v1 shows in nanoseconds.
109+
| [http_request_failure][runtime_seconds] | [event][duration] | When server throws exception | elapsed time of calling endpoint. ECS v1 shows in nanoseconds.
110+
| [@metadata][times_retried] | [@metadata][input][http_poller][request][retry_count] | When the poller calls server successfully | retry count from http client library
111+
| [@metadata][name] / [http_request_failure][name] | [@metadata][input][http_poller][request][name] | Always | The key of `urls` from poller config
112+
| [@metadata][request] / [http_request_failure][request]| [@metadata][input][http_poller][request][original] | Always | The whole object of `urls` from poller config
113+
| [http_request_failure][error] | [error][message] | When server throws exception | Error message
114+
| [http_request_failure][backtrace] | [error][stack_trace] | When server throws exception | Stack trace of error
115+
| -- | [url][full] | When server throws exception | The URL of the endpoint
116+
| -- | [http][request][method] | When server throws exception | HTTP request method
117+
| -- | [host][hostname] | When server throws exception | Hostname
118+
|=======================================================================
119+
90120
[id="plugins-{type}s-{plugin}-options"]
91121
==== Http_poller Input Configuration Options
92122

@@ -101,6 +131,7 @@ This plugin supports the following configuration options plus the <<plugins-{typ
101131
| <<plugins-{type}s-{plugin}-client_key>> |a valid filesystem path|No
102132
| <<plugins-{type}s-{plugin}-connect_timeout>> |<<number,number>>|No
103133
| <<plugins-{type}s-{plugin}-cookies>> |<<boolean,boolean>>|No
134+
| <<plugins-{type}s-{plugin}-ecs_compatibility>> | <<string,string>>|No
104135
| <<plugins-{type}s-{plugin}-follow_redirects>> |<<boolean,boolean>>|No
105136
| <<plugins-{type}s-{plugin}-keepalive>> |<<boolean,boolean>>|No
106137
| <<plugins-{type}s-{plugin}-keystore>> |a valid filesystem path|No
@@ -180,6 +211,81 @@ Timeout (in seconds) to wait for a connection to be established. Default is `10s
180211
Enable cookie support. With this enabled the client will persist cookies
181212
across requests as a normal web browser would. Enabled by default
182213

214+
[id="plugins-{type}s-{plugin}-ecs_compatibility"]
215+
===== `ecs_compatibility`
216+
217+
* Value type is <<string,string>>
218+
* Supported values are:
219+
** `disabled`: unstructured data added at root level
220+
** `v1`: uses `error`, `url` and `http` fields that are compatible with Elastic Common Schema
221+
222+
Controls this plugin's compatibility with the
223+
{ecs-ref}[Elastic Common Schema (ECS)].
224+
See <<plugins-{type}s-{plugin}-ecs_metadata>> for detailed information.
225+
226+
Example output:
227+
228+
**Sample output: ECS disabled**
229+
[source,text]
230+
-----
231+
{
232+
"http_poller_data" => {
233+
"@version" => "1",
234+
"@timestamp" => 2021-01-01T00:43:22.388Z,
235+
"status" => "UP"
236+
},
237+
"@version" => "1",
238+
"@timestamp" => 2021-01-01T00:43:22.389Z,
239+
}
240+
-----
241+
242+
**Sample output: ECS enabled**
243+
[source,text]
244+
-----
245+
{
246+
"http_poller_data" => {
247+
"status" => "UP",
248+
"@version" => "1",
249+
"event" => {
250+
"original" => "{\"status\":\"UP\"}"
251+
},
252+
"@timestamp" => 2021-01-01T00:40:59.558Z
253+
},
254+
"@version" => "1",
255+
"@timestamp" => 2021-01-01T00:40:59.559Z
256+
}
257+
-----
258+
259+
**Sample error output: ECS enabled**
260+
[source,text]
261+
----
262+
{
263+
"@timestamp" => 2021-07-09T09:53:48.721Z,
264+
"@version" => "1",
265+
"host" => {
266+
"hostname" => "MacBook-Pro"
267+
},
268+
"http" => {
269+
"request" => {
270+
"method" => "get"
271+
}
272+
},
273+
"event" => {
274+
"duration" => 259019
275+
},
276+
"error" => {
277+
"stack_trace" => nil,
278+
"message" => "Connection refused (Connection refused)"
279+
},
280+
"url" => {
281+
"full" => "http://localhost:8080/actuator/health"
282+
},
283+
"tags" => [
284+
[0] "_http_request_failure"
285+
]
286+
}
287+
----
288+
183289
[id="plugins-{type}s-{plugin}-follow_redirects"]
184290
===== `follow_redirects`
185291

@@ -315,6 +421,10 @@ Timeout (in seconds) to wait for data on the socket. Default is `10s`
315421

316422
Define the target field for placing the received data. If this setting is omitted, the data will be stored at the root (top level) of the event.
317423

424+
TIP: When ECS is enabled, set `target` in the codec (if the codec has a `target` option).
425+
Example: `codec => json { target => "TARGET_FIELD_NAME" }`
426+
427+
318428
[id="plugins-{type}s-{plugin}-truststore"]
319429
===== `truststore`
320430

lib/logstash/inputs/http_poller.rb

Lines changed: 80 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,18 @@
55
require "socket" # for Socket.gethostname
66
require "manticore"
77
require "rufus/scheduler"
8+
require "logstash/plugin_mixins/ecs_compatibility_support"
9+
require 'logstash/plugin_mixins/ecs_compatibility_support/target_check'
10+
require 'logstash/plugin_mixins/validator_support/field_reference_validation_adapter'
11+
require 'logstash/plugin_mixins/event_support/event_factory_adapter'
812

913
class LogStash::Inputs::HTTP_Poller < LogStash::Inputs::Base
1014
include LogStash::PluginMixins::HttpClient
15+
include LogStash::PluginMixins::ECSCompatibilitySupport(:disabled, :v1, :v8 => :v1)
16+
include LogStash::PluginMixins::ECSCompatibilitySupport::TargetCheck
17+
include LogStash::PluginMixins::EventSupport::EventFactoryAdapter
18+
19+
extend LogStash::PluginMixins::ValidatorSupport::FieldReferenceValidationAdapter
1120

1221
config_name "http_poller"
1322

@@ -28,7 +37,7 @@ class LogStash::Inputs::HTTP_Poller < LogStash::Inputs::Base
2837
config :schedule, :validate => :hash, :required => true
2938

3039
# Define the target field for placing the received data. If this setting is omitted, the data will be stored at the root (top level) of the event.
31-
config :target, :validate => :string
40+
config :target, :validate => :field_reference
3241

3342
# If you'd like to work with the request/response metadata.
3443
# Set this value to the name of the field you'd like to store a nested
@@ -42,6 +51,7 @@ def register
4251

4352
@logger.info("Registering http_poller Input", :type => @type, :schedule => @schedule, :timeout => @timeout)
4453

54+
setup_ecs_field!
4555
setup_requests!
4656
end
4757

@@ -55,6 +65,35 @@ def setup_requests!
5565
@requests = Hash[@urls.map {|name, url| [name, normalize_request(url)] }]
5666
end
5767

68+
private
69+
# In the context of ECS, there are two type of events in this plugin, valid HTTP response and failure
70+
# For a valid HTTP response, `url`, `request_method` and `host` are metadata of request.
71+
# The call could retrieve event which contain `[url]`, `[http][request][method]`, `[host][hostname]` data
72+
# Therefore, metadata should not write to those fields
73+
# For a failure, `url`, `request_method` and `host` are primary data of the event because the plugin owns this event,
74+
# so it writes to url.*, http.*, host.*
75+
def setup_ecs_field!
76+
@request_host_field = ecs_select[disabled: "[#{metadata_target}][host]", v1: "[#{metadata_target}][input][http_poller][request][host][hostname]"]
77+
@response_code_field = ecs_select[disabled: "[#{metadata_target}][code]", v1: "[#{metadata_target}][input][http_poller][response][status_code]"]
78+
@response_headers_field = ecs_select[disabled: "[#{metadata_target}][response_headers]", v1: "[#{metadata_target}][input][http_poller][response][headers]"]
79+
@response_message_field = ecs_select[disabled: "[#{metadata_target}][response_message]", v1: "[#{metadata_target}][input][http_poller][response][status_message]"]
80+
@response_time_s_field = ecs_select[disabled: "[#{metadata_target}][runtime_seconds]", v1: nil]
81+
@response_time_ns_field = ecs_select[disabled: nil, v1: "[#{metadata_target}][input][http_poller][response][elapsed_time_ns]"]
82+
@request_retry_count_field = ecs_select[disabled: "[#{metadata_target}][times_retried]", v1: "[#{metadata_target}][input][http_poller][request][retry_count]"]
83+
@request_name_field = ecs_select[disabled: "[#{metadata_target}][name]", v1: "[#{metadata_target}][input][http_poller][request][name]"]
84+
@original_request_field = ecs_select[disabled: "[#{metadata_target}][request]", v1: "[#{metadata_target}][input][http_poller][request][original]"]
85+
86+
@error_msg_field = ecs_select[disabled: "[http_request_failure][error]", v1: "[error][message]"]
87+
@stack_trace_field = ecs_select[disabled: "[http_request_failure][backtrace]", v1: "[error][stack_trace]"]
88+
@fail_original_request_field = ecs_select[disabled: "[http_request_failure][request]", v1: nil]
89+
@fail_request_name_field = ecs_select[disabled: "[http_request_failure][name]", v1: nil]
90+
@fail_response_time_s_field = ecs_select[disabled: "[http_request_failure][runtime_seconds]", v1: nil]
91+
@fail_response_time_ns_field = ecs_select[disabled: nil, v1: "[event][duration]"]
92+
@fail_request_url_field = ecs_select[disabled: nil, v1: "[url][full]"]
93+
@fail_request_method_field = ecs_select[disabled: nil, v1: "[http][request][method]"]
94+
@fail_request_host_field = ecs_select[disabled: nil, v1: "[host][hostname]"]
95+
end
96+
5897
private
5998
def normalize_request(url_or_spec)
6099
if url_or_spec.is_a?(String)
@@ -151,10 +190,14 @@ def request_async(queue, name, request)
151190

152191
method, *request_opts = request
153192
client.async.send(method, *request_opts).
154-
on_success {|response| handle_success(queue, name, request, response, Time.now - started)}.
155-
on_failure {|exception|
156-
handle_failure(queue, name, request, exception, Time.now - started)
157-
}
193+
on_success {|response| handle_success(queue, name, request, response, Time.now - started) }.
194+
on_failure {|exception| handle_failure(queue, name, request, exception, Time.now - started) }
195+
end
196+
197+
private
198+
# time diff in float to nanoseconds
199+
def to_nanoseconds(time_diff)
200+
(time_diff * 1000000).to_i
158201
end
159202

160203
private
@@ -164,11 +207,11 @@ def handle_success(queue, name, request, response, execution_time)
164207
# responses come up as "" which will cause the codec to not yield anything
165208
if body && body.size > 0
166209
decode_and_flush(@codec, body) do |decoded|
167-
event = @target ? LogStash::Event.new(@target => decoded.to_hash) : decoded
210+
event = @target ? targeted_event_factory.new_event(decoded.to_hash) : decoded
168211
handle_decoded_event(queue, name, request, response, event, execution_time)
169212
end
170213
else
171-
event = ::LogStash::Event.new
214+
event = event_factory.new_event
172215
handle_decoded_event(queue, name, request, response, event, execution_time)
173216
end
174217
end
@@ -197,20 +240,10 @@ def handle_decoded_event(queue, name, request, response, event, execution_time)
197240
private
198241
# Beware, on old versions of manticore some uncommon failures are not handled
199242
def handle_failure(queue, name, request, exception, execution_time)
200-
event = LogStash::Event.new
201-
apply_metadata(event, name, request)
202-
243+
event = event_factory.new_event
203244
event.tag("_http_request_failure")
204-
205-
# This is also in the metadata, but we send it anyone because we want this
206-
# persisted by default, whereas metadata isn't. People don't like mysterious errors
207-
event.set("http_request_failure", {
208-
"request" => structure_request(request),
209-
"name" => name,
210-
"error" => exception.to_s,
211-
"backtrace" => exception.backtrace,
212-
"runtime_seconds" => execution_time
213-
})
245+
apply_metadata(event, name, request, nil, execution_time)
246+
apply_failure_fields(event, name, request, exception, execution_time)
214247

215248
queue << event
216249
rescue StandardError, java.lang.Exception => e
@@ -231,29 +264,39 @@ def handle_failure(queue, name, request, exception, execution_time)
231264
end
232265

233266
private
234-
def apply_metadata(event, name, request, response=nil, execution_time=nil)
267+
def apply_metadata(event, name, request, response, execution_time)
235268
return unless @metadata_target
236-
event.set(@metadata_target, event_metadata(name, request, response, execution_time))
237-
end
238-
239-
private
240-
def event_metadata(name, request, response=nil, execution_time=nil)
241-
m = {
242-
"name" => name,
243-
"host" => @host,
244-
"request" => structure_request(request),
245-
}
246269

247-
m["runtime_seconds"] = execution_time
270+
event.set(@request_host_field, @host)
271+
event.set(@response_time_s_field, execution_time) if @response_time_s_field
272+
event.set(@response_time_ns_field, to_nanoseconds(execution_time)) if @response_time_ns_field
273+
event.set(@request_name_field, name)
274+
event.set(@original_request_field, structure_request(request))
248275

249276
if response
250-
m["code"] = response.code
251-
m["response_headers"] = response.headers
252-
m["response_message"] = response.message
253-
m["times_retried"] = response.times_retried
277+
event.set(@response_code_field, response.code)
278+
event.set(@response_headers_field, response.headers)
279+
event.set(@response_message_field, response.message)
280+
event.set(@request_retry_count_field, response.times_retried)
254281
end
282+
end
255283

256-
m
284+
private
285+
def apply_failure_fields(event, name, request, exception, execution_time)
286+
# This is also in the metadata, but we send it anyone because we want this
287+
# persisted by default, whereas metadata isn't. People don't like mysterious errors
288+
event.set(@fail_original_request_field, structure_request(request)) if @fail_original_request_field
289+
event.set(@fail_request_name_field, name) if @fail_request_name_field
290+
291+
method, url, _ = request
292+
event.set(@fail_request_url_field, url) if @fail_request_url_field
293+
event.set(@fail_request_method_field, method.to_s) if @fail_request_method_field
294+
event.set(@fail_request_host_field, @host) if @fail_request_host_field
295+
296+
event.set(@fail_response_time_s_field, execution_time) if @fail_response_time_s_field
297+
event.set(@fail_response_time_ns_field, to_nanoseconds(execution_time)) if @fail_response_time_ns_field
298+
event.set(@error_msg_field, exception.to_s)
299+
event.set(@stack_trace_field, exception.backtrace)
257300
end
258301

259302
private

logstash-input-http_poller.gemspec

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Gem::Specification.new do |s|
22
s.name = 'logstash-input-http_poller'
3-
s.version = '5.0.2'
3+
s.version = '5.1.0'
44
s.licenses = ['Apache License (2.0)']
55
s.summary = "Decodes the output of an HTTP API into events"
66
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"
@@ -23,6 +23,9 @@ Gem::Specification.new do |s|
2323
s.add_runtime_dependency 'logstash-mixin-http_client', "~> 7"
2424
s.add_runtime_dependency 'stud', "~> 0.0.22"
2525
s.add_runtime_dependency 'rufus-scheduler', "~>3.0.9"
26+
s.add_runtime_dependency 'logstash-mixin-ecs_compatibility_support', '~>1.3'
27+
s.add_runtime_dependency 'logstash-mixin-event_support', '~> 1.0', '>= 1.0.1'
28+
s.add_runtime_dependency 'logstash-mixin-validator_support', '~> 1.0'
2629

2730
s.add_development_dependency 'logstash-codec-json'
2831
s.add_development_dependency 'logstash-codec-line'

0 commit comments

Comments
 (0)