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)
A unique GetItem
operation has a property Resource names
and Table name
which are equal:
Details for `GetItem` operation
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"}
But for my custom transaction we can see my custom Table names
but no Resource names
...
Details for `TransactWriteItems` operation
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"}
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 aDynamoDB AWS::DynamoDB::Table
type trace ;- while
TransactWriteItems
is linked to aDynamoDB AWS::DynamoDB
type trace.
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...
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,