Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions crates/observation-tools-shared/src/group_id.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
use serde::Deserialize;
use serde::Serialize;
use utoipa::ToSchema;

/// Unique identifier for a group
///
/// Group IDs are user-provided strings. By default, a UUID v7 string is generated,
/// but any string value is accepted.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
#[serde(transparent)]
#[schema(value_type = String, example = "018e9a3a2c1b7e3f8d2a4b5c6d7e8f9b")]
pub struct GroupId(String);

impl GroupId {
/// Generate a new UUIDv7 group ID
pub fn new() -> Self {
Self(uuid::Uuid::now_v7().as_simple().to_string())
}

/// Get the string value of this group ID
pub fn as_str(&self) -> &str {
&self.0
}
}

impl Default for GroupId {
fn default() -> Self {
Self::new()
}
}

impl From<String> for GroupId {
fn from(s: String) -> Self {
Self(s)
}
}

impl From<&str> for GroupId {
fn from(s: &str) -> Self {
Self(s.to_string())
}
}
9 changes: 7 additions & 2 deletions crates/observation-tools-shared/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@

pub mod error;
pub mod models;
mod group_id;
mod observation;
mod payload;
mod payload_id;

pub use error::Error;
pub use error::Result;
pub use group_id::GroupId;
pub use models::Execution;
pub use models::ExecutionId;
pub use observation::LogLevel;
Expand All @@ -16,7 +19,9 @@ pub use observation::ObservationType;
pub use observation::SourceInfo;
pub use payload::Markdown;
pub use payload::Payload;
pub use payload::PayloadBuilder;
pub use payload::MIME_TYPE_RUST_DEBUG;
pub use payload_id::PayloadId;

/// Payload size threshold for blob storage (64KB)
/// Payloads larger than this will be uploaded as separate blobs
Expand All @@ -26,10 +31,10 @@ pub const BLOB_THRESHOLD_BYTES: usize = 65536;
pub const BATCH_SIZE: usize = 100;

/// Estimated maximum observation metadata overhead (in bytes)
/// This includes JSON structure, field names, IDs, timestamps, labels, etc.
/// This includes JSON structure, field names, IDs, timestamps, group_ids, etc.
/// Assumes reasonable limits:
/// - name: ~256 bytes
/// - labels: ~10 labels * ~100 bytes = 1KB
/// - group_ids: ~10 groups * ~40 bytes = 400 bytes
/// - metadata: ~10 entries * ~200 bytes = 2KB
/// - source info: ~256 bytes
/// - IDs, timestamps, JSON structure: ~512 bytes
Expand Down
17 changes: 8 additions & 9 deletions crates/observation-tools-shared/src/observation.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::group_id::GroupId;
use crate::ExecutionId;
use chrono::DateTime;
use chrono::Utc;
Expand Down Expand Up @@ -78,23 +79,20 @@ pub struct Observation {
#[serde(default)]
pub metadata: HashMap<String, String>,

/// Hierarchical labels for grouping observations
/// Uses path convention (e.g., "api/request/headers")
/// IDs of groups this observation belongs to
#[serde(default)]
pub labels: Vec<String>,
pub group_ids: Vec<GroupId>,

/// Parent group ID (used when observation_type == Group)
#[serde(skip_serializing_if = "Option::is_none")]
pub parent_group_id: Option<GroupId>,

/// Parent span ID (for tracing integration)
#[serde(skip_serializing_if = "Option::is_none")]
pub parent_span_id: Option<String>,

/// When this observation was created
pub created_at: DateTime<Utc>,

/// MIME type of the payload (e.g., "text/plain", "application/json")
pub mime_type: String,

/// Size of the payload in bytes
pub payload_size: usize,
}

/// Type of observation
Expand All @@ -103,6 +101,7 @@ pub enum ObservationType {
LogEntry,
Payload,
Span,
Group,
}

/// Log level for observations
Expand Down
51 changes: 51 additions & 0 deletions crates/observation-tools-shared/src/payload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,3 +112,54 @@ impl From<Markdown> for Payload {
Payload::with_mime_type(md.content, "text/markdown")
}
}

/// Builder for creating named payloads to attach to observations.
///
/// Each `PayloadBuilder` pairs a name with a `Payload`, allowing observations
/// to carry multiple named payloads (e.g., "headers", "body", "response").
pub struct PayloadBuilder {
pub name: String,
pub payload: Payload,
}

impl PayloadBuilder {
/// Create a new named payload
pub fn new(name: impl Into<String>, payload: impl Into<Payload>) -> Self {
Self {
name: name.into(),
payload: payload.into(),
}
}

/// Create a named payload from a serde-serializable value (JSON)
pub fn json<T: ?Sized + serde::Serialize>(name: impl Into<String>, value: &T) -> Self {
Self {
name: name.into(),
payload: Payload::json(serde_json::to_string(value).unwrap_or_default()),
}
}

/// Create a named payload from a Debug-formatted value
pub fn debug<T: std::fmt::Debug + ?Sized>(name: impl Into<String>, value: &T) -> Self {
Self {
name: name.into(),
payload: Payload::debug(format!("{:#?}", value)),
}
}

/// Create a named plain text payload
pub fn text(name: impl Into<String>, data: impl Into<String>) -> Self {
Self {
name: name.into(),
payload: Payload::text(data),
}
}

/// Create a named markdown payload
pub fn markdown(name: impl Into<String>, content: impl Into<String>) -> Self {
Self {
name: name.into(),
payload: Payload::with_mime_type(content, "text/markdown"),
}
}
}
27 changes: 27 additions & 0 deletions crates/observation-tools-shared/src/payload_id.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
use serde::Deserialize;
use serde::Serialize;
use utoipa::ToSchema;

/// Unique identifier for a payload (UUIDv7)
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema)]
#[serde(transparent)]
#[schema(value_type = String, example = "018e9a3a2c1b7e3f8d2a4b5c6d7e8f9c")]
pub struct PayloadId(String);

impl PayloadId {
/// Generate a new UUIDv7 payload ID
pub fn new() -> Self {
Self(uuid::Uuid::now_v7().as_simple().to_string())
}

/// Get the string value of this payload ID
pub fn as_str(&self) -> &str {
&self.0
}
}

impl Default for PayloadId {
fn default() -> Self {
Self::new()
}
}
Loading