|
5 | 5 | import unittest |
6 | 6 | import glob |
7 | 7 | import subprocess |
| 8 | +import time |
8 | 9 |
|
9 | 10 | import json |
10 | 11 | import logging |
11 | 12 | from parameterized import parameterized |
12 | 13 | from elasticsearch import Elasticsearch, NotFoundError |
13 | 14 | from deepdiff import DeepDiff |
14 | 15 |
|
| 16 | +# Maximum time (seconds) Filebeat is allowed to ingest a module's test file. |
| 17 | +# Inputs started with --once exit as soon as the file is fully read; tailing |
| 18 | +# inputs (journald, filestream) never exit, so the test stops Filebeat once the |
| 19 | +# expected events have been indexed or this timeout elapses. |
| 20 | +MODULE_INGEST_TIMEOUT = 60 |
| 21 | + |
15 | 22 | # datasets for which @timestamp is removed due to date missing |
16 | 23 | remove_timestamp = { |
17 | 24 | "activemq.audit", |
@@ -258,17 +265,19 @@ def run_on_file(self, module, fileset, test_file, cfgfile): |
258 | 265 | stdout=output, |
259 | 266 | stderr=subprocess.STDOUT, |
260 | 267 | bufsize=0) |
261 | | - # The journald input (used by some modules like 'system') does not |
262 | | - # support the --once flag, hence we run Filebeat for at most |
263 | | - # 15 seconds, if it does not finish, then kill the process. |
264 | | - # If for any reason the Filebeat process gets stuck, only SIGKILL |
265 | | - # will terminate it. We use SIGKILL to avoid leaking any running |
266 | | - # process that could interfere with other tests |
267 | | - try: |
268 | | - proc.wait(15) |
269 | | - except subprocess.TimeoutExpired: |
270 | | - # Send SIGKILL |
271 | | - proc.kill() |
| 268 | + if "--once" in cmd: |
| 269 | + # Process will exit on its own once the file is fully read. Wait the maximum time, |
| 270 | + # if it doesn't exit by then, kill it to avoid leaking a process. |
| 271 | + try: |
| 272 | + proc.wait(MODULE_INGEST_TIMEOUT) |
| 273 | + except subprocess.TimeoutExpired: |
| 274 | + proc.kill() |
| 275 | + proc.wait() |
| 276 | + else: |
| 277 | + # The journald and filestream inputs do not support --once and |
| 278 | + # tail the file forever. Stop filebeat once elasticsearch has |
| 279 | + # indexed the expected number of events (or the timeout elapses). |
| 280 | + self._wait_for_events_then_stop(proc, test_file, MODULE_INGEST_TIMEOUT) |
272 | 281 |
|
273 | 282 | # List of errors to check in filebeat output logs |
274 | 283 | errors = ["error loading pipeline for fileset"] |
@@ -311,6 +320,38 @@ def run_on_file(self, module, fileset, test_file, cfgfile): |
311 | 320 |
|
312 | 321 | self._test_expected_events(test_file, objects) |
313 | 322 |
|
| 323 | + def _wait_for_events_then_stop(self, proc, test_file, timeout): |
| 324 | + expected = self._expected_event_count(test_file) |
| 325 | + deadline = time.monotonic() + timeout |
| 326 | + try: |
| 327 | + while time.monotonic() < deadline: |
| 328 | + if proc.poll() is not None: |
| 329 | + # Filebeat exited on its own; nothing left to stop. |
| 330 | + return |
| 331 | + if expected is not None and self._indexed_event_count() >= expected: |
| 332 | + break |
| 333 | + time.sleep(0.5) |
| 334 | + finally: |
| 335 | + if proc.poll() is None: |
| 336 | + proc.kill() |
| 337 | + proc.wait() |
| 338 | + |
| 339 | + def _expected_event_count(self, test_file): |
| 340 | + if os.getenv("GENERATE"): |
| 341 | + return None |
| 342 | + try: |
| 343 | + with open(test_file + "-expected.json", "r") as f: |
| 344 | + return len(json.load(f)) |
| 345 | + except (FileNotFoundError, ValueError): |
| 346 | + return None |
| 347 | + |
| 348 | + def _indexed_event_count(self): |
| 349 | + try: |
| 350 | + self.es.indices.refresh(index=self.index_name) |
| 351 | + return self.es.count(index=self.index_name)["count"] |
| 352 | + except NotFoundError: |
| 353 | + return 0 |
| 354 | + |
314 | 355 | def _test_expected_events(self, test_file, objects): |
315 | 356 |
|
316 | 357 | # Generate expected files if GENERATE env variable is set |
|
0 commit comments