1#![deny(missing_docs)]
2
3use std::{collections::BTreeMap, fmt, panic::AssertUnwindSafe, sync::Arc, time::Duration};
11
12use async_trait::async_trait;
13use futures_util::FutureExt;
14use serde::{Deserialize, Serialize, de::DeserializeOwned};
15use thiserror::Error;
16
17pub const DEFAULT_MAX_ENVELOPE_BYTES: usize = 1024 * 1024;
19
20const MAX_NAME_BYTES: usize = 255;
21const MAX_CORRELATION_BYTES: usize = 255;
22const MAX_HEADER_COUNT: usize = 64;
23const MAX_HEADER_NAME_BYTES: usize = 128;
24const MAX_HEADER_VALUE_BYTES: usize = 4096;
25
26pub type Result<T> = std::result::Result<T, IntegrationError>;
28
29#[derive(Debug, Error)]
31pub enum IntegrationError {
32 #[error("message name must contain 1..={MAX_NAME_BYTES} bytes")]
34 InvalidMessageName,
35 #[error("correlation identifiers must contain 1..={MAX_CORRELATION_BYTES} bytes")]
37 InvalidCorrelationId,
38 #[error("traceparent is not a valid W3C trace context value")]
40 InvalidTraceparent,
41 #[error("header names must contain 1..={MAX_HEADER_NAME_BYTES} visible ASCII bytes")]
43 InvalidHeaderName,
44 #[error("header values must contain at most {MAX_HEADER_VALUE_BYTES} visible bytes")]
46 InvalidHeaderValue,
47 #[error("an envelope may contain at most {MAX_HEADER_COUNT} headers")]
49 TooManyHeaders,
50 #[error("serialized envelope is {actual} bytes, exceeding the {maximum}-byte limit")]
52 EnvelopeTooLarge {
53 actual: usize,
55 maximum: usize,
57 },
58 #[error(transparent)]
60 Json(#[from] serde_json::Error),
61}
62
63#[derive(Clone, Default, Deserialize, Eq, PartialEq, Serialize)]
65pub struct EnvelopeMetadata {
66 correlation_id: Option<String>,
67 causation_id: Option<String>,
68 traceparent: Option<String>,
69 headers: BTreeMap<String, String>,
70}
71
72impl EnvelopeMetadata {
73 pub fn new() -> Self {
75 Self::default()
76 }
77
78 pub fn correlation_id(mut self, correlation_id: impl Into<String>) -> Result<Self> {
80 self.correlation_id = Some(validate_correlation_id(correlation_id.into())?);
81 Ok(self)
82 }
83
84 pub fn causation_id(mut self, causation_id: impl Into<String>) -> Result<Self> {
86 self.causation_id = Some(validate_correlation_id(causation_id.into())?);
87 Ok(self)
88 }
89
90 pub fn traceparent(mut self, traceparent: impl Into<String>) -> Result<Self> {
92 let traceparent = traceparent.into();
93 if !valid_traceparent(&traceparent) {
94 return Err(IntegrationError::InvalidTraceparent);
95 }
96 self.traceparent = Some(traceparent);
97 Ok(self)
98 }
99
100 pub fn header(mut self, name: impl Into<String>, value: impl Into<String>) -> Result<Self> {
105 let name = name.into();
106 let value = value.into();
107 validate_header_name(&name)?;
108 validate_header_value(&value)?;
109 if !self.headers.contains_key(&name) && self.headers.len() == MAX_HEADER_COUNT {
110 return Err(IntegrationError::TooManyHeaders);
111 }
112 self.headers.insert(name, value);
113 Ok(self)
114 }
115
116 pub fn correlation_id_value(&self) -> Option<&str> {
118 self.correlation_id.as_deref()
119 }
120
121 pub fn causation_id_value(&self) -> Option<&str> {
123 self.causation_id.as_deref()
124 }
125
126 pub fn traceparent_value(&self) -> Option<&str> {
128 self.traceparent.as_deref()
129 }
130
131 pub fn headers(&self) -> &BTreeMap<String, String> {
133 &self.headers
134 }
135}
136
137impl fmt::Debug for EnvelopeMetadata {
138 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
139 formatter
140 .debug_struct("EnvelopeMetadata")
141 .field("correlation_id", &self.correlation_id)
142 .field("causation_id", &self.causation_id)
143 .field("traceparent", &self.traceparent)
144 .field("header_names", &self.headers.keys().collect::<Vec<_>>())
145 .finish()
146 }
147}
148
149#[derive(Clone, Deserialize, Eq, PartialEq, Serialize)]
155pub struct MessageEnvelope<T> {
156 id: String,
157 name: String,
158 schema_version: u32,
159 occurred_at_ms: i64,
160 metadata: EnvelopeMetadata,
161 payload: T,
162}
163
164impl<T> MessageEnvelope<T> {
165 pub fn new(name: impl Into<String>, payload: T) -> Result<Self> {
167 Self::from_parts(
168 uuid::Uuid::new_v4().to_string(),
169 name,
170 1,
171 time::OffsetDateTime::now_utc().unix_timestamp_nanos() as i64 / 1_000_000,
172 EnvelopeMetadata::new(),
173 payload,
174 )
175 }
176
177 pub fn from_parts(
179 id: impl Into<String>,
180 name: impl Into<String>,
181 schema_version: u32,
182 occurred_at_ms: i64,
183 metadata: EnvelopeMetadata,
184 payload: T,
185 ) -> Result<Self> {
186 let id = validate_correlation_id(id.into())?;
187 let name = name.into();
188 if name.is_empty() || name.len() > MAX_NAME_BYTES {
189 return Err(IntegrationError::InvalidMessageName);
190 }
191 Ok(Self {
192 id,
193 name,
194 schema_version,
195 occurred_at_ms,
196 metadata,
197 payload,
198 })
199 }
200
201 pub fn with_metadata(mut self, metadata: EnvelopeMetadata) -> Self {
203 self.metadata = metadata;
204 self
205 }
206
207 pub fn id(&self) -> &str {
209 &self.id
210 }
211
212 pub fn name(&self) -> &str {
214 &self.name
215 }
216
217 pub const fn schema_version(&self) -> u32 {
219 self.schema_version
220 }
221
222 pub const fn occurred_at_ms(&self) -> i64 {
224 self.occurred_at_ms
225 }
226
227 pub fn metadata(&self) -> &EnvelopeMetadata {
229 &self.metadata
230 }
231
232 pub fn payload(&self) -> &T {
234 &self.payload
235 }
236
237 pub fn into_payload(self) -> T {
239 self.payload
240 }
241}
242
243impl<T> MessageEnvelope<T>
244where
245 T: Serialize,
246{
247 pub fn to_json(&self) -> Result<Vec<u8>> {
249 self.to_json_with_limit(DEFAULT_MAX_ENVELOPE_BYTES)
250 }
251
252 pub fn to_json_with_limit(&self, maximum: usize) -> Result<Vec<u8>> {
254 let encoded = serde_json::to_vec(self)?;
255 if encoded.len() > maximum {
256 return Err(IntegrationError::EnvelopeTooLarge {
257 actual: encoded.len(),
258 maximum,
259 });
260 }
261 Ok(encoded)
262 }
263}
264
265impl<T> MessageEnvelope<T>
266where
267 T: DeserializeOwned,
268{
269 pub fn from_json(encoded: &[u8]) -> Result<Self> {
271 Self::from_json_with_limit(encoded, DEFAULT_MAX_ENVELOPE_BYTES)
272 }
273
274 pub fn from_json_with_limit(encoded: &[u8], maximum: usize) -> Result<Self> {
276 if encoded.len() > maximum {
277 return Err(IntegrationError::EnvelopeTooLarge {
278 actual: encoded.len(),
279 maximum,
280 });
281 }
282 let envelope: Self = serde_json::from_slice(encoded)?;
283 Self::from_parts(
284 envelope.id,
285 envelope.name,
286 envelope.schema_version,
287 envelope.occurred_at_ms,
288 envelope.metadata,
289 envelope.payload,
290 )
291 }
292}
293
294impl<T> fmt::Debug for MessageEnvelope<T> {
295 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
296 formatter
297 .debug_struct("MessageEnvelope")
298 .field("id", &self.id)
299 .field("name", &self.name)
300 .field("schema_version", &self.schema_version)
301 .field("occurred_at_ms", &self.occurred_at_ms)
302 .field("metadata", &self.metadata)
303 .field("payload", &"<redacted>")
304 .finish()
305 }
306}
307
308#[derive(Clone, Copy, Debug, Eq, PartialEq)]
310pub enum IntegrationStatus {
311 Success,
313 Failure,
315 Cancelled,
317}
318
319#[derive(Clone, Debug, Eq, PartialEq)]
321pub struct IntegrationEvent {
322 adapter: &'static str,
323 operation: &'static str,
324 status: IntegrationStatus,
325 duration: Duration,
326 correlation_id: Option<String>,
327}
328
329impl IntegrationEvent {
330 pub fn new(
332 adapter: &'static str,
333 operation: &'static str,
334 status: IntegrationStatus,
335 duration: Duration,
336 ) -> Self {
337 Self {
338 adapter,
339 operation,
340 status,
341 duration,
342 correlation_id: None,
343 }
344 }
345
346 pub fn correlation_id(mut self, correlation_id: impl Into<String>) -> Result<Self> {
348 self.correlation_id = Some(validate_correlation_id(correlation_id.into())?);
349 Ok(self)
350 }
351
352 pub const fn adapter(&self) -> &'static str {
354 self.adapter
355 }
356
357 pub const fn operation(&self) -> &'static str {
359 self.operation
360 }
361
362 pub const fn status(&self) -> IntegrationStatus {
364 self.status
365 }
366
367 pub const fn duration(&self) -> Duration {
369 self.duration
370 }
371
372 pub fn correlation_id_value(&self) -> Option<&str> {
374 self.correlation_id.as_deref()
375 }
376}
377
378#[async_trait]
383pub trait IntegrationObserver: Send + Sync + 'static {
384 async fn record(&self, event: &IntegrationEvent);
386}
387
388#[derive(Clone, Default)]
390pub struct IntegrationTelemetry {
391 observers: Arc<Vec<Arc<dyn IntegrationObserver>>>,
392}
393
394impl IntegrationTelemetry {
395 pub fn new() -> Self {
397 Self::default()
398 }
399
400 pub fn with_observer<O>(self, observer: O) -> Self
402 where
403 O: IntegrationObserver,
404 {
405 let mut observers = self.observers.as_ref().clone();
406 observers.push(Arc::new(observer));
407 Self {
408 observers: Arc::new(observers),
409 }
410 }
411
412 pub fn tracing(self) -> Self {
414 self.with_observer(TracingIntegrationObserver)
415 }
416
417 #[cfg(feature = "observability")]
419 pub fn observability(
420 self,
421 observer: nidus_observability::ObservabilityAdapterObserver,
422 ) -> Self {
423 self.with_observer(ObservabilityIntegrationObserver(observer))
424 }
425
426 #[cfg(feature = "dashboard")]
428 pub fn dashboard(
429 self,
430 collector: nidus_dashboard::DashboardCollector<
431 nidus_dashboard::storage::DashboardStorageHandle,
432 >,
433 ) -> Self {
434 self.with_observer(DashboardIntegrationObserver(collector))
435 }
436
437 pub fn is_empty(&self) -> bool {
439 self.observers.is_empty()
440 }
441
442 pub async fn record(&self, event: &IntegrationEvent) {
444 for observer in self.observers.iter() {
445 if AssertUnwindSafe(observer.record(event))
446 .catch_unwind()
447 .await
448 .is_err()
449 {
450 tracing::warn!(
451 adapter = event.adapter(),
452 operation = event.operation(),
453 "integration telemetry observer panicked and was isolated"
454 );
455 }
456 }
457 }
458}
459
460impl fmt::Debug for IntegrationTelemetry {
461 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
462 formatter
463 .debug_struct("IntegrationTelemetry")
464 .field("observer_count", &self.observers.len())
465 .finish()
466 }
467}
468
469#[derive(Clone, Copy, Debug)]
470struct TracingIntegrationObserver;
471
472#[async_trait]
473impl IntegrationObserver for TracingIntegrationObserver {
474 async fn record(&self, event: &IntegrationEvent) {
475 tracing::info!(
476 adapter = event.adapter(),
477 operation = event.operation(),
478 status = ?event.status(),
479 duration_ms = event.duration().as_millis(),
480 correlation_id = event.correlation_id_value(),
481 "integration operation completed"
482 );
483 }
484}
485
486#[cfg(feature = "observability")]
487#[derive(Clone, Debug)]
488struct ObservabilityIntegrationObserver(nidus_observability::ObservabilityAdapterObserver);
489
490#[cfg(feature = "observability")]
491#[async_trait]
492impl IntegrationObserver for ObservabilityIntegrationObserver {
493 async fn record(&self, event: &IntegrationEvent) {
494 let status = match event.status() {
495 IntegrationStatus::Success => nidus_observability::OperationStatus::Success,
496 IntegrationStatus::Failure | IntegrationStatus::Cancelled => {
497 nidus_observability::OperationStatus::Failure
498 }
499 };
500 self.0
501 .record(event.adapter(), event.operation(), status, event.duration());
502 }
503}
504
505#[cfg(feature = "dashboard")]
506#[derive(Clone, Debug)]
507struct DashboardIntegrationObserver(
508 nidus_dashboard::DashboardCollector<nidus_dashboard::storage::DashboardStorageHandle>,
509);
510
511#[cfg(feature = "dashboard")]
512#[async_trait]
513impl IntegrationObserver for DashboardIntegrationObserver {
514 async fn record(&self, event: &IntegrationEvent) {
515 if let Err(error) = self
516 .0
517 .record_adapter(
518 format!("{}.{}", event.adapter(), event.operation()),
519 event.correlation_id_value(),
520 event.status() == IntegrationStatus::Success,
521 event.duration().as_millis().min(u128::from(u64::MAX)) as u64,
522 )
523 .await
524 {
525 tracing::warn!(error = %error, "dashboard adapter telemetry was dropped");
526 }
527 }
528}
529
530fn validate_correlation_id(value: String) -> Result<String> {
531 if value.is_empty()
532 || value.len() > MAX_CORRELATION_BYTES
533 || value.chars().any(char::is_control)
534 {
535 return Err(IntegrationError::InvalidCorrelationId);
536 }
537 Ok(value)
538}
539
540fn validate_header_name(name: &str) -> Result<()> {
541 if name.is_empty()
542 || name.len() > MAX_HEADER_NAME_BYTES
543 || !name
544 .bytes()
545 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.'))
546 {
547 return Err(IntegrationError::InvalidHeaderName);
548 }
549 Ok(())
550}
551
552fn validate_header_value(value: &str) -> Result<()> {
553 if value.len() > MAX_HEADER_VALUE_BYTES || value.chars().any(char::is_control) {
554 return Err(IntegrationError::InvalidHeaderValue);
555 }
556 Ok(())
557}
558
559fn valid_traceparent(value: &str) -> bool {
560 let bytes = value.as_bytes();
561 if bytes.len() < 55
562 || bytes.len() > MAX_HEADER_VALUE_BYTES
563 || bytes.iter().any(|byte| !(0x21..=0x7e).contains(byte))
564 || bytes[2] != b'-'
565 || bytes[35] != b'-'
566 || bytes[52] != b'-'
567 {
568 return false;
569 }
570
571 let version = &bytes[..2];
572 let trace_id = &bytes[3..35];
573 let parent_id = &bytes[36..52];
574 let flags = &bytes[53..55];
575 let valid_lower_hex = |part: &[u8]| {
576 part.iter()
577 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(byte))
578 };
579 if !valid_lower_hex(version)
580 || version == b"ff"
581 || !valid_lower_hex(trace_id)
582 || trace_id.iter().all(|byte| *byte == b'0')
583 || !valid_lower_hex(parent_id)
584 || parent_id.iter().all(|byte| *byte == b'0')
585 || !valid_lower_hex(flags)
586 {
587 return false;
588 }
589
590 if version == b"00" {
591 bytes.len() == 55
592 } else {
593 bytes.len() == 55 || bytes[55] == b'-'
594 }
595}