Skip to content

enhancement(app): add support for always-on compressed storage of debug logs - #2116

Open
tobz wants to merge 1 commit into
mainfrom
tobz/debug-logging-ring-buffer
Open

enhancement(app): add support for always-on compressed storage of debug logs#2116
tobz wants to merge 1 commit into
mainfrom
tobz/debug-logging-ring-buffer

Conversation

@tobz

@tobz tobz commented Jul 14, 2026

Copy link
Copy Markdown
Member

Summary

This PR adds support for always-on compressed storage of debug logs to allow for "looking back" at previous debug logs without having to have explicitly set the application's log level to DEBUG or higher.

In many debugging scenarios, it can often be necessary to change configuration settings to collect more verbose output to assist in said debugging. However, this can often be suboptimal because it means the adverse conditions must be triggered again in order to be captured... when if only you had diagnostics/debug output from the initial adverse condition, you could avoid having to spend the time to recreate the issue.

This PR introduces a new logging mechanism that aims to keep as many of the most recent debug logs as possible in a fixed amount of space, in a rotating fashion, such that when we want to debug an adverse condition that recently occurred, we don't have to enable debug logs and then hope the issue happens again or hope that we can reproduce it.. the logs are just already there, and automatically present in flare output. It employs a number of techniques that allow it to store a large number of debug logs, in a nearly lossless form, in a small amount of space.

As part of this PR, we've done the following:

  • created a new CompressedRingBuffer layer implementation for tracing
  • wired it up in saluki-app to collect debug logs by default (configurable), up to 1MiB (that is, it uses a maximum of 1MiB of actual heap to store the values in memory)
  • wired up a diagnostic collector such that the log is present in flare output from ADP

Architecturally, this implementation works by:

  • taking individual log events and breaking them into their constituent pieces -- level, target, message, structured fields -- and then storing them in the most appropriate way: delta/RLE encoding, string interner, log message template extraction, and so on
  • "condensed" events are packed into "segments" with enough information to extract an equivalent represent of the original log events, and then the segments are compressed
  • segments are kept around until we exceed our target maximum in-memory size, with the oldest segments being dropped
  • all of this happens on a dedicated background thread, decoupled by a fixed-size channel (with pre-allocation/string reuse tricks), in order to minimize overhead/latency in the caller-facing side of the logging system, while also ensuring deterministic memory usage

Change Type

  • Bug fix
  • New feature
  • Non-functional (chore, refactoring, docs)
  • Performance

How did you test this PR?

New and existing tests.

References

DADP-2

@tobz tobz added the type/enhancement An enhancement in functionality or support. label Jul 14, 2026
@dd-octo-sts dd-octo-sts Bot added area/core Core functionality, event model, etc. area/observability Internal observability of ADP and Saluki. labels Jul 14, 2026
@datadog-datadog-us1-prod

This comment has been minimized.

@tobz tobz changed the title enhancement(app):add support for always-on compressed storage of debug logs enhancement(app): add support for always-on compressed storage of debug logs Jul 14, 2026
@tobz
tobz force-pushed the tobz/debug-logging-ring-buffer branch from 54ecf80 to a087a94 Compare August 19, 2026 18:15

tobz commented Aug 19, 2026

Copy link
Copy Markdown
Member Author

This stack of pull requests is managed by Graphite. Learn more about stacking.

@pr-commenter

pr-commenter Bot commented Aug 19, 2026

Copy link
Copy Markdown

Binary Size Analysis (Agent Data Plane)

Baseline: ee113cc · Comparison: 59970d9 · diff
Analysis Configuration: stripped binaries · Pass/Fail Threshold: +5%
Sizes: 41.44 MiB (baseline) vs 41.54 MiB (comparison)
Size Change: +101.34 KiB (+0.24%)

✅ Binary size difference within threshold

