Skip to content

Commit 2762a27

Browse files
authored
refactor(gax): extract shared observability attribute helpers (#9742)
Move attribute resolution logic that was inline or private in TracerHelper into exported helpers so LoggerHelper can reuse it: - `contextUtil.ts`: `resolveRpcMethod()` (rpc.method) and `resolveEndpointAndDomain()` (url.domain / server.address), the latter moved unchanged from TracerHelper. - `errorUtil.ts`: `resolveExceptionMessage()` (exception.message) and `resolveResponseStatusCode()` (rpc.response.status_code / http.response.status_code, including the OK / 200 success defaults). TracerHelper now calls these helpers, and span output is unchanged. Adds unit tests for each helper.
1 parent 62c65db commit 2762a27

5 files changed

Lines changed: 352 additions & 82 deletions

File tree

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

Lines changed: 12 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -26,23 +26,23 @@ import {
2626
Tracer,
2727
} from '@opentelemetry/api';
2828
import {APICallback, GaxCallResult} from '../apitypes';
29-
import {Status} from '../status';
3029
import {
3130
ATTEMPT_SPAN_KEY,
3231
AttemptTraceContext,
3332
buildInitialAttributes,
3433
CLIENT_REQUEST_SPAN_KEY,
3534
DynamicTraceContext,
3635
getActiveTraceContext,
36+
resolveEndpointAndDomain,
37+
resolveRpcMethod,
3738
StaticTraceContext,
3839
} from './contextUtil';
3940
import {
4041
isPreConnectionFailure,
4142
recordExceptionEvent,
4243
resolveErrorMessage,
4344
resolveErrorType,
44-
resolveHttpStatusCode,
45-
resolveRpcStatusName,
45+
resolveResponseStatusCode,
4646
} from './errorUtil';
4747

4848
const attemptUrlTemplates = new WeakMap<Span, string>();
@@ -284,59 +284,6 @@ export function handleStream(
284284
}
285285
}
286286

