Skip to content

Commit c10dc63

Browse files
committed
merge: fix import conflict
2 parents c04f2f9 + 66f050c commit c10dc63

File tree

3 files changed

+192
-48
lines changed

3 files changed

+192
-48
lines changed

packages/core/src/shared/awsClientBuilderV3.ts

Lines changed: 64 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,13 @@ 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,
1516
Handler,
17+
FinalizeHandler,
18+
FinalizeRequestMiddleware,
1619
HandlerExecutionContext,
1720
MetadataBearer,
1821
MiddlewareStack,
@@ -22,13 +25,14 @@ import {
2225
RetryStrategy,
2326
UserAgent,
2427
} from '@aws-sdk/types'
25-
import { HttpResponse } from '@aws-sdk/protocol-http'
28+
import { HttpResponse, HttpRequest } from '@aws-sdk/protocol-http'
2629
import { ConfiguredRetryStrategy } from '@smithy/util-retry'
2730
import { telemetry } from './telemetry/telemetry'
2831
import { getRequestId, getTelemetryReason, getTelemetryReasonDesc, getTelemetryResult } from './errors'
2932
import { extensionVersion } from './vscode/env'
3033
import { getLogger } from './logger/logger'
3134
import { partialClone } from './utilities/collectionUtils'
35+
import { selectFrom } from './utilities/tsUtils'
3236

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

@@ -105,8 +109,9 @@ export class AWSClientBuilderV3 {
105109
}
106110

107111
const service = new type(opt)
108-
// TODO: add middleware for logging, telemetry, endpoints.
109-
service.middlewareStack.add(telemetryMiddleware, { step: 'deserialize' } as DeserializeHandlerOptions)
112+
service.middlewareStack.add(telemetryMiddleware, { step: 'deserialize' })
113+
service.middlewareStack.add(loggingMiddleware, { step: 'finalizeRequest' })
114+
service.middlewareStack.add(getEndpointMiddleware(settings), { step: 'build' })
110115
return service
111116
}
112117
}
@@ -132,29 +137,67 @@ export function recordErrorTelemetry(err: Error, serviceName?: string) {
132137
function logAndThrow(e: any, serviceId: string, errorMessageAppend: string): never {
133138
if (e instanceof Error) {
134139
recordErrorTelemetry(e, serviceId)
135-
const err = { ...e }
136-
delete err['stack']
137-
getLogger().error('API Response %s: %O', errorMessageAppend, err)
140+
getLogger().error('API Response %s: %O', errorMessageAppend, e)
138141
}
139142
throw e
140143
}
141-
/**
142-
* Telemetry logic to be added to all created clients. Adds logging and emitting metric on errors.
143-
*/
144+
144145
const telemetryMiddleware: DeserializeMiddleware<any, any> =
145-
(next: DeserializeHandler<any, any>, context: HandlerExecutionContext) => async (args: any) => {
146-
if (!HttpResponse.isInstance(args.request)) {
147-
return next(args)
148-
}
149-
const serviceId = getServiceId(context as object)
150-
const { hostname, path } = args.request
151-
const logTail = `(${hostname} ${path})`
152-
const result = await next(args).catch((e: any) => logAndThrow(e, serviceId, logTail))
146+
(next: DeserializeHandler<any, any>, context: HandlerExecutionContext) => async (args: any) =>
147+
emitOnRequest(next, context, args)
148+
149+
const loggingMiddleware: FinalizeRequestMiddleware<any, any> = (next: FinalizeHandler<any, any>) => async (args: any) =>
150+
logOnRequest(next, args)
151+
152+
function getEndpointMiddleware(settings: DevSettings = DevSettings.instance): BuildMiddleware<any, any> {
153+
return (next: BuildHandler<any, any>, context: HandlerExecutionContext) => async (args: any) =>
154+
overwriteEndpoint(next, context, settings, args)
155+
}
156+
157+
export async function emitOnRequest(next: DeserializeHandler<any, any>, context: HandlerExecutionContext, args: any) {
158+
if (!HttpResponse.isInstance(args.request)) {
159+
return next(args)
160+
}
161+
const serviceId = getServiceId(context as object)
162+
const { hostname, path } = args.request
163+
const logTail = `(${hostname} ${path})`
164+
try {
165+
const result = await next(args)
153166
if (HttpResponse.isInstance(result.response)) {
154-
// TODO: omit credentials / sensitive info from the logs / telemetry.
167+
// TODO: omit credentials / sensitive info from the telemetry.
155168
const output = partialClone(result.output, 3)
156-
getLogger().debug('API Response %s: %O', logTail, output)
169+
getLogger().debug(`API Response %s: %O`, logTail, output)
157170
}
158-
159171
return result
172+
} catch (e: any) {
173+
logAndThrow(e, serviceId, logTail)
174+
}
175+
}
176+
177+
export async function logOnRequest(next: FinalizeHandler<any, any>, args: any) {
178+
if (HttpRequest.isInstance(args.request)) {
179+
const { hostname, path } = args.request
180+
// TODO: omit credentials / sensitive info from the logs.
181+
const input = partialClone(args.input, 3)
182+
getLogger().debug(`API Request (%s %s): %O`, hostname, path, input)
160183
}
184+
return next(args)
185+
}
186+
187+
export function overwriteEndpoint(
188+
next: BuildHandler<any, any>,
189+
context: HandlerExecutionContext,
190+
settings: DevSettings,
191+
args: any
192+
) {
193+
if (HttpRequest.isInstance(args.request)) {
194+
const serviceId = getServiceId(context as object)
195+
const endpoint = serviceId ? settings.get('endpoints', {})[serviceId] : undefined
196+
if (endpoint) {
197+
const url = new URL(endpoint)
198+
Object.assign(args.request, selectFrom(url, 'hostname', 'port', 'protocol', 'pathname'))
199+
args.request.path = args.request.pathname
200+
}
201+
}
202+
return next(args)
203+
}

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)