Skip to content

feat: add observability DB schema (Phase 0) - #48

Merged
Aliciapet11 merged 2 commits into
mainfrom
feat/observability-mlflow
Jul 14, 2026
Merged

feat: add observability DB schema (Phase 0)#48
Aliciapet11 merged 2 commits into
mainfrom
feat/observability-mlflow

Conversation

@Aliciapet11

Copy link
Copy Markdown
Collaborator

Summary

  • Add 4 new normalized database tables: scorecards, gate_results, certifications, observability_metrics
  • Add 4 Alembic migrations (001-004) for schema management
  • Update store_results.py to persist scorecard data alongside evaluation runs
  • Add pyyaml to store task dependencies (required by new Scorecard import chain)
  • Add backfill script for existing MinIO artifacts (scripts/backfill_scorecards.py)
  • Add Kubernetes migration job manifest (config/migrations/migration-job.yaml)
  • 24 new tests (693 total passing)

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

  • All 693 tests pass locally (SQLite in-memory)
  • CI Harbor pipeline ci-harbor-k4rtw passed successfully on the cluster with this branch
  • Scorecard data confirmed persisted: 1 scorecard, 4 gate results, 3 certifications

Test plan

  • Existing tests pass (693/693)
  • New model unit tests pass (17 tests)
  • New integration tests pass (7 tests)
  • CI Harbor pipeline passes on cluster
  • Data visible in production DB tables

🤖 Generated with Claude Code

@Aliciapet11
Aliciapet11 requested a review from GuyZivRH July 8, 2026 13:09

@GuyZivRH GuyZivRH left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Branch: feat/observability-mlflowmain
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 verdict
  • gate_results — one row per gate per run (normalized)
  • certifications — one row per certification level per run
  • observability_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>..HEAD

Major

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
  1. :latest tag — image digest changes without notice
  2. Runtime pip install — fails without outbound internet; versions unconstrained
  3. git clone from GitHub — always clones current main, 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 sync with 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 head workflow 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/abevalflow

alembic/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)
  • BigInteger for 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

@Aliciapet11
Aliciapet11 force-pushed the feat/observability-mlflow branch from 5000ab4 to fd8e6c4 Compare July 12, 2026 14:50
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
@Aliciapet11
Aliciapet11 force-pushed the feat/observability-mlflow branch from fd8e6c4 to 368fee5 Compare July 12, 2026 14:56
@Aliciapet11

Copy link
Copy Markdown
Collaborator Author

Thanks for the thorough review! All 15 items addressed in the latest push:

Critical/Major (must fix):

  • Item 1 — Scorecard writes now wrapped in session.begin_nested() savepoint — partial writes roll back atomically
  • Item 2 — Squashed into a single commit, Co-Authored-By trailers removed
  • Item 3 — Migration job: pinned base image by digest, uses pip install -e . with lockfile, git ref parameterized via PIPELINE_REPO_REVISION env var

Medium:

  • Item 5 — Added UniqueConstraint("scorecard_id", "level") on certifications table + migration
  • Item 6 — Documented in docstring: metrics intentionally have no FK to scorecards (metrics may outlive scorecard lifecycle)
  • Item 7 — Added alembic/README.md documenting baseline migration strategy for existing tables and the sequential revision ID convention

Minor:

  • Item 4 — Added docstring noting ObservabilityMetricsRow is schema-only in Phase 0, populated by MetricsContext in Phase A
  • Item 8 — Changed alembic.ini default to abevalflow_dev with comment about DATABASE_URL requirement
  • Item 9 — Widened recommendation column to String(20) in both model and migration
  • Item 10 — Backfill script now uses start_after for streaming pagination instead of loading all keys into memory
  • Item 11 — Added tests/test_backfill.py — 6 tests covering BackfillState, checkpoint save/load, error truncation
  • Item 12 — Added test_scorecard_unique_constraint that exercises the DB-level uniqueness directly
  • Item 13 — Documented sequential revision ID convention in alembic/README.md
  • Item 14 — Added docstring on ScorecardRow explaining scorecard_json duplication as intentional design choice
  • Item 15_save_checkpoint now accepts configurable max_errors parameter

@GuyZivRH GuyZivRH left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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
@Aliciapet11

Copy link
Copy Markdown
Collaborator Author

Fixed — migration job now uses pip install uv && uv sync --frozen instead of pip install -e .. This ensures lockfile-based installation for reproducible builds in air-gapped environments.

@GuyZivRH GuyZivRH left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

rebase and verify with pipeline runs

@Aliciapet11
Aliciapet11 merged commit a5180c9 into main Jul 14, 2026
1 check passed
GuyZivRH added a commit that referenced this pull request Jul 19, 2026
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.
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.

2 participants