aws_xray_sdk 0.4.1
aws_xray_sdk: ^0.4.1 copied to clipboard
AWS X-Ray distributed tracing for Dart. Auto-traces dart:io HTTP and Smithy SDK clients, propagates X-Amzn-Trace-Id, and sends segments via UDP. First-class Lambda support.
aws_xray_sdk #
A Dart package for distributed tracing with AWS X-Ray.
Traces outbound HTTP calls and AWS SDK operations, propagates the
X-Amzn-Trace-Id header, and delivers completed segments to the X-Ray daemon
via UDP — with first-class support for AWS Lambda custom runtimes.
Contents:
Install ·
Quick start ·
HTTP tracing ·
captureAsync ·
Manual subsegments ·
AWS SDK clients ·
Lambda ·
Sampling ·
Sender options ·
Schema ·
Architecture ·
Local dev
See it in action #
One AWS Lambda request, traced two ways. Out of the box it's a single opaque
span: you see the total latency but nothing about where it went. Add
aws_xray_sdk and the same request fans out into a timed subsegment for every
downstream call — outbound HTTP, DynamoDB, KMS, Secrets Manager, a nested Lambda
invoke — so a slow dependency is obvious at a glance in the X-Ray service map.

Top: uninstrumented — one opaque span. Bottom: the same request with
aws_xray_sdk, each downstream call broken out as its own timed subsegment.
Features #
| ⚡ One-call tracing | XRay.trace('op', fn) — segment lifecycle in a single call; traceHeader: links into the caller's distributed trace |
| 🔍 Automatic HTTP tracing | Patch dart:io globally — every HttpClient call gets a subsegment with method, URL, and status |
| ☁️ AWS SDK client wrapping | Instrument any Smithy-generated client (DynamoDB, S3, KMS, …) via XRay.fromClient<T>() |
| λ Lambda-native | runLambda() attaches to the auto-created Lambda segment instead of competing with it |
| 📡 UDP-first transport | Fire-and-forget to the X-Ray daemon; reads AWS_XRAY_DAEMON_ADDRESS automatically |
| 🎛️ Flexible sampling | Fixed-rate and reservoir samplers; pluggable SamplingStrategy interface |
| 🔒 Zone-based context | Sampling decision and active segment flow across await chains with no manual threading |
| 🧊 AOT-safe | Zero dart:mirrors; no build_runner step — compiles with dart compile exe |
Installation #
dart pub add aws_xray_sdk
Quick start #
Two lines: configure once, then trace any unit of work.
import 'package:aws_xray_sdk/aws_xray_sdk.dart';
void main() async {
XRay.configure(); // reads env, installs the global tracer, patches HTTP
await XRay.trace('process-order', () async {
// Every HttpClient / AWS call in here becomes a subsegment automatically.
final result = await fetchInventory('order-1');
// Nested span with indexed annotations:
await XRay.capture('validate', (span) async {
span.annotate('orderId', 'order-1');
});
return result;
});
}
XRay.configure() builds a tracer from the standard AWS environment
(AWS_XRAY_DAEMON_ADDRESS, AWS_LAMBDA_FUNCTION_NAME), installs it as the
process-wide default (XRay.tracer), and patches dart:io HTTP — in one
idempotent call. Until it runs, XRay.tracer is a no-op that discards
everything, so instrumentation is always safe to call. Override any piece
(XRay.configure(serviceName: 'svc', sampling: ReservoirSampler())) and use
XRay.reset() to return to the unconfigured state, e.g. in tests.
Continuing a distributed trace: pass the incoming X-Amzn-Trace-Id header
and this service links into the caller's trace:
await XRay.trace('handle-request', handler,
traceHeader: request.headers.value('x-amzn-trace-id'),
httpMethod: 'POST', urlPath: '/checkout');
Explicit tracer #
Prefer no global state? Construct and hold the tracer yourself — trace() is
the same one-call API, and run() remains for pre-built segments:
final tracer = XRayTracer(serviceName: 'order-service');
Future<void> processOrder(String orderId) =>
tracer.trace('process-order', () async {
final result = await fetchInventory(orderId);
return result;
});
HTTP tracing #
Two ways to trace outbound HTTP, both injecting X-Amzn-Trace-Id, recording
the request URL and response status, marking HTTP errors, and closing the
subsegment when the response body stream finishes.
dart:io — global patch #
Call XRay.patchHttp(tracer) once at startup. Every HttpClient created
afterwards — including those inside third-party packages that use
package:http's IOClient — is automatically wrapped.
void main() async {
final tracer = XRayTracer(serviceName: 'my-service');
XRay.patchHttp(tracer); // patch dart:io globally
await tracer.trace('my-service', () async {
// This HttpClient call produces a subsegment automatically:
// name: 'api.example.com'
// namespace: 'remote' (or 'aws' for *.amazonaws.com)
// http.request.method / url
// http.response.status
final client = HttpClient();
final req = await client.getUrl(Uri.parse('https://api.example.com/data'));
final res = await req.close();
client.close();
});
}
To stop tracing: XRay.unpatchHttp().
package:http — wrap a client #
Wrap any package:http Client with XRayBaseClient:
final client = XRayBaseClient(http.Client(), tracer);
await tracer.trace('my-op', () => client.get(Uri.parse('https://api.example.com')));
XRayBaseClient injects the trace header into the http.BaseRequest it sends.
Treat package:http request objects as single-use, as intended by the package;
re-sending the same request instance can reuse the first attempt's Parent= id.
AWS SDKs on package:http (aws_client / aws_*_api)
AWS SDK packages that take an http.Client (e.g. aws_client and the agilord
aws_*_api packages) can be traced by handing them a wrapped client. Use the
XRay.httpClientFor convenience:
final dynamoDB = DynamoDB(
region: 'us-east-1',
client: XRay.httpClientFor(tracer), // every call is traced
);
await tracer.trace('my-op', () => dynamoDB.getItem(/* ... */));
For *.amazonaws.com hosts the subsegment is named after the service and gains
AWS resource data (operation, table/queue/bucket, and — for the Lambda
data-plane Invoke call — the target function_name) automatically; no
registerClient needed (that path is for Smithy-generated clients).
Response-body lifecycle #
If a response body is never drained, the SDK does not drop the span. At
trace finalization it emits the subsegment once, closed, with
metadata.xray.incomplete = true and the request/response fields known so far.
This covers status-only callers, HEAD, and 204/304-style responses while still
letting body-stream errors mark the span as faulted when the body is consumed.
Nested subsegments — captureAsync #
captureAsync wraps a block as a subsegment and nests anything traced
inside it — manual subsegments and auto-instrumented HTTP/AWS calls become
children of it, so the X-Ray service map shows the real call tree:
await tracer.trace('checkout', () async {
await tracer.captureAsync('process-order', (span) async {
span.annotate('orderId', id); // indexed — filterable in console
span.addMetadata('items', cart.length); // non-indexed detail
await http.get(itemsUri); // nested under 'process-order'
await ddb.putItem(...); // nested under 'process-order'
});
});
The scope is bound to a forked Zone, so concurrent captureAsync calls (e.g.
inside Future.wait) stay independent — parallel branches don't tangle. An
uncaught error inside the block marks the subsegment as faulted and rethrows.
Live annotations & metadata #
tracer.annotate(key, value) and tracer.addMetadata(key, value) apply to
whatever is currently being traced — the active captureAsync subsegment, or
the segment itself at the top level:
await tracer.trace('handle-request', () async {
tracer.annotate('userId', userId); // → segment annotations (indexed)
tracer.addMetadata('region', 'us-east-1');
});
Annotations are indexed and searchable in the console via filter expressions. X-Ray restricts them, and the SDK enforces the rules by sanitizing rather than throwing (a bad annotation never drops the trace):
- Keys may contain only
A-Z,a-z,0-9, and_. Any other character is replaced with_(so'order.id'becomes'order_id'). - Values must be a scalar —
String,bool,int, ordouble. Anything else is coerced to itstoString()(so aListis stored as its text form).
Metadata is not indexed and not validated: the value may be any
JSON-serializable object (maps, lists, nested structures) — use it for richer
detail you don't need to filter on. Avoid the AWS. namespace prefix, which
X-Ray reserves for its own use. X-Ray also applies a soft limit of ~50
annotations per trace.
Missing trace context #
tracer.annotate, addMetadata, and the manual beginSubsegment /
endSubsegment API only record when called inside a tracer.trace() /
run() / runLambda() / captureAsync() zone. When there is no active trace,
the data would be dropped; ContextMissingPolicy controls what happens:
XRayTracer(
serviceName: 'svc',
// ignore — drop silently (default; fire-and-forget)
// logError — write a diagnostic to stderr, then drop
// runtimeError — throw a StateError (surfaces missing instrumentation in tests)
contextMissingPolicy: ContextMissingPolicy.logError,
);
The auto-instrumented HTTP clients are intentionally exempt: called outside a
run() zone they pass the request through untraced rather than triggering the
policy. tracer.currentSegment is also exempt — it is a side-effect-free getter
that returns null to signal "no active trace".
Manual subsegments #
For flat, sibling subsegments (or when begin and end straddle a callback), use the manual API. Each attaches under whatever scope is active when it is opened:
await tracer.trace('ingest', () async {
// Instrument any synchronous or async block:
final sub = tracer.beginSubsegment('parse-payload');
try {
final result = heavyParsing(data);
tracer.endSubsegment(sub.addMetadata('recordCount', result.length));
return result;
} catch (e) {
tracer.failSubsegment(sub, e); // marks fault=true, records exception
rethrow;
}
});
Recording errors on the segment itself #
Segment mirrors the Subsegment error API — pass an optional exception to
capture it in the X-Ray cause block:
final segment = tracer.beginSegment();
try {
await tracer.run(segment, fn);
} catch (e) {
await tracer.closeSegment(segment.withFault(e)); // fault=true + cause
}
AWS SDK client wrapping #
Register a descriptor once at cold-start, then wrap any instance:
// 1. Register (once, at startup)
XRay.registerClient<DynamoDbClient>(
requestAdapter: (req) {
final r = req as DdbReq;
return (
operationName: r.operation,
method: 'POST',
url: r.endpoint,
body: {'TableName': r.tableName}, // used for resource extraction
// Inject X-Amzn-Trace-Id into the outbound request and return the copy.
withTraceHeader: (original, header) =>
original.rebuild(headers: {...original.headers, 'X-Amzn-Trace-Id': header}),
);
},
responseAdapter: (res) {
final r = res as DdbRes;
return (
statusCode: r.statusCode,
contentLength: null,
requestId: r.requestId,
region: null, // omitted here: derived from the request URL when possible
errorCode: null, // set for modeled AWS throttle/error responses if available
);
},
rebuild: (client, wrapSend) {
final inner = (req) => client.rawSend(req as DdbReq);
return client.copyWith(httpSend: wrapSend(inner));
},
);
// 2. Wrap (per instance)
final ddb = XRay.fromClient(DynamoDbClient(...), tracer: tracer);
await ddb.getItem(...); // subsegment created automatically
The response adapter fields map directly to X-Ray metadata:
requestIdbecomesaws.request_id, the primary AWS support correlation key.regionbecomesaws.region; if omitted, the SDK derives it from standard regional AWS hosts such asdynamodb.us-east-1.amazonaws.com.errorCodeis used to mark AWS throttles even when the HTTP status is not429.
Registered client namespaces are normalized to X-Ray schema values. Use aws
for AWS service clients and remote for other downstream clients; legacy
AWS::... values are treated as aws.
See
example/aws_sdk_tracing.dartfor a complete, runnable version with a stub client.
Lambda integration #
Lambda's provided:al2023 runtime automatically creates an
AWS::Lambda::Function segment. Sending a second top-level segment conflicts
and is silently dropped by the daemon. Use runLambda() instead — it emits an
independent subsegment document parented to Lambda's auto-created segment.
// In your Lambda runtime loop, after parsing the Lambda-Runtime-Trace-Id header:
final rawHeader = ctx.traceId ?? '';
final traceId = TraceId.tryParse(rawHeader) ?? TraceId.generate();
final parentId = TraceId.parseParentId(rawHeader); // 'Parent=' field
final sampled = TraceId.parseSampled(rawHeader) ?? true;
if (parentId != null) {
await tracer.runLambda(traceId, parentId, functionName, fn, sampled: sampled);
} else {
// Local testing fallback — no Lambda runtime present.
await tracer.trace(functionName, fn, traceHeader: rawHeader);
}
Resulting X-Ray trace:
AWS::Lambda (facade) [auto]
AWS::Lambda::Function [auto — id from Lambda-Runtime-Trace-Id]
Overhead [auto]
<function-name> ← our handler subsegment ✓
validation ← manual subsegment
dynamodb.us-east-1.amazonaws.com ← auto HTTP, namespace=aws
api.downstream.com ← auto HTTP, namespace=remote
Trace header source #
Always read the trace context from the Lambda-Runtime-Trace-Id HTTP
response header returned by the Runtime API's /invocation/next call —
not from _X_AMZN_TRACE_ID (the process environment variable).
Lambda sets both per invocation, but they often carry different trace IDs:
the env var reflects the incoming request trace (e.g. from API Gateway), while
the header carries the function-level trace ID that Lambda's auto-created
AWS::Lambda::Function segment uses. Reading from the env var causes your
subsegment document to land in a separate, unlinked trace.
❌ Platform.environment['_X_AMZN_TRACE_ID'] // incoming request trace ID
✅ nextRes.headers.value('lambda-runtime-trace-id') // function-level trace ID
Daemon address #
Lambda injects the daemon address via AWS_XRAY_DAEMON_ADDRESS. Do not
hardcode 127.0.0.1:2000 — newer Lambda environments use a link-local address
(169.254.100.1:2000). The recommended setup:
(String host, int port) _daemonAddress() {
final raw = Platform.environment['AWS_XRAY_DAEMON_ADDRESS'] ?? '127.0.0.1:2000';
final colon = raw.lastIndexOf(':');
return colon == -1 ? (raw, 2000) : (raw.substring(0, colon), int.parse(raw.substring(colon + 1)));
}
final tracer = XRayTracer(
serviceName: Platform.environment['AWS_LAMBDA_FUNCTION_NAME'] ?? 'my-function',
sender: UdpSender(host: host, port: port),
sampling: FixedRateSampler(1.0), // Lambda decides sampling; always forward to daemon
);
Using aws_lambda_dart_runtime_ns #
LambdaTraceCapture does the trace-header capture for you. It overrides the
global package:http client for a zone (the runtime polls /invocation/next
through it) and parses the Lambda-Runtime-Trace-Id header into a
LambdaTraceContext. You supply only the runtime-specific handler glue, which
depends on the aws_lambda_dart_runtime_ns
types.
XRay.runLambdaInvocation(capture, functionName, fn) collapses the whole
capture.context() → parentId check → runLambda/run branch into one call,
so each handler reduces to a thin adapter. It uses the process-wide tracer
installed by XRay.configure(), so no tracer needs to be threaded through:
final capture = LambdaTraceCapture();
FunctionHandler xRayHandler(FunctionAction action) => FunctionHandler(
name: 'xray',
action: (ctx, event) => XRay.runLambdaInvocation(
capture, ctx.functionName, () => action(ctx, event)),
);
void main() {
XRay.configure(sampling: FixedRateSampler(1.0));
capture.run(() => invokeAwsLambdaRuntime([
xRayHandler(handleEvent),
]));
}
Under the hood, runLambdaInvocation is exactly this branch — reach for it
directly only if you need to customize the dispatch:
final tc = capture.context(); // parsed from the captured header
if (tc.parentId != null) {
return tracer.runLambda(
tc.traceId, tc.parentId!, ctx.functionName,
() async => action(ctx, event),
sampled: tc.sampled,
);
}
// No header captured — start a fresh top-level segment.
final segment = Segment.begin(name: ctx.functionName, traceId: tc.traceId);
return tracer.run(segment, () async => action(ctx, event));
Complete Lambda example #
See example/lambda_runtime.dart for a runnable
reference showing how to read the Lambda-Runtime-Trace-Id header and forward it
to runLambda().
Sampling #
The sampling decision is made once at trace entry (tracer.trace() /
run()) and stored in the zone so every downstream header injection uses the
same Sampled=1/0 flag. An unsampled trace is still built (so your code runs
identically) but the segment is never sent to the daemon. Pass httpMethod and
urlPath to give the sampler contextual info:
await tracer.trace('checkout', fn, httpMethod: 'POST', urlPath: '/checkout');
// Fixed rate — sample N% of all requests
XRayTracer(serviceName: 'svc', sampling: FixedRateSampler(0.05)) // 5 %
// Reservoir — keep up to N traces/second, then fall back to fixed rate
XRayTracer(serviceName: 'svc',
sampling: ReservoirSampler(reservoirSize: 50, fixedRate: 0.05))
// Custom strategy — sample based on request properties
class MyRuleSampler implements SamplingStrategy {
@override
bool shouldSample(SamplingRequest req) =>
req.urlPath.startsWith('/checkout');
}
Sampler semantics #
| Sampler | Decision rule |
|---|---|
FixedRateSampler(rate) |
Each request is an independent coin flip sampled with probability rate (via Random.secure). 0.0 = none, 1.0 = all. No per-second guarantee — low rates over low traffic can produce streaks of unsampled requests. |
ReservoirSampler(reservoirSize, fixedRate) |
The first reservoirSize requests each calendar second are always sampled; once that second's reservoir is exhausted, further requests fall back to a fixedRate coin flip. The reservoir resets every second. |
Both ignore the SamplingRequest fields by default; only a custom
SamplingStrategy reads them.
Local-only — no centralized rules (yet) #
These samplers are local: each isolate decides independently with no
coordination and no call to the X-Ray sampling API. There is no
centralized-rule fallback — the configured local strategy is always
authoritative, and GetSamplingRules / GetSamplingTargets are not consulted
(centralized sampling is a planned feature, see the Roadmap). Two consequences:
- Per-isolate reservoir.
ReservoirSampler's budget is per isolate, not per service: N isolates each admit up toreservoirSizetraces/second. Give each isolate its ownXRayTracer/ReservoirSampler(sharing one across isolates is unsupported and miscounts). - No active context ⇒ always sampled. Code that builds a segment and calls
closeSegmentoutside a trace zone is sampled fail-open (so a manually constructed segment is never silently dropped). Under Lambda, theSampled=flag from the runtime trace header is forwarded as-is viarunLambda(..., sampled:).
Sender options #
| Sender | Description |
|---|---|
UdpSender (default) |
Fire-and-forget UDP to the X-Ray daemon (127.0.0.1:2000) |
NoopSender |
Discards all segments; useful for local dev |
InMemorySender |
Records emitted segments and packets so tests can assert on them |
Note: The package currently sends traces through the X-Ray daemon protocol (
UdpSender). The PutTraceSegments HTTP API path is not shipped because SigV4 signing is not implemented.
// Tests — capture segments and assert on them
final sender = InMemorySender();
final tracer = XRayTracer(serviceName: 'svc', sender: sender);
// ... exercise code, then: expect(sender.segments, hasLength(1));
// Local dev — discard all segments
XRayTracer(serviceName: 'svc', sender: NoopSender());
// Custom daemon host (e.g. container-based setup)
XRayTracer(serviceName: 'svc',
sender: UdpSender(host: 'xray-daemon.local', port: 2000));
UdpSender never throws into your traced code — a resolution, bind, or send
failure is contained. To observe local send failures, pass an onError
callback (silent by default):
XRayTracer(
serviceName: 'svc',
sender: UdpSender(onError: (e) => log.warning('X-Ray send failed', e)),
);
Traces not showing up? #
UDP is fire-and-forget with no delivery acknowledgment — a datagram sent to
a daemon that isn't listening still succeeds locally, so the SDK cannot tell you
"the daemon didn't receive it" (and onError only fires on local failures
like an unreachable network or a failed DNS lookup). If segments aren't
appearing in the X-Ray console, work down this list:
- Is the daemon running and reachable? Start it in verbose mode and watch
its own logs for received segments:
amazon/aws-xray-daemon -o -l dev. - Is the address right? The default is
127.0.0.1:2000. On newer Lambda runtimes it is the link-localAWS_XRAY_DAEMON_ADDRESS(e.g.169.254.100.1:2000) — read it at cold start, don't hardcode. - Is the segment sampled? With
FixedRateSampler(0.05)only ~5% of traces are sent. UseFixedRateSampler(1.0)while verifying. - Did the segment close? A segment is only sent after
run()/closeSegment()completes. An un-awaitedrun()may exit before the flush. - Are the daemon's AWS credentials/region valid? The daemon (not the SDK)
uploads to X-Ray; check its logs for
PutTraceSegmentserrors.
Delivery can only be confirmed at the daemon or the X-Ray console — not from inside the process.
Segment document schema #
The SDK emits JSON that conforms to the X-Ray segment document schema. Key fields:
{
"trace_id": "1-5759e988-bd862e3fe1be46a994272793",
"id": "70de5b6f19ff9a70",
"name": "order-service",
"start_time": 1461096053.37518,
"end_time": 1461096053.40701,
"subsegments": [
{
"id": "4cd3d1ec0a974eef",
"name": "api.example.com",
"namespace": "remote",
"http": {
"request": { "method": "GET", "url": "https://api.example.com/data" },
"response": { "status": 200 }
}
}
]
}
Oversized segments (> 64 KB) are automatically split into a skeleton segment plus one independent subsegment document per subsegment.
Architecture #
Application code
│
▼
XRayTracer.trace(name, fn) builds the Segment (fresh, or continued
│ from traceHeader), then delegates to:
▼
XRayTracer.run / runLambda Zone stores: Segment, TraceState (entity
│ tree + current scope), sampled
│
├──── captureAsync(name, fn) (nested; forks a child scope/zone)
├──── annotate / addMetadata (mutate the current scope)
├──── beginSubsegment / endSubsegment / failSubsegment (manual, flat)
│
├──── XRayHttpClient (auto via XRay.patchHttp — dart:io)
│ └─ openUrl → beginSubsegment, inject X-Amzn-Trace-Id
│ └─ close → endSubsegment with status
│
├──── XRayBaseClient (package:http — XRay.aws / httpClientFor)
│ └─ send → beginSubsegment, inject header, endSubsegment
│
└──── XRayInterceptor (auto via XRay.fromClient<T> — Smithy)
└─ wrap send fn → beginSubsegment, await, endSubsegment
│
▼
finally block (run path) runLambda path
│
├── encode(segment) encodeSubsegmentDoc()
│ (independent subsegment)
└── UdpSender.send UdpSender.sendPackets
fire-and-forget UDP → 127.0.0.1:2000
Zone-based context means you never pass the tracer or segment through
function arguments. Any code that runs inside tracer.trace(…) / run(…) — including
library code — can call tracer.currentSegment to get the active segment.
Package layout #
lib/
aws_xray_sdk.dart # public barrel export
src/
tracer.dart # XRayTracer — trace(), run(), runLambda(), captureAsync(), annotate(), subsegment API
xray.dart # XRay facade — trace(), capture(), patchHttp(), fromClient<T>(), untracedHttpClient()
utils.dart # randomHex(), nowSeconds()
context/
trace_scope.dart # TraceScope / TraceContext — live runtime entity tree (mutable, serialized at close)
trace_suppression.dart # runWithoutDartIoTracing() — avoids double-tracing under patchHttp
models/
segment.dart # Segment (immutable value object)
subsegment.dart # Subsegment (immutable value object)
trace_id.dart # TraceId — generate, parse, header fields
trace_header.dart # X-Amzn-Trace-Id formatter
http_data.dart # HttpData, HttpRequestData, HttpResponseData
aws_data.dart # AwsData — operation, tableName, …
cause.dart # Cause + XRayException
annotation.dart # annotation key/value sanitize + coerce helpers
sampling/
sampling_strategy.dart # SamplingRequest, SamplingStrategy interface
fixed_rate_sampler.dart # FixedRateSampler
reservoir_sampler.dart # ReservoirSampler (one instance per isolate)
sender/
sender.dart # Sender abstract class (send, close, sendPackets)
udp_sender.dart # UdpSender — fire-and-forget UDP
noop_sender.dart # NoopSender — discard (local dev)
in_memory_sender.dart # InMemorySender — capture segments/packets for tests
segment_encoder.dart # encode(), encodeSubsegmentDoc()
http/
xray_http_client.dart # XRayHttpClient — wraps dart:io HttpClient
xray_http_overrides.dart # XRayHttpOverrides — global dart:io patch
xray_base_client.dart # XRayBaseClient — wraps package:http BaseClient
xray_server_middleware.dart# handleTraced() — dart:io HttpServer request tracing
wrappers/
xray_interceptor.dart # XRayInterceptor<Req,Res>, adapter records
client_registry.dart # internal descriptor registry
resource_extractor.dart # ResourceExtractor — DDB/S3/KMS/SQS/SNS
lambda/
lambda_trace_capture.dart # LambdaTraceCapture — captures Lambda-Runtime-Trace-Id
aws/
region.dart # AWS endpoint host/region parsing (isAwsHost, regionFromAwsHost)
throttle_codes.dart # AWS throttling error-code detection
Roadmap #
In progress / known stubs #
| Item | File | Notes |
|---|---|---|
| PutTraceSegments HTTP API sender | new sender module | Implement AWS SigV4 request signing to enable daemon-less delivery. |
Planned features #
| Feature | Priority | Description |
|---|---|---|
| Shelf server middleware | Medium | handleTraced already covers dart:io HttpServer (parses incoming X-Amzn-Trace-Id, continues the Segment, and injects the header into responses). A package:shelf middleware variant would extend this to that ecosystem. |
| Dynamic sampling rules (X-Ray API) | Medium | Poll the X-Ray GetSamplingRules / GetSamplingTargets API and apply centrally managed rules, matching the behaviour of the official SDKs. |
| X-Ray groups and filter expressions | Low | Support emitting service and origin metadata that X-Ray filter expressions can target for trace grouping and alerting. |
TracedSpan mixin / base class |
Low | Segment and Subsegment share ~150 lines of identical fields and _copyWith / toJson logic. Extracting a common mixin would reduce duplication and make future field additions cheaper. |
| Injected clock for testing | Low | nowSeconds() reads DateTime.now() directly, making timing-sensitive tests depend on wall-clock sleep. An injectable clock would allow deterministic tests with no Future.delayed. |
Contributing #
git clone https://github.com/mohamed-abdelsamei/aws-xray-sdk-dart.git
cd aws-xray-sdk-dart
dart pub get
dart test # run all tests
dart analyze --fatal-warnings # must pass clean
dart format . # format code
Local development #
Run the X-Ray daemon locally to push traces to the AWS X-Ray console:
1. Start the daemon #
With Docker and an SSO profile:
./scripts/run-daemon.sh <profile-name>
Or manually with the export-credentials workaround:
eval "$(aws configure export-credentials --profile <your-profile> --format env)"
docker run --rm \
-e AWS_ACCESS_KEY_ID -e AWS_SECRET_ACCESS_KEY -e AWS_SESSION_TOKEN \
-e AWS_REGION=us-east-1 \
-p 2000:2000/udp \
amazon/aws-xray-daemon:3.x -o -n us-east-1
2. Run an instrumented Dart program #
# Basic traced operation
dart run example/basic_usage.dart
# Zero-config setup (XRay.configure / global tracer)
dart run example/zero_config.dart
# Automatic dart:io HTTP tracing
dart run example/http_tracing.dart
# package:http tracing via XRayBaseClient
dart run example/package_http_tracing.dart
# AWS SDK client wrapping (prints the captured subsegments)
dart run example/aws_sdk_tracing.dart
# Server-side tracing with handleTraced
dart run example/server_middleware.dart
The full set of runnable examples lives in example/ — see its
README for the complete index.
3. View traces #
Open the X-Ray console — segments appear within ~10 seconds of the run.
License #
MIT — see LICENSE.