aws / aws-xray-sdk-node

The official AWS X-Ray SDK for Node.js.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Xray doesn't create `Resource Names` for DynamoDB `TransactWriteItems` operation

scorsi opened this issue · comments

Hello there,

I was working on a continuation of the PR #527, after I saw that TransactWriteItems operations had never sent the Table Names of the impacted query (as it could do for BatchGetItem or BatchWriteItem).

I so created a patch on the aws-xray-sdk-core package like so:

see the patch file

This is also taking changes from the PR #527.

diff --git a/node_modules/aws-xray-sdk-core/dist/lib/patchers/aws3_p.js b/node_modules/aws-xray-sdk-core/dist/lib/patchers/aws3_p.js
index 74a7aa5..935d15e 100644
--- a/node_modules/aws-xray-sdk-core/dist/lib/patchers/aws3_p.js
+++ b/node_modules/aws-xray-sdk-core/dist/lib/patchers/aws3_p.js
@@ -13,15 +13,17 @@ const logger = require('../logger');
 const { safeParseInt } = require('../utils');
 const utils_1 = require("../utils");
 const XRAY_PLUGIN_NAME = 'XRaySDKInstrumentation';
-const buildAttributesFromMetadata = async (service, operation, region, res, error) => {
+const buildAttributesFromMetadata = async (service, operation, region, commandInput, res, error) => {
     var _a, _b, _c;
     const { extendedRequestId, requestId, httpStatusCode: statusCode, attempts } = ((_a = res === null || res === void 0 ? void 0 : res.output) === null || _a === void 0 ? void 0 : _a.$metadata) || (error === null || error === void 0 ? void 0 : error.$metadata);
     const aws = new aws_1.default({
         extendedRequestId,
         requestId,
         retryCount: attempts,
+        data: res?.output,
         request: {
             operation,
+            params: commandInput,
             httpRequest: {
                 region,
                 statusCode,
@@ -60,10 +62,12 @@ function addFlags(http, subsegment, err) {
 const getXRayMiddleware = (config, manualSegment) => (next, context) => async (args) => {
     const segment = contextUtils.isAutomaticMode() ? contextUtils.resolveSegment() : manualSegment;
     const { clientName, commandName } = context;
-    const operation = commandName.slice(0, -7); // Strip trailing "Command" string
+    const { input: commandInput } = args;
+    const commandOperation = commandName.slice(0, -7); // Strip trailing "Command" string
+    const operation = commandOperation.charAt(0).toLowerCase() + commandOperation.slice(1);
     const service = clientName.slice(0, -6); // Strip trailing "Client" string
     if (!segment) {
-        const output = service + '.' + operation.charAt(0).toLowerCase() + operation.slice(1);
+        const output = service + '.' + operation;
         if (!contextUtils.isAutomaticMode()) {
             logger.getLogger().info('Call ' + output + ' requires a segment object' +
                 ' passed to captureAWSv3Client for tracing in manual mode. Ignoring.');
@@ -88,7 +92,7 @@ const getXRayMiddleware = (config, manualSegment) => (next, context) => async (a
         if (!res) {
             throw new Error('Failed to get response from instrumented AWS Client.');
         }
-        const [aws, http] = await buildAttributesFromMetadata(service, operation, await config.region(), res, null);
+        const [aws, http] = await buildAttributesFromMetadata(service, operation, await config.region(), commandInput, res, null);
         subsegment.addAttribute('aws', aws);
         subsegment.addAttribute('http', http);
         addFlags(http, subsegment);
@@ -97,7 +101,7 @@ const getXRayMiddleware = (config, manualSegment) => (next, context) => async (a
     }
     catch (err) {
         if (err.$metadata) {
-            const [aws, http] = await buildAttributesFromMetadata(service, operation, await config.region(), null, err);
+            const [aws, http] = await buildAttributesFromMetadata(service, operation, await config.region(), commandInput, null, err);
             subsegment.addAttribute('aws', aws);
             subsegment.addAttribute('http', http);
             addFlags(http, subsegment, err);
diff --git a/node_modules/aws-xray-sdk-core/dist/lib/patchers/call_capturer.js b/node_modules/aws-xray-sdk-core/dist/lib/patchers/call_capturer.js
index 410b7e6..6efe03b 100644
--- a/node_modules/aws-xray-sdk-core/dist/lib/patchers/call_capturer.js
+++ b/node_modules/aws-xray-sdk-core/dist/lib/patchers/call_capturer.js
@@ -94,6 +94,14 @@ function captureDescriptors(descriptors, params, data) {
             if (attributes.list && attributes.get_count) {
                 paramData = params[paramName] ? params[paramName].length : 0;
             }
+            else if (attributes.list && attributes.get_key) {
+                paramData = Object.entries(params[paramName]).reduce(function (acc, [_, v]) {
+                    var v2 = Array.isArray(attributes.in)
+                        ? Object.keys(v).filter((k)=> attributes.in.includes(k)).map((k)=> v[k][attributes.get_key])
+                        : [v[attributes.in][attributes.get_key]];
+                    return v2.reduce((acc, v)=> acc.includes(v) ? acc : [...acc, v], acc);
+                }, []);
+            }
             else {
                 paramData = attributes.get_keys === true ? Object.keys(params[paramName]) : params[paramName];
             }
diff --git a/node_modules/aws-xray-sdk-core/dist/lib/resources/aws_whitelist.json b/node_modules/aws-xray-sdk-core/dist/lib/resources/aws_whitelist.json
index 3de583d..9adf1d5 100644
--- a/node_modules/aws-xray-sdk-core/dist/lib/resources/aws_whitelist.json
+++ b/node_modules/aws-xray-sdk-core/dist/lib/resources/aws_whitelist.json
@@ -25,6 +25,20 @@
             "ItemCollectionMetrics"
           ]
         },
+        "transactWriteItems": {
+          "request_descriptors": {
+            "TransactItems": {
+              "list": true,
+              "in": ["Put", "Update", "Delete"],
+              "get_key": "TableName",
+              "rename_to": "table_names"
+            }
+          },
+          "response_parameters": [
+            "ConsumedCapacity",
+            "ItemCollectionMetrics"
+          ]
+        },
         "createTable": {
           "request_parameters": [
             "GlobalSecondaryIndexes",

What to remember about this change is that I created a new request_descriptors type :

{
  "[paramName]": {
    "list": true,
    "in": [ "[innerObjectKey]" ],
    "get_key": "[KeyToGet inside innerObjectKey]",
    "rename_to": "[rename_to]"
  }
}

Like so:

{
  "TransactItems": {
    "list": true,
    "in": ["Put", "Update", "Delete"],
    "get_key": "TableName",
    "rename_to": "table_names"
  }
}
see my manual test using the `captureDescriptors` in JS
function captureDescriptors(descriptors, params, data) {
  for (var paramName in descriptors) {
    var attributes = descriptors[paramName];

    if (typeof params[paramName] !== "undefined") {
      var paramData;

      if (attributes.list && attributes.get_count) {
        paramData = params[paramName] ? params[paramName].length : 0;
      } else if (attributes.list && attributes.get_key) {
        paramData = Object.entries(params[paramName]).reduce(function(acc, [_, v]) {
          var v2 = Array.isArray(attributes.in)
            ? Object.keys(v).filter((k) => attributes.in.includes(k)).map((k) => v[k][attributes.get_key])
            : [v[attributes.in][attributes.get_key]];
          return v2.reduce((acc, v) => acc.includes(v) ? acc : [...acc, v], acc);
        }, []);
      } else {
        paramData = attributes.get_keys === true ? Object.keys(params[paramName]) : params[paramName];
      }

      if (typeof attributes.rename_to === "string") {
        data[attributes.rename_to] = paramData;
      } else {
        data[paramName] = paramData;
      }
    }
  }
}

let data = {};

captureDescriptors({
  "TransactItems": {
    "list": true,
    "in": ["Put", "Update", "Delete"],
    "get_key": "TableName",
    "rename_to": "table_names"
  }
}, {
  "TransactItems": [
    { "Update": { "TableName": "Table1" } },
    { "Update": { "TableName": "Table2" } },
    { "Delete": { "TableName": "Table1" } },
    { "Put": { "TableName": "Table1" } },
    { "Update": { "TableName": "Table3" } }
  ]
}, data);

console.log(data);
// prints: { table_names: [ 'Table1', 'Table2', 'Table3' ] }

Unfortunately, this is not working:
(the node with the resource name is not in a transaction, this is to show the difference between the two operations)
Capture d’écran 2022-09-12 à 23 25 51

A unique GetItem operation has a property Resource names and Table name which are equal:

Details for `GetItem` operation Capture d’écran 2022-09-12 à 23 27 47
2022-09-12T21:00:36.539Z e921c988-3e7b-412e-b46e-1aecf6a257e5 DEBUG UDP message sent: {"id":"d83c378d49d48dc3","name":"DynamoDB","start_time":1663016436.26,"namespace":"aws","aws":{"operation":"GetItem","region":"eu-west-1","request_id":"Q2UO7QFOG0TI7D38ECKQB4B80VVV4KQNSO5AEMVJF66Q9ASUAAJG","retries":1,"table_name":"IdentityService-Table"},"http":{"response":{"status":200,"content_length":635}},"end_time":1663016436.441,"type":"subsegment","parent_id":"f742f905cc868ab3","trace_id":"1-631f9df3-142735ad488b3c9d380906f9"}
Capture d’écran 2022-09-12 à 23 36 09

But for my custom transaction we can see my custom Table names but no Resource names...

Details for `TransactWriteItems` operation Capture d’écran 2022-09-12 à 23 29 15
2022-09-12T21:00:36.599Z e921c988-3e7b-412e-b46e-1aecf6a257e5 DEBUG UDP message sent: {"id":"d6408cbb04f4a8dc","name":"DynamoDB","start_time":1663016436.499,"namespace":"aws","aws":{"operation":"TransactWriteItems","region":"eu-west-1","request_id":"H1BCEJ8R56T9TLUE8PQH3VCE7JVV4KQNSO5AEMVJF66Q9ASUAAJG","retries":1,"table_names":["IdentityService-Table"]},"http":{"response":{"status":200,"content_length":2}},"end_time":1663016436.541,"type":"subsegment","parent_id":"f742f905cc868ab3","trace_id":"1-631f9df3-142735ad488b3c9d380906f9"}
Capture d’écran 2022-09-12 à 23 37 13

Tell me if I'm wrong but I think that Resource names field is created either internally in Xray inside AWS or by DynamoDB itself, I think that only this field is used to create nodes inside the service map, and if no Resource names field is present it fallbacks to "Service Name" name field.
I can see that the DynamoDB trace has the same Parent-ID than my Subsegment-ID trace sent from Lambda. They seems to be linked together.
I tried to manually create and pass resource_names field through the event but it wasn't taken into account in Xray.

Or maybe is it because of the difference here:

  • GetItem is linked to a DynamoDB AWS::DynamoDB::Table type trace ;
  • while TransactWriteItems is linked to a DynamoDB AWS::DynamoDB type trace.

Capture d’écran 2022-09-12 à 23 47 02

Edit : After some tries, it seems that this is the issue, BatchWriteItem and BatchGetItem are also linked to a DynamoDB AWS::DynamoDB::Table type trace... I think this issue is internal to DynamoDB... 😭
Capture d’écran 2022-09-13 à 00 33 05

Is there a way to set that Resource names for TransactWriteItems ? Does anyone have ideas ? Or can confirme my assumption that only the Resource names is used and can only be created by DynamoDB itself ?

Thanks,