Skip to content
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
6f46dcb
feat(telemetry): config, envelope, sink, and payload definitions
sirahd Jul 28, 2026
984be35
feat(telemetry): emit from session finalize, Drop, and heartbeats
sirahd Jul 28, 2026
e275ac0
feat(telemetry): simulation route, integration tests, and docs
sirahd Jul 29, 2026
a701369
docs(telemetry): keep the receiving service's storage layout out of t…
sirahd Jul 30, 2026
28f2f6d
fix(telemetry): downloads never reported; no caller finalized the ses…
sirahd Jul 30, 2026
a95b389
fix(telemetry): restore the wasm and Windows builds
sirahd Jul 30, 2026
0983538
docs(telemetry): drop the private repo reference from the api_changes…
sirahd Jul 31, 2026
ec6715b
docs(telemetry): describe the endpoint's failures without naming its …
sirahd Jul 31, 2026
06ac796
fix(deps): bump event-listener to 5.4.2 for RUSTSEC-2026-0221
sirahd Jul 31, 2026
9e02529
fix(telemetry): infer a download's Drop outcome from its progress
sirahd Aug 3, 2026
ac3adae
Merge main into sira/client-transfer-telemetry
sirahd Aug 3, 2026
d639ce6
fix(telemetry): make the in-flight cap process-wide
sirahd Aug 3, 2026
4cd858c
fix(telemetry): emit the envelope's user_agent in snake_case
sirahd Aug 5, 2026
aeec419
fix(telemetry): preserve HTTP status so downloads can report 429 and 5xx
sirahd Aug 5, 2026
72681cb
test(telemetry): drop the two redundant envelope casing tests
sirahd Aug 5, 2026
d1a9f2c
refactor(telemetry): match on StatusCode instead of a u16 conversion
sirahd Aug 5, 2026
6c824da
fix(telemetry): drop the dry_run metric, which could only ever be false
sirahd Aug 5, 2026
173d45a
fix(telemetry): abort rather than finish when a with block raises
sirahd Aug 6, 2026
218528b
docs(telemetry): correct what skipping finish() actually reports
sirahd Aug 6, 2026
a05ec97
docs(openapi): use snake_case for the telemetry fields in prose and e…
sirahd Aug 10, 2026
cc9a9c4
fix(telemetry): drain in-flight documents before the runtime shuts down
sirahd Aug 10, 2026
8af0427
fix(telemetry): a contended metrics snapshot skips one beat, not ever…
sirahd Aug 10, 2026
3d48e2d
chore: refresh the sub-workspace lockfiles
sirahd Aug 10, 2026
9cf0fbf
fix(telemetry): keep stream-group finish and abort responsive to signals
sirahd Aug 10, 2026
94e478c
test(telemetry): assert what finish and abort leave behind on a strea…
sirahd Aug 10, 2026
388238b
fix(telemetry): return a 500 rather than panic on a poisoned lock in …
sirahd Aug 10, 2026
b7ddd15
fix(telemetry): measure peak_concurrency from active permits, not the…
sirahd Aug 10, 2026
92fbc1f
fix(telemetry): log an accepted document at info!
sirahd Aug 10, 2026
e33f4fe
fix(telemetry): log a failed send at info! as well
sirahd Aug 10, 2026
cae4cef
refactor(telemetry): one telemetry_of, since Client is already Send +…
sirahd Aug 10, 2026
3f1119a
perf(telemetry): count items without snapshotting every one of them
sirahd Aug 10, 2026
6098134
style: apply nightly rustfmt
sirahd Aug 10, 2026
f039f8b
refactor(telemetry): drop the HF_HUB env-var opt-out
sirahd Aug 10, 2026
fea0f48
fix(telemetry): close the group on abort, as its callers already promise
sirahd Aug 10, 2026
edad220
fix(telemetry): report an upload commit's own failure, not just the s…
sirahd Aug 10, 2026
cdbd48d
refactor(telemetry): name the upload override finalize_with, matching…
sirahd Aug 10, 2026
2c45835
fix(telemetry): keep dedup metrics readable when finalize fails late
sirahd Aug 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,31 @@ RUST_LOG=info # enable hf-xet logging
HF_XET_LOG_FILE=/tmp/xet.log # write logs to a file (defaults to stdout)
```

### Telemetry

hf-xet reports a small performance summary — byte counts, dedup effectiveness, throughput, and
wall time — to the CAS server at the end of each upload or download. It is best-effort: never
retried, and it cannot delay or fail a transfer. The payload contains **no file names, paths,
hashes, repository ids, or user ids**.

To turn it off, use any of:

```bash
HF_XET_TELEMETRY_ENABLED=0 # hf-xet specific

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's remove mentioning of HF_HUB env vars as disabling telemetry is done programmatically.

