HTTP connectors¶
A family of HTTP(S) connectors (generic webhook sink, Elasticsearch / OpenSearch, Splunk HEC, InfluxDB, Prometheus Pushgateway, an HTTP polling source, and Google Cloud Pub/Sub) built on one vendored HTTP client. The module provides both sinks and sources.
Overview¶
clink::http_connector is a single module that registers several connectors over a shared keep-alive HTTP/1.1 client. Every connector carries records as std::string: on the SQL path each row is rendered to a JSON object string (row_to_json_string), and a polling source emits each JSON array element as a single-line JSON string. The sinks buffer records and POST them in batches; the request body framing is per connector (a JSON array, NDJSON action+doc pairs for the _bulk API, InfluxDB line protocol, a Prometheus exposition body, or a Pub/Sub messages envelope). The transport is the vendored cpp-httplib library, confined behind a pImpl in http_request.cpp; HTTPS is available only when that library is built with OpenSSL.
The same HttpRequest transport is reused by the WebHDFS connector in a separate module.
Dependency and version¶
| Component | Provenance | Version |
|---|---|---|
| cpp-httplib | Vendored into the build via FetchContent (header-only) | Not pinned by clink in scripts/versions.env |
| OpenSSL | System package via apt (Debian) / brew (macOS); used only for HTTPS, via cpp-httplib's own CMake | Not pinned by clink |
There is no Arrow dependency on this module. The connectors do not link Arrow; records flow as JSON strings.
Enabling it¶
The CMake knob is CLINK_WITH_HTTP (AUTO / ON / OFF, default AUTO, set in the top-level CMakeLists.txt). cpp-httplib is vendored at the top level and is header-only, so the module builds whenever the httplib::httplib target is present; there is no external client library to install. With CLINK_WITH_HTTP=ON the build fails if that target is absent; with AUTO it is silently skipped.
HTTPS support is decided by cpp-httplib's own CMake: when it finds OpenSSL it sets CPPHTTPLIB_OPENSSL_SUPPORT on the interface target, which propagates to this module. On a build without OpenSSL, HttpRequest::tls_supported() returns false and every sink and source rejects an https:// URL up front with a clear error.
cmake -S . -B build -DCLINK_WITH_HTTP=ON
Factories¶
| Factory name | Direction | Record type |
|---|---|---|
http_sink |
Sink | std::string |
elasticsearch_sink |
Sink | std::string |
opensearch_sink |
Sink | std::string |
splunk_hec_sink |
Sink | std::string |
influxdb_sink |
Sink | std::string |
prometheus_sink |
Sink | std::string |
http_poll_source |
Source | std::string |
pubsub_sink |
Sink | std::string |
pubsub_source |
Source | std::string |
All factory names above are the exact strings passed to register_sink<std::string> / register_source<std::string> in impls/http_connector/src/register_factories.cpp.
Configuration¶
Options are parsed from the BuildContext in register_factories.cpp and map onto each connector's Options struct. The tables below list the keys per factory.
Shared options¶
These keys are read by every connector unless noted otherwise (defaults and the [0, 20] max_retries clamp come from the bulk-sink base in batched_http_bulk_sink.hpp and the per-connector factory code):
| Option | Required | Default | Description |
|---|---|---|---|
url |
Yes | (none) | scheme://host[:port] base URL. https:// requires an OpenSSL build. |
headers |
No | (empty) | Extra request headers as "K1: V1; K2: V2". Used for auth (e.g. Authorization: Bearer ...). |
verify_tls |
No | true |
HTTPS server-certificate verification; false skips it. |
batch_records |
No | 500 |
Flush when this many records are buffered. |
batch_bytes |
No | 4194304 |
Flush when the buffered body reaches this many bytes. |
max_retries |
No | 4 |
Retry attempts beyond the first; clamped to [0, 20]. |
linger_ms |
No | 0 |
Flush a partial batch once its oldest record is this old (evaluated on record arrival, not on a timer). 0 disables. |
dlq |
No | fail |
drop routes a permanently-rejected (4xx) batch to the dead-letter path (a BadRecord report plus metrics) instead of failing the job. Any other value keeps the fail-and-replay default. |
http_poll_source and the Prometheus sink do not use all of these; their connector-specific keys are below.
http_sink¶
| Option | Required | Default | Description |
|---|---|---|---|
path |
No | / |
Request path. |
bulk_format |
No | json_array |
Request body framing: json_array ([rec0,rec1,...]) or ndjson (newline-delimited). Named bulk_format, not format, because format is the SQL channel selector. |
content_type |
No | application/json |
Request Content-Type. |
elasticsearch_sink / opensearch_sink¶
Identical _bulk API; records are framed as NDJSON action+doc pairs.
| Option | Required | Default | Description |
|---|---|---|---|
index |
Yes | (none) | Target index for the _index action metadata. |
document_id |
No | (empty) | Record field used as the document _id; setting it makes re-delivery an idempotent overwrite. |
path |
No | /_bulk |
Bulk endpoint path. |
splunk_hec_sink¶
Posts events to a Splunk HTTP Event Collector with Authorization: Splunk <token>.
| Option | Required | Default | Description |
|---|---|---|---|
token |
Yes | (empty) | HEC token; sets the Authorization header. |
path |
No | /services/collector/event |
HEC event endpoint. |
sourcetype |
No | (empty) | Event sourcetype (e.g. _json to auto-extract fields). |
source |
No | (empty) | Event source metadata. |
host |
No | (empty) | Event host metadata. |
index |
No | (empty) | Target index (must be writable by the token). |
influxdb_sink¶
Converts each JSON-object row to one InfluxDB v2 line-protocol point and POSTs NDJSON batches to /api/v2/write with Authorization: Token <token>.
| Option | Required | Default | Description |
|---|---|---|---|
org |
Yes | (empty) | InfluxDB v2 organisation. |
bucket |
Yes | (empty) | InfluxDB v2 bucket. |
token |
Yes | (empty) | API token; sets the Authorization header. |
measurement |
Yes | (empty) | Line-protocol measurement name. |
tags |
No | (empty) | CSV of row fields to emit as tags; remaining scalar fields become fields. |
timestamp_field |
No | (empty) | Row field holding the point timestamp; otherwise the server assigns it. |
precision |
No | ns |
Write precision: ns, us, ms, or s. |
The required fields above are validated by make_influxdb_sink and the factory; the factory reads them with empty defaults, so they are required in practice rather than by a param_or default.
prometheus_sink¶
Pushes batched records as gauge samples to a Prometheus Pushgateway. This sink does not read batch_bytes or dlq.
| Option | Required | Default | Description |
|---|---|---|---|
job |
Yes | (empty) | Grouping job (first path segment under /metrics/job/). |
grouping |
No | (empty) | Extra static path labels as k=v,k2=v2. |
metric_name |
No | clink_value |
The single gauge metric name. |
value_field |
No | value |
Record field holding the gauge value. |
help |
No | (empty) | Optional # HELP text. |
batch_records |
No | 500 |
Maximum distinct series per push. |
http_poll_source¶
GETs a JSON REST endpoint on an interval and emits the array elements, with a checkpointed cursor. This source does not use batch_records / batch_bytes / dlq / linger_ms.
| Option | Required | Default | Description |
|---|---|---|---|
path |
No | / |
Request path (may carry a fixed query string). |
headers |
No | (empty) | Request headers (auth). |
verify_tls |
No | true |
HTTPS certificate verification. |
cursor_param |
No | (empty) | Query-parameter name to send the cursor as. |
cursor_field |
No | (empty) | Record field holding the next cursor; the API must return records ascending by it. |
records_field |
No | (empty) | Response field holding the array; if empty, the response itself is the array. |
initial_cursor |
No | (empty) | Starting cursor on a fresh run. |
poll_interval_ms |
No | 1000 |
Interval between polls. |
jitter_frac |
No | 0 |
Fraction of jitter applied to the poll interval. |
bounded |
No | false |
true stops after a poll returns nothing (one-shot). |
max_retries |
No | 4 |
Transient-GET retries within one poll; clamped to [0, 20]. |
retry_base_backoff_ms |
No | 200 |
Base backoff between transient retries. |
pubsub_sink / pubsub_source¶
Google Cloud Pub/Sub over the REST v1 API. The base URL resolves in order: the endpoint option, else the PUBSUB_EMULATOR_HOST environment variable (host:port becomes http://host:port), else https://pubsub.googleapis.com.
Shared Pub/Sub options:
| Option | Required | Default | Description |
|---|---|---|---|
endpoint |
No | (env / real service) | Override base URL (e.g. an emulator). |
auth_token |
No | (empty) | Static bearer token; sets Authorization: Bearer <token>. Valid only for its lifetime (no refresh). |
auth_token_file |
No | (empty) | Path to a file holding the bearer token, re-read per request when the file changes. An external refresher (Workload Identity / sidecar / Vault agent) rotating the file is picked up without restarting the job. Takes precedence over auth_token. |
headers |
No | (empty) | Extra headers, merged after the token (an explicit Authorization header wins). |
verify_tls |
No | true |
HTTPS certificate verification. |
max_retries |
No | 4 |
Retry attempts; clamped to [0, 20]. |
pubsub_sink additional options:
| Option | Required | Default | Description |
|---|---|---|---|
project |
Yes | (none) | GCP project id. |
topic |
Yes | (none) | Topic id. |
batch_records |
No | 1000 |
Messages per publish; capped at the 1000-message Pub/Sub limit. |
batch_bytes |
No | 9437184 |
Flush threshold in bytes; stays under the request size limit. |
dlq |
No | fail |
Permanent-failure policy (fail / drop). |
linger_ms |
No | 0 |
Partial-batch linger. |
pubsub_source additional options:
| Option | Required | Default | Description |
|---|---|---|---|
project |
Yes | (none) | GCP project id. |
subscription |
Yes | (none) | Subscription id. |
max_messages |
No | 1000 |
Pull maxMessages; capped at 1000. |
return_immediately |
No | true |
Pull returnImmediately flag. |
poll_interval_ms |
No | 500 |
Sleep after an empty pull. |
retry_base_backoff_ms |
No | 200 |
Base backoff between transient-pull retries. |
Authentication note: this build does not mint OAuth2 tokens. Supply a bearer token via auth_token (static) or headers, or via auth_token_file (re-read per request, so an external refresher rotating the file is picked up without a restart); against the Pub/Sub emulator no auth is needed.
SQL usage¶
Several of these connectors are mapped in src/sql/physical_plan.cpp. The SQL connector='...' name differs from the factory name in some cases:
SQL connector |
Factory | Direction |
|---|---|---|
http |
http_sink |
Sink |
elasticsearch |
elasticsearch_sink |
Sink |
opensearch |
opensearch_sink |
Sink |
splunk_hec (or splunk) |
splunk_hec_sink |
Sink |
influxdb |
influxdb_sink |
Sink |
prometheus |
prometheus_sink |
Sink |
http_poll |
http_poll_source |
Source |
pubsub |
pubsub_sink / pubsub_source |
Both |
http_sink, elasticsearch_sink, opensearch_sink, splunk_hec_sink, influxdb_sink, and prometheus_sink are not registered as standalone SQL source/sink names; they are reached only through these connector aliases. The SQL planner rejects exactly_once and upsert modes on these sinks (they are at-least-once).
Sink example (Elasticsearch):
CREATE TABLE es_out (
id STRING,
body STRING
) WITH (
connector = 'elasticsearch',
format = 'json',
url = 'http://es:9200',
index = 'events',
document_id = 'id'
);
Source example (HTTP poll):
CREATE TABLE feed (
id BIGINT,
msg STRING
) WITH (
connector = 'http_poll',
format = 'json',
url = 'http://api:8080',
path = '/events',
records_field = 'items',
cursor_param = 'since',
cursor_field = 'id',
poll_interval_ms = '2000'
);
A connector='http' table must declare format='json' so the row is rendered to a JSON string before the string-channel http_sink receives it.
Example¶
Programmatic use of the Elasticsearch sink, based on the live test (impls/http_connector/tests/test_elasticsearch_sink_live.cpp). Each record is already a JSON object string:
#include "clink/http_connector/bulk_sink_builders.hpp"
using namespace clink::http_connector;
EsBulkOptions o;
o.url = "http://es:9200";
o.index = "events";
o.document_id = "id"; // re-delivery overwrites the same _id
o.batch_records = 100;
auto sink = make_es_bulk_sink(o);
sink->open();
sink->on_data(/* Batch<std::string> of JSON object strings */);
sink->flush(); // also runs on every checkpoint barrier
Via the registry (string channel), the same connector is reachable by its factory name:
const auto& rr = clink::cluster::RunnerRegistry::default_instance();
auto factory = rr.find_sink("elasticsearch_sink", "string");
Delivery semantics¶
- Sinks (
http_sink,elasticsearch_sink,opensearch_sink,splunk_hec_sink,influxdb_sink,prometheus_sink,pubsub_sink) are at-least-once. The sharedBatchedHttpBulkSinkflushes on every checkpoint barrier (before the sink acks), on the size and linger thresholds, and at end-of-stream. On permanent failure with retries exhausted it throws rather than dropping, so the job replays from the last checkpoint. There is no two-phase commit, so a replay re-delivers buffered records. elasticsearch_sink/opensearch_sinkupgrade toward effectively-once whendocument_idis set: a replayed document overwrites the same_idrather than duplicating. The_bulkresponse is parsed per item, so a transient per-item failure resends only the failed records and a permanent per-item failure is DLQ'd or thrown per thedlqpolicy.influxdb_sinkis effectively-once whentimestamp_fieldis set, since an identical point is idempotent.splunk_hec_sinkandprometheus_sinkare at-least-once with no dedup key; HEC ingestion is append, and the Pushgateway stores the latest value per series (last-write-wins within a push).pubsub_sinkis at-least-once with no producer dedup; a replay re-publishes.http_poll_sourceis at-least-once: the cursor is checkpointed. Avoiding re-emission of the boundary record requires an exclusive cursor server-side.pubsub_sourceis at-least-once: it pulls, holds the ackIds, and acknowledges on the checkpoint (the ack is the offset commit); unacked messages are redelivered by the server after the subscriptionackDeadline, which is also the crash-recovery mechanism. Two honest caveats from the source comments: the:acknowledgecall is not transactional with the global checkpoint, so if a checkpoint acks a batch and the global checkpoint then fails, that one batch is at-most-once; and a message whosedatais not valid base64 is acked and dropped (with an error metric) rather than redelivered forever.
Limitations¶
- All connectors carry records as
std::stringonly; there is no int64 or typed-Arrow channel. On the SQL path rows are serialised to JSON strings. - No two-phase commit on any sink; exactly-once delivery is not offered. The SQL planner rejects
exactly_onceandupsertmodes on these sinks. - HTTPS works only if cpp-httplib was built with OpenSSL; otherwise an
https://URL is rejected at construction. prometheus_sinktargets a Pushgateway, which is a latest-value store for low-cardinality service metrics, not a high-throughput event TSDB. Records whosevalue_fieldis missing or non-numeric are dropped (no sample). Remote-write is not implemented.http_poll_sourcerequires the API to return records ascending bycursor_field; it has no client-side ordering or de-duplication beyond the cursor.- Pub/Sub uses the synchronous REST
PullwithreturnImmediately; gRPC StreamingPull is not used. The build does not mint OAuth2 tokens itself; a staticauth_tokenexpires after its lifetime, butauth_token_filelets an external refresher rotate the token without a restart. Multiple subtasks pull the same subscription and Pub/Sub load-balances across them (no client-side sharding). - The bulk sinks buffer rendered records in memory until a flush trigger;
batch_records/batch_bytes/linger_msbound that buffer. linger_msis evaluated only when a record arrives (no timer thread), so a fully idle buffer waits for the next checkpoint barrier.
Testing¶
The module's tests live in impls/http_connector/tests/ and carry the ctest label http_connector. Most are in-process or mock tests (factory registration, framing, the Splunk and Prometheus body builders) and run in a normal build:
ctest --test-dir build -L http_connector
Three live integration tests are skipped unless their endpoint environment variable is set, and gate on the Live name filter. The sidecar services are defined in docker/integration-services.yml:
| Test | Env var(s) | Service / image |
|---|---|---|
Elasticsearch (test_elasticsearch_sink_live.cpp) |
CLINK_ELASTICSEARCH_TEST_ENDPOINT (e.g. http://localhost:9200) |
docker.elastic.co/elasticsearch/elasticsearch:8.13.4 |
InfluxDB (test_influxdb_live.cpp) |
CLINK_INFLUXDB_TEST_ENDPOINT, plus optional CLINK_INFLUXDB_TEST_ORG / _BUCKET / _TOKEN (defaults clink / clink / clink-token) |
influxdb:2 |
Pub/Sub (test_pubsub_live.cpp) |
CLINK_PUBSUB_EMULATOR_HOST (e.g. localhost:8085) |
gcr.io/google.com/cloudsdktool/cloud-sdk:emulators |
docker compose -f docker/integration-services.yml up -d
export CLINK_ELASTICSEARCH_TEST_ENDPOINT=http://localhost:9200
ctest --test-dir build -L http_connector -R Live --output-on-failure
docker compose -f docker/integration-services.yml down -v
There is no live test for splunk_hec_sink, prometheus_sink, or http_poll_source; these are covered by in-process and mock tests only.