Logger
Use app.logger for structured business logs, child loggers for Service identity and request IDs for correlation. The built-in implementation supports JSON and pretty output, six log methods, a runtime threshold, Error serialization and optional field redaction without a third-party logging package in its default kernel.
Complete the request flow below first, then choose formatting and collection as needed. Later API snippets belong in a route or plugin that already has app: VextApp. Merge config fragments with your existing config rather than replacing the whole file each time.
Basic usage
Prerequisite: the TypeScript API-only project from Quick Start. Keep its package.json, tsconfig.json and scripts, merge this config and add a Service and route:
Run npm run dev, then request these URLs in another terminal:
Use curl.exe in Windows PowerShell. The first request returns 200 and two users; JSON logs include levels 20/30, service=LogDemoService and requestId=log-demo-1. Trace is filtered by the debug threshold. The second returns 200 with data.logged: true and logs level 50, requestId=log-demo-2, and the Error's type/message/name/stack.
Stop dev, run npm run build and npm start, repeat both requests, then stop with Ctrl+C. Change the config level to info and restart: debug should disappear while info/error remain. Set pretty: false explicitly for production; application JSON logs and CLI startup notices may share one stream and need separating during collection.
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', debug() calls are filtered by the logger threshold and produce no log record.
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().- Use supported levels; invalid values are outside the public contract.
fatal()only records level 60; it does not itself exit the process or perform graceful shutdown.
Lifecycle log levels
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:
These are Bash forms. In PowerShell, set $env:VEXT_LIFECYCLE_LEVEL="verbose" before npm start and remove the temporary variable afterward. CLI --verbose also enables detail. Lifecycle verbosity is separate from the business log threshold. Some CLI startup notices use a separate output path and are not guaranteed to obey logger.level.
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
Without an explicit pretty override, production (NODE_ENV=production) uses JSON. These are two JSON Lines with pid/hostname omitted, not one JSON document containing both objects:
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 included in the pretty formatter's default ignore list, so pretty mode hides it. A request-scoped JSON log still contains it when context is active. Remove it fromprettyIgnoreto show it in pretty output.
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:
prettyIgnorecontrols only the display of extra pretty fields. It does not change JSON. JSON includes fields actually produced by that call, subject to level and redaction rules; a log outside request scope may have no requestId.
Log field redaction
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.- Redaction occurs before pretty/JSON output, making both formats consistent.
- Redaction does 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.
- Redaction does not scan passwords or tokens inside a message string. Content embedded in
msgorerr.messageis hidden only when that entire field is configured for replacement.prettyIgnoreis display filtering, not redaction.
Custom Pretty output
The default logger has no messageFormat template option. Prefer prettySingleLine, prettyIgnore and prettyColor. For completely custom formatting or forwarding, use the setLogger wrapper below.
Calling original retains the default output path. Returning custom info or error methods alone does not automatically apply the default serializer, threshold or redaction. A wrapper must decide which behavior to delegate to original.
requestId automatic injection
The default logger associates an ID when request context is enabled and the current chain has a nonempty requestId. Startup logs, independent tasks and logs after request context is disabled are not guaranteed to have one. Pretty output hides it by default.
Working principle
After threshold filtering, the default logger reads current context and merges user mixin and call fields. Handlers, middleware and Services share the ID while still in that request chain.
Field merge order is child bindings → context fields → user mixin → per-call object; ordinary duplicate fields use the later value. Structured objects cannot override the top-level protocol fields level/time/msg. Special requestId protection prevents mixin spoofing only: a per-call object can still replace requestId, and child bindings can retain it when ALS is absent. Avoid conflicting manual IDs; see Request Context.
Performance
Below-threshold default calls skip serialization and mixin, though JavaScript still evaluates argument expressions before the call. User mixins must be synchronous and inexpensive. A returned Promise or thrown error is ignored, with at most one attempted warning that still depends on the log threshold.
With requestContext.enabled: false, the default logger skips ALS reads. If ordinary getStore() is undefined, context fields are omitted; a background task within a manual run may still retain context fields.
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
The complete LogDemoService above shows this pattern: bind only the static Service name in the constructor. When a method runs, the logger reads the current request context; do not cache one request's store in a Service field.
Top-level bindings are independent across child loggers. A nested child wins on a duplicate top-level binding, although nested object values may still share references. Runtime level control is shared. A logger reference saved before a wrapper is installed does not automatically become wrapped; install the wrapper during plugin setup before Services load.
Nested Child Logger
Child loggers can be created nested, and fields will accumulate:
Error log
Log an Error object
Pass an Error directly to error/fatal or as a structured field. Built-in serialization keeps type, message, name and stack; arbitrary custom properties and cause chains are not fully expanded automatically.
Ordinary BigInt becomes a string and circular references become [Circular]. Undefined, functions and symbols are omitted from objects and become null in arrays. Dates become ISO strings. This is a log serializer, not a complete storage format for arbitrary business objects.
Log error context
The caller supplies the app and an implemented payment function. Logging does not swallow payment errors:
Extended Logger: setLogger()
Call app.setLogger(wrapper) during plugin setup to wrap the current app logger. A wrapper may return only selected methods; missing methods fall back to the original. Repeated calls wrap the preceding result in order.
Signature
The wrapper factory must synchronously return a plain object whose provided log members are functions. If the factory throws or returns an invalid result, installation fails. Exceptions thrown by log methods themselves propagate to callers; the framework does not catch them automatically.
Complete wrapper example
Add this plugin to the complete project above, restart and request both routes again. It needs no external SDK. It supplies only info and omits child, so the framework reapplies the factory to new child loggers:
Business info logs should still contain LogDemoService and the request ID; unchanged debug/error methods still output. On shutdown, inspect bridgeInfoCalls. This count includes framework info calls passing through the wrapper; it is not an HTTP request count.
Child fallback and bridging
When child is omitted, setLogger runs its factory again for the original child, retaining bindings and wrapping it. The factory may therefore run many times; do not open connections or register duplicate close hooks in it.
Returning child: bindings => original.child(bindings) explicitly yields an unbridged child. Wrap that child if a custom child method is necessary. If a child factory fails, normalization may fall back to the original child; that tolerance does not apply to ordinary info/error methods.
Bridging OpenTelemetry Logs
Initialization, endpoint, credentials, async queues and flushing belong to the external SDK integration. Prepare the environment with the OpenTelemetry example, then implement forwarding in your wrapper. Merely calling setLogger does not start a Collector or report logs.
If forwarding happens before original handles a call, it receives raw arguments; the default logger's threshold, redaction and formatting do not automatically apply to the SDK. Handle forwarding errors, filtering and field policy. Async writing must not leave unhandled Promise rejections in synchronous log methods. Close and flush the SDK through app.onClose; default logger shutdown does not flush it.
Log storage and collection
The default logger writes all levels, including error and fatal, to stdout. Process failures, CLI or other libraries may write stderr. Process managers normally collect streams, so an stderr file is not an "all error-level logs" file. Collection options below require their own installed components, permissions and network. Verify persistence, rotation and remote delivery in the actual deployment; VextJS does not guarantee them.
Solution Overview
Recommended log directory structure
Keep logs/ out of version control.
Solution 1: stdout → Cloud native
VextJS outputs logs. Persistence and search depend on platform logging configuration:
Send a request with a known requestId and find its business log on the target platform. Local stdout alone does not prove remote collection.
Solution 2: PM2/systemd file collection
These are Linux fragments. Install the chosen process manager, build the project and ensure directories exist with write permissions. Adjust paths for PM2 on Windows; systemd does not apply there.
PM2 example
PM2 out_file/error_file split stdout and stderr. Do not add a log_date_format or time prefix to JSON lines; see PM2 log management.
systemd example
Use a systemd version supporting append output. Adjust Node path, service account and directory permissions. This is only the Service fragment, not complete installation. See the systemd source documentation. After startup, send this page's requests and confirm business JSON in app.log, manager status and stderr.
Solution 3: System-level logrotate (Linux)
If logrotate is installed and invoked by a scheduler, it can rotate files. copytruncate suits writers that cannot be coordinated to reopen files, but writes between copy and truncation can be lost. It is not a lossless guarantee; see the logrotate manual:
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
The old type: log input was deprecated in Filebeat 7.16 and disabled in 9.0; see the official migration note. Use filestream with an ndjson parser. This is only an input fragment to merge into an existing Filebeat config; set output address, authentication, TLS and index/data-stream policy for your environment.
target: vext separates application fields from collector metadata. The parser expects one JSON object per line. CLI text produces a parse error; container-wrapped logs may need their outer envelope parsed first. See filestream parsers.
Run Filebeat's own test config and test output, then send a request from this page and confirm vext.requestId and vext.level appear in the actual index. Passing config checks does not establish successful indexing. Configure rotated-file matching and deduplication for the collector version.
Kibana Data View
Create a Data View for the index or data stream actually written. Choose a time field matching its mapping. Vext's time is an ISO string; map it to Elasticsearch date before selecting it as the time field. Filebeat reception time is not automatically business event time.
With the vext target above, query vext.requestId: "log-demo-1", vext.level >= 50 or vext.service: "LogDemoService". Change queries if you choose another target or mapping.
Option 5: Docker → Loki
Install the Loki driver on the Docker host and provide a Loki address reachable by the host/driver before configuring the service. The Compose service name loki is not guaranteed to resolve from the driver's network. This 127.0.0.1:3100 example assumes the port is published on the Docker host. See the driver configuration.
Check Compose config before deployment and driver send errors and Loki receipt afterward. Then query from Grafana with a configured Loki data source. Batch size is in bytes; finite retries do not guarantee delivery:
- Query by requestId:
{app="myapp"} |= "abc-123" - Filter by JSON field:
{app="myapp"} | json | level >= 50
Solution six: app.setLogger bridges external SDK
Reuse the wrapper pattern above. The SDK writer is an application-provided dependency: create it once during plugin setup and close it in app.onClose; the wrapper factory only binds methods. Verify default stdout first, then the SDK's success/failure behavior, child loggers, filtering, redaction and queue flush on shutdown.
This page does not supply an individual cloud SDK's installation or credentials. Follow that integration's documentation. Undefined cloudLogger or Sentry objects are not built-in VextJS features.
Logging and OpenTelemetry
With a configured tracing SDK, a synchronous mixin can read the current active span. Alternatively, put fields in request context within the correct request chain; see Request Context.
This typed config factory receives readActiveSpan from an already initialized SDK adapter. It only creates logger config; it does not create a tracing SDK or span:
Merge the return value into config.logger. The SDK adapter decides whether unsampled traces still need log correlation; isRecording need not be the sole condition. A mixin may override ALS trace_id/span_id but not requestId; per-call fields have higher priority. See the OpenTelemetry example for complete SDK setup.
VextLogger interface
Import the public types from vextjs rather than copying a potentially stale interface:
The complete LogDemoService above shows type usage. Public app.logger has no writable level property or public flush/close method. App lifecycle manages default kernel shutdown; external SDK resources still need plugin close hooks.
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.
This comparison defines Vext's current boundary; consult Pino's official API for its full options. For additional transports or formatting, integrate through a wrapper or collector and verify that external path separately.
Configuration reference
Best Practices
1. Use structured fields instead of string concatenation
For example, app.logger.info({ userId: "u-1", action: "login" }, "User logged in") keeps fields queryable. Put an Error in err rather than losing its stack in a concatenated message.
2. Create a child logger for each Service
As shown above, bind the static Service name in its constructor and pass business fields at call time. Read runtime context on each call rather than binding one request's identity to a long-lived logger.
3. Handle sensitive data according to project policy
Choose logged fields under your project's data policy. If redaction is required, configure it explicitly and verify JSON, pretty and external bridge outputs. Built-in redaction is off by default; field names are not automatically hidden.
4. Choose levels intentionally
Whether debug is emitted depends on the current threshold; it can also be enabled explicitly in production. Neither error nor fatal automatically throws or exits. Use the appropriate business or lifecycle mechanism when a request or process must change state.
5. Use JSON in production
Set pretty: false explicitly and parse JSON at the collector. Do not prefix JSON lines with another timestamp. CLI notices and third-party stdout may not be JSON; use a separate parser or retain parse-error events.
Common questions
Next step
- Request Context: identify sources of IDs, locale and trace fields.
- Access Log API: configure HTTP request access logs.
- Deployment: choose runtime and collection paths.
- OpenTelemetry: prepare an SDK and Collector.
- Configuration: understand profile overrides and validation.