Skip to content

Commit 4799bde

Browse files
authored
Merge pull request #2986 from murgatroid99/grpc-js_client_inline_metrics
grpc-js: Implement ORCA client-side per-call metrics
2 parents 2ea58f5 + 0e44501 commit 4799bde

File tree

8 files changed

+69
-15
lines changed

8 files changed

+69
-15
lines changed

packages/grpc-js-xds/src/load-balancer-xds-cluster-impl.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -179,8 +179,8 @@ class XdsClusterImplPicker implements Picker {
179179
pickSubchannel?.getStatsObject()?.addCallStarted();
180180
callCounterMap.startCall(this.callCounterMapKey);
181181
},
182-
onCallEnded: status => {
183-
originalPick.onCallEnded?.(status);
182+
onCallEnded: (status, details, metadata) => {
183+
originalPick.onCallEnded?.(status, details, metadata);
184184
pickSubchannel?.getStatsObject()?.addCallFinished(status !== Status.OK)
185185
callCounterMap.endCall(this.callCounterMapKey);
186186
}

packages/grpc-js/src/load-balancer-outlier-detection.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -428,13 +428,13 @@ class OutlierDetectionPicker implements Picker {
428428
if (mapEntry) {
429429
let onCallEnded = wrappedPick.onCallEnded;
430430
if (this.countCalls) {
431-
onCallEnded = statusCode => {
431+
onCallEnded = (statusCode, details, metadata) => {
432432
if (statusCode === Status.OK) {
433433
mapEntry.counter.addSuccess();
434434
} else {
435435
mapEntry.counter.addFailure();
436436
}
437-
wrappedPick.onCallEnded?.(statusCode);
437+
wrappedPick.onCallEnded?.(statusCode, details, metadata);
438438
};
439439
}
440440
return {

packages/grpc-js/src/load-balancer-pick-first.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ import { StatusOr, statusOrFromValue } from './call-interface';
4747
import { OrcaLoadReport__Output } from './generated/xds/data/orca/v3/OrcaLoadReport';
4848
import { OpenRcaServiceClient } from './generated/xds/service/orca/v3/OpenRcaService';
4949
import { ClientReadableStream, ServiceError } from './call';
50-
import { createOrcaClient } from './orca';
50+
import { createOrcaClient, MetricsListener } from './orca';
5151
import { msToDuration } from './duration';
5252
import { BackoffTimeout } from './backoff-timeout';
5353

@@ -65,8 +65,6 @@ const TYPE_NAME = 'pick_first';
6565
*/
6666
const CONNECTION_DELAY_INTERVAL_MS = 250;
6767

68-
export type MetricsListener = (loadReport: OrcaLoadReport__Output) => void;
69-
7068
export class PickFirstLoadBalancingConfig implements TypedLoadBalancingConfig {
7169
constructor(private readonly shuffleAddressList: boolean) {}
7270

packages/grpc-js/src/load-balancing-call.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import { LogVerbosity, Status } from './constants';
2929
import { Deadline, formatDateDifference, getDeadlineTimeoutString } from './deadline';
3030
import { InternalChannel } from './internal-channel';
3131
import { Metadata } from './metadata';
32-
import { PickResultType } from './picker';
32+
import { OnCallEnded, PickResultType } from './picker';
3333
import { CallConfig } from './resolver';
3434
import { splitHostPort } from './uri-parser';
3535
import * as logging from './logging';
@@ -60,7 +60,7 @@ export class LoadBalancingCall implements Call, DeadlineInfoProvider {
6060
private serviceUrl: string;
6161
private metadata: Metadata | null = null;
6262
private listener: InterceptingListener | null = null;
63-
private onCallEnded: ((statusCode: Status) => void) | null = null;
63+
private onCallEnded: OnCallEnded | null = null;
6464
private startTime: Date;
6565
private childStartTime: Date | null = null;
6666
constructor(
@@ -127,7 +127,7 @@ export class LoadBalancingCall implements Call, DeadlineInfoProvider {
127127
);
128128
const finalStatus = { ...status, progress };
129129
this.listener?.onReceiveStatus(finalStatus);
130-
this.onCallEnded?.(finalStatus.code);
130+
this.onCallEnded?.(finalStatus.code, finalStatus.details, finalStatus.metadata);
131131
}
132132
}
133133

packages/grpc-js/src/metadata.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ export interface MetadataOptions {
8989
export class Metadata {
9090
protected internalRepr: MetadataObject = new Map<string, MetadataValue[]>();
9191
private options: MetadataOptions;
92+
private opaqueData: Map<string, unknown> = new Map();
9293

9394
constructor(options: MetadataOptions = {}) {
9495
this.options = options;
@@ -245,6 +246,27 @@ export class Metadata {
245246
return result;
246247
}
247248

249+
/**
250+
* Attach additional data of any type to the metadata object, which will not
251+
* be included when sending headers. The data can later be retrieved with
252+
* `getOpaque`. Keys with the prefix `grpc` are reserved for use by this
253+
* library.
254+
* @param key
255+
* @param value
256+
*/
257+
setOpaque(key: string, value: unknown) {
258+
this.opaqueData.set(key, value);
259+
}
260+
261+
/**
262+
* Retrieve data previously added with `setOpaque`.
263+
* @param key
264+
* @returns
265+
*/
266+
getOpaque(key: string) {
267+
return this.opaqueData.get(key);
268+
}
269+
248270
/**
249271
* Returns a new Metadata object based fields in a given IncomingHttpHeaders
250272
* object.

packages/grpc-js/src/orca.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
*
1616
*/
1717

18-
import { OrcaLoadReport } from "./generated/xds/data/orca/v3/OrcaLoadReport";
18+
import { OrcaLoadReport, OrcaLoadReport__Output } from "./generated/xds/data/orca/v3/OrcaLoadReport";
1919

2020
import type { loadSync } from '@grpc/proto-loader';
2121
import { ProtoGrpcType as OrcaProtoGrpcType } from "./generated/orca";
@@ -25,6 +25,7 @@ import { durationMessageToDuration, durationToMs } from "./duration";
2525
import { Server } from "./server";
2626
import { ChannelCredentials } from "./channel-credentials";
2727
import { Channel } from "./channel";
28+
import { OnCallEnded } from "./picker";
2829

2930
const loadedOrcaProto: OrcaProtoGrpcType | null = null;
3031
function loadOrcaProto(): OrcaProtoGrpcType {
@@ -213,3 +214,35 @@ export function createOrcaClient(channel: Channel): OpenRcaServiceClient {
213214
const ClientClass = loadOrcaProto().xds.service.orca.v3.OpenRcaService;
214215
return new ClientClass('unused', ChannelCredentials.createInsecure(), {channelOverride: channel});
215216
}
217+
218+
export type MetricsListener = (loadReport: OrcaLoadReport__Output) => void;
219+
220+
export const GRPC_METRICS_HEADER = 'endpoint-load-metrics-bin';
221+
const PARSED_LOAD_REPORT_KEY = 'grpc_orca_load_report';
222+
223+
/**
224+
* Create an onCallEnded callback for use in a picker.
225+
* @param listener The listener to handle metrics, whenever they are provided.
226+
* @param previousOnCallEnded The previous onCallEnded callback to propagate
227+
* to, if applicable.
228+
* @returns
229+
*/
230+
export function createMetricsReader(listener: MetricsListener, previousOnCallEnded: OnCallEnded | null): OnCallEnded {
231+
return (code, details, metadata) => {
232+
let parsedLoadReport = metadata.getOpaque(PARSED_LOAD_REPORT_KEY) as (OrcaLoadReport__Output | undefined);
233+
if (parsedLoadReport) {
234+
listener(parsedLoadReport);
235+
} else {
236+
const serializedLoadReport = metadata.get(GRPC_METRICS_HEADER);
237+
if (serializedLoadReport.length > 0) {
238+
const orcaProto = loadOrcaProto();
239+
parsedLoadReport = orcaProto.xds.data.orca.v3.OrcaLoadReport.deserialize(serializedLoadReport[0] as Buffer);
240+
listener(parsedLoadReport);
241+
metadata.setOpaque(PARSED_LOAD_REPORT_KEY, parsedLoadReport);
242+
}
243+
}
244+
if (previousOnCallEnded) {
245+
previousOnCallEnded(code, details, metadata);
246+
}
247+
}
248+
}

packages/grpc-js/src/picker.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ export enum PickResultType {
2828
DROP,
2929
}
3030

31+
export type OnCallEnded = (statusCode: Status, details: string, metadata: Metadata) => void;
32+
3133
export interface PickResult {
3234
pickResultType: PickResultType;
3335
/**
@@ -42,15 +44,15 @@ export interface PickResult {
4244
*/
4345
status: StatusObject | null;
4446
onCallStarted: (() => void) | null;
45-
onCallEnded: ((statusCode: Status) => void) | null;
47+
onCallEnded: OnCallEnded | null;
4648
}
4749

4850
export interface CompletePickResult extends PickResult {
4951
pickResultType: PickResultType.COMPLETE;
5052
subchannel: SubchannelInterface | null;
5153
status: null;
5254
onCallStarted: (() => void) | null;
53-
onCallEnded: ((statusCode: Status) => void) | null;
55+
onCallEnded: OnCallEnded | null;
5456
}
5557

5658
export interface QueuePickResult extends PickResult {

packages/grpc-js/src/server-interceptors.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ import { CallEventTracker } from './transport';
3535
import * as logging from './logging';
3636
import { AuthContext } from './auth-context';
3737
import { TLSSocket } from 'tls';
38-
import { PerRequestMetricRecorder } from './orca';
38+
import { GRPC_METRICS_HEADER, PerRequestMetricRecorder } from './orca';
3939

4040
const TRACER_NAME = 'server_call';
4141

@@ -491,7 +491,6 @@ const GRPC_ENCODING_HEADER = 'grpc-encoding';
491491
const GRPC_MESSAGE_HEADER = 'grpc-message';
492492
const GRPC_STATUS_HEADER = 'grpc-status';
493493
const GRPC_TIMEOUT_HEADER = 'grpc-timeout';
494-
const GRPC_METRICS_HEADER = 'endpoint-load-metrics-bin';
495494
const DEADLINE_REGEX = /(\d{1,8})\s*([HMSmun])/;
496495
const deadlineUnitsToMs: DeadlineUnitIndexSignature = {
497496
H: 3600000,

0 commit comments

Comments
 (0)