All posts

Logging a Spark stream: the job you cannot restart to debug

A Structured Streaming query logs every micro-batch for as long as it runs, so the volume tracks partition count rather than data volume, and you cannot restart the job to raise a level. Here is the Log4j 2 config that bounds it, the live reconfiguration that avoids the restart, and the Log4j 1 equivalents.

20 min read Spark

TL;DR

  • A streaming query’s log volume is driven by spark.sql.shuffle.partitions, not by how much data arrives. Measured on a stateful query below: 43 lines per micro-batch at 4 partitions, 151 at 16, with identical input.
  • The loudest loggers are not the ones narrating your query. They are StateStore, HDFSBackedStateStoreProvider and CheckpointFileManager, which log once per partition per batch.
  • A batch job can be resubmitted with DEBUG. A streaming job cannot, so set monitorInterval and Log4j 2 will re-read the file on a running query. Verified below: config edited at 13:04:33, first new line at 13:04:36.
  • Console and file appenders on a job with no end need a rollover policy and a retention cap, or the container’s disk is the retention policy.
  • StreamingQueryListener gives you one structured line per batch with the batch id, input rows, state rows and watermark, which is what you were grepping the other 150 lines to reconstruct.

Why does a streaming job break the batch logging playbook?

The usual way to debug a Spark job is to resubmit it with a custom Log4j config and DEBUG on two packages. That advice is sound, and it is written up for batch work in a Spark JVM playbook. Three things about a streaming query make it insufficient.

The run has no end. A batch job’s log is bounded by the job. A streaming query writes until someone stops it, so “how big does this file get” has no answer until you impose one.

You cannot resubmit it. Restarting a streaming query to turn on DEBUG means an outage, a checkpoint recovery, and the loss of whatever transient condition you were trying to observe. The interesting failures are the ones that show up after six hours, and they do not survive a restart.

The volume is structural. A batch job logs roughly in proportion to its stages. A streaming query logs the same lines again every trigger interval, and the count of those lines is set by the shape of the query, not by the traffic.

flowchart LR
    T[Trigger fires] --> O[Read offsets]
    O --> P[Plan micro-batch]
    P --> S[Per-partition state store work]
    S --> C[Checkpoint offsets and commit]
    C --> W[Write progress event]
    W --> T
    S -.->|"one set of log lines<br/>per partition, per batch"| L[(Log file)]
    C -.-> L
    W -.-> L

That loop is the whole problem. Everything below is about deciding what it is allowed to write each time round.

What does a single micro-batch actually log?

Rather than guess, measure. The query is a windowed count over a rate source, a five second tumbling window with a ten second watermark, on a two second trigger, run on Spark 3.5.9:

from pyspark.sql import SparkSession
from pyspark.sql.functions import window, col, count

spark = (SparkSession.builder
         .appName("order-events-logging-demo")
         .config("spark.ui.enabled", "false")
         .config("spark.eventLog.enabled", "false")
         .config("spark.sql.shuffle.partitions", "4")
         .getOrCreate())

events = (spark.readStream.format("rate")
          .option("rowsPerSecond", 200)
          .option("numPartitions", 2)
          .load()
          .withColumnRenamed("value", "order_id"))

orders_per_window = (events
                     .withWatermark("timestamp", "10 seconds")
                     .groupBy(window(col("timestamp"), "5 seconds"))
                     .agg(count("order_id").alias("order_count")))

query = (orders_per_window.writeStream
         .format("console")
         .option("truncate", "false")
         .outputMode("update")
         .trigger(processingTime="2 seconds")
         .queryName("orders_per_window")
         .start())

query.awaitTermination(70)
query.stop()
spark.stop()

Run it with the root logger at WARN and only the streaming package at INFO, so nothing but the query’s own narration is counted:

rootLogger.level = warn
rootLogger.appenderRef.stdout.ref = console

appender.console.type = Console
appender.console.name = console
appender.console.target = SYSTEM_ERR
appender.console.layout.type = PatternLayout
appender.console.layout.pattern = %d{HH:mm:ss} %-5p %c{1} - %m%n

logger.streaming.name = org.apache.spark.sql.execution.streaming
logger.streaming.level = info
spark-submit \
  --conf spark.eventLog.enabled=false \
  --driver-java-options "-Dlog4j.configurationFile=/work/log4j2-probe.properties" \
  /work/steady.py

Counting the lines between the fourth and the ninth committed batch, so startup is excluded, gives this split by logger:

