logging option, in the shape Sequelize uses, so existing callbacks workLogger — a levelled application logger (debug/info/warn/error) that the ORM uses for its own lifecycle messages, and that emitsquery / slowQuery events.query event fires on every branch, including logging: false, which iscount, bulkCreate, increment and the aggregates, which are easy to missNote: sync()DDL reaches theloggingcallback too, once per
statement:sync({ logging })for that call, or the instance'slogging
otherwise, andsync({ logging: false })keeps it quiet. Before 4.9.2 the
DDL that the dialect helpers issued duringsync()did not reach the
callback at all; only the built-in logger's descriptions (Created table: users) appeared.
import { Prorm } from 'ts-prorm-orm';
const prorm = new Prorm({
dialect: 'postgres',
/* … */
logging: console.log, // false | true | (sql, timing) => void
benchmark: true, // append elapsed time
logQueryParameters: true, // append bound values
});logging value | Effect |
|---|---|
false | Nothing is logged. |
true | Built-in logger writes to console.log. |
| a function | Called as (sql, timing). |
| unset | Quiet — unless benchmark: true, which turns the built-in logger on. |
Executed (User): SELECT * FROM "users" WHERE "name" = ?; ["Ada"] Elapsed time: 0msUser.findAll({ where: { name: 'Ada' } }) on SQLitelogging: true, benchmark: true, logQueryParameters: true.Executed (<model>) names the model. The ; [...] parameter list appears withlogQueryParameters, and the elapsed time with benchmark. A rawprorm.query() goes through a shorter path and prints the duration in place ofExecuted (1ms): SELECT 1benchmark.logging on a single query wins over the instance setting, in both directions:// Trace one query on an otherwise-quiet connection
await User.findAll({ where: { id: 1 }, logging: console.log, benchmark: true });
// Silence one noisy query on a logging connection
await Session.findAll({ where: { expired: true }, logging: false });false at either level wins over a function at the other — silencing is treatedLoggerimport { createLogger, LogLevel } from 'ts-prorm-orm';
const logger = createLogger({
level: 'debug', // debug | info | warn | error | quiet
logSQL: true,
slowQueryThreshold: 500, // ms
logSlowQueries: true,
colors: true,
});
logger.info('starting up', { region: 'eu-west-1' });
logger.child({ requestId: 'req-42' }).debug('handling request');
logger.logQuery('SELECT pg_sleep(1)', 1003);colors: false, that prints:[2026-09-26T22:21:40.377Z] INFO : starting up {"region":"eu-west-1"}
[2026-09-26T22:21:40.389Z] DEBUG: handling request {"requestId":"req-42"}
[2026-09-26T22:21:40.389Z] DEBUG: Executing SQL: SELECT pg_sleep(1)
[2026-09-26T22:21:40.389Z] WARN : Slow query (1003ms): SELECT pg_sleep(1)DEBUG: Executing SQL line comes from logSQL: true; the WARN line fromlogSlowQueries with a 1003 ms query over the 500 ms threshold.| Level | Value | Shows |
|---|---|---|
debug | 0 | everything |
info | 1 | info and above (the default) |
warn | 2 | warnings and errors |
error | 3 | errors only |
quiet | 4 | nothing |
| Method | Purpose |
|---|---|
debug / info / warn / error | Log at a level, with optional structured meta. |
setLevel / getLevel / getLevelName / isLevelEnabled | Level control. |
logQuery(sql, duration) | Feed a query in — emits query, and slowQuery past the threshold. |
setLogSQL / setSlowQueryThreshold | Adjust at runtime. |
child(meta) | A logger that stamps meta onto every entry. |
loggingcreateSilentLogger() gives a logger that swallows everything — the rightgetDefaultLogger() / setDefaultLogger() / resetDefaultLogger()Important: those three manage the default logger, which a Prorm
instance does not use — each instance builds its own from its connection
options.setDefaultLogger()therefore will not capture a connection's
lifecycle messages; configure the connection itself, or reach its logger withprorm.getLogger().
prorm.getLogger() returns the instance's logger, andprorm.setLoggingLevel('debug' | 'info' | 'warn' | 'error') adjusts it.Prorm instance, whether or not logging isprorm.on('query', ({ sql, duration, model, timestamp }) => {
metrics.histogram('db.query.ms', duration, { model });
});
prorm.on('slowQuery', ({ sql, duration, threshold, model }) => {
logger.warn('slow query', { sql, duration, threshold, model });
});
prorm.on('error', (err) => Sentry.captureException(err));query payload, captured on SQLite from connect(), User.create() andUser.count() in that order:{"sql":"ping","duration":0,"timestamp":"2026-09-26T22:21:40.430Z","type":"SELECT"}
{"sql":"INSERT INTO \"users\" (\"name\") VALUES (?)","duration":1,"timestamp":"2026-09-26T22:21:40.437Z","model":"User","type":"INSERT"}
{"sql":"SELECT COUNT(*) AS \"count\" FROM \"users\"","duration":0,"timestamp":"2026-09-26T22:21:40.440Z","model":"User"}connect() emits one for its liveness ping, so a query counter attachederror: true.slowQuery fires when a query's duration exceeds slowQueryThresholdLogger emits its own log, query, slowQuery and error events, ifconsole.log is fine locally and wrong in production: no levels, no structure,import pino from 'pino';
import { Prorm, createLogger, setDefaultLogger } from 'ts-prorm-orm';
const log = pino();
const db = new Prorm({
dialect: 'postgres',
/* … */
// Query logging → a debug-level event with the statement as a field, not
// interpolated into the message, so it stays queryable.
logging: (sql, ms) => log.debug({ sql, ms }, 'query'),
});
// The ORM's own lifecycle messages → the same logger.
setDefaultLogger(createLogger({
level: 'info',
colors: false, // no ANSI codes into a JSON transport
logging: (message) => log.info(message),
}));import winston from 'winston';
import { Prorm } from 'ts-prorm-orm';
const log = winston.createLogger({
level: 'debug',
format: winston.format.json(),
transports: [new winston.transports.Console()],
});
const db = new Prorm({
dialect: 'postgres',
/* … */
logging: (sql, ms) => log.debug('query', { sql, ms }),
});
db.on('slowQuery', ({ sql, duration, threshold }) =>
log.warn('slow query', { sql, duration, threshold }));colors off whenever the destination is not a terminal. The default is on,const requestLog = logger.child({ requestId: 'r-8f2', userId: 41 });
requestLog.debug('loading dashboard');
// → [2026-09-26T22:21:40.389Z] DEBUG: loading dashboard {"requestId":"r-8f2","userId":41}loggingAsyncLocalStorage carries it without threading an argument throughimport { AsyncLocalStorage } from 'node:async_hooks';
const ctx = new AsyncLocalStorage<{ requestId: string }>();
app.use((req, _res, next) => ctx.run({ requestId: req.id }, next));
const db = new Prorm({
/* … */
logging: (sql, ms) => log.debug({ sql, ms, ...ctx.getStore() }, 'query'),
});const rows = await User.findAll({ where: { id }, logging: console.log, benchmark: true });const db = new Prorm({ /* … */ logSlowQueries: true, slowQueryThreshold: 200 });
db.on('slowQuery', ({ sql, duration, model }) =>
log.warn({ sql, duration, model }, 'slow query'));let n = 0;
db.on('query', () => { n++; });
await renderPage();
console.log(`${n} queries`);db.setLoggingLevel('debug');
await db.sync();
db.setLoggingLevel('info');LOG_LEVEL=debug
DB_LOG_SQL=true
DB_SLOW_QUERY_MS=200config/config.ts, the file prorm init scaffolds:import { Prorm, createLogger } from 'ts-prorm-orm';
const level = (process.env.LOG_LEVEL ?? 'info') as 'debug' | 'info' | 'warn' | 'error';
const logSql = process.env.DB_LOG_SQL === 'true';
const logger = createLogger({ level, colors: process.stdout.isTTY === true });
export const prorm = new Prorm({
dialect: 'postgres',
host: process.env.DB_HOST,
database: process.env.DB_NAME,
username: process.env.DB_USER,
password: process.env.DB_PASSWORD,
logging: logSql ? (sql, ms) => logger.debug(`${sql} (${ms}ms)`) : false,
slowQueryThreshold: Number(process.env.DB_SLOW_QUERY_MS ?? 1000),
logSlowQueries: true,
});
prorm.setLoggingLevel(level);debug, not info. It is one line per statement;info it will dominate the log volume and cost.logQueryParameters off. See Redacting values.on('query') gives you thecolors: false anywhere the output is not a terminal.import { Prorm } from 'ts-prorm-orm';
const prorm = new Prorm({
dialect: 'postgres',
logging: (sql: string, ms?: number) => { if (Math.random() < 0.01) log.debug({ sql, ms }, 'query'); },
});import { createSilentLogger, setDefaultLogger, resetDefaultLogger } from 'ts-prorm-orm';
beforeAll(() => setDefaultLogger(createSilentLogger()));
afterAll(() => resetDefaultLogger());Prorm instance buildsconst db = new Prorm({ dialect: 'sqlite', storage: ':memory:', logging: false });const lines: string[] = [];
const db = new Prorm({ dialect: 'sqlite', storage: ':memory:', logging: (sql) => lines.push(sql) });
await User.create({ name: 'Ada' });
expect(lines.some((l) => l.startsWith('INSERT INTO'))).toBe(true);[!CAUTION] logQueryParametersprints bound values, which will include passwords,
tokens and personal data, into every log line. Keep it off in production.
const prorm = new Prorm({
/* … */
logging: (sql) => logger.debug(sql.replace(/'[^']*'/g, "'?'")),
});@Sensitive (or @FieldClassification / @Personal) are[REDACTED] in logged SQL, loggedquery / slowQuery events, and an instance's toJSON()mask). The statementfalse:const prorm = new Prorm({
/* … */
sensitive: { redactInLogs: false, redactInToJSON: false },
});SlowQueryLogger with percentiles and alerting