use alloc::borrow::Cow;
use alloc::format;
use alloc::string::ToString;
use alloc::vec;
use alloc::vec::Vec;
use core::convert::TryFrom;
use core::fmt;
use core::fmt::Display;
use ref_str::RefStr;
use crate::render_impl::DiagnosticIr;
use crate::report::{
Attachment, AttachmentValue, CauseTraversalState, ContextValue, ErrorCode, HasSeverity,
SourceErrorChain, StackTrace,
};
use crate::utils::{ParentSpanId, SpanId, TraceId, TraceState};
#[cfg(feature = "opentelemetry")]
#[path = "otel/opentelemetry.rs"]
pub mod opentelemetry;
#[cfg(feature = "opentelemetry")]
pub use opentelemetry::{OtelSdkEmitStats, OtelSdkEmitter};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(transparent)]
#[cfg_attr(feature = "json", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "json", serde(try_from = "u8", into = "u8"))]
pub struct OtelSeverityNumber(u8);
impl OtelSeverityNumber {
pub const TRACE: Self = Self(1);
pub const DEBUG: Self = Self(5);
pub const INFO: Self = Self(9);
pub const WARN: Self = Self(13);
pub const ERROR: Self = Self(17);
pub const FATAL: Self = Self(21);
pub const fn as_u8(self) -> u8 {
self.0
}
}
impl From<OtelSeverityNumber> for u8 {
fn from(value: OtelSeverityNumber) -> Self {
value.0
}
}
impl TryFrom<u8> for OtelSeverityNumber {
type Error = &'static str;
fn try_from(value: u8) -> Result<Self, Self::Error> {
match value {
1 | 5 | 9 | 13 | 17 | 21 => Ok(Self(value)),
_ => Err("severity number must be one of 1, 5, 9, 13, 17, or 21"),
}
}
}
impl From<crate::report::Severity> for OtelSeverityNumber {
fn from(level: crate::report::Severity) -> Self {
match level {
crate::report::Severity::Trace => Self::TRACE,
crate::report::Severity::Debug => Self::DEBUG,
crate::report::Severity::Info => Self::INFO,
crate::report::Severity::Warn => Self::WARN,
crate::report::Severity::Error => Self::ERROR,
crate::report::Severity::Fatal => Self::FATAL,
}
}
}
#[cfg(feature = "trace")]
impl From<crate::report::TraceEventLevel> for OtelSeverityNumber {
fn from(level: crate::report::TraceEventLevel) -> Self {
match level {
crate::report::TraceEventLevel::Trace => Self::TRACE,
crate::report::TraceEventLevel::Debug => Self::DEBUG,
crate::report::TraceEventLevel::Info => Self::INFO,
crate::report::TraceEventLevel::Warn => Self::WARN,
crate::report::TraceEventLevel::Error => Self::ERROR,
}
}
}
impl fmt::Display for OtelSeverityNumber {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "json", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "json", serde(bound(deserialize = "'de: 'a")))]
pub struct OtelAttribute<'a> {
pub key: RefStr<'a>,
pub value: OtelValue<'a>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "json", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "json", serde(bound(deserialize = "'de: 'a")))]
pub struct OtelEnvelopeConfig<'a> {
namespace: Option<RefStr<'a>>,
}
impl<'a> OtelEnvelopeConfig<'a> {
pub const fn new() -> Self {
Self { namespace: None }
}
pub fn with_namespace(mut self, namespace: impl Into<RefStr<'a>>) -> Self {
self.namespace = Some(namespace.into());
self
}
pub fn namespace(&self) -> Option<&str> {
self.namespace.as_ref().map(RefStr::as_ref)
}
fn namespace_ref(&self) -> Option<&str> {
self.namespace.as_ref().map(RefStr::as_ref)
}
fn prefixed_key(&self, prefix: &str, key: &'a str, include_prefix: bool) -> RefStr<'a> {
match self.namespace_ref() {
Some(ns) => format!("{ns}.{prefix}.{key}").into(),
None if include_prefix => format!("{prefix}.{key}").into(),
None => key.into(),
}
}
fn context_key(&self, key: &'a str) -> RefStr<'a> {
self.prefixed_key("context", key, false)
}
fn system_key(&self, key: &'a str) -> RefStr<'a> {
self.prefixed_key("system", key, true)
}
fn diagnostic_bag_key(&self, key: &'a str) -> RefStr<'a> {
self.prefixed_key("diagnostic_bag", key, true)
}
fn attachment_key(&self, key: &'a str) -> RefStr<'a> {
self.prefixed_key("attachment", key, true)
}
}
impl Default for OtelEnvelopeConfig<'_> {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "json", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "json", serde(bound(deserialize = "'de: 'a")))]
pub struct OtelEvent<'a> {
pub name: RefStr<'a>,
#[cfg_attr(feature = "json", serde(skip_serializing_if = "Option::is_none"))]
pub body: Option<OtelValue<'a>>,
#[cfg_attr(feature = "json", serde(skip_serializing_if = "Option::is_none"))]
pub timestamp_unix_nano: Option<u64>,
#[cfg_attr(feature = "json", serde(skip_serializing_if = "Option::is_none"))]
pub observed_timestamp_unix_nano: Option<u64>,
#[cfg_attr(feature = "json", serde(skip_serializing_if = "Option::is_none"))]
pub severity_text: Option<ref_str::StaticRefStr>,
#[cfg_attr(feature = "json", serde(skip_serializing_if = "Option::is_none"))]
pub severity_number: Option<OtelSeverityNumber>,
#[cfg_attr(feature = "json", serde(skip_serializing_if = "Option::is_none"))]
pub trace_id: Option<TraceId>,
#[cfg_attr(feature = "json", serde(skip_serializing_if = "Option::is_none"))]
pub span_id: Option<SpanId>,
#[cfg_attr(feature = "json", serde(skip_serializing_if = "Option::is_none"))]
pub trace_sampled: Option<bool>,
#[cfg_attr(feature = "json", serde(skip_serializing_if = "Option::is_none"))]
pub trace_context: Option<OtelTraceContext>,
pub attributes: Vec<OtelAttribute<'a>>,
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "json", derive(serde::Serialize, serde::Deserialize))]
pub struct OtelTraceContext {
#[cfg_attr(feature = "json", serde(skip_serializing_if = "Option::is_none"))]
pub parent_span_id: Option<ParentSpanId>,
#[cfg_attr(feature = "json", serde(skip_serializing_if = "Option::is_none"))]
pub trace_state: Option<TraceState>,
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "json", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "json", serde(bound(deserialize = "'de: 'a")))]
pub enum OtelValue<'a> {
String(RefStr<'a>),
Int(i64),
U64(u64),
Double(f64),
Bool(bool),
Bytes(Vec<u8>),
Array(Vec<OtelValue<'a>>),
KvList(Vec<OtelAttribute<'a>>),
}
impl<'a> OtelValue<'a> {
pub fn as_cow(&self) -> Cow<'_, str> {
match self {
Self::String(v) => v.as_ref().into(),
Self::Int(v) => Cow::Owned(v.to_string()),
Self::U64(v) => Cow::Owned(v.to_string()),
Self::Double(v) => Cow::Owned(v.to_string()),
Self::Bool(v) => Cow::Owned(v.to_string()),
Self::Bytes(v) => Cow::Owned(format!("<{} bytes>", v.len())),
Self::Array(v) => Cow::Owned(format!("{v:?}")),
Self::KvList(v) => Cow::Owned(format!("{v:?}")),
}
}
pub fn from_attachment_ref(value: &'a AttachmentValue) -> Self {
match value {
AttachmentValue::String(v) => Self::String(v.as_str().into()),
AttachmentValue::Integer(v) => Self::Int(*v),
AttachmentValue::Unsigned(v) => Self::U64(*v),
AttachmentValue::Float(v) => Self::Double(*v),
AttachmentValue::Bool(v) => Self::Bool(*v),
AttachmentValue::Array(values) => {
Self::Array(values.iter().map(OtelValue::from_attachment_ref).collect())
}
AttachmentValue::Object(values) => Self::KvList(
values
.iter()
.map(|(k, v)| OtelAttribute {
key: k.as_str().into(),
value: Self::from_attachment_ref(v),
})
.collect(),
),
AttachmentValue::Bytes(v) => Self::Bytes(v.clone()),
AttachmentValue::Redacted { kind, reason } => {
Self::KvList(redacted_attrs(kind.as_ref(), reason.as_ref()))
}
}
}
pub fn from_context_ref(value: &'a ContextValue) -> Self {
match value {
ContextValue::String(v) => Self::String(v.as_str().into()),
ContextValue::Integer(v) => Self::Int(*v),
ContextValue::Unsigned(v) => Self::U64(*v),
ContextValue::Float(v) => Self::Double(*v),
ContextValue::Bool(v) => Self::Bool(*v),
ContextValue::StringArray(values) => Self::Array(
values
.iter()
.map(|value| OtelValue::String(value.as_str().into()))
.collect(),
),
ContextValue::IntegerArray(values) => {
Self::Array(values.iter().copied().map(OtelValue::Int).collect())
}
ContextValue::UnsignedArray(values) => {
Self::Array(values.iter().copied().map(OtelValue::U64).collect())
}
ContextValue::FloatArray(values) => {
Self::Array(values.iter().copied().map(OtelValue::Double).collect())
}
ContextValue::BoolArray(values) => {
Self::Array(values.iter().copied().map(OtelValue::Bool).collect())
}
ContextValue::Redacted { kind, reason } => {
Self::KvList(redacted_attrs(kind.as_ref(), reason.as_ref()))
}
}
}
}
impl core::fmt::Display for OtelValue<'_> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str(self.as_cow().as_ref())
}
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "json", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "json", serde(bound(deserialize = "'de: 'a")))]
pub struct OtelEnvelope<'a> {
pub records: Vec<OtelEvent<'a>>,
}
pub const REPORT_OTEL_SCHEMA_VERSION: &str = "v0.1.0";
pub const REPORT_OTEL_SCHEMA_DRAFT: &str = "https://json-schema.org/draft/2020-12/schema";
pub fn report_otel_schema() -> &'static str {
include_str!("../schemas/report-otel-v0.1.0.schema.json")
}
impl<'a> DiagnosticIr<'a, HasSeverity> {
pub fn to_otel_envelope(&'a self, config: OtelEnvelopeConfig<'a>) -> OtelEnvelope<'a> {
let trace_ids = self.otel_trace_ids();
let trace_context = self.otel_trace_context();
let mut records = Vec::with_capacity(1);
records.push(self.otel_report_ev(&config, trace_ids, trace_context.clone()));
#[cfg(feature = "trace")]
self.otel_trace_ev(&config, &mut records, trace_ids, trace_context);
OtelEnvelope { records }
}
pub fn to_otel_envelope_default(&'a self) -> OtelEnvelope<'a> {
self.to_otel_envelope(OtelEnvelopeConfig::default())
}
fn otel_report_ev(
&'a self,
config: &OtelEnvelopeConfig<'a>,
trace_ids: (Option<TraceId>, Option<SpanId>, Option<bool>),
trace_context: Option<OtelTraceContext>,
) -> OtelEvent<'a> {
let report_level = self.metadata.required_severity();
let error_message: RefStr<'a> = self.error.message.as_cow().into();
let error_message_value = OtelValue::String(error_message.clone());
let error_type = self.error.r#type.clone();
let error_type_value = OtelValue::String(error_type.clone());
let mut attributes = Vec::with_capacity(self.otel_report_attr_capacity());
self.otel_diagnostic_bag(config, &mut attributes);
self.otel_attach_attrs(config, &mut attributes);
attributes.push(OtelAttribute {
key: "exception.type".into(),
value: error_type_value,
});
attributes.push(OtelAttribute {
key: "exception.message".into(),
value: error_message_value.clone(),
});
attributes.push(OtelAttribute {
key: "exception.raw_data".into(),
value: otel_error_raw_data(error_message_value.clone(), error_type),
});
if let Some(st) = self.metadata.stack_trace() {
attributes.push(OtelAttribute {
key: "exception.stacktrace".into(),
value: otel_stack_trace_value(st),
});
}
if let Some(ec) = self.metadata.error_code() {
attributes.push(OtelAttribute {
key: "error.code".into(),
value: match ec {
ErrorCode::Integer(v) => OtelValue::Int(*v),
ErrorCode::String(v) => OtelValue::String(v.as_str().into()),
},
});
}
if let Some(cat) = self.metadata.category() {
attributes.push(OtelAttribute {
key: "error.category".into(),
value: OtelValue::String(cat.into()),
});
}
if let Some(retry) = self.metadata.retryable() {
attributes.push(OtelAttribute {
key: "error.retryable".into(),
value: OtelValue::Bool(retry),
});
}
let (trace_id, span_id, trace_sampled) = trace_ids;
OtelEvent {
name: "exception".into(),
body: Some(error_message_value),
timestamp_unix_nano: None,
observed_timestamp_unix_nano: None,
severity_text: Some(severity_ref(report_level)),
severity_number: Some(report_level.into()),
trace_id,
span_id,
trace_sampled,
trace_context,
attributes,
}
}
fn otel_report_attr_capacity(&'a self) -> usize {
let mut count = 3 + self.context.len() + self.system.len();
count += self.metadata.stack_trace().is_some() as usize;
count += self.metadata.error_code().is_some() as usize;
count += self.metadata.category().is_some() as usize;
count += self.metadata.retryable().is_some() as usize;
count += (!self.display_causes.is_empty()) as usize;
count += self.origin_source_errors.is_some() as usize;
count += self.diagnostic_source_errors.is_some() as usize;
count += self
.attachments
.iter()
.map(|a| match a {
Attachment::Note { .. } => 1,
Attachment::Payload { media_type, .. } => 1 + media_type.is_some() as usize,
})
.sum::<usize>();
count
}
#[cfg(feature = "trace")]
fn otel_trace_ids(&'a self) -> (Option<TraceId>, Option<SpanId>, Option<bool>) {
let ctx = self.trace.context();
(
ctx.and_then(|c| c.trace_id),
ctx.and_then(|c| c.span_id),
ctx.and_then(|c| c.sampled),
)
}
#[cfg(not(feature = "trace"))]
fn otel_trace_ids(&'a self) -> (Option<TraceId>, Option<SpanId>, Option<bool>) {
(None, None, None)
}
#[cfg(feature = "trace")]
fn otel_trace_context(&'a self) -> Option<OtelTraceContext> {
let context = self.trace.context()?;
let parent_span_id = context.parent_span_id;
let trace_state = context.trace_state.clone();
if parent_span_id.is_none() && trace_state.is_none() {
return None;
}
Some(OtelTraceContext {
parent_span_id,
trace_state,
})
}
#[cfg(not(feature = "trace"))]
fn otel_trace_context(&'a self) -> Option<OtelTraceContext> {
None
}
fn otel_diagnostic_bag(
&'a self,
config: &OtelEnvelopeConfig<'a>,
attributes: &mut Vec<OtelAttribute<'a>>,
) {
if !self.display_causes.is_empty() {
attributes.push(OtelAttribute {
key: config.diagnostic_bag_key("display_causes"),
value: otel_display_causes_value(self.display_causes, self.display_causes_state),
});
}
if let Some(source_errors) = self.origin_source_errors.as_ref() {
attributes.push(OtelAttribute {
key: config.diagnostic_bag_key("origin_source_errors"),
value: otel_source_errors_value(source_errors, true),
});
}
if let Some(source_errors) = self.diagnostic_source_errors.as_ref() {
attributes.push(OtelAttribute {
key: config.diagnostic_bag_key("diagnostic_source_errors"),
value: otel_source_errors_value(source_errors, false),
});
}
}
fn otel_attach_attrs(
&'a self,
config: &OtelEnvelopeConfig<'a>,
attributes: &mut Vec<OtelAttribute<'a>>,
) {
for (key, value) in self.context {
attributes.push(OtelAttribute {
key: config.context_key(key.as_ref()),
value: OtelValue::from_context_ref(value),
});
}
for (key, value) in self.system {
attributes.push(OtelAttribute {
key: config.system_key(key.as_ref()),
value: OtelValue::from_context_ref(value),
});
}
for attachment in self.attachments {
match attachment {
Attachment::Note { message } => {
attributes.push(OtelAttribute {
key: config.attachment_key("note"),
value: OtelValue::String(message.to_string().into()),
});
}
Attachment::Payload {
name,
value,
media_type,
} => {
attributes.push(OtelAttribute {
key: config
.attachment_key(&format!("payload.{name}"))
.into_static_str()
.into(),
value: OtelValue::from_attachment_ref(value),
});
if let Some(media_type) = media_type {
attributes.push(OtelAttribute {
key: config
.attachment_key(&format!("payload.{name}.media_type"))
.into_static_str()
.into(),
value: OtelValue::String(media_type.as_str().into()),
});
}
}
}
}
}
#[cfg(feature = "trace")]
fn otel_trace_ev(
&'a self,
_config: &OtelEnvelopeConfig<'a>,
records: &mut Vec<OtelEvent<'a>>,
trace_ids: (Option<TraceId>, Option<SpanId>, Option<bool>),
trace_context: Option<OtelTraceContext>,
) {
let Some(events) = self.trace.events() else {
return;
};
let fallback_level = self.metadata.required_severity();
let fallback_severity_text = severity_ref(fallback_level);
let fallback_severity_number = fallback_level.into();
for trace_event in events.iter() {
let (severity_text, severity_number) = trace_event.level.map_or(
(
Some(fallback_severity_text.clone()),
Some(fallback_severity_number),
),
|level| (Some(trace_event_level_ref(level)), Some(level.into())),
);
let mut attributes = Vec::with_capacity(trace_event.attributes.len());
attributes.extend(trace_event.attributes.iter().map(|attr| OtelAttribute {
key: attr.key.as_str().into(),
value: OtelValue::from_attachment_ref(&attr.value),
}));
records.push(OtelEvent {
name: trace_event.name.as_str().into(),
body: None,
timestamp_unix_nano: trace_event.timestamp_unix_nano,
observed_timestamp_unix_nano: None,
severity_text,
severity_number,
trace_id: trace_ids.0,
span_id: trace_ids.1,
trace_sampled: trace_ids.2,
trace_context: trace_context.clone(),
attributes,
});
}
}
}
#[cfg(feature = "trace")]
fn trace_event_level_ref(level: crate::report::TraceEventLevel) -> ref_str::StaticRefStr {
match level {
crate::report::TraceEventLevel::Trace => "trace".into(),
crate::report::TraceEventLevel::Debug => "debug".into(),
crate::report::TraceEventLevel::Info => "info".into(),
crate::report::TraceEventLevel::Warn => "warn".into(),
crate::report::TraceEventLevel::Error => "error".into(),
}
}
fn severity_ref(level: crate::report::Severity) -> ref_str::StaticRefStr {
match level {
crate::report::Severity::Trace => "trace".into(),
crate::report::Severity::Debug => "debug".into(),
crate::report::Severity::Info => "info".into(),
crate::report::Severity::Warn => "warn".into(),
crate::report::Severity::Error => "error".into(),
crate::report::Severity::Fatal => "fatal".into(),
}
}
fn otel_error_raw_data<'a>(message: OtelValue<'a>, error_type: RefStr<'a>) -> OtelValue<'a> {
OtelValue::KvList(vec![
OtelAttribute {
key: "message".into(),
value: message,
},
OtelAttribute {
key: "type".into(),
value: OtelValue::String(error_type),
},
])
}
fn redacted_attrs<'a>(
kind: Option<&'a ref_str::StaticRefStr>,
reason: Option<&'a ref_str::StaticRefStr>,
) -> Vec<OtelAttribute<'a>> {
let mut attrs = Vec::with_capacity(2);
if let Some(k) = kind {
attrs.push(OtelAttribute {
key: "kind".into(),
value: OtelValue::String(k.as_str().into()),
});
}
if let Some(r) = reason {
attrs.push(OtelAttribute {
key: "reason".into(),
value: OtelValue::String(r.as_str().into()),
});
}
attrs
}
fn otel_stack_trace_value<'a>(stack_trace: &'a StackTrace) -> OtelValue<'a> {
let frames = stack_trace
.frames
.iter()
.map(|f| {
let mut attrs = Vec::with_capacity(5);
if let Some(s) = f.symbol.as_ref() {
attrs.push(OtelAttribute {
key: "symbol".into(),
value: OtelValue::String(s.as_str().into()),
});
}
if let Some(m) = f.module_path.as_ref() {
attrs.push(OtelAttribute {
key: "module_path".into(),
value: OtelValue::String(m.as_str().into()),
});
}
if let Some(f) = f.file.as_ref() {
attrs.push(OtelAttribute {
key: "file".into(),
value: OtelValue::String(f.as_str().into()),
});
}
if let Some(l) = f.line {
attrs.push(OtelAttribute {
key: "line".into(),
value: OtelValue::U64(l as u64),
});
}
if let Some(c) = f.column {
attrs.push(OtelAttribute {
key: "column".into(),
value: OtelValue::U64(c as u64),
});
}
OtelValue::KvList(attrs)
})
.collect();
let mut attrs = vec![
OtelAttribute {
key: "format".into(),
value: OtelValue::String(
match stack_trace.format {
crate::report::StackTraceFormat::Native => "native",
crate::report::StackTraceFormat::Raw => "raw",
}
.into(),
),
},
OtelAttribute {
key: "frames".into(),
value: OtelValue::Array(frames),
},
];
if let Some(raw) = stack_trace.raw.as_ref() {
attrs.push(OtelAttribute {
key: "raw".into(),
value: OtelValue::String(raw.as_str().into()),
});
}
OtelValue::KvList(attrs)
}
fn otel_display_causes_value<'a>(
display_causes: &'a [alloc::sync::Arc<dyn Display + Send + Sync + 'static>],
state: CauseTraversalState,
) -> OtelValue<'a> {
OtelValue::KvList(vec![
OtelAttribute {
key: "items".into(),
value: OtelValue::Array(
display_causes
.iter()
.map(|v| OtelValue::String(v.to_string().into()))
.collect(),
),
},
OtelAttribute {
key: "truncated".into(),
value: OtelValue::Bool(state.truncated),
},
OtelAttribute {
key: "cycle_detected".into(),
value: OtelValue::Bool(state.cycle_detected),
},
])
}
fn otel_source_errors_value<'a>(
source_errors: &'a SourceErrorChain,
hide_report_wrapper_types: bool,
) -> OtelValue<'a> {
let exported = source_errors.export_with_options(hide_report_wrapper_types);
let nodes = exported
.nodes
.into_iter()
.map(|node| {
let mut attrs = Vec::with_capacity(3);
attrs.push(OtelAttribute {
key: "message".into(),
value: OtelValue::String(node.message.into()),
});
if let Some(t) = node.type_name.as_ref() {
attrs.push(OtelAttribute {
key: "type".into(),
value: OtelValue::String(t.clone().into()),
});
}
attrs.push(OtelAttribute {
key: "source_roots".into(),
value: OtelValue::Array(
node.source_roots
.iter()
.map(|&i| OtelValue::Int(i as i64))
.collect(),
),
});
OtelValue::KvList(attrs)
})
.collect();
OtelValue::KvList(vec![
OtelAttribute {
key: "roots".into(),
value: OtelValue::Array(
exported
.roots
.iter()
.map(|&i| OtelValue::Int(i as i64))
.collect(),
),
},
OtelAttribute {
key: "nodes".into(),
value: OtelValue::Array(nodes),
},
OtelAttribute {
key: "truncated".into(),
value: OtelValue::Bool(exported.truncated),
},
OtelAttribute {
key: "cycle_detected".into(),
value: OtelValue::Bool(exported.cycle_detected),
},
])
}