☰
ZenML Step 日志检索 API 实战:/api/v1/steps/{step_id}/logs 端点的参数、响应与底层实现解析
2026/10/14 15:22:27 网站建设 项目流程
  • MLOps
  • 机器学习
  • 后端
  • 工作流自动化
  • AI Agent

【免费下载链接】zenml

ZenML 🙏: One AI Platform from Pipelines to Agents. https://zenml.io.

项目地址:https://gitcode.com/GitHub_Trending/ze/zenml
点击查看免费下载

在 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,用于直接定位某条日志记录

调用规则(互斥校验),对应源码中的两段校验逻辑:

  1. 若source与logs_id均为空,抛出ValueError,响应400 Bad Request,提示 "Either source or logs_id must be provided.";
  2. 若两者同时提供,同样抛出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 序列化格式,也是服务端查询时返回给客户端的数据结构,其字段如下:

字段类型说明
messagestr日志消息正文
nameOptional[str]产生该条日志的 logger 名称
levelOptional[LoggingLevels]日志级别(DEBUG / INFO / WARN / ERROR / CRITICAL)
timestampOptional[datetime]日志产生时间
moduleOptional[str]产生该条日志的模块名
filenameOptional[str]产生该条日志的源文件名
linenoOptional[int]源文件中的行号
chunk_indexint(默认 0)分块日志的块索引
total_chunksint(默认 1)分块日志的块总数
idUUID日志条目唯一标识,用于重组分块消息

分块机制:对于超长日志消息(约超过 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):

  1. 先通过 Zen Store 获取step(get_run_step(step_id, hydrate=True)),拿到其所属的pipeline_run_id与project_id;
  2. 随后以ResourceType.PIPELINE_RUN资源、Action.READ动作进行权限校验——读取步骤日志的前提是调用者对日志所属的 Pipeline Run 拥有读权限。

路由声明中注册的错误响应包括400、401、403、404、422,结合实现可归纳如下:

状态码触发场景
400source与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)为:

  1. 校验source与logs_id的互斥关系;
  2. 从 Zen Store 获取 Step Run(hydrate=True),并校验对所属 Pipeline Run 的读权限;
  3. 若step.log_collection存在:
    • 提供source时,调用search_logs_by_source匹配,命中后调用fetch_logs拉取日志条目;
    • 提供logs_id时,调用search_logs_by_id匹配,命中后同样调用fetch_logs;
    • 未命中则抛KeyError;
  4. 若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 组件指南)包含三步:

  1. stdout/stderr 包装:ZenML 包装标准输出与错误流,捕获所有print()及写入这两个流的消息;
  2. 根 logger handler:为 Python 根 logger 挂载自定义 Handler,捕获所有向上传播的logging消息(含第三方库输出);
  3. 日志路由:所有被捕获的消息经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/logscreate_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_REQUEST50000单次 API / fetch 请求返回的日志条目上限,直接影响/api/v1/steps/{step_id}/logs的响应规模
ZENML_LOGS_OTEL_MAX_QUEUE_SIZE100000OpenTelemetry 批量日志处理器的最大队列长度
ZENML_LOGS_OTEL_SCHEDULE_DELAY_MILLIS5000批量导出之间的调度延迟(毫秒)
ZENML_LOGS_OTEL_MAX_EXPORT_BATCH_SIZE5000单次导出的最大批大小
ZENML_LOGS_OTEL_EXPORT_TIMEOUT_MILLIS15000单批导出的超时时间(毫秒)
ZENML_LOGS_STORAGE_MAX_QUEUE_SIZE100000日志存储队列最大容量
ZENML_LOGS_STORAGE_QUEUE_TIMEOUT-1日志存储队列超时(-1 表示无限等待)
ZENML_LOGS_WRITE_INTERVAL_SECONDS1日志写入间隔(秒)
ZENML_LOGS_MERGE_INTERVAL_SECONDS600不可变文件系统日志合并间隔(秒)
ZENML_DISABLE_PIPELINE_LOGS_STORAGEFalse置为true可全局禁用 Pipeline 级日志存储
ZENML_DISABLE_STEP_LOGS_STORAGEFalse置为true可全局禁用 Step 级日志存储
ZENML_DISABLE_STEP_NAMES_IN_LOGSTrue控制在控制台日志中是否打印 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.

项目地址:https://gitcode.com/GitHub_Trending/ze/zenml
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询