16

我有一个简单的 Python 应用程序在 Google Kubernetes Engine 上的容器中运行。我正在尝试使用本指南将标准 Python 日志记录连接到 Google Stackdriver 日志记录。我几乎成功了,但是我得到了重复的日志条目,其中一个总是处于“错误”级别......


显示重复条目的 Stackdriver 日志屏幕截图

显示重复条目的 Stackdriver 日志屏幕截图

这是我根据上述指南设置日志记录的 python 代码:

import webapp2
from paste import httpserver
import rpc

# Imports the Google Cloud client library
import google.cloud.logging
# Instantiates a client
client = google.cloud.logging.Client()
# Connects the logger to the root logging handler; by default this captures
# all logs at INFO level and higher
client.setup_logging()

app = webapp2.WSGIApplication([('/rpc/([A-Za-z]+)', rpc.RpcHandler),], debug=True)
httpserver.serve(app, host='0.0.0.0', port='80')

这是从屏幕截图触发日志的代码:

import logging

logging.info("INFO Entering PostEchoPost...")
logging.warning("WARNING Entering PostEchoPost...")
logging.error("ERROR Entering PostEchoPost...")
logging.critical("CRITICAL Entering PostEchoPost...")

以下是完整的 Stackdriver 日志,从屏幕截图扩展而来,错误级别解释不正确:

{
 insertId:  "1mk4fkaga4m63w1"  
 labels: {
  compute.googleapis.com/resource_name:  "gke-alg-microservice-default-pool-xxxxxxxxxx-ttnz"   
  container.googleapis.com/namespace_name:  "default"   
  container.googleapis.com/pod_name:  "esp-alg-xxxxxxxxxx-xj2p2"   
  container.googleapis.com/stream:  "stderr"   
 }
 logName:  "projects/projectname/logs/algorithm"  
 receiveTimestamp:  "2018-01-03T12:18:22.479058645Z"  
 resource: {
  labels: {
   cluster_name:  "alg-microservice"    
   container_name:  "alg"    
   instance_id:  "703849119xxxxxxxxxx"   
   namespace_id:  "default"    
   pod_id:  "esp-alg-xxxxxxxxxx-xj2p2"    
   project_id:  "projectname"    
   zone:  "europe-west1-b"    
  }
  type:  "container"   
 }
 severity:  "ERROR"  
 textPayload:  "INFO Entering PostEchoPost...
"  
 timestamp:  "2018-01-03T12:18:20Z"  
}

以下是完整的 Stackdriver 日志,从屏幕截图扩展而来,具有正确解释的 INFO 级别:

{
 insertId:  "1mk4fkaga4m63w0"  
 jsonPayload: {
  message:  "INFO Entering PostEchoPost..."   
  thread:  140348659595008   
 }
 labels: {
  compute.googleapis.com/resource_name:  "gke-alg-microservi-default-pool-xxxxxxxxxx-ttnz"   
  container.googleapis.com/namespace_name:  "default"   
  container.googleapis.com/pod_name:  "esp-alg-xxxxxxxxxx-xj2p2"   
  container.googleapis.com/stream:  "stderr"   
 }
 logName:  "projects/projectname/logs/algorithm"  
 receiveTimestamp:  "2018-01-03T12:18:22.479058645Z"  
 resource: {
  labels: {
   cluster_name:  "alg-microservice"    
   container_name:  "alg"    
   instance_id:  "703849119xxxxxxxxxx"    
   namespace_id:  "default"    
   pod_id:  "esp-alg-xxxxxxxxxx-xj2p2"    
   project_id:  "projectname"    
   zone:  "europe-west1-b"    
  }
  type:  "container"   
 }
 severity:  "INFO"  
 timestamp:  "2018-01-03T12:18:20.260099887Z"  
}

所以,这个条目可能是关键:

container.googleapis.com/stream:  "stderr" 

看起来除了我的日志记录设置工作之外,来自容器的所有日志都被发送到容器中的 stderr,我相信默认情况下,至少在 Kubernetes Container Engine 上,所有 stdout/stderr 都由 Google 拾取Stackdriver via FluentD...话虽如此,我现在已经超出了我的深度。

任何想法为什么我会收到这些重复的条目?

4

3 回答 3

6

我通过在调用该方法后立即覆盖handlers我的根记录器上的属性解决了这个问题setup_logging

import logging
from google.cloud import logging as gcp_logging
from google.cloud.logging.handlers import CloudLoggingHandler, ContainerEngineHandler, AppEngineHandler

logging_client = gcp_logging.Client()
logging_client.setup_logging(log_level=logging.INFO)
root_logger = logging.getLogger()
# use the GCP handler ONLY in order to prevent logs from getting written to STDERR
root_logger.handlers = [handler
                        for handler in root_logger.handlers
                        if isinstance(handler, (CloudLoggingHandler, ContainerEngineHandler, AppEngineHandler))]

