- MLOps
- 机器学习
- 后端
- 工作流自动化
- AI Agent
【免费下载链接】zenml
ZenML 🙏: One AI Platform from Pipelines to Agents. https://zenml.io.
在 ZenML 的 API 参考文档中,步骤日志端点文档 以 OpenAPI 引用块的形式,声明了GET /api/v1/steps/{step_id}/logs这一用于按步(Step)拉取日志的 REST 接口。本文以该端点为骨架,结合 ZenML 开源仓库的服务端路由、日志存储抽象与数据模型实现,完整解析该接口的路径构成、请求参数、响应结构、鉴权规则、错误场景,以及日志从 Pipeline/Step 运行中产生、被采集、落盘到最终可被 API 检索的完整链路,并给出通过 Python Client 与 ZenML Dashboard 获取步骤日志的实操方案。
端点概览:从 OpenAPI 占位符到真实实现
logs.md文档页本身是 ZenML OpenAPI 文档的自动渲染页,其核心信息是声明了以下接口:
GET /api/v1/steps/{step_id}/logs该端点并非仅存在于文档声明中,其真实实现位于服务端路由模块 steps_endpoints.py 的get_step_logs函数。从路由定义来看,路径由以下常量拼接而成(见 constants.py):
API = "/api"(constants.py)VERSION_1 = "/v1"(constants.py)STEPS = "/steps"(constants.py)LOGS = "/logs"(constants.py)
即GET /api/v1/steps/{step_id}/logs,挂载在steps路由上,用于返回某个 Step Run 关联的日志条目列表。它的典型使用场景是:在 Pipeline Run 执行完成后,按 Step 维度精确拉取其 stdout/stderr、Pythonlogging输出与print()语句,用于调试、审计或接入外部系统。
请求参数与调用规则
从 get_step_logs 的函数签名可知,该接口除路径参数外,还接受两个互斥的查询参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
step_id(路径参数) | UUID | 是 | 目标 Step Run 的 ID,用于在 Zen Store 中定位对应的步骤运行 |
source(查询参数) | str | 与logs_id二选一 | 日志来源标识,例如"client"、"orchestrator"、"step"等,用于在步骤的日志集合中按来源精确匹配 |
logs_id(查询参数) | UUID | 与source二选一 | 日志记录(LogsResponse)的 ID,用于直接定位某条日志记录 |
调用规则(互斥校验),对应源码中的两段校验逻辑:
- 若
source与logs_id均为空,抛出ValueError,响应400 Bad Request,提示 "Either source or logs_id must be provided."; - 若两者同时提供,同样抛出
ValueError,响应400 Bad Request,提示 "Only one of source or logs_id must be provided."。
也就是说,source与logs_id是“必须且只能提供一个”的关系。两者本质上对应日志集合中的两种索引方式:source面向业务语义(这个日志是哪一层产生的),而logs_id面向精确 ID(直接指向某条LogsResponse实体)。服务端分别通过 search_logs_by_source 与 search_logs_by_id 在步骤的日志集合中进行匹配查找。
响应结构:List[LogEntry]
接口返回的是List[LogEntry]。LogEntry是 ZenML 定义的结构化日志条目模型(见 logging_utils.py),既被 Artifact Log Store 用作 JSON 序列化格式,也是服务端查询时返回给客户端的数据结构,其字段如下:
| 字段 | 类型 | 说明 |
|---|---|---|
message | str | 日志消息正文 |
name | Optional[str] | 产生该条日志的 logger 名称 |
level | Optional[LoggingLevels] | 日志级别(DEBUG / INFO / WARN / ERROR / CRITICAL) |
timestamp | Optional[datetime] | 日志产生时间 |
module | Optional[str] | 产生该条日志的模块名 |
filename | Optional[str] | 产生该条日志的源文件名 |
lineno | Optional[int] | 源文件中的行号 |
chunk_index | int(默认 0) | 分块日志的块索引 |
total_chunks | int(默认 1) | 分块日志的块总数 |
id | UUID | 日志条目唯一标识,用于重组分块消息 |
分块机制:对于超长日志消息(约超过 5KB),ZenML 会自动将消息拆分为多个块,各块共享同一个id,并通过递增的chunk_index与固定的total_chunks标记顺序,供客户端重组完整消息。这一约定同时体现在 Artifact Log Store 的 NDJSON 落盘格式中(详见后文)。
此外,单次请求返回的日志条目数量受上限约束:服务端调用fetch_logs(..., limit=LOGS_MAX_ENTRIES_PER_REQUEST)拉取日志,LOGS_MAX_ENTRIES_PER_REQUEST默认值为50000(见 constants.py),可通过环境变量ZENML_LOGS_MAX_ENTRIES_PER_REQUEST调整。
鉴权与错误场景
该端点通过Security(authorize)强制鉴权,并遵循 ZenML 的 RBAC 权限模型。其关键逻辑(见 steps_endpoints.py):
- 先通过 Zen Store 获取
step(get_run_step(step_id, hydrate=True)),拿到其所属的pipeline_run_id与project_id; - 随后以
ResourceType.PIPELINE_RUN资源、Action.READ动作进行权限校验——读取步骤日志的前提是调用者对日志所属的 Pipeline Run 拥有读权限。
路由声明中注册的错误响应包括400、401、403、404、422,结合实现可归纳如下:
| 状态码 | 触发场景 |
|---|---|
400 | source与logs_id均未提供,或同时提供(互斥校验失败) |
401 | 未携带有效认证凭据 |
403 | 已认证但对所属 Pipeline Run 无读权限 |
404 | 步骤不存在;或步骤无日志集合("No logs found for step {step_id}.");或按source未匹配到日志;或按logs_id未匹配到日志 |
422 | 参数校验失败(如step_id非合法 UUID) |
值得注意的细节:当step.log_collection为空时直接抛出KeyError(对应 404);当按source或logs_id在集合中找不到匹配项时,同样抛出KeyError,错误信息中会包含具体的source值或logs_id值,便于定位问题。
数据模型:LogsRequest / LogsResponse / LogsUpdate
/api/v1/steps/{step_id}/logs端点背后承载的是 ZenML 的 Logs 实体。围绕该实体,数据模型定义在 models/v2/core/logs.py,分为三类:
LogsRequest(创建请求),关键字段:
id: UUID:日志记录 ID(默认uuid4()自动生成);uri: Optional[str]:日志文件 URI(用于 Artifact Log Store 场景),且受TEXT_FIELD_MAX_LENGTH长度约束;source: str:日志来源标识;artifact_store_id/log_store_id:分别指代日志由哪个 Artifact Store 或 Log Store 收集;pipeline_run_id/step_run_id:日志关联的 Pipeline Run 或 Step Run(二者互斥,不可同时设置);hook_invocation_id:日志关联的 Hook 调用。
模型级校验(model_validator)保证:artifact_store_id与log_store_id至少设置其一(除非source为特殊的"runner"来源LOGS_RUNNER_SOURCE);step_run_id与pipeline_run_id不能同时存在。
LogsResponse(查询响应):分为 body 与 metadata 两层——
- body:
uri、source; - metadata:
step_run_id、pipeline_run_id、artifact_store_id、log_store_id、hook_invocation_id。
LogsUpdate(更新请求):支持重新关联pipeline_run_id或step_run_id,同样受“二者不可同时设置”约束。
这些模型通过 Step Run / Pipeline Run 的log_collection资源字段与运行实体关联(见 step_run.py 中StepRunResponseResources.log_collection的定义)。换言之,一个 Step Run 可以挂载多条日志记录(例如client、orchestrator、step等不同来源各有其一),/api/v1/steps/{step_id}/logs的职责正是从这组集合中按source或logs_id挑选并返回。
底层实现:get_step_logs 的完整调用链
从服务端视角,一次GET /api/v1/steps/{step_id}/logs请求的处理流程(见 steps_endpoints.py)为:
- 校验
source与logs_id的互斥关系; - 从 Zen Store 获取 Step Run(
hydrate=True),并校验对所属 Pipeline Run 的读权限; - 若
step.log_collection存在:- 提供
source时,调用search_logs_by_source匹配,命中后调用fetch_logs拉取日志条目; - 提供
logs_id时,调用search_logs_by_id匹配,命中后同样调用fetch_logs; - 未命中则抛
KeyError;
- 提供
- 若
step.log_collection为空,直接抛KeyError。
其中fetch_logs是关键的跨层函数(见 logging_utils.py),它只能在服务端环境(ZENML_SERVER环境变量已设置)中调用,避免在客户端实例化重复的 Log Store。其内部逻辑为:
- 若
logs.log_store_id存在:通过 Zen Store 获取对应的 Stack 组件模型,校验其类型确为LOG_STORE,再用StackComponent.from_model实例化具体的 Log Store,调用其fetch()方法,最后执行cleanup()释放资源;若集成依赖缺失,则抛NotImplementedError; - 若
logs.artifact_store_id存在:实例化对应的 Artifact Store,并构建ArtifactLogStore,同样调用fetch(); - 两者皆无时返回空列表
[]。
也就是说,该 API 端点本身并不直接读取日志文件,而是将“按模型定位后端并读取日志”的职责委托给对应 Log Store 的fetch抽象。这一设计使得服务端无需知道日志究竟存在 Artifact Store、OpenTelemetry 还是 Datadog,实现了后端无关的日志检索。
日志从产生到可查询:LoggingContext 与 Log Store 机制
要理解/api/v1/steps/{step_id}/logs为何能取到日志,需要了解日志在运行时是如何被捕获与关联的。ZenML 的日志采集系统(参见 Log Stores 组件指南)包含三步:
- stdout/stderr 包装:ZenML 包装标准输出与错误流,捕获所有
print()及写入这两个流的消息; - 根 logger handler:为 Python 根 logger 挂载自定义 Handler,捕获所有向上传播的
logging消息(含第三方库输出); - 日志路由:所有被捕获的消息经
LoggingContext路由到当前 Stack 的活跃 Log Store。
核心上下文管理器是 LoggingContext,它通过ContextVar维护当前活跃的日志上下文,并提供了完整的生命周期管理:
begin():在 Log Store 上注册一个origin(即日志来源);emit(record):将 LogRecord 交给 Log Store 的emit()处理,并附带元数据(如zenml.step.run.id、zenml.pipeline.run.id、zenml.project.id等);end():注销origin;若这是最后一个活跃 origin,将触发 Log Store 的flush();- 退出上下文时,若发生异常,会额外以 ERROR 级别注入一条 "An exception has occurred." 记录,元数据标记为
zenml.event.type=exception。
Origin 的生命周期定义在 base_log_store.py,流程为:实例化 Log Store →register_origin()(传入 log model 与元数据)→emit()逐条发送日志 →deregister_origin()(最后一个 origin 注销时 flush)。setup_logging_context(logging_utils.py)负责组装这一切:查找已存在的 LogsResponse(按source匹配),否则通过generate_logs_request生成LogsRequest并调用Client().zen_store.create_logs创建新的日志记录,再把 Step Run / Pipeline Run 的 ID 通过LogsUpdate关联回日志实体——这正是步骤的log_collection集合被填充、最终可供 API 检索的底层机制。
日志存储格式与位置
默认情况下,若 Stack 中未显式配置 Log Store,ZenML 会自动使用Artifact Log Store(零配置,开箱即用,详见 Artifact Log Store 指南),将日志写入 Artifact Store 的logs目录。
存储位置规则(见 artifact_log_store.py 的prepare_logs_uri):
- 可变文件系统(本地、S3、Azure 等):日志以单文件形式追加写入,路径为
<artifact_store_path>/logs/<log_id>.log; - 不可变文件系统(如 GCS):日志写入按时间戳命名的目录
<artifact_store_path>/logs/<log_id>/,其中包含多个*.log分片文件,并在最终化时合并为<timestamp>_merged.log。
落盘格式为换行分隔的 JSON(NDJSON)。每行对应一条日志,字段包括message、level、timestamp、name、filename、lineno、module、chunk_index、total_chunks、id,与LogEntry模型一一对应。读取时,fetch_log_records 与_stream_logs_line_by_line会按行流式读取(不把整个文件载入内存),并通过 parse_log_entry 兼容两种格式:以{开头且以}结尾的 JSON 行走model_validate_json完整解析;普通文本行则降级为INFO级别、仅填充message字段的LogEntry,同时兼容旧版[YYYY-MM-DD HH:MM:SS UTC]时间戳前缀格式,并自动剥离 ANSI 转义序列(remove_ansi_escape_codes)。
除默认的 Artifact Log Store 外,ZenML 还提供otel(通用 OpenTelemetry 后端,不支持日志回拉,Dashboard 会显示 "Logs not available")与datadog(完整支持导出与回拉)等 flavor,可用zenml log-store flavor list查看全部可用 flavor。无论底层是哪种 Log Store,/api/v1/steps/{step_id}/logs都会经由 Log Store 抽象的fetch()统一返回List[LogEntry]。
相关 API:/api/v1/logs 端点家族
/api/v1/steps/{step_id}/logs是日志查询的“便捷入口”,而完整的日志实体 CRUD 路由则定义在 logs_endpoints.py,挂载前缀为API + VERSION_1 + LOGS(即/api/v1/logs):
| 方法与路径 | 功能 | 说明 |
|---|---|---|
POST /api/v1/logs | create_logs | 创建日志记录(LogsRequest)。创建前会校验对关联 Pipeline Run / Step Run 的UPDATE权限,以及对 Artifact Store / Log Store 组件的READ权限 |
GET /api/v1/logs/{logs_id} | get_logs | 按 ID 获取单条日志记录。日志必须已关联 Pipeline Run 或 Step Run,否则抛IllegalOperationError;通过hydrate参数控制是否返回 metadata |
PUT /api/v1/logs/{logs_id} | update_logs | 更新日志关联的pipeline_run_id/step_run_id,同样需要所属运行的UPDATE权限 |
其中update_logs的持久化逻辑可在 sql_zen_store.py 中看到:若日志已关联了不同的实体,再次修改关联会抛IllegalOperationError("The logs entry is already associated with a different entity."),从而保证一条日志记录的归属一旦确定即不可转移。这也解释了为什么setup_logging_context是先创建、后attach()关联运行 ID 的异步两段式设计。
通过 Python Client 与 Dashboard 获取步骤日志
方式一:Python Client 编程式获取
虽然 REST 端点要求source与logs_id互斥传参,但日常使用中更推荐通过 Python Client 直接读取(见 Log Stores 组件指南 中的代码示例):
from zenml.client import Client client = Client() # 获取 Pipeline Run,或按步骤 ID 获取步骤 run = client.get_pipeline_run("<RUN_NAME_OR_ID>") step = client.get_run_step(step_run_id="<STEP_RUN_ID>") # 注意:使用的 Log Store 必须与采集日志时一致 log_store = client.active_stack.log_store # 拉取运行级日志 log_entries = log_store.fetch(logs_model=run.logs, limit=1000) for entry in log_entries: print(f"[{entry.level}] {entry.message}")Client.get_run_step(step_run_id, hydrate=True)会返回带log_collection资源的StepRunResponse(见 client.py),其中每一条LogsResponse对应一个日志来源。需要精确按来源过滤时,可直接在集合中匹配source字段。
方式二:ZenML Dashboard
在 ZenML Dashboard 中,进入 Pipeline Run 页面即可按 Step 查看其日志,Dashboard 后端调用的正是本文所述的/api/v1/steps/{step_id}/logs系列接口。对于datadog等外部 Log Store,日志也会在对应平台的原生界面中展示。
环境变量与调优
围绕日志采集、批处理与检索,ZenML 暴露了若干可调环境变量,集中定义于 constants.py:
| 环境变量 | 默认值 | 作用 |
|---|---|---|
ZENML_LOGS_MAX_ENTRIES_PER_REQUEST | 50000 | 单次 API / fetch 请求返回的日志条目上限,直接影响/api/v1/steps/{step_id}/logs的响应规模 |
ZENML_LOGS_OTEL_MAX_QUEUE_SIZE | 100000 | OpenTelemetry 批量日志处理器的最大队列长度 |
ZENML_LOGS_OTEL_SCHEDULE_DELAY_MILLIS | 5000 | 批量导出之间的调度延迟(毫秒) |
ZENML_LOGS_OTEL_MAX_EXPORT_BATCH_SIZE | 5000 | 单次导出的最大批大小 |
ZENML_LOGS_OTEL_EXPORT_TIMEOUT_MILLIS | 15000 | 单批导出的超时时间(毫秒) |
ZENML_LOGS_STORAGE_MAX_QUEUE_SIZE | 100000 | 日志存储队列最大容量 |
ZENML_LOGS_STORAGE_QUEUE_TIMEOUT | -1 | 日志存储队列超时(-1 表示无限等待) |
ZENML_LOGS_WRITE_INTERVAL_SECONDS | 1 | 日志写入间隔(秒) |
ZENML_LOGS_MERGE_INTERVAL_SECONDS | 600 | 不可变文件系统日志合并间隔(秒) |
ZENML_DISABLE_PIPELINE_LOGS_STORAGE | False | 置为true可全局禁用 Pipeline 级日志存储 |
ZENML_DISABLE_STEP_LOGS_STORAGE | False | 置为true可全局禁用 Step 级日志存储 |
ZENML_DISABLE_STEP_NAMES_IN_LOGS | True | 控制在控制台日志中是否打印 Step 名称 |
上述默认值针对大多数场景已足够;仅在高并发、大日志量的流水线场景下,建议适当调大批处理与队列相关参数,并在 Pipeline / Step 配置层面通过enable_step_logs、enable_pipeline_logs显式控制日志存储的开关。禁用后,/api/v1/steps/{step_id}/logs将返回 404 或空结果,因此在排查"API 取不到日志"的问题时,应优先检查这些开关以及实际使用的 Log Store flavor 是否支持日志回拉。
总结
GET /api/v1/steps/{step_id}/logs是 ZenML 面向 Step 粒度的日志检索接口:它通过source或logs_id两个互斥查询参数在步骤的log_collection中精确挑选日志,经 RBAC 校验后委托给底层 Log Store 的fetch()抽象返回结构化的LogEntry列表。从运行时的 stdout/stderr 包装、LoggingContext路由、Origin 注册与 flush,到 Artifact Log Store 的 NDJSON 落盘与分块重组,再到服务端fetch_logs的跨层装配,整个链路环环相扣。掌握该端点及其背后的数据模型、日志存储机制与调优变量,即可在自建运维、日志审计与集成第三方监控平台时,精确、高效地获取每一步骤的执行日志。
- MLOps
- 机器学习
- 后端
- 工作流自动化
- AI Agent
【免费下载链接】zenml
ZenML 🙏: One AI Platform from Pipelines to Agents. https://zenml.io.
相关推荐
AI Agent 浏览器怎么挑?ego-lite 让你和 Agent 共用一个登录态的完整指南
AI Agent 浏览器怎么挑?ego lite 让你和 Agent 共用一个登录态的完整指南 ego lite 是一款为 AI Agent 打造的 Chrom
AI 技能浏览器控制GUI 自动化ZenML API 令牌端点深度解析:Generic 与 Workload 两类 API Token 的生成规则、参数与源码实现
ZenML API 令牌端点深度解析:Generic 与 Workload 两类 API Token 的生成规则、参数与源码实现 本篇围绕 ZenML REST
MLOps机器学习后端工作流自动化AI AgentZenML 健康检查 API 详解:/health 端点的实现原理与运维实践
ZenML 健康检查 API 详解:/health 端点的实现原理与运维实践 本篇文章围绕 ZenML ZenServer 提供的 GET /health 健康
MLOps机器学习后端工作流自动化AI Agent
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考