aws_xray_sdk 0.3.1 copy "aws_xray_sdk: ^0.3.1" to clipboard
aws_xray_sdk: ^0.3.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.

Before and after instrumenting a Dart Lambda with aws_xray_sdk: an untraced run shows one opaque span, while the instrumented run breaks the same request into per-call subsegments

Top: uninstrumented — one opaque span. Bottom: the same request with aws_xray_sdk, each downstream call broken out as its own timed subsegment.


Features #

🔍 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 #

pub package

dart pub add aws_xray_sdk

Quick start #

import 'package:aws_xray_sdk/aws_xray_sdk.dart';

final tracer = XRayTracer(serviceName: 'order-service');

Future<void> processOrder(String orderId) {
  final segment = Segment.begin(
    name: 'order-service',
    traceId: TraceId.generate(),
  );

  return tracer.run(segment, () async {
    // All HttpClient calls inside this closure are traced automatically.
    final result = await fetchInventory(orderId);
    return result;
  });
}

Zero-config setup #

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 call. It is idempotent, so it's safe to call from multiple entry points.

void main() {
  XRay.configure(); // reads env, installs the global tracer, patches HTTP

  // Anywhere else, with no tracer threading:
  final ddb = DynamoDB(region: 'us-east-1', client: XRay.aws());
  // XRay.tracer is the configured tracer; until configure() runs it is a
  // no-op that discards everything, so instrumentation is safe either way.
}

Override any piece: XRay.configure(serviceName: 'svc', sampling: ReservoirSampler()). Use XRay.reset() to return to the unconfigured (no-op) state, e.g. in tests.


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

  final segment = Segment.begin(name: 'my-service', traceId: TraceId.generate());
  await tracer.run(segment, () 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.run(segment, () => 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.run(segment, () => dynamoDB.getItem(/* ... */));

For *.amazonaws.com hosts the subsegment is named after the service and gains AWS resource data (operation, table/queue/bucket) 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.run(segment, () 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.run(segment, () 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, or double. Anything else is coerced to its toString() (so a List is 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.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.run(segment, () 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:

  • requestId becomes aws.request_id, the primary AWS support correlation key.
  • region becomes aws.region; if omitted, the SDK derives it from standard regional AWS hosts such as dynamodb.us-east-1.amazonaws.com.
  • errorCode is used to mark AWS throttles even when the HTTP status is not 429.

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.dart for 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.
  final segment = Segment.begin(name: functionName, traceId: traceId);
  await tracer.run(segment, fn);
}

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',
      // `() async =>` coerces FunctionAction's FutureOr return to the Future
      // that runLambdaInvocation expects.
      action: (ctx, event) => XRay.runLambdaInvocation(
        capture, ctx.functionName, () async => 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 tracer.run() entry 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 run() to give the sampler contextual info:

await tracer.run(segment, 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 to reservoirSize traces/second. Give each isolate its own XRayTracer/ReservoirSampler (sharing one across isolates is unsupported and miscounts).
  • No active context ⇒ always sampled. Code that builds a segment and calls closeSegment outside a tracer.run() zone is sampled fail-open (so a manually constructed segment is never silently dropped). Under Lambda, the Sampled= flag from the runtime trace header is forwarded as-is via runLambda(..., 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:

  1. 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.
  2. Is the address right? The default is 127.0.0.1:2000. On newer Lambda runtimes it is the link-local AWS_XRAY_DAEMON_ADDRESS (e.g. 169.254.100.1:2000) — read it at cold start, don't hardcode.
  3. Is the segment sampled? With FixedRateSampler(0.05) only ~5% of traces are sent. Use FixedRateSampler(1.0) while verifying.
  4. Did the segment close? A segment is only sent after run() / closeSegment() completes. An un-awaited run() may exit before the flush.
  5. Are the daemon's AWS credentials/region valid? The daemon (not the SDK) uploads to X-Ray; check its logs for PutTraceSegments errors.

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.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.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 — run(), runLambda(), captureAsync(), annotate(), subsegment API
    xray.dart                    # XRay facade — patchHttp(), fromClient<T>(), registerClient<T>(), untracedHttpClient()
    trace_scope.dart             # TraceScope / TraceContext — live runtime entity tree (mutable, serialized at close)
    trace_suppression.dart       # runWithoutDartIoTracing() — avoids double-tracing under patchHttp
    utils.dart                   # randomHex(), nowSeconds()
    models/
      segment.dart               # Segment       (immutable value object)
      subsegment.dart            # Subsegment    (immutable value object)
      trace_id.dart              # TraceId       — generate, parse, header fields
      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 region parsing
      throttle_codes.dart        # AWS throttling error-code detection
    trace_header.dart            # X-Amzn-Trace-Id formatter

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.

0
likes
160
points
547
downloads

Documentation

API reference

Publisher

verified publishermabdelsamei.com

Weekly Downloads

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.

Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

http

More

Packages that depend on aws_xray_sdk