【问题标题】:gunicorn access log formatgunicorn 访问日志格式
【发布时间】:2018-05-21 13:11:26
【问题描述】:

我打算在 kubernetes 上通过 gunicorn 运行烧瓶。为了正确记录日志,我想以 json 格式输出所有日志。

目前我正在使用 minikube 和 https://github.com/inovex/kubernetes-logging 进行测试,以流畅地收集日志。

由于以下原因,我设法正确格式化了错误日志(回溯): JSON formatted logging with Flask and gunicorn

我仍在为访问日志格式而苦恼。 我指定了以下 gunicorn 访问日志格式:

access_log_format = '{"remote_ip":"%(h)s","request_id":"%({X-Request-Id}i)s","response_code":"%(s)s","request_method":"%(m)s","request_path":"%(U)s","request_querystring":"%(q)s","request_timetaken":"%(D)s","response_length":"%(B)s"}'

并且生成的日志是 json 格式的。但是消息部分(基于 access_log_format 的格式)现在包含转义的双引号,并且不会被 fluentd / ELK 解析为它的各个字段

{"tags": [], "timestamp": "2017-12-07T11:50:20.362559Z", "level": "INFO", "host": "ubuntu", "path": "/usr/local/lib/python2.7/dist-packages/gunicorn/glogging.py", "message": "{\"remote_ip\":\"127.0.0.1\",\"request_id\":\"-\",\"response_code\":\"200\",\"request_method\":\"GET\",\"request_path\":\"/v1/records\",\"request_querystring\":\"\",\"request_timetaken\":\"19040\",\"response_length\":\"20\"}", "logger": "gunicorn.access"}

谢谢 日文

