收集 Google App Engine 日志
本文档介绍了如何使用 Google Cloud Storage V2 将 Google App Engine 日志注入到 Google Security Operations。
Google App Engine 是全代管式无服务器平台,可用于构建和部署 Web 应用和 API。App Engine 会自动生成 HTTP 请求的请求日志和代码中的应用日志。这些日志会发送到 Cloud Logging,并且可以导出到 Cloud Storage 以供 Google Security Operations 提取。
准备工作
确保您满足以下前提条件:
- Google SecOps 实例
- 已启用 Cloud Storage API 的 GCP 项目
- 创建和管理 GCS 存储分区的权限
- 管理 GCS 存储分区的 IAM 政策的权限
- 创建 Cloud Logging 接收器的权限 (roles/logging.configWriter)
- 处于活跃状态的 App Engine 应用(标准环境或柔性环境)
创建 Google Cloud Storage 存储分区
- 转到 Google Cloud Console。
- 选择您的项目或创建新项目。
- 在导航菜单中,依次前往 Cloud Storage > 存储分区。
- 点击创建存储分区。
提供以下配置详细信息:
设置 值 为存储分区命名 输入一个全局唯一的名称(例如 appengine-logs-export)位置类型 根据您的需求进行选择(区域级、双区域级、多区域级) 位置 选择相应位置(例如 us-central1)存储类别 标准(建议用于经常访问的日志) 访问权限控制 统一(推荐) 保护工具 可选:启用对象版本控制或保留政策 点击创建。
配置 Cloud Logging 以将 App Engine 日志导出到 GCS
Cloud Logging 使用日志接收器将日志条目路由到支持的目标位置,包括 Cloud Storage 存储分区。接收器的写入者身份需要目标存储桶上的 Storage Object Creator 角色 (roles/storage.objectCreator)。
创建 Cloud Logging 接收器
- 在 Google Cloud 控制台中,依次前往日志记录 > 日志路由器。
- 点击创建接收器。
- 提供以下配置详细信息:
- 接收器名称:输入一个描述性名称(例如
appengine-to-gcs)。 - 接收器说明:可选说明。
- 接收器名称:输入一个描述性名称(例如
- 点击下一步。
- 在选择接收器服务部分中:
- 接收器服务:选择 Cloud Storage 存储桶。
- 选择 Cloud Storage 存储桶:从下拉菜单中选择
appengine-logs-export。
- 点击下一步。
在选择要包含在接收器中的日志部分,输入过滤查询以选择 App Engine 日志。资源类型必须正好是“gae_app”。
对于所有 App Engine 日志(请求日志和应用日志):
resource.type="gae_app"仅适用于 App Engine 请求日志:
resource.type="gae_app" logName="projects/PROJECT_ID/logs/appengine.googleapis.com/request_log"对于 App Engine 应用日志(stdout/stderr):
resource.type="gae_app" (logName="projects/PROJECT_ID/logs/stdout" OR logName="projects/PROJECT_ID/logs/stderr")将
PROJECT_ID替换为您的 GCP 项目 ID。点击下一步。
检查配置,然后点击创建接收器。
向接收器写入者身份授予权限
创建接收器后,您必须向接收器的写入者身份授予目标存储桶的 Storage Object Creator 角色。服务账号的写入者身份类似于:serviceAccount:service-123456789012@gcp-sa-logging.iam.gserviceaccount.com
- 在日志路由器页面中,找到新创建的接收器。
- 点击接收器名称旁边的菜单图标(三个竖点)。
- 选择查看接收器详情。
- 复制写入者身份(服务账号电子邮件地址)。
- 前往 Cloud Storage > 存储分区。
- 点击相应存储桶的名称 (
appengine-logs-export)。 - 前往权限标签页。
- 点击授予访问权限。
- 提供以下配置详细信息:
- 添加主账号:粘贴接收器的写入者身份(服务账号电子邮件地址)。
- 分配角色:选择 Storage Object Creator。
- 点击保存。
检索 Google SecOps 服务账号
Google SecOps 使用唯一的服务账号从您的 GCS 存储分区中读取数据。您必须授予此服务账号对您的存储分区的访问权限。
在 Google SecOps 中配置 Feed 以注入 App Engine 日志
- 依次前往 SIEM 设置 > Feed。
- 点击添加新 Feed。
- 点击配置单个 Feed。
- 在Feed 名称字段中,输入 Feed 的名称(例如
App Engine Logs)。 - 选择 Google Cloud Storage V2 作为来源类型。
选择 GCP_APP_ENGINE 作为日志类型。
点击获取服务账号。
系统会显示一个唯一的服务账号电子邮件地址,例如:
chronicle-12345678@chronicle-gcp-prod.iam.gserviceaccount.com复制此电子邮件地址。您将在下一步骤中用到它。
点击下一步。
为以下输入参数指定值:
存储分区网址:输入带有前缀路径的 GCS 存储分区 URI:
gs://appengine-logs-export/Cloud Logging 会在目录分层结构下按日志类型和日期整理导出的日志文件。日志类型可以是复合名称,例如 appengine.googleapis.com/request_log。文件会进行分片,并以时间段命名(例如 08:00:00_08:59:59_S0.json)。
来源删除选项:根据您的偏好选择删除选项:
- 永不:永不删除转移后的任何文件(建议用于测试)。
- 删除已转移的文件:在成功转移后删除文件。
- 删除已转移的文件和空目录:在成功转移后删除文件和空目录。
文件存在时间上限:包含在过去指定天数内修改的文件。默认值为 180 天。
资产命名空间:资产命名空间。
注入标签:要应用于此 Feed 中事件的标签。
点击下一步。
在最终确定界面中查看新的 Feed 配置,然后点击提交。
向 Google SecOps 服务账号授予 IAM 权限
Google SecOps 服务账号需要对您的 GCS 存储分区具有 Storage Object Viewer 角色。
- 前往 Cloud Storage > 存储分区。
- 点击相应存储桶的名称 (
appengine-logs-export)。 - 前往权限标签页。
- 点击授予访问权限。
- 提供以下配置详细信息:
- 添加主账号:粘贴 Google SecOps 服务账号电子邮件地址。
- 分配角色:选择 Storage Object Viewer。
点击保存。
了解 App Engine 日志结构
App Engine 会自动将请求日志和应用日志发送到 Cloud Logging。App Engine 会自动针对发送到应用的请求发出日志,因此无需写入请求日志。本部分介绍了如何写入应用日志。
App Engine 请求日志中的日志条目包含 protoPayload 字段,此字段保存着类型为 RequestLog 的对象,其 @type 为“type.googleapis.com/google.appengine.logging.v1.RequestLog”。资源类型为“gae_app”。
默认情况下,日志载荷是存储在日志条目的 textPayload 字段中的文本字符串。这些字符串在 Logs Explorer 中显示为消息,并与发出它们的 App Engine 服务和版本相关联。
如需写入结构化日志,请以单行序列化 JSON 的形式写入日志。如果您以 JSON 字典形式提供结构化日志,则系统会从 jsonPayload 中删除某些特殊字段,并将这些字段写入所生成 LogEntry 中的相应字段。例如,如果您的 JSON 包含严重程度属性,则系统会将该属性从 jsonPayload 中移除,而将其显示为日志条目的严重程度。
已知限制
将日志从日志接收器路由到 Cloud Storage 时,Cloud Storage 目标位置仅包含请求日志。App Engine 将应用日志写入不同的文件夹。
路由的日志条目每小时向 Cloud Storage 存储分区保存一批。第一批条目可能需要 2 到 3 个小时才会开始显示。
在 App Engine 柔性环境中,日志记录会自动进行。不过,日志会以不同的格式收集。日志不会被请求捆绑,并且来自 stdout 和 stderr 的日志是分开收集的。
UDM 映射表
| 日志字段 | UDM 映射 | 逻辑 |
|---|---|---|
| jsonPayload.logger, taskTypeName, jsonPayload.@type, jsonPayload.backendTargetProjectNumber, jsonPayload.cacheDecision, resource.labels.version_id, resource.labels.module_id, logName, spanId, trace, protoPayload.@type, labels.clone_id, operation.producer | additional.fields | 与根据每个字段创建的键值对标签合并 |
| 元数据 | 元数据 | 已从元数据重命名 |
| receiveTimestamp | metadata.collected_timestamp | 使用 RFC3339 通过日期过滤条件进行解析 |
| metadata.event_type | 如果具有 has_principal、has_target、has_principal_user,则设置为“USER_LOGIN”;如果具有 has_principal 和 has_target,则设置为“NETWORK_CONNECTION”;如果未具有 has_principal 但具有 has_target,则设置为“USER_UNCATEGORIZED”;如果具有 has_principal,则设置为“STATUS_UPDATE”;如果具有 has_principal_user,则设置为“USER_UNCATEGORIZED”;否则设置为“GENERIC_EVENT” | |
| metadata.extensions.auth.type | 如果具有 has_principal、has_target、has_principal_user,则设置为“AUTHTYPE_UNSPECIFIED” | |
| insertId | metadata.product_log_id | 直接复制值 |
| httpRequest.requestMethod,protoPayload.method | network.http.method | 如果 httpRequest.requestMethod 不为空,则为该值;否则为 protoPayload.method |
| httpRequest.userAgent | network.http.parsed_user_agent | 已转换为 parseduseragent |
| httpRequest.status | network.http.response_code | 转换为字符串,然后再转换为整数 |
| httpRequest.userAgent | network.http.user_agent | 直接复制值 |
| httpRequest.responseSize | network.received_bytes | 已转换为 uinteger |
| httpRequest.requestSize | network.sent_bytes | 已转换为 uinteger |
| 主账号 | 主账号 | 如果正文不为空,则重命名为正文 |
| protoPayload.host | principal.asset.hostname | 直接复制值 |
| httpRequest.serverIp, protoPayload.ip | principal.asset.ip | 与 httpRequest.serverIp 或 protoPayload.ip 中的 server_ip 合并 |
| protoPayload.host | principal.hostname | 直接复制值 |
| httpRequest.serverIp, protoPayload.ip | principal.ip | 与 httpRequest.serverIp 或 protoPayload.ip 中的 server_ip 合并 |
| protoPayload.appId | principal.resource.attribute.labels | 与包含键“appId”和相应字段值的 appId_label 合并 |
| requestUser | principal.user.email_addresses | 如果与电子邮件地址格式匹配,则与 requestUser 合并 |
| security_result | security_result | 从 security_result 合并 |
| resource.labels.forwarding_rule_name | security_result.rule_labels | 与包含键“forwarding_rule_name”和字段中值的 rule_label 合并 |
| 和程度上减少 | security_result.severity | 如果匹配 (?i)ERROR|CRITICAL,则设置为严重程度;如果匹配 (?i)INFO,则设置为 INFORMATIONAL;如果匹配 (?i)WARN,则设置为 MEDIUM;如果匹配 (?i)DEBUG,则设置为 LOW;否则设置为 UNKNOWN_SEVERITY |
| jsonPayload.statusDetails | security_result.summary | 直接复制值 |
| 目标 | 目标 | 如果目标不为空,则重命名为目标 |
| resource.labels.backend_service_name | target.application | 直接复制值 |
| httpRequest.remoteIp, jsonPayload.remoteIp | target.asset.ip | 与从 httpRequest.remoteIp 或 jsonPayload.remoteIp 中提取的 remote_ip 合并 |
| resource.labels.project_id | target.cloud.project.name | 直接复制值 |
| httpRequest.remoteIp, jsonPayload.remoteIp | target.ip | 与从 httpRequest.remoteIp 或 jsonPayload.remoteIp 中提取的 remote_ip 合并 |
| resource.labels.zone | target.resource.attribute.cloud.availability_zone | 直接复制值 |
| resource.labels.target_proxy_name, resource.labels.url_map_name | target.resource.attribute.labels | 与每个来源的标签合并 |
| resource.type | target.resource.type | 直接复制值 |
| httpRequest.requestUrl | target.url | 直接复制值 |
| metadata.product_name | 设置为“GCP_APP_ENGINE” | |
| metadata.vendor_name | 设置为“GCP” |
需要更多帮助?获得社区成员和 Google SecOps 专业人士的解答。