Events & logging¶
Every step of a run emits an event on a process-wide bus: runs starting and ending, operations
moving through their lifecycle, data() calls, destination reads and writes, and the messages
assets log. Events are how you observe execution, and what the platform persists.
Receiving events¶
The simplest way is a runner's on_event, which receives only that run's events:
import interloper as il
def on_event(event: il.Event) -> None:
print(event)
result = il.run(il.AsyncRunner(on_event=on_event).run(dag))
ConsoleEventHandler is a ready-made handler that renders events through the standard logging
stack on the interloper.run logger, so they share the format, stream and verbosity of your
other log lines:
from interloper.events import ConsoleEventHandler
result = il.run(il.AsyncRunner(on_event=ConsoleEventHandler()).run(dag))
Failures log at ERROR, cancellations at WARNING, run and operation lifecycle at INFO, and
the high-frequency data() and destination I/O events at DEBUG.
ConsoleEventHandler(json_lines=True) writes raw JSON lines to stdout instead.
To observe everything in the process, subscribe to the bus directly:
il.EventBus.subscribe(on_event) # every event
il.EventBus.subscribe(on_event, event_types=[il.EventType.RUN_FAILED]) # a subset
il.EventBus.unsubscribe(on_event)
Handlers run on a background worker thread in subscription order; an exception in one handler
is isolated from the others. il.EventBus.flush(timeout) blocks until everything queued so far
has been delivered.
The event¶
event.type # EventType
event.timestamp # aware UTC datetime, set by the producer
event.metadata # dict
event.id # stable id, preserved across serialization
Metadata always carries what the producer knows: run_id, component_id, component_kind,
component_key, qualified_key, source_id, partition_or_window, plus message and, on
failures, error and traceback. Destination events add destination_key; log events add
level.
Events serialize with to_dict() / to_json() and parse back with Event.from_dict() /
Event.from_json(). Operation lifecycle events get a deterministic id derived from run,
component and type, so the same logical event produced twice (a child process and its host)
collapses to one when persisted.
Event types¶
| Group | Types |
|---|---|
| Run | RUN_STARTED, RUN_COMPLETED, RUN_FAILED |
| Operation | OPERATION_QUEUED, OPERATION_STARTED, OPERATION_COMPLETED, OPERATION_FAILED, OPERATION_CANCELED |
| Asset data | ASSET_DATA_STARTED, ASSET_DATA_COMPLETED, ASSET_DATA_FAILED |
| Destination I/O | DEST_READ_STARTED, DEST_READ_COMPLETED, DEST_READ_FAILED, DEST_WRITE_STARTED, DEST_WRITE_COMPLETED, DEST_WRITE_FAILED |
| Backfill | BACKFILL_STARTED, BACKFILL_COMPLETED, BACKFILL_FAILED |
| Hooks | HOOK_FIRED, HOOK_FAILED |
| User | LOG |
Backfill and hook events are emitted by the platform, not by the core runners. The full table with metadata per type is in Event types.
Logging from assets¶
context.logger emits LOG events attributed to the asset, with the standard level names:
@il.asset
def ads_stats(self, context: il.ExecutionContext) -> list[dict]:
context.logger.info("Requesting report")
rows = fetch()
context.logger.debug(f"{len(rows)} rows")
return rows
Because these are events, they reach on_event, the console handler, the persisted run log and
any other subscriber, not just a local logger. EventLogger(component_key, metadata) from
interloper.events builds the same logger for code outside an asset. Emitting an arbitrary
event is il.EventBus.emit(il.EventType.LOG, metadata={...}).
Events across processes¶
Container-based runners run assets in child processes that have their own bus. With
INTERLOPER_EVENTS_TO_STDERR=true, the interloper run command subscribes a
StderrEventHandler that writes each event as an @EVENT:{json} line on stderr; the host
parses those lines with Event.from_log_line() and re-emits them with EventBus.emit_event(),
preserving ids and timestamps. Subscribers on the host therefore see one unified stream.
Metrics¶
The built-in OpenTelemetry metrics are computed from these events by a bus subscriber, so they cost nothing on the execution path. See Telemetry.