HF_HUB_DISABLE_TELEMETRY=1 # shared with the rest of the huggingface_hub stack
HF_HUB_OFFLINE=1 # implies the above
```

Tuning (rarely needed):

```bash
HF_XET_TELEMETRY_FINAL_FLUSH_TIMEOUT=2s # how long a transfer waits for its final report; 0 = don't wait
HF_XET_TELEMETRY_HEARTBEAT_AFTER=5m # long transfers report progress after this; 0 disables
HF_XET_TELEMETRY_HEARTBEAT_INTERVAL=5m # and then at this interval
HF_XET_TELEMETRY_REQUEST_TIMEOUT=5s # per-request budget
HF_XET_TELEMETRY_MAX_IN_FLIGHT=4 # concurrent reports before dropping
```

## Local Development

### Repo Organization
Expand Down
82 changes: 82 additions & 0 deletions api_changes/update_260728_client_transfer_telemetry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# Client transfer telemetry

**Date**: 2026-07-28
**Crates**: `xet-runtime` (new `telemetry` config group), `xet-client`
(`cas_client::telemetry`, `Client` trait), `xet-data` (`telemetry` module, session hooks)

## What changed

The client now reports one performance summary per transfer to `POST /v1/telemetry` on the CAS
server (added server-side in `huggingface-internal/xetcas#1207`). Reporting is best-effort: it is
never retried, never surfaces an error, and never blocks data movement.

### New config group: `telemetry`

| Field | Env var | Default |
|---|---|---|
| `enabled` | `HF_XET_TELEMETRY_ENABLED` | `true` |
| `heartbeat_after` | `HF_XET_TELEMETRY_HEARTBEAT_AFTER` | `300s` |
| `heartbeat_interval` | `HF_XET_TELEMETRY_HEARTBEAT_INTERVAL` | `300s` |
| `request_timeout` | `HF_XET_TELEMETRY_REQUEST_TIMEOUT` | `5s` |
| `final_flush_timeout` | `HF_XET_TELEMETRY_FINAL_FLUSH_TIMEOUT` | `2s` |
| `max_in_flight` | `HF_XET_TELEMETRY_MAX_IN_FLIGHT` | `4` |

`HF_HUB_DISABLE_TELEMETRY` and `HF_HUB_OFFLINE` also force it off, and win over

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's remove mentioning of HF_HUB env vars as disabling telemetry is done programmatically.

`HF_XET_TELEMETRY_ENABLED=1`. These are applied at the end of `XetConfig::with_env_overrides`
rather than through `ENVIRONMENT_NAME_ALIASES`, because that table maps names with identical
polarity and these are inverted.

### `Client` trait gained a method — **with a default body**

```rust
#[cfg(not(target_family = "wasm"))]
fn transfer_telemetry(&self) -> Option<Arc<TransferTelemetry>> { None }
```

**No existing implementor needs to change.** Only `RemoteClient` overrides it; the local,
in-memory, and simulation clients inherit `None` and report nothing.

Note the failure mode this creates: an override with a mistyped signature compiles cleanly and is
silently never called. `xet_data/tests/test_transfer_telemetry.rs` exists to catch that and should
not be deleted.

### Session behavior

