1#[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#[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 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
55fn 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
63const REQUEST_INVOCATION_NAME: &str = "onRequest";
65
66const QUEUE_SEND_INVOCATION_NAME: &str = "queue.send";
68
69const WORKFLOW_RUN_INVOCATION_NAME: &str = "workflow";
71
72#[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#[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#[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#[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 takes_message_ray: bool,
158 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
173pub(crate) struct InvocationWorkGuard(ActorInvocationTelemetry);
177
178#[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#[doc(hidden)]
190#[derive(Clone, Debug)]
191pub struct ActorInvocationTraceContext {
192 pub ray_id: Option<String>,
193 pub span: Option<ActorInvocationSpanContext>,
195}
196
197#[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#[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#[doc(hidden)]
258pub struct OutboundCallInvocation {
259 span: Option<tracing::Span>,
260 context: Option<ActorInvocationSpanContext>,
261}
262
263#[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#[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#[doc(hidden)]
336pub struct WorkflowRunInvocation {
337 ctx: ActorContext,
338 invocation: ActorInvocation,
339}
340
341#[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 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 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 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 pub fn ctx(&self) -> ActorContext {
532 self.ctx.clone()
533 }
534
535 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 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 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 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 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 #[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 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 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 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 pub fn span_context(&self) -> Option<ActorInvocationSpanContext> {
842 self.context.clone()
843 }
844
845 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 pub fn ctx(&self) -> ActorContext {
878 self.ctx.clone()
879 }
880
881 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
935fn 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
964fn 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
975fn 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
983pub(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
1033fn 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}