/// TODO: spanを送信する際の形式はprotobufから生成したstructを使うった方が良いので
/// ライブラリとしてのI/Fを整理したSpanと送信フォーマットしてのSpanを分けたい
// TraceID represents a unique 128-bit trace identifier
///|
pub struct TraceID {
value : String // 32-character hex string
} derive(Eq, Show)
///|
impl ToJson for TraceID with to_json(self : TraceID) -> Json {
Json::string(self.value)
}
// Create a new TraceID
///|
pub fn TraceID::new(value : String) -> TraceID {
{ value, }
}
// Get the string value of TraceID
///|
pub fn TraceID::to_string(self : TraceID) -> String {
self.value
}
// Check if TraceID is valid (non-zero)
///|
pub fn TraceID::is_valid(self : TraceID) -> Bool {
self.value != "00000000000000000000000000000000"
}
// SpanID represents a unique 64-bit span identifier
///|
pub struct SpanID {
value : String // 16-character hex string
} derive(Eq, Show)
///|
impl ToJson for SpanID with to_json(self : SpanID) -> Json {
Json::string(self.value)
}
// Create a new SpanID
///|
pub fn SpanID::new(value : String) -> SpanID {
{ value, }
}
// Get the string value of SpanID
///|
pub fn SpanID::to_string(self : SpanID) -> String {
self.value
}
// Check if SpanID is valid (non-zero)
///|
pub fn SpanID::is_valid(self : SpanID) -> Bool {
self.value != "0000000000000000"
}
///|
pub struct TraceState {
priv map : Map[String, String]
} derive(Eq, Show)
///|
pub fn TraceState::to_http_header(self : TraceState) -> String {
self.map
.iter()
.map(fn(kv) {
let key = kv.0
let value = kv.1
key + "=" + value
})
.join(",")
}
///|
impl Default for TraceState with default() -> TraceState {
TraceState::{ map: Default::default() }
}
///|
impl ToJson for TraceState with to_json(self : TraceState) -> Json {
Json::string(self.to_http_header())
}
///|
pub fn TraceState::parse(value : String) -> TraceState raise {
value
.split(",")
.fold(init=TraceState::default(), fn(
acc : TraceState,
pair,
) -> TraceState raise {
let parts = pair.split("=").to_array()
if parts.length() != 2 {
fail("Invalid trace state entry: " + pair.to_string())
}
let key = parts[0].trim()
let val = parts[1].trim()
let new_map = acc.map.copy()
new_map.set(key.to_string(), val.to_string())
{ map: new_map }
})
}
///|
pub fn TraceState::delete(self : TraceState, key : String) -> TraceState {
let new_map = self.map.copy()
new_map.remove(key)
{ map: new_map }
}
///|
pub fn TraceState::insert(
self : TraceState,
key : String,
value : String,
) -> TraceState {
// TODO: check value validity
let new_map = self.map.copy()
new_map.set(key, value)
{ map: new_map }
}
///|
pub fn TraceState::get(self : TraceState, key : String) -> String? {
self.map.get(key)
}
///|
pub fn TraceState::to_map(self : TraceState) -> Map[String, String] {
self.map.copy()
}
///|
pub struct TraceFlags {
priv value : Byte
} derive(Eq, Show)
///|
impl ToJson for TraceFlags with to_json(self : TraceFlags) -> Json {
Json::number(self.value.to_double())
}
///|
pub fn TraceFlags::is_sampled(self : TraceFlags) -> Bool {
(self.value & 0x01) != 0
}
///|
fn TraceFlags::to_uint(self : TraceFlags) -> UInt {
self.value.to_uint()
}
///|
pub fn TraceFlags::sampled() -> TraceFlags {
{ value: 0x01 }
}
// SpanContext represents the portion of a Span which must be serialized and
// propagated along side of a distributed context.
///|
pub struct SpanContext {
trace_id : TraceID
span_id : SpanID
trace_flags : TraceFlags
is_remote : Bool
} derive(ToJson)
// Create a new SpanContext
///|
pub fn SpanContext::new(
trace_id : TraceID,
span_id : SpanID,
trace_flags : TraceFlags,
is_remote : Bool,
) -> SpanContext {
{ trace_id, span_id, trace_flags, is_remote }
}
// Check if the span context is valid (non-zero IDs)
///|
pub fn SpanContext::is_valid(self : SpanContext) -> Bool {
self.trace_id.is_valid() && self.span_id.is_valid()
}
// Check if the span is sampled
///|
pub fn SpanContext::is_sampled(self : SpanContext) -> Bool {
self.trace_flags.is_sampled()
}
// SpanKind describes the relationship between the Span, its parents, and its children
///|
pub(all) enum SpanKind {
Unspecified // Default value. Indicates that the SpanKind is unspecified.
Internal // Internal operation within an application
Server // Server-side handling of a synchronous RPC or other remote request
Client // Client-side of a synchronous RPC or other remote request
Producer // Parent of an asynchronous request (e.g., message producer)
Consumer // Child of an asynchronous request (e.g., message consumer)
} derive(Eq, Show)
///|
pub impl ToJson for SpanKind with to_json(self : SpanKind) -> Json {
let kind_code = match self {
SpanKind::Unspecified => 0
SpanKind::Internal => 1
SpanKind::Server => 2
SpanKind::Client => 3
SpanKind::Producer => 4
SpanKind::Consumer => 5
}
Json::number(kind_code.to_double())
}
// SpanStatus represents the status of a finished Span
///|
pub(all) enum SpanStatus {
Unset // The default status
Ok // The operation completed successfully
Error // The operation contains an error
} derive(Eq, Show)
///|
pub impl ToJson for SpanStatus with to_json(self : SpanStatus) -> Json {
let status_code = match self {
SpanStatus::Unset => "STATUS_CODE_UNSET"
SpanStatus::Ok => "STATUS_CODE_OK"
SpanStatus::Error => "STATUS_CODE_ERROR"
}
let message = match self {
SpanStatus::Unset => "Unset"
SpanStatus::Ok => "Ok"
SpanStatus::Error => "Error"
}
Json::object({
"code": Json::string(status_code),
"message": Json::string(message),
})
}
// SpanEvent represents a single event within a Span
///|
pub struct SpanEvent {
name : String
timestamp : Int64 // Unix timestamp in nanoseconds
attributes : Attributes
} derive(ToJson)
// Create a new SpanEvent
///|
pub fn SpanEvent::new(
name : String,
attributes : Attributes,
timestamp? : Int64 = current_time_nanos(),
) -> SpanEvent {
{ name, timestamp, attributes }
}
///|
pub fn SpanEvent::to_protobuf(self : SpanEvent) -> @trace.Span_Event {
let p_event = @trace.Span_Event::default()
p_event.time_unix_nano = self.timestamp.reinterpret_as_uint64()
p_event.name = self.name
p_event.attributes = self.attributes.to_protobuf()
p_event
}
// Span represents a single operation within a trace
///|
pub struct Span {
traceId : TraceID
spanId : SpanID
trace_state : TraceState
parent_span_id : SpanID?
trace_flags : TraceFlags
name : String
kind : SpanKind
start_time_unix_nano : Int64
end_time_unix_nano : Int64?
attributes : Attributes
dropped_attributes_count : Int
events : Array[SpanEvent]
links : Array[SpanLink]
dropped_events_count : Int
status : SpanStatus
resource : Resource
tracer_identifier : TracerIdentifier
on_end : ((Span) -> Unit)?
}
///|
pub fn hex_to_bytes(hex : String) -> Bytes? {
// Hex string must have even length (2 chars per byte)
if hex.length() % 2 != 0 {
return None
}
let byte_count = hex.length() / 2
let byte_array = Array::new(capacity=byte_count)
for i = 0; i < byte_count; i = i + 1 {
let high_char = hex[i * 2].to_int().unsafe_to_char()
let low_char = hex[i * 2 + 1].to_int().unsafe_to_char()
match (char_to_int(high_char), char_to_int(low_char)) {
(Some(high), Some(low)) => {
let byte_value = high * 16 + low
byte_array.push(byte_value.to_byte())
}
_ => return None
}
}
Some(Bytes::from_array(byte_array))
}
///|
pub fn char_to_int(c : Char) -> Int? {
if c >= '0' && c <= '9' {
Some(c.to_int() - '0'.to_int())
} else if c >= 'a' && c <= 'f' {
Some(c.to_int() - 'a'.to_int() + 10)
} else if c >= 'A' && c <= 'F' {
Some(c.to_int() - 'A'.to_int() + 10)
} else {
None
}
}
///|
impl ToJson for Span with to_json(self : Span) -> Json {
// OTLP JSON format requires hex-encoded traceId and spanId (not base64!)
let json_obj : Map[String, Json] = {}
// Required fields with hex encoding (lowercase)
json_obj["traceId"] = Json::string(self.traceId.value)
json_obj["spanId"] = Json::string(self.spanId.value)
// Optional parent span ID
match self.parent_span_id {
Some(id) => json_obj["parentSpanId"] = Json::string(id.value)
None => ()
}
// Trace state
json_obj["traceState"] = self.trace_state.to_json()
// Span name and kind
json_obj["name"] = Json::string(self.name)
json_obj["kind"] = @trace.Span_SpanKind::to_string(
match self.kind {
SpanKind::Unspecified => @trace.Span_SpanKind::SPAN_KIND_UNSPECIFIED
SpanKind::Internal => @trace.Span_SpanKind::SPAN_KIND_INTERNAL
SpanKind::Server => @trace.Span_SpanKind::SPAN_KIND_SERVER
SpanKind::Client => @trace.Span_SpanKind::SPAN_KIND_CLIENT
SpanKind::Producer => @trace.Span_SpanKind::SPAN_KIND_PRODUCER
SpanKind::Consumer => @trace.Span_SpanKind::SPAN_KIND_CONSUMER
},
).to_json()
// Timestamps as strings
json_obj["startTimeUnixNano"] = Json::string(
self.start_time_unix_nano.to_string(),
)
match self.end_time_unix_nano {
Some(end) => json_obj["endTimeUnixNano"] = Json::string(end.to_string())
None => ()
}
// Flags
if self.trace_flags.is_sampled() {
json_obj["flags"] = self.trace_flags.to_json()
}
json_obj["status"] = self.status.to_json()
json_obj["attributes"] = self.attributes.to_json()
json_obj["events"] = Json::array(self.events.map(e => e.to_json()))
json_obj["links"] = Json::array(self.links.map(l => l.to_json()))
Json::object(json_obj)
}
///|
pub fn Span::to_protobuf(self : Span) -> @trace.Span {
let p_span = @trace.Span::default()
p_span.trace_id = hex_to_bytes(self.traceId.value).unwrap()
p_span.span_id = hex_to_bytes(self.spanId.value).unwrap()
// p_span.span_id = self.spanId.value
match self.parent_span_id {
Some(id) => p_span.parent_span_id = hex_to_bytes(id.value).unwrap()
None => ()
}
p_span.trace_state = self.trace_state.to_http_header()
p_span.name = self.name
p_span.kind = match self.kind {
SpanKind::Unspecified => @trace.Span_SpanKind::SPAN_KIND_UNSPECIFIED
SpanKind::Internal => @trace.Span_SpanKind::SPAN_KIND_INTERNAL
SpanKind::Server => @trace.Span_SpanKind::SPAN_KIND_SERVER
SpanKind::Client => @trace.Span_SpanKind::SPAN_KIND_CLIENT
SpanKind::Producer => @trace.Span_SpanKind::SPAN_KIND_PRODUCER
SpanKind::Consumer => @trace.Span_SpanKind::SPAN_KIND_CONSUMER
}
p_span.start_time_unix_nano = self.start_time_unix_nano.reinterpret_as_uint64()
match self.end_time_unix_nano {
Some(end) => p_span.end_time_unix_nano = end.reinterpret_as_uint64()
None => ()
}
p_span.flags = self.trace_flags.to_uint()
p_span.status = match self.status {
SpanStatus::Unset =>
@trace.Status::{
code: @trace.Status_StatusCode::STATUS_CODE_UNSET,
message: "unset",
}
SpanStatus::Ok =>
@trace.Status::{
code: @trace.Status_StatusCode::STATUS_CODE_OK,
message: "ok",
}
SpanStatus::Error =>
@trace.Status::{
code: @trace.Status_StatusCode::STATUS_CODE_ERROR,
message: "error",
}
}
// Serialize attributes
p_span.attributes = self.attributes.to_protobuf()
// Serialize events
p_span.events = self.events.map(e => e.to_protobuf())
p_span.links = self.links.map(l => l.to_protobuf())
p_span
}
// Create a new Span
///|
pub fn Span::new(
context : SpanContext,
name : String,
kind : SpanKind,
start_time : Int64,
resource : Resource,
tracer_identifier : TracerIdentifier,
parent_span_id : SpanID?,
on_end? : (Span) -> Unit,
) -> Span {
{
traceId: context.trace_id,
spanId: context.span_id,
trace_state: Default::default(),
parent_span_id,
trace_flags: context.trace_flags,
name,
kind,
start_time_unix_nano: start_time,
end_time_unix_nano: None,
status: SpanStatus::Unset,
attributes: Attributes::new(),
dropped_attributes_count: 0,
events: [],
links: [],
dropped_events_count: 0,
resource,
tracer_identifier,
on_end,
}
}
// Add an attribute to the Span
///|
pub fn Span::set_attribute(
self : Span,
key : String,
value : AttributeValue,
) -> Span {
let new_attributes = self.attributes.set(key, value)
{ ..self, attributes: new_attributes }
}
// Add an event to the Span
///|
pub fn Span::add_event(self : Span, event : SpanEvent) -> Span {
{ ..self, events: [..self.events, event] }
}
///|
pub fn Span::add_link(
self : Span,
linked_context : SpanContext,
attributes? : Attributes = Default::default(),
) -> Span {
let link = SpanLink::from_span_context(
linked_context,
self.trace_state,
attributes,
)
{ ..self, links: [..self.links, link] }
}
// Set the status of the Span
///|
pub fn Span::set_status(self : Span, status : SpanStatus) -> Span {
{ ..self, status, }
}
// End the Span with the given timestamp
///|
pub fn Span::end(
self : Span,
end_time_ns? : Int64 = current_time_nanos(),
) -> Span {
let end_span = { ..self, end_time_unix_nano: Some(end_time_ns) }
guard end_span.on_end is Some(on_end_fn) else { return end_span }
on_end_fn(end_span)
end_span
}
// Check if the Span has ended
///|
pub fn Span::is_ended(self : Span) -> Bool {
self.end_time_unix_nano is Some(_)
}
// Get the duration of the Span in nanoseconds (returns None if not ended)
///|
pub fn Span::duration(self : Span) -> Int64? {
match self.end_time_unix_nano {
Some(end) => Some(end - self.start_time_unix_nano)
None => None
}
}
///|
pub fn Span::context(self : Span) -> SpanContext {
SpanContext::new(self.traceId, self.spanId, self.trace_flags, false)
}
///|
pub struct SpanLink {
trace_id : TraceID
span_id : SpanID
trace_state : TraceState
trace_flags : TraceFlags
attributes : Attributes
}
///|
pub fn SpanLink::from_span_context(
context : SpanContext,
trace_state : TraceState,
attributes : Attributes,
) -> SpanLink {
{
trace_id: context.trace_id,
span_id: context.span_id,
trace_state,
trace_flags: context.trace_flags,
attributes,
}
}
///|
pub impl ToJson for SpanLink with to_json(self : SpanLink) -> Json {
let json_obj : Map[String, Json] = {}
json_obj["trace_id"] = Json::string(self.trace_id.value)
json_obj["span_id"] = Json::string(self.span_id.value)
json_obj["trace_state"] = self.trace_state.to_json()
json_obj["trace_flags"] = self.trace_flags.to_json()
json_obj["attributes"] = self.attributes.to_json()
Json::object(json_obj)
}
///|
pub fn SpanLink::to_protobuf(self : SpanLink) -> @trace.Span_Link {
let p_link = @trace.Span_Link::default()
p_link.trace_id = hex_to_bytes(self.trace_id.value).unwrap()
p_link.span_id = hex_to_bytes(self.span_id.value).unwrap()
p_link.trace_state = self.trace_state.to_http_header()
p_link.attributes = self.attributes.to_protobuf()
p_link.flags = self.trace_flags.to_uint()
p_link
}
///|
#cfg(target="js")
extern "js" fn js_date_now_double() -> Double =
#|() => Date.now()
///|
#cfg(target="js")
pub fn get_current_time_ms() -> Int64 {
js_date_now_double().to_int64()
}
///|
#cfg(target="native")
fn get_current_time_ms() -> Int64 {
@async.now()
}
///|
pub fn current_time_nanos() -> Int64 {
get_current_time_ms() * 1_000_000
}
// Helper function to generate a random hex string of given length
// Note: Uses current timestamp as seed for randomness
///|
pub fn rand_by_current_time() -> @random.Rand {
// Create a 32-byte seed from timestamp to ensure different values on each call
let timestamp = get_current_time_ms()
let seed_array = Array::new(capacity=32)
// Fill seed with timestamp bytes (repeat as needed)
for i = 0; i < 32; i = i + 1 {
let shift = i % 8 * 8
let timestamp_int = timestamp.to_int()
let byte_val = ((timestamp_int >> shift) & 0xFF).to_byte()
seed_array.push(byte_val)
}
let seed_bytes = Bytes::from_array(seed_array)
@random.Rand::chacha8(seed=seed_bytes)
}
///|
fn random_hex(length : Int, rand : @random.Rand) -> String {
let chars = [
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f',
]
let mut result = ""
for i = 0; i < length; i = i + 1 {
let index = rand.int(limit=16)
result = result + chars[index].to_string()
}
result
}
///|
test "random_hex generates hex string of correct length" {
let hex16 = random_hex(16, rand_by_current_time())
assert_eq(hex16.length(), 16)
let hex32 = random_hex(32, rand_by_current_time())
assert_eq(hex32.length(), 32)
// let h_s = hex_to_bytes(hex16).unwrap()
// println(@encoding/utf8.decode(h_s))
}
// Generate a new TraceID (128-bit, 32 hex characters)
///|
pub fn generate_trace_id(rand : @random.Rand) -> TraceID {
TraceID::new(random_hex(32, rand))
}
// Generate a new SpanID (64-bit, 16 hex characters)
///|
pub fn generate_span_id(rand : @random.Rand) -> SpanID {
SpanID::new(random_hex(16, rand))
}