- `FileUploadSession::finalize_impl` now delegates to a new private `finalize_inner` and reports on
both the success and error paths. Public signatures are unchanged.
- `FileDownloadSession::finalize` likewise.
- **Both sessions gained a `Drop` impl**, emitting an `aborted`/`dropped` summary when the session
was never finalized. This is the only reporting path for `XetDownloadStreamGroup`, which holds a
download session and has no explicit `finish()`. Anything constructing these sessions in a
non-tokio context is unaffected: `Drop` returns early when there is no runtime handle.
- New public accessors: `FileDownloadSession::client()`, and
`TestEnvironment::telemetry_docs()` under the `simulation` feature.

### Simulation server

`POST /v1/telemetry` is now routed by the local test server, and received documents are readable
via `LocalServer::telemetry_docs()` / `LocalTestServer::telemetry_docs()`.

### New dependencies

`chrono` and `uuid` were added to `xet-client`, gated to non-wasm targets.

## Why

We had no client-side view of upload/download performance, so a throughput regression shipped in
an `hf-xet` release was undetectable. Server-side metrics cover CAS request latency but not
end-to-end client throughput, dedup effectiveness, or where a transfer's wall time goes.

## Notes for downstream agents

- **The metric key set is a contract.** `xet_data/src/telemetry/payload.rs` is the source of truth.
Adding a key is safe; changing an existing key's JSON type is not — Elasticsearch field mappings
are immutable once established, so a type change produces per-document indexing failures and
needs a reindex. `test_upload_key_set_is_exact` and `test_numeric_types_stable` enforce this.
- **All `f64` values must go through the finite guard** in that module. `serde_json` renders NaN
and infinity as `null`, and a single such document poisons the field's mapping.
- **No PII.** The payload carries no file names, paths, hashes, repository ids, or user ids; the
server derives identity from the request's JWT.
- A follow-up PR will add the generated schema artifacts (`telemetry/metrics.schema.json` and
`telemetry/es-index-template.json`) and a CI compatibility gate. Until then the const key lists
in the tests are the only thing pinning the contract.
2 changes: 2 additions & 0 deletions hf_xet/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

84 changes: 84 additions & 0 deletions openapi/cas.openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,57 @@ paths:
description: Unauthorized — Missing/expired token
'403':
description: Forbidden — Token does not have required scope
/v1/telemetry:
post:
summary: Report Client Telemetry
description: |
Ingests a single client transfer-performance document. Fire-and-forget: the client never
retries and ignores the response, so a failure here has no effect on a transfer.

The server enriches each document with its own context (`serverTime`, `env`, `casVersion`,
`clientIp`) and the request token's claims (`repoId`, `userId`, ...), so the client sends
no repository or user identity of its own.
Comment thread
sirahd marked this conversation as resolved.
Outdated

The `metrics` object is a flat map of scalars whose keys are defined by the client. Their
vocabulary is intentionally not enumerated here — see
`xet_data/src/telemetry/payload.rs`, which is the source of truth.

Minimum token scope: `read`.
x-required-scope: read
operationId: postTelemetry
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/TelemetryEnvelope'
examples:
uploadSummary:
summary: Terminal document for a completed upload
value:
time: '2026-07-28T12:00:00.000Z'
event: xet_upload_summary
session_id: 019813f1-0000-7000-8000-000000000000
userAgent: hf_xet/1.5.4
Comment thread
sirahd marked this conversation as resolved.
Outdated
metrics:
schema_version: 1
direction: upload
terminal: true
duration_ms: 4210
outcome: ok
responses:
'200':
description: Accepted — also returned when server-side telemetry is disabled
'400':
description: Bad Request — Missing key, wrong type, or non-ISO `time`
'401':
description: Unauthorized — Missing/expired token
'413':
description: Payload Too Large — Body exceeds 1 MiB
'429':
description: Too Many Requests — Indexing saturated. Retryable, but the client does not retry.
'500':
description: Internal Server Error — Indexing failed
components:
securitySchemes:
bearerAuth:
Expand Down Expand Up @@ -411,5 +462,38 @@ components:
0 = Shard already exists, 1 = SyncPerformed — the Shard was registered. Any 200 OK means success.
required: [result]
additionalProperties: false
TelemetryEnvelope:
type: object
description: |
Body of `POST /v1/telemetry`. Note the deliberate casing mix: `session_id` is snake_case
while `userAgent` is camelCase. The server also accepts `user_agent`, but a body carrying
both spellings is rejected as a duplicate field.
properties:
time:
type: string
format: date-time
description: ISO-8601 / RFC3339. Any offset is accepted; the server normalizes to UTC.
event:
type: string
description: |
`xet_upload_summary` and `xet_download_summary` are terminal, one per transfer.
`xet_transfer_heartbeat` is emitted periodically by transfers that run long enough.
session_id:
type: string
description: |
Groups activity within one client session. Not unique per transfer — an upload and a
download in the same session share it, so use `metrics.transfer_id` to identify a
single transfer.
userAgent:
type: string
metrics:
type: object
description: Flat map of scalar values. Never null, nested, or an array.
additionalProperties:
oneOf:
- type: string
- type: number
- type: boolean
required: [time, event, session_id, userAgent, metrics]