Logger Lines per batch, 4 partitions Lines per batch, 16 partitions
...streaming.state.StateStore 16 64
...streaming.state.HDFSBackedStateStoreProvider 12 48
...streaming.CheckpointFileManager 12 36
...streaming.MicroBatchExecution 2 2
...streaming.WatermarkTracker 1 1
Total 43 151

The input was identical in both runs. Only spark.sql.shuffle.partitions changed, from 4 to 16, and the per-batch log volume more than tripled.

Three of those five loggers are per-partition bookkeeping. StateStore prints that it resolved the coordinator and that a provider instance is active, once per partition. HDFSBackedStateStoreProvider prints the version it retrieved and the version it committed, once per partition. CheckpointFileManager prints a write and a rename for every delta file, which is again one per partition:

INFO StateStore - Retrieved reference to StateStoreCoordinator: ...StateStoreCoordinatorRef@56f649e2
INFO StateStore - Reported that the loaded instance StateStoreProviderId(StateStoreId[ ... partitionId=1 ...]) is active
INFO HDFSBackedStateStoreProvider - Retrieved version 0 of HDFSStateStoreProvider[id = (op=0,part=1), ...] for update
INFO CheckpointFileManager - Writing atomically to .../state/0/1/1.delta using temp file .../.1.delta.6c8dde0d....tmp
INFO CheckpointFileManager - Renamed temp file .../.1.delta.6c8dde0d....tmp to .../state/0/1/1.delta
INFO HDFSBackedStateStoreProvider - Committed version 1 for HDFSStateStore[id=(op=0,part=1), ...] to file .../1.delta

Only two lines per batch come from MicroBatchExecution, which is the logger actually describing your query, and one of those two is the progress JSON.

The arithmetic from there is unkind. At 151 lines per batch and a two second trigger, a query produces 43,200 batches a day, so roughly 6.5 million lines. Nothing in that total scales with how much data you are processing.

The first decision, then, is not which level to use. It is whether the per-partition loggers are on at all.

How do you write a Log4j 2 config for a query with no end?

Spark has used Log4j 2 since 3.3.0. The file is log4j2.properties, the flag is -Dlog4j.configurationFile=, and the syntax is logger.<key>.name rather than log4j.logger.<name>. Start from the log4j2.properties.template in the distribution’s conf/ directory rather than writing one from memory; the property names are not guessable.

Bound the file before anything else

A Console appender on a streaming job writes into whatever the resource manager captured, and that file grows until the disk fills. Use RollingFile with both a triggering policy and a retention cap:

rootLogger.level = warn
rootLogger.appenderRef.rolling.ref = rolling

appender.rolling.type = RollingFile
appender.rolling.name = rolling
appender.rolling.fileName = ${sys:spark.yarn.app.container.log.dir:-/var/log/spark}/streaming.log
appender.rolling.filePattern = ${sys:spark.yarn.app.container.log.dir:-/var/log/spark}/streaming-%d{yyyy-MM-dd}-%i.log.gz
appender.rolling.layout.type = PatternLayout
appender.rolling.layout.pattern = %d{yyyy-MM-dd HH:mm:ss} %-5p %c{1} - %m%n

appender.rolling.policies.type = Policies
appender.rolling.policies.size.type = SizeBasedTriggeringPolicy
appender.rolling.policies.size.size = 64MB
appender.rolling.policies.time.type = TimeBasedTriggeringPolicy
appender.rolling.policies.time.interval = 1

appender.rolling.strategy.type = DefaultRolloverStrategy
appender.rolling.strategy.max = 10

strategy.max is the part people leave out, and it is the part that matters. It caps how many rolled files are kept, so the appender’s total footprint is bounded by size times max plus the active file. Without it, rollover renames files forever and the disk still fills, just more tidily.

The .gz suffix on filePattern is not decoration. Log4j 2 compresses on rollover when the pattern ends in .gz, .zip or .xz, and Spark logs compress well.

Running the demo query with size = 64KB and max = 3, shrunk so rollover happens in a short run, produces exactly the promised set:

-rw-r--r-- 1 root root  5938 Sep 21 13:06 streaming-2026-09-21-1.log.gz
-rw-r--r-- 1 root root  6092 Sep 21 13:06 streaming-2026-09-21-2.log.gz
-rw-r--r-- 1 root root  5889 Sep 21 13:06 streaming-2026-09-21-3.log.gz
-rw-r--r-- 1 root root 48102 Sep 21 13:06 streaming.log

