-
Notifications
You must be signed in to change notification settings - Fork 238
feat: add instrumentation for aws-sdk S3 client #3287
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
34 commits
Select commit
Hold shift + click to select a range
ce6e8d1
feat: add instrumentation for aws-sdk S3 client
david-luna f07ca78
chore: add otel attributes like in sdk versions < 3
david-luna c808bca
chore: fix module mapping for smithy client instrumentation
david-luna f4ee1cb
chore: guard smithy client instrumentation from next major version
david-luna 02470fb
chore: adress trentm comments in PR
david-luna fc7d82c
chore: add s3 v3 tests
david-luna a71f200
chore: fix run context for S3 v3 spans
david-luna fe5cb5b
chore: use async/await for s3 test fixtures
david-luna b4205a0
chore: use exports for specific S3 constants in client instrumentation
david-luna 2a14d0b
chore: add tav tests
david-luna 74091fe
Update .tav.yml
david-luna 18da06b
update check of content-length header
david-luna 5d7edf1
use dedicated bucket name for testing
david-luna 3a1f573
remove console.log
david-luna f2eadfe
fix typo
david-luna eed317b
avoid testing with contextManager=patch
david-luna adb13e2
chore: skip tests for node versions <14
david-luna 968d80d
fix: allow body_size=0 to be reported in httpContext
david-luna 339f054
test: update s3 test assertions
david-luna f995099
Merge branch 'main' into luna/2955-instrumentation-aws-s3-v3
david-luna aa3eb05
test: better naming for client-s3 test files
david-luna 37aa0c4
fix: handle non async functions for region config props
david-luna d09736b
chore: remove logs
david-luna 285527c
docs: add @aws-sdk/client-s3 to supported technologies table
david-luna 49b2c14
docs: add changelog entry
david-luna f8639e8
fix: check for bucket defined when getting region from ARN
david-luna f7dd132
chore: add an early return in smithy-client instrumentation
david-luna d31d6f0
test: fix timeouts in use-client-s3
david-luna b13396a
test: fix getting region for S3 test fixtures
david-luna da8e31c
need this to ensure the TAV tests for this module are run in CI
trentm b40dceb
chore: merge last changes from main
david-luna 9ba06c6
chore: update changelog
david-luna b11d571
Merge branch 'luna/2955-instrumentation-aws-s3-v3' of github.com:elas…
david-luna 7bb716a
chore: update package-lock.json
david-luna File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| #!/bin/sh | ||
| # | ||
| # Calculate and emit the "versions:" block of ".tav.yml" for aws-sdk. | ||
| # This will include: | ||
| # - the first supported release (2.858.0) | ||
| # - the latest current release | ||
| # - and ~5 releases in between | ||
|
|
||
| npm info -j @aws-sdk/client-s3 | node -e ' | ||
| var semver = require("semver"); | ||
| var chunks = []; | ||
| process.stdin | ||
| .resume() | ||
| .on("data", (chunk) => { chunks.push(chunk) }) | ||
| .on("end", () => { | ||
| var input = JSON.parse(chunks.join("")); | ||
| var vers = input.versions.filter(v => semver.satisfies(v, ">=3 <4")); | ||
| var modulus = Math.floor((vers.length - 2) / 5); | ||
| console.log(" # Test v3.0.0, every N=%d of %d releases, and current latest.", modulus, vers.length); | ||
| vers = vers.filter((v, idx, arr) => idx % modulus === 0 || idx === arr.length - 1); | ||
| console.log(" versions: '\''%s || >%s <4'\''", vers.join(" || "), vers[vers.length-1]) | ||
| }) | ||
| ' |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,209 @@ | ||
| /* | ||
| * Copyright Elasticsearch B.V. and other contributors where applicable. | ||
| * Licensed under the BSD 2-Clause License; you may not use this file except in | ||
| * compliance with the BSD 2-Clause License. | ||
| */ | ||
|
|
||
| 'use strict' | ||
|
|
||
| const constants = require('../../../constants') | ||
| const NAME = 'S3' | ||
| const TYPE = 'storage' | ||
| const SUBTYPE = 's3' | ||
| const elasticAPMStash = Symbol('elasticAPMStash') | ||
|
|
||
| /** | ||
| * Gets the region from the ARN | ||
| * | ||
| * @param {String} s3Arn | ||
| * @returns {String} | ||
| */ | ||
| function regionFromS3Arn (s3Arn) { | ||
| return s3Arn.split(':')[3] | ||
| } | ||
|
|
||
| /** | ||
| * Return an APM "resource" string for the bucket, Access Point ARN, or Outpost | ||
| * ARN. ARNs are normalized to a shorter resource name. | ||
| * Known ARN patterns: | ||
| * - arn:aws:s3:<region>:<account-id>:accesspoint/<accesspoint-name> | ||
| * - arn:aws:s3-outposts:<region>:<account>:outpost/<outpost-id>/bucket/<bucket-name> | ||
| * - arn:aws:s3-outposts:<region>:<account>:outpost/<outpost-id>/accesspoint/<accesspoint-name> | ||
| * | ||
| * In general that is: | ||
| * arn:$partition:$service:$region:$accountId:$resource | ||
| * | ||
| * This parses using the same "split on colon" used by the JavaScript AWS SDK v3. | ||
| * https://github.com/aws/aws-sdk-js-v3/blob/v3.18.0/packages/util-arn-parser/src/index.ts#L14-L37 | ||
| * | ||
| * @param {String} bucket The bucket string | ||
| * @returns {String | null} | ||
| */ | ||
| function resourceFromBucket (bucket) { | ||
| let resource = null | ||
| if (bucket) { | ||
| resource = bucket | ||
| if (resource.startsWith('arn:')) { | ||
| resource = bucket.split(':').slice(5).join(':') | ||
| } | ||
| } | ||
| return resource | ||
| } | ||
|
|
||
| /** | ||
| * Returns middlewares to instrument an S3Client instance | ||
| * | ||
| * @param {import('@aws-sdk/client-s3').S3Client} client | ||
| * @param {any} agent | ||
| * @returns {import('./smithy-client').AWSMiddlewareEntry[]} | ||
| */ | ||
| function s3MiddlewareFactory (client, agent) { | ||
| return [ | ||
| { | ||
| middleware: (next, context) => async (args) => { | ||
| const input = args.input | ||
| const bucket = input && input.Bucket | ||
| const resource = resourceFromBucket(bucket) | ||
| const span = agent._instrumentation.currSpan() | ||
|
|
||
| if (!span) { | ||
| return await next(args) | ||
| } | ||
| // The given span comes with the operation name and we need to | ||
| // add the resource if applies | ||
| if (resource) { | ||
| span.name += ' ' + resource | ||
| span.setServiceTarget('s3', resource) | ||
| } | ||
|
|
||
| // As for now OTel spec defines attributes for operations that require a Bucket | ||
| // if that changes we should review this guard | ||
| // https://github.com/open-telemetry/opentelemetry-specification/blob/v1.20.0/semantic_conventions/trace/instrumentation/aws-sdk.yml#L435 | ||
| if (bucket) { | ||
| const otelAttrs = span._getOTelAttributes() | ||
|
|
||
| otelAttrs['aws.s3.bucket'] = bucket | ||
|
|
||
| if (input.Key) { | ||
| otelAttrs['aws.s3.key'] = input.Key | ||
| } | ||
| } | ||
|
|
||
| let err | ||
| let result | ||
| let response | ||
| let statusCode | ||
| try { | ||
| result = await next(args) | ||
| response = result && result.response | ||
| statusCode = response && response.statusCode | ||
| } catch (ex) { | ||
| // Save the error for use in `finally` below, but re-throw it to | ||
| // not impact code flow. | ||
| err = ex | ||
|
|
||
| // This code path happens with a GetObject conditional request | ||
| // that returns a 304 Not Modified. | ||
| statusCode = err && err.$metadata && err.$metadata.httpStatusCode | ||
| throw ex | ||
| } finally { | ||
| if (statusCode) { | ||
| span._setOutcomeFromHttpStatusCode(statusCode) | ||
| } else { | ||
| span._setOutcomeFromErrorCapture(constants.OUTCOME_FAILURE) | ||
| } | ||
| if (err && (!statusCode || statusCode >= 400)) { | ||
| agent.captureError(err, { skipOutcome: true }) | ||
| } | ||
|
|
||
| // Set the httpContext | ||
| if (statusCode) { | ||
| const httpContext = { | ||
| status_code: statusCode | ||
| } | ||
|
|
||
| if (response && response.headers && response.headers['content-length']) { | ||
| const encodedBodySize = Number(response.headers['content-length']) | ||
| if (!isNaN(encodedBodySize)) { | ||
| httpContext.response = { encoded_body_size: encodedBodySize } | ||
| } | ||
| } | ||
| span.setHttpContext(httpContext) | ||
| } | ||
|
|
||
| // Configuring `new S3Client({useArnRegion:true})` allows one to | ||
| // use an Access Point bucket ARN for a region *other* than the | ||
| // one for which the client is configured. Therefore, we attempt | ||
| // to get the bucket region from the ARN first. | ||
| const config = client.config | ||
| let useArnRegion | ||
| if (typeof config.useArnRegion === 'boolean') { | ||
| useArnRegion = config.useArnRegion | ||
| } else { | ||
| useArnRegion = await config.useArnRegion() | ||
| } | ||
trentm marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| let region | ||
| if (useArnRegion && bucket && bucket.startsWith('arn:')) { | ||
| region = regionFromS3Arn(args.input.Bucket) | ||
| } else { | ||
| region = typeof config.region === 'boolean' ? region : await config.region() | ||
| } | ||
|
|
||
| // Destination context. | ||
| const destContext = { | ||
| address: context[elasticAPMStash].hostname, | ||
| port: context[elasticAPMStash].port, | ||
| service: { | ||
| name: SUBTYPE, | ||
| type: TYPE | ||
| } | ||
| } | ||
| if (resource) { | ||
| destContext.service.resource = resource | ||
| } | ||
|
|
||
| if (region) { | ||
| destContext.cloud = { region } | ||
| } | ||
| span._setDestinationContext(destContext) | ||
|
|
||
| span.end() | ||
| } | ||
|
|
||
| return result | ||
| }, | ||
| options: { step: 'initialize', priority: 'high', name: 'elasticAPMSpan' } | ||
| }, | ||
| { | ||
| middleware: (next, context) => async (args) => { | ||
| const req = args.request | ||
| let port = req.port | ||
|
|
||
| // Resolve port for HTTP(S) protocols | ||
| if (port === undefined) { | ||
| if (req.protocol === 'https:') { | ||
| port = 443 | ||
| } else if (req.protocol === 'http:') { | ||
| port = 80 | ||
| } | ||
| } | ||
|
|
||
| context[elasticAPMStash] = { | ||
| protocol: req.protocol, | ||
| hostname: req.hostname, | ||
| port: port | ||
| } | ||
| return next(args) | ||
| }, | ||
| options: { step: 'finalizeRequest', name: 'elasticAPMHTTPInfo' } | ||
| } | ||
| ] | ||
| } | ||
|
|
||
| module.exports = { | ||
| S3_NAME: NAME, | ||
| S3_TYPE: TYPE, | ||
| S3_SUBTYPE: SUBTYPE, | ||
| s3MiddlewareFactory | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.