Skip to content

Commit 183f317

Browse files
authored
Merge #3438 codecatalyst: logging
2 parents bb40b22 + 8de856c commit 183f317

File tree

4 files changed

+136
-26
lines changed

4 files changed

+136
-26
lines changed

src/shared/clients/codecatalystClient.ts

Lines changed: 30 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const localize = nls.loadMessageBundle()
1010

1111
import * as AWS from 'aws-sdk'
1212
import * as logger from '../logger/logger'
13+
import { PerfLog } from '../logger/logger'
1314
import { ServiceConfigurationOptions } from 'aws-sdk/lib/service'
1415
import { CancellationError, Timeout, waitTimeout, waitUntil } from '../utilities/timeoutUtils'
1516
import { isUserCancelledError } from '../../shared/errors'
@@ -28,8 +29,8 @@ import {
2829
ListSourceRepositoriesItem,
2930
ListSourceRepositoriesItems,
3031
} from 'aws-sdk/clients/codecatalyst'
32+
import { truncateProps } from '../utilities/textUtilities'
3133

32-
// REMOVE ME SOON: only used for development
3334
interface CodeCatalystConfig {
3435
readonly region: string
3536
readonly endpoint: string
@@ -224,10 +225,12 @@ class CodeCatalystClientInternal {
224225
const log = this.log
225226
const bearerToken = (await this.connection.getToken()).accessToken
226227
req.httpRequest.headers['Authorization'] = `Bearer ${bearerToken}`
228+
const perflog = new PerfLog('API request')
227229

228230
return new Promise<T>((resolve, reject) => {
229231
req.send(function (e, data) {
230232
const r = req as any
233+
const timecost = perflog.elapsed().toFixed(1)
231234
if (e) {
232235
if (e.code === 'AccessDeniedException' || e.statusCode === 401) {
233236
CodeCatalystClientInternal.identityCache.delete(bearerToken)
@@ -237,19 +240,16 @@ class CodeCatalystClientInternal {
237240
const logHeaders = {}
238241
// Selected headers which are useful for logging.
239242
const logHeaderNames = [
240-
// 'access-control-expose-headers',
241-
// 'cache-control',
242-
// 'strict-transport-security',
243-
'x-amz-apigw-id',
244-
'x-amz-cf-id',
245-
'x-amz-cf-pop',
246-
'x-amzn-remapped-content-length',
247-
'x-amzn-remapped-x-amzn-requestid',
248243
'x-amzn-requestid',
249-
'x-amzn-served-from',
250244
'x-amzn-trace-id',
245+
'x-amzn-served-from',
251246
'x-cache',
252-
'x-request-id', // <- Request id for caws/fusi!
247+
'x-amz-cf-id',
248+
'x-amz-cf-pop',
249+
// 'access-control-expose-headers',
250+
// 'cache-control',
251+
// 'strict-transport-security',
252+
// 'x-amz-apigw-id',
253253
]
254254
if (allHeaders && Object.keys(allHeaders).length > 0) {
255255
for (const k of logHeaderNames) {
@@ -260,21 +260,24 @@ class CodeCatalystClientInternal {
260260
// Stack is noisy and useless in production.
261261
const errNoStack = { ...e }
262262
delete errNoStack.stack
263-
// Remove confusing "requestId" field (= "x-amzn-requestid" header)
264-
// because for caws/fusi, "x-request-id" is more relevant.
265-
// All of the various request-ids can be found in the logged headers.
266-
delete errNoStack.requestId
263+
delete errNoStack.requestId // redundant (= "x-amzn-requestid" header).
267264

268265
if (r.operation || r.params) {
269266
log.error(
270-
'API request failed: %s\nparams: %O\nerror: %O\nheaders: %O',
267+
'API request failed (time: %dms): %s\nparams: %O\nerror: %O\nheaders: %O',
268+
timecost,
271269
r.operation,
272-
r.params,
270+
truncateProps(r.params, 20, ['nextToken']),
273271
errNoStack,
274272
logHeaders
275273
)
276274
} else {
277-
log.error('API request failed:%O\nheaders: %O', req, logHeaders)
275+
log.error(
276+
'API request failed (time: %dms):%O\nheaders: %O',
277+
timecost,
278+
truncateProps(req, 20, ['nextToken']),
279+
logHeaders
280+
)
278281
}
279282
if (silent) {
280283
if (defaultVal === undefined) {
@@ -286,7 +289,15 @@ class CodeCatalystClientInternal {
286289
}
287290
return
288291
}
289-
log.verbose('API request (%s):\nparams: %O\nresponse: %O', r.operation ?? '?', r.params ?? '?', data)
292+
if (log.logLevelEnabled('verbose')) {
293+
log.verbose(
294+
'API request (time: %dms): %s\nparams: %O\nresponse: %O',
295+
timecost,
296+
r.operation ?? '?',
297+
r.params ? truncateProps(r.params, 20, ['nextToken']) : '?',
298+
truncateProps(data as object, 20, ['nextToken'])
299+
)
300+
}
290301
resolve(data)
291302
})
292303
})

src/shared/logger/logger.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
*/
55

66
import { Uri } from 'vscode'
7+
import globals from '../extensionGlobals'
78

89
const toolkitLoggers: {
910
main: Logger | undefined
@@ -144,22 +145,24 @@ export function setLogger(logger: Logger | undefined, type?: 'channel' | 'debugC
144145
}
145146

146147
export class PerfLog {
147-
private log
148+
private readonly log
148149
public readonly start
149150

150151
public constructor(public readonly topic: string) {
151152
const log = getLogger()
152153
this.log = log
153-
if (log.logLevelEnabled('verbose')) {
154-
this.start = Date.now()
155-
}
154+
this.start = globals.clock.Date.now()
155+
}
156+
157+
public elapsed(): number {
158+
return globals.clock.Date.now() - this.start
156159
}
157160

158161
public done(): void {
159-
if (!this.start) {
162+
if (!this.log.logLevelEnabled('verbose')) {
160163
return
161164
}
162-
const elapsed = Date.now() - this.start
165+
const elapsed = this.elapsed()
163166
this.log.verbose('%s took %dms', this.topic, elapsed.toFixed(1))
164167
}
165168
}

src/shared/utilities/textUtilities.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,63 @@ import { default as stripAnsi } from 'strip-ansi'
1010
import { isCloud9 } from '../extensionUtilities'
1111
import { getLogger } from '../logger'
1212

13+
/**
14+
* Truncates string `s` if it exceeds `len` chars.
15+
*
16+
* @param s String to truncate
17+
* @param len Truncate top-level string properties exceeding this length
18+
* @param suffix String appended to truncated value (default: "…")
19+
*/
20+
export function truncate(s: string, len: number, suffix?: string): string {
21+
suffix = suffix ?? '…'
22+
if (len <= 0) {
23+
throw Error(`invalid len: ${len}`)
24+
}
25+
if (s === undefined || s.length <= len) {
26+
return s
27+
}
28+
return `${s.substring(0, len)}${suffix}`
29+
}
30+
31+
/**
32+
* Creates a (shallow) clone of `obj` and truncates its top-level string properties.
33+
*
34+
* @param obj Object to copy and truncate
35+
* @param len Truncate top-level string properties exceeding this length
36+
* @param propNames Only truncate properties in this list
37+
* @param suffix String appended to truncated values (default: "…")
38+
*/
39+
export function truncateProps(obj: object, len: number, propNames?: string[], suffix?: string): object {
40+
if (len <= 0) {
41+
throw Error(`invalid len: ${len}`)
42+
}
43+
// Shallow-copy to avoid modifying the original object.
44+
const r = { ...obj }
45+
46+
if (propNames) {
47+
for (const propName of propNames) {
48+
try {
49+
const val = (r as any)[propName]
50+
if (val !== undefined && typeof val === 'string') {
51+
;(r as any)[propName] = truncate(val, len, suffix)
52+
}
53+
} catch {
54+
// Do nothing ("best effort").
55+
}
56+
}
57+
} else {
58+
for (const propName of Object.getOwnPropertyNames(r)) {
59+
try {
60+
;(r as any)[propName] = truncate((r as any)[propName], len, suffix)
61+
} catch {
62+
// Do nothing ("best effort").
63+
}
64+
}
65+
}
66+
67+
return r
68+
}
69+
1370
export function removeAnsi(text: string): string {
1471
try {
1572
return stripAnsi(text)

src/test/shared/utilities/textUtilities.test.ts

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,46 @@
44
*/
55

66
import * as assert from 'assert'
7-
import { getRelativeDate, getStringHash, removeAnsi } from '../../../shared/utilities/textUtilities'
7+
import { getRelativeDate, getStringHash, removeAnsi, truncateProps } from '../../../shared/utilities/textUtilities'
8+
9+
describe('textUtilities', async function () {
10+
it('truncateProps()', async function () {
11+
const testObj = {
12+
a: 34234234234,
13+
b: '123456789',
14+
c: new Date(2023, 1, 1),
15+
d: '123456789_abcdefg_ABCDEFG',
16+
e: {
17+
e1: [4, 3, 7],
18+
e2: 'loooooooooo \n nnnnnnnnnnn \n gggggggg \n string',
19+
},
20+
f: () => {
21+
throw Error()
22+
},
23+
}
24+
const expected = {
25+
...testObj,
26+
e: {
27+
e1: [...testObj.e.e1],
28+
e2: testObj.e.e2,
29+
},
30+
}
31+
32+
assert.deepStrictEqual(truncateProps(testObj, 25), expected)
33+
assert.deepStrictEqual(truncateProps(testObj, 3, ['b']), {
34+
...expected,
35+
b: '123…',
36+
})
37+
// Assert that original object didn't change.
38+
assert.deepStrictEqual(truncateProps(testObj, 25), expected)
39+
40+
assert.deepStrictEqual(truncateProps(testObj, 3, ['a', 'b', 'd', 'f']), {
41+
...expected,
42+
b: '123…',
43+
d: '123…',
44+
})
45+
})
46+
})
847

948
describe('removeAnsi', async function () {
1049
it('removes ansi code from text', async function () {

0 commit comments

Comments
 (0)