Logging POST data from $request_body
Asked Answered
S

8

94

I have my config setup to handle a bunch of GET requests which render pixels that work fine to handle analytics and parse query strings for logging. With an additional third party data stream, I need to handle a POST request to a given url that has JSON in an expected loggable format inside of it's request body. I don't want to use a secondary server with proxy_pass and just want to log the whole response into an associated log file like what it does with GET requests. A snippet of some code that I'm using looks like the following:

GET request (which works great):

location ^~ /rl.gif {
  set $rl_lcid $arg_lcid;
  if ($http_cookie ~* "lcid=(.*\S)")
  {
    set $rl_lcid $cookie_lcid;
  }
  empty_gif;
  log_format my_tracking '{ "guid" : "$rl_lcid", "data" : "$arg__rlcdnsegs" }';
  access_log  /mnt/logs/nginx/my.access.log my_tracking;
  rewrite ^(.*)$ http://my/url?id=$cookie_lcid? redirect;
}

Here is kinda what I am trying to do: POST request (which does not work):

location /bk {
  log_format bk_tracking $request_body;
  access_log  /mnt/logs/nginx/bk.access.log bk_tracking;
}

Curling curl http://myurl/bk -d name=example gives me a 404 page not found.

Then I tried:

location /bk.gif {
  empty_gif;
  log_format bk_tracking $request_body;
  access_log  /mnt/logs/nginx/bk.access.log bk_tracking;
}

Curling curl http://myurl/bk.gif -d name=example gives me a 405 Not Allowed.

My current version is nginx/0.7.62. Any help in the right direction is very much appreciated! Thanks!

UPDATE So now my post looks like this:

location /bk {
  if ($request_method != POST) {
    return 405;
  }
  proxy_pass $scheme://127.0.0.1:$server_port/dummy;
  log_format my_tracking $request_body;
  access_log  /mnt/logs/nginx/my.access.log my_tracking;
}
location /dummy { set $test 0; }

It is logging the post data correctly, but returns a 404 on the requesters end. If I change the above code to return a 200 like so:

location /bk {
  if ($request_method != POST) {
    return 405;
  }
  proxy_pass $scheme://127.0.0.1:$server_port/dummy;
  log_format my_tracking $request_body;
  access_log  /mnt/logs/nginx/my.access.log my_tracking;
  return 200;
}
location /dummy { set $test 0; }

Then it return the 200 correctly, but no longer records the post data.

ANOTHER UPDATE Kinda found a working solution. Hopefully this can help other on their way.

Safko answered 8/2, 2011 at 22:44 Comment(2)
did you get anywhere with this? I'm up against a similar problem.Caporal
Instead of doing the set $test 0; try doing return 200; from the dummy location, for me it then returns a 200 as expected and I get the body logged still.Tincal
C
113

This solution works like a charm:

http {

   log_format postdata $request_body;

   server {     
      location = /post.php {
         access_log  /var/log/nginx/postdata.log  postdata;
         fastcgi_pass php_cgi;
      }
   }
}

I think the trick is making nginx believe that you will call a CGI script.

Edit 2022-03-15: there is some discussion on where log_format should be set. The documentation clearly says that it needs to be in the http context: http://nginx.org/en/docs/http/ngx_http_log_module.html#log_format

If you put log_format in the server context, nginx will fail to load the config: nginx: [emerg] "log_format" directive is not allowed here in <path>:<line> (tested with nginx 1.20 on ubuntu 18.04)

Credulous answered 19/9, 2011 at 15:45 Comment(6)
I have to move "log_format postdata $request_body;" to server part because nginx says "log_format directive is not allowed here"Unripe
This is the right answer. Except, like @OlegNeumyvakin mentioned, I had to move the log_format part. But in my case, I had to move it to inside the "http" block, instead of inside the "server" block, for it to work. But this did the trick!Ettore
@OlegNeumyvakin & Matt : I have put your remarks into the answer itself (awaiting review currently). Thank you!Schmitt
I even had to move the log_format line above the server part. But then it worked well. ( nginx/1.9.3 )Aponeurosis
yes, log_format is only allowed in the http part of the nginx config. I changed the example accordingly.Credulous
Tested in nginx 1.20. The fastcgi is not required; it worked for me with proxy_pass in the location.Sycosis
H
53

Try echo_read_request_body.

"echo_read_request_body ... Explicitly reads request body so that the $request_body variable will always have non-empty values (unless the body is so big that it has been saved by Nginx to a local temporary file)."

location /log {
  log_format postdata $request_body;
  access_log /mnt/logs/nginx/my_tracking.access.log postdata;
  echo_read_request_body;
}
Hungarian answered 25/12, 2012 at 23:39 Comment(7)
This worked better than the accepted answer, since it did not need fastcgi to be installedFiddlehead
install by apt-get install nginx-fullChiasma
@user4I can see dash only in my logs. What's the problem? I use echo_read_request_body. and echo module installedCaine
for me, if I redirect (eg 301/302), request_body disappearsPretor
@dcousens at what point are you serving the 301/302? If you're setting the 301/302 redirect right in the nginx location directive, then the behaviour you're seeing makes sense. If it's happening after some php processing, that would be a little odd. You should start a new stack overflow question so someone can help you.Lass
in the location directive. I don't hesitate to announce that my experience with nginx is relatively low, but it wasn't immediately clear to me why the request_body would disappear, or why that would be sensical.Pretor
Work like a charm. Though echo module externally needs to be added in docker Nginx version by building dynamically. Follow this for building dynamically nginx.com/blog/compiling-dynamic-modules-nginx-plus and also download module from here github.com/openresty/echo-nginx-module/releases/tag/v0.62Membership
O
21

The solution below was the best format I found.

log_format postdata escape=json '$remote_addr - $remote_user [$time_local] '
                       '"$request" $status $bytes_sent '
                       '"$http_referer" "$http_user_agent" "$request_body"';
server {
        listen 80;

        server_name api.some.com;

        location / {
         access_log  /var/log/nginx/postdata.log  postdata;
         proxy_pass      http://127.0.0.1:8080;
        }

}

For this input

curl -d '{"key1":"value1", "key2":"value2"}' -H "Content-Type: application/json" -X POST http://api.deprod.com/postEndpoint

Generate that great result

201.23.89.149 -  [22/Aug/2019:15:58:40 +0000] "POST /postEndpoint HTTP/1.1" 200 265 "" "curl/7.64.0" "{\"key1\":\"value1\", \"key2\":\"value2\"}"
Oahu answered 22/8, 2019 at 19:17 Comment(8)
Also, note that using escape=none will just output the JSON as it came, without having the quotes escaped (if that is desirable for your use case)Gravedigger
Not working. Is there anything special needs to be done. For e.g. some specific module needs to be added?Membership
I tested in brand new nginx , if you wanna test, just create a raw docker image from hub.docker.com/_/nginx and try. Maybe your nginx have a different configOahu
this log GET requests too. I'm trying to log only postKreiner
postdata log remains empty for me.Impossibility
I was using return after logging instead of forwarding the request.Membership
This should be the accepted answer!Kreiner
You included the result! I know this should be a given, but I think it should be encouraged. :)Ardelia
S
20

Ok. So finally I was able to log the post data and return a 200. It's kind of a hacky solution that I'm not too proud of which basically overrides the natural behavior for error_page, but my inexperience of nginx plus timelines lead me to this solution:

location /bk {
  if ($request_method != POST) {
    return 405;
  }
  proxy_set_header  X-Forwarded-For $proxy_add_x_forwarded_for;
  proxy_set_header Host $host;
  proxy_set_header X-Real-IP $remote_addr;
  proxy_redirect off;
  proxy_pass $scheme://127.0.0.1:$server_port/success;
  log_format my_tracking $request_body;
  access_log  /mnt/logs/nginx/my_tracking.access.log my_tracking;
}
location /success {
  return 200;
}
error_page   500 502 503 504  /50x.html;
location = /50x.html {
  root   /var/www/nginx-default;
  log_format my_tracking $request_body;
  access_log  /mnt/logs/nginx/my_tracking.access.log my_tracking_2;
}

Now according to that config, it would seem that the proxy pass would return a 200 all the time. Occasionally I would get 500 but when I threw in an error_log to see what was going on, all of my request_body data was in there and I couldn't see a problem. So I caught that and wrote to the same log. Since nginx doesn't like the same name for the tracking variable, I just used my_tracking_2 and wrote to the same log as when it returns a 200. Definitely not the most elegant solution and I welcome any better solution. I've seen the post module, but in my scenario, I couldn't recompile from source.

Safko answered 5/7, 2011 at 18:50 Comment(2)
The solution you came up with is very similar to the accepted answer here: #17609972 Since nginx doesn't parse the body unless sending to a proxy or a fast-cgi server, proxying to itself works.Bravissimo
we use a similar solution, by proxy_pass the POST request to the another location.Smiley
F
10

FWIW, this config worked for me:

location = /logpush.html {
  if ($request_method = POST) {
    access_log /var/log/nginx/push.log push_requests;
    proxy_pass $scheme://127.0.0.1/logsink;
    break;
  }   
  return 200 $scheme://$host/serviceup.html;
}   
#
location /logsink {
  return 200;
}
Finlay answered 21/2, 2013 at 19:47 Comment(0)
G
6

nginx log format taken from here: http://nginx.org/en/docs/http/ngx_http_log_module.html

no need to install anything extra

worked for me for GET and POST requests:

upstream my_upstream {
   server upstream_ip:upstream_port;
}

location / {
    log_format postdata '$remote_addr - $remote_user [$time_local] '
                       '"$request" $status $bytes_sent '
                       '"$http_referer" "$http_user_agent" "$request_body"';
    access_log /path/to/nginx_access.log postdata;
    proxy_set_header Host $http_host;
    proxy_pass http://my_upstream;
    }
}

just change upstream_ip and upstream_port

Gifferd answered 13/1, 2015 at 10:12 Comment(0)
F
0

I had a similar problem. GET requests worked and their (empty) request bodies got written to the the log file. POST requests failed with a 404. Experimenting a bit, I found that all POST requests were failing. I found a forum posting asking about POST requests and the solution there worked for me. That solution? Add a proxy_header line right before the proxy_pass line, exactly like the one in the example below.

server {
    listen       192.168.0.1:45080;
    server_name  foo.example.org;

    access_log  /path/to/log/nginx/post_bodies.log post_bodies;
    location / {
      ### add the following proxy_header line to get POSTs to work
      proxy_set_header Host $http_host;
      proxy_pass   http://10.1.2.3;
    }
}

(This is with nginx 1.2.1 for what it is worth.)

Fowliang answered 14/6, 2012 at 13:44 Comment(0)
W
0

I think the correct answer is below exhibit:

location /bk {
  if ($request_method != POST) {
    return 405;
  }
  proxy_pass $scheme://127.0.0.1:$server_port/dummy;
  log_format my_tracking $request_body;
  access_log  /mnt/logs/nginx/my.access.log my_tracking;
  
}
location /dummy {
   access_log off; 
   return 200;
}
Wicked answered 8/10, 2021 at 8:46 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.