Skip to content

[OPIK-7402] [BE] Add audit (shadow / log-only) mode to UUIDv7 ingestion validation#7573

Open
thiagohora wants to merge 7 commits into
mainfrom
thiaghora/OPIK-7402-uuid-validation-audit-mode
Open

[OPIK-7402] [BE] Add audit (shadow / log-only) mode to UUIDv7 ingestion validation#7573
thiagohora wants to merge 7 commits into
mainfrom
thiaghora/OPIK-7402-uuid-validation-audit-mode

Conversation

@thiagohora

Copy link
Copy Markdown
Contributor

Details

Adds a third audit (shadow / log-only) state to uuidValidation, on top of the existing enabled kill-switch. When enabled=true and auditOnly=true, ids whose embedded UUIDv7 timestamp is out of the window are counted and logged but not rejected, so clients emitting them surface in real time without breaking ingestion. This is the freshness mechanism for safely re-enabling rejection later.

  • Effective mode: enabled=false → disabled (no-op); enabled=true & auditOnly=true → audit; enabled=true & auditOnly=false → reject (HTTP 400, unchanged).
  • Audit mode emits opik.ingestion.uuid_v7.rejected (mode=audit) tagged by workspace_id + reason + resource, so offenders can be broken down per workspace. It shares the counter/instrument with the reject-path mapper.
  • Scope is the window check (the enabled-gated one). The always-on NOT_V7 version check is unchanged. workspace_id is read from the reactive context on the async paths and threaded through the batch bind on the sync path; project_id is not available at this layer and is left as a follow-up. Reject-path workspace tagging is likewise a follow-up.

Change checklist

  • User facing
  • Documentation update

Issues

  • OPIK-7402

AI-WATERMARK

AI-WATERMARK: yes

  • Tools: Claude Code
  • Model(s): Claude Opus 4.8 (1M context)
  • Scope: full implementation (config, validator, metrics recorder, call-site plumbing, unit test) and this description, authored under human direction and review.
  • Human verification: design decisions (additive auditOnly flag, scope limited to the window check, workspace-only tagging) chosen by the author; mvn test-compile and the new unit test run and green locally; diff reviewed.

Testing

  • mvn test-compile — BUILD SUCCESS (main + tests).
  • mvn surefire:test -Dtest=UuidV7TimestampValidatorTest — 5 tests, 0 failures. Covers all three modes: disabled (accepts out-of-window ids), reject (accepts in-window; rejects too-old/too-far-future on create, only too-far-future on update), audit (never rejects, tolerates a blank workspace id).
  • Not run locally: resource/integration tests (bootstrap is flaky locally) — left to CI.

Documentation

Config documented inline in config.yml / config-test.yml (new auditOnly key, effective-mode table) and in the UuidValidationConfig / UuidV7TimestampValidator / UuidValidationMetrics javadoc. No user-facing docs change.

…on validation

Adds a third `auditOnly` state to uuidValidation on top of the `enabled`
kill-switch. When enabled=true and auditOnly=true, out-of-window UUIDv7 ids
are counted and logged but NOT rejected, so clients emitting them surface in
real time without breaking ingestion. Effective mode:
enabled=false -> disabled; enabled=true & auditOnly=true -> audit;
enabled=true & auditOnly=false -> reject (HTTP 400, unchanged).

