解决方案:使用 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>
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
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 '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for" $request_body';
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 '{"code": 0, data:"soulteary"}';
}
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 '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for" $request_body';
然后,关闭access_log;在不需要记录日志的路径中设置指令,避免记录不必要的日志。
location /internal-api-path {
access_log off;
default_type application/json;
return 200 '{"code": 0, data:"soulteary"}';
}
然后使用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 '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for" $request_body';
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 '{"code": 0, "data":"soulteary"}';
}
error_page 405 =200 $uri;
}
}
但是真的到这里了吗?
模拟前端客户端常见的跨域请求
我们打开熟悉的“百度”,在控制台输入如下代码,模拟一个常见的业务跨域请求。
async function testCorsPost(url = '', data = {}) {
const response = await fetch(url, {
method: 'POST',
mode: 'cors',
cache: 'no-cache',
credentials: 'same-origin',
headers: { 'Content-Type': 'application/json' },
redirect: 'follow',
referrerPolicy: 'no-referrer',
body: JSON.stringify(data)
});
return response.json();
}
testCorsPost('http://localhost:3000', { hello: "soulteary" }).then(data => console.log(data));
<p>
</p>
代码执行后,会看到经典的提示信息:
Access to fetch at 'http://localhost:3000/' from origin 'https://www.baidu.com' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' 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 'Access-Control-Allow-Credentials' 'false';
add_header 'Access-Control-Allow-Headers' 'Accept,Authorization,Cache-Control,Content-Type,DNT,If-Modified-Since,Keep-Alive,Origin,User-Agent,X-Mx-ReqToken,X-Requested-With,Date,Pragma';
add_header 'Access-Control-Allow-Methods' 'POST,OPTIONS';
add_header 'Access-Control-Allow-Origin' '$http_origin';
}
# OPTION 请求返回 204 ,并去掉 BODY响应,因 NGINX 限制,需要重复上面的前四行配置
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Credentials' 'false';
add_header 'Access-Control-Allow-Headers' 'Accept,Authorization,Cache-Control,Content-Type,DNT,If-Modified-Since,Keep-Alive,Origin,User-Agent,X-Mx-ReqToken,X-Requested-With,Date,Pragma';
add_header 'Access-Control-Allow-Methods' 'POST,OPTIONS';
add_header 'Access-Control-Allow-Origin' '$http_origin';
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Type' 'text/plain charset=UTF-8';
add_header 'Content-Length' 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 '{"key1":"value1", "key2":"value2"}' -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 '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for" $request_body';
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 'Access-Control-Allow-Credentials' 'false';
add_header 'Access-Control-Allow-Headers' 'Accept,Authorization,Cache-Control,Content-Type,DNT,If-Modified-Since,Keep-Alive,Origin,User-Agent,X-Mx-ReqToken,X-Requested-With,Date,Pragma';
add_header 'Access-Control-Allow-Methods' 'POST,OPTIONS';
add_header 'Access-Control-Allow-Origin' '$http_origin';
}
# OPTION 请求返回 204 ,并去掉 BODY响应,因 NGINX 限制,需要重复上面的前四行配置
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Credentials' 'false';
add_header 'Access-Control-Allow-Headers' 'Accept,Authorization,Cache-Control,Content-Type,DNT,If-Modified-Since,Keep-Alive,Origin,User-Agent,X-Mx-ReqToken,X-Requested-With,Date,Pragma';
add_header 'Access-Control-Allow-Methods' 'POST,OPTIONS';
add_header 'Access-Control-Allow-Origin' '$http_origin';
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Type' 'text/plain charset=UTF-8';
add_header 'Content-Length' 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 '{"code": 0, "data":"soulteary"}';
}
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、超强组件库,实现所有用户资源共享,确保所有网站都走在时代前沿