Skip to content

Commit 7a481d6

Browse files
authored
Merge pull request #211 from observerr411/feat/tracing-span-parent-child
feat(tracing): add parent-child span hierarchy and get_trace function
2 parents 39dd7b7 + a5218cf commit 7a481d6

2 files changed

Lines changed: 343 additions & 2 deletions

File tree

src/contract.rs

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,18 @@ pub struct TracingSpan {
8989
pub started_at: u64,
9090
pub completed_at: u64,
9191
pub status: String,
92+
/// Raw bytes of the parent span's request_id.id, or empty Bytes if this is a root span.
93+
pub parent_request_id_bytes: Bytes,
94+
/// Zero-based index of this span within the trace, used for ordering.
95+
pub span_index: u32,
96+
}
97+
98+
/// Holds the root request ID bytes and the current span index counter for a trace.
99+
#[contracttype]
100+
#[derive(Clone)]
101+
pub struct TracingContext {
102+
pub root_request_id_bytes: Bytes,
103+
pub next_span_index: u32,
92104
}
93105

94106
#[contracttype]
@@ -1024,6 +1036,93 @@ pub fn is_attestor(env: Env, attestor: Address) -> bool {
10241036
.get::<_, TracingSpan>(&(symbol_short!("SPAN"), request_id_bytes))
10251037
}
10261038

1039+
/// Create a child span under a parent span, setting parent_request_id and
1040+
/// incrementing the span_index from the TracingContext stored for the root.
1041+
///
1042+
/// The TracingContext for the root must have been initialised by a prior
1043+
/// `submit_with_request_id` call (which stores span_index = 0).
1044+
pub fn propagate_span(
1045+
env: Env,
1046+
parent_request_id: RequestId,
1047+
child_request_id: RequestId,
1048+
operation: String,
1049+
actor: Address,
1050+
) {
1051+
actor.require_auth();
1052+
let now = env.ledger().timestamp();
1053+
1054+
// Load or create the TracingContext for this root trace
1055+
let ctx_key = (symbol_short!("TRACECTX"), parent_request_id.id.clone());
1056+
let mut ctx: TracingContext = env
1057+
.storage()
1058+
.temporary()
1059+
.get(&ctx_key)
1060+
.unwrap_or(TracingContext {
1061+
root_request_id_bytes: parent_request_id.id.clone(),
1062+
next_span_index: 1,
1063+
});
1064+
1065+
let span_index = ctx.next_span_index;
1066+
ctx.next_span_index += 1;
1067+
env.storage().temporary().set(&ctx_key, &ctx);
1068+
env.storage().temporary().extend_ttl(&ctx_key, SPAN_TTL, SPAN_TTL);
1069+
1070+
// Register child span ID under the root so get_trace can find it
1071+
let child_list_key = (symbol_short!("TRACEIDS"), parent_request_id.id.clone(), span_index);
1072+
env.storage().temporary().set(&child_list_key, &child_request_id.id.clone());
1073+
env.storage().temporary().extend_ttl(&child_list_key, SPAN_TTL, SPAN_TTL);
1074+
1075+
Self::store_span_with_parent(
1076+
&env,
1077+
&child_request_id,
1078+
operation,
1079+
actor,
1080+
now,
1081+
String::from_str(&env, "success"),
1082+
parent_request_id.id.clone(),
1083+
span_index,
1084+
);
1085+
}
1086+
1087+
/// Retrieve all spans associated with a root request ID, ordered by span_index.
1088+
/// Returns the root span first, followed by child spans in creation order.
1089+
pub fn get_trace(env: Env, root_request_id_bytes: Bytes) -> Vec<TracingSpan> {
1090+
let mut spans = Vec::new(&env);
1091+
1092+
// Root span (span_index = 0)
1093+
if let Some(root_span) = env
1094+
.storage()
1095+
.temporary()
1096+
.get::<_, TracingSpan>(&(symbol_short!("SPAN"), root_request_id_bytes.clone()))
1097+
{
1098+
spans.push_back(root_span);
1099+
}
1100+
1101+
// Child spans registered via propagate_span
1102+
let ctx_key = (symbol_short!("TRACECTX"), root_request_id_bytes.clone());
1103+
let ctx: Option<TracingContext> = env.storage().temporary().get(&ctx_key);
1104+
if let Some(ctx) = ctx {
1105+
for i in 1..ctx.next_span_index {
1106+
let child_list_key = (symbol_short!("TRACEIDS"), root_request_id_bytes.clone(), i);
1107+
if let Some(child_id) = env
1108+
.storage()
1109+
.temporary()
1110+
.get::<_, Bytes>(&child_list_key)
1111+
{
1112+
if let Some(child_span) = env
1113+
.storage()
1114+
.temporary()
1115+
.get::<_, TracingSpan>(&(symbol_short!("SPAN"), child_id))
1116+
{
1117+
spans.push_back(child_span);
1118+
}
1119+
}
1120+
}
1121+
}
1122+
1123+
spans
1124+
}
1125+
10271126
// -----------------------------------------------------------------------
10281127
// Attestation retrieval
10291128
// -----------------------------------------------------------------------
@@ -2438,6 +2537,19 @@ pub fn is_attestor(env: Env, attestor: Address) -> bool {
24382537
actor: Address,
24392538
now: u64,
24402539
status: String,
2540+
) {
2541+
Self::store_span_with_parent(env, request_id, operation, actor, now, status, Bytes::new(env), 0);
2542+
}
2543+
2544+
fn store_span_with_parent(
2545+
env: &Env,
2546+
request_id: &RequestId,
2547+
operation: String,
2548+
actor: Address,
2549+
now: u64,
2550+
status: String,
2551+
parent_request_id_bytes: Bytes,
2552+
span_index: u32,
24412553
) {
24422554
let span = TracingSpan {
24432555
request_id: request_id.clone(),
@@ -2446,6 +2558,8 @@ pub fn is_attestor(env: Env, attestor: Address) -> bool {
24462558
started_at: now,
24472559
completed_at: now,
24482560
status,
2561+
parent_request_id_bytes,
2562+
span_index,
24492563
};
24502564
let key = (symbol_short!("SPAN"), request_id.id.clone());
24512565
env.storage().temporary().set(&key, &span);

tests/tracing_span_tests.rs

Lines changed: 229 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,61 @@ mod tracing_span_tests {
2828

2929
#[test]
3030
fn test_span_propagates_across_operations() {
31+
let env = make_env();
32+
env.ledger().set(LedgerInfo {
33+
timestamp: 1000,
34+
protocol_version: 21,
35+
sequence_number: 0,
36+
network_id: Default::default(),
37+
base_reserve: 0,
38+
min_persistent_entry_ttl: 4096,
39+
min_temp_entry_ttl: 16,
40+
max_entry_ttl: 6312000,
41+
});
42+
let contract_id = env.register_contract(None, AnchorKitContract);
43+
let client = AnchorKitContractClient::new(&env, &contract_id);
44+
45+
let admin = Address::generate(&env);
46+
let attestor = Address::generate(&env);
47+
let subject = Address::generate(&env);
48+
49+
client.initialize(&admin);
50+
let sk = SigningKey::generate(&mut OsRng);
51+
register_attestor_with_sep10(&env, &client, &attestor, &attestor, &sk);
52+
53+
// Root span
54+
let root_id = client.generate_request_id();
55+
client.submit_with_request_id(
56+
&root_id,
57+
&attestor,
58+
&subject,
59+
&1000u64,
60+
&payload(&env, 0x01),
61+
&Bytes::new(&env),
62+
);
63+
64+
// Child span
65+
let child_id = client.generate_request_id();
66+
client.propagate_span(
67+
&root_id,
68+
&child_id,
69+
&String::from_str(&env, "fetch_transaction_status"),
70+
&attestor,
71+
);
72+
73+
// Verify child references parent
74+
let child_span = client.get_tracing_span(&child_id.id).unwrap();
75+
assert_eq!(child_span.parent_request_id_bytes, root_id.id);
76+
assert_eq!(child_span.span_index, 1);
77+
78+
// Root span has no parent (empty bytes)
79+
let root_span = client.get_tracing_span(&root_id.id).unwrap();
80+
assert!(root_span.parent_request_id_bytes.is_empty());
81+
assert_eq!(root_span.span_index, 0);
82+
}
83+
84+
#[test]
85+
fn test_root_span_has_no_parent() {
3186
let env = make_env();
3287
env.ledger().set(LedgerInfo {
3388
timestamp: 0,
@@ -44,14 +99,186 @@ mod tracing_span_tests {
4499

45100
let admin = Address::generate(&env);
46101
let attestor = Address::generate(&env);
102+
let subject = Address::generate(&env);
47103

48104
client.initialize(&admin);
105+
let sk = SigningKey::generate(&mut OsRng);
106+
register_attestor_with_sep10(&env, &client, &attestor, &attestor, &sk);
107+
49108
let req_id = client.generate_request_id();
109+
client.submit_with_request_id(
110+
&req_id,
111+
&attestor,
112+
&subject,
113+
&1000u64,
114+
&payload(&env, 0x02),
115+
&Bytes::new(&env),
116+
);
117+
118+
let span = client.get_tracing_span(&req_id.id).unwrap();
119+
assert!(span.parent_request_id_bytes.is_empty(), "root span must have no parent");
120+
assert_eq!(span.span_index, 0);
121+
}
122+
123+
#[test]
124+
fn test_sibling_spans_share_same_parent() {
125+
let env = make_env();
126+
env.ledger().set(LedgerInfo {
127+
timestamp: 500,
128+
protocol_version: 21,
129+
sequence_number: 0,
130+
network_id: Default::default(),
131+
base_reserve: 0,
132+
min_persistent_entry_ttl: 4096,
133+
min_temp_entry_ttl: 16,
134+
max_entry_ttl: 6312000,
135+
});
136+
let contract_id = env.register_contract(None, AnchorKitContract);
137+
let client = AnchorKitContractClient::new(&env, &contract_id);
138+
139+
let admin = Address::generate(&env);
140+
let attestor = Address::generate(&env);
141+
let subject = Address::generate(&env);
142+
143+
client.initialize(&admin);
50144
let sk = SigningKey::generate(&mut OsRng);
51145
register_attestor_with_sep10(&env, &client, &attestor, &attestor, &sk);
52146

53-
let span = client.get_tracing_span(&req_id.id);
54-
assert!(span.is_none());
147+
let root_id = client.generate_request_id();
148+
client.submit_with_request_id(
149+
&root_id,
150+
&attestor,
151+
&subject,
152+
&1000u64,
153+
&payload(&env, 0x03),
154+
&Bytes::new(&env),
155+
);
156+
157+
let child_a = client.generate_request_id();
158+
let child_b = client.generate_request_id();
159+
160+
client.propagate_span(
161+
&root_id,
162+
&child_a,
163+
&String::from_str(&env, "step_a"),
164+
&attestor,
165+
);
166+
client.propagate_span(
167+
&root_id,
168+
&child_b,
169+
&String::from_str(&env, "step_b"),
170+
&attestor,
171+
);
172+
173+
let span_a = client.get_tracing_span(&child_a.id).unwrap();
174+
let span_b = client.get_tracing_span(&child_b.id).unwrap();
175+
176+
// Both siblings reference the same parent
177+
assert_eq!(span_a.parent_request_id_bytes, root_id.id);
178+
assert_eq!(span_b.parent_request_id_bytes, root_id.id);
179+
// Siblings have different span indices
180+
assert_ne!(span_a.span_index, span_b.span_index);
181+
}
182+
183+
#[test]
184+
fn test_get_trace_returns_all_spans_in_order() {
185+
let env = make_env();
186+
env.ledger().set(LedgerInfo {
187+
timestamp: 100,
188+
protocol_version: 21,
189+
sequence_number: 0,
190+
network_id: Default::default(),
191+
base_reserve: 0,
192+
min_persistent_entry_ttl: 4096,
193+
min_temp_entry_ttl: 16,
194+
max_entry_ttl: 6312000,
195+
});
196+
let contract_id = env.register_contract(None, AnchorKitContract);
197+
let client = AnchorKitContractClient::new(&env, &contract_id);
198+
199+
let admin = Address::generate(&env);
200+
let attestor = Address::generate(&env);
201+
let subject = Address::generate(&env);
202+
203+
client.initialize(&admin);
204+
let sk = SigningKey::generate(&mut OsRng);
205+
register_attestor_with_sep10(&env, &client, &attestor, &attestor, &sk);
206+
207+
let root_id = client.generate_request_id();
208+
client.submit_with_request_id(
209+
&root_id,
210+
&attestor,
211+
&subject,
212+
&1000u64,
213+
&payload(&env, 0x04),
214+
&Bytes::new(&env),
215+
);
216+
217+
let child1 = client.generate_request_id();
218+
let child2 = client.generate_request_id();
219+
220+
client.propagate_span(&root_id, &child1, &String::from_str(&env, "op1"), &attestor);
221+
client.propagate_span(&root_id, &child2, &String::from_str(&env, "op2"), &attestor);
222+
223+
let trace = client.get_trace(&root_id.id);
224+
assert_eq!(trace.len(), 3);
225+
// First span is root (span_index 0)
226+
assert_eq!(trace.get(0).unwrap().span_index, 0);
227+
assert_eq!(trace.get(1).unwrap().span_index, 1);
228+
assert_eq!(trace.get(2).unwrap().span_index, 2);
229+
}
230+
231+
#[test]
232+
fn test_structured_log_format_includes_parent_request_id() {
233+
let env = make_env();
234+
env.ledger().set(LedgerInfo {
235+
timestamp: 200,
236+
protocol_version: 21,
237+
sequence_number: 0,
238+
network_id: Default::default(),
239+
base_reserve: 0,
240+
min_persistent_entry_ttl: 4096,
241+
min_temp_entry_ttl: 16,
242+
max_entry_ttl: 6312000,
243+
});
244+
let contract_id = env.register_contract(None, AnchorKitContract);
245+
let client = AnchorKitContractClient::new(&env, &contract_id);
246+
247+
let admin = Address::generate(&env);
248+
let attestor = Address::generate(&env);
249+
let subject = Address::generate(&env);
250+
251+
client.initialize(&admin);
252+
let sk = SigningKey::generate(&mut OsRng);
253+
register_attestor_with_sep10(&env, &client, &attestor, &attestor, &sk);
254+
255+
let root_id = client.generate_request_id();
256+
client.submit_with_request_id(
257+
&root_id,
258+
&attestor,
259+
&subject,
260+
&1000u64,
261+
&payload(&env, 0x05),
262+
&Bytes::new(&env),
263+
);
264+
265+
let child_id = client.generate_request_id();
266+
client.propagate_span(
267+
&root_id,
268+
&child_id,
269+
&String::from_str(&env, "sep6_deposit"),
270+
&attestor,
271+
);
272+
273+
let child_span = client.get_tracing_span(&child_id.id).unwrap();
274+
// Structured log: parent_request_id_bytes is non-empty when span is a child
275+
assert!(!child_span.parent_request_id_bytes.is_empty());
276+
assert_eq!(
277+
child_span.parent_request_id_bytes,
278+
root_id.id,
279+
"structured log must include parent_request_id"
280+
);
281+
assert_eq!(child_span.operation, String::from_str(&env, "sep6_deposit"));
55282
}
56283

57284
#[test]

0 commit comments

Comments
 (0)