logstash-input-elasticsearch_compat 0.1.0-java
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 +7 -0
- data/CHANGELOG.md +23 -0
- data/LICENSE +202 -0
- data/NOTICE.TXT +331 -0
- data/README.md +136 -0
- data/RELEASING.md +52 -0
- data/VERSION +1 -0
- data/docs/archive/README.md +3 -0
- data/docs/archive/design.md +306 -0
- data/docs/compatibility.md +12 -0
- data/docs/index.asciidoc +295 -0
- data/lib/logstash/inputs/elasticsearch_compat/batch_handler.rb +66 -0
- data/lib/logstash/inputs/elasticsearch_compat/metrics_publisher.rb +50 -0
- data/lib/logstash/inputs/elasticsearch_compat/ssl_validation.rb +64 -0
- data/lib/logstash/inputs/elasticsearch_compat.rb +149 -0
- data/lib/logstash-input-elasticsearch_compat_jars.rb +5 -0
- data/logstash-input-elasticsearch_compat.gemspec +24 -0
- data/vendor/jar-dependencies/org/logstash/plugins/inputs/logstash-input-elasticsearch_compat/0.1.0/logstash-input-elasticsearch_compat-0.1.0.jar +0 -0
- metadata +108 -0
data/docs/index.asciidoc
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
:plugin: elasticsearch_compat
|
|
2
|
+
:type: input
|
|
3
|
+
:default_codec: plain
|
|
4
|
+
|
|
5
|
+
///////////////////////////////////////////
|
|
6
|
+
START - GENERATED VARIABLES, DO NOT EDIT!
|
|
7
|
+
///////////////////////////////////////////
|
|
8
|
+
:version: %VERSION%
|
|
9
|
+
:release_date: %RELEASE_DATE%
|
|
10
|
+
:changelog_url: %CHANGELOG_URL%
|
|
11
|
+
:include_path: ../../../../logstash/docs/include
|
|
12
|
+
///////////////////////////////////////////
|
|
13
|
+
END - GENERATED VARIABLES, DO NOT EDIT!
|
|
14
|
+
///////////////////////////////////////////
|
|
15
|
+
|
|
16
|
+
[id="plugins-{type}s-{plugin}"]
|
|
17
|
+
|
|
18
|
+
=== Elasticsearch compat input plugin
|
|
19
|
+
|
|
20
|
+
include::{include_path}/plugin_header.asciidoc[]
|
|
21
|
+
|
|
22
|
+
==== Description
|
|
23
|
+
|
|
24
|
+
Receives Elasticsearch Bulk API requests and emits one Logstash event per `index` or `create`
|
|
25
|
+
action. Point a client that normally writes to Elasticsearch, such as the OpenTelemetry
|
|
26
|
+
Collector `elasticsearch` exporter (tested with its default settings), at Logstash instead.
|
|
27
|
+
|
|
28
|
+
This plugin is a protocol-compatible receiver, not an Elasticsearch node.
|
|
29
|
+
|
|
30
|
+
===== Routes
|
|
31
|
+
|
|
32
|
+
* `POST` or `PUT` `/_bulk` and `/{index}/_bulk`. On `/{index}/_bulk`, `{index}` is used only for
|
|
33
|
+
actions without `_index`.
|
|
34
|
+
* `GET /` and `HEAD /` return a minimal identity response for client version detection. It
|
|
35
|
+
reports version `8.19.0` as a fixed compatibility placeholder (it keeps the OTel exporter on its
|
|
36
|
+
most widely compatible metrics format).
|
|
37
|
+
* Anything else: `404`; wrong methods: `405`.
|
|
38
|
+
|
|
39
|
+
===== Events
|
|
40
|
+
|
|
41
|
+
Event fields are the source document. Routing metadata (`action`, `index`, and `id`, `routing`,
|
|
42
|
+
`pipeline` when supplied) is stored at `metadata_target`
|
|
43
|
+
(default `[@metadata][elasticsearch_compat]`). `@metadata` is not written by outputs; copy what
|
|
44
|
+
downstream consumers need.
|
|
45
|
+
|
|
46
|
+
A numeric or digit-string `@timestamp` is read as epoch milliseconds, as Elasticsearch reads it
|
|
47
|
+
(the OTel exporter sends this form), with nanosecond precision.
|
|
48
|
+
|
|
49
|
+
===== Acknowledgments
|
|
50
|
+
|
|
51
|
+
* An item's `201` means the event was accepted by the Logstash queue, not that an output
|
|
52
|
+
delivered it.
|
|
53
|
+
* `update` and `delete` are not supported: each gets an item-level `400`.
|
|
54
|
+
* A malformed body gets `400` and nothing from it is queued.
|
|
55
|
+
* If queuing fails mid-request: items already queued are `201`, the failing item `500`, the rest
|
|
56
|
+
`503`. Clients retry these, so duplicates are possible.
|
|
57
|
+
|
|
58
|
+
===== Compression
|
|
59
|
+
|
|
60
|
+
Compressed request bodies (`gzip`, `deflate`, `x-gzip`, `x-deflate`) are always accepted;
|
|
61
|
+
`max_content_length` applies after decompression. Other encodings get `415`. Responses are
|
|
62
|
+
compressed (level 3, no size threshold) when the client accepts it, unless
|
|
63
|
+
`compression_enabled => false`.
|
|
64
|
+
|
|
65
|
+
===== Credentials
|
|
66
|
+
|
|
67
|
+
`Authorization` headers are accepted and ignored; no request ever gets `401` or `403`. Restrict
|
|
68
|
+
access with TLS client certificates (`ssl_client_authentication`) or at the network layer.
|
|
69
|
+
|
|
70
|
+
===== Monitoring
|
|
71
|
+
|
|
72
|
+
This input adds counters to its entry in the Logstash node stats API
|
|
73
|
+
(`GET /_node/stats/pipelines/<id>`): `bulk_requests`, `bulk_requests_rejected_overload` (`429`),
|
|
74
|
+
`bulk_requests_rejected_too_large` (`413`), `bulk_requests_rejected_unsupported_encoding`
|
|
75
|
+
(`415`), `bulk_requests_rejected_bad_request` (`400`), `bulk_items_failed`, and
|
|
76
|
+
`queue_submit_failures`. They are updated per request and visible within about a second. Disable
|
|
77
|
+
with `enable_metric => false`.
|
|
78
|
+
|
|
79
|
+
[id="plugins-{type}s-{plugin}-options"]
|
|
80
|
+
==== Elasticsearch compat input configuration options
|
|
81
|
+
|
|
82
|
+
[cols="<,<,<",options="header",]
|
|
83
|
+
|=======================================================================
|
|
84
|
+
|Setting |Input type|Required
|
|
85
|
+
| <<plugins-{type}s-{plugin}-compression_enabled>> |<<boolean,boolean>>|No
|
|
86
|
+
| <<plugins-{type}s-{plugin}-host>> |<<string,string>>|No
|
|
87
|
+
| <<plugins-{type}s-{plugin}-max_content_length>> |<<number,number>>|No
|
|
88
|
+
| <<plugins-{type}s-{plugin}-max_pending_requests>> |<<number,number>>|No
|
|
89
|
+
| <<plugins-{type}s-{plugin}-metadata_target>> |<<string,string>>|No
|
|
90
|
+
| <<plugins-{type}s-{plugin}-port>> |<<number,number>>|No
|
|
91
|
+
| <<plugins-{type}s-{plugin}-ssl_certificate>> |a valid filesystem path|No
|
|
92
|
+
| <<plugins-{type}s-{plugin}-ssl_certificate_authorities>> |<<array,array>>|No
|
|
93
|
+
| <<plugins-{type}s-{plugin}-ssl_cipher_suites>> |<<array,array>>|No
|
|
94
|
+
| <<plugins-{type}s-{plugin}-ssl_client_authentication>> |<<string,string>>, one of `["none", "optional", "required"]`|No
|
|
95
|
+
| <<plugins-{type}s-{plugin}-ssl_enabled>> |<<boolean,boolean>>|No
|
|
96
|
+
| <<plugins-{type}s-{plugin}-ssl_handshake_timeout>> |<<number,number>>|No
|
|
97
|
+
| <<plugins-{type}s-{plugin}-ssl_key>> |a valid filesystem path|No
|
|
98
|
+
| <<plugins-{type}s-{plugin}-ssl_key_passphrase>> |<<password,password>>|No
|
|
99
|
+
| <<plugins-{type}s-{plugin}-ssl_keystore_password>> |<<password,password>>|No
|
|
100
|
+
| <<plugins-{type}s-{plugin}-ssl_keystore_path>> |a valid filesystem path|No
|
|
101
|
+
| <<plugins-{type}s-{plugin}-ssl_keystore_type>> |<<string,string>>, one of `["pkcs12", "jks"]`|No
|
|
102
|
+
| <<plugins-{type}s-{plugin}-ssl_supported_protocols>> |<<string,string>>|No
|
|
103
|
+
| <<plugins-{type}s-{plugin}-ssl_truststore_password>> |<<password,password>>|No
|
|
104
|
+
| <<plugins-{type}s-{plugin}-ssl_truststore_path>> |a valid filesystem path|No
|
|
105
|
+
| <<plugins-{type}s-{plugin}-threads>> |<<number,number>>|No
|
|
106
|
+
|=======================================================================
|
|
107
|
+
|
|
108
|
+
Also see <<plugins-{type}s-{plugin}-common-options>> for a list of options supported by all
|
|
109
|
+
input plugins.
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
[id="plugins-{type}s-{plugin}-compression_enabled"]
|
|
114
|
+
===== `compression_enabled`
|
|
115
|
+
|
|
116
|
+
* Value type is <<boolean,boolean>>
|
|
117
|
+
* Default value is `true`
|
|
118
|
+
|
|
119
|
+
Compress responses when the client accepts gzip or deflate. Compressed requests are always
|
|
120
|
+
accepted regardless of this setting.
|
|
121
|
+
|
|
122
|
+
[id="plugins-{type}s-{plugin}-host"]
|
|
123
|
+
===== `host`
|
|
124
|
+
|
|
125
|
+
* Value type is <<string,string>>
|
|
126
|
+
* Default value is `"0.0.0.0"`
|
|
127
|
+
|
|
128
|
+
The address to listen on.
|
|
129
|
+
|
|
130
|
+
[id="plugins-{type}s-{plugin}-max_content_length"]
|
|
131
|
+
===== `max_content_length`
|
|
132
|
+
|
|
133
|
+
* Value type is <<number,number>>
|
|
134
|
+
* Default value is `104857600` (100 MB)
|
|
135
|
+
|
|
136
|
+
Maximum request body size in bytes, after decompression. Larger requests get `413`.
|
|
137
|
+
|
|
138
|
+
[id="plugins-{type}s-{plugin}-max_pending_requests"]
|
|
139
|
+
===== `max_pending_requests`
|
|
140
|
+
|
|
141
|
+
* Value type is <<number,number>>
|
|
142
|
+
* Default value is `200`
|
|
143
|
+
|
|
144
|
+
Requests waiting for a worker thread. Beyond this, new requests get `429` (retryable) before
|
|
145
|
+
anything is queued.
|
|
146
|
+
|
|
147
|
+
[id="plugins-{type}s-{plugin}-metadata_target"]
|
|
148
|
+
===== `metadata_target`
|
|
149
|
+
|
|
150
|
+
* Value type is <<string,string>>
|
|
151
|
+
* Default value is `"[@metadata][elasticsearch_compat]"`
|
|
152
|
+
|
|
153
|
+
Field where routing metadata is stored.
|
|
154
|
+
|
|
155
|
+
[id="plugins-{type}s-{plugin}-port"]
|
|
156
|
+
===== `port`
|
|
157
|
+
|
|
158
|
+
* Value type is <<number,number>>
|
|
159
|
+
* Default value is `9200`
|
|
160
|
+
|
|
161
|
+
The port to listen on.
|
|
162
|
+
|
|
163
|
+
[id="plugins-{type}s-{plugin}-ssl_certificate"]
|
|
164
|
+
===== `ssl_certificate`
|
|
165
|
+
|
|
166
|
+
* Value type is <<path,path>>
|
|
167
|
+
* There is no default value for this setting.
|
|
168
|
+
|
|
169
|
+
PEM certificate (chain). Requires `ssl_key`. Use either this or `ssl_keystore_path`.
|
|
170
|
+
|
|
171
|
+
[id="plugins-{type}s-{plugin}-ssl_certificate_authorities"]
|
|
172
|
+
===== `ssl_certificate_authorities`
|
|
173
|
+
|
|
174
|
+
* Value type is <<array,array>>
|
|
175
|
+
* Default value is `[]`
|
|
176
|
+
|
|
177
|
+
PEM certificate authorities that client certificates are checked against. Requires
|
|
178
|
+
`ssl_client_authentication` `optional` or `required`. Use either this or `ssl_truststore_path`.
|
|
179
|
+
|
|
180
|
+
[id="plugins-{type}s-{plugin}-ssl_cipher_suites"]
|
|
181
|
+
===== `ssl_cipher_suites`
|
|
182
|
+
|
|
183
|
+
* Value type is a list of <<string,string>>
|
|
184
|
+
* Default value is a modern list (TLS 1.3 suites and ECDHE AES-GCM/ChaCha20, then ECDHE AES-CBC),
|
|
185
|
+
limited to those the JVM supports — the same as `logstash-input-http`.
|
|
186
|
+
|
|
187
|
+
Cipher suites to enable, in order of preference.
|
|
188
|
+
|
|
189
|
+
[id="plugins-{type}s-{plugin}-ssl_client_authentication"]
|
|
190
|
+
===== `ssl_client_authentication`
|
|
191
|
+
|
|
192
|
+
* Value can be any of: `none`, `optional`, `required`
|
|
193
|
+
* Default value is `"none"`
|
|
194
|
+
|
|
195
|
+
Whether to request (`optional`) or require (`required`) a client certificate. Needs
|
|
196
|
+
`ssl_certificate_authorities`, `ssl_truststore_path`, or a keystore that also holds trusted
|
|
197
|
+
certificates.
|
|
198
|
+
|
|
199
|
+
[id="plugins-{type}s-{plugin}-ssl_enabled"]
|
|
200
|
+
===== `ssl_enabled`
|
|
201
|
+
|
|
202
|
+
* Value type is <<boolean,boolean>>
|
|
203
|
+
* Default value is `false`
|
|
204
|
+
|
|
205
|
+
Serve HTTPS. Requires `ssl_certificate` and `ssl_key`, or `ssl_keystore_path`. Other `ssl_*`
|
|
206
|
+
settings with `ssl_enabled => false` are ignored with a warning.
|
|
207
|
+
|
|
208
|
+
[id="plugins-{type}s-{plugin}-ssl_handshake_timeout"]
|
|
209
|
+
===== `ssl_handshake_timeout`
|
|
210
|
+
|
|
211
|
+
* Value type is <<number,number>>
|
|
212
|
+
* Default value is `10000`
|
|
213
|
+
|
|
214
|
+
Time in milliseconds for an incomplete TLS handshake before the connection is closed.
|
|
215
|
+
|
|
216
|
+
[id="plugins-{type}s-{plugin}-ssl_key"]
|
|
217
|
+
===== `ssl_key`
|
|
218
|
+
|
|
219
|
+
* Value type is <<path,path>>
|
|
220
|
+
* There is no default value for this setting.
|
|
221
|
+
|
|
222
|
+
PEM private key in PKCS#8 format (convert PKCS#1 with
|
|
223
|
+
`openssl pkcs8 -topk8 -nocrypt -in key.pem -out key.pkcs8.pem`). Requires `ssl_certificate`.
|
|
224
|
+
|
|
225
|
+
[id="plugins-{type}s-{plugin}-ssl_key_passphrase"]
|
|
226
|
+
===== `ssl_key_passphrase`
|
|
227
|
+
|
|
228
|
+
* Value type is <<password,password>>
|
|
229
|
+
* There is no default value for this setting.
|
|
230
|
+
|
|
231
|
+
Passphrase of an encrypted `ssl_key`.
|
|
232
|
+
|
|
233
|
+
[id="plugins-{type}s-{plugin}-ssl_keystore_password"]
|
|
234
|
+
===== `ssl_keystore_password`
|
|
235
|
+
|
|
236
|
+
* Value type is <<password,password>>
|
|
237
|
+
* There is no default value for this setting.
|
|
238
|
+
|
|
239
|
+
Keystore password.
|
|
240
|
+
|
|
241
|
+
[id="plugins-{type}s-{plugin}-ssl_keystore_path"]
|
|
242
|
+
===== `ssl_keystore_path`
|
|
243
|
+
|
|
244
|
+
* Value type is <<path,path>>
|
|
245
|
+
* There is no default value for this setting.
|
|
246
|
+
|
|
247
|
+
PKCS12 or JKS keystore with the server key and certificate. Use either this or `ssl_certificate`.
|
|
248
|
+
|
|
249
|
+
[id="plugins-{type}s-{plugin}-ssl_keystore_type"]
|
|
250
|
+
===== `ssl_keystore_type`
|
|
251
|
+
|
|
252
|
+
* Value can be any of: `pkcs12`, `jks`
|
|
253
|
+
* There is no default value; when unset, `.p12`/`.pfx`/`.pkcs12` files are PKCS12 and others JKS.
|
|
254
|
+
|
|
255
|
+
Keystore format.
|
|
256
|
+
|
|
257
|
+
[id="plugins-{type}s-{plugin}-ssl_supported_protocols"]
|
|
258
|
+
===== `ssl_supported_protocols`
|
|
259
|
+
|
|
260
|
+
* Value type is <<string,string>>
|
|
261
|
+
* Allowed values are: `'TLSv1.1'`, `'TLSv1.2'`, `'TLSv1.3'`
|
|
262
|
+
* Default value is `['TLSv1.2', 'TLSv1.3']`
|
|
263
|
+
|
|
264
|
+
TLS versions to accept. The JVM disables `TLSv1.1` by default.
|
|
265
|
+
|
|
266
|
+
[id="plugins-{type}s-{plugin}-ssl_truststore_password"]
|
|
267
|
+
===== `ssl_truststore_password`
|
|
268
|
+
|
|
269
|
+
* Value type is <<password,password>>
|
|
270
|
+
* There is no default value for this setting.
|
|
271
|
+
|
|
272
|
+
Truststore password.
|
|
273
|
+
|
|
274
|
+
[id="plugins-{type}s-{plugin}-ssl_truststore_path"]
|
|
275
|
+
===== `ssl_truststore_path`
|
|
276
|
+
|
|
277
|
+
* Value type is <<path,path>>
|
|
278
|
+
* There is no default value for this setting.
|
|
279
|
+
|
|
280
|
+
PKCS12 or JKS truststore with the certificates client certificates are checked against. Requires
|
|
281
|
+
`ssl_client_authentication` `optional` or `required`. Use either this or
|
|
282
|
+
`ssl_certificate_authorities`.
|
|
283
|
+
|
|
284
|
+
[id="plugins-{type}s-{plugin}-threads"]
|
|
285
|
+
===== `threads`
|
|
286
|
+
|
|
287
|
+
* Value type is <<number,number>>
|
|
288
|
+
* Default value is the number of CPU cores
|
|
289
|
+
|
|
290
|
+
Worker threads that parse requests and queue events.
|
|
291
|
+
|
|
292
|
+
[id="plugins-{type}s-{plugin}-common-options"]
|
|
293
|
+
include::{include_path}/{type}.asciidoc[]
|
|
294
|
+
|
|
295
|
+
:default_codec!:
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# encoding: utf-8
|
|
2
|
+
require "java"
|
|
3
|
+
|
|
4
|
+
module LogStash
|
|
5
|
+
module ElasticsearchCompat
|
|
6
|
+
# Implements the Java BatchHandler: called once per request from a worker thread.
|
|
7
|
+
# Pushes one event per operation, in order, and records each known outcome.
|
|
8
|
+
# Entries left nil are reported by Java as NOT_ATTEMPTED (503, safe to retry).
|
|
9
|
+
class BatchHandler
|
|
10
|
+
include Java::OrgLogstashPluginsInputsEscompat::BatchHandler
|
|
11
|
+
|
|
12
|
+
ItemOutcome = Java::OrgLogstashPluginsInputsEscompatBulk::ItemOutcome
|
|
13
|
+
QUEUED = ItemOutcome::QUEUED
|
|
14
|
+
SUBMIT_FAILED = ItemOutcome::SUBMIT_FAILED
|
|
15
|
+
|
|
16
|
+
# @param sink [#call] called with (document, metadata); builds and queues one event.
|
|
17
|
+
# Raising means the outcome is unknown.
|
|
18
|
+
# @param logger [#error]
|
|
19
|
+
# @param after_submit [#call, nil] called once after each request's submission (metrics)
|
|
20
|
+
def initialize(sink, logger, after_submit = nil)
|
|
21
|
+
@sink = sink
|
|
22
|
+
@logger = logger
|
|
23
|
+
@after_submit = after_submit
|
|
24
|
+
@stopping = java.util.concurrent.atomic.AtomicBoolean.new(false)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# After this, operations not yet started are left NOT_ATTEMPTED.
|
|
28
|
+
def stop!
|
|
29
|
+
@stopping.set(true)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def submit(ops, results)
|
|
33
|
+
submit_all(ops, results)
|
|
34
|
+
ensure
|
|
35
|
+
begin
|
|
36
|
+
@after_submit.call if @after_submit
|
|
37
|
+
rescue StandardError, java.lang.Exception
|
|
38
|
+
# metrics must never affect ingestion
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def submit_all(ops, results)
|
|
45
|
+
n = ops.size
|
|
46
|
+
i = 0
|
|
47
|
+
while i < n
|
|
48
|
+
return if @stopping.get
|
|
49
|
+
op = ops.get(i)
|
|
50
|
+
begin
|
|
51
|
+
@sink.call(op.document, op.metadata)
|
|
52
|
+
rescue StandardError, java.lang.Exception => e
|
|
53
|
+
results[i] = SUBMIT_FAILED
|
|
54
|
+
# Never log the document (constitution VI).
|
|
55
|
+
@logger.error("elasticsearch_compat: failed to queue event; remaining items in the " \
|
|
56
|
+
"request were not attempted", :exception => e.class.name, :message => e.message)
|
|
57
|
+
return
|
|
58
|
+
end
|
|
59
|
+
results[i] = QUEUED
|
|
60
|
+
i += 1
|
|
61
|
+
end
|
|
62
|
+
nil
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# encoding: utf-8
|
|
2
|
+
|
|
3
|
+
module LogStash
|
|
4
|
+
module ElasticsearchCompat
|
|
5
|
+
# Publishes the Java RequestCounters into this input's Logstash metrics as gauges of
|
|
6
|
+
# cumulative values (research R6): after each request that reaches Ruby, and every second so
|
|
7
|
+
# refused requests (which never reach Ruby) still show up. Gauges are idempotent, so
|
|
8
|
+
# concurrent publishes need no locking. With enable_metric => false, `metric` is Logstash's
|
|
9
|
+
# null metric and nothing is reported.
|
|
10
|
+
class MetricsPublisher
|
|
11
|
+
INTERVAL_SECONDS = 1
|
|
12
|
+
|
|
13
|
+
# @param metric [#gauge] the plugin's namespaced metric
|
|
14
|
+
# @param counters [#snapshot] Java RequestCounters
|
|
15
|
+
def initialize(metric, counters)
|
|
16
|
+
@metric = metric
|
|
17
|
+
@counters = counters
|
|
18
|
+
@thread = nil
|
|
19
|
+
@running = java.util.concurrent.atomic.AtomicBoolean.new(false)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def publish
|
|
23
|
+
@counters.snapshot.each { |key, value| @metric.gauge(key.to_sym, value) }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def start
|
|
27
|
+
return unless @running.compare_and_set(false, true)
|
|
28
|
+
@thread = Thread.new do
|
|
29
|
+
Thread.current.name = "escompat-metrics" if Thread.current.respond_to?(:name=)
|
|
30
|
+
while @running.get
|
|
31
|
+
begin
|
|
32
|
+
publish
|
|
33
|
+
rescue StandardError, java.lang.Exception
|
|
34
|
+
# metrics must never affect ingestion
|
|
35
|
+
end
|
|
36
|
+
sleep INTERVAL_SECONDS
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def stop
|
|
42
|
+
@running.set(false)
|
|
43
|
+
thread = @thread
|
|
44
|
+
thread.wakeup rescue nil if thread
|
|
45
|
+
thread.join(5) if thread
|
|
46
|
+
publish rescue nil
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# encoding: utf-8
|
|
2
|
+
|
|
3
|
+
module LogStash
|
|
4
|
+
module ElasticsearchCompat
|
|
5
|
+
# TLS setting rules identical to logstash-input-http's validate_ssl_settings!, including its
|
|
6
|
+
# messages (contracts/plugin-config.md rules 1-6). Pure: no Logstash dependency, so it can be
|
|
7
|
+
# specified with plain JRuby.
|
|
8
|
+
module SslValidation
|
|
9
|
+
OPTIONAL = "optional".freeze
|
|
10
|
+
REQUIRED = "required".freeze
|
|
11
|
+
|
|
12
|
+
# @param s [Hash] setting name (String) => value, as configured
|
|
13
|
+
# @param explicit [Array<String>] names the user actually set (for the warning)
|
|
14
|
+
# @return [Hash] { errors: [String] (first rule only, as logstash-input-http raises), warnings: [String] }
|
|
15
|
+
def self.check(s, explicit)
|
|
16
|
+
unless s["ssl_enabled"]
|
|
17
|
+
ignored = explicit.select { |k| k != "ssl_enabled" && k.start_with?("ssl_") }
|
|
18
|
+
warnings = ignored.empty? ? [] :
|
|
19
|
+
["Configured SSL settings are not used when `ssl_enabled` is set to `false`: #{ignored}"]
|
|
20
|
+
return { errors: [], warnings: warnings }
|
|
21
|
+
end
|
|
22
|
+
{ errors: Array(first_error(s)), warnings: [] }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.first_error(s)
|
|
26
|
+
cert = present?(s["ssl_certificate"])
|
|
27
|
+
key = present?(s["ssl_key"])
|
|
28
|
+
keystore = present?(s["ssl_keystore_path"])
|
|
29
|
+
truststore = present?(s["ssl_truststore_path"])
|
|
30
|
+
cas = Array(s["ssl_certificate_authorities"]).any?
|
|
31
|
+
client_auth = s["ssl_client_authentication"].to_s.downcase
|
|
32
|
+
|
|
33
|
+
return "Using an `ssl_certificate` requires an `ssl_key`" if cert && !key
|
|
34
|
+
return "An `ssl_certificate` is required when using an `ssl_key`" if key && !cert
|
|
35
|
+
unless cert || keystore
|
|
36
|
+
return "Either an `ssl_certificate` or `ssl_keystore_path` is required when SSL is enabled `ssl_enabled => true`"
|
|
37
|
+
end
|
|
38
|
+
return "Use either an `ssl_certificate` or an `ssl_keystore_path`" if cert && keystore
|
|
39
|
+
return "Use either an `ssl_certificate_authorities` or an `ssl_truststore_path`" if cas && truststore
|
|
40
|
+
|
|
41
|
+
if client_auth == OPTIONAL || client_auth == REQUIRED
|
|
42
|
+
unless cas || truststore || keystore
|
|
43
|
+
return "Using `ssl_client_authentication` set to `#{OPTIONAL}` or `#{REQUIRED}`, requires the " \
|
|
44
|
+
"configuration of `ssl_certificate_authorities` or `ssl_truststore_path`"
|
|
45
|
+
end
|
|
46
|
+
else
|
|
47
|
+
if truststore
|
|
48
|
+
return "The configuration of `ssl_truststore_path` requires setting `ssl_client_authentication` " \
|
|
49
|
+
"to `#{OPTIONAL}` or '#{REQUIRED}'"
|
|
50
|
+
end
|
|
51
|
+
if cas
|
|
52
|
+
return "The configuration of `ssl_certificate_authorities` requires setting " \
|
|
53
|
+
"`ssl_client_authentication` to `#{OPTIONAL}` or '#{REQUIRED}'"
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
nil
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def self.present?(v)
|
|
60
|
+
!(v.nil? || v.to_s.empty?)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# encoding: utf-8
|
|
2
|
+
require "logstash/inputs/base"
|
|
3
|
+
require "logstash/namespace"
|
|
4
|
+
require "logstash-input-elasticsearch_compat_jars"
|
|
5
|
+
require "logstash/inputs/elasticsearch_compat/batch_handler"
|
|
6
|
+
require "logstash/inputs/elasticsearch_compat/ssl_validation"
|
|
7
|
+
require "logstash/inputs/elasticsearch_compat/metrics_publisher"
|
|
8
|
+
|
|
9
|
+
# Receives Elasticsearch Bulk API requests (POST /_bulk) and emits one event per
|
|
10
|
+
# index/create action. Items are acknowledged only after the event is accepted by
|
|
11
|
+
# the Logstash queue. update/delete get item-level errors. Credentials are accepted
|
|
12
|
+
# and ignored.
|
|
13
|
+
class LogStash::Inputs::ElasticsearchCompat < LogStash::Inputs::Base
|
|
14
|
+
config_name "elasticsearch_compat"
|
|
15
|
+
|
|
16
|
+
# Bulk bodies are parsed by the Java backend, not by a codec.
|
|
17
|
+
default :codec, "plain"
|
|
18
|
+
|
|
19
|
+
# Listen address.
|
|
20
|
+
config :host, :validate => :string, :default => "0.0.0.0"
|
|
21
|
+
|
|
22
|
+
# Listen port.
|
|
23
|
+
config :port, :validate => :number, :default => 9200
|
|
24
|
+
|
|
25
|
+
# Worker threads that parse requests and push events.
|
|
26
|
+
config :threads, :validate => :number, :default => ::LogStash::Config::CpuCoreStrategy.maximum
|
|
27
|
+
|
|
28
|
+
# Requests waiting for a worker; beyond this, new requests get 429.
|
|
29
|
+
config :max_pending_requests, :validate => :number, :default => 200
|
|
30
|
+
|
|
31
|
+
# Maximum request body size in bytes; larger bodies get 413.
|
|
32
|
+
config :max_content_length, :validate => :number, :default => 100 * 1024 * 1024
|
|
33
|
+
|
|
34
|
+
# Where routing metadata (action, index, id, routing, pipeline) is stored.
|
|
35
|
+
config :metadata_target, :validate => :string, :default => "[@metadata][elasticsearch_compat]"
|
|
36
|
+
|
|
37
|
+
# Compress responses when the client accepts gzip or deflate (level 3, like Elasticsearch's
|
|
38
|
+
# http.compression). Compressed request bodies are always accepted.
|
|
39
|
+
config :compression_enabled, :validate => :boolean, :default => true
|
|
40
|
+
|
|
41
|
+
# TLS: same settings, defaults and rules as logstash-input-http.
|
|
42
|
+
config :ssl_enabled, :validate => :boolean, :default => false
|
|
43
|
+
config :ssl_certificate, :validate => :path
|
|
44
|
+
config :ssl_key, :validate => :path
|
|
45
|
+
config :ssl_key_passphrase, :validate => :password
|
|
46
|
+
config :ssl_keystore_path, :validate => :path
|
|
47
|
+
config :ssl_keystore_password, :validate => :password
|
|
48
|
+
config :ssl_keystore_type, :validate => %w[pkcs12 jks]
|
|
49
|
+
config :ssl_certificate_authorities, :validate => :array, :default => []
|
|
50
|
+
config :ssl_truststore_path, :validate => :path
|
|
51
|
+
config :ssl_truststore_password, :validate => :password
|
|
52
|
+
config :ssl_client_authentication, :validate => %w[none optional required], :default => "none"
|
|
53
|
+
config :ssl_supported_protocols, :validate => %w[TLSv1.1 TLSv1.2 TLSv1.3], :default => %w[TLSv1.2 TLSv1.3],
|
|
54
|
+
:list => true
|
|
55
|
+
config :ssl_cipher_suites,
|
|
56
|
+
:validate => Java::OrgLogstashPluginsInputsEscompatServerTls::SslContextFactory.supportedCiphers.to_a,
|
|
57
|
+
:default => Java::OrgLogstashPluginsInputsEscompatServerTls::SslContextFactory.defaultCiphers.to_a,
|
|
58
|
+
:list => true
|
|
59
|
+
config :ssl_handshake_timeout, :validate => :number, :default => 10000
|
|
60
|
+
|
|
61
|
+
def register
|
|
62
|
+
[[:threads, @threads], [:max_pending_requests, @max_pending_requests],
|
|
63
|
+
[:max_content_length, @max_content_length]].each do |name, value|
|
|
64
|
+
raise LogStash::ConfigurationError, "#{name} must be > 0" unless value > 0
|
|
65
|
+
end
|
|
66
|
+
if @max_content_length > java.lang.Integer::MAX_VALUE
|
|
67
|
+
raise LogStash::ConfigurationError, "max_content_length must be <= #{java.lang.Integer::MAX_VALUE}"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
raise LogStash::ConfigurationError, "ssl_handshake_timeout must be > 0" unless @ssl_handshake_timeout > 0
|
|
71
|
+
|
|
72
|
+
counters = Java::OrgLogstashPluginsInputsEscompatServer::RequestCounters.new
|
|
73
|
+
@metrics_publisher = LogStash::ElasticsearchCompat::MetricsPublisher.new(metric, counters)
|
|
74
|
+
@batch_handler = LogStash::ElasticsearchCompat::BatchHandler.new(
|
|
75
|
+
method(:push_event), logger, @metrics_publisher.method(:publish))
|
|
76
|
+
|
|
77
|
+
config = Java::OrgLogstashPluginsInputsEscompat::ServerConfig.builder
|
|
78
|
+
.name("escompat-#{id}").host(@host).port(@port).threads(@threads)
|
|
79
|
+
.maxPendingRequests(@max_pending_requests).maxContentLength(@max_content_length)
|
|
80
|
+
.compressionEnabled(@compression_enabled).counters(counters)
|
|
81
|
+
.sslContext(build_ssl_context).sslHandshakeTimeoutMillis(@ssl_handshake_timeout)
|
|
82
|
+
.build
|
|
83
|
+
@server = Java::OrgLogstashPluginsInputsEscompatServer::EsCompatServer.new(config, @batch_handler)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def run(queue)
|
|
87
|
+
@queue = queue
|
|
88
|
+
logger.info("Starting elasticsearch_compat input", :address => "#{@host}:#{@port}", :ssl_enabled => @ssl_enabled)
|
|
89
|
+
@metrics_publisher.start
|
|
90
|
+
@server.run
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def stop
|
|
94
|
+
@batch_handler.stop! if @batch_handler
|
|
95
|
+
@server.close if @server
|
|
96
|
+
@metrics_publisher.stop if @metrics_publisher
|
|
97
|
+
rescue => e
|
|
98
|
+
logger.debug("error while stopping elasticsearch_compat input", :exception => e.class.name)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def close
|
|
102
|
+
@server.close if @server
|
|
103
|
+
@metrics_publisher.stop if @metrics_publisher
|
|
104
|
+
rescue => e
|
|
105
|
+
logger.debug("error while closing elasticsearch_compat input", :exception => e.class.name)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
private
|
|
109
|
+
|
|
110
|
+
# Validates like logstash-input-http, then builds the SslContext (nil when TLS is off).
|
|
111
|
+
def build_ssl_context
|
|
112
|
+
settings = {
|
|
113
|
+
"ssl_enabled" => @ssl_enabled, "ssl_certificate" => @ssl_certificate, "ssl_key" => @ssl_key,
|
|
114
|
+
"ssl_keystore_path" => @ssl_keystore_path, "ssl_truststore_path" => @ssl_truststore_path,
|
|
115
|
+
"ssl_certificate_authorities" => @ssl_certificate_authorities,
|
|
116
|
+
"ssl_client_authentication" => @ssl_client_authentication
|
|
117
|
+
}
|
|
118
|
+
explicit = original_params.keys.select { |k| k.start_with?("ssl_") }
|
|
119
|
+
result = LogStash::ElasticsearchCompat::SslValidation.check(settings, explicit)
|
|
120
|
+
result[:warnings].each { |w| logger.warn(w) }
|
|
121
|
+
raise LogStash::ConfigurationError, result[:errors].first if result[:errors].any?
|
|
122
|
+
return nil unless @ssl_enabled
|
|
123
|
+
|
|
124
|
+
tls = Java::OrgLogstashPluginsInputsEscompatServerTls::TlsSettings.new(
|
|
125
|
+
@ssl_certificate, @ssl_key, secret(@ssl_key_passphrase),
|
|
126
|
+
@ssl_keystore_path, secret(@ssl_keystore_password), @ssl_keystore_type,
|
|
127
|
+
@ssl_certificate_authorities, @ssl_truststore_path, secret(@ssl_truststore_password),
|
|
128
|
+
@ssl_client_authentication, @ssl_supported_protocols, @ssl_cipher_suites)
|
|
129
|
+
Java::OrgLogstashPluginsInputsEscompatServerTls::SslContextFactory.build(tls)
|
|
130
|
+
rescue java.lang.IllegalArgumentException => e
|
|
131
|
+
logger.error("SSL configuration invalid", :message => e.message)
|
|
132
|
+
raise LogStash::ConfigurationError, e.message
|
|
133
|
+
rescue java.lang.Exception => e
|
|
134
|
+
logger.error("SSL configuration failed", :exception => e.class.name, :message => e.message)
|
|
135
|
+
raise LogStash::ConfigurationError, e.message
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def secret(password)
|
|
139
|
+
password.nil? ? nil : password.value
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Called from Java worker threads via the batch handler.
|
|
143
|
+
def push_event(document, metadata)
|
|
144
|
+
event = LogStash::Event.new(document)
|
|
145
|
+
event.set(@metadata_target, metadata)
|
|
146
|
+
decorate(event)
|
|
147
|
+
@queue << event
|
|
148
|
+
end
|
|
149
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
Gem::Specification.new do |s|
|
|
2
|
+
s.name = "logstash-input-elasticsearch_compat"
|
|
3
|
+
s.version = File.read(File.expand_path("VERSION", __dir__)).strip
|
|
4
|
+
s.licenses = ["Apache-2.0"]
|
|
5
|
+
s.summary = "Receives Elasticsearch Bulk API requests and emits Logstash events"
|
|
6
|
+
s.description = "This gem is a Logstash plugin required to be installed on top of the " \
|
|
7
|
+
"Logstash core pipeline using $LS_HOME/bin/logstash-plugin install gemname. " \
|
|
8
|
+
"This gem is not a stand-alone program."
|
|
9
|
+
s.authors = ["J. M. Becker"]
|
|
10
|
+
s.homepage = "https://github.com/techzilla/logstash-input-elasticsearch_compat"
|
|
11
|
+
s.require_paths = ["lib", "vendor/jar-dependencies"]
|
|
12
|
+
s.platform = "java"
|
|
13
|
+
s.required_ruby_version = ">= 3.1" # JRuby 9.4+, as bundled across Logstash 9.x
|
|
14
|
+
|
|
15
|
+
s.files = Dir["lib/**/*", "vendor/jar-dependencies/**/*.jar", "docs/**/*", "*.gemspec", "*.md",
|
|
16
|
+
"VERSION", "LICENSE", "NOTICE.TXT"]
|
|
17
|
+
|
|
18
|
+
s.metadata = { "logstash_plugin" => "true", "logstash_group" => "input" }
|
|
19
|
+
|
|
20
|
+
s.add_runtime_dependency "logstash-core-plugin-api", ">= 1.60", "<= 2.99"
|
|
21
|
+
s.add_runtime_dependency "logstash-codec-plain", "~> 3.1"
|
|
22
|
+
|
|
23
|
+
s.add_development_dependency "rspec", "~> 3.13"
|
|
24
|
+
end
|