Reference

Metrics

On this page

Every Prometheus metric the proxy exposes, with labels, types, and bucket boundaries.

The Analytics view of the dashboard shows the storage that delta compression saves, in total and for each bucket.The Analytics view of the dashboard shows the storage that delta compression saves, in total and for each bucket.

GET /_/metrics returns Prometheus text format on the same port as the S3 API. The proxy collects metrics with lock-free atomics on the hot path. It uses no mutexes and no sampling, and the collection has no measurable performance impact.

For scrape configuration, Grafana panels, and alerting rules, see How to monitor with Prometheus and Grafana.

Quick sanity check#

curl -s http://localhost:9000/_/metrics | head -20

# If you have promtool:
curl -s http://localhost:9000/_/metrics | promtool check metrics

Process and build#

MetricTypeLabelsDescription
process_start_time_secondsGauge—Unix timestamp when the process started
deltaglider_build_infoGaugeversion, backend_typeAlways 1. version is empty unless DGP_METRICS_EXPOSE_VERSION=true, because this endpoint is unauthenticated; the authenticated admin API (GET /_/api/whoami with a session) always reports the running version
process_peak_rss_bytesGauge—Peak resident set size (updated on scrape)
process_* (Linux only)various—Standard process collector: RSS, CPU seconds, open FDs, virtual memory

The endpoint is public by default so that any Prometheus can scrape it. Set DGP_METRICS_BEARER_TOKEN to require Authorization: Bearer <token> (or an admin session, which the dashboard uses); anonymous clients then receive 401 and cannot read the metric set, which changes from release to release. See Monitor with Prometheus.

HTTP requests#

MetricTypeLabelsDescription
deltaglider_http_requests_totalCountermethod, status, operationTotal requests by method, HTTP status code, S3 operation
deltaglider_http_request_duration_secondsHistogrammethod, operationRequest latency distribution
deltaglider_http_request_size_bytesHistogrammethodRequest body size distribution
deltaglider_http_response_size_bytesHistogrammethodResponse body size distribution
deltaglider_connections_refused_totalCounter—Connections that the listener closed at once because it had its maximum of open connections (see connection limits)

operation label values (bounded)#