The run rolled more than three times. Three archives is what survived, which is strategy.max doing its job.

Silence the per-partition bookkeeping

With the file bounded, cut what goes into it. These are the three loggers the measurement identified, plus the one you want to keep:

# the per-partition bookkeeping: one set of lines per partition per batch
logger.statestore.name = org.apache.spark.sql.execution.streaming.state
logger.statestore.level = warn

logger.ckpt.name = org.apache.spark.sql.execution.streaming.CheckpointFileManager
logger.ckpt.level = warn

# the query's own narration: two lines per batch, keep it
logger.micro.name = org.apache.spark.sql.execution.streaming.MicroBatchExecution
logger.micro.level = info

# the scheduler, which logs per task and not per batch
logger.dag.name = org.apache.spark.scheduler
logger.dag.level = warn
logger.blockmgr.name = org.apache.spark.storage.BlockManagerInfo
logger.blockmgr.level = warn

That last pair is worth its own note. Leaving rootLogger.level = info and only muting the state loggers is not enough: with the root at INFO, the same run wrote 155 lines per batch, dominated by DAGScheduler, TaskSetManager, Executor and ShuffleBlockFetcherIterator. The scheduler logs per task, and a streaming query runs a full set of tasks every trigger.

The working rule is to set the root to WARN and name the handful of loggers you want at INFO, rather than the reverse.

Raise a level without restarting the query

This is the setting that only matters on a streaming job, and it is one line:

monitorInterval = 5

Log4j 2 then stats the configuration file every five seconds and reconfigures itself if the timestamp moved. No restart, no checkpoint recovery, no lost state.

The test: start the query with MicroBatchExecution at WARN, wait, then edit the file in place to INFO while the query is running.

# with the query already running
sed -i 's/^logger.micro.level = warn/logger.micro.level = info/' /work/log4j2-live.properties
query started at                13:03:48
config edited at                13:04:33
13:04:36 INFO MicroBatchExecution - Committed offsets for batch ...
13:04:36 INFO MicroBatchExecution - Streaming query made progress: {...}
13:04:38 INFO MicroBatchExecution - Committed offsets for batch ...
13:04:38 INFO MicroBatchExecution - Streaming query made progress: {...}

Not one INFO line from that logger appeared in the 45 seconds before the edit, and the first one appeared three seconds after it, inside the five second interval. Turn it back down the same way when you have seen enough.

Two caveats. monitorInterval costs a stat on every interval, per JVM, so do not set it to one second across a thousand executors. And the file it watches is the one the JVM resolved at startup, so on a cluster you edit the copy that was shipped to the container, not the one on the submitting machine.

Get the file to the driver and the executors

Nothing here is streaming-specific, and the mechanics are the same as for a batch job. In cluster mode both sides need the file shipped and referenced by its bare name, because --files places it in the container’s working directory:

spark-submit \
  --master yarn \
  --deploy-mode cluster \
  --files /tmp/log4j2-streaming.properties \
  --conf spark.driver.extraJavaOptions="-Dlog4j.configurationFile=log4j2-streaming.properties" \
  --conf spark.executor.extraJavaOptions="-Dlog4j.configurationFile=log4j2-streaming.properties" \
  orders_stream.py

In client mode the driver JVM is already running on the submitting machine, so it takes an absolute local path while the executors still take the bare name:

spark-submit \
  --master yarn \
  --deploy-mode client \
  --files /tmp/log4j2-streaming.properties \
  --driver-java-options "-Dlog4j.configurationFile=/tmp/log4j2-streaming.properties" \
  --conf spark.executor.extraJavaOptions="-Dlog4j.configurationFile=log4j2-streaming.properties" \
  orders_stream.py

Use --driver-java-options rather than spark.driver.extraJavaOptions in client mode. The driver JVM has already been launched by the time the latter is read.

If the two sides need different levels, which is common because the driver runs the query and the executors run the tasks, ship two files and point each side at its own:

spark-submit \
  --master yarn \
  --deploy-mode cluster \
  --files /tmp/log4j2-driver.properties,/tmp/log4j2-executor.properties \
  --conf spark.driver.extraJavaOptions="-Dlog4j.configurationFile=log4j2-driver.properties" \
  --conf spark.executor.extraJavaOptions="-Dlog4j.configurationFile=log4j2-executor.properties" \
  orders_stream.py

What changes on a Log4j 1 cluster?

Spark used Log4j 1.x up to and including 3.2.x. Plenty of streaming jobs are still running there, so the equivalents are worth having, and one detail about how the two versions coexist is worth more than the syntax.

The syntax and the flag

Log4j 1 names a level and an appender list on one property, and configures the appender with dotted sub-properties. The same “root quiet, streaming loud” policy as above looks like this:

log4j.rootLogger=WARN, rolling

log4j.appender.rolling=org.apache.log4j.RollingFileAppender
log4j.appender.rolling.File=/var/log/spark/streaming.log
log4j.appender.rolling.MaxFileSize=64MB
log4j.appender.rolling.MaxBackupIndex=10
log4j.appender.rolling.layout=org.apache.log4j.PatternLayout
log4j.appender.rolling.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1} - %m%n

log4j.logger.org.apache.spark.sql.execution.streaming=INFO
log4j.logger.org.apache.spark.sql.execution.streaming.state=WARN
log4j.logger.org.apache.spark.scheduler=WARN

Two details differ from the Log4j 2 file. The template Spark 3.2.4 ships writes log4j.rootCategory rather than log4j.rootLogger; both work, because rootCategory is the older alias. And rollover here is two properties on the appender, MaxFileSize and MaxBackupIndex, rather than a separate policy and strategy. Running the config above on Spark 3.2.4 with MaxBackupIndex=3 keeps exactly three, uncompressed:

-rw-r--r-- 1 root root 18160 Sep 22 05:00 streaming.log
-rw-r--r-- 1 root root 65686 Sep 22 04:59 streaming.log.1
-rw-r--r-- 1 root root 66431 Sep 22 05:00 streaming.log.2
-rw-r--r-- 1 root root 66947 Sep 22 05:00 streaming.log.3

The flag is -Dlog4j.configuration=, which accepts either a file: URL or a plain absolute path; both were honoured on 3.2.4, and the URL form is the unambiguous one:

spark-submit \
  --master yarn \
  --deploy-mode cluster \
  --files /tmp/log4j.properties \
  --conf spark.driver.extraJavaOptions="-Dlog4j.configuration=log4j.properties" \
  --conf spark.executor.extraJavaOptions="-Dlog4j.configuration=log4j.properties" \
  orders_stream.py

The --files and client-versus-cluster rules are identical to the Log4j 2 case, because they are Spark’s rules rather than Log4j’s.

The old flag still works on a current Spark, which is not what you would expect

The reasonable assumption is that a Log4j 1 config on Spark 3.5.x is ignored and you fall back to the cluster default. That is not what happens. Spark ships log4j-1.2-api alongside log4j-core, and that bridge registers a configuration factory which still answers to log4j.configuration and still parses Log4j 1 syntax. Passing the old flag with the old file on Spark 3.5.9 honours it, including the pattern layout:

spark-submit --driver-java-options "-Dlog4j.configuration=file:/work/log4j-legacy.properties" tiny.py
LEGACY1 26/09/21 13:02:59 WARN util.NativeCodeLoader: Unable to load native-hadoop library ...

LEGACY1 is a marker in the old file’s ConversionPattern, so its presence proves the file was read. -Dlog4j2.debug=true names the factory that won:

DEBUG StatusLogger Configuration org.apache.log4j.config.PropertiesConfiguration@21282ed8 initialized
DEBUG StatusLogger Reconfiguration complete for context[name=5ffd2b27] at URI /work/log4j-legacy.properties

org.apache.log4j.config.PropertiesConfiguration is the bridge’s parser, not Log4j 2’s own. The -Dlog4j1.compatibility=true property that the Log4j documentation associates with this path was not required on a stock Spark 3.5.9 distribution.

This is a useful thing to know and a bad thing to depend on. It explains why a pre-2022 snippet can still appear to work, which otherwise looks like magic. It also means a team can sit on the bridge for years without noticing what it costs them.

What the bridge costs you

Capability Log4j 1 syntax Log4j 2 syntax
Size-based rollover MaxFileSize, MaxBackupIndex SizeBasedTriggeringPolicy, DefaultRolloverStrategy
Time-based rollover separate DailyRollingFileAppender, no retention cap same appender, TimeBasedTriggeringPolicy alongside size
Compress on rollover not supported .gz / .zip / .xz suffix on filePattern
Re-read config on a running JVM not supported monitorInterval
JSON output not supported JsonTemplateLayout

The row that matters for streaming is monitorInterval. On Log4j 1 there is no way to raise a level on a running query, so the only route to more detail is a restart, which is exactly the thing a streaming job makes expensive. If you maintain one long-running streaming job on a 3.2.x cluster, that single capability is a reasonable argument for the upgrade.

Migrating a config

The mapping is mechanical for everything except the appender policies:

Log4j 1 Log4j 2
log4j.rootLogger=WARN, console rootLogger.level = warn plus rootLogger.appenderRef.console.ref = console
log4j.logger.org.apache.spark=INFO logger.spark.name = org.apache.spark plus logger.spark.level = info
log4j.appender.console=org.apache.log4j.ConsoleAppender appender.console.type = Console
log4j.appender.console.layout.ConversionPattern=%d %p %c - %m%n appender.console.layout.pattern = %d %p %c - %m%n
log4j.additivity.foo=false logger.foo.additivity = false

The logger.<key> part of a Log4j 2 name is an arbitrary handle. It has to be unique within the file and it has nothing to do with the logger’s actual name, which is what logger.<key>.name sets. Reusing a handle silently overwrites the earlier entry, which is the most common way a migrated file loses a rule.

Where do the logs actually go on a job that never finishes?

This trips people up more than the configuration does.

On YARN, log aggregation copies container logs to HDFS when the container completes. A streaming job’s containers do not complete, so yarn logs -applicationId <id> returns nothing useful for a running query. You read the live logs through the YARN UI’s node manager links, or you go to the node and read the file, or you configure a rolling aggregation interval with yarn.nodemanager.log-aggregation.roll-monitoring-interval-seconds so partial logs are shipped while the application runs.

That last option is the one worth setting up front on a streaming cluster, and it has a Spark-side companion: spark.yarn.rolledLog.includePattern is a Java regex naming which log files get aggregated in a rolling fashion, and the Spark documentation is explicit that it only does anything once the YARN interval above is set in yarn-site.xml. Set one without the other and nothing rolls.

The alternative is discovering during an incident that the logs you want are only on a node you have to go find.

On Kubernetes, the driver and executor pods write to stdout and whatever collects container logs picks them up, which means a RollingFile appender writing inside the pod is usually the wrong choice: the file is invisible to the collector and vanishes with the pod. Prefer the Console appender there and let the platform’s retention apply.

spark.yarn.app.container.log.dir is set by Spark inside a YARN container, and ${sys:spark.yarn.app.container.log.dir} in a Log4j 2 config resolves to it, which is how you write into the directory YARN already knows about.

Is there something better than parsing log lines?

Yes, and for a streaming query it is the right answer more often than not. Every micro-batch produces a StreamingQueryProgress object, and StreamingQueryListener hands it to you as a callback. One line of your own beats reconstructing the same facts from 151 lines of someone else’s:

import logging
from pyspark.sql.streaming import StreamingQueryListener

log = logging.getLogger("orders.progress")

class ProgressLogger(StreamingQueryListener):
    def onQueryStarted(self, event):
        log.warning("query started id=%s name=%s", event.id, event.name)

    def onQueryProgress(self, event):
        p = event.progress
        log.warning(
            "batch=%s input_rows=%s input_rps=%.1f process_rps=%.1f "
            "batch_duration_ms=%s state_rows=%s watermark=%s",
            p.batchId, p.numInputRows, p.inputRowsPerSecond or 0.0,
            p.processedRowsPerSecond or 0.0, p.batchDuration,
            p.stateOperators[0].numRowsTotal if p.stateOperators else None,
            p.eventTime.get("watermark") if p.eventTime else None,
        )

    def onQueryTerminated(self, event):
        log.warning("query terminated id=%s exception=%s", event.id, event.exception)

spark.streams.addListener(ProgressLogger())

Output from the demo query, one line per batch:

query started id=1264f772-e538-45aa-b397-d433d81f92d7 name=orders_per_window
batch=0 input_rows=0 input_rps=0.0 process_rps=0.0 batch_duration_ms=1166 state_rows=0 watermark=1970-01-01T00:00:00.000Z
batch=1 input_rows=200 input_rps=143.6 process_rps=684.9 batch_duration_ms=292 state_rows=1 watermark=1970-01-01T00:00:00.000Z
batch=2 input_rows=400 input_rps=199.5 process_rps=1556.4 batch_duration_ms=257 state_rows=2 watermark=2026-09-21T13:07:39.586Z
batch=3 input_rows=400 input_rps=200.5 process_rps=1877.9 batch_duration_ms=213 state_rows=2 watermark=2026-09-21T13:07:41.586Z

