Metrics¶
Nineteen instruments, all under the io.flare.spark meter, all turned off by
FLARE_METRICS_ENABLED=false.
| Instrument | Kind | Unit | Labels |
|---|---|---|---|
flare.task.duration |
histogram | ms |
executor.id, task.result |
flare.task.records_throughput |
histogram | {records}/s |
executor.id, task.result |
flare.task.shuffle.read_bytes |
counter | By |
executor.id, task.result |
flare.task.shuffle.write_bytes |
counter | By |
executor.id, task.result |
flare.stage.executor.run_time |
histogram | ms |
stage.name, sql.description |
flare.stage.input.bytes |
counter | By |
stage.name, sql.description |
flare.stage.output.bytes |
counter | By |
stage.name, sql.description |
flare.stage.shuffle.read_bytes |
counter | By |
stage.name, sql.description |
flare.stage.shuffle.write_bytes |
counter | By |
stage.name, sql.description |
flare.job.duration |
histogram | ms |
job.result, sql.description |
flare.application.duration |
histogram | s |
application.result |
flare.application.end_time |
gauge | s |
application.result |
flare.executor.count |
updowncounter | {executor} |
executor.id |
flare.executor.removed |
counter | {executor} |
executor.id, reason |
flare.task.lost |
counter | {task} |
executor.id, reason |
flare.executor.excluded |
counter | {executor} |
executor.id |
flare.block_manager.count |
updowncounter | {block_manager} |
executor.id |
flare.rdd.unpersisted |
counter | {rdd} |
none |
flare.storage.memory.bytes |
updowncounter | By |
executor.id |
flare.storage.disk.bytes |
updowncounter | By |
executor.id |
flare.storage.blocks |
updowncounter | {block} |
executor.id |
task.result is SUCCESS, FAILED, or KILLED for a task Spark killed itself: the losing
attempt under speculation, or the tasks of a cancelled job or stage. A task error rate counts
FAILED only.
Histogram buckets¶
| Histogram | Bucket boundaries |
|---|---|
flare.task.duration |
5, 10, 25, 50, 100, 250, 500 ms; 1, 2.5, 5, 10, 30 s; 1, 2, 5, 10, 30, 60 min |
flare.stage.executor.run_time |
100 ms; 1, 5, 10, 30 s; 1, 5, 10, 30 min; 1, 3, 10, 24 h. Summed across the stage's tasks, so far longer than its wall-clock time |
flare.task.records_throughput |
10 to 100,000,000 records/s, one boundary per power of ten |
They are set as advice on each instrument, so a view configured in the SDK or the agent overrides them.
No label is new for every stage. stage.name and sql.description are call sites, such as
collect at Job.scala:42, so stages from the same line of code share a series, and the number of
series depends on the code, not on how long the application runs. A single stage is on its
spark.stage span. Up to 1.2.0 the task and stage metrics also carried stage.id, which made one
series per stage (#136).
executor.id is the one label that grows with the application's lifetime. Spark never reuses an
executor id, and with cumulative temporality, the default, the SDK keeps every series it has seen
and re-sends it on each export, including those of executors long gone. Delta temporality
(OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE=delta) sends only what changed since the last
export, though the backend still stores a series per id. On a fixed cluster that is a handful of series. Under dynamic
allocation or autoscaling, an application that runs for days adds tens to hundreds of ids a day,
and past 2,000 series per instrument the SDK folds new ones into a single overflow series, losing
the per-executor breakdown. Restarting the application starts the count again
(#144).
The counters are only incremented for non-zero values, so a stage that read nothing produces no
flare.stage.input.bytes series rather than a flat zero one.
Cluster lifecycle. The flare.executor.*, flare.block_manager.* and flare.storage.*
instruments describe the cluster rather than any one query, and are deliberately metrics rather
than spans: an executor's lifetime is a level over time, not an operation, and an executor alive
for the whole application would otherwise be a span longer than every trace it overlaps.
They are up-down counters because they go both ways. An increment-only counter would tell you how many executors were ever created, never how many exist now.
flare.executor.count includes the executors that registered before Flare did. Installed through
the agent alone, Flare's listener starts as the SparkContext finishes starting, by which time
Spark has waited for most executors to register on YARN and Kubernetes; Flare counts the ones
already there at that point.
flare.executor.removed carries a reason tag, which is the point of it: on a dynamically
allocated cluster a routine scale-down and a crash both reduce the executor count, and only the
reason separates them. Spark's reason string is free text that sometimes embeds ids or hostnames,
so it is bucketed into a fixed set (idle_or_decommissioned, preempted, heartbeat_timeout,
lost, killed, exited, other, unknown) rather than passed through: an unbounded tag on
a counter is the cardinality problem these instruments exist to avoid.
Each executor is counted once on removal, with the reason Spark gave first. Spark removes a
decommissioned executor twice, once as decommissioned and again when its process exits; the second
is ignored, so a scale-down by decommissioning reads idle_or_decommissioned, not also exited.
flare.task.lost counts tasks that ended because their executor was lost: killed, out of memory,
preempted or gone silent. The executor records every other task on the flare.task.* instruments,
but not these, since it died with them; the driver records them, labelled like
flare.executor.removed. A task error rate that should include them adds flare_task_lost to the
failed tasks.
flare.block_manager.count includes the driver's block manager, not just executors', because
Spark registers one there too. Expect it to sit one above the executor count.
The flare.storage.* instruments require FLARE_TRACK_BLOCK_UPDATES=true. They track running
totals per executor; block ids are never used as tags. Spark reports each block's state, again
whenever it changes, such as moving from memory to disk, so Flare keeps each block's last reported
sizes on the driver and records only the difference. An invalid StorageLevel means the block is
gone; when a block manager is removed, its blocks are taken out of the totals with it.
flare.task.* are recorded on the executor while the task span's scope is still open, so the SDK's
default trace_based exemplar filter attaches an exemplar linking each measurement back to its
trace. Under FLARE_SLOW_TASK_MS a fast task's span is dropped, so its measurement's exemplar
names the task's stage instead, which is always exported, when the stage's context is current on
the executor thread. A task that carries no trace context, such as one from an unsampled
application, gets no exemplar. A slow task's exemplar names the task span. Some backends drop exemplars by default: Mimir's
max_global_exemplars_per_user is 0 unless you set it.
Outcomes¶
flare.job.duration and flare.application.duration record one point per job and one per
application, from the driver, so a scheduled pipeline can be alerted on from metrics: whether a
run happened, whether it failed, and whether a query took longer than its last runs. The same
facts are on the spans, which alert rules cannot read.
flare.job.durationruns from the job's submission to its end, by Spark's own clock.job.resultisSUCCESSorFAILED;sql.descriptionis set when the job belongs to a query, so one query's runs share a series.flare.application.durationis recorded once, when the application ends: on a normal stop, or at shutdown when the JVM goes down without one. Spark reports no result for an application, soapplication.resultisFAILEDwhen any of its jobs failed, or when a job was still running as it ended: an application killed mid-run, by an orchestrator's timeout or a SIGTERM, readsFAILED. An application that catches a failed job and carries on still readsFAILED, and one that fails outside any job, in driver code or before its first query runs, readsSUCCESS.flare.application.end_timeis set alongside it, to when the application ended in seconds since the Unix epoch, with the sameapplication.result. It puts runs in order, which the example rule that alerts only on a service's latest run needs (#202).
Neither carries an application or run id, so successive runs of the same service can be compared.
sql.description, here and on the stage metrics, is the query's call site, such as
collect at Job.scala:42, or the job description when you set one with setJobDescription. Keep
those descriptions stable: one that embeds a date, a batch id or any value that changes per run
makes a new series every time. Under the Thrift server and the spark-sql CLI the description is
the SQL statement itself. The label is capped by FLARE_SQL_DESCRIPTION_MAX_CHARS (1,024 characters
by default) and is never longer than 2,048 bytes in UTF-8, the limit Mimir and similar backends
apply, which a statement in Chinese reaches at about 680 characters. A cap limits length, not
cardinality; 0 drops the label.
In Prometheus¶
Prometheus-compatible backends rename the instruments: dots become underscores, and most add the
unit, so flare.task.duration is stored as flare_task_duration_milliseconds_bucket and
flare.stage.input.bytes as flare_stage_input_bytes_total. Mimir's OTLP ingestion leaves the
unit off by default (flare_task_duration_bucket, flare_stage_input_bytes). Labels follow the
same rule: executor.id becomes executor_id, and service.name becomes job.
Dashboard¶
The repository ships a Grafana dashboard,
flare-spark.json.
Import it under Dashboards → New → Import, then pick your metrics, logs and traces data
sources and the services to show at the top. It reads the metric names with or without the unit,
so it works on Prometheus, Mimir and Grafana Cloud alike.
The job count comes from Tempo's span metrics, which need its metrics generator; without it, that panel stays empty and the rest still work.