From 7d3de064d6d6a2a2657576d5df4ab899ddcfefae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yoaquim=20Cintr=C3=B3n?= Date: Fri, 3 Apr 2026 23:11:55 -0400 Subject: [PATCH] feat: enrich OTEL spans with job_kind, trigger_kind, trigger, created_by, and script_hash MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- backend/tests/otel.rs | 8 +++++++ backend/windmill-types/src/jobs.rs | 24 +++++++++++++++++++ .../windmill-worker/src/result_processor.rs | 19 +++++++++++++++ backend/windmill-worker/src/worker.rs | 14 +++++++++++ 4 files changed, 65 insertions(+) diff --git a/backend/tests/otel.rs b/backend/tests/otel.rs index dd56cbf425..7b4f7c5ec4 100644 --- a/backend/tests/otel.rs +++ b/backend/tests/otel.rs @@ -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)] diff --git a/backend/windmill-types/src/jobs.rs b/backend/windmill-types/src/jobs.rs index 3c26c96a70..1eed946ad8 100644 --- a/backend/windmill-types/src/jobs.rs +++ b/backend/windmill-types/src/jobs.rs @@ -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, diff --git a/backend/windmill-worker/src/result_processor.rs b/backend/windmill-worker/src/result_processor.rs index e6faf74f69..91d22fb183 100644 --- a/backend/windmill-worker/src/result_processor.rs +++ b/backend/windmill-worker/src/result_processor.rs @@ -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, + script_hash = 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, + script_hash = 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(script_hash) = jc.job.runnable_id.as_ref() { + span.record("script_hash", script_hash.to_string().as_str()); + } if !success { if let Ok(result_error) = serde_json::from_str::(jc.result.get()) { span.record("error.message", result_error.message.as_str()); diff --git a/backend/windmill-worker/src/worker.rs b/backend/windmill-worker/src/worker.rs index bea06588ed..67b3580a40 100644 --- a/backend/windmill-worker/src/worker.rs +++ b/backend/windmill-worker/src/worker.rs @@ -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, + script_hash = 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(script_hash) = arc_job.runnable_id.as_ref() { + span.record("script_hash", script_hash.to_string().as_str()); + } windmill_common::otel_oss::set_span_parent(&span, &rj); span