使用Fluentd收集容器内PHP-FPM下Monolog的日志


K8S回归Docker Compose

接上回说到,从K8S回归到Docker Compose,有一块调整的比较大,就是日志。原来有点懒惰,腾讯云的TKE集群可以默认接入他的LogListener,这个日志组件可以很方便从Container里取日志,方便到你根本不用改造原来的应用。但现在由于离开了K8S,还是要提升下收集日志的效率,所以所有容器内的应用都会由init进程管理输出到stdout或stderr,再由容器接住,新版本docker已经支持--log-driver=fluentd来指定一个fluentd的后端来接受所有日志。

日志规划流向

应用日志

Monolog写php://stderr → PHP-FPM接到error_log → init进程Supervisor接到/dev/stderr → Container接到日志 → Docker daemon设置了log-driver → 写到Fluentd → Fluentd处理后通过Kafka协议发到腾讯云CLS日志处理

访问日志

因为Traefik不支持FastCGI协议,所以FPM前面是挡了一层Nginx,访问日志都从Nginx取,类似应用日志,不同的是访问日志都写了stdout,这其实并没有关系,因为最后在Fluentd处理的时候还是会用正则匹配的

STEP0: NGINX ACCESS_LOG 访问日志

在nginx.conf的http段中配置JSON格式的字段,根据你的需要来,我建议加入request_id,方便关联应用日志。

http {
  log_format mylog escape=json '{"time_local":"$time_iso8601",'
                           '"request_id":"$request_id",'
                           '"host":"$server_addr",'
                           '"clientip":"$remote_addr",'
                           '"size":$body_bytes_sent,'
                           '"request":"$request",'
                           '"request_body":"$request_body",'
                           '"responsetime":$request_time,'
                           '"upstreamtime":"$upstream_response_time",'
                           '"http_host":"$host",'
                           '"url":"$uri",'
                           '"domain":"$host",'
                           '"xff":"$http_x_forwarded_for",'
                           '"referer":"$http_referer",'
                           '"http_user_agent":"$http_user_agent",'
                           '"status":"$status"}';
  include mime.types;
  server_tokens off;
  tcp_nopush on;
  tcp_nodelay on;
  keepalive_timeout 120;
  types_hash_max_size 2048;
  client_max_body_size 100m;
  default_type application/octet-stream;
  access_log off;
  error_log off;
  gzip on;
  gzip_disable "msie6";
  include vhost/*.conf;
}

在具体的虚拟机中就可以配置你设置的json格式access log

server {
    server_name symfony.tld;
    root /data/www/public;

    location / {
        # try to serve file directly, fallback to index.php
        try_files $uri /index.php$is_args$args;
    }

    location ~ ^/index\.php(/|$) {
        fastcgi_pass unix:/dev/shm/php-fpm.sock;
        fastcgi_split_path_info ^(.+\.php)(/.*)$;
        include fastcgi_params;

        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        fastcgi_param DOCUMENT_ROOT $realpath_root;
        fastcgi_param HTTPS off;
        fastcgi_param REQUEST_ID $request_id;

        internal;
    }

    # return 404 for all other php files not matching the front controller
    # this prevents access to other php files you don't want to be accessible.
    location ~ \.php$ {
        return 404;
    }

    error_log stderr;
    access_log /dev/stdout mylog;
}

STEP1: 改造APP LOG

App的日志一般都会用主流的包可以选择,在这个例子中,我用的是Symfony原配的Monolog,这里可以根据自己的需要进行拦截。例如访问量小的时候,你可以把所有用户侧的异常抓下来看看,所以app这个pool里,level是info的我都抓了。

在你的异常拦截里面加上日志,可以从$_SERVER['REQUEST_ID']取得你需要的nginx侧请求ID。

$this->logger->info($exception->getMessage(),[你需要的其他数据]);

mololog的配置

monolog:
    handlers:
        app:
            level: info
            type: stream
            path: "php://stderr"
            channels: [ app ]
            formatter: 'monolog.formatter.json'
        main:
            type: fingers_crossed
            action_level: error
            handler: nested
            excluded_404s:
                # regex: exclude all 404 errors from the logs
                - ^/
        nested:
            type: stream
            path: "php://stderr"
            formatter: 'monolog.formatter.json'
        console:
            type:   console
            process_psr_3_messages: false
            channels: ["!event", "!doctrine"]

STEP2: 给Supervisor加上参数

记得给启动命令加上参数--force-stderr --nodaemonize,否则PHP-FPM的/proc/pid/fd/2不是pipe,参考之前文章PHP-FPM在Docker没有日志输出到/dev/stder

[program:php-fpm]
command=/usr/local/sbin/php-fpm --force-stderr --nodaemonize --fpm-config /usr/local/etc/php-fpm.d/www.conf
autostart=true
autorestart=true
priority=5
stdout_events_enabled=true
stderr_events_enabled=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
stopsignal=QUIT

STEP3: 修改容器日志输出驱动格式

普通容器可以在run的时候加上--log-driver=fluentd,如果是compose可以在配置里加上logging字段。

version: '3'

services:
  delta:
    image: ccr.ccs.tencentyun.com/zhouzhou/phpallinone:allinone-master-20220331
    ports:
      - "80:80"
    logging:
      driver: "fluentd"
      options:
        fluentd-address: localhost:24224
        tag: delta
networks:
  default:
    driver: bridge

STEP4: 配置Fluentd

Fluentd可以做很多事情,当然其他日志收集器也可以,但Docker内置了Fluentd驱动,就用了它。我使用的场景很简单,单纯的在宿主机进行收集,简单的格式化处理,由于日质体量很小,所以发送到腾讯云CLS来处理。

发送到腾讯云CLS需要Kafka协议,所以我们要做一个自己的Fluentd镜像,加上了kafka协议和兼容腾讯侧的rdkafka库,请注意,github官网的debian镜像是有问题的,例子用的是Alpine版本。

FROM fluentd:v1.14.0-1.0

# Use root account to use apk
USER root

# below RUN includes plugin as examples elasticsearch is not required
# you may customize including plugins as you wish
# 安装rdkafka和kafka插件
RUN apk add --no-cache --update --virtual .build-deps \
        sudo build-base ruby-dev bash\
 && sudo gem install rdkafka  \
 && sudo gem install fluent-plugin-kafka \
 && sudo gem sources --clear-all \
 && apk del .build-deps \
 && rm -rf /tmp/* /var/tmp/* /usr/lib/ruby/gems/*/cache/*.gem

USER fluent

# 覆盖一下
ENTRYPOINT ["tini",  "--", "/bin/entrypoint.sh"]
CMD ["fluentd"]

配置这里说下思路,Fluentd的配置顺序简单来说是由上自下,并没有很强大的语法,利用本身的插件来完成一些其实本身很简单的操作,所以有点反直觉。我的容器中目前有访问日志和应用日志两类,我需要根据各自的特征发送到各自在腾讯云的主题进行分析。

<source>
  @type forward
  port 24224
  bind 0.0.0.0
</source>

<match *>
  @type copy
  <store>
    @type relabel
    @label @PHPERROR
  </store>
  <store>
    @type relabel
    @label @NGINXACCESS
  </store>
</match>

<label @PHPERROR>
  <filter delta>
    @type grep
    <regexp>
      key log
      pattern \[pool ([^\]]*)\]
    </regexp>
  </filter>

  # 解析原始的PHP-FPM日志
  <filter delta>
    @type parser
    key_name log
    reserve_data true
    # reserve_data true
    # hash_value_field parsed
    <parse>
      @type regexp
      expression /^\[(?<logtime>[^\]]*)\] (?<level>(DEBUG|INFO|NOTICE|WARNING|ERROR|CRITICAL|ALERT|EMERGENCY)): \[pool (?<pool>[^\]]*)\] child (?<child>\d+) said into stderr: \"(?<message>.+)\"$/
    </parse>
  </filter>

  # 转json
  <filter delta>
    @type parser
    reserve_data true
    key_name message
    <parse>
      @type json
      time_key logtime
    </parse>
  </filter>

  <match delta>
    @type rdkafka2

    brokers sh-producer.cls.tencentcs.com:9096
    use_event_time true

    <format>
      @type json
    </format>

    <buffer topic>
      @type memory
      flush_interval 3s
    </buffer>

    # 腾讯云日志主题
    default_topic 9d7fc234-e090-4a48-8888-642f5b56e82c

    # producer settings
    required_acks -1
    rdkafka_delivery_handle_poll_timeout 5

    rdkafka_options {
      "security.protocol" : "SASL_PLAINTEXT",
      "sasl.mechanism" : "PLAIN",
      # 日志集主题
      "sasl.username" : "e63de52e-a012-45f6-8888-ca2dab7aeda1",
      "sasl.password" : "SecurityId#SecurityKey"
    }
  </match>

</label>

<label @NGINXACCESS>
  <filter delta>
    @type grep
    <regexp>
      key source
      pattern stdout
    </regexp>
  </filter>

  <filter delta>
    @type parser
    key_name log
    <parse>
      @type json
      # 指定nginx原始日志的事件作为事件事件
      time_key time_local
    </parse>
  </filter>

  <match delta>
    @type rdkafka2

    brokers sh-producer.cls.tencentcs.com:9096
    use_event_time true

    <format>
      @type json
    </format>

    <buffer topic>
      @type memory
      flush_interval 3s
    </buffer>

    # 腾讯云日志主题
    default_topic 0c0872c5-00b5-8888-b658-e88769118c30

    # producer settings
    required_acks -1
    rdkafka_delivery_handle_poll_timeout 5

    rdkafka_options {
      "security.protocol" : "SASL_PLAINTEXT",
      "sasl.mechanism" : "PLAIN",
      # 日志集主题
      "sasl.username" : "e63de52e-a012-8888-8932-ca2dab7aeda1",
      "sasl.password" : "SecurityId#SecurityKey"
    }
  </match>
</label>

以上配置简单的把单容器所有日志输出接住,先用relabel这个插件把日志复制出两股Label进行分别处理,这里有点反直觉。然后在不同的Label下进行json处理和投递工作,注意的是,PHP-FPM的Error LOG是不能定义格式的,所以需要正则匹配,然后把message部分进行json处理,注意使用reserve_data = true防止之前的key被覆写。最后记得腾讯云目前的Kafka环境必须用rdkafka库,SASL_PLAINTEXT密码的配置请注意!

全部搞定

请注意的时候,腾讯云的kafka可以走内网,域名和端口和外网的不同。你也可以自己做Elasticsearch和Kibana,组你自己的EFK


发表回复

您的电子邮箱地址不会被公开。