Skip to content

feat: Phase 5-6 — stream processing, drift detection, and a full review-driven hardening pass#45

Merged
VjayRam merged 11 commits into
masterfrom
feat/classifier
Jul 3, 2026
Merged

feat: Phase 5-6 — stream processing, drift detection, and a full review-driven hardening pass#45
VjayRam merged 11 commits into
masterfrom
feat/classifier

Conversation

@VjayRam

@VjayRam VjayRam commented Jul 3, 2026

Copy link
Copy Markdown
Owner

Summary

Merges Phases 5-6 (trace ingestion/stream processing + drift detection) along with a full round of code-review-driven fixes.

Features:

  • Kafka-based stream processor: consumes OTLP traces, classifies via the classifier service, writes to PostgreSQL + MongoDB with at-least-once/idempotent semantics
  • OpenTelemetry GenAI semantic conventions adopted; classifier's primary endpoint is now /v1/moderations (OpenAI Moderation API-compatible)
  • PySpark-based drift detection pipeline (PSI/JSD metrics), spark-on-k8s-operator integration
  • Config migrated to Pydantic settings; readiness probes and queue-depth monitoring added
  • Evaluation pipeline (pipelines/evaluation/) implemented from scratch: benchmarks a candidate model against a real 3780-row ground-truth set and gates promotion on accuracy vs. a minimum and vs. baseline

Fixes (from a structured multi-agent code review of the full branch — 34 issues found and resolved across three passes):

  • Data-loss / false-signal class: Kafka losing all topic data on pod restart (PVC mount pointed at a path the image never wrote to), drift_stats table missing from Terraform's schema (only created by an ad-hoc script), drift job attributing stats to the wrong model version during rollouts, a false-positive drift signal possible on a too-small reference window, classifier batcher leaking futures (requests hang forever) on shutdown, MongoDB writes not idempotent under Kafka redelivery
  • Leaks / bounds: asyncpg pool leak on startup failure, unbounded row fetch in the drift job risking a Spark driver OOM, MinIO bucket name hardcoded separately from the configurable env var
  • Dev tooling: dev-start.sh error handling that aborted the whole script under set -e instead of warning and continuing, bare SQL string interpolation replaced with psql -v substitution, Kafka/Jaeger images pinned off :latest, Prometheus/Grafana version drift between the k8s and docker-compose dev paths resolved
  • Cleanup: deduplicated several near-identical code blocks (classifier's batch/moderation endpoints, model timestamp resolution, optimizer's register+report branches), cached S3 clients instead of rebuilding per call, parallelized optimizer file uploads, removed dead test fixtures

One correction worth flagging in review: an earlier fix in this pass had moved the stream processor from /v1/moderations back to /classify/batch to avoid a response-shape remapping — this accidentally reverted a deliberate prior decision (dogfooding the same OpenAI-compatible endpoint external callers use). It's been corrected back to /v1/moderations, with the actual underlying concern (an internal persist field leaking into the public schema) fixed properly via an X-Sentinel-Skip-Persist header instead.