Changes by Module
Module File Size Symbols
saluki_app::logging::ring_buffer +43.16 KiB 40
hashbrown +21.46 KiB 407
hyper_util +20.86 KiB 33
hyper -19.64 KiB 192
alloc +17.60 KiB 1023
anon.6ceef29bffea882e2c220988e8d60e5d.25.llvm.1339684207499455115 +17.17 KiB 1
anon.1b7e341bd1cac560d3928ea902a2cdc7.1044.llvm.5840801955275616958 -17.00 KiB 1
serde_core -15.15 KiB 422
tracing +13.91 KiB 29
saluki_common::resource_tracking::groups -12.64 KiB 17
anon.6ceef29bffea882e2c220988e8d60e5d.1334.llvm.1339684207499455115 +12.46 KiB 1
anon.b671e73729a1ec756f069624b851320b.494.llvm.14495687269700133346 -12.02 KiB 1
tokio -11.71 KiB 1827
tracing_subscriber +10.29 KiB 176
anon.1b7e341bd1cac560d3928ea902a2cdc7.200.llvm.5840801955275616958 -10.26 KiB 1
anon.6ceef29bffea882e2c220988e8d60e5d.297.llvm.1339684207499455115 +10.26 KiB 1
anon.1b7e341bd1cac560d3928ea902a2cdc7.1024.llvm.5840801955275616958 -9.69 KiB 1
anon.6ceef29bffea882e2c220988e8d60e5d.29.llvm.1339684207499455115 +9.69 KiB 1
rustls -9.54 KiB 24
core +9.54 KiB 6745
Detailed Symbol Changes
    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +0.5% +57.4Ki  +0.6% +44.3Ki    [23589 Others]
  [NEW] +39.6Ki  [NEW] +39.5Ki    agent_data_plane::cli::run::create_topology::_{{closure}}::hf09f4717c533cfde
  [NEW] +38.0Ki  [NEW] +37.9Ki    agent_data_plane::cli::run::handle_run_command::_{{closure}}::h92ebddaa0fa1c324
  [NEW] +33.2Ki  [NEW] +33.0Ki    datadog_agent_commons::ipc::client::RemoteAgentClient::from_client_configuration::_{{closure}}::_{{closure}}::_{{closure}}::heecfaf5c19027808
  [NEW] +31.3Ki  [NEW] +31.1Ki    agent_data_plane::cli::dogstatsd::run_dogstatsd_command::_{{closure}}::hf1e12ee97ec65f0d
  [NEW] +28.5Ki  [NEW] +28.3Ki    agent_data_plane::dogstatsd_contexts::artifact::for_each_record::h1451209fbfe04dd6
  [NEW] +27.3Ki  [NEW] +27.1Ki    core::ptr::drop_in_place<agent_data_plane::cli::run::handle_run_command::{{closure}}>::h145050361fa6b347
  [NEW] +24.9Ki  [NEW] +24.8Ki    agent_data_plane::internal::remote_agent::run_remote_agent_registration_loop::_{{closure}}::h81002b8b87825355
  [NEW] +22.8Ki  [NEW] +22.7Ki    agent_data_plane::main::_{{closure}}::h85fa2e1bfd19d127
  [NEW] +22.3Ki  [NEW] +22.2Ki    _<figment::value::de::ConfiguredValueDe<I> as serde_core::de::Deserializer>::deserialize_struct::hcb9af46447ebc54a
  [NEW] +22.0Ki  [NEW] +21.9Ki    h2::proto::connection::DynConnection<B>::recv_frame::hb6b642c5865cf93a
  [NEW] +21.2Ki  [NEW] +21.1Ki    agent_data_plane::cli::debug::handle_debug_command::_{{closure}}::h2a5fe378dcb8c61f
  [DEL] -21.1Ki  [DEL] -20.9Ki    agent_data_plane::cli::debug::handle_debug_command::_{{closure}}::h61bd5199604a2107
  [DEL] -23.0Ki  [DEL] -22.8Ki    agent_data_plane::main::_{{closure}}::h102b88cf8847817a
  [DEL] -25.3Ki  [DEL] -25.1Ki    agent_data_plane::internal::remote_agent::run_remote_agent_registration_loop::_{{closure}}::h2a978551a2aad94b
  [DEL] -27.2Ki  [DEL] -27.1Ki    core::ptr::drop_in_place<agent_data_plane::cli::run::handle_run_command::{{closure}}>::hd595d2007cfb4519
  [DEL] -28.5Ki  [DEL] -28.4Ki    agent_data_plane::dogstatsd_contexts::artifact::for_each_record::h36777873e269fe8b
  [DEL] -31.1Ki  [DEL] -31.0Ki    agent_data_plane::cli::dogstatsd::run_dogstatsd_command::_{{closure}}::ha65003f831a66cf5
  [DEL] -33.6Ki  [DEL] -33.4Ki    datadog_agent_commons::ipc::client::RemoteAgentClient::from_client_configuration::_{{closure}}::_{{closure}}::_{{closure}}::hf1b4277c3979677d
  [DEL] -37.8Ki  [DEL] -37.7Ki    agent_data_plane::cli::run::handle_run_command::_{{closure}}::h7e4c7c265f2f9f7c
  [DEL] -39.6Ki  [DEL] -39.5Ki    agent_data_plane::cli::run::create_topology::_{{closure}}::h756a072f25a27136
  +0.2%  +101Ki  +0.2% +88.0Ki    TOTAL