287-
/**
288-
* Resolves and parses the server address, port, and target service domain (`url.domain`)
289-
* from dynamic and static trace contexts, splitting `"host:port"` or `"[ipv6]:port"`
290-
* strings. When `defaultPort` is provided (for low level network attempt spans), falls back to `url.domain`
291-
* for the server address and defaults the port to `defaultPort` if not otherwise specified.
292-
*
293-
* @param {DynamicTraceContext} dynamicArgs - Dynamic trace context for the call or attempt.
294-
* @param {StaticTraceContext} staticArgs - Static trace context for the client library.
295-
* @param {number} [defaultPort] - Optional default port (e.g. 443 for low level network attempt spans).
296-
* @returns {{rawAddress?: string; rawPort?: number; urlDomain?: string}} Resolved address, port, and domain.
297-
*/
298-
function resolveEndpointAndDomain(
299-
dynamicArgs: DynamicTraceContext,
300-
staticArgs: StaticTraceContext,
301-
defaultPort?: number,
302-
): {rawAddress?: string; rawPort?: number; urlDomain?: string} {
303-
let rawAddress = dynamicArgs.serverAddress ?? staticArgs.serverAddress;
304-
let rawPort = dynamicArgs.serverPort ?? staticArgs.serverPort;
305-
const splitHostAndPort = (addr: string) => {
306-
const match = addr.match(/^(\[[^\]]+\]|[^:]+):(\d+)$/);
307-
if (match) {
308-
rawAddress = match[1];
309-
rawPort = rawPort ?? Number(match[2]);
310-
} else {
311-
rawAddress = addr;
312-
}
313-
};
314-
315-
if (rawAddress) {
316-
// Split embedded port from "host:port" or "[ipv6]:port" if present.
317-
splitHostAndPort(rawAddress);
318-
}
319-
320-
let urlDomain = dynamicArgs.urlDomain ?? staticArgs.urlDomain ?? rawAddress;
321-
if (!urlDomain && staticArgs.gcpClientService) {
322-
// Append ".googleapis.com" when gcpClientService is a short service name.
323-
urlDomain = staticArgs.gcpClientService.includes('.')
324-
? staticArgs.gcpClientService
325-
: `${staticArgs.gcpClientService}.googleapis.com`;
326-
}
327-
328-
if (defaultPort !== undefined) {
329-
if (!rawAddress && urlDomain) {
330-
splitHostAndPort(urlDomain);
331-
}
332-
if (rawAddress) {
333-
rawPort = rawPort ?? defaultPort;
334-
}
335-
}
336-
337-
return {rawAddress, rawPort, urlDomain};
338-
}
339-
340287
/**
341288
* Returns the transport-specific resend count attribute key.
342289
*
@@ -402,25 +349,20 @@ function setFinalStatusAttributes(
402349

403350
/**
404351
* Records error attributes (`error.type`, `status.message`), `exception` span event,
405-
* and `ERROR` status on a span, returning the resolved transport status codes.
352+
* and `ERROR` status on a span.
406353
*
407354
* @param {Span} span - The span on which to record the error.
408355
* @param {unknown} e - The error thrown or passed to the callback/stream/promise.
409356
* @param {'grpc' | 'http'} rpcType - The RPC transport protocol.
410357
* @param {boolean} [includeExceptionMessage=false] - Whether to include `exception.message`
411358
* on the `exception` event (true for low level network attempt spans, false for client request spans).
412-
* @returns {{rpcStatusName?: string; httpStatusCode?: number}} The resolved status codes.
413359
*/
414360
function recordSpanError(
415361
span: Span,
416362
e: unknown,
417363
rpcType: 'grpc' | 'http',
418364
includeExceptionMessage = false,
419-
): {rpcStatusName?: string; httpStatusCode?: number} {
420-
const rpcStatusName =
421-
rpcType === 'grpc' ? resolveRpcStatusName(e) : undefined;
422-
const httpStatusCode =
423-
rpcType === 'http' ? resolveHttpStatusCode(e) : undefined;
365+
): void {
424366
const message = e instanceof Error ? e.message : resolveErrorMessage(e);
425367
span.setAttributes({
426368
'error.type': resolveErrorType(e, rpcType),
@@ -430,7 +372,6 @@ function recordSpanError(
430372
recordExceptionEvent(span, e, rpcType, includeExceptionMessage);
431373
}
432374
span.setStatus({code: SpanStatusCode.ERROR, message});
433-
return {rpcStatusName, httpStatusCode};
434375
}
435376

436377
/**
@@ -471,28 +412,22 @@ function createSpanCompletionHandlers(
471412
let spanEnded = false;
472413
let errorRecorded = false;
473414
let recordedError: unknown;
474-
let rpcStatusName: string | undefined;
475-
let httpStatusCode: number | undefined;
476415

477416
const recordError = (e: unknown) => {
478417
recordedError = e;
479418
errorRecorded = true;
480-
({rpcStatusName, httpStatusCode} = recordSpanError(
481-
span,
482-
e,
483-
rpcType,
484-
includeExceptionMessage,
485-
));
419+
recordSpanError(span, e, rpcType, includeExceptionMessage);
486420
};
487421

488422
const endSpan = () => {
489423
if (!spanEnded) {
490424
spanEnded = true;
491425
// Default to OK / 200 when no error was recorded.
492-
if (!errorRecorded) {
493-
rpcStatusName = Status[Status.OK];
494-
httpStatusCode = 200;
495-
}
426+
const {rpcStatusName, httpStatusCode} = resolveResponseStatusCode(
427+
rpcType,
428+
errorRecorded,
429+
recordedError,
430+
);
496431
setFinalStatusAttributes(
497432
span,
498433
rpcType,
@@ -675,9 +610,7 @@ export function traceAttempt<T = GaxCallResult>(
675610
parentContext?: Context,
676611
): T {
677612
// Resolve RPC method and transport-specific span name.
678-
const rpcMethod = dynamicArgs.apiName
679-
? `${dynamicArgs.apiName}/${dynamicArgs.methodName}`
680-
: dynamicArgs.methodName;
613+
const rpcMethod = resolveRpcMethod(dynamicArgs);
681614
const httpMethod = dynamicArgs.httpMethod ?? 'POST';
682615
const urlTemplate = dynamicArgs.urlTemplate;
683616
const spanName =

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

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,74 @@ export function resolveStaticTraceContext(
393393
return context;
394394
}
395395

396+
/**
397+
* Resolves and parses the server address, port, and target service domain (`url.domain`)
398+
* from dynamic and static trace contexts, splitting `"host:port"` or `"[ipv6]:port"`
399+
* strings. When `defaultPort` is provided (for low level network attempt spans), falls back to `url.domain`
400+
* for the server address and defaults the port to `defaultPort` if not otherwise specified.
401+
*
402+
* @param {DynamicTraceContext} dynamicArgs - Dynamic trace context for the call or attempt.
403+
* @param {StaticTraceContext} staticArgs - Static trace context for the client library.
404+
* @param {number} [defaultPort] - Optional default port (e.g. 443 for low level network attempt spans).
405+
* @returns {{rawAddress?: string; rawPort?: number; urlDomain?: string}} Resolved address, port, and domain.
406+
*/
407+
export function resolveEndpointAndDomain(
408+
dynamicArgs: DynamicTraceContext,
409+
staticArgs: StaticTraceContext,
410+
defaultPort?: number,
411+
): {rawAddress?: string; rawPort?: number; urlDomain?: string} {
412+
let rawAddress = dynamicArgs.serverAddress ?? staticArgs.serverAddress;
413+
let rawPort = dynamicArgs.serverPort ?? staticArgs.serverPort;
414+
const splitHostAndPort = (addr: string) => {
415+
const match = addr.match(/^(\[[^\]]+\]|[^:]+):(\d+)$/);
416+
if (match) {
417+
rawAddress = match[1];
418+
rawPort = rawPort ?? Number(match[2]);
419+
} else {
420+
rawAddress = addr;
421+
}
422+
};
423+
424+
if (rawAddress) {
425+
// Split embedded port from "host:port" or "[ipv6]:port" if present.
426+
splitHostAndPort(rawAddress);
427+
}
428+
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('.')
433+
? staticArgs.gcpClientService
434+
: `${staticArgs.gcpClientService}.googleapis.com`;
435+
}
436+
437+
if (defaultPort !== undefined) {
438+
if (!rawAddress && urlDomain) {
439+
splitHostAndPort(urlDomain);
440+
}
441+
if (rawAddress) {
442+
rawPort = rawPort ?? defaultPort;
443+
}
444+
}
445+
446+
return {rawAddress, rawPort, urlDomain};
447+
}
448+
449+
/**
450+
* Resolves the `rpc.method` attribute value as `"{apiName}/{methodName}"` when the
451+
* fully-qualified protobuf service name is available, or `"{methodName}"` otherwise.
452+
*
453+
* @param {Pick<AttemptTraceContext, 'apiName' | 'methodName'>} dynamicArgs - API and method names for the call or attempt.
454+
* @returns {string} The resolved `rpc.method` value.
455+
*/
456+
export function resolveRpcMethod(
457+
dynamicArgs: Pick<AttemptTraceContext, 'apiName' | 'methodName'>,
458+
): string {
459+
return dynamicArgs.apiName
460+
? `${dynamicArgs.apiName}/${dynamicArgs.methodName}`
461+
: dynamicArgs.methodName;
462+
}
463+
396464
/**
397465
* Builds the common initial client, transport, and domain attributes shared by
398466
* client request spans and low level network attempt spans.

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

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -553,6 +553,34 @@ export function resolveHttpStatusCode(e: unknown): number | undefined {
553553
return undefined;
554554
}
555555

556+
/**
557+
* Resolves the transport response status code for a completed call or attempt,
558+
* recorded as `rpc.response.status_code` (gRPC) or `http.response.status_code` (HTTP).
559+
* Successful calls resolve to `OK` (gRPC) or `200` (HTTP). Failed calls resolve the
560+
* status from the error, which is undefined when the error carries no status.
561+
*
562+
* @param {'grpc' | 'http'} rpcType - The RPC transport protocol.
563+
* @param {boolean} failed - Whether the call or attempt failed.
564+
* @param {unknown} [e] - The error the call or attempt failed with, if it failed.
565+
* @returns {{rpcStatusName?: string; httpStatusCode?: number}} The gRPC status name
566+
* (gRPC) or HTTP status code (HTTP) for the transport.
567+
*/
568+
export function resolveResponseStatusCode(
569+
rpcType: 'grpc' | 'http',
570+
failed: boolean,
571+
e?: unknown,
572+
): {rpcStatusName?: string; httpStatusCode?: number} {
573+
if (rpcType === 'grpc') {
574+
return {
575+
rpcStatusName: failed ? resolveRpcStatusName(e) : Status[Status.OK],
576+
};
577+
}
578+
if (rpcType === 'http') {
579+
return {httpStatusCode: failed ? resolveHttpStatusCode(e) : 200};
580+
}
581+
return {};
582+
}
583+
556584
/**
557585
* Determines if a failure occurred on the client side before DNS resolution
558586
* or connection establishment.
@@ -802,6 +830,25 @@ export function resolveServerExceptionDetails(e: Error): {
802830
};
803831
}
804832

833+
/**
834+
* Resolves the `exception.message` for a failed call or attempt. Server-side Error
835+
* instances prefer the server-provided error details over the generic error message
836+
* (see `resolveServerExceptionDetails`). All other values use their `message`, or the
837+
* stringified value for non-Error throws (see `resolveErrorMessage`).
838+
*
839+
* @param {unknown} e - The error or other value thrown or reported for the call.
840+
* @param {'grpc' | 'http'} rpcType - The RPC transport protocol.
841+
* @returns {string} The resolved `exception.message` string.
842+
*/
843+
export function resolveExceptionMessage(
844+
e: unknown,
845+
rpcType: 'grpc' | 'http',
846+
): string {
847+
return e instanceof Error && isServerSideError(e, rpcType)
848+
? resolveServerExceptionDetails(e).message
849+
: resolveErrorMessage(e);
850+
}
851+
805852
/**
806853
* Records an `exception` span event with `exception.type` (and `exception.message`
807854
* when `includeMessage` is true, such as on low level network attempt spans).
@@ -822,9 +869,7 @@ export function recordExceptionEvent(
822869
'exception.type': resolveExceptionType(e),
823870
};
824871
if (includeMessage) {
825-
attributes['exception.message'] = isServerSideError(e, rpcType)
826-
? resolveServerExceptionDetails(e).message
827-
: e.message;
872+
attributes['exception.message'] = resolveExceptionMessage(e, rpcType);
828873
}
829874
span.addEvent('exception', attributes);
830875
}

0 commit comments

Comments
 (0)