解决方案:使用 Nginx 构建前端日志统计服务(打点采集)服务

优采云 发布时间: 2022-12-12 09:52

  解决方案:使用 Nginx 构建前端日志统计服务(打点采集)服务

  在工作中,我们经常会遇到需要“数据支持”决策的情况,那么你有没有想过这些数据从何而来?如果业务涉及Web服务,这些数据的来源之一就是服务器上各个服务器的请求数据。如果我们区分专门用于统计的数据,一些服务器专注于接收“统计类型”的请求,那么这些产生的日志就是“管理日志”。

  本文将介绍如何在容器中使用Nginx来简单搭建一个支持前端使用的统计(dot采集)服务,避免引入过多的技术栈,增加维护成本。

  写在前面

  不知道大家有没有想过一个问题。当一个页面有很多打点事件时,打开页面时会同时发起无数个请求。此时,非宽带环境下的用户体验将不复存在,打点服务器也将面临友军的攻击。业务 DDoS 行为。

  因此,这几年,一些公司不断将数据统计方案从GET方案切换到POST方案,结合自研定制化SDK,将客户端的数据统计“打包合并”,并以一定的频率上报增量日志。极大的解决了前端的性能问题,减轻了服务器的压力。

  五年前,我分享了如何搭建一个易于扩展的前端统计脚本,有兴趣的可以看看。

  Nginx环境下POST请求的问题

  看到本节的标题,你可能会觉得一头雾水。POST 与 Nginx 交互是家常便饭,那有什么问题呢?

  我们来做一个小实验,使用容器启动一个 Nginx 服务:

  docker run --rm -it -p 3000:80 nginx:1.19.3-alpine

  然后在日常业务中使用curl模拟POST请求:

  curl -d '{"key1":"value1", "key2":"value2"}' -X POST http://localhost:3000

  你会看到如下返回结果:

  

405 Not Allowed

405 Not Allowed

nginx/1.19.3

  根据图查看Nginx模块modules/ngx_http_stub_status_module.c和http/ngx_http_special_response.c的源码可以看到如下实现:

  static ngx_int_t

ngx_http_stub_status_handler(ngx_http_request_t *r)

{

size_t size;

ngx_int_t rc;

ngx_buf_t *b;

ngx_chain_t out;

ngx_atomic_int_t ap, hn, ac, rq, rd, wr, wa;

if (!(r->method & (NGX_HTTP_GET|NGX_HTTP_HEAD))) {

return NGX_HTTP_NOT_ALLOWED;

}

...

}

...

static char ngx_http_error_405_page[] =

"" CRLF

"405 Not Allowed" CRLF

"" CRLF

"405 Not Allowed" CRLF

;

#define NGX_HTTP_OFF_4XX (NGX_HTTP_LAST_3XX - 301 + NGX_HTTP_OFF_3XX)

...

ngx_string(ngx_http_error_405_page),

ngx_string(ngx_http_error_406_page),

...

  没错,NGINX默认是不支持记录POST请求的,根据RFC7231会显示错误码405。所以一般情况下,我们会使用Lua/Java/PHP/Go/Node等动态语言进行辅助分析。

  那么如何解决这个问题呢?是否可以不借助外力,单纯使用性能好、重量轻的Nginx来完成对POST请求的支持?

  让Nginx“原生”支持POST请求

  为了更清楚的展示配置,我们接下来使用compose启动Nginx进行实验。在编写脚本之前,我们需要先获取配置文件,使用如下命令行将指定版本Nginx的配置文件保存到当前目录。

  docker run --rm -it nginx:1.19.3-alpine cat /etc/nginx/conf.d/default.conf > default.conf

  默认配置文件内容如下:

  server {

listen 80;

server_name localhost;

#charset koi8-r;

#access_log /var/log/nginx/host.access.log main;

location / {

root /usr/share/nginx/html;

index index.html index.htm;

}

#error_page 404 /404.html;

# redirect server error pages to the static page /50x.html

#

error_page 500 502 503 504 /50x.html;

location = /50x.html {

root /usr/share/nginx/html;

}

# proxy the PHP scripts to Apache listening on 127.0.0.1:80

#

#location ~ \.php$ {

# proxy_pass http://127.0.0.1;

#}

# pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000

#

#location ~ \.php$ {

# root html;

# fastcgi_pass 127.0.0.1:9000;

# fastcgi_index index.php;

# fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name;

# include fastcgi_params;

#}

# deny access to .htaccess files, if Apache's document root

# concurs with nginx's one

#

#location ~ /\.ht {

# deny all;

#}

}

  稍微压缩一下,我们得到一个更简单的配置文件并添加一行 error_page 405 =200 $uri; 对它:

  server {

listen 80;

server_name localhost;

charset utf-8;

location / {

return 200 "soulteary";

}

error_page 405 =200 $uri;

}

  将本节开头的命令重写为docker-compose.yml,并添加volumes将刚刚导出的配置文件映射到容器中,方便后续使用compose启动容器进行验证。

  version: "3"

services:

ngx:

image: nginx:1.19.3-alpine

restart: always

ports:

- 3000:80

volumes:

- ./default.conf/:/etc/nginx/conf.d/default.conf

  使用docker-compose up启动服务,然后使用之前的curl模拟POST验证请求是否正常。

  curl -d '{"key1":"value1", "key2":"value2"}' -H "Content-Type: application/json" -H "origin:gray.baai.ac.cn" -X POST http://localhost:3000

soulteary

  执行后,Nginx的日志记录中除了会返回字符串“soulteary”外,还会有一条看起来很正常的记录:

  ngx_1 | 192.168.16.1 - - [31/Oct/2020:14:24:48 +0000] "POST / HTTP/1.1" 200 0 "-" "curl/7.64.1" "-"

  但是,如果细心的话,你会发现我们发送的数据并没有收录在日志中,那么如何解决这个问题呢?

  修复 Nginx 日志中丢失的 POST 数据

  这个问题其实是家常便饭。默认的Nginx服务器日志格式是不收录POST Body的(性能考虑),没有proxy_pass也不会解析POST Body。

  首先执行以下命令:

  docker run --rm -it nginx:1.19.3-alpine cat /etc/nginx/nginx.conf

  可以看到默认的log_format配置规则是没有任何关于POST Body的数据的。

  user nginx;

worker_processes auto;

error_log /var/log/nginx/error.log warn;

pid /var/run/nginx.pid;

events {

worker_connections 1024;

}

http {

include /etc/nginx/mime.types;

default_type application/octet-stream;

log_format main '$remote_addr - $remote_user [$time_local] "$request" '

<p>

&#39;$status $body_bytes_sent "$http_referer" &#39;

&#39;"$http_user_agent" "$http_x_forwarded_for"&#39;;

access_log /var/log/nginx/access.log main;

sendfile on;

#tcp_nopush on;

keepalive_timeout 65;

#gzip on;

include /etc/nginx/conf.d/*.conf;

}

</p>

  所以解决这个问题并不难。添加新的日志格式,添加POST Body变量(request_body),然后添加proxy_pass路径,激活Nginx解析POST Body的处理逻辑。

  考虑到维护问题,我们之前的配置文件合并到这个配置中,定义了一个名为/internal-api-path的路径:

  user nginx;

worker_processes auto;

error_log /var/log/nginx/error.log warn;

pid /var/run/nginx.pid;

events {

worker_connections 1024;

}

http {

include /etc/nginx/mime.types;

default_type application/octet-stream;

log_format main &#39;$remote_addr - $remote_user [$time_local] "$request" &#39;

&#39;$status $body_bytes_sent "$http_referer" &#39;

&#39;"$http_user_agent" "$http_x_forwarded_for" $request_body&#39;;

access_log /var/log/nginx/access.log main;

sendfile on;

keepalive_timeout 65;

server {

listen 80;

server_name localhost;

charset utf-8;

location / {

proxy_pass http://127.0.0.1/internal-api-path;

}

location /internal-api-path {

# access_log off;

default_type application/json;

return 200 &#39;{"code": 0, data:"soulteary"}&#39;;

}

error_page 405 =200 $uri;

}

}

  将新的配置文件保存为nginx.conf后,在compose中调整volumes配置信息,再次使用docker-compose up启动服务。

  volumes:

- ./nginx.conf/:/etc/nginx/nginx.conf

  再次使用curl模拟之前的POST请求,会看到Nginx日志中多了两条记录。第一条记录收录我们需要的 POST 数据:

  192.168.192.1 - - [31/Oct/2020:15:05:48 +0000] "POST / HTTP/1.1" 200 29 "-" "curl/7.64.1" "-" {\x22key1\x22:\x22value1\x22, \x22key2\x22:\x22value2\x22}

127.0.0.1 - - [31/Oct/2020:15:05:48 +0000] "POST /internal-api-path HTTP/1.0" 200 29 "-" "curl/7.64.1" "-" -

  但是这里还有很多不完善的地方:

  接下来,让我们继续解决这些问题。

  改进 Nginx 配置,优化日志记录

  首先在日志格式中加入escape=json参数,让Nginx解析日志请求中的JSON数据:

  log_format main escape=json &#39;$remote_addr - $remote_user [$time_local] "$request" &#39;

&#39;$status $body_bytes_sent "$http_referer" &#39;

&#39;"$http_user_agent" "$http_x_forwarded_for" $request_body&#39;;

  然后,关闭access_log;在不需要记录日志的路径中设置指令,避免记录不必要的日志。

  location /internal-api-path {

access_log off;

default_type application/json;

return 200 &#39;{"code": 0, data:"soulteary"}&#39;;

}

  然后使用Nginx的map命令和Nginx中的条件判断过滤非POST请求的日志记录,拒绝处理非POST请求。

  map $request_method $loggable {

default 0;

POST 1;

}

...

server {

location / {

if ( $request_method !~ ^POST$ ) { return 405; }

access_log /var/log/nginx/access.log main if=$loggable;

proxy_pass http://127.0.0.1/internal-api-path;

}

...

}

  再次使用curl请求,会看到日志可以正常解析,不会出现两条日志。

  192.168.224.1 - [31/Oct/2020:15:19:59 +0000] "POST / HTTP/1.1" 200 29 "" "curl/7.64.1" "" {\"key1\":\"value1\", \"key2\":\"value2\"}

  同时,不再记录任何非POST请求。使用POST请求时,会提示405错误状态。

  这时候你可能会好奇,为什么这个405和上一篇不一样,不会重定向到200呢?这是因为这个405是我们根据触发条件“手动设置”的,而不是Nginx逻辑运行过程中判断出来的新结果。

  目前的Nginx配置如下:

  user nginx;

worker_processes auto;

error_log /var/log/nginx/error.log warn;

pid /var/run/nginx.pid;

events {

worker_connections 1024;

}

http {

include /etc/nginx/mime.types;

default_type application/octet-stream;

log_format main escape=json &#39;$remote_addr - $remote_user [$time_local] "$request" &#39;

&#39;$status $body_bytes_sent "$http_referer" &#39;

&#39;"$http_user_agent" "$http_x_forwarded_for" $request_body&#39;;

sendfile on;

keepalive_timeout 65;

map $request_method $loggable {

default 0;

POST 1;

}

server {

listen 80;

server_name localhost;

charset utf-8;

location / {

if ( $request_method !~ ^POST$ ) { return 405; }

access_log /var/log/nginx/access.log main if=$loggable;

proxy_pass http://127.0.0.1/internal-api-path;

}

location /internal-api-path {

access_log off;

default_type application/json;

return 200 &#39;{"code": 0, "data":"soulteary"}&#39;;

}

error_page 405 =200 $uri;

}

}

  但是真的到这里了吗?

  模拟前端客户端常见的跨域请求

  我们打开熟悉的“百度”,在控制台输入如下代码,模拟一个常见的业务跨域请求。

  async function testCorsPost(url = &#39;&#39;, data = {}) {

const response = await fetch(url, {

method: &#39;POST&#39;,

mode: &#39;cors&#39;,

cache: &#39;no-cache&#39;,

credentials: &#39;same-origin&#39;,

headers: { &#39;Content-Type&#39;: &#39;application/json&#39; },

redirect: &#39;follow&#39;,

referrerPolicy: &#39;no-referrer&#39;,

body: JSON.stringify(data)

});

return response.json();

}

testCorsPost(&#39;http://localhost:3000&#39;, { hello: "soulteary" }).then(data => console.log(data));

<p>

</p>

  代码执行后,会看到经典的提示信息:

  Access to fetch at &#39;http://localhost:3000/&#39; from origin &#39;https://www.baidu.com&#39; has been blocked by CORS policy: Response to preflight request doesn&#39;t pass access control check: No &#39;Access-Control-Allow-Origin&#39; header is present on the requested resource. If an opaque response serves your needs, set the request&#39;s mode to &#39;no-cors&#39; to fetch the resource with CORS disabled.

POST http://localhost:3000/ net::ERR_FAILED

  查看网络面板,您将看到两个失败的新请求:

  请求地址::3000/

  让我们继续调整配置以解决这个常见问题。

  使用Nginx解决前端跨域问题

  我们首先调整之前的过滤规则,允许处理 OPTIONS 请求。

  if ( $request_method !~ ^(POST|OPTIONS)$ ) { return 405; }

  跨域请求是常见的前端场景,很多人会懒得用“*”来解决问题,但是Chrome等现代浏览器在新版本的某些场景下不能使用这种松散的规则,为了业务安全,一般来说,我们会在服务器设置一个允许跨域请求的域名白名单。参考上面的方法,我们可以很容易的定义一个类似如下的Nginx map配置来拒绝所有前端未授权的跨域请求:

  map $http_origin $corsHost {

default 0;

"~(.*).soulteary.com" 1;

"~(.*).baidu.com" 1;

}

server {

...

location / {

...

if ( $corsHost = 0 ) { return 405; }

...

}

}

  这里有个窍门。Nginx 路由中的规则与级别编程语言并不完全相似。它们可以按顺序执行并具有“优先/覆盖”关系。所以,为了让前端能够正常调用接口进行数据提交,这里需要这样写规则,有四行代码冗余。

  if ( $corsHost = 0 ) { return 405; }

if ( $corsHost = 1 ) {

# 不需要 Cookie

add_header &#39;Access-Control-Allow-Credentials&#39; &#39;false&#39;;

add_header &#39;Access-Control-Allow-Headers&#39; &#39;Accept,Authorization,Cache-Control,Content-Type,DNT,If-Modified-Since,Keep-Alive,Origin,User-Agent,X-Mx-ReqToken,X-Requested-With,Date,Pragma&#39;;

add_header &#39;Access-Control-Allow-Methods&#39; &#39;POST,OPTIONS&#39;;

add_header &#39;Access-Control-Allow-Origin&#39; &#39;$http_origin&#39;;

}

# OPTION 请求返回 204 ,并去掉 BODY响应,因 NGINX 限制,需要重复上面的前四行配置

if ($request_method = &#39;OPTIONS&#39;) {

add_header &#39;Access-Control-Allow-Credentials&#39; &#39;false&#39;;

add_header &#39;Access-Control-Allow-Headers&#39; &#39;Accept,Authorization,Cache-Control,Content-Type,DNT,If-Modified-Since,Keep-Alive,Origin,User-Agent,X-Mx-ReqToken,X-Requested-With,Date,Pragma&#39;;

add_header &#39;Access-Control-Allow-Methods&#39; &#39;POST,OPTIONS&#39;;

add_header &#39;Access-Control-Allow-Origin&#39; &#39;$http_origin&#39;;

add_header &#39;Access-Control-Max-Age&#39; 1728000;

add_header &#39;Content-Type&#39; &#39;text/plain charset=UTF-8&#39;;

add_header &#39;Content-Length&#39; 0;

return 204;

}

  再次在网页上执行之前的JavaScript代码,会发现请求可以正常执行,前端数据会返回:

  {code: 0, data: "soulteary"}

  在Nginx的日志中,符合预期的会多出一条记录:

  172.20.0.1 - [31/Oct/2020:15:49:17 +0000] "POST / HTTP/1.1" 200 31 "" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.111 Safari/537.36" "" {\"hello\":\"soulteary\"}

  如果使用curl执行前面的命令,继续模拟纯接口调用,会发现405错误响应。这是因为我们的请求中没有收录origin请求头,无法表明我们的来源身份。在请求中使用-H参数完成这个数据,就可以得到预期的返回:

  curl -d &#39;{"key1":"value1", "key2":"value2"}&#39; -H "Content-Type: application/json" -H "origin:www.baidu.com" -X POST http://localhost:3000/

{"code": 0, "data":"soulteary"}

  比较完整的Nginx配置

  至此,我们基本实现了通用的采集功能,满足基本需求的Nginx配置信息如下:

  user nginx;

worker_processes auto;

error_log /var/log/nginx/error.log warn;

pid /var/run/nginx.pid;

events {

worker_connections 1024;

}

http {

include /etc/nginx/mime.types;

default_type application/octet-stream;

log_format main escape=json &#39;$remote_addr - $remote_user [$time_local] "$request" &#39;

&#39;$status $body_bytes_sent "$http_referer" &#39;

&#39;"$http_user_agent" "$http_x_forwarded_for" $request_body&#39;;

sendfile on;

keepalive_timeout 65;

map $request_method $loggable {

default 0;

POST 1;

}

map $http_origin $corsHost {

default 0;

"~(.*).soulteary.com" 1;

"~(.*).baidu.com" 1;

}

server {

listen 80;

server_name localhost;

charset utf-8;

location / {

if ( $request_method !~ ^(POST|OPTIONS)$ ) { return 405; }

access_log /var/log/nginx/access.log main if=$loggable;

if ( $corsHost = 0 ) { return 405; }

if ( $corsHost = 1 ) {

# 不需要 Cookie

add_header &#39;Access-Control-Allow-Credentials&#39; &#39;false&#39;;

add_header &#39;Access-Control-Allow-Headers&#39; &#39;Accept,Authorization,Cache-Control,Content-Type,DNT,If-Modified-Since,Keep-Alive,Origin,User-Agent,X-Mx-ReqToken,X-Requested-With,Date,Pragma&#39;;

add_header &#39;Access-Control-Allow-Methods&#39; &#39;POST,OPTIONS&#39;;

add_header &#39;Access-Control-Allow-Origin&#39; &#39;$http_origin&#39;;

}

# OPTION 请求返回 204 ,并去掉 BODY响应,因 NGINX 限制,需要重复上面的前四行配置

if ($request_method = &#39;OPTIONS&#39;) {

add_header &#39;Access-Control-Allow-Credentials&#39; &#39;false&#39;;

add_header &#39;Access-Control-Allow-Headers&#39; &#39;Accept,Authorization,Cache-Control,Content-Type,DNT,If-Modified-Since,Keep-Alive,Origin,User-Agent,X-Mx-ReqToken,X-Requested-With,Date,Pragma&#39;;

add_header &#39;Access-Control-Allow-Methods&#39; &#39;POST,OPTIONS&#39;;

add_header &#39;Access-Control-Allow-Origin&#39; &#39;$http_origin&#39;;

add_header &#39;Access-Control-Max-Age&#39; 1728000;

add_header &#39;Content-Type&#39; &#39;text/plain charset=UTF-8&#39;;

add_header &#39;Content-Length&#39; 0;

return 204;

}

proxy_pass http://127.0.0.1/internal-api-path;

}

location /internal-api-path {

access_log off;

default_type application/json;

return 200 &#39;{"code": 0, "data":"soulteary"}&#39;;

}

error_page 405 =200 $uri;

}

}

  如果结合容器使用,我们只需要为其单独添加一个额外的路由定义来进行健康检查,就可以实现一个简单稳定的采集服务。继续连接后续的数据传输和处理程序。

  location /health {

access_log off;

return 200;

}

  compose配置文件,相比之前,只多了几行健康检查定义:

  version: "3"

services:

ngx:

image: nginx:1.19.3-alpine

restart: always

ports:

- 3000:80

volumes:

- /etc/localtime:/etc/localtime:ro

- /etc/timezone:/etc/timezone:ro

- ./nginx.conf:/etc/nginx/nginx.conf

healthcheck:

test: wget --spider localhost/health || exit 1

interval: 5s

timeout: 10s

retries: 3

  结合 Traefik,实例可以很容易地水平扩展以处理更多的请求。有兴趣的可以看看我之前的文章。

  最后

  本文只介绍了数据采集的表层内容,更多内容以后有时间可能会详细介绍。毛孩的猫粮要付尾款了,先写到这里吧。

  解决方案:最简单的自助建站系统?

  触动心灵

  构建 网站 使用网站构建软件可以花更少的钱并获得快速的结果。建好网站后,不用请人维护网站。1. 首创页面可视化编辑,所见即所得

  1) 无需模板,只需选择您需要的栏目模块组件网站,即可自由编辑界面;

  2)无需提前规划布局,直接拖动网站版块,自由改变大小、位置和显示的数据信息,实现网站精准布局;

  

  3) 无需美工,直接选择选中的组件样式即可创建统一的网站;

  4)网站施工过程完全可视化操作,网站前台设计效果为网站真实效果。2.全面的DIV CSS结构,网站更规范,网速更快,推广更优化

  页面布局全面采用DIV CSS架构,真正做到W3C内容与性能分离,充分保证网站页面加载速度,更有利于搜索引擎优化。

  3.自动新闻在线采集,告别繁琐的手动操作

  4.强大的自定义表单功能,鼠标拖放即可完成表单创建

  5. 便捷精细的SEO优化,网站推广效果更佳

  

  6. 精准权限控制,网站管理轻松

  7.网站一键分离,轻松满足各种操作需求

  8.图片在线编辑器,鼠标拖动绘制精美

  九、多种技术加密,全方位保障软件和网站的安全

  10、超强组件库,实现所有用户资源共享,确保所有网站都走在时代前沿

0 个评论

要回复文章请先登录注册


官方客服QQ群

微信人工客服

QQ人工客服


线