id: pinot-ingestion-freshness-sentinel
namespace: company.team
description: |
Measure Apache Pinot real-time ingestion freshness directly from the data
itself, classify the result HEALTHY, STALE, or EMPTY_TABLE, and alert
Slack when a streaming consumer has stalled or a table has stopped
receiving rows.
triggers:
- id: periodic_freshness_check
type: io.kestra.plugin.core.trigger.Schedule
description: "Checks every 10 minutes by default - real-time ingestion lag on a
stalled consumer compounds quickly, so this needs a tight cadence. Shipped
disabled; enable it once pinot_broker_url and pinot_table point at a real
cluster and a manual run has been verified."
cron: "*/10 * * * *"
disabled: true
inputs:
pinot_broker_url: "jdbc:pinot://localhost:9000"
pinot_table: "airlineStats"
timestamp_column: "__time"
max_lag_minutes: "30"
inputs:
- id: pinot_broker_url
type: STRING
displayName: "Pinot Broker JDBC URL"
defaults: "jdbc:pinot://localhost:9000"
description: "JDBC URL of the Apache Pinot broker, in the form
jdbc:pinot://host:port, used by the io.kestra.plugin.jdbc.pinot.Query
task."
- id: pinot_table
type: STRING
displayName: "Target Pinot Table"
defaults: "airlineStats"
description: "Pinot table audited for ingestion freshness. Defaults to
airlineStats, the sample table loaded by the apachepinot/pinot QuickStart
batch image."
- id: timestamp_column
type: STRING
displayName: "Event Time Column"
defaults: "__time"
description: "Column holding the event timestamp in epoch milliseconds, used to
compute ingestion lag. Must match the table's declared timeColumnName if
__time does not resolve for your schema."
- id: max_lag_minutes
type: SELECT
displayName: "Max Acceptable Ingestion Lag (minutes)"
defaults: "30"
values:
- "5"
- "15"
- "30"
- "60"
- "120"
description: "Above this many minutes between now() and the newest row's
timestamp_column, a non-empty table is classified STALE."
tasks:
- id: probe_table_freshness
type: io.kestra.plugin.jdbc.pinot.Query
description: "Single SQL statement (io.kestra.plugin.jdbc.pinot.Query supports
only one statement per task) reading row_count, latest_event_time, and
lag_ms directly from Pinot using its own now() scalar function, which
returns the current system time in epoch milliseconds per Apache Pinot's
own datetime functions documentation."
url: "{{ inputs.pinot_broker_url }}"
username: "{{ secret('PINOT_USERNAME') }}"
password: "{{ secret('PINOT_PASSWORD') }}"
fetchType: FETCH_ONE
sql: |
SELECT count(*) AS row_count, max({{ inputs.timestamp_column }}) AS latest_event_time, (now() - max({{ inputs.timestamp_column }})) AS lag_ms FROM {{ inputs.pinot_table }}
- id: evaluate_row_presence
type: io.kestra.plugin.core.flow.If
description: "EMPTY_TABLE when row_count is 0 - the table exists but no rows
have ever reached it, distinct from rows existing but going stale."
condition: "{{ outputs.probe_table_freshness.row.row_count is defined and
outputs.probe_table_freshness.row.row_count > 0 }}"
then:
- id: evaluate_freshness
type: io.kestra.plugin.core.flow.If
description: "STALE when the newest row is older than max_lag_minutes, converted
to milliseconds for comparison against lag_ms."
condition: "{{ outputs.probe_table_freshness.row.lag_ms is defined and
outputs.probe_table_freshness.row.lag_ms is not null and
outputs.probe_table_freshness.row.lag_ms > (inputs.max_lag_minutes |
number) * 60000 }}"
then:
- id: alert_stale_ingestion
type: io.kestra.plugin.slack.notifications.SlackIncomingWebhook
description: "Always alert on STALE so an on-call engineer sees it without
running the probe query by hand."
url: "{{ secret('SLACK_WEBHOOK_URL') }}"
payload: |
{
"text": "Pinot ingestion freshness sentinel: STALE. Table {{ inputs.pinot_table }} has lag_ms={{ outputs.probe_table_freshness.row.lag_ms }} (threshold {{ inputs.max_lag_minutes }} minutes), row_count={{ outputs.probe_table_freshness.row.row_count }}, latest_event_time={{ outputs.probe_table_freshness.row.latest_event_time }}. Execution {{ execution.id }}."
}
else:
- id: log_freshness_healthy
type: io.kestra.plugin.core.log.Log
message: "HEALTHY: {{ inputs.pinot_table }} lag_ms={{
outputs.probe_table_freshness.row.lag_ms }} (threshold {{
inputs.max_lag_minutes }} minutes), row_count={{
outputs.probe_table_freshness.row.row_count }}."
else:
- id: alert_empty_table
type: io.kestra.plugin.slack.notifications.SlackIncomingWebhook
description: "EMPTY_TABLE means count(*) returned 0 - no stream consumer has
ever written to this table, or pinot_table/timestamp_column point at
the wrong place."
url: "{{ secret('SLACK_WEBHOOK_URL') }}"
payload: |
{
"text": "Pinot ingestion freshness sentinel: EMPTY_TABLE. {{ inputs.pinot_table }} has row_count=0. Check that the stream consumer is running and that pinot_table and timestamp_column are correct. Execution {{ execution.id }}."
}
- id: log_audit_complete
type: io.kestra.plugin.core.log.Log
description: "Final status line for every run, independent of which branch fired."
message: "Ingestion freshness audit complete for {{ inputs.pinot_table }}.
row_count={{ outputs.probe_table_freshness.row.row_count }}. lag_ms={{
outputs.probe_table_freshness.row.lag_ms | default('N/A') }}."
errors:
- id: alert_execution_failure
type: io.kestra.plugin.slack.notifications.SlackIncomingWebhook
description: "Alert when the flow itself fails outside the handled branches, for
example an unreachable Pinot broker, invalid credentials, or a
table/column that does not exist."
url: "{{ secret('SLACK_WEBHOOK_URL') }}"
payload: |
{
"text": "Pinot ingestion freshness sentinel FAILED in execution {{ execution.id }} for {{ inputs.pinot_table }}. The freshness probe did not complete; investigate before trusting the last known HEALTHY/STALE/EMPTY_TABLE status."
}