Skip to content

Commit 66f050c

Browse files
authored
deps(sdkv3): implement logging and endpoint override for client builder. (#6441)
## Problem The V3 client builder currently only adds telemetry. We want some additional functionality on each request/response. ## Solution - Allow user to override the endpoint used by the sdk. - Add debugging messages for each request and response. --- - Treat all work as PUBLIC. Private `feature/x` branches will not be squash-merged at release time. - Your code changes must meet the guidelines in [CONTRIBUTING.md](https://github.com/aws/aws-toolkit-vscode/blob/master/CONTRIBUTING.md#guidelines). - License: I confirm that my contribution is made under the terms of the Apache 2.0 license.
1 parent 11ea8c5 commit 66f050c

File tree

3 files changed

+195
-51
lines changed

3 files changed

+195
-51
lines changed

packages/core/src/shared/awsClientBuilderV3.ts

Lines changed: 67 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,12 @@ import { AwsCredentialIdentityProvider, RetryStrategyV2 } from '@smithy/types'
99
import { getUserAgent } from './telemetry/util'
1010
import { DevSettings } from './settings'
1111
import {
12+
BuildHandler,
13+
BuildMiddleware,
1214
DeserializeHandler,
13-
DeserializeHandlerOptions,
1415
DeserializeMiddleware,
16+
FinalizeHandler,
17+
FinalizeRequestMiddleware,
1518
HandlerExecutionContext,
1619
MetadataBearer,
1720
MiddlewareStack,
@@ -21,13 +24,14 @@ import {
2124
RetryStrategy,
2225
UserAgent,
2326
} from '@aws-sdk/types'
24-
import { HttpResponse } from '@aws-sdk/protocol-http'
27+
import { HttpResponse, HttpRequest } from '@aws-sdk/protocol-http'
2528
import { ConfiguredRetryStrategy } from '@smithy/util-retry'
26-
import { telemetry } from './telemetry'
29+
import { telemetry } from './telemetry/telemetry'
2730
import { getRequestId, getTelemetryReason, getTelemetryReasonDesc, getTelemetryResult } from './errors'
28-
import { extensionVersion } from '.'
29-
import { getLogger } from './logger'
31+
import { extensionVersion } from './vscode/env'
32+
import { getLogger } from './logger/logger'
3033
import { partialClone } from './utilities/collectionUtils'
34+
import { selectFrom } from './utilities/tsUtils'
3135

3236
export type AwsClientConstructor<C> = new (o: AwsClientOptions) => C
3337

@@ -96,8 +100,9 @@ export class AWSClientBuilderV3 {
96100
}
97101

98102
const service = new type(opt)
99-
// TODO: add middleware for logging, telemetry, endpoints.
100-
service.middlewareStack.add(telemetryMiddleware, { step: 'deserialize' } as DeserializeHandlerOptions)
103+
service.middlewareStack.add(telemetryMiddleware, { step: 'deserialize' })
104+
service.middlewareStack.add(loggingMiddleware, { step: 'finalizeRequest' })
105+
service.middlewareStack.add(getEndpointMiddleware(settings), { step: 'build' })
101106
return service
102107
}
103108
}
@@ -123,29 +128,67 @@ export function recordErrorTelemetry(err: Error, serviceName?: string) {
123128
function logAndThrow(e: any, serviceId: string, errorMessageAppend: string): never {
124129
if (e instanceof Error) {
125130
recordErrorTelemetry(e, serviceId)
126-
const err = { ...e }
127-
delete err['stack']
128-
getLogger().error('API Response %s: %O', errorMessageAppend, err)
131+
getLogger().error('API Response %s: %O', errorMessageAppend, e)
129132
}
130133
throw e
131134
}
132-
/**
133-
* Telemetry logic to be added to all created clients. Adds logging and emitting metric on errors.
134-
*/
135+
135136
const telemetryMiddleware: DeserializeMiddleware<any, any> =
136-
(next: DeserializeHandler<any, any>, context: HandlerExecutionContext) => async (args: any) => {
137-
if (!HttpResponse.isInstance(args.request)) {
138-
return next(args)
139-
}
140-
const serviceId = getServiceId(context as object)
141-
const { hostname, path } = args.request
142-
const logTail = `(${hostname} ${path})`
143-
const result = await next(args).catch((e: any) => logAndThrow(e, serviceId, logTail))
137+
(next: DeserializeHandler<any, any>, context: HandlerExecutionContext) => async (args: any) =>
138+
emitOnRequest(next, context, args)
139+
140+
const loggingMiddleware: FinalizeRequestMiddleware<any, any> = (next: FinalizeHandler<any, any>) => async (args: any) =>
141+
logOnRequest(next, args)
142+
143+
function getEndpointMiddleware(settings: DevSettings = DevSettings.instance): BuildMiddleware<any, any> {
144+
return (next: BuildHandler<any, any>, context: HandlerExecutionContext) => async (args: any) =>
145+
overwriteEndpoint(next, context, settings, args)
146+
}
147+
148+
export async function emitOnRequest(next: DeserializeHandler<any, any>, context: HandlerExecutionContext, args: any) {
149+
if (!HttpResponse.isInstance(args.request)) {
150+
return next(args)
151+
}
152+
const serviceId = getServiceId(context as object)
153+
const { hostname, path } = args.request
154+
const logTail = `(${hostname} ${path})`
155+
try {
156+
const result = await next(args)
144157
if (HttpResponse.isInstance(result.response)) {
145-
// TODO: omit credentials / sensitive info from the logs / telemetry.
158+
// TODO: omit credentials / sensitive info from the telemetry.
146159
const output = partialClone(result.output, 3)
147-
getLogger().debug('API Response %s: %O', logTail, output)
160+
getLogger().debug(`API Response %s: %O`, logTail, output)
148161
}
149-
150162
return result
163+
} catch (e: any) {
164+
logAndThrow(e, serviceId, logTail)
165+
}
166+
}
167+
168+
export async function logOnRequest(next: FinalizeHandler<any, any>, args: any) {
169+
if (HttpRequest.isInstance(args.request)) {
170+
const { hostname, path } = args.request
171+
// TODO: omit credentials / sensitive info from the logs.
172+
const input = partialClone(args.input, 3)
173+
getLogger().debug(`API Request (%s %s): %O`, hostname, path, input)
151174
}
175+
return next(args)
176+
}
177+
178+
export function overwriteEndpoint(
179+
next: BuildHandler<any, any>,
180+
context: HandlerExecutionContext,
181+
settings: DevSettings,
182+
args: any
183+
) {
184+
if (HttpRequest.isInstance(args.request)) {
185+
const serviceId = getServiceId(context as object)
186+
const endpoint = serviceId ? settings.get('endpoints', {})[serviceId] : undefined
187+
if (endpoint) {
188+
const url = new URL(endpoint)
189+
Object.assign(args.request, selectFrom(url, 'hostname', 'port', 'protocol', 'pathname'))
190+
args.request.path = args.request.pathname
191+
}
192+
}
193+
return next(args)
194+
}

packages/core/src/test/globalSetup.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,10 @@ async function writeLogsToFile(testName: string) {
167167
await fs.appendFile(testLogOutput, entries?.join('\n') ?? '')
168168
}
169169

170+
export function assertLogsContainAllOf(keywords: string[], exactMatch: boolean, severity: LogLevel) {
171+
return keywords.map((k) => assertLogsContain(k, exactMatch, severity))
172+
}
173+
170174
// TODO: merge this with `toolkitLogger.test.ts:checkFile`
171175
export function assertLogsContain(text: string, exactMatch: boolean, severity: LogLevel) {
172176
const logs = getTestLogger().getLoggedEntries(severity)

packages/core/src/test/shared/awsClientBuilderV3.test.ts

Lines changed: 124 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,28 @@
22
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
33
* SPDX-License-Identifier: Apache-2.0
44
*/
5-
5+
import sinon from 'sinon'
66
import assert from 'assert'
77
import { version } from 'vscode'
88
import { getClientId } from '../../shared/telemetry/util'
99
import { FakeMemento } from '../fakeExtensionContext'
1010
import { FakeAwsContext } from '../utilities/fakeAwsContext'
1111
import { GlobalState } from '../../shared/globalState'
12-
import { AWSClientBuilderV3, getServiceId, recordErrorTelemetry } from '../../shared/awsClientBuilderV3'
12+
import {
13+
AWSClientBuilderV3,
14+
emitOnRequest,
15+
getServiceId,
16+
logOnRequest,
17+
overwriteEndpoint,
18+
recordErrorTelemetry,
19+
} from '../../shared/awsClientBuilderV3'
1320
import { Client } from '@aws-sdk/smithy-client'
14-
import { extensionVersion } from '../../shared'
21+
import { DevSettings, extensionVersion } from '../../shared'
1522
import { assertTelemetry } from '../testUtil'
1623
import { telemetry } from '../../shared/telemetry'
24+
import { HttpRequest, HttpResponse } from '@aws-sdk/protocol-http'
25+
import { assertLogsContain, assertLogsContainAllOf } from '../globalSetup.test'
26+
import { TestSettings } from '../utilities/testSettingsConfiguration'
1727
import { CredentialsShim } from '../../auth/deprecated/loginManager'
1828
import { Credentials } from '@aws-sdk/types'
1929
import { oneDay } from '../../shared/datetime'
@@ -25,39 +35,126 @@ describe('AwsClientBuilderV3', function () {
2535
builder = new AWSClientBuilderV3(new FakeAwsContext())
2636
})
2737

28-
describe('createAndConfigureSdkClient', function () {
29-
it('includes Toolkit user-agent if no options are specified', async function () {
30-
const service = await builder.createAwsService(Client)
31-
const clientId = getClientId(new GlobalState(new FakeMemento()))
32-
33-
assert.ok(service.config.userAgent)
34-
assert.strictEqual(
35-
service.config.userAgent![0][0].replace('---Insiders', ''),
36-
`AWS-Toolkit-For-VSCode/testPluginVersion Visual-Studio-Code/${version} ClientId/${clientId}`
37-
)
38-
assert.strictEqual(service.config.userAgent![0][1], extensionVersion)
38+
it('includes Toolkit user-agent if no options are specified', async function () {
39+
const service = await builder.createAwsService(Client)
40+
const clientId = getClientId(new GlobalState(new FakeMemento()))
41+
42+
assert.ok(service.config.userAgent)
43+
assert.strictEqual(
44+
service.config.userAgent![0][0].replace('---Insiders', ''),
45+
`AWS-Toolkit-For-VSCode/testPluginVersion Visual-Studio-Code/${version} ClientId/${clientId}`
46+
)
47+
assert.strictEqual(service.config.userAgent![0][1], extensionVersion)
48+
})
49+
50+
it('adds region to client', async function () {
51+
const service = await builder.createAwsService(Client, { region: 'us-west-2' })
52+
53+
assert.ok(service.config.region)
54+
assert.strictEqual(service.config.region, 'us-west-2')
55+
})
56+
57+
it('adds Client-Id to user agent', async function () {
58+
const service = await builder.createAwsService(Client)
59+
const clientId = getClientId(new GlobalState(new FakeMemento()))
60+
const regex = new RegExp(`ClientId/${clientId}`)
61+
assert.ok(service.config.userAgent![0][0].match(regex))
62+
})
63+
64+
it('does not override custom user-agent if specified in options', async function () {
65+
const service = await builder.createAwsService(Client, {
66+
userAgent: [['CUSTOM USER AGENT']],
3967
})
4068

41-
it('adds region to client', async function () {
42-
const service = await builder.createAwsService(Client, { region: 'us-west-2' })
69+
assert.strictEqual(service.config.userAgent[0][0], 'CUSTOM USER AGENT')
70+
})
4371

44-
assert.ok(service.config.region)
45-
assert.strictEqual(service.config.region, 'us-west-2')
72+
describe('middlewareStack', function () {
73+
let args: { request: { hostname: string; path: string }; input: any }
74+
let context: { clientName?: string; commandName?: string }
75+
let response: { response: { statusCode: number }; output: { message: string } }
76+
let httpRequestStub: sinon.SinonStub
77+
let httpResponseStub: sinon.SinonStub
78+
79+
before(function () {
80+
httpRequestStub = sinon.stub(HttpRequest, 'isInstance')
81+
httpResponseStub = sinon.stub(HttpResponse, 'isInstance')
82+
httpRequestStub.callsFake(() => true)
83+
httpResponseStub.callsFake(() => true)
4684
})
4785

48-
it('adds Client-Id to user agent', async function () {
49-
const service = await builder.createAwsService(Client)
50-
const clientId = getClientId(new GlobalState(new FakeMemento()))
51-
const regex = new RegExp(`ClientId/${clientId}`)
52-
assert.ok(service.config.userAgent![0][0].match(regex))
86+
beforeEach(function () {
87+
args = {
88+
request: {
89+
hostname: 'testHost',
90+
path: 'testPath',
91+
},
92+
input: {
93+
testKey: 'testValue',
94+
},
95+
}
96+
context = {
97+
clientName: 'fooClient',
98+
}
99+
response = {
100+
response: {
101+
statusCode: 200,
102+
},
103+
output: {
104+
message: 'test output',
105+
},
106+
}
107+
})
108+
after(function () {
109+
sinon.restore()
110+
})
111+
112+
it('logs messages on request', async function () {
113+
await logOnRequest((_: any) => _, args as any)
114+
assertLogsContainAllOf(['testHost', 'testPath'], false, 'debug')
115+
})
116+
117+
it('adds telemetry metadata and logs on error failure', async function () {
118+
const next = (_: any) => {
119+
throw new Error('test error')
120+
}
121+
await telemetry.vscode_executeCommand.run(async (span) => {
122+
await assert.rejects(emitOnRequest(next, context, args))
123+
})
124+
assertLogsContain('test error', false, 'error')
125+
assertTelemetry('vscode_executeCommand', { requestServiceType: 'foo' })
53126
})
54127

55-
it('does not override custom user-agent if specified in options', async function () {
56-
const service = await builder.createAwsService(Client, {
57-
userAgent: [['CUSTOM USER AGENT']],
128+
it('does not emit telemetry, but still logs on successes', async function () {
129+
const next = async (_: any) => {
130+
return response
131+
}
132+
await telemetry.vscode_executeCommand.run(async (span) => {
133+
assert.deepStrictEqual(await emitOnRequest(next, context, args), response)
58134
})
135+
assertLogsContainAllOf(['testHost', 'testPath'], false, 'debug')
136+
assert.throws(() => assertTelemetry('vscode_executeCommand', { requestServiceType: 'foo' }))
137+
})
138+
139+
it('custom endpoints overwrite request url', async function () {
140+
const settings = new TestSettings()
141+
await settings.update('aws.dev.endpoints', { foo: 'http://example.com:3000/path' })
142+
const next = async (args: any) => args
143+
const newArgs: any = await overwriteEndpoint(next, context, new DevSettings(settings), args)
144+
145+
assert.strictEqual(newArgs.request.hostname, 'example.com')
146+
assert.strictEqual(newArgs.request.protocol, 'http:')
147+
assert.strictEqual(newArgs.request.port, '3000')
148+
assert.strictEqual(newArgs.request.pathname, '/path')
149+
})
150+
151+
it('custom endpoints are not overwritten if not specified', async function () {
152+
const settings = new TestSettings()
153+
const next = async (args: any) => args
154+
const newArgs: any = await overwriteEndpoint(next, context, new DevSettings(settings), args)
59155

60-
assert.strictEqual(service.config.userAgent[0][0], 'CUSTOM USER AGENT')
156+
assert.strictEqual(newArgs.request.hostname, 'testHost')
157+
assert.strictEqual(newArgs.request.path, 'testPath')
61158
})
62159
})
63160

0 commit comments

Comments
 (0)