@ocubist/diagnostics-alchemy
v0.3.1
Published
A unified diagnostics package combining a typed error framework with a structured hierarchical logger.
Maintainers
Readme
@ocubist/diagnostics-alchemy
A unified TypeScript diagnostics library — typed error framework + structured hierarchical logger.
npm install @ocubist/diagnostics-alchemyFull ESM. Node 20+. Works in browser environments too.
Error Framework
Define typed errors
import { useErrorAlchemy } from "@ocubist/diagnostics-alchemy";
const { craftMysticError, craftErrorTransmuter } = useErrorAlchemy(
"auth-service", // module — stamps every error produced here
"LoginHandler" // context
);
export const LoginFailedError = craftMysticError({
name: "LoginFailedError",
errorCode: "AUTH_INVALID_CREDENTIALS",
severity: "critical",
reason: "Credentials did not match any known account",
});
// At runtime
throw new LoginFailedError({
message: `Login failed for ${email}`,
payload: { email },
});Check error types
// Safe instanceof — survives ESM module boundary differences
if (LoginFailedError.compare(err)) {
// err is narrowed to LoginFailedError
}Convert foreign errors into typed ones
import { ZodError } from "zod";
const zodTransmuter = craftErrorTransmuter(
(err) => err instanceof ZodError,
(err: ZodError) => new ValidationError({
message: err.errors.map(e => e.message).join(" | "),
origin: err,
})
);
const typed = zodTransmuter.execute(unknownError);
// → typed ValidationError if ZodError, otherwise passes through unchangedChain transmuters into a pipeline
const { craftErrorSynthesizer } = useErrorAlchemy("api", "requestHandler");
const synthesizer = craftErrorSynthesizer([zodTransmuter, dbTransmuter, networkTransmuter]);
const typedError = synthesizer.synthesize(caughtError); // first match winsRoute errors to handlers
const { craftErrorResolver, craftErrorResolverMap } = useErrorAlchemy("api", "requestHandler");
const resolver = craftErrorResolver({
synthesizer,
logger: (err) => log.error("Error", { payload: { err: objectifyError(err) } }),
errorResolverMap: craftErrorResolverMap(
[NotFoundError, (err) => res.status(404).json({ error: err.message })],
[AuthError, (err) => res.status(401).json({ error: err.message })],
),
defaultResolver: (err) => res.status(500).json({ error: "Internal error" }),
});
router.use((err, req, res, next) => resolver(err));Validation helpers
import {
parse, asyncParse,
validate, asyncValidate,
assert, assertDefined, assertNotEmpty, assertTruthy,
} from "@ocubist/diagnostics-alchemy";
import { z } from "zod";
const schema = z.object({ id: z.string().uuid(), age: z.number().int().min(0) });
const user = parse(rawInput, schema); // throws ParseFailedError on failure
const user = await asyncParse(rawInput, schema); // async version
validate(rawInput, schema); // returns boolean, never throws
asyncValidate(rawInput, schema); // async boolean
assertDefined(value); // throws if null | undefined
assertNotEmpty(arr, 1, 100); // throws if empty or out of [min, max]
assert(value, schema); // Zod schema assertionLogger
Create a logger
import { useLogger } from "@ocubist/diagnostics-alchemy";
const log = useLogger({
where: "api-server",
console: {
timezone: "Europe/Berlin", // any IANA timezone — default "UTC"
minLevel: "debug", // default "debug"
},
});
log.info("Server started", { payload: { port: 3000 } });
log.warn("Slow query", { where: "db", payload: { ms: 1230 } });
// 2026-04-30 01:58:08 WARN [api-server.db] Slow query
// {"ms":1230}Hierarchical context — specialize()
const authLog = log.specialize({ where: "auth", why: "user-session" });
const loginLog = authLog.specialize({ where: "login" });
loginLog.warn("Token expired");
// where: "api-server.auth.login"
// why: "user-session"
loginLog.error("Credentials rejected", { where: "validator", payload: { userId } });
// where: "api-server.auth.login.validator"Options
| Option | Type | Default | Description |
|---|---|---|---|
| where | string | — | Location segment |
| why | string | — | Intent segment |
| console | ConsoleTransportConfig | see below | Built-in console transport config. Only affects useLogger(). |
| transports | Transport[] | [] | Additional transports, each with its own minLevel |
ConsoleTransportConfig
| Field | Type | Default | Description |
|---|---|---|---|
| enableTransport | boolean | true | Set false to suppress all console output |
| timezone | string | "UTC" | IANA timezone name for timestamp formatting |
| minLevel | LogLevel | "debug" | Minimum level to print to console |
Transports
A Transport is an interface — no class needed:
interface Transport {
write(entry: LogEntry): void;
minLevel?: LogLevel; // default "debug"
}Each transport manages its own minLevel independently. The Logger checks it before calling write().
const log = useLogger({
console: { minLevel: "debug" },
transports: [
// Remote HTTP sink — only warnings and above
{
write: (entry) => fetch("https://logs.example.com/ingest", {
method: "POST",
body: JSON.stringify(entry),
headers: { "Content-Type": "application/json" },
}).catch(() => {}),
minLevel: "warn",
},
],
});Transports added in specialize() stack on top of the parent's — all fire for their respective levels.
File output
File transport lives in a separate package so sonic-boom never enters browser bundles:
npm install @ocubist/da-file-transportimport { useLogger } from "@ocubist/diagnostics-alchemy";
import { createFileTransport } from "@ocubist/da-file-transport";
const log = useLogger({
console: { timezone: "Europe/Berlin" },
transports: [
createFileTransport({ path: "logs/app.log", minLevel: "info" }),
],
});Plain output — logger.plain
For raw string output with no metadata, colouring, or filtering:
logger.plain.info("Starting migration...");
logger.plain.warn("Slow query detected\nSELECT * FROM users WHERE ...");logger.plain.{debug,info,warn,error,fatal}(string) routes directly to the matching console.* method. No timestamp, no level badge, no context, no transport filtering — always fires.
Serialize errors for log payloads
import { objectifyError } from "@ocubist/diagnostics-alchemy";
log.error("Request failed", {
payload: { err: objectifyError(caughtError) },
});
// TransmutedError → all typed fields (severity, errorCode, reason, payload, origin chain, ...)
// plain Error → { type, message, stack }
// anything else → { value: ... }Severity levels
| Level | Description |
|---|---|
| unimportant | Can be ignored |
| minor | No action needed |
| unexpected | Should not happen (default) |
| critical | Requires attention |
| fatal | System cannot continue normally |
| catastrophic | Data loss / breach possible |
Error codes
79 predefined codes. Examples: AUTH_INVALID_CREDENTIALS, DB_CONNECTION_FAILED, FILE_NOT_FOUND, HTTP_NOT_FOUND, VALIDATION_ERROR, NETWORK_ERROR, RUNTIME_ERROR, UNKNOWN_ERROR …
Full list: src/errors/error-code/errorCodeSelector.ts
Docs
docs/error-framework.md— full error framework referencedocs/logger.md— full logger referencedocs/releasing.md— release script & CI publish workflowdocs/origins.md— where this package came from
