|
| 1 | +'use strict' |
| 2 | + |
| 3 | +// Instrument AWS S3 operations via the 'aws-sdk' package. |
| 4 | + |
| 5 | +const constants = require('../../../constants') |
| 6 | + |
| 7 | +const TYPE = 'storage' |
| 8 | +const SUBTYPE = 's3' |
| 9 | + |
| 10 | +// This regex adapted from "OutpostArn" pattern at |
| 11 | +// https://docs.aws.amazon.com/outposts/latest/APIReference/API_Outpost.html |
| 12 | +const ARN_REGEX = /^arn:aws([a-z-]+)?:(s3|s3-outposts):[a-z\d-]+:\d{12}:(.*?)$/ |
| 13 | + |
| 14 | +// Instrument an awk-sdk@2.x operation (i.e. a AWS.Request.send or |
| 15 | +// AWS.Request.promise). |
| 16 | +// |
| 17 | +// @param {AWS.Request} request https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/Request.html |
| 18 | +function instrumentationS3 (orig, origArguments, request, AWS, agent, { version, enabled }) { |
| 19 | + // console.warn('XXX endpoint: ', request.service.endpoint) |
| 20 | + // console.warn('XXX service.config before: ', request.service.config) |
| 21 | + |
| 22 | + // XXX action is 'listBuckets' from the SDK. Our spec is 'ListBuckets'. Is a |
| 23 | + // simple string.capitalize sufficient? TODO: Find a aws-sdk-js-v3 ref for that. |
| 24 | + const action = request.operation[0].toUpperCase() + request.operation.slice(1) |
| 25 | + let name = 'S3 ' + action |
| 26 | + let resource = null // The bucket name or normalized Access Point/Outpost ARN. |
| 27 | + if (request.params && request.params.Bucket) { |
| 28 | + resource = request.params.Bucket |
| 29 | + if (resource.startsWith('arn:')) { |
| 30 | + // This is an Access Point or Outpost ARN, e.g.: |
| 31 | + // - arn:aws:s3:region:account-id:accesspoint/resource |
| 32 | + // - arn:aws:s3-outposts:<region>:<account>:outpost/<outpost-id>/bucket/<bucket-name> |
| 33 | + // - arn:aws:s3-outposts:<region>:<account>:outpost/<outpost-id>/accesspoint/<accesspoint-name></accesspoint-name> |
| 34 | + const match = ARN_REGEX.exec(resource) |
| 35 | + if (match) { |
| 36 | + resource = match[3] |
| 37 | + } |
| 38 | + } |
| 39 | + name += ' ' + resource |
| 40 | + } |
| 41 | + |
| 42 | + const span = agent.startSpan(name, TYPE, SUBTYPE, action) |
| 43 | + if (span) { |
| 44 | + // Determining the bucket region is not always possible. |
| 45 | + // - `request.service.config.region` can be "us-west-2" and talk to a bucket |
| 46 | + // in "us-west-1" |
| 47 | + // - `request.service.endpoint` can be to a regional endpoint, e.g. |
| 48 | + // "https://s3.us-east-2.amazonaws.com", to access a bucket hosted in a |
| 49 | + // different region. |
| 50 | + // - The `x-amz-bucket-region` response header is set for HeadBucket calls. |
| 51 | + // - A separate API call to GetBucketLocation will return the |
| 52 | + // "LocationConstraint" specified at CreateBucket. API docs |
| 53 | + // (https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#getBucketLocation-property) |
| 54 | + // state "Buckets in Region us-east-1 have a LocationConstraint of null." |
| 55 | + // though the empty string has been observed. |
| 56 | + // - If there is a `response.error` set, it sometimes has a 'region' prop. |
| 57 | + span.setDbContext({ |
| 58 | + type: 's3' |
| 59 | + // instance: region // XXX see above |
| 60 | + }) |
| 61 | + |
| 62 | + // Destination context. |
| 63 | + const endpoint = request.service.endpoint |
| 64 | + const destContext = { |
| 65 | + address: endpoint.hostname, |
| 66 | + port: endpoint.port, |
| 67 | + service: { |
| 68 | + name: 's3', |
| 69 | + type: 'storage' |
| 70 | + } |
| 71 | + } |
| 72 | + if (resource) { |
| 73 | + destContext.service.resource = resource |
| 74 | + } |
| 75 | + // TODO: destination.cloud.region is pending https://github.com/elastic/ecs/issues/1282 |
| 76 | + span.setDestinationContext(destContext) |
| 77 | + |
| 78 | + request.on('complete', function (response) { |
| 79 | + // console.warn('XXX response: ', response) |
| 80 | + // console.warn('XXX response info: ', |
| 81 | + // 'retryCount', response.retryCount, |
| 82 | + // 'redirectCount', response.redirectCount, |
| 83 | + // 'statusCode', response.httpResponse.statusCode, |
| 84 | + // 'headers', response.httpResponse.headers |
| 85 | + // ) |
| 86 | + if (response && response.error) { |
| 87 | + console.warn('XXX response.error ', response.error) |
| 88 | + // XXX is skipOutcome necessary for S3 error responses? |
| 89 | + const errOpts = { |
| 90 | + skipOutcome: true |
| 91 | + } |
| 92 | + agent.captureError(response.error, errOpts) |
| 93 | + span._setOutcomeFromErrorCapture(constants.OUTCOME_FAILURE) |
| 94 | + } |
| 95 | + |
| 96 | + // Workaround a bug in the agent's handling of `span.sync`. |
| 97 | + // |
| 98 | + // The bug: Currently this span.sync is not set `false` because there is |
| 99 | + // an HTTP span created (for this S3 request) in the same async op. That |
| 100 | + // HTTP span becomes the "active span" for this async op, and *it* gets |
| 101 | + // marked as sync=false in `before()` in async-hooks.js. |
| 102 | + // TODO: move this to a separate issue. |
| 103 | + span.sync = false |
| 104 | + |
| 105 | + span.end() |
| 106 | + }) |
| 107 | + } |
| 108 | + |
| 109 | + const origResult = orig.apply(request, origArguments) |
| 110 | + |
| 111 | + return origResult |
| 112 | +} |
| 113 | + |
| 114 | +module.exports = { |
| 115 | + instrumentationS3 |
| 116 | +} |
0 commit comments