ryerraguntla opened a new pull request, #3515:
URL: https://github.com/apache/iggy/pull/3515

   **Which issue does this PR close?**
   Closes #3503
   
   **Rationale**
   Iggy had no native way to ingest documents from OpenSearch  clusters into 
Iggy streams. This connector fills that gap for observability pipelines, audit 
log forwarding, and data lake ingestion scenarios where OpenSearch is the 
source of truth.
   
   **What changed?**
   **Before**: no OpenSearch source connector existed. Users had to write 
custom consumers using the OpenSearch client to bridge documents into Iggy.
   
   **After**: a new iggy_connector_opensearch_source cdylib plugin implements 
the Source trait and is loaded by the connectors runtime via FFI. Each poll() 
issues a search_after-paginated query on (timestamp_field, _id), publishes each 
hit's _source as a JSON ProducedMessage, and persists the cursor in 
ConnectorState.
   
   **Module layout**
   ```
   core/connectors/sources/opensearch_source/
   ├── Cargo.toml
   ├── README.md
   ├── dependencies.md
   └── src/
       ├── lib.rs            # Source trait, poll loop, search_after cursor, 
config, 16 unit tests
       ├── state_manager.rs  # File-backed SourceState mirror, atomic rename 
protocol, 4 unit tests
       ├── retry.rs  # Retry logic and circuit breaker logic. 2 unit tests 
       └── http_tests.rs     # Axum mock server tests (20 tests, no Docker 
required)
   core/connectors/runtime/example_config/connectors/
   └── opensearch_source.toml
   core/integration/tests/connectors/opensearch/
   ├── mod.rs
   ├── opensearch_source.rs      # 7 Docker-backed integration tests
   ├── opensearch_source_types.rs
   ├── opensearch_source_resilience.rs
   └── fixtures/opensearch/      # Container start/stop, document indexing 
helpers
   
   ```
   
   **Key implementation decisionsCursor mechanism** — 
   **search_after on (timestamp_field, _id) sorted ascending.** 
   Two sort keys give stable pagination when timestamps collide; _id is the 
tiebreaker. No Scroll API, PIT, or offset paging.
   
   **State authority** — runtime **ConnectorState** (MessagePack) is 
authoritative. An optional file-backed JSON mirror (SourceState) is written 
atomically on close() using write-tmp → fdatasync → rename → dir-fsync. On 
restart, if a valid runtime state exists it takes priority and file state is 
not loaded. If state deserialization fails, open() returns InitError to prevent 
a silent cursor reset.
   
   **Validation** at open() — timestamp_field is required and non-empty 
(validated before any network call). batch_size = 0 is rejected. File state 
config is validated if state.enabled = true.
   
   **Error handling** — transient OpenSearch errors fail the poll with the 
error counter incremented. No circuit breaker or HTTP retry in this version 
(documented limitation). poll() still sleeps polling_interval on error so the 
runtime does not spin.
   
   **At-least-once delivery** — cursor advances in memory before ConnectorState 
is persisted by the runtime. A crash after poll() returns but before the 
runtime writes state can re-emit the last batch. Documented in README and 
Limitations.
   
   **Validation**
   
   ```
   cargo fmt --all
   cargo sort --no-format --workspace
   cargo clippy -p iggy_connector_opensearch_source --all-targets -- -D warnings
   cargo test -p iggy_connector_opensearch_source
   ./scripts/ci/taplo.sh
   ./scripts/ci/license-headers.sh
   # Docker-backed integration (requires running iggy-server and 
iggy-connectors binaries):
   env CARGO_BIN_EXE_iggy-server=... CARGO_BIN_EXE_iggy-connectors=... \
     cargo test -p integration --test mod -- 
connectors::opensearch::opensearch_source
   
   ```
   
   All 40 tests pass (16 unit + 4 state_manager unit + 20 axum HTTP mock + 7 
Docker integration).
   
   **Local Execution**
   Passed
   Pre-commit hooks ran
   
   **AI Usage**
   
   1. Tools used: Claude
   2. Scope: scaffolding initial file structure and boilerplate; all logic 
reviewed and iterated manually
   3. Verification: full local compilation, all tests executed, end-to-end 
tested with Docker OpenSearch + Iggy server + connector runtime
   4. Can explain every line: yes
   
   **Configuration reference**
   ```
   type = "source"
   key = "opensearch"
   enabled = true
   version = 0
   name = "OpenSearch source"
   path = "<BASE_DIR>/target/release/libiggy_connector_opensearch_source"
   plugin_config_format = "json"
   [[streams]]
   stream = "opensearch_stream"
   topic  = "documents"
   schema = "json"
   batch_length = 100
   linger_time  = "5ms"
   [plugin_config]
   url              = "http://localhost:9200";
   index            = "logs-*"
   timestamp_field  = "@timestamp"       # required; must exist on every 
document
   polling_interval = "10s"              # default
   batch_size       = 100                # default
   # username       = "admin"
   # password       = "replace_with_secret"
   # verbose_logging = false
   # Optional: custom query DSL (default: match_all)
   # [plugin_config.query]
   # match = { "log.level" = "error" }
   # Optional: file-backed state mirror (runtime msgpack is authoritative)
   # [plugin_config.state]
   # enabled      = true
   # storage_type = "file"
   # state_id     = "opensearch_logs_connector"
   # [plugin_config.state.storage_config]
   # base_path = "./connector_states"
   
   ```
   
   <h2>Required Fields</h2>
   
   Field | Type | Description
   -- | -- | --
   url | String | OpenSearch HTTP base URL
   index | String | Index name or pattern (logs-* supported)
   timestamp_field | String | Document field used as sort key and cursor; must 
exist on every document
   
   </body></html>
   
   <h2 class="text-text-100 mt-3 -mb-1 text-[1.125rem] font-bold" 
data-sourcepos="11:1-11:19;335-353">Optional Fields</h2>
   
   <div class="overflow-x-auto w-full px-2 mb-6" 
data-sourcepos="13:1-19:92;355-829">
   
   Field | Type | Default | Description
   -- | -- | -- | --
   polling_interval | humantime | "10s" | Sleep after each completed poll cycle
   batch_size | usize | 100 | Documents per search request (min 1)
   query | JSON | {"match_all":{}} | OpenSearch query DSL applied on every poll
   username / password | String | — | HTTP Basic auth
   verbose_logging | bool | false | Log per-poll counts at info! instead of 
debug!
   
   </div>
   
   
   **Poll cycle**
   
   Each poll() call:Issues POST /{index}/_search sorted by (timestamp_field 
asc, _id asc) with optional search_after.Maps each hit's _source to a JSON 
ProducedMessage.Advances the search_after cursor to the sort tuple of the last 
successfully published hit.Returns ProducedMessages with serialized 
ConnectorState.Sleeps polling_interval.Hits missing a sort tuple or _source are 
skipped with warn!; they do not block the cursor.
   
   **Known limitations**
   
   1. At-least-once delivery — cursor advances before the runtime persists 
ConnectorState; last batch may be re-emitted on crash.
   2. Single sequential reader — one search_after cursor, one batch per poll; 
no parallel shard workers.
   3. search_after only — no Scroll API, PIT, or sliced export.
   4. Backfill gap — documents with timestamp_field older than the current 
cursor are not read without a state reset.
   5. _source-disabled documents skipped permanently.Single-node transport — no 
cluster sniffing.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to