diff --git a/CHANGELOG.md b/CHANGELOG.md index d9e628e..d26a706 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## 9.5.5 + +### Fixed + +- Output client initialization now retries up to 4 times with exponential backoff before exiting. This fixes persistent `Connection refused` errors in Docker when OpenSearch or Elasticsearch is momentarily unavailable at startup. +- Use tuple format for `http_auth` in OpenSearch and Elasticsearch connections, matching the documented convention and avoiding potential issues if the password contains a colon. + +### Changes + +- Added debug logging of host and SSL settings when connecting to OpenSearch. + ## 9.5.4 ### Fixed diff --git a/parsedmarc/cli.py b/parsedmarc/cli.py index ab10754..816cfd4 100644 --- a/parsedmarc/cli.py +++ b/parsedmarc/cli.py @@ -9,6 +9,7 @@ import logging import os import signal import sys +import time from argparse import ArgumentParser, Namespace from configparser import ConfigParser from glob import glob @@ -1849,15 +1850,30 @@ def _main(): logger.info("Starting parsedmarc") - # Initialize output clients - try: - clients = _init_output_clients(opts) - except ConfigurationError as e: - logger.critical(str(e)) - exit(1) - except Exception as error_: - logger.error("Output client error: {0}".format(error_)) - exit(1) + # Initialize output clients (with retry for transient connection errors) + max_retries = 4 + retry_delay = 5 + for attempt in range(max_retries + 1): + try: + clients = _init_output_clients(opts) + break + except ConfigurationError as e: + logger.critical(str(e)) + exit(1) + except Exception as error_: + if attempt < max_retries: + logger.warning( + "Output client error (attempt %d/%d, retrying in %ds): %s", + attempt + 1, + max_retries + 1, + retry_delay, + error_, + ) + time.sleep(retry_delay) + retry_delay *= 2 + else: + logger.error("Output client error: {0}".format(error_)) + exit(1) file_paths = [] mbox_paths = [] diff --git a/parsedmarc/constants.py b/parsedmarc/constants.py index 91a01fe..234d443 100644 --- a/parsedmarc/constants.py +++ b/parsedmarc/constants.py @@ -1,3 +1,3 @@ -__version__ = "9.5.4" +__version__ = "9.5.5" USER_AGENT = f"parsedmarc/{__version__}" diff --git a/parsedmarc/elastic.py b/parsedmarc/elastic.py index f2e56f2..9103a80 100644 --- a/parsedmarc/elastic.py +++ b/parsedmarc/elastic.py @@ -299,7 +299,7 @@ def set_hosts( else: conn_params["verify_certs"] = True if username and password: - conn_params["http_auth"] = username + ":" + password + conn_params["http_auth"] = (username, password) if api_key: conn_params["api_key"] = api_key connections.create_connection(**conn_params) diff --git a/parsedmarc/opensearch.py b/parsedmarc/opensearch.py index c99ea5b..c9dcaf2 100644 --- a/parsedmarc/opensearch.py +++ b/parsedmarc/opensearch.py @@ -298,6 +298,7 @@ def set_hosts( """ if not isinstance(hosts, list): hosts = [hosts] + logger.debug("Connecting to OpenSearch: hosts=%s, use_ssl=%s", hosts, use_ssl) conn_params = {"hosts": hosts, "timeout": timeout} if use_ssl: conn_params["use_ssl"] = True @@ -323,7 +324,7 @@ def set_hosts( conn_params["connection_class"] = RequestsHttpConnection elif normalized_auth_type == "basic": if username and password: - conn_params["http_auth"] = username + ":" + password + conn_params["http_auth"] = (username, password) if api_key: conn_params["api_key"] = api_key else: