Airflow Logging, Monitoring & Alerting
A pipeline you canโt observe when it fails is barely better than no pipeline at all. Airflowโs logging, retry, and alerting mechanisms are what turn โa task failed at 3 a.m.โ from a silent, undiscovered problem into a page someone actually receives โ and remote logging in particular becomes a hard requirement, not a nice-to-have, once you move beyond LocalExecutor.
Where Task Logs Live by Default
Each task instance writes its own log file, stored locally on whatever machine actually ran it, under a structured path (dag_id/task_id/execution_date/attempt_number.log). This works fine for LocalExecutor on a single machine โ the webserver can just read the file directly. It breaks down immediately for CeleryExecutor or KubernetesExecutor: the webserver has no direct filesystem access to whichever worker or ephemeral pod actually ran the task, especially once that pod has already terminated.
Remote Logging: Required for Distributed Executors
# airflow.cfg[logging]remote_logging = Trueremote_base_log_folder = s3://my-airflow-logs/logsremote_log_conn_id = aws_defaultWith remote logging configured (S3, GCS, or Elasticsearch are the common choices), every task pushes its logs to the remote store on completion, and the webserver fetches from there instead of relying on direct filesystem access โ this is what makes logs actually viewable in the UI once tasks run on ephemeral Kubernetes pods or a distributed Celery worker pool. Skipping this on KubernetesExecutor specifically means losing task logs entirely the moment a pod terminates.
Retries: The First Line of Defense
from datetime import timedelta
default_args = { "retries": 3, "retry_delay": timedelta(minutes=5), "retry_exponential_backoff": True, "max_retry_delay": timedelta(minutes=30),}retries and retry_delay are set per-task (or via default_args applied DAG-wide) and handle the extremely common case of transient failures โ a flaky network call, a momentarily unavailable API, a database connection pool briefly exhausted โ without any human intervention. retry_exponential_backoff is worth enabling on anything hitting external services, so retries space themselves out rather than hammering a struggling system at a fixed interval.
SLAs: Flagging Tasks That Are Running Too Long
task = PythonOperator( task_id="critical_transform", python_callable=transform, sla=timedelta(hours=1),)An SLA doesnโt stop or fail a task โ it fires an SLA-miss event (visible in the UI, and hookable into alerting) if the task hasnโt completed within the specified duration relative to the DAGโs expected schedule. This is distinct from a timeout (which actually kills the task); an SLA is purely an observability/alerting signal for โthis is taking longer than it should,โ useful for catching gradual performance degradation before it becomes an outright failure.
Failure Callbacks and Alerting
def notify_slack_on_failure(context): from airflow.providers.slack.hooks.slack_webhook import SlackWebhookHook hook = SlackWebhookHook(slack_webhook_conn_id="slack_alerts") hook.send(text=f"Task {context['task_instance'].task_id} failed in {context['dag'].dag_id}")
default_args = { "on_failure_callback": notify_slack_on_failure,}on_failure_callback (and its siblings on_success_callback, on_retry_callback) fire arbitrary Python code โ commonly a Slack/PagerDuty/email notification โ whenever a task instance ends in that state. Setting this via default_args applies it DAG-wide without repeating the wiring on every task individually.
Metrics and External Monitoring
Beyond built-in logging and callbacks, Airflow exposes StatsD-compatible metrics (statsd_on = True in airflow.cfg) โ task duration, queue depth, scheduler heartbeat health, DAG run counts โ that feed into standard observability stacks (Prometheus/Grafana, Datadog) for dashboards and threshold-based alerting beyond what per-task callbacks alone provide, especially for instance-wide health signals like scheduler lag.
Common Mistakes
Not configuring remote logging before moving off LocalExecutor, then discovering task logs are simply inaccessible the first time something fails on a Celery worker or Kubernetes pod thatโs already gone.
Setting retries high without retry_exponential_backoff on tasks hitting rate-limited external APIs, effectively hammering the same endpoint repeatedly at a fixed short interval and making the underlying problem worse.
Confusing SLA misses with task failures. An SLA miss means โrunning slower than expected,โ not โfailedโ โ treating every SLA-miss alert as a fire-drill-worthy failure leads to alert fatigue and people eventually ignoring real ones.
Frequently Asked Questions
Do retries count against SLA timing? Yes โ an SLA is measured against the DAGโs expected schedule, so a task that needs several retries before succeeding is more likely to trigger an SLA miss even if each individual attempt was otherwise fast.
Can I set different alerting for different severities of failure? Yes โ on_failure_callback receives the full task/DAG context, so the callback logic itself can route differently (e.g., PagerDuty for tasks tagged critical, Slack-only for everything else) based on task metadata.
Is StatsD required for basic monitoring? No โ logs and failure callbacks alone cover a lot of ground; StatsD/Prometheus integration is most valuable once you need instance-wide health dashboards and trend analysis beyond individual task/DAG alerting.
Summary
Logging, retries, SLAs, and failure callbacks form Airflowโs observability stack, and remote logging specifically stops being optional the moment tasks run somewhere other than the schedulerโs own machine. Retries absorb routine transient failures automatically; SLAs and failure callbacks are what turn โsilently failed at 3 a.m.โ into โsomeone got pagedโ โ configure both deliberately rather than discovering their absence during an actual incident.