Skip to content

Persist custom and OOTB log fields in state - #633

Open
snowp wants to merge 31 commits into
mainfrom
snow/stateful-log-fields
Open

snowp wants to merge 31 commits into
mainfrom
snow/stateful-log-fields

Conversation

@snowp

@snowp snowp commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Persists custom and OOTB SDK fields in typed state namespaces and resolves them as virtual fields with OOTB > captured log > custom precedence. State admission is authoritative: accepted writes remain live whether journal-backed or bounded in memory; a capacity-rejected runtime update preserves the existing state and metadata, while a rejected startup field is removed from both.

While fields are dual-written, normal and previous-process logs retain their existing inline field maps. Previous-process replay uses its captured state snapshot only for matcher and workflow evaluation, preventing current-process values from leaking into that path. Ring-buffer field elision remains a separately gated rollout.

Handling bd tail, field association with crash reports, etc. are not handled as part of this PR.

@snowp
snowp force-pushed the snow/stateful-log-fields branch from a6fb2db to 1eca5e7 Compare September 9, 2026 12:49
@snowp
snowp requested a balanced review from Copilot September 9, 2026 14:30
@snowp
snowp force-pushed the snow/stateful-log-fields branch from 1eca5e7 to 5d5b97f Compare September 9, 2026 14:34

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

Persistence failures alter live logging behavior, and several typed-state workflow and matcher paths are incorrect or unreachable.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Adds persistent, typed state namespaces for custom and SDK-owned log fields, based on the schema introduced by API PR #204.

Changes:

  • Persists typed custom and OOTB fields and exposes them to matchers/workflows.
  • Extends state storage, recovery, and scope handling.
  • Removes the synthetic session ID from state snapshot artifacts.
File summaries
File Description
fuzz/src/versioned_kv_journal.rs Updates state payload imports.
bd-workflows/src/workflow.rs Centralizes state-value conversion.
bd-workflows/src/metrics.rs Supports new scopes in metric extraction.
bd-workflows/src/engine_test_helpers.rs Maps new scopes in tests.
bd-workflows/src/config.rs Parses new scopes and typed values.
bd-state/src/lib_test.rs Tests field-scope persistence.
bd-resilient-kv/src/versioned_kv_journal/store.rs Stores new scoped maps.
bd-resilient-kv/src/versioned_kv_journal/recovery.rs Updates payload import.
bd-resilient-kv/src/versioned_kv_journal/mod.rs Updates state value types.
bd-resilient-kv/src/versioned_kv_journal/framing_test.rs Updates test imports.
bd-resilient-kv/src/tests/versioned_kv_store_test.rs Updates test imports.
bd-resilient-kv/src/tests/mod.rs Updates test value helpers.
bd-resilient-kv/src/scope.rs Adds custom and OOTB scopes.
bd-resilient-kv/src/lib.rs Re-exports updated payload types.
bd-proto/src/protos/state/with_source/state_payload.rs Generated schema binding update.
bd-proto/src/protos/state/with_source/scope.rs Generated scope binding update.
bd-proto/src/protos/state/state_payload.rs Generated schema binding update.
bd-proto/src/protos/state/scope.rs Generated scope binding update.
bd-proto/src/protos/state/mod.rs Updates generated-module payload aliasing.
bd-logger/src/state_upload.rs Removes snapshot artifact session ID.
bd-logger/src/metadata.rs Exposes initial persistent fields.
bd-logger/src/async_log_buffer.rs Persists field updates in state.
bd-logger/src/async_log_buffer_test.rs Tests typed persistence conversion.
bd-log-matcher/src/matcher.rs Reads typed fields from state.
bd-log-matcher/src/matcher_test.rs Tests typed state matching.
bd-api/src/api.rs Maps API scopes to state scopes.
Review details

Suppressed comments (3)

bd-logger/src/async_log_buffer.rs:911

  • A journal write failure now prevents the custom field from being applied to subsequent logs. This changes the existing inline behavior during disk-full/capacity/I/O errors even though persistence is ancillary; warn about the persistence failure, but still update metadata_collector.
                  {
                    log::warn!("failed to persist custom log field ({key:?}): {e}");
                    continue;

bd-logger/src/async_log_buffer.rs:925

  • An OOTB field update is dropped from live log metadata whenever persistence fails. Preserve the prior best-effort inline behavior by logging the journal error and continuing with update_ootb_field.
                  {
                    log::warn!("failed to persist OOTB log field ({key:?}): {e}");
                    continue;

bd-logger/src/async_log_buffer.rs:950

  • A persistence error now suppresses removal from the in-memory metadata collector, so the field remains attached to logs despite remove_log_field. Log the persistence failure but still perform the live removal.
                  if let Err(e) = state_store.remove(Scope::CustomFields, &field_name).await {
                    log::warn!("failed to remove custom log field ({field_name:?}): {e}");
                    continue;
                  }
  • Files reviewed: 27/27 changed files
  • Comments generated: 5
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread bd-log-matcher/src/matcher.rs Outdated
Comment thread bd-logger/src/async_log_buffer.rs Outdated
Comment thread bd-logger/src/async_log_buffer.rs Outdated
Comment thread bd-workflows/src/config.rs Outdated
Comment thread bd-workflows/src/metrics.rs Outdated
@snowp
snowp force-pushed the snow/stateful-log-fields branch 3 times, most recently from 3af9637 to b345fe5 Compare September 9, 2026 15:08
@snowp
snowp requested a balanced review from Copilot September 9, 2026 15:16

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

Pre-initialization transitions can be lost, unsupported values can match as empty strings, and persistence failures can leave matching state inconsistent with inline fields.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (2)

bd-logger/src/async_log_buffer.rs:988

  • The metadata value is updated before this write, so a failed update leaves an older OOTB state value authoritative. field_value_with_state checks that persisted value before the concrete log fields, causing subsequent logs to carry the new inline value while workflows match and extract the old one. This error path needs an in-memory override or rollback so both views agree.
                    Err(e) => {
                      log::warn!("failed to persist OOTB log field ({key:?}): {e}");
                      continue;

bd-logger/src/async_log_buffer.rs:1032

  • The custom field is removed from metadata before the journal deletion. If deletion fails, the stale custom state becomes the virtual fallback, so workflows continue matching and extracting a field that is no longer emitted inline. Preserve a tombstone/override or roll back the metadata removal when persistence fails.
                    Err(e) => {
                      log::warn!("failed to remove custom log field ({field_name:?}): {e}");
                    },
  • Files reviewed: 32/32 changed files
  • Comments generated: 3
  • Review effort level: Balanced

Comment thread bd-logger/src/async_log_buffer.rs Outdated
Comment thread bd-logger/src/async_log_buffer.rs Outdated
Comment thread bd-log-matcher/src/matcher.rs Outdated
@snowp
snowp force-pushed the snow/stateful-log-fields branch 3 times, most recently from 289d304 to f1e716b Compare September 9, 2026 15:46
@snowp
snowp changed the base branch from main to snow/bd-state-in-memory-fallback September 9, 2026 16:04
@snowp
snowp force-pushed the snow/stateful-log-fields branch from f1e716b to 27f8b58 Compare September 9, 2026 16:04
@snowp
snowp added this pull request to stack #635 September 9, 2026 16:05
@snowp
snowp requested a balanced review from Copilot September 9, 2026 16:43

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

State-backed matching has JSON and presence regressions, and server updates can mutate SDK-owned namespaces.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 32/32 changed files
  • Comments generated: 3
  • Review effort level: Balanced

Comment thread bd-api/src/api.rs Outdated
Comment thread bd-log-matcher/src/matcher.rs
Comment thread bd-log-matcher/src/matcher.rs Outdated
@snowp
snowp force-pushed the snow/stateful-log-fields branch 3 times, most recently from 2f7f468 to 8255e9c Compare September 9, 2026 18:12
@snowp
snowp requested a balanced review from Copilot September 9, 2026 18:44

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

Matcher regressions and stale custom-state handling can produce incorrect workflow and matching results.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (2)

bd-log-matcher/src/matcher.rs:612

  • IsSetValue uses this string-conversion path, so a present typed state value such as binary, map, or array now reports as unset because state_value_as_cow correctly returns None for non-stringable data. State is_set must test key/value presence independently from conversion; the existing test establishes that even an empty value is considered set.
      Self::State(scope, flag_key) => state
        .get(*scope, flag_key)
        .and_then(|value| ResolvedFieldValue::State(value).as_cow()),

bd-log-matcher/src/matcher.rs:609

  • This bypasses FieldsRef::field_value, changing existing string matcher behavior when a captured field has an unsupported string type but a same-key matching-only field is stringable: the old code fell through to the matching-only value, while this now selects the captured value and returns None. Use field_value_with_state here after making that helper preserve FieldsRef::field_value semantics.
      Self::Field(field_key) => resolved_field_value_with_state(fields, state, field_key)
        .and_then(ResolvedFieldValue::as_cow),
  • Files reviewed: 33/33 changed files
  • Comments generated: 3
  • Review effort level: Balanced

Comment thread bd-log-matcher/src/matcher.rs Outdated
Comment thread bd-log-matcher/src/matcher.rs
Comment thread bd-logger/src/async_log_buffer.rs Outdated
@snowp
snowp force-pushed the snow/stateful-log-fields branch from 0f6dc5d to 0a6ab72 Compare September 10, 2026 13:52
Base automatically changed from snow/bd-state-in-memory-fallback to main September 10, 2026 13:58
@snowp
snowp force-pushed the snow/stateful-log-fields branch from 6211493 to 51081b5 Compare September 10, 2026 18:00
@snowp
snowp requested a balanced review from Copilot September 10, 2026 18:47
snowp and others added 2 commits September 10, 2026 18:23
Co-Authored-By: GPT-5 <codex@openai.com>
Co-Authored-By: GPT-5 <codex@openai.com>
@snowp
snowp force-pushed the snow/stateful-log-fields branch from 4788794 to bfa3c66 Compare September 10, 2026 22:28
@snowp
snowp requested a balanced review from Copilot September 10, 2026 22:32

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

Scalar metric state extraction still emits unsupported typed values as empty tags, conflicting with the corrected multi-tag behavior.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 32/32 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Comment thread bd-workflows/src/metrics.rs
snowp and others added 2 commits September 10, 2026 18:56
Co-Authored-By: GPT-5 <codex@openai.com>
Co-Authored-By: GPT-5 <codex@openai.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

Virtual-field fallback can bypass field transformations, and batch clears still mishandle journal-capacity failures.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

bd-logger/src/test/stats_integration.rs:968

  • These assertions still rely on wall-clock polling and can time out under a slow or contended test runner. Since the transition under test is completion of the async stats flush/rotation, wait on an explicit completion notification or lifecycle gate from that worker rather than repeatedly reading the index file.
  • Files reviewed: 33/33 changed files
  • Comments generated: 3
  • Review effort level: Balanced

Comment thread bd-log-matcher/src/matcher.rs
Comment thread bd-versioned-kv/src/versioned_kv_journal/store.rs
Comment thread bd-logger/src/async_log_buffer.rs
snowp and others added 2 commits September 10, 2026 19:36
Co-Authored-By: GPT-5 <codex@openai.com>
Co-Authored-By: GPT-5 <codex@openai.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

State overlays can bypass filter transformations and can override captured OOTB values during previous-process replay.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 35/35 changed files
  • Comments generated: 2
  • Review effort level: Balanced

Comment thread bd-log-filter/src/lib.rs
Comment thread bd-logger/src/async_log_buffer.rs
Co-Authored-By: GPT-5 <codex@openai.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

The builder deep-clones and retains the complete previous-state snapshot, potentially duplicating up to 1 GiB of state.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 36/36 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Comment thread bd-logger/src/builder.rs
Co-Authored-By: GPT-5 <codex@openai.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 Needs a closer look

The cross-cutting persistence, wire-format, and previous-process replay changes warrant final human validation.

Review details
  • Files reviewed: 38/38 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

@snowp
snowp force-pushed the snow/stateful-log-fields branch from a311333 to 8efb6a8 Compare September 11, 2026 01:59
Co-Authored-By: GPT-5 <codex@openai.com>
Comment thread bd-log-filter/src/lib.rs
Comment on lines +93 to +95
/// Matchers may read state-backed custom and OOTB fields through `state`, but transforms never
/// materialize, redact, replace, or remove those values. Later state-aware consumers resolve
/// the same state again, so transforms apply only to fields concretely present on `log`.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Isn't this a problem in the future when we don't dual write? We have to be able to redact/transform what winds up in state also. Maybe this PR is too bug but is there any reason to not just do this now?

IIRC even today we do transforms before matching so how is this any different?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yeah part of this is that it was getting too complex since you end up having to create a new map that holds all the re-written fields from state that needs to be plumbed through in addition to the state reader which holds the current feature flags.

This is all a bit messy since if we want to rewrite state in a way that is reflected in timeline the state transformation needs to be consistently applied in the snapshot as well, but the current Filter logic allows different transformations for different logs. I figured for now we'd keep this simple and figure out how we want this to work for state separately. I can update the comment to be clearer on the path forward, it's something I need to figure out

match value {
Some(value) => {
if let Err(e) = state_store.insert(scope, key.clone(), value).await {
log::warn!("state rejected initial {scope:?} log field {key:?}; dropping it: {e}");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Unrelated to this PR but we should be more consistent about log::warn in the SDK. in the field it's just compiled code taking up space that no one sees. Not sure the best way to handle this as it is useful in local dev.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yeah I agree, it's probably worth a wide discussion. One thing that I have considered is having our own logging facade so that we can do log levels in dev and keep user-visible logs separate. That way we can continue to use this in dev without polluting the release binary

Comment thread bd-logger/src/async_log_buffer.rs Outdated
self
}

#[cfg(test)]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is an AI instruction that I've been meaning to add. It has a tendency to be lazy and leave crud around because it doesn't want to update tests. Make it clean this up or do it in a follow up. Same below if possible. Sometimes these can be moved to helpers inside the test code. Another cleanup is to use a builder vs. having N constructors.

) {
match async_log_buffer_message {
LoggerControl::AddLogField(key, value) => {
if self.metadata_collector.is_ootb_field(&key)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is where I think I would expect filter/transform to be applied before inserting, same below. This and below can probably be a shared helper.

Comment on lines +285 to +286
custom_fields: AHashMap<String, Option<StateValue>>,
ootb_fields: AHashMap<String, Option<StateValue>>,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pretty sure we have already had this conversation several times but I don't understand why we need N maps here vs. a key that includes the type. Seems simpler and probably uses less resources.

Co-Authored-By: GPT-5 <codex@openai.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants