Commit 69a12a0
fix(plugin-audit): the Audit write FAILED line names the refused table and the row it lost (#21383)
Fixes #21262
Clause-②: no
## What changed
`reportAuditWriteFailure` in
`packages/plugins/plugin-audit/src/audit-writers.ts` prints the `error`
line for a refused audit-trail insert. The record writer stores the
`sys_audit_log` row that records who did it, then, when activities are
enabled and the write has one, its `sys_activity` timeline row.
- **Which table.** `persistAuditTrailRow` now moves a progress marker
forward before the activity insert, so after a throw the caller knows
which insert was refused. The line opens `Audit write FAILED on TABLE`
and names that table. The writer decides the table. The table is no
longer picked from a list.
- **Which row is lost.** A refused `sys_activity` insert says the ledger
row landed and only the activity row is lost. Its header reads "the
activity timeline is now INCOMPLETE". A refused `sys_audit_log` insert
says the ledger row is lost. When the object writes an activity row, it
says that row is lost too, because it is written only after the ledger
row.
- **Which remedy.** The missing-table question is asked about the
refused table first, then about the other table the writer writes. A
SQLSTATE such as `42P01` with no phrase naming a relation answers
"missing" for any table name. Asked in list order, it would have named
`sys_audit_log` for a refused `sys_activity` insert. For a missing
table, the line no longer asserts the datasource split. It names both
causes the evidence cannot tell apart, in the order to check them: (1)
schema sync never created the table, so look for `Schema sync FAILED for
object 'TABLE'` in the boot log; (2) otherwise the ADR-0057 §3.6
telemetry-datasource split, with `OS_TELEMETRY_DB=0`. Any other cause
keeps the driver-fault remedy unchanged.
- **The once key.** The key gains the refused table: (audited object,
refused table, error code). The sentence now states that key in place of
"this CAUSE is reported ONCE". The `debug` repeat and the `error` meta
carry `table`.
`auditFailureCauseKey` takes an optional third argument for the table.
The two single-table sinks (`auth-event-audit.ts`, `read-audit.ts`) do
not pass it, and their keys are byte-identical to before. Neither
sibling file is touched.
## Measured on `main`, then on this branch
Fixture: a MySQL-shaped refusal (`ER_NO_SUCH_TABLE`, errno 1146, `Table
'objectstack.TABLE' doesn't exist`) thrown by that table's own `create`.
Four audited objects, three writes each. The fixture was a temporary
test file, run and then deleted, never committed.
On `main` at `f39760864`, refused insert into `sys_activity`: 4 `error`
lines, 8 `debug` lines, and 12 `sys_audit_log` rows landed. The first
line:
```text
Audit write FAILED (ER_NO_SUCH_TABLE: Table 'objectstack.sys_activity' doesn't exist) — the compliance trail is now INCOMPLETE. The audited write itself SUCCEEDED and is on disk, so the API returned success and nothing downstream looks broken; only the `sys_audit_log` row that records who did it never landed, and nothing retries it. Every subsequent audited write failing THIS WAY is losing its row the same way (this CAUSE is reported ONCE — raise the log level to `debug` to see the rest; a DIFFERENT cause gets its own `error` line). Fix: confirm `sys_audit_log` is reachable from the connection this write ran on. Its ADR-0057 §3.6 lifecycle class routes it to the dedicated `telemetry` datasource whenever one is registered (`os dev` provisions one by default as a SIBLING SQLite file), so a "no such table" here usually means the write executed against a DIFFERENT datasource than the one the table was created in — see framework#5226. Set `OS_TELEMETRY_DB=0` to keep every lifecycle-classed object on the primary datasource.
```
On `main`, refused insert into `sys_audit_log`: the same text with only
the quoted driver message changed. 4 `error` lines, 8 `debug` lines, and
no rows landed in either table.
On this branch, refused insert into `sys_activity`: still 4 `error`
lines and 8 `debug` lines, and 12 `sys_audit_log` rows landed. The first
line:
```text
Audit write FAILED on `sys_activity` (ER_NO_SUCH_TABLE: Table 'objectstack.sys_activity' doesn't exist) — the activity timeline is now INCOMPLETE. The audited write itself SUCCEEDED and is on disk, and so did its `sys_audit_log` row that records who did it, so the API returned success and the compliance ledger is whole; only the `sys_activity` row — the entry the record's activity timeline and the recent-activity feed show — never landed, and nothing retries it. Every later audited write of 'crm_lead' that `sys_activity` refuses with this same code loses its `sys_activity` row the same way. This line is printed ONCE per audited object, refused table and error code: the same fault on another audited object prints its own line, and so does a different code. Raise the log level to `debug` to see every repeat. Fix: `sys_activity` does not exist on the connection this write reached, and this line cannot tell which of two causes that is, so check them in this order. (1) Schema sync never created it: the boot log then carries `Schema sync FAILED for object 'sys_activity'` with the driver's refusal of its DDL; fix that error and restart, and the table is created (a deployment that runs `OS_SKIP_SCHEMA_SYNC` creates it out-of-band instead). (2) Otherwise it was created on a DIFFERENT datasource than the one this write reached: its ADR-0057 §3.6 lifecycle class routes it to the dedicated `telemetry` datasource whenever one is registered (`os dev` provisions one by default as a SIBLING SQLite file) — see framework#5226. Set `OS_TELEMETRY_DB=0` to keep every lifecycle-classed object on the primary datasource.
```
On this branch, refused insert into `sys_audit_log`: the line opens
`Audit write FAILED on` `sys_audit_log`, keeps "the compliance trail is
now INCOMPLETE", and says that the `sys_audit_log` row never landed,
"and neither did its `sys_activity` timeline row, which is written only
after it". The remedy names `sys_audit_log` in both causes.
## The once key: kept per audited object, with the table added
- **Per audited object, kept.** It is #15166's granularity, and that
card's pin `separates causes per OBJECT as well as per code` holds two
objects failing the same way to two lines. The direction says #15166's
pins stay green. A per-table key would turn that pin red.
- **Refused table, added.** The line now names the table and the lost
row, and it describes every repeat folded under its key. Without the
table in the key, the other table refusing with the same code on the
same object would fold into a line that names the wrong table and the
wrong lost row. The bound is still fixed at boot: the object registry,
times two tables, times the driver's code vocabulary.
- **Count.** One missing table therefore prints one line per audited
object that writes through it. That is 4 lines in the measured fixture,
the same as on `main`. The line now states this count where it used to
say "reported ONCE".
## Why the missing-table remedy names two causes
The writer holds the error and nothing else. The boot reports a refused
DDL through objectql's schema-sync report (`Schema sync FAILED for
object 'X'`), but nothing records it where this writer can read it. The
engine's datasource binding resolver is private. Telling the two causes
apart would need a new seam in `@objectstack/objectql`, which is outside
this card's file surface. So the line names both. The boot-log check
comes first because it settles the question whenever that line is
present.
## Pins
These are in `audit-writers.test.ts`, block `the line names the refused
table and the lost row (#21262)`, with 9 cases. The engine refuses one
table's insert from that table's own `create`.
1. Refused `sys_activity`: the line names `sys_activity`, says the
ledger row landed, and never says the `sys_audit_log` row was lost. It
also checks the rows that actually landed.
2. Refused `sys_audit_log`: the line names it and says the activity row
due after it was lost too.
3. An object with `enable.activities: false`: no activity row is claimed
as lost.
4. Missing table: both causes appear, each naming the refused table,
with the boot-log check first.
5. Any other refusal (a NOT NULL constraint): the driver-fault remedy
appears, with neither missing-table cause.
6. Code-only `42P01` on a refused `sys_activity` insert: `sys_activity`
is named. This guards against picking the table by list order.
7. Four objects with three writes each give 4 `error` lines and 8
`debug` lines, and the line states its key.
8. The same object and the same code, with the two tables refusing in
turn, give two lines, one per table.
9. No value stored in the audited row appears in any log line or its
meta. The line is operator text only.
The `#5226` and `#15166` blocks are byte-unchanged. The test diff is
+232 / -0, and all of their pins pass.
## Ablations (one-time proof, not kept)
Each one was run through `scripts/ablation-replace.mjs` in wrap mode,
under the verify lock, on the committed fix. The test imports
`./audit-writers.js`, which resolves straight to `src/audit-writers.ts`.
No package `exports` or `dist/` is in the path, so no rebuild leg and no
dist preflight was owed.
| Mutation | On-disk evidence | Red |
|---|---|---|
| Refused table hard-coded: `table: progress.writing,` replaced by
`table: 'sys_audit_log' as AuditTrailTable,` | anchor 1 to 0, blob
`437819f06fb9` to `0e5e65f028f2` | 4 of 87: pins 1, 5, 6 and 8 (pin 1 is
the `sys_activity` line) |
| Key reverted: `auditFailureCauseKey(object, err, table)` replaced by
`auditFailureCauseKey(object, err)` | anchor 1 to 0, blob `437819f06fb9`
to `b89d418cd559` | 1 of 87: pin 8 (the count per table) |
| List order: the refused table is no longer asked first
(`sys_audit_log` is asked first) | anchor 1 to 0, blob `437819f06fb9` to
`119abca80eea` | 1 of 87: pin 6 |
Every restore was proven the same way: the blob after restore equals the
blob at HEAD (`437819f06fb9`), and `git diff HEAD` is empty.
## Verification, at `743fc4e5e`
- `pnpm --filter @objectstack/plugin-audit test`: 36 files, 581 tests
passed.
- `pnpm --filter @objectstack/plugin-audit typecheck`: `tsc --noEmit`
for src and for `tsconfig.scripts.json`, plus `check:test-typecheck`
over `tsconfig.test.json`, which compiles the test files. All green.
- `packages/types` `driver-error-classification.callers.test.ts` reads
every `isMissingTableError` call under `packages/`, so it reads this
file: 7 of 7 passed.
- `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack
--commands` derived 65 commands, and all 65 exited 0.
`check:dual-build-cjs-loads` and `check:i18n` first answered
`PREREQUISITE NOT MET` (exit 3, nothing measured). After the turbo
builds they name (all cached), both re-ran with exit 0. Reconciling with
`--ran` gives 65 derived, 65 run, 0 NOT-MEASURED. That zero is derived
from the recorded exit codes.
- Also run, outside the derivation, all exit 0:
`check:durability-log-level` (39 durability catch seams, all loud),
`check-changeset-fixed`, `check:authz-resolver`,
`check:error-code-casing`, `check:filter-alias-parity`,
`check:startup-registry-verdict`.
- `check:doc-authoring`: the runtime string keeps its one
`framework#5226` id, so the sibling-package prose-id ledger holds, with
no growth and no burn-down.
- ESLint, narrowed to the 2 changed TypeScript files with
`--no-inline-config --format json`: 2 files linted, 0 errors, 0
warnings. Both files resolve a non-empty config under `--print-config`,
so they are in eslint's own population. The config never enables
type-aware linting (`parserOptions.project` and `projectService` are
null for both), so this diff cannot move a verdict on any untouched
file. The repo-wide `pnpm lint` is left to CI.
- Left to CI: the path-scheduled CI jobs (Test Core, Temporal
Conformance, Dogfood, Build Core) and the workspace type-check lanes.
## Acceptance notes (not filed)
- **The same remedy in two sibling sinks.** `read-audit.ts` (`Read-audit
write FAILED`) and `auth-event-audit.ts` (`Auth-event audit write
FAILED`) write only `sys_audit_log`, so the table they name is right.
For a missing table, their remedy still says the datasource split is the
usual cause, and a refused DDL gets the same misdirection there. Nobody
has measured a missing `sys_audit_log` table through either seam, so
this is a note, not a card.
`content/docs/kernel/runtime-services/audit-service.mdx` ("The most
common cause of a failing insert is a datasource split") describes the
auth-event seam and carries the same claim. This PR makes none of those
sentences false, so they stay as they are. Carrier: none.
- **The split may no longer be reachable by its original route.**
`#5226` was a ledger insert riding a primary-datasource transaction. The
engine's `enforceTransactionOrigin` now runs system-ledger writes inside
a transaction outside it, on their own connection. That route to "no
such table" looks closed, but this is inference from reading the code.
The line keeps the split as cause (2) because the writer cannot rule it
out.
- **The tracker id in the runtime string.** The remedy still ends cause
(2) with `see framework#5226`. Removing it would shrink
`scripts/doc-authoring-prose-id.baseline.json` by one, and that file is
outside this card's file surface.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 9f13c94 commit 69a12a0
3 files changed
Lines changed: 407 additions & 30 deletions
File tree
- .changeset
- packages/plugins/plugin-audit/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1406 | 1406 | | |
1407 | 1407 | | |
1408 | 1408 | | |
| 1409 | + | |
| 1410 | + | |
| 1411 | + | |
| 1412 | + | |
| 1413 | + | |
| 1414 | + | |
| 1415 | + | |
| 1416 | + | |
| 1417 | + | |
| 1418 | + | |
| 1419 | + | |
| 1420 | + | |
| 1421 | + | |
| 1422 | + | |
| 1423 | + | |
| 1424 | + | |
| 1425 | + | |
| 1426 | + | |
| 1427 | + | |
| 1428 | + | |
| 1429 | + | |
| 1430 | + | |
| 1431 | + | |
| 1432 | + | |
| 1433 | + | |
| 1434 | + | |
| 1435 | + | |
| 1436 | + | |
| 1437 | + | |
| 1438 | + | |
| 1439 | + | |
| 1440 | + | |
| 1441 | + | |
| 1442 | + | |
| 1443 | + | |
| 1444 | + | |
| 1445 | + | |
| 1446 | + | |
| 1447 | + | |
| 1448 | + | |
| 1449 | + | |
| 1450 | + | |
| 1451 | + | |
| 1452 | + | |
| 1453 | + | |
| 1454 | + | |
| 1455 | + | |
| 1456 | + | |
| 1457 | + | |
| 1458 | + | |
| 1459 | + | |
| 1460 | + | |
| 1461 | + | |
| 1462 | + | |
| 1463 | + | |
| 1464 | + | |
| 1465 | + | |
| 1466 | + | |
| 1467 | + | |
| 1468 | + | |
| 1469 | + | |
| 1470 | + | |
| 1471 | + | |
| 1472 | + | |
| 1473 | + | |
| 1474 | + | |
| 1475 | + | |
| 1476 | + | |
| 1477 | + | |
| 1478 | + | |
| 1479 | + | |
| 1480 | + | |
| 1481 | + | |
| 1482 | + | |
| 1483 | + | |
| 1484 | + | |
| 1485 | + | |
| 1486 | + | |
| 1487 | + | |
| 1488 | + | |
| 1489 | + | |
| 1490 | + | |
| 1491 | + | |
| 1492 | + | |
| 1493 | + | |
| 1494 | + | |
| 1495 | + | |
| 1496 | + | |
| 1497 | + | |
| 1498 | + | |
| 1499 | + | |
| 1500 | + | |
| 1501 | + | |
| 1502 | + | |
| 1503 | + | |
| 1504 | + | |
| 1505 | + | |
| 1506 | + | |
| 1507 | + | |
| 1508 | + | |
| 1509 | + | |
| 1510 | + | |
| 1511 | + | |
| 1512 | + | |
| 1513 | + | |
| 1514 | + | |
| 1515 | + | |
| 1516 | + | |
| 1517 | + | |
| 1518 | + | |
| 1519 | + | |
| 1520 | + | |
| 1521 | + | |
| 1522 | + | |
| 1523 | + | |
| 1524 | + | |
| 1525 | + | |
| 1526 | + | |
| 1527 | + | |
| 1528 | + | |
| 1529 | + | |
| 1530 | + | |
| 1531 | + | |
| 1532 | + | |
| 1533 | + | |
| 1534 | + | |
| 1535 | + | |
| 1536 | + | |
| 1537 | + | |
| 1538 | + | |
| 1539 | + | |
| 1540 | + | |
| 1541 | + | |
| 1542 | + | |
| 1543 | + | |
| 1544 | + | |
| 1545 | + | |
| 1546 | + | |
| 1547 | + | |
| 1548 | + | |
| 1549 | + | |
| 1550 | + | |
| 1551 | + | |
| 1552 | + | |
| 1553 | + | |
| 1554 | + | |
| 1555 | + | |
| 1556 | + | |
| 1557 | + | |
| 1558 | + | |
| 1559 | + | |
| 1560 | + | |
| 1561 | + | |
| 1562 | + | |
| 1563 | + | |
| 1564 | + | |
| 1565 | + | |
| 1566 | + | |
| 1567 | + | |
| 1568 | + | |
| 1569 | + | |
| 1570 | + | |
| 1571 | + | |
| 1572 | + | |
| 1573 | + | |
| 1574 | + | |
| 1575 | + | |
| 1576 | + | |
| 1577 | + | |
| 1578 | + | |
| 1579 | + | |
| 1580 | + | |
| 1581 | + | |
| 1582 | + | |
| 1583 | + | |
| 1584 | + | |
| 1585 | + | |
| 1586 | + | |
| 1587 | + | |
| 1588 | + | |
| 1589 | + | |
| 1590 | + | |
| 1591 | + | |
| 1592 | + | |
| 1593 | + | |
| 1594 | + | |
| 1595 | + | |
| 1596 | + | |
| 1597 | + | |
| 1598 | + | |
| 1599 | + | |
| 1600 | + | |
| 1601 | + | |
| 1602 | + | |
| 1603 | + | |
| 1604 | + | |
| 1605 | + | |
| 1606 | + | |
| 1607 | + | |
| 1608 | + | |
| 1609 | + | |
| 1610 | + | |
| 1611 | + | |
| 1612 | + | |
| 1613 | + | |
| 1614 | + | |
| 1615 | + | |
| 1616 | + | |
| 1617 | + | |
| 1618 | + | |
| 1619 | + | |
| 1620 | + | |
| 1621 | + | |
| 1622 | + | |
| 1623 | + | |
| 1624 | + | |
| 1625 | + | |
| 1626 | + | |
| 1627 | + | |
| 1628 | + | |
| 1629 | + | |
| 1630 | + | |
| 1631 | + | |
| 1632 | + | |
| 1633 | + | |
| 1634 | + | |
| 1635 | + | |
| 1636 | + | |
| 1637 | + | |
| 1638 | + | |
| 1639 | + | |
| 1640 | + | |
1409 | 1641 | | |
1410 | 1642 | | |
1411 | 1643 | | |
| |||
0 commit comments