@pr-commenter

pr-commenter Bot commented Aug 19, 2026

Copy link
Copy Markdown

Regression Detector (Agent Data Plane)

Run ID: 5f21e5d1-db9a-4898-83b7-af530d51bbea
Baseline: ee113ccb · Comparison: 59970d9e · diff

Optimization Goals: ✅ No significant changes detected

Fine details of change detection per experiment (5)

Experiments configured erratic: true are tagged (ignored) and skipped when determining which experiments regressed or improved. Experiments which are detected as erratic at runtime are tagged (erratic) to flag that the run's sample dispersion was high, but their regression / improvement signal still counts.

experiment goal Δ mean % links
quality_gates_rss_dsd_low memory ⚪ +2.32 metrics profiles logs
quality_gates_rss_idle memory ⚪ +1.86 metrics profiles logs
quality_gates_rss_dsd_heavy memory ⚪ +1.52 metrics profiles logs
quality_gates_rss_dsd_ultraheavy memory ⚪ +1.50 metrics profiles logs
quality_gates_rss_dsd_medium memory ⚪ +1.40 metrics profiles logs
Bounds Checks: ❌ Failed (5)
experiment check replicates observed links
quality_gates_rss_dsd_heavy memory_usage 0/10 ❌ 284 MiB ≤ 250 MiB metrics profiles logs
quality_gates_rss_dsd_low memory_usage 5/10 ❌ 133 MiB ≤ 60 MiB metrics profiles logs
quality_gates_rss_dsd_medium memory_usage 0/10 ❌ 170 MiB ≤ 100 MiB metrics profiles logs
quality_gates_rss_dsd_ultraheavy memory_usage 10/10 ✅ 393 MiB ≤ 420 MiB metrics profiles logs
quality_gates_rss_idle memory_usage 10/10 ✅ 32.6 MiB ≤ 40 MiB metrics profiles logs
Explanation

A change is flagged as a regression when |Δ mean %| > 5.00% in the regressing direction for its optimization goal AND SMP marks the experiment as a regression (is_regression: true). Improvements use the matching criteria for the improving direction. Experiments configured erratic: true (tagged (ignored)) are skipped outright; experiments detected as erratic at runtime (tagged (erratic)) still count, since that flag describes sample dispersion rather than directional certainty. The Δ mean % cell is colored accordingly: 🟢 = improvement, 🔴 = regression, ⚪ = neutral. Reduction in CPU or memory is an improvement; reduction in ingress throughput is a regression. Experiments tagged (no analysis) show ⚠️ n/a: SMP ran them but produced no analysis, usually because a replicate failed and exhausted its retries. Check the SMP report for that experiment's replicate failures.

@tobz
tobz force-pushed the tobz/debug-logging-ring-buffer branch from a087a94 to 45d2157 Compare August 19, 2026 19:11
@tobz
tobz force-pushed the tobz/debug-logging-ring-buffer branch from 45d2157 to 59970d9 Compare August 19, 2026 19:27
@tobz
tobz marked this pull request as ready for review August 19, 2026 19:35
@tobz
tobz requested a review from a team as a code owner August 19, 2026 19:35

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 59970d9e81

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".


tracing_subscriber::registry()
.with(output_layer.with_filter(filter_layer))
.with(ring_buffer.with_filter(LevelFilter::DEBUG))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Retain a readable handle to the captured logs

This moves the ring buffer directly into the global subscriber, while CompressedRingBuffer exposes no read/export API and the processor thread exclusively owns ProcessorState. Since this is the only production construction and no diagnostic collector receives a handle, flare collection cannot retrieve any of the stored events; the feature continuously captures logs that are permanently inaccessible.

Useful? React with 👍 / 👎.

static VALUE_BUF: std::cell::RefCell<String> = const { std::cell::RefCell::new(String::new()) };
}

match self.events_tx.send_ref() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Use a nonblocking send on the logging path

When the 1,024-slot channel fills during a debug-log burst or while the processor is performing level-19 zstd compression, mpsc::blocking::Sender::send_ref waits for a slot instead of dropping the event. Because on_event invokes this synchronously, arbitrary application threads emitting logs can stall behind the compression worker, contrary to the layer's stated nonblocking behavior; use the channel's try-send operation and handle a full result as a dropped event.

Useful? React with 👍 / 👎.

Comment on lines +95 to +99
self.col_timestamps.len()
+ self.col_msg_variables.len()
+ self.col_field_key_indices.len()
+ self.col_field_values.len()
+ self.col_callsite_indices.len()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Account for all allocations in the ring-buffer limit

