Event Tables in Snowflake: The Missing Link in Data Observability
Ever since Snowflake pivoted from being “just a data warehouse” to a “data cloud” capable of running complex Python, Java, and Scala code via Snowpark, a massive gap emerged: Observability.
For years, if a stored procedure failed, developers had to resort to “print-statement debugging” by inserting rows into a manual log table — a process that was clunky, unscalable, and difficult to manage. Enter Snowflake Event Tables.
Get Nazeer Syed’s stories in your inbox
Join Medium for free to get updates from this writer.
In this blog, we’ll dive into why Event Tables are a game-changer, the specific problems they solve, and how you can implement them.
The Real Need: Why “Manual Logging” Failed
Before Event Tables, developers used standard permanent tables to store logs. This approach had three major flaws:
- Transactional Overhead: If your main procedure failed and rolled back, your log entries often rolled back with it, leaving you with zero visibility into the failure.
- Code Bloat: You had to manually write
INSERTstatements inside everytry-catchblock. - Lack of Standardization: Every team had a different schema for their “Log_Table,” making account-wide monitoring impossible.
Snowflake Event Tables solve this by providing a unified, serverless, and asynchronous telemetry pipeline.
What Problems Do Event Tables Solve?
- Decoupled Logging: Logging is handled outside your main transaction. If your code crashes, the logs still persist.
- Standardization: They follow the OpenTelemetry standard. This means your logs, spans, and traces are structured identically across Python, Java, and SQL.
- Centralized Observability: You can set one Event Table for your entire account. All logs from every UDF and Procedure flow into one place.
Real-World Examples
1. Logging from a Python Stored Procedure
Instead of building a custom table, you use the standard Python logging library. Snowflake intercepts these calls and routes them to the Event Table.
CREATE OR REPLACE FUNCTION process_customer_data(customer_id INT)
RETURNS TABLE (processed_data VARIANT)
LANGUAGE PYTHON
RUNTIME_VERSION = '3.8'
PACKAGES = ('snowflake-snowpark-python')
HANDLER = 'CustomerProcessor'
AS $$
import logging
from snowflake.snowpark import Session
# Configure logging to Event Table
logger = logging.getLogger("customer_processor")
class CustomerProcessor:
def process(self, session: Session, customer_id: int):
try:
logger.info(f"Processing customer: {customer_id}")
# Your complex business logic here
customer_data = session.table("customers").filter(
col("customer_id") == customer_id
).collect()
if not customer_data:
logger.warning(f"No data found for customer: {customer_id}")
return []
# Transform data
result = transform_logic(customer_data)
logger.info(f"Successfully processed customer: {customer_id}")
return result
except Exception as e:
logger.error(f"Failed processing customer {customer_id}: {str(e)}")
raise
$$;Now query your Event Table to see exactly what happened:
SELECT
TIMESTAMP,
RECORD['severity_text'] as LOG_LEVEL,
RECORD['message'] as MESSAGE,
RECORD['attributes'] as ATTRIBUTES
FROM my_event_table
WHERE RECORD_TYPE = 'LOG'
AND TIMESTAMP > DATEADD(hour, -1, CURRENT_TIMESTAMP())
AND RECORD['severity_text'] = 'ERROR'
ORDER BY TIMESTAMP DESC;Build a real-time monitoring dashboard that shows pipeline health:
-- Count errors by pipeline in the last hour
SELECT
RECORD['attributes']['pipeline_name']::STRING as PIPELINE,
COUNT(*) as ERROR_COUNT,
ARRAY_AGG(DISTINCT RECORD['message']::STRING) as ERROR_MESSAGES
FROM my_event_table
WHERE RECORD_TYPE = 'LOG'
AND RECORD['severity_text'] = 'ERROR'
AND TIMESTAMP > DATEADD(hour, -1, CURRENT_TIMESTAMP())
GROUP BY PIPELINE
ORDER BY ERROR_COUNT DESC;Track how long each component of your distributed data pipeline takes:
-- Analyze trace spans to identify bottlenecks
SELECT
RECORD['name']::STRING as OPERATION,
AVG(RECORD['end_time']::NUMBER - RECORD['start_time']::NUMBER) as AVG_DURATION_MS,
MAX(RECORD['end_time']::NUMBER - RECORD['start_time']::NUMBER) as MAX_DURATION_MS,
COUNT(*) as EXECUTION_COUNT
FROM my_event_table
WHERE RECORD_TYPE = 'SPAN'
AND TIMESTAMP > DATEADD(day, -7, CURRENT_TIMESTAMP())
GROUP BY OPERATION
ORDER BY AVG_DURATION_MS DESC;Best Practices
- One Table Per Account: While you can create multiple event tables, only one can be active at the account level. Set it and forget it using:
ALTER ACCOUNT SET EVENT_TABLE = my_db.public.my_events; - Use Log Levels Strategically: Use
INFOfor production andDEBUGorTRACEonly during active troubleshooting to save on storage and processing costs. - Alerting: Combine Event Tables with Snowflake Alerts. For example, set an alert to email you whenever a row with
SEVERITY_TEXT = 'ERROR'is inserted.
Limitations to Keep in Mind
- Payload Size: Individual log messages and trace events are limited to 1 MB.
- Append-Only: You cannot
UPDATErows in an Event Table. You can onlyDELETEorTRUNCATE. - Latency: There is a slight delay (usually a few minutes) between the code execution and the log appearing in the table.
Pricing: What Does It Cost?
Event Tables use a Serverless Compute model.
- Compute: You don’t pay for a virtual warehouse to “insert” the logs. Instead, Snowflake charges based on a serverless multiplier (roughly 1.25 credits per hour of background processing).
- Storage: You pay standard Snowflake storage rates for the data held in the table.
- Monitoring Costs: You can track your spend using the
EVENT_USAGE_HISTORYview in theACCOUNT_USAGEschema.
Conclusion
Event Tables are the “black box flight recorder” for your Snowflake environment. They move logging from a manual chore to a built-in platform capability. If you are running production-grade Snowpark apps or complex stored procedures, setting up an Event Table isn’t just a “nice-to-have” — it’s a requirement for professional data engineering.
Found this useful? 👏 Clap it so that more people can find it, and follow me on Linkedin for more content on data engineering and AI. See you there.








