Skip to main content

rivetkit_core/
telemetry.rs

1//! Internal OpenTelemetry spans owned by the actor runtime.
2
3#[cfg(feature = "native-runtime")]
4pub mod export;
5
6use std::sync::Arc;
7
8use opentelemetry::Context;
9use opentelemetry::propagation::{Extractor, Injector, TextMapPropagator as _};
10use opentelemetry::trace::{SpanContext, TraceContextExt as _};
11use opentelemetry_sdk::propagation::TraceContextPropagator;
12use parking_lot::Mutex;
13use tracing_opentelemetry::OpenTelemetrySpanExt as _;
14
15use crate::ActorContext;
16use crate::actor::queue::QueueMessage;
17
18/// Correlation fields accepted at an invocation boundary.
19#[derive(Debug, Default)]
20pub struct IncomingInvocationContext {
21	pub(crate) ray_id: Option<String>,
22	remote_parent: Option<SpanContext>,
23}
24
25pub(crate) use rivetkit_client_protocol::telemetry_headers::HEADER_RIVET_RAY_ID;
26
27impl IncomingInvocationContext {
28	pub(crate) fn from_headers(
29		ray_id: Option<String>,
30		traceparent: Option<&str>,
31		tracestate: Option<&str>,
32	) -> Self {
33		Self {
34			ray_id,
35			remote_parent: parse_remote_parent(traceparent, tracestate),
36		}
37	}
38
39	/// Reads the ray ID and W3C trace context an HTTP request carries. Every
40	/// HTTP entry point into an actor reads them through here, so they all
41	/// apply the same bounds.
42	pub(crate) fn from_http_headers(headers: &http::HeaderMap) -> Self {
43		Self::from_headers(
44			invocation_ray_id(headers),
45			headers
46				.get("traceparent")
47				.and_then(|value| value.to_str().ok()),
48			headers
49				.get("tracestate")
50				.and_then(|value| value.to_str().ok()),
51		)
52	}
53}
54
55/// Reads the caller's ray ID. The header is untrusted, so it is bounded by
56/// the rule shared with the clients that send it; anything else counts as
57/// absent.
58fn invocation_ray_id(headers: &http::HeaderMap) -> Option<String> {
59	let value = headers.get(HEADER_RIVET_RAY_ID)?.to_str().ok()?;
60	rivetkit_client_protocol::telemetry_headers::bounded_ray_id(value).map(str::to_owned)
61}
62
63/// Name a request invocation is reported under, in place of a caller-supplied path.
64const REQUEST_INVOCATION_NAME: &str = "onRequest";
65
66/// Name a queue send invocation is reported under. The queue itself is an attribute.
67const QUEUE_SEND_INVOCATION_NAME: &str = "queue.send";
68
69/// Name a workflow run invocation is reported under.
70const WORKFLOW_RUN_INVOCATION_NAME: &str = "workflow";
71
72/// What an invocation ran, which decides its name and the attributes that
73/// identify it on the span.
74#[derive(Clone, Copy, Debug)]
75enum InvocationSubject<'a> {
76	Action(&'a str),
77	Request { method: &'a str },
78	QueueSend { queue: &'a str },
79	WorkflowRun,
80}
81
82impl<'a> InvocationSubject<'a> {
83	fn name(self) -> &'a str {
84		match self {
85			Self::Action(name) => name,
86			Self::Request { .. } => REQUEST_INVOCATION_NAME,
87			Self::QueueSend { .. } => QUEUE_SEND_INVOCATION_NAME,
88			Self::WorkflowRun => WORKFLOW_RUN_INVOCATION_NAME,
89		}
90	}
91
92	fn record_attributes(self, span: &tracing::Span) {
93		match self {
94			Self::Action(name) => span.record("rivet.action.name", name),
95			Self::Request { method } => span.record("http.request.method", method),
96			Self::QueueSend { queue } => span.record("rivet.queue.name", queue),
97			Self::WorkflowRun => span,
98		};
99	}
100}
101
102/// Owns the complete lifecycle of one actor invocation.
103#[derive(Debug)]
104pub(crate) struct ActorInvocation {
105	telemetry: ActorInvocationTelemetry,
106}
107
108#[derive(Clone, Copy, Debug)]
109enum InvocationType {
110	Action,
111	Scheduled,
112	Request,
113	QueueSend,
114	Workflow,
115}
116
117impl InvocationType {
118	fn as_label(self) -> &'static str {
119		match self {
120			Self::Action => "action",
121			Self::Scheduled => "scheduled",
122			Self::Request => "request",
123			Self::QueueSend => "queue_send",
124			Self::Workflow => "workflow",
125		}
126	}
127}
128
129/// Opaque invocation context carried across foreign-runtime adapters.
130///
131/// Every clone of a handle shares one invocation. `application_span` is the
132/// span the host runtime had active when it resolved this handle; Core cannot
133/// see the host's span stack, so spans opened through the handle parent there
134/// when it is set and to the invocation span otherwise. `step_span` is the
135/// workflow step the handle runs inside.
136#[doc(hidden)]
137#[derive(Clone, Debug)]
138pub struct ActorInvocationTelemetry {
139	inner: Arc<InvocationInner>,
140	application_span: Option<SpanContext>,
141	step_span: Option<SpanContext>,
142}
143
144/// Identity fields that do not change while an actor is alive. Built once per
145/// actor and shared by every invocation, so starting one does not re-allocate
146/// them.
147#[derive(Debug)]
148pub(crate) struct ActorTelemetryIdentity {
149	pub(crate) actor_id: String,
150	pub(crate) actor_name: String,
151	pub(crate) actor_key: String,
152}
153
154#[derive(Debug)]
155struct InvocationInner {
156	/// A workflow run takes the ray of each queue message it receives.
157	takes_message_ray: bool,
158	// This lock is used from Drop paths, and its guard never crosses an await.
159	state: Mutex<InvocationState>,
160	identity: Arc<ActorTelemetryIdentity>,
161}
162
163#[derive(Debug)]
164struct InvocationState {
165	ray_id: Option<String>,
166	span: Option<tracing::Span>,
167	finished: bool,
168	pending_work: usize,
169}
170
171const OPERATION_ABANDONED_ERROR_TYPE: &str = "actor.operation_abandoned";
172
173/// Keeps an invocation open while one piece of `wait_until` work runs. The
174/// span is released when the last guard drops after the terminal status has
175/// been recorded, so work that settles before the reply changes nothing.
176pub(crate) struct InvocationWorkGuard(ActorInvocationTelemetry);
177
178/// Where later work came from: the ray ID of the invocation that caused it and
179/// the span that was active there. Persisted beside schedules and queue
180/// messages so the work they cause can link back to the invocation that caused it.
181#[derive(Clone, Debug, Default, Eq, PartialEq)]
182pub struct IncomingTraceContext {
183	pub ray_id: Option<String>,
184	pub traceparent: Option<String>,
185	pub tracestate: Option<String>,
186}
187
188/// Active actor invocation fields exposed to foreign-runtime adapters.
189#[doc(hidden)]
190#[derive(Clone, Debug)]
191pub struct ActorInvocationTraceContext {
192	pub ray_id: Option<String>,
193	/// Present only while the invocation runs inside a valid span.
194	pub span: Option<ActorInvocationSpanContext>,
195}
196
197/// W3C span context of the current invocation span. A span context is either
198/// complete or absent, so these fields are never optional individually.
199#[doc(hidden)]
200#[derive(Clone, Debug)]
201pub struct ActorInvocationSpanContext {
202	pub trace_id: String,
203	pub span_id: String,
204	pub trace_flags: u8,
205	pub tracestate: Option<String>,
206}
207
208/// The closed set of SQLite operations that get a span.
209///
210/// Both names are `&'static str`, so starting one of these spans allocates
211/// nothing. Adding an operation is a compile error here rather than a silently
212/// wrong span name.
213#[derive(Clone, Copy, Debug)]
214pub(crate) enum SqliteOperation {
215	Exec,
216	Execute,
217	ExecuteBatch,
218	Query,
219	Run,
220	TransactionBegin,
221	TransactionExec,
222	TransactionExecute,
223	TransactionCommit,
224	TransactionRollback,
225}
226
227impl SqliteOperation {
228	fn names(self) -> (&'static str, &'static str) {
229		match self {
230			Self::Exec => ("rivet.sqlite.exec", "exec"),
231			Self::Execute => ("rivet.sqlite.execute", "execute"),
232			Self::ExecuteBatch => ("rivet.sqlite.execute_batch", "execute_batch"),
233			Self::Query => ("rivet.sqlite.query", "query"),
234			Self::Run => ("rivet.sqlite.run", "run"),
235			Self::TransactionBegin => ("rivet.sqlite.transaction.begin", "transaction.begin"),
236			Self::TransactionExec => ("rivet.sqlite.transaction.exec", "transaction.exec"),
237			Self::TransactionExecute => ("rivet.sqlite.transaction.execute", "transaction.execute"),
238			Self::TransactionCommit => ("rivet.sqlite.transaction.commit", "transaction.commit"),
239			Self::TransactionRollback => {
240				("rivet.sqlite.transaction.rollback", "transaction.rollback")
241			}
242		}
243	}
244}
245
246pub(crate) struct SqliteOperationSpan {
247	span: Option<tracing::Span>,
248}
249
250/// One call from this invocation out to another actor, held open across a
251/// foreign-runtime boundary.
252///
253/// The call is made by the host runtime's client, so it is opened and closed by
254/// two separate calls rather than by one Rust scope. Dropping this without
255/// finishing records the call as cancelled, matching how a dropped SQLite span
256/// is treated.
257#[doc(hidden)]
258pub struct OutboundCallInvocation {
259	span: Option<tracing::Span>,
260	context: Option<ActorInvocationSpanContext>,
261}
262
263/// How one run of the workflow function ended. `Sleeping` covers a timed
264/// sleep, a wait for a message, and a retry backoff.
265#[doc(hidden)]
266#[derive(Clone, Copy, Debug, Eq, PartialEq)]
267pub enum WorkflowRunOutcome {
268	Completed,
269	Sleeping,
270	Evicted,
271	Failed,
272	Cancelled,
273}
274
275impl WorkflowRunOutcome {
276	pub fn parse(value: &str) -> anyhow::Result<Self> {
277		match value {
278			"completed" => Ok(Self::Completed),
279			"sleeping" => Ok(Self::Sleeping),
280			"evicted" => Ok(Self::Evicted),
281			"failed" => Ok(Self::Failed),
282			"cancelled" => Ok(Self::Cancelled),
283			other => anyhow::bail!("unknown workflow run outcome `{other}`"),
284		}
285	}
286
287	fn as_label(self) -> &'static str {
288		match self {
289			Self::Completed => "completed",
290			Self::Sleeping => "sleeping",
291			Self::Evicted => "evicted",
292			Self::Failed => "failed",
293			Self::Cancelled => "cancelled",
294		}
295	}
296
297	fn is_error(self) -> bool {
298		match self {
299			Self::Completed | Self::Sleeping | Self::Evicted => false,
300			Self::Failed | Self::Cancelled => true,
301		}
302	}
303}
304
305/// How one step attempt ended. `Retry` will be tried again, `Failed` will not.
306#[doc(hidden)]
307#[derive(Clone, Copy, Debug, Eq, PartialEq)]
308pub enum WorkflowStepOutcome {
309	Ok,
310	Retry,
311	Failed,
312}
313
314impl WorkflowStepOutcome {
315	pub fn parse(value: &str) -> anyhow::Result<Self> {
316		match value {
317			"ok" => Ok(Self::Ok),
318			"retry" => Ok(Self::Retry),
319			"failed" => Ok(Self::Failed),
320			other => anyhow::bail!("unknown workflow step outcome `{other}`"),
321		}
322	}
323
324	fn as_label(self) -> &'static str {
325		match self {
326			Self::Ok => "ok",
327			Self::Retry => "retry",
328			Self::Failed => "failed",
329		}
330	}
331}
332
333/// One run of the workflow function. Dropping this without finishing records
334/// the run as abandoned.
335#[doc(hidden)]
336pub struct WorkflowRunInvocation {
337	ctx: ActorContext,
338	invocation: ActorInvocation,
339}
340
341/// One attempt at one workflow step. Dropping this without finishing records
342/// the attempt as abandoned.
343#[doc(hidden)]
344pub struct WorkflowStepSpan {
345	ctx: ActorContext,
346	span: Option<tracing::Span>,
347}
348
349impl ActorInvocation {
350	pub(crate) fn start_action(
351		ctx: &ActorContext,
352		action_name: &str,
353		incoming: IncomingInvocationContext,
354	) -> Self {
355		Self::start(
356			ctx,
357			InvocationSubject::Action(action_name),
358			InvocationType::Action,
359			incoming.ray_id,
360			incoming.remote_parent,
361			None,
362		)
363	}
364
365	pub(crate) fn start_scheduled(
366		ctx: &ActorContext,
367		action_name: &str,
368		trace_context: IncomingTraceContext,
369	) -> Self {
370		let remote_parent = parse_remote_parent(
371			trace_context.traceparent.as_deref(),
372			trace_context.tracestate.as_deref(),
373		);
374		Self::start(
375			ctx,
376			InvocationSubject::Action(action_name),
377			InvocationType::Scheduled,
378			trace_context.ray_id,
379			None,
380			remote_parent,
381		)
382	}
383
384	/// Starts a new trace for one workflow run and links it to the `previous`
385	/// run.
386	fn start_workflow_run(ctx: &ActorContext, previous: IncomingTraceContext) -> Self {
387		let previous_run = parse_remote_parent(
388			previous.traceparent.as_deref(),
389			previous.tracestate.as_deref(),
390		);
391		Self::start(
392			ctx,
393			InvocationSubject::WorkflowRun,
394			InvocationType::Workflow,
395			previous.ray_id,
396			None,
397			previous_run,
398		)
399	}
400
401	/// Starts the invocation for one message sent into `queue_name` from
402	/// outside the actor. It ends when the send is acknowledged, or when the
403	/// sender's wait for a completion ends.
404	pub(crate) fn start_queue_send(
405		ctx: &ActorContext,
406		queue_name: &str,
407		incoming: IncomingInvocationContext,
408	) -> Self {
409		Self::start(
410			ctx,
411			InvocationSubject::QueueSend { queue: queue_name },
412			InvocationType::QueueSend,
413			incoming.ray_id,
414			incoming.remote_parent,
415			None,
416		)
417	}
418
419	/// Starts the invocation for one raw HTTP request served by `onRequest`.
420	/// The span is named after the handler rather than the path, because a
421	/// path is caller-supplied and would make the name a cardinality surface.
422	pub(crate) fn start_request(
423		ctx: &ActorContext,
424		request: &crate::actor::messages::Request,
425		incoming: IncomingInvocationContext,
426	) -> Self {
427		Self::start(
428			ctx,
429			InvocationSubject::Request {
430				method: request.method().as_str(),
431			},
432			InvocationType::Request,
433			incoming.ray_id,
434			incoming.remote_parent,
435			None,
436		)
437	}
438
439	fn start(
440		ctx: &ActorContext,
441		subject: InvocationSubject<'_>,
442		invocation_type: InvocationType,
443		ray_id: Option<String>,
444		parent: Option<SpanContext>,
445		link: Option<SpanContext>,
446	) -> Self {
447		let identity = ctx.telemetry_identity();
448		let subject_name = subject.name();
449		let span = if tracing::enabled!(target: "rivetkit::telemetry", tracing::Level::INFO) {
450			let span = tracing::info_span!(
451				target: "rivetkit::telemetry",
452				parent: None,
453				"rivet.actor.invoke",
454				otel.name = %format!("{}/{}", identity.actor_name, subject_name),
455				otel.kind = otel_kind(invocation_type),
456				rivet.invocation.type = invocation_type.as_label(),
457				rivet.actor.id = %identity.actor_id,
458				rivet.actor.name = %identity.actor_name,
459				rivet.actor.key = %identity.actor_key,
460				rivet.action.name = tracing::field::Empty,
461				rivet.ray.id = tracing::field::Empty,
462				http.request.method = tracing::field::Empty,
463				http.response.status_code = tracing::field::Empty,
464				rivet.queue.name = tracing::field::Empty,
465				rivet.workflow.run.outcome = tracing::field::Empty,
466				otel.status_code = tracing::field::Empty,
467				error.type = tracing::field::Empty,
468			);
469			if let Some(ray_id) = ray_id.as_deref() {
470				span.record("rivet.ray.id", ray_id);
471			}
472			subject.record_attributes(&span);
473			if let Some(parent) = parent {
474				span.set_parent(Context::new().with_remote_span_context(parent));
475			}
476			if let Some(link) = link {
477				span.add_link(link);
478			}
479			Some(span)
480		} else {
481			None
482		};
483
484		Self {
485			telemetry: ActorInvocationTelemetry::new(invocation_type, ray_id, span, identity),
486		}
487	}
488
489	pub(crate) fn telemetry(&self) -> ActorInvocationTelemetry {
490		self.telemetry.clone()
491	}
492
493	pub(crate) fn finish(self, error: Option<&anyhow::Error>) {
494		self.telemetry.finish(error);
495	}
496
497	pub(crate) fn finish_request(
498		self,
499		response: std::result::Result<&crate::actor::messages::ActorHttpResponse, &anyhow::Error>,
500	) {
501		match response {
502			Ok(response) => {
503				let status = response.status();
504				self.telemetry.finish_with(|span| {
505					span.record("http.response.status_code", status);
506					if status >= 500 {
507						span.record("otel.status_code", "ERROR");
508						span.record("error.type", status.to_string());
509					} else {
510						record_outcome(span, None);
511					}
512				});
513			}
514			Err(error) => self.finish(Some(error)),
515		}
516	}
517}
518
519impl WorkflowRunInvocation {
520	pub(crate) fn start(ctx: &ActorContext, previous: IncomingTraceContext) -> Self {
521		let invocation = ActorInvocation::start_workflow_run(ctx, previous);
522		Self {
523			ctx: ctx
524				.clone()
525				.with_invocation_telemetry(Some(invocation.telemetry())),
526			invocation,
527		}
528	}
529
530	/// The context workflow code runs under.
531	pub fn ctx(&self) -> ActorContext {
532		self.ctx.clone()
533	}
534
535	/// Returns the span and ray the next run continues, or nothing when
536	/// tracing is off.
537	pub(crate) fn finish(self, outcome: WorkflowRunOutcome) -> Option<IncomingTraceContext> {
538		let ray_id = self.invocation.telemetry.ray_id();
539		let mut trace_context = None;
540		self.invocation.telemetry.finish_with(|span| {
541			if let Some(ray_id) = ray_id.as_deref() {
542				span.record("rivet.ray.id", ray_id);
543			}
544			span.record("rivet.workflow.run.outcome", outcome.as_label());
545			span.record(
546				"otel.status_code",
547				if outcome.is_error() { "ERROR" } else { "OK" },
548			);
549			let headers = otel_span_context_of(span)
550				.map(|span_context| w3c_trace_headers(&span_context))
551				.unwrap_or_default();
552			trace_context = Some(IncomingTraceContext {
553				ray_id,
554				traceparent: headers.traceparent,
555				tracestate: headers.tracestate,
556			});
557		});
558		trace_context
559	}
560}
561
562impl Drop for WorkflowRunInvocation {
563	fn drop(&mut self) {
564		let ray_id = self.invocation.telemetry.ray_id();
565		self.invocation.telemetry.finish_with(|span| {
566			if let Some(ray_id) = ray_id.as_deref() {
567				span.record("rivet.ray.id", ray_id);
568			}
569			span.record("otel.status_code", "ERROR");
570			span.record("error.type", OPERATION_ABANDONED_ERROR_TYPE);
571		});
572	}
573}
574
575impl Drop for ActorInvocation {
576	fn drop(&mut self) {
577		self.telemetry.finish_dropped();
578	}
579}
580
581impl ActorInvocationTelemetry {
582	fn new(
583		invocation_type: InvocationType,
584		ray_id: Option<String>,
585		span: Option<tracing::Span>,
586		identity: Arc<ActorTelemetryIdentity>,
587	) -> Self {
588		Self {
589			inner: Arc::new(InvocationInner {
590				takes_message_ray: matches!(invocation_type, InvocationType::Workflow),
591				state: Mutex::new(InvocationState {
592					ray_id,
593					span,
594					finished: false,
595					pending_work: 0,
596				}),
597				identity,
598			}),
599			application_span: None,
600			step_span: None,
601		}
602	}
603
604	fn ray_id(&self) -> Option<String> {
605		self.inner.state.lock().ray_id.clone()
606	}
607
608	/// Records the ray on a span opened inside this invocation. The ray is
609	/// borrowed under the lock, so this allocates nothing.
610	fn record_ray(&self, span: &tracing::Span) {
611		if let Some(ray_id) = self.inner.state.lock().ray_id.as_deref() {
612			span.record("rivet.ray.id", ray_id);
613		}
614	}
615
616	/// Returns a handle for the same invocation whose spans parent to the
617	/// application span identified by `traceparent` and `tracestate`. Absent
618	/// or invalid context, or the invocation span itself, which the host sees
619	/// as active when no application span is open, yields a handle that
620	/// parents to the invocation span.
621	pub(crate) fn with_application_span(
622		&self,
623		traceparent: Option<&str>,
624		tracestate: Option<&str>,
625	) -> Self {
626		let application_span = parse_remote_parent(traceparent, tracestate).filter(|parent| {
627			self.inner
628				.state
629				.lock()
630				.span
631				.as_ref()
632				.and_then(otel_span_context_of)
633				.map_or(true, |own| own.span_id() != parent.span_id())
634		});
635		Self {
636			inner: self.inner.clone(),
637			application_span,
638			step_span: self.step_span.clone(),
639		}
640	}
641
642	/// The context a span opened through this handle parents to: the
643	/// application span when set, else the workflow step it runs inside, else
644	/// the invocation span while it is open.
645	fn parent_context(&self) -> Option<Context> {
646		let state = self.inner.state.lock();
647		if state.finished && state.pending_work == 0 {
648			return None;
649		}
650		match self.application_span.as_ref().or(self.step_span.as_ref()) {
651			Some(span_context) => {
652				Some(Context::new().with_remote_span_context(span_context.clone()))
653			}
654			None => state.span.as_ref().map(tracing::Span::context),
655		}
656	}
657
658	/// Registers work that outlives the reply, so the invocation span stays
659	/// open and keeps parenting operations until the returned guard drops.
660	pub(crate) fn hold_open(&self) -> Option<InvocationWorkGuard> {
661		let mut state = self.inner.state.lock();
662		if state.finished {
663			return None;
664		}
665		state.pending_work += 1;
666		Some(InvocationWorkGuard(self.clone()))
667	}
668
669	/// Returns correlation fields only while this actor invocation is active.
670	#[doc(hidden)]
671	pub fn trace_context(&self) -> Option<ActorInvocationTraceContext> {
672		let (ray_id, span) = {
673			let state = self.inner.state.lock();
674			if state.finished && state.pending_work == 0 {
675				return None;
676			}
677			(state.ray_id.clone(), state.span.clone())
678		};
679		let span = match &self.step_span {
680			Some(step_span) => w3c_span_context(step_span),
681			None => span.and_then(|span| span_context_of(&span)),
682		};
683
684		Some(ActorInvocationTraceContext { ray_id, span })
685	}
686
687	/// Trace context that work caused by this invocation records: the invocation's
688	/// ray ID, and the application span active in the host runtime at that
689	/// moment, or the invocation span when there was none. Work that links
690	/// back to it then points at the code that caused it rather than at the
691	/// whole invocation around that code.
692	pub(crate) fn incoming_trace_context(&self) -> IncomingTraceContext {
693		let Some(parent) = self.parent_context() else {
694			return IncomingTraceContext::default();
695		};
696		let parent_span = parent.span();
697		let headers = w3c_trace_headers(parent_span.span_context());
698		IncomingTraceContext {
699			ray_id: self.ray_id(),
700			traceparent: headers.traceparent,
701			tracestate: headers.tracestate,
702		}
703	}
704
705	/// Opens the span covering one call out to another actor.
706	///
707	/// The callee parents to this span rather than to the invocation making the
708	/// call, so the time spent reaching it, which includes routing and waking a
709	/// sleeping actor, is attributed to the call instead of falling in the gap
710	/// between the two invocations.
711	/// When this handle carries the application span active in the host
712	/// runtime, the call span parents there instead, which is what puts a
713	/// callee under the application span that issued the call rather than
714	/// beside it.
715	pub(crate) fn start_outbound_call(
716		&self,
717		actor_name: &str,
718		action_name: &str,
719	) -> Option<OutboundCallInvocation> {
720		let parent = self.parent_context()?;
721		let span = tracing::info_span!(
722			target: "rivetkit::telemetry",
723			parent: None,
724			"rivet.actor.call",
725			otel.name = %format!("{actor_name}/{action_name}"),
726			otel.kind = "client",
727			rivet.actor.name = %actor_name,
728			rivet.action.name = %action_name,
729			rivet.ray.id = tracing::field::Empty,
730			otel.status_code = tracing::field::Empty,
731			error.type = tracing::field::Empty,
732		);
733		self.record_ray(&span);
734		span.set_parent(parent);
735		let context = span_context_of(&span);
736		Some(OutboundCallInvocation {
737			span: Some(span),
738			context,
739		})
740	}
741
742	/// Opens the span for one step attempt. Spans opened through the returned
743	/// handle parent to it.
744	pub(crate) fn start_workflow_step(
745		&self,
746		step_name: &str,
747		attempt: u32,
748	) -> Option<(tracing::Span, Self)> {
749		let parent = self.parent_context()?;
750		let span = tracing::info_span!(
751			target: "rivetkit::telemetry",
752			parent: None,
753			"rivet.workflow.step",
754			otel.name = %format!("{}/{}", self.inner.identity.actor_name, step_name),
755			otel.kind = "internal",
756			rivet.workflow.step.name = %step_name,
757			rivet.workflow.step.attempt = attempt,
758			rivet.workflow.step.outcome = tracing::field::Empty,
759			rivet.ray.id = tracing::field::Empty,
760			rivet.actor.id = %self.inner.identity.actor_id,
761			rivet.actor.name = %self.inner.identity.actor_name,
762			rivet.actor.key = %self.inner.identity.actor_key,
763			otel.status_code = tracing::field::Empty,
764			error.type = tracing::field::Empty,
765		);
766		self.record_ray(&span);
767		span.set_parent(parent);
768		let step_telemetry = Self {
769			inner: self.inner.clone(),
770			application_span: None,
771			step_span: otel_span_context_of(&span),
772		};
773		Some((span, step_telemetry))
774	}
775
776	pub(crate) fn start_sqlite(&self, operation: SqliteOperation) -> Option<SqliteOperationSpan> {
777		let parent = self.parent_context()?;
778		let (span_name, operation_name) = operation.names();
779		let span = tracing::info_span!(
780			target: "rivetkit::telemetry",
781			parent: None,
782			"rivet.sqlite.operation",
783			otel.name = span_name,
784			otel.kind = "internal",
785			rivet.operation.system = "sqlite",
786			rivet.operation.name = operation_name,
787			rivet.ray.id = tracing::field::Empty,
788			rivet.actor.id = %self.inner.identity.actor_id,
789			rivet.actor.name = %self.inner.identity.actor_name,
790			rivet.actor.key = %self.inner.identity.actor_key,
791			otel.status_code = tracing::field::Empty,
792			error.type = tracing::field::Empty,
793		);
794		self.record_ray(&span);
795		span.set_parent(parent);
796		Some(SqliteOperationSpan { span: Some(span) })
797	}
798
799	fn finish(&self, error: Option<&anyhow::Error>) {
800		self.finish_with(|span| record_outcome(span, error));
801	}
802
803	fn finish_dropped(&self) {
804		self.finish_with(|span| {
805			span.record("otel.status_code", "ERROR");
806			span.record("error.type", "actor.dropped_reply");
807		});
808	}
809
810	fn finish_with(&self, record: impl FnOnce(&tracing::Span)) {
811		let mut state = self.inner.state.lock();
812		if state.finished {
813			return;
814		}
815		if let Some(span) = state.span.as_ref() {
816			record(span);
817			if state.pending_work > 0 {
818				tracing::info!(target: "rivetkit::telemetry", parent: span, "reply sent");
819			}
820		}
821		state.finished = true;
822		if state.pending_work == 0 {
823			state.span.take();
824		}
825	}
826}
827
828impl Drop for InvocationWorkGuard {
829	fn drop(&mut self) {
830		let mut state = self.0.inner.state.lock();
831		state.pending_work -= 1;
832		if state.pending_work == 0 && state.finished {
833			state.span.take();
834		}
835	}
836}
837
838impl OutboundCallInvocation {
839	/// W3C context of this call's span, to send to the callee so it parents
840	/// here. Absent when the call is not sampled.
841	pub fn span_context(&self) -> Option<ActorInvocationSpanContext> {
842		self.context.clone()
843	}
844
845	/// Records the call's outcome. `error` is the failure the callee returned,
846	/// and its group and code become the span's `error.type`.
847	pub fn finish(mut self, error: Option<&anyhow::Error>) {
848		let Some(span) = self.span.take() else {
849			return;
850		};
851		record_outcome(&span, error);
852	}
853}
854
855impl Drop for OutboundCallInvocation {
856	fn drop(&mut self) {
857		let Some(span) = self.span.take() else {
858			return;
859		};
860		span.record("otel.status_code", "ERROR");
861		span.record("error.type", OPERATION_ABANDONED_ERROR_TYPE);
862	}
863}
864
865impl WorkflowStepSpan {
866	pub(crate) fn start(ctx: &ActorContext, step_name: &str, attempt: u32) -> Option<Self> {
867		let (span, step_telemetry) = ctx
868			.invocation_telemetry()?
869			.start_workflow_step(step_name, attempt)?;
870		Some(Self {
871			ctx: ctx.clone().with_invocation_telemetry(Some(step_telemetry)),
872			span: Some(span),
873		})
874	}
875
876	/// The context the step's callback runs under.
877	pub fn ctx(&self) -> ActorContext {
878		self.ctx.clone()
879	}
880
881	/// `error` is what the step threw. Its group and code become `error.type`.
882	pub fn finish(mut self, outcome: WorkflowStepOutcome, error: Option<&anyhow::Error>) {
883		let Some(span) = self.span.take() else {
884			return;
885		};
886		span.record("rivet.workflow.step.outcome", outcome.as_label());
887		record_outcome(&span, error);
888	}
889}
890
891impl Drop for WorkflowStepSpan {
892	fn drop(&mut self) {
893		let Some(span) = self.span.take() else {
894			return;
895		};
896		span.record("otel.status_code", "ERROR");
897		span.record("error.type", OPERATION_ABANDONED_ERROR_TYPE);
898	}
899}
900
901impl SqliteOperationSpan {
902	pub(crate) fn span(&self) -> tracing::Span {
903		self.span.as_ref().expect("sqlite span is present").clone()
904	}
905
906	pub(crate) fn finish(&mut self, error: Option<&anyhow::Error>) {
907		let Some(span) = self.span.take() else {
908			return;
909		};
910		record_outcome(&span, error);
911	}
912}
913
914impl Drop for SqliteOperationSpan {
915	fn drop(&mut self) {
916		let Some(span) = self.span.take() else {
917			return;
918		};
919		span.record("otel.status_code", "ERROR");
920		span.record("error.type", OPERATION_ABANDONED_ERROR_TYPE);
921	}
922}
923
924fn otel_span_context_of(span: &tracing::Span) -> Option<SpanContext> {
925	let context = span.context();
926	let context_span = context.span();
927	let span_context = context_span.span_context();
928	if span_context.is_valid() {
929		Some(span_context.clone())
930	} else {
931		None
932	}
933}
934
935/// W3C fields of a span context, or nothing when it is not valid and so
936/// carries nothing worth propagating.
937fn w3c_span_context(span_context: &SpanContext) -> Option<ActorInvocationSpanContext> {
938	if !span_context.is_valid() {
939		return None;
940	}
941	let tracestate = span_context.trace_state().header();
942	Some(ActorInvocationSpanContext {
943		trace_id: span_context.trace_id().to_string(),
944		span_id: span_context.span_id().to_string(),
945		trace_flags: span_context.trace_flags().to_u8(),
946		tracestate: if tracestate.is_empty() {
947			None
948		} else {
949			Some(tracestate)
950		},
951	})
952}
953
954fn w3c_trace_headers(span_context: &SpanContext) -> OwnedTraceHeaders {
955	if !span_context.is_valid() {
956		return OwnedTraceHeaders::default();
957	}
958	let context = Context::new().with_remote_span_context(span_context.clone());
959	let mut headers = OwnedTraceHeaders::default();
960	TraceContextPropagator::new().inject_context(&context, &mut headers);
961	headers
962}
963
964/// An action or a raw HTTP request is entered from outside the actor, a
965/// scheduled fire and a workflow run originate inside it, and a queue send
966/// produces a message the actor consumes later.
967fn otel_kind(invocation_type: InvocationType) -> &'static str {
968	match invocation_type {
969		InvocationType::Action | InvocationType::Request => "server",
970		InvocationType::Scheduled | InvocationType::Workflow => "internal",
971		InvocationType::QueueSend => "producer",
972	}
973}
974
975/// Reads a span's W3C context, or nothing when it carries no valid context to
976/// propagate.
977fn span_context_of(span: &tracing::Span) -> Option<ActorInvocationSpanContext> {
978	let context = span.context();
979	let context_span = context.span();
980	w3c_span_context(context_span.span_context())
981}
982
983/// Opens the span covering the moment one queue message is handed to the
984/// actor. It links to the span that sent the message, which is what connects
985/// the consumer's trace to the sender's. Inside an invocation it sits under
986/// that invocation's application span, or the invocation span, and carries its
987/// ray ID. Outside one, as from the run handler, it is a root span carrying the
988/// ray ID the message was sent under. It closes when the caller drops it, which
989/// `try_receive_batch` does as it hands the message back.
990pub(crate) fn start_queue_receive(ctx: &ActorContext, message: &QueueMessage) -> tracing::Span {
991	if !tracing::enabled!(target: "rivetkit::telemetry", tracing::Level::INFO) {
992		return tracing::Span::none();
993	}
994	let identity = ctx.telemetry_identity();
995	let span = tracing::info_span!(
996		target: "rivetkit::telemetry",
997		parent: None,
998		"rivet.queue.receive",
999		otel.name = %format!("{}/queue.receive", identity.actor_name),
1000		otel.kind = "consumer",
1001		rivet.actor.id = %identity.actor_id,
1002		rivet.actor.name = %identity.actor_name,
1003		rivet.actor.key = %identity.actor_key,
1004		rivet.queue.name = message.name,
1005		rivet.ray.id = tracing::field::Empty,
1006	);
1007	let invocation_parent = ctx
1008		.invocation_telemetry()
1009		.and_then(|telemetry| telemetry.parent_context().map(|parent| (telemetry, parent)));
1010	if let Some((telemetry, parent)) = invocation_parent {
1011		let message_ray_id = message.trace_context.ray_id.as_deref();
1012		let mut state = telemetry.inner.state.lock();
1013		if let (true, Some(message_ray_id)) = (telemetry.inner.takes_message_ray, message_ray_id) {
1014			state.ray_id = Some(message_ray_id.to_owned());
1015		}
1016		if let Some(ray_id) = state.ray_id.as_deref().or(message_ray_id) {
1017			span.record("rivet.ray.id", ray_id);
1018		}
1019		drop(state);
1020		span.set_parent(parent);
1021	} else if let Some(ray_id) = &message.trace_context.ray_id {
1022		span.record("rivet.ray.id", ray_id);
1023	}
1024	if let Some(link) = parse_remote_parent(
1025		message.trace_context.traceparent.as_deref(),
1026		message.trace_context.tracestate.as_deref(),
1027	) {
1028		span.add_link(link);
1029	}
1030	span
1031}
1032
1033/// Records the terminal status and error identity of a finished span.
1034fn record_outcome(span: &tracing::Span, error: Option<&anyhow::Error>) {
1035	span.record(
1036		"otel.status_code",
1037		if error.is_none() { "OK" } else { "ERROR" },
1038	);
1039	if let Some(error) = error {
1040		let error = rivet_error::RivetError::extract(error);
1041		span.record("error.type", format!("{}.{}", error.group(), error.code()));
1042	}
1043}
1044
1045fn parse_remote_parent(traceparent: Option<&str>, tracestate: Option<&str>) -> Option<SpanContext> {
1046	traceparent?;
1047	let context = TraceContextPropagator::new().extract(&TraceHeaders {
1048		traceparent,
1049		tracestate,
1050	});
1051	let span = context.span();
1052	if span.span_context().is_valid() {
1053		Some(span.span_context().clone())
1054	} else {
1055		None
1056	}
1057}
1058
1059struct TraceHeaders<'a> {
1060	traceparent: Option<&'a str>,
1061	tracestate: Option<&'a str>,
1062}
1063
1064#[derive(Default)]
1065struct OwnedTraceHeaders {
1066	traceparent: Option<String>,
1067	tracestate: Option<String>,
1068}
1069
1070impl Injector for OwnedTraceHeaders {
1071	fn set(&mut self, key: &str, value: String) {
1072		match key {
1073			"traceparent" => self.traceparent = Some(value),
1074			"tracestate" if !value.is_empty() => self.tracestate = Some(value),
1075			_ => {}
1076		}
1077	}
1078}
1079
1080impl Extractor for TraceHeaders<'_> {
1081	fn get(&self, key: &str) -> Option<&str> {
1082		match key {
1083			"traceparent" => self.traceparent,
1084			"tracestate" => self.tracestate,
1085			_ => None,
1086		}
1087	}
1088
1089	fn keys(&self) -> Vec<&str> {
1090		let mut keys = Vec::with_capacity(2);
1091		if self.traceparent.is_some() {
1092			keys.push("traceparent");
1093		}
1094		if self.tracestate.is_some() {
1095			keys.push("tracestate");
1096		}
1097		keys
1098	}
1099}