rust-observability
Skill5 starsChanged 10 days agoBSD-3-ClauseView on GitHub
Use when adding or reviewing tracing, logging, or production metrics in Rust, especially in a library or cdylib behind FFI that exposes a telemetry snapshot to its host. Also for metric naming, histogram boundaries, label cardinality, OpenTelemetry context propagation, and exporter shutdown. Triggers on tracing-subscriber, set_global_default, LogTracer, SetLoggerError, EnvFilter, instrument skip_all, android_logger, tracing-opentelemetry, opentelemetry_sdk.
3 other projects use a version of this.
Adopted by
Most recently taken up by
- adxptived/Rust-Skillstheir own version, first seen Oct 17, 2026
- full-stack-skills/rust-skillstheir own version, first seen Oct 15, 2026
- huiali/rust-skillstheir own version, first seen Feb 9, 2026
Other versions3 projects use a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 3
- huiali/rust-skills★ 47same name
- full-stack-skills/rust-skills★ 8same name
- adxptived/Rust-Skills★ 1same name
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository46 other files it gives its agents.
- AGENTS.mdAGENTS.md
- cargo-workflowsSkill
- ffi-error-progress-cancelSkill
- memory-modelSkill
- rust-android-buildSkill
- rust-async-internalsSkill
- rust-borrow-semanticsSkill
- rust-callback-boundsSkill
- rust-cliSkill
- rust-code-styleSkill
- rust-compiler-errorsSkill
- rust-copy-on-writeSkill
- rust-crate-architectureSkill
- rust-crate-releaseSkill
- rust-databaseSkill
- rust-debuggingSkill
- rust-disciplineSkill
- rust-embedded-no-stdSkill
- rust-event-loop-stateSkill
- rust-hot-pathSkill
- rust-ios-buildSkill
- rust-iterator-implSkill
- rust-jniSkill
- rust-lintsSkill
- rust-macrosSkill
- rust-native-linkingSkill
- rust-networkingSkill
- rust-panic-safetySkill
- rust-pattern-semanticsSkill
- rust-performanceSkill
- rust-pin-projectionSkill
- rust-portSkill
- rust-reviewSkill
- rust-sanitizers-miriSkill
- rust-securitySkill
- rust-send-syncSkill
- rust-serdeSkill
- rust-swift-ffiSkill
- rust-tddSkill
- rust-test-toolsSkill
- rust-type-erasureSkill
- rust-unsafeSkill
- rust-varianceSkill
- rust-wasmSkill
- uniffi-boundarySkill
- uniffi-packaging-versioningSkill
The file
Show its rules and text
We split this skill into the rules it gives an agent
18 rules. 0 are common, 7 are distinctive, and 11 are very specific to this project.
0
Common
Most of the 3 edited versions we read keep these.
7
Distinctive
This author's own choices. Worth a look.
11
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 15
One dispatcher install per process: Only the host calls tracing::subscriber::set_global_default, once, during process bootstrap.
- Distinctive: line 34
Diagnostics are observational: No code path reads the outcome of an emission.
- Distinctive: line 43
Run these before you push a change to the observability crate or to an emission.
- Very specific: line 55
Run the tracing-subscriber check once for each engine crate and inner FFI crate.
- Very specific: line 63
Run the hot-path search on each crate that does per-item or per-byte work.
- Distinctive: line 77
Add a repository gate that reads the field tables from the observability crate rather than repeating them, so the gate and the runtime …
- Distinctive: line 168
Never derive it from a forbidden value.
- Distinctive: line 231
No tracing level is off in a release build by default.
- Very specific: line 236
Do not enable the max_level_* or release_max_level_* features of tracing or log in a library.
- Very specific: line 259
Put the dispatcher and the sink registry in one shared observability crate.
- Very specific: line 268
Do not call android_logger::init_once.
- Very specific: line 270
Only a host binary whose filter is fixed at install may use try_init().
- Very specific: line 277
Read references/dispatcher-install.md when you write or change install_dispatcher, register_sink, the fan-out subscriber, the log bridge, …
- Very specific: line 309
Only a host binary and the bootstrap cdylib depend on tracing-subscriber.
- Very specific: line 320
Put a data-plane event that a consumer must see individually into a bounded queue, one per domain.
- Distinctive: line 339
Treat a metric and its labels as a public resource contract.
- Very specific: line 346
Do not keep a thread-local context guard alive across .await.
- Distinctive: line 348
Keep exporter work outside request and data-plane paths.
“Common” is measured: kept by at least 6 in 10 of the 3 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Rust Observability
These rules assume a Rust library that another process, language, or runtime embeds: a cdylib behind FFI, a staticlib, or a crate that a host CLI or service links. The host owns the process. The library owns only its emissions.
The six rules that are not style
- One dispatcher install per process. Only the host calls
tracing::subscriber::set_global_default, once, during process bootstrap. The host is the binary, or the application-owned outermost bootstrapcdylibthat plays that role. The installed subscriber owns a fan-out registry for all embedded sinks. Do not install once per sink or once per FFI boundary: the second install fails, and that boundary stays silent. skip_allplus explicitfields(...)on every#[instrument]. Withoutskip_allthe macro records every argument, throughDebugfor any non-primitive type. Do not adderrorreton a path that can reach a sink you do not control. They record the error throughDisplayand the return value throughDebug.- No
?and no%sigils, and noformat!inside an emission that can reach a sink you do not control. All three produce free text. - Field names come from one declared vocabulary. A name that is not in the vocabulary fails the gate.
- Nothing on the data plane emits an event. Data-plane work increments an
atomic counter or pushes into a bounded queue. It never calls a log macro,
except an
error!orwarn!on an error path, where the slow case is acceptable. - Diagnostics are observational. No code path reads the outcome of an emission. Turning a subscriber on must not change a single output byte.
The gates below enforce rules 1 to 4 and the max_level_* rule in Severity.
The hot-path search in Gates and what they prove finds rule 5 violations,
and the determinism test in Done when proves rule 6.
Gates and what they prove
Run these before you push a change to the observability crate or to an emission.
cargo test --locked -p <observability-crate>
cargo clippy --locked --workspace --all-targets -- -D warnings
cargo tree --locked -p <library-crate> -e normal --target all --prefix none > deps.txt
if grep -q '^tracing-subscriber ' deps.txt; then echo 'FAIL: library links tracing-subscriber'; exit 1; fi
cargo tree -e features -i tracing --workspace # no library under a max_level_* feature
cargo tree -e features -i log --workspace # the same for logRun the tracing-subscriber check once for each engine crate and inner FFI
crate. Gate on the grep, not on the exit status of an inverted
cargo tree -i tracing-subscriber. That command exits 101 with "did not match
any packages" when the library is clean, and it also exits 0 when it prints the
dependency path of a real failure. Do not write ! grep: set -e ignores a
negated command. The cargo tree line above fails only when
Cargo itself fails, so a wrong -p name stops the gate instead of passing it.
Run the hot-path search on each crate that does per-item or per-byte work. It
finds rule 5 candidates for review. The pattern leaves out error! and warn!,
and catches both the qualified and the imported macro forms.
rg -n --type rust \
'\b(trace|debug|info|event|span|trace_span|debug_span|info_span)!|#\[(tracing::)?instrument' \
<hot-path-crate>/srcThe test proves the install sequence and the redaction visitor. The cargo tree
checks prove the dependency rules. None of them proves that a field value is
safe. That stays a review task.
Add a repository gate that reads the field tables from the observability crate rather than repeating them, so the gate and the runtime visitor cannot disagree. The gate rejects:
- a
?or%sigil in an emission, format!inside an emission,#[instrument]withoutskip_all, or witherrorreton a path to an embedded sink,- a field name that is not declared in the vocabulary,
- a crate whose declared coverage state disagrees with the coverage record.
Give the gate a --self-test mode that runs it against one known-bad fixture per
rule, and run that mode first. A gate with no self-test rots into a no-op after
the first refactor of its patterns.
Deny stdio macros in library, FFI, and embedded crates. Stdio bypasses the redacting visitor, and on a mobile or embedded target it reaches no reader.
# clippy.toml
allow-print-in-tests = true// crate root: lib.rs or main.rs
#![deny(clippy::print_stdout, clippy::print_stderr, clippy::dbg_macro)]Exempt only a host-side tool whose job is to write to a terminal. Put
#![expect(clippy::print_stdout, reason = "...")] after the #![deny] line, or
leave print_stdout out of that crate's deny list. An expect before the
deny loses: clippy reports the println! and an unfulfilled expectation.
allow-print-in-tests covers test code inside src/, where a path rule cannot
see it.
Done when
The gates are green. Then review what they cannot see:
- Each field value is safe, not only its name. No correlation identifier is derived from a forbidden value.
- The instrumented entry point is the one the boundary actually calls.
- No event, span, or
#[instrument]sits on a per-item or per-byte path, except anerror!orwarn!on an error path. - Only one bootstrap path installs the dispatcher:
set_global_default, thenLogTracer::init(). A library bootstrap never callsinit()ortry_init(). Aninit()inJNI_OnLoador an FFI init export panics on a second install and can abort the process. Boundary initializers only register sinks. - An embedded sink record holds only declared fields and no message.
- The queue is bounded, drops the oldest, and counts the drop. The drop counter is in the snapshot.
- The determinism test passes. It runs the same operation twice in one process, once with no subscriber and once with a subscriber at the most verbose level, and byte-compares every output artifact. Its negative control mutates one input and asserts that the comparison fails, so a pass cannot be vacuous.
- A panic hook emits only bounded structured fields. It never formats raw
PanicHookInfo, its payload, or a backtrace.
"It emits nothing"
Work down this list before you suspect the instrumentation.
| Check | Symptom when it is the cause |
| Is a sink registered at all? | With no sink the dispatcher's level ceiling is off, and enabled rejects every callsite by design. |
| Was this callsite emitted before the first sink was registered? | The subscriber cached Interest::never() from the empty registry. Return Interest::sometimes() for a mutable registry, or rebuild the interest cache after each mutation. |
| Did the one-time dispatcher install fail? | Another global subscriber won ("a global default trace dispatcher has already been set"). Report the bootstrap error; do not retry from each boundary. |
Do log records from dependencies vanish? | A second log logger was installed first, for example android_logger, so LogTracer got SetLoggerError. Keep one bridge. |
Do log records never arrive, even after a sink registers? | try_init() set log::max_level() to Off at install, when no sink existed. A later sink or reload filter raises only the tracing ceiling. Install with set_global_default plus LogTracer::init(). |
| Did sink registration fail? | The dispatcher is unavailable or the sink ID is already registered. Inspect the distinct registration status. |
| Is the sink's level above the emission's level? | Higher-severity events still arrive; the quiet ones do not. |
Does a dependency enable a tracing or log max_level_* feature? | The callsites are compiled out. cargo tree -e features -i tracing --workspace (or -i log) names the crate under that feature node. |
On a host, is RUST_LOG valid? | EnvFilter::from_default_env prints ignoring ... on stderr, drops each invalid directive, and falls back to error when none is valid. try_from_default_env returns Err instead. |
| Is the instrumented function on the path you exercise? | Unit tests pass, the CLI prints nothing. See step 3 of Add an emission. |
Is the emission in a #[cfg(test)] module? | The release build drops it. |
| Is the crate target in the filter? | RUST_LOG=debug is not the same as RUST_LOG=my_crate=debug when a dependency floods the output. |
Privacy floor
Diagnostic emission and every telemetry snapshot must not carry:
- Device or network identifiers, raw or hashed. A salted hash still supports correlation and dictionary attacks. Emit only aggregate counts or a local non-identifying category.
- Hardware and subscriber identifiers under any encoding.
- Addresses that identify a user's device or its location.
- Secrets, key material, or handshake payload bytes.
- Message or packet payloads. Counters and sizes are allowed. An opaque flow identifier must be random, short-lived, and scoped to one process or session. Never derive it from a forbidden value. Bytes are not allowed.
- User data in a panic,
expect, orunreachable!message. A panic message is a log line: on Android,panic = "abort"copies it into the tombstone "Abort message". Therust-panic-safetyskill owns the redacted panic report.
Audit before a release.
rg -i 'bssid|ssid|imei|imsi|raw_ip|latitude|longitude' \
. --type rust -n | grep -v '// allow:'The grep is a floor, not a proof. Two leaks no regex finds are in the smuggling routes of references/redaction-and-field-vocabulary.md.
Add an emission
#[tracing::instrument(skip_all, fields(stage = stage.code()))]
fn decode(stage: DecodeStage, items: &[Item]) -> Result<Output, EngineError> {
tracing::debug!(item_count = items.len(), "decoding");
todo!()
}- Record a closed code, not the argument.
fields(stage = stage.code())records a small enum code. - Take each field name from the vocabulary tables in your observability crate.
Adding a name means adding it to the table. Judge the value that the name
carries, not the name. A field called
countthat holds a fixed-point coordinate is a leak with a safe-looking name. - Instrument the entry point that the boundary calls. Many crates have two entry points for the same work: a budgeted or cancellable entry for the FFI boundary, and a plain entry for the CLI. Instrumenting the wrong one compiles, passes unit tests, and emits nothing at run time. Exercise the path with the host tool before you trust it.
- Watch
clippy::large_stack_frames. The lint is innursery, so it is off by default. Enable it and setstack-size-thresholdinclippy.toml. A span or an event macro can push a frame past a small threshold such as 4096 bytes. For a once-per-operation call, write#[expect(clippy::large_stack_frames, reason = "...")]. In a hot loop, move the emission into an#[inline(never)]helper, or delete it.
Read references/redaction-and-field-vocabulary.md when you add a field name, write a sink visitor, or review an emission for leakage. It has the vocabulary tables, the three smuggling routes, and the visitor contract.
Severity
| Level | Use for |
error | The operation failed. |
warn | Recoverable degradation. The operation continues. |
info | A boundary event: start, stop, configuration applied. |
debug | Detail for one operation. |
trace | Per-item detail in a control-plane loop, for targeted diagnostics. Never on the data plane. |
Cancellation is debug. A cancelled operation is the caller getting what it
asked for. Reporting it as an error teaches readers to ignore the severity that
matters.
No tracing level is off in a release build by default. The installed host
subscriber and its filter decide which events are enabled. Configure and test
the release filter explicitly. Do not rely on cfg(debug_assertions) unless the
product contract deliberately removes those callsites.
Do not enable the max_level_* or release_max_level_* features of tracing
or log in a library. Cargo unifies features across the build, so one
library's choice compiles out callsites in every crate of the binary. Only the
final binary may set them. The cargo tree -e features gates prove it.
Control plane and data plane
Pick the channel by the path, not by the information you want. tracing events
and spans, and log records bridged into the same subscriber, serve the control
plane: lifecycle, configuration, errors, single-shot diagnostics. The data plane
uses an AtomicU64 counter or a bounded queue. A native trace backend, for
example Perfetto, stays behind a debug-only feature flag.
Any event, span, or #[instrument] on a per-item or per-byte path is rejected
in review, except an error! or warn! on an error path. Run the hot-path
search in Gates and what they prove.
Read references/embedded-telemetry-surface.md when you choose a channel for a new path. It has the per-channel cost and forbidden-use table.
One dispatcher install, multiple sinks
Put the dispatcher and the sink registry in one shared observability crate.
install_dispatcher is the only function that calls set_global_default. Each
FFI boundary only calls register_sink, which adds its sink to the installed
subscriber's fan-out registry. Rows 2 to 5 of "It emits nothing" show the
silent defects. Also:
- Store the install result in a one-time state. Do not retry a failed install from a boundary.
- Report the
set_global_defaultandLogTracer::init()results separately. - Do not call
android_logger::init_once. That logger writes dependency records to logcat past the redacting visitor. - Only a host binary whose filter is fixed at install may use
try_init().
Logging stays optional for start-up. Return install and registration status to the host. Do not panic, and do not turn a diagnostic failure into a domain failure. Test the install in one process-level test, alone in its own integration-test file, because the global install succeeds once per process.
Read references/dispatcher-install.md when
you write or change install_dispatcher, register_sink, the fan-out
subscriber, the log bridge, the install test, or an Android bootstrap sink.
It has the registry API, the interest-cache rebuild rule, the exact init
failure messages, and the six install-test steps.
Host and embedded sink see different things
Host (a CLI, a service, or a test binary): a
tracing-subscriberformatting layer renders the full event, including an error'sDisplaymessage. Write to stderr so stdout stays machine-readable. In a CLI, keep the layer off unless an environment filter is set. The layer renders untrusted text: usetracing-subscriber0.3.20 or later, which escapes ANSI sequences in logged values (RUSTSEC-2025-0055), and gate advisories withcargo deny --config deny.toml --locked check advisories.RUST_LOG=my_pipeline=debug my-cli render input.toml- Bootstrap
cdylib(the application-owned outermost library that a mobile app or plugin host loads): it owns the install and may depend ontracing-subscriber. In a shipped build, its platform sink receives the redacted record, like any embedded sink. On Android, stderr andRUST_LOGdo not apply, andtracing-logcatwrites the full text, message included: add it only in a debug or opt-in build. - Embedded sink (a callback registered across FFI): receives a redacted record — severity, target, callsite name, and the fields that survive the visitor. Never the message. An error arrives as its kind, a frozen enumeration case, and nothing else.
Only a host binary and the bootstrap cdylib depend on tracing-subscriber.
Engine crates, the shared observability crate, and inner FFI library crates do
not. Put the layer in the host instead.
Counters, the bounded queue, and the snapshot
Count data-plane work with AtomicU64::fetch_add(n, Ordering::Relaxed). A
counter needs atomicity, not a happens-before edge; see the memory-model skill.
Two relaxed counters read in one pass are not consistent with each other, so do
not derive an invariant from their ratio.
Put a data-plane event that a consumer must see individually into a bounded
queue, one per domain. On overflow it atomically drops the oldest record, for
example with crossbeam_queue::ArrayQueue::force_push, and counts the drop. The
newest records are the ones you need after a fault. Do not use
tokio::sync::broadcast, an unbounded queue, or a blocking mutex-backed buffer.
The host polls one serialized snapshot that carries the counters and the drained queue. Do not call across the FFI boundary per item or per telemetry event: the per-call cost is the whole budget on a hot path. A one-shot readiness callback is the one exception.
Read references/embedded-telemetry-surface.md when you implement or change the event queue, the snapshot entry point, or readiness, golden-test a snapshot, or work on the telemetry side of panic reporting or on stall detection. It has the ring code and queue contract, the snapshot key-order and scrub-path rules, and the heartbeat.
Production metrics and distributed context
Treat a metric and its labels as a public resource contract. Choose the instrument from the value semantics. Set the unit, histogram boundaries, and a numeric series budget before you emit it. Do not use an identifier, free-text error, URL, or trace ID as a label.
Preserve one OpenTelemetry context across each logical operation. Make the
handoff explicit at async task, HTTP, message, callback, and FFI boundaries.
Do not keep a thread-local context guard alive across .await.
Keep exporter work outside request and data-plane paths. Bound its queue, batch, export time, retry time, and shutdown time. Export failure never changes the domain result.
Read
references/production-metrics-and-propagation.md
when you add a production metric, propagate OpenTelemetry context, change an
exporter, or upgrade the opentelemetry crates. It has the Rust SDK traps: the
version pairing, the millisecond-scaled default histogram buckets, the
thread-based batch processors, and provider shutdown.
Related skills
memory-model— whyRelaxedis correct for counters.rust-async-internals—broadcastLaggedhandling, task shutdown.rust-security—cargo denypolicy and advisory triage.rust-panic-safety— the redacted panic report, hook ownership, unwinding.rust-debugging— reading logcat, tombstones, and a live process.rust-serde—serde_jsonmap order and feature unification.rust-jni— FFI call cost, thread naming for readable logs.ffi-error-progress-cancel— error kinds, progress, cancellation at a boundary.rust-lints— enforcing the clippy configuration above.rust-performance— measuring the cost an emission adds.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

