yum upgrades for production use, this is the repository for you.
Active subscription is required.
You add $upstream_http_x_request_id to your NGINX log format, deploy it, and check the logs. Every entry shows a dash. The variable is empty. The upstream is sending the header — you can see it with curl -I — but by the time NGINX writes the access log, the value has already been evaluated and cached from an earlier phase when the upstream response hadn’t arrived yet. This is where the NGINX log_var_set module comes in.
Most NGINX variables are evaluated during request processing, not at log time. The set directive runs in the rewrite phase. The map directive evaluates lazily on first access, which can work at log time — but map is limited to the http block, has no conditional execution, and cannot override a variable that was already set.
The NGINX log_var_set module solves this by running a handler in NGX_HTTP_LOG_PHASE — the very last phase in NGINX’s request pipeline. Your variables get assigned after the response is fully sent but before the access log handler writes its entry. Upstream response headers, response times, and any other late-arriving data are guaranteed to be available.
How NGINX log_var_set Works
The module inserts itself at the beginning of the log phase, before NGINX’s built-in access log handler. When a request completes, the log_var_set handler runs first, evaluates each log_var_set directive in order, and writes the computed values into the request’s variable array. The access log handler runs immediately after and reads those values.
Because it runs in NGX_HTTP_LOG_PHASE, the module has access to everything: upstream response headers ($upstream_http_*), response timing ($upstream_response_time), final status codes, and any variables set during earlier phases.
When to Use log_var_set vs. map
NGINX’s built-in map directive evaluates lazily and can capture upstream headers at log time. For simple one-to-one mappings, map is fine. However, NGINX log_var_set is the better choice when you need:
| Feature | map |
log_var_set |
|---|---|---|
| Location-level control | No (http block only) | Yes (http, server, location) |
| Conditional execution | No | Yes (if= / if!=) |
| Inheritance behavior | Global | Hierarchical with override |
| Variable override | No (read-only transform) | Yes (sets new value) |
If you need per-location behavior, conditional assignment, or variable overrides for logging, log_var_set is what you want.
Installation
RHEL, CentOS, AlmaLinux, Rocky Linux
sudo dnf install https://extras.getpagespeed.com/release-latest.rpm
sudo dnf install nginx-module-log-var-set
Load the module by adding this line to the top of /etc/nginx/nginx.conf, before the events block:
load_module modules/ngx_http_log_var_set_module.so;
Debian and Ubuntu
First, set up the GetPageSpeed APT repository, then install:
sudo apt-get update
sudo apt-get install nginx-module-log-var-set
On Debian/Ubuntu, the package handles module loading automatically. No
load_moduledirective is needed.
For package details, see the module page.
Verifying Installation
Confirm the directive is available in the compiled binary:
strings /usr/lib64/nginx/modules/ngx_http_log_var_set_module.so | grep log_var_set
The log_var_set Directive
log_var_set $variable value [if=condition];
log_var_set $variable value [if!=condition];
| Property | Value |
|---|---|
| Default | — |
| Context | http, server, location |
Assigns a value to a variable right before access log writing. The value supports NGINX variable interpolation, including $upstream_http_* headers.
Parameters:
$variable— Target variable name. Created automatically if it does not exist.value— The value to assign. Can contain other NGINX variables.if=condition— Only set when the condition is non-empty and non-zero.if!=condition— Only set when the condition is empty or zero.
Inheritance: Directives at a parent level (server) are inherited by child levels (location). If a child defines the same variable, the child’s value wins. If a child defines a different variable, both apply.
Capturing Upstream Response Headers
This is the primary use case. Your backend sends headers like X-Request-ID and X-Backend-Server, and you want them in your access log:
log_format upstream_debug escape=json
'{"timestamp":"$time_iso8601",'
'"remote_addr":"$remote_addr",'
'"request":"$request",'
'"status":"$status",'
'"request_id":"$log_req_id",'
'"upstream_time":"$log_upstream_time",'
'"backend_server":"$log_backend"}';
server {
listen 80;
server_name app.example.com;
access_log /var/log/nginx/app.log upstream_debug;
location / {
proxy_pass http://app_backend;
log_var_set $log_req_id $upstream_http_x_request_id;
log_var_set $log_upstream_time $upstream_response_time;
log_var_set $log_backend $upstream_http_x_backend_server;
}
}
Because NGINX log_var_set runs in the log phase, the upstream variables are populated by the time they are read. No more empty dashes in your JSON logs.
For more on upstream communication tuning, see tuning proxy_buffer_size in NGINX.
Tagging Locations for Log Filtering
A simpler use case: add a tag to each location so you can filter logs by application section:
log_format custom '$remote_addr "$request" $status "$log_tag"';
server {
listen 80;
server_name example.com;
access_log /var/log/nginx/access.log custom;
location / {
log_var_set $log_tag "general";
root /var/www/html;
}
location /api/ {
log_var_set $log_tag "api";
proxy_pass http://backend;
}
location /admin/ {
log_var_set $log_tag "admin";
proxy_pass http://backend;
}
}
Each location gets its own tag. Filtering with jq, grep, or your log aggregation tool becomes straightforward.
Conditional Assignment with if= and if!=
The log_var_set directive supports conditions. You can set different values depending on request attributes:
log_format conditional '$remote_addr "$request" $status '
'method_tag="$log_method_tag"';
server {
listen 80;
server_name example.com;
access_log /var/log/nginx/access.log conditional;
location / {
set $is_post "";
if ($request_method = POST) {
set $is_post "1";
}
log_var_set $log_method_tag "write-operation" if=$is_post;
log_var_set $log_method_tag "read-operation" if!=$is_post;
proxy_pass http://backend;
}
}
A POST request sets $is_post to "1". The if=$is_post condition matches and $log_method_tag becomes "write-operation". For GET requests, $is_post is empty, so the if!=$is_post branch fires instead.
Conditions follow NGINX’s truthiness rules: any non-empty string that is not "0" is true. You can use any variable — for example, if=$http_authorization is true when the Authorization header is present.
Redacting Sensitive Data
A practical security use case: prevent authorization tokens from appearing in log files:
log_format secure '$remote_addr "$request" $status auth=$log_auth';
server {
listen 80;
server_name api.example.com;
access_log /var/log/nginx/api.log secure;
location / {
log_var_set $log_auth "REDACTED" if=$http_authorization;
log_var_set $log_auth "none" if!=$http_authorization;
proxy_pass http://api_backend;
}
}
Authenticated requests log auth=REDACTED. Unauthenticated requests log auth=none. Your logs remain useful for debugging without leaking credentials.
For more NGINX security techniques, see the security headers module.
Variable Inheritance
The log_var_set inheritance model lets you define defaults at the server level and override per location:
log_format inherit_demo escape=json
'{"request":"$request",'
'"backend":"$log_backend",'
'"request_id":"$log_req_id"}';
upstream app_backend {
server 127.0.0.1:8081;
}
server {
listen 80;
server_name example.com;
access_log /var/log/nginx/app.log inherit_demo;
# Default — inherited by all locations
log_var_set $log_backend "unknown";
location / {
proxy_pass http://app_backend;
log_var_set $log_req_id $upstream_http_x_request_id;
log_var_set $log_backend $upstream_http_x_backend_server;
}
location /static/ {
# Inherits $log_backend = "unknown" from server level
# Does NOT inherit $log_req_id (not defined here)
root /var/www;
}
}
The /static/ location inherits $log_backend = "unknown" from the server block. The $log_req_id and overridden $log_backend from / do not leak into /static/. Each location’s log_var_set directives are isolated.
Performance Impact
The log_var_set module adds negligible overhead:
- Log phase only — runs once per request, after the response is sent. Zero impact on request processing or response delivery.
- No network requests — reads values already in the request context.
- No allocations — writes to NGINX’s existing
r->variablesarray. - Linear iteration — walks the configured directives in order. Sub-microsecond even with dozens of directives.
Troubleshooting
Variable Shows Empty in Logs
- Module not loaded — Verify
load_module modules/ngx_http_log_var_set_module.so;is at the top ofnginx.conf. - Source variable unavailable —
$upstream_http_*variables only exist for proxied requests. Local responses (return 200) leave them empty. - Conditional not matching — Check that your
if=condition evaluates to non-empty, non-zero.
unknown directive "log_var_set"
The module file is missing or not loaded:
ls /usr/lib64/nginx/modules/ngx_http_log_var_set_module.so
head -5 /etc/nginx/nginx.conf
SELinux Blocking Upstream Connections
On RHEL systems with SELinux enforcing, NGINX cannot connect to backends by default:
sudo setsebool -P httpd_can_network_connect 1
For more, see our NGINX SELinux configuration guide.
Complete Production Configuration
A production-ready setup combining upstream capture, auth redaction, and section tagging:
load_module modules/ngx_http_log_var_set_module.so;
events {
worker_connections 1024;
}
http {
log_format api_log escape=json
'{"timestamp":"$time_iso8601",'
'"remote_addr":"$remote_addr",'
'"request_method":"$request_method",'
'"request_uri":"$request_uri",'
'"status":"$status",'
'"body_bytes_sent":"$body_bytes_sent",'
'"request_time":"$request_time",'
'"upstream_response_time":"$log_upstream_time",'
'"request_id":"$log_request_id",'
'"backend":"$log_backend",'
'"auth_status":"$log_auth_status",'
'"section":"$log_section"}';
upstream api_backend {
server 127.0.0.1:3000;
}
server {
listen 80;
server_name api.example.com;
access_log /var/log/nginx/api-access.log api_log;
# Server-level defaults
log_var_set $log_backend "direct";
log_var_set $log_section "general";
location /api/ {
proxy_pass http://api_backend;
log_var_set $log_section "api";
log_var_set $log_request_id $upstream_http_x_request_id;
log_var_set $log_upstream_time $upstream_response_time;
log_var_set $log_backend $upstream_http_x_served_by;
log_var_set $log_auth_status "authenticated" if=$http_authorization;
log_var_set $log_auth_status "anonymous" if!=$http_authorization;
}
location /health {
log_var_set $log_section "health";
return 200 "OK";
}
location / {
root /var/www/html;
}
}
}
Conclusion
The NGINX log_var_set module fills a gap that native NGINX directives cannot cover: setting and overriding variables at the exact moment before access logs are written. If you have been fighting empty upstream headers in your log files, or need per-location conditional logic for logging, this module is the straightforward fix.
Install it from the GetPageSpeed repository and check the source code on GitHub.
