Logging and Metrics
A VMM that runs thousands of microVMs per host and powers Lambda and Fargate cannot be debugged with
println!. Firecracker ships two structured observability subsystems: a logger (human-readable
log lines at configurable levels, to a file or pipe) and a metrics system (a tree of numeric
counters, serialized as JSON, flushed on demand or on a timer). Together they are how an operator
answers "is this microVM healthy, and if not, where did it break" without attaching a debugger to a
process that is, by design, seccomp-jailed and hard to introspect.
This chapter covers the logger (PUT /logger, levels, log_path), the metrics system (PUT /metrics, the serialized counter tree, the FlushMetrics action), the structure of the
instance/Firecracker metrics, and the log-instrument/tracing tooling — and how operators actually
use all of it to debug.
Note: Both subsystems are configured before boot by pointing them at a path (a file or a named pipe). Firecracker writes structured output to that path; it does not run an agent or open a network socket. In production the path is usually a FIFO the orchestrator drains, so the jailed VMM needs no extra privileges to be observable.
The logger
rg -n "struct Logger|LoggerConfig|log_path|level|LevelFilter|fn init|show_level|show_log_origin" src/vmm/src/logger/
find src/vmm/src/logger -name "*.rs"
The logger is configured by PUT /logger (pre-boot) or the logger section of a config file:
curl -X PUT --unix-socket /tmp/fc.sock --data '{
"log_path": "fc.log",
"level": "Debug",
"show_level": true,
"show_log_origin": true
}' http://localhost/logger
| Field | Meaning |
|---|---|
log_path | file or named pipe to write log lines to |
level | one of Error, Warn, Info, Debug, Trace (verify casing on your branch) |
show_level | prefix each line with its level |
show_log_origin | include the source file/line that emitted the line |
Levels filter monotonically: Debug shows everything down to Debug; Error shows only errors.
Crucially, the logger is set up before the heavy machinery starts, so configuration errors and boot
failures land in the log. Inside the code, logging goes through the standard log macros
(error!, warn!, info!, debug!, trace!) — rg -n "error!\(|warn!\(|info!\(" src/vmm/src/ to
see call sites.
The metrics system
rg -n "struct .*Metrics|FirecrackerMetrics|SharedIncMetric|SharedStoreMetric|IncMetric|StoreMetric|fn flush" src/vmm/src/logger/
Metrics are a fixed tree of counters, not arbitrary key/values. Firecracker defines a global
metrics structure (find it with rg -n "FirecrackerMetrics|struct .*Metrics" src/vmm/src/logger/)
whose fields are sub-structs per subsystem (API server, block, net, vsock, vcpu, seccomp, signals,
…), each containing individual counters. The counter types:
| Metric type | Behavior |
|---|---|
SharedIncMetric (IncMetric) | monotonically increasing counter; inc()/add(n) (e.g. block.read_count) |
SharedStoreMetric (StoreMetric) | stores a latest value, not a running sum (e.g. a gauge) |
They're "shared" because multiple threads (vCPU threads, the VMM thread) update them concurrently —
the types use atomics so increments are lock-free. Configure the sink with PUT /metrics:
curl -X PUT --unix-socket /tmp/fc.sock \
--data '{"metrics_path":"fc-metrics.fifo"}' http://localhost/metrics
On flush, the whole tree is serialized to JSON and written to metrics_path as one line, e.g.
(shape, abbreviated):
{"utc_timestamp_ms":...,"api_server":{"process_startup_time_us":...},
"block":{"read_count":1234,"write_count":56,"queue_event_count":...,"flush_count":...},
"net":{"rx_packets_count":...,"tx_packets_count":...},
"seccomp":{"num_faults":0},"signals":{"sigbus":0,"sigsegv":0},
"vcpu":{"exit_io_in":...,"exit_mmio_read":...,"failures":...}}
Each flush emits a delta-able snapshot: counters are cumulative, so an operator diffs two flushes to get rates.
Flushing metrics
rg -n "FlushMetrics|flush_metrics|metrics_interval|periodic|TimerFd|fn flush" src/vmm/src/
Metrics are flushed in two ways:
- On demand —
PUT /actions {"action_type":"FlushMetrics"}writes one snapshot immediately. - Periodically — Firecracker also flushes on a timer (a timerfd registered with the EventManager), so a draining orchestrator gets a steady stream without polling the API.
flowchart LR
Counters["FirecrackerMetrics tree (atomic counters)"] --> Trigger{flush trigger}
Trigger -->|"PUT /actions FlushMetrics"| Ser["serialize tree → JSON"]
Trigger -->|"periodic timerfd"| Ser
Ser --> Path["write one line to metrics_path (file/FIFO)"]
Path --> Drain["orchestrator drains + ships to monitoring"]
Tip: Because each flush is a full cumulative snapshot, the natural monitoring pattern is "ship every line, diff consecutive lines for rates, alert on counters that should stay zero" —
seccomp.num_faults,signals.sigbus/sigsegv, and*.failuresare the ones that should never move.
The instance/Firecracker metrics structure
rg -n "struct .*Metrics" src/vmm/src/logger/ | head -40
rg -n "BlockDeviceMetrics|NetDeviceMetrics|VcpuMetrics|SeccompMetrics|SignalMetrics|ApiServerMetrics" src/vmm/src/
The tree mirrors the architecture, which makes it a map of where to look when something breaks:
| Metrics group | Tells you about |
|---|---|
api_server | request counts, parse failures, startup timing |
block / net / vsock / balloon / entropy | per-device I/O counts, queue events, rate-limiter throttles, errors |
vcpu | VM-exit counts by type, run failures |
seccomp | filter faults (should be 0) |
signals | SIGBUS/SIGSEGV counts (should be 0) |
mmds | metadata request counts |
latencies_us | boot/load/pause/resume timings (verify name on your branch) |
When a device misbehaves, its metrics group is the first place to look — a climbing *.failures or a
nonzero error counter localizes the fault before you ever open a log.
log-instrument and tracing
find src/log-instrument src/log-instrument-macros -name "*.rs" 2>/dev/null
rg -n "log_instrument|#\[instrument|trace!|clippy-tracing" src/
Firecracker carries an in-tree tracing facility, log-instrument (plus its proc-macro crate
log-instrument-macros and the CI helper clippy-tracing). It can instrument function entry/exit so
that, at Trace level, you get a call trace through the code — invaluable when tracing a request from
the API socket all the way to a KVM_RUN exit. It is normally compiled out / disabled to keep the
fast path lean; enable it when you are deliberately tracing a path. clippy-tracing is the tool that
keeps the instrumentation annotations consistent across the tree (a CI/lint concern, not a runtime
one).
How operators debug with these
A realistic debugging loop:
symptom: a microVM's guest network "feels stuck"
1. metrics: diff two flushes → net.tx_packets_count flat, net.tx_rate_limiter_throttled climbing
→ it's being throttled, not broken
2. logs at Debug: confirm rate-limiter messages, find the configured bucket size
3. fix: PATCH the network-interface rate limiter, or correct the config
vs.
symptom: VMM process vanished
1. metrics last flush: seccomp.num_faults > 0 → a denied syscall (a real bug or a missing rule)
OR signals.sigsegv > 0 → a fault in the VMM
2. log tail: the SIGSYS/SIGSEGV handler logged the offending syscall / address
→ see signals-shutdown-and-reset.md and seccomp-filtering.md
The two subsystems are complementary: metrics tell you that and where (which subsystem's counter moved); logs tell you why (the message and origin). Neither requires attaching to the process, which matters because the process is jailed.
Reading exercise
# 1. Logger config and levels.
rg -n "LoggerConfig|log_path|LevelFilter|show_log_origin" src/vmm/src/logger/
# 2. The metrics tree and counter types.
rg -n "struct .*Metrics|SharedIncMetric|SharedStoreMetric|IncMetric|StoreMetric" src/vmm/src/logger/
# 3. Configure both and trigger a flush by hand.
# PUT /logger, PUT /metrics, then:
# curl -X PUT --unix-socket /tmp/fc.sock --data '{"action_type":"FlushMetrics"}' http://localhost/actions
# then read the metrics file.
# 4. The periodic flush timer.
rg -n "FlushMetrics|metrics.*timer|TimerFd|periodic" src/vmm/src/
# 5. Per-device metrics increments at the call site.
rg -n "\.inc\(\)|METRICS\.|\.add\(" src/vmm/src/devices/virtio/block/
# 6. The tracing facility.
rg -n "log_instrument|#\[instrument|clippy-tracing" src/
Answer:
- How is the logger configured, what are the levels, and why must it be set up before boot?
- What kind of structure are metrics? Contrast
IncMetricandStoreMetric, and explain why the types are "shared." - What are the two ways metrics get flushed, and what does a single flush contain?
- Name four metrics groups and what each tells you about the running microVM.
- Which counters should always be zero, and what does it mean if they're not?
- What is
log-instrumentfor, and why is it normally compiled out?
Common bugs and symptoms
| Symptom | Root cause | Where to look |
|---|---|---|
| No log output at all | log_path not set, or set after the failure occurred | PUT /logger ordering; config-file logger section |
| Log file grows unboundedly | Trace/Debug left on in production | lower the level |
| Metrics file empty | never flushed (no FlushMetrics, periodic flush not firing) | FlushMetrics action; the flush timerfd |
| Can't compute rates | reading absolute counters as if they were deltas | counters are cumulative — diff two flushes |
seccomp.num_faults nonzero | a denied syscall — real bug or a missing filter rule | seccomp-filtering.md |
| Adding a metric doesn't appear in output | new field not wired into the serialized tree | the metrics struct + its Serialize |
Validation: prove you understand this
- Configure the logger and the metrics sink with two
curlcalls and explain each field. - Describe the metrics counter tree and the difference between an inc-metric and a store-metric.
- Explain on-demand vs periodic flushing and why a flush is a cumulative snapshot.
- Map three subsystems to their metrics groups and name a counter that localizes a fault in each.
- Name the counters that must stay zero and what a nonzero value implies for each.
- Walk a concrete debugging loop that uses metrics to localize and logs to diagnose a stuck device.
Next: signals-shutdown-and-reset.md — the signal handlers whose
counters you just saw (SIGSYS, SIGBUS, SIGSEGV) and the orderly teardown of a microVM.