Access Log middleware
Access Log uses app.logger to record the method, path, status, downstream processing time, and client IP for requests that reach the middleware. This page covers the seven config.accessLog fields and their boundaries. For business logs, see the Logger Guide.
Basic behavior
Production and development bootstrap register it by default, without app.use(). A request that is not excluded and whose await next() returns normally produces a line such as:
The recording scope matters:
- Earlier middleware such as CORS, body parsing, and rate limiting may return or throw before the request reaches Access Log.
- If downstream execution throws through Access Log's
await next(), this access line is not produced. Inspect Error Handling logs too. A 5xx response that returns normally follows the level-promotion rules below. - Timing starts when Access Log runs and ends when downstream middleware returns normally. It excludes earlier processing and does not mean the client received an entire streamed response.
- The path is
req.pathwithout query string. IP isreq.ip, affected by adapter andtrustProxy; it is not an identity credential.
Log format
Access Log uses compact single-line format and presents different output styles in development and production environments:
Development mode (Pretty)
When logger.pretty is true (the default value in the development environment), the built-in pretty formatter will output a readable format; if logger.prettyColor is parsed to enabled, only the level label will be colored:
The log message itself is always a compact single line of text in the format:
Production Mode (JSON)
When logger.pretty is false (production default), Vext logger outputs structured JSON. The examples omit pid and hostname. req-1 and req-2 are supplied request IDs; the built-in default generator uses UUIDs:
Each log is a complete line of JSON object, which is easy to parse by log collection systems such as ELK and Loki.
Note: The built-in logger reads requestId from AsyncLocalStorage through its context provider. No user
logger.mixinconfig is required. Logger thresholds and output formatting still apply.
requestId automatic injection
Working principle
With request context and requestId enabled, the adapter creates an AsyncLocalStorage scope, requestId middleware writes an accepted or generated ID to it, and the built-in logger reads it. No automatic field is promised when requestContext/requestId is disabled or logging occurs outside a request scope. Pretty output hides requestId by default; JSON exposes it.
Cross-service tracking
The default incoming header is x-request-id. A nonempty value takes precedence; otherwise the plugin-registered generator, configured generate(), or default crypto.randomUUID() provides one. The response includes the ID. It must be a 1–512 character string without control characters; invalid values throw. Header names are configurable.
Outbound propagation belongs to app.fetch. This correlates request IDs but is not a complete distributed trace. See Request Context for configuration and scope.
Middleware execution location
Access Log is registered after response wrapping and enabled frontend rendering, before Session and plugin global middleware. Enabled modules change chain length, so there is no fixed position number.
Optional stages appear only when enabled; see the Middleware Guide for phases and short circuits. Access Log reads current status on the return path, before outer post-processing completes.
Configuration items
Merge config.accessLog into the existing src/config/default.ts. This example selects common exclusions and a slow threshold; table defaults below describe unconfigured behavior:
enabled
When set to false, Vext does not register the built-in access-log middleware during bootstrap, so it is absent from the request middleware chain.
level
With 'debug', control ordinary access logs through logger.level or runtime app.logger.setLevel(). Recorded 5xx uses error, non-5xx slow requests use warn, and other 4xx uses warn only with warnOn4xx: true. Logger thresholds still filter all levels; silent suppresses all output.
skipPaths
Internally uses Set to implement O(1) exact search.
Matching is case-sensitive against req.path, without query strings or automatic child-path matching.
Common uses: exclude high-frequency paths such as health checks, Kubernetes probes, and Prometheus metrics:
skipPathPrefixes
This uses case-sensitive string startsWith(), without glob or path-segment boundaries. /api/internal matches both /api/internal/users and /api/internal-tools. To exclude only one directory, combine an exact root path and a slash-terminated prefix:
slowThreshold
For a positive threshold, a non-5xx downstream duration strictly greater than it is promoted to warn and gets [SLOW]; equality does not match. 5xx takes precedence as error and does not get the marker:
logResponseSize
When enabled, log messages will append Content-Length after the IP (if present in the response header):
The implementation probes getHeader() on the response or underlying _serverResponse.getHeader(). Native exposes the latter; other built-in adapter wrappers do not expose these read channels, so the size field is not guaranteed across adapters. Even Native needs a header at that moment. With no read channel or header, no size is appended; a parsed zero or nonpositive value shows [-]. Units use 1024 with one decimal above 1 kB/1 MB. This is not a network-byte counter or a complete streamed-download measure.
warnOn4xx
The first matching rule determines level and marker:
Alerts can use collected levels, but also collect error-handling logs. Access Log alone misses the earlier short circuits and propagated errors described above.
Performance optimization
Access Log middleware has made a number of performance optimizations internally:
- Set precomputation —
skipPathsis converted toSetduring initialization, and the search complexity is O(1) - Method pre-binding —
logger.info.bind(logger)is bound during initialization to avoid dynamic search for each request - Quick skip — Disabled middleware is not registered in normal bootstrap; excluded paths skip timing and message construction
- Single-line message — Use string concatenation instead of structured objects to avoid pretty mode expanding fields into multiple lines
TypeScript types
Relationship with log storage
Access Log uses the unified app.logger. Storage requires deployment-side or plugin integration; accessLog alone does not create a log file or cloud connection. See the Logger Guide for options:
- stdout → Cloud — cloud native logging pipeline
- PM2 / systemd + logrotate — drop and rotate during stand-alone deployment
- Filebeat / Fluent Bit → ELK — Collect JSON logs to Elasticsearch
- Docker → Loki — Container log driver or Agent push
- app.setLogger bridging — Plug-in layer is forwarded to external SDK synchronously
For a separate access-log store, filter by msg, path, or level in collection. If the app must forward synchronously, a plugin setup() can wrap the current logger with app.setLogger() while preserving level control, child loggers, and existing output semantics.
Next step
- Understand the complete configuration and storage solution of Log System
- See Configuration Document to learn about the environment coverage mechanism
- Understand requestId and Request Context