ValueMeaning
list_bucketsGET /
head_rootHEAD /
list_objectsGET /:bucket
create_bucketPUT /:bucket
delete_bucketDELETE /:bucket
head_bucketHEAD /:bucket
post_bucketPOST /:bucket (batch delete)
get_objectGET /:bucket/*key
put_objectPUT /:bucket/*key
delete_objectDELETE /:bucket/*key
head_objectHEAD /:bucket/*key
post_objectPOST /:bucket/*key (multipart)
healtha request to the path /health
statsa request to the path /stats
metricsa request to the path /metrics
unknownany other method, for example OPTIONS

These series count S3 API requests only. The requests to the endpoints under /_/ (the admin UI, the admin API, /_/health, /_/ready, /_/stats, and /_/metrics itself) are not counted.

HTTP request histogram buckets#

  • Duration: default Prometheus buckets (0.005s … 10s)
  • Body sizes: exponential [1KB, 10KB, 100KB, 1MB, 10MB, 100MB]

Delta compression#

MetricTypeLabelsDescription
deltaglider_delta_compression_ratioHistogram—Ratio distribution (delta_size / original_size). Lower = better; 0.1 = 90% saved
deltaglider_delta_bytes_saved_totalCounter—Cumulative bytes saved by delta compression
deltaglider_delta_encode_duration_secondsHistogram—Time spent in xdelta3 encode
deltaglider_delta_decode_duration_secondsHistogram—Time spent in xdelta3 decode
deltaglider_delta_decisions_totalCounterdecisionStorage decision counts

decision label values#

  • delta: stored as a delta patch against the reference baseline
  • passthrough: stored as-is (non-eligible file type, or poor compression ratio)
  • reference: new reference baseline created for a deltaspace

Delta compression histogram buckets#

  • Codec duration: [1ms, 5ms, 10ms, 25ms, 50ms, 100ms, 250ms, 500ms, 1s, 2.5s, 5s, 10s, 30s]
  • Compression ratio: [0.01, 0.05, 0.1, 0.15, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0]

Cache#

MetricTypeLabelsDescription
deltaglider_cache_hits_totalCounter—Reference cache hits (cheap Bytes refcount clone)
deltaglider_cache_misses_totalCounter—Reference cache misses (triggers backend read)
deltaglider_cache_size_bytesGauge—Current weighted cache size (updated on scrape)
deltaglider_cache_entriesGauge—Current number of cached reference entries
deltaglider_cache_max_bytesGauge—Configured max capacity (constant, set at startup)
deltaglider_cache_utilization_ratioGauge—weighted_size / max_capacity (0.0 to 1.0)
deltaglider_cache_miss_rate_ratioGauge—misses / (hits + misses) since startup (0.0 to 1.0)

The ratio gauges are pre-computed, so dashboards and alerts do not need PromQL arithmetic:

deltaglider_cache_utilization_ratio > 0.9   # cache nearly full
deltaglider_cache_miss_rate_ratio > 0.5     # cache thrashing

Backend requests for listings#

MetricTypeLabelsDescription
deltaglider_backend_head_requests_totalCounter—Object metadata (HEAD) requests that the proxy sends to S3
deltaglider_delegated_list_upstream_pages_totalCounter—Backend listing pages that client listings read
deltaglider_delegated_list_probe_requests_totalCounter—Exact-key probes that client listings send to complete a page
deltaglider_listing_facts_misses_totalCounter—Listed objects whose listing facts were not found, so the listing shows their stored size. An object stored before the index existed, or restored from a backend version, counts until a download or a HEAD request writes its index entry
deltaglider_listing_facts_read_errors_totalCounteroutcomeFailed requests that read the listing facts index. retried: the proxy sent the request once more. failed: the second request failed too, and the listing page shows stored sizes where it could not read the index
deltaglider_listing_token_misses_totalCounterkindListed objects of an encrypted S3 backend that have no listing token. passthrough_inverted: the listing shows the exact size, computed from the stored size, and the stored ETag. delta_headed: a HEAD request during the listing gave the size and the ETag of a delta. unresolved: the listing shows the stored size. healed: a background HEAD request wrote a missing token again
deltaglider_listing_facts_requests_totalCounterkindRequests for the listing facts index on S3 (.dg/facts/): list (a listing page reads the original sizes of its objects, or a cleanup finds old entries), put (an upload or a lazy backfill writes an entry), delete (a cleanup drops the entries of overwritten or deleted objects; one batched DeleteObjects request counts once)
deltaglider_list_legacy_continuation_tokens_totalCounter—ListObjectsV2 requests whose continuation token has the old raw-key form instead of the opaque dg1. form. This release still accepts such tokens, and a later release refuses them. A value that stays at zero means that no client depends on the old form

A client listing of an S3-backed bucket costs one list request per page when the page holds deltas or ciphertext that this proxy did not write or read since it started. A list rate far above the client listing rate means that the index holds many old entries.

Encryption conversion#

MetricTypeLabelsDescription
deltaglider_plaintext_reads_totalCounteroutcomeClient reads of objects without encryption in buckets that encrypt. served_in_window: the conversion window of the bucket is open, and the backend stored the object before its cutoff, so the proxy served it as history. refused: the proxy refused the object
deltaglider_conversion_adopted_after_cutoff_totalCounter—Objects without encryption that the re-encrypt job converted although clients could not read them, because the backend stored them after the cutoff of the conversion, or names no time for them

A refused rate above zero in a bucket that encrypts means that clients read objects that the proxy did not encrypt: the history of a bucket whose conversion is not open, or objects that someone writes to the backend around the proxy. Each adopted object is also a row on the Failures tab of its job run. See Plaintext objects in a bucket that encrypts.

Backend downloads#

MetricTypeLabelsDescription
deltaglider_backend_get_body_resumes_totalCounter—Ranged requests that the proxy sent to S3 to get the remaining bytes of a response body that broke off. One download can add up to three

A response body breaks off when the backend stops sending bytes for longer than DGP_S3_STALL_GRACE_SECS (20 seconds by default), or when the connection closes before the last byte. The proxy then requests the remaining bytes of the same object version, up to three times for one download.

What the client sees when the last of these requests fails depends on when the proxy answers. For a delta-stored object, the proxy reads the whole object before it sends the first byte, so the client gets 503 ServiceUnavailable. For a passthrough object, the proxy sends the 200 headers first and then streams the bytes as they arrive, so the response ends before its last byte. A counter that rises steadily means that the backend often stalls.

Backend request timing#

MetricTypeLabelsDescription
deltaglider_backend_request_duration_secondsHistogrambackend, operation, outcomeThe time of each S3 request that the proxy sends to a storage backend or to the coordination bucket. The time starts when the proxy starts the request and ends when the response headers arrive. It includes every retry of the S3 client, the wait between the retries, and the wait in the client's rate limiter. The body of a download streams after the headers, so it is not part of the time
deltaglider_backend_request_attempts_totalCounterbackend, operationThe attempts that these requests sent: the first attempt of each request and every retry

The labels have these values:

  • backend is the name of the backend definition (default for the single backend of storage.backend). The value coordination marks the requests to the coordination bucket: the config DB sync, the leases and the reference locks. The value health marks the requests of the backend health probes, and the value capability marks the requests of the conditional-write capability probes. The value unnamed marks a request that no named backend sends, for example a connection test.
  • operation is the name of the S3 operation, for example HeadObject, GetObject, PutObject, ListObjectsV2, DeleteObject, DeleteObjects, CopyObject or UploadPart.
  • outcome is ok when the backend answered with a success, or with an answer that the proxy reads as information: 404 Not Found (the proxy checks whether an object exists), 304 Not Modified, or 412 Precondition Failed (a conditional write lost). It is cancelled when the proxy dropped the request before it ended, for example because the client disconnected. Every other end is error: another error status, a timeout, a failed connection, or a response that the proxy could not read.

The duration buckets are [10ms, 25ms, 50ms, 100ms, 250ms, 500ms, 1s, 2.5s, 5s, 10s, 30s, 60s].

Each request adds its attempts to the counter, so the difference between the counter and the request count of the histogram is the number of retries. A request that the proxy dropped before its first attempt adds no attempts.

# Retries per second, by backend
sum by (backend) (rate(deltaglider_backend_request_attempts_total[5m]))
  - sum by (backend) (rate(deltaglider_backend_request_duration_seconds_count[5m]))

# The 95th percentile of the HEAD requests, by backend
histogram_quantile(0.95, sum by (backend, le) (
  rate(deltaglider_backend_request_duration_seconds_bucket{operation="HeadObject"}[5m])))

The slow-request log line#

A request that takes DGP_BACKEND_SLOW_REQUEST_SECS (5 seconds by default) or longer logs one WARN line. The time of an upload grows with the size of its body, so an upload (PutObject, UploadPart) gets one more second for each MiB of its body, as its deadline does. Without this allowance, every large upload would log a line. The line says where the time went:

WARN deltaglider_proxy::storage::s3::timing: slow backend request backend="hetzner-fsn1" operation="DeleteObject" bucket="releases" key="v2.4/app.zip" prefix="" body_bytes=0 total_secs=6.212 attempts=3 before_first_attempt_secs=0.002 status=204 attempt_statuses=503,503,204 result=ok
FieldMeaning
backend, operationThe same values as the labels of the histogram
bucketThe bucket of the request. It is empty for a request without a bucket, such as ListBuckets
keyThe object key of the request, percent-decoded. It is empty for a listing
prefixThe prefix of a listing, percent-decoded. It is empty for other requests
body_bytesThe size of the request body: the bytes of an upload, 0 for a request without a body
total_secsThe time of the request, measured like the histogram
attemptsThe attempts that the request sent
before_first_attempt_secsThe time from the start of the request until its first attempt was sent. It holds the wait in the rate limiter of the S3 client and the time to sign the request. It is not sent when the request ended before its first attempt
statusThe HTTP status of the last attempt, or none when the last attempt got no response
attempt_statusesThe HTTP status of each attempt, in order. A - marks an attempt without a response: its connection failed, or its attempt timeout ended it
resultHow the request ended: ok, HTTP <status> for an error status, timeout, connection failure, response failure, cancelled, or failure

The line holds no credentials and no signature, because the proxy reads only the key and the listing prefix from the request. With DGP_LOG_FORMAT=json, the fields are JSON fields of the log entry.

Read the line in this order:

  • A large before_first_attempt_secs means that the request waited before the proxy sent it. The S3 client of each backend has an adaptive rate limiter. After the backend answers 503 SlowDown, the rate limiter slows down the later requests to that backend, so the wait comes from earlier throttles and not from this request.
  • Several attempts with 503 or 500 statuses mean that the backend refused or failed the request, and the S3 client retried it. The wait between the retries grows with each retry, so most of total_secs is the wait and not the backend's answers.
  • One attempt and a small before_first_attempt_secs mean that the backend itself was slow to answer.

Codec concurrency#

MetricTypeLabelsDescription
deltaglider_codec_semaphore_availableGauge—Available xdelta3 subprocess permits. 0 = all slots busy

Multipart uploads#

MetricTypeLabelsDescription
deltaglider_multipart_uploads_inflightGauge—Current in-flight multipart upload count
deltaglider_multipart_sweep_runs_totalCounterphaseMultipart sweeper runs by phase
deltaglider_multipart_sweep_duration_secondsHistogramphaseSweeper run duration in seconds
deltaglider_multipart_swept_uploads_totalCounterstateUploads reclaimed by sweeper, by upload state
deltaglider_multipart_sweep_reclaimed_bytes_totalCounter—Cumulative bytes reclaimed by the sweeper
deltaglider_multipart_sweep_orphan_relay_dirs_totalCounter—Orphan multipart relay directories removed
deltaglider_multipart_sweep_orphan_relay_files_totalCounter—Orphan multipart relay files removed
deltaglider_multipart_sweep_last_uploads_reclaimedGauge—Uploads reclaimed in the latest sweep run
deltaglider_multipart_sweep_last_reclaimed_bytesGauge—Bytes reclaimed in the latest sweep run

Auth#

MetricTypeLabelsDescription
deltaglider_auth_attempts_totalCounterresultAuth attempts: success or failure
deltaglider_auth_failures_totalCounterreasonFailure breakdown: missing_header, invalid_presigned, invalid_access_key, user_disabled, signature_rejected, replay (a replayed signed request), form_post_denied (a browser form upload that was refused)

Auth metrics stay at zero when SigV4 is disabled.

Replication copies#

MetricTypeLabelsDescription
deltaglider_replication_objects_inflightGauge—Replication objects that are copied now
deltaglider_replication_objects_inflight_peakGauge—The highest value of deltaglider_replication_objects_inflight since the start
deltaglider_replication_parts_inflightGauge—Parts of a streaming multipart copy that are copied now
deltaglider_replication_parts_inflight_peakGauge—The highest value of deltaglider_replication_parts_inflight since the start
deltaglider_replication_part_bytes_residentGauge—Bytes held now in the part buffers of streaming copies
deltaglider_replication_part_bytes_resident_peakGauge—The highest value of deltaglider_replication_part_bytes_resident since the start
deltaglider_replication_multipart_parts_totalCounter—Parts that streaming copies uploaded
deltaglider_replication_part_retries_totalCounter—Parts that a streaming copy read again from the point where the read stopped
deltaglider_replication_bytes_streamed_totalCounter—Bytes that went through the streaming multipart copy path
deltaglider_replication_delta_passthrough_bytes_saved_totalCounter—Bytes not sent because a copy shipped a delta as it is (the object size minus the delta size)
deltaglider_replication_list_calls_totalCounter—Listing pages that the replication reconcile walk read, on the source and on the destination
deltaglider_replication_head_calls_totalCounter—Object HEAD requests that the replication reconcile walk sent
deltaglider_replication_dirs_completed_totalCounter—Directories that the replication reconcile walk finished

Event outbox#

MetricTypeLabelsDescription
deltaglider_outbox_queue_full_totalCounter—Object events that had to wait because the queue in front of the outbox writer was full. Each wait delays the response of one write request; no event is dropped. A rising value means that the jobs DB cannot keep up with the write traffic
deltaglider_outbox_queued_eventsGauge—Object events in the queue in front of the outbox writer that are not written to the jobs DB yet. A crash of the process loses these events (the objects themselves are stored). The value is usually close to 0; a high value means that the jobs DB cannot keep up with the write traffic

Config DB sync#

MetricTypeLabelsDescription
deltaglider_config_sync_healthyGauge—1 while the config DB sync works, 0 while a pull or an upload fails or a local change waits for upload. Always 1 without a sync bucket

GET /_/api/admin/config/sync shows the reason when the gauge is 0.

Background tasks#

MetricTypeLabelsDescription
deltaglider_background_task_panics_totalCountertaskPanics of a background task, such as replication-scheduler, lifecycle-scheduler, event-dispatcher or session-cleanup. The proxy logs each panic and starts the task again after a delay that grows from 1 second to 60 seconds. Any value above 0 is a bug to report

Label cardinality#

All label sets are bounded:

LabelMax values
method8 (GET, PUT, HEAD, DELETE, POST, OPTIONS, PATCH, OTHER)
status~15 HTTP status codes in practice
operation16 (see table above)
operation (backend requests)One for each S3 operation that the proxy sends, about 25
backendOne for each backend definition, plus coordination, health, capability and unnamed
outcome3 (ok, error or cancelled)
decision3 (delta, passthrough, reference)
result2 (success or failure)
reason7 (see the Auth table)

No label contains a bucket name or an object key, so no label has unbounded cardinality.

Tokio runtime series (opt-in build flag)#

These series exist only when the binary is built with RUSTFLAGS="--cfg tokio_unstable". A default build omits them entirely, and the operator pages should treat that as "not compiled in", not "all zeros". The same flag also switches on Tokio's schedule-latency crate feature, because Cargo.toml enables that feature only when the flag is set. You do not pass a Cargo feature yourself. Tokio supports the schedule-latency histogram only on 64-bit targets, so a 32-bit build omits that one series.

RUSTFLAGS="--cfg tokio_unstable" cargo build --release
SeriesTypeMeaning
deltaglider_tokio_worker_mean_poll_secondsgaugeWorst worker's mean task poll duration (EWMA). Polls should run microseconds-to-low-milliseconds; a sustained value in the tens of milliseconds means blocking work runs inside the async context.
deltaglider_tokio_global_queue_depthgaugeTasks pending in the runtime's global queue. A healthy runtime keeps this near zero; sustained depth means the workers cannot drain the schedule.
deltaglider_tokio_blocking_queue_depthgaugeTasks waiting for a spawn_blocking thread. Sustained depth means the blocking pool (size DGP_BLOCKING_THREADS) is saturated.
deltaglider_tokio_budget_forced_yields_totalcounterPolls the scheduler force-yielded after exhausting their budget. Rapid growth points at a task hogging a worker in a tight loop. Use rate()/increase().
deltaglider_tokio_poll_time_range_total{range}counterTask polls per poll-duration range (label range, for example 10-100us), summed across workers. Counts in the high ranges are the long polls the mean hides. Use rate()/increase().
deltaglider_tokio_schedule_latency_range_total{range}counterTask wake-ups per schedule-latency range, summed across workers. The schedule latency is the time from the moment a task becomes ready (a socket has data, a lock is free, a timer fires) to the moment a worker starts to poll it. The label range has the same shape as the poll-time series. Use rate()/increase().
deltaglider_tokio_workersgaugeWorker thread count, for context when reading the per-worker series.

Reading guidance: start with worker_mean_poll_seconds. If it is high, request handling contains inline blocking work (the class of problem behind the bucket-usage and periodic-sweep fixes). The mean hides the tail, so confirm with deltaglider_tokio_poll_time_range_total: growth in the high range buckets is what a few long polls look like. If the mean is low and the tail is flat but latency is still poor, check the two queue depths for saturation, then the budget-yield counter for CPU-hogging loops.

The schedule-latency series answers a different question from the poll-time series. Poll time measures how long a task runs once a worker picks it up. Schedule latency measures how long a ready task waits before a worker picks it up. Both histograms use Tokio's default buckets: ten linear ranges of 100 microseconds each, and the last range (from 900 microseconds up) is open-ended.

On a healthy proxy, almost all wake-ups land in the first range (0-100us), and the last range stays nearly flat. A bad value looks like this: rate() of the last range grows under load, and its share of all wake-ups rises to a few percent or more. That means ready tasks wait a millisecond or more for a worker, so every request that passes through those tasks gets slower even though no single poll is long. Read it together with the other series:

  • A high schedule latency with a high poll-time tail means that long polls occupy the workers, and the other tasks queue behind them. Fix the long polls first.
  • A high schedule latency with a flat poll-time tail and a deep global queue means that the runtime has more ready work than workers. The proxy is CPU-saturated, or it has too few worker threads for the load.
  • A high schedule latency on its own, while the queues stay short, usually means that the host does not give the worker threads enough CPU time (CPU throttling from a container limit, or a noisy neighbour).

What's not in /_/metrics#

/_/stats returns aggregate storage statistics (total_objects, total_original_size, total_stored_size, savings_percentage). It requires an admin session (401 without one), because it reveals the size of every bucket. These are intentionally excluded from /_/metrics. The proxy reads them from a per-bucket running counter (object count, logical bytes, stored bytes) that it updates inline on every PUT and DELETE, and not from the Prometheus collectors. The read is O(1). There is no object scan, no 1,000-object cap, and no truncated field. The endpoint keeps a 10-second server-side cache for the all-buckets aggregate; ?bucket=NAME reads one bucket's counter uncached. The counter is per-instance and approximate across a fleet; reconcile it against ground truth with POST /_/api/admin/usage/refresh?bucket=NAME (an uncapped full scan that overwrites the counter). Use /_/stats for admin dashboards; use /_/metrics for Prometheus.

Implementation details#

  • Counters and histograms use the prometheus crate's atomic collectors, so the hot path has no mutex.
  • Gauges requiring state inspection (cache_size_bytes, codec_semaphore_available, process_peak_rss_bytes) are computed lazily on each scrape via O(1) atomic reads.
  • The HTTP metrics middleware sits between TraceLayer and auth, so it captures the full request lifecycle including auth time.
  • The process feature of the prometheus crate adds standard Linux process metrics. On macOS, only process_peak_rss_bytes is populated (via getrusage).