CertNote/ Commute Study Notes
Roadmap
KOEN
CLF-C02 · FoundationalCloud Practitioner - Foundational
DVA-C02 · AssociateDeveloper - Associate
SAA-C03 · AssociateSolutions Architect - Associate
SOA-C02 · AssociateCloudOps Engineer - Associate
SAP-C02 · ProfessionalSolutions Architect - Professional
DOP-C02 · ProfessionalDevOps Engineer - Professional
  • Week 1
    • 1.DevOps as an Operating Model: The Five Axes of CALMS and the Truth DORA Proved Through Measurement
    • 2.Well-Architected Framework: Rereading It Through the Six Lenses of DevOps
    • 3.The AWS DevOps Tool Map: The Code* Series and the Real Picture Beyond It
    • 4.Multi-Account Strategy: The Real Picture of Organizations, Control Tower, and IAM Identity Center
    • 5.Week 1 Wrap-Up: Cementing the DevOps Thinking Frame with Scenarios
  • Week 2
  • Week 3
  • Week 4
  • Week 5
  • Week 6
  • Week 7
  • Week 8
  • Week 9
  • Week 10
  • Week 11
    • 1.X-Ray: Causal Graphs of Distributed Tracing and the Deep Story of the Trace Model
    • 2.X-Ray Sampling: Reservoir Algorithm and the Economics of Tracing at Operational Scale
    • 3.ADOT: The Deep Story of OpenTelemetry Ending the Tracing Tool Wars
    • 4.OpenSearch · AMP · AMG: The Two Worlds of Inverted Indices and Time-Series Databases
    • 5.Week 11 Synthesis: Real-World Decision-Making in Tracing and Telemetry Observability
  • Week 12
  • Week 13
  • Week 14
  • Week 15
  • Week 16
Linux Master 2 · 2급KAIT Linux Master - Level 2
SCS-C03 · SpecialtySecurity - Specialty
Linux Master 1 · 1급KAIT Linux Master - Level 1
MLA-C01 · AssociateMachine Learning Engineer - Associate
AIF-C01 · FoundationalAI Practitioner - Foundational
DEA-C01 · AssociateData Engineer - Associate
MLS-C01 · SpecialtyMachine Learning - Specialty
SQLD · 개발자SQL Developer (SQLD)

© CertNote · 주간랩스

  • About
  • Changelog
  • Contact
  • Terms
  • Privacy
← DOP-C02/Week 11/Day 1

Based on the DOP-C02 exam guide·Report an error

DOP-C02· ProWeek 11 · Day 1~46 min read

Day 1 - X-Ray: Causal Graphs of Distributed Tracing and the Deep Story of the Trace Model

Debugging in the monolithic era ended with a single stack trace. A function called another function, and when an exception occurred, the call stack was printed line by line from top to bottom. But the moment that monolith was shattered into dozens of microservices, Lambda hung behind an API Gateway, that Lambda called other Lambdas, and DynamoDB, SQS, and external HTTP APIs wedged themselves in between, "the stack trace" as a concept collapsed entirely. A single user request scattered across process boundaries, networks, and asynchronous queues. To answer "why did this request take 3 seconds," you must thread the scattered work fragments back together as a single causal chain. That threading is distributed tracing, and AWS X-Ray is that model implemented on the AWS ecosystem.

Today we don't see X-Ray simply as a "tool for viewing Service Maps," but excavate the causal model beneath it. Why is a Trace more like a directed acyclic graph (DAG) than a tree, how does a Trace ID propagate context across process boundaries, how did X-Ray's segment model originate from the Google Dapper paper and how does it differ from OpenTelemetry's span model, how does the indexing difference between Annotation and Metadata connect to the cost of reverse indices in search engines. X-Ray is a core piece of the observability domain in the DOP exam, and the scenarios "how do you find which segment is slow in a microservice," "how do you visualize the bottleneck in a Lambda chain," "how do you reduce tracing costs" repeat across test cycles.

Why Distributed Tracing Was Born — From Dapper to X-Ray

The history of distributed tracing begins with the Dapper paper released by Google in 2010 ("Dapper, a Large-Scale Distributed Systems Tracing Infrastructure"). Google was in an environment where a single search query passed through hundreds of services scattered across thousands of machines, and had no way to know "which service is slow." Dapper solved this problem by introducing three core ideas. First, assign a unique trace ID to each request and propagate it to all downstream calls. Second, record each unit of work as a span and restore causality by referencing the parent span ID. Third, because tracing every request is too expensive, use sampling to collect only a subset.

X-Ray's Trace/Segment/Subsegment model is Dapper's trace/span model translated into AWS terminology. Dapper's "trace" becomes X-Ray's "Trace," and Dapper's "span" splits into two levels in X-Ray: "Segment" (service boundary) and "Subsegment" (work within a service). Knowing this lineage reveals why X-Ray looks this way and why it's conceptually compatible with OpenTelemetry (another descendant of Dapper).

Trace (1 user request = one causal graph)
 ├─ Segment (Service A) — work at a single service/resource boundary
 │   ├─ Subsegment (DynamoDB GetItem) — downstream call
 │   ├─ Subsegment (invoke Lambda B) — remote call
 │   └─ Subsegment (business_logic) — internal code block
 └─ Segment (Service B)
     └─ Subsegment (...)

💡 Related theory: The three design goals defined by the Dapper paper for distributed tracing remain the standard for all tracing systems to this day — low overhead (tracing must not slow down the main service), application-level transparency (developers don't have to instrument everything by hand, i.e., auto-instrumentation), scalability (works at Google scale). X-Ray's SDK auto-patching (patch_all() auto-instruments boto3·requests), sampling (overhead control), and Daemon asynchronous batch transmission (low overhead) implement exactly these three goals. Zipkin (Twitter), Jaeger (Uber), and OpenTelemetry (CNCF) are all direct descendants of Dapper and stand on the same trace-span-context propagation triangle.

🔍 Going deeper: A Trace is commonly drawn as a "tree," but strictly speaking it's closer to a DAG (directed acyclic graph). A simple synchronous call chain is a tree, but when one task calls multiple downstream services in parallel (fan-out), the parent-child relationship becomes 1:N, and in asynchronous messaging where one message reaches multiple consumers (SQS fan-out via SNS) or batches multiple traces into a single processing unit, span links emerge. OpenTelemetry added a separate edge type called "link" in addition to parent-child relationships. X-Ray's segment expresses causality through parent_id, but a practical trap is that the causal chain can break at asynchronous boundaries — when passing through SQS, producer traces and consumer traces often don't automatically connect, so you must manually propagate trace context in message attributes.

Trace ID and Context Propagation — The String That Crosses Process Boundaries

All the magic of distributed tracing hinges on one thing: context propagation. When Service A calls Service B via HTTP, you must somehow communicate to B "this call is part of trace X, and your parent is segment Y." If this doesn't happen, the segment created by B becomes an orphan and can't attach to the trace. X-Ray carries this information in HTTP headers.

X-Amzn-Trace-Id: Root=1-65500000-1234abcd5678ef90; Parent=53995c3f42cd8ad8; Sampled=1
  • Root: Trace ID. Version 1-, 65500000 is the Unix epoch of the request time encoded in hexadecimal, the rest is 96 bits of random. In other words, a timestamp is embedded in the Trace ID.
  • Parent: The caller's segment ID. The receiver makes this their parent_id and hangs their own segment from it.
  • Sampled: Whether to collect this trace (1) or discard it (0). The sampling decision is made at the entry point of the trace and propagated. Intermediate services don't decide anew on their own.

Embedding a timestamp in the Trace ID is no accident. X-Ray partitions and expires traces by time (deleted after 30 days), and with the timestamp already in the ID, you can tell which time bucket it belongs to without a separate index lookup. This is the same thinking behind time-sortable identifiers like KSUID or ULID replacing UUIDv4.

💡 Related theory: Carrying trace context via HTTP headers went through a standards war. X-Ray used X-Amzn-Trace-Id, Zipkin used X-B3-* (B3 propagation), Jaeger used uber-trace-id, and they weren't compatible with each other. The W3C Trace Context standard (2020, traceparent/tracestate headers) ended this confusion. It defined vendor-neutral propagation with the format traceparent: 00-{trace-id}-{parent-id}-{flags}. OpenTelemetry uses W3C Trace Context by default, and ADOT (Day 3) provides a propagator that bidirectionally converts X-Ray headers and W3C headers, bridging the two worlds. The answer to "why does the trace break when X-Ray and OTel mix" is exactly this propagation format mismatch.

📚 Case study: An e-commerce company operated an order service (using X-Ray SDK) and a new payment service (using OpenTelemetry) together, and the trace split in two on the Service Map when crossing from orders to payments. The cause was a propagation header mismatch — orders expected X-Amzn-Trace-Id, payments expected traceparent. When both the X-Ray propagator and W3C propagator were configured in an ADOT Collector to bidirectionally convert headers, the traces connected all the way through. The lesson: in multi-SDK environments, the first failure point in distributed tracing is almost always a context propagation format mismatch.

Active vs Passive Tracing — Who Starts the Trace

A frequently confused concept in X-Ray is the distinction between Active and Passive tracing. The difference is "does this service have the authority to start/create a trace."

  • Active Tracing: If an incoming request has no trace, the service creates one, and propagates the Trace ID to the downstream services it calls. Turning Lambda on as Active lets the function become the starting point of a trace.
  • Passive Tracing: When a Trace ID arrives from outside, just follow it, and don't start a new trace on your own.
# SAM — Lambda Active Tracing
Globals:
  Function:
    Tracing: Active
# API Gateway stage
aws apigateway update-stage \
  --rest-api-id abc --stage-name prod \
  --patch-operations op=replace,path=/tracingEnabled,value=true

The typical pattern is the trace starts at the request's front gate (API Gateway or ALB), and all services downstream propagate the ID they received. Sampling is decided once at the entrance and shared across the entire chain, so a single request is never partially traced.

⚠️ Pitfall: Lambda's Active Tracing captures not just the function code execution time but also the Init (cold start) period as a segment. When viewing traces from a function with frequent cold starts, the Init subsegment is recorded in hundreds of milliseconds, tempting you to think "why is this so slow." X-Ray isn't lying — it's honestly showing you the real cold start cost. Another pitfall: if you turn on Active Tracing alone and don't do SDK instrumentation (patch_all), you see the Lambda invocation segment itself but the DynamoDB·HTTP call subsegments inside the function stay empty, so you miss the bottleneck section. Active Tracing (who starts the trace) and SDK instrumentation (how finely you break down the interior) are separate switches.

SDK and Auto-Instrumentation — Transparency of Instrumentation

"Application-level transparency," one of Dapper's three goals, is realized here. It's impossible for developers to put trace code in every function, so the SDK intercepts libraries (monkey-patch) and automatically creates subsegments.

from aws_xray_sdk.core import patch_all, xray_recorder
patch_all()   # patch boto3, requests, mysql, httplib, etc. at runtime
 
@xray_recorder.capture('business_logic')
def process(order):
    table = boto3.resource('dynamodb').Table('orders')
    table.put_item(Item={'id': order['id']})  # subsegment created automatically

patch_all() replaces the methods of libraries like boto3·requests with tracing wrappers. After that, every time that library makes an external call, the SDK automatically opens a subsegment (records start time), and when the call ends, closes it (records end time·errors). DynamoDB·HTTP calls appear in the trace without changing a line of code.

In practice, Lambda Powertools' Tracer is recommended over the raw SDK. It bundles annotation helpers, auto-marking of cold starts, and response capture toggles.

from aws_lambda_powertools import Tracer
tracer = Tracer(service="checkout")
 
@tracer.capture_lambda_handler
def handler(event, context):
    tracer.put_annotation(key="OrderId", value=event['order_id'])  # indexed
    tracer.put_metadata(key="rawEvent", value=event)               # not indexed
    process(event['order_id'])
 
@tracer.capture_method
def process(order_id):
    ...

🔍 Going deeper: The implementation mechanism of auto-instrumentation differs by language. Python/Ruby use monkey-patching, replacing methods at runtime; Java uses bytecode instrumentation at class loading time (javaagent, ASM/ByteBuddy-based); .NET uses the CLR Profiler API. The X-Ray SDK has this patching logic built into the language-specific SDK, while OpenTelemetry splits it into separate instrumentation libraries. This difference matters: Java auto-instrumentation only needs the -javaagent flag and doesn't touch source code at all, but Python's monkey-patching is sensitive to import order — if you don't call patch_all() before other imports, subtle bugs occur where some libraries don't get patched.

Annotation vs Metadata — The Economics of Reverse Indices

There are two ways to attach additional context to X-Ray, and the difference between these two is not merely an API choice but a matter of search index cost.

  • Annotation: Key-value pairs that are indexed. That is, in trace searches they become targets of filtering and searching like annotation.OrderId = "abc". However, they're limited to a maximum of 50 per trace, and values can only be strings, numbers, or booleans.
  • Metadata: Not indexed. You can't search for it, but you can store arbitrary JSON (large payloads, nested objects) and see it as debug information when you open an individual trace.
tracer.put_annotation(key="UserId", value=user_id)        # searchable, low-cardinality keys
tracer.put_metadata(key="RequestBody", value=event_body)  # not searchable, for detailed debugging

This design is the fundamental trade-off of search engines as-is. Indexed fields are searched quickly but come with the cost of building and maintaining the inverted index. That's why X-Ray limits the number of indexed annotations and forces you to send non-searchable details as metadata to avoid the cost.

💡 Related theory: The separation of annotation/metadata is exactly the same principle as the separation we saw yesterday (Week 10) in CloudWatch of metrics (low-cardinality aggregation) and logs (high-cardinality detail). Generalizing, observability data always splits into two classes — low-cardinality keys like dimensions/tags/annotations that are indexed for searching and aggregation, and high-cardinality details like payloads/metadata that are stored without indexing. Because the cost of reverse indices is proportional to cardinality. If you carelessly dump OrderId (unique per request, high-cardinality) into annotation, the index explodes, so the best practice is to keep only the keys you absolutely need to search in annotation and push the rest down to metadata. This is the same as how in Honeycomb·Elasticsearch, "which fields to index" is itself a cost design question.

Service Map — Aggregating Causal Graphs Into Topology

Thousands of individual traces can't be viewed one by one by humans. X-Ray automatically generates a Service Map by aggregating them — a service topology diagram. It groups the segments of each trace by service into nodes, and draws the call relationships as edges.

  • Nodes: Services (Lambda functions, API Gateway, DynamoDB tables, ...). A node colored red means a high error rate.
  • Edges: Call relationships. Thicker edges mean more calls, and color·numbers show average response time and error rates (fault/error/throttle) on each edge.

Problem interpretation is intuitive — follow the red and thick nodes/edges and the bottlenecks and failure points emerge. Click a node and you get a list of traces that passed through that service, then drill down to individual traces to see which subsegment consumed the time.

  User → API Gateway → Lambda A → Lambda B → DynamoDB
   Service Map (automatically aggregated):
     APIGW ──→ LambdaA ──→ LambdaB ──→ DynamoDB
              (p50/p99 latency, error% on each edge)

🔍 Going deeper: The Service Map is the topological union of the individual trace causal graphs. One trace is "the path this request took," but the Service Map is "the sum of the paths all requests took," revealing the entire system structure. The key here is the three classifications of errors — fault (5xx, server's fault), error (4xx, client's fault), throttle (429, rate limit). When Service Map is red, not all red is the same red. If throttle is frequent, it's a capacity/throttling issue; if fault is frequent, it's code/dependency failures; if error is frequent, malformed requests are coming in. If you can't distinguish these classifications, you'll fix the wrong place when you see a "red node."

X-Ray Daemon — Why Send Through a Daemon Instead of Directly

Lambda integrates automatically with X-Ray with no extra configuration, but on EC2·ECS·EKS you must go through an X-Ray Daemon. The application doesn't send segments directly to the X-Ray API; instead it throws them to a local Daemon via UDP. The Daemon batches and sends them to the X-Ray service.

  • ECS: Run the Daemon as a sidecar container
  • EC2: systemd service
  • EKS: DaemonSet (one per node)

Why introduce this indirection? Because of Dapper's "low overhead" goal. If the application synchronously calls the X-Ray API on every invocation, a network round-trip gets added on top of the main request. With UDP, it's fire-and-forget — the application doesn't wait for a response, and the Daemon takes on asynchronous batch transmission, retry, and buffering. This is exactly the same pattern as logging, where the application doesn't transmit remotely directly but throws to a local agent (Fluent Bit).

💡 Related theory: The pattern of placing a local agent between the application and the telemetry backend is a textbook example of the sidecar pattern. Separation of concerns — the application just takes on the responsibility of "throwing telemetry," while the sidecar handles "batching·retry·compression·authentication·routing." UDP is used because loss tolerance of trace data. Traces are statistical samples so losing some doesn't greatly shake the overall picture, so we choose low overhead (UDP) over reliability (TCP). By contrast, you'd never use UDP for payment transactions. "The cost-benefit ratio of data determines the transmission protocol" is a general principle illustrated here.

X-Ray Insights — Automatic Anomaly Detection

Humans can't constantly stare at traces. X-Ray Insights learns the normal baseline of the Service Map, and when latency·error rate spikes abnormally, it automatically creates an Insight and publishes an EventBridge event. Connect this to Lambda/SNS for automatic alerts and automatic remediation.

{ "source": ["aws.xray"], "detail-type": ["AWS X-Ray Insight Update"] }

This is the same lineage as CloudWatch Anomaly Detection from Week 10 — instead of static thresholds, it detects deviations from learned normal patterns. The difference is that the target is not metrics but trace-based health of service topology.

Wrapping Up

Today we covered five things. First, distributed tracing was born from Dapper (2010) with the trace-span-context propagation model, and X-Ray's Trace/Segment/Subsegment is its AWS version. Second, all the magic hinges on one thing: context propagation — Trace ID is propagated via headers, and the mismatch between X-Ray headers and W3C traceparent is the first reason traces break in multi-SDK environments. Third, Active vs Passive is the difference in authority to start a trace, and Active Tracing and SDK instrumentation are separate switches. Fourth, Annotation is indexed (searchable, low-cardinality) and Metadata is non-indexed (detailed), and this follows the economics of reverse indices. Fifth, Service Map is the topological union of individual causal graphs, and you must distinguish the three error classifications to fix the right place.

In the next article, we'll examine X-Ray's sampling and cost tuning at operational scale. We'll explore why Reservoir sampling has this structure, how sampling decisions consistently propagate in a distributed environment, and how to manage massive Service Maps with Groups and search expressions.

📝 Practice Questions

Click a choice to reveal the answer and explanation.

Question 1

An order service (X-Ray SDK) and a payment service (OpenTelemetry) run together, but on the order → payment hop the trace in the Service Map is split in two. What is the MOST likely cause and fix?

Question 2

Active Tracing is enabled on a Lambda function, and the Service Map shows the function invocation segment, but the subsegments for DynamoDB and HTTP calls made inside the function are not visible at all. What is the cause?

Question 3

Which additional X-Ray data is limited to 50 per trace and is indexed so that it can be searched with annotation.OrderId = "..."?

Question 4

What is the standard path by which an application running on EC2 sends traces to X-Ray?

Question 5

One node in the Service Map is shown in red. Why is it important to distinguish between throttle, fault (5xx), and error (4xx)?

Question 6

What does the 65500000 part of the X-Ray Trace ID 1-65500000-1234abcd... mean, and what is the design benefit?

Question 7

Which of the following is NOT one of the three design goals defined by distributed tracing (Dapper)?

📌 Today's Summary

There are five key takeaways today. First, distributed tracing was born from the trace-span-context propagation model of Google Dapper (2010), X-Ray's Trace/Segment/Subsegment is the AWS version of it, and a trace is closer to a DAG than a tree. Second, everything hinges on context propagation — the Trace ID is propagated in headers, and the mismatch between X-Ray's X-Amzn-Trace-Id and W3C traceparent is the first cause of trace breaks in multi-SDK environments, bridged with the ADOT propagator. Third, Active vs Passive is a difference in the authority to start a trace and is a separate switch from SDK instrumentation; Active Tracing alone does not show internal subsegments. Fourth, Annotations are indexed (searchable, low-cardinality, limit of 50) and Metadata is not indexed (detailed JSON), following the economics of inverted indexes. Fifth, the Service Map is the topological union of individual causal graphs, the three categories fault/error/throttle must be distinguished, and the Daemon implements low overhead as a UDP sidecar.

PreviousWeek 10 Review: Weaving the Observability Stack into an IncidentWeek 10 · Day 5Next X-Ray Sampling: Reservoir Algorithm and the Economics of Tracing at Operational ScaleWeek 11 · Day 2

Official references

  • DOP-C02 official exam page(opens in a new tab)
  • AWS X-Ray documentation(opens in a new tab)
  • AWS Lambda documentation(opens in a new tab)
  • Amazon DynamoDB documentation(opens in a new tab)
  • Amazon EC2 documentation(opens in a new tab)
  • Amazon API Gateway documentation(opens in a new tab)

On this page

  • Why Distributed Tracing Was Born — From Dapper to X-Ray
  • Trace ID and Context Propagation — The String That Crosses Process Boundaries
  • Active vs Passive Tracing — Who Starts the Trace
  • SDK and Auto-Instrumentation — Transparency of Instrumentation
  • Annotation vs Metadata — The Economics of Reverse Indices
  • Service Map — Aggregating Causal Graphs Into Topology
  • X-Ray Daemon — Why Send Through a Daemon Instead of Directly
  • X-Ray Insights — Automatic Anomaly Detection
  • Wrapping Up
  • Practice Questions
  • Today's Summary