Events and Analytics¶
Imbi keeps its analytics rows in ClickHouse. No service writes to ClickHouse directly. Each service publishes rows to Apache Iggy, and the Iggy connectors runtime (iggy-connect) writes them to ClickHouse. The services only read from ClickHouse.
ADR 0019 records the decision.
The pipeline¶
flowchart LR
subgraph producers[Producers]
api[imbi-api]
gw[imbi-gateway]
sch[imbi-scheduler]
wf[imbi-workflows]
end
iggy[(Apache Iggy<br/>one stream per table)]
conn[iggy-connect<br/>ClickHouse sink]
ch[(ClickHouse<br/>one table per stream)]
api -- publish --> iggy
gw -- publish --> iggy
sch -- publish --> iggy
wf -- publish --> iggy
iggy -- consumer group per stream --> conn
conn -- insert --> ch
conn -. sink configuration at startup .-> api
api -- query --> ch
- A producer calls
imbi.common.iggy.publish()with a list of Pydantic models. Each model becomes one JSON message. - Iggy holds the message in a stream and a topic. The stream name
is the name of the ClickHouse table. The topic is the feature that
made the row, for example
githubormaintenance. - iggy-connect runs one ClickHouse sink for each stream. Each sink reads all the topics of its stream in a consumer group that has the stream name. It inserts batches of up to 1000 rows and polls every 250 ms.
- The services read the tables with
imbi.common.clickhouse.query(). The ClickHouse client in imbi-common has no insert function.
The GitHub plugin also publishes. Its commit and pull request handlers run in imbi-gateway (webhook actions) and in imbi-api (on-demand syncs).
imbi-workflows can start without Iggy. If it cannot publish, an imbi-api sweep publishes the run history later. The other producers do not start without Iggy.
Topics¶
imbi.common.iggy.TOPICS is the one list of streams and topics. These
parts of Imbi read it:
- The producers. At startup, each producer creates each stream and topic in the list that does not exist. Each topic has one partition.
- The sink configuration. imbi-api makes one sink for each stream in the list. See Sink configuration.
- The metrics. imbi-api reports the state of each topic and of its consumer group.
The table below comes from TOPICS and from the ClickHouse schema file,
libraries/common/src/imbi/common/clickhouse/schemata.toml. To refresh it,
run uv run python scripts/docs_iggy_topics.py and paste the output.
| Stream and ClickHouse table | Topics | Engine | TTL |
|---|---|---|---|
agent_task_events |
agent_tasks |
ReplacingMergeTree() |
none |
agent_usage |
agent_tasks |
ReplacingMergeTree() |
none |
commit_drift |
drift |
ReplacingMergeTree(recorded_at) |
none |
commits |
github, maintenance |
ReplacingMergeTree(recorded_at) |
none |
document_read_events |
documents |
ReplacingMergeTree(recorded_at) |
toDateTime(session_started_at) + INTERVAL 90 DAY |
document_read_sessions |
documents |
ReplacingMergeTree(finalized_at) |
toDateTime(started_at) + INTERVAL 2 YEAR |
document_versions |
documents |
ReplacingMergeTree(recorded_at) |
none |
email_audit |
email |
MergeTree() |
toDateTime(sent_at) + INTERVAL 1 YEAR |
events |
comments, documents, gateway, lifecycle, projects |
ReplacingMergeTree(version) |
none |
maintenance_log |
maintenance |
MergeTree() |
toDateTime(occurred_at) + INTERVAL 90 DAY |
operations_log |
api, configuration, deployments, maintenance |
ReplacingMergeTree(_row_version) |
none |
pull_request_review_requests |
github |
ReplacingMergeTree(recorded_at) |
none |
pull_request_reviews |
github |
ReplacingMergeTree(recorded_at) |
none |
pull_requests |
github |
ReplacingMergeTree(recorded_at) |
none |
release_component_batches |
sbom |
MergeTree() |
none |
release_components |
sbom |
MergeTree() |
none |
scheduler_runs |
scheduler |
ReplacingMergeTree(_row_version) |
toDateTime(fired_at) + INTERVAL 90 DAY |
score_history |
scoring |
MergeTree() |
none |
secret_audit_events |
secrets |
ReplacingMergeTree() |
none |
tags |
github |
ReplacingMergeTree(recorded_at) |
none |
workflow_run_events |
workflows |
ReplacingMergeTree() |
none |
workflow_runs |
workflows |
ReplacingMergeTree(_row_version) |
none |
Sink configuration¶
iggy-connect has no sink configuration of its own. At startup it gets the
configuration from imbi-api, at /iggy/connectors/configs/active under the
imbi-api route prefix (the path of IMBI_API_URL). The response contains
the ClickHouse URL, user name, and password. imbi-api takes them from its
own CLICKHOUSE_URL.
The response contains credentials, so the endpoint needs a bearer key:
- Set
IMBI_IGGY_CONNECTORS_API_KEYSon imbi-api. To rotate a key, put two keys in the value, with a comma between them. - Set the same key as
IGGY_CONNECTORS_API_KEYon iggy-connect. - If
IMBI_IGGY_CONNECTORS_API_KEYSis not set, the endpoint returns 503. It never returns the configuration without a key.
The endpoint is not in the OpenAPI schema. Thus it is not in the UI client and it is not an MCP tool.
Restart rule for new topics¶
iggy-connect reads its configuration one time, at startup. A stream or
topic that is added to TOPICS gets no sink until iggy-connect restarts.
After you deploy a release that adds a stream or a topic:
- Make sure that imbi-api runs the new release. imbi-api creates the new topic at startup.
- Restart iggy-connect. On Kubernetes, use
kubectl rollout restart deployment/<name>.
iggy-connect stops at startup if imbi-api does not answer, or if a stream
in its configuration does not exist. In both cases, start it again after
imbi-api is up. The Compose file (restart: on-failure) and the Helm chart
(a Deployment) do this automatically.
For other Iggy operations, for example a consumer group member that holds a partition and reads nothing, see Operations.
Table design rules¶
ReplacingMergeTree¶
Iggy delivers a message at least one time. A retry or a second sync can
write the same row again. Thus most tables use ReplacingMergeTree.
ClickHouse keeps one row for each sort key when it merges the parts of a
table:
- The sort key (
ORDER BY) is the identity of a row. Two rows with the same key are the same record. - The version argument, for example
ReplacingMergeTree(recorded_at), selects the row that stays: the row with the highest version. - ClickHouse selects the time of a merge. Until the merge, the duplicates
stay in the table. A query that must see one row for each key uses
FINAL.
Some records change state. For example, a scheduler run has a running
row and then a terminal row. The producer writes the new state as a new
row with a higher version (_row_version). The merge keeps the newest
state.
Tables that only append use MergeTree. Each row in these tables is a
separate fact, for example one email that Imbi sent.
TTL¶
A TTL deletes old rows when ClickHouse merges parts. Obey these rules for a TTL:
- Use a column that every row of a record has. ClickHouse evaluates the
TTL for each row. Think of a record with a
runningrow and a terminal row, and a TTL column that only the terminal row has. The terminal row expires first. The merge then keeps therunningrow, and the record shows as still running. For this reasonscheduler_runsusesfired_atanddocument_read_eventsusessession_started_at. - Do not use
DELETE WHEREin a TTL. A condition that keeps some rows of a record and deletes others causes the same problem. - Put the column in
toDateTime().DateTime64 + INTERVALgives aDateTime64. ClickHouse refuses it in a TTL withBAD_TTL_EXPRESSION.
A table without a TTL keeps its rows. The agent, workflow, and secret audit tables are audit trails and have no TTL.
Clusters¶
Each table engine in schemata.toml starts with the {replicated}
placeholder. When CLICKHOUSE_CLUSTER_NAME is set, the placeholder becomes
Replicated and the DDL runs ON CLUSTER. When it is not set, the tables
are single-node tables.
New tables¶
The services do not create ClickHouse tables when they start.
imbi-api setup and imbi-api setup-clickhouse apply schemata.toml.
After you deploy a release that adds a table, run
imbi-api setup-clickhouse. Until you do, a query on the new table fails
with UNKNOWN_TABLE.
Metrics¶
Each producer that connects through imbi.common.iggy counts the messages
it publishes (imbi_iggy_published_total) and the failures
(imbi_iggy_publish_errors_total), by stream and topic. imbi-api also
reports the state of each topic and of its sink consumer group
(imbi_iggy_topic_* gauges). iggy-connect serves its own metrics on port
8081. See Observability.