Move docs/ package mirror into document-navigator plugin
Relocate the ~6k-file docs tree from the repo root into plugins/document-navigator/docs/ so the plugin is self-contained and portable. docs/superpowers/ (specs and plans) stays at the repo root. Moved via cp+rm rather than git mv because directory rename() fails with EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the identical-content files at commit time. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
# OTel Instruments
|
||||
|
||||
## Purpose
|
||||
|
||||
`instruments.ts` provides lazily-initialised OpenTelemetry metric instruments for `@suaveplan/db`. Rather than creating meters at module load time (which would execute OTel SDK code before any SDK is registered and could silently fail), instruments are created on first use via a module-level singleton guard. This ensures the package works correctly both when an OTel SDK exporter is configured and when it is not — in the latter case, the OTel no-op provider handles all calls without errors.
|
||||
|
||||
Four instruments cover the core observability surface: query/execute latency, connection lifecycle, transaction outcomes, and migration counts.
|
||||
|
||||
## Features
|
||||
|
||||
- Lazy initialisation — OTel meter created on first call, not at module load
|
||||
- `resetInstruments()` — clears the singleton for test isolation
|
||||
- `recordQueryDuration(ms, operation)` — histogram labelled by `db.operation`
|
||||
- `recordConnectionDelta(±1, dbName)` — UpDownCounter labelled by `db.name`
|
||||
- `recordTransaction(outcome)` — counter labelled by `db.outcome`
|
||||
- `recordMigration(name)` — counter labelled by `db.migration.name`
|
||||
- Works with no-op OTel provider — safe when no SDK is configured
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
recordQueryDuration,
|
||||
recordConnectionDelta,
|
||||
} from "@suaveplan/db/telemetry/instruments";
|
||||
|
||||
// In a concrete adapter's connect():
|
||||
recordConnectionDelta(1, "main");
|
||||
|
||||
// In a concrete adapter's query():
|
||||
const start = performance.now();
|
||||
const rows = await driver.query(sql);
|
||||
recordQueryDuration(performance.now() - start, "query");
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
recordTransaction,
|
||||
recordMigration,
|
||||
resetInstruments,
|
||||
} from "@suaveplan/db/telemetry/instruments";
|
||||
|
||||
// In a concrete adapter's transaction():
|
||||
let outcome: "committed" | "rolled_back" = "rolled_back";
|
||||
try {
|
||||
await fn(tx);
|
||||
outcome = "committed";
|
||||
} finally {
|
||||
recordTransaction(outcome);
|
||||
}
|
||||
|
||||
// In a migration runner after each applied migration:
|
||||
recordMigration("003_add_index");
|
||||
|
||||
// In tests — reset between test runs to avoid state leak:
|
||||
beforeEach(() => resetInstruments());
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `recordQueryDuration(durationMs, operation)`
|
||||
|
||||
Records a query or execute duration.
|
||||
|
||||
- `durationMs` — elapsed time in milliseconds
|
||||
- `operation` — `"query"` or `"execute"`
|
||||
|
||||
### `recordConnectionDelta(delta, dbName)`
|
||||
|
||||
Adjusts the active connection count.
|
||||
|
||||
- `delta` — `1` on connect, `-1` on disconnect
|
||||
- `dbName` — logical name of the database (used as the `db.name` attribute)
|
||||
|
||||
### `recordTransaction(outcome)`
|
||||
|
||||
Increments the transaction counter.
|
||||
|
||||
- `outcome` — `"committed"` or `"rolled_back"`
|
||||
|
||||
### `recordMigration(migrationName)`
|
||||
|
||||
Increments the migration counter.
|
||||
|
||||
- `migrationName` — the name of the applied migration file
|
||||
|
||||
### `resetInstruments()`
|
||||
|
||||
Clears the cached instruments singleton. Useful in unit tests to avoid state from a prior test bleeding into subsequent ones.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The instruments singleton guard uses an `undefined` check rather than a lazy initialiser class so there is no closure overhead — the check is a single `!== undefined` comparison per call. The `metrics.getMeter("@suaveplan/db")` call is safe under the OTel no-op provider because it returns a no-op meter whose `createHistogram`, `createUpDownCounter`, and `createCounter` methods all return no-op instruments that silently discard all data.
|
||||
|
||||
`recordQueryDuration` uses a histogram (not a counter) so downstream dashboards can compute percentiles — P50/P95/P99 query latency is far more useful than a total call count for diagnosing slow queries.
|
||||
@@ -0,0 +1,76 @@
|
||||
# withDbSpan
|
||||
|
||||
## Purpose
|
||||
|
||||
`withDbSpan` is a thin OTel tracing helper that wraps any async database operation in a named span. It handles the boilerplate of starting a span, setting its status on success or error, recording exceptions, and always ending the span — so concrete adapters and migration runners can add tracing with a single wrapper call rather than repeating the span lifecycle pattern in every method.
|
||||
|
||||
Like the instruments module, `withDbSpan` relies on the OTel no-op tracer when no SDK is configured, so it is safe to call in any environment including tests with no OTel setup.
|
||||
|
||||
## Features
|
||||
|
||||
- Creates a named span via `trace.getTracer("@suaveplan/db")`
|
||||
- Sets `SpanStatusCode.OK` on successful completion
|
||||
- Sets `SpanStatusCode.ERROR` and records the exception on throw
|
||||
- Always ends the span (via `finally`) — no span leaks
|
||||
- Accepts optional `Attributes` for span metadata (e.g. `db.name`, `db.operation`)
|
||||
- Works with the OTel no-op provider — zero overhead when no exporter is registered
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { withDbSpan } from "@suaveplan/db/telemetry/trace";
|
||||
|
||||
const rows = await withDbSpan("db.query", async () => {
|
||||
return driver.query("SELECT * FROM users");
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { withDbSpan } from "@suaveplan/db/telemetry/trace";
|
||||
|
||||
// With span attributes for richer trace data
|
||||
const result = await withDbSpan(
|
||||
"db.execute",
|
||||
async () => driver.execute("DELETE FROM sessions WHERE expires_at < ?", [Date.now()]),
|
||||
{
|
||||
"db.name": "app",
|
||||
"db.operation": "execute",
|
||||
"db.statement": "DELETE FROM sessions",
|
||||
}
|
||||
);
|
||||
|
||||
// Wrapping a full transaction
|
||||
const transferResult = await withDbSpan("db.transaction.transfer", async () => {
|
||||
return adapter.transaction(async (tx) => {
|
||||
await tx.execute("UPDATE accounts SET balance = balance - ? WHERE id = ?", [100, "sender"]);
|
||||
await tx.execute("UPDATE accounts SET balance = balance + ? WHERE id = ?", [100, "receiver"]);
|
||||
return { ok: true };
|
||||
});
|
||||
}, { "transfer.amount": 100 });
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `withDbSpan<T>`
|
||||
|
||||
```ts
|
||||
withDbSpan<T>(
|
||||
name: string,
|
||||
fn: () => Promise<T>,
|
||||
attrs?: Attributes
|
||||
): Promise<T>
|
||||
```
|
||||
|
||||
- **`name`** — OTel span name (e.g. `"db.query"`, `"db.migrate"`)
|
||||
- **`fn`** — async function to execute inside the span
|
||||
- **`attrs`** — optional `Attributes` map attached to the span at creation time
|
||||
- **Returns** — the resolved value of `fn()`
|
||||
- **Throws** — re-throws whatever `fn()` throws after recording the exception on the span
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`withDbSpan` uses the module-level `tracer` constant (created once at import time via `trace.getTracer("@suaveplan/db")`) rather than calling `getTracer` on every invocation. The OTel spec guarantees that `getTracer` with the same name returns the same tracer instance, so this is a safe optimisation.
|
||||
|
||||
Non-Error thrown values (e.g. strings, numbers) are wrapped in `new Error(String(err))` before being passed to `span.recordException`. This satisfies the OTel SDK's type requirement for `recordException` while still re-throwing the original value, so callers that catch strings continue to work as before.
|
||||
Reference in New Issue
Block a user