OneUptime 工作流运行与日志(Runs & Logs)完全指南:状态语义、执行追踪与故障排错
【免费下载链接】oneuptimeComplete open-source monitoring and observability platform.项目地址: https://gitcode.com/GitHub_Trending/on/oneuptime
工作流的每一次执行都会在 OneUptime 中留下一份完整记录——它何时运行、是否成功、每一个组件块分别做了什么。这份记录被称为一次运行(Run),而本指南将围绕运行与日志的完整生命周期展开:如何找到它们、如何解读七种运行状态、如何利用Steps(步骤)与Full Log(完整日志)两个视图定位故障,并给出从"工作流没跑"到"变量为空"的常见排错路径。读完本文,你将能够像排查任何生产系统一样,熟练地对一条 OneUptime 工作流做运行审计、错误定位与状态研判。
本文以仓库中的德文版文档 runs-and-logs.md 为主体,并结合工作流引擎的核心源码(运行器 RunWorkflow.ts、状态枚举 WorkflowStatus.ts、步骤追踪结构 StepTrace.ts、模板引用语法 TemplateSyntax.ts)展开印证。
什么是工作流"运行"(Run)
每次工作流被触发,OneUptime 都会保存一份发生了什么的事后记录——运行的时间、是否成功、以及每个组件块具体做了什么。这份记录就是运行(Run)。它承担三个职责:
- 确认工作流正常工作:看到一次Executed运行,等于确认整条链路走完了;
- 排查工作流故障:失败的运行携带错误信息与失败步骤的完整上下文;
- 回溯历史活动:查看过去 30 天内任何一次运行的数据与轨迹。
从实现上看,运行记录对应数据库中的WorkflowLog模型,运行器在每次状态变更时都会把状态、日志文本、步骤追踪(stepTrace)和恢复数据(resumeData)写回该记录——这一点在 RunWorkflow.ts 的主流程中可以看到:创建日志时先写入Scheduled状态,随后依次改写为Running、Waiting、Success/Error/Timeout等终态。
在哪些页面查看运行记录
OneUptime 提供了三个不同粒度的查看入口,按覆盖面从大到小排列:
| 页面 | 你能看到什么 |
|---|---|
| 工作流(Workflows)→ 运行与日志(Runs & Logs) | 项目内所有工作流的全部运行记录,支持按工作流名称、状态和时间筛选 |
| 某个工作流(Workflow)→ 运行与日志(Runs & Logs) | 仅这一个工作流的运行记录;这里的筛选器不再是"工作流",而是运行 ID(Run ID) |
| 单条运行 | 通过运行行上的查看日志(View Logs)按钮打开——注意:运行行本身不可点击,必须使用按钮 |
运行状态全解析
一次运行的生命周期由多种状态描述。下表汇总了全部七种状态及其含义:
| 状态 | 含义 |
|---|---|
| 已计划(Scheduled) | 触发器已触发,运行已排队等待 Runner 拾取。通常只持续一瞬间。若一条运行超过 5 分钟仍停留在 Scheduled,即视为失败——说明没有任何 Runner 接收它 |
| 运行中(Running) | 工作流正在执行。长时间运行的组件块会让运行持续停留在此状态 |
| 等待中(Waiting) | 运行被停放在一个Sleep(休眠)组件块上,到点会自动继续。等待期间不占用任何 Worker |
| Executed | 运行顺利到达终点、没有失败。这就是"成功"状态——标签上显示的是Executed而非 "Success" |
| 错误(Error) | 运行因某个组件块抛错而停止。此外以下情况也会落入 Error:已排队的运行始终无人拾取、休眠运行的续跑丢失、定时表达式无法解析、或工作流在运行中途被停用 |
| 超时(Timeout) | 运行时长超过了允许的限额。限额配置参见 工作流配置与安全 |
| Execution Exceeded Current Plan | 项目已用尽最近 30 天的工作流运行额度,或订阅处于未付费状态。该运行会被记录但不会真正执行。仅适用于 OneUptime Cloud |
源码层面的印证:状态枚举定义在 WorkflowStatus.ts 中,内部取值为Scheduled、Running、Waiting、Success、Error、Timeout和WorkflowCountExceeded——可见 UI 标签(如 "Executed"、"Execution Exceeded Current Plan")与内部存储值并不完全一致,这是产品层的文案映射,排错时不必惊讶于这种差异。
关于错误分支与 "Executed" 的边界
需要特别澄清一个容易误判的点:一个组件块把控制流转交给它的Error输出口(例如 API 组件遇到 4xx 状态码),并不会让整次运行失败。此时错误分支照常执行,运行最终仍然以Executed结束。只是该步骤本身会被标记为红色,方便你找到它。
代码侧印证了这一点:在 RunWorkflow.ts 的recordStep中,步骤的成败判定是"是否有 errorMessage 或是否从error端口离开"(executedPort === "error"),但这只影响单步的颜色与状态;只有组件真正抛出异常(或调用options.onError)才会走didWorkflowErrorOut路径,最终把整次运行写成Error。也就是说:步骤级错误 ≠ 运行级错误,读取运行记录时必须同时注意两个层级。
如何解读一次运行
点击运行行的查看日志(View Logs)即可打开运行详情。Workflow Run视图包含两个选项卡。
Steps(步骤)选项卡
Steps按执行顺序为每个运行过的组件块渲染一行。每一行展示:
- 组件块的标题;
- 它的组件 ID(component id);
- 执行耗时;
- 它经由哪个输出口离开(显示为
→ success、→ error、→ yes等)。
展开一行,会得到两个细节区块:
- Received(收到的):变量全部解析完成后,该组件块实际拿到的配置参数;
- Returned(返回的):该组件块产生的结果。
失败的步骤以红色呈现,并且默认展开,错误信息打印在Received区块之上。
对应到数据结构,Steps 视图渲染的正是WorkflowStepTraceEntry(定义见 StepTrace.ts):每条记录包含componentId、metadataId、title、status、startedAt、completedAt、durationInMs、脱敏后的argumentValues(对应 Received)与returnValues(对应 Returned)、executedPort(离开的输出口),失败时还带errorMessage。这套结构由 Runner 写入、API 返回、Dashboard 渲染,三个环节共享同一份类型定义。
Full Log(完整日志)选项卡
Full Log是 Runner 打印的原始、逐行日志,包含所有组件块自行记录的输出。当Steps视图不足以解释失败原因时,就到这里翻原始日志。实现上,运行器将所有日志行累积在内存数组中,最后以\n拼接写入WorkflowLog.logs字段(见 RunWorkflow.ts 中对this.logs.join("\n")的使用),因此它是排查问题的最后兜底。
组件 ID 与引用语法的关系
一个值得牢记的细节:每个步骤标题下方印出的组件 ID,正是你可以直接粘进{{local.components.<id>.returnValues.…}}引用里的那串字符串——这是拿到一条正确引用的最快路径。
引用语法的完整形态定义在 TemplateSyntax.ts 中,componentReturnValueReference函数生成的引用格式为:
{{local.components.<componentId>.returnValues.<returnValueId>}}同文件还定义了另外两种根路径:本地变量{{local.variables.<name>}}(variableReference)和全局变量{{global.variables.<name>}}(globalVariableReference)。运行器在执行时正是按local.variables、local.components.<id>.returnValues、global.variables这个 storage map 结构去解析引用的。
100 步上限与值截断
一次运行只保留最近 100 个步骤。对于超长或多次被恢复续跑(多次 Sleep 恢复)的运行,被丢弃的早期步骤位置会显示一条琥珀色提示,说明此处有步骤被截断,而不是让用户误以为这是一条完整的运行。
数值上限在 StepTrace.ts 中定义:MAX_TRACE_STEPS = 100,同时MAX_TRACE_VALUE_LENGTH = 4000——单个值超过 4000 字符会被截断并追加后缀… (truncated)(即TRUNCATED_VALUE_SUFFIX)。截断策略是:字符串直接截断;结构化对象按 JSON 序列化长度判断,过长则整体替换为截断后的 JSON 文本,避免"把序列化对象拦腰切断"产生看似数据实则无法解析的内容。appendTraceStep在达到上限时丢弃最旧的步骤并置truncated: true——因为一次失败运行的排查总是从尾部往前读,丢掉的是最不重要的头部。
敏感信息脱敏
Steps 视图展示的值是变量填充后组件块实际看到的内容,但有两个例外:
- 密钥(Secrets)与组件标记为敏感(sensitive)的字段会被遮蔽(redacted);
- 超长值会被以
… (truncated)截断。
源码对此有双重保障:redactSensitiveComponentValuesForLogs按组件元数据中isSensitive标记把字段替换为WORKFLOW_LOG_REDACTED_VALUE;redactSecretValues则递归地扫描整个追踪结构(包括键名——因为工作流变量可能被替换进 JSON 属性名,比如 HTTP 头名称),把所有密钥内容擦除。cleanLogs会在每次持久化前对日志文本与步骤追踪统一执行脱敏(见 RunWorkflow.ts)。值得一提的还有:从休眠恢复(resume)的运行,其恢复数据中刻意不持久化变量,而是恢复时重新读取,从根上保证密钥不会进入resumeData。
从 Builder 启动运行:边跑边看
如果你从Builder(构建器)内直接启动一次运行,打开的正是这同一个运行详情视图,并且它会实时跟随这次运行——你可以看着它一步步执行,而不用事后再去翻找。这对于快速验证"手动运行是否正常"非常有用。
常见排错实战
场景一:"我的工作流没有运行"
按以下顺序排查:
- 确认工作流已启用:在它的Overview(概览)页面检查是否处于Enabled状态。新工作流默认是停用的,而停用的工作流会拒绝一切运行——包括手动触发;
- OneUptime 事件触发器:确认事件确实发生过——打开对应记录查看其历史;
- Webhook 触发器:确认外部系统发送到了正确的 URL——大多数工具在发出 Webhook 时都有日志,去那边查;
- 定时(Schedule)触发器:确认 cron 表达式与你预期的时间匹配。
如果运行确实出现了,但状态是Execution Exceeded Current Plan,那么说明项目已经用尽了最近 30 天的工作流运行额度,或订阅未付费。该运行的日志里会写明已用次数与当前计划的限额。此状态仅适用于 OneUptime Cloud。
补充一个源码角度的细节:如果运行一直停留在Scheduled,说明触发已发生但没有任何 Runner 拾取——运行器创建WorkflowLog时初始写入的就是Scheduled状态(见 RunWorkflow.ts),只有 Runner 真正开始执行时才会更新为Running。超过 5 分钟仍未拾取,即按文档规则视为失败。
场景二:"后面的组件块从来没执行"
某个组件块不运行,绝大多数情况是连线(wiring)问题。打开Builder检查:
- 前一个组件块的输出口是否连接到了这个组件块的输入口?
- 前一个组件块是否走了与你预期不同的输出口——走了Error而不是Success,或走了No而不是Yes?Steps选项卡会明确显示它实际走了哪个输出口(
→ error、→ yes等)。
代码层面还有一个值得知道的兜底机制:如果某个组件块抛出的错误在图中没有对应的错误分支可走,运行器会抛出异常并终止运行;而若图中出现循环依赖(某组件已执行过又被推入执行栈),运行器会抛出Cyclic Workflow Detected错误并停止执行——这类问题同样会在 Steps 视图与 Full Log 中留下痕迹。
场景三:"变量传进来是空的"
打开这次运行,查看失败步骤的Received区块:
- 如果看到的是字面的
{{local.components.…}}文本,说明引用没有被解析。这通常是组件 ID 或返回值 ID 拼写错误——记住:引用用的是组件块的Identifier(标识符),而不是它在画布上显示的名称。同时检查local.components本身的拼写:{{local.componets.api-get-1.returnValues.response-body}}会被当作字面文本原样发出,而这次运行依然报告Executed,不会报错; - 如果看到的是空字符串,说明前面的组件块虽然执行了,但没有产生这个字段。
Full Log选项卡中有一条警告行,会点名列出每一个未能解析的引用——这通常是最快的定位方式。该行为由运行器在参数替换后主动写入:logUnresolvedReferences会对比替换前后的值,凡是{{...}}原样出现在输出中的引用,都会以Warning: ... did not resolve to anything and was left as literal text的形式记录到日志(见 RunWorkflow.ts)。这条警告的意义在于:未解析的引用既不报错也不阻塞,静默地以字面文本通过,如果不主动点名,排查者根本无法区分"引用写错"与"值本来就是这段文本"。
场景四:"手动运行正常,但从触发器触发就不行"
打开Builder,点击运行工作流(Run Workflow),把触发器各字段填成真实触发器会发送的值,然后把这个运行在Received下的取值与真实运行的取值并排对比。差异通常只是一个字段名或类型。
重新执行一个工作流
没有"重试这次运行"按钮。OneUptime 不会自动重新执行旧的运行,因为运行带来的副作用——Slack 消息、API 调用、工单(tickets)——不一定能安全地重复执行。要重新完成这项工作,有两种方式:
- 修复工作流,然后让下一次真实触发器再次触发它;
- 打开Builder,使用相同的值点击运行工作流。
这一设计取舍也体现在执行模型上:触发子工作流的组件(见 Workflow 组件定义)是fire-and-forget语义——它只负责把子工作流入队,不等待其完成,因此"重复执行"必须由操作者明确发起。
运行记录保留多久
- OneUptime Cloud:运行记录保留30 天,到期后删除——这就是为什么两个运行列表都自我描述为"覆盖最近 30 天";
- 自托管(Self-hosted):运行记录一直保留,直到你手动删除。如果某个工作流运行极其频繁、把历史记录刷得杂乱,可以停用或删除该工作流,让它不再继续产生噪音。
另一个兼容性细节:在步骤追踪(step tracing)功能上线之前记录的老运行,没有 Steps 内容,只会显示Full Log。代码侧parseTrace对无法解析的历史数据会宽容地返回空追踪,视图随之回退到原始日志(见 StepTrace.ts),因此老数据不会导致页面报错。
继续阅读
- 工作流配置与安全(Configuration & Safety)——超时、递归限制、隐藏密钥;
- 工作流变量(Variables)——在组件块中使用的变量语法;
- 工作流组件(Components)——每个组件块分别产生什么。
【免费下载链接】oneuptimeComplete open-source monitoring and observability platform.项目地址: https://gitcode.com/GitHub_Trending/on/oneuptime
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考