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.
This commit is contained in:
Yoaquim Cintrón
2026-04-03 23:11:55 -04:00
parent 2b865c0694
commit 7d3de064d6
4 changed files with 65 additions and 0 deletions
+8
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)]
+24
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,
@@ -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::<ErrorMessage>(jc.result.get()) {
span.record("error.message", result_error.message.as_str());
+14
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,
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