Het cluster beëindigt zich, en het bewijs verdwijnt mee
Als een Databricks job mislukt, is de eerste reflex om de driver log te controleren. Als het cluster was geconfigureerd met autoscaling en de job een job cluster gebruikte (geen interactive cluster), dan beëindigt dat cluster zich binnen enkele minuten na de failure. De driver log verdwijnt dan mee.
Databricks bewaart driver logs van beëindigde clusters, maar slechts 30 dagen via de cluster UI, en alleen als je naar de specifieke cluster-ID navigeert. Het Jobs API endpoint 2.1/jobs/runs/get-output geeft het foutbericht en een afgekapte notebook output terug, niet de volledige stderr. Als de failure een Java stack trace was, begraven 400 regels diep in de driver log (een OOM bij een shuffle spill, een Delta ACID conflict, een network timeout naar een extern catalog), dan vertelt die afgekapte output je bijna niets.
Cluster event logs leggen lifecycle events vast: DRIVER_HEALTHY, DRIVER_NOT_RESPONDING, AUTOSCALING, INIT_SCRIPTS_FAILED. Die overleven het beëindigen van het cluster en zijn toegankelijk via de 2.0/clusters/events API. Maar ze beschrijven infrastructuur, geen applicatielogica. Je weet dat de driver niet meer reageerde. Niet of dat door een slechte UDF, een skewed join of een verlopen credential kwam.
De kloof tussen 'de job mislukte' en 'hier is waarom' is waar het grootste deel van de debugging tijd naartoe gaat. Teams die driver logs opslaan op een persistente locatie (DBFS, cloud storage via een custom log4j appender of cluster log delivery geconfigureerd in de cluster spec) dichten die kloof. Teams die dat niet doen staan de Spark UI te refreshen op een cluster dat al niet meer bestaat.
Voor elke productie job moet de cluster spec een cluster_log_conf bevatten die naar een S3, ADLS of GCS pad wijst. Dat is één regel configuratie die uren forensisch werk bespaart. Zonder die configuratie onderzoek je een plaats delict die 10 minuten na het incident al gesloopt wordt.
Job run metadata vertelt je wat er is gebeurd, niet wat het heeft kapotgemaakt
De Databricks Jobs API geeft gestructureerde metadata terug voor elke run: run_id, state, start_time, end_time, error_code en result_state. Het error_code veld gebruikt waarden als INTERNAL_ERROR, INVALID_PARAMETER_VALUE, RESOURCE_DOES_NOT_EXIST en RUN_EXECUTION_ERROR. Dat zijn brede categorieën. RUN_EXECUTION_ERROR dekt alles van een syntaxfout in je notebook tot een Delta table schema mismatch tot een tijdelijke cloud storage timeout.
Het echte probleem is niet de foutclassificatie, maar het ontbreken van dependency context. Databricks Workflows ondersteunt task dependencies binnen één job (taak A moet klaar zijn vóór taak B), maar heeft geen native concept voor cross-job dependencies. Als Job A schrijft naar catalog.schema.silver_orders en Job B leest daarvan, verbindt niets in de Jobs API die twee jobs. Job B draait op schema ongeacht of Job A is geslaagd, mislukt of nooit gestart.
Je kunt run history programmatisch opvragen via 2.1/jobs/runs/list gefilterd op job_id, maar failures correleren over jobs heen vereist dat je die grafiek zelf bouwt. Sommige teams gebruiken Delta table properties (TBLPROPERTIES) om last-write timestamps te stempelen en die te controleren aan het begin van downstream jobs. Anderen gebruiken Unity Catalog lineage, dat read/write-relaties bijhoudt op tabelniveau. Maar alleen voor operaties die via Unity Catalog lopen, en met een vertraging die het ongeschikt maakt voor real-time alerting.
Het praktische gevolg: een job mislukt om 02:47 uur, vier downstream jobs draaien succesvol op stale data tussen 03:00 en 04:00 uur, en de eerste mens die het opmerkt is een stakeholder om 09:00 uur die zich afvraagt waarom het omzetcijfer van gisteren niet is veranderd. De failure werd gedetecteerd. De impact niet.
Stil succes op stale input is erger dan een luidruchtige failure
Een mislukte job geeft een Slack alert (als je die hebt geconfigureerd). Een job die slaagt op stale data geeft niets. Dit is het scenario dat teams hun geloofwaardigheid bij stakeholders kost.
Neem een veelvoorkomend Databricks pipeline patroon: een ingestion job laadt elke uur ruwe data in een Bronze Delta table, een transformatie job leest Bronze en schrijft Silver elke twee uur en een dbt of notebook-gebaseerde job bouwt Gold aggregaten op een dagelijks schema. Als de ingestion job om 01:00 uur mislukt, leest de transformatie job om 02:00 uur dezelfde Bronze data die die om middernacht heeft gelezen. Die slaagt. Het schema is geldig. De aantallen rijen zien er normaal uit. Het enige signaal dat er iets mis is, is de max(event_timestamp) in de Bronze table, die niemand programmatisch controleert.
Freshness assertions inbouwen in je jobs is eenvoudig maar zelden standaard gedaan. Een simpele pre-flight check bevraagt DESCRIBE HISTORY catalog.schema.bronze_orders LIMIT 1 en vergelijkt de timestamp kolom met een drempelwaarde. Als de laatste schrijfactie meer dan 90 minuten geleden was, mislukt de job snel met een duidelijke melding, in plaats van stil stale output te produceren.
Het DESCRIBE HISTORY commando van Delta Lake geeft het volledige transactielogboek terug: elke commit, de timestamp, het operatietype en de metrics (geschreven rijen, toegevoegde bestanden). Dat is het goedkoopste freshness signaal dat beschikbaar is. Het vereist geen externe tooling, draait in milliseconden en werkt op elke Delta table in Unity Catalog of de legacy Hive metastore.
Het moeilijkere probleem is het bepalen van drempelwaarden. Een hourly table die 91 minuten stale is, kan prima zijn tijdens een bekend onderhoudsvenster en catastrofaal tijdens de kwartaalafsluiting. Statische drempelwaarden genereren ruis. Dynamische drempelwaarden op basis van historische schrijffrequentie vereisen dat je die geschiedenis ergens bijhoudt, wat je terugbrengt bij het observability probleem.
Cluster event logs onthullen infrastructure failures die job metadata verbergt
Als een job mislukt met INTERNAL_ERROR en de driver log niets nuttigs zegt, zijn cluster event logs de volgende plek om te kijken. Het 2.0/clusters/events endpoint geeft events terug voor een gegeven cluster_id met pagination support. De events die er het meest toe doen bij debugging zijn INIT_SCRIPTS_FAILED, DRIVER_NOT_RESPONDING, SPARK_EXCEPTION, DBFS_DOWN en METASTORE_DOWN.
INIT_SCRIPTS_FAILED is bijzonder veelvoorkomend en bijzonder ondoorzichtig. Als je cluster init scripts gebruikt om Python packages te installeren, networking te configureren of storage te mounten, zorgt een failure in een van die scripts ervoor dat het cluster in een TERMINATED staat terechtkomt met termination_reason.code = INIT_SCRIPT_FAILURE. Het termination_reason.parameters veld bevat het scriptpad en de exit code, maar niet de stderr output van het script. Je hebt het cluster log delivery pad (cluster_log_conf) nodig om te zien wat er werkelijk mis is gegaan. Als je dat niet hebt geconfigureerd, is die output verdwenen.
Een ander veelvoorkomend patroon: DRIVER_NOT_RESPONDING gevolgd door TERMINATED met reden DRIVER_UNREACHABLE. Dit gebeurt als de driver node geen geheugen meer heeft, vaak doordat een collect() aanroep of een broadcast join te veel data op één node heeft getrokken. De Spark UI (als het cluster nog actief is) toont het geheugengebruik onder de Executors tab. Maar voor een beëindigd job cluster heb je de GC logs of de Spark event logs nodig. Beide vereisen cluster log delivery om bewaard te blijven.
De Jobs API, de Clusters API en de cluster event log bevatten elk een stuk van het verhaal. Geen enkel endpoint geeft je het volledige beeld. Teams die een post-failure diagnostische routine scripten (run output, cluster events en persistente logs samengebracht in één incident record) herstellen sneller dan teams die door drie verschillende UI tabs klikken om te reconstrueren wat er is gebeurd.
MetricSign maakt verbinding met de Databricks Jobs API en correleert job failures met de downstream pipeline staat, groepeert gerelateerde incidents en toont welke downstream consumers op stale data hebben gedraaid na een upstream failure. In plaats van de impact om 09:00 uur te ontdekken, zie je die om 02:48 uur. Één minuut na de root cause.
Het spoor bouwen: een praktische checklist voor productie jobs
Het verschil tussen een onderzoek van 10 minuten en een van 2 uur zit in of je observability hebt geconfigureerd vóórdat de failure plaatsvond. Dit zijn de zaken die ertoe doen.
Ten eerste: zet cluster log delivery aan op elke job cluster. Voeg in de job JSON spec cluster_log_conf toe met een bestemmingspad in je cloud storage. Databricks schrijft driver logs, executor logs en init script output naar dat pad binnen 5 minuten na het beëindigen van het cluster. De opslagkosten zijn verwaarloosbaar. Een typische job produceert 1-10 MB aan logs per run.
Ten tweede: gebruik structured streaming of Delta Change Data Feed voor afspraken tussen jobs, in plaats van impliciete table reads. Als Job B afhankelijk is van de output van Job A, moet Job B controleren of die output vers is voordat die verder gaat. Een SQL check van drie regels tegen DESCRIBE HISTORY is voldoende. Laat de job expliciet mislukken in plaats van stil stale resultaten te produceren.
Ten derde: tag je job runs met metadata. De Jobs API ondersteunt idempotency_token en je kunt eigen parameters doorgeven via notebook_params of python_params. Door elke run te voorzien van een correlation ID, de git commit SHA van het notebook en de verwachte input table versies, versnelt post-mortem onderzoek aanzienlijk.
Ten vierde: bewaar run outputs langer dan de standaard 30 dagen. Het 2.1/jobs/runs/export endpoint laat je notebook output als HTML ophalen. Een geplande opschoningstaak die deze archiveert naar cloud storage kost bijna niets en heeft meerdere teams gered van het verlies van de enige leesbare registratie van wat een mislukte run daadwerkelijk heeft geproduceerd.
Tot slot: script je diagnostische routine. Als een job mislukt, haal dan automatisch de run output op, de cluster events voor de bijbehorende cluster_id en de laatste 100 regels van de driver log uit cloud storage. Stel die samen tot één document. Het doel is niet het oplossen automatiseren. Het doel is de 45 minuten van tab-wisselen en API-aanroepen elimineren die plaatsvinden voordat iemand überhaupt begint na te denken over het werkelijke probleem.