Skip to content

Commit a4b2a43

Browse files
committed
fix(gax): populate exception and server.address in buildLogAttributes and fix HTTP status and url.domain resolution
1 parent ac9f175 commit a4b2a43

4 files changed

Lines changed: 133 additions & 32 deletions

File tree

‎core/packages/gax/src/observability/LoggerHelper.ts‎

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
StaticTraceContext,
2929
} from './contextUtil';
3030
import {
31+
isPreConnectionFailure,
3132
resolveErrorInfoDomain,
3233
resolveErrorInfoMetadataAttributes,
3334
resolveErrorType,
@@ -53,6 +54,7 @@ export interface LogPayload {
5354
'gcp.client.language'?: 'node';
5455
'gcp.client.service'?: string;
5556
'url.domain'?: string;
57+
'server.address'?: string;
5658
'response.payload'?: string;
5759
'exception.type'?: string;
5860
'exception.message'?: string;
@@ -85,7 +87,11 @@ export function buildLogAttributes(
8587
dynamicArgs: AttemptTraceContext,
8688
staticArgs: StaticTraceContext = {},
8789
): LogPayload {
88-
const {urlDomain} = resolveEndpointAndDomain(dynamicArgs, staticArgs);
90+
const {rawAddress, urlDomain} = resolveEndpointAndDomain(
91+
dynamicArgs,
92+
staticArgs,
93+
443,
94+
);
8995
const initialAttributes = buildInitialAttributes(
9096
dynamicArgs,
9197
staticArgs,
@@ -98,6 +104,9 @@ export function buildLogAttributes(
98104
}
99105
}
100106
attributes['gcp.client.language'] = 'node';
107+
if (rawAddress !== undefined && !isPreConnectionFailure(err)) {
108+
attributes['server.address'] = rawAddress;
109+
}
101110

102111
const rpcMethod = resolveRpcMethod(dynamicArgs);
103112
if (rpcMethod) {
@@ -111,10 +120,10 @@ export function buildLogAttributes(
111120
);
112121
const normalizedRpcStatusName =
113122
dynamicArgs.rpcType === 'http'
114-
? (resolveRpcStatusName(err) ??
115-
(httpStatusCode !== undefined
116-
? Status[rpcCodeFromHttpStatusCode(httpStatusCode)]
117-
: undefined))
123+
? httpStatusCode !== undefined
124+
? (resolveRpcStatusName(err) ??
125+
Status[rpcCodeFromHttpStatusCode(httpStatusCode)])
126+
: undefined
118127
: rpcStatusName;
119128
if (normalizedRpcStatusName !== undefined) {
120129
attributes['rpc.response.status_code'] = normalizedRpcStatusName;
@@ -131,6 +140,16 @@ export function buildLogAttributes(
131140
}
132141
Object.assign(attributes, resolveErrorInfoMetadataAttributes(err));
133142

143+
attributes['exception.type'] = resolveExceptionType(err);
144+
attributes['exception.message'] = resolveExceptionMessage(
145+
err,
146+
dynamicArgs.rpcType,
147+
);
148+
const stacktrace = resolveExceptionStacktrace(err);
149+
if (stacktrace !== undefined) {
150+
attributes['exception.stacktrace'] = stacktrace;
151+
}
152+
134153
return attributes;
135154
}
136155

@@ -180,14 +199,7 @@ export function logCall<T = GaxCallResult>(
180199
const spanId = spanContext?.spanId;
181200

182201
// Build structured log attributes with exception details.
183-
const exceptionMessage = resolveExceptionMessage(err, dynamicArgs.rpcType);
184-
const stacktrace = resolveExceptionStacktrace(err);
185-
const attributes: LogPayload = {
186-
...buildLogAttributes(err, dynamicArgs, staticArgs),
187-
'exception.type': resolveExceptionType(err),
188-
'exception.message': exceptionMessage,
189-
...(stacktrace !== undefined ? {'exception.stacktrace': stacktrace} : {}),
190-
};
202+
const attributes = buildLogAttributes(err, dynamicArgs, staticArgs);
191203

192204
const warningSeverity =
193205
(
@@ -205,7 +217,7 @@ export function logCall<T = GaxCallResult>(
205217
other: attributes,
206218
},
207219
'Client API call failed: %s',
208-
exceptionMessage || err,
220+
attributes['exception.message'] || err,
209221
);
210222
};
211223

‎core/packages/gax/src/observability/contextUtil.ts‎

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -426,13 +426,18 @@ export function resolveEndpointAndDomain(
426426
splitHostAndPort(rawAddress);
427427
}
428428

429-
let urlDomain = dynamicArgs.urlDomain ?? staticArgs.urlDomain ?? rawAddress;
430-
if (!urlDomain && staticArgs.gcpClientService) {
431-
// Append ".googleapis.com" when gcpClientService is a short service name.
432-
urlDomain = staticArgs.gcpClientService.includes('.')
429+
// Prefer nominal service domain from gcpClientService over an overridden
430+
// serverAddress (e.g. 127.0.0.1 or a regional endpoint) so url.domain stays canonical.
431+
const serviceDomain = staticArgs.gcpClientService
432+
? staticArgs.gcpClientService.includes('.')
433433
? staticArgs.gcpClientService
434-
: `${staticArgs.gcpClientService}.googleapis.com`;
435-
}
434+
: `${staticArgs.gcpClientService}.googleapis.com`
435+
: undefined;
436+
const urlDomain =
437+
dynamicArgs.urlDomain ??
438+
staticArgs.urlDomain ??
439+
serviceDomain ??
440+
rawAddress;
436441

437442
if (defaultPort !== undefined) {
438443
if (!rawAddress && urlDomain) {

‎core/packages/gax/test/unit/contextUtil.ts‎

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -415,11 +415,35 @@ describe('contextUtil', () => {
415415
);
416416
});
417417

418-
it('falls back to the server address, then gcpClientService, for url.domain', () => {
418+
it('prefers nominal gcpClientService domain over an overridden server address for url.domain', () => {
419+
assert.deepStrictEqual(
420+
resolveEndpointAndDomain(
421+
{...dynamicArgs, serverAddress: '127.0.0.1'},
422+
{gcpClientService: 'texttospeech'},
423+
),
424+
{
425+
rawAddress: '127.0.0.1',
426+
rawPort: undefined,
427+
urlDomain: 'texttospeech.googleapis.com',
428+
},
429+
);
430+
assert.deepStrictEqual(
431+
resolveEndpointAndDomain(
432+
dynamicArgs,
433+
{gcpClientService: 'texttospeech', serverAddress: '127.0.0.1:8080'},
434+
443,
435+
),
436+
{
437+
rawAddress: '127.0.0.1',
438+
rawPort: 8080,
439+
urlDomain: 'texttospeech.googleapis.com',
440+
},
441+
);
442+
// Falls back to server address only when neither urlDomain nor gcpClientService is set.
419443
assert.deepStrictEqual(
420444
resolveEndpointAndDomain(
421445
{...dynamicArgs, serverAddress: 'example.com'},
422-
{gcpClientService: 'cloudkms'},
446+
{},
423447
),
424448
{
425449
rawAddress: 'example.com',

‎core/packages/gax/test/unit/loggerHelper.ts‎

Lines changed: 70 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -144,11 +144,15 @@ describe('LoggerHelper', () => {
144144
'rpc.system.name': 'grpc',
145145
'rpc.method': 'google.storage.v2.Storage/GetObject',
146146
'url.domain': 'storage.googleapis.com',
147+
'server.address': 'storage.googleapis.com',
147148
'rpc.response.status_code': 'INVALID_ARGUMENT',
148149
'error.type': 'INVALID_FIELD',
149150
'gcp.errors.domain': 'googleapis.com',
150151
'gcp.errors.metadata.service': 'storage.googleapis.com',
151152
'gcp.errors.metadata.field': 'bucket',
153+
'exception.type': 'GoogleError',
154+
'exception.message': 'Field name is invalid',
155+
'exception.stacktrace': err.stack,
152156
});
153157
});
154158

@@ -173,13 +177,14 @@ describe('LoggerHelper', () => {
173177
'rpc.response.status_code': 'NOT_FOUND',
174178
'http.response.status_code': 404,
175179
'error.type': '404',
180+
'exception.type': 'GoogleError',
181+
'exception.message': 'Not Found',
182+
'exception.stacktrace': httpErr.stack,
176183
});
177184
assert.strictEqual('gcp.client.service' in attributes, false);
178185
assert.strictEqual('rpc.method' in attributes, false);
186+
assert.strictEqual('server.address' in attributes, false);
179187
assert.strictEqual('gcp.errors.domain' in attributes, false);
180-
assert.strictEqual('exception.type' in attributes, false);
181-
assert.strictEqual('exception.message' in attributes, false);
182-
assert.strictEqual('exception.stacktrace' in attributes, false);
183188

184189
// Prefers explicit gRPC status code parsed from Google HTTP error body over generic HTTP bucket.
185190
const specificHttpErr = Object.assign(
@@ -195,6 +200,7 @@ describe('LoggerHelper', () => {
195200
clientName: 'StorageClient',
196201
methodName: 'GetObject',
197202
rpcType: 'http',
203+
serverAddress: 'custom.storage.googleapis.com:8443',
198204
},
199205
{},
200206
);
@@ -203,24 +209,60 @@ describe('LoggerHelper', () => {
203209
'FAILED_PRECONDITION',
204210
);
205211
assert.strictEqual(specificAttrs['http.response.status_code'], 400);
212+
assert.strictEqual(
213+
specificAttrs['server.address'],
214+
'custom.storage.googleapis.com',
215+
);
206216

207-
// Translates HTTP transport errors carrying a gRPC status code without an HTTP status.
208-
const httpTransportErr = Object.assign(new GoogleError('Unavailable'), {
209-
code: Status.UNAVAILABLE,
210-
});
217+
// Omits rpc.response.status_code on HTTP timeouts and pre-response transport
218+
// errors where fallbackServiceStub sets a synthetic retry Status code without httpStatusCode.
219+
const httpTimeoutErr = Object.assign(
220+
new GoogleError(
221+
'Deadline exceeded: GetObject did not respond within 1000 milliseconds.',
222+
),
223+
{
224+
code: Status.DEADLINE_EXCEEDED,
225+
},
226+
);
227+
const timeoutAttrs = buildLogAttributes(
228+
httpTimeoutErr,
229+
{
230+
clientName: 'StorageClient',
231+
methodName: 'GetObject',
232+
rpcType: 'http',
233+
},
234+
staticArgs,
235+
);
236+
assert.strictEqual(timeoutAttrs['error.type'], 'CLIENT_TIMEOUT');
237+
assert.strictEqual('rpc.response.status_code' in timeoutAttrs, false);
238+
assert.strictEqual('http.response.status_code' in timeoutAttrs, false);
239+
240+
const econnRefusedCause = Object.assign(
241+
new Error('connect ECONNREFUSED'),
242+
{
243+
code: 'ECONNREFUSED',
244+
},
245+
);
246+
const httpTransportErr = Object.assign(
247+
new GoogleError('connect ECONNREFUSED', {cause: econnRefusedCause}),
248+
{
249+
code: Status.UNAVAILABLE,
250+
},
251+
);
211252
const transportAttrs = buildLogAttributes(
212253
httpTransportErr,
213254
{
214255
clientName: 'StorageClient',
215256
methodName: 'GetObject',
216257
rpcType: 'http',
217258
},
218-
{},
259+
staticArgs,
219260
);
220261
assert.strictEqual(
221-
transportAttrs['rpc.response.status_code'],
222-
'UNAVAILABLE',
262+
transportAttrs['error.type'],
263+
'CLIENT_CONNECTION_ERROR',
223264
);
265+
assert.strictEqual('rpc.response.status_code' in transportAttrs, false);
224266
assert.strictEqual('http.response.status_code' in transportAttrs, false);
225267
});
226268

@@ -232,8 +274,21 @@ describe('LoggerHelper', () => {
232274
staticArgs,
233275
);
234276
assert.strictEqual(clientAttrs['error.type'], 'TypeError');
277+
assert.strictEqual(clientAttrs['exception.type'], 'TypeError');
278+
assert.strictEqual(clientAttrs['exception.message'], 'invalid argument');
279+
assert.strictEqual(clientAttrs['exception.stacktrace'], clientErr.stack);
280+
assert.strictEqual(clientAttrs['url.domain'], 'storage.googleapis.com');
281+
assert.strictEqual('server.address' in clientAttrs, false);
235282
assert.strictEqual('rpc.response.status_code' in clientAttrs, false);
236283

284+
const dnsErr = Object.assign(new Error('getaddrinfo ENOTFOUND'), {
285+
code: 'ENOTFOUND',
286+
});
287+
const dnsAttrs = buildLogAttributes(dnsErr, dynamicArgs, staticArgs);
288+
assert.strictEqual(dnsAttrs['error.type'], 'CLIENT_CONNECTION_ERROR');
289+
assert.strictEqual(dnsAttrs['url.domain'], 'storage.googleapis.com');
290+
assert.strictEqual('server.address' in dnsAttrs, false);
291+
237292
const httpClientAttrs = buildLogAttributes(
238293
clientErr,
239294
{...dynamicArgs, rpcType: 'http'},
@@ -249,6 +304,10 @@ describe('LoggerHelper', () => {
249304
staticArgs,
250305
);
251306
assert.strictEqual(primitiveAttrs['error.type'], 'INTERNAL');
307+
assert.strictEqual(primitiveAttrs['exception.type'], 'Error');
308+
assert.strictEqual(primitiveAttrs['exception.message'], 'string failure');
309+
assert.strictEqual('exception.stacktrace' in primitiveAttrs, false);
310+
assert.strictEqual('server.address' in primitiveAttrs, false);
252311
});
253312
});
254313

@@ -336,6 +395,7 @@ describe('LoggerHelper', () => {
336395
'rpc.system.name': 'grpc',
337396
'rpc.method': 'google.storage.v2.Storage/GetObject',
338397
'url.domain': 'storage.googleapis.com',
398+
'server.address': 'storage.googleapis.com',
339399
'rpc.response.status_code': 'PERMISSION_DENIED',
340400
'error.type': 'IAM_PERMISSION_DENIED',
341401
'gcp.errors.domain': 'googleapis.com',

0 commit comments

Comments
 (0)