Test plan

  • pytest tests/ — 15/15 passing
  • pytest pipelines/drift/tests/test_metrics.py — 14/14 passing (PySpark)
  • terraform validate — passing
  • Live smoke test: real ONNX model load + inference against actual exported artifacts
  • Live smoke test: real HTTP calls to /classify/batch and /v1/moderations (with and without X-Sentinel-Skip-Persist) against a running server
  • Direct PySpark verification that negative scores now clamp to bin 0 instead of vanishing from drift metrics
  • Full end-to-end verification on a live k3d cluster (cluster wasn't running during this session — Kafka data-loss fix, drift job against live Postgres, and evaluation pipeline against a live model registry should be re-verified once the cluster is back up)

🤖 Generated with Claude Code

VjayRam and others added 10 commits June 26, 2026 18:00
…ng, and update classification logic to support multi-class outputs.
…on and persistence to PostgreSQL and MongoDB
…n attributes to events and renaming the classifier endpoint to /v1/moderations
…nd implement batching for moderation requests
… reliability with database connection handling.
…code review

Fixes seven issues flagged as highest-severity in a multi-agent code review
(#29, #30, #31, #32, #33, #34, #5) — each one triggers on a normal operation
(restart, redeploy, routine drift run) and either loses data or corrupts a
signal something downstream automates on.

- Kafka: point KAFKA_LOG_DIRS at the mounted PVC — apache/kafka:latest was
  defaulting to /tmp/kraft-combined-logs, silently discarding all topic data
  on every pod restart (#29)
- Postgres: move drift_stats into Terraform's schema init so a plain
  `terraform apply` provisions it; drop the now-redundant manual migration
  from dev-start.sh (#30)
- Drift job: read the active model version from model_registry (mirroring
  the classifier's own active/staging selection) instead of "last row
  written to classifications wins", which raced during rolling restarts (#33)
- Drift job: exit early on a too-small reference window instead of
  computing PSI/JSD against an unreliable baseline and risking a false
  drift_flagged=True (#34)
- Classifier batcher: stop() now drains the queue and fails in-flight
  batches with a clear exception instead of leaking futures that hang
  forever past a CancelledError (#31)
- Stream processor: flagged_content writes upsert on (span_id, text_type)
  via bulk_write, with a matching unique partial index in Mongo, so Kafka
  redelivery can no longer duplicate retraining data (#32)
- Evaluation pipeline: implement benchmark.py + validate.py (previously
  empty files) — scores a candidate model against a real 3780-row
  ground-truth set and gates promotion on accuracy vs. a minimum and vs.
  baseline, closing the "any model gets promoted with zero validation" gap (#5)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…nd bucket mismatch

Fixes six "fix soon" issues from the code review (#35, #36, #37, #6, #42, #43)
— real bugs with bounded blast radius, as opposed to the data-loss/false-signal
class fixed in 03d2f9b.

- Optimizer: derive model_path from the quantize stage's actual upload_stage()
  return value instead of hardcoding the "models" bucket prefix, so it can't
  diverge from a customized MINIO_BUCKET (#35)
- Drift job: cap read_current_scores at 100k most-recent rows (via a
  subquery, not a naive LIMIT that would return the oldest rows) to bound
  driver memory on a large --hours window (#36)
- Classifier: close the asyncpg pool before discarding it on a post-creation
  startup failure, instead of leaking the connection (#37)
- Stream processor: close the replacement connection too when a reconnect
  retry itself fails, instead of leaking it on every double-failure (#6)
- Classifier: unify MAX_BATCH_SIZE — schemas.py now reads config.py's
  settings.max_batch_size instead of a second hardcoded constant, so the
  env var actually controls both the batcher and the request-validation
  cap (previously the ClassifierBatchBackpressure alert's "raise
  MAX_BATCH_SIZE" remediation was a no-op). Stream processor's chunk size
  is now the CLASSIFY_CHUNK_SIZE env var instead of a silent hardcoded
  copy (#42)
- Classifier: skip registering a local-filesystem model_path in
  model_registry — that path only exists on one pod's filesystem, and a
  second pod reading it back from the registry would crash on startup (#43)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…d API cleanup

Fixes the last 20 open review issues (#8, #9, #13, #14, #15, #16, #17, #19,
#20, #21, #22, #23, #24, #25, #27, #28, #38, #39, #40, #41, #44), closes #26
as resolved-by-different-means rather than fixed.

Classifier service:
- Switch tokenizer padding to max_length so inference latency doesn't vary
  with what else happens to land in the same batch (#8)
- Extract _deployed_at_from_dir() to remove duplicated onnx-mtime logic
  between Classifier.__init__ and _resolve_model_dir; parse each
  report.json once instead of twice when auto-selecting a model (#19, #20)
- Extract _classify_and_persist() shared by classify_batch()/moderate() —
  removes ~25 duplicated lines of timing/metrics/persistence logic (#21)
- Remove the persist field from ModerationRequest (OpenAI-compatible public
  schema) now that nothing needs to skip persistence there — the stream
  processor calls /classify/batch instead, which keeps its own persist
  field since that endpoint is explicitly internal/testing-only (#24)
- write_classification/write_classifications_batch now include the same
  span_id/text_type columns + ON CONFLICT DO NOTHING conflict target as
  the stream processor's writer, instead of a plain INSERT with no
  idempotency (#25)
- Update CLAUDE.md's classifier design rule to describe the actual
  async+executor/batcher pattern instead of a stale "must be sync def"
  rule the code no longer followed (#40)
- Cache the MinIO S3 client with lru_cache instead of rebuilding it (TLS
  handshake + credential resolution) on every download call (#17)

Stream processor:
- Call /classify/batch instead of /v1/moderations — returns {label, score}
  directly, eliminating the flagged/category_scores.harm remapping that
  coupled this consumer to the OpenAI-compatible wire format (#23)

Optimizer pipeline:
- Add pipelines/optimizer/__main__.py; invoke via `python -m
  pipelines.optimizer` instead of the fragile `...pipeline` module path
  (updated README.md, docs/local-dev.md, dev-start.sh, explanation.md) (#13)
- Cache the MinIO S3 client; parallelize upload_stage()'s file uploads via
  ThreadPoolExecutor instead of uploading one at a time (#17, #27)
- register_model() now takes a connection instead of opening its own per
  call; pipeline.py dedupes the register+report if/else branches that only
  differed by model_path and a suffix string (#22, #28)

Infra:
- Pin apache/kafka and jaegertracing/all-in-one off :latest to specific
  versions for reproducible applies (#9)
- Align docker-compose's Prometheus/Grafana versions with Terraform's
  (the primary deployment path) instead of drifting onto different major
  versions (#16)
- Split prometheus.compose.yml from prometheus.yml — the classifier scrape
  target differs (compose network name vs. k8s FQDN), and listing both
  targets in one file would leave one permanently down in every
  environment, firing ClassifierDown nonstop (#41)

Dev tooling:
- Wrap all 9 wait_for_port call sites so a timeout warns and continues
  instead of aborting the whole script under set -e (#38)
- Replace bare SQL string interpolation (PG_PASSWORD, $_minio_path) with
  psql -v variable substitution (#39)
- Delete the dead mock_predict fixture and the placeholder test now that
  real tests exist (#14, #15)

Drift pipeline:
- Symmetric clamp in _bin_scores: negative scores now fold into bin 0
  instead of silently vanishing from the PSI/JSD left join (#44)

#26 (ad-hoc SQL migrations) is closed as resolved-by-different-means:
schema ownership already moved fully into Terraform's postgres_init
ConfigMap in an earlier commit; adopting Alembic/Flyway now would be new
infrastructure ahead of this project's current phase.

Verified: classifier test suite (15/15), drift PySpark test suite (14/14),
terraform validate, two live smoke tests (real ONNX model load +
predict, real /classify/batch HTTP call against a running server), and a
direct PySpark check that negative scores clamp to bin 0.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…sion

Commit a1f4421's fix for #23 moved the stream processor from /v1/moderations
back to /classify/batch to avoid a response-shape remapping. That undid a
deliberate architectural decision from 46ea2aa ("adopting OpenTelemetry
GenAI semantic conventions... renaming the classifier endpoint to
/v1/moderations"), which had moved the stream processor onto
/v1/moderations specifically so Sentinel's own highest-volume internal
traffic exercises the same OpenAI-compatible code path external callers
use. I made that change without checking git history for why the endpoint
was chosen in the first place — caught after the fact.

Reverted the endpoint choice back to /v1/moderations. Properly fixed the
actual problem #24 was about (the `persist` field leaking Sentinel-internal
plumbing into the public OpenAI-compatible schema) using the header-based
approach that issue originally suggested and I'd skipped: an
X-Sentinel-Skip-Persist request header instead of a body field.
ModerationRequest carries no Sentinel-internal fields now, and the stream
processor still gets to skip classifier-side persistence.

The response-shape coupling #23 raised is real but inherent to calling an
OpenAI-compatible endpoint from internal code — extracted it into a named
_moderation_results_to_label_score() helper so the translation is a visible,
intentional boundary rather than an inline remap, without abandoning the
dogfooding architecture.

Verified: 15/15 classifier tests pass, and two live smoke tests against a
running server — /v1/moderations without the header returns the full
public response, with the header still returns correctly (schema/routing
confirmed; DB persistence itself untestable here with no live database).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@VjayRam, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 56 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Free

Run ID: 713ed47f-5f43-4b55-a30b-5f784ded0cfe

📥 Commits

Reviewing files that changed from the base of the PR and between f1658d2 and 1e8aef1.

⛔ Files ignored due to path filters (2)
  • datasets/test_dataset.csv is excluded by !**/*.csv
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (59)
  • .gitignore
  • CLAUDE.md
  • README.md
  • datasets/__init__.py
  • datasets/eval_holdout.py
  • docker-compose.yml
  • docs/local-dev.md
  • infra/explanation.md
  • infra/grafana/explanation.md
  • infra/prometheus/explanation.md
  • infra/prometheus/prometheus.compose.yml
  • infra/prometheus/prometheus.yml
  • infra/prometheus/rules/classifier.yml
  • infra/terraform/local/explanation.md
  • infra/terraform/local/main.tf
  • infra/terraform/local/outputs.tf
  • infra/terraform/local/variables.tf
  • pipelines/drift/Dockerfile
  • pipelines/drift/__init__.py
  • pipelines/drift/db.py
  • pipelines/drift/drift_job.py
  • pipelines/drift/metrics.py
  • pipelines/drift/pyproject.toml
  • pipelines/drift/spark-application.yaml
  • pipelines/drift/tests/__init__.py
  • pipelines/drift/tests/conftest.py
  • pipelines/drift/tests/test_db.py
  • pipelines/drift/tests/test_metrics.py
  • pipelines/evaluation/benchmark.py
  • pipelines/evaluation/pyproject.toml
  • pipelines/evaluation/validate.py
  • pipelines/optimizer/__main__.py
  • pipelines/optimizer/explanation.md
  • pipelines/optimizer/pipeline.py
  • pipelines/optimizer/registry.py
  • pipelines/optimizer/upload.py
  • pyproject.toml
  • scripts/dev-start.sh
  • scripts/simulate-traces.py
  • services/classifier/Dockerfile
  • services/classifier/batcher.py
  • services/classifier/config.py
  • services/classifier/db.py
  • services/classifier/download.py
  • services/classifier/explanation.md
  • services/classifier/main.py
  • services/classifier/metrics.py
  • services/classifier/model.py
  • services/classifier/pyproject.toml
  • services/classifier/schemas.py
  • services/stream-processor/Dockerfile
  • services/stream-processor/explanation.md
  • services/stream-processor/main.py
  • services/stream-processor/processor.py
  • services/stream-processor/pyproject.toml
  • services/stream-processor/writer.py
  • tests/conftest.py
  • tests/test_classifier_api.py
  • tests/test_placeholder.py

Note

🎁 Summarized by CodeRabbit Free

Your organization is on the Free plan. CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please upgrade your subscription to CodeRabbit Pro by visiting https://app.coderabbit.ai/login.

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

CI's Lint & format job failed on PR #45 — 4 pre-existing unsorted-import
errors (pipelines/drift/tests/test_db.py, test_metrics.py; the import I
added to pipelines/optimizer/pipeline.py wasn't alphabetized either) plus
formatting drift across files touched in this session's fix commits.
No behavior change — ruff check --fix + ruff format only.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@VjayRam
VjayRam merged commit 63753da into master Jul 3, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant