智慧医院智能运维实训指导书-实训指南04:实现网站访问用户行为分析

553 阅读28分钟

一、实训目的

  1. 掌握Wordpress的部署方法;
  2. 掌握Apache访问日志格式的配置方法;
  3. 掌握基于Filebeat实现Apache访问日志的采集;
  4. 掌握基于Logstash实现Apache访问日志的清洗;
  5. 掌握基于Kibana实现Apache访问日志的可视化分析。

二、实训学时

4 学时

三、实训类型

综合性

四、实训需求

1、硬件

每人配备计算机 1 台。

2、软件

安装Edge、Firefox、Chrome等最新版本浏览器,安装Mobaxterm软件。

3、网络

本地主机能够访问教学云计算平台,虚拟机按照配置指南配置网络。

4、工具

无。

五、实训任务

1、完成Wordpress网站的部署;

2、完成Apache访问日志格式配置;

3、完成Apache访问日志的采集清洗与分析;

4、完成Apache访问日志的可视化分析。

六、实训环境

本实训使用《实训指南02:实现Linux Syslog日志分析》中创建的虚拟机Labs-ELK-VM-102。

七、实训内容及步骤

1、部署Wordpress

1.1 配置安全策略

(1)在主机Labs-ELK-VM-102上配置firewalld防火墙规则,放行http服务,允许任意地址访问主机的httpd服务,参考命令如下。

# 配置firewalld防火墙,放行http服务,允许任意地址访问主机的httpd服务
[root@Labs-ELK-VM-102 ~]# firewall-cmd --zone=public --add-service=http --permanent
[root@Labs-ELK-VM-102 ~]# firewall-cmd --reload
[root@Labs-ELK-VM-102 ~]# firewall-cmd --list-all

1.2 安装Apache HTTP Server

(1)在主机Labs-ELK-VM-102上安装Apache HTTP Server,参考命令如下。

# 安装Apache HTTP Server
[root@Labs-ELK-VM-102 ~]# yum install -y  httpd

(2)启动Apache服务,设置服务开机自启,查看服务状态,参考命令如下。

# 启动Apache服务,设置开机自启
[root@Labs-ELK-VM-102 ~]# systemctl start httpd
[root@Labs-ELK-VM-102 ~]# systemctl enable httpd

# 查看Apache服务状态
[root@Labs-ELK-VM-102 ~]# systemctl status httpd

(3)使用浏览器访问http://10.10.2.102,出现欢迎界面,说明Apache Httpd Server安装成功,如图2-1所示。

1.3 安装PHP

(1)在主机Labs-ELK-VM-102上下载安装PHP及相应的模块,并查看php版本,参考命令如下。

# 下载安装PHP相应的模块
[root@Labs-ELK-VM-102 ~]# yum install -y php php-curl php-dom php-exif php-fileinfo php-fpm php-gd php-hash php-mbstring php-mysqli php-openssl php-pcre php-xml libsodium
# 查看PHP版本
[root@Labs-ELK-VM-102 ~]# php -v

(2)启动php-fpm服务,设置服务开机自启,查看服务状态,参考命令如下。

# 启动php-fpm服务,设置服务开机自启动
[root@Labs-ELK-VM-102 ~]# systemctl start php-fpm
[root@Labs-ELK-VM-102 ~]# systemctl enable php-fpm

# 查看php-fpm服务状态
[root@Labs-ELK-VM-102 ~]# systemctl status php-fpm

(3)修改php-fpm服务配置文件/etc/php-fpm.d/www.conf,将php-fpm的监听方式从Unix Socket改为TCP端口,参考命令如下。

[root@Labs-ELK-VM-102 ~]# vi /etc/php-fpm.d/www.conf
---------------/etc/php-fpm.d/www.conf---------------
...省略部分内容...
; listen = /run/php-fpm/www.sock
listen = 127.0.0.1:9000
...省略部分内容...
---------------/etc/php-fpm.d/www.conf---------------

(4)修改apache配置文件/etc/httpd/conf.d/php.conf,配置php请求转到php-fpm处理,重启php-fpm和apache服务,参考命令如下。

[root@Labs-ELK-VM-102 ~]# vi /etc/httpd/conf.d/php.conf
---------------/etc/httpd/conf.d/php.conf---------------
...省略部分内容...
DirectoryIndex index.php
# 配置文件中添加如下内容配置php请求转到php-fpm处理:
<FilesMatch .php$>
SetHandler "proxy:fcgi://127.0.0.1:9000"
</FilesMatch>
...省略部分内容...
---------------/etc/httpd/conf.d/php.conf---------------

[root@Labs-ELK-VM-102 ~]# systemctl restart php-fpm httpd

(5)在Apache网站根目录/var/www/html创建测试文件phpinfo.php验证php环境,重启Apache服务,参考命令如下。

# 在Apache网站的根目录创建测试文件
[root@Labs-ELK-VM-102 ~]# echo "<?php phpinfo(); ?>" > /var/www/html/phpinfo.php
# 重启Apache服务
[root@Labs-ELK-VM-102 ~]# systemctl restart httpd

(6)使用浏览器访问http://10.10.2.102/phpinfo.php查看php信息,如图2-2所示。

(7)php环境完成,删除验证文件/var/www/html/``phpinfo.php,参考命令如下。

# 删除验证文件/var/www/html/phpinfo.php
[root@Labs-ELK-VM-102 ~]# rm -rf /var/www/html/phpinfo.php

1.4 部署WordPress

(1)在主机Labs-ELK-VM-102上安装wgettar软件包,使用wget获取WordPress应用程序,参考命令如下。

# 安装下载工具
 [root@Labs-ELK-VM-102 ]# yum install -y wget tar
 # 下载WordPress安装包
 [root@Labs-ELK-VM-102 ]# wget https://cn.wordpress.org/latest-zh_CN.tar.gz

(2) 解压WordPress应用程序到Apache网站根目录/var/www/html,参考命令如下。

 # 解压WordPress安装包
 [root@Labs-ELK-VM-102 ]# tar zxvf latest-zh_CN.tar.gz -C /var/www/html

(3)进入目录/var/www/html/wordpress,复制WordPress数据库配置文件,参考命令如下。

# 进入目录/var/www/html/wordpress
[root@Labs-ELK-VM-102 ~]# cd /var/www/html/wordpress
# 复制WordPress数据库配置文件
[root@Labs-ELK-VM-102 wordpress]# cp -a wp-config-sample.php wp-config.php

(4)赋予apache用户对相关目录的操作权限,参考命令如下。

# 赋予Apache对相关目录的操作权限
[root@Labs-ELK-VM-102 wordpress]# chown -R apache:apache /var/www/html
[root@Labs-ELK-VM-102 wordpress]# chmod -R 755 /var/www/html
[root@Labs-ELK-VM-102 wordpress]# chown -R :apache /var/www/html/wordpress

(5)使用root用户本地连接MySQL数据库,创建WordPress数据库,创建用于管理WordPress数据库的管理用户,参考命令如下。

# 连接数据库
[root@Labs-ELK-VM-102 ~]# mysql -uroot -p

# 创建WordPress数据库
mysql> CREATE DATABASE wordpress;

# 创建数据库用户wordpressuser,并赋予用户权限、设置用户密码
mysql> CREATE USER 'wordpressuser'@'%' IDENTIFIED BY 'Wordpress@123';

# 设置权限,允许wordpressuser用户远程访问wordpress数据库
mysql> GRANT ALL PRIVILEGES ON wordpress.* TO 'wordpressuser'@'%';

# 刷新权限使更改生效,退出
mysql> flush privileges;
mysql> exit;

📌 WordPress程序使用的数据库为《实训指南03:实现MySQL数据库日志分析》中创建的MySQL数据库。

(6)编辑WordPress数据库配置文件/var/www/html/wordpress/wp-config.php,修改数据库连接配置,参考命令如下。

# 编辑wp-config.php文件。根据已配置的WordPress数据库信息,修改MySQL相关配置信息
[root@Labs-ELK-VM-102 ]# vi /var/www/html/wordpress/wp-config.php
--------------------wp-config.php--------------------
# 修改如下配置项
# /** WordPress数据库的名称 */
define( 'DB_NAME', 'wordpress' );
# /** MySQL数据库用户名 */
define( 'DB_USER', 'wordpressuser' );
# /** MySQL数据库密码 */
define( 'DB_PASSWORD', 'Wordpress@123' );
# /** MySQL主机 */
define( 'DB_HOST', 'localhost' );
--------------------wp-config.php--------------------

(7)修改完成重启Apache服务,参考命令如下。

[root@Labs-ELK-VM-102 ~]# systemctl restart httpd

1.6 初始化配置WordPress

(1)使用浏览器访问http://10.10.2.102/wordpress,根据安装向导填写安装信息,点击【安装WordPress】按钮,完成安装并创建站点,如图2-3所示。

(2)等待初始化完成,单击【登录】,输入WordPress初始化用户名和密码完成登录,如图2-4所示。

2、配置Apache访问日志格式

(1)在主机Labs-ELK-VM-102上修改Apache的配置文件/etc/httpd/conf/httpd.conf,自定义Apache日志记录格式,参考命令如下。

# 自定义需要采集的日志格式
# Apache日志可通过/etc/httpd/conf/httpd.conf文件进行设置
[root@Labs-ELK-VM-102 ~]# vi /etc/httpd/conf/httpd.conf
#访问日志的配置信息,通过日志格式字符串可定义访问日志记录的字段
---------------------httpd.conf--------------------
.........省略部分内容......
<IfModule log_config_module>
    #
    # The following directives define some format nicknames for use with
    # a CustomLog directive (see below).
    #
    LogFormat "%h %l %u %t "%r" %>s %b "%{Referer}i" "%{User-Agent}i"" combined
    LogFormat "%h %l %u %t "%r" %>s %b" common
    # 添加:定义名为“filebeat”的日志记录格式
    LogFormat "%a | %h | %{remote}p | %u | \"%{Referer}i\" | \"%{User-Agent}i\" | %C | %t | %H | %m | %D | %U | %q | %I | %k | %X | %v | %A | %{local}p | %P | %f | %>s | %O | %b | %{c}a" filebeat

    <IfModule logio_module>
      # You need to enable mod_logio.c to use %I and %O
      LogFormat "%h %l %u %t "%r" %>s %b "%{Referer}i" "%{User-Agent}i" %I %O" combinedio
    </IfModule>

    #
    # The location and format of the access logfile (Common Logfile Format).
    # If you do not define any access logfiles within a <VirtualHost>
    # container, they will be logged here.  Contrariwise, if you *do*
    # define per-<VirtualHost> access logfiles, transactions will be
    # logged therein and *not* in this file.
    #
    #CustomLog "logs/access_log" common
    #访问日志存放在目录/var/log/httpd/logs/access_log下,日志记录格式为filebeat定义的格式
    CustomLog /var/log/httpd/logs/access_log filebeat

    #
    # If you prefer a logfile with access, agent, and referer information
    # (Combined Logfile Format) you can use the following directive.
    #
.........省略部分内容......
---------------------------------------------------
#创建新格式日志输出保存的目录,并重载Apache服务
[root@Labs-ELK-VM-102 ~]# mkdir -p /var/log/httpd/logs/
[root@Labs-ELK-VM-102 ~]# systemctl reload httpd

#验证查看配置后的日志
[root@Labs-ELK-VM-102 ~]# cat /var/log/httpd/logs/access_log
10.10.0.1 | 10.10.0.1 | 64580 | - | "http://10.10.2.102/wordpress/" | "Mozilla/5.0 (Windows NT 1                                                      0.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36 Edg/138.0                                                      .0.0" | - | [01/Aug/2025:18:56:07 +0800] | HTTP/1.1 | GET | 228 | /wordpress/wp-content/themes/t                                                      wentytwentyfive/assets/fonts/manrope/Manrope-VariableFont_wght.woff2 |  | 444 | 1 | + | localhos                                                      t | 10.10.2.102 | 80 | 204225 | /var/www/html/wordpress/wp-content/themes/twentytwentyfive/asset                                                      s/fonts/manrope/Manrope-VariableFont_wght.woff2 | 200 | 53886 | 53600 | 10.10.0.1

📌小贴士:

Apache日志记录格式-LogFormat格式串的变量及其含义:

  • %a:请求的客户端 IP 地址
  • %h:来访主机名
  • %{remote}p:来访端口
  • %u:来访用户
  • \”%{Referer}i\”:来源地址
  • \”%{User-Agent}i\”:UA
  • %C:Cookie
  • %t:请求时间
  • %H:请求协议
  • %m:请求方法
  • %D:请求耗时
  • %U:请求的 URL 路径
  • %q:请求参数
  • %I:接收字节数
  • %k:KeepAlive
  • %X:响应完成时的连接状态
  • %v:服务主机名
  • %A:本地 IP 地址、
  • %{local}p:服务端口
  • %P:进程ID
  • %f:请求文件地址
  • %>s:响应状态
  • %O:发送的字节数
  • %b:响应大小(以字节为单位)
  • %{c}a:连接的基础对等IP地址

3、安装Filebeat日志采集器

在主机Labs-ELK-VM-102上,解压Filebeat日志采集器安装包到/opt目录下,并重命名解压目录,参考命令如下。

#解压安装包到/opt目录下
[root@Labs-ELK-VM-102 ~]# tar -zxvf filebeat-9.0.4-linux-x86_64.tar.gz -C /opt

# 进入/opt目录下,重命名解压目录
[root@Labs-ELK-VM-102 ~]# cd /opt/
[root@Labs-ELK-VM-102 opt]# mv filebeat-9.0.4-linux-x86_64 filebeat-apache

4、配置Filebeat收集Apache访问日志

(1)在主机Labs-ELK-VM-102上创建filebeat配置文件/opt/filebeat-apache/filebeat-apache.yml,配置filebeat输入内容,参考命令如下。

# 创建filebeat配置文件/opt/filebeat-apache/filebeat-apache.yml
[root@Labs-ELK-VM-102 opt]# vi /opt/filebeat-apache/filebeat-apache.yml
-------------------/opt/filebeat-apache/filebeat-apache.yml-------------------
#配置Filebeat输入
filebeat.inputs:
#声明输入类型,指定要收集的日志类型为文件日志
- type: filestream
  id: labs-elk-apache-id
#指示Filebeat启用该输入,即开始收集指定的日志
  enabled: true
#定义要收集的日志文件路径的列表
  paths:
    - /var/log/httpd/logs/access_log
#定义日志字段
  fields:
    type: labs-elk-apache
#定义fields下的字段添加到根级别,这样所有的字段将作为日志事件的顶级字段
  fields_under_root: true
--------------------/opt/filebeat-apache/filebeat-apache.yml--------------------

(2)修改filebeat配置文件/opt/filebeat-apache/filebeat-apache.yml,配置filebeat输出,参考命令如下。

#启用 Logstash 输出
-------------------/opt/filebeat-apache/filebeat-apache.yml-------------------
output.logstash:
#指定Logstash服务器和Logstash配置为侦听传入的端口 () 击败连接
#hosts5044
  hosts: ["10.10.2.101:5046"]
-------------------/opt/filebeat-apache/filebeat-apache.yml-------------------

(3)在主机Labs-ELK-VM-102上创建自定义系统服务配置文件/lib/systemd/system/filebeat-apache.service将filebaet注册为系统服务filebeat-apache,参考命令如下。

# 将filebaet注册为系统服务
[root@Labs-ELK-VM-102 opt]# cat > /lib/systemd/system/filebeat-apache.service <<EOF
[Unit]
Description=filebeat
Wants=network-online.target
After=network-online.target

[Service]
User=root
ExecStart=/opt/filebeat-apache/filebeat -e -c /opt/filebeat-apache/filebeat-apache.yml
# 设置为掉线自动重启,进程强制杀掉后会自动重新启动
Restart=always

[Install]
WantedBy=multi-user.target
EOF

(4)启动filebeat-apache服务(通过filebeat收集Apache访问日志并推送至logstash),设置服务为开机自启动,并查看服务状态,参考命令如下。

# 启动filebeat-apache服务,设置服务为开机自启动
[root@Labs-ELK-VM-102 opt]# systemctl enable filebeat-apache --now

# 查看服务状态
[root@Labs-ELK-VM-102 opt]# systemctl status filebeat-apache

5、使用Logstash进行Apache日志清洗

(1)在主机Labs-ELK-VM-101上修改容器labs-elk-logstash03的配置文件/data/docker/volumes/labs-elk-logstash03-pipeline/_data/logstash.conf配置logstash输入,使用beats输入插件接收数据,参考命令如下。

#使用beats输入将日志导入Logstash,处理Beats送的日志事件
#修改Logstash配置文件/data/docker/volumes/labs-elk-logstash03-pipeline/_data/logstash.conf
[root@Labs-ELK-VM-101 ~]# vi /data/docker/volumes/labs-elk-logstash03-pipeline/_data/logstash.conf
----------------------logstash--------------------
# Sample Logstash configuration for creating a simple
# 创建一个从Beats到Logstash到Elasticsearch的数据管道
# Beats -> Logstash -> Elasticsearch pipeline.
input {
  # 定义输入插件Beats,允许Logstash接收来自Beats(Filebeat、Winlogbeat)的数据
  beats {
    # 指定Logstash监听的端口号,以接收来自Beats的数据
    port => 5044
  }
}
---------------------------------------------------

(2)在主机Labs-ELK-VM-101上修改容器labs-elk-logstash03的配置文件,配置过滤器清洗日志,参考命令如下。

# 在“输入”下面添加以下过滤内容
----------------------logstash--------------------
filter {
  # 指定字段类型,如果字段类型是'labs-elk-apache',则执行以下操作
  if [type] == "labs-elk-apache" {
  # 使用mutate插件,对消息字段进行修改和转换
  mutate {
    # 使用split插件,将消息字段(通常是日志行)按照指定的分隔符 " | " 进行拆分
    # 将每个日志行被分割成多个部分,每个部分都成为一个单独的字段
    split => { "message" => " | " }
    # 使用mutate插件的add_filed功能
    # 为每个新创建的字段添加一个字段名,并从拆分后的消息数组中选择相应的索引位置作为字段值
    add_field => { "c_ip" => "%{[message][0]}" }
    add_field => { "c_host" => "%{[message][1]}" }
    add_field => { "c_port" => "%{[message][2]}" }
    add_field => { "c_user" => "%{[message][3]}" }
    add_field => { "cs_referer" => "%{[message][4]}" }
    add_field => { "cs_user_agent" => "%{[message][5]}" }
    add_field => { "cs_cookie" => "%{[message][6]}" }
    add_field => { "cs_date_time_temp" => "%{[message][7]}" }
    add_field => { "cs_protocol" => "%{[message][8]}" }
    add_field => { "cs_method" => "%{[message][9]}" }
    add_field => { "cs_time_taken" => "%{[message][10]}" }
    add_field => { "cs_uri_stem" => "%{[message][11]}" }
    add_field => { "cs_uri_query" => "%{[message][12]}" }
    add_field => { "cs_bytes" => "%{[message][13]}" }
    add_field => { "cs_keep_alive" => "%{[message][14]}" }
    add_field => { "cs_conn_status" => "%{[message][15]}" }
    add_field => { "s_servername" => "%{[message][16]}" }
    add_field => { "s_ip" => "%{[message][17]}" }
    add_field => { "s_port" => "%{[message][18]}" }
    add_field => { "s_pid" => "%{[message][19]}" }
    add_field => { "s_file_path" => "%{[message][20]}" }
    add_field => { "sc_status" => "%{[message][21]}" }
    add_field => { "sc_bytes" => "%{[message][22]}" }
    add_field => { "sc_body_bytes" => "%{[message][23]}" }
    add_field => { "r_ip" => "%{[message][24]}" }
  }

  # 使用date插件,用于从字符串中提取日期和时间,并将其转换为 Logstash 事件的时间戳
  date {
    # 指定日志中的时间信息所采用的时区
    timezone => "GMT"
    # 使用match指定原始数据中日期时间的格式:日/月/年:小时:分钟:秒 时区
    match => ["cs_date_time_temp", "[dd/MMM/yyyy:HH:mm:ss Z]"]
    # 使用target将解析后的日期时间存储在名为"cs_date_time"的字段中
    target => "cs_date_time"
  }

  # 使用ruby插件提取时间戳,然后将其转换为特定格式的日期时间,并将其各个组成部分存储到新的时间字段中
  ruby {
    code => "
      # 加载ruby的时间处理库
      require 'time'
      # 获取'cs_date_time'字段的值,并将其转换为字符串形式,存储在变量 timestamp 中
      timestamp = event.get('cs_date_time').to_s
      # 将时间戳转换为Time对象,进行时区调整(原始时间戳是 GMT 时间,因此需要添加 8 小时(86060 秒)来转换为中国标准时间(CST))
      time = Time.parse(timestamp)+8*60*60
      # 通过Time对象的方法获取年、月、日、时、分、秒、星期几和年内第几天的信息,并分别存储在变量中
      year = time.year
      month = time.month
      day = time.day
      hour = time.hour
      minute = time.min
      second = time.sec
      weekday = time.wday
      yearday = time.yday
      # 通过event.set方法将获取到的各个时间组成部分存储到date_time_arr的新字段中
      event.set('[date_time_arr][year]', year)
      event.set('[date_time_arr][month]', month)
      event.set('[date_time_arr][day]', day)
      event.set('[date_time_arr][hour]', hour)
      event.set('[date_time_arr][min]', minute)
      event.set('[date_time_arr][sec]', second)
      event.set('[date_time_arr][wday]', weekday)
      event.set('[date_time_arr][yday]', yearday)
    "
  }  

  # 使用geoip插件,用于从 IP 地址中解析地理位置信息,并将解析结果存储到指定的字段中
  geoip {
    # 指定提取IP地址的字段名称为"c_ip"
    source => "c_ip"
    # 使用GeoIP数据库解析IP地址的地理位置信息
    database => "/usr/share/logstash/vendor/bundle/jruby/3.1.0/gems/logstash-filter-geoip-7.3.1-java/vendor/GeoLite2-City.mmdb"
    # 指定要从GeoIP解析结果中提取的字段列表,并将其存储到事件中
    fields => ["ip","city_name","country_name","continent_code","continent_name","country_code2","country_code3","postal_code","region_name","region_code","region_iso_code","timezone","location","latitude","longitude","domain","isp","dma_code","organization"]        
    # 指定存储解析结果的目标字段名称为'c_ip_geo',并且解析结果将以一个嵌套的哈希结构存储在该字段中
    target => "c_ip_geo"
  }
 # 使用mutate插件中的rename功能,将存储到'c_ip_geo'中的地理位置字段重命名为定义的字段
 mutate {
    rename => { "[c_ip_geo][geo][city_name]" => "[c_ip_geo][city_name]" }
    rename => { "[c_ip_geo][geo][continent_code]" => "[c_ip_geo][continent_code]" }
    rename => { "[c_ip_geo][geo][continent_name]" => "[c_ip_geo][continent_name]" }
    rename => { "[c_ip_geo][geo][country_iso_code]" => "[c_ip_geo][country_iso_code]" }
    rename => { "[c_ip_geo][geo][country_name]" => "[c_ip_geo][country_name]" }
    rename => { "[c_ip_geo][geo][location][lat]" => "[c_ip_geo][location][lat]" }
    rename => { "[c_ip_geo][geo][location][lon]" => "[c_ip_geo][location][lon]" }
    rename => { "[c_ip_geo][geo][postal_code]" => "[c_ip_geo][postal_code]" }
    rename => { "[c_ip_geo][geo][region_iso_code]" => "[c_ip_geo][region_iso_code]" }
    rename => { "[c_ip_geo][geo][region_code]" => "[c_ip_geo][region_code]" }
    rename => { "[c_ip_geo][geo][region_name]" => "[c_ip_geo][region_name]" }
    rename => { "[c_ip_geo][geo][timezone]" => "[c_ip_geo][timezone]" }
    rename => { "[c_ip_geo][mmdb][dma_code]" => "[c_ip_geo][dma_code]" }
  } 
  # 同上述过滤方法一致
  geoip {
    source => "r_ip"
    database => "/usr/share/logstash/vendor/bundle/jruby/3.1.0/gems/logstash-filter-geoip-7.3.1-java/vendor/GeoLite2-City.mmdb"
    fields => ["ip","city_name","country_name","continent_code","continent_name","country_code2","country_code3","postal_code","region_name","region_code","region_iso_code","timezone","location","latitude","longitude","domain","isp","dma_code","organization"]
    target => "r_ip_geo"
  }
  mutate {
    rename => { "[r_ip_geo][geo][city_name]" => "[r_ip_geo][city_name]" }
    rename => { "[r_ip_geo][geo][continent_code]" => "[r_ip_geo][continent_code]" }
    rename => { "[r_ip_geo][geo][continent_name]" => "[r_ip_geo][continent_name]" }
    rename => { "[r_ip_geo][geo][country_iso_code]" => "[r_ip_geo][country_iso_code]" }
    rename => { "[r_ip_geo][geo][country_name]" => "[r_ip_geo][country_name]" }
    rename => { "[r_ip_geo][geo][location][lat]" => "[r_ip_geo][location][lat]" }
    rename => { "[r_ip_geo][geo][location][lon]" => "[r_ip_geo][location][lon]" }
    rename => { "[r_ip_geo][geo][postal_code]" => "[r_ip_geo][postal_code]" }
    rename => { "[r_ip_geo][geo][region_iso_code]" => "[r_ip_geo][region_iso_code]" }
    rename => { "[r_ip_geo][geo][region_code]" => "[r_ip_geo][region_code]" }
    rename => { "[r_ip_geo][geo][region_name]" => "[r_ip_geo][region_name]" }
    rename => { "[r_ip_geo][geo][timezone]" => "[r_ip_geo][timezone]" }
    rename => { "[r_ip_geo][mmdb][dma_code]" => "[r_ip_geo][dma_code]" }
  } 

  # 使用useragent插件,从指定字段中解析用户代理字符串,并将解析后的结果存储在新的字段中
  # 用户代理字符串通常包含了关于用户使用的浏览器、操作系统等信息
  useragent {
    # 指定字段'cs_user_agent'
    source => 'cs_user_agent'
    # 指定解析后的结果存储在新的字段 ua 中
    target => "ua"
  }
  # 使用mutate插件中的rename功能,将存储到'ua'中的字段用户代理字段重命名为定义的字段
  mutate {
    # 用户代理信息中的版本信息字段重命名
    rename => { "[ua][version]" => "[cs_user_agent_version]" }
    # 操作系统全名字段重命名
    rename => { "[ua][os][full]" => "[cs_user_agent_osname]" }
    # 用户代理名称字段重命名
    rename => { "[ua][name]" => "[cs_user_agent_name]" }
    # 使用convert将字段'cs_user_agent'的类型转换为字符串类型
    convert => {
      "cs_user_agent" => "string"
    }
  }

  # 使用ruby插件,执行logstash-apache.rb脚本,用来进行操作系统、浏览器渲染引擎、搜索引擎的统计,并进行来访IP地址类型判断
  ruby {
    # 将脚本通过docker cp命令复制到指定目录下
    path => "/usr/share/logstash/config/logstash-apache.rb"
  }
  # 使用mutate插件中的rename功能,将存储到'ua'中的字段重命名为定义的字段
  mutate {
    rename => { "[ua_sb]" => "[cs_user_agent_device]" }
    rename => { "[ua_so]" => "[cs_referer_source]" }
    rename => { "[ua_se]" => "[cs_referer_search]" }
  }  
  # 使用mutate插件中的convert功能,将字段的类型转换为定义的类型
  mutate{
    convert => {"c_port" => "integer"}
    convert => {"cs_time_taken" => "integer"}  
    convert => {"cs_bytes" => "integer"} 
    convert => {"cs_keep_alive" => "integer"} 
    convert => {"s_port" => "integer"} 
    convert => {"s_pid" => "integer"}
    convert => {"sc_bytes" => "integer"} 
    convert => {"sc_body_bytes" => "integer"} 
    remove_field => [ "cs_date_time_temp" ]        
  }
 }
}
------------------------------------------------

(3)创建Ruby脚本文件/data/docker/volumes/labs-elk-logstash03-config/_data/logstash-apache.rb,参考命令如下。

[root@Labs-ELK-VM-101 ~]# vi /data/docker/volumes/labs-elk-logstash03-config/_data/logstash-apache.rb
----------------------logstash-apache.rb--------------------
# the value of `params` is the value of the hash passed to `script_params`
# in the logstash configuration
#用于注册插件并初始化其配置参数
def register(params)

end

# the filter method receives an event and must return a list of events.
# Dropping an event means not including it in the return array,
# while creating new ones only requires you to add a new instance of
# LogStash::Event to the returned array
# 用于处理事件数据
def filter(event)
                # 用于根据用户代理字符串来判断设备类型,并将结果存储在事件中的一个新字段 ua_sb 中
                # 操作系统统计
                # 从事件中获取 cs_user_agent 字段的值,并将其转换为小写字母,以便后续的比较操作不区分大小写
                user_agent=event.get('cs_user_agent').downcase
                # 使用 index 方法检查用户代理字符串是否包含特定的子字符串
                if(user_agent.index('android')||user_agent.index('iphone')||user_agent.index('ipad')||user_agent.index('windows phone'))
                        event.set('ua_sb', 'mobile')
                elsif(user_agent.index('windows nt 5.0')||user_agent.index('windows nt 5.1')||user_agent.index('windows nt 5.2')||user_agent.index('windows nt 6.0')||user_agent.index('windows nt 6.1')||user_agent.index('windows nt 6.2')||user_agent.index('windows nt 6.3')||user_agent.index('windows nt')||user_agent.index('windows nt 10.0')||user_agent.index('mac')||user_agent.index('linux'))
                        event.set('ua_sb', 'pc')
                end

                # 使用index方法检查用户代理字符串是否包含特定的子字符串              
                # 浏览器渲染引擎
                if user_agent.index('trident') !=nil
                        event.set('cs_user_agent_engine', 'Trident')
                elsif user_agent.index('gecko')!=nil
                        event.set('cs_user_agent_engine', 'Gecko')
                elsif user_agent.index('presto')!=nil
                        event.set('cs_user_agent_engine', 'Presto')
                elsif user_agent.index('webkit')!=nil
                        event.set('cs_user_agent_engine', 'Webkit')
                end

                # 从事件中获取 cs_referer 字段的值,该字段包含了 HTTP 请求的引用来源(即来源网址)
                # 将其转换为小写字母,以便后续的比较操作不区分大小写
                #搜索引擎统计
                http_referer=event.get('cs_referer').downcase
                if http_referer.index('baidu.com')!=nil
                        event.set('ua_se', '百度')
                        event.set('ua_so', 'search')
                end
                if http_referer.index('google.com') !=nil
                        event.set('ua_se', 'Google')
                        event.set('ua_so', 'search')
                end
                if user_agent.index('baiduspider') !=nil
                        event.set('ua_se', '百度')
                        event.set('ua_so', 'search')
                end
                if user_agent.index('sogou+web+spider') !=nil
                        event.set('ua_se', '搜狗')
                        event.set('ua_so', 'search')
                end
                if http_referer.index('bing.com') !=nil
                        event.set('ua_se', 'Bing')
                        event.set('ua_so', 'search')
                end
                if http_referer.index('bingbot') !=nil
                        event.set('ua_se', 'Bing')
                        event.set('ua_so', 'search')
                end
                if http_referer.index('sogou.com') !=nil
                        event.set('ua_se', '搜狗')
                        event.set('ua_so', 'search')
                end
                if http_referer.index('yahoo.com') !=nil
                        event.set('ua_se', 'Yahoo')
                        event.set('ua_so', 'search')
                end
                if http_referer.index('youdao.com') !=nil
                        event.set('ua_se', '有道')
                        event.set('ua_so', 'search')
                end
                if http_referer.index('so.com') !=nil
                        event.set('ua_se', '360搜索')
                        event.set('ua_so', 'search')
                end
                if http_referer.index('163.com') !=nil
                        event.set('ua_se', '网易')
                        event.set('ua_so', 'search')
                end
                if http_referer.index('toutiao.com') !=nil
                        event.set('ua_se', '头条')
                        event.set('ua_so', 'search')
                end
                if http_referer.index('quark.sm.cn') !=nil
                        event.set('ua_se', '夸克')
                        event.set('ua_so', 'search')
                end
                if http_referer.index('soso.com') !=nil
                        event.set('ua_se', '搜搜')
                        event.set('ua_so', 'search')
                end
                if http_referer.index('semrush') !=nil
                        event.set('ua_se', 'SEMRush')
                        event.set('ua_so', 'search')
                end
                if http_referer.index('zznu.edu.cn') !=nil
                        event.set('ua_se', '-')
                        event.set('ua_so', 'direct')
                end
                if http_referer=="-"
                        event.set('ua_se', '-')
                        event.set('ua_so', 'direct')
                end
                if http_referer==""
                        event.set('ua_se', '-')
                        event.set('ua_so', 'other')
                end

                #来访IP地址类型判断
                remote_addr=event.get("c_ip")
                #使用获取到的IP地址创建一个 IPAddr 对象
                remote_addr_info=IPAddr.new remote_addr
                #将IP地址转换为整数表示
                remote_addr_num=remote_addr_info.to_i
                #用于判断A类私有IP地址段
                #将IP地址段的起始和结束IP地址都转换为整数表示
                a_b_ip=IPAddr.new "10.0.0.0"
                a_e_ip=IPAddr.new "10.255.255.255"
                a_b_num=a_b_ip.to_i
                a_e_num=a_e_ip.to_i
                #用于判断C类私有IP地址段
                c_b_ip=IPAddr.new "192.168.0.0"
                c_e_ip=IPAddr.new "192.168.255.255"
                c_b_num=c_b_ip.to_i
                c_e_num=c_e_ip.to_i
                #用于判断D类私有IP地址段
                d_b_ip=IPAddr.new "172.16.0.0"
                d_e_ip=IPAddr.new "172.31.255.255"
                d_b_num=d_b_ip.to_i
                d_e_num=d_e_ip.to_i
                #条件判断句判断访问事件的IP地址类型,并将结果存储在事件的字段中
                if((remote_addr_num >= a_b_num && remote_addr_num < a_e_num)||(remote_addr_num >= c_b_num && remote_addr_num < c_e_num)||(remote_addr_num >= d_b_num && remote_addr_num < d_e_num))
                        event.set('c_ip_type', 'private')
                else
                        event.set('c_ip_type', 'public')
                end
        return [event]

end
------------------------------------------------------------

Apache访问日志清洗字段标准如表4-1所示。

表4-1 Apache访问日志字段清洗标准

日志配置项日志字段字段清洗结果字段说明
%ac_ipc_ip来访地址
c_ip_geo.city_name来访地址-城市
c_ip_geo.continent_code来访地址-州代码
c_ip_geo.continent_name来访地址-州
c_ip_geo.country_iso_code来访地址-国家代码(ISO)
c_ip_geo.country_name来访地址-国家名称
c_ip_geo.dma_code来访地址-DMA Code
c_ip_geo.location.lat来访地址-坐标-维度
c_ip_geo.location.lon来访地址-坐标-经度
c_ip_geo.postal_code来访地址-邮编
c_ip_geo.region_iso_code来访地址-地区代码(ISO)
c_ip_geo.region_code来访地址-地区代码
c_ip_geo.region_name来访地址-地区名称
c_ip_geo.timezone来访地址-时区
c_ip_type来访地址-类型
c_ip_private_location来访地址-私有-地理位置
%hc_hostc_host来访主机名
%{remote}pc_portc_port来访端口
%uc_userc_user来访用户
\”%{Referer}i\”cs_referercs_referer来源地址
cs_referer_source访问来源
cs_referer_search来源搜索引擎
\”%{User-Agent}i\”cs_user_agentcs_user_agentUA
cs_user_agent_nameUA-浏览器名称
cs_user_agent_versionUA-浏览器版本号
cs_user_agent_engineUA-浏览器渲染引擎
cs_user_agent_osnameUA-操作系统-名称
cs_user_agent_deviceUA-设备类型
%Ccs_cookiecs_cookieCookie
%tcs_date_timecs_date_time请求时间
date_time_arr.year请求时间-年
date_time_arr.month请求时间-月
date_time_arr.day请求时间-日
date_time_arr.hour请求时间-时
date_time_arr.min请求时间-分
date_time_arr.sec请求时间-秒
date_time_arr.yday请求时间-一年中的第几天
date_time_arr.wday请求时间-周几
date_time_arr.time_zone请求时间-时区
%Hcs_protocolcs_protocol请求协议
%mcs_methodcs_method请求方法
%Dcs_time_takencs_time_taken请求耗时
%Ucs_uri_stemcs_uri_stem请求地址(不包括query_string)
%qcs_uri_querycs_uri_query请求参数
cs_uri_query_arr请求参数数组
%Ics_bytescs_bytes接收字节数
%kcs_keep_alivecs_keep_aliveKeepAlive
%Xcs_conn_statuscs_conn_status连接状态
%vs_servernames_servername服务主机名
%As_ips_ip服务地址
%{local}ps_ports_port服务端口
%Ps_pids_pid进程ID
%fs_file_paths_file_path请求文件地址
%>ssc_statussc_status响应状态
%Osc_bytessc_bytes发送字节数
%bsc_body_bytessc_body_bytes发送内容字节数(不包括Header)
%{c}ar_ipr_ip代理来访地址
r_ip_geo.city_name来访地址-城市
r_ip_geo.continent_code来访地址-州代码
r_ip_geo.continent_name来访地址-州
r_ip_geo.country_iso_code来访地址-国家代码(ISO)
r_ip_geo.country_name来访地址-国家名称
r_ip_geo.dma_code来访地址-DMA Code
r_ip_geo.location.lat来访地址-坐标-维度
r_ip_geo.location.lon来访地址-坐标-经度
r_ip_geo.postal_code来访地址-邮编
r_ip_geo.region_iso_code来访地址-地区代码(ISO)
r_ip_geo.region_code来访地址-地区代码
r_ip_geo.region_name来访地址-地区名称
r_ip_geo.timezone来访地址-时区
r_ip_type来访地址-类型
r_ip_private_location来访地址-私有-地理位置

(4)在主机Labs-ELK-VM-101上修改容器labs-elk-logstash03的配置文件,配置输出模块为Elasticsearch,参考命令如下。

#在“过滤”下面添加以下过滤内容
#修改Logstash配置文件/data/docker/volumes/labs-elk-logstash03-pipeline/_data/logstash.conf
[root@Labs-ELK-VM-101 ~]# vi /data/docker/volumes/labs-elk-logstash03-pipeline/_data/logstash.conf
----------------------logstash.conf--------------------
# 定义输出插件的配置
output {
  # 判断type 字段是否等于'labs-elk-apache'
  if [type] == "labs-elk-apache" {
    # 将清洗后的日志输出到标准输出(控制台),方便调试和测试
    stdout {codec => rubydebug}
    # 定义输出插件Elasticsearch
    elasticsearch {
      # 指定Elasticsearch的主机地址和端口号,Logstash将把日志事件发送到这个地址
      hosts => ["https://labs-elk-es-node-1:9200"]
      # 指定要索引到Elasticsearch中的索引名称
      index => "labs-elk-apache-%{+YYYY.MM.dd}"
      # 指定连接 Elasticsearch 集群所使用的用户名
      user => "elastic"
      # 指定连接 Elasticsearch 集群所使用的密码
      password => "Labs#ELK@2025"
      # 指定Logstash用于与Elasticsearch进行安全连接时的CA证书的路径
      ssl_certificate_authorities => ["/usr/share/logstash/config/certs/ca/ca.crt"]
    }
  }
}
----------------------logstash.conf--------------------

(5)在主机Labs-ELK-VM-101上,重启容器labs-elk-logstash-3,通过docker logs命令查看日志的推送情况,验证完成后修改logstash配置文件,禁用输出到控制台,并重启容器labs-elk-logstash-3,参考命令如下。

#Logstash配置文件修改完成后,重启Logstash容器,使配置生效
[root@Labs-ELK-VM-101 ~]# docker restart labs-elk-logstash-3
#等待Logstash启动后,访问网站Wordpress生成访问日志,通过控制台实时查看Apache访问日志
[root@Labs-ELK-VM-101 ~]# docker logs labs-elk-logstash-3 --tail=100 -f
#此处省略部分日志内容
{
            "cs_user_agent" => ""Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (                                                      KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36 Edg/138.0.0.0"",
                     "tags" => [
        [0] "beats_input_codec_plain_applied",
        [1] "_geoip_lookup_failure"
    ],
                      "ecs" => {
        "version" => "8.0.0"
    },
                "sc_status" => "200",
              "s_file_path" => "proxy:fcgi://127.0.0.1:9000/var/www/html/wordpress/index.php",
                "c_ip_type" => "private",
                    "s_pid" => 204938,
     "cs_user_agent_device" => "pc",
     "cs_user_agent_osname" => "Windows 10",
       "cs_user_agent_name" => "Edge",
                     "r_ip" => "10.10.0.1",
             "s_servername" => "localhost",
            "sc_body_bytes" => 50639,
                 "cs_bytes" => 476,
             "cs_uri_query" => "",
                       "ua" => {
            "os" => {
               "name" => "Windows",
            "version" => "10"
        },
        "device" => {
            "name" => "Other"
        }
    },
                   "s_port" => 80,
            "cs_time_taken" => 96071,
                  "message" => [
        [ 0] "10.10.0.1",
        [ 1] "10.10.0.1",
        [ 2] "62463",
        [ 3] "-",
        [ 4] ""-"",
        [ 5] ""Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko)                                                       Chrome/138.0.0.0 Safari/537.36 Edg/138.0.0.0"",
        [ 6] "-",
        [ 7] "[01/Aug/2025:22:38:25 +0800]",
        [ 8] "HTTP/1.1",
        [ 9] "GET",
        [10] "96071",
        [11] "/wordpress/index.php",
        [12] "",
        [13] "476",
        [14] "0",
        [15] "+",
        [16] "localhost",
        [17] "10.10.2.102",
        [18] "80",
        [19] "204938",
        [20] "proxy:fcgi://127.0.0.1:9000/var/www/html/wordpress/index.php",
        [21] "200",
        [22] "51025",
        [23] "50639",
        [24] "10.10.0.1"
    ],
                   "c_port" => 62463,
                "cs_cookie" => "-",
                    "event" => {
        "original" => "10.10.0.1 | 10.10.0.1 | 62463 | - | "-" | "Mozilla/5.0 (Windows NT 10.                                                      0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36 Edg/138.0.0                                                      .0" | - | [01/Aug/2025:22:38:25 +0800] | HTTP/1.1 | GET | 96071 | /wordpress/index.php |  | 476                                                       | 0 | + | localhost | 10.10.2.102 | 80 | 204938 | proxy:fcgi://127.0.0.1:9000/var/www/html/word                                                      press/index.php | 200 | 51025 | 50639 | 10.10.0.1"
    },
                "cs_method" => "GET",
           "cs_conn_status" => "+",
    "cs_user_agent_version" => "138.0.0.0",
                 "sc_bytes" => 51025,
                     "c_ip" => "10.10.0.1",
     "cs_user_agent_engine" => "Gecko",
             "cs_date_time" => 2025-08-01T14:38:25.000Z,
                 "r_ip_geo" => {},
                    "input" => {
        "type" => "log"
    },
              "cs_protocol" => "HTTP/1.1",
               "cs_referer" => ""-"",
                     "host" => {
        "name" => "Labs-ELK-VM-102"
    },
                    "agent" => {
                "name" => "Labs-ELK-VM-102",
        "ephemeral_id" => "f63ec0b9-a25e-49f8-b732-0438220a4971",
                "type" => "filebeat",
                  "id" => "8f30e8ee-8865-42cb-9bf8-22a46e136757",
             "version" => "9.0.4"
    },
                     "type" => "labs-elk-apache",
            "cs_keep_alive" => 0,
                      "log" => {
        "offset" => 6755,
          "file" => {
            "path" => "/var/log/httpd/logs/access_log"
        }
    },
                   "c_host" => "10.10.0.1",
                 "c_ip_geo" => {},
                 "@version" => "1",
              "cs_uri_stem" => "/wordpress/index.php",
                     "s_ip" => "10.10.2.102",
               "@timestamp" => 2025-08-01T14:38:33.268Z,
                   "c_user" => "-",
            "date_time_arr" => {
         "year" => 2025,
          "day" => 1,
         "wday" => 5,
         "yday" => 213,
          "min" => 38,
          "sec" => 25,
         "hour" => 22,
        "month" => 8
    }
}

#验证完成后将容器日志的输出关闭
[root@Labs-ELK-VM-101 ~]# vi /data/docker/volumes/labs-elk-logstash03-pipeline/_data/logstash.conf
----------------------logstash--------------------
#此处省略其他配置内容
  # stdout {codec => rubydebug}
--------------------------------------------------

# 重启logstash容器
[root@Labs-ELK-VM-101 ~]# docker restart labs-elk-logstash-3

6、使用Kibana检索日志

(1)使用浏览器访问Kibana地址http://10.10.2.101:5601,输入用户和密码(用户名:elastic,密码:Labs#ELK@2025)登录Kibana,如图4-1所示。

(2)进入系统界面中,单击左上角的三条横线,导航到左侧菜单,下滑选择“Management”选项卡中“Stack Management”选项,如图4-2所示。

(3)在“Stack Management”界面,选择“Kibana”选项卡中“Data Views”选项,创建和管理数据视图,如图4-3所示。

(4)单击【Data Views】,在数据视图界面中单击【Create data view】,创建Linux Syslog日志的数据视图。如图4-4所示。

(5)输入视图名称为“labs-elk-apache”,索引模式为“labs-elk-apache-*”,Timestamp字段选择“cs_date_time”,单击【Save data view to Kibana】,保存视图配置,如图4-5、4-6所示。

(6)单击左上角导航栏,选择“Analytics”选项卡中“Discover”选项,进入日志分析视图界面。在左上角选择“labs-elk-apache”的数据视图,单击【Search entire time range】检索查看Apache访问日志,如图4-7所示。

(7)将鼠标移至左侧字段列表,单击左侧可用字段中【+】,可对Apache访问日志中的字段日志进行检索查看,如图4-8所示。

7、Apache服务日志可视化分析

(1)通过Apache访问日志字段清洗标准建立日志分析模型,实现Apache日志可视化分析。Apache日志分析模型如表4-2所示。

分类分析模型图表类型分析模型用途
数据概览请求量数字展示请求总量
发送字节量数字展示发送字节总量
接收字节量数字展示接收字节总量
访客量数字展示来访IP个数
请求量趋势折线图展示请求量、来访IP数随时间的变化趋势
网站请求次数统计柱状图展示每天的请求量
网站请求量排行表格展示请求最多的网站,每个网站的请求次数
搜索引擎请求排行表格展示每种搜索引擎请求的次数
操作系统请求排行表格展示每种操作系统请求次数
网站响应状态排行柱状图展示每种网站响应状态次数
请求类型排行柱状图展示每种请求类型次数
访问分析一天中每小时的请求量统计柱状图展示每天每个时间段的请求量
请求量对比统计折线图展示目前与前一天请求量的对比分析
请求协议统计饼状图展示每种请求协议的占比
请求协议趋势统计折线图展示每种请求协议实时变化(X-时间,Y-次数)(多条线)
访客分析操作系统统计饼状图展示每种操作系统的占比
操作系统趋势统计折线图展示每种操作系统请求量的实时变化
操作系统请求详情统计表格展示操作系统名称、请求次数、独立IP数,体现出常用的操作系统
设备类型分析饼状图展示设备类型请求占比
设备类型趋势折线图展示设备类型时间请求趋势
设备类型表格展示表格展示设备类型名称、请求次数、独立IP数
网站分析异常请求站点排行柱状图展示异常请求的网站排行
异常请求时间趋势折线图展示异常请求随时间的变化趋势

表4-2 Apache日志分析模型

(2)通过Kibana平台的可视化图表功能,绘制上述日志分析模型,如图4-9所示。