Skip to main content

Project Tuning

The BootstrapOptions object passed to bootstrap() has several fields that control logging behavior, configuration source selection, and global error handling. These are rarely needed in basic apps but matter for production deployments, log aggregators, and scripting use cases.

LoggerOptions​

loggerOptions fine-tunes the built-in logger without replacing it. All fields are optional.

await MY_APP.bootstrap({
loggerOptions: {
pretty: true,
mergeData: { env: "production", service: "my-api" },
als: true,
levelOverrides: {
"my_app:database": "warn",
},
},
});

Field reference​

FieldTypeDefaultDescription
mergeDataobject—Data merged into every log line. Use for application tags in log aggregators.
timestampFormatstring"ddd HH:mm:ss.SSS"dayjs format string for the timestamp prefix.
prettybooleantruePretty-print logs with color and alignment. Set false for JSON output in production.
msbooleanfalsePrefix each line with milliseconds elapsed since the previous log.
counterbooleanfalseAdd an incrementing counter to every log line, starting at 0.
alsbooleanfalseExtract data from AsyncLocalStorage and merge it into every log line. Requires ALS setup — see Async Local Storage.
levelOverridesRecord<string, TConfigLogLevel>—Override the log level for a specific service or module. Keys are "module:service" or "module".
stdOutbooleantrueEmit logs to stdout. Set to false if you're using a customLogger and don't want double output.

Pretty vs JSON​

In development, pretty: true produces human-readable colored output. In production behind a log aggregator (Datadog, Loki, CloudWatch), you typically want pretty: false to get structured JSON:

// production
await MY_APP.bootstrap({
loggerOptions: {
pretty: false,
mergeData: {
env: process.env.ENVIRONMENT,
version: process.env.APP_VERSION,
},
},
});

Every log line becomes a JSON object that the aggregator can index.

levelOverrides​

Use levelOverrides to suppress noisy services without raising the global log level:

loggerOptions: {
levelOverrides: {
"my_app": "warn", // entire module: suppress info/debug
"my_lib:scheduler": "error", // one service: only errors
"boilerplate": "silent", // suppress boilerplate completely
},
},

Keys are "module" or "module:service". Values are any LOG_LEVEL string: "silent", "error", "warn", "info", "debug", "trace".

configSources​

Configuration sources are processed in this order:

declaration defaults / bootstrap inputs
→ env + argv (argv wins within this pass)
→ optional file loader
→ registered custom loaders
→ native schema processing

In normal bootstrap, env and argv default to enabled. The file loader is opt-in:

await MY_APP.bootstrap({
configSources: {
env: true,
argv: false,
file: false,
},
});

Custom loader names are enabled unless set to false. These flags control loader channels, not explicit bootstrap configuration or the output-shaped configuration.set() and merge() APIs. An entry-level source array can further restrict a key to env, argv, file, or a custom loader name.

The file loader reads JSON, YAML/YML, and INI. --config path and boilerplate.CONFIG select its path. Dotenv loading is separate: envFile or --env-file selects a dotenv file that populates process.env for the environment loader.

For tests that must not read host sources, disable all three channels explicitly:

TestRunner({ target: MY_APP }).setOptions({
configSources: { env: false, argv: false, file: false },
});

See Config Sourcing for lookup details and Schema-backed Configuration for provider processing.

customLogger​

Replace the built-in logger entirely. This is useful when you need custom log transports, a specific log format not supported by loggerOptions, or integration with an external logging library.

import pino from "pino";

const logger = pino({ level: "info" });

await MY_APP.bootstrap({
customLogger: (context) => ({
debug: (data, message) => logger.debug(data, message),
error: (data, message) => logger.error(data, message),
fatal: (data, message) => logger.fatal(data, message),
info: (data, message) => logger.info(data, message),
trace: (data, message) => logger.trace(data, message),
warn: (data, message) => logger.warn(data, message),
}),
});

customLogger is typed as GetLogger — a function that receives a context string ("module:service") and returns an object with the six log-level methods.

When customLogger is provided, the built-in logger is not created. loggerOptions fields that control formatting (pretty, ms, counter, etc.) are ignored — your custom logger is responsible for formatting. levelOverrides is still applied before routing calls to the custom logger.

If you're also emitting logs through the built-in path via a wrapper, set loggerOptions.stdOut: false to avoid duplicate output.

handleGlobalErrors​

Controls whether DA installs a handler for uncaught exceptions and unhandled promise rejections.

await MY_APP.bootstrap({
handleGlobalErrors: false, // default: true
});

When true (the default), DA registers listeners on process.on("uncaughtException") and process.on("unhandledRejection"). These log the error and trigger the shutdown sequence.

Disable this if:

  • Your host environment (Lambda, a test framework, a supervisor process) already handles these signals
  • You want full control over crash behavior
  • You're running multiple DA applications in the same process (only one should own global error handling)
caution

With handleGlobalErrors: false, an unhandled rejection will not trigger graceful shutdown. Make sure your host environment handles teardown.

Combining options​

A typical production entrypoint with all three areas configured:

src/main.mts
await MY_APP.bootstrap({
loggerOptions: {
pretty: false,
mergeData: {
env: "production",
service: "my-api",
version: process.env.APP_VERSION,
},
als: true, // correlate logs by request ID
levelOverrides: {
"boilerplate": "warn",
},
},
configSources: {
file: false, // no config-file loader in prod
},
handleGlobalErrors: true, // explicit, for documentation clarity
});