Those rate figures come from a rate source in a container and say nothing about what your query will do; the point is which fields exist, not their values.

Two details in that transcript are worth reading. The watermark is 1970-01-01T00:00:00.000Z for the first two batches, because a watermark is computed from data already seen and there is none yet, so a fresh query is briefly unable to drop anything as late. And state_rows is the number the query fails on eventually: if it climbs without bound, the watermark is not evicting and the job will run out of memory on a schedule you can extrapolate.

Neither fact is easy to see in a log file, and both are one field in the progress object. Log the object, alert on the fields, and keep the Log4j config for the times something throws.

Recommendations

  • Set the root logger to WARN and name what you want at INFO. A streaming query at rootLogger.level = info wrote 155 lines per micro-batch in the run above, most of it from the scheduler.
  • Mute org.apache.spark.sql.execution.streaming.state and ...streaming.CheckpointFileManager unless you are debugging state. They are where the per-partition volume comes from, and they scale with spark.sql.shuffle.partitions.
  • Give every file appender a DefaultRolloverStrategy with max. A triggering policy alone bounds each file, not the set of them.
  • Set monitorInterval on every streaming job, even when you never use it. It costs a stat per interval and it is the difference between raising a log level and taking an outage.
  • Add a StreamingQueryListener before you tune the Log4j config. Most of what people raise the log level to find is already a field on StreamingQueryProgress.
  • On YARN, set a rolling aggregation interval. Otherwise the logs for a running streaming job are only on the node that produced them.

The mental model worth keeping: a batch job’s log describes a thing that happened, and a streaming job’s log describes a loop that is still going. You read the first one afterwards and the second one while it runs. Everything above follows from that, including why the same config that is fine for a batch job fills a disk here, and why the one Log4j 2 feature that matters most is the one that lets you change your mind without stopping.

Frequently asked questions

Why is my streaming log full of StateStore and CheckpointFileManager lines? Both log once per partition per micro-batch, so the volume is set by spark.sql.shuffle.partitions rather than by traffic. Measured on a windowed count, the per-batch total went from 43 lines at 4 partitions to 151 at 16 with identical input. Set logger.statestore.name = org.apache.spark.sql.execution.streaming.state to warn to remove most of it.

Can I change the log level of a running Spark streaming query? Yes, with Log4j 2. Add monitorInterval = 5 to the config and edit the file the container actually resolved; Log4j 2 re-reads it within the interval. There is no equivalent on Log4j 1, so a Spark 3.2.x or earlier cluster requires a restart.

Does spark.sparkContext.setLogLevel() work for this? It sets the root logger’s level, so it is all or nothing. Calling setLogLevel("INFO") on a run whose config had rootLogger.level = warn turned on CodeGenerator, MemoryStore and ShutdownHookManager along with everything else. It is the right tool for “show me everything for a moment” and the wrong one for raising a single package, which is what monitorInterval and a named logger give you.

Does the old log4j.properties still work on Spark 3.3.0 and later? On a stock distribution, yes. Spark bundles log4j-1.2-api, whose configuration factory answers to -Dlog4j.configuration and parses Log4j 1 syntax. You give up monitorInterval, rollover compression, retention caps and JSON layout by staying on it, so treat it as a migration aid.

Why is my log file still growing after I set a rollover policy? A triggering policy bounds each file; DefaultRolloverStrategy with max bounds how many are kept. Without strategy.max the appender renames files forever.

Why does yarn logs -applicationId return nothing for my streaming job? Log aggregation runs when a container completes, and a streaming job’s containers do not. Read the live logs through the node manager, or set yarn.nodemanager.log-aggregation.roll-monitoring-interval-seconds so partial logs are aggregated while the application runs.

My executor logging config has no effect but the driver’s works. Either spark.executor.extraJavaOptions is unset, or the file was not listed in --files, or the executor side was given an absolute path from the submitting machine. Executors need the bare filename, because --files places the file in the container’s working directory.

References

Trademarks

Apache Spark, Apache Log4j, Apache Hadoop, Apache Kafka and Apache are either registered trademarks or trademarks of The Apache Software Foundation in the United States and other countries.

Found this useful?

These posts and tools are free. If one saved you an afternoon, you can buy me a coffee.

Buy me a coffee