Counters and Diagnostics
When a Tez DAG misbehaves you get exactly two machine-readable signals out of the
Application Master: counters (numeric aggregates rolled up from every task
attempt) and diagnostics (free-text cause strings attached at every level of
the DAG hierarchy). Everything else — the Tez UI, _history files on HDFS, the
YARN Timeline entities, the job-analyzer plugins in tez-tools — is just a
different rendering of those two data structures. This chapter is the
maintainer's reference for how both are modeled, aggregated, size-limited,
persisted, and surfaced. It runs almost entirely inside the AM (see
dag-app-master.md) and is fed by the runtime heartbeat
path described in event-routing.md.
After this chapter you can:
- Trace a single counter increment from user code in a task JVM up through
TaskAttemptImpl→TaskImpl→VertexImpl→DAGImplto theDAGStatusthe client polls. - Explain why counters have a hard cap, which config keys set it, and what
LimitExceededExceptiondoes to a running DAG. - Read the real
TaskCounter/DAGCounterenums and know which member means what — no guessingSHUFFLE_BYTES_TO_MEMvsSHUFFLE_BYTES_DISK_DIRECT. - Follow a diagnostic string from a failed attempt up to what the client sees.
- Name the real history-logging services on master and pick the config key that selects one.
- Find the
.dotvisualization the AM writes and render it with Graphviz.
Every class, enum, and config key below is quoted from the checkout at
/Users/s0x/src/oss-repos/tez. Code moves between branches, so each section
opens with the grep/rg that locates the symbol rather than a line number —
run it yourself before you trust anything here.
The TezCounters model
cd /Users/s0x/src/oss-repos/tez
ls tez-api/src/main/java/org/apache/tez/common/counters/
rg -n "class TezCounters|extends AbstractCounters" \
tez-api/src/main/java/org/apache/tez/common/counters/TezCounters.java
A TezCounters is a two-level map: groupName → CounterGroup → counterName → TezCounter. The whole thing is generic machinery lifted from Hadoop's
org.apache.hadoop.mapreduce.counters package. In tez-api, module
org.apache.tez.common.counters, the class hierarchy is:
| Type | Role |
|---|---|
TezCounters extends AbstractCounters<TezCounter, CounterGroup> | Top-level container; wires in three group factories |
CounterGroup extends CounterGroupBase<TezCounter> | One group of counters (one Enum class, or a user group) |
TezCounter | A single named long with increment(long) / setValue(long) |
AbstractCounters / AbstractCounterGroup | Generic base classes shared with the FS/framework specializations |
TezCounters mixes three concrete group implementations, visible in the class:
// tez-api org.apache.tez.common.counters.TezCounters
public class TezCounters extends AbstractCounters<TezCounter, CounterGroup> {
private static class FrameworkGroupImpl<T extends Enum<T>> // TaskCounter, DAGCounter
extends FrameworkCounterGroup<T, TezCounter> implements CounterGroup { /* ... */ }
private static class GenericGroup // user-defined groups
extends AbstractCounterGroup<TezCounter> implements CounterGroup { /* ... */ }
private static class FileSystemGroup // FileSystemCounter
extends FileSystemCounterGroup<TezCounter> implements CounterGroup { /* ... */ }
// ...
}
The distinction matters: framework groups (TaskCounter, DAGCounter) are
backed by a dense array indexed by enum ordinal — cheap, fixed-size. Generic
groups (your app counters) are backed by a HashMap<String, TezCounter> and are
what blow the limits below. findCounter(Enum) and findCounter(String group, String name) on AbstractCounters are the accessors:
rg -n "public synchronized C findCounter|public synchronized G getGroup|incrAllCounters|aggrAllCounters" \
tez-api/src/main/java/org/apache/tez/common/counters/AbstractCounters.java
Standard groups
| Group name | Defining class (module) | What lives there |
|---|---|---|
org.apache.tez.common.counters.TaskCounter | TaskCounter enum (tez-api) | Per-task framework metrics (I/O, shuffle, spill, memory) |
org.apache.tez.common.counters.DAGCounter | DAGCounter enum (tez-api) | Per-DAG aggregates the AM computes itself |
org.apache.tez.common.counters.FileSystemCounter | FileSystemCounter enum (tez-api) | Per-scheme bytesRead/bytesWritten/readOps/… |
org.apache.hadoop.mapreduce.JobCounter | JobCounter enum (tez-api) | Legacy MR compatibility shim |
<your class or string> | user code | App counters |
Note: Both enums carry the comment
// Keep in sync with tez-ui/src/main/webapp/config/default-app-conf.js. The Tez UI hard-codes the counter names it knows how to display. If you add aTaskCountermember and want it shown in the UI, you must add it there too — verify withrg -n "SHUFFLE_BYTES" tez-ui/src/main/webapp/config/default-app-conf.js.
Counter limits (and how they kill DAGs)
rg -n "TEZ_COUNTERS_MAX|TEZ_COUNTERS_MAX_GROUPS|COUNTER_NAME_MAX_LENGTH|GROUP_NAME_MAX_LENGTH" \
tez-api/src/main/java/org/apache/tez/dag/api/TezConfiguration.java
Counters are unbounded by construction — nothing stops user code from calling
findCounter("perFile", path) in a hot loop — so Tez imposes hard caps. The
keys and defaults are defined in TezConfiguration (tez-api) and enforced by
Limits (tez-api, org.apache.tez.common.counters.Limits):
| Config key | Default | Caps |
|---|---|---|
tez.counters.max | 1200 | Total counters in one TezCounters instance |
tez.counters.max.groups | 500 | Number of groups |
tez.counters.counter-name.max-length | 64 | Chars in a counter name (longer names are truncated) |
tez.counters.group-name.max-length | 256 | Chars in a group name (truncated) |
// tez-api org.apache.tez.common.counters.Limits
public synchronized void checkCounters(int size) {
if (firstViolation != null) {
throw new LimitExceededException(firstViolation);
}
if (size > COUNTERS_MAX) {
firstViolation = new LimitExceededException("Too many counters: " + size +
" max=" + COUNTERS_MAX);
throw firstViolation;
}
}
public synchronized void incrCounters() {
checkCounters(totalCounters + 1);
++totalCounters;
}
Two subtleties every contributor trips over:
- Name-length limits truncate silently; count limits throw.
filterName()inLimitschops an over-long name tomaxLen - 1characters — no exception. But adding one counter pastCOUNTERS_MAXthrowsLimitExceededException(tez-api, same package). Truncation is why two counters with long, near-identical names can collide into one. - The violation is sticky. Once
firstViolationis set, every subsequentcheckCounterson thatLimitsre-throws it. A single overflow poisons the instance.
The static Limits config is seeded from a default TezConfiguration and can be
overridden once via Limits.setConfiguration(conf). Because it is process-wide
static state, the AM and each task JVM each hold their own Limits.
Where the exception lands
The dangerous moment is aggregation on completion, not the increment itself.
When a vertex or DAG finishes, it materializes a full counter set by summing
children (next section). If that sum exceeds tez.counters.max, the aggregation
throws and the entity fails:
rg -n "LimitExceededException|constructFinalFullcounters" \
tez-dag/src/main/java/org/apache/tez/dag/app/dag/impl/DAGImpl.java \
tez-dag/src/main/java/org/apache/tez/dag/app/dag/impl/VertexImpl.java
// tez-dag DAGImpl.finished(...)
try {
counters = constructFinalFullcounters();
} catch (LimitExceededException e) {
addDiagnostic("Counters limit exceeded: " + e.getMessage());
finalState = DAGState.FAILED;
}
So a DAG that ran every task successfully can still end in FAILED purely
because the aggregate counter count crossed 1200. VertexImpl does the same in
logJobHistoryVertexFailedEvent. The tell-tale in the AM log is the diagnostic
string Counters limit exceeded: followed by Too many counters: N max=1200.
Warning: The classic production cause is a
VertexManagerPluginor processor that creates one counter per input file, per partition, or per user key. At small scale it works; at 2,000 partitions it fails the DAG at the finish line with zero task failures. Treat counter names as a bounded, static vocabulary — never derive them from data.
The TaskCounter enum
rg -n "SHUFFLE_BYTES|OUTPUT_BYTES|SPILLED_RECORDS|INPUT_RECORDS_PROCESSED|MERGE_PHASE_TIME|GC_TIME_MILLIS" \
tez-api/src/main/java/org/apache/tez/common/counters/TaskCounter.java
TaskCounter (tez-api, org.apache.tez.common.counters.TaskCounter) is the
enum every runtime input/output populates. The members that actually matter when
you are debugging a slow or skewed vertex:
| Member | Meaning (from the enum's own Javadoc) |
|---|---|
INPUT_RECORDS_PROCESSED | Records actually processed; set by MRInput and the unordered KV input |
OUTPUT_RECORDS | Records written by an output |
OUTPUT_BYTES | Serialized, uncompressed output size |
OUTPUT_BYTES_WITH_OVERHEAD | Uncompressed size plus format overhead |
OUTPUT_BYTES_PHYSICAL | Actual bytes on disk — factors in compression + overhead |
SPILLED_RECORDS | Records spilled to disk (sorter output, or unnecessary spills on the input side) |
ADDITIONAL_SPILL_COUNT / ADDITIONAL_SPILLS_BYTES_WRITTEN / ..._READ | Spills generated and re-read by the same task due to memory pressure |
NUM_SHUFFLED_INPUTS | Number of physical inputs copied from |
NUM_SKIPPED_INPUTS | Physical inputs skipped (typically empty) |
NUM_FAILED_SHUFFLE_INPUTS | Failed copy attempts |
SHUFFLE_BYTES | Physical bytes moved over the wire; = SHUFFLE_BYTES_TO_MEM + SHUFFLE_BYTES_TO_DISK |
SHUFFLE_BYTES_DECOMPRESSED | Uncompressed size of shuffled data being processed |
SHUFFLE_BYTES_TO_MEM | Bytes fetched directly into memory |
SHUFFLE_BYTES_TO_DISK | Bytes fetched directly to disk (memory budget exceeded) |
SHUFFLE_BYTES_DISK_DIRECT | Bytes read directly from local disk (no network) |
SHUFFLE_PHASE_TIME / MERGE_PHASE_TIME | ms fetching / ms merging, relative to task start |
FIRST_EVENT_RECEIVED / LAST_EVENT_RECEIVED | ms to first/last source event — a source-lag signal |
REDUCE_INPUT_GROUPS | Distinct key-groups seen by a ShuffledMergedInput / reduce |
REDUCE_INPUT_RECORDS | Total values across all groups |
GC_TIME_MILLIS / CPU_MILLISECONDS | JVM GC time / process CPU time in the task |
WALL_CLOCK_MILLISECONDS | Task init + execution wall time |
PHYSICAL_MEMORY_BYTES / VIRTUAL_MEMORY_BYTES / COMMITTED_HEAP_BYTES | Process memory snapshot at task end |
DATA_BYTES_VIA_EVENT | Payload bytes transmitted inline in events (small-output optimization) |
Tip: The three
OUTPUT_BYTES*counters are the single most useful triad for diagnosing a shuffle.OUTPUT_BYTESis what your serializer produced,OUTPUT_BYTES_WITH_OVERHEADadds the IFile/format framing, andOUTPUT_BYTES_PHYSICALis what hit the disk after compression. A large gap betweenWITH_OVERHEADandPHYSICALmeans compression is earning its keep; a tiny gap means your codec is doing nothing and you are paying CPU for it.
Note there is no NUM_SPILLS member and no MR-style MERGED_MAP_OUTPUTS
semantics: the real spill members are ADDITIONAL_SPILL_COUNT and
ADDITIONAL_SPILLS_BYTES_*, and MERGED_MAP_OUTPUTS exists but counts
source-side merges. Read the enum before citing a counter name — the MR-era names
you have muscle memory for often do not exist here.
DAGCounter
rg -n "NUM_FAILED_TASKS|NUM_KILLED_TASKS|TOTAL_LAUNCHED_TASKS|DATA_LOCAL_TASKS|WALL_CLOCK_MILLIS|AM_CPU_MILLISECONDS" \
tez-api/src/main/java/org/apache/tez/common/counters/DAGCounter.java
DAGCounter (tez-api) holds metrics the AM computes about the DAG as a whole —
these are not summed from tasks, they are incremented by the AM state machines:
| Member | Meaning |
|---|---|
NUM_SUCCEEDED_TASKS / NUM_FAILED_TASKS / NUM_KILLED_TASKS | Attempt-outcome tallies |
TOTAL_LAUNCHED_TASKS | Lifetime launched-attempt count |
DURATION_SUCCEEDED_TASKS_MILLIS / DURATION_FAILED_TASKS_MILLIS / DURATION_KILLED_TASKS_MILLIS | Time spent by attempts of each outcome |
WALL_CLOCK_MILLIS | = sum of the three DURATION_* counters (per the enum's own comment) |
DATA_LOCAL_TASKS / RACK_LOCAL_TASKS / OTHER_LOCAL_TASKS | Locality histogram |
AM_CPU_MILLISECONDS / AM_GC_TIME_MILLIS | AM process CPU / GC |
TOTAL_CONTAINERS_USED / TOTAL_CONTAINER_ALLOCATION_COUNT / ..._LAUNCH_COUNT / ..._RELEASE_COUNT / ..._REUSE_COUNT | Container lifecycle tallies (see container-reuse.md) |
NODE_USED_COUNT / NODE_TOTAL_COUNT | Distinct nodes used / visible to the scheduler |
The locality and outcome counters are incremented from TaskAttemptImpl, not
DAGImpl. createDAGCounterUpdateEventTAFinished builds a
DAGEventCounterUpdate on every attempt finish:
// tez-dag TaskAttemptImpl.createDAGCounterUpdateEventTAFinished(...)
jce.addCounterUpdate(DAGCounter.WALL_CLOCK_MILLIS, amSideWallClockTimeMs);
if (taState == TaskAttemptState.FAILED) {
jce.addCounterUpdate(DAGCounter.NUM_FAILED_TASKS, 1);
jce.addCounterUpdate(DAGCounter.DURATION_FAILED_TASKS_MILLIS, amSideWallClockTimeMs);
} else if (taState == TaskAttemptState.KILLED) {
jce.addCounterUpdate(DAGCounter.NUM_KILLED_TASKS, 1);
// ...
} else if (taState == TaskAttemptState.SUCCEEDED) {
jce.addCounterUpdate(DAGCounter.NUM_SUCCEEDED_TASKS, 1);
// ...
}
That event is routed to DAGImpl.CounterUpdateTransition, which applies each
increment to dagCounters.findCounter(key). The locality counter is chosen when
the attempt is scheduled — TaskAttemptImpl sets its localityCounter field to
DATA_LOCAL_TASKS, RACK_LOCAL_TASKS, or OTHER_LOCAL_TASKS.
Warning:
NUM_FAILED_TASKScounts failed attempts, not distinct tasks. A DAG that retries one poison task five times before it succeeds reportsNUM_FAILED_TASKS=4, NUM_SUCCEEDED_TASKS=1. Do not read it as "4 tasks were lost." See failure-handling.md for how attempts and tasks differ.
Aggregation: attempt → task → vertex → DAG
This is the spine of the whole subsystem. Follow it in code:
rg -n "getCounters|reportedStatus.counters|selectBestAttempt" \
tez-dag/src/main/java/org/apache/tez/dag/app/dag/impl/TaskAttemptImpl.java \
tez-dag/src/main/java/org/apache/tez/dag/app/dag/impl/TaskImpl.java
rg -n "aggrAllCounters|incrAllCounters|constructFinalFullcounters|aggrTaskCounters" \
tez-dag/src/main/java/org/apache/tez/dag/app/dag/impl/VertexImpl.java \
tez-dag/src/main/java/org/apache/tez/dag/app/dag/impl/DAGImpl.java
1. In the task JVM. LogicalIOProcessorRuntimeTask owns the live
TezCounters. Each input/output increments its own TaskCounter members plus
any user counters. TaskReporter (tez-runtime-internals) snapshots them into a
TaskStatusUpdateEvent and heartbeats it to the AM — but only every
tez.task.am.heartbeat.counter.interval-ms.max (default 4000 ms), because
counter objects are large:
// tez-runtime-internals TaskReporter.HeartbeatCallable
if ((nonOobHeartbeatCounter.get() - prevCounterSendHeartbeatNum) * pollInterval
>= sendCounterInterval) {
sendCounters = true;
prevCounterSendHeartbeatNum = nonOobHeartbeatCounter.get();
}
2. At the attempt. TaskAttemptImpl.StatusUpdaterTransition stores the
latest snapshot in reportedStatus.counters; getCounters() returns it (or
EMPTY_COUNTERS). This is the only place raw task counters live in the AM.
3. At the task. TaskImpl.getCounters() returns the winning attempt's
counters via selectBestAttempt() — losers are discarded:
// tez-dag TaskImpl.getCounters()
TaskAttempt bestAttempt = selectBestAttempt();
TezCounters taskCounters = (bestAttempt != null)
? bestAttempt.getCounters() : TaskAttemptImpl.EMPTY_COUNTERS;
return taskCounters;
4. At the vertex and DAG. On completion each entity builds an
AggregateTezCounters by summing children. This is constructFinalFullcounters,
guarded by mayBeConstructFinalFullCounters so it happens once:
// tez-dag DAGImpl.constructFinalFullcounters()
final AggregateTezCounters aggregateTezCounters = new AggregateTezCounters();
aggregateTezCounters.aggrAllCounters(dagCounters); // the AM's own DAG_* counters
for (Vertex v : this.vertices.values()) {
aggregateTezCounters.aggrAllCounters(v.getAllCounters());
}
return aggregateTezCounters;
VertexImpl.constructFinalFullcounters is symmetric: it starts from the vertex's
own counters, then aggrAllCounters(task.getCounters()) for every task.
Note:
incrAllCountersandaggrAllCountersdiffer in intent. On a plainTezCounters,incrAllCounters(other)delegates toaggrAllCounters(simple addition). ButconstructFinalFullcountersuses anAggregateTezCounters, whose framework counters track min / max / count in addition to the sum — that is how the Tez UI shows per-vertex min/avg/max for a counter across tasks. Verify withrg -n "min|max|aggregate" tez-api/src/main/java/org/apache/tez/common/counters/AggregateFrameworkCounter.java.
When are full counters computed?
getAllCounters() short-circuits: if the entity is in a terminal state
(SUCCEEDED/FAILED/KILLED/ERROR) it returns the cached fullCounters;
otherwise it computes a live aggregate on the fly. There is also
getCachedCounters(), which serves a 10-second cached snapshot to avoid
re-summing every status poll of a running DAG:
// tez-dag DAGImpl.getCachedCounters()
if (fullCounters == null && cachedCounters != null
&& ((cachedCountersTimestamp + 10000) - System.currentTimeMillis() > 0)) {
return cachedCounters; // reuse for up to 10s
}
Counters only reach the client when the caller asks for them.
DAGImpl.getDAGStatus(Set<StatusGetOpts>) attaches counters only if
StatusGetOpts.GET_COUNTERS is present:
// tez-dag DAGImpl.getDAGStatus(...)
if (statusOptions.contains(StatusGetOpts.GET_COUNTERS)) {
status.setDAGCounters(getAllCounters());
}
So a client that polls DAGStatus without GET_COUNTERS never pays the
aggregation cost — and never sees counters. StatusGetOpts (tez-api,
org.apache.tez.dag.api.client) has exactly two members: GET_COUNTERS and
GET_MEMORY_USAGE.
in-process (task JVM) Application Master
┌───────────────────────────┐ heartbeat ┌────────────────────────────────┐
│ LogicalIOProcessorRuntime │ ≤ 4000 ms │ TaskAttemptImpl │
│ TaskCounter + user cntrs │──TaskStatus───▶│ reportedStatus.counters │
└───────────────────────────┘ UpdateEvent └───────────────┬────────────────┘
│ selectBestAttempt()
▼ (losers discarded)
TaskImpl.getCounters() → winning attempt
│ VERTEX terminal
▼ aggrAllCounters(task)
VertexImpl.constructFinalFullcounters()
│ DAG terminal
▼ aggrAllCounters(vertex)
DAGImpl.constructFinalFullcounters()
│ getDAGStatus(GET_COUNTERS)
▼
DAGStatus.getDAGCounters() → client
flowchart TD
RT["LogicalIOProcessorRuntimeTask<br/>live TezCounters"] -->|TaskReporter heartbeat, every ~4s| TA["TaskAttemptImpl.reportedStatus.counters"]
TA -->|TASK_SUCCEEDED / selectBestAttempt| T["TaskImpl.getCounters<br/>(winning attempt only)"]
T -->|VertexImpl.constructFinalFullcounters<br/>aggrAllCounters| V["VertexImpl fullCounters<br/>(AggregateTezCounters: sum/min/max)"]
V -->|DAGImpl.constructFinalFullcounters<br/>aggrAllCounters| D["DAGImpl fullCounters"]
DAGSELF["dagCounters<br/>DAGCounter.* from AM state machines"] --> D
D -->|getDAGStatus GET_COUNTERS| CS["DAGStatus.getDAGCounters → client"]
D -->|DAGFinishedEvent| H["HistoryEventHandler → history log / ATS / proto"]
Diagnostics strings
rg -n "addDiagnostic|addDiagnosticInfo|getDiagnostics" \
tez-dag/src/main/java/org/apache/tez/dag/app/dag/impl/TaskAttemptImpl.java \
tez-dag/src/main/java/org/apache/tez/dag/app/dag/impl/VertexImpl.java \
tez-dag/src/main/java/org/apache/tez/dag/app/dag/impl/DAGImpl.java
Every level keeps its own List<String> of diagnostics. The API is almost
uniform — note the name difference, which trips up grep:
| Level | Class (tez-dag) | Append method | Field |
|---|---|---|---|
| Task attempt | TaskAttemptImpl | addDiagnosticInfo(String) (private) | diagnostics |
| Task | TaskImpl | aggregates failed-attempt diagnostics | — |
| Vertex | VertexImpl | addDiagnostic(String) | diagnostics |
| DAG | DAGImpl | addDiagnostic(String) | diagnostics |
Propagation is not a full union — each level appends a summary of the child
failure plus its own context. A failed attempt's stack goes into its
TaskFinishedEvent/VertexFinishedEvent diagnostics field; the vertex records
"Vertex ... killed/failed due to: <terminationCause>" plus the aggregated child
diag; the DAG records "DAG did not succeed due to <cause>. failedVertices:N killedVertices:M":
// tez-dag VertexImpl (FAILED/KILLED path)
addDiagnostic("Vertex " + logIdentifier + " killed/failed due to:" + terminationCause);
if (!StringUtils.isEmpty(diag)) {
addDiagnostic(diag); // aggregated child diagnostics
}
// tez-dag DAGImpl.finishWithTerminationCause(...)
String diagnosticMsg = "DAG did not succeed due to " + dag.terminationCause
+ ". failedVertices:" + dag.numFailedVertices
+ " killedVertices:" + dag.numKilledVertices;
dag.addDiagnostic(diagnosticMsg);
What the client sees
The client polls DAGStatus (tez-api,
org.apache.tez.dag.api.client.DAGStatus). Its getDiagnostics() returns the
DAG-level list; VertexStatus.getDiagnostics() returns the vertex-level list.
Both are simple pass-throughs to the protobuf getDiagnosticsList():
// tez-api DAGStatus.getDiagnostics()
public List<String> getDiagnostics() {
return proxy.getDiagnosticsList();
}
To reach the attempt-level detail for a specific failed task you must go to the
history log or Tez UI, not the top-level DAGStatus — the DAG string only
summarizes the cause. This is the number-one confusion when triaging: the client
prints "DAG did not succeed due to VERTEX_FAILURE" and the actual Java
exception is three levels down in the attempt's diagnostics.
Tip: In the AM log, diagnostic strings ride along in the
*FinishedEventtoString output, and the critical-event stream is tagged[HISTORY]. Grep thesyslogfor[HISTORY][DAG:to get the ordered stream of DAG/vertex/task lifecycle events with their diagnostics inline — that logger isHistoryEventHandler.criticalEvents.
History and ATS logging
rg -n "TEZ_HISTORY_LOGGING_SERVICE_CLASS|SimpleHistoryLoggingService|history.logging.log.level" \
tez-api/src/main/java/org/apache/tez/dag/api/TezConfiguration.java
ls tez-dag/src/main/java/org/apache/tez/dag/history/logging/impl/
ls tez-plugins/
Every lifecycle transition emits a HistoryEvent (there are ~24 types — see
HistoryEventType in tez-dag, e.g. DAG_SUBMITTED, VERTEX_FINISHED,
TASK_ATTEMPT_FINISHED). HistoryEventHandler (tez-dag,
org.apache.tez.dag.history) is the fan-out point: it routes recovery events to
the RecoveryService and history events to the configured
HistoryLoggingService.
The service is selected by one key:
| Config key | Default |
|---|---|
tez.history.logging.service.class | org.apache.tez.dag.history.logging.impl.SimpleHistoryLoggingService |
The real implementations on master:
| Class | Module | Sink |
|---|---|---|
SimpleHistoryLoggingService | tez-dag (built-in default) | Text file in the container log dir; tez.simple.history.logging.dir overrides |
DevNullHistoryLoggingService | tez-dag | Discards everything (set to disable logging) |
ATSHistoryLoggingService | tez-plugins/tez-yarn-timeline-history | YARN Timeline Server (ATS v1) |
ATSV15HistoryLoggingService | tez-plugins/tez-yarn-timeline-history-with-fs | ATS v1.5 (writes to a filesystem the Timeline reader scans) |
ProtoHistoryLoggingService | tez-plugins/tez-protobuf-history-plugin | Protobuf-encoded events under tez.history.logging.proto-base-dir |
There is also tez-yarn-timeline-history-with-acls (ACL-aware ATS variant) and
tez-yarn-timeline-cache-plugin (the Timeline cache-plugin the UI uses to group
DAGs). Confirm the full set with ls tez-plugins/.
Filtering by log level
HistoryEventHandler gates each event through shouldLogEvent, which compares
the DAG's HistoryLogLevel against the event type's level:
// tez-dag HistoryEventHandler.shouldLogEvent(...)
if (dagLogLevel.shouldLog(historyEvent.getEventType().getHistoryLogLevel())) {
return shouldLogTaskAttemptEvents(event, dagLogLevel);
}
return false;
HistoryLogLevel (tez-api, org.apache.tez.dag.api) is an ordered enum:
NONE < AM < DAG < VERTEX < TASK < TASK_ATTEMPT < ALL, with DEFAULT = ALL. Set
tez.history.logging.log.level=VERTEX to suppress the per-attempt firehose on a
DAG with millions of tasks — a real scalability lever, since
TASK_ATTEMPT_FINISHED events dominate the volume. There is a finer filter,
tez.history.logging.taskattempt-filters, that drops attempts with specified
TaskAttemptTerminationCause values when the level is TASK_ATTEMPT.
Warning: Dropping to
AMorDAGlevel makes the Tez UI's per-task drill- down go blank — the events simply were never emitted. That is a debugging trade-off, not a bug. If a user reports "the UI shows the DAG but no tasks," checktez.history.logging.log.levelbefore anything else.
Tez UI data flow
ls tez-ui/src/main/webapp/app/ # Ember app: adapters, serializers, models, routes
ls tez-plugins/tez-history-parser/src/main/java/org/apache/tez/history/parser/
The Tez UI is an Ember.js single-page app (tez-ui/). It does not read the
AM directly. The flow is: AM state machines → HistoryEventHandler →
HistoryLoggingService → sink (Timeline Server, or proto/simple files) → the UI
reads via REST. With ATS, the UI's adapters query the Timeline Server's entity
REST API; the tez-yarn-timeline-cache-plugin supplies the DAG-grouping and
domain logic the Timeline reader needs. For proto/simple logs there is
tez-history-parser (tez-plugins), whose parsers — ATSFileParser,
ProtoHistoryParser, SimpleHistoryParser — reconstruct the DAG object model
from persisted events so offline tools can consume it.
Those offline tools are the job-analyzer plugins in
tez-tools/analyzers/job-analyzer — real, runnable analyzers such as
SkewAnalyzer, SpillAnalyzerImpl, SlowNodeAnalyzer, CriticalPathAnalyzer,
and ShuffleTimeAnalyzer. They read the parsed history and emit CSV. That is the
supported path for "diff two runs' counters" — see also tez-tools/counter-diff.
Note: Don't over-model the UI in your head. From a contributor's angle the only invariant that matters is: the UI can only show what a
HistoryLoggingServicepersisted. If a field is missing in the UI, trace it back to the event that should carry it and check whether it was emitted at the active log level.
DAG visualization: the .dot file
rg -n "generateDAGVizFile|\.dot|TEZ_GENERATE_DEBUG_ARTIFACTS" \
tez-dag/src/main/java/org/apache/tez/Utils.java \
tez-dag/src/main/java/org/apache/tez/dag/app/DAGAppMaster.java \
tez-dag/src/main/java/org/apache/tez/dag/app/dag/impl/DAGImpl.java
rg -n "TEZ_GENERATE_DEBUG_ARTIFACTS" \
tez-api/src/main/java/org/apache/tez/dag/api/TezConfiguration.java
Yes — the AM emits a Graphviz .dot file, but only when debug artifacts are
enabled. The gate is:
| Config key | Default |
|---|---|
tez.generate.debug.artifacts | false |
When true, DAGAppMaster.writeDebugArtifacts and DAGImpl call
Utils.generateDAGVizFile (tez-dag, org.apache.tez.Utils). It walks the
DAGPlan protobuf, emitting a node per vertex (labeled with the processor's
short class name, and — when a scheduler is present — a priority), boxes for
each root input/leaf output, and an edge per EdgePlan labeled with its
dataMovementType and schedulingType:
// tez-dag org.apache.tez.Utils.generateDAGVizFile(...)
String outputFile = "";
if (logDirs != null && logDirs.length != 0) {
outputFile += logDirs[0] + File.separator; // → AM container log dir
}
outputFile += dagId.toString();
if (scheduler != null) {
outputFile += "_priority"; // second pass, once priorities exist
}
outputFile += ".dot";
graph.save(outputFile);
So the file lands in the AM's first log directory as <dagId>.dot (and
<dagId>_priority.dot once vertex priorities are known — the second write
overrides the first). Render it locally:
# after copying the .dot out of the AM container log dir
dot -Tpng dag_1700000000000_0001_1.dot -o dag.png
dot -Tsvg dag_1700000000000_0001_1_priority.dot -o dag.svg
This is the DAG plan only — topology, edge types, priorities. It carries no
counters or diagnostics; for those, use the UI or the history log. Enabling
tez.generate.debug.artifacts also writes the DAG plan as text
(writePBTextFile), which is the fastest way to confirm what plan the client
actually submitted (see dag-client.md and
dag-model.md).
Reading exercise
Work these against the checkout; each is a one-liner that returns hits.
cd /Users/s0x/src/oss-repos/tez
# 1. Read the whole TaskCounter enum — know every member before you cite one.
sed -n '1,240p' tez-api/src/main/java/org/apache/tez/common/counters/TaskCounter.java
# 2. Every place the runtime library increments a counter.
rg -n "\.increment\(|findCounter\(" tez-runtime-library/src/main/java | head -30
# 3. Trace the LimitExceededException kill path end to end.
rg -rn "LimitExceededException" tez-api/src/main/java tez-dag/src/main/java
# 4. Confirm aggregation uses AggregateTezCounters (min/max), not plain sums.
rg -n "AggregateTezCounters|aggrAllCounters" \
tez-dag/src/main/java/org/apache/tez/dag/app/dag/impl/VertexImpl.java
# 5. Count diagnostic append sites per level — build a mental model of the flow.
rg -c "addDiagnostic|addDiagnosticInfo" \
tez-dag/src/main/java/org/apache/tez/dag/app/dag/impl/TaskAttemptImpl.java \
tez-dag/src/main/java/org/apache/tez/dag/app/dag/impl/VertexImpl.java \
tez-dag/src/main/java/org/apache/tez/dag/app/dag/impl/DAGImpl.java
# 6. See how a status poll decides whether to attach counters at all.
rg -n "GET_COUNTERS|getDAGStatus" \
tez-dag/src/main/java/org/apache/tez/dag/app/dag/impl/DAGImpl.java
Then, on a real run: enable tez.generate.debug.artifacts=true, submit a small
DAG, pull <dagId>.dot from the AM log dir, render it, and cross-check the edge
dataMovementType labels against the plan you built in
logical-physical.md. Finally open the same DAG in the
Tez UI and confirm the per-vertex counter min/avg/max matches what
AggregateTezCounters would compute from the tasks you see.
Common bugs and symptoms
| Symptom | Likely cause | Where to look |
|---|---|---|
DAG ends FAILED with every task succeeded | Aggregate counter count crossed tez.counters.max (1200) | AM log Counters limit exceeded: Too many counters: N max=1200; DAGImpl.finished |
| Two distinct counters collapse into one | Names exceed tez.counters.counter-name.max-length (64) and truncate to the same prefix | Limits.filterName; shorten the names |
Client DAGStatus has no counters | Poll didn't pass StatusGetOpts.GET_COUNTERS | DAGImpl.getDAGStatus; the caller's status request |
| DAG-level diagnostic says "VERTEX_FAILURE" but not the real exception | Top-level diagnostics only summarize; the stack is at attempt level | History log / UI; TaskAttemptImpl.diagnostics |
| UI shows the DAG but no tasks/attempts | tez.history.logging.log.level below TASK/TASK_ATTEMPT | HistoryEventHandler.shouldLogEvent; raise the level |
NUM_FAILED_TASKS > 0 but DAG succeeded | Counter tallies failed attempts; retries then succeeded | DAGCounter; correlate with NUM_SUCCEEDED_TASKS |
SHUFFLE_BYTES_TO_DISK ≫ SHUFFLE_BYTES_TO_MEM | Fetcher exhausted its memory budget, spilling to disk | tune shuffle memory (see shuffle-sort.md) |
OUTPUT_BYTES_PHYSICAL ≈ OUTPUT_BYTES_WITH_OVERHEAD | Compression is doing nothing; paying CPU for no gain | change/disable the codec |
No .dot file in the log dir | tez.generate.debug.artifacts is false (default) | set it true and resubmit |
| Counter group absent from history | Counter never incremented — zero-valued counters aren't created | check the increment path in the processor/input |
Validation: prove you understand this
- Name the four standard framework/FS counter groups and the exact class that
defines each. Which two are backed by a dense enum-indexed array and which by
a
HashMap, and why does that distinction matter fortez.counters.max? - A DAG runs 2,000 tasks, all succeed, and it still ends
FAILED. Give the precise mechanism — which method throws, what it was doing, and the config key and default that set the ceiling. - Two attempts of the same task report different counter values. Explain which attempt's counters survive to the task level, the method that chooses, and what happens to the loser's counters.
- Trace one
counters.findCounter("MyApp","ROWS").increment(1)call in a processor all the way toDAGStatus.getDAGCounters()on the client, naming every class it passes through and the event that carries it off the task JVM. - Distinguish
SHUFFLE_BYTES,SHUFFLE_BYTES_TO_MEM,SHUFFLE_BYTES_TO_DISK, andSHUFFLE_BYTES_DISK_DIRECTusing the enum's own definitions, and give the arithmetic relationship the Javadoc states among the first three. - Given the AM diagnostic
DAG did not succeed due to VERTEX_FAILURE. failedVertices:1 killedVertices:0, name the four levels whose diagnostics lists you would read to reach the underlying Java stack, and the exact class/field that stores each copy. - You set
tez.history.logging.log.level=DAG. Predict exactly what disappears from the Tez UI and why, citing the method inHistoryEventHandlerthat makes the decision and the ordering of theHistoryLogLevelenum.