1use std::collections::BTreeSet;
12use std::sync::Arc;
13
14use async_trait::async_trait;
15use http::{Method, StatusCode};
16use serde_json::{json, Map, Value};
17use tokio::sync::Mutex as AsyncMutex;
18use uuid::Uuid;
19
20use fakecloud_core::service::{AwsRequest, AwsResponse, AwsService, AwsServiceError};
21use fakecloud_persistence::SnapshotStore;
22
23use crate::graph::build_service_graph;
24use crate::persistence::save_snapshot;
25use crate::segment::{parse_segment, StoredSegment};
26use crate::state::{now_epoch, SharedXrayState, XrayData, DEFAULT_SAMPLING_RULE};
27
28pub const XRAY_ACTIONS: &[&str] = &[
30 "BatchGetTraces",
31 "CancelTraceRetrieval",
32 "CreateGroup",
33 "CreateSamplingRule",
34 "DeleteGroup",
35 "DeleteResourcePolicy",
36 "DeleteSamplingRule",
37 "GetEncryptionConfig",
38 "GetGroup",
39 "GetGroups",
40 "GetIndexingRules",
41 "GetInsight",
42 "GetInsightEvents",
43 "GetInsightImpactGraph",
44 "GetInsightSummaries",
45 "GetRetrievedTracesGraph",
46 "GetSamplingRules",
47 "GetSamplingStatisticSummaries",
48 "GetSamplingTargets",
49 "GetServiceGraph",
50 "GetTimeSeriesServiceStatistics",
51 "GetTraceGraph",
52 "GetTraceSegmentDestination",
53 "GetTraceSummaries",
54 "ListResourcePolicies",
55 "ListRetrievedTraces",
56 "ListTagsForResource",
57 "PutEncryptionConfig",
58 "PutResourcePolicy",
59 "PutTelemetryRecords",
60 "PutTraceSegments",
61 "StartTraceRetrieval",
62 "TagResource",
63 "UntagResource",
64 "UpdateGroup",
65 "UpdateIndexingRule",
66 "UpdateSamplingRule",
67 "UpdateTraceSegmentDestination",
68];
69
70const MUTATING: &[&str] = &[
72 "PutTraceSegments",
73 "CreateGroup",
74 "UpdateGroup",
75 "DeleteGroup",
76 "CreateSamplingRule",
77 "UpdateSamplingRule",
78 "DeleteSamplingRule",
79 "PutEncryptionConfig",
80 "PutResourcePolicy",
81 "DeleteResourcePolicy",
82 "TagResource",
83 "UntagResource",
84 "UpdateTraceSegmentDestination",
85 "UpdateIndexingRule",
86 "StartTraceRetrieval",
87 "CancelTraceRetrieval",
88];
89
90pub struct XrayService {
91 state: SharedXrayState,
92 snapshot_store: Option<Arc<dyn SnapshotStore>>,
93 snapshot_lock: Arc<AsyncMutex<()>>,
94}
95
96impl XrayService {
97 pub fn new(state: SharedXrayState) -> Self {
98 Self {
99 state,
100 snapshot_store: None,
101 snapshot_lock: Arc::new(AsyncMutex::new(())),
102 }
103 }
104
105 pub fn with_snapshot_store(mut self, store: Arc<dyn SnapshotStore>) -> Self {
106 self.snapshot_store = Some(store);
107 self
108 }
109
110 async fn save(&self) {
111 save_snapshot(
112 &self.state,
113 self.snapshot_store.clone(),
114 &self.snapshot_lock,
115 )
116 .await;
117 }
118
119 pub fn snapshot_hook(&self) -> Option<fakecloud_persistence::SnapshotHook> {
121 let store = self.snapshot_store.clone()?;
122 let state = self.state.clone();
123 let lock = self.snapshot_lock.clone();
124 Some(Arc::new(move || {
125 let state = state.clone();
126 let store = store.clone();
127 let lock = lock.clone();
128 Box::pin(async move {
129 crate::persistence::save_snapshot(&state, Some(store), &lock).await;
130 })
131 }))
132 }
133
134 fn resolve_action(req: &AwsRequest) -> Option<&'static str> {
136 if req.method != Method::POST {
137 return None;
138 }
139 let raw = req.raw_path.split('?').next().unwrap_or(&req.raw_path);
140 let path = raw.strip_suffix('/').unwrap_or(raw);
141 let action = match path {
142 "/Traces" => "BatchGetTraces",
143 "/CancelTraceRetrieval" => "CancelTraceRetrieval",
144 "/CreateGroup" => "CreateGroup",
145 "/CreateSamplingRule" => "CreateSamplingRule",
146 "/DeleteGroup" => "DeleteGroup",
147 "/DeleteResourcePolicy" => "DeleteResourcePolicy",
148 "/DeleteSamplingRule" => "DeleteSamplingRule",
149 "/EncryptionConfig" => "GetEncryptionConfig",
150 "/GetGroup" => "GetGroup",
151 "/Groups" => "GetGroups",
152 "/GetIndexingRules" => "GetIndexingRules",
153 "/Insight" => "GetInsight",
154 "/InsightEvents" => "GetInsightEvents",
155 "/InsightImpactGraph" => "GetInsightImpactGraph",
156 "/InsightSummaries" => "GetInsightSummaries",
157 "/GetRetrievedTracesGraph" => "GetRetrievedTracesGraph",
158 "/GetSamplingRules" => "GetSamplingRules",
159 "/SamplingStatisticSummaries" => "GetSamplingStatisticSummaries",
160 "/SamplingTargets" => "GetSamplingTargets",
161 "/ServiceGraph" => "GetServiceGraph",
162 "/TimeSeriesServiceStatistics" => "GetTimeSeriesServiceStatistics",
163 "/TraceGraph" => "GetTraceGraph",
164 "/GetTraceSegmentDestination" => "GetTraceSegmentDestination",
165 "/TraceSummaries" => "GetTraceSummaries",
166 "/ListResourcePolicies" => "ListResourcePolicies",
167 "/ListRetrievedTraces" => "ListRetrievedTraces",
168 "/ListTagsForResource" => "ListTagsForResource",
169 "/PutEncryptionConfig" => "PutEncryptionConfig",
170 "/PutResourcePolicy" => "PutResourcePolicy",
171 "/TelemetryRecords" => "PutTelemetryRecords",
172 "/TraceSegments" => "PutTraceSegments",
173 "/StartTraceRetrieval" => "StartTraceRetrieval",
174 "/TagResource" => "TagResource",
175 "/UntagResource" => "UntagResource",
176 "/UpdateGroup" => "UpdateGroup",
177 "/UpdateIndexingRule" => "UpdateIndexingRule",
178 "/UpdateSamplingRule" => "UpdateSamplingRule",
179 "/UpdateTraceSegmentDestination" => "UpdateTraceSegmentDestination",
180 _ => return None,
181 };
182 Some(action)
183 }
184}
185
186#[async_trait]
187impl AwsService for XrayService {
188 fn service_name(&self) -> &str {
189 "xray"
190 }
191
192 async fn handle(&self, req: AwsRequest) -> Result<AwsResponse, AwsServiceError> {
193 let Some(action) = Self::resolve_action(&req) else {
194 return Err(AwsServiceError::aws_error(
195 StatusCode::NOT_FOUND,
196 "UnknownOperationException",
197 format!("Unknown operation: {} {}", req.method, req.raw_path),
198 ));
199 };
200 let result = self.dispatch(action, &req);
201 let success = matches!(result.as_ref(), Ok(resp) if resp.status.is_success());
202 if MUTATING.contains(&action) && success {
203 self.save().await;
204 }
205 result
206 }
207
208 fn supported_actions(&self) -> &[&str] {
209 XRAY_ACTIONS
210 }
211}
212
213struct Ctx {
215 account: String,
216 region: String,
217}
218
219impl XrayService {
220 fn dispatch(&self, action: &str, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
221 let body = parse_body(req)?;
222 crate::validate::validate_input(action, &body)?;
223 let ctx = Ctx {
224 account: req.account_id.clone(),
225 region: req.region.clone(),
226 };
227 match action {
228 "PutTraceSegments" => self.put_trace_segments(&ctx, &body),
229 "BatchGetTraces" => self.batch_get_traces(&ctx, &body),
230 "GetTraceSummaries" => self.get_trace_summaries(&ctx, &body),
231 "GetServiceGraph" => self.get_service_graph(&ctx, &body),
232 "GetTraceGraph" => self.get_trace_graph(&ctx, &body),
233 "GetTimeSeriesServiceStatistics" => self.get_time_series(&ctx, &body),
234 "CreateGroup" => self.create_group(&ctx, &body),
235 "GetGroup" => self.get_group(&ctx, &body),
236 "GetGroups" => self.get_groups(&ctx),
237 "UpdateGroup" => self.update_group(&ctx, &body),
238 "DeleteGroup" => self.delete_group(&ctx, &body),
239 "CreateSamplingRule" => self.create_sampling_rule(&ctx, &body),
240 "GetSamplingRules" => self.get_sampling_rules(&ctx),
241 "UpdateSamplingRule" => self.update_sampling_rule(&ctx, &body),
242 "DeleteSamplingRule" => self.delete_sampling_rule(&ctx, &body),
243 "GetSamplingTargets" => self.get_sampling_targets(&ctx, &body),
244 "GetSamplingStatisticSummaries" => Self::get_sampling_statistic_summaries(),
245 "GetEncryptionConfig" => self.get_encryption_config(&ctx),
246 "PutEncryptionConfig" => self.put_encryption_config(&ctx, &body),
247 "PutResourcePolicy" => self.put_resource_policy(&ctx, &body),
248 "DeleteResourcePolicy" => self.delete_resource_policy(&ctx, &body),
249 "ListResourcePolicies" => self.list_resource_policies(&ctx),
250 "TagResource" => self.tag_resource(&ctx, &body),
251 "UntagResource" => self.untag_resource(&ctx, &body),
252 "ListTagsForResource" => self.list_tags_for_resource(&ctx, &body),
253 "GetIndexingRules" => Self::get_indexing_rules(),
254 "UpdateIndexingRule" => Self::update_indexing_rule(&body),
255 "GetTraceSegmentDestination" => self.get_trace_segment_destination(&ctx),
256 "UpdateTraceSegmentDestination" => self.update_trace_segment_destination(&ctx, &body),
257 "PutTelemetryRecords" => Ok(ok(json!({}))),
258 "StartTraceRetrieval" => self.start_trace_retrieval(&ctx, &body),
259 "ListRetrievedTraces" => self.list_retrieved_traces(&ctx, &body),
260 "GetRetrievedTracesGraph" => self.get_retrieved_traces_graph(&ctx, &body),
261 "CancelTraceRetrieval" => self.cancel_trace_retrieval(&ctx, &body),
262 "GetInsight" => Self::get_insight(&body),
263 "GetInsightEvents" => Self::get_insight_events(&body),
264 "GetInsightImpactGraph" => Self::get_insight_impact_graph(&body),
265 "GetInsightSummaries" => Self::get_insight_summaries(),
266 _ => Err(AwsServiceError::action_not_implemented("xray", action)),
267 }
268 }
269
270 fn put_trace_segments(&self, ctx: &Ctx, body: &Value) -> Result<AwsResponse, AwsServiceError> {
273 let docs = body
274 .get("TraceSegmentDocuments")
275 .and_then(Value::as_array)
276 .cloned()
277 .unwrap_or_default();
278 let mut unprocessed: Vec<Value> = Vec::new();
279 let mut guard = self.state.write();
280 let data = guard.get_or_create(&ctx.account);
281 for doc in &docs {
282 let Some(s) = doc.as_str() else {
283 unprocessed.push(json!({
284 "ErrorCode": "MalformedJson",
285 "Message": "Trace segment document must be a JSON string."
286 }));
287 continue;
288 };
289 match parse_segment(s) {
290 Ok(seg) => {
291 let entry = data.traces.entry(seg.trace_id.clone()).or_default();
292 entry.retain(|e| e.id != seg.id);
295 entry.push(seg);
296 }
297 Err(e) => {
298 let mut m = Map::new();
299 if let Some(id) = e.id {
300 m.insert("Id".into(), json!(id));
301 }
302 m.insert("ErrorCode".into(), json!(e.code));
303 m.insert("Message".into(), json!(e.message));
304 unprocessed.push(Value::Object(m));
305 }
306 }
307 }
308 Ok(ok(json!({ "UnprocessedTraceSegments": unprocessed })))
309 }
310
311 fn batch_get_traces(&self, ctx: &Ctx, body: &Value) -> Result<AwsResponse, AwsServiceError> {
312 let ids = string_list(body, "TraceIds");
313 let guard = self.state.read();
314 let data = guard.get(&ctx.account);
315 let mut traces: Vec<Value> = Vec::new();
316 let mut unprocessed: Vec<Value> = Vec::new();
317 for id in &ids {
318 match data.and_then(|d| d.traces.get(id)) {
319 Some(segs) if !segs.is_empty() => traces.push(assemble_trace(id, segs)),
320 _ => unprocessed.push(json!(id)),
321 }
322 }
323 Ok(ok(json!({
324 "Traces": traces,
325 "UnprocessedTraceIds": unprocessed,
326 })))
327 }
328
329 fn get_trace_summaries(&self, ctx: &Ctx, body: &Value) -> Result<AwsResponse, AwsServiceError> {
330 let (start, end) = time_range(body);
331 let filter = body
332 .get("FilterExpression")
333 .and_then(Value::as_str)
334 .unwrap_or("");
335 let guard = self.state.read();
336 let data = guard.get(&ctx.account);
337 let mut summaries: Vec<Value> = Vec::new();
338 let mut processed: i64 = 0;
339 if let Some(data) = data {
340 for (id, segs) in &data.traces {
341 let Some(root_start) = segs.iter().map(|s| s.start_time).reduce(f64::min) else {
342 continue;
343 };
344 if root_start < start || root_start > end {
345 continue;
346 }
347 processed += 1;
348 if trace_matches_filter(filter, segs) {
349 summaries.push(build_trace_summary(id, segs));
350 }
351 }
352 }
353 Ok(ok(json!({
354 "TraceSummaries": summaries,
355 "ApproximateTime": now_epoch(),
356 "TracesProcessedCount": processed,
357 })))
358 }
359
360 fn get_service_graph(&self, ctx: &Ctx, body: &Value) -> Result<AwsResponse, AwsServiceError> {
361 let (start, end) = time_range(body);
362 let guard = self.state.read();
363 let services = match guard.get(&ctx.account) {
364 Some(data) => {
365 let segs = segments_in_range(data, start, end);
366 build_service_graph(&segs)
367 }
368 None => Vec::new(),
369 };
370 Ok(ok(json!({
371 "StartTime": start,
372 "EndTime": end,
373 "Services": services,
374 "ContainsOldGroupVersions": false,
375 })))
376 }
377
378 fn get_trace_graph(&self, ctx: &Ctx, body: &Value) -> Result<AwsResponse, AwsServiceError> {
379 let ids: BTreeSet<String> = string_list(body, "TraceIds").into_iter().collect();
380 let guard = self.state.read();
381 let services = match guard.get(&ctx.account) {
382 Some(data) => {
383 let segs: Vec<&StoredSegment> = data
384 .traces
385 .iter()
386 .filter(|(id, _)| ids.contains(*id))
387 .flat_map(|(_, s)| s.iter())
388 .collect();
389 build_service_graph(&segs)
390 }
391 None => Vec::new(),
392 };
393 Ok(ok(json!({ "Services": services })))
394 }
395
396 fn get_time_series(&self, ctx: &Ctx, body: &Value) -> Result<AwsResponse, AwsServiceError> {
397 let (start, end) = time_range(body);
398 const MAX_BUCKETS: usize = 1440;
403 let mut period = body
404 .get("Period")
405 .and_then(Value::as_i64)
406 .filter(|p| *p > 0)
407 .unwrap_or(60) as f64;
408 let guard = self.state.read();
409 let mut out: Vec<Value> = Vec::new();
410 if let Some(data) = guard.get(&ctx.account) {
411 let span = end - start;
412 if span > 0.0 && period > 0.0 {
413 if span / period > MAX_BUCKETS as f64 {
414 period = span / MAX_BUCKETS as f64;
415 }
416 let mut bucket = start;
417 let mut count = 0usize;
418 while bucket < end && count < MAX_BUCKETS {
419 let bucket_end = (bucket + period).min(end);
420 let segs: Vec<&StoredSegment> = data
421 .traces
422 .values()
423 .flat_map(|s| s.iter())
424 .filter(|s| s.start_time >= bucket && s.start_time < bucket_end)
425 .collect();
426 if !segs.is_empty() {
427 out.push(json!({
428 "Timestamp": bucket,
429 "ServiceSummaryStatistics": aggregate_statistics(&segs),
430 "ResponseTimeHistogram": [],
431 }));
432 }
433 bucket = bucket_end;
434 count += 1;
435 }
436 }
437 }
438 Ok(ok(json!({
439 "TimeSeriesServiceStatistics": out,
440 "ContainsOldGroupVersions": false,
441 })))
442 }
443
444 fn create_group(&self, ctx: &Ctx, body: &Value) -> Result<AwsResponse, AwsServiceError> {
447 let name = body
448 .get("GroupName")
449 .and_then(Value::as_str)
450 .unwrap_or_default()
451 .to_string();
452 let mut guard = self.state.write();
453 let data = guard.get_or_create(&ctx.account);
454 if data.groups.contains_key(&name) {
455 return Err(invalid(&format!("Group {name} already exists.")));
456 }
457 let arn = group_arn(&ctx.region, &ctx.account, &name);
458 let mut group = Map::new();
459 group.insert("GroupName".into(), json!(name));
460 group.insert("GroupARN".into(), json!(arn));
461 group.insert(
462 "FilterExpression".into(),
463 body.get("FilterExpression").cloned().unwrap_or(json!("")),
464 );
465 group.insert(
466 "InsightsConfiguration".into(),
467 insights_config(body.get("InsightsConfiguration")),
468 );
469 data.groups.insert(name, Value::Object(group.clone()));
470 store_tags(data, &arn, body.get("Tags"));
471 Ok(ok(json!({ "Group": Value::Object(group) })))
472 }
473
474 fn get_group(&self, ctx: &Ctx, body: &Value) -> Result<AwsResponse, AwsServiceError> {
475 let guard = self.state.read();
476 let data = guard.get(&ctx.account);
477 let group = data
478 .and_then(|d| find_group(d, body))
479 .ok_or_else(|| invalid("Group not found."))?;
480 Ok(ok(json!({ "Group": group })))
481 }
482
483 fn get_groups(&self, ctx: &Ctx) -> Result<AwsResponse, AwsServiceError> {
484 let guard = self.state.read();
485 let groups: Vec<Value> = guard
486 .get(&ctx.account)
487 .map(|d| d.groups.values().cloned().collect())
488 .unwrap_or_default();
489 Ok(ok(json!({ "Groups": groups })))
490 }
491
492 fn update_group(&self, ctx: &Ctx, body: &Value) -> Result<AwsResponse, AwsServiceError> {
493 let mut guard = self.state.write();
494 let data = guard.get_or_create(&ctx.account);
495 let Some(name) = group_key(data, body) else {
496 return Err(invalid("Group not found."));
497 };
498 let group = data
499 .groups
500 .get_mut(&name)
501 .and_then(Value::as_object_mut)
502 .expect("group key resolved above");
503 if let Some(fe) = body.get("FilterExpression") {
504 group.insert("FilterExpression".into(), fe.clone());
505 }
506 if body.get("InsightsConfiguration").is_some() {
507 group.insert(
508 "InsightsConfiguration".into(),
509 insights_config(body.get("InsightsConfiguration")),
510 );
511 }
512 let out = group.clone();
513 Ok(ok(json!({ "Group": Value::Object(out) })))
514 }
515
516 fn delete_group(&self, ctx: &Ctx, body: &Value) -> Result<AwsResponse, AwsServiceError> {
517 let mut guard = self.state.write();
518 let data = guard.get_or_create(&ctx.account);
519 let Some(name) = group_key(data, body) else {
520 return Err(invalid("Group not found."));
521 };
522 if let Some(g) = data.groups.remove(&name) {
523 if let Some(arn) = g.get("GroupARN").and_then(Value::as_str) {
524 data.tags.remove(arn);
525 }
526 }
527 Ok(ok(json!({})))
528 }
529
530 fn create_sampling_rule(
533 &self,
534 ctx: &Ctx,
535 body: &Value,
536 ) -> Result<AwsResponse, AwsServiceError> {
537 let rule_in = body.get("SamplingRule").cloned().unwrap_or(json!({}));
538 let name = rule_in
539 .get("RuleName")
540 .and_then(Value::as_str)
541 .filter(|s| !s.is_empty())
542 .map(std::string::ToString::to_string)
543 .unwrap_or_else(|| format!("rule-{}", short_id()));
544 let mut guard = self.state.write();
545 let data = guard.get_or_create(&ctx.account);
546 if data.sampling_rules.contains_key(&name) {
547 return Err(invalid(&format!("Sampling rule {name} already exists.")));
548 }
549 let arn = sampling_rule_arn(&ctx.region, &ctx.account, &name);
550 let mut rule = rule_in.as_object().cloned().unwrap_or_default();
551 rule.insert("RuleName".into(), json!(name));
552 rule.insert("RuleARN".into(), json!(arn));
553 rule.entry("Version").or_insert(json!(1));
554 rule.entry("Attributes")
555 .or_insert(Value::Object(Map::new()));
556 let now = now_epoch();
557 let record = json!({
558 "SamplingRule": Value::Object(rule),
559 "CreatedAt": now,
560 "ModifiedAt": now,
561 });
562 data.sampling_rules.insert(name, record.clone());
563 store_tags(data, &arn, body.get("Tags"));
564 Ok(ok(json!({ "SamplingRuleRecord": record })))
565 }
566
567 fn get_sampling_rules(&self, ctx: &Ctx) -> Result<AwsResponse, AwsServiceError> {
568 let guard = self.state.read();
569 let records: Vec<Value> = guard
570 .get(&ctx.account)
571 .map(|d| d.sampling_rules.values().cloned().collect())
572 .unwrap_or_default();
573 Ok(ok(json!({ "SamplingRuleRecords": records })))
574 }
575
576 fn update_sampling_rule(
577 &self,
578 ctx: &Ctx,
579 body: &Value,
580 ) -> Result<AwsResponse, AwsServiceError> {
581 let update = body.get("SamplingRuleUpdate").cloned().unwrap_or(json!({}));
582 let mut guard = self.state.write();
583 let data = guard.get_or_create(&ctx.account);
584 let Some(name) = sampling_rule_key(data, &update) else {
585 return Err(invalid("Sampling rule not found."));
586 };
587 let record = data
588 .sampling_rules
589 .get_mut(&name)
590 .and_then(Value::as_object_mut)
591 .expect("rule key resolved above");
592 if let Some(rule) = record
593 .get_mut("SamplingRule")
594 .and_then(Value::as_object_mut)
595 {
596 for key in [
597 "ResourceARN",
598 "Priority",
599 "FixedRate",
600 "ReservoirSize",
601 "ServiceName",
602 "ServiceType",
603 "Host",
604 "HTTPMethod",
605 "URLPath",
606 "Attributes",
607 ] {
608 if let Some(v) = update.get(key) {
609 rule.insert(key.to_string(), v.clone());
610 }
611 }
612 }
613 record.insert("ModifiedAt".into(), json!(now_epoch()));
614 let out = record.clone();
615 Ok(ok(json!({ "SamplingRuleRecord": Value::Object(out) })))
616 }
617
618 fn delete_sampling_rule(
619 &self,
620 ctx: &Ctx,
621 body: &Value,
622 ) -> Result<AwsResponse, AwsServiceError> {
623 let mut guard = self.state.write();
624 let data = guard.get_or_create(&ctx.account);
625 let Some(name) = sampling_rule_key(data, body) else {
626 return Err(invalid("Sampling rule not found."));
627 };
628 if name == DEFAULT_SAMPLING_RULE {
629 return Err(invalid("The Default sampling rule cannot be deleted."));
630 }
631 let record = data
632 .sampling_rules
633 .remove(&name)
634 .expect("rule key resolved above");
635 if let Some(arn) = record
636 .get("SamplingRule")
637 .and_then(|r| r.get("RuleARN"))
638 .and_then(Value::as_str)
639 {
640 data.tags.remove(arn);
641 }
642 Ok(ok(json!({ "SamplingRuleRecord": record })))
643 }
644
645 fn get_sampling_targets(
646 &self,
647 ctx: &Ctx,
648 body: &Value,
649 ) -> Result<AwsResponse, AwsServiceError> {
650 let docs = body
651 .get("SamplingStatisticsDocuments")
652 .and_then(Value::as_array)
653 .cloned()
654 .unwrap_or_default();
655 let guard = self.state.read();
656 let data = guard.get(&ctx.account);
657 let mut targets: Vec<Value> = Vec::new();
658 for doc in &docs {
659 let rule_name = doc
660 .get("RuleName")
661 .and_then(Value::as_str)
662 .unwrap_or(DEFAULT_SAMPLING_RULE);
663 let fixed_rate = data
664 .and_then(|d| d.sampling_rules.get(rule_name))
665 .and_then(|r| r.get("SamplingRule"))
666 .and_then(|r| r.get("FixedRate"))
667 .and_then(Value::as_f64)
668 .unwrap_or(0.05);
669 targets.push(json!({
670 "RuleName": rule_name,
671 "FixedRate": fixed_rate,
672 "ReservoirQuota": 1,
673 "ReservoirQuotaTTL": now_epoch() + 10.0,
674 "Interval": 10,
675 }));
676 }
677 Ok(ok(json!({
678 "SamplingTargetDocuments": targets,
679 "LastRuleModification": now_epoch(),
680 "UnprocessedStatistics": [],
681 })))
682 }
683
684 fn get_sampling_statistic_summaries() -> Result<AwsResponse, AwsServiceError> {
685 Ok(ok(json!({ "SamplingStatisticSummaries": [] })))
686 }
687
688 fn get_encryption_config(&self, ctx: &Ctx) -> Result<AwsResponse, AwsServiceError> {
691 let guard = self.state.read();
692 let cfg = guard
693 .get(&ctx.account)
694 .and_then(|d| d.encryption_config.clone())
695 .unwrap_or_else(default_encryption_config);
696 Ok(ok(json!({ "EncryptionConfig": cfg })))
697 }
698
699 fn put_encryption_config(
700 &self,
701 ctx: &Ctx,
702 body: &Value,
703 ) -> Result<AwsResponse, AwsServiceError> {
704 let etype = body.get("Type").and_then(Value::as_str).unwrap_or("NONE");
705 let mut cfg = Map::new();
706 cfg.insert("Type".into(), json!(etype));
707 cfg.insert("Status".into(), json!("ACTIVE"));
708 if etype == "KMS" {
709 let key = body
710 .get("KeyId")
711 .and_then(Value::as_str)
712 .unwrap_or_default();
713 cfg.insert("KeyId".into(), json!(key));
714 }
715 let cfg = Value::Object(cfg);
716 let mut guard = self.state.write();
717 guard.get_or_create(&ctx.account).encryption_config = Some(cfg.clone());
718 Ok(ok(json!({ "EncryptionConfig": cfg })))
719 }
720
721 fn put_resource_policy(&self, ctx: &Ctx, body: &Value) -> Result<AwsResponse, AwsServiceError> {
724 let name = body
725 .get("PolicyName")
726 .and_then(Value::as_str)
727 .unwrap_or_default()
728 .to_string();
729 let doc = body
730 .get("PolicyDocument")
731 .and_then(Value::as_str)
732 .unwrap_or_default()
733 .to_string();
734 let mut guard = self.state.write();
735 let data = guard.get_or_create(&ctx.account);
736 let policy = json!({
737 "PolicyName": name,
738 "PolicyDocument": doc,
739 "PolicyRevisionId": short_id(),
740 "LastUpdatedTime": now_epoch(),
741 });
742 data.resource_policies.insert(name, policy.clone());
743 Ok(ok(json!({ "ResourcePolicy": policy })))
744 }
745
746 fn delete_resource_policy(
747 &self,
748 ctx: &Ctx,
749 body: &Value,
750 ) -> Result<AwsResponse, AwsServiceError> {
751 let name = body
752 .get("PolicyName")
753 .and_then(Value::as_str)
754 .unwrap_or_default();
755 let mut guard = self.state.write();
756 guard
757 .get_or_create(&ctx.account)
758 .resource_policies
759 .remove(name);
760 Ok(ok(json!({})))
761 }
762
763 fn list_resource_policies(&self, ctx: &Ctx) -> Result<AwsResponse, AwsServiceError> {
764 let guard = self.state.read();
765 let policies: Vec<Value> = guard
766 .get(&ctx.account)
767 .map(|d| d.resource_policies.values().cloned().collect())
768 .unwrap_or_default();
769 Ok(ok(json!({ "ResourcePolicies": policies })))
770 }
771
772 fn tag_resource(&self, ctx: &Ctx, body: &Value) -> Result<AwsResponse, AwsServiceError> {
775 let arn = body
776 .get("ResourceARN")
777 .and_then(Value::as_str)
778 .unwrap_or_default()
779 .to_string();
780 let mut guard = self.state.write();
781 let data = guard.get_or_create(&ctx.account);
782 if !resource_exists(data, &arn) {
783 return Err(not_found(&format!("Resource {arn} not found.")));
784 }
785 let entry = data.tags.entry(arn).or_default();
786 if let Some(list) = body.get("Tags").and_then(Value::as_array) {
787 for tag in list {
788 if let (Some(k), Some(v)) = (
789 tag.get("Key").and_then(Value::as_str),
790 tag.get("Value").and_then(Value::as_str),
791 ) {
792 entry.insert(k.to_string(), v.to_string());
793 }
794 }
795 }
796 if entry.len() > 50 {
797 return Err(AwsServiceError::aws_error(
798 StatusCode::BAD_REQUEST,
799 "TooManyTagsException",
800 "The maximum number of tags (50) has been exceeded.",
801 ));
802 }
803 Ok(ok(json!({})))
804 }
805
806 fn untag_resource(&self, ctx: &Ctx, body: &Value) -> Result<AwsResponse, AwsServiceError> {
807 let arn = body
808 .get("ResourceARN")
809 .and_then(Value::as_str)
810 .unwrap_or_default()
811 .to_string();
812 let mut guard = self.state.write();
813 let data = guard.get_or_create(&ctx.account);
814 if !resource_exists(data, &arn) {
815 return Err(not_found(&format!("Resource {arn} not found.")));
816 }
817 if let Some(entry) = data.tags.get_mut(&arn) {
818 for key in string_list(body, "TagKeys") {
819 entry.remove(&key);
820 }
821 }
822 Ok(ok(json!({})))
823 }
824
825 fn list_tags_for_resource(
826 &self,
827 ctx: &Ctx,
828 body: &Value,
829 ) -> Result<AwsResponse, AwsServiceError> {
830 let arn = body
831 .get("ResourceARN")
832 .and_then(Value::as_str)
833 .unwrap_or_default()
834 .to_string();
835 let guard = self.state.read();
836 let data = guard
837 .get(&ctx.account)
838 .ok_or_else(|| not_found(&format!("Resource {arn} not found.")))?;
839 if !resource_exists(data, &arn) {
840 return Err(not_found(&format!("Resource {arn} not found.")));
841 }
842 let tags: Vec<Value> = data
843 .tags
844 .get(&arn)
845 .map(|m| {
846 m.iter()
847 .map(|(k, v)| json!({ "Key": k, "Value": v }))
848 .collect()
849 })
850 .unwrap_or_default();
851 Ok(ok(json!({ "Tags": tags })))
852 }
853
854 fn get_indexing_rules() -> Result<AwsResponse, AwsServiceError> {
857 Ok(ok(json!({ "IndexingRules": [default_indexing_rule()] })))
858 }
859
860 fn update_indexing_rule(body: &Value) -> Result<AwsResponse, AwsServiceError> {
861 let name = body
862 .get("Name")
863 .and_then(Value::as_str)
864 .unwrap_or("Default");
865 Ok(ok(json!({
866 "IndexingRule": {
867 "Name": name,
868 "ModifiedAt": now_epoch(),
869 "Rule": body.get("Rule").cloned().unwrap_or_else(|| default_indexing_rule()["Rule"].clone()),
870 }
871 })))
872 }
873
874 fn get_trace_segment_destination(&self, ctx: &Ctx) -> Result<AwsResponse, AwsServiceError> {
877 let guard = self.state.read();
878 let dest = guard
879 .get(&ctx.account)
880 .map(|d| d.trace_segment_destination.clone())
881 .unwrap_or_else(|| "XRay".to_string());
882 Ok(ok(json!({ "Destination": dest, "Status": "ACTIVE" })))
883 }
884
885 fn update_trace_segment_destination(
886 &self,
887 ctx: &Ctx,
888 body: &Value,
889 ) -> Result<AwsResponse, AwsServiceError> {
890 let dest = body
891 .get("Destination")
892 .and_then(Value::as_str)
893 .unwrap_or("XRay")
894 .to_string();
895 let mut guard = self.state.write();
896 guard.get_or_create(&ctx.account).trace_segment_destination = dest.clone();
897 Ok(ok(json!({ "Destination": dest, "Status": "ACTIVE" })))
898 }
899
900 fn start_trace_retrieval(
903 &self,
904 ctx: &Ctx,
905 body: &Value,
906 ) -> Result<AwsResponse, AwsServiceError> {
907 let token = format!("{}{}", Uuid::new_v4().simple(), Uuid::new_v4().simple());
908 let record = json!({
909 "TraceIds": body.get("TraceIds").cloned().unwrap_or(json!([])),
910 "StartTime": body.get("StartTime").cloned().unwrap_or(json!(0)),
911 "EndTime": body.get("EndTime").cloned().unwrap_or(json!(0)),
912 });
913 let mut guard = self.state.write();
914 guard
915 .get_or_create(&ctx.account)
916 .retrievals
917 .insert(token.clone(), record);
918 Ok(ok(json!({ "RetrievalToken": token })))
919 }
920
921 fn list_retrieved_traces(
922 &self,
923 ctx: &Ctx,
924 body: &Value,
925 ) -> Result<AwsResponse, AwsServiceError> {
926 let token = body
927 .get("RetrievalToken")
928 .and_then(Value::as_str)
929 .unwrap_or_default();
930 let format = body
931 .get("TraceFormat")
932 .and_then(Value::as_str)
933 .unwrap_or("XRAY");
934 let guard = self.state.read();
935 let exists = guard
936 .get(&ctx.account)
937 .map(|d| d.retrievals.contains_key(token))
938 .unwrap_or(false);
939 if !exists {
940 return Err(not_found("Trace retrieval not found for the given token."));
941 }
942 Ok(ok(json!({
943 "RetrievalStatus": "COMPLETE",
944 "TraceFormat": format,
945 "Traces": [],
946 })))
947 }
948
949 fn get_retrieved_traces_graph(
950 &self,
951 ctx: &Ctx,
952 body: &Value,
953 ) -> Result<AwsResponse, AwsServiceError> {
954 let token = body
955 .get("RetrievalToken")
956 .and_then(Value::as_str)
957 .unwrap_or_default();
958 let guard = self.state.read();
959 let data = guard
960 .get(&ctx.account)
961 .ok_or_else(|| not_found("Trace retrieval not found for the given token."))?;
962 let record = data
963 .retrievals
964 .get(token)
965 .ok_or_else(|| not_found("Trace retrieval not found for the given token."))?;
966 let ids: BTreeSet<String> = record
967 .get("TraceIds")
968 .and_then(Value::as_array)
969 .map(|a| {
970 a.iter()
971 .filter_map(|v| v.as_str().map(std::string::ToString::to_string))
972 .collect()
973 })
974 .unwrap_or_default();
975 let segs: Vec<&StoredSegment> = data
976 .traces
977 .iter()
978 .filter(|(id, _)| ids.contains(*id))
979 .flat_map(|(_, s)| s.iter())
980 .collect();
981 let services = build_service_graph(&segs);
982 Ok(ok(json!({
983 "RetrievalStatus": "COMPLETE",
984 "Services": services,
985 })))
986 }
987
988 fn cancel_trace_retrieval(
989 &self,
990 ctx: &Ctx,
991 body: &Value,
992 ) -> Result<AwsResponse, AwsServiceError> {
993 let token = body
994 .get("RetrievalToken")
995 .and_then(Value::as_str)
996 .unwrap_or_default()
997 .to_string();
998 let mut guard = self.state.write();
999 let data = guard.get_or_create(&ctx.account);
1000 if data.retrievals.remove(&token).is_none() {
1001 return Err(not_found("Trace retrieval not found for the given token."));
1002 }
1003 Ok(ok(json!({})))
1004 }
1005
1006 fn get_insight(body: &Value) -> Result<AwsResponse, AwsServiceError> {
1015 let id = body.get("InsightId").and_then(Value::as_str).unwrap_or("");
1016 Err(invalid(&format!("Insight {id} not found.")))
1017 }
1018
1019 fn get_insight_events(body: &Value) -> Result<AwsResponse, AwsServiceError> {
1020 let id = body.get("InsightId").and_then(Value::as_str).unwrap_or("");
1021 Err(invalid(&format!("Insight {id} not found.")))
1022 }
1023
1024 fn get_insight_impact_graph(body: &Value) -> Result<AwsResponse, AwsServiceError> {
1025 let id = body.get("InsightId").and_then(Value::as_str).unwrap_or("");
1026 Err(invalid(&format!("Insight {id} not found.")))
1027 }
1028
1029 fn get_insight_summaries() -> Result<AwsResponse, AwsServiceError> {
1030 Ok(ok(json!({ "InsightSummaries": [] })))
1031 }
1032}
1033
1034fn ok(v: Value) -> AwsResponse {
1037 AwsResponse::json_value(StatusCode::OK, v)
1038}
1039
1040fn parse_body(req: &AwsRequest) -> Result<Value, AwsServiceError> {
1041 if req.body.is_empty() {
1042 return Ok(json!({}));
1043 }
1044 serde_json::from_slice(&req.body)
1045 .map_err(|e| invalid(&format!("Request body is malformed: {e}")))
1046}
1047
1048fn invalid(msg: &str) -> AwsServiceError {
1049 AwsServiceError::aws_error(StatusCode::BAD_REQUEST, "InvalidRequestException", msg)
1050}
1051
1052fn not_found(msg: &str) -> AwsServiceError {
1056 AwsServiceError::aws_error(StatusCode::NOT_FOUND, "ResourceNotFoundException", msg)
1057}
1058
1059fn short_id() -> String {
1060 fakecloud_core::ids::short_id(12)
1061}
1062
1063fn group_arn(region: &str, account: &str, name: &str) -> String {
1064 format!(
1065 "arn:aws:xray:{region}:{account}:group/{name}/{}",
1066 short_id()
1067 )
1068}
1069
1070fn sampling_rule_arn(region: &str, account: &str, name: &str) -> String {
1071 format!("arn:aws:xray:{region}:{account}:sampling-rule/{name}")
1072}
1073
1074fn default_encryption_config() -> Value {
1075 json!({ "Type": "NONE", "Status": "ACTIVE" })
1076}
1077
1078fn default_indexing_rule() -> Value {
1079 json!({
1080 "Name": "Default",
1081 "ModifiedAt": now_epoch(),
1082 "Rule": { "Probabilistic": { "DesiredSamplingPercentage": 100.0, "ActualSamplingPercentage": 100.0 } },
1083 })
1084}
1085
1086fn insights_config(input: Option<&Value>) -> Value {
1087 let enabled = input
1088 .and_then(|c| c.get("InsightsEnabled"))
1089 .and_then(Value::as_bool)
1090 .unwrap_or(false);
1091 let notifications = input
1092 .and_then(|c| c.get("NotificationsEnabled"))
1093 .and_then(Value::as_bool)
1094 .unwrap_or(false);
1095 json!({ "InsightsEnabled": enabled, "NotificationsEnabled": notifications })
1096}
1097
1098fn resource_exists(data: &XrayData, arn: &str) -> bool {
1100 data.groups
1101 .values()
1102 .any(|g| g.get("GroupARN").and_then(Value::as_str) == Some(arn))
1103 || data.sampling_rules.values().any(|r| {
1104 r.get("SamplingRule")
1105 .and_then(|s| s.get("RuleARN"))
1106 .and_then(Value::as_str)
1107 == Some(arn)
1108 })
1109}
1110
1111fn store_tags(data: &mut XrayData, arn: &str, tags: Option<&Value>) {
1112 let Some(list) = tags.and_then(Value::as_array) else {
1113 return;
1114 };
1115 if list.is_empty() {
1116 return;
1117 }
1118 let entry = data.tags.entry(arn.to_string()).or_default();
1119 for tag in list {
1120 if let (Some(k), Some(v)) = (
1121 tag.get("Key").and_then(Value::as_str),
1122 tag.get("Value").and_then(Value::as_str),
1123 ) {
1124 entry.insert(k.to_string(), v.to_string());
1125 }
1126 }
1127}
1128
1129fn find_group(data: &XrayData, body: &Value) -> Option<Value> {
1132 group_key(data, body).and_then(|k| data.groups.get(&k).cloned())
1133}
1134
1135fn group_key(data: &XrayData, body: &Value) -> Option<String> {
1137 if let Some(name) = body.get("GroupName").and_then(Value::as_str) {
1138 if data.groups.contains_key(name) {
1139 return Some(name.to_string());
1140 }
1141 }
1142 if let Some(arn) = body.get("GroupARN").and_then(Value::as_str) {
1143 return data
1144 .groups
1145 .iter()
1146 .find(|(_, g)| g.get("GroupARN").and_then(Value::as_str) == Some(arn))
1147 .map(|(k, _)| k.clone());
1148 }
1149 None
1150}
1151
1152fn sampling_rule_key(data: &XrayData, body: &Value) -> Option<String> {
1155 if let Some(name) = body.get("RuleName").and_then(Value::as_str) {
1156 if data.sampling_rules.contains_key(name) {
1157 return Some(name.to_string());
1158 }
1159 }
1160 if let Some(arn) = body.get("RuleARN").and_then(Value::as_str) {
1161 return data
1162 .sampling_rules
1163 .iter()
1164 .find(|(_, r)| {
1165 r.get("SamplingRule")
1166 .and_then(|s| s.get("RuleARN"))
1167 .and_then(Value::as_str)
1168 == Some(arn)
1169 })
1170 .map(|(k, _)| k.clone());
1171 }
1172 None
1173}
1174
1175fn string_list(body: &Value, key: &str) -> Vec<String> {
1176 body.get(key)
1177 .and_then(Value::as_array)
1178 .map(|a| {
1179 a.iter()
1180 .filter_map(|v| v.as_str().map(std::string::ToString::to_string))
1181 .collect()
1182 })
1183 .unwrap_or_default()
1184}
1185
1186fn time_range(body: &Value) -> (f64, f64) {
1188 let start = body.get("StartTime").and_then(Value::as_f64).unwrap_or(0.0);
1189 let end = body
1190 .get("EndTime")
1191 .and_then(Value::as_f64)
1192 .unwrap_or_else(now_epoch);
1193 (start, end)
1194}
1195
1196fn segments_in_range(data: &XrayData, start: f64, end: f64) -> Vec<&StoredSegment> {
1197 data.traces
1198 .values()
1199 .flat_map(|s| s.iter())
1200 .filter(|s| s.start_time >= start && s.start_time <= end)
1201 .collect()
1202}
1203
1204fn assemble_trace(id: &str, segs: &[StoredSegment]) -> Value {
1207 let start = segs.iter().map(|s| s.start_time).reduce(f64::min);
1208 let end = segs
1209 .iter()
1210 .filter_map(|s| s.end_time)
1211 .reduce(f64::max)
1212 .or(start);
1213 let duration = match (start, end) {
1214 (Some(s), Some(e)) => (e - s).max(0.0),
1215 _ => 0.0,
1216 };
1217 let segments: Vec<Value> = segs
1218 .iter()
1219 .map(|s| json!({ "Id": s.id, "Document": s.document }))
1220 .collect();
1221 json!({
1222 "Id": id,
1223 "Duration": duration,
1224 "LimitExceeded": false,
1225 "Segments": segments,
1226 })
1227}
1228
1229fn build_trace_summary(id: &str, segs: &[StoredSegment]) -> Value {
1231 let start = segs
1232 .iter()
1233 .map(|s| s.start_time)
1234 .reduce(f64::min)
1235 .unwrap_or(0.0);
1236 let end = segs
1237 .iter()
1238 .filter_map(|s| s.end_time)
1239 .reduce(f64::max)
1240 .unwrap_or(start);
1241 let duration = (end - start).max(0.0);
1242 let root = segs.iter().find(|s| s.parent_id.is_none()).or(segs.first());
1244 let response_time = root.and_then(StoredSegment::duration).unwrap_or(duration);
1245
1246 let mut http = Map::new();
1247 if let Some(r) = root {
1248 if let Some(u) = &r.http_url {
1249 http.insert("HttpURL".into(), json!(u));
1250 }
1251 if let Some(s) = r.http_status {
1252 http.insert("HttpStatus".into(), json!(s));
1253 }
1254 if let Some(m) = &r.http_method {
1255 http.insert("HttpMethod".into(), json!(m));
1256 }
1257 }
1258
1259 let mut service_names: BTreeSet<&str> = BTreeSet::new();
1260 for s in segs {
1261 service_names.insert(s.name.as_str());
1262 }
1263 let service_ids: Vec<Value> = service_names
1264 .iter()
1265 .map(|n| json!({ "Name": n, "Names": [n], "Type": "AWS::EC2::Instance" }))
1266 .collect();
1267
1268 json!({
1269 "Id": id,
1270 "StartTime": start,
1271 "Duration": duration,
1272 "ResponseTime": response_time,
1273 "HasFault": segs.iter().any(|s| s.fault),
1274 "HasError": segs.iter().any(|s| s.error),
1275 "HasThrottle": segs.iter().any(|s| s.throttle),
1276 "IsPartial": false,
1277 "Http": Value::Object(http),
1278 "Annotations": {},
1279 "ServiceIds": service_ids,
1280 })
1281}
1282
1283fn aggregate_statistics(segs: &[&StoredSegment]) -> Value {
1286 let mut ok_count = 0i64;
1287 let mut throttle = 0i64;
1288 let mut error_other = 0i64;
1289 let mut fault = 0i64;
1290 let mut total_rt = 0.0f64;
1291 for s in segs {
1292 if let Some(d) = s.duration() {
1293 total_rt += d;
1294 }
1295 if s.fault {
1296 fault += 1;
1297 } else if s.throttle {
1298 throttle += 1;
1299 } else if s.error {
1300 error_other += 1;
1301 } else {
1302 ok_count += 1;
1303 }
1304 }
1305 json!({
1306 "OkCount": ok_count,
1307 "ErrorStatistics": {
1308 "ThrottleCount": throttle,
1309 "OtherCount": error_other,
1310 "TotalCount": throttle + error_other,
1311 },
1312 "FaultStatistics": { "OtherCount": fault, "TotalCount": fault },
1313 "TotalCount": segs.len() as i64,
1314 "TotalResponseTime": (total_rt * 1_000_000.0).round() / 1_000_000.0,
1315 })
1316}
1317
1318fn trace_matches_filter(filter: &str, segs: &[StoredSegment]) -> bool {
1327 let f = filter.trim();
1328 if f.is_empty() {
1329 return true;
1330 }
1331 if let Some(rest) = f.strip_prefix("service(") {
1332 if let Some(inner) = rest.strip_suffix(')') {
1333 let want = inner.trim().trim_matches(|c| c == '"' || c == '\'');
1334 return segs.iter().any(|s| s.name == want);
1335 }
1336 }
1337 match f {
1338 "fault" => segs.iter().any(|s| s.fault),
1339 "error" => segs.iter().any(|s| s.error),
1340 "throttle" => segs.iter().any(|s| s.throttle),
1341 _ => true,
1342 }
1343}
1344
1345#[cfg(test)]
1346mod tests {
1347 use super::*;
1348 use fakecloud_core::multi_account::MultiAccountState;
1349 use parking_lot::RwLock;
1350
1351 fn svc() -> XrayService {
1352 XrayService::new(Arc::new(RwLock::new(MultiAccountState::new(
1353 "000000000000",
1354 "us-east-1",
1355 "",
1356 ))))
1357 }
1358
1359 fn ctx() -> Ctx {
1360 Ctx {
1361 account: "000000000000".into(),
1362 region: "us-east-1".into(),
1363 }
1364 }
1365
1366 fn body_json(resp: &AwsResponse) -> Value {
1367 serde_json::from_slice(resp.body.expect_bytes()).expect("json response body")
1368 }
1369
1370 fn err_of(r: Result<AwsResponse, AwsServiceError>) -> AwsServiceError {
1371 match r {
1372 Ok(_) => panic!("expected an error"),
1373 Err(e) => e,
1374 }
1375 }
1376
1377 fn ingest(s: &XrayService, docs: &[&str]) {
1378 let arr: Vec<Value> = docs.iter().map(|d| json!(*d)).collect();
1379 let out = s
1380 .put_trace_segments(&ctx(), &json!({ "TraceSegmentDocuments": arr }))
1381 .unwrap();
1382 let body = body_json(&out);
1383 assert!(body["UnprocessedTraceSegments"]
1384 .as_array()
1385 .unwrap()
1386 .is_empty());
1387 }
1388
1389 #[test]
1390 fn put_then_batch_get_roundtrips() {
1391 let s = svc();
1392 ingest(
1393 &s,
1394 &[r#"{"trace_id":"1-aaaa","id":"1111","name":"web","start_time":1.0,"end_time":2.0}"#],
1395 );
1396 let out = s
1397 .batch_get_traces(&ctx(), &json!({ "TraceIds": ["1-aaaa", "1-missing"] }))
1398 .unwrap();
1399 let body = body_json(&out);
1400 assert_eq!(body["Traces"].as_array().unwrap().len(), 1);
1401 assert_eq!(body["Traces"][0]["Id"], json!("1-aaaa"));
1402 assert_eq!(body["UnprocessedTraceIds"], json!(["1-missing"]));
1403 }
1404
1405 #[test]
1406 fn trace_summaries_filter_by_time_and_expression() {
1407 let s = svc();
1408 ingest(
1409 &s,
1410 &[
1411 r#"{"trace_id":"1-ok","id":"1","name":"web","start_time":100.0,"end_time":101.0}"#,
1412 r#"{"trace_id":"1-bad","id":"2","name":"web","start_time":150.0,"end_time":151.0,"fault":true}"#,
1413 ],
1414 );
1415 let all = body_json(
1416 &s.get_trace_summaries(&ctx(), &json!({ "StartTime": 0, "EndTime": 200 }))
1417 .unwrap(),
1418 );
1419 assert_eq!(all["TraceSummaries"].as_array().unwrap().len(), 2);
1420 assert_eq!(all["TracesProcessedCount"], json!(2));
1421
1422 let faults = body_json(
1423 &s.get_trace_summaries(
1424 &ctx(),
1425 &json!({ "StartTime": 0, "EndTime": 200, "FilterExpression": "fault" }),
1426 )
1427 .unwrap(),
1428 );
1429 assert_eq!(faults["TraceSummaries"].as_array().unwrap().len(), 1);
1430 assert_eq!(faults["TraceSummaries"][0]["Id"], json!("1-bad"));
1431 }
1432
1433 #[test]
1434 fn service_graph_derives_edge() {
1435 let s = svc();
1436 ingest(
1437 &s,
1438 &[
1439 r#"{"trace_id":"1-a","id":"1","name":"web","start_time":1.0,"end_time":3.0,"subsegments":[{"id":"2","name":"db","namespace":"remote","start_time":1.5,"end_time":2.5}]}"#,
1440 ],
1441 );
1442 let body = body_json(
1443 &s.get_service_graph(&ctx(), &json!({ "StartTime": 0, "EndTime": 10 }))
1444 .unwrap(),
1445 );
1446 let services = body["Services"].as_array().unwrap();
1447 assert_eq!(services.len(), 2);
1448 let web = services.iter().find(|x| x["Name"] == json!("web")).unwrap();
1449 assert_eq!(web["Edges"].as_array().unwrap().len(), 1);
1450 }
1451
1452 #[test]
1453 fn create_get_group_roundtrips() {
1454 let s = svc();
1455 let created = body_json(
1456 &s.create_group(
1457 &ctx(),
1458 &json!({ "GroupName": "g1", "FilterExpression": "fault" }),
1459 )
1460 .unwrap(),
1461 );
1462 assert_eq!(created["Group"]["GroupName"], json!("g1"));
1463 let got = body_json(&s.get_group(&ctx(), &json!({ "GroupName": "g1" })).unwrap());
1464 assert_eq!(got["Group"]["FilterExpression"], json!("fault"));
1465 let groups = body_json(&s.get_groups(&ctx()).unwrap());
1466 assert_eq!(groups["Groups"].as_array().unwrap().len(), 1);
1467 }
1468
1469 #[test]
1470 fn default_sampling_rule_present_and_undeletable() {
1471 let s = svc();
1472 let rules = body_json(&s.get_sampling_rules(&ctx()).unwrap());
1473 let recs = rules["SamplingRuleRecords"].as_array().unwrap();
1474 assert!(recs
1475 .iter()
1476 .any(|r| r["SamplingRule"]["RuleName"] == json!("Default")));
1477 let err = err_of(s.delete_sampling_rule(&ctx(), &json!({ "RuleName": "Default" })));
1478 assert_eq!(err.status(), StatusCode::BAD_REQUEST);
1479 }
1480
1481 #[test]
1482 fn create_and_delete_sampling_rule() {
1483 let s = svc();
1484 let rule = json!({ "SamplingRule": {
1485 "RuleName": "r1", "ResourceARN": "*", "Priority": 5, "FixedRate": 0.1,
1486 "ReservoirSize": 2, "ServiceName": "*", "ServiceType": "*", "Host": "*",
1487 "HTTPMethod": "*", "URLPath": "*", "Version": 1
1488 }});
1489 let created = body_json(&s.create_sampling_rule(&ctx(), &rule).unwrap());
1490 assert_eq!(
1491 created["SamplingRuleRecord"]["SamplingRule"]["RuleName"],
1492 json!("r1")
1493 );
1494 let deleted = s.delete_sampling_rule(&ctx(), &json!({ "RuleName": "r1" }));
1495 assert!(deleted.is_ok());
1496 }
1497
1498 #[test]
1499 fn encryption_config_roundtrips() {
1500 let s = svc();
1501 let default = body_json(&s.get_encryption_config(&ctx()).unwrap());
1502 assert_eq!(default["EncryptionConfig"]["Type"], json!("NONE"));
1503 let put = body_json(
1504 &s.put_encryption_config(&ctx(), &json!({ "Type": "KMS", "KeyId": "alias/xray" }))
1505 .unwrap(),
1506 );
1507 assert_eq!(put["EncryptionConfig"]["Type"], json!("KMS"));
1508 let got = body_json(&s.get_encryption_config(&ctx()).unwrap());
1509 assert_eq!(got["EncryptionConfig"]["KeyId"], json!("alias/xray"));
1510 }
1511
1512 #[test]
1513 fn tagging_requires_existing_resource() {
1514 let s = svc();
1515 let err = err_of(s.tag_resource(
1517 &ctx(),
1518 &json!({ "ResourceARN": "arn:aws:xray:us-east-1:000000000000:group/ghost/x", "Tags": [] }),
1519 ));
1520 assert_eq!(err.status(), StatusCode::NOT_FOUND);
1521
1522 let created = body_json(
1523 &s.create_group(&ctx(), &json!({ "GroupName": "tg" }))
1524 .unwrap(),
1525 );
1526 let arn = created["Group"]["GroupARN"].as_str().unwrap().to_string();
1527 s.tag_resource(
1528 &ctx(),
1529 &json!({ "ResourceARN": arn, "Tags": [{ "Key": "team", "Value": "obs" }] }),
1530 )
1531 .unwrap();
1532 let listed = body_json(
1533 &s.list_tags_for_resource(&ctx(), &json!({ "ResourceARN": arn }))
1534 .unwrap(),
1535 );
1536 assert_eq!(listed["Tags"][0]["Key"], json!("team"));
1537 }
1538
1539 #[test]
1540 fn trace_retrieval_lifecycle() {
1541 let s = svc();
1542 let started = body_json(
1543 &s.start_trace_retrieval(
1544 &ctx(),
1545 &json!({ "TraceIds": ["1-a"], "StartTime": 0, "EndTime": 1 }),
1546 )
1547 .unwrap(),
1548 );
1549 let token = started["RetrievalToken"].as_str().unwrap().to_string();
1550 let listed = body_json(
1551 &s.list_retrieved_traces(&ctx(), &json!({ "RetrievalToken": token }))
1552 .unwrap(),
1553 );
1554 assert_eq!(listed["RetrievalStatus"], json!("COMPLETE"));
1555 let err = err_of(s.list_retrieved_traces(&ctx(), &json!({ "RetrievalToken": "nope" })));
1557 assert_eq!(err.status(), StatusCode::NOT_FOUND);
1558 }
1559
1560 fn req(path: &str) -> AwsRequest {
1561 AwsRequest {
1562 service: "xray".to_string(),
1563 action: String::new(),
1564 region: "us-east-1".to_string(),
1565 account_id: "000000000000".to_string(),
1566 request_id: "rid".to_string(),
1567 headers: http::HeaderMap::new(),
1568 query_params: std::collections::HashMap::new(),
1569 body: bytes::Bytes::new(),
1570 body_stream: parking_lot::Mutex::new(None),
1571 path_segments: vec![],
1572 raw_path: path.to_string(),
1573 raw_query: String::new(),
1574 method: Method::POST,
1575 is_query_protocol: false,
1576 access_key_id: None,
1577 principal: None,
1578 }
1579 }
1580
1581 #[test]
1582 fn resolve_action_routes_and_rejects() {
1583 assert_eq!(
1584 XrayService::resolve_action(&req("/TraceSegments")),
1585 Some("PutTraceSegments")
1586 );
1587 assert_eq!(
1588 XrayService::resolve_action(&req("/ServiceGraph")),
1589 Some("GetServiceGraph")
1590 );
1591 assert!(XrayService::resolve_action(&req("/NotARealOp")).is_none());
1592 }
1593}