For workloads with static or varied message templates, this estimate omits col_msg_template_indices, col_field_counts, the string and callsite tables, the persistent cluster cache, and all vector capacities; it also counts each usize callsite entry as one byte. ensure_size_limits relies exclusively on this value, so the configured 2 MiB ceiling can be exceeded by many megabytes (or by an arbitrarily large static message) even while the reported crb_bytes_live remains under budget.

Useful? React with 👍 / 👎.

Comment on lines +297 to +300
// Quick check: split by whitespace and count.
let tokens: Vec<&str> = message.split_ascii_whitespace().collect();
if tokens.len() == *expected_token_count {
for &pos in wildcard_positions.iter() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Revalidate static tokens before using a cached skeleton

After a callsite converges, any later message with the same token count takes this fast path without checking that its non-wildcard tokens still match the cached skeleton. For example, if a dynamic-message callsite converges on Request 1 completed and later emits Request 2 failed, up to 999 events are encoded using the old Request … completed skeleton, silently changing the recovered logs; compare the static positions as well as the token count before returning the cached template.

Useful? React with 👍 / 👎.

@datadog-datadog-us1-prod datadog-datadog-us1-prod Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Datadog Autotest: FAIL

The new logging layer can block event emitters when its queue fills. It also has no flare read path, does not enforce its memory limit, and can write stale message text.

Open Bits AI session

🤖 Datadog Autotest · Commit 59970d9 · What is Autotest? · @DataDog review to ask questions · Any feedback? Reach out in #autotest

static VALUE_BUF: std::cell::RefCell<String> = const { std::cell::RefCell::new(String::new()) };
}

match self.events_tx.send_ref() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Keep the logging send path non-blocking

A log burst can stop request or ingestion work while compression falls behind.

Assertion details
  • Input: More than 1024 matching tracing events arrive before the background compressor can consume them.
  • Expected: The logging path must use a non-blocking send. It must drop an event when the queue is full.
  • Actual: The blocking send waits for free queue space. The event-emitting thread stalls when the 1024-slot queue is full.

Was this helpful? React 👍 or 👎
🤖 Datadog Autotest · What is Autotest? · @DataDog review to ask questions · Any feedback? Reach out in #autotest

};

let processor_state = ProcessorState::new(config);
std::thread::spawn(move || run_processor(events_rx, processor_state));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Expose stored logs to diagnostics

Users cannot get these logs in a flare, so the main feature has no usable output.

Assertion details
  • Input: Any caller creates the compressed ring buffer and later requests diagnostic output.
  • Expected: The layer must expose a read handle. The flare collector must use that handle to read the stored logs.
  • Actual: The code moves all compressed segments into the background thread. It returns only the write-side sender. No diagnostic collector or read handle can access the stored logs.

Was this helpful? React 👍 or 👎
🤖 Datadog Autotest · What is Autotest? · @DataDog review to ask questions · Any feedback? Reach out in #autotest

// callsite index column compresses dramatically -- the serialized RLE is typically a small
// fraction of the element count. We estimate ~1 byte per element as a rough upper bound,
// since most runs are longer than 1.
self.col_timestamps.len()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Count all retained memory

The process can use much more heap than the stated fixed memory limit.

Assertion details
  • Input: Events contain many unique callsites or large unique message templates.
  • Expected: The configured limit must cover all retained ring-buffer allocations.
  • Actual: The size calculation counts only five column lengths. It omits retained strings, patterns, template indexes, field counts, callsite data, hash maps, and allocation capacity.

Was this helpful? React 👍 or 👎
🤖 Datadog Autotest · What is Autotest? · @DataDog review to ask questions · Any feedback? Reach out in #autotest

if !needs_revalidation {
// Quick check: split by whitespace and count.
let tokens: Vec<&str> = message.split_ascii_whitespace().collect();
if tokens.len() == *expected_token_count {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Check fixed tokens before template reuse

The diagnostic log can contain incorrect message text during an incident.

Assertion details
  • Input: A converged callsite emits a new message shape that has the same number of tokens as the cached shape.
  • Expected: The fast path must also compare fixed token positions before it uses the cached template.
  • Actual: The fast path checks only the token count. It reuses the old fixed words for up to 999 events when a message changes to a different shape with the same token count.

Was this helpful? React 👍 or 👎
🤖 Datadog Autotest · What is Autotest? · @DataDog review to ask questions · Any feedback? Reach out in #autotest

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/core Core functionality, event model, etc. area/observability Internal observability of ADP and Saluki. type/enhancement An enhancement in functionality or support.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant