Skip to content

feat(audit): S3-backed audit sink for request capture and replay - #10903

Closed
YiqiuLiu wants to merge 1 commit into
ai-dynamo:mainfrom
YiqiuLiu:feat/s3-audit-sink
Closed

feat(audit): S3-backed audit sink for request capture and replay#10903
YiqiuLiu wants to merge 1 commit into
ai-dynamo:mainfrom
YiqiuLiu:feat/s3-audit-sink

Conversation

@YiqiuLiu

@YiqiuLiu YiqiuLiu commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Summary

Add an s3 audit sink peer to the existing stderr / nats / jsonl / jsonl_gz sinks. Selected at runtime via DYN_AUDIT_SINKS=s3. Records are uploaded as rotated, gzip-compressed NDJSON segments to S3 (or any S3-compatible endpoint), partitioned by deployment and date.

Also includes:

  • A SegmentSink trait extraction from JsonlGzipWriter so the rotation engine can drive both disk and cloud destinations
  • A global head-based DYN_AUDIT_SAMPLE_RATE gate with force_logging bypass
  • Auto-detection of deployment label from operator-injected DYN_PARENT_DGD_K8S_NAME
  • Prometheus metrics for audit upload health
  • Operator CRD addition (spec.audit.aws_s3.irsaRoleArn) for cloud-identity SA management

Motivation

Three offline consumer patterns the existing sinks don't serve:

  1. Integration test fixtures — capture production traffic slices, filter by model/token-count/date, replay as regression fixtures.
  2. Continuous tuning corpus — feed real traffic shapes into autotune sweeps rather than synthetic prompts that miss prefix-cache patterns and tool-call diversity.
  3. Compliance/audit retention — long-term queryable storage for regulatory review.

All three want records in cheap durable storage queryable by Athena/Glue/Spark/jq. S3 is the canonical answer.

Changes

Rust (lib/llm/)

File Change
src/telemetry/jsonl_gz.rs Extract SegmentSink trait + FileSegmentSink. Add roll_interval for time-based rotation. Existing callers unchanged.
src/telemetry/s3_segment_sink.rs New. S3SegmentSink impl using aws-sdk-s3. Buffers segments in memory, uploads on close_segment. Key format: {prefix}/[{deployment}/]YYYY/MM/DD/HH/{pod}-{uuid8}-{seq}.jsonl.gz
src/audit/sink.rs S3AuditSink trait impl + "s3" match arm in parse_sinks_from_env. Bus-lag metrics wiring.
src/audit/handle.rs AuditRecord gains deployment: Option<String> + emitted_at_unix_ms: i64. create_handle adds xxh3-based sampling gate.
src/audit/config.rs AuditPolicy gains S3 fields + sample_rate + deployment (auto from DYN_PARENT_DGD_K8S_NAME).
src/audit/metrics.rs New. Prometheus counters/histograms: dynamo_audit_s3_segments_total, dynamo_audit_records_dropped_total, dynamo_audit_s3_upload_duration_seconds, dynamo_audit_s3_segment_size_bytes.
src/audit/README.md New. IAM trust-policy template, env-var reference, terminationGracePeriodSeconds guidance.
tests/audit_s3_integration.rs New. #[ignore]'d end-to-end test against LocalStack/MinIO.
Cargo.toml (root + lib/llm) Add aws-sdk-s3, aws-config workspace deps.
lib/runtime/src/config/environment_names.rs Register 14 new DYN_AUDIT_S3_* + DYN_AUDIT_SAMPLE_RATE + DYN_AUDIT_DEPLOYMENT consts.

Go (deploy/operator/)

File Change
api/v1beta1/dynamographdeployment_types.go Add AuditSpec with per-cloud sub-blocks (AwsS3, GcpGcs, AzureBlob). Each carries only the cloud-identity field.
internal/controller/dynamographdeployment_controller.go reconcileAuditIdentity() — attaches/removes cloud-identity annotations on the existing <dgd>-k8s-service-discovery SA based on spec.audit. Non-fatal on error.
internal/controller/audit_identity_test.go New. 5 unit tests: add annotation, remove on clear, update in place, no-op when SA missing, multi-cloud.

Design decisions

  • Hot-path safety. emit() is a non-blocking mpsc::send. PutObject runs on a background task with SDK retry (3 attempts, exponential backoff). On terminal failure: log + drop. The audit bus drops oldest records on lag — S3 can never stall inference.
  • No new ServiceAccount. The operator annotates the existing <dgd>-k8s-service-discovery SA rather than creating a separate audit SA. A K8s SA can carry one IRSA-style annotation per cloud (distinct keys) while being bound to multiple K8s Roles.
  • Per-cloud sub-blocks. spec.audit.aws_s3.irsaRoleArn is type-safe and extensible. Future clouds add their own sub-blocks without changing the operator's core logic.
  • Env vars for runtime config. Bucket/region/prefix/sampling/SSE stay as DYN_AUDIT_S3_* env vars — they change more often than identity and shouldn't require operator reconciliation.
  • Sampling is deterministic. xxh3(request_id) % 10000 — same ID always produces the same keep/drop decision. force_logging=true bypasses sampling; request.store=true does not.

Test plan

  • 1117 Rust unit tests pass (includes 15 audit + 19 telemetry + 29 request_trace)
  • 5 Go operator unit tests pass (fake client)
  • cargo fmt --check clean
  • End-to-end validated on EKS cluster: 100 requests captured to S3, Athena-queryable, correct key layout, correct record schema
  • #[ignore]'d integration test passes against MinIO at localhost:4566
  • CI: pre-merge checks (pre-commit hooks, operator checks, CodeQL, docs-link-check)

New environment variables

DYN_AUDIT_SAMPLE_RATE          # [0.0, 1.0], default 1.0
DYN_AUDIT_DEPLOYMENT           # optional override; auto = DYN_PARENT_DGD_K8S_NAME
DYN_AUDIT_S3_BUCKET            # required when sinks=s3
DYN_AUDIT_S3_PREFIX            # default "dynamo-audit"
DYN_AUDIT_S3_REGION
DYN_AUDIT_S3_ENDPOINT_URL      # LocalStack/MinIO
DYN_AUDIT_S3_BATCH_BYTES       # default 1 MiB
DYN_AUDIT_S3_ROLL_BYTES        # default 64 MiB
DYN_AUDIT_S3_ROLL_INTERVAL_MS  # default 60000
DYN_AUDIT_S3_ROLL_LINES
DYN_AUDIT_S3_SSE               # AES256 (default), aws:kms, none
DYN_AUDIT_S3_KMS_KEY_ID
DYN_AUDIT_S3_INSTANCE_ID       # override; defaults to POD_NAME
DYN_AUDIT_S3_CHANNEL_CAPACITY  # default 4096

🤖 Generated with Claude Code


Open in Devin Review

Summary by CodeRabbit

  • New Features

    • Added audit system capturing request/response data with S3 sink support
    • Added cloud-specific audit identity configuration (AWS, GCP, Azure)
    • Added audit sampling and deployment tracking capabilities
    • Added Prometheus metrics for audit operations monitoring
  • Documentation

    • Added comprehensive audit subsystem documentation with Kubernetes and cloud provider setup guidance

@YiqiuLiu
YiqiuLiu requested a review from a team as a code owner June 23, 2026 22:07
@YiqiuLiu
YiqiuLiu requested a review from a team June 23, 2026 22:07
@copy-pr-bot

copy-pr-bot Bot commented Jun 23, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@github-actions

Copy link
Copy Markdown
Contributor

👋 Hi YiqiuLiu! Thank you for contributing to ai-dynamo/dynamo.

Just a reminder: The NVIDIA Test Github Validation CI runs an essential subset of the testing framework to quickly catch errors.Your PR reviewers may elect to test the changes comprehensively before approving your changes.

🚀

@github-actions github-actions Bot added external-contribution Pull request is from an external contributor feat documentation Improvements or additions to documentation deployment::k8s Relates to dynamo deployment in kubernetes labels Jun 23, 2026
@datadog-official

This comment has been minimized.

@devin-ai-integration devin-ai-integration Bot 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.

Devin Review found 4 potential issues.

View 1 additional finding in Devin Review.

Open in Devin Review

// storage (e.g., S3 via IRSA). Whether audit is enabled at runtime
// is controlled by the DYN_AUDIT_SINKS env var, not by this field.
// +optional
Audit *AuditSpec `json:"audit,omitempty"`

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.

🔴 New Audit field silently lost during API conversion — v1alpha1 is storage version

The PR adds Audit *AuditSpec to DynamoGraphDeploymentSpec in v1beta1 but does not add any conversion handling. Since v1alpha1 is still the storage version (see comment at deploy/operator/api/v1beta1/dynamographdeployment_types.go:141-142), every write through the v1beta1 API triggers v1beta1→v1alpha1 conversion for storage. ConvertToDynamoGraphDeploymentSpec at deploy/operator/api/v1alpha1/dynamographdeployment_conversion.go:407-469 does not save the Audit field into the sparse hub payload (hubSave), and ConvertFromDynamoGraphDeploymentSpec at line 100-179 does not restore it from restored. This means any spec.audit value set via v1beta1 is silently dropped on every write and never round-trips. Additionally, knownV1Beta1ConversionFieldSet in deploy/operator/api/v1alpha1/conversion_field_coverage_test.go:32-161 was not updated, so TestV1Beta1ConversionFieldSetIsAcknowledged will fail. This violates the mandatory rule in deploy/operator/api/AGENTS.md: "Every API type change in any version must update the corresponding conversion code and conversion tests."

Prompt for agents
The new Audit field on DynamoGraphDeploymentSpec (v1beta1) has no conversion handling. Since v1alpha1 is the storage version, this field is silently dropped on every write.

Required changes per deploy/operator/api/CONVERSION.md:

1. In deploy/operator/api/v1alpha1/dynamographdeployment_conversion.go, function ConvertToDynamoGraphDeploymentSpec: save src.Audit into the hubSave sparse payload (save.Audit = src.Audit) so it round-trips through v1alpha1 storage.

2. In ConvertFromDynamoGraphDeploymentSpec: restore dst.Audit from the restored hub payload when the live v1alpha1 source cannot represent it (if restored != nil && restored.Audit != nil, set dst.Audit = restored.Audit).

3. Update knownV1Beta1ConversionFieldSet in conversion_field_coverage_test.go to include the new audit sub-fields (audit.aws_s3.irsaRoleArn, audit.gcp_gcs.gcpServiceAccount, audit.azure_blob.clientId).

4. Add focused round-trip conversion tests for the Audit field.

5. Regenerate zz_generated.deepcopy.go (make generate) so DeepCopy covers AuditSpec and its children.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines 89 to 134
Audit *AuditSpec `json:"audit,omitempty"`
}

// AuditSpec carries per-cloud identity metadata the operator attaches to
// the frontend pod's ServiceAccount. Each sub-field maps to a specific
// cloud's workload-identity annotation. Multiple sub-fields may be set
// simultaneously for multi-cloud deployments.
type AuditSpec struct {
// aws_s3 configures AWS IRSA for the S3 audit sink.
// +optional
AwsS3 *AuditAwsS3Spec `json:"aws_s3,omitempty"`

// gcp_gcs configures GCP Workload Identity for a future GCS audit sink.
// +optional
GcpGcs *AuditGcpGcsSpec `json:"gcp_gcs,omitempty"`

// azure_blob configures Azure Workload Identity for a future Azure Blob audit sink.
// +optional
AzureBlob *AuditAzureBlobSpec `json:"azure_blob,omitempty"`
}

// AuditAwsS3Spec holds the IAM role ARN for EKS IRSA. The operator
// translates this into an `eks.amazonaws.com/role-arn` annotation on the
// per-DGD ServiceAccount.
type AuditAwsS3Spec struct {
// irsaRoleArn is the ARN of the IAM role the frontend pod should
// assume via IRSA to write audit segments to S3.
// +kubebuilder:validation:Pattern=`^arn:aws:iam::\d{12}:role/.+$`
IrsaRoleArn string `json:"irsaRoleArn"`
}

// AuditGcpGcsSpec holds the GCP service account for GKE Workload Identity.
// Future use — not implemented in v1.
type AuditGcpGcsSpec struct {
// gcpServiceAccount is the GCP service account email the frontend pod
// should use via GKE Workload Identity.
// +kubebuilder:validation:Pattern=`^.+@.+\.iam\.gserviceaccount\.com$`
GcpServiceAccount string `json:"gcpServiceAccount"`
}

// AuditAzureBlobSpec holds the Azure client ID for AKS Workload Identity.
// Future use — not implemented in v1.
type AuditAzureBlobSpec struct {
// clientId is the Azure AD application (client) ID.
ClientId string `json:"clientId"`
}

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.

🔴 DeepCopy not regenerated — Audit pointer shared after DeepCopyInto

The zz_generated.deepcopy.go was not regenerated after adding Audit *AuditSpec to DynamoGraphDeploymentSpec. The DeepCopyInto method at deploy/operator/api/v1beta1/zz_generated.deepcopy.go:712-757 does *out = *in (shallow struct copy) and then deep-copies each pointer field — but there is no block for Audit. After DeepCopy(), both the original and copy share the same *AuditSpec pointer. Mutations through one will silently corrupt the other. Furthermore, no DeepCopy/DeepCopyInto methods exist for AuditSpec, AuditAwsS3Spec, AuditGcpGcsSpec, or AuditAzureBlobSpec. In a Kubernetes operator, DeepCopy is used pervasively (status updates, before-mutation snapshots, informer caches), so shared pointer mutation can cause data races and incorrect reconciliation behavior.

Prompt for agents
Run make generate (or controller-gen) to regenerate deploy/operator/api/v1beta1/zz_generated.deepcopy.go. This will add DeepCopyInto/DeepCopy methods for AuditSpec, AuditAwsS3Spec, AuditGcpGcsSpec, AuditAzureBlobSpec, and update DynamoGraphDeploymentSpec.DeepCopyInto to deep-copy the Audit pointer field.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +105 to 108
// Forced requests bypass head-based sampling so debugging /
// compliance flags always land in the audit log.
if !force_logging && sample_rate < 1.0 && !is_sampled(request_id, sample_rate) {
return None;

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.

🟡 Sampling applied to store=true requests despite docstring claiming bypass

The sample_rate field docstring at lib/llm/src/audit/config.rs:33-34 states: "Bypassed by force_logging and by request.store == true." However, the implementation at lib/llm/src/audit/handle.rs:107 checks !force_logging — not !force (which is force_logging || store_flag). This means requests with store=true ARE subject to sampling when force_logging=false. When a client explicitly sets store=true to request audit capture and the operator has configured sample_rate < 1.0, those requests can be silently dropped. The test at handle.rs:249-256 confirms this behavior, but it contradicts the documented contract. Either the code should use !force (matching the docstring's stated semantics that store=true bypasses sampling), or the docstring should be corrected to remove the store=true bypass claim.

Suggested change
// Forced requests bypass head-based sampling so debugging /
// compliance flags always land in the audit log.
if !force_logging && sample_rate < 1.0 && !is_sampled(request_id, sample_rate) {
return None;
// Forced requests bypass head-based sampling so debugging /
// compliance flags always land in the audit log.
if !force && sample_rate < 1.0 && !is_sampled(request_id, sample_rate) {
return None;
}
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +17 to +60
pub static AUDIT_S3_SEGMENTS_TOTAL: LazyLock<IntCounterVec> = LazyLock::new(|| {
let opts = Opts::new(
"dynamo_audit_s3_segments_total",
"Total audit segments uploaded to S3",
)
.namespace("dynamo");
IntCounterVec::new(opts, &["result"]).expect("audit_s3_segments_total metric")
});

/// Total audit records dropped, labeled by reason:
/// - "bus_lag": broadcast channel lagged, records were overwritten before the
/// sink worker could consume them.
/// - "channel_full": the per-sink mpsc channel was full (try_send failed).
/// - "serialize_error": serde_json serialization of AuditRecord failed.
pub static AUDIT_RECORDS_DROPPED_TOTAL: LazyLock<IntCounterVec> = LazyLock::new(|| {
let opts = Opts::new(
"dynamo_audit_records_dropped_total",
"Total audit records dropped before reaching the destination",
)
.namespace("dynamo");
IntCounterVec::new(opts, &["reason"]).expect("audit_records_dropped_total metric")
});

/// Histogram of S3 PutObject durations in seconds.
pub static AUDIT_S3_UPLOAD_DURATION_SECONDS: LazyLock<HistogramVec> = LazyLock::new(|| {
let opts = HistogramOpts::new(
"dynamo_audit_s3_upload_duration_seconds",
"Duration of S3 PutObject calls for audit segments",
)
.namespace("dynamo")
.buckets(exponential_buckets(0.01, 2.0, 12).expect("valid histogram buckets"));
HistogramVec::new(opts, &[]).expect("audit_s3_upload_duration_seconds metric")
});

/// Histogram of uploaded segment sizes in bytes (compressed).
pub static AUDIT_S3_SEGMENT_SIZE_BYTES: LazyLock<HistogramVec> = LazyLock::new(|| {
let opts = HistogramOpts::new(
"dynamo_audit_s3_segment_size_bytes",
"Size of uploaded audit segments in bytes (gzip-compressed)",
)
.namespace("dynamo")
.buckets(exponential_buckets(1024.0, 4.0, 10).expect("valid histogram buckets"));
HistogramVec::new(opts, &[]).expect("audit_s3_segment_size_bytes metric")
});

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.

🚩 Prometheus metrics use LazyLock but are never registered with a Registry

The audit metrics in lib/llm/src/audit/metrics.rs use LazyLock for global initialization but are never explicitly registered with Prometheus's default registry or any custom registry. In prometheus 0.14, IntCounterVec::new and HistogramVec::new create metrics but do NOT auto-register them. They must be registered via prometheus::register(...) or registry.register(...) to appear in /metrics scrape output. As written, these metrics are created and incremented correctly in-process, but will never be exposed to Prometheus scrapers. This should be verified — if the codebase has a central registration pattern elsewhere that picks these up, it's fine; otherwise these metrics are silently invisible.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

This PR adds an S3-backed audit sink to the LLM library: a rotating gzip JSONL writer is refactored around a new SegmentSink trait, and a new S3SegmentSink implementation uploads completed segments to S3. AuditRecord gains deployment and timestamp fields; AuditHandle gains head-based sampling. Prometheus metrics track uploads and dropped records. Separately, the Kubernetes operator gains new AuditSpec CRD types and a reconcileAuditIdentity controller method that annotates the discovery ServiceAccount with IRSA/GCP/Azure workload-identity metadata from the deployment spec.

Changes

Rust LLM Audit S3 Sink

Layer / File(s) Summary
AuditPolicy S3 config fields and env-var constants
Cargo.toml, lib/llm/Cargo.toml, lib/llm/src/audit/config.rs, lib/runtime/src/config/environment_names.rs
AuditPolicy gains sample_rate, deployment, and 12 S3 fields populated by an extended load_from_env(). New DYN_AUDIT_* env-var constants are defined and added to the duplicate-name test. AWS workspace dependencies are pinned.
SegmentSink trait and JsonlGzipWriter refactor
lib/llm/src/telemetry/jsonl_gz.rs, lib/llm/src/telemetry/mod.rs, lib/llm/src/request_trace/sink.rs
SegmentSink trait (append_to_segment, close_segment) replaces direct file I/O in GzipBatchWriter. FileSegmentSink preserves file behavior. JsonlGzipSinkOptions gains roll_interval. Worker loop adds a roll_tick timer and closes segments on shutdown and drain.
S3SegmentSink implementation
lib/llm/src/telemetry/s3_segment_sink.rs
Per-seq gzip bytes are buffered in a Mutex<HashMap> and uploaded via put_object on close_segment with optional AES256/aws:kms SSE. S3SegmentIdentity resolves instance via a fallback chain. format_object_key produces timestamped prefix/deployment/instance paths.
AuditHandle sampling and AuditRecord fields
lib/llm/src/audit/handle.rs
AuditRecord gains deployment (omitted when None) and emitted_at_unix_ms. create_handle_with_config performs deterministic xxh3_64-based head sampling; force-logging bypasses it. AuditHandle stores and emits configured deployment.
Prometheus metrics for audit subsystem
lib/llm/src/audit/metrics.rs, lib/llm/src/audit/mod.rs
Four global LazyLock Prometheus metrics: AUDIT_S3_SEGMENTS_TOTAL, AUDIT_RECORDS_DROPPED_TOTAL, AUDIT_S3_UPLOAD_DURATION_SECONDS, AUDIT_S3_SEGMENT_SIZE_BYTES. Module exported via audit/mod.rs.
S3AuditSink wiring and sink routing
lib/llm/src/audit/sink.rs
S3AuditSink builds an S3SegmentSink from AuditPolicy and wraps it in JsonlGzipWriter with roll_interval. parse_sinks_from_env gains an "s3" arm. Worker loops increment AUDIT_RECORDS_DROPPED_TOTAL on bus lag.
S3 integration test and audit README
lib/llm/tests/audit_s3_integration.rs, lib/llm/src/audit/README.md
Ignored Tokio test (test_audit_s3_basic_flow) validates end-to-end upload against LocalStack/MinIO: emits two records, waits for roll, asserts NDJSON line count, deployment field, and emitted_at_unix_ms. README documents IRSA setup, env vars, and S3 key format.

Kubernetes Operator Audit Identity Annotations

Layer / File(s) Summary
AuditSpec CRD types
deploy/operator/api/v1beta1/dynamographdeployment_types.go
DynamoGraphDeploymentSpec gains an optional Audit *AuditSpec field. New types AuditSpec, AuditAwsS3Spec, AuditGcpGcsSpec, and AuditAzureBlobSpec are added with JSON tags and kubebuilder validation patterns.
reconcileAuditIdentity controller method and tests
deploy/operator/internal/controller/dynamographdeployment_controller.go, deploy/operator/internal/controller/audit_identity_test.go
reconcileAuditIdentity fetches the discovery ServiceAccount and synchronizes managed annotations/labels for AWS IRSA, GCP Workload Identity, and Azure Workload Identity based on spec.audit. The call is non-fatal. Five tests cover add, remove, in-place update, missing-SA no-op, and multi-cloud scenarios.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The linked issue #39 concerns NIXL KV transfer flow and scheduler improvements, which are unrelated to S3 audit sink implementation. This PR does not satisfy the requirements of the linked issue. Verify the correct issue number is linked. This PR implements an S3 audit sink feature, not NIXL KV transfer functionality. Link to the actual audit feature issue or remove the unrelated link.
Docstring Coverage ⚠️ Warning Docstring coverage is 74.42% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed Title accurately describes the main change: adding S3-backed audit sink for request capture and replay.
Description check ✅ Passed Description comprehensively covers changes, motivation, design decisions, test plan, and new environment variables, following the template structure with Overview and Details sections.
Out of Scope Changes check ✅ Passed All changes align with S3 audit sink implementation: operator CRD additions, audit config/handle/sink/metrics Rust modules, telemetry refactoring for SegmentSink, environment variable registration, and integration tests.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.12.2)

level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies"


Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 7

🧹 Nitpick comments (1)
deploy/operator/api/v1beta1/dynamographdeployment_types.go (1)

96-133: 📐 Maintainability & Code Quality | 🔵 Trivial

Use camelCase for the AuditSpec JSON field names to align with Kubernetes API conventions.

The JSON tags aws_s3, gcp_gcs, and azure_blob use snake_case while their nested fields (irsaRoleArn, gcpServiceAccount, clientId) use camelCase. This inconsistency conflicts with Kubernetes API naming conventions and will be a breaking change if corrected after release. Since the GCP and Azure specs are marked "Future use — not implemented in v1", renaming now is the safest approach.

The controller logic reads only the typed Go fields, not JSON tags, so this change is JSON-only and won't affect the reconciliation code.

♻️ Proposed naming alignment
 	// aws_s3 configures AWS IRSA for the S3 audit sink.
 	// +optional
-	AwsS3 *AuditAwsS3Spec `json:"aws_s3,omitempty"`
+	AwsS3 *AuditAwsS3Spec `json:"awsS3,omitempty"`
 
 	// gcp_gcs configures GCP Workload Identity for a future GCS audit sink.
 	// +optional
-	GcpGcs *AuditGcpGcsSpec `json:"gcp_gcs,omitempty"`
+	GcpGcs *AuditGcpGcsSpec `json:"gcpGcs,omitempty"`
 
 	// azure_blob configures Azure Workload Identity for a future Azure Blob audit sink.
 	// +optional
-	AzureBlob *AuditAzureBlobSpec `json:"azure_blob,omitempty"`
+	AzureBlob *AuditAzureBlobSpec `json:"azureBlob,omitempty"`
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@deploy/operator/api/v1beta1/dynamographdeployment_types.go` around lines 96 -
133, The JSON field tags in the AuditSpec struct use snake_case (aws_s3,
gcp_gcs, azure_blob) while their nested field types use camelCase, creating an
inconsistency with Kubernetes API conventions. Update the JSON tags on the three
pointer fields in AuditSpec (AwsS3, GcpGcs, and AzureBlob) to use camelCase
format (awsS3, gcpGcs, azureBlob) to match the naming convention used throughout
the nested specifications and maintain consistency with Kubernetes standards.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@deploy/operator/internal/controller/dynamographdeployment_controller.go`:
- Around line 1799-1808: The code silently returns nil when the discovery
ServiceAccount is not found, making it unobservable to operators when
audit-at-runtime is misconfigured. In the error handling block where
errors.IsNotFound(err) is checked and currently returns nil, add logging at info
or warn level before the return statement to inform operators that the IRSA/WI
annotation was skipped due to the ServiceAccount not existing. This log message
should clarify that the absence of the SA (typically due to discovery being
disabled) means the audit pod will not have the necessary annotations for S3
authentication.

In `@lib/llm/src/audit/config.rs`:
- Around line 93-97: The sample_rate calculation in the audit config parsing
chain allows NaN values from the environment variable to bypass the clamp
operation, which causes issues downstream in the is_sampled function. Add a
filter using the is_finite() method in the parsing chain after mapping the
clamped value to reject any non-finite numbers (including NaN, infinity, or
negative infinity) before falling back to DEFAULT_SAMPLE_RATE. This ensures only
valid finite values in the [0.0, 1.0] range are accepted, maintaining the
documented invariant.
- Around line 133-137: The s3_sse match statement accepts any value other than
"none" without validation, which could lead to unsupported encryption modes
being passed to S3. Modify the match expression for
read_string(env_audit::DYN_AUDIT_S3_SSE) to validate that the value is one of
the three supported modes: "AES256", "aws:kms", or "none" (case-insensitive).
For the "none" case, return None; for valid "AES256" and "aws:kms" values,
return Some(v); and for any invalid input or None, default to
Some("AES256".to_string()) to ensure safe encryption defaults.

In `@lib/llm/src/audit/handle.rs`:
- Around line 98-107: The sampling gate logic on line 107 contradicts the
AuditPolicy contract where request.store==true should bypass sampling entirely.
Currently the condition checks !force_logging instead of !force, which means
requests with store=true still get sampled even though they should bypass
sampling. Fix this by changing the condition from !force_logging to !force so
that all forced requests (including those where store_flag is true) bypass the
sampling check and are guaranteed to land in the audit log, while only
non-forced requests undergo the head-based sampling rate check.

In `@lib/llm/src/audit/metrics.rs`:
- Around line 18-23: The metric names being passed to Opts::new() include a
redundant "dynamo_" prefix when the namespace "dynamo" is also being set via the
.namespace("dynamo") call, resulting in doubled namespace prefixes like
"dynamo_dynamo_audit_s3_segments_total". Remove the "dynamo_" prefix from the
metric name strings in all Opts::new() calls (such as in the IntCounterVec
creation and other similar metric definitions) while keeping the
.namespace("dynamo") calls unchanged, so metric names should start with just
"audit_" instead of "dynamo_audit_". Apply this fix to all metric definitions
mentioned in the file.

In `@lib/llm/src/audit/README.md`:
- Around line 117-119: The fenced code block containing the file path format
string is missing a language tag, which violates markdownlint rule MD040. Add
the language identifier "text" to the opening fence (the triple backticks) that
precedes the line with the prefix format containing the pod-name and
startup-uuid8 variables. This will properly label the code block as a text
format example.

In `@lib/llm/src/audit/sink.rs`:
- Around line 212-223: The JsonlGzipWriter::with_segment_sink() constructor call
is not using the configurable s3_channel_capacity parameter from the
AuditPolicy, which means the DYN_AUDIT_S3_CHANNEL_CAPACITY configuration knob
has no effect and a hardcoded channel size is used instead. Add the
s3_channel_capacity field from the policy object as a parameter to the
JsonlGzipWriter::with_segment_sink() call to wire the configuration into the S3
sink queue and allow proper backpressure and memory tuning.

---

Nitpick comments:
In `@deploy/operator/api/v1beta1/dynamographdeployment_types.go`:
- Around line 96-133: The JSON field tags in the AuditSpec struct use snake_case
(aws_s3, gcp_gcs, azure_blob) while their nested field types use camelCase,
creating an inconsistency with Kubernetes API conventions. Update the JSON tags
on the three pointer fields in AuditSpec (AwsS3, GcpGcs, and AzureBlob) to use
camelCase format (awsS3, gcpGcs, azureBlob) to match the naming convention used
throughout the nested specifications and maintain consistency with Kubernetes
standards.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: f5415a5f-216e-4afc-8797-ca04d9f92e71

📥 Commits

Reviewing files that changed from the base of the PR and between 5fa34ad and 3b94f59.

📒 Files selected for processing (17)
  • Cargo.toml
  • deploy/operator/api/v1beta1/dynamographdeployment_types.go
  • deploy/operator/internal/controller/audit_identity_test.go
  • deploy/operator/internal/controller/dynamographdeployment_controller.go
  • lib/llm/Cargo.toml
  • lib/llm/src/audit/README.md
  • lib/llm/src/audit/config.rs
  • lib/llm/src/audit/handle.rs
  • lib/llm/src/audit/metrics.rs
  • lib/llm/src/audit/mod.rs
  • lib/llm/src/audit/sink.rs
  • lib/llm/src/request_trace/sink.rs
  • lib/llm/src/telemetry/jsonl_gz.rs
  • lib/llm/src/telemetry/mod.rs
  • lib/llm/src/telemetry/s3_segment_sink.rs
  • lib/llm/tests/audit_s3_integration.rs
  • lib/runtime/src/config/environment_names.rs

Comment on lines +1799 to +1808
// Fetch the SA
sa := &corev1.ServiceAccount{}
if err := r.Get(ctx, types.NamespacedName{Name: saName, Namespace: dynamoDeployment.Namespace}, sa); err != nil {
if errors.IsNotFound(err) {
// SA hasn't been created yet (e.g., discovery is disabled).
// Nothing to annotate — skip silently.
return nil
}
return fmt.Errorf("audit identity: failed to get SA %s: %w", saName, err)
}

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.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Audit IRSA is silently skipped when discovery is disabled.

When the discovery SA does not exist, the method returns nil and never applies the IRSA/WI annotation. Since audit-at-runtime is gated separately by DYN_AUDIT_SINKS, an operator can enable the S3 audit sink with spec.audit.aws_s3 set while discovery is off, and the frontend pod will then fail to authenticate to S3 with no operator-side signal. Consider logging at info/warn level here (or surfacing a status condition) so this misconfiguration is observable rather than silent.

🔎 Suggested observability
 		if errors.IsNotFound(err) {
-			// SA hasn't been created yet (e.g., discovery is disabled).
-			// Nothing to annotate — skip silently.
+			// SA hasn't been created yet (e.g., discovery is disabled).
+			// Nothing to annotate — skip. Warn if audit identity was requested
+			// so the misconfiguration (audit enabled without discovery) is visible.
+			if dynamoDeployment.Spec.Audit != nil {
+				logger.Info("audit identity requested but discovery ServiceAccount is absent; skipping annotation", "sa", saName)
+			}
 			return nil
 		}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@deploy/operator/internal/controller/dynamographdeployment_controller.go`
around lines 1799 - 1808, The code silently returns nil when the discovery
ServiceAccount is not found, making it unobservable to operators when
audit-at-runtime is misconfigured. In the error handling block where
errors.IsNotFound(err) is checked and currently returns nil, add logging at info
or warn level before the return statement to inform operators that the IRSA/WI
annotation was skipped due to the ServiceAccount not existing. This log message
should clarify that the absence of the SA (typically due to discovery being
disabled) means the audit pod will not have the necessary annotations for S3
authentication.

Comment on lines +93 to +97
let sample_rate = std::env::var(env_audit::DYN_AUDIT_SAMPLE_RATE)
.ok()
.and_then(|v| v.parse::<f32>().ok())
.map(|v| v.clamp(0.0, 1.0))
.unwrap_or(DEFAULT_SAMPLE_RATE);

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.

🎯 Functional Correctness | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, locate and examine the audit config file
find . -name "config.rs" -path "*/audit/*" | head -5

Repository: ai-dynamo/dynamo

Length of output: 184


🏁 Script executed:

# Get the file structure around the relevant lines
ast-grep outline lib/llm/src/audit/config.rs --view expanded

Repository: ai-dynamo/dynamo

Length of output: 1642


🏁 Script executed:

# Read the specific section and surrounding context
sed -n '1,120p' lib/llm/src/audit/config.rs | cat -n

Repository: ai-dynamo/dynamo

Length of output: 5840


🏁 Script executed:

# Check if f32::clamp actually handles NaN correctly
cat > /tmp/test_nan.rs << 'EOF'
fn main() {
    let nan_val = f32::NAN;
    let clamped = nan_val.clamp(0.0, 1.0);
    println!("NaN clamp result: {}", clamped);
    println!("Is finite: {}", clamped.is_finite());
    
    let parsed = "NaN".parse::<f32>().ok();
    println!("Parsed NaN: {:?}", parsed);
    if let Some(v) = parsed {
        println!("Parsed is finite: {}", v.is_finite());
        println!("Parsed clamp result: {}", v.clamp(0.0, 1.0));
    }
}
EOF
# We'll use python to verify NaN behavior since we can't compile
python3 << 'PY'
import math
nan = float('nan')
print(f"NaN value: {nan}")
print(f"NaN is finite: {math.isfinite(nan)}")
# Simulating clamp behavior
clamped = max(0.0, min(1.0, nan))
print(f"Clamp(0, 1) of NaN: {clamped}")
print(f"Clamped is finite: {math.isfinite(clamped)}")
# String parse
parsed = float("NaN")
print(f"Parsed 'NaN': {parsed}")
PY

Repository: ai-dynamo/dynamo

Length of output: 256


🏁 Script executed:

# Check for Linear ticket references in the config.rs file
rg "DIS-|DYN-|OPS-|DEP-" lib/llm/src/audit/config.rs -n

Repository: ai-dynamo/dynamo

Length of output: 154


🏁 Script executed:

# Check how sample_rate is used downstream to understand impact
rg "sample_rate" lib/llm/src/audit/ -A 2 -B 2

Repository: ai-dynamo/dynamo

Length of output: 5267


Guard sample_rate against non-finite values.

DYN_AUDIT_SAMPLE_RATE=NaN parses successfully and bypasses the clamp operation, passing NaN downstream. In is_sampled(), this causes the threshold calculation to produce 0, blocking all non-forced audit requests. Add a finiteness check before clamping to enforce the [0.0, 1.0] invariant documented in the struct.

Proposed fix
     let sample_rate = std::env::var(env_audit::DYN_AUDIT_SAMPLE_RATE)
         .ok()
         .and_then(|v| v.parse::<f32>().ok())
+        .filter(|v| v.is_finite())
         .map(|v| v.clamp(0.0, 1.0))
         .unwrap_or(DEFAULT_SAMPLE_RATE);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let sample_rate = std::env::var(env_audit::DYN_AUDIT_SAMPLE_RATE)
.ok()
.and_then(|v| v.parse::<f32>().ok())
.map(|v| v.clamp(0.0, 1.0))
.unwrap_or(DEFAULT_SAMPLE_RATE);
let sample_rate = std::env::var(env_audit::DYN_AUDIT_SAMPLE_RATE)
.ok()
.and_then(|v| v.parse::<f32>().ok())
.filter(|v| v.is_finite())
.map(|v| v.clamp(0.0, 1.0))
.unwrap_or(DEFAULT_SAMPLE_RATE);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/llm/src/audit/config.rs` around lines 93 - 97, The sample_rate
calculation in the audit config parsing chain allows NaN values from the
environment variable to bypass the clamp operation, which causes issues
downstream in the is_sampled function. Add a filter using the is_finite() method
in the parsing chain after mapping the clamped value to reject any non-finite
numbers (including NaN, infinity, or negative infinity) before falling back to
DEFAULT_SAMPLE_RATE. This ensures only valid finite values in the [0.0, 1.0]
range are accepted, maintaining the documented invariant.

Comment on lines +133 to +137
let s3_sse = match read_string(env_audit::DYN_AUDIT_S3_SSE) {
Some(v) if v.eq_ignore_ascii_case("none") => None,
Some(v) => Some(v),
None => Some(DEFAULT_S3_SSE.to_string()),
};

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.

🔒 Security & Privacy | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, examine the specific file and lines mentioned
cat -n lib/llm/src/audit/config.rs | sed -n '125,145p'

Repository: ai-dynamo/dynamo

Length of output: 1265


🏁 Script executed:

# Check if there's a reference mapping for DYN_AUDIT_S3_SSE
cat .ai/linear-ticket-refs.md 2>/dev/null || echo "File not found"

Repository: ai-dynamo/dynamo

Length of output: 1643


🏁 Script executed:

# Search for DYN_AUDIT_S3_SSE definition and usage
rg "DYN_AUDIT_S3_SSE" --type rust

Repository: ai-dynamo/dynamo

Length of output: 154


🏁 Script executed:

# Look for DEFAULT_S3_SSE and understand what the default is
rg "DEFAULT_S3_SSE" --type rust -B 2 -A 2

Repository: ai-dynamo/dynamo

Length of output: 154


🏁 Script executed:

# Find the env_audit module definition
fd -t f "env_audit" --type rust || find . -name "*env*audit*" -type f

Repository: ai-dynamo/dynamo

Length of output: 326


🏁 Script executed:

# Search more broadly for DEFAULT_S3_SSE
rg "DEFAULT_S3_SSE"

Repository: ai-dynamo/dynamo

Length of output: 154


🏁 Script executed:

# Find where env_audit is defined and look for DYN_AUDIT_S3_SSE
rg "DYN_AUDIT_S3_SSE|DEFAULT_S3_SSE" -t rust -t toml

Repository: ai-dynamo/dynamo

Length of output: 154


🏁 Script executed:

# Check the structure of the audit module
ls -la lib/llm/src/audit/

Repository: ai-dynamo/dynamo

Length of output: 751


🏁 Script executed:

# Read the entire config.rs to understand the full context
cat -n lib/llm/src/audit/config.rs

Repository: ai-dynamo/dynamo

Length of output: 9028


🏁 Script executed:

# Search for where read_string function is defined
rg "fn read_string"

Repository: ai-dynamo/dynamo

Length of output: 154


🏁 Script executed:

# Look for env_audit module
rg "mod env_audit|use.*env_audit"

Repository: ai-dynamo/dynamo

Length of output: 154


🏁 Script executed:

# Find where env_audit constants are defined
find . -path "*dynamo_runtime*" -name "*.rs" | head -20

Repository: ai-dynamo/dynamo

Length of output: 154


🏁 Script executed:

# Search for where these DYN_AUDIT constants are defined
rg "DYN_AUDIT_S3_SSE\s*=" --type-list

Repository: ai-dynamo/dynamo

Length of output: 544


🏁 Script executed:

# Look at how S3 SSE is actually used when creating S3 objects
rg -A 5 -B 5 "s3_sse" lib/llm/src/audit/

Repository: ai-dynamo/dynamo

Length of output: 2856


🏁 Script executed:

# Check if there are comments or documentation about valid S3 SSE values
rg "AES256|aws:kms|ServerSideEncryption" lib/llm/src/audit/

Repository: ai-dynamo/dynamo

Length of output: 321


Validate s3_sse environment variable to supported encryption modes only.

The current code accepts any value other than "none" and passes it directly to S3, which may silently degrade to bucket-default behavior. Per the audit README, only three values are valid: "AES256", "aws:kms", and "none". Normalize accepted values and default to "AES256" for invalid input to prevent accidental encryption downgrades.

Proposed fix
-    let s3_sse = match read_string(env_audit::DYN_AUDIT_S3_SSE) {
-        Some(v) if v.eq_ignore_ascii_case("none") => None,
-        Some(v) => Some(v),
-        None => Some(DEFAULT_S3_SSE.to_string()),
-    };
+    let s3_sse = match read_string(env_audit::DYN_AUDIT_S3_SSE) {
+        Some(v) if v.eq_ignore_ascii_case("none") => None,
+        Some(v) if v.eq_ignore_ascii_case("aes256") => Some("AES256".to_string()),
+        Some(v) if v.eq_ignore_ascii_case("aws:kms") => Some("aws:kms".to_string()),
+        Some(_) => Some(DEFAULT_S3_SSE.to_string()),
+        None => Some(DEFAULT_S3_SSE.to_string()),
+    };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let s3_sse = match read_string(env_audit::DYN_AUDIT_S3_SSE) {
Some(v) if v.eq_ignore_ascii_case("none") => None,
Some(v) => Some(v),
None => Some(DEFAULT_S3_SSE.to_string()),
};
let s3_sse = match read_string(env_audit::DYN_AUDIT_S3_SSE) {
Some(v) if v.eq_ignore_ascii_case("none") => None,
Some(v) if v.eq_ignore_ascii_case("aes256") => Some("AES256".to_string()),
Some(v) if v.eq_ignore_ascii_case("aws:kms") => Some("aws:kms".to_string()),
Some(_) => Some(DEFAULT_S3_SSE.to_string()),
None => Some(DEFAULT_S3_SSE.to_string()),
};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/llm/src/audit/config.rs` around lines 133 - 137, The s3_sse match
statement accepts any value other than "none" without validation, which could
lead to unsupported encryption modes being passed to S3. Modify the match
expression for read_string(env_audit::DYN_AUDIT_S3_SSE) to validate that the
value is one of the three supported modes: "AES256", "aws:kms", or "none"
(case-insensitive). For the "none" case, return None; for valid "AES256" and
"aws:kms" values, return Some(v); and for any invalid input or None, default to
Some("AES256".to_string()) to ensure safe encryption defaults.

Comment on lines +98 to +107
let store_flag = req.inner.store.unwrap_or(false);
let force = force_logging || store_flag;
// If neither force_logging nor the request `store` flag is set, this
// request is not eligible for capture at all.
if !force {
return None;
}
// Forced requests bypass head-based sampling so debugging /
// compliance flags always land in the audit log.
if !force_logging && sample_rate < 1.0 && !is_sampled(request_id, sample_rate) {

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Sampling gate currently contradicts the store bypass contract and blocks normal head-sampling.

Line 102-104 drops all requests when force_logging=false and store=false, so regular traffic can never be captured by sampling. Then Line 107 still samples store=true requests, even though AuditPolicy documents request.store==true as a sampling bypass (lib/llm/src/audit/config.rs).

Suggested fix
-    let store_flag = req.inner.store.unwrap_or(false);
-    let force = force_logging || store_flag;
-    // If neither force_logging nor the request `store` flag is set, this
-    // request is not eligible for capture at all.
-    if !force {
-        return None;
-    }
-    // Forced requests bypass head-based sampling so debugging /
-    // compliance flags always land in the audit log.
-    if !force_logging && sample_rate < 1.0 && !is_sampled(request_id, sample_rate) {
+    let store_flag = req.inner.store.unwrap_or(false);
+    let forced = force_logging || store_flag;
+    // force_logging and request.store bypass sampling; all other requests
+    // are decided by deterministic head sampling.
+    if !forced && !is_sampled(request_id, sample_rate) {
         return None;
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let store_flag = req.inner.store.unwrap_or(false);
let force = force_logging || store_flag;
// If neither force_logging nor the request `store` flag is set, this
// request is not eligible for capture at all.
if !force {
return None;
}
// Forced requests bypass head-based sampling so debugging /
// compliance flags always land in the audit log.
if !force_logging && sample_rate < 1.0 && !is_sampled(request_id, sample_rate) {
let store_flag = req.inner.store.unwrap_or(false);
let forced = force_logging || store_flag;
// force_logging and request.store bypass sampling; all other requests
// are decided by deterministic head sampling.
if !forced && !is_sampled(request_id, sample_rate) {
return None;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/llm/src/audit/handle.rs` around lines 98 - 107, The sampling gate logic
on line 107 contradicts the AuditPolicy contract where request.store==true
should bypass sampling entirely. Currently the condition checks !force_logging
instead of !force, which means requests with store=true still get sampled even
though they should bypass sampling. Fix this by changing the condition from
!force_logging to !force so that all forced requests (including those where
store_flag is true) bypass the sampling check and are guaranteed to land in the
audit log, while only non-forced requests undergo the head-based sampling rate
check.

Comment on lines +18 to +23
let opts = Opts::new(
"dynamo_audit_s3_segments_total",
"Total audit segments uploaded to S3",
)
.namespace("dynamo");
IntCounterVec::new(opts, &["result"]).expect("audit_s3_segments_total metric")

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.

📐 Maintainability & Code Quality | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n lib/llm/src/audit/metrics.rs

Repository: ai-dynamo/dynamo

Length of output: 3242


🌐 Web query:

prometheus-rs Opts namespace metric name prefix behavior

💡 Result:

In the prometheus-rs library, the Opts struct uses the namespace, subsystem, and name fields to construct the fully-qualified metric name [1][2]. These components are joined together using an underscore () as a separator [1][3]. The behavior for generating the fully-qualified name follows these rules: 1. Components (namespace, subsystem, and name) are concatenated with "" [1][3]. 2. Empty components are ignored during this construction process [3]. 3. Only the name field is mandatory; namespace and subsystem are optional and serve to help structure the metric name [1][4]. 4. The resulting fully-qualified name must be a valid Prometheus metric name [1][5]. For example, if you set the namespace to "my_app", the subsystem to "database", and the name to "connections", the resulting metric name will be "my_app_database_connections" [1][3]. It is important to distinguish this from the Registry, which also supports an optional prefix [6]. When a prefix is defined at the Registry level, it is prepended to the metric names gathered by that registry during the exposition process [6]. This is distinct from the Opts structure, which handles the naming of individual metrics at the time of creation [1][3]. Additionally, newer Prometheus client implementations (such as prometheus_client) offer sub-registry functionality to manage prefixes for groups of metrics more efficiently [7].

Citations:


Remove redundant namespace prefix from metric names.

Opts::new("dynamo_audit_s3_segments_total", ...).namespace("dynamo") will produce dynamo_dynamo_audit_s3_segments_total. The namespace is automatically prepended to the metric name, so the base names should not include the "dynamo_" prefix.

Suggested fix
-        "dynamo_audit_s3_segments_total",
+        "audit_s3_segments_total",
...
-        "dynamo_audit_records_dropped_total",
+        "audit_records_dropped_total",
...
-        "dynamo_audit_s3_upload_duration_seconds",
+        "audit_s3_upload_duration_seconds",
...
-        "dynamo_audit_s3_segment_size_bytes",
+        "audit_s3_segment_size_bytes",

Also applies to: 32-37, 42-48, 53-59

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/llm/src/audit/metrics.rs` around lines 18 - 23, The metric names being
passed to Opts::new() include a redundant "dynamo_" prefix when the namespace
"dynamo" is also being set via the .namespace("dynamo") call, resulting in
doubled namespace prefixes like "dynamo_dynamo_audit_s3_segments_total". Remove
the "dynamo_" prefix from the metric name strings in all Opts::new() calls (such
as in the IntCounterVec creation and other similar metric definitions) while
keeping the .namespace("dynamo") calls unchanged, so metric names should start
with just "audit_" instead of "dynamo_audit_". Apply this fix to all metric
definitions mentioned in the file.

Comment on lines +117 to +119
```
<prefix>/[<deployment>/]YYYY/MM/DD/HH/<pod-name>-<startup-uuid8>-<seq>.jsonl.gz
```

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add a language tag to the fenced key-format block.

Line 117 uses an unlabeled fenced code block, which triggers markdownlint MD040.

💡 Suggested fix
-```
+```text
 <prefix>/[<deployment>/]YYYY/MM/DD/HH/<pod-name>-<startup-uuid8>-<seq>.jsonl.gz
</details>

<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion

🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 117-117: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/llm/src/audit/README.md` around lines 117 - 119, The fenced code block
containing the file path format string is missing a language tag, which violates
markdownlint rule MD040. Add the language identifier "text" to the opening fence
(the triple backticks) that precedes the line with the prefix format containing
the pod-name and startup-uuid8 variables. This will properly label the code
block as a text format example.

Source: Linters/SAST tools

Comment thread lib/llm/src/audit/sink.rs
Comment on lines +212 to +223
let writer = JsonlGzipWriter::with_segment_sink(
segment_sink,
JsonlGzipSinkOptions {
buffer_bytes: policy.s3_batch_bytes,
// The buffer flush cadence keeps in-memory bytes from
// sitting around between PUTs; segment rotation is
// controlled by roll_bytes / roll_lines / roll_interval.
flush_interval: Duration::from_millis(policy.jsonl_flush_interval_ms.max(1)),
roll_uncompressed_bytes: policy.s3_roll_bytes,
roll_lines: policy.s3_roll_lines,
roll_interval: Some(Duration::from_millis(policy.s3_roll_interval_ms.max(1))),
},

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.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

DYN_AUDIT_S3_CHANNEL_CAPACITY is not wired into the S3 sink queue.

AuditPolicy exposes s3_channel_capacity, but this path always constructs JsonlGzipWriter::with_segment_sink(...) without a capacity parameter, and the writer uses a hardcoded channel size (2048). That makes the env/config knob ineffective for backpressure and memory tuning.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/llm/src/audit/sink.rs` around lines 212 - 223, The
JsonlGzipWriter::with_segment_sink() constructor call is not using the
configurable s3_channel_capacity parameter from the AuditPolicy, which means the
DYN_AUDIT_S3_CHANNEL_CAPACITY configuration knob has no effect and a hardcoded
channel size is used instead. Add the s3_channel_capacity field from the policy
object as a parameter to the JsonlGzipWriter::with_segment_sink() call to wire
the configuration into the S3 sink queue and allow proper backpressure and
memory tuning.

@YiqiuLiu
YiqiuLiu force-pushed the feat/s3-audit-sink branch 3 times, most recently from d94cd46 to 6a38539 Compare June 23, 2026 22:40
#[serde(skip_serializing_if = "Option::is_none")]
pub deployment: Option<String>,
/// UTC wall-clock unix-millis stamped at `AuditHandle::emit()`.
pub emitted_at_unix_ms: i64,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Adding emitted_at_unix_ms as a required deserialization field breaks replay/reading of existing audit records that were written without it. Fix: give the field a serde default or make it optional.

🤖 AI Fix

In lib/llm/src/audit/handle.rs, add #[serde(default)] above AuditRecord::emitted_at_unix_ms so older audit JSON without this field still deserializes.

let path = segment_path(&self.base_path, self.current_index);
let batch = std::mem::take(&mut self.batch);
let gz_bytes = compress_member(batch).context("compressing gzip jsonl batch")?;
let seq = self.current_seq;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Gzip compression now runs synchronously inside the Tokio async writer task, which can block executor threads for large audit/request-trace batches. Fix: move compress_member(batch) into tokio::task::spawn_blocking before appending to the segment sink.

🤖 AI Fix

In lib/llm/src/telemetry/jsonl_gz.rs, update GzipBatchWriter::flush_batch to run compress_member(batch) via tokio::task::spawn_blocking(move || compress_member(batch)), await it with context/error handling, then pass the resulting bytes to append_to_segment.

/// Construct a writer that hands each rotated segment to the supplied
/// [`SegmentSink`]. Use this for cloud destinations such as S3.
pub fn with_segment_sink(
segment_sink: Arc<dyn SegmentSink>,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The new DYN_AUDIT_S3_CHANNEL_CAPACITY setting is loaded and documented but ignored because JsonlGzipWriter hard-codes its channel capacity to 2048. Fix: thread a channel capacity option through the gzip writer and pass policy.s3_channel_capacity for the S3 sink.

🤖 AI Fix

Add channel_capacity: usize to JsonlGzipSinkOptions with default 2048, use it in JsonlGzipWriter::with_segment_sink when calling mpsc::channel, and in lib/llm/src/audit/sink.rs set channel_capacity: policy.s3_channel_capacity for S3AuditSink::from_policy.

@YiqiuLiu
YiqiuLiu force-pushed the feat/s3-audit-sink branch from 6a38539 to 6fe5340 Compare June 23, 2026 23:34
@YiqiuLiu
YiqiuLiu temporarily deployed to external_collaborator June 23, 2026 23:34 — with GitHub Actions Inactive
@YiqiuLiu
YiqiuLiu force-pushed the feat/s3-audit-sink branch from 6fe5340 to 0030f63 Compare June 23, 2026 23:38
@YiqiuLiu
YiqiuLiu temporarily deployed to external_collaborator June 23, 2026 23:38 — with GitHub Actions Inactive
@YiqiuLiu
YiqiuLiu force-pushed the feat/s3-audit-sink branch from 0030f63 to 020e9e3 Compare June 23, 2026 23:40
@YiqiuLiu
YiqiuLiu temporarily deployed to external_collaborator June 23, 2026 23:40 — with GitHub Actions Inactive
@YiqiuLiu
YiqiuLiu force-pushed the feat/s3-audit-sink branch from 020e9e3 to 6777bea Compare June 23, 2026 23:48
@YiqiuLiu
YiqiuLiu temporarily deployed to external_collaborator June 23, 2026 23:48 — with GitHub Actions Inactive
@YiqiuLiu
YiqiuLiu force-pushed the feat/s3-audit-sink branch from 6777bea to c34e547 Compare June 23, 2026 23:51
@YiqiuLiu
YiqiuLiu temporarily deployed to external_collaborator June 23, 2026 23:51 — with GitHub Actions Inactive
@YiqiuLiu
YiqiuLiu force-pushed the feat/s3-audit-sink branch from c34e547 to 237616e Compare June 24, 2026 00:14
@YiqiuLiu
YiqiuLiu temporarily deployed to external_collaborator June 24, 2026 00:14 — with GitHub Actions Inactive
Add a new S3-backed audit sink as a peer of the existing stderr/nats/
jsonl/jsonl_gz sinks. Selected at runtime via DYN_AUDIT_SINKS=s3.

The sink ships per-request audit records (full request + full response,
already in AuditRecord today) to S3 as rotated, gzip-compressed NDJSON
objects. Object keys partition by deployment + date for Athena/Glue and
include a per-process startup uuid so pod restarts cannot collide:

  {prefix}/[{deployment}/]YYYY/MM/DD/HH/{instance}-{startup}-{seq:06}.jsonl.gz

Rotation engine
---------------
The existing rotating gzip writer (telemetry/jsonl_gz.rs) is generalized
behind a new SegmentSink trait so it can drive both disk and S3:

  trait SegmentSink {
      async fn append_to_segment(&self, seq, gz_bytes);
      async fn close_segment(&self, seq);
  }

FileSegmentSink keeps the existing on-disk behavior used by jsonl_gz
audit and request_trace (no callers changed). S3SegmentSink is a peer
that buffers the segment in memory across appends and uploads on close.

The writer also gains an optional roll_interval for time-based
rotation, default None (unchanged for jsonl_gz). The S3 sink defaults
it to 60s so quiet periods do not leave records in memory.

Hot-path safety
---------------
All I/O is off the request path. emit() pushes serialized records into
a bounded mpsc that an uploader task consumes; PutObject runs there
with the SDK's standard retry policy (3 attempts, exponential backoff
+ jitter). On terminal failure: log + drop the segment, never
propagate. The audit bus already drops the oldest records on
back-pressure, so a slow S3 cannot stall inference.

Sampling
--------
A new global DYN_AUDIT_SAMPLE_RATE (default 1.0) gates handle creation
via a deterministic xxh3 hash of request_id. Bypassed by force_logging
so debug/compliance flags always capture, but not by request.store
alone (store is a normal opt-in; sampling still applies). Unsampled
requests skip every Arc clone of the request body.

AuditRecord schema
------------------
Two new fields:
  deployment: Option<String>     // skip-if-none
  emitted_at_unix_ms: i64        // UTC wall-clock at emit()

The deployment field is auto-detected from DYN_PARENT_DGD_K8S_NAME on
Kubernetes (the Dynamo operator unconditionally injects this env var
on every worker pod with the parent DynamoGraphDeployment CR name).
DYN_AUDIT_DEPLOYMENT remains an explicit override for non-K8s deploys
or test environments.

The per-record gzip envelope timestamp also moves from millis-since-
process-start to UTC unix-millis: process-start-relative timestamps
are not portable across pod lifetimes for an audit log shipped
off-host.

New environment variables
-------------------------
  DYN_AUDIT_SAMPLE_RATE
  DYN_AUDIT_DEPLOYMENT           # optional override; auto = DYN_PARENT_DGD_K8S_NAME
  DYN_AUDIT_S3_BUCKET            # required when sinks=s3
  DYN_AUDIT_S3_PREFIX            # default "dynamo-audit"
  DYN_AUDIT_S3_REGION
  DYN_AUDIT_S3_ENDPOINT_URL      # for LocalStack/MinIO
  DYN_AUDIT_S3_BATCH_BYTES       # default 1 MiB
  DYN_AUDIT_S3_ROLL_BYTES        # default 64 MiB compressed
  DYN_AUDIT_S3_ROLL_INTERVAL_MS  # default 60_000
  DYN_AUDIT_S3_ROLL_LINES
  DYN_AUDIT_S3_SSE               # AES256 (default), aws:kms, or "none"
  DYN_AUDIT_S3_KMS_KEY_ID
  DYN_AUDIT_S3_INSTANCE_ID       # override; defaults to POD_NAME / hostname
  DYN_AUDIT_S3_CHANNEL_CAPACITY  # default 4096

All consts registered in lib/runtime/src/config/environment_names.rs.

Identity resolution
-------------------
S3 object keys identify the writer with full pod name + 8-char startup
uuid. Resolution chain at sink construction:
  DYN_AUDIT_S3_INSTANCE_ID -> POD_NAME (K8s downward API) -> HOSTNAME
  -> /etc/hostname -> "unknown"

Encryption
----------
Server-side encryption is configurable: AES256 (default), aws:kms with
optional KMS key ARN, or none (LocalStack/MinIO compatibility). No
client-side encryption in v1.

Cargo dependencies
------------------
aws-sdk-s3 1.120.0 and aws-config 1.8.11 added to workspace and to
lib/llm/Cargo.toml (versions match the existing pin in lib/kvbm-engine
to share the SDK compile). No Cargo feature gate, matching the
precedent set by NVIDIA's OTLP audit-sink PR.

Tests
-----
- 4 new unit tests in audit::handle for sampling and deployment.
- 5 new unit tests in telemetry::s3_segment_sink for key formatting
  and identity fallback.
- 3 new unit tests in telemetry::jsonl_gz for the SegmentSink trait
  (collecting fake) and time-based rotation.
- 1 #[ignore]'d integration test in tests/audit_s3_integration.rs that
  drives the full bus -> sink -> S3 PUT path against a LocalStack /
  MinIO endpoint and asserts:
    * NDJSON lines reach the destination, gzip-decoded correctly
    * deployment field is populated in the record envelope
    * deployment partition appears in the object key
  Verified end-to-end against MinIO at localhost:4566 (see file
  module-level docs for setup).

All existing tests stay green: 15 audit, 19 telemetry, 29 request_trace.
Clippy and rustfmt clean.

Signed-off-by: Yiqiu Liu <yiqiuliu@amazon.com>
@YiqiuLiu
YiqiuLiu force-pushed the feat/s3-audit-sink branch from 237616e to c75a75c Compare June 24, 2026 00:39
@YiqiuLiu
YiqiuLiu requested a review from a team as a code owner June 24, 2026 00:39
@YiqiuLiu
YiqiuLiu temporarily deployed to external_collaborator June 24, 2026 00:39 — with GitHub Actions Inactive
YiqiuLiu added a commit to YiqiuLiu/dynamo that referenced this pull request Jul 16, 2026
Adds an S3 destination for `dynamo.request.trace.v1` records as a
new `RequestTraceSink` implementation, parallel to the existing
OTLP sink. Same record shape as every other sink -- new destination,
no schema change.

Records are batched in-process as gzipped JSONL and each finished
batch is uploaded via a single `PutObject`. Object keys use a simple
time-based layout for this PR (`{prefix}/{yyyy}/{mm}/{dd}/{host}-
{HHMMSS}-{seq}.jsonl.gz`); richer partitioning (model/day/hour Hive
style), tunable roll thresholds, and Prometheus metrics ship in a
follow-up.

Credentials come from the AWS SDK default provider chain -- env vars,
IMDS, IRSA, Pod Identity, and shared profiles are all handled by the
SDK. How the frontend pod is credentialed is a deployment concern
documented in a separate PR, not this sink's.

The `request-trace-s3` cargo feature is off by default so the AWS
SDK dependency is opt-in. When the feature is off, selecting
`DYN_REQUEST_TRACE_SINKS=...,s3` fails startup with a clear error.

Refs: ai-dynamo#11768 (supersedes ai-dynamo#10903).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
YiqiuLiu added a commit to YiqiuLiu/dynamo that referenced this pull request Jul 16, 2026
Adds an S3 destination for `dynamo.request.trace.v1` records as a
new `RequestTraceSink` implementation, parallel to the existing
OTLP sink. Same record shape as every other sink -- new destination,
no schema change.

Records are batched in-process as gzipped JSONL and each finished
batch is uploaded via a single `PutObject`. Object keys use a simple
time-based layout for this PR (`{prefix}/{yyyy}/{mm}/{dd}/{host}-
{HHMMSS}-{seq}.jsonl.gz`); richer partitioning (model/day/hour Hive
style), tunable roll thresholds, and Prometheus metrics ship in a
follow-up.

Credentials come from the AWS SDK default provider chain -- env vars,
IMDS, IRSA, Pod Identity, and shared profiles are all handled by the
SDK. How the frontend pod is credentialed is a deployment concern
documented in a separate PR, not this sink's.

The `request-trace-s3` cargo feature is off by default so the AWS
SDK dependency is opt-in. When the feature is off, selecting
`DYN_REQUEST_TRACE_SINKS=...,s3` fails startup with a clear error.

Refs: ai-dynamo#11768 (supersedes ai-dynamo#10903).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
YiqiuLiu added a commit to YiqiuLiu/dynamo that referenced this pull request Jul 16, 2026
Adds an S3 destination for `dynamo.request.trace.v1` records as a
new `RequestTraceSink` implementation, parallel to the existing
OTLP sink. Same record shape as every other sink -- new destination,
no schema change.

Records are batched in-process as gzipped JSONL and each finished
batch is uploaded via a single `PutObject`. Object keys use a simple
time-based layout for this PR (`{prefix}/{yyyy}/{mm}/{dd}/{host}-
{HHMMSS}-{seq}.jsonl.gz`); richer partitioning (model/day/hour Hive
style), tunable roll thresholds, and Prometheus metrics ship in a
follow-up.

Credentials come from the AWS SDK default provider chain -- env vars,
IMDS, IRSA, Pod Identity, and shared profiles are all handled by the
SDK. How the frontend pod is credentialed is a deployment concern
documented in a separate PR, not this sink's.

The `request-trace-s3` cargo feature is off by default so the AWS
SDK dependency is opt-in. When the feature is off, selecting
`DYN_REQUEST_TRACE_SINKS=...,s3` fails startup with a clear error.

Refs: ai-dynamo#11768 (supersedes ai-dynamo#10903).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Yiqiu Liu <yiqiuliu@amazon.com>
YiqiuLiu added a commit to YiqiuLiu/dynamo that referenced this pull request Jul 16, 2026
Adds an S3 destination for `dynamo.request.trace.v1` records as a
new `RequestTraceSink` implementation, parallel to the existing
OTLP sink. Same record shape as every other sink -- new destination,
no schema change.

Records are batched in-process as gzipped JSONL and each finished
batch is uploaded via a single `PutObject`. Object keys use a simple
time-based layout for this PR (`{prefix}/{yyyy}/{mm}/{dd}/{host}-
{HHMMSS}-{seq}.jsonl.gz`); richer partitioning (model/day/hour Hive
style), tunable roll thresholds, and Prometheus metrics ship in a
follow-up.

Credentials come from the AWS SDK default provider chain -- env vars,
IMDS, IRSA, Pod Identity, and shared profiles are all handled by the
SDK. How the frontend pod is credentialed is a deployment concern
documented in a separate PR, not this sink's.

The `request-trace-s3` cargo feature is off by default so the AWS
SDK dependency is opt-in. When the feature is off, selecting
`DYN_REQUEST_TRACE_SINKS=...,s3` fails startup with a clear error.

Refs: ai-dynamo#11768 (supersedes ai-dynamo#10903).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Yiqiu Liu <yiqiuliu@amazon.com>
@YiqiuLiu

Copy link
Copy Markdown
Contributor Author

PR outdated, check #11768 for details

@YiqiuLiu YiqiuLiu closed this Jul 16, 2026
YiqiuLiu added a commit to YiqiuLiu/dynamo that referenced this pull request Jul 16, 2026
Adds an S3 destination for `dynamo.request.trace.v1` records as a
new `RequestTraceSink` implementation, parallel to the existing
OTLP sink. Same record shape as every other sink -- new destination,
no schema change.

Records are batched in-process as gzipped JSONL and each finished
batch is uploaded via a single `PutObject`. Object keys use a simple
time-based layout for this PR (`{prefix}/{yyyy}/{mm}/{dd}/{host}-
{HHMMSS}-{seq}.jsonl.gz`); richer partitioning (model/day/hour Hive
style), tunable roll thresholds, and Prometheus metrics ship in a
follow-up.

Credentials come from the AWS SDK default provider chain -- env vars,
IMDS, IRSA, Pod Identity, and shared profiles are all handled by the
SDK. How the frontend pod is credentialed is a deployment concern
documented in a separate PR, not this sink's.

The `request-trace-s3` cargo feature is off by default so the AWS
SDK dependency is opt-in. When the feature is off, selecting
`DYN_REQUEST_TRACE_SINKS=...,s3` fails startup with a clear error.

Refs: ai-dynamo#11768 (supersedes ai-dynamo#10903).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Yiqiu Liu <yiqiuliu@amazon.com>
YiqiuLiu added a commit to YiqiuLiu/dynamo that referenced this pull request Jul 17, 2026
Adds an S3 destination for `dynamo.request.trace.v1` records as a
new `RequestTraceSink` implementation, parallel to the existing
OTLP sink. Same record shape as every other sink -- new destination,
no schema change.

Records are batched in-process as gzipped JSONL and each finished
batch is uploaded via a single `PutObject`. Object keys use a simple
time-based layout for this PR (`{prefix}/{yyyy}/{mm}/{dd}/{host}-
{HHMMSS}-{seq}.jsonl.gz`); richer partitioning (model/day/hour Hive
style), tunable roll thresholds, and Prometheus metrics ship in a
follow-up.

Credentials come from the AWS SDK default provider chain -- env vars,
IMDS, IRSA, Pod Identity, and shared profiles are all handled by the
SDK. How the frontend pod is credentialed is a deployment concern
documented in a separate PR, not this sink's.

The `request-trace-s3` cargo feature is off by default so the AWS
SDK dependency is opt-in. When the feature is off, selecting
`DYN_REQUEST_TRACE_SINKS=...,s3` fails startup with a clear error.

Refs: ai-dynamo#11768 (supersedes ai-dynamo#10903).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Yiqiu Liu <yiqiuliu@amazon.com>
YiqiuLiu added a commit to YiqiuLiu/dynamo that referenced this pull request Jul 17, 2026
Adds an S3 destination for `dynamo.request.trace.v1` records as a
new `RequestTraceSink` implementation, parallel to the existing
OTLP sink. Same record shape as every other sink -- new destination,
no schema change.

Records are batched in-process as gzipped JSONL and each finished
batch is uploaded via a single `PutObject`. Object keys use a simple
time-based layout for this PR (`{prefix}/{yyyy}/{mm}/{dd}/{host}-
{HHMMSS}-{seq}.jsonl.gz`); richer partitioning (model/day/hour Hive
style), tunable roll thresholds, and Prometheus metrics ship in a
follow-up.

Credentials come from the AWS SDK default provider chain -- env vars,
IMDS, IRSA, Pod Identity, and shared profiles are all handled by the
SDK. How the frontend pod is credentialed is a deployment concern
documented in a separate PR, not this sink's.

The `request-trace-s3` cargo feature is off by default so the AWS
SDK dependency is opt-in. When the feature is off, selecting
`DYN_REQUEST_TRACE_SINKS=...,s3` fails startup with a clear error.

Refs: ai-dynamo#11768 (supersedes ai-dynamo#10903).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Yiqiu Liu <yiqiuliu@amazon.com>
YiqiuLiu added a commit to YiqiuLiu/dynamo that referenced this pull request Jul 21, 2026
Adds an S3 destination for `dynamo.request.trace.v1` records as a
new `RequestTraceSink` implementation, parallel to the existing
OTLP sink. Same record shape as every other sink -- new destination,
no schema change.

Records are batched in-process as gzipped JSONL and each finished
batch is uploaded via a single `PutObject`. Object keys use a simple
time-based layout for this PR (`{prefix}/{yyyy}/{mm}/{dd}/{host}-
{HHMMSS}-{seq}.jsonl.gz`); richer partitioning (model/day/hour Hive
style), tunable roll thresholds, and Prometheus metrics ship in a
follow-up.

Credentials come from the AWS SDK default provider chain -- env vars,
IMDS, IRSA, Pod Identity, and shared profiles are all handled by the
SDK. How the frontend pod is credentialed is a deployment concern
documented in a separate PR, not this sink's.

The `request-trace-s3` cargo feature is off by default so the AWS
SDK dependency is opt-in. When the feature is off, selecting
`DYN_REQUEST_TRACE_SINKS=...,s3` fails startup with a clear error.

Refs: ai-dynamo#11768 (supersedes ai-dynamo#10903).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Yiqiu Liu <yiqiuliu@amazon.com>
YiqiuLiu added a commit to YiqiuLiu/dynamo that referenced this pull request Jul 21, 2026
Adds an S3 destination for `dynamo.request.trace.v1` records as a
new `RequestTraceSink` implementation, parallel to the existing
OTLP sink. Same record shape as every other sink -- new destination,
no schema change.

Records are batched in-process as gzipped JSONL and each finished
batch is uploaded via a single `PutObject`. Object keys use a simple
time-based layout for this PR (`{prefix}/{yyyy}/{mm}/{dd}/{host}-
{HHMMSS}-{seq}.jsonl.gz`); richer partitioning (model/day/hour Hive
style), tunable roll thresholds, and Prometheus metrics ship in a
follow-up.

Credentials come from the AWS SDK default provider chain -- env vars,
IMDS, IRSA, Pod Identity, and shared profiles are all handled by the
SDK. How the frontend pod is credentialed is a deployment concern
documented in a separate PR, not this sink's.

The `request-trace-s3` cargo feature is off by default so the AWS
SDK dependency is opt-in. When the feature is off, selecting
`DYN_REQUEST_TRACE_SINKS=...,s3` fails startup with a clear error.

Refs: ai-dynamo#11768 (supersedes ai-dynamo#10903).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Yiqiu Liu <yiqiuliu@amazon.com>
YiqiuLiu added a commit to YiqiuLiu/dynamo that referenced this pull request Jul 21, 2026
Adds an S3 destination for `dynamo.request.trace.v1` records as a
new `RequestTraceSink` implementation, parallel to the existing
OTLP sink. Same record shape as every other sink -- new destination,
no schema change.

Records are batched in-process as gzipped JSONL and each finished
batch is uploaded via a single `PutObject`. Object keys use a simple
time-based layout for this PR (`{prefix}/{yyyy}/{mm}/{dd}/{host}-
{HHMMSS}-{seq}.jsonl.gz`); richer partitioning (model/day/hour Hive
style), tunable roll thresholds, and Prometheus metrics ship in a
follow-up.

Credentials come from the AWS SDK default provider chain -- env vars,
IMDS, IRSA, Pod Identity, and shared profiles are all handled by the
SDK. How the frontend pod is credentialed is a deployment concern
documented in a separate PR, not this sink's.

The `request-trace-s3` cargo feature is off by default so the AWS
SDK dependency is opt-in. When the feature is off, selecting
`DYN_REQUEST_TRACE_SINKS=...,s3` fails startup with a clear error.

Refs: ai-dynamo#11768 (supersedes ai-dynamo#10903).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Yiqiu Liu <yiqiuliu@amazon.com>
YiqiuLiu added a commit to YiqiuLiu/dynamo that referenced this pull request Jul 24, 2026
Adds an S3 destination for `dynamo.request.trace.v1` records as a
new `RequestTraceSink` implementation, parallel to the existing
OTLP sink. Same record shape as every other sink -- new destination,
no schema change.

Records are batched in-process as gzipped JSONL and each finished
batch is uploaded via a single `PutObject`. Object keys use a simple
time-based layout for this PR (`{prefix}/{yyyy}/{mm}/{dd}/{host}-
{HHMMSS}-{seq}.jsonl.gz`); richer partitioning (model/day/hour Hive
style), tunable roll thresholds, and Prometheus metrics ship in a
follow-up.

Credentials come from the AWS SDK default provider chain -- env vars,
IMDS, IRSA, Pod Identity, and shared profiles are all handled by the
SDK. How the frontend pod is credentialed is a deployment concern
documented in a separate PR, not this sink's.

The `request-trace-s3` cargo feature is off by default so the AWS
SDK dependency is opt-in. When the feature is off, selecting
`DYN_REQUEST_TRACE_SINKS=...,s3` fails startup with a clear error.

Refs: ai-dynamo#11768 (supersedes ai-dynamo#10903).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Yiqiu Liu <yiqiuliu@amazon.com>
YiqiuLiu added a commit to YiqiuLiu/dynamo that referenced this pull request Jul 27, 2026
Adds an S3 destination for `dynamo.request.trace.v1` records as a
new `RequestTraceSink` implementation, parallel to the existing
OTLP sink. Same record shape as every other sink -- new destination,
no schema change.

Records are batched in-process as gzipped JSONL and each finished
batch is uploaded via a single `PutObject`. Object keys use a simple
time-based layout for this PR (`{prefix}/{yyyy}/{mm}/{dd}/{host}-
{HHMMSS}-{seq}.jsonl.gz`); richer partitioning (model/day/hour Hive
style), tunable roll thresholds, and Prometheus metrics ship in a
follow-up.

Credentials come from the AWS SDK default provider chain -- env vars,
IMDS, IRSA, Pod Identity, and shared profiles are all handled by the
SDK. How the frontend pod is credentialed is a deployment concern
documented in a separate PR, not this sink's.

The `request-trace-s3` cargo feature is off by default so the AWS
SDK dependency is opt-in. When the feature is off, selecting
`DYN_REQUEST_TRACE_SINKS=...,s3` fails startup with a clear error.

Refs: ai-dynamo#11768 (supersedes ai-dynamo#10903).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Yiqiu Liu <yiqiuliu@amazon.com>
YiqiuLiu added a commit to YiqiuLiu/dynamo that referenced this pull request Jul 27, 2026
Adds an S3 destination for `dynamo.request.trace.v1` records as a
new `RequestTraceSink` implementation, parallel to the existing
OTLP sink. Same record shape as every other sink -- new destination,
no schema change.

Records are batched in-process as gzipped JSONL and each finished
batch is uploaded via a single `PutObject`. Object keys use a simple
time-based layout for this PR (`{prefix}/{yyyy}/{mm}/{dd}/{host}-
{HHMMSS}-{seq}.jsonl.gz`); richer partitioning (model/day/hour Hive
style), tunable roll thresholds, and Prometheus metrics ship in a
follow-up.

Credentials come from the AWS SDK default provider chain -- env vars,
IMDS, IRSA, Pod Identity, and shared profiles are all handled by the
SDK. How the frontend pod is credentialed is a deployment concern
documented in a separate PR, not this sink's.

The `request-trace-s3` cargo feature is off by default so the AWS
SDK dependency is opt-in. When the feature is off, selecting
`DYN_REQUEST_TRACE_SINKS=...,s3` fails startup with a clear error.

Refs: ai-dynamo#11768 (supersedes ai-dynamo#10903).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Yiqiu Liu <yiqiuliu@amazon.com>
YiqiuLiu added a commit to YiqiuLiu/dynamo that referenced this pull request Jul 27, 2026
Adds an S3 destination for `dynamo.request.trace.v1` records as a
new `RequestTraceSink` implementation, parallel to the existing
OTLP sink. Same record shape as every other sink -- new destination,
no schema change.

Records are batched in-process as gzipped JSONL and each finished
batch is uploaded via a single `PutObject`. Object keys use a simple
time-based layout for this PR (`{prefix}/{yyyy}/{mm}/{dd}/{host}-
{HHMMSS}-{seq}.jsonl.gz`); richer partitioning (model/day/hour Hive
style), tunable roll thresholds, and Prometheus metrics ship in a
follow-up.

Credentials come from the AWS SDK default provider chain -- env vars,
IMDS, IRSA, Pod Identity, and shared profiles are all handled by the
SDK. How the frontend pod is credentialed is a deployment concern
documented in a separate PR, not this sink's.

The `request-trace-s3` cargo feature is off by default so the AWS
SDK dependency is opt-in. When the feature is off, selecting
`DYN_REQUEST_TRACE_SINKS=...,s3` fails startup with a clear error.

Refs: ai-dynamo#11768 (supersedes ai-dynamo#10903).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Yiqiu Liu <yiqiuliu@amazon.com>
YiqiuLiu added a commit to YiqiuLiu/dynamo that referenced this pull request Jul 28, 2026
Adds an S3 destination for `dynamo.request.trace.v1` records as a
new `RequestTraceSink` implementation, parallel to the existing
OTLP sink. Same record shape as every other sink -- new destination,
no schema change.

Records are batched in-process as gzipped JSONL and each finished
batch is uploaded via a single `PutObject`. Object keys use a simple
time-based layout for this PR (`{prefix}/{yyyy}/{mm}/{dd}/{host}-
{HHMMSS}-{seq}.jsonl.gz`); richer partitioning (model/day/hour Hive
style), tunable roll thresholds, and Prometheus metrics ship in a
follow-up.

Credentials come from the AWS SDK default provider chain -- env vars,
IMDS, IRSA, Pod Identity, and shared profiles are all handled by the
SDK. How the frontend pod is credentialed is a deployment concern
documented in a separate PR, not this sink's.

The `request-trace-s3` cargo feature is off by default so the AWS
SDK dependency is opt-in. When the feature is off, selecting
`DYN_REQUEST_TRACE_SINKS=...,s3` fails startup with a clear error.

Refs: ai-dynamo#11768 (supersedes ai-dynamo#10903).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Yiqiu Liu <yiqiuliu@amazon.com>
YiqiuLiu added a commit to YiqiuLiu/dynamo that referenced this pull request Jul 29, 2026
Adds an S3 destination for `dynamo.request.trace.v1` records as a
new `RequestTraceSink` implementation, parallel to the existing
OTLP sink. Same record shape as every other sink -- new destination,
no schema change.

Records are batched in-process as gzipped JSONL and each finished
batch is uploaded via a single `PutObject`. Object keys use a simple
time-based layout for this PR (`{prefix}/{yyyy}/{mm}/{dd}/{host}-
{HHMMSS}-{seq}.jsonl.gz`); richer partitioning (model/day/hour Hive
style), tunable roll thresholds, and Prometheus metrics ship in a
follow-up.

Credentials come from the AWS SDK default provider chain -- env vars,
IMDS, IRSA, Pod Identity, and shared profiles are all handled by the
SDK. How the frontend pod is credentialed is a deployment concern
documented in a separate PR, not this sink's.

The `request-trace-s3` cargo feature is off by default so the AWS
SDK dependency is opt-in. When the feature is off, selecting
`DYN_REQUEST_TRACE_SINKS=...,s3` fails startup with a clear error.

Refs: ai-dynamo#11768 (supersedes ai-dynamo#10903).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Yiqiu Liu <yiqiuliu@amazon.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

deployment::k8s Relates to dynamo deployment in kubernetes documentation Improvements or additions to documentation external-contribution Pull request is from an external contributor feat size/XXL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant