Compare commits

...

2 Commits

Author SHA1 Message Date
Ruben Fiszel
f71c4e8c98 refactor: rename script_hash OTEL span attribute to runnable_id
The underlying field on the job struct is `runnable_id` (can represent
script, flow_node, or app_script hashes depending on JobKind). Naming
the span attribute `script_hash` was misleading for flow and app
runnables. Rename to `runnable_id` for consistency and generality.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 14:10:59 +00:00
Yoaquim Cintrón
7d3de064d6 feat: enrich OTEL spans with job_kind, trigger_kind, trigger, created_by, and script_hash
Add five new attributes to the `job` and `job_postprocessing` tracing spans
so that OTEL-consuming backends (Sentry, Honeycomb, Datadog, etc.) can
filter and group telemetry by how a job was triggered and what type it is.

New span attributes:
- `job_kind`     — Script, Flow, AppScript, AIAgent, Preview, etc.
- `created_by`   — the user or system identity that queued the job
- `trigger_kind` — schedule, webhook, kafka, http, sqs, etc.
- `trigger`      — the schedule/trigger path (when applicable)
- `script_hash`  — hex hash of the script version that ran

Also adds `JobKind::as_str()` for a consistent lowercase string
representation, following the same pattern as `ScriptLang::as_str()`.

Existing attributes (job_id, workspace_id, script_path, language, tag,
flow_step_id, parent_job, root_job) are unchanged.

Note: the EE `full_job` span in `otel_ee.rs` and the log records emitted
by `job_logger_ee.rs` would also benefit from these attributes. This PR
covers only the public-repo spans; a follow-up EE change would propagate
the same fields to logs and the full_job span.
2026-04-03 23:11:55 -04:00
4 changed files with 65 additions and 0 deletions

View File

@@ -395,6 +395,14 @@ async fn test_root_job_span_created_on_success() {
attrs.contains(&"script_path"),
"missing script_path attribute"
);
assert!(
attrs.contains(&"job_kind"),
"missing job_kind attribute"
);
assert!(
attrs.contains(&"created_by"),
"missing created_by attribute"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]

View File

@@ -105,6 +105,30 @@ pub enum JobStatus {
}
impl JobKind {
pub fn as_str(&self) -> &'static str {
match self {
JobKind::Script => "script",
JobKind::Script_Hub => "script_hub",
JobKind::Preview => "preview",
JobKind::Dependencies => "dependencies",
JobKind::Flow => "flow",
JobKind::FlowPreview => "flowpreview",
JobKind::SingleStepFlow => "singlestepflow",
JobKind::Identity => "identity",
JobKind::FlowDependencies => "flowdependencies",
JobKind::AppDependencies => "appdependencies",
JobKind::Noop => "noop",
JobKind::DeploymentCallback => "deploymentcallback",
JobKind::FlowScript => "flowscript",
JobKind::FlowNode => "flownode",
JobKind::AppScript => "appscript",
JobKind::AIAgent => "aiagent",
JobKind::UnassignedScript => "unassigned_script",
JobKind::UnassignedFlow => "unassigned_flow",
JobKind::UnassignedSinglestepFlow => "unassigned_singlestepflow",
}
}
pub fn is_flow(&self) -> bool {
matches!(
self,

View File

@@ -86,6 +86,11 @@ async fn process_jc(
script_path = field::Empty,
flow_step_id = field::Empty,
parent_job = field::Empty,
job_kind = %jc.job.kind.as_str(),
created_by = %jc.job.created_by,
trigger_kind = field::Empty,
trigger = field::Empty,
runnable_id = field::Empty,
otel.name = field::Empty,
success = %success,
labels = field::Empty,
@@ -100,6 +105,11 @@ async fn process_jc(
script_path = field::Empty,
flow_step_id = field::Empty,
parent_job = field::Empty,
job_kind = %jc.job.kind.as_str(),
created_by = %jc.job.created_by,
trigger_kind = field::Empty,
trigger = field::Empty,
runnable_id = field::Empty,
otel.name = field::Empty,
success = %success,
error.message = field::Empty,
@@ -141,6 +151,15 @@ async fn process_jc(
if let Some(root_job) = jc.job.flow_innermost_root_job.as_ref() {
span.record("root_job", root_job.to_string().as_str());
}
if let Some(trigger_kind) = jc.job.trigger_kind.as_ref() {
span.record("trigger_kind", trigger_kind.to_string().as_str());
}
if let Some(trigger) = jc.job.trigger.as_ref() {
span.record("trigger", trigger.as_str());
}
if let Some(runnable_id) = jc.job.runnable_id.as_ref() {
span.record("runnable_id", runnable_id.to_string().as_str());
}
if !success {
if let Ok(result_error) = serde_json::from_str::<ErrorMessage>(jc.result.get()) {
span.record("error.message", result_error.message.as_str());

View File

@@ -1316,6 +1316,11 @@ pub fn create_span_with_name(
script_path = field::Empty,
flow_step_id = field::Empty,
parent_job = field::Empty,
job_kind = %arc_job.kind.as_str(),
created_by = %arc_job.created_by,
trigger_kind = field::Empty,
trigger = field::Empty,
runnable_id = field::Empty,
otel.name = field::Empty
);
@@ -1342,6 +1347,15 @@ pub fn create_span_with_name(
if let Some(hostname) = hostname {
span.record("hostname", hostname);
}
if let Some(trigger_kind) = arc_job.trigger_kind.as_ref() {
span.record("trigger_kind", trigger_kind.to_string().as_str());
}
if let Some(trigger) = arc_job.trigger.as_ref() {
span.record("trigger", trigger.as_str());
}
if let Some(runnable_id) = arc_job.runnable_id.as_ref() {
span.record("runnable_id", runnable_id.to_string().as_str());
}
windmill_common::otel_oss::set_span_parent(&span, &rj);
span