2 changes: 2 additions & 0 deletions wasm/hf_xet_thin_wasm/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions wasm/hf_xet_wasm/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions xet_client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ web-time = { workspace = true }

[target.'cfg(not(target_family = "wasm"))'.dependencies]
axum = { workspace = true, optional = true }
# Telemetry only; it is compiled out on wasm (no XetRuntime::spawn there), which also keeps
# chrono's clock off a target where it needs `wasmbind` to work.
chrono = { workspace = true }
uuid = { workspace = true, features = ["v7"] }
humantime = { workspace = true, optional = true }
futures-util = { workspace = true, optional = true }
redb = { workspace = true }
Expand Down
17 changes: 17 additions & 0 deletions xet_client/src/cas_client/interface.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ use xet_core_structures::xorb_object::SerializedXorbObject;

use super::adaptive_concurrency::ConnectionPermit;
use super::progress_tracked_streams::ProgressCallback;
#[cfg(not(target_family = "wasm"))]
use super::telemetry::TransferTelemetry;
use crate::cas_types::{
BatchQueryReconstructionResponse, FileChunkHashesResponse, FileRange, HttpRange, QueryReconstructionResponseV2,
ShardUploadEvent,
Expand Down Expand Up @@ -107,4 +109,19 @@ pub trait Client: Send + Sync {
file_id: &MerkleHash,
dirty_ranges: Vec<FileRange>,
) -> Result<FileChunkHashesResponse>;

/// This transfer's performance telemetry aggregator, if it has one.
///
/// Defaults to `None` so only [`RemoteClient`](crate::cas_client::RemoteClient) has to
/// implement it; the local, in-memory, and simulation clients inherit the default and are
/// silently excluded from reporting. `RemoteClient` also returns `None` when telemetry is
/// disabled or this is a dry run.
///
/// Because this has a default body, an override with a mistyped signature would compile and
/// silently never be called - the integration tests in `xet_data/tests/test_transfer_telemetry.rs`
/// exist to catch that.
#[cfg(not(target_family = "wasm"))]
fn transfer_telemetry(&self) -> Option<Arc<TransferTelemetry>> {
None
}
}
5 changes: 5 additions & 0 deletions xet_client/src/cas_client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,14 @@ pub mod retry_wrapper;
#[cfg(not(target_family = "wasm"))]
mod shard_upload_v2;
pub mod simulation;
// No `XetRuntime::spawn` on wasm, so there is no way to report without blocking a transfer.
#[cfg(not(target_family = "wasm"))]
pub mod telemetry;

pub use interface::{ShardUploadProgressCallback, ShardUploadProgressType};
pub use progress_tracked_streams::{DownloadProgressStream, ProgressCallback, UploadProgressStream};
#[cfg(not(target_family = "wasm"))]
pub use telemetry::{Direction, TelemetryEnvelope, TransferTelemetry};

#[cfg(not(feature = "elevated_information_level"))]
pub const INFORMATION_LOG_LEVEL: Level = Level::DEBUG;
Expand Down
Loading
Loading