【问题讨论】:

    标签: python json gunicorn fluentd


    【解决方案1】:

    最简单的解决方法是把外层单引号改成双引号,把内层双引号改成单引号,如下所述。

    --access-logformat  "{'remote_ip':'%(h)s','request_id':'%({X-Request-Id}i)s','response_code':'%(s)s','request_method':'%(m)s','request_path':'%(U)s','request_querystring':'%(q)s','request_timetaken':'%(D)s','response_length':'%(B)s'}"
    

    以下是示例日志

    {'remote_ip':'127.0.0.1','request_id':'-','response_code':'404','request_method':'GET','request_path':'/test','request_querystring':'','request_timetaken':'6642','response_length':'233'}
    {'remote_ip':'127.0.0.1','request_id':'-','response_code':'200','request_method':'GET','request_path':'/','request_querystring':'','request_timetaken':'881','response_length':'20'}
    

    【讨论】:

    • 这个答案可能不被接受,因为单引号不是有效的 JSON。但是,我想指出,我可以通过使用替换轻松地将单引号换成双引号:somestring.replace("'", '"')
    【解决方案2】:

    您可以直接在 --access-logformat 的值中转义双引号 (\"),以使您的日志保持为有效的 JSON。

    因此,如果您在 Docker 容器中运行 Gunicorn,您的 Dockerfile 可能会以如下方式结束:

    CMD ["gunicorn",            \
        "-b", "0.0.0.0:5000",   \
        "--access-logfile", "-",\
        "--access-logformat", "{\"remote_ip\":\"%(h)s\",\"request_id\":\"%({X-Request-Id}i)s\",\"response_code\":\"%(s)s\",\"request_method\":\"%(m)s\",\"request_path\":\"%(U)s\",\"request_querystring\":\"%(q)s\",\"request_timetaken\":\"%(D)s\",\"response_length\":\"%(B)s\"}", \
        "app:create_app()"]
    

    找到其余的 Gunicorn 日志记录选项 here。

    【讨论】:

      【解决方案3】:

      2 年过去了,我假设流利的 python 记录器已经改变,我现在遇到了一个稍微不同的问题,每次 Google 搜索都指向这个讨论。

      在 gunicorn 配置文件中使用您的示例时

      access_log_format = '{"remote_ip":"%(h)s","request_id":"%({X-Request-Id}i)s","response_code":"%(s)s","request_method":"%(m)s","request_path":"%(U)s","request_querystring":"%(q)s","request_timetaken":"%(D)s","response_length":"%(B)s"}'
      

      我得到了将其读取为 json 并将其与流利的 json 数据合并的所需行为,但是未填充 gunicorn 字段

      {"tags": [], "level": "INFO", "host": "ubuntu", "logger": "gunicorn.access", "remote_ip":"%(h)s","request_id":"%({X-Request-Id}i)s","response_code":"%(s)s","request_method":"%(m)s","request_path":"%(U)s","request_querystring":"%(q)s","request_timetaken":"%(D)s","response_length":"%(B)s"}
      

      看起来原因是 Gunicorn 将 access_log_format 作为消息传递给记录器,所有参数 (safe_atoms) 作为附加参数,例如

      /gunicorn/glogging.py

              safe_atoms = self.atoms_wrapper_class(
                  self.atoms(resp, req, environ, request_time)
              )
      
              try:
                  # safe_atoms = {"s": "200", "m": "GET", ...}
                  self.access_log.info(self.cfg.access_log_format, safe_atoms)
      

      但是,如果 FluentRecordFormatter 将字符串视为有效的 json,它将使用 json.loads 读取它,但忽略传递的任何参数

      /fluent/handler.py

          def _format_msg_json(self, record, msg):
              try:
                  json_msg = json.loads(str(msg))  # <------- doesn't merge params
                  if isinstance(json_msg, dict):
                      return json_msg
                  else:
                      return self._format_msg_default(record, msg)
              except ValueError:
                  return self._format_msg_default(record, msg)
      

      将此与调用record.message = record.getMessage() 的default Python formatter 进行比较,后者又将参数合并到

      /Lib/logging/init.py

          def getMessage(self):
              """
              Return the message for this LogRecord.
              Return the message for this LogRecord after merging any user-supplied
              arguments with the message.
              """
              msg = str(self.msg)
              if self.args:
                  msg = msg % self.args  # <------ args get merged in
              return msg
      

      我已经 logged an issue 使用了 fluent-logger-python 项目。

      解决方法

      使用logging filter 在传递给FluentRecordFormatter 之前执行合并。

      logger = logging.getLogger('fluent.test')
      
      class ContextFilter(logging.Filter):
          def filter(self, record):
              record.msg = record.msg % record.args
              return True
      
      fluent_handler = handler.FluentHandler('app.follow', host='localhost', port=24224)
      formatter = handler.FluentRecordFormatter()
      fluent_handler.setFormatter(formatter)
      merge_filter = ContextFilter()
      fluent_handler.addFilter(merge_filter)
      logger.addHandler(fluent_handler)
      

      编辑:日志过滤器不起作用

      在使用日志过滤器的解决方法一段时间后,我开始收到类似的错误

      ValueError: unsupported format character ';' (0x3b) at index 166
      

      事实证明,FluentRecordFormatter 确实调用了基本的getMessage 实现,将参数合并到消息中

          def format(self, record):
              # Compute attributes handled by parent class.
              super(FluentRecordFormatter, self).format(record)  # <------ record.messge = record.msg % record.args
              # Add ours
              record.hostname = self.hostname
      
              # Apply format
              data = self._formatter(record)
      
              self._structuring(data, record)
              return data
      

      问题在于_format_msg_json(self, record, msg) 使用record.msg 属性,这是未合并 数据,而record.message 是合并数据。这会产生一个问题,即我的日志过滤器正在合并/格式化数据,但随后日志格式化程序也试图这样做,并且偶尔会看到无效的语法。

      解决方法 2:不要使用 Json

      我已经完全放弃了从 gunicorn / python 日志中输出 json。相反,我使用 Fluentd 的解析器来解析 json,例如

      <filter *.gunicorn.access>
        @type parser
        key_name message
        reserve_time true
        reserve_data true
        remove_key_name_field true
        hash_value_field access_log
        <parse>
          @type regexp
          expression /^(?<host>[^ ]*) [^ ]* (?<user>[^ ]*) \[(?<time>[^\]]*)\] "(?<method>\S+)(?: +(?<path>[^ ]*) +\S*)?" (?<code>[^ ]*) (?<size>[^ ]*) "(?<referer>[^\"]*)" "(?<agent>[^\"]*)"$/
          time_format %d/%b/%Y:%H:%M:%S %z
        </parse>
      </filter>
      

      您可以在此处了解这些选项的作用:https://docs.fluentd.org/filter/parser

      【讨论】:

        猜你喜欢
        • 2020-11-03
        • 1970-01-01
        • 2018-09-17
        • 1970-01-01
        • 1970-01-01
        • 2015-11-27
        • 1970-01-01
        • 2019-04-14
        • 1970-01-01
        相关资源
        最近更新 更多