- New UuidValidationMetrics records opik.ingestion.uuid_v7.rejected in audit
  mode, tagged mode=audit + reason + resource + workspace_id (shares the
  instrument with InvalidUUIDExceptionMapper's reject-path counter).
- Validator gains resource/workspaceId params; audit branch emits + logs
  instead of throwing. Scope is the window check; the always-on NOT_V7
  version check is unchanged.
- workspaceId is read from the reactive context on the async paths and
  threaded through the batch bind on the sync path.
- Unit test covers disabled/reject/audit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@thiagohora
thiagohora requested a review from a team as a code owner July 22, 2026 14:17
@github-actions github-actions Bot added java Pull requests that update Java code Backend tests Including test files, or tests related like configuration. 🟠 size/L labels Jul 22, 2026
@github-actions

github-actions Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

⏱️ pre-commit per-hook timing

Hook Description Result Duration
☕ spotless — java backend Format Java code 5.80s
Total (1 ran) 5.80s
⏭️ 40 skipped (no matching files changed)
Hook Description Result
🐍 trim trailing whitespace — python sdk Strip trailing whitespace ⏭️
🐍 fix end of files — python sdk Ensure files end in a newline ⏭️
🐍 ruff — python sdk Lint + autofix Python (ruff) ⏭️
🐍 ruff-format — python sdk Format Python code (ruff) ⏭️
🐍 mypy — python sdk Static type check ⏭️
🤖 trim trailing whitespace — optimizer Strip trailing whitespace ⏭️
🤖 fix end of files — optimizer Ensure files end in a newline ⏭️
🤖 check yaml — optimizer Validate YAML syntax ⏭️
🤖 check json — optimizer Validate JSON syntax ⏭️
🤖 check toml — optimizer Validate TOML syntax ⏭️
🤖 check for added large files — optimizer Block large files (>1MB) ⏭️
🔐 detect private key — optimizer Block committed private keys ⏭️
🤖 check for merge conflicts — optimizer Block merge-conflict markers ⏭️
🤖 check for case conflicts — optimizer Block case-only name clashes ⏭️
🤖 pyupgrade — optimizer Modernize Python syntax ⏭️
🤖 ruff — optimizer Lint + autofix Python (ruff) ⏭️
🤖 ruff-format — optimizer Format Python code (ruff) ⏭️
🤖 mypy — optimizer Static type check ⏭️
📓 nbstripout — optimizer notebooks Strip notebook output ⏭️
📝 markdownlint — optimizer Lint Markdown ⏭️
🔤 codespell — optimizer Fix common misspellings ⏭️
📊 radon cc — optimizer Cyclomatic-complexity gate ⏭️
📊 radon raw — optimizer Raw size metrics gate ⏭️
📊 xenon — optimizer Fail on complexity thresholds ⏭️
📊 lizard — optimizer Cyclomatic-complexity gate ⏭️
🧹 vulture — optimizer Find dead code ⏭️
🛡️ trim trailing whitespace — guardrails Strip trailing whitespace ⏭️
🛡️ fix end of files — guardrails Ensure files end in a newline ⏭️
🛡️ ruff — guardrails Lint + autofix Python (ruff) ⏭️
🛡️ ruff-format — guardrails Format Python code (ruff) ⏭️
🛡️ mypy — guardrails Static type check ⏭️
⚓ helm-docs Regenerate Helm chart README ⏭️
block non-public FE plugins Block non-public FE plugins ⏭️
🧪 pre-commit wrapper smoke tests Self-test the wrapper scripts ⏭️
🌐 eslint — frontend Lint + autofix JS/TS ⏭️
🌐 typecheck — frontend Whole-project tsc type check ⏭️
📘 eslint — typescript sdk Lint + autofix JS/TS ⏭️
📘 typecheck — typescript sdk Whole-project tsc type check ⏭️
⚙️ actionlint — github workflows Lint GitHub Actions workflows ⏭️
🐳 hadolint — dockerfiles Lint Dockerfiles ⏭️

Comment thread apps/opik-backend/src/main/java/com/comet/opik/domain/SpanService.java Outdated
Comment thread apps/opik-backend/src/main/java/com/comet/opik/domain/IdGenerator.java Outdated
thiagohora and others added 6 commits July 22, 2026 17:29
…ounter

Address review: InvalidUUIDExceptionMapper recorded opik.ingestion.uuid_v7.rejected
without a mode label, so the shared counter had series with and without mode and a
`mode=reject` query would miss enforced rejections. Add mode=reject on the reject
path (workspace_id stays audit-only, as the mapper has no threaded workspace), and
align the UuidValidationMetrics contract javadoc: mode is always present; workspace_id
is audit-only; the reject path additionally carries http_route.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…mmed javadoc

- Add IdGeneratorAsyncValidationTest covering validateIdAsync /
  validateIdForUpdateAsync reject+audit behavior, with and without
  RequestContext.WORKSPACE_ID in the reactive context (the deferContextual
  workspace lookup + UNKNOWN fallback).
- Keep a fixed, searchable prefix on the audit log line and move the variable
  fields to the end.
- Trim the IdGenerator#validateId javadoc to validation semantics +
  InvalidUUIDException; keep workspaceId threading (the sync/batch path is the
  primary ingestion route where per-workspace attribution is the goal).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…d validation

Resolve the semantic overlap after merging main (#7553): unify IdGenerator on
the merged API — implement validateIdNotInFuture / validateIdNotInFutureAsync
(and the IfPresent variants) and drop the superseded validateIdForUpdate*.
Own-id validateId stays workspace-tagged for the audit metric; referenced-id
validateIdNotInFuture falls back to UNKNOWN on the sync path and resolves the
workspace from the reactive context on the async path.

Keep #7553's fail-fast (validate before side effects) for span batches, now run
inside deferContextual so the batch's own ids attribute to the request workspace;
bindSpanToProjectAndId no longer re-validates. Trace batches keep validating in
bindTraceToProjectAndId with the workspace threaded through.

Update the async test to the renamed methods.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
bindTraceToProjectAndId validated own ids only after deleteAutoStrippedAttachments
and project getOrCreate, so a bad trace id in a batch still mutated state before
failing (unlike spans, which #7553 validates fail-fast). Hoist trace own-id
validation into a leading deferContextual that runs before any side effect and
attributes the audit metric to the request workspace, and stop re-validating in
bindTraceToProjectAndId — mirroring create(SpanBatch).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@@ -251,8 +262,8 @@ private List<Trace> bindTraceToProjectAndId(List<Trace> traces, List<Project> pr
String projectName = WorkspaceUtils.getProjectName(trace.projectName());

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.

Duplicated project-binding logic

bindTraceToProjectAndId(...) repeats the same project-map lookup and id assignment flow as SpanService.bindSpanToProjectAndId(...), so the project-resolution and id-generation rules live in two places and any change needs duplicate edits — should we factor out a shared binder that takes the entity-specific copy step?

Severity

Want Baz to fix this for you? Activate Fixer

Comment on lines +110 to +114
// Keep a fixed, searchable prefix ("UUIDv7 audit: would-reject id ...") and append the
// variable fields at the end, so log searches match on the message rather than the values.
log.info(
"UUIDv7 audit: would-reject id, embedded timestamp '{}' outside window '{}', reason '{}', resource '{}', workspace '{}'",
timestamp, window, reason.getValue(), resource, workspaceId);

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.

Missing offender correlation

This audit log records timestamp, window, reason, resource, and workspaceId but not the offending UUID, so repeated UUIDv7 audit: would-reject ... entries can't be tied back to a specific bad payload — should we add the actual id to the template/arguments while keeping the fixed prefix?

Severity

Want Baz to fix this for you? Activate Fixer

Other fix methods

Fix in Cursor

Prompt for AI Agents
Before applying, verify this suggestion against the current code. In
apps/opik-backend/src/main/java/com/comet/opik/infrastructure/db/UuidV7TimestampValidator.java
around lines 110-114, in the audit log statement used when rejecting an invalid UUIDv7
(the block that starts with “UUIDv7 audit: would-reject id ...”), the message
template does not include the actual offending `id` value. Refactor the log
template/arguments to append the passed-in UUID `id` to the end (keeping the fixed
“UUIDv7 audit:” prefix unchanged) and add the corresponding placeholder argument so
the log can be tied back to a specific bad payload even when multiple invalid ids share
the same embedded timestamp.

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

Labels

Backend java Pull requests that update Java code 🟠 size/L tests Including test files, or tests related like configuration.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant