From 6c4138a6baf119bb207c9e29148495ca2325c8c4 Mon Sep 17 00:00:00 2001 From: "james.eastham" Date: Thu, 14 Aug 2025 17:57:34 +0100 Subject: [PATCH 1/4] feat: add configurable support for span links instead of parent-child --- src/index.ts | 7 + src/trace/listener.spec.ts | 51 +++ src/trace/listener.ts | 15 +- src/trace/tracer-wrapper.ts | 1 + yarn.lock | 617 +++++++++--------------------------- 5 files changed, 220 insertions(+), 471 deletions(-) diff --git a/src/index.ts b/src/index.ts index 1d401e4f..ee898d33 100644 --- a/src/index.ts +++ b/src/index.ts @@ -52,6 +52,7 @@ export const minColdStartTraceDurationEnvVar = "DD_MIN_COLD_START_DURATION"; export const coldStartTraceSkipLibEnvVar = "DD_COLD_START_TRACE_SKIP_LIB"; export const localTestingEnvVar = "DD_LOCAL_TESTING"; export const addSpanPointersEnvVar = "DD_TRACE_AWS_ADD_SPAN_POINTERS"; +export const useSpanLinksEnvVar = "DD_USE_SPAN_LINKS"; interface GlobalConfig { /** @@ -97,6 +98,7 @@ export const defaultConfig: Config = { coldStartTraceSkipLib: "", localTesting: false, addSpanPointers: true, + useSpanLinks: false } as const; export const _metricsQueue: MetricsQueue = new MetricsQueue(); @@ -419,6 +421,11 @@ function getConfig(userConfig?: Partial): Config { config.addSpanPointers = result === "true"; } + if (userConfig === undefined || userConfig.useSpanLinks === undefined) { + const result = getEnvValue(useSpanLinksEnvVar, "false").toLowerCase(); + config.useSpanLinks = result === "true"; + } + return config; } diff --git a/src/trace/listener.spec.ts b/src/trace/listener.spec.ts index 5b27fb6c..91dcc761 100644 --- a/src/trace/listener.spec.ts +++ b/src/trace/listener.spec.ts @@ -89,6 +89,7 @@ describe("TraceListener", () => { minColdStartTraceDuration: 3, coldStartTraceSkipLib: "", addSpanPointers: true, + useSpanLinks: false }; const context = { invokedFunctionArn: "arn:aws:lambda:us-east-1:123456789101:function:my-lambda", @@ -189,6 +190,56 @@ describe("TraceListener", () => { ); }); + it("wraps dd-trace span around invocation, with linked trace context from event", async () => { + const listener = new TraceListener({ + ...defaultConfig, + useSpanLinks: true + }); + mockTraceSource = TraceSource.Event; + mockSpanContext = { + toTraceId: () => "4110911582297405551", + toSpanId: () => "797643193680388251", + _sampling: { + priority: "2", + }, + }; + mockSpanContextWrapper = { + spanContext: mockSpanContext, + }; + await listener.onStartInvocation({}, context as any); + const unwrappedFunc = () => {}; + const wrappedFunc = listener.onWrap(unwrappedFunc); + wrappedFunc(); + await listener.onCompleteInvocation(); + + expect(mockWrap).toHaveBeenCalledWith( + "aws.lambda", + { + resource: "my-Lambda", + service: "my-Lambda", + tags: { + cold_start: "true", + function_arn: "arn:aws:lambda:us-east-1:123456789101:function:my-lambda", + function_version: "$LATEST", + request_id: "1234", + resource_names: "my-Lambda", + functionname: "my-lambda", + "_dd.parent_source": "event", + datadog_lambda: datadogLambdaVersion, + dd_trace: ddtraceVersion, + }, + type: "serverless", + childOf: undefined, + links: expect.arrayContaining([ + expect.objectContaining({ + context: mockSpanContext, + }), + ]), + }, + unwrappedFunc, + ); + }); + it("wraps dd-trace span around invocation, without trace context from xray", async () => { const listener = new TraceListener(defaultConfig); mockTraceSource = TraceSource.Xray; diff --git a/src/trace/listener.ts b/src/trace/listener.ts index bc2cb647..91d380e9 100644 --- a/src/trace/listener.ts +++ b/src/trace/listener.ts @@ -74,6 +74,11 @@ export interface TraceConfig { * @default true */ addSpanPointers: boolean; + /** + * Whether to use SpanLinks instead of parent-child spans + * @default true + */ + useSpanLinks: boolean; } export class TraceListener { @@ -316,7 +321,15 @@ export class TraceListener { }; } if (this.lambdaSpanParentContext) { - options.childOf = this.lambdaSpanParentContext; + if (this.config.useSpanLinks){ + options.links = [ + { + context: this.lambdaSpanParentContext, + }, + ]; + } else { + options.childOf = this.lambdaSpanParentContext; + } } options.type = "serverless"; diff --git a/src/trace/tracer-wrapper.ts b/src/trace/tracer-wrapper.ts index 97097b6e..252c0000 100644 --- a/src/trace/tracer-wrapper.ts +++ b/src/trace/tracer-wrapper.ts @@ -21,6 +21,7 @@ export interface TraceOptions { type?: string; tags?: { [key: string]: any }; childOf?: SpanContext; + links?: Array<{context: SpanContext, attributes?: Object}> } // TraceWrapper is used to remove dd-trace as a hard dependency from the npm package. diff --git a/yarn.lock b/yarn.lock index 8a0bab15..443007de 100644 --- a/yarn.lock +++ b/yarn.lock @@ -23,7 +23,7 @@ "@smithy/util-utf8" "^2.0.0" tslib "^2.6.2" -"@aws-crypto/sha256-js@5.2.0", "@aws-crypto/sha256-js@^5.2.0": +"@aws-crypto/sha256-js@^5.2.0", "@aws-crypto/sha256-js@5.2.0": version "5.2.0" resolved "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz" integrity sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA== @@ -144,7 +144,7 @@ tslib "^2.6.2" uuid "^9.0.1" -"@aws-sdk/client-sso-oidc@3.677.0": +"@aws-sdk/client-sso-oidc@^3.667.0", "@aws-sdk/client-sso-oidc@3.677.0": version "3.677.0" resolved "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.677.0.tgz" integrity sha512-2zgZkRIU7DsnUVOy+9bjfJ0IYMzi9ONWXQt/WqMa7HOnj4RfenfpipyhHYxGZR5kmehgv53EI79yvUu+SAfGNg== @@ -189,7 +189,7 @@ "@smithy/util-utf8" "^3.0.0" tslib "^2.6.2" -"@aws-sdk/client-sso-oidc@3.721.0": +"@aws-sdk/client-sso-oidc@^3.721.0", "@aws-sdk/client-sso-oidc@3.721.0": version "3.721.0" resolved "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.721.0.tgz" integrity sha512-jwsgdUEbNJqs1O0AQtf9M6SI7hFIjxH+IKeKCMca0xVt+Tr1UqLr/qMK/6W8LoMtRFnE0lpBSHW6hvmLp2OCoQ== @@ -322,7 +322,7 @@ "@smithy/util-utf8" "^3.0.0" tslib "^2.6.2" -"@aws-sdk/client-sts@3.677.0": +"@aws-sdk/client-sts@^3.677.0", "@aws-sdk/client-sts@3.677.0": version "3.677.0" resolved "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.677.0.tgz" integrity sha512-N5fs1GLSthnwrs44b4IJI//dcShuIT42g4pM8FCUJZwbrWn9Sp9F876R1mvb8A9TAy2S4qCXi7TkHS0REnuicQ== @@ -368,7 +368,7 @@ "@smithy/util-utf8" "^3.0.0" tslib "^2.6.2" -"@aws-sdk/client-sts@3.721.0": +"@aws-sdk/client-sts@^3.716.0", "@aws-sdk/client-sts@^3.721.0", "@aws-sdk/client-sts@3.721.0": version "3.721.0" resolved "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.721.0.tgz" integrity sha512-1Pv8F02hQFmPZs7WtGfQNlnInbG1lLzyngJc/MlZ3Ld2fIoWjaWp7bJWgYAjnzHNEuDtCabWJvIfePdRqsbYoA== @@ -778,7 +778,7 @@ "@smithy/types" "^3.7.2" tslib "^2.6.2" -"@aws-sdk/types@3.667.0": +"@aws-sdk/types@^3.222.0", "@aws-sdk/types@3.667.0": version "3.667.0" resolved "https://registry.npmjs.org/@aws-sdk/types/-/types-3.667.0.tgz" integrity sha512-gYq0xCsqFfQaSL/yT1Gl1vIUjtsg7d7RhnUfsXaHt8xTxOKRTdH9GjbesBjXOzgOvB0W0vfssfreSNGFlOOMJg== @@ -794,14 +794,6 @@ "@smithy/types" "^3.7.2" tslib "^2.6.2" -"@aws-sdk/types@^3.222.0": - version "3.664.0" - resolved "https://registry.npmjs.org/@aws-sdk/types/-/types-3.664.0.tgz" - integrity sha512-+GtXktvVgpreM2b+NJL9OqZGsOzHwlCUrO8jgQUvH/yA6Kd8QO2YFhQCp0C9sSzTteZJVqGBu8E0CQurxJHPbw== - dependencies: - "@smithy/types" "^3.5.0" - tslib "^2.6.2" - "@aws-sdk/util-endpoints@3.667.0": version "3.667.0" resolved "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.667.0.tgz" @@ -884,7 +876,7 @@ resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.9.tgz" integrity sha512-yD+hEuJ/+wAJ4Ox2/rpNv5HIuPG82x3ZlQvYVn8iYCprdxzE7P1udpGF1jyjQVBU4dgznN+k2h103vxZ7NdPyw== -"@babel/core@^7.1.0", "@babel/core@^7.12.3", "@babel/core@^7.7.2", "@babel/core@^7.8.0": +"@babel/core@^7.0.0", "@babel/core@^7.0.0-0", "@babel/core@^7.1.0", "@babel/core@^7.12.3", "@babel/core@^7.7.2", "@babel/core@^7.8.0", "@babel/core@>=7.0.0-beta.0 <8": version "7.25.9" resolved "https://registry.npmjs.org/@babel/core/-/core-7.25.9.tgz" integrity sha512-WYvQviPw+Qyib0v92AwNIrdLISTp7RfDkM7bPqBvpbnhY4wq8HvHBZREVdYDXk98C8BkOIVnHAY3yvj7AVISxQ== @@ -1146,12 +1138,12 @@ "@datadog/libdatadog@0.7.0": version "0.7.0" - resolved "https://registry.yarnpkg.com/@datadog/libdatadog/-/libdatadog-0.7.0.tgz#81e07d3040c628892db697ccd01ae3c4d2a76315" + resolved "https://registry.npmjs.org/@datadog/libdatadog/-/libdatadog-0.7.0.tgz" integrity sha512-VVZLspzQcfEU47gmGCVoRkngn7RgFRR4CHjw4YaX8eWT+xz4Q4l6PvA45b7CMk9nlt3MNN5MtGdYttYMIpo6Sg== "@datadog/native-appsec@9.0.0": version "9.0.0" - resolved "https://registry.yarnpkg.com/@datadog/native-appsec/-/native-appsec-9.0.0.tgz#3ac854d8597ca75af0aa534aae28fa7f13057d2a" + resolved "https://registry.npmjs.org/@datadog/native-appsec/-/native-appsec-9.0.0.tgz" integrity sha512-C7v16pP4p4Y+Cx0jcxTYmhZTptfVs8TYbn6LH/aQgTkwx2tWsWN5lss7fjBYjWyZoPMwVh2UX/yDm4ES25hJnQ== dependencies: node-gyp-build "^3.9.0" @@ -1165,7 +1157,7 @@ "@datadog/native-metrics@3.1.1": version "3.1.1" - resolved "https://registry.yarnpkg.com/@datadog/native-metrics/-/native-metrics-3.1.1.tgz#4e5c9775751af13e353e64e573ab724104538cee" + resolved "https://registry.npmjs.org/@datadog/native-metrics/-/native-metrics-3.1.1.tgz" integrity sha512-MU1gHrolwryrU4X9g+fylA1KPH3S46oqJPEtVyrO+3Kh29z80fegmtyrU22bNt8LigPUK/EdPCnSbMe88QbnxQ== dependencies: node-addon-api "^6.1.0" @@ -1173,7 +1165,7 @@ "@datadog/pprof@5.9.0": version "5.9.0" - resolved "https://registry.yarnpkg.com/@datadog/pprof/-/pprof-5.9.0.tgz#beacd1507304e3490900ff39ea108e7ea772921b" + resolved "https://registry.npmjs.org/@datadog/pprof/-/pprof-5.9.0.tgz" integrity sha512-7KretVkHUANWe31u9cGJpxmUkyrXsCD+fmlZQUz/zk9mtQNC4uBIKX53VUFfrVj/bxAhEEIPw5XTYiMc5RJLsw== dependencies: delay "^5.0.0" @@ -1184,7 +1176,7 @@ "@datadog/sketches-js@2.1.1": version "2.1.1" - resolved "https://registry.yarnpkg.com/@datadog/sketches-js/-/sketches-js-2.1.1.tgz#9ec2251b3c932b4f43e1d164461fa6cb6f28b7d0" + resolved "https://registry.npmjs.org/@datadog/sketches-js/-/sketches-js-2.1.1.tgz" integrity sha512-d5RjycE+MObE/hU+8OM5Zp4VjTwiPLRa8299fj7muOmR16fb942z8byoMbCErnGh0lBevvgkGrLclQDvINbIyg== "@datadog/wasm-js-rewriter@4.0.1": @@ -1421,17 +1413,17 @@ "@jsep-plugin/assignment@^1.3.0": version "1.3.0" - resolved "https://registry.yarnpkg.com/@jsep-plugin/assignment/-/assignment-1.3.0.tgz#fcfc5417a04933f7ceee786e8ab498aa3ce2b242" + resolved "https://registry.npmjs.org/@jsep-plugin/assignment/-/assignment-1.3.0.tgz" integrity sha512-VVgV+CXrhbMI3aSusQyclHkenWSAm95WaiKrMxRFam3JSUiIaQjoMIw2sEs/OX4XifnqeQUN4DYbJjlA8EfktQ== "@jsep-plugin/regex@^1.0.4": version "1.0.4" - resolved "https://registry.yarnpkg.com/@jsep-plugin/regex/-/regex-1.0.4.tgz#cb2fc423220fa71c609323b9ba7f7d344a755fcc" + resolved "https://registry.npmjs.org/@jsep-plugin/regex/-/regex-1.0.4.tgz" integrity sha512-q7qL4Mgjs1vByCaTnDFcBnV9HS7GVPJX5vyVoCgZHNSC9rjwIlmbXG5sUuorR5ndfHAIlJ8pVStxvjXHbNvtUg== -"@opentelemetry/api@1.8.0": +"@opentelemetry/api@>=1.0.0 <1.10.0", "@opentelemetry/api@1.8.0": version "1.8.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/api/-/api-1.8.0.tgz#5aa7abb48f23f693068ed2999ae627d2f7d902ec" + resolved "https://registry.npmjs.org/@opentelemetry/api/-/api-1.8.0.tgz" integrity sha512-I/s6F7yKUDdtMsoBWXJe8Qz40Tui5vsuKCWJEWVL+5q9sSWRzzx6v2KeNsOBEwd94j0eWkpWCH4yB6rZg9Mf0w== "@opentelemetry/core@^1.14.0": @@ -1513,14 +1505,6 @@ dependencies: "@sinonjs/commons" "^1.7.0" -"@smithy/abort-controller@^3.1.6": - version "3.1.6" - resolved "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-3.1.6.tgz" - integrity sha512-0XuhuHQlEqbNQZp7QxxrFTdVWdwxch4vjxYgfInF91hZFkPxf9QDrdQka0KfxFMPqLNzSw0b95uGTrLliQUavQ== - dependencies: - "@smithy/types" "^3.6.0" - tslib "^2.6.2" - "@smithy/abort-controller@^3.1.9": version "3.1.9" resolved "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-3.1.9.tgz" @@ -1529,18 +1513,7 @@ "@smithy/types" "^3.7.2" tslib "^2.6.2" -"@smithy/config-resolver@^3.0.10", "@smithy/config-resolver@^3.0.9": - version "3.0.10" - resolved "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-3.0.10.tgz" - integrity sha512-Uh0Sz9gdUuz538nvkPiyv1DZRX9+D15EKDtnQP5rYVAzM/dnYk3P8cg73jcxyOitPgT3mE3OVj7ky7sibzHWkw== - dependencies: - "@smithy/node-config-provider" "^3.1.9" - "@smithy/types" "^3.6.0" - "@smithy/util-config-provider" "^3.0.0" - "@smithy/util-middleware" "^3.0.8" - tslib "^2.6.2" - -"@smithy/config-resolver@^3.0.13": +"@smithy/config-resolver@^3.0.13", "@smithy/config-resolver@^3.0.9": version "3.0.13" resolved "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-3.0.13.tgz" integrity sha512-Gr/qwzyPaTL1tZcq8WQyHhTZREER5R1Wytmz4WnVGL4onA3dNk6Btll55c8Vr58pLdvWZmtG8oZxJTw3t3q7Jg== @@ -1551,21 +1524,7 @@ "@smithy/util-middleware" "^3.0.11" tslib "^2.6.2" -"@smithy/core@^2.4.8", "@smithy/core@^2.5.1": - version "2.5.1" - resolved "https://registry.npmjs.org/@smithy/core/-/core-2.5.1.tgz" - integrity sha512-DujtuDA7BGEKExJ05W5OdxCoyekcKT3Rhg1ZGeiUWaz2BJIWXjZmsG/DIP4W48GHno7AQwRsaCb8NcBgH3QZpg== - dependencies: - "@smithy/middleware-serde" "^3.0.8" - "@smithy/protocol-http" "^4.1.5" - "@smithy/types" "^3.6.0" - "@smithy/util-body-length-browser" "^3.0.0" - "@smithy/util-middleware" "^3.0.8" - "@smithy/util-stream" "^3.2.1" - "@smithy/util-utf8" "^3.0.0" - tslib "^2.6.2" - -"@smithy/core@^2.5.5", "@smithy/core@^2.5.7": +"@smithy/core@^2.4.8", "@smithy/core@^2.5.5", "@smithy/core@^2.5.7": version "2.5.7" resolved "https://registry.npmjs.org/@smithy/core/-/core-2.5.7.tgz" integrity sha512-8olpW6mKCa0v+ibCjoCzgZHQx1SQmZuW/WkrdZo73wiTprTH6qhmskT60QLFdT9DRa5mXxjz89kQPZ7ZSsoqqg== @@ -1579,18 +1538,7 @@ "@smithy/util-utf8" "^3.0.0" tslib "^2.6.2" -"@smithy/credential-provider-imds@^3.2.4", "@smithy/credential-provider-imds@^3.2.5": - version "3.2.5" - resolved "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-3.2.5.tgz" - integrity sha512-4FTQGAsuwqTzVMmiRVTn0RR9GrbRfkP0wfu/tXWVHd2LgNpTY0uglQpIScXK4NaEyXbB3JmZt8gfVqO50lP8wg== - dependencies: - "@smithy/node-config-provider" "^3.1.9" - "@smithy/property-provider" "^3.1.8" - "@smithy/types" "^3.6.0" - "@smithy/url-parser" "^3.0.8" - tslib "^2.6.2" - -"@smithy/credential-provider-imds@^3.2.8": +"@smithy/credential-provider-imds@^3.2.4", "@smithy/credential-provider-imds@^3.2.8": version "3.2.8" resolved "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-3.2.8.tgz" integrity sha512-ZCY2yD0BY+K9iMXkkbnjo+08T2h8/34oHd0Jmh6BZUSZwaaGlGCyBT/3wnS7u7Xl33/EEfN4B6nQr3Gx5bYxgw== @@ -1612,18 +1560,18 @@ "@smithy/util-base64" "^3.0.0" tslib "^2.6.2" -"@smithy/fetch-http-handler@^4.0.0": - version "4.0.0" - resolved "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-4.0.0.tgz" - integrity sha512-MLb1f5tbBO2X6K4lMEKJvxeLooyg7guq48C2zKr4qM7F2Gpkz4dc+hdSgu77pCJ76jVqFBjZczHYAs6dp15N+g== +"@smithy/fetch-http-handler@^4.1.2": + version "4.1.3" + resolved "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-4.1.3.tgz" + integrity sha512-6SxNltSncI8s689nvnzZQc/dPXcpHQ34KUj6gR/HBroytKOd/isMG3gJF/zBE1TBmTT18TXyzhg3O3SOOqGEhA== dependencies: - "@smithy/protocol-http" "^4.1.5" - "@smithy/querystring-builder" "^3.0.8" - "@smithy/types" "^3.6.0" + "@smithy/protocol-http" "^4.1.8" + "@smithy/querystring-builder" "^3.0.11" + "@smithy/types" "^3.7.2" "@smithy/util-base64" "^3.0.0" tslib "^2.6.2" -"@smithy/fetch-http-handler@^4.1.2", "@smithy/fetch-http-handler@^4.1.3": +"@smithy/fetch-http-handler@^4.1.3": version "4.1.3" resolved "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-4.1.3.tgz" integrity sha512-6SxNltSncI8s689nvnzZQc/dPXcpHQ34KUj6gR/HBroytKOd/isMG3gJF/zBE1TBmTT18TXyzhg3O3SOOqGEhA== @@ -1634,7 +1582,7 @@ "@smithy/util-base64" "^3.0.0" tslib "^2.6.2" -"@smithy/hash-node@^3.0.11": +"@smithy/hash-node@^3.0.11", "@smithy/hash-node@^3.0.7": version "3.0.11" resolved "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-3.0.11.tgz" integrity sha512-emP23rwYyZhQBvklqTtwetkQlqbNYirDiEEwXl2v0GYWMnCzxst7ZaRAnWuy28njp5kAH54lvkdG37MblZzaHA== @@ -1644,17 +1592,7 @@ "@smithy/util-utf8" "^3.0.0" tslib "^2.6.2" -"@smithy/hash-node@^3.0.7": - version "3.0.8" - resolved "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-3.0.8.tgz" - integrity sha512-tlNQYbfpWXHimHqrvgo14DrMAgUBua/cNoz9fMYcDmYej7MAmUcjav/QKQbFc3NrcPxeJ7QClER4tWZmfwoPng== - dependencies: - "@smithy/types" "^3.6.0" - "@smithy/util-buffer-from" "^3.0.0" - "@smithy/util-utf8" "^3.0.0" - tslib "^2.6.2" - -"@smithy/invalid-dependency@^3.0.11": +"@smithy/invalid-dependency@^3.0.11", "@smithy/invalid-dependency@^3.0.7": version "3.0.11" resolved "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-3.0.11.tgz" integrity sha512-NuQmVPEJjUX6c+UELyVz8kUx8Q539EDeNwbRyu4IIF8MeV7hUtq1FB3SHVyki2u++5XLMFqngeMKk7ccspnNyQ== @@ -1662,14 +1600,6 @@ "@smithy/types" "^3.7.2" tslib "^2.6.2" -"@smithy/invalid-dependency@^3.0.7": - version "3.0.8" - resolved "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-3.0.8.tgz" - integrity sha512-7Qynk6NWtTQhnGTTZwks++nJhQ1O54Mzi7fz4PqZOiYXb4Z1Flpb2yRvdALoggTS8xjtohWUM+RygOtB30YL3Q== - dependencies: - "@smithy/types" "^3.6.0" - tslib "^2.6.2" - "@smithy/is-array-buffer@^2.2.0": version "2.2.0" resolved "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz" @@ -1684,7 +1614,7 @@ dependencies: tslib "^2.6.2" -"@smithy/middleware-content-length@^3.0.13": +"@smithy/middleware-content-length@^3.0.13", "@smithy/middleware-content-length@^3.0.9": version "3.0.13" resolved "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-3.0.13.tgz" integrity sha512-zfMhzojhFpIX3P5ug7jxTjfUcIPcGjcQYzB9t+rv0g1TX7B0QdwONW+ATouaLoD7h7LOw/ZlXfkq4xJ/g2TrIw== @@ -1693,30 +1623,7 @@ "@smithy/types" "^3.7.2" tslib "^2.6.2" -"@smithy/middleware-content-length@^3.0.9": - version "3.0.10" - resolved "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-3.0.10.tgz" - integrity sha512-T4dIdCs1d/+/qMpwhJ1DzOhxCZjZHbHazEPJWdB4GDi2HjIZllVzeBEcdJUN0fomV8DURsgOyrbEUzg3vzTaOg== - dependencies: - "@smithy/protocol-http" "^4.1.5" - "@smithy/types" "^3.6.0" - tslib "^2.6.2" - -"@smithy/middleware-endpoint@^3.1.4", "@smithy/middleware-endpoint@^3.2.1": - version "3.2.1" - resolved "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-3.2.1.tgz" - integrity sha512-wWO3xYmFm6WRW8VsEJ5oU6h7aosFXfszlz3Dj176pTij6o21oZnzkCLzShfmRaaCHDkBXWBdO0c4sQAvLFP6zA== - dependencies: - "@smithy/core" "^2.5.1" - "@smithy/middleware-serde" "^3.0.8" - "@smithy/node-config-provider" "^3.1.9" - "@smithy/shared-ini-file-loader" "^3.1.9" - "@smithy/types" "^3.6.0" - "@smithy/url-parser" "^3.0.8" - "@smithy/util-middleware" "^3.0.8" - tslib "^2.6.2" - -"@smithy/middleware-endpoint@^3.2.6", "@smithy/middleware-endpoint@^3.2.8": +"@smithy/middleware-endpoint@^3.1.4", "@smithy/middleware-endpoint@^3.2.6", "@smithy/middleware-endpoint@^3.2.8": version "3.2.8" resolved "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-3.2.8.tgz" integrity sha512-OEJZKVUEhMOqMs3ktrTWp7UvvluMJEvD5XgQwRePSbDg1VvBaL8pX8mwPltFn6wk1GySbcVwwyldL8S+iqnrEQ== @@ -1730,22 +1637,7 @@ "@smithy/util-middleware" "^3.0.11" tslib "^2.6.2" -"@smithy/middleware-retry@^3.0.23": - version "3.0.25" - resolved "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-3.0.25.tgz" - integrity sha512-m1F70cPaMBML4HiTgCw5I+jFNtjgz5z5UdGnUbG37vw6kh4UvizFYjqJGHvicfgKMkDL6mXwyPp5mhZg02g5sg== - dependencies: - "@smithy/node-config-provider" "^3.1.9" - "@smithy/protocol-http" "^4.1.5" - "@smithy/service-error-classification" "^3.0.8" - "@smithy/smithy-client" "^3.4.2" - "@smithy/types" "^3.6.0" - "@smithy/util-middleware" "^3.0.8" - "@smithy/util-retry" "^3.0.8" - tslib "^2.6.2" - uuid "^9.0.1" - -"@smithy/middleware-retry@^3.0.31": +"@smithy/middleware-retry@^3.0.23", "@smithy/middleware-retry@^3.0.31": version "3.0.34" resolved "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-3.0.34.tgz" integrity sha512-yVRr/AAtPZlUvwEkrq7S3x7Z8/xCd97m2hLDaqdz6ucP2RKHsBjEqaUA2ebNv2SsZoPEi+ZD0dZbOB1u37tGCA== @@ -1760,7 +1652,7 @@ tslib "^2.6.2" uuid "^9.0.1" -"@smithy/middleware-serde@^3.0.11": +"@smithy/middleware-serde@^3.0.11", "@smithy/middleware-serde@^3.0.7": version "3.0.11" resolved "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-3.0.11.tgz" integrity sha512-KzPAeySp/fOoQA82TpnwItvX8BBURecpx6ZMu75EZDkAcnPtO6vf7q4aH5QHs/F1s3/snQaSFbbUMcFFZ086Mw== @@ -1768,15 +1660,7 @@ "@smithy/types" "^3.7.2" tslib "^2.6.2" -"@smithy/middleware-serde@^3.0.7", "@smithy/middleware-serde@^3.0.8": - version "3.0.8" - resolved "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-3.0.8.tgz" - integrity sha512-Xg2jK9Wc/1g/MBMP/EUn2DLspN8LNt+GMe7cgF+Ty3vl+Zvu+VeZU5nmhveU+H8pxyTsjrAkci8NqY6OuvZnjA== - dependencies: - "@smithy/types" "^3.6.0" - tslib "^2.6.2" - -"@smithy/middleware-stack@^3.0.11": +"@smithy/middleware-stack@^3.0.11", "@smithy/middleware-stack@^3.0.7": version "3.0.11" resolved "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-3.0.11.tgz" integrity sha512-1HGo9a6/ikgOMrTrWL/WiN9N8GSVYpuRQO5kjstAq4CvV59bjqnh7TbdXGQ4vxLD3xlSjfBjq5t1SOELePsLnA== @@ -1784,15 +1668,7 @@ "@smithy/types" "^3.7.2" tslib "^2.6.2" -"@smithy/middleware-stack@^3.0.7", "@smithy/middleware-stack@^3.0.8": - version "3.0.8" - resolved "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-3.0.8.tgz" - integrity sha512-d7ZuwvYgp1+3682Nx0MD3D/HtkmZd49N3JUndYWQXfRZrYEnCWYc8BHcNmVsPAp9gKvlurdg/mubE6b/rPS9MA== - dependencies: - "@smithy/types" "^3.6.0" - tslib "^2.6.2" - -"@smithy/node-config-provider@^3.1.12": +"@smithy/node-config-provider@^3.1.12", "@smithy/node-config-provider@^3.1.8": version "3.1.12" resolved "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-3.1.12.tgz" integrity sha512-O9LVEu5J/u/FuNlZs+L7Ikn3lz7VB9hb0GtPT9MQeiBmtK8RSY3ULmsZgXhe6VAlgTw0YO+paQx4p8xdbs43vQ== @@ -1802,28 +1678,7 @@ "@smithy/types" "^3.7.2" tslib "^2.6.2" -"@smithy/node-config-provider@^3.1.8", "@smithy/node-config-provider@^3.1.9": - version "3.1.9" - resolved "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-3.1.9.tgz" - integrity sha512-qRHoah49QJ71eemjuS/WhUXB+mpNtwHRWQr77J/m40ewBVVwvo52kYAmb7iuaECgGTTcYxHS4Wmewfwy++ueew== - dependencies: - "@smithy/property-provider" "^3.1.8" - "@smithy/shared-ini-file-loader" "^3.1.9" - "@smithy/types" "^3.6.0" - tslib "^2.6.2" - -"@smithy/node-http-handler@^3.2.4", "@smithy/node-http-handler@^3.2.5": - version "3.2.5" - resolved "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-3.2.5.tgz" - integrity sha512-PkOwPNeKdvX/jCpn0A8n9/TyoxjGZB8WVoJmm9YzsnAgggTj4CrjpRHlTQw7dlLZ320n1mY1y+nTRUDViKi/3w== - dependencies: - "@smithy/abort-controller" "^3.1.6" - "@smithy/protocol-http" "^4.1.5" - "@smithy/querystring-builder" "^3.0.8" - "@smithy/types" "^3.6.0" - tslib "^2.6.2" - -"@smithy/node-http-handler@^3.3.2", "@smithy/node-http-handler@^3.3.3": +"@smithy/node-http-handler@^3.2.4", "@smithy/node-http-handler@^3.3.2", "@smithy/node-http-handler@^3.3.3": version "3.3.3" resolved "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-3.3.3.tgz" integrity sha512-BrpZOaZ4RCbcJ2igiSNG16S+kgAc65l/2hmxWdmhyoGWHTLlzQzr06PXavJp9OBlPEG/sHlqdxjWmjzV66+BSQ== @@ -1834,7 +1689,7 @@ "@smithy/types" "^3.7.2" tslib "^2.6.2" -"@smithy/property-provider@^3.1.11": +"@smithy/property-provider@^3.1.11", "@smithy/property-provider@^3.1.7": version "3.1.11" resolved "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-3.1.11.tgz" integrity sha512-I/+TMc4XTQ3QAjXfOcUWbSS073oOEAxgx4aZy8jHaf8JQnRkq2SZWw8+PfDtBvLUjcGMdxl+YwtzWe6i5uhL/A== @@ -1842,23 +1697,7 @@ "@smithy/types" "^3.7.2" tslib "^2.6.2" -"@smithy/property-provider@^3.1.7", "@smithy/property-provider@^3.1.8": - version "3.1.8" - resolved "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-3.1.8.tgz" - integrity sha512-ukNUyo6rHmusG64lmkjFeXemwYuKge1BJ8CtpVKmrxQxc6rhUX0vebcptFA9MmrGsnLhwnnqeH83VTU9hwOpjA== - dependencies: - "@smithy/types" "^3.6.0" - tslib "^2.6.2" - -"@smithy/protocol-http@^4.1.4", "@smithy/protocol-http@^4.1.5": - version "4.1.5" - resolved "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-4.1.5.tgz" - integrity sha512-hsjtwpIemmCkm3ZV5fd/T0bPIugW1gJXwZ/hpuVubt2hEUApIoUTrf6qIdh9MAWlw0vjMrA1ztJLAwtNaZogvg== - dependencies: - "@smithy/types" "^3.6.0" - tslib "^2.6.2" - -"@smithy/protocol-http@^4.1.8": +"@smithy/protocol-http@^4.1.4", "@smithy/protocol-http@^4.1.8": version "4.1.8" resolved "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-4.1.8.tgz" integrity sha512-hmgIAVyxw1LySOwkgMIUN0kjN8TG9Nc85LJeEmEE/cNEe2rkHDUWhnJf2gxcSRFLWsyqWsrZGw40ROjUogg+Iw== @@ -1866,7 +1705,7 @@ "@smithy/types" "^3.7.2" tslib "^2.6.2" -"@smithy/querystring-builder@^3.0.11": +"@smithy/querystring-builder@^3.0.11", "@smithy/querystring-builder@^3.0.7": version "3.0.11" resolved "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-3.0.11.tgz" integrity sha512-u+5HV/9uJaeLj5XTb6+IEF/dokWWkEqJ0XiaRRogyREmKGUgZnNecLucADLdauWFKUNbQfulHFEZEdjwEBjXRg== @@ -1875,15 +1714,6 @@ "@smithy/util-uri-escape" "^3.0.0" tslib "^2.6.2" -"@smithy/querystring-builder@^3.0.7", "@smithy/querystring-builder@^3.0.8": - version "3.0.8" - resolved "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-3.0.8.tgz" - integrity sha512-btYxGVqFUARbUrN6VhL9c3dnSviIwBYD9Rz1jHuN1hgh28Fpv2xjU1HeCeDJX68xctz7r4l1PBnFhGg1WBBPuA== - dependencies: - "@smithy/types" "^3.6.0" - "@smithy/util-uri-escape" "^3.0.0" - tslib "^2.6.2" - "@smithy/querystring-parser@^3.0.11": version "3.0.11" resolved "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-3.0.11.tgz" @@ -1892,14 +1722,6 @@ "@smithy/types" "^3.7.2" tslib "^2.6.2" -"@smithy/querystring-parser@^3.0.8": - version "3.0.8" - resolved "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-3.0.8.tgz" - integrity sha512-BtEk3FG7Ks64GAbt+JnKqwuobJNX8VmFLBsKIwWr1D60T426fGrV2L3YS5siOcUhhp6/Y6yhBw1PSPxA5p7qGg== - dependencies: - "@smithy/types" "^3.6.0" - tslib "^2.6.2" - "@smithy/service-error-classification@^3.0.11": version "3.0.11" resolved "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-3.0.11.tgz" @@ -1907,14 +1729,7 @@ dependencies: "@smithy/types" "^3.7.2" -"@smithy/service-error-classification@^3.0.8": - version "3.0.8" - resolved "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-3.0.8.tgz" - integrity sha512-uEC/kCCFto83bz5ZzapcrgGqHOh/0r69sZ2ZuHlgoD5kYgXJEThCoTuw/y1Ub3cE7aaKdznb+jD9xRPIfIwD7g== - dependencies: - "@smithy/types" "^3.6.0" - -"@smithy/shared-ini-file-loader@^3.1.12": +"@smithy/shared-ini-file-loader@^3.1.12", "@smithy/shared-ini-file-loader@^3.1.8": version "3.1.12" resolved "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-3.1.12.tgz" integrity sha512-1xKSGI+U9KKdbG2qDvIR9dGrw3CNx+baqJfyr0igKEpjbHL5stsqAesYBzHChYHlelWtb87VnLWlhvfCz13H8Q== @@ -1922,29 +1737,7 @@ "@smithy/types" "^3.7.2" tslib "^2.6.2" -"@smithy/shared-ini-file-loader@^3.1.8", "@smithy/shared-ini-file-loader@^3.1.9": - version "3.1.9" - resolved "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-3.1.9.tgz" - integrity sha512-/+OsJRNtoRbtsX0UpSgWVxFZLsJHo/4sTr+kBg/J78sr7iC+tHeOvOJrS5hCpVQ6sWBbhWLp1UNiuMyZhE6pmA== - dependencies: - "@smithy/types" "^3.6.0" - tslib "^2.6.2" - -"@smithy/signature-v4@^4.2.0": - version "4.2.1" - resolved "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-4.2.1.tgz" - integrity sha512-NsV1jF4EvmO5wqmaSzlnTVetemBS3FZHdyc5CExbDljcyJCEEkJr8ANu2JvtNbVg/9MvKAWV44kTrGS+Pi4INg== - dependencies: - "@smithy/is-array-buffer" "^3.0.0" - "@smithy/protocol-http" "^4.1.5" - "@smithy/types" "^3.6.0" - "@smithy/util-hex-encoding" "^3.0.0" - "@smithy/util-middleware" "^3.0.8" - "@smithy/util-uri-escape" "^3.0.0" - "@smithy/util-utf8" "^3.0.0" - tslib "^2.6.2" - -"@smithy/signature-v4@^4.2.4": +"@smithy/signature-v4@^4.2.0", "@smithy/signature-v4@^4.2.4": version "4.2.4" resolved "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-4.2.4.tgz" integrity sha512-5JWeMQYg81TgU4cG+OexAWdvDTs5JDdbEZx+Qr1iPbvo91QFGzjy0IkXAKaXUHqmKUJgSHK0ZxnCkgZpzkeNTA== @@ -1958,20 +1751,7 @@ "@smithy/util-utf8" "^3.0.0" tslib "^2.6.2" -"@smithy/smithy-client@^3.4.0", "@smithy/smithy-client@^3.4.2": - version "3.4.2" - resolved "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-3.4.2.tgz" - integrity sha512-dxw1BDxJiY9/zI3cBqfVrInij6ShjpV4fmGHesGZZUiP9OSE/EVfdwdRz0PgvkEvrZHpsj2htRaHJfftE8giBA== - dependencies: - "@smithy/core" "^2.5.1" - "@smithy/middleware-endpoint" "^3.2.1" - "@smithy/middleware-stack" "^3.0.8" - "@smithy/protocol-http" "^4.1.5" - "@smithy/types" "^3.6.0" - "@smithy/util-stream" "^3.2.1" - tslib "^2.6.2" - -"@smithy/smithy-client@^3.5.1", "@smithy/smithy-client@^3.7.0": +"@smithy/smithy-client@^3.4.0", "@smithy/smithy-client@^3.5.1", "@smithy/smithy-client@^3.7.0": version "3.7.0" resolved "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-3.7.0.tgz" integrity sha512-9wYrjAZFlqWhgVo3C4y/9kpc68jgiSsKUnsFPzr/MSiRL93+QRDafGTfhhKAb2wsr69Ru87WTiqSfQusSmWipA== @@ -1984,21 +1764,14 @@ "@smithy/util-stream" "^3.3.4" tslib "^2.6.2" -"@smithy/types@^3.5.0", "@smithy/types@^3.6.0": - version "3.6.0" - resolved "https://registry.npmjs.org/@smithy/types/-/types-3.6.0.tgz" - integrity sha512-8VXK/KzOHefoC65yRgCn5vG1cysPJjHnOVt9d0ybFQSmJgQj152vMn4EkYhGuaOmnnZvCPav/KnYyE6/KsNZ2w== - dependencies: - tslib "^2.6.2" - -"@smithy/types@^3.7.2": +"@smithy/types@^3.5.0", "@smithy/types@^3.7.2": version "3.7.2" resolved "https://registry.npmjs.org/@smithy/types/-/types-3.7.2.tgz" integrity sha512-bNwBYYmN8Eh9RyjS1p2gW6MIhSO2rl7X9QeLM8iTdcGRP+eDiIWDt66c9IysCc22gefKszZv+ubV9qZc7hdESg== dependencies: tslib "^2.6.2" -"@smithy/url-parser@^3.0.11": +"@smithy/url-parser@^3.0.11", "@smithy/url-parser@^3.0.7": version "3.0.11" resolved "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-3.0.11.tgz" integrity sha512-TmlqXkSk8ZPhfc+SQutjmFr5FjC0av3GZP4B/10caK1SbRwe/v+Wzu/R6xEKxoNqL+8nY18s1byiy6HqPG37Aw== @@ -2007,15 +1780,6 @@ "@smithy/types" "^3.7.2" tslib "^2.6.2" -"@smithy/url-parser@^3.0.7", "@smithy/url-parser@^3.0.8": - version "3.0.8" - resolved "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-3.0.8.tgz" - integrity sha512-4FdOhwpTW7jtSFWm7SpfLGKIBC9ZaTKG5nBF0wK24aoQKQyDIKUw3+KFWCQ9maMzrgTJIuOvOnsV2lLGW5XjTg== - dependencies: - "@smithy/querystring-parser" "^3.0.8" - "@smithy/types" "^3.6.0" - tslib "^2.6.2" - "@smithy/util-base64@^3.0.0": version "3.0.0" resolved "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-3.0.0.tgz" @@ -2062,18 +1826,7 @@ dependencies: tslib "^2.6.2" -"@smithy/util-defaults-mode-browser@^3.0.23": - version "3.0.25" - resolved "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-3.0.25.tgz" - integrity sha512-fRw7zymjIDt6XxIsLwfJfYUfbGoO9CmCJk6rjJ/X5cd20+d2Is7xjU5Kt/AiDt6hX8DAf5dztmfP5O82gR9emA== - dependencies: - "@smithy/property-provider" "^3.1.8" - "@smithy/smithy-client" "^3.4.2" - "@smithy/types" "^3.6.0" - bowser "^2.11.0" - tslib "^2.6.2" - -"@smithy/util-defaults-mode-browser@^3.0.31": +"@smithy/util-defaults-mode-browser@^3.0.23", "@smithy/util-defaults-mode-browser@^3.0.31": version "3.0.34" resolved "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-3.0.34.tgz" integrity sha512-FumjjF631lR521cX+svMLBj3SwSDh9VdtyynTYDAiBDEf8YPP5xORNXKQ9j0105o5+ARAGnOOP/RqSl40uXddA== @@ -2084,20 +1837,7 @@ bowser "^2.11.0" tslib "^2.6.2" -"@smithy/util-defaults-mode-node@^3.0.23": - version "3.0.25" - resolved "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-3.0.25.tgz" - integrity sha512-H3BSZdBDiVZGzt8TG51Pd2FvFO0PAx/A0mJ0EH8a13KJ6iUCdYnw/Dk/MdC1kTd0eUuUGisDFaxXVXo4HHFL1g== - dependencies: - "@smithy/config-resolver" "^3.0.10" - "@smithy/credential-provider-imds" "^3.2.5" - "@smithy/node-config-provider" "^3.1.9" - "@smithy/property-provider" "^3.1.8" - "@smithy/smithy-client" "^3.4.2" - "@smithy/types" "^3.6.0" - tslib "^2.6.2" - -"@smithy/util-defaults-mode-node@^3.0.31": +"@smithy/util-defaults-mode-node@^3.0.23", "@smithy/util-defaults-mode-node@^3.0.31": version "3.0.34" resolved "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-3.0.34.tgz" integrity sha512-vN6aHfzW9dVVzkI0wcZoUXvfjkl4CSbM9nE//08lmUMyf00S75uuCpTrqF9uD4bD9eldIXlt53colrlwKAT8Gw== @@ -2110,16 +1850,7 @@ "@smithy/types" "^3.7.2" tslib "^2.6.2" -"@smithy/util-endpoints@^2.1.3": - version "2.1.4" - resolved "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-2.1.4.tgz" - integrity sha512-kPt8j4emm7rdMWQyL0F89o92q10gvCUa6sBkBtDJ7nV2+P7wpXczzOfoDJ49CKXe5CCqb8dc1W+ZdLlrKzSAnQ== - dependencies: - "@smithy/node-config-provider" "^3.1.9" - "@smithy/types" "^3.6.0" - tslib "^2.6.2" - -"@smithy/util-endpoints@^2.1.7": +"@smithy/util-endpoints@^2.1.3", "@smithy/util-endpoints@^2.1.7": version "2.1.7" resolved "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-2.1.7.tgz" integrity sha512-tSfcqKcN/Oo2STEYCABVuKgJ76nyyr6skGl9t15hs+YaiU06sgMkN7QYjo0BbVw+KT26zok3IzbdSOksQ4YzVw== @@ -2135,7 +1866,7 @@ dependencies: tslib "^2.6.2" -"@smithy/util-middleware@^3.0.11": +"@smithy/util-middleware@^3.0.11", "@smithy/util-middleware@^3.0.7": version "3.0.11" resolved "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-3.0.11.tgz" integrity sha512-dWpyc1e1R6VoXrwLoLDd57U1z6CwNSdkM69Ie4+6uYh2GC7Vg51Qtan7ITzczuVpqezdDTKJGJB95fFvvjU/ow== @@ -2143,15 +1874,7 @@ "@smithy/types" "^3.7.2" tslib "^2.6.2" -"@smithy/util-middleware@^3.0.7", "@smithy/util-middleware@^3.0.8": - version "3.0.8" - resolved "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-3.0.8.tgz" - integrity sha512-p7iYAPaQjoeM+AKABpYWeDdtwQNxasr4aXQEA/OmbOaug9V0odRVDy3Wx4ci8soljE/JXQo+abV0qZpW8NX0yA== - dependencies: - "@smithy/types" "^3.6.0" - tslib "^2.6.2" - -"@smithy/util-retry@^3.0.11": +"@smithy/util-retry@^3.0.11", "@smithy/util-retry@^3.0.7": version "3.0.11" resolved "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-3.0.11.tgz" integrity sha512-hJUC6W7A3DQgaee3Hp9ZFcOxVDZzmBIRBPlUAk8/fSOEl7pE/aX7Dci0JycNOnm9Mfr0KV2XjIlUOcGWXQUdVQ== @@ -2160,30 +1883,7 @@ "@smithy/types" "^3.7.2" tslib "^2.6.2" -"@smithy/util-retry@^3.0.7", "@smithy/util-retry@^3.0.8": - version "3.0.8" - resolved "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-3.0.8.tgz" - integrity sha512-TCEhLnY581YJ+g1x0hapPz13JFqzmh/pMWL2KEFASC51qCfw3+Y47MrTmea4bUE5vsdxQ4F6/KFbUeSz22Q1ow== - dependencies: - "@smithy/service-error-classification" "^3.0.8" - "@smithy/types" "^3.6.0" - tslib "^2.6.2" - -"@smithy/util-stream@^3.1.9", "@smithy/util-stream@^3.2.1": - version "3.2.1" - resolved "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-3.2.1.tgz" - integrity sha512-R3ufuzJRxSJbE58K9AEnL/uSZyVdHzud9wLS8tIbXclxKzoe09CRohj2xV8wpx5tj7ZbiJaKYcutMm1eYgz/0A== - dependencies: - "@smithy/fetch-http-handler" "^4.0.0" - "@smithy/node-http-handler" "^3.2.5" - "@smithy/types" "^3.6.0" - "@smithy/util-base64" "^3.0.0" - "@smithy/util-buffer-from" "^3.0.0" - "@smithy/util-hex-encoding" "^3.0.0" - "@smithy/util-utf8" "^3.0.0" - tslib "^2.6.2" - -"@smithy/util-stream@^3.3.2", "@smithy/util-stream@^3.3.4": +"@smithy/util-stream@^3.1.9", "@smithy/util-stream@^3.3.2", "@smithy/util-stream@^3.3.4": version "3.3.4" resolved "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-3.3.4.tgz" integrity sha512-SGhGBG/KupieJvJSZp/rfHHka8BFgj56eek9px4pp7lZbOF+fRiVr4U7A3y3zJD8uGhxq32C5D96HxsTC9BckQ== @@ -2296,7 +1996,7 @@ dependencies: "@types/istanbul-lib-report" "*" -"@types/jest@^27.5.2": +"@types/jest@^27.0.0", "@types/jest@^27.5.2": version "27.5.2" resolved "https://registry.npmjs.org/@types/jest/-/jest-27.5.2.tgz" integrity sha512-mpT8LJJ4CMeeahobofYWIjFo0xonRS/HfxnVEPMPFSQdGUt1uHCnoPT7Zhb+sjDU2wz0oKV0OLUR0WzrHNgfeA== @@ -2311,14 +2011,7 @@ dependencies: "@types/node" "*" -"@types/node@*": - version "22.7.9" - resolved "https://registry.npmjs.org/@types/node/-/node-22.7.9.tgz" - integrity sha512-jrTfRC7FM6nChvU7X2KqcrgquofrWLFDeYC1hKfwNWomVvrn7JIksqf344WN2X/y8xrgqBd2dJATZV4GbatBfg== - dependencies: - undici-types "~6.19.2" - -"@types/node@>=13.7.0", "@types/node@^20.12.10": +"@types/node@*", "@types/node@^20.12.10", "@types/node@>=13.7.0": version "20.17.0" resolved "https://registry.npmjs.org/@types/node/-/node-20.17.0.tgz" integrity sha512-a7zRo0f0eLo9K5X9Wp5cAqTUNGzuFLDG2R7C4HY2BhcMAsxgSPuRvAC1ZB6QkuUQXf0YZAgfOX2ZyrBa2n4nHQ== @@ -2397,7 +2090,7 @@ acorn@^7.1.1: resolved "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz" integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== -acorn@^8.14.0, acorn@^8.2.4: +acorn@^8, acorn@^8.14.0, acorn@^8.2.4: version "8.15.0" resolved "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz" integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg== @@ -2488,7 +2181,7 @@ aws-sdk@*: uuid "8.0.0" xml2js "0.6.2" -babel-jest@^27.5.1: +babel-jest@^27.5.1, "babel-jest@>=27.0.0 <28": version "27.5.1" resolved "https://registry.npmjs.org/babel-jest/-/babel-jest-27.5.1.tgz" integrity sha512-cdQ5dXjGRd0IBRATiQ4mZGlGlRE8kJpjPOixdNRdT+m3UcNqmYWN6rK6nvtXYfY3D76cb8s/O1Ss8ea24PIwcg== @@ -2587,7 +2280,7 @@ browser-process-hrtime@^1.0.0: resolved "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz" integrity sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow== -browserslist@^4.24.0: +browserslist@^4.24.0, "browserslist@>= 4.21.0": version "4.24.2" resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.24.2.tgz" integrity sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg== @@ -2632,7 +2325,7 @@ builtin-modules@^1.1.1: call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: version "1.0.2" - resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6" + resolved "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz" integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ== dependencies: es-errors "^1.3.0" @@ -2669,7 +2362,16 @@ caniuse-lite@^1.0.30001669: resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001669.tgz" integrity sha512-DlWzFDJqstqtIVx1zeSpIMLjunf5SmwOw0N2Ck/QSQdS8PLS4+9HrLaYei4w8BIAL7IB/UEDu889d8vhCTPA0w== -chalk@^2.3.0, chalk@^2.4.2: +chalk@^2.3.0: + version "2.4.2" + resolved "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +chalk@^2.4.2: version "2.4.2" resolved "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz" integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== @@ -2734,16 +2436,16 @@ color-convert@^2.0.1: dependencies: color-name "~1.1.4" -color-name@1.1.3: - version "1.1.3" - resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz" - integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== - color-name@~1.1.4: version "1.1.4" resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz" + integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== + combined-stream@^1.0.8: version "1.0.8" resolved "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz" @@ -2818,7 +2520,7 @@ dc-polyfill@^0.1.3, dc-polyfill@^0.1.9: dd-trace@^5.58.0: version "5.58.0" - resolved "https://registry.yarnpkg.com/dd-trace/-/dd-trace-5.58.0.tgz#fa5cd37fd808b3844485e870eecb462cce36fd2c" + resolved "https://registry.npmjs.org/dd-trace/-/dd-trace-5.58.0.tgz" integrity sha512-DiogPbR2Sv96jBOBTfvoyLqGZoGYERH9d5Ym1oHgJIleWzwXxlAO7g43MAmx0DgdglLsorg2OMZNvlmgHXrcjw== dependencies: "@datadog/libdatadog" "0.7.0" @@ -2856,7 +2558,7 @@ dd-trace@^5.58.0: tlhunter-sorted-set "^0.1.0" ttl-set "^1.0.0" -debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1: +debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@4: version "4.3.7" resolved "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz" integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ== @@ -2921,7 +2623,7 @@ domexception@^2.0.1: dunder-proto@^1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a" + resolved "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz" integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A== dependencies: call-bind-apply-helpers "^1.0.1" @@ -2955,16 +2657,9 @@ error-ex@^1.3.1: dependencies: is-arrayish "^0.2.1" -es-define-property@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz" - integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== - dependencies: - get-intrinsic "^1.2.4" - -es-define-property@^1.0.1: +es-define-property@^1.0.0, es-define-property@^1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa" + resolved "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz" integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== es-errors@^1.3.0: @@ -2974,14 +2669,14 @@ es-errors@^1.3.0: es-object-atoms@^1.0.0, es-object-atoms@^1.1.1: version "1.1.1" - resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1" + resolved "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz" integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA== dependencies: es-errors "^1.3.0" es-set-tostringtag@^2.1.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz#f31dbbe0c183b00a6d26eb6325c810c0fd18bd4d" + resolved "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz" integrity sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA== dependencies: es-errors "^1.3.0" @@ -3070,7 +2765,7 @@ fast-fifo@^1.3.2: resolved "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz" integrity sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ== -fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0: +fast-json-stable-stringify@^2.0.0, fast-json-stable-stringify@2.x: version "2.1.0" resolved "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== @@ -3113,7 +2808,7 @@ for-each@^0.3.3: form-data@^3.0.0: version "3.0.4" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.4.tgz#938273171d3f999286a4557528ce022dc2c98df1" + resolved "https://registry.npmjs.org/form-data/-/form-data-3.0.4.tgz" integrity sha512-f0cRzm6dkyVYV3nPoooP8XlccPQukegwhAnpoLcXy+X+A8KfpGOoXwDr9FLZd3wzgLaBGQBE3lY93Zm/i1JvIQ== dependencies: asynckit "^0.4.0" @@ -3127,11 +2822,6 @@ fs.realpath@^1.0.0: resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== -fsevents@^2.3.2: - version "2.3.3" - resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz" - integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== - function-bind@^1.1.2: version "1.1.2" resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz" @@ -3147,20 +2837,9 @@ get-caller-file@^2.0.5: resolved "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== -get-intrinsic@^1.1.3, get-intrinsic@^1.2.4: - version "1.2.4" - resolved "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz" - integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== - dependencies: - es-errors "^1.3.0" - function-bind "^1.1.2" - has-proto "^1.0.1" - has-symbols "^1.0.3" - hasown "^2.0.0" - -get-intrinsic@^1.2.6: +get-intrinsic@^1.2.4, get-intrinsic@^1.2.6: version "1.3.0" - resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01" + resolved "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz" integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== dependencies: call-bind-apply-helpers "^1.0.2" @@ -3181,7 +2860,7 @@ get-package-type@^0.1.0: get-proto@^1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1" + resolved "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz" integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g== dependencies: dunder-proto "^1.0.1" @@ -3209,16 +2888,9 @@ globals@^11.1.0: resolved "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz" integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== -gopd@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz" - integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== - dependencies: - get-intrinsic "^1.1.3" - -gopd@^1.2.0: +gopd@^1.0.1, gopd@^1.2.0: version "1.2.0" - resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" + resolved "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz" integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== graceful-fs@^4.2.9: @@ -3243,19 +2915,9 @@ has-property-descriptors@^1.0.2: dependencies: es-define-property "^1.0.0" -has-proto@^1.0.1: - version "1.0.3" - resolved "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz" - integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q== - -has-symbols@^1.0.3: - version "1.0.3" - resolved "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz" - integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== - -has-symbols@^1.1.0: +has-symbols@^1.0.3, has-symbols@^1.1.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338" + resolved "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz" integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== has-tostringtag@^1.0.0, has-tostringtag@^1.0.2: @@ -3265,7 +2927,7 @@ has-tostringtag@^1.0.0, has-tostringtag@^1.0.2: dependencies: has-symbols "^1.0.3" -hasown@^2.0.0, hasown@^2.0.2: +hasown@^2.0.2: version "2.0.2" resolved "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz" integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== @@ -3313,16 +2975,11 @@ iconv-lite@0.4.24: dependencies: safer-buffer ">= 2.1.2 < 3" -ieee754@1.1.13: +ieee754@^1.1.4, ieee754@1.1.13: version "1.1.13" resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz" integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg== -ieee754@^1.1.4: - version "1.2.1" - resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz" - integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== - ignore@^5.2.4: version "5.3.2" resolved "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz" @@ -3330,7 +2987,7 @@ ignore@^5.2.4: import-in-the-middle@^1.14.2: version "1.14.2" - resolved "https://registry.yarnpkg.com/import-in-the-middle/-/import-in-the-middle-1.14.2.tgz#283661625a88ff7c0462bd2984f77715c3bc967c" + resolved "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-1.14.2.tgz" integrity sha512-5tCuY9BV8ujfOpwtAGgsTx9CGUapcFMEEyByLv1B+v2+6DhAcw+Zr0nhQT7uwaZ7DiourxFEscghOR8e1aPLQw== dependencies: acorn "^8.14.0" @@ -3359,7 +3016,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@^2.0.3: +inherits@^2.0.3, inherits@2: version "2.0.4" resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -3445,7 +3102,7 @@ isexe@^2.0.0: istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.0, istanbul-lib-coverage@^3.2.2: version "3.2.2" - resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz#2d166c4b0644d43a39f04bf6c2edd1e585f31756" + resolved "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz" integrity sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg== istanbul-lib-instrument@^5.0.4, istanbul-lib-instrument@^5.1.0: @@ -3735,7 +3392,7 @@ jest-resolve-dependencies@^27.5.1: jest-regex-util "^27.5.1" jest-snapshot "^27.5.1" -jest-resolve@^27.5.1: +jest-resolve@*, jest-resolve@^27.5.1: version "27.5.1" resolved "https://registry.npmjs.org/jest-resolve/-/jest-resolve-27.5.1.tgz" integrity sha512-FFDy8/9E6CV83IMbDpcjOhumAQPDyETnU2KZ1O98DwTnz8AOBsW/Xv3GySr1mOZdItLR+zDZ7I/UdTFbgSOVCw== @@ -3888,7 +3545,7 @@ jest-worker@^27.5.1: merge-stream "^2.0.0" supports-color "^8.0.0" -jest@^27.0.1: +jest@^27.0.0, jest@^27.0.1: version "27.5.1" resolved "https://registry.npmjs.org/jest/-/jest-27.5.1.tgz" integrity sha512-Yn0mADZB89zTtjkPJEXwrac3LHudkQMR+Paqa8uxJHCBr9agxztUifWCyiYrjhMPBoUVBjyny0I7XH6ozDr7QQ== @@ -3955,9 +3612,9 @@ jsdom@^16.6.0: ws "^7.4.6" xml-name-validator "^3.0.0" -jsep@^1.4.0: +jsep@^0.4.0||^1.0.0, jsep@^1.4.0: version "1.4.0" - resolved "https://registry.yarnpkg.com/jsep/-/jsep-1.4.0.tgz#19feccbfa51d8a79f72480b4b8e40ce2e17152f0" + resolved "https://registry.npmjs.org/jsep/-/jsep-1.4.0.tgz" integrity sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw== jsesc@^3.0.2: @@ -3975,14 +3632,14 @@ json-stringify-safe@^5.0.1: resolved "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz" integrity sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA== -json5@2.x, json5@^2.2.3: +json5@^2.2.3, json5@2.x: version "2.2.3" resolved "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== jsonpath-plus@^10.3.0: version "10.3.0" - resolved "https://registry.yarnpkg.com/jsonpath-plus/-/jsonpath-plus-10.3.0.tgz#59e22e4fa2298c68dfcd70659bb47f0cad525238" + resolved "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-10.3.0.tgz" integrity sha512-8TNmfeTCk2Le33A3vRRwtuworG/L5RrgMvdjhKZxvyShO+mBu2fP50OWUjRLNtvw344DdDarFh9buFAZs5ujeA== dependencies: "@jsep-plugin/assignment" "^1.3.0" @@ -4006,7 +3663,7 @@ leven@^3.1.0: limiter@^1.1.5: version "1.1.5" - resolved "https://registry.yarnpkg.com/limiter/-/limiter-1.1.5.tgz#8f92a25b3b16c6131293a0cc834b4a838a2aa7c2" + resolved "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz" integrity sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA== lines-and-columns@^1.1.6: @@ -4074,7 +3731,7 @@ makeerror@1.0.12: math-intrinsics@^1.1.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9" + resolved "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz" integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== merge-stream@^2.0.0: @@ -4097,7 +3754,7 @@ mime-db@1.52.0: mime-types@^2.1.35: version "2.1.35" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz" integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== dependencies: mime-db "1.52.0" @@ -4167,7 +3824,7 @@ node-addon-api@^6.1.0: resolved "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz" integrity sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA== -node-gyp-build@<4.0, node-gyp-build@^3.9.0: +node-gyp-build@^3.9.0, node-gyp-build@<4.0: version "3.9.0" resolved "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-3.9.0.tgz" integrity sha512-zLcTg6P4AbcHPq465ZMFNXx7XpKKJh+7kkN699NiQWisR2uWYOWNWqRHAmbnmKiL4e9aLSlmy5U7rEMUXV59+A== @@ -4379,16 +4036,16 @@ psl@^1.1.33: resolved "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz" integrity sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag== -punycode@1.3.2: - version "1.3.2" - resolved "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz" - integrity sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw== - punycode@^2.1.1: version "2.3.1" resolved "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz" integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== +punycode@1.3.2: + version "1.3.2" + resolved "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz" + integrity sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw== + querystring@0.2.0: version "0.2.0" resolved "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz" @@ -4472,16 +4129,11 @@ rimraf@^3.0.0: resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -sax@1.2.1: +sax@>=0.6.0, sax@1.2.1: version "1.2.1" resolved "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz" integrity sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA== -sax@>=0.6.0: - version "1.4.1" - resolved "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz" - integrity sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg== - saxes@^5.0.1: version "5.0.1" resolved "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz" @@ -4494,11 +4146,6 @@ semifies@^1.0.0: resolved "https://registry.npmjs.org/semifies/-/semifies-1.0.0.tgz" integrity sha512-xXR3KGeoxTNWPD4aBvL5NUpMTT7WMANr3EWnaS190QVkY52lqqcVRD7Q05UVbBhiWDGWMlJEUam9m7uFFGVScw== -semver@7.x, semver@^7.3.2, semver@^7.5.3: - version "7.6.3" - resolved "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz" - integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== - semver@^5.3.0: version "5.7.2" resolved "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz" @@ -4509,6 +4156,21 @@ semver@^6.3.0, semver@^6.3.1: resolved "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== +semver@^7.3.2: + version "7.6.3" + resolved "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz" + integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== + +semver@^7.5.3: + version "7.6.3" + resolved "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz" + integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== + +semver@7.x: + version "7.6.3" + resolved "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz" + integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== + serialize-error@^8.1.0: version "8.1.0" resolved "https://registry.npmjs.org/serialize-error/-/serialize-error-8.1.0.tgz" @@ -4573,7 +4235,12 @@ source-map-support@^0.5.6: buffer-from "^1.0.0" source-map "^0.6.0" -source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1: +source-map@^0.6.0: + version "0.6.1" + resolved "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +source-map@^0.6.1: version "0.6.1" resolved "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== @@ -4583,6 +4250,11 @@ source-map@^0.7.3, source-map@^0.7.4: resolved "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz" integrity sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA== +source-map@~0.6.1: + version "0.6.1" + resolved "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz" @@ -4748,7 +4420,12 @@ ts-jest@^27.0.1: semver "7.x" yargs-parser "20.x" -tslib@^1.13.0, tslib@^1.8.1: +tslib@^1.13.0: + version "1.14.1" + resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz" + integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== + +tslib@^1.8.1: version "1.14.1" resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== @@ -4813,7 +4490,7 @@ typedarray-to-buffer@^3.1.5: dependencies: is-typedarray "^1.0.0" -typescript@^4.3.2: +typescript@^4.3.2, "typescript@>=2.1.0 || >=2.1.0-dev || >=2.2.0-dev || >=2.3.0-dev || >=2.4.0-dev || >=2.5.0-dev || >=2.6.0-dev || >=2.7.0-dev || >=2.8.0-dev || >=2.9.0-dev || >= 3.0.0-dev || >= 3.1.0-dev", "typescript@>=2.3.0-dev || >=2.4.0-dev || >=2.5.0-dev || >=2.6.0-dev || >=2.7.0-dev || >=2.8.0-dev || >=2.9.0-dev || >=3.0.0-dev || >= 3.1.0-dev || >= 3.2.0-dev || >= 4.0.0-dev", "typescript@>=3.8 <5.0": version "4.9.5" resolved "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz" integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== @@ -4863,16 +4540,16 @@ util@^0.12.4: is-typed-array "^1.1.3" which-typed-array "^1.1.2" -uuid@8.0.0: - version "8.0.0" - resolved "https://registry.npmjs.org/uuid/-/uuid-8.0.0.tgz" - integrity sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw== - uuid@^9.0.1: version "9.0.1" resolved "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz" integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== +uuid@8.0.0: + version "8.0.0" + resolved "https://registry.npmjs.org/uuid/-/uuid-8.0.0.tgz" + integrity sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw== + v8-to-istanbul@^8.1.0: version "8.1.1" resolved "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-8.1.1.tgz" @@ -5014,7 +4691,7 @@ yallist@^3.0.2: resolved "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz" integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== -yargs-parser@20.x, yargs-parser@^20.2.2: +yargs-parser@^20.2.2, yargs-parser@20.x: version "20.2.9" resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz" integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== From 33419eaaaaff40137ff99cf9228f4b3a166bd253 Mon Sep 17 00:00:00 2001 From: "james.eastham" Date: Thu, 4 Sep 2025 13:32:31 +0100 Subject: [PATCH 2/4] feat: update extractors to handle span links --- src/trace/context/extractors/app-sync.spec.ts | 14 +- src/trace/context/extractors/app-sync.ts | 2 +- .../extractors/event-bridge-sqs.spec.ts | 162 +++++++++++-- .../context/extractors/event-bridge-sqs.ts | 27 ++- .../context/extractors/event-bridge.spec.ts | 26 +- src/trace/context/extractors/event-bridge.ts | 4 +- src/trace/context/extractors/http.spec.ts | 36 +-- src/trace/context/extractors/http.ts | 10 +- src/trace/context/extractors/kinesis.spec.ts | 146 +++++++++++- src/trace/context/extractors/kinesis.ts | 56 +++-- .../context/extractors/lambda-context.spec.ts | 16 +- .../context/extractors/lambda-context.ts | 12 +- src/trace/context/extractors/sns-sqs.spec.ts | 180 +++++++++++--- src/trace/context/extractors/sns-sqs.ts | 73 +++--- src/trace/context/extractors/sns.spec.ts | 224 +++++++++++++++--- src/trace/context/extractors/sns.ts | 53 +++-- src/trace/context/extractors/sqs.spec.ts | 203 ++++++++++++++-- src/trace/context/extractors/sqs.ts | 61 +++-- .../context/extractors/step-function.spec.ts | 50 ++-- src/trace/context/extractors/step-function.ts | 6 +- 20 files changed, 1056 insertions(+), 305 deletions(-) diff --git a/src/trace/context/extractors/app-sync.spec.ts b/src/trace/context/extractors/app-sync.spec.ts index a1c217d9..c841109a 100644 --- a/src/trace/context/extractors/app-sync.spec.ts +++ b/src/trace/context/extractors/app-sync.spec.ts @@ -52,7 +52,7 @@ describe("AppSyncEventTraceExtractor", () => { const extractor = new AppSyncEventTraceExtractor(tracerWrapper); const traceContext = extractor.extract(payload); - expect(traceContext).not.toBeNull(); + expect(traceContext.length).toBe(1); expect(spyTracerWrapper).toHaveBeenCalledWith({ "x-datadog-parent-id": "797643193680388254", @@ -60,13 +60,13 @@ describe("AppSyncEventTraceExtractor", () => { "x-datadog-trace-id": "4110911582297405557", }); - expect(traceContext?.toTraceId()).toBe("797643193680388254"); - expect(traceContext?.toSpanId()).toBe("4110911582297405557"); - expect(traceContext?.sampleMode()).toBe("2"); - expect(traceContext?.source).toBe("event"); + expect(traceContext?.[0].toTraceId()).toBe("797643193680388254"); + expect(traceContext?.[0].toSpanId()).toBe("4110911582297405557"); + expect(traceContext?.[0].sampleMode()).toBe("2"); + expect(traceContext?.[0].source).toBe("event"); }); - it("returns null when extracted span context by tracer is null", () => { + it("returns an empty array when extracted span context by tracer is null", () => { const tracerWrapper = new TracerWrapper(); const payload = { @@ -83,7 +83,7 @@ describe("AppSyncEventTraceExtractor", () => { const extractor = new AppSyncEventTraceExtractor(tracerWrapper); const traceContext = extractor.extract(payload); - expect(traceContext).toBeNull(); + expect(traceContext).toStrictEqual([]); }); }); }); diff --git a/src/trace/context/extractors/app-sync.ts b/src/trace/context/extractors/app-sync.ts index 1cce5a22..c3d77673 100644 --- a/src/trace/context/extractors/app-sync.ts +++ b/src/trace/context/extractors/app-sync.ts @@ -10,7 +10,7 @@ export class AppSyncEventTraceExtractor implements EventTraceExtractor { this.httpEventExtractor = new HTTPEventTraceExtractor(this.tracerWrapper, false); } - extract(event: any): SpanContextWrapper | null { + extract(event: any): SpanContextWrapper[] { event.headers = event.request.headers; return this.httpEventExtractor.extract(event); diff --git a/src/trace/context/extractors/event-bridge-sqs.spec.ts b/src/trace/context/extractors/event-bridge-sqs.spec.ts index 24ca16fa..31a691d3 100644 --- a/src/trace/context/extractors/event-bridge-sqs.spec.ts +++ b/src/trace/context/extractors/event-bridge-sqs.spec.ts @@ -1,6 +1,7 @@ import { TracerWrapper } from "../../tracer-wrapper"; import { EventBridgeSQSEventTraceExtractor } from "./event-bridge-sqs"; import { StepFunctionContextService } from "../../step-function-service"; +import { TraceConfig } from "../../listener"; let mockSpanContext: any = null; @@ -60,7 +61,7 @@ describe("EventBridgeSQSEventTraceExtractor", () => { ], }; - const extractor = new EventBridgeSQSEventTraceExtractor(tracerWrapper); + const extractor = new EventBridgeSQSEventTraceExtractor(tracerWrapper, {} as TraceConfig); const traceContext = extractor.extract(payload); expect(traceContext).not.toBeNull(); @@ -72,10 +73,139 @@ describe("EventBridgeSQSEventTraceExtractor", () => { "x-datadog-trace-id": "7379586022458917877", }); - expect(traceContext?.toTraceId()).toBe("7379586022458917877"); - expect(traceContext?.toSpanId()).toBe("2644033662113726488"); - expect(traceContext?.sampleMode()).toBe("1"); - expect(traceContext?.source).toBe("event"); + expect(traceContext?.[0].toTraceId()).toBe("7379586022458917877"); + expect(traceContext?.[0].toSpanId()).toBe("2644033662113726488"); + expect(traceContext?.[0].sampleMode()).toBe("1"); + expect(traceContext?.[0].source).toBe("event"); + }); + + it("only extracts first trace context with multiple input payloads", () => { + mockSpanContext = { + toTraceId: () => "7379586022458917877", + toSpanId: () => "2644033662113726488", + _sampling: { + priority: "1", + }, + }; + const tracerWrapper = new TracerWrapper(); + + const payload = { + Records: [ + { + messageId: "e995e54f-1724-41fa-82c0-8b81821f854e", + receiptHandle: + "AQEB4mIfRcyqtzn1X5Ss+ConhTejVGc+qnAcmu3/Z9ZvbNkaPcpuDLX/bzvPD/ZkAXJUXZcemGSJmd7L3snZHKMP2Ck8runZiyl4mubiLb444pZvdiNPuGRJ6a3FvgS/GQPzho/9nNMyOi66m8Viwh70v4EUCPGO4JmD3TTDAUrrcAnqU4WSObjfC/NAp9bI6wH2CEyAYEfex6Nxplbl/jBf9ZUG0I3m3vQd0Q4l4gd4jIR4oxQUglU2Tldl4Kx5fMUAhTRLAENri6HsY81avBkKd9FAuxONlsITB5uj02kOkvLlRGEcalqsKyPJ7AFaDLrOLaL3U+yReroPEJ5R5nwhLOEbeN5HROlZRXeaAwZOIN8BjqdeooYTIOrtvMEVb7a6OPLMdH1XB+ddevtKAH8K9Tm2ZjpaA7dtBGh1zFVHzBk=", + body: '{"version":"0","id":"af718b2a-b987-e8c0-7a2b-a188fad2661a","detail-type":"my.Detail","source":"my.Source","account":"123456123456","time":"2023-08-03T22:49:03Z","region":"us-east-1","resources":[],"detail":{"text":"Hello, world!","_datadog":{"x-datadog-trace-id":"7379586022458917877","x-datadog-parent-id":"2644033662113726488","x-datadog-sampling-priority":"1","x-datadog-tags":"_dd.p.dm=-0"}}}', + attributes: { + ApproximateReceiveCount: "1", + AWSTraceHeader: "Root=1-64cc2edd-112fbf1701d1355973a11d57;Parent=7d5a9776024b2d42;Sampled=0", + SentTimestamp: "1691102943638", + SenderId: "AIDAJXNJGGKNS7OSV23OI", + ApproximateFirstReceiveTimestamp: "1691102943647", + }, + messageAttributes: {}, + md5OfBody: "93d9f0cd8886d1e000a1a0b7007bffc4", + eventSource: "aws:sqs", + eventSourceARN: "arn:aws:sqs:us-east-1:425362996713:lambda-eb-sqs-lambda-dev-demo-queue", + awsRegion: "us-east-1", + }, + { + messageId: "e995e54f-1724-41fa-82c0-8b81821f854e", + receiptHandle: + "AQEB4mIfRcyqtzn1X5Ss+ConhTejVGc+qnAcmu3/Z9ZvbNkaPcpuDLX/bzvPD/ZkAXJUXZcemGSJmd7L3snZHKMP2Ck8runZiyl4mubiLb444pZvdiNPuGRJ6a3FvgS/GQPzho/9nNMyOi66m8Viwh70v4EUCPGO4JmD3TTDAUrrcAnqU4WSObjfC/NAp9bI6wH2CEyAYEfex6Nxplbl/jBf9ZUG0I3m3vQd0Q4l4gd4jIR4oxQUglU2Tldl4Kx5fMUAhTRLAENri6HsY81avBkKd9FAuxONlsITB5uj02kOkvLlRGEcalqsKyPJ7AFaDLrOLaL3U+yReroPEJ5R5nwhLOEbeN5HROlZRXeaAwZOIN8BjqdeooYTIOrtvMEVb7a6OPLMdH1XB+ddevtKAH8K9Tm2ZjpaA7dtBGh1zFVHzBk=", + body: '{"version":"0","id":"af718b2a-b987-e8c0-7a2b-a188fad2661a","detail-type":"my.Detail","source":"my.Source","account":"123456123456","time":"2023-08-03T22:49:03Z","region":"us-east-1","resources":[],"detail":{"text":"Hello, world!","_datadog":{"x-datadog-trace-id":"7379586022458917877","x-datadog-parent-id":"2644033662113726488","x-datadog-sampling-priority":"1","x-datadog-tags":"_dd.p.dm=-0"}}}', + attributes: { + ApproximateReceiveCount: "1", + AWSTraceHeader: "Root=1-64cc2edd-112fbf1701d1355973a11d57;Parent=7d5a9776024b2d42;Sampled=0", + SentTimestamp: "1691102943638", + SenderId: "AIDAJXNJGGKNS7OSV23OI", + ApproximateFirstReceiveTimestamp: "1691102943647", + }, + messageAttributes: {}, + md5OfBody: "93d9f0cd8886d1e000a1a0b7007bffc4", + eventSource: "aws:sqs", + eventSourceARN: "arn:aws:sqs:us-east-1:425362996713:lambda-eb-sqs-lambda-dev-demo-queue", + awsRegion: "us-east-1", + }, + ], + }; + + const extractor = new EventBridgeSQSEventTraceExtractor(tracerWrapper, {} as TraceConfig); + + const traceContext = extractor.extract(payload); + expect(traceContext).not.toBeNull(); + + expect(spyTracerWrapper).toHaveBeenCalledWith({ + "x-datadog-parent-id": "2644033662113726488", + "x-datadog-sampling-priority": "1", + "x-datadog-tags": "_dd.p.dm=-0", + "x-datadog-trace-id": "7379586022458917877", + }); + + expect(traceContext?.length).toBe(1); + expect(traceContext?.[0].toTraceId()).toBe("7379586022458917877"); + expect(traceContext?.[0].toSpanId()).toBe("2644033662113726488"); + expect(traceContext?.[0].sampleMode()).toBe("1"); + expect(traceContext?.[0].source).toBe("event"); + }); + + it("extract all trace contexts when span links is enabled", () => { + mockSpanContext = { + toTraceId: () => "7379586022458917877", + toSpanId: () => "2644033662113726488", + _sampling: { + priority: "1", + }, + }; + const tracerWrapper = new TracerWrapper(); + + const payload = { + Records: [ + { + messageId: "e995e54f-1724-41fa-82c0-8b81821f854e", + receiptHandle: + "AQEB4mIfRcyqtzn1X5Ss+ConhTejVGc+qnAcmu3/Z9ZvbNkaPcpuDLX/bzvPD/ZkAXJUXZcemGSJmd7L3snZHKMP2Ck8runZiyl4mubiLb444pZvdiNPuGRJ6a3FvgS/GQPzho/9nNMyOi66m8Viwh70v4EUCPGO4JmD3TTDAUrrcAnqU4WSObjfC/NAp9bI6wH2CEyAYEfex6Nxplbl/jBf9ZUG0I3m3vQd0Q4l4gd4jIR4oxQUglU2Tldl4Kx5fMUAhTRLAENri6HsY81avBkKd9FAuxONlsITB5uj02kOkvLlRGEcalqsKyPJ7AFaDLrOLaL3U+yReroPEJ5R5nwhLOEbeN5HROlZRXeaAwZOIN8BjqdeooYTIOrtvMEVb7a6OPLMdH1XB+ddevtKAH8K9Tm2ZjpaA7dtBGh1zFVHzBk=", + body: '{"version":"0","id":"af718b2a-b987-e8c0-7a2b-a188fad2661a","detail-type":"my.Detail","source":"my.Source","account":"123456123456","time":"2023-08-03T22:49:03Z","region":"us-east-1","resources":[],"detail":{"text":"Hello, world!","_datadog":{"x-datadog-trace-id":"7379586022458917877","x-datadog-parent-id":"2644033662113726488","x-datadog-sampling-priority":"1","x-datadog-tags":"_dd.p.dm=-0"}}}', + attributes: { + ApproximateReceiveCount: "1", + AWSTraceHeader: "Root=1-64cc2edd-112fbf1701d1355973a11d57;Parent=7d5a9776024b2d42;Sampled=0", + SentTimestamp: "1691102943638", + SenderId: "AIDAJXNJGGKNS7OSV23OI", + ApproximateFirstReceiveTimestamp: "1691102943647", + }, + messageAttributes: {}, + md5OfBody: "93d9f0cd8886d1e000a1a0b7007bffc4", + eventSource: "aws:sqs", + eventSourceARN: "arn:aws:sqs:us-east-1:425362996713:lambda-eb-sqs-lambda-dev-demo-queue", + awsRegion: "us-east-1", + }, + { + messageId: "e995e54f-1724-41fa-82c0-8b81821f854e", + receiptHandle: + "AQEB4mIfRcyqtzn1X5Ss+ConhTejVGc+qnAcmu3/Z9ZvbNkaPcpuDLX/bzvPD/ZkAXJUXZcemGSJmd7L3snZHKMP2Ck8runZiyl4mubiLb444pZvdiNPuGRJ6a3FvgS/GQPzho/9nNMyOi66m8Viwh70v4EUCPGO4JmD3TTDAUrrcAnqU4WSObjfC/NAp9bI6wH2CEyAYEfex6Nxplbl/jBf9ZUG0I3m3vQd0Q4l4gd4jIR4oxQUglU2Tldl4Kx5fMUAhTRLAENri6HsY81avBkKd9FAuxONlsITB5uj02kOkvLlRGEcalqsKyPJ7AFaDLrOLaL3U+yReroPEJ5R5nwhLOEbeN5HROlZRXeaAwZOIN8BjqdeooYTIOrtvMEVb7a6OPLMdH1XB+ddevtKAH8K9Tm2ZjpaA7dtBGh1zFVHzBk=", + body: '{"version":"0","id":"af718b2a-b987-e8c0-7a2b-a188fad2661a","detail-type":"my.Detail","source":"my.Source","account":"123456123456","time":"2023-08-03T22:49:03Z","region":"us-east-1","resources":[],"detail":{"text":"Hello, world!","_datadog":{"x-datadog-trace-id":"7379586022458917877","x-datadog-parent-id":"2644033662113726488","x-datadog-sampling-priority":"1","x-datadog-tags":"_dd.p.dm=-0"}}}', + attributes: { + ApproximateReceiveCount: "1", + AWSTraceHeader: "Root=1-64cc2edd-112fbf1701d1355973a11d57;Parent=7d5a9776024b2d42;Sampled=0", + SentTimestamp: "1691102943638", + SenderId: "AIDAJXNJGGKNS7OSV23OI", + ApproximateFirstReceiveTimestamp: "1691102943647", + }, + messageAttributes: {}, + md5OfBody: "93d9f0cd8886d1e000a1a0b7007bffc4", + eventSource: "aws:sqs", + eventSourceARN: "arn:aws:sqs:us-east-1:425362996713:lambda-eb-sqs-lambda-dev-demo-queue", + awsRegion: "us-east-1", + }, + ], + }; + + const extractor = new EventBridgeSQSEventTraceExtractor(tracerWrapper, {useSpanLinks: true} as TraceConfig); + + const traceContext = extractor.extract(payload); + expect(traceContext).not.toBeNull(); + + expect(traceContext?.length).toBe(2); }); it.each([ @@ -85,15 +215,15 @@ describe("EventBridgeSQSEventTraceExtractor", () => { ["valid data in body", { Records: [{ body: "{" }] }], // JSON.parse should fail ["detail in body", { Records: [{ body: "{}" }] }], ["_datadog in detail", { Records: [{ body: '{"detail":{"text":"Hello, world!"}}' }] }], - ])("returns null and skips extracting when payload is missing '%s'", (_, payload) => { + ])("returns empty array and skips extracting when payload is missing '%s'", (_, payload) => { const tracerWrapper = new TracerWrapper(); - const extractor = new EventBridgeSQSEventTraceExtractor(tracerWrapper); + const extractor = new EventBridgeSQSEventTraceExtractor(tracerWrapper, {} as TraceConfig); const traceContext = extractor.extract(payload as any); - expect(traceContext).toBeNull(); + expect(traceContext).toEqual([]); }); - it("returns null when extracted span context by tracer is null", () => { + it("returns empty array when extracted span context by tracer is null", () => { const tracerWrapper = new TracerWrapper(); const payload = { @@ -120,10 +250,10 @@ describe("EventBridgeSQSEventTraceExtractor", () => { ], }; - const extractor = new EventBridgeSQSEventTraceExtractor(tracerWrapper); + const extractor = new EventBridgeSQSEventTraceExtractor(tracerWrapper, {} as TraceConfig); const traceContext = extractor.extract(payload); - expect(traceContext).toBeNull(); + expect(traceContext).toEqual([]); }); it("extracts trace context from Step Function EventBridge-SQS event", () => { @@ -154,16 +284,16 @@ describe("EventBridgeSQSEventTraceExtractor", () => { ], }; - const extractor = new EventBridgeSQSEventTraceExtractor(tracerWrapper); + const extractor = new EventBridgeSQSEventTraceExtractor(tracerWrapper, {} as TraceConfig); const traceContext = extractor.extract(payload); expect(traceContext).not.toBeNull(); // The trace IDs are deterministically generated from the Step Function execution context - expect(traceContext?.toTraceId()).toBe("7858567057595668526"); - expect(traceContext?.toSpanId()).toBe("3674709292670593712"); - expect(traceContext?.sampleMode()).toBe("1"); - expect(traceContext?.source).toBe("event"); + expect(traceContext?.[0].toTraceId()).toBe("7858567057595668526"); + expect(traceContext?.[0].toSpanId()).toBe("3674709292670593712"); + expect(traceContext?.[0].sampleMode()).toBe("1"); + expect(traceContext?.[0].source).toBe("event"); }); }); }); diff --git a/src/trace/context/extractors/event-bridge-sqs.ts b/src/trace/context/extractors/event-bridge-sqs.ts index 45428478..75b048d7 100644 --- a/src/trace/context/extractors/event-bridge-sqs.ts +++ b/src/trace/context/extractors/event-bridge-sqs.ts @@ -3,24 +3,33 @@ import { TracerWrapper } from "../../tracer-wrapper"; import { EventTraceExtractor } from "../extractor"; import { SpanContextWrapper } from "../../span-context-wrapper"; import { extractTraceContext, handleExtractionError } from "../extractor-utils"; +import { TraceConfig } from "trace/listener"; export class EventBridgeSQSEventTraceExtractor implements EventTraceExtractor { - constructor(private tracerWrapper: TracerWrapper) {} + constructor(private tracerWrapper: TracerWrapper, private config: TraceConfig) {} - extract(event: SQSEvent): SpanContextWrapper | null { + extract(event: SQSEvent): SpanContextWrapper[] { + const spanContexts: SpanContextWrapper[] = []; try { - const body = event?.Records?.[0]?.body; - if (body) { - const parsedBody = JSON.parse(body); - const headers = parsedBody?.detail?._datadog; - if (headers) { - return extractTraceContext(headers, this.tracerWrapper); + for (const record of event.Records) { + const body = record?.body; + if (body) { + const parsedBody = JSON.parse(body); + const headers = parsedBody?.detail?._datadog; + if (headers) { + spanContexts.push(...extractTraceContext(headers, this.tracerWrapper)); + } + } + + // If span links is disabled only extract from the first record. + if (!this.config.useSpanLinks) { + break; } } } catch (error) { handleExtractionError(error, "EventBridge-SQS"); } - return null; + return spanContexts; } } diff --git a/src/trace/context/extractors/event-bridge.spec.ts b/src/trace/context/extractors/event-bridge.spec.ts index 346b89c4..dd6975e7 100644 --- a/src/trace/context/extractors/event-bridge.spec.ts +++ b/src/trace/context/extractors/event-bridge.spec.ts @@ -59,7 +59,7 @@ describe("EventBridgeEventTraceExtractor", () => { const extractor = new EventBridgeEventTraceExtractor(tracerWrapper); const traceContext = extractor.extract(payload); - expect(traceContext).not.toBeNull(); + expect(traceContext.length).toBe(1); expect(spyTracerWrapper).toHaveBeenCalledWith({ "x-datadog-trace-id": "5827606813695714842", @@ -67,24 +67,24 @@ describe("EventBridgeEventTraceExtractor", () => { "x-datadog-sampling-priority": "1", }); - expect(traceContext?.toTraceId()).toBe("5827606813695714842"); - expect(traceContext?.toSpanId()).toBe("4726693487091824375"); - expect(traceContext?.sampleMode()).toBe("1"); - expect(traceContext?.source).toBe("event"); + expect(traceContext?.[0].toTraceId()).toBe("5827606813695714842"); + expect(traceContext?.[0].toSpanId()).toBe("4726693487091824375"); + expect(traceContext?.[0].sampleMode()).toBe("1"); + expect(traceContext?.[0].source).toBe("event"); }); it.each([ ["detail", {}], ["_datadog in detail", { hello: "there" }], - ])("returns null and skips extracting when payload is missing '%s'", (_, payload) => { + ])("returns an empty array and skips extracting when payload is missing '%s'", (_, payload) => { const tracerWrapper = new TracerWrapper(); const extractor = new EventBridgeEventTraceExtractor(tracerWrapper); const traceContext = extractor.extract(payload as any); - expect(traceContext).toBeNull(); + expect(traceContext).toStrictEqual([]); }); - it("returns null when extracted span context by tracer is null", () => { + it("returns an empty array when extracted span context by tracer is null", () => { const tracerWrapper = new TracerWrapper(); const payload = { @@ -105,7 +105,7 @@ describe("EventBridgeEventTraceExtractor", () => { const extractor = new EventBridgeEventTraceExtractor(tracerWrapper); const traceContext = extractor.extract(payload); - expect(traceContext).toBeNull(); + expect(traceContext).toStrictEqual([]); }); it("extracts trace context from Step Function EventBridge event", () => { @@ -163,10 +163,10 @@ describe("EventBridgeEventTraceExtractor", () => { const traceContext = extractor.extract(payload); expect(traceContext).not.toBeNull(); - expect(traceContext?.toTraceId()).toBe("1503104665848096006"); - expect(traceContext?.toSpanId()).toBe("159267866761498620"); - expect(traceContext?.sampleMode()).toBe("1"); - expect(traceContext?.source).toBe("event"); + expect(traceContext?.[0].toTraceId()).toBe("1503104665848096006"); + expect(traceContext?.[0].toSpanId()).toBe("159267866761498620"); + expect(traceContext?.[0].sampleMode()).toBe("1"); + expect(traceContext?.[0].source).toBe("event"); }); }); }); diff --git a/src/trace/context/extractors/event-bridge.ts b/src/trace/context/extractors/event-bridge.ts index 41086f6a..b37cddd2 100644 --- a/src/trace/context/extractors/event-bridge.ts +++ b/src/trace/context/extractors/event-bridge.ts @@ -7,7 +7,7 @@ import { extractTraceContext, handleExtractionError } from "../extractor-utils"; export class EventBridgeEventTraceExtractor implements EventTraceExtractor { constructor(private tracerWrapper: TracerWrapper) {} - extract(event: EventBridgeEvent): SpanContextWrapper | null { + extract(event: EventBridgeEvent): SpanContextWrapper[] { try { const headers = event?.detail?._datadog; if (headers) { @@ -17,6 +17,6 @@ export class EventBridgeEventTraceExtractor implements EventTraceExtractor { handleExtractionError(error, "EventBridge"); } - return null; + return []; } } diff --git a/src/trace/context/extractors/http.spec.ts b/src/trace/context/extractors/http.spec.ts index 1cb7b3f4..f1a2304b 100644 --- a/src/trace/context/extractors/http.spec.ts +++ b/src/trace/context/extractors/http.spec.ts @@ -56,10 +56,10 @@ describe("HTTPEventTraceExtractor", () => { "x-datadog-trace-id": "797643193680388254", }); - expect(traceContext?.toTraceId()).toBe("797643193680388254"); - expect(traceContext?.toSpanId()).toBe("4726693487091824375"); - expect(traceContext?.sampleMode()).toBe("2"); - expect(traceContext?.source).toBe("event"); + expect(traceContext?.[0].toTraceId()).toBe("797643193680388254"); + expect(traceContext?.[0].toSpanId()).toBe("4726693487091824375"); + expect(traceContext?.[0].sampleMode()).toBe("2"); + expect(traceContext?.[0].source).toBe("event"); }); // The tracer is not handling mixed casing Datadog headers yet @@ -92,10 +92,10 @@ describe("HTTPEventTraceExtractor", () => { "x-datadog-trace-id": "797643193680388254", }); - expect(traceContext?.toTraceId()).toBe("797643193680388254"); - expect(traceContext?.toSpanId()).toBe("4726693487091824375"); - expect(traceContext?.sampleMode()).toBe("2"); - expect(traceContext?.source).toBe("event"); + expect(traceContext?.[0].toTraceId()).toBe("797643193680388254"); + expect(traceContext?.[0].toSpanId()).toBe("4726693487091824375"); + expect(traceContext?.[0].sampleMode()).toBe("2"); + expect(traceContext?.[0].source).toBe("event"); }); it("extracts trace context from payload with multiValueHeaders", () => { @@ -122,9 +122,9 @@ describe("HTTPEventTraceExtractor", () => { "x-datadog-sampling-priority": "1", }); - expect(traceContext?.toTraceId()).toBe("123"); - expect(traceContext?.toSpanId()).toBe("456"); - expect(traceContext?.sampleMode()).toBe("1"); + expect(traceContext?.[0].toTraceId()).toBe("123"); + expect(traceContext?.[0].toSpanId()).toBe("456"); + expect(traceContext?.[0].sampleMode()).toBe("1"); }); it("flattens a real ALB multiValueHeaders payload into a lowercase, single-value map", () => { @@ -188,7 +188,7 @@ describe("HTTPEventTraceExtractor", () => { const extractor = new HTTPEventTraceExtractor(tracerWrapper); const traceContext = extractor.extract(payload); - expect(traceContext).not.toBeNull(); + expect(traceContext.length).toBe(1); expect(spyTracerWrapper).toHaveBeenCalledWith({ "x-datadog-authorizing-requestid": "random-id", @@ -198,13 +198,13 @@ describe("HTTPEventTraceExtractor", () => { "x-datadog-trace-id": "2389589954026090296", }); - expect(traceContext?.toTraceId()).toBe("2389589954026090296"); - expect(traceContext?.toSpanId()).toBe("2389589954026090296"); - expect(traceContext?.sampleMode()).toBe("1"); - expect(traceContext?.source).toBe("event"); + expect(traceContext?.[0].toTraceId()).toBe("2389589954026090296"); + expect(traceContext?.[0].toSpanId()).toBe("2389589954026090296"); + expect(traceContext?.[0].sampleMode()).toBe("1"); + expect(traceContext?.[0].source).toBe("event"); }); - it("returns null when extracted span context by tracer is null", () => { + it("returns an empty array when extracted span context by tracer is null", () => { const tracerWrapper = new TracerWrapper(); const payload = { @@ -214,7 +214,7 @@ describe("HTTPEventTraceExtractor", () => { const extractor = new HTTPEventTraceExtractor(tracerWrapper); const traceContext = extractor.extract(payload); - expect(traceContext).toBeNull(); + expect(traceContext).toStrictEqual([]); }); }); diff --git a/src/trace/context/extractors/http.ts b/src/trace/context/extractors/http.ts index 2e57a21d..79d86e06 100644 --- a/src/trace/context/extractors/http.ts +++ b/src/trace/context/extractors/http.ts @@ -18,7 +18,7 @@ export class HTTPEventTraceExtractor implements EventTraceExtractor { this.decodeAuthorizerContext = decodeAuthorizerContext; } - extract(event: any): SpanContextWrapper | null { + extract(event: any): SpanContextWrapper[] { if (this.decodeAuthorizerContext) { // need to set the trace context if using authorizer lambda in authorizing (non-cached) cases try { @@ -29,10 +29,10 @@ export class HTTPEventTraceExtractor implements EventTraceExtractor { ); if (injectedAuthorizerHeaders !== null) { const _traceContext = this.tracerWrapper.extract(injectedAuthorizerHeaders); - if (_traceContext === null) return null; + if (_traceContext === null) return []; logDebug(`Extracted trace context from authorizer event`, { traceContext: _traceContext, event }); - return _traceContext; + return [_traceContext]; } } catch (error) { if (error instanceof Error) { @@ -55,10 +55,10 @@ export class HTTPEventTraceExtractor implements EventTraceExtractor { } const traceContext = this.tracerWrapper.extract(lowerCaseHeaders); - if (traceContext === null) return null; + if (traceContext === null) return []; logDebug(`Extracted trace context from HTTP event`, { traceContext, event }); - return traceContext; + return [traceContext]; } public static getEventSubType(event: any): HTTPEventSubType { diff --git a/src/trace/context/extractors/kinesis.spec.ts b/src/trace/context/extractors/kinesis.spec.ts index aa68a082..b252c2f7 100644 --- a/src/trace/context/extractors/kinesis.spec.ts +++ b/src/trace/context/extractors/kinesis.spec.ts @@ -1,5 +1,7 @@ +import { TraceConfig } from "../../listener"; import { TracerWrapper } from "../../tracer-wrapper"; import { KinesisEventTraceExtractor } from "./kinesis"; +import { EventBridgeSQSEventTraceExtractor } from "./event-bridge-sqs"; let mockSpanContext: any = null; @@ -57,7 +59,73 @@ describe("KinesisEventTraceExtractor", () => { ], }; - const extractor = new KinesisEventTraceExtractor(tracerWrapper); + const extractor = new KinesisEventTraceExtractor(tracerWrapper, {} as TraceConfig); + + const traceContext = extractor.extract(payload); + expect(traceContext.length).toBe(1); + + expect(spyTracerWrapper).toHaveBeenLastCalledWith({ + "x-datadog-parent-id": "1350735035497811828", + "x-datadog-sampled": "1", + "x-datadog-sampling-priority": "1", + "x-datadog-trace-id": "667309514221035538", + }); + + expect(traceContext?.[0].toTraceId()).toBe("667309514221035538"); + expect(traceContext?.[0].toSpanId()).toBe("1350735035497811828"); + expect(traceContext?.[0].sampleMode()).toBe("1"); + expect(traceContext?.[0].source).toBe("event"); + }); + + + it("only extracts first trace context with multiple input payloads", () => { + mockSpanContext = { + toTraceId: () => "667309514221035538", + toSpanId: () => "1350735035497811828", + _sampling: { + priority: "1", + }, + }; + const tracerWrapper = new TracerWrapper(); + + const payload = { + Records: [ + { + kinesis: { + kinesisSchemaVersion: "1.0", + partitionKey: "cdbfd750-cec0-4f0f-a4b0-82ae6152c7fb", + sequenceNumber: "49625698045709644136382874226371117765484751339579768834", + data: "eyJJJ20gbWFkZSBvZiB3YXgsIExhcnJ5IjoiV2hhdCBhcmUgeW91IG1hZGUgb2Y/IiwiX2RhdGFkb2ciOnsieC1kYXRhZG9nLXRyYWNlLWlkIjoiNjY3MzA5NTE0MjIxMDM1NTM4IiwieC1kYXRhZG9nLXBhcmVudC1pZCI6IjEzNTA3MzUwMzU0OTc4MTE4MjgiLCJ4LWRhdGFkb2ctc2FtcGxlZCI6IjEiLCJ4LWRhdGFkb2ctc2FtcGxpbmctcHJpb3JpdHkiOiIxIn19", + approximateArrivalTimestamp: 1642518727.248, + }, + eventSource: "aws:kinesis", + eventID: "shardId-000000000000:49545115243490985018280067714973144582180062593244200961", + invokeIdentityArn: "arn:aws:iam::EXAMPLE", + eventVersion: "1.0", + eventName: "aws:kinesis:record", + eventSourceARN: "arn:aws:kinesis:EXAMPLE", + awsRegion: "us-east-1", + }, + { + kinesis: { + kinesisSchemaVersion: "1.0", + partitionKey: "cdbfd750-cec0-4f0f-a4b0-82ae6152c7fb", + sequenceNumber: "49625698045709644136382874226371117765484751339579768834", + data: "eyJJJ20gbWFkZSBvZiB3YXgsIExhcnJ5IjoiV2hhdCBhcmUgeW91IG1hZGUgb2Y/IiwiX2RhdGFkb2ciOnsieC1kYXRhZG9nLXRyYWNlLWlkIjoiNjY3MzA5NTE0MjIxMDM1NTM4IiwieC1kYXRhZG9nLXBhcmVudC1pZCI6IjEzNTA3MzUwMzU0OTc4MTE4MjgiLCJ4LWRhdGFkb2ctc2FtcGxlZCI6IjEiLCJ4LWRhdGFkb2ctc2FtcGxpbmctcHJpb3JpdHkiOiIxIn19", + approximateArrivalTimestamp: 1642518727.248, + }, + eventSource: "aws:kinesis", + eventID: "shardId-000000000000:49545115243490985018280067714973144582180062593244200961", + invokeIdentityArn: "arn:aws:iam::EXAMPLE", + eventVersion: "1.0", + eventName: "aws:kinesis:record", + eventSourceARN: "arn:aws:kinesis:EXAMPLE", + awsRegion: "us-east-1", + }, + ], + }; + + const extractor = new KinesisEventTraceExtractor(tracerWrapper, {} as TraceConfig); const traceContext = extractor.extract(payload); expect(traceContext).not.toBeNull(); @@ -69,26 +137,82 @@ describe("KinesisEventTraceExtractor", () => { "x-datadog-trace-id": "667309514221035538", }); - expect(traceContext?.toTraceId()).toBe("667309514221035538"); - expect(traceContext?.toSpanId()).toBe("1350735035497811828"); - expect(traceContext?.sampleMode()).toBe("1"); - expect(traceContext?.source).toBe("event"); + expect(traceContext?.[0].toTraceId()).toBe("667309514221035538"); + expect(traceContext?.[0].toSpanId()).toBe("1350735035497811828"); + expect(traceContext?.[0].sampleMode()).toBe("1"); + expect(traceContext?.[0].source).toBe("event"); + }); + + it("extract all trace contexts when span links is enabled", () => { + mockSpanContext = { + toTraceId: () => "667309514221035538", + toSpanId: () => "1350735035497811828", + _sampling: { + priority: "1", + }, + }; + const tracerWrapper = new TracerWrapper(); + + const payload = { + Records: [ + { + kinesis: { + kinesisSchemaVersion: "1.0", + partitionKey: "cdbfd750-cec0-4f0f-a4b0-82ae6152c7fb", + sequenceNumber: "49625698045709644136382874226371117765484751339579768834", + data: "eyJJJ20gbWFkZSBvZiB3YXgsIExhcnJ5IjoiV2hhdCBhcmUgeW91IG1hZGUgb2Y/IiwiX2RhdGFkb2ciOnsieC1kYXRhZG9nLXRyYWNlLWlkIjoiNjY3MzA5NTE0MjIxMDM1NTM4IiwieC1kYXRhZG9nLXBhcmVudC1pZCI6IjEzNTA3MzUwMzU0OTc4MTE4MjgiLCJ4LWRhdGFkb2ctc2FtcGxlZCI6IjEiLCJ4LWRhdGFkb2ctc2FtcGxpbmctcHJpb3JpdHkiOiIxIn19", + approximateArrivalTimestamp: 1642518727.248, + }, + eventSource: "aws:kinesis", + eventID: "shardId-000000000000:49545115243490985018280067714973144582180062593244200961", + invokeIdentityArn: "arn:aws:iam::EXAMPLE", + eventVersion: "1.0", + eventName: "aws:kinesis:record", + eventSourceARN: "arn:aws:kinesis:EXAMPLE", + awsRegion: "us-east-1", + }, + { + kinesis: { + kinesisSchemaVersion: "1.0", + partitionKey: "cdbfd750-cec0-4f0f-a4b0-82ae6152c7fb", + sequenceNumber: "49625698045709644136382874226371117765484751339579768834", + data: "eyJJJ20gbWFkZSBvZiB3YXgsIExhcnJ5IjoiV2hhdCBhcmUgeW91IG1hZGUgb2Y/IiwiX2RhdGFkb2ciOnsieC1kYXRhZG9nLXRyYWNlLWlkIjoiNjY3MzA5NTE0MjIxMDM1NTM4IiwieC1kYXRhZG9nLXBhcmVudC1pZCI6IjEzNTA3MzUwMzU0OTc4MTE4MjgiLCJ4LWRhdGFkb2ctc2FtcGxlZCI6IjEiLCJ4LWRhdGFkb2ctc2FtcGxpbmctcHJpb3JpdHkiOiIxIn19", + approximateArrivalTimestamp: 1642518727.248, + }, + eventSource: "aws:kinesis", + eventID: "shardId-000000000000:49545115243490985018280067714973144582180062593244200961", + invokeIdentityArn: "arn:aws:iam::EXAMPLE", + eventVersion: "1.0", + eventName: "aws:kinesis:record", + eventSourceARN: "arn:aws:kinesis:EXAMPLE", + awsRegion: "us-east-1", + }, + ], + }; + + const extractor = new KinesisEventTraceExtractor(tracerWrapper, {useSpanLinks: true} as TraceConfig); + + const traceContext = extractor.extract(payload); + expect(traceContext).not.toBeNull(); + + expect(traceContext?.length).toBe(2); }); + it.each([ ["Records", {}], ["Records first entry", { Records: [] }], ["valid data in kinesis", { Records: [{ kinesis: { data: "{" } }] }], // JSON.parse should fail ["_datadog in data", { Records: [{ kinesis: { data: "e30=" } }] }], - ])("returns null and skips extracting when payload is missing '%s'", (_, payload) => { + ])("returns empty array and skips extracting when payload is missing '%s'", (_, payload) => { const tracerWrapper = new TracerWrapper(); - const extractor = new KinesisEventTraceExtractor(tracerWrapper); + const extractor = new KinesisEventTraceExtractor(tracerWrapper, {} as TraceConfig); const traceContext = extractor.extract(payload as any); - expect(traceContext).toBeNull(); + expect(traceContext).toStrictEqual([]); }); - it("returns null when extracted span context by tracer is null", () => { + it("returns empty array when extracted span context by tracer is null", () => { const tracerWrapper = new TracerWrapper(); const payload = { @@ -112,10 +236,10 @@ describe("KinesisEventTraceExtractor", () => { ], }; - const extractor = new KinesisEventTraceExtractor(tracerWrapper); + const extractor = new KinesisEventTraceExtractor(tracerWrapper, {} as TraceConfig); const traceContext = extractor.extract(payload); - expect(traceContext).toBeNull(); + expect(traceContext).toStrictEqual([]); }); }); }); diff --git a/src/trace/context/extractors/kinesis.ts b/src/trace/context/extractors/kinesis.ts index 5f5d0b1f..4fd7d707 100644 --- a/src/trace/context/extractors/kinesis.ts +++ b/src/trace/context/extractors/kinesis.ts @@ -3,31 +3,45 @@ import { logDebug } from "../../../utils"; import { EventTraceExtractor } from "../extractor"; import { TracerWrapper } from "../../tracer-wrapper"; import { SpanContextWrapper } from "../../span-context-wrapper"; +import { TraceConfig } from "trace/listener"; export class KinesisEventTraceExtractor implements EventTraceExtractor { - constructor(private tracerWrapper: TracerWrapper) {} - - extract(event: KinesisStreamEvent): SpanContextWrapper | null { - const kinesisData = event?.Records?.[0]?.kinesis.data; - if (kinesisData === undefined) return null; - - try { - const decodedData = Buffer.from(kinesisData, "base64").toString("ascii"); - const parsedBody = JSON.parse(decodedData); - const headers = parsedBody?._datadog; - if (headers) { - const traceContext = this.tracerWrapper.extract(headers); - if (traceContext === null) return null; - - logDebug(`Extracted trace context from Kinesis event`, { traceContext, headers }); - return traceContext; - } - } catch (error) { - if (error instanceof Error) { - logDebug("Unable to extract trace context from Kinesis event", error); + constructor(private tracerWrapper: TracerWrapper, private config: TraceConfig) {} + + extract(event: KinesisStreamEvent): SpanContextWrapper[] { + const spanContexts: SpanContextWrapper[] = []; + + if (!event.Records) { + return spanContexts; + } + + for (const record of event.Records) { + const kinesisData = record.kinesis.data; + if (kinesisData === undefined) continue; + + try { + const decodedData = Buffer.from(kinesisData, "base64").toString("ascii"); + const parsedBody = JSON.parse(decodedData); + const headers = parsedBody?._datadog; + if (headers) { + const traceContext = this.tracerWrapper.extract(headers); + if (traceContext === null) continue; + + logDebug(`Extracted trace context from Kinesis event`, { traceContext, headers }); + spanContexts.push(traceContext); + } + + // If span links is disabled only extract from the first record. + if (!this.config.useSpanLinks) { + break; + } + } catch (error) { + if (error instanceof Error) { + logDebug("Unable to extract trace context from Kinesis event", error); + } } } - return null; + return spanContexts; } } diff --git a/src/trace/context/extractors/lambda-context.spec.ts b/src/trace/context/extractors/lambda-context.spec.ts index fb3ef4f4..b3b676a8 100644 --- a/src/trace/context/extractors/lambda-context.spec.ts +++ b/src/trace/context/extractors/lambda-context.spec.ts @@ -64,7 +64,7 @@ describe("LambdaContextTraceExtractor", () => { const extractor = new LambdaContextTraceExtractor(tracerWrapper); const traceContext = extractor.extract(payload); - expect(traceContext).not.toBeNull(); + expect(traceContext.length).toBe(1); expect(spyTracerWrapper).toHaveBeenCalledWith({ "x-datadog-parent-id": "1350735035497811828", @@ -72,10 +72,10 @@ describe("LambdaContextTraceExtractor", () => { "x-datadog-trace-id": "667309514221035538", }); - expect(traceContext?.toTraceId()).toBe("667309514221035538"); - expect(traceContext?.toSpanId()).toBe("1350735035497811828"); - expect(traceContext?.sampleMode()).toBe("1"); - expect(traceContext?.source).toBe("event"); + expect(traceContext?.[0].toTraceId()).toBe("667309514221035538"); + expect(traceContext?.[0].toSpanId()).toBe("1350735035497811828"); + expect(traceContext?.[0].sampleMode()).toBe("1"); + expect(traceContext?.[0].source).toBe("event"); }); it.each([ @@ -88,10 +88,10 @@ describe("LambdaContextTraceExtractor", () => { const extractor = new LambdaContextTraceExtractor(tracerWrapper); const traceContext = extractor.extract(payload as any); - expect(traceContext).toBeNull(); + expect(traceContext).toStrictEqual([]); }); - it("returns null when extracted span context by tracer is null", () => { + it("returns an empty array when extracted span context by tracer is null", () => { const tracerWrapper = new TracerWrapper(); const payload = { @@ -105,7 +105,7 @@ describe("LambdaContextTraceExtractor", () => { const extractor = new LambdaContextTraceExtractor(tracerWrapper); const traceContext = extractor.extract(payload); - expect(traceContext).toBeNull(); + expect(traceContext).toStrictEqual([]); }); }); }); diff --git a/src/trace/context/extractors/lambda-context.ts b/src/trace/context/extractors/lambda-context.ts index de7224b4..c550edae 100644 --- a/src/trace/context/extractors/lambda-context.ts +++ b/src/trace/context/extractors/lambda-context.ts @@ -3,17 +3,17 @@ import { TracerWrapper } from "../../tracer-wrapper"; import { SpanContextWrapper } from "../../span-context-wrapper"; interface ContextTraceExtractor { - extract(context: any): SpanContextWrapper | null; + extract(context: any): SpanContextWrapper[]; } export class LambdaContextTraceExtractor implements ContextTraceExtractor { constructor(private tracerWrapper: TracerWrapper) {} - extract(context: any): SpanContextWrapper | null { - if (!context || typeof context !== "object") return null; + extract(context: any): SpanContextWrapper[] { + if (!context || typeof context !== "object") return []; const custom = context.clientContext?.custom; - if (!custom || typeof custom !== "object") return null; + if (!custom || typeof custom !== "object") return []; // TODO: Note of things to deprecate in next release, headers being just // custom field, instead of custom._datadog @@ -23,9 +23,9 @@ export class LambdaContextTraceExtractor implements ContextTraceExtractor { } const spanContext = this.tracerWrapper.extract(headers); - if (spanContext === null) return null; + if (spanContext === null) return []; logDebug("Extracted trace context from Lambda Context event", { spanContext, headers }); - return spanContext; + return [spanContext]; } } diff --git a/src/trace/context/extractors/sns-sqs.spec.ts b/src/trace/context/extractors/sns-sqs.spec.ts index 82b39c32..34556872 100644 --- a/src/trace/context/extractors/sns-sqs.spec.ts +++ b/src/trace/context/extractors/sns-sqs.spec.ts @@ -1,6 +1,9 @@ import { TracerWrapper } from "../../tracer-wrapper"; import { SNSSQSEventTraceExtractor } from "./sns-sqs"; import { StepFunctionContextService } from "../../step-function-service"; +import { TraceConfig } from "../../listener"; +import { SNSEvent } from "aws-lambda"; +import { SNSEventTraceExtractor } from "./sns"; let mockSpanContext: any = null; @@ -60,10 +63,10 @@ describe("SNSSQSEventTraceExtractor", () => { ], }; - const extractor = new SNSSQSEventTraceExtractor(tracerWrapper); + const extractor = new SNSSQSEventTraceExtractor(tracerWrapper, {} as TraceConfig); const traceContext = extractor.extract(payload); - expect(traceContext).not.toBeNull(); + expect(traceContext.length).toBe(1); expect(spyTracerWrapper).toHaveBeenCalledWith({ "x-datadog-parent-id": "4493917105238181843", @@ -72,10 +75,10 @@ describe("SNSSQSEventTraceExtractor", () => { "x-datadog-trace-id": "2776434475358637757", }); - expect(traceContext?.toTraceId()).toBe("2776434475358637757"); - expect(traceContext?.toSpanId()).toBe("4493917105238181843"); - expect(traceContext?.sampleMode()).toBe("1"); - expect(traceContext?.source).toBe("event"); + expect(traceContext?.[0].toTraceId()).toBe("2776434475358637757"); + expect(traceContext?.[0].toSpanId()).toBe("4493917105238181843"); + expect(traceContext?.[0].sampleMode()).toBe("1"); + expect(traceContext?.[0].source).toBe("event"); }); it("extracts trace context with valid payload with Binary Value", () => { @@ -110,7 +113,7 @@ describe("SNSSQSEventTraceExtractor", () => { ], }; - const extractor = new SNSSQSEventTraceExtractor(tracerWrapper); + const extractor = new SNSSQSEventTraceExtractor(tracerWrapper, {} as TraceConfig); const traceContext = extractor.extract(payload); expect(traceContext).not.toBeNull(); @@ -121,10 +124,133 @@ describe("SNSSQSEventTraceExtractor", () => { "x-datadog-trace-id": "7102291628443134919", }); - expect(traceContext?.toTraceId()).toBe("7102291628443134919"); - expect(traceContext?.toSpanId()).toBe("4247550101648618618"); - expect(traceContext?.sampleMode()).toBe("1"); - expect(traceContext?.source).toBe("event"); + expect(traceContext?.[0].toTraceId()).toBe("7102291628443134919"); + expect(traceContext?.[0].toSpanId()).toBe("4247550101648618618"); + expect(traceContext?.[0].sampleMode()).toBe("1"); + expect(traceContext?.[0].source).toBe("event"); + }); + + + it("only extracts first trace context with multiple input payloads", () => { + mockSpanContext = { + toTraceId: () => "2776434475358637757", + toSpanId: () => "4493917105238181843", + _sampling: { + priority: "1", + }, + }; + const tracerWrapper = new TracerWrapper(); + + const payload = { + Records: [ + { + messageId: "64812b68-4d9b-4dca-b3fb-9b18f255ee51", + receiptHandle: + "AQEBER6aRkfG8092GvkL7FRwCwbQ7LLDW9Tlk/CembqHe+suS2kfFxXiukomvaIN61QoyQMoRgWuV52SDkiQno2u+5hP64BDbmw+e/KR9ayvIfHJ3M6RfyQLaWNWm3hDFBCKTnBMVIxtdx0N9epZZewyokjKcrNYtmCghFgTCvZzsQkowi5rnoHAVHJ3je1c3bDnQ1KLrZFgajDnootYXDwEPuMq5FIxrf4EzTe0S7S+rnRm+GaQfeBLBVAY6dASL9usV3/AFRqDtaI7GKI+0F2NCgLlqj49VlPRz4ldhkGknYlKTZTluAqALWLJS62/J1GQo53Cs3nneJcmu5ajB2zzmhhRXoXINEkLhCD5ujZfcsw9H4xqW69Or4ECvlqx14bUU2rtMIW0QM2p7pEeXnyocymQv6m1te113eYWTVmaJ4I=", + body: '{\n "Type" : "Notification",\n "MessageId" : "0a0ab23e-4861-5447-82b7-e8094ff3e332",\n "TopicArn" : "arn:aws:sns:eu-west-1:601427279990:js-library-test-dev-demoTopic-15WGUVRCBMPAA",\n "Message" : "{\\"hello\\":\\"harv\\",\\"nice of you to join us\\":\\"david\\",\\"anotherThing\\":{\\"foo\\":\\"bar\\",\\"blah\\":null,\\"harv\\":123},\\"vals\\":[{\\"thingOne\\":1},{\\"thingTwo\\":2}],\\"ajTimestamp\\":1639777617957}",\n "Timestamp" : "2021-12-17T21:46:58.040Z",\n "SignatureVersion" : "1",\n "Signature" : "FR35/7E8C3LHEVk/rC4XxXlXwV/5mNkFNPgDhHSnJ2I6hIoSrTROAm7h5xm1PuBkAeFDvq0zofw91ouk9zZyvhdrMLFIIgrjEyNayRmEffmoEAkzLFUsgtQX7MmTl644r4NuWiM0Oiz7jueRvIcKXcZr7Nc6GJcWV1ymec8oOmuHNMisnPMxI07LIQVYSyAfv6P9r2jEWMVIukRoCzwTnRk4bUUYhPSGHI7OC3AsxxXBbv8snqTrLM/4z2rXCf6jHCKNxWeLlm9/45PphCkEyx5BWS4/71KaoMWUWy8+6CCsy+uF3XTCVmvSEYLyEwTSzOY+vCUjazrRW93498i70g==",\n "SigningCertUrl" : "https://sns.eu-west-1.amazonaws.com/SimpleNotificationService-************************33ab7e69.pem",\n "UnsubscribeUrl" : "https://sns.eu-west-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:eu-west-1:601427279990:js-library-test-dev-demoTopic-15WGUVRCBMPAA:1290f550-9a8a-4e8f-a900-8f5f96dcddda",\n "MessageAttributes" : {\n "_datadog" : {"Type":"String","Value":"{\\"x-datadog-trace-id\\":\\"2776434475358637757\\",\\"x-datadog-parent-id\\":\\"4493917105238181843\\",\\"x-datadog-sampled\\":\\"1\\",\\"x-datadog-sampling-priority\\":\\"1\\"}"}\n }\n}', + attributes: { + ApproximateReceiveCount: "1", + SentTimestamp: "1639777618130", + SenderId: "AIDAIOA2GYWSHW4E2VXIO", + ApproximateFirstReceiveTimestamp: "1639777618132", + }, + messageAttributes: {}, + md5OfBody: "ee19d8b1377919239ad3fd5dabc33739", + eventSource: "aws:sqs", + eventSourceARN: "arn:aws:sqs:eu-west-1:601427279990:aj-js-library-test-dev-demo-queue", + awsRegion: "eu-west-1", + }, + { + messageId: "64812b68-4d9b-4dca-b3fb-9b18f255ee51", + receiptHandle: + "AQEBER6aRkfG8092GvkL7FRwCwbQ7LLDW9Tlk/CembqHe+suS2kfFxXiukomvaIN61QoyQMoRgWuV52SDkiQno2u+5hP64BDbmw+e/KR9ayvIfHJ3M6RfyQLaWNWm3hDFBCKTnBMVIxtdx0N9epZZewyokjKcrNYtmCghFgTCvZzsQkowi5rnoHAVHJ3je1c3bDnQ1KLrZFgajDnootYXDwEPuMq5FIxrf4EzTe0S7S+rnRm+GaQfeBLBVAY6dASL9usV3/AFRqDtaI7GKI+0F2NCgLlqj49VlPRz4ldhkGknYlKTZTluAqALWLJS62/J1GQo53Cs3nneJcmu5ajB2zzmhhRXoXINEkLhCD5ujZfcsw9H4xqW69Or4ECvlqx14bUU2rtMIW0QM2p7pEeXnyocymQv6m1te113eYWTVmaJ4I=", + body: '{\n "Type" : "Notification",\n "MessageId" : "0a0ab23e-4861-5447-82b7-e8094ff3e332",\n "TopicArn" : "arn:aws:sns:eu-west-1:601427279990:js-library-test-dev-demoTopic-15WGUVRCBMPAA",\n "Message" : "{\\"hello\\":\\"harv\\",\\"nice of you to join us\\":\\"david\\",\\"anotherThing\\":{\\"foo\\":\\"bar\\",\\"blah\\":null,\\"harv\\":123},\\"vals\\":[{\\"thingOne\\":1},{\\"thingTwo\\":2}],\\"ajTimestamp\\":1639777617957}",\n "Timestamp" : "2021-12-17T21:46:58.040Z",\n "SignatureVersion" : "1",\n "Signature" : "FR35/7E8C3LHEVk/rC4XxXlXwV/5mNkFNPgDhHSnJ2I6hIoSrTROAm7h5xm1PuBkAeFDvq0zofw91ouk9zZyvhdrMLFIIgrjEyNayRmEffmoEAkzLFUsgtQX7MmTl644r4NuWiM0Oiz7jueRvIcKXcZr7Nc6GJcWV1ymec8oOmuHNMisnPMxI07LIQVYSyAfv6P9r2jEWMVIukRoCzwTnRk4bUUYhPSGHI7OC3AsxxXBbv8snqTrLM/4z2rXCf6jHCKNxWeLlm9/45PphCkEyx5BWS4/71KaoMWUWy8+6CCsy+uF3XTCVmvSEYLyEwTSzOY+vCUjazrRW93498i70g==",\n "SigningCertUrl" : "https://sns.eu-west-1.amazonaws.com/SimpleNotificationService-************************33ab7e69.pem",\n "UnsubscribeUrl" : "https://sns.eu-west-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:eu-west-1:601427279990:js-library-test-dev-demoTopic-15WGUVRCBMPAA:1290f550-9a8a-4e8f-a900-8f5f96dcddda",\n "MessageAttributes" : {\n "_datadog" : {"Type":"String","Value":"{\\"x-datadog-trace-id\\":\\"2776434475358637757\\",\\"x-datadog-parent-id\\":\\"4493917105238181843\\",\\"x-datadog-sampled\\":\\"1\\",\\"x-datadog-sampling-priority\\":\\"1\\"}"}\n }\n}', + attributes: { + ApproximateReceiveCount: "1", + SentTimestamp: "1639777618130", + SenderId: "AIDAIOA2GYWSHW4E2VXIO", + ApproximateFirstReceiveTimestamp: "1639777618132", + }, + messageAttributes: {}, + md5OfBody: "ee19d8b1377919239ad3fd5dabc33739", + eventSource: "aws:sqs", + eventSourceARN: "arn:aws:sqs:eu-west-1:601427279990:aj-js-library-test-dev-demo-queue", + awsRegion: "eu-west-1", + }, + ], + }; + + const extractor = new SNSSQSEventTraceExtractor(tracerWrapper, {} as TraceConfig); + + const traceContext = extractor.extract(payload); + expect(traceContext.length).toBe(1); + + expect(spyTracerWrapper).toHaveBeenCalledWith({ + "x-datadog-parent-id": "4493917105238181843", + "x-datadog-sampled": "1", + "x-datadog-sampling-priority": "1", + "x-datadog-trace-id": "2776434475358637757", + }); + + expect(traceContext?.[0].toTraceId()).toBe("2776434475358637757"); + expect(traceContext?.[0].toSpanId()).toBe("4493917105238181843"); + expect(traceContext?.[0].sampleMode()).toBe("1"); + expect(traceContext?.[0].source).toBe("event"); + }); + + it("extract all trace contexts when span links is enabled", () => { + mockSpanContext = { + toTraceId: () => "2776434475358637757", + toSpanId: () => "4493917105238181843", + _sampling: { + priority: "1", + }, + }; + const tracerWrapper = new TracerWrapper(); + + const payload = { + Records: [ + { + messageId: "64812b68-4d9b-4dca-b3fb-9b18f255ee51", + receiptHandle: + "AQEBER6aRkfG8092GvkL7FRwCwbQ7LLDW9Tlk/CembqHe+suS2kfFxXiukomvaIN61QoyQMoRgWuV52SDkiQno2u+5hP64BDbmw+e/KR9ayvIfHJ3M6RfyQLaWNWm3hDFBCKTnBMVIxtdx0N9epZZewyokjKcrNYtmCghFgTCvZzsQkowi5rnoHAVHJ3je1c3bDnQ1KLrZFgajDnootYXDwEPuMq5FIxrf4EzTe0S7S+rnRm+GaQfeBLBVAY6dASL9usV3/AFRqDtaI7GKI+0F2NCgLlqj49VlPRz4ldhkGknYlKTZTluAqALWLJS62/J1GQo53Cs3nneJcmu5ajB2zzmhhRXoXINEkLhCD5ujZfcsw9H4xqW69Or4ECvlqx14bUU2rtMIW0QM2p7pEeXnyocymQv6m1te113eYWTVmaJ4I=", + body: '{\n "Type" : "Notification",\n "MessageId" : "0a0ab23e-4861-5447-82b7-e8094ff3e332",\n "TopicArn" : "arn:aws:sns:eu-west-1:601427279990:js-library-test-dev-demoTopic-15WGUVRCBMPAA",\n "Message" : "{\\"hello\\":\\"harv\\",\\"nice of you to join us\\":\\"david\\",\\"anotherThing\\":{\\"foo\\":\\"bar\\",\\"blah\\":null,\\"harv\\":123},\\"vals\\":[{\\"thingOne\\":1},{\\"thingTwo\\":2}],\\"ajTimestamp\\":1639777617957}",\n "Timestamp" : "2021-12-17T21:46:58.040Z",\n "SignatureVersion" : "1",\n "Signature" : "FR35/7E8C3LHEVk/rC4XxXlXwV/5mNkFNPgDhHSnJ2I6hIoSrTROAm7h5xm1PuBkAeFDvq0zofw91ouk9zZyvhdrMLFIIgrjEyNayRmEffmoEAkzLFUsgtQX7MmTl644r4NuWiM0Oiz7jueRvIcKXcZr7Nc6GJcWV1ymec8oOmuHNMisnPMxI07LIQVYSyAfv6P9r2jEWMVIukRoCzwTnRk4bUUYhPSGHI7OC3AsxxXBbv8snqTrLM/4z2rXCf6jHCKNxWeLlm9/45PphCkEyx5BWS4/71KaoMWUWy8+6CCsy+uF3XTCVmvSEYLyEwTSzOY+vCUjazrRW93498i70g==",\n "SigningCertUrl" : "https://sns.eu-west-1.amazonaws.com/SimpleNotificationService-************************33ab7e69.pem",\n "UnsubscribeUrl" : "https://sns.eu-west-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:eu-west-1:601427279990:js-library-test-dev-demoTopic-15WGUVRCBMPAA:1290f550-9a8a-4e8f-a900-8f5f96dcddda",\n "MessageAttributes" : {\n "_datadog" : {"Type":"String","Value":"{\\"x-datadog-trace-id\\":\\"2776434475358637757\\",\\"x-datadog-parent-id\\":\\"4493917105238181843\\",\\"x-datadog-sampled\\":\\"1\\",\\"x-datadog-sampling-priority\\":\\"1\\"}"}\n }\n}', + attributes: { + ApproximateReceiveCount: "1", + SentTimestamp: "1639777618130", + SenderId: "AIDAIOA2GYWSHW4E2VXIO", + ApproximateFirstReceiveTimestamp: "1639777618132", + }, + messageAttributes: {}, + md5OfBody: "ee19d8b1377919239ad3fd5dabc33739", + eventSource: "aws:sqs", + eventSourceARN: "arn:aws:sqs:eu-west-1:601427279990:aj-js-library-test-dev-demo-queue", + awsRegion: "eu-west-1", + }, + { + messageId: "64812b68-4d9b-4dca-b3fb-9b18f255ee51", + receiptHandle: + "AQEBER6aRkfG8092GvkL7FRwCwbQ7LLDW9Tlk/CembqHe+suS2kfFxXiukomvaIN61QoyQMoRgWuV52SDkiQno2u+5hP64BDbmw+e/KR9ayvIfHJ3M6RfyQLaWNWm3hDFBCKTnBMVIxtdx0N9epZZewyokjKcrNYtmCghFgTCvZzsQkowi5rnoHAVHJ3je1c3bDnQ1KLrZFgajDnootYXDwEPuMq5FIxrf4EzTe0S7S+rnRm+GaQfeBLBVAY6dASL9usV3/AFRqDtaI7GKI+0F2NCgLlqj49VlPRz4ldhkGknYlKTZTluAqALWLJS62/J1GQo53Cs3nneJcmu5ajB2zzmhhRXoXINEkLhCD5ujZfcsw9H4xqW69Or4ECvlqx14bUU2rtMIW0QM2p7pEeXnyocymQv6m1te113eYWTVmaJ4I=", + body: '{\n "Type" : "Notification",\n "MessageId" : "0a0ab23e-4861-5447-82b7-e8094ff3e332",\n "TopicArn" : "arn:aws:sns:eu-west-1:601427279990:js-library-test-dev-demoTopic-15WGUVRCBMPAA",\n "Message" : "{\\"hello\\":\\"harv\\",\\"nice of you to join us\\":\\"david\\",\\"anotherThing\\":{\\"foo\\":\\"bar\\",\\"blah\\":null,\\"harv\\":123},\\"vals\\":[{\\"thingOne\\":1},{\\"thingTwo\\":2}],\\"ajTimestamp\\":1639777617957}",\n "Timestamp" : "2021-12-17T21:46:58.040Z",\n "SignatureVersion" : "1",\n "Signature" : "FR35/7E8C3LHEVk/rC4XxXlXwV/5mNkFNPgDhHSnJ2I6hIoSrTROAm7h5xm1PuBkAeFDvq0zofw91ouk9zZyvhdrMLFIIgrjEyNayRmEffmoEAkzLFUsgtQX7MmTl644r4NuWiM0Oiz7jueRvIcKXcZr7Nc6GJcWV1ymec8oOmuHNMisnPMxI07LIQVYSyAfv6P9r2jEWMVIukRoCzwTnRk4bUUYhPSGHI7OC3AsxxXBbv8snqTrLM/4z2rXCf6jHCKNxWeLlm9/45PphCkEyx5BWS4/71KaoMWUWy8+6CCsy+uF3XTCVmvSEYLyEwTSzOY+vCUjazrRW93498i70g==",\n "SigningCertUrl" : "https://sns.eu-west-1.amazonaws.com/SimpleNotificationService-************************33ab7e69.pem",\n "UnsubscribeUrl" : "https://sns.eu-west-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:eu-west-1:601427279990:js-library-test-dev-demoTopic-15WGUVRCBMPAA:1290f550-9a8a-4e8f-a900-8f5f96dcddda",\n "MessageAttributes" : {\n "_datadog" : {"Type":"String","Value":"{\\"x-datadog-trace-id\\":\\"2776434475358637757\\",\\"x-datadog-parent-id\\":\\"4493917105238181843\\",\\"x-datadog-sampled\\":\\"1\\",\\"x-datadog-sampling-priority\\":\\"1\\"}"}\n }\n}', + attributes: { + ApproximateReceiveCount: "1", + SentTimestamp: "1639777618130", + SenderId: "AIDAIOA2GYWSHW4E2VXIO", + ApproximateFirstReceiveTimestamp: "1639777618132", + }, + messageAttributes: {}, + md5OfBody: "ee19d8b1377919239ad3fd5dabc33739", + eventSource: "aws:sqs", + eventSourceARN: "arn:aws:sqs:eu-west-1:601427279990:aj-js-library-test-dev-demo-queue", + awsRegion: "eu-west-1", + }, + ], + }; + + const extractor = new SNSSQSEventTraceExtractor(tracerWrapper, {useSpanLinks: true} as TraceConfig); + + const traceContext = extractor.extract(payload); + expect(traceContext.length).toBe(2); }); it.each([ @@ -135,15 +261,15 @@ describe("SNSSQSEventTraceExtractor", () => { ["MessageAttributes in body", { Records: [{ body: "{}" }] }], ["_datadog in MessageAttributes", { Records: [{ body: '{"MessageAttributes":{"text":"Hello, world!"}}' }] }], ["Value in _datadog", { Records: [{ body: '{"MessageAttributes":{"_datadog":{}}}' }] }], - ])("returns null and skips extracting when payload is missing '%s'", (_, payload) => { + ])("returns empty array and skips extracting when payload is missing '%s'", (_, payload) => { const tracerWrapper = new TracerWrapper(); - const extractor = new SNSSQSEventTraceExtractor(tracerWrapper); + const extractor = new SNSSQSEventTraceExtractor(tracerWrapper, {} as TraceConfig); const traceContext = extractor.extract(payload as any); - expect(traceContext).toBeNull(); + expect(traceContext).toStrictEqual([]); }); - it("returns null when extracted span context by tracer is null", () => { + it("returns empty array when extracted span context by tracer is null", () => { const tracerWrapper = new TracerWrapper(); const payload = { @@ -168,10 +294,10 @@ describe("SNSSQSEventTraceExtractor", () => { ], }; - const extractor = new SNSSQSEventTraceExtractor(tracerWrapper); + const extractor = new SNSSQSEventTraceExtractor(tracerWrapper, {} as TraceConfig); const traceContext = extractor.extract(payload); - expect(traceContext).toBeNull(); + expect(traceContext).toStrictEqual([]); }); it("extracts trace context from AWSTraceHeader with valid payload", () => { const tracerWrapper = new TracerWrapper(); @@ -199,7 +325,7 @@ describe("SNSSQSEventTraceExtractor", () => { ], }; - const extractor = new SNSSQSEventTraceExtractor(tracerWrapper); + const extractor = new SNSSQSEventTraceExtractor(tracerWrapper, {} as TraceConfig); const traceContext = extractor.extract(payload); expect(traceContext).not.toBeNull(); @@ -208,10 +334,10 @@ describe("SNSSQSEventTraceExtractor", () => { // 2. More importantly, DD_TRACE_PROPAGATION_STYLE could cause extraction fail expect(spyTracerWrapper).not.toHaveBeenCalled(); - expect(traceContext?.toTraceId()).toBe("5027664352514971920"); - expect(traceContext?.toSpanId()).toBe("1797917631284315439"); - expect(traceContext?.sampleMode()).toBe("1"); - expect(traceContext?.source).toBe("event"); + expect(traceContext?.[0].toTraceId()).toBe("5027664352514971920"); + expect(traceContext?.[0].toSpanId()).toBe("1797917631284315439"); + expect(traceContext?.[0].sampleMode()).toBe("1"); + expect(traceContext?.[0].source).toBe("event"); }); it("extracts trace context from Step Function SNS-SQS event", () => { @@ -251,16 +377,16 @@ describe("SNSSQSEventTraceExtractor", () => { ], }; - const extractor = new SNSSQSEventTraceExtractor(tracerWrapper); + const extractor = new SNSSQSEventTraceExtractor(tracerWrapper, {} as TraceConfig); const traceContext = extractor.extract(payload); expect(traceContext).not.toBeNull(); // The StepFunctionContextService generates deterministic trace IDs - expect(traceContext?.toTraceId()).toBe("1657966791618574655"); - expect(traceContext?.toSpanId()).toBe("5100002956473485303"); - expect(traceContext?.sampleMode()).toBe("1"); - expect(traceContext?.source).toBe("event"); + expect(traceContext?.[0].toTraceId()).toBe("1657966791618574655"); + expect(traceContext?.[0].toSpanId()).toBe("5100002956473485303"); + expect(traceContext?.[0].sampleMode()).toBe("1"); + expect(traceContext?.[0].source).toBe("event"); }); }); }); diff --git a/src/trace/context/extractors/sns-sqs.ts b/src/trace/context/extractors/sns-sqs.ts index fd23ad9e..a2933b7b 100644 --- a/src/trace/context/extractors/sns-sqs.ts +++ b/src/trace/context/extractors/sns-sqs.ts @@ -4,56 +4,65 @@ import { logDebug } from "../../../utils"; import { EventTraceExtractor } from "../extractor"; import { SpanContextWrapper } from "../../span-context-wrapper"; import { extractTraceContext, extractFromAWSTraceHeader, handleExtractionError } from "../extractor-utils"; +import { TraceConfig } from "trace/listener"; export class SNSSQSEventTraceExtractor implements EventTraceExtractor { - constructor(private tracerWrapper: TracerWrapper) {} + constructor(private tracerWrapper: TracerWrapper, private config: TraceConfig) {} - extract(event: SQSEvent): SpanContextWrapper | null { + extract(event: SQSEvent): SpanContextWrapper[] { logDebug("SNS-SQS Extractor Being Used"); + const spanContexts: SpanContextWrapper[] = []; try { - // Try to extract trace context from SNS wrapped in SQS - const body = event?.Records?.[0]?.body; - if (body) { - const parsedBody = JSON.parse(body); - const snsMessageAttribute = parsedBody?.MessageAttributes?._datadog; - if (snsMessageAttribute?.Value) { - let headers; - if (snsMessageAttribute.Type === "String") { - headers = JSON.parse(snsMessageAttribute.Value); - } else { - // Try decoding base64 values - const decodedValue = Buffer.from(snsMessageAttribute.Value, "base64").toString("ascii"); - headers = JSON.parse(decodedValue); + for (const record of event.Records) { + // Try to extract trace context from SNS wrapped in SQS + const body = record?.body; + if (body) { + const parsedBody = JSON.parse(body); + const snsMessageAttribute = parsedBody?.MessageAttributes?._datadog; + if (snsMessageAttribute?.Value) { + let headers; + if (snsMessageAttribute.Type === "String") { + headers = JSON.parse(snsMessageAttribute.Value); + } else { + // Try decoding base64 values + const decodedValue = Buffer.from(snsMessageAttribute.Value, "base64").toString("ascii"); + headers = JSON.parse(decodedValue); + } + + const traceContext = extractTraceContext(headers, this.tracerWrapper); + if (traceContext) { + spanContexts.push(...traceContext); + } + logDebug("Failed to extract trace context from SNS-SQS event"); } + } + // Check SQS message attributes as a fallback + const sqsMessageAttribute = event?.Records?.[0]?.messageAttributes?._datadog; + if (sqsMessageAttribute?.stringValue) { + const headers = JSON.parse(sqsMessageAttribute.stringValue); const traceContext = extractTraceContext(headers, this.tracerWrapper); if (traceContext) { - return traceContext; + spanContexts.push(...traceContext); } - logDebug("Failed to extract trace context from SNS-SQS event"); } - } - // Check SQS message attributes as a fallback - const sqsMessageAttribute = event?.Records?.[0]?.messageAttributes?._datadog; - if (sqsMessageAttribute?.stringValue) { - const headers = JSON.parse(sqsMessageAttribute.stringValue); - const traceContext = extractTraceContext(headers, this.tracerWrapper); - if (traceContext) { - return traceContext; + // Else try to extract trace context from attributes.AWSTraceHeader + // (Upstream Java apps can pass down Datadog trace context in the attributes.AWSTraceHeader in SQS case) + const awsTraceHeader = event?.Records?.[0]?.attributes?.AWSTraceHeader; + if (awsTraceHeader !== undefined) { + spanContexts.push(...extractFromAWSTraceHeader(awsTraceHeader, "SNS-SQS")); } - } - // Else try to extract trace context from attributes.AWSTraceHeader - // (Upstream Java apps can pass down Datadog trace context in the attributes.AWSTraceHeader in SQS case) - const awsTraceHeader = event?.Records?.[0]?.attributes?.AWSTraceHeader; - if (awsTraceHeader !== undefined) { - return extractFromAWSTraceHeader(awsTraceHeader, "SNS-SQS"); + // If span links is disabled only extract from the first record. + if (!this.config.useSpanLinks) { + break; + } } } catch (error) { handleExtractionError(error, "SQS"); } - return null; + return spanContexts; } } diff --git a/src/trace/context/extractors/sns.spec.ts b/src/trace/context/extractors/sns.spec.ts index a3ef1159..135db344 100644 --- a/src/trace/context/extractors/sns.spec.ts +++ b/src/trace/context/extractors/sns.spec.ts @@ -2,6 +2,8 @@ import { SNSEvent } from "aws-lambda"; import { TracerWrapper } from "../../tracer-wrapper"; import { SNSEventTraceExtractor } from "./sns"; import { StepFunctionContextService } from "../../step-function-service"; +import { TraceConfig } from "../../listener"; +import { EventBridgeSQSEventTraceExtractor } from "./event-bridge-sqs"; let mockSpanContext: any = null; @@ -72,10 +74,10 @@ describe("SNSEventTraceExtractor", () => { ], }; - const extractor = new SNSEventTraceExtractor(tracerWrapper); + const extractor = new SNSEventTraceExtractor(tracerWrapper, {} as TraceConfig); const traceContext = extractor.extract(payload); - expect(traceContext).not.toBeNull(); + expect(traceContext.length).toBe(1); expect(spyTracerWrapper).toHaveBeenCalledWith({ "x-datadog-parent-id": "4297634551783724228", @@ -84,10 +86,10 @@ describe("SNSEventTraceExtractor", () => { "x-datadog-trace-id": "6966585609680374559", }); - expect(traceContext?.toTraceId()).toBe("6966585609680374559"); - expect(traceContext?.toSpanId()).toBe("4297634551783724228"); - expect(traceContext?.sampleMode()).toBe("1"); - expect(traceContext?.source).toBe("event"); + expect(traceContext?.[0].toTraceId()).toBe("6966585609680374559"); + expect(traceContext?.[0].toSpanId()).toBe("4297634551783724228"); + expect(traceContext?.[0].sampleMode()).toBe("1"); + expect(traceContext?.[0].source).toBe("event"); }); it("extracts trace context with valid payload with Binary Value", () => { @@ -133,7 +135,7 @@ describe("SNSEventTraceExtractor", () => { ], }; - const extractor = new SNSEventTraceExtractor(tracerWrapper); + const extractor = new SNSEventTraceExtractor(tracerWrapper, {} as TraceConfig); const traceContext = extractor.extract(payload); expect(traceContext).not.toBeNull(); @@ -144,10 +146,178 @@ describe("SNSEventTraceExtractor", () => { "x-datadog-trace-id": "7102291628443134919", }); - expect(traceContext?.toTraceId()).toBe("7102291628443134919"); - expect(traceContext?.toSpanId()).toBe("4247550101648618618"); - expect(traceContext?.sampleMode()).toBe("1"); - expect(traceContext?.source).toBe("event"); + expect(traceContext?.[0].toTraceId()).toBe("7102291628443134919"); + expect(traceContext?.[0].toSpanId()).toBe("4247550101648618618"); + expect(traceContext?.[0].sampleMode()).toBe("1"); + expect(traceContext?.[0].source).toBe("event"); + }); + + + it("only extracts first trace context with multiple input payloads", () => { + mockSpanContext = { + toTraceId: () => "6966585609680374559", + toSpanId: () => "4297634551783724228", + _sampling: { + priority: "1", + }, + }; + const tracerWrapper = new TracerWrapper(); + + const payload: SNSEvent = { + Records: [ + { + EventSource: "aws:sns", + EventVersion: "1.0", + EventSubscriptionArn: + "arn:aws:sns:eu-west-1:601427279990:aj-js-library-test-dev-solo-topic:1bd19208-a99a-46d9-8398-f90f8699c641", + Sns: { + Type: "Notification", + MessageId: "f19d39fa-8c61-5df9-8f49-639247b6cece", + TopicArn: "arn:aws:sns:eu-west-1:601427279990:aj-js-library-test-dev-solo-topic", + Subject: undefined, + Message: '{"hello":"there","ajTimestamp":1643039127879}', + Timestamp: "2022-01-24T15:45:27.968Z", + SignatureVersion: "1", + Signature: + "mzp2Ou0fASw4LYRxY6SSww7qFfofn4luCJBRaTjLpQ5uhwhsAUKdyLz9VPD+/dlRbi1ImsWtIZ7A+wxj1oV7Z2Gyu/N4RpGalae37+jTluDS7AhjgcD7Bs4bgQtFkCfMFEwbhICQfukLLzbwbgczZ4NTPn6zj5o28c5NBKSJMYSnLz82ohw77GgnZ/m26E32ZQNW4+VCEMINg9Ne2rHstwPWRXPr5xGTrx8jH8CNUZnVpFVfhU8o+OSeAdpzm2l99grHIo7qPhekERxANz6QHynMlhdzD3UNSgc3oZkamZban/NEKd4MKJzgNQdNOYVj3Kw6eF2ZweEoBQ5sSFK5fQ==", + SigningCertUrl: + "https://sns.eu-west-1.amazonaws.com/SimpleNotificationService-************************33ab7e69.pem", + UnsubscribeUrl: + "https://sns.eu-west-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:eu-west-1:601427279990:aj-js-library-test-dev-solo-topic:1bd19208-a99a-46d9-8398-f90f8699c641", + MessageAttributes: { + _datadog: { + Type: "String", + Value: + '{"x-datadog-trace-id":"6966585609680374559","x-datadog-parent-id":"4297634551783724228","x-datadog-sampled":"1","x-datadog-sampling-priority":"1"}', + }, + }, + }, + }, + { + EventSource: "aws:sns", + EventVersion: "1.0", + EventSubscriptionArn: + "arn:aws:sns:eu-west-1:601427279990:aj-js-library-test-dev-solo-topic:1bd19208-a99a-46d9-8398-f90f8699c641", + Sns: { + Type: "Notification", + MessageId: "f19d39fa-8c61-5df9-8f49-639247b6cece", + TopicArn: "arn:aws:sns:eu-west-1:601427279990:aj-js-library-test-dev-solo-topic", + Subject: undefined, + Message: '{"hello":"there","ajTimestamp":1643039127879}', + Timestamp: "2022-01-24T15:45:27.968Z", + SignatureVersion: "1", + Signature: + "mzp2Ou0fASw4LYRxY6SSww7qFfofn4luCJBRaTjLpQ5uhwhsAUKdyLz9VPD+/dlRbi1ImsWtIZ7A+wxj1oV7Z2Gyu/N4RpGalae37+jTluDS7AhjgcD7Bs4bgQtFkCfMFEwbhICQfukLLzbwbgczZ4NTPn6zj5o28c5NBKSJMYSnLz82ohw77GgnZ/m26E32ZQNW4+VCEMINg9Ne2rHstwPWRXPr5xGTrx8jH8CNUZnVpFVfhU8o+OSeAdpzm2l99grHIo7qPhekERxANz6QHynMlhdzD3UNSgc3oZkamZban/NEKd4MKJzgNQdNOYVj3Kw6eF2ZweEoBQ5sSFK5fQ==", + SigningCertUrl: + "https://sns.eu-west-1.amazonaws.com/SimpleNotificationService-************************33ab7e69.pem", + UnsubscribeUrl: + "https://sns.eu-west-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:eu-west-1:601427279990:aj-js-library-test-dev-solo-topic:1bd19208-a99a-46d9-8398-f90f8699c641", + MessageAttributes: { + _datadog: { + Type: "String", + Value: + '{"x-datadog-trace-id":"6966585609680374559","x-datadog-parent-id":"4297634551783724228","x-datadog-sampled":"1","x-datadog-sampling-priority":"1"}', + }, + }, + }, + }, + ], + }; + + const extractor = new SNSEventTraceExtractor(tracerWrapper, {} as TraceConfig); + + const traceContext = extractor.extract(payload); + expect(traceContext.length).toBe(1); + + expect(spyTracerWrapper).toHaveBeenCalledWith({ + "x-datadog-parent-id": "4297634551783724228", + "x-datadog-sampled": "1", + "x-datadog-sampling-priority": "1", + "x-datadog-trace-id": "6966585609680374559", + }); + + expect(traceContext?.[0].toTraceId()).toBe("6966585609680374559"); + expect(traceContext?.[0].toSpanId()).toBe("4297634551783724228"); + expect(traceContext?.[0].sampleMode()).toBe("1"); + expect(traceContext?.[0].source).toBe("event"); + }); + + it("extract all trace contexts when span links is enabled", () => { + mockSpanContext = { + toTraceId: () => "6966585609680374559", + toSpanId: () => "4297634551783724228", + _sampling: { + priority: "1", + }, + }; + const tracerWrapper = new TracerWrapper(); + + const payload: SNSEvent = { + Records: [ + { + EventSource: "aws:sns", + EventVersion: "1.0", + EventSubscriptionArn: + "arn:aws:sns:eu-west-1:601427279990:aj-js-library-test-dev-solo-topic:1bd19208-a99a-46d9-8398-f90f8699c641", + Sns: { + Type: "Notification", + MessageId: "f19d39fa-8c61-5df9-8f49-639247b6cece", + TopicArn: "arn:aws:sns:eu-west-1:601427279990:aj-js-library-test-dev-solo-topic", + Subject: undefined, + Message: '{"hello":"there","ajTimestamp":1643039127879}', + Timestamp: "2022-01-24T15:45:27.968Z", + SignatureVersion: "1", + Signature: + "mzp2Ou0fASw4LYRxY6SSww7qFfofn4luCJBRaTjLpQ5uhwhsAUKdyLz9VPD+/dlRbi1ImsWtIZ7A+wxj1oV7Z2Gyu/N4RpGalae37+jTluDS7AhjgcD7Bs4bgQtFkCfMFEwbhICQfukLLzbwbgczZ4NTPn6zj5o28c5NBKSJMYSnLz82ohw77GgnZ/m26E32ZQNW4+VCEMINg9Ne2rHstwPWRXPr5xGTrx8jH8CNUZnVpFVfhU8o+OSeAdpzm2l99grHIo7qPhekERxANz6QHynMlhdzD3UNSgc3oZkamZban/NEKd4MKJzgNQdNOYVj3Kw6eF2ZweEoBQ5sSFK5fQ==", + SigningCertUrl: + "https://sns.eu-west-1.amazonaws.com/SimpleNotificationService-************************33ab7e69.pem", + UnsubscribeUrl: + "https://sns.eu-west-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:eu-west-1:601427279990:aj-js-library-test-dev-solo-topic:1bd19208-a99a-46d9-8398-f90f8699c641", + MessageAttributes: { + _datadog: { + Type: "String", + Value: + '{"x-datadog-trace-id":"6966585609680374559","x-datadog-parent-id":"4297634551783724228","x-datadog-sampled":"1","x-datadog-sampling-priority":"1"}', + }, + }, + }, + }, + { + EventSource: "aws:sns", + EventVersion: "1.0", + EventSubscriptionArn: + "arn:aws:sns:eu-west-1:601427279990:aj-js-library-test-dev-solo-topic:1bd19208-a99a-46d9-8398-f90f8699c641", + Sns: { + Type: "Notification", + MessageId: "f19d39fa-8c61-5df9-8f49-639247b6cece", + TopicArn: "arn:aws:sns:eu-west-1:601427279990:aj-js-library-test-dev-solo-topic", + Subject: undefined, + Message: '{"hello":"there","ajTimestamp":1643039127879}', + Timestamp: "2022-01-24T15:45:27.968Z", + SignatureVersion: "1", + Signature: + "mzp2Ou0fASw4LYRxY6SSww7qFfofn4luCJBRaTjLpQ5uhwhsAUKdyLz9VPD+/dlRbi1ImsWtIZ7A+wxj1oV7Z2Gyu/N4RpGalae37+jTluDS7AhjgcD7Bs4bgQtFkCfMFEwbhICQfukLLzbwbgczZ4NTPn6zj5o28c5NBKSJMYSnLz82ohw77GgnZ/m26E32ZQNW4+VCEMINg9Ne2rHstwPWRXPr5xGTrx8jH8CNUZnVpFVfhU8o+OSeAdpzm2l99grHIo7qPhekERxANz6QHynMlhdzD3UNSgc3oZkamZban/NEKd4MKJzgNQdNOYVj3Kw6eF2ZweEoBQ5sSFK5fQ==", + SigningCertUrl: + "https://sns.eu-west-1.amazonaws.com/SimpleNotificationService-************************33ab7e69.pem", + UnsubscribeUrl: + "https://sns.eu-west-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:eu-west-1:601427279990:aj-js-library-test-dev-solo-topic:1bd19208-a99a-46d9-8398-f90f8699c641", + MessageAttributes: { + _datadog: { + Type: "String", + Value: + '{"x-datadog-trace-id":"6966585609680374559","x-datadog-parent-id":"4297634551783724228","x-datadog-sampled":"1","x-datadog-sampling-priority":"1"}', + }, + }, + }, + }, + ], + }; + + const extractor = new SNSEventTraceExtractor(tracerWrapper, {useSpanLinks: true} as TraceConfig); + + const traceContext = extractor.extract(payload); + + expect(traceContext?.length).toBe(2); }); it.each([ @@ -157,15 +327,15 @@ describe("SNSEventTraceExtractor", () => { ["MessageAttributes in Sns", { Records: [{ Sns: "{}" }] }], ["_datadog in MessageAttributes", { Records: [{ Sns: '{"MessageAttributes":{"text":"Hello, world!"}}' }] }], ["Value in _datadog", { Records: [{ Sns: '{"MessageAttributes":{"_datadog":{}}}' }] }], - ])("returns null and skips extracting when payload is missing '%s'", (_, payload) => { + ])("returns an empty array and skips extracting when payload is missing '%s'", (_, payload) => { const tracerWrapper = new TracerWrapper(); - const extractor = new SNSEventTraceExtractor(tracerWrapper); + const extractor = new SNSEventTraceExtractor(tracerWrapper, {} as TraceConfig); const traceContext = extractor.extract(payload as any); - expect(traceContext).toBeNull(); + expect(traceContext).toStrictEqual([]); }); - it("returns null when extracted span context by tracer is null", () => { + it("returns an empty array when extracted span context by tracer is null", () => { const tracerWrapper = new TracerWrapper(); const payload: SNSEvent = { @@ -200,10 +370,10 @@ describe("SNSEventTraceExtractor", () => { ], }; - const extractor = new SNSEventTraceExtractor(tracerWrapper); + const extractor = new SNSEventTraceExtractor(tracerWrapper, {} as TraceConfig); const traceContext = extractor.extract(payload); - expect(traceContext).toBeNull(); + expect(traceContext).toStrictEqual([]); }); it("extracts trace context from AWSTraceHeader when no tracecontext found from payload", () => { @@ -241,7 +411,7 @@ describe("SNSEventTraceExtractor", () => { ], }; - const extractor = new SNSEventTraceExtractor(tracerWrapper); + const extractor = new SNSEventTraceExtractor(tracerWrapper, {} as TraceConfig); process.env["_X_AMZN_TRACE_ID"] = "Root=1-66393a26-0000000017acacbad335fb99;Parent=12f5e70cc905dfb7;Sampled=1;Lineage=48e79d5f:0"; const traceContext = extractor.extract(payload); @@ -252,10 +422,10 @@ describe("SNSEventTraceExtractor", () => { // 2. More importantly, DD_TRACE_PROPAGATION_STYLE could cause extraction fail expect(spyTracerWrapper).not.toHaveBeenCalled(); - expect(traceContext?.toTraceId()).toBe("1705928277274000281"); - expect(traceContext?.toSpanId()).toBe("1366252104075042743"); - expect(traceContext?.sampleMode()).toBe("1"); - expect(traceContext?.source).toBe("event"); + expect(traceContext?.[0].toTraceId()).toBe("1705928277274000281"); + expect(traceContext?.[0].toSpanId()).toBe("1366252104075042743"); + expect(traceContext?.[0].sampleMode()).toBe("1"); + expect(traceContext?.[0].source).toBe("event"); }); it("extracts trace context from Step Function SNS event", () => { @@ -298,16 +468,16 @@ describe("SNSEventTraceExtractor", () => { ], }; - const extractor = new SNSEventTraceExtractor(tracerWrapper); + const extractor = new SNSEventTraceExtractor(tracerWrapper, {} as TraceConfig); const traceContext = extractor.extract(payload); expect(traceContext).not.toBeNull(); // The StepFunctionContextService generates deterministic trace IDs - expect(traceContext?.toTraceId()).toBe("3995810302240690842"); - expect(traceContext?.toSpanId()).toBe("8347071195300897803"); - expect(traceContext?.sampleMode()).toBe("1"); - expect(traceContext?.source).toBe("event"); + expect(traceContext?.[0].toTraceId()).toBe("3995810302240690842"); + expect(traceContext?.[0].toSpanId()).toBe("8347071195300897803"); + expect(traceContext?.[0].sampleMode()).toBe("1"); + expect(traceContext?.[0].source).toBe("event"); }); }); }); diff --git a/src/trace/context/extractors/sns.ts b/src/trace/context/extractors/sns.ts index 7336f552..96e89aeb 100644 --- a/src/trace/context/extractors/sns.ts +++ b/src/trace/context/extractors/sns.ts @@ -5,40 +5,49 @@ import { EventTraceExtractor } from "../extractor"; import { SpanContextWrapper } from "../../span-context-wrapper"; import { AMZN_TRACE_ID_ENV_VAR } from "../../xray-service"; import { extractTraceContext, extractFromAWSTraceHeader, handleExtractionError } from "../extractor-utils"; +import { TraceConfig } from "trace/listener"; export class SNSEventTraceExtractor implements EventTraceExtractor { - constructor(private tracerWrapper: TracerWrapper) {} + constructor(private tracerWrapper: TracerWrapper, private config: TraceConfig) {} - extract(event: SNSEvent): SpanContextWrapper | null { + extract(event: SNSEvent): SpanContextWrapper[] { + const spanContexts: SpanContextWrapper[] = []; try { - // First try to extract trace context from message attributes - const messageAttribute = event?.Records?.[0]?.Sns?.MessageAttributes?._datadog; - if (messageAttribute?.Value) { - let headers; - if (messageAttribute.Type === "String") { - headers = JSON.parse(messageAttribute.Value); - } else { - // Try decoding base64 values - const decodedValue = Buffer.from(messageAttribute.Value, "base64").toString("ascii"); - headers = JSON.parse(decodedValue); + for (const record of event.Records) { + // First try to extract trace context from message attributes + const messageAttribute = record.Sns.MessageAttributes?._datadog; + if (messageAttribute?.Value) { + let headers; + if (messageAttribute.Type === "String") { + headers = JSON.parse(messageAttribute.Value); + } else { + // Try decoding base64 values + const decodedValue = Buffer.from(messageAttribute.Value, "base64").toString("ascii"); + headers = JSON.parse(decodedValue); + } + + const traceContext = extractTraceContext(headers, this.tracerWrapper); + if (traceContext) { + spanContexts.push(...traceContext); + } + logDebug("Failed to extract trace context from SNS event"); } - const traceContext = extractTraceContext(headers, this.tracerWrapper); - if (traceContext) { - return traceContext; + // Then try to extract trace context from _X_AMZN_TRACE_ID header (Upstream Java apps can + // pass down Datadog trace id (parent id wrong) in the env in SNS case) + if (process.env[AMZN_TRACE_ID_ENV_VAR]) { + spanContexts.push(...extractFromAWSTraceHeader(process.env[AMZN_TRACE_ID_ENV_VAR], "SNS")); } - logDebug("Failed to extract trace context from SNS event"); - } - // Then try to extract trace context from _X_AMZN_TRACE_ID header (Upstream Java apps can - // pass down Datadog trace id (parent id wrong) in the env in SNS case) - if (process.env[AMZN_TRACE_ID_ENV_VAR]) { - return extractFromAWSTraceHeader(process.env[AMZN_TRACE_ID_ENV_VAR], "SNS"); + // If span links is disabled only extract from the first record. + if (!this.config.useSpanLinks) { + break; + } } } catch (error) { handleExtractionError(error, "SNS"); } - return null; + return spanContexts; } } diff --git a/src/trace/context/extractors/sqs.spec.ts b/src/trace/context/extractors/sqs.spec.ts index 4a1d41cd..c19d03d7 100644 --- a/src/trace/context/extractors/sqs.spec.ts +++ b/src/trace/context/extractors/sqs.spec.ts @@ -2,6 +2,7 @@ import { SQSEvent } from "aws-lambda"; import { TracerWrapper } from "../../tracer-wrapper"; import { SQSEventTraceExtractor } from "./sqs"; import { StepFunctionContextService } from "../../step-function-service"; +import { TraceConfig } from "../../listener"; let mockSpanContext: any = null; @@ -68,7 +69,7 @@ describe("SQSEventTraceExtractor", () => { ], }; - const extractor = new SQSEventTraceExtractor(tracerWrapper); + const extractor = new SQSEventTraceExtractor(tracerWrapper, {} as TraceConfig); const traceContext = extractor.extract(payload); expect(traceContext).not.toBeNull(); @@ -80,10 +81,10 @@ describe("SQSEventTraceExtractor", () => { "x-datadog-trace-id": "4555236104497098341", }); - expect(traceContext?.toTraceId()).toBe("4555236104497098341"); - expect(traceContext?.toSpanId()).toBe("3369753143434738315"); - expect(traceContext?.sampleMode()).toBe("1"); - expect(traceContext?.source).toBe("event"); + expect(traceContext?.[0].toTraceId()).toBe("4555236104497098341"); + expect(traceContext?.[0].toSpanId()).toBe("3369753143434738315"); + expect(traceContext?.[0].sampleMode()).toBe("1"); + expect(traceContext?.[0].source).toBe("event"); }); it("extracts trace context from _datadog binaryValue when raw message delivery is used", () => { @@ -131,16 +132,166 @@ describe("SQSEventTraceExtractor", () => { ], }; - const extractor = new SQSEventTraceExtractor(tracerWrapper); + const extractor = new SQSEventTraceExtractor(tracerWrapper, {} as TraceConfig); const traceContext = extractor.extract(payload); expect(traceContext).not.toBeNull(); expect(spyTracerWrapper).toHaveBeenCalledWith(ddHeaders); - expect(traceContext?.toTraceId()).toBe("1234567890"); - expect(traceContext?.toSpanId()).toBe("0987654321"); - expect(traceContext?.sampleMode()).toBe("1"); - expect(traceContext?.source).toBe("event"); + expect(traceContext?.[0].toTraceId()).toBe("1234567890"); + expect(traceContext?.[0].toSpanId()).toBe("0987654321"); + expect(traceContext?.[0].sampleMode()).toBe("1"); + expect(traceContext?.[0].source).toBe("event"); + }); + + it("only extracts first trace context with multiple input payloads", () => { + mockSpanContext = { + toTraceId: () => "4555236104497098341", + toSpanId: () => "3369753143434738315", + _sampling: { + priority: "1", + }, + }; + const tracerWrapper = new TracerWrapper(); + + const payload: SQSEvent = { + Records: [ + { + body: "Hello world", + attributes: { + ApproximateReceiveCount: "1", + SentTimestamp: "1605544528092", + SenderId: "AROAYYB64AB3JHSRKO6XR:sqs-trace-dev-producer", + ApproximateFirstReceiveTimestamp: "1605544528094", + }, + messageAttributes: { + _datadog: { + stringValue: + '{"x-datadog-trace-id":"4555236104497098341","x-datadog-parent-id":"3369753143434738315","x-datadog-sampled":"1","x-datadog-sampling-priority":"1"}', + stringListValues: undefined, + binaryListValues: undefined, + dataType: "String", + }, + }, + eventSource: "aws:sqs", + eventSourceARN: "arn:aws:sqs:eu-west-1:601427279990:metal-queue", + awsRegion: "eu-west-1", + messageId: "foo", + md5OfBody: "x", + receiptHandle: "x", + }, + { + body: "Hello world", + attributes: { + ApproximateReceiveCount: "1", + SentTimestamp: "1605544528092", + SenderId: "AROAYYB64AB3JHSRKO6XR:sqs-trace-dev-producer", + ApproximateFirstReceiveTimestamp: "1605544528094", + }, + messageAttributes: { + _datadog: { + stringValue: + '{"x-datadog-trace-id":"4555236104497098341","x-datadog-parent-id":"3369753143434738315","x-datadog-sampled":"1","x-datadog-sampling-priority":"1"}', + stringListValues: undefined, + binaryListValues: undefined, + dataType: "String", + }, + }, + eventSource: "aws:sqs", + eventSourceARN: "arn:aws:sqs:eu-west-1:601427279990:metal-queue", + awsRegion: "eu-west-1", + messageId: "foo", + md5OfBody: "x", + receiptHandle: "x", + }, + ], + }; + + const extractor = new SQSEventTraceExtractor(tracerWrapper, {} as TraceConfig); + + const traceContext = extractor.extract(payload); + expect(traceContext.length).toBe(1); + + expect(spyTracerWrapper).toHaveBeenCalledWith({ + "x-datadog-parent-id": "3369753143434738315", + "x-datadog-sampled": "1", + "x-datadog-sampling-priority": "1", + "x-datadog-trace-id": "4555236104497098341", + }); + + expect(traceContext?.[0].toTraceId()).toBe("4555236104497098341"); + expect(traceContext?.[0].toSpanId()).toBe("3369753143434738315"); + expect(traceContext?.[0].sampleMode()).toBe("1"); + expect(traceContext?.[0].source).toBe("event"); + }); + + it("extract all trace contexts when span links is enabled", () => { + mockSpanContext = { + toTraceId: () => "4555236104497098341", + toSpanId: () => "3369753143434738315", + _sampling: { + priority: "1", + }, + }; + const tracerWrapper = new TracerWrapper(); + + const payload: SQSEvent = { + Records: [ + { + body: "Hello world", + attributes: { + ApproximateReceiveCount: "1", + SentTimestamp: "1605544528092", + SenderId: "AROAYYB64AB3JHSRKO6XR:sqs-trace-dev-producer", + ApproximateFirstReceiveTimestamp: "1605544528094", + }, + messageAttributes: { + _datadog: { + stringValue: + '{"x-datadog-trace-id":"4555236104497098341","x-datadog-parent-id":"3369753143434738315","x-datadog-sampled":"1","x-datadog-sampling-priority":"1"}', + stringListValues: undefined, + binaryListValues: undefined, + dataType: "String", + }, + }, + eventSource: "aws:sqs", + eventSourceARN: "arn:aws:sqs:eu-west-1:601427279990:metal-queue", + awsRegion: "eu-west-1", + messageId: "foo", + md5OfBody: "x", + receiptHandle: "x", + }, + { + body: "Hello world", + attributes: { + ApproximateReceiveCount: "1", + SentTimestamp: "1605544528092", + SenderId: "AROAYYB64AB3JHSRKO6XR:sqs-trace-dev-producer", + ApproximateFirstReceiveTimestamp: "1605544528094", + }, + messageAttributes: { + _datadog: { + stringValue: + '{"x-datadog-trace-id":"4555236104497098341","x-datadog-parent-id":"3369753143434738315","x-datadog-sampled":"1","x-datadog-sampling-priority":"1"}', + stringListValues: undefined, + binaryListValues: undefined, + dataType: "String", + }, + }, + eventSource: "aws:sqs", + eventSourceARN: "arn:aws:sqs:eu-west-1:601427279990:metal-queue", + awsRegion: "eu-west-1", + messageId: "foo", + md5OfBody: "x", + receiptHandle: "x", + }, + ], + }; + + const extractor = new SQSEventTraceExtractor(tracerWrapper, {useSpanLinks: true} as TraceConfig); + + const traceContext = extractor.extract(payload); + expect(traceContext.length).toBe(2); }); it.each([ @@ -149,15 +300,15 @@ describe("SQSEventTraceExtractor", () => { ["messageAttributes in first entry", { Records: [{ messageAttributes: "{}" }] }], ["_datadog in messageAttributes", { Records: [{ messageAttributes: {} }] }], ["stringValue in _datadog", { Records: [{ messageAttributes: { _datadog: {} } }] }], - ])("returns null and skips extracting when payload is missing '%s'", (_, payload) => { + ])("returns an empty array and skips extracting when payload is missing '%s'", (_, payload) => { const tracerWrapper = new TracerWrapper(); - const extractor = new SQSEventTraceExtractor(tracerWrapper); + const extractor = new SQSEventTraceExtractor(tracerWrapper, {} as TraceConfig); const traceContext = extractor.extract(payload as any); - expect(traceContext).toBeNull(); + expect(traceContext).toStrictEqual([]); }); - it("returns null when extracted span context by tracer is null", () => { + it("returns an empty array when extracted span context by tracer is null", () => { const tracerWrapper = new TracerWrapper(); const payload: SQSEvent = { @@ -180,10 +331,10 @@ describe("SQSEventTraceExtractor", () => { }, ], }; - const extractor = new SQSEventTraceExtractor(tracerWrapper); + const extractor = new SQSEventTraceExtractor(tracerWrapper, {} as TraceConfig); const traceContext = extractor.extract(payload); - expect(traceContext).toBeNull(); + expect(traceContext).toStrictEqual([]); }); it("extracts trace context from AWSTraceHeader with valid payload", () => { @@ -210,7 +361,7 @@ describe("SQSEventTraceExtractor", () => { ], }; - const extractor = new SQSEventTraceExtractor(tracerWrapper); + const extractor = new SQSEventTraceExtractor(tracerWrapper, {} as TraceConfig); const traceContext = extractor.extract(payload); expect(traceContext).not.toBeNull(); @@ -219,10 +370,10 @@ describe("SQSEventTraceExtractor", () => { // 2. More importantly, DD_TRACE_PROPAGATION_STYLE could cause extraction fail expect(spyTracerWrapper).not.toHaveBeenCalled(); - expect(traceContext?.toTraceId()).toBe("625397077193750208"); - expect(traceContext?.toSpanId()).toBe("6538302989251745223"); - expect(traceContext?.sampleMode()).toBe("1"); - expect(traceContext?.source).toBe("event"); + expect(traceContext?.[0].toTraceId()).toBe("625397077193750208"); + expect(traceContext?.[0].toSpanId()).toBe("6538302989251745223"); + expect(traceContext?.[0].sampleMode()).toBe("1"); + expect(traceContext?.[0].source).toBe("event"); }); it("extracts trace context from Step Function SQS event", () => { @@ -262,16 +413,16 @@ describe("SQSEventTraceExtractor", () => { ], }; - const extractor = new SQSEventTraceExtractor(tracerWrapper); + const extractor = new SQSEventTraceExtractor(tracerWrapper, {} as TraceConfig); const traceContext = extractor.extract(payload); expect(traceContext).not.toBeNull(); // The StepFunctionContextService generates deterministic trace IDs - expect(traceContext?.toTraceId()).toBe("7148114900282288397"); - expect(traceContext?.toSpanId()).toBe("6711327198021343353"); - expect(traceContext?.sampleMode()).toBe("1"); - expect(traceContext?.source).toBe("event"); + expect(traceContext?.[0].toTraceId()).toBe("7148114900282288397"); + expect(traceContext?.[0].toSpanId()).toBe("6711327198021343353"); + expect(traceContext?.[0].sampleMode()).toBe("1"); + expect(traceContext?.[0].source).toBe("event"); }); }); }); diff --git a/src/trace/context/extractors/sqs.ts b/src/trace/context/extractors/sqs.ts index f4b883e4..f634980d 100644 --- a/src/trace/context/extractors/sqs.ts +++ b/src/trace/context/extractors/sqs.ts @@ -4,46 +4,55 @@ import { logDebug } from "../../../utils"; import { EventTraceExtractor } from "../extractor"; import { SpanContextWrapper } from "../../span-context-wrapper"; import { extractTraceContext, extractFromAWSTraceHeader, handleExtractionError } from "../extractor-utils"; +import { TraceConfig } from "trace/listener"; export class SQSEventTraceExtractor implements EventTraceExtractor { - constructor(private tracerWrapper: TracerWrapper) {} + constructor(private tracerWrapper: TracerWrapper, private config: TraceConfig) {} - extract(event: SQSEvent): SpanContextWrapper | null { + extract(event: SQSEvent): SpanContextWrapper[] { logDebug("SQS Extractor Being Used"); + const spanContexts: SpanContextWrapper[] = []; try { - // First try to extract trace context from message attributes - let headers = event?.Records?.[0]?.messageAttributes?._datadog?.stringValue; - - if (!headers) { - // Then try to get from binary value. This happens when SNS->SQS, but SNS has raw message delivery enabled. - // In this case, SNS maps any messageAttributes to the SQS messageAttributes. - // We can at least get trace context from SQS, but we won't be able to create the SNS inferred span. - const encodedTraceContext = event?.Records?.[0]?.messageAttributes?._datadog?.binaryValue; - if (encodedTraceContext) { - headers = Buffer.from(encodedTraceContext, "base64").toString("ascii"); + for (const record of event.Records) { + // First try to extract trace context from message attributes + let headers = record?.messageAttributes?._datadog?.stringValue; + + if (!headers) { + // Then try to get from binary value. This happens when SNS->SQS, but SNS has raw message delivery enabled. + // In this case, SNS maps any messageAttributes to the SQS messageAttributes. + // We can at least get trace context from SQS, but we won't be able to create the SNS inferred span. + const encodedTraceContext = event?.Records?.[0]?.messageAttributes?._datadog?.binaryValue; + if (encodedTraceContext) { + headers = Buffer.from(encodedTraceContext, "base64").toString("ascii"); + } } - } - if (headers) { - const parsedHeaders = JSON.parse(headers); + if (headers) { + const parsedHeaders = JSON.parse(headers); - const traceContext = extractTraceContext(parsedHeaders, this.tracerWrapper); - if (traceContext) { - return traceContext; + const traceContext = extractTraceContext(parsedHeaders, this.tracerWrapper); + if (traceContext) { + spanContexts.push(...traceContext); + } + logDebug("Failed to extract trace context from SQS event"); } - logDebug("Failed to extract trace context from SQS event"); - } - // Else try to extract trace context from attributes.AWSTraceHeader - // (Upstream Java apps can pass down Datadog trace context in the attributes.AWSTraceHeader in SQS case) - const awsTraceHeader = event?.Records?.[0]?.attributes?.AWSTraceHeader; - if (awsTraceHeader !== undefined) { - return extractFromAWSTraceHeader(awsTraceHeader, "SQS"); + // Else try to extract trace context from attributes.AWSTraceHeader + // (Upstream Java apps can pass down Datadog trace context in the attributes.AWSTraceHeader in SQS case) + const awsTraceHeader = event?.Records?.[0]?.attributes?.AWSTraceHeader; + if (awsTraceHeader !== undefined) { + spanContexts.push(...extractFromAWSTraceHeader(awsTraceHeader, "SQS")); + } + + // If span links is disabled only extract from the first record. + if (!this.config.useSpanLinks) { + break; + } } } catch (error) { handleExtractionError(error, "SQS"); } - return null; + return spanContexts; } } diff --git a/src/trace/context/extractors/step-function.spec.ts b/src/trace/context/extractors/step-function.spec.ts index 6007daeb..37193c48 100644 --- a/src/trace/context/extractors/step-function.spec.ts +++ b/src/trace/context/extractors/step-function.spec.ts @@ -55,10 +55,10 @@ describe("StepFunctionEventTraceExtractor", () => { const traceContext = extractor.extract(payload); expect(traceContext).not.toBeNull(); - expect(traceContext?.toTraceId()).toBe("435175499815315247"); - expect(traceContext?.toSpanId()).toBe("3929055471293792800"); - expect(traceContext?.sampleMode()).toBe("1"); - expect(traceContext?.source).toBe("event"); + expect(traceContext?.[0].toTraceId()).toBe("435175499815315247"); + expect(traceContext?.[0].toSpanId()).toBe("3929055471293792800"); + expect(traceContext?.[0].sampleMode()).toBe("1"); + expect(traceContext?.[0].source).toBe("event"); }); // https://github.com/DataDog/logs-backend/blob/c17618cb552fc369ca40282bae0a65803f82f694/domains/serverless/apps/logs-to-traces-reducer/src/test/resources/test-json-files/stepfunctions/RedriveTest/snapshots/RedriveLambdaSuccessTraceMerging.json#L46 @@ -72,10 +72,10 @@ describe("StepFunctionEventTraceExtractor", () => { const traceContext = extractor.extract(redrivePayload); expect(traceContext).not.toBeNull(); - expect(traceContext?.toTraceId()).toBe("435175499815315247"); - expect(traceContext?.toSpanId()).toBe("8782364156266188026"); - expect(traceContext?.sampleMode()).toBe("1"); - expect(traceContext?.source).toBe("event"); + expect(traceContext?.[0].toTraceId()).toBe("435175499815315247"); + expect(traceContext?.[0].toSpanId()).toBe("8782364156266188026"); + expect(traceContext?.[0].sampleMode()).toBe("1"); + expect(traceContext?.[0].source).toBe("event"); }); it("extracts trace context with valid payload when instance wasn't initialized", () => { @@ -85,10 +85,10 @@ describe("StepFunctionEventTraceExtractor", () => { const traceContext = extractor.extract(payload); expect(traceContext).not.toBeNull(); - expect(traceContext?.toTraceId()).toBe("435175499815315247"); - expect(traceContext?.toSpanId()).toBe("3929055471293792800"); - expect(traceContext?.sampleMode()).toBe("1"); - expect(traceContext?.source).toBe("event"); + expect(traceContext?.[0].toTraceId()).toBe("435175499815315247"); + expect(traceContext?.[0].toSpanId()).toBe("3929055471293792800"); + expect(traceContext?.[0].sampleMode()).toBe("1"); + expect(traceContext?.[0].source).toBe("event"); }); it("extracts trace context with valid legacy lambda payload", () => { @@ -101,17 +101,17 @@ describe("StepFunctionEventTraceExtractor", () => { const traceContext = extractor.extract({ Payload: payload }); expect(traceContext).not.toBeNull(); - expect(traceContext?.toTraceId()).toBe("435175499815315247"); - expect(traceContext?.toSpanId()).toBe("3929055471293792800"); - expect(traceContext?.sampleMode()).toBe("1"); - expect(traceContext?.source).toBe("event"); + expect(traceContext?.[0].toTraceId()).toBe("435175499815315247"); + expect(traceContext?.[0].toSpanId()).toBe("3929055471293792800"); + expect(traceContext?.[0].sampleMode()).toBe("1"); + expect(traceContext?.[0].source).toBe("event"); }); - it("returns null when StepFunctionContextService.context is undefined", async () => { + it("returns empty array when StepFunctionContextService.context is undefined", async () => { const extractor = new StepFunctionEventTraceExtractor(); const traceContext = extractor.extract({}); - expect(traceContext).toBeNull(); + expect(traceContext).toStrictEqual([]); }); it("extracts trace context end-to-end with deterministic IDs for v1 nested context", () => { @@ -136,16 +136,16 @@ describe("StepFunctionEventTraceExtractor", () => { const traceContext = extractor.extract(nestedPayload); expect(traceContext).not.toBeNull(); - expect(traceContext?.source).toBe("event"); + expect(traceContext?.[0].source).toBe("event"); // Verify trace ID is based on root execution ID (deterministic) - const traceId = traceContext?.toTraceId(); + const traceId = traceContext?.[0].toTraceId(); expect(traceId).toBeDefined(); expect(traceId).not.toBe("0"); // Extract again to verify deterministic behavior const traceContext2 = extractor.extract(nestedPayload); - expect(traceContext2?.toTraceId()).toBe(traceId); + expect(traceContext2?.[0].toTraceId()).toBe(traceId); }); it("extracts trace context end-to-end with propagated IDs for v1 lambda root context", () => { @@ -171,19 +171,19 @@ describe("StepFunctionEventTraceExtractor", () => { const traceContext = extractor.extract(lambdaRootPayload); expect(traceContext).not.toBeNull(); - expect(traceContext?.source).toBe("event"); + expect(traceContext?.[0].source).toBe("event"); // Verify trace ID comes from propagated headers, not deterministic hash - expect(traceContext?.toTraceId()).toBe("1234567890123456789"); + expect(traceContext?.[0].toTraceId()).toBe("1234567890123456789"); // Verify deterministic span ID based on execution details - const spanId = traceContext?.toSpanId(); + const spanId = traceContext?.[0].toSpanId(); expect(spanId).toBeDefined(); expect(spanId).not.toBe("0"); // Extract again to verify deterministic span ID const traceContext2 = extractor.extract(lambdaRootPayload); - expect(traceContext2?.toSpanId()).toBe(spanId); + expect(traceContext2?.[0].toSpanId()).toBe(spanId); }); }); }); diff --git a/src/trace/context/extractors/step-function.ts b/src/trace/context/extractors/step-function.ts index 3fb0b0f9..d6457c78 100644 --- a/src/trace/context/extractors/step-function.ts +++ b/src/trace/context/extractors/step-function.ts @@ -3,7 +3,7 @@ import { StepFunctionContextService } from "../../step-function-service"; import { EventTraceExtractor } from "../extractor"; export class StepFunctionEventTraceExtractor implements EventTraceExtractor { - extract(event: any): SpanContextWrapper | null { + extract(event: any): SpanContextWrapper[] { // Probably StepFunctionContextService hasn't been called const stepFunctionInstance = StepFunctionContextService.instance(event); const stepFunctionContext = stepFunctionInstance.context; @@ -11,10 +11,10 @@ export class StepFunctionEventTraceExtractor implements EventTraceExtractor { if (stepFunctionContext !== undefined) { const spanContext = stepFunctionInstance.spanContext; if (spanContext !== null) { - return spanContext; + return [spanContext]; } } - return null; + return []; } } From abdf8576236f84a5f35e4a1b05d97a225924bb08 Mon Sep 17 00:00:00 2001 From: "james.eastham" Date: Thu, 4 Sep 2025 13:32:46 +0100 Subject: [PATCH 3/4] feat: add span links to integration test --- integration_tests/serverless.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/integration_tests/serverless.yml b/integration_tests/serverless.yml index 49486eb3..64fc18aa 100644 --- a/integration_tests/serverless.yml +++ b/integration_tests/serverless.yml @@ -71,6 +71,17 @@ functions: environment: DD_FLUSH_TO_LOG: true + # process-input-traced + with-span-links-traced_node: + name: integration-tests-js-${sls:stage}-with-span-links-traced_${env:RUNTIME} + handler: process-input-traced.handle + runtime: ${env:SERVERLESS_RUNTIME} + layers: + - { Ref: NodeLambdaLayer } + environment: + DD_FLUSH_TO_LOG: true + DD_USE_SPAN_LINKS: true + # throw-error-traced throw-error-traced_node: name: integration-tests-js-${sls:stage}-throw-error-traced_${env:RUNTIME} From 322670de8951b43164b9d0ee01405d68d4dafecd Mon Sep 17 00:00:00 2001 From: "james.eastham" Date: Thu, 4 Sep 2025 13:32:57 +0100 Subject: [PATCH 4/4] feat: handle span links in wrapper --- src/index.spec.ts | 8 +- src/index.ts | 2 +- src/trace/context/extractor-utils.spec.ts | 6 +- src/trace/context/extractor-utils.ts | 14 +-- src/trace/context/extractor.spec.ts | 122 +++++++++++----------- src/trace/context/extractor.ts | 58 ++++++---- src/trace/listener.spec.ts | 16 +-- src/trace/listener.ts | 27 ++--- src/trace/patch-console.spec.ts | 8 +- src/trace/patch-console.ts | 6 +- src/trace/patch-http.spec.ts | 18 ++-- src/trace/patch-http.ts | 6 +- src/trace/trace-context-service.spec.ts | 36 +++---- src/trace/trace-context-service.ts | 34 +++--- 14 files changed, 193 insertions(+), 168 deletions(-) diff --git a/src/index.spec.ts b/src/index.spec.ts index d1b545f4..7eca6242 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -49,19 +49,19 @@ let mockTraceSource: TraceSource | undefined = undefined; jest.mock("./trace/trace-context-service", () => { class MockTraceContextService { - extract(event: any, context: Context): SpanContextWrapper { - return mockSpanContextWrapper; + extract(event: any, context: Context): SpanContextWrapper[] { + return [mockSpanContextWrapper]; } get traceSource() { return mockTraceSource; } get currentTraceContext() { - return mockSpanContextWrapper; + return [mockSpanContextWrapper]; } get currentTraceHeaders() { - return mockTraceHeaders; + return [mockTraceHeaders]; } } return { diff --git a/src/index.ts b/src/index.ts index ee898d33..e60ad19f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -324,7 +324,7 @@ export function getTraceHeaders(): Partial { if (currentTraceListener === undefined) { return {}; } - return currentTraceListener.currentTraceHeaders; + return currentTraceListener.currentTraceHeaders[0]; } function getConfig(userConfig?: Partial): Config { diff --git a/src/trace/context/extractor-utils.spec.ts b/src/trace/context/extractor-utils.spec.ts index afe577e5..204e914a 100644 --- a/src/trace/context/extractor-utils.spec.ts +++ b/src/trace/context/extractor-utils.spec.ts @@ -37,7 +37,7 @@ describe("extractor-utils", () => { expect(result).not.toBeNull(); }); - it("returns null when no trace context can be extracted", () => { + it("returns an empty array when no trace context can be extracted", () => { const emptyEvent = { someOtherProperty: "value", }; @@ -45,7 +45,7 @@ describe("extractor-utils", () => { const tracerWrapper = new TracerWrapper(); const result = extractTraceContext(emptyEvent, tracerWrapper); - expect(result).toBeNull(); + expect(result).toStrictEqual([]); }); it("extracts context from LambdaRootStepFunctionContext", () => { @@ -158,7 +158,7 @@ describe("extractor-utils", () => { const result = extractFromAWSTraceHeader(invalidHeader, eventType); - expect(result).toBeNull(); + expect(result).toStrictEqual([]); }); }); }); diff --git a/src/trace/context/extractor-utils.ts b/src/trace/context/extractor-utils.ts index 5f0f1e17..a8c0faa4 100644 --- a/src/trace/context/extractor-utils.ts +++ b/src/trace/context/extractor-utils.ts @@ -14,11 +14,11 @@ import { XrayService } from "../xray-service"; * @param tracerWrapper The tracer wrapper instance * @returns SpanContextWrapper or null */ -export function extractTraceContext(headers: any, tracerWrapper: TracerWrapper): SpanContextWrapper | null { +export function extractTraceContext(headers: any, tracerWrapper: TracerWrapper): SpanContextWrapper[] { // First try to extract as regular trace headers const traceContext = tracerWrapper.extract(headers); if (traceContext) { - return traceContext; + return [traceContext]; } // If that fails, check if this is a Step Function context @@ -26,11 +26,11 @@ export function extractTraceContext(headers: any, tracerWrapper: TracerWrapper): if (stepFunctionInstance.context !== undefined) { if (stepFunctionInstance.spanContext !== null) { - return stepFunctionInstance.spanContext; + return [stepFunctionInstance.spanContext]; } } - return null; + return []; } /** @@ -39,14 +39,14 @@ export function extractTraceContext(headers: any, tracerWrapper: TracerWrapper): * @param eventType The type of event (for logging) * @returns SpanContextWrapper or null */ -export function extractFromAWSTraceHeader(awsTraceHeader: string, eventType: string): SpanContextWrapper | null { +export function extractFromAWSTraceHeader(awsTraceHeader: string, eventType: string): SpanContextWrapper[] { const traceContext = XrayService.extraceDDContextFromAWSTraceHeader(awsTraceHeader); if (traceContext) { logDebug(`Extracted trace context from ${eventType} event attributes AWSTraceHeader`); - return traceContext; + return [traceContext]; } else { logDebug(`No Datadog trace context found from ${eventType} event attributes AWSTraceHeader`); - return null; + return []; } } diff --git a/src/trace/context/extractor.spec.ts b/src/trace/context/extractor.spec.ts index 37cae4a3..ef99e77d 100644 --- a/src/trace/context/extractor.spec.ts +++ b/src/trace/context/extractor.spec.ts @@ -141,10 +141,10 @@ describe("TraceContextExtractor", () => { expect(spyCustomExtractor).toHaveBeenCalled(); - expect(traceContext?.toTraceId()).toBe("4110911582297405551"); - expect(traceContext?.toSpanId()).toBe("797643193680388251"); - expect(traceContext?.sampleMode()).toBe("2"); - expect(traceContext?.source).toBe("event"); + expect(traceContext?.[0].toTraceId()).toBe("4110911582297405551"); + expect(traceContext?.[0].toSpanId()).toBe("797643193680388251"); + expect(traceContext?.[0].sampleMode()).toBe("2"); + expect(traceContext?.[0].source).toBe("event"); }); }); describe("event", () => { @@ -180,10 +180,10 @@ describe("TraceContextExtractor", () => { "x-datadog-trace-id": "4110911582297405551", }); - expect(traceContext?.toTraceId()).toBe("4110911582297405551"); - expect(traceContext?.toSpanId()).toBe("797643193680388251"); - expect(traceContext?.sampleMode()).toBe("2"); - expect(traceContext?.source).toBe("event"); + expect(traceContext?.[0].toTraceId()).toBe("4110911582297405551"); + expect(traceContext?.[0].toSpanId()).toBe("797643193680388251"); + expect(traceContext?.[0].sampleMode()).toBe("2"); + expect(traceContext?.[0].source).toBe("event"); }); // SNS message event (String Value) @@ -242,10 +242,10 @@ describe("TraceContextExtractor", () => { "x-datadog-trace-id": "6966585609680374559", }); - expect(traceContext?.toTraceId()).toBe("6966585609680374559"); - expect(traceContext?.toSpanId()).toBe("4297634551783724228"); - expect(traceContext?.sampleMode()).toBe("1"); - expect(traceContext?.source).toBe("event"); + expect(traceContext?.[0].toTraceId()).toBe("6966585609680374559"); + expect(traceContext?.[0].toSpanId()).toBe("4297634551783724228"); + expect(traceContext?.[0].sampleMode()).toBe("1"); + expect(traceContext?.[0].source).toBe("event"); }); // SNS message event (Binary Value) @@ -303,10 +303,10 @@ describe("TraceContextExtractor", () => { "x-datadog-trace-id": "7102291628443134919", }); - expect(traceContext?.toTraceId()).toBe("7102291628443134919"); - expect(traceContext?.toSpanId()).toBe("4247550101648618618"); - expect(traceContext?.sampleMode()).toBe("1"); - expect(traceContext?.source).toBe("event"); + expect(traceContext?.[0].toTraceId()).toBe("7102291628443134919"); + expect(traceContext?.[0].toSpanId()).toBe("4247550101648618618"); + expect(traceContext?.[0].sampleMode()).toBe("1"); + expect(traceContext?.[0].source).toBe("event"); }); // SNS message delivered to SQS queue event (String Value) @@ -354,10 +354,10 @@ describe("TraceContextExtractor", () => { "x-datadog-trace-id": "2776434475358637757", }); - expect(traceContext?.toTraceId()).toBe("2776434475358637757"); - expect(traceContext?.toSpanId()).toBe("4493917105238181843"); - expect(traceContext?.sampleMode()).toBe("1"); - expect(traceContext?.source).toBe("event"); + expect(traceContext?.[0].toTraceId()).toBe("2776434475358637757"); + expect(traceContext?.[0].toSpanId()).toBe("4493917105238181843"); + expect(traceContext?.[0].sampleMode()).toBe("1"); + expect(traceContext?.[0].source).toBe("event"); }); // SNS message delivered to SQS queue event (Binary Value) @@ -404,10 +404,10 @@ describe("TraceContextExtractor", () => { "x-datadog-trace-id": "7102291628443134919", }); - expect(traceContext?.toTraceId()).toBe("7102291628443134919"); - expect(traceContext?.toSpanId()).toBe("4247550101648618618"); - expect(traceContext?.sampleMode()).toBe("1"); - expect(traceContext?.source).toBe("event"); + expect(traceContext?.[0].toTraceId()).toBe("7102291628443134919"); + expect(traceContext?.[0].toSpanId()).toBe("4247550101648618618"); + expect(traceContext?.[0].sampleMode()).toBe("1"); + expect(traceContext?.[0].source).toBe("event"); }); // EventBridge message delivered to SQS queue event @@ -456,10 +456,10 @@ describe("TraceContextExtractor", () => { "x-datadog-trace-id": "7379586022458917877", }); - expect(traceContext?.toTraceId()).toBe("7379586022458917877"); - expect(traceContext?.toSpanId()).toBe("2644033662113726488"); - expect(traceContext?.sampleMode()).toBe("1"); - expect(traceContext?.source).toBe("event"); + expect(traceContext?.[0].toTraceId()).toBe("7379586022458917877"); + expect(traceContext?.[0].toSpanId()).toBe("2644033662113726488"); + expect(traceContext?.[0].sampleMode()).toBe("1"); + expect(traceContext?.[0].source).toBe("event"); }); // AppSync event @@ -496,10 +496,10 @@ describe("TraceContextExtractor", () => { "x-datadog-trace-id": "4110911582297405557", }); - expect(traceContext?.toTraceId()).toBe("797643193680388254"); - expect(traceContext?.toSpanId()).toBe("4110911582297405557"); - expect(traceContext?.sampleMode()).toBe("2"); - expect(traceContext?.source).toBe("event"); + expect(traceContext?.[0].toTraceId()).toBe("797643193680388254"); + expect(traceContext?.[0].toSpanId()).toBe("4110911582297405557"); + expect(traceContext?.[0].sampleMode()).toBe("2"); + expect(traceContext?.[0].source).toBe("event"); }); // SQS queue message event @@ -554,10 +554,10 @@ describe("TraceContextExtractor", () => { "x-datadog-trace-id": "4555236104497098341", }); - expect(traceContext?.toTraceId()).toBe("4555236104497098341"); - expect(traceContext?.toSpanId()).toBe("3369753143434738315"); - expect(traceContext?.sampleMode()).toBe("1"); - expect(traceContext?.source).toBe("event"); + expect(traceContext?.[0].toTraceId()).toBe("4555236104497098341"); + expect(traceContext?.[0].toSpanId()).toBe("3369753143434738315"); + expect(traceContext?.[0].sampleMode()).toBe("1"); + expect(traceContext?.[0].source).toBe("event"); }); // Kinesis stream event @@ -604,10 +604,10 @@ describe("TraceContextExtractor", () => { "x-datadog-trace-id": "667309514221035538", }); - expect(traceContext?.toTraceId()).toBe("667309514221035538"); - expect(traceContext?.toSpanId()).toBe("1350735035497811828"); - expect(traceContext?.sampleMode()).toBe("1"); - expect(traceContext?.source).toBe("event"); + expect(traceContext?.[0].toTraceId()).toBe("667309514221035538"); + expect(traceContext?.[0].toSpanId()).toBe("1350735035497811828"); + expect(traceContext?.[0].sampleMode()).toBe("1"); + expect(traceContext?.[0].source).toBe("event"); }); // EventBridge message event @@ -651,10 +651,10 @@ describe("TraceContextExtractor", () => { "x-datadog-sampling-priority": "1", }); - expect(traceContext?.toTraceId()).toBe("5827606813695714842"); - expect(traceContext?.toSpanId()).toBe("4726693487091824375"); - expect(traceContext?.sampleMode()).toBe("1"); - expect(traceContext?.source).toBe("event"); + expect(traceContext?.[0].toTraceId()).toBe("5827606813695714842"); + expect(traceContext?.[0].toSpanId()).toBe("4726693487091824375"); + expect(traceContext?.[0].sampleMode()).toBe("1"); + expect(traceContext?.[0].source).toBe("event"); }); // StepFunction context event @@ -688,10 +688,10 @@ describe("TraceContextExtractor", () => { const traceContext = await extractor.extract(event, {} as Context); expect(traceContext).not.toBeNull(); - expect(traceContext?.toTraceId()).toBe("1139193989631387307"); - expect(traceContext?.toSpanId()).toBe("7747304477664363642"); - expect(traceContext?.sampleMode()).toBe("1"); - expect(traceContext?.source).toBe("event"); + expect(traceContext?.[0].toTraceId()).toBe("1139193989631387307"); + expect(traceContext?.[0].toSpanId()).toBe("7747304477664363642"); + expect(traceContext?.[0].sampleMode()).toBe("1"); + expect(traceContext?.[0].source).toBe("event"); }); it("extracts and applies deterministic trace ID from StepFunction event end-to-end", async () => { @@ -721,25 +721,25 @@ describe("TraceContextExtractor", () => { expect(traceContext).not.toBeNull(); // Verify the trace context was extracted from Step Function - expect(traceContext?.source).toBe("event"); + expect(traceContext?.[0].source).toBe("event"); // Verify the trace ID is deterministic based on execution ID // The trace ID should be generated from SHA256 hash of the execution ID - const traceId = traceContext?.toTraceId(); + const traceId = traceContext?.[0].toTraceId(); expect(traceId).toBeDefined(); expect(traceId).not.toBe("0"); // Should not be zero expect(traceId).toMatch(/^\d+$/); // Should be numeric string // Verify the span ID is deterministic based on execution ID, state name, and entered time - const spanId = traceContext?.toSpanId(); + const spanId = traceContext?.[0].toSpanId(); expect(spanId).toBeDefined(); expect(spanId).not.toBe("0"); // Should not be zero expect(spanId).toMatch(/^\d+$/); // Should be numeric string // Verify that extracting the same event produces the same trace IDs (deterministic) const traceContext2 = await extractor.extract(event, {} as Context); - expect(traceContext2?.toTraceId()).toBe(traceId); - expect(traceContext2?.toSpanId()).toBe(spanId); + expect(traceContext2?.[0].toTraceId()).toBe(traceId); + expect(traceContext2?.[0].toSpanId()).toBe(spanId); }); }); @@ -775,10 +775,10 @@ describe("TraceContextExtractor", () => { "x-datadog-trace-id": "667309514221035538", }); - expect(traceContext?.toTraceId()).toBe("667309514221035538"); - expect(traceContext?.toSpanId()).toBe("1350735035497811828"); - expect(traceContext?.sampleMode()).toBe("1"); - expect(traceContext?.source).toBe("event"); + expect(traceContext?.[0].toTraceId()).toBe("667309514221035538"); + expect(traceContext?.[0].toSpanId()).toBe("1350735035497811828"); + expect(traceContext?.[0].sampleMode()).toBe("1"); + expect(traceContext?.[0].source).toBe("event"); }); }); }); @@ -794,10 +794,10 @@ describe("TraceContextExtractor", () => { const traceContext = await extractor.extract({}, {} as Context); expect(traceContext).not.toBeNull(); - expect(traceContext?.toTraceId()).toBe("4110911582297405557"); - expect(traceContext?.toSpanId()).toBe("797643193680388254"); - expect(traceContext?.sampleMode()).toBe("2"); - expect(traceContext?.source).toBe("xray"); + expect(traceContext?.[0].toTraceId()).toBe("4110911582297405557"); + expect(traceContext?.[0].toSpanId()).toBe("797643193680388254"); + expect(traceContext?.[0].sampleMode()).toBe("2"); + expect(traceContext?.[0].source).toBe("xray"); }); }); }); diff --git a/src/trace/context/extractor.ts b/src/trace/context/extractor.ts index 35150a2c..687cca79 100644 --- a/src/trace/context/extractor.ts +++ b/src/trace/context/extractor.ts @@ -25,7 +25,7 @@ export const DATADOG_PARENT_ID_HEADER = "x-datadog-parent-id"; export const DATADOG_SAMPLING_PRIORITY_HEADER = "x-datadog-sampling-priority"; export interface EventTraceExtractor { - extract(event: any): SpanContextWrapper | null; + extract(event: any): SpanContextWrapper[]; } export interface DatadogTraceHeaders { @@ -42,40 +42,58 @@ export class TraceContextExtractor { this.xrayService = new XrayService(); } - async extract(event: any, context: Context): Promise { - let spanContext: SpanContextWrapper | null = null; + async extract(event: any, context: Context): Promise { + let spanContexts: SpanContextWrapper[] = []; if (this.config.traceExtractor) { const customExtractor = new CustomTraceExtractor(this.config.traceExtractor); - spanContext = await customExtractor.extract(event, context); + const spanContext = await customExtractor.extract(event, context); + + if (spanContext !== null){ + spanContexts.push(spanContext); + } } - if (spanContext === null) { + if (spanContexts === null || spanContexts.length === 0) { const eventExtractor = this.getTraceEventExtractor(event); if (eventExtractor !== undefined) { - spanContext = eventExtractor.extract(event); + spanContexts.push(...eventExtractor.extract(event)); } } - - if (spanContext === null) { + + if (spanContexts === null || spanContexts.length === 0) { this.stepFunctionContextService = StepFunctionContextService.instance(event); if (this.stepFunctionContextService?.context) { const extractor = new StepFunctionEventTraceExtractor(); - spanContext = extractor?.extract(event); + let spanContext = extractor?.extract(event); + + if (spanContext !== null) { + spanContexts.push(...spanContext); + } } } - if (spanContext === null) { + if (spanContexts.length === 0) { const contextExtractor = new LambdaContextTraceExtractor(this.tracerWrapper); - spanContext = contextExtractor.extract(context); + let spanContext = contextExtractor.extract(context); + + if (spanContext !== null) { + spanContexts.push(...spanContext); + } } - if (spanContext !== null) { - this.addTraceContextToXray(spanContext); + if (spanContexts.length > 0) { + this.addTraceContextToXray(spanContexts[0]); + + return spanContexts; + } - return spanContext; + const xRayContext = this.xrayService.extract(); + + if (xRayContext !== null) { + spanContexts.push(xRayContext); } - return this.xrayService.extract(); + return spanContexts; } private getTraceEventExtractor(event: any): EventTraceExtractor | undefined { @@ -86,12 +104,12 @@ export class TraceContextExtractor { return new HTTPEventTraceExtractor(this.tracerWrapper, this.config.decodeAuthorizerContext); } - if (EventValidator.isSNSEvent(event)) return new SNSEventTraceExtractor(this.tracerWrapper); - if (EventValidator.isSNSSQSEvent(event)) return new SNSSQSEventTraceExtractor(this.tracerWrapper); - if (EventValidator.isEventBridgeSQSEvent(event)) return new EventBridgeSQSEventTraceExtractor(this.tracerWrapper); + if (EventValidator.isSNSEvent(event)) return new SNSEventTraceExtractor(this.tracerWrapper, this.config); + if (EventValidator.isSNSSQSEvent(event)) return new SNSSQSEventTraceExtractor(this.tracerWrapper, this.config); + if (EventValidator.isEventBridgeSQSEvent(event)) return new EventBridgeSQSEventTraceExtractor(this.tracerWrapper, this.config); if (EventValidator.isAppSyncResolverEvent(event)) return new AppSyncEventTraceExtractor(this.tracerWrapper); - if (EventValidator.isSQSEvent(event)) return new SQSEventTraceExtractor(this.tracerWrapper); - if (EventValidator.isKinesisStreamEvent(event)) return new KinesisEventTraceExtractor(this.tracerWrapper); + if (EventValidator.isSQSEvent(event)) return new SQSEventTraceExtractor(this.tracerWrapper, this.config); + if (EventValidator.isKinesisStreamEvent(event)) return new KinesisEventTraceExtractor(this.tracerWrapper, this.config); if (EventValidator.isEventBridgeEvent(event)) return new EventBridgeEventTraceExtractor(this.tracerWrapper); return; diff --git a/src/trace/listener.spec.ts b/src/trace/listener.spec.ts index 91dcc761..0d4dad46 100644 --- a/src/trace/listener.spec.ts +++ b/src/trace/listener.spec.ts @@ -158,9 +158,9 @@ describe("TraceListener", () => { priority: "2", }, }; - mockSpanContextWrapper = { + mockSpanContextWrapper = [{ spanContext: mockSpanContext, - }; + }]; await listener.onStartInvocation({}, context as any); const unwrappedFunc = () => {}; const wrappedFunc = listener.onWrap(unwrappedFunc); @@ -203,9 +203,9 @@ describe("TraceListener", () => { priority: "2", }, }; - mockSpanContextWrapper = { + mockSpanContextWrapper = [{ spanContext: mockSpanContext, - }; + }]; await listener.onStartInvocation({}, context as any); const unwrappedFunc = () => {}; const wrappedFunc = listener.onWrap(unwrappedFunc); @@ -281,9 +281,9 @@ describe("TraceListener", () => { priority: "2", }, }; - mockSpanContextWrapper = { + mockSpanContextWrapper = [{ spanContext: mockSpanContext, - }; + }]; await listener.onStartInvocation({}, context as any); const unwrappedFunc = () => {}; @@ -384,9 +384,9 @@ describe("TraceListener", () => { priority: "1", }, }; - mockSpanContextWrapper = { + mockSpanContextWrapper = [{ spanContext: mockSpanContext, - }; + }]; const stepFunctionSQSEvent = { Records: [ diff --git a/src/trace/listener.ts b/src/trace/listener.ts index 91d380e9..6408ed3e 100644 --- a/src/trace/listener.ts +++ b/src/trace/listener.ts @@ -90,7 +90,7 @@ export class TraceListener { private inferredSpan?: SpanWrapper; private wrappedCurrentSpan?: SpanWrapper; private triggerTags?: { [key: string]: string }; - private lambdaSpanParentContext?: SpanContext; + private lambdaSpanParentContexts?: SpanContext[]; private spanPointerAttributesList: SpanPointerAttributes[] | undefined; public get currentTraceHeaders() { @@ -122,9 +122,11 @@ export class TraceListener { // The aws.lambda span needs to have a parented to the Datadog trace context from the // incoming event if available or the X-Ray trace context if hybrid tracing is enabled const spanContextWrapper = await this.contextService.extract(event, context); - let parentSpanContext: SpanContext | undefined; + // Then incoming event may have multiple span contexts + let parentSpanContexts: SpanContext[] = []; + if (this.contextService.traceSource === TraceSource.Event || this.config.mergeDatadogXrayTraces) { - parentSpanContext = tracerInitialized ? spanContextWrapper?.spanContext : undefined; + parentSpanContexts = tracerInitialized ? spanContextWrapper?.map((sc) => sc.spanContext) : []; logDebug("Attempting to find parent for the aws.lambda span"); } else { logDebug("Didn't attempt to find parent for aws.lambda span", { @@ -132,16 +134,18 @@ export class TraceListener { traceSource: this.contextService.traceSource, }); } + if (this.config.createInferredSpan) { this.inferredSpan = this.inferrer.createInferredSpan( event, context, - parentSpanContext, + // Create inferred spans with the first parent span context + parentSpanContexts[0], this.config.encodeAuthorizerContext, ); } - this.lambdaSpanParentContext = this.inferredSpan?.span || parentSpanContext; + this.lambdaSpanParentContexts = this.inferredSpan ? [this.inferredSpan?.span] : parentSpanContexts; this.context = context; const eventSource = parseEventSource(event); this.triggerTags = extractTriggerTags(event, context, eventSource); @@ -320,15 +324,12 @@ export class TraceListener { ...this.stepFunctionContext, }; } - if (this.lambdaSpanParentContext) { - if (this.config.useSpanLinks){ - options.links = [ - { - context: this.lambdaSpanParentContext, - }, - ]; + + if (this.lambdaSpanParentContexts && this.lambdaSpanParentContexts.length > 0) { + if (this.config.useSpanLinks) { + options.links = this.lambdaSpanParentContexts?.map((psc) => ({ context: psc })); } else { - options.childOf = this.lambdaSpanParentContext; + options.childOf = this.lambdaSpanParentContexts?.[0]; } } options.type = "serverless"; diff --git a/src/trace/patch-console.spec.ts b/src/trace/patch-console.spec.ts index 6d8a087d..7851ae20 100644 --- a/src/trace/patch-console.spec.ts +++ b/src/trace/patch-console.spec.ts @@ -28,13 +28,13 @@ describe("patchConsole", () => { trace = jest.fn(); cnsole = { log, info, debug, error, warn, trace } as any; contextService = new TraceContextService(traceWrapper as any, {} as any); - contextService["rootTraceContext"] = { + contextService["rootTraceContexts"] = [{ spanContext: {}, toTraceId: () => "123456", toSpanId: () => "78910", sampleMode: () => SampleMode.USER_KEEP, source: TraceSource.Event, - } as SpanContextWrapper; + }]; }); afterEach(() => { @@ -73,7 +73,7 @@ describe("patchConsole", () => { }); it("doesn't inject trace context when none is present", () => { - contextService["rootTraceContext"] = undefined as any; + contextService["rootTraceContexts"] = [] as any; patchConsole(cnsole as any, contextService); cnsole.log("Hello"); expect(log).toHaveBeenCalledWith("Hello"); @@ -92,7 +92,7 @@ describe("patchConsole", () => { ); }); it("leaves empty message unmodified when there is no trace context", () => { - contextService["rootTraceContext"] = undefined as any; + contextService["rootTraceContexts"] = [] as any; patchConsole(cnsole as any, contextService); cnsole.log(); expect(log).toHaveBeenCalledWith(); diff --git a/src/trace/patch-console.ts b/src/trace/patch-console.ts index 1d4c4438..bc76e2ca 100644 --- a/src/trace/patch-console.ts +++ b/src/trace/patch-console.ts @@ -56,9 +56,9 @@ function patchMethod(mod: wrappedConsole, method: LogMethod, contextService: Tra setLogLevel(LogLevel.NONE); try { const context = contextService.currentTraceContext; - if (context !== null) { - const traceId = context.toTraceId(); - const parentId = context.toSpanId(); + if (context.length > 0) { + const traceId = context[0].toTraceId(); + const parentId = context[0].toSpanId(); prefix = `[dd.trace_id=${traceId} dd.span_id=${parentId}]`; if (arguments.length === 0) { arguments.length = 1; diff --git a/src/trace/patch-http.spec.ts b/src/trace/patch-http.spec.ts index 650d3227..8081940d 100644 --- a/src/trace/patch-http.spec.ts +++ b/src/trace/patch-http.spec.ts @@ -33,13 +33,15 @@ describe("patchHttp", () => { beforeEach(() => { contextService = new TraceContextService(traceWrapper as any, {} as any); - contextService["rootTraceContext"] = { - spanContext: {}, - toTraceId: () => "123456", - toSpanId: () => "78910", - sampleMode: () => SampleMode.USER_KEEP, - source: TraceSource.Event, - } as SpanContextWrapper; + contextService["rootTraceContexts"] = [ + { + spanContext: {}, + toTraceId: () => "123456", + toSpanId: () => "78910", + sampleMode: () => SampleMode.USER_KEEP, + source: TraceSource.Event, + }, + ]; setLogLevel(LogLevel.NONE); }); @@ -130,7 +132,7 @@ describe("patchHttp", () => { it("doesn't inject tracing headers when context is empty", () => { nock("http://www.example.com").get("/").reply(200, {}); - contextService["rootTraceContext"] = null as any; + contextService["rootTraceContexts"] = [] as any; patchHttp(contextService); const req = http.request("http://www.example.com"); const headers = req.getHeaders(); diff --git a/src/trace/patch-http.ts b/src/trace/patch-http.ts index d755b4bf..cb00ee5f 100644 --- a/src/trace/patch-http.ts +++ b/src/trace/patch-http.ts @@ -104,9 +104,11 @@ function getRequestOptionsWithTraceContext( headers = {}; } const traceHeaders = traceService.currentTraceHeaders; + + // If HTTP request use first trace header headers = { ...headers, - ...traceHeaders, + ...(traceHeaders ? traceHeaders[0] : {}), }; const requestOpts = { ...options, @@ -115,7 +117,7 @@ function getRequestOptionsWithTraceContext( // Logging all http requests during integration tests let's // us track traffic in our test snapshots if (isIntegrationTest()) { - _logHttpRequest(requestOpts, traceHeaders); + _logHttpRequest(requestOpts, traceHeaders[0]); } return requestOpts; } diff --git a/src/trace/trace-context-service.spec.ts b/src/trace/trace-context-service.spec.ts index 01fbf815..0d43539c 100644 --- a/src/trace/trace-context-service.spec.ts +++ b/src/trace/trace-context-service.spec.ts @@ -30,53 +30,53 @@ describe("TraceContextService", () => { source: TraceSource.Event, spanContext: spanContext, }; - traceContextService["rootTraceContext"] = { + traceContextService["rootTraceContexts"] = [{ toTraceId: () => "123456", toSpanId: () => "abcdef", sampleMode: () => 1, source: TraceSource.Event, spanContext: spanContext, - }; + }]; const currentTraceContext = traceContextService.currentTraceContext; - expect(currentTraceContext?.toTraceId()).toBe("123456"); - expect(currentTraceContext?.toSpanId()).toBe("78910"); - expect(currentTraceContext?.sampleMode()).toBe(1); - expect(currentTraceContext?.source).toBe("event"); + expect(currentTraceContext?.[0].toTraceId()).toBe("123456"); + expect(currentTraceContext?.[0].toSpanId()).toBe("78910"); + expect(currentTraceContext?.[0].sampleMode()).toBe(1); + expect(currentTraceContext?.[0].source).toBe("event"); }); it("uses parent trace parent id when trace id is invalid", () => { mockXRayShouldThrow = true; mockXRaySegment = { id: "0b11cc", }; - traceContextService["rootTraceContext"] = { + traceContextService["rootTraceContexts"] = [{ toTraceId: () => "123456", toSpanId: () => "abcdef", sampleMode: () => 1, source: TraceSource.Xray, spanContext: spanContext, - }; + }]; const currentTraceContext = traceContextService.currentTraceContext; - expect(currentTraceContext?.toTraceId()).toBe("123456"); - expect(currentTraceContext?.toSpanId()).toBe("abcdef"); - expect(currentTraceContext?.sampleMode()).toBe(1); - expect(currentTraceContext?.source).toBe("xray"); + expect(currentTraceContext?.[0].toTraceId()).toBe("123456"); + expect(currentTraceContext?.[0].toSpanId()).toBe("abcdef"); + expect(currentTraceContext?.[0].sampleMode()).toBe(1); + expect(currentTraceContext?.[0].source).toBe("xray"); }); it("uses parent trace parent id when no datadog trace context is available and xray throws", () => { mockXRayShouldThrow = true; - traceContextService["rootTraceContext"] = { + traceContextService["rootTraceContexts"] = [{ toTraceId: () => "123456", toSpanId: () => "abcdef", sampleMode: () => 1, source: TraceSource.Xray, spanContext: spanContext, - }; + }]; const currentTraceContext = traceContextService.currentTraceContext; - expect(currentTraceContext?.toTraceId()).toBe("123456"); - expect(currentTraceContext?.toSpanId()).toBe("abcdef"); - expect(currentTraceContext?.sampleMode()).toBe(1); - expect(currentTraceContext?.source).toBe("xray"); + expect(currentTraceContext?.[0].toTraceId()).toBe("123456"); + expect(currentTraceContext?.[0].toSpanId()).toBe("abcdef"); + expect(currentTraceContext?.[0].sampleMode()).toBe(1); + expect(currentTraceContext?.[0].source).toBe("xray"); }); }); diff --git a/src/trace/trace-context-service.ts b/src/trace/trace-context-service.ts index 8e1d2b55..83c56948 100644 --- a/src/trace/trace-context-service.ts +++ b/src/trace/trace-context-service.ts @@ -42,44 +42,46 @@ export interface TraceContext { export type TraceExtractor = (event: any, context: Context) => Promise | TraceContext; export class TraceContextService { - public rootTraceContext: SpanContextWrapper | null = null; + public rootTraceContexts: SpanContextWrapper[] = []; private traceExtractor: TraceContextExtractor; constructor(private tracerWrapper: TracerWrapper, private config: TraceConfig) { this.traceExtractor = new TraceContextExtractor(this.tracerWrapper, this.config); } - async extract(event: any, context: Context): Promise { - this.rootTraceContext = await this.traceExtractor?.extract(event, context); + async extract(event: any, context: Context): Promise { + this.rootTraceContexts = await this.traceExtractor?.extract(event, context); return this.currentTraceContext; } - get currentTraceHeaders(): Partial { - const traceContext = this.currentTraceContext as SpanContextWrapper; - if (traceContext === null) return {}; + get currentTraceHeaders(): Partial[] { + const traceContext = this.currentTraceContext; + if (traceContext === null) return [{}]; - return { - [DATADOG_TRACE_ID_HEADER]: traceContext.toTraceId(), - [DATADOG_PARENT_ID_HEADER]: traceContext.toSpanId(), - [DATADOG_SAMPLING_PRIORITY_HEADER]: traceContext.sampleMode().toString(), - }; + return traceContext.map((tc) => { + return { + [DATADOG_TRACE_ID_HEADER]: tc.toTraceId(), + [DATADOG_PARENT_ID_HEADER]: tc.toSpanId(), + [DATADOG_SAMPLING_PRIORITY_HEADER]: tc.sampleMode().toString(), + }; + }); } - get currentTraceContext(): SpanContextWrapper | null { - if (this.rootTraceContext === null) return null; + get currentTraceContext(): SpanContextWrapper[] { + if (this.rootTraceContexts === null || this.rootTraceContexts.length === 0) return []; - const traceContext = this.rootTraceContext; + const traceContext = this.rootTraceContexts; const currentDatadogContext = this.tracerWrapper.traceContext(); if (currentDatadogContext) { logDebug(`set trace context from dd-trace with parent ${currentDatadogContext.toTraceId()}`); - return currentDatadogContext; + return [currentDatadogContext]; } return traceContext; } get traceSource() { - return this.rootTraceContext !== null ? this.rootTraceContext?.source : null; + return this.rootTraceContexts.length > 0 ? this.rootTraceContexts[0].source : null; } }