Event publishing
The event publisher sends catalog change events to another system as they happen, so a downstream catalog, a data quality service or an audit pipeline can follow tables being created, altered and dropped without polling Gravitino. It delivers to a Kafka topic or to an HTTP endpoint, and every event is the same CloudEvents document either way.
The publisher is an event listener. Its source, build and tests are in the gravitino-event-publisher repository under the Apache License 2.0. It is not part of the Datastrato Enterprise image, so an install that uses it runs a server image with the publisher's jar added.
Quick start
The example publishes to a Kafka topic over SASL_SSL with SCRAM credentials. It uses the
RELEASE, NAMESPACE and LICENSE_FILE variables from Install with Helm.
-
Build the publisher. The build needs JDK 17 or later and runs the publisher's HTTP smoke test.
Build the publisher jargit clone https://github.com/markhoerth/gravitino-event-publisher.git
cd gravitino-event-publisher
./gradlew build
ls build/libs/gravitino-event-publisher-0.1.0.jar -
Build a server image that carries it. The server loads every jar in
/opt/gravitino/libs. Start from the Datastrato Enterprise image of the version you run, and push the result to a registry your cluster pulls from.DockerfileFROM {registry}/datastrato/enterprise:{version}
COPY build/libs/gravitino-event-publisher-0.1.0.jar /opt/gravitino/libs/Build and push the imagedocker build -t {your_registry}/datastrato/enterprise-events:{version} .
docker push {your_registry}/datastrato/enterprise-events:{version} -
Store the Kafka credentials in a Secret. The publisher reads them from a mounted file, so no password appears in the server's configuration.
credentials.propertiesjaas=org.apache.kafka.common.security.scram.ScramLoginModule required username="{kafka_user}" password="{kafka_password}";
truststore_password={truststore_password}Create the credentials Secretkubectl -n "$NAMESPACE" create secret generic gravitino-kafka-credentials \
--from-file=credentials.properties \
--from-file=truststore.p12={kafka_truststore_file} -
Configure the release. Add the publisher to the listener list, point it at the topic, mount the Secret, and switch the server to the new image.
my-values.yamlglobal:
imageSets:
gravitino:
image:
registry: {your_registry}
repository: datastrato/enterprise-events
tag: "{version}"
gravitino:
eventListener:
names: "search,dashboard-metrics,eventPublisher"
additionalConfigItems:
gravitino.eventListener.eventPublisher.class: "com.datastrato.gravitino.eventpublisher.EventPublisherListener"
gravitino.eventListener.eventPublisher.source: "https://gravitino.example.com/prod"
gravitino.eventListener.eventPublisher.transport: "kafka"
gravitino.eventListener.eventPublisher.bootstrapServers: "{broker_1}:9093,{broker_2}:9093"
gravitino.eventListener.eventPublisher.topic: "gravitino-catalog-events"
gravitino.eventListener.eventPublisher.kafka.security.protocol: "SASL_SSL"
gravitino.eventListener.eventPublisher.kafka.sasl.mechanism: "SCRAM-SHA-512"
gravitino.eventListener.eventPublisher.kafka.config.providers: "file"
gravitino.eventListener.eventPublisher.kafka.config.providers.file.class: "org.apache.kafka.common.config.provider.FileConfigProvider"
gravitino.eventListener.eventPublisher.kafka.sasl.jaas.config: "${file:/etc/gravitino/kafka/credentials.properties:jaas}"
gravitino.eventListener.eventPublisher.kafka.ssl.truststore.location: "/etc/gravitino/kafka/truststore.p12"
gravitino.eventListener.eventPublisher.kafka.ssl.truststore.type: "PKCS12"
gravitino.eventListener.eventPublisher.kafka.ssl.truststore.password: "${file:/etc/gravitino/kafka/credentials.properties:truststore_password}"
extraVolumes:
- name: kafka-credentials
secret:
secretName: gravitino-kafka-credentials
extraVolumeMounts:
- name: kafka-credentials
mountPath: /etc/gravitino/kafka
readOnly: trueEach
${file:...}reference reads one key fromcredentials.propertiesin the mounted Secret. Keepsearchanddashboard-metricsin the listener list; replacing the list instead of extending it removes them. -
Apply it and confirm the publisher started. Run the install command from Install with Helm again. The server log reports the transport the publisher chose.
Find the publisher in the server logkubectl -n "$NAMESPACE" exec deploy/"$RELEASE-gravitino" -c gravitino -- \
sh -c 'grep -E "event publisher (initialised|started)|Kafka transport initialised" /opt/gravitino/logs/$HOSTNAME/gravitino-server.log' -
Create a table and read the topic. Each change arrives as one record.
client.propertiesholds your own consumer's connection settings.Read the topickafka-console-consumer.sh --bootstrap-server {broker_1}:9093 \
--consumer.config client.properties \
--topic gravitino-catalog-events --from-beginning --property print.key=true
What is published
Events
By default the publisher publishes changes to tables, schemas, filesets and models: objects created,
altered, dropped or purged, and model versions linked. The operations property narrows or widens
the list. Operations that failed are left out, so a failed create never tells a consumer that an
object exists. Read traffic such as loading or listing a table is not in the default list and should
stay out of it, since it would bury the changes.
A created or altered table carries the table's full state after the operation: its columns with
their types and comments, its properties, its owner and who last changed it. A consumer can apply the
state as given, with no merge, which makes a replayed event harmless. An alter also carries the list
of changes for a consumer that wants them. A dropped table carries no state, because the server's
drop event has none, so a consumer deletes by the event's subject. Changes to schemas, filesets and
models arrive as notices that name the object and the change, without its state; a consumer that
needs the state loads the object from the Gravitino REST API.
Format
Every event is a CloudEvents 1.0 JSON document.
{
"specversion": "1.0",
"id": "a34cc863-d442-476c-8cc9-576acfc4dc52",
"source": "https://gravitino.example.com/prod",
"type": "org.apache.gravitino.table.created",
"time": "2026-10-18T07:23:40.372Z",
"datacontenttype": "application/json",
"subject": "enterprise.analytics.sales.daily_orders",
"sequence": 1,
"actor": "dana.okafor@example.com",
"eventsource": "gravitino_server",
"metalake": "enterprise",
"catalog": "analytics",
"data": {
"entityType": "table",
"fqn": "enterprise.analytics.sales.daily_orders",
"columns": [
{"name": "customer_id", "type": "string", "nullable": false, "position": 0}
],
"owner": "analytics-eng"
}
}
| Attribute | Content |
|---|---|
type | The object and the change, such as org.apache.gravitino.table.created or org.apache.gravitino.schema.dropped. |
subject | The object's full name, such as {metalake}.{catalog}.{schema}.{table}. Key consumer state on this. |
source | The source property, which identifies the server. Consumers fed by more than one server deduplicate on source and id. |
actor | The user who made the change, from the request's credentials. |
sequence | A counter that increases with each event the server publishes, starting again at 1 when the server restarts. |
data | A table's state after the operation. Absent on a drop and on changes to other objects. |
Kafka records
The record value is the event document. The record key is the event's subject, so every event for
one object lands on the same partition in the order the changes were made, and a consumer applying
the state never sees an older version after a newer one. Each record carries a content-type header
of application/cloudevents+json; charset=UTF-8.
HTTP requests
Each event is one POST with the document as its body, a Content-Type of
application/cloudevents+json; charset=utf-8, and an Idempotency-Key header holding the event
id, which stays the same across retries of one event.
Configuration
Every property is set under gravitino.additionalConfigItems in my-values.yaml, prefixed with
gravitino.eventListener.eventPublisher., as a quoted string. The name eventPublisher is the one
added to gravitino.eventListener.names; any name works if it matches everywhere.
All transports
| Property | Description | Default |
|---|---|---|
class | com.datastrato.gravitino.eventpublisher.EventPublisherListener. Required. | (none) |
transport | kafka or http. | http |
source | A stable URI for this server, unique per server and unchanged across restarts. | (none) |
operations | Comma-separated operation types to publish, in lower case, such as create_table,alter_table,drop_table. | Table, schema, fileset and model changes |
includeFailures | Also publish operations that failed. | false |
Kafka
| Property | Description | Default |
|---|---|---|
bootstrapServers | Comma-separated brokers. Required. | (none) |
topic | The topic to write to. Required. The publisher does not create it. | (none) |
kafka.* | Passed to the Kafka producer with the kafka. prefix removed. TLS, SASL and config providers are set here. | (none) |
deliveryTimeoutMs | How long the producer keeps retrying one event before it is given up. | 30000 |
requestTimeoutMs | How long the producer waits for one broker response. | 10000 |
maxBlockMs | How long sending waits for broker metadata when no broker answers. | 5000 |
The connection must authenticate. The publisher refuses to start unless kafka.security.protocol is
SASL_SSL with kafka.sasl.jaas.config set, or SSL with a client keystore in
kafka.ssl.keystore.location. The producer writes with acks=all, retries without duplicating a
record, and compresses with gzip.
HTTP
| Property | Description | Default |
|---|---|---|
url | The https endpoint to POST to. Required. | (none) |
auth.type | oauth2, mtls or bearer. Required. | (none) |
auth.oauth2.tokenEndpoint, auth.oauth2.clientId, auth.oauth2.clientSecret, auth.oauth2.scope | OAuth client credentials the publisher exchanges for a token. | (none) |
auth.mtls.keyStorePath, auth.mtls.keyStorePassword, auth.mtls.trustStorePath, auth.mtls.trustStorePassword | Client certificate for mutual TLS. | (none) |
auth.bearer.token | A static token, for an endpoint that accepts nothing else. | (none) |
header.{name} | A header sent on every request. | (none) |
maxAttempts | Attempts per event before it is given up. | 4 |
initialBackoffMs, maxBackoffMs | Wait before the first retry, doubling up to the maximum. | 250, 5000 |
connectTimeoutMs, requestTimeoutMs | Connection and per-request timeouts. | 5000, 10000 |
The HTTP transport reads its credentials only from these properties, which the chart writes into the server's ConfigMap in plain text. Restrict who can read ConfigMaps in the release's namespace when you use it. Kafka avoids this through the file references in the quick start.
Delivery
Delivery is at most once. The publisher runs on a queue of its own, so a slow or unreachable destination never slows a catalog operation, but an event can be lost in three ways:
- The queue is full when the event arrives. The server drops it and logs a warning; see
Queue behavior. Raise
gravitino.eventListener.queueCapacityif the warning appears. - The server stops with events still queued.
- Delivery keeps failing until its retries run out. The event is then written to the server log under
the logger
com.datastrato.gravitino.eventpublisher.DeadLetter, with its full payload. Alert on that logger.
A consumer that must hold a complete copy reconciles against the Gravitino REST API from time to time, and treats the event stream as the fast path rather than the record of truth.