Logger
VextJS has a built-in Vext logger kernel with zero runtime dependency, which can be used anywhere in the framework through app.logger. By default, it provides capabilities such as structured JSON, pretty/JSON dual mode, pretty level color output, requestId automatic injection, child logger, runtime level control, and minimalist log desensitization.
Basic usage
app.logger can be used directly in routes, services, plugins and middleware:
Log level
app.logger exposes 6 commonly used methods, ordered from lowest to highest severity:
logger.level accepts trace and silent as threshold configurations: trace will enable all logging methods, and silent will turn off all output.
Configure log level
After setting a certain level, logs lower than this level will not be output. For example, with level: 'info', calls to debug() are silently ignored (zero overhead).
Adjust log level at runtime
The default logger supports adjusting subsequent log thresholds at runtime, which is suitable for online temporary troubleshooting:
setLevel()only affects subsequent logs and does not review historical logs.- The created child logger shares the current runtime level with the parent logger.
app.logger.level = "debug"is not supported for this writable property compatibility; please usesetLevel().- Illegal levels will throw an explicit error and will not downgrade silently.
Life cycle log layering
In addition to the regular logger.level, VextJS also provides logger.lifecycleLevel, which specifically controls the framework's own startup/loading/hot reload system logs:
concise(default): only output single-line results of initialization start, aggregate load number, ready, cold restart / hot reloadverbose: Additional output of per-plugin/per-service/watcher file list/reload phased time consumption
It can also be overridden via environment variables or CLI:
Structured log
The core concept of Vext logger is structured logging - each log is a JSON object, which is easy for machine parsing and query.
Call signature
Always use the form logger.info(object, message) - structured fields are easy for logging systems to index and filter, and messages are easy for humans to read.
JSON output format
In the production environment (NODE_ENV=production), the log output is in JSON format:
Pretty output format
In the development environment (default), the built-in pretty formatter is used to output formatted logs that are easy to read. Single-line mode is enabled by default (prettySingleLine: true), and structured fields are appended inline to the end of the message as JSON:
If prettySingleLine: false is set, the multiline expansion format is used:
Note:
requestIdis excluded from theignorelist of the built-in pretty formatter by default (prettyIgnoreconfiguration item) and will not be output in pretty mode. This makes the development log more compact.requestIdis still present in the production JSON output. If you want to display requestId in pretty mode, you can remove it through theprettyIgnoreconfiguration item (see configuration instructions below).
In the TTY terminal, the pretty formatter will add fixed ANSI colors to the level labels of trace / debug / info / warn / error / fatal by default, making it easier to scan during the development period. The color only wraps the level label and does not affect message, URL, extras, redaction replacement values or JSON output.
Configure Pretty mode
pretty default value depends on NODE_ENV:
NODE_ENV !== 'production'→pretty: trueNODE_ENV === 'production'→pretty: false
Color Pretty Level
prettyColor only affects pretty text output and supports three modes:
Production JSON logs will not output ANSI, even if prettyColor: "always" is set, as long as pretty: false will still remain pure JSON.
Use FORCE_COLOR=1 when you need to force observing colors in npm run dev, CI or redirect logs.
Single line vs multi-line format
The prettySingleLine configuration item can be used to control how the built-in pretty formatter displays structured fields in development mode. The default value is true (single-line mode).
If a multi-line expansion format is preferred, this can be set to false:
Note:
prettySingleLineonly affects pretty mode (development environment). The JSON output format for production environments is not affected.
Custom Pretty ignore field
The prettyIgnore configuration item can be used to control which structured fields are hidden by the built-in pretty formatter in development mode. The default value is "pid,hostname,requestId", which hides the process ID, hostname and request ID to avoid unnecessary field noise in the development log.
If you need to display the requestId in pretty mode (for example when debugging the request link), you can remove it from the ignore list:
It is also possible to add additional ignored fields:
Note:
prettyIgnoreonly affects pretty mode (development environment). The production JSON output always includes all fields (includingrequestId) to ensure complete parsing by the log collection system.
Log desensitization
The default logger provides a minimalist redaction that is turned off by default and is used to replace structured log fields before writing to stdout:
Effect:
user.email, password and headers.authorization will be replaced with "[Redacted]" in the output.
Boundary:
redactKeysis an exact key match at any level.redactPathsis dot notation exact path and supports array numeric subscripts.- Desensitization occurs before pretty/JSON output, making both formats consistent.
- Desensitization will not modify the original object passed in by the caller.
- The top level
levelis the log protocol field and will not be overwritten by redaction. - No support for wildcard, glob, regex, bracket notation, remove or function censor.
Custom Pretty output
VextJS does not expose messageFormat template configuration by default. Most development scenarios can directly use prettySingleLine and prettyIgnore to control output compactness:
prettySingleLine: true: extra fields are inlined in the same line of the message as JSONprettySingleLine: false: extra fields multi-line expansionprettyIgnore: Hide structured fields that you don’t care about in the development log.
If you need to synchronize logs to an external system or completely take over the formatting logic, it is recommended to wrap the current logger in the plug-in through app.setLogger(). This does not affect the framework's default JSON fields, requestId injection, and child logger behavior.
requestId automatic injection
This is one of the most important features of the VextJS logging system. No need to manually pass in the requestId, all logs automatically carry the requestId of the current request.
Working principle
Vext logger's mixin is called before each log is written, reading the requestId of the current request from requestContext (based on AsyncLocalStorage) and appending it to the log field. This means:
- Log in handler: automatically carry requestId ✅
- Log in service: automatically carry requestId ✅
- Log in middleware: automatically carry requestId ✅
- Log of startup phase: No requestId (non-request context)✅
Performance optimization
Vext logger's request field injection takes the synchronous provider link, and directly skips the corresponding merging step when there is no request context or the user mixin is not configured. VextJS has made two optimizations:
- Empty context returns quickly: Non-request contexts such as startup phase and background tasks will not generate
requestId/trace_id/span_idfields. - ALS Disabled Detection: When AsyncLocalStorage is disabled, skip the
getStore()call
Child Logger
The child() method creates a child logger. The child logger inherits all configurations (level, format, mixin) of the parent logger, and additionally carries the specified binding fields:
Used in Service
It is recommended to create the child logger in the Service constructor:
Output example:
Nested Child Logger
Child loggers can be created nested, and fields will accumulate:
Error log
Logging Error object
Vext logger automatically serializes Error objects (retains message, stack, name):
Both direct Error and { err } fields are supported. When additional business context is required, it is recommended to use { err, ...context }:
Log error context
Extended Logger: setLogger()
app.setLogger(wrapper) is a plug-in-specific API that allows you to wrap all logging methods without replacing the default logger kernel - a common use is to forward framework logs to external systems (OTel Logs, Sentry, cloud logging platforms, etc.) simultaneously.
Function signature
wrapper takes the current complete runtime logger (the default Vext logger or the normalized result of the previous wrapper) and returns a complete or partial VextLoggerLike implementation. Missing methods fall back to the original logger. This can be done in the new implementation:
- Call external SDK to report logs
- Filter or sample certain levels
- Inject global fields
Typical usage: Bridge to OpenTelemetry Logs
When you use the vextjs-opentelemetry plugin, it will call app.setLogger() in setup() to automatically forward all calls to app.logger to the OTel Logs SDK without additional configuration:
Custom Logger extension example
setLogger adopts exactly the same wrapper pattern as setThrow: it receives the original implementation and returns the wrapped implementation. This means:
- Can be called multiple times (each time wrapping the previous result)
- Default logger functions (requestId injection, pretty format, child logger and runtime level control) are retained for methods that the wrapper does not override
- Exceptions thrown in the wrapper function will not affect the original logger :::
:::warning child logger fallback and bridging
When the wrapper does not return child(), child loggers fall back to the original logger. If child loggers should also be bridged, return a child() method from the wrapper and wrap the child logger there.
Log storage and collection
For production environments, it is recommended that VextJS output structured JSON to stdout/stderr, and then the process manager, container platform or log agent is responsible for persistence, rotation and reporting. In this way, the application process does not need additional log transport dependencies, and the log pipeline can be kept replaceable.
Solution Overview
Recommended log directory structure
Make sure .gitignore contains the logs/ directory and do not submit log files to the repository.
Solution 1: stdout → Cloud native
In platforms such as Kubernetes / AWS ECS / Google Cloud Run, output directly to stdout, which is automatically collected by the platform:
This is the simplest and most recommended cloud native solution - don't do any log configuration and let the platform handle everything.
Solution 2: PM2/systemd file collection
When deploying on a single machine, you can ask the process manager to write stdout/stderr to a file, and then rotate it with logrotate.
PM2 Example
systemd example
Solution 3: System-level logrotate (Linux)
If you use PM2 or systemd to manage the process, you can use the system's own logrotate to manage log rotation:
Solution 4: Filebeat → Elasticsearch → Kibana (ELK)
Complete ELK log analysis pipeline. Suitable for medium and large projects that require full-text search, aggregate analysis, and visualization panels.
Architecture
Filebeat collection
Kibana index mode
- Open Kibana → Stack Management → Index Patterns
- Create index mode:
myapp-* - Select
timefor the time field (ISO timestamp of Vext logger) - Search logs in Discover
Common queries:
- Track by requestId:
requestId: "abc-123" - Filter by error level:
level: 50(Vext logger level 50 = error) - Filter by service:
service: "UserService"
Option 5: Docker → Loki
When deploying containers, use the Docker logging driver to push directly to Grafana Loki:
Add the Loki data source to Grafana to query the logs:
- Query by requestId:
{app="myapp"} |= "abc-123" - Filter by JSON field:
{app="myapp"} | json | level >= 50
Solution six: app.setLogger bridges external SDK
If you must call the external log SDK synchronously within the application, you can wrap the current logger through app.setLogger(). This method is suitable for plug-in encapsulation, and the default logger continues to output to stdout.
This is not a replacement mechanism for the default logger, but a forwarding bridge at the plug-in layer. If you need official OTel Logs, Sentry, Loki/ELK plug-ins in the future, you can continue to expand on this wrapper contract.
Logging and OpenTelemetry
Combined with OpenTelemetry, trace_id and span_id can be automatically injected into the log to associate the log with link tracking:
How it works:
mixin()is called before each log is written, and the return value will be merged and injected with the framework's built-in fieldsrequestIdis a framework protected field and cannot be overridden by user mixin;trace_id/span_idand other fields are given priority by user mixin- When
mixinis not configured, user mixin calls will not be executed and the default request field injection behavior remains unchanged - The framework does not depend on
@opentelemetry/api, which is introduced by the user during tracing initialization
Relationship with F-03 (ALS automatic injection): If you write traceId / spanId to requestContext in the tracing middleware, the framework's built-in mixin will automatically inject it into the log - no need to configure the mixin option. The mixin configuration is suitable for scenarios where the currently active Span needs to be read directly from the OTEL Context API in real time.
For details, see the log correlation chapter in OpenTelemetry Access Example.
VextLogger interface
VextLogger is the logging interface exposed by the framework. You can use this interface in type declarations:
Differences in abilities from Pino
The goal of Vext's built-in logger is to override a stable subset of the framework's default logging requirements and remove the logger runtime dependency from the default installation path. It is not a complete compatibility layer for Pino, nor does it move all Pino extension points into the core.
These gaps do not affect the Vext default framework log, access log, requestId/trace field injection, child logger, Error serialization, and stdout-first collection. If official OTel Logs, Sentry, Loki/ELK plug-ins are needed in the future, priority should be based on app.setLogger() and external Agent extensions, rather than building the transport system back into the core.
Configuration reference
Best Practices
1. Use structured fields instead of string concatenation
2. Create Child Logger for each Service
3. Do not output sensitive information in the log
4. Use log levels appropriately
5. Use JSON format in production environment
JSON logs are the standard input format for log collection systems (ELK, Loki, Datadog, etc.). Ensure production environment pretty: false (default behavior).
Next step
- Understand the log collection solutions in Deployment and Production Environment
- View OpenTelemetry Access to associate logs with link tracking
- Learn how middleware generates logs during the request life cycle
- Explore the environment configuration override mechanism in Configuration