为了详细说明这一点,该client.setup_logging方法设置了2 个处理程序,一个普通处理程序logging.StreamHandler和一个 GCP 特定处理程序。因此,日志将同时发送到 stderr 和 Cloud Logging。您需要从处理程序列表中删除流处理程序以防止重复。

编辑:我已经向谷歌提交了一个问题,以添加一个论点来减少这个问题。

于 2020-05-04T22:00:03.320 回答
4

问题在于日志记录客户端如何初始化根记录器

    logger = logging.getLogger()
    logger.setLevel(log_level)
    logger.addHandler(handler)
    logger.addHandler(logging.StreamHandler())

除了 Stackdriver 处理程序之外,它还添加了默认流处理程序。我现在的解决方法是手动初始化适当的 Stackdriver 处理程序:

            # this basically manually sets logger compatible with GKE/fluentd
            # as LoggingClient automatically add another StreamHandler - so 
            # log records are duplicated
            from google.cloud.logging.handlers import ContainerEngineHandler
            formatter = logging.Formatter("%(message)s")
            handler = ContainerEngineHandler(stream=sys.stderr)
            handler.setFormatter(formatter)
            handler.setLevel(level)
            root = logging.getLogger()
            root.addHandler(handler)
            root.setLevel(level)

于 2019-11-01T06:57:04.990 回答
0

写于 2022 年,在 v3.0.0 发布后不久google-cloud-logging,我也遇到了这个问题(尽管几乎可以肯定是出于不同的原因)。

调试

我在调试它的过程中做的最有用的事情是在我的代码中粘贴以下内容:

import logging
...
root_logger = logging.getLogger()  # no arguments = return the root logger
print(root_logger.handlers, flush=True)  # tell me what handlers are attached
...

如果您收到重复的日志,这似乎是因为您的记录器附加了多个处理程序,而 Stackdriver 正在从它们中捕获日志!公平地说,这是 Stackdriver 的工作;只是可惜google-cloud-logging不能默认解决这个问题。

好消息是 Stackdriver 也会捕获print语句(进入 STDOUT 流)。就我而言,记录了以下处理程序列表[<StreamHandler <stderr> (NOTSET)>, <StructuredLogHandler <stderr> (NOTSET)>]:所以:两个处理程序附加到根记录器。

修复它

您可能会发现您的代码将处理程序附加到其他地方,然后简单地删除该部分。但它可能是这样的情况,例如,依赖项正在设置额外的处理程序,这是我努力解决的问题。

我使用了基于Andy Carlson 所写答案的解决方案。保持通用/可扩展:

import google.cloud.logging
import logging

def is_cloud_handler(handler: logging.Handler) -> bool:
    """
    is_cloud_handler

    Returns True or False depending on whether the input is a
    google-cloud-logging handler class

    """
    accepted_handlers = (
        google.cloud.logging.handlers.StructuredLogHandler,
        google.cloud.logging.handlers.CloudLoggingHandler,
        google.cloud.logging.handlers.ContainerEngineHandler,
        google.cloud.logging.handlers.AppEngineHandler,
    )
    return isinstance(handler, accepted_handlers)


def set_up_logging():
    # here we assume you'll be using the basic logging methods
    # logging.info, logging.warn etc. which invoke the root logger
    client = google.cloud.logging.Client()
    client.setup_logging()

    root_logger = logging.getLogger()
    root_logger.handlers = [h for h in root_logger.handlers if is_cloud_handler(h)]

更多上下文

对于那些觉得这个解决方案令人困惑的人

在 Python 中,“记录器”和“处理程序”是分开的:记录器生成日志,处理程序决定它们会发生什么。因此,您可以将多个处理程序附加到同一个记录器(如果您希望该记录器的日志发生多件事情)。

google-cloud-logging库建议您运行它的setup_logging方法,然后只使用内置logging库的基本日志记录方法来创建您的日志。它们是:logging.debuglogging.infologging.warninglogging.errorlogging.critical(按紧急程度递增的顺序)。

所有实例都有相同的方法,包括一个称为根记录器logging.Logger的特殊实例。Logger如果您查看基本日志记录方法的源代码,它们只是在此根记录器上调用这些方法。

可以设置特定Logger的 s,这是划分应用程序不同区域生成的日志的标准做法(而不是通过根记录器发送所有内容)。这是使用logging.getLogger("name-of-logger"). 但是,logging.getLogger()不带参数返回根记录器。

同时,该google.cloud.logging.Client.setup_logging方法的目的是为根 logger 附加一个特殊的日志处理程序。因此,使用 etc. 创建的日志logging.info将由处理google-cloud-logging程序处理。但是您必须确保没有其他处理程序也附加到根记录器。

幸运的是,Loggers 有一个属性 ,.handlers它是附加日志处理程序的列表。在此解决方案中,我们只需编辑该列表以确保我们只有一个处理程序。

于 2022-02-28